我已经仔细阅读了这份设计规范文档。以下是我的详细审查结果：

---

## 设计规范文档审查报告

**文件**: `/root/projects/qq-shell/docs/superpowers/specs/2026-04-07-butler-shell-design.md`

---

### 一、优点概述

文档整体结构清晰，技术栈选型合理，项目结构规划得当。以下方面设计良好：
- 数据模型定义清晰
- 工作流设计思路清晰
- Skill 扩展系统设计合理
- 错误分类明确
- 实施计划分阶段可行

---

### 二、Critical Issues (必须修复)

#### 2.1 安全设计严重缺失

文档缺少专门的安全设计章节，存在以下重大风险：

**问题位置**: 整个文档缺少安全章节

**具体问题**:

1. **认证机制缺失**: 没有说明如何验证消息来源的合法性
   - 任何知道机器人 QQ 号的人都能发送命令执行 Shell 指令
   - 缺少用户白名单/黑名单机制

2. **危险命令检测可被绕过** (第269-282行):
   ```
   用户可以通过以下方式绕过检测：
   - sudo rm -rf /  (未检测 sudo)
   - sh -c 'rm -rf /'  (命令嵌套)
   - echo "rm -rf /" | bash  (管道执行)
   - chmod -R 777 /  (递归参数未检测)
   - :(){ :|:& };:  (fork bomb)
   ```

3. **会话隔离安全**:
   - `butler_<user_id>` 命名规则可预测
   - 没有防止会话劫持的机制

**修改建议**:

```markdown
## 安全设计

### 认证与授权

| 机制 | 说明 |
|------|------|
| 用户白名单 | 仅允许配置文件中的 QQ 用户 ID |
| 消息签名验证 | 验证 NapCatQQ 消息来源 |
| 会话令牌 | 每个会话生成唯一令牌，防止劫持 |

### 命令安全检测

危险命令检测应使用 AST 解析而非正则匹配：
- 使用 `shlex.split()` 解析命令
- 检测命令树中的危险操作节点
- 阻止命令嵌套执行

### 敏感信息处理

- 终端输出过滤：移除匹配密码模式的文本
- 日志脱敏：API Key、Token 等不记录明文
```

#### 2.2 模式切换逻辑不完整

**问题位置**: 第13行提到"双模式切换"，但第214-222行工作流只描述了检测后的处理，缺少：

1. **INTERACTIVE 模式何时切换回 NATURAL_LANGUAGE?**
   - 进程退出后自动切换？
   - 用户手动切换？如何操作？
   - 检测失败的降级策略？

**修改建议** (在第214行后添加):

```markdown
### 模式切换规则

| 切换方向 | 触发条件 | 用户可干预 |
|----------|----------|------------|
| NL → INTERACTIVE | 检测到交互进程启动 | 否 |
| INTERACTIVE → NL | 检测到进程退出 | 是，发送 `/nl` |
| INTERACTIVE → NL | 发送 `C-d` 或 `C-c` | 是 |

### 模式检测降级

当模式检测失败时（如 /proc 访问权限问题）：
- 默认使用 NATURAL_LANGUAGE 模式
- 记录告警日志
- 提示用户手动切换
```

---

### 三、Important Issues (应该修复)

#### 3.1 数据模型不完整

**问题位置**: 第198-199行

```python
def get_pty_state(self, session_name: str) -> PtyState:
    """检测 PTY 是否在等待输入（通过 /proc 状态监控）"""
```

**问题**: `PtyState` 类型未定义

**修改建议** (在 SessionState 定义后添加):

```python
class PtyState(str, Enum):
    """PTY 状态"""
    IDLE = "idle"           # 空闲，等待输入
    RUNNING = "running"     # 正在执行命令
    BLOCKED = "blocked"     # 阻塞中（如等待 I/O）
    UNKNOWN = "unknown"     # 状态检测失败
```

#### 3.2 会话生命周期管理缺失

**问题位置**: 整个文档

**问题**: 没有说明会话的生命周期管理：
- 会话何时创建？
- 会话何时销毁？
- 长时间不活跃的会话如何处理？
- 并发会话数量限制？

**修改建议** (添加新章节):

```markdown
## 会话生命周期管理

### 会话策略

| 配置项 | 默认值 | 说明 |
|--------|--------|------|
| SESSION_IDLE_TIMEOUT | 3600s | 空闲超时后自动销毁 |
| MAX_SESSIONS_PER_USER | 1 | 每用户最大会话数 |
| MAX_TOTAL_SESSIONS | 100 | 全局最大会话数 |

### 会话清理

定时任务每小时执行：
1. 扫描所有会话的最后活跃时间
2. 超过 `SESSION_IDLE_TIMEOUT` 的会话标记为待清理
3. 发送通知给用户（可选）
4. 优雅关闭 tmux 会话
```

#### 3.3 一致性问题

**问题位置**: 第45行 vs 第202行

```
第45行：Session Manager (TmuxWrapper) 有 detect_mode
第202行：TmuxWrapper 类有 detect_mode 方法
```

**问题**: 架构图显示 `detect_mode` 在 Session Manager 层，但 API 定义在 TmuxWrapper 类中，需要澄清职责边界。

**修改建议**: 明确说明 TmuxWrapper 是 Session Manager 的核心实现类，或调整架构图。

#### 3.4 SQLite 日志存储 Schema 未定义

**问题位置**: 第26行提到 "日志存储 | SQLite"

**问题**: 没有定义数据库表结构

**修改建议** (添加):

```markdown
### 日志存储 Schema

```sql
CREATE TABLE shell_logs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id TEXT NOT NULL,
    user_id TEXT NOT NULL,
    command TEXT NOT NULL,
    output TEXT,
    executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    approved BOOLEAN DEFAULT TRUE,
    approval_id TEXT
);

CREATE INDEX idx_session_logs ON shell_logs(session_id);
CREATE INDEX idx_user_logs ON shell_logs(user_id);
```
```

#### 3.5 错误处理缺少具体策略

**问题位置**: 第308-323行

**问题**: 错误分类表只列出了类型，缺少：
- 具体哪些错误属于哪种类型
- 重试策略（最大重试次数、退避算法）
- 用户友好消息模板

**修改建议**:

```markdown
### 错误分类详情

| 错误类型 | 具体错误 | 分类 | 处理策略 |
|----------|----------|------|----------|
| tmux session 不存在 | SessionNotFoundError | TRANSIENT | 自动重建 session |
| tmux 命令超时 | TmuxTimeoutError | TRANSIENT | 重试 3 次，指数退避 |
| NapCat 连接断开 | GatewayDisconnectedError | TRANSIENT | 重连 + 重试 |
| 用户无权限 | UnauthorizedError | PERMANENT | 直接返回错误消息 |
| 命令被拒绝 | CommandRejectedError | PERMANENT | 记录日志，通知用户 |

### 重试策略

```python
RETRY_CONFIG = {
    "max_retries": 3,
    "initial_delay": 1,  # 秒
    "max_delay": 30,
    "multiplier": 2,
}
```
```

---

### 四、Suggestions (建议改进)

#### 4.1 可观测性设计

建议添加以下内容：

```markdown
## 可观测性

### 日志级别

| 级别 | 使用场景 |
|------|----------|
| DEBUG | 详细执行流程 |
| INFO | 会话创建/销毁、命令执行 |
| WARN | 模式检测失败、降级操作 |
| ERROR | 命令执行失败、外部服务异常 |

### 关键指标

- `butler_sessions_active`: 当前活跃会话数
- `butler_commands_total`: 命令执行总数 (按状态分组)
- `butler_command_duration_seconds`: 命令执行耗时
- `butler_mode_switches_total`: 模式切换次数

### 健康检查端点

- `GET /health`: 服务状态
- `GET /ready`: 就绪状态（检查 NapCat 连接、tmux 可用性）
```

#### 4.2 消息格式规范

建议添加 QQ 消息格式说明：

```markdown
## 消息格式

### 输入消息

NapCatQQ 消息格式（JSON）：
```json
{
  "time": 1712345678,
  "self_id": 12345678,
  "post_type": "message",
  "message_type": "private",
  "user_id": 87654321,
  "message": "命令内容",
  "raw_message": "命令内容"
}
```

### 输出消息

支持的消息类型：
- 文本消息
- 图片（终端截图，可选）
- Markdown 格式（转换为 QQ 富文本）
```

#### 4.3 配置管理

建议添加配置文件结构：

```markdown
## 配置管理

### 配置文件

```yaml
butler:
  session:
    idle_timeout: 3600
    max_per_user: 1

  guardrail:
    enabled: true
    approval_timeout: 300
    danger_patterns:
      - "rm -rf"
      - "sudo"

  users:
    whitelist:
      - "12345678"
      - "87654321"

  logging:
    level: INFO
    path: /var/log/butler/shell.log
```

### 配置热更新

支持运行时重新加载配置：
- `POST /config/reload` 触发重载
- 监听 SIGHUP 信号
```

#### 4.4 Skill 系统完善

**问题位置**: 第283-299行

建议添加：

```markdown
### Skill 注册机制

```python
class SkillRegistry:
    def __init__(self):
        self._skills: dict[str, SkillBase] = {}

    def register(self, skill: SkillBase) -> None:
        """注册 Skill"""
        self._skills[skill.name] = skill

    def match(self, content: str) -> Optional[SkillBase]:
        """根据内容匹配 Skill"""
        for skill in self._skills.values():
            if skill.matches(content):
                return skill
        return None
```

### Skill 动态加载

支持从 `skills/` 目录自动发现并加载 Skill：
- 扫描 `skills/*.py` 文件
- 查找 `SkillBase` 子类
- 自动注册到 SkillRegistry
```

#### 4.5 端到端测试细节

**问题位置**: 第331行

建议添加：

```markdown
### E2E 测试场景

| 场景 | 描述 | 验证点 |
|------|------|--------|
| 新用户首次交互 | 发送消息，创建会话 | 会话创建成功 |
| 正常命令执行 | `ls -la` | 返回正确输出 |
| 危险命令审批 | `rm -rf /tmp/test` | 进入审批流程 |
| 审批超时 | 不响应审批请求 | 5分钟后超时取消 |
| 交互模式切换 | 在 vim 中发送按键 | 正确透传 |
| 模式自动恢复 | 退出 vim | 切换回 NL 模式 |
```

---

### 五、总结

| 类别 | 数量 | 说明 |
|------|------|------|
| Critical | 2 | 安全设计缺失、模式切换逻辑不完整 |
| Important | 5 | 数据模型不完整、生命周期管理缺失、一致性问题、Schema未定义、错误处理策略不具体 |
| Suggestions | 5 | 可观测性、消息格式、配置管理、Skill系统完善、E2E测试细节 |

**整体评价**: 文档框架良好，但安全设计和系统完整性方面需要加强。建议在实施前优先解决 Critical 问题，并在 Phase 1 开始前完善 Important 问题。

agentId: a6bcf2d01615d4a97 (use SendMessage with to: 'a6bcf2d01615d4a97' to continue this agent)
<usage>total_tokens: 20948
tool_uses: 1
duration_ms: 72705</usage>

Prompt:
审查以下设计规范文档的质量、完整性和一致性：

文件路径: /root/projects/qq-shell/docs/superpowers/specs/2026-04-07-butler-shell-design.md

审查要点：
1. **完整性** - 是否覆盖了所有必要的设计方面（架构、数据模型、API、错误处理、测试）
2. **一致性** - 各部分之间是否有矛盾或不一致的地方
3. **清晰度** - 是否足够清晰，能让实施者理解要构建什么
4. **可行性** - 设计是否可实现，是否有明显的技术障碍
5. **安全考虑** - 是否有安全漏洞或风险未考虑
6. **扩展性** - 是否有足够的扩展点

请提供具体的改进建议，标注问题位置和修改方案。


规范审查返回了详细的改进建议。有2个 Critical Issues 和 5个 Important Issues 需要修复。我需要根据这些建议更新设计文档。

Critical Issues:
1. 安全设计严重缺失 - 需要添加认证机制、危险命令检测增强、敏感信息处理
2. 模式切换逻辑不完整 - 需要添加模式切换规则和降级策略

Important Issues:
1. PtyState 类型未定义
2. 会话生命周期管理缺失
3. 架构图与 API 定义一致性问题
4. SQLite Schema 未定义
5. 错误处理策略不具体

让我更新设计文档来解决这些问题。


规范审查发现了 2 个 Critical 和 5 个 Important 问题。让我修复这些问题。


1	---
2	name: Butler-Shell 系统设计
3	description: 基于 Inngest 与 tmux 的持久化 Shell 助手 (QQ 桥接)
4	type: project
5	---
6
7	# Butler-Shell 系统设计
8
9	## 概述
10
11	Butler-Shell 是一个通过 QQ 接收指令、在远程服务器上维护持久化 Shell 会话的系统。核心特性：
12
13	- **双模式切换**：自动在自然语言模式与 PTY 透传交互模式间切换
14	- **命令准入**：危险命令需要用户审批
15	- **上下文压缩**：`/compact` 指令压缩会话上下文
16	- **可扩展 Skills**：插件化功能扩展
17
18	## 技术栈
19
20	| 组件 | 技术选型 |
21	|------|----------|
22	| 任务编排 | Inngest (Python SDK) + 自托管 Server |
23	| PTY 容器 | tmux |
24	| QQ 接入 | NapCatQQ (WebSocket) |
25	| 状态存储 | Inngest State |
26	| 日志存储 | SQLite |
27	| Agent Logic | Claude API |
28
29	## 架构
30
31	```
32	┌─────────────────────────────────────────────────────────┐
33	│                    QQ Gateway                           │
34	│              (NapCatQQ WebSocket Handler)               │
35	└─────────────────────┬───────────────────────────────────┘
36	                      │ 消息事件
37	┌─────────────────────▼───────────────────────────────────┐
38	│                 Inngest Functions                       │
39	│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐       │
40	│  │handle_msg   │ │guardrail    │ │compaction   │       │
41	│  └─────────────┘ └─────────────┘ └─────────────┘       │
42	└─────────────────────┬───────────────────────────────────┘
43	                      │ 状态管理
44	┌─────────────────────▼───────────────────────────────────┐
45	│              Session Manager (TmuxWrapper)              │
46	│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐    │
47	│  │create_session│ │capture_pane │ │detect_mode   │    │
48	│  └──────────────┘ └──────────────┘ └──────────────┘    │
49	└─────────────────────┬───────────────────────────────────┘
50	                      │ PTY 控制
51	┌─────────────────────▼───────────────────────────────────┐
52	│                    tmux sessions                        │
53	│              [user_001] [user_002] ...                  │
54	└─────────────────────────────────────────────────────────┘
55	```
56
57	## 项目结构
58
59	```
60	butler-shell/
61	├── src/
62	│   └── butler/
63	│       ├── __init__.py
64	│       ├── main.py                    # FastAPI 入口 + Inngest Serve
65	│       │
66	│       ├── gateway/                   # QQ 消息接入层
67	│       │   ├── __init__.py
68	│       │   ├── napcat.py              # NapCatQQ WebSocket 客户端
69	│       │   └── events.py              # 消息事件定义
70	│       │
71	│       ├── workflows/                 # Inngest 工作流
72	│       │   ├── __init__.py
73	│       │   ├── handle_message.py      # 主消息处理流
74	│       │   ├── guardrail.py           # 命令准入审批
75	│       │   └── compaction.py          # 上下文压缩
76	│       │
77	│       ├── session/                   # Tmux 会话管理
78	│       │   ├── __init__.py
79	│       │   ├── wrapper.py             # TmuxWrapper 核心类
80	│       │   ├── detector.py            # 模式检测器
81	│       │   └── state.py               # 会话状态模型
82	│       │
83	│       ├── skills/                    # Skill 扩展系统
84	│       │   ├── __init__.py
85	│       │   ├── base.py                # SkillManager 基类
86	│       │   ├── log_skill.py           # 日志记录 Skill
87	│       │   └── system_skill.py        # 系统信息 Skill
88	│       │
89	│       └── config.py                  # 配置管理
90	│
91	├── tests/
92	│   ├── conftest.py
93	│   ├── test_wrapper.py
94	│   ├── test_detector.py
95	│   └── test_workflows.py
96	│
97	├── docs/
98	│   └── specs/
99	│
100	├── pyproject.toml
101	├── requirements.txt
102	└── docker-compose.yml                 # Inngest Server + 依赖
103	```
104
105	## 核心数据模型
106
107	### SessionState
108
109	```python
110	from enum import Enum
111	from pydantic import BaseModel, Field
112	from datetime import datetime
113	from typing import Optional
114
115	class SessionMode(str, Enum):
116	    """会话运行模式"""
117	    NATURAL_LANGUAGE = "nl"      # 自然语言模式 - 通过 LLM 解析
118	    INTERACTIVE = "interactive"  # 交互模式 - 直接透传到 PTY
119
120	class SessionState(BaseModel):
121	    """会话状态快照"""
122	    session_id: str = Field(..., description="tmux session 名称")
123	    user_id: str = Field(..., description="QQ 用户 ID")
124	    mode: SessionMode = Field(default=SessionMode.NATURAL_LANGUAGE)
125
126	    # Shell 状态
127	    pwd: str = Field(default="", description="当前工作目录")
128	    env: dict[str, str] = Field(default_factory=dict, description="关键环境变量")
129	    last_output: str = Field(default="", description="最近终端输出摘要")
130
131	    # 交互模式追踪
132	    interactive_process: Optional[str] = Field(
133	        default=None,
134	        description="当前交互进程名 (vim, top, etc.)"
135	    )
136
137	    # 元数据
138	    created_at: datetime = Field(default_factory=datetime.utcnow)
139	    updated_at: datetime = Field(default_factory=datetime.utcnow)
140	```
141
142	### MessageContext
143
144	```python
145	class MessageContext(BaseModel):
146	    """消息处理上下文"""
147	    message_id: str
148	    user_id: str
149	    content: str
150	    is_command: bool = False          # 是否以 / 开头
151	    requires_approval: bool = False   # 危险命令标记
152	    session: Optional[SessionState] = None
153	```
154
155	## TmuxWrapper 核心 API
156
157	```python
158	class TmuxWrapper:
159	    """tmux 会话封装器"""
160
161	    # 交互模式进程名单
162	    INTERACTIVE_PROCESSES = frozenset([
163	        "vim", "nvim", "vi",
164	        "top", "htop", "btop",
165	        "less", "more", "man",
166	        "claude", "aider",
167	        "python", "node", "irb", "ipython",
168	    ])
169
170	    # 快捷键映射
171	    SPECIAL_KEYS = {
172	        "C-c", "C-d", "C-z", "C-a", "C-e",
173	        "Esc", "Tab", "Up", "Down", "Left", "Right",
174	        "Enter", "Space",
175	    }
176
177	    def create_session(self, user_id: str) -> str:
178	        """为用户创建独立 tmux session"""
179
180	    def session_exists(self, session_name: str) -> bool:
181	        """检查 session 是否存在"""
182
183	    def kill_session(self, session_name: str) -> None:
184	        """销毁 session"""
185
186	    def send_keys(self, session_name: str, keys: str, enter: bool = True) -> None:
187	        """向 session 发送按键"""
188
189	    def send_raw(self, session_name: str, data: str) -> None:
190	        """发送原始数据（交互模式精确控制）"""
191
192	    def capture_pane(self, session_name: str, as_ansi: bool = False) -> str:
193	        """抓取终端屏幕内容"""
194
195	    def get_active_process(self, session_name: str) -> Optional[str]:
196	        """获取 tmux 窗口中的活跃进程名"""
197
198	    def get_pty_state(self, session_name: str) -> PtyState:
199	        """检测 PTY 是否在等待输入（通过 /proc 状态监控）"""
200
201	    def detect_mode(self, session_name: str) -> SessionMode:
202	        """混合模式检测：进程名单匹配 + PTY 状态检测"""
203	```
204
205	## Inngest 工作流
206
207	### handle_im_message
208
209	```
210	Step 1: get-or-create-session → butler_<user_id>
211	Step 2: load-state → SessionState
212	Step 3: detect-mode → NATURAL_LANGUAGE | INTERACTIVE
213
214	INTERACTIVE 模式:
215	  └─ send_keys(content, enter=False) → capture_pane() → 返回快照
216
217	NATURAL_LANGUAGE 模式:
218	  ├─ Skill 匹配? → execute Skill → 返回结果
219	  ├─ 危险命令? → 发送 im/command-needs-approval 事件
220	  ├─ 特殊指令? → 执行对应逻辑
221	  └─ send_keys(content, enter=True) → capture_pane() → 返回输出
222	```
223
224	### command_guardrail
225
226	```
227	Step 1: send-confirmation-request → 发送确认请求到 QQ
228	Step 2: wait-approval → ctx.step.wait_for_event (最长 5 分钟)
229	Step 3: 处理用户回复 → 执行或取消
230	```
231
232	### context_compaction
233
234	```
235	Step 1: get-pwd → 获取当前工作目录
236	Step 2: get-env → 获取关键环境变量
237	Step 3: get-output → 抓取最近终端输出
238	Step 4: summarize → LLM 总结（可选）
239	Step 5: 更新状态快照 → ctx.state.set()
240	```
241
242	## 消息处理流程
243
244	```
245	QQ 消息到达
246	     │
247	     ▼
248	NapCatGateway ──→ 转换为 NapCatEvent
249	     │
250	     ▼
251	Inngest Event ──→ 发送 im/message 事件
252	     │
253	     ▼
254	handle_im_message workflow
255	     │
256	     ├─ INTERACTIVE 模式 ──→ send_keys + capture_pane
257	     │
258	     └─ NATURAL_LANGUAGE 模式
259	          ├─ Skill 匹配 ──→ execute Skill
260	          ├─ 危险命令 ──→ command_guardrail
261	          └─ 正常命令 ──→ execute + capture
262	     │
263	     ▼
264	NapCatGateway ──→ 发送 QQ 回复
265	```
266
267	## 危险命令检测
268
269	扩展集合，包括：
270
271	| 命令模式 | 说明 |
272	|----------|------|
273	| `rm -rf` | 递归强制删除 |
274	| `chmod 777` | 开放所有权限 |
275	| `chown` | 修改所有者 |
276	| `dd` | 磁盘操作 |
277	| `mkfs` | 格式化 |
278	| `fdisk`/`parted` | 分区操作 |
279	| `> /dev/sd*` | 写入磁盘设备 |
280	| `iptables` | 防火墙规则 |
281	| `shutdown`/`reboot` | 系统关机/重启 |
282
283	## Skill 扩展系统
284
285	### 基类 API
286
287	```python
288	class SkillBase(ABC):
289	    name: str
290	    description: str
291	    triggers: list[str]
292
293	    @abstractmethod
294	    async def execute(self, ctx: SkillContext) -> SkillResult:
295	        pass
296
297	    def matches(self, content: str) -> bool:
298	        """检查是否匹配触发条件"""
299	```
300
301	### 内置 Skills
302
303	| Skill | 触发器 | 功能 |
304	|-------|--------|------|
305	| LogSkill | `/log`, `/history` | 记录和检索 Shell 交互日志 |
306	| SystemSkill | `/sys`, `/system`, `/load` | 获取系统负载和资源信息 |
307
308	## 错误处理
309
310	### 错误分类
311
312	| 类型 | 说明 | 处理方式 |
313	|------|------|----------|
314	| TRANSIENT | 暂时性错误 | Inngest 自动重试 |
315	| PERMANENT | 永久性错误 | 返回用户友好消息 |
316	| USER_ERROR | 用户输入错误 | 提示正确用法 |
317
318	### 关键错误类型
319
320	- `SessionNotFoundError`: 会话不存在（可恢复）
321	- `TmuxCommandError`: tmux 命令执行失败（可恢复）
322	- `ApprovalTimeoutError`: 审批超时
323
324	## 测试策略
325
326	| 层级 | 测试类型 | 覆盖目标 |
327	|------|----------|----------|
328	| TmuxWrapper | 单元测试 | 100% 方法覆盖 |
329	| 模式检测 | 单元测试 | 所有进程类型 |
330	| 工作流 | 集成测试 | 主路径 + 错误路径 |
331	| Skill | 单元测试 | 所有触发场景 |
332	| 端到端 | E2E 测试 | 完整消息流程 |
333
334	## 部署配置
335
336	### docker-compose.yml
337
338	```yaml
339	version: '3.8'
340	services:
341	  inngest:
342	    image: inngest/inngest:latest
343	    ports:
344	      - "8288:8288"
345	    command: start --url http://host.docker.internal:8000/api/inngest
346	    extra_hosts:
347	      - "host.docker.internal:host-gateway"
348
349	  napcat:
350	    image: mlikiowa/napcat-docker:latest
351	    ports:
352	      - "3001:3001"
353	    volumes:
354	      - ./napcat-data:/app/napcat/config
355	```
356
357	### 环境变量
358
359	```bash
360	INNGEST_EVENT_KEY=your_event_key
361	INNGEST_SIGNING_KEY=your_signing_key
362	NAPCAT_WS_URL=ws://localhost:3001
363	NAPCAT_TOKEN=your_napcat_token
364	ANTHROPIC_API_KEY=your_claude_api_key
365	```
366
367	## 实施计划
368
369	按以下顺序实施：
370
371	1. **Phase 1: 核心会话层**
372	   - TmuxWrapper 实现
373	   - 模式检测器实现
374	   - 单元测试
375
376	2. **Phase 2: Inngest 工作流**
377	   - handle_message 工作流
378	   - guardrail 审批流程
379	   - 集成测试
380
381	3. **Phase 3: QQ Gateway**
382	   - NapCatQQ 集成
383	   - 消息转换
384	   - 端到端测试
385
386	4. **Phase 4: Skills 系统**
387	   - Skill 基类
388	   - LogSkill 实现
389	   - SystemSkill 实现
390
391	5. **Phase 5: 完善**
392	   - 上下文压缩
393	   - 错误处理优化
394	   - Mock 测试脚本
395
[result-id: r3]

---
name: Butler-Shell 系统设计
description: 基于 Inngest 与 tmux 的持久化 Shell 助手 (QQ 桥接)
type: project
---

# Butler-Shell 系统设计

## 概述

Butler-Shell 是一个通过 QQ 接收指令、在远程服务器上维护持久化 Shell 会话的系统。核心特性：

- **双模式切换**：自动在自然语言模式与 PTY 透传交互模式间切换
- **命令准入**：危险命令需要用户审批
- **上下文压缩**：`/compact` 指令压缩会话上下文
- **可扩展 Skills**：插件化功能扩展

## 技术栈

| 组件 | 技术选型 |
|------|----------|
| 任务编排 | Inngest (Python SDK) + 自托管 Server |
| PTY 容器 | tmux |
| QQ 接入 | NapCatQQ (WebSocket) |
| 状态存储 | Inngest State |
| 日志存储 | SQLite |
| Agent Logic | Claude API |

## 架构

```
┌─────────────────────────────────────────────────────────┐
│                    QQ Gateway                           │
│              (NapCatQQ WebSocket Handler)               │
└─────────────────────┬───────────────────────────────────┘
                      │ 消息事件
┌─────────────────────▼───────────────────────────────────┐
│                 Inngest Functions                       │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐       │
│  │handle_msg   │ │guardrail    │ │compaction   │       │
│  └─────────────┘ └─────────────┘ └─────────────┘       │
└─────────────────────┬───────────────────────────────────┘
                      │ 状态管理
┌─────────────────────▼───────────────────────────────────┐
│              Session Manager (TmuxWrapper)              │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐    │
│  │create_session│ │capture_pane │ │detect_mode   │    │
│  └──────────────┘ └──────────────┘ └──────────────┘    │
└─────────────────────┬───────────────────────────────────┘
                      │ PTY 控制
┌─────────────────────▼───────────────────────────────────┐
│                    tmux sessions                        │
│              [user_001] [user_002] ...                  │
└─────────────────────────────────────────────────────────┘
```

## 项目结构

```
butler-shell/
├── src/
│   └── butler/
│       ├── __init__.py
│       ├── main.py                    # FastAPI 入口 + Inngest Serve
│       │
│       ├── gateway/                   # QQ 消息接入层
│       │   ├── __init__.py
│       │   ├── napcat.py              # NapCatQQ WebSocket 客户端
│       │   └── events.py              # 消息事件定义
│       │
│       ├── workflows/                 # Inngest 工作流
│       │   ├── __init__.py
│       │   ├── handle_message.py      # 主消息处理流
│       │   ├── guardrail.py           # 命令准入审批
│       │   └── compaction.py          # 上下文压缩
│       │
│       ├── session/                   # Tmux 会话管理
│       │   ├── __init__.py
│       │   ├── wrapper.py             # TmuxWrapper 核心类
│       │   ├── detector.py            # 模式检测器
│       │   └── state.py               # 会话状态模型
│       │
│       ├── skills/                    # Skill 扩展系统
│       │   ├── __init__.py
│       │   ├── base.py                # SkillManager 基类
│       │   ├── log_skill.py           # 日志记录 Skill
│       │   └── system_skill.py        # 系统信息 Skill
│       │
│       └── config.py                  # 配置管理
│
├── tests/
│   ├── conftest.py
│   ├── test_wrapper.py
│   ├── test_detector.py
│   └── test_workflows.py
│
├── docs/
│   └── specs/
│
├── pyproject.toml
├── requirements.txt
└── docker-compose.yml                 # Inngest Server + 依赖
```

## 核心数据模型

### SessionState

```python
from enum import Enum
from pydantic import BaseModel, Field
from datetime import datetime
from typing import Optional

class SessionMode(str, Enum):
    """会话运行模式"""
    NATURAL_LANGUAGE = "nl"      # 自然语言模式 - 通过 LLM 解析
    INTERACTIVE = "interactive"  # 交互模式 - 直接透传到 PTY

class SessionState(BaseModel):
    """会话状态快照"""
    session_id: str = Field(..., description="tmux session 名称")
    user_id: str = Field(..., description="QQ 用户 ID")
    mode: SessionMode = Field(default=SessionMode.NATURAL_LANGUAGE)

    # Shell 状态
    pwd: str = Field(default="", description="当前工作目录")
    env: dict[str, str] = Field(default_factory=dict, description="关键环境变量")
    last_output: str = Field(default="", description="最近终端输出摘要")

    # 交互模式追踪
    interactive_process: Optional[str] = Field(
        default=None,
        description="当前交互进程名 (vim, top, etc.)"
    )

    # 元数据
    created_at: datetime = Field(default_factory=datetime.utcnow)
    updated_at: datetime = Field(default_factory=datetime.utcnow)
```

### MessageContext

```python
class MessageContext(BaseModel):
    """消息处理上下文"""
    message_id: str
    user_id: str
    content: str
    is_command: bool = False          # 是否以 / 开头
    requires_approval: bool = False   # 危险命令标记
    session: Optional[SessionState] = None
```

## TmuxWrapper 核心 API

```python
class TmuxWrapper:
    """tmux 会话封装器"""

    # 交互模式进程名单
    INTERACTIVE_PROCESSES = frozenset([
        "vim", "nvim", "vi",
        "top", "htop", "btop",
        "less", "more", "man",
        "claude", "aider",
        "python", "node", "irb", "ipython",
    ])

    # 快捷键映射
    SPECIAL_KEYS = {
        "C-c", "C-d", "C-z", "C-a", "C-e",
        "Esc", "Tab", "Up", "Down", "Left", "Right",
        "Enter", "Space",
    }

    def create_session(self, user_id: str) -> str:
        """为用户创建独立 tmux session"""

    def session_exists(self, session_name: str) -> bool:
        """检查 session 是否存在"""

    def kill_session(self, session_name: str) -> None:
        """销毁 session"""

    def send_keys(self, session_name: str, keys: str, enter: bool = True) -> None:
        """向 session 发送按键"""

    def send_raw(self, session_name: str, data: str) -> None:
        """发送原始数据（交互模式精确控制）"""

    def capture_pane(self, session_name: str, as_ansi: bool = False) -> str:
        """抓取终端屏幕内容"""

    def get_active_process(self, session_name: str) -> Optional[str]:
        """获取 tmux 窗口中的活跃进程名"""

    def get_pty_state(self, session_name: str) -> PtyState:
        """检测 PTY 是否在等待输入（通过 /proc 状态监控）"""

    def detect_mode(self, session_name: str) -> SessionMode:
        """混合模式检测：进程名单匹配 + PTY 状态检测"""
```

## Inngest 工作流

### handle_im_message

```
Step 1: get-or-create-session → butler_<user_id>
Step 2: load-state → SessionState
Step 3: detect-mode → NATURAL_LANGUAGE | INTERACTIVE

INTERACTIVE 模式:
  └─ send_keys(content, enter=False) → capture_pane() → 返回快照

NATURAL_LANGUAGE 模式:
  ├─ Skill 匹配? → execute Skill → 返回结果
  ├─ 危险命令? → 发送 im/command-needs-approval 事件
  ├─ 特殊指令? → 执行对应逻辑
  └─ send_keys(content, enter=True) → capture_pane() → 返回输出
```

### command_guardrail

```
Step 1: send-confirmation-request → 发送确认请求到 QQ
Step 2: wait-approval → ctx.step.wait_for_event (最长 5 分钟)
Step 3: 处理用户回复 → 执行或取消
```

### context_compaction

```
Step 1: get-pwd → 获取当前工作目录
Step 2: get-env → 获取关键环境变量
Step 3: get-output → 抓取最近终端输出
Step 4: summarize → LLM 总结（可选）
Step 5: 更新状态快照 → ctx.state.set()
```

## 消息处理流程

```
QQ 消息到达
     │
     ▼
NapCatGateway ──→ 转换为 NapCatEvent
     │
     ▼
Inngest Event ──→ 发送 im/message 事件
     │
     ▼
handle_im_message workflow
     │
     ├─ INTERACTIVE 模式 ──→ send_keys + capture_pane
     │
     └─ NATURAL_LANGUAGE 模式
          ├─ Skill 匹配 ──→ execute Skill
          ├─ 危险命令 ──→ command_guardrail
          └─ 正常命令 ──→ execute + capture
     │
     ▼
NapCatGateway ──→ 发送 QQ 回复
```

## 危险命令检测

扩展集合，包括：

| 命令模式 | 说明 |
|----------|------|
| `rm -rf` | 递归强制删除 |
| `chmod 777` | 开放所有权限 |
| `chown` | 修改所有者 |
| `dd` | 磁盘操作 |
| `mkfs` | 格式化 |
| `fdisk`/`parted` | 分区操作 |
| `> /dev/sd*` | 写入磁盘设备 |
| `iptables` | 防火墙规则 |
| `shutdown`/`reboot` | 系统关机/重启 |

## Skill 扩展系统

### 基类 API

```python
class SkillBase(ABC):
    name: str
    description: str
    triggers: list[str]

    @abstractmethod
    async def execute(self, ctx: SkillContext) -> SkillResult:
        pass

    def matches(self, content: str) -> bool:
        """检查是否匹配触发条件"""
```

### 内置 Skills

| Skill | 触发器 | 功能 |
|-------|--------|------|
| LogSkill | `/log`, `/history` | 记录和检索 Shell 交互日志 |
| SystemSkill | `/sys`, `/system`, `/load` | 获取系统负载和资源信息 |

## 错误处理

### 错误分类

| 类型 | 说明 | 处理方式 |
|------|------|----------|
| TRANSIENT | 暂时性错误 | Inngest 自动重试 |
| PERMANENT | 永久性错误 | 返回用户友好消息 |
| USER_ERROR | 用户输入错误 | 提示正确用法 |

### 关键错误类型

- `SessionNotFoundError`: 会话不存在（可恢复）
- `TmuxCommandError`: tmux 命令执行失败（可恢复）
- `ApprovalTimeoutError`: 审批超时

## 测试策略

| 层级 | 测试类型 | 覆盖目标 |
|------|----------|----------|
| TmuxWrapper | 单元测试 | 100% 方法覆盖 |
| 模式检测 | 单元测试 | 所有进程类型 |
| 工作流 | 集成测试 | 主路径 + 错误路径 |
| Skill | 单元测试 | 所有触发场景 |
| 端到端 | E2E 测试 | 完整消息流程 |

## 部署配置

### docker-compose.yml

```yaml
version: '3.8'
services:
  inngest:
    image: inngest/inngest:latest
    ports:
      - "8288:8288"
    command: start --url http://host.docker.internal:8000/api/inngest
    extra_hosts:
      - "host.docker.internal:host-gateway"

  napcat:
    image: mlikiowa/napcat-docker:latest
    ports:
      - "3001:3001"
    volumes:
      - ./napcat-data:/app/napcat/config
```

### 环境变量

```bash
INNGEST_EVENT_KEY=your_event_key
INNGEST_SIGNING_KEY=your_signing_key
NAPCAT_WS_URL=ws://localhost:3001
NAPCAT_TOKEN=your_napcat_token
ANTHROPIC_API_KEY=your_claude_api_key
```

## 实施计划

按以下顺序实施：

1. **Phase 1: 核心会话层**
   - TmuxWrapper 实现
   - 模式检测器实现
   - 单元测试

2. **Phase 2: Inngest 工作流**
   - handle_message 工作流
   - guardrail 审批流程
   - 集成测试

3. **Phase 3: QQ Gateway**
   - NapCatQQ 集成
   - 消息转换
   - 端到端测试

4. **Phase 4: Skills 系统**
   - Skill 基类
   - LogSkill 实现
   - SystemSkill 实现

5. **Phase 5: 完善**
   - 上下文压缩
   - 错误处理优化
   - Mock 测试脚本


规范审查发现了多个问题，我需要修复：

Critical:
1. 安全设计缺失 - 需要添加认证、授权、安全检测机制
2. 模式切换逻辑不完整 - 需要明确 INTERACTIVE → NL 的切换规则

Important:
1. PtyState 类型未定义
2. 会话生命周期管理缺失
3. 一致性问题 - 需要澄清架构图和 API 的对应关系
4. SQLite Schema 未定义
5. 错误处理缺少具体策略

让我更新设计文档，添加这些缺失的部分。


让我更新设计文档，修复审查发现的问题。


Edit

## 核心数据模型

### SessionState

```python
from enum import Enum
from pydantic import BaseModel, Field
from datetime import datetime
from typing import Optional

class SessionMode(str, Enum):
    """会话运行模式"""
    NATURAL_LANGUAGE = "nl"      # 自然语言模式 - 通过 LLM 解析
    INTERACTIVE = "interactive"  # 交互模式 - 直接透传到 PTY

class SessionState(BaseModel):

## 核心数据模型

### SessionMode & PtyState

```python
from enum import Enum
from pydantic import BaseModel, Field
from datetime import datetime
from typing import Optional

class SessionMode(str, Enum):
    """会话运行模式"""
    NATURAL_LANGUAGE = "nl"      # 自然语言模式 - 通过 LLM 解析
    INTERACTIVE = "interactive"  # 交互模式 - 直接透传到 PTY

class PtyState(str, Enum):
    """PTY 进程状态"""
    IDLE = "idle"           # 空闲，等待输入
    RUNNING = "running"     # 正在执行命令
    BLOCKED = "blocked"     # 阻塞中（如等待 I/O）
    UNKNOWN = "unknown"     # 状态检测失败

class SessionState(BaseModel):


The file /root/projects/qq-shell/docs/superpowers/specs/2026-04-07-butler-shell-design.md has been updated successfully.

## 核心数据模型

### SessionMode & PtyState

```python
from enum import Enum
from pydantic import BaseModel, Field
from datetime import datetime
from typing import Optional

class SessionMode(str, Enum):
    """会话运行模式"""
    NATURAL_LANGUAGE = "nl"      # 自然语言模式 - 通过 LLM 解析
    INTERACTIVE = "interactive"  # 交互模式 - 直接透传到 PTY

class PtyState(str, Enum):
    """PTY 进程状态"""
    IDLE = "idle"           # 空闲，等待输入
    RUNNING = "running"     # 正在执行命令
    BLOCKED = "blocked"     # 阻塞中（如等待 I/O）
    UNKNOWN = "unknown"     # 状态检测失败

class SessionState(BaseModel):

---
name: Butler-Shell 系统设计
description: 基于 Inngest 与 tmux 的持久化 Shell 助手 (QQ 桥接)
type: project
---

# Butler-Shell 系统设计

## 概述

Butler-Shell 是一个通过 QQ 接收指令、在远程服务器上维护持久化 Shell 会话的系统。核心特性：

- **双模式切换**：自动在自然语言模式与 PTY 透传交互模式间切换
- **命令准入**：危险命令需要用户审批
- **上下文压缩**：`/compact` 指令压缩会话上下文
- **可扩展 Skills**：插件化功能扩展

## 技术栈

| 组件 | 技术选型 |
|------|----------|
| 任务编排 | Inngest (Python SDK) + 自托管 Server |
| PTY 容器 | tmux |
| QQ 接入 | NapCatQQ (WebSocket) |
| 状态存储 | Inngest State |
| 日志存储 | SQLite |
| Agent Logic | Claude API |

## 架构

```
┌─────────────────────────────────────────────────────────┐
│                    QQ Gateway                           │
│              (NapCatQQ WebSocket Handler)               │
└─────────────────────┬───────────────────────────────────┘
                      │ 消息事件
┌─────────────────────▼───────────────────────────────────┐
│                 Inngest Functions                       │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐       │
│  │handle_msg   │ │guardrail    │ │compaction   │       │
│  └─────────────┘ └─────────────┘ └─────────────┘       │
└─────────────────────┬───────────────────────────────────┘
                      │ 状态管理
┌─────────────────────▼───────────────────────────────────┐
│              Session Manager (TmuxWrapper)              │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐    │
│  │create_session│ │capture_pane │ │detect_mode   │    │
│  └──────────────┘ └──────────────┘ └──────────────┘    │
└─────────────────────┬───────────────────────────────────┘
                      │ PTY 控制
┌─────────────────────▼───────────────────────────────────┐
│                    tmux sessions                        │
│              [user_001] [user_002] ...                  │
└─────────────────────────────────────────────────────────┘
```

## 项目结构

```
butler-shell/
├── src/
│   └── butler/
│       ├── __init__.py
│       ├── main.py                    # FastAPI 入口 + Inngest Serve
│       │
│       ├── gateway/                   # QQ 消息接入层
│       │   ├── __init__.py
│       │   ├── napcat.py              # NapCatQQ WebSocket 客户端
│       │   └── events.py              # 消息事件定义
│       │
│       ├── workflows/                 # Inngest 工作流
│       │   ├── __init__.py
│       │   ├── handle_message.py      # 主消息处理流
│       │   ├── guardrail.py           # 命令准入审批
│       │   └── compaction.py          # 上下文压缩
│       │
│       ├── session/                   # Tmux 会话管理
│       │   ├── __init__.py
│       │   ├── wrapper.py             # TmuxWrapper 核心类
│       │   ├── detector.py            # 模式检测器
│       │   └── state.py               # 会话状态模型
│       │
│       ├── skills/                    # Skill 扩展系统
│       │   ├── __init__.py
│       │   ├── base.py                # SkillManager 基类
│       │   ├── log_skill.py           # 日志记录 Skill
│       │   └── system_skill.py        # 系统信息 Skill
│       │
│       └── config.py                  # 配置管理
│
├── tests/
│   ├── conftest.py
│   ├── test_wrapper.py
│   ├── test_detector.py
│   └── test_workflows.py
│
├── docs/
│   └── specs/
│
├── pyproject.toml
├── requirements.txt
└── docker-compose.yml                 # Inngest Server + 依赖
```

## 核心数据模型

### SessionState

```python
from enum import Enum
from pydantic import BaseModel, Field
from datetime import datetime
from typing import Optional

class SessionMode(str, Enum):
    """会话运行模式"""
    NATURAL_LANGUAGE = "nl"      # 自然语言模式 - 通过 LLM 解析
    INTERACTIVE = "interactive"  # 交互模式 - 直接透传到 PTY

class SessionState(BaseModel):
    """会话状态快照"""
    session_id: str = Field(..., description="tmux session 名称")
    user_id: str = Field(..., description="QQ 用户 ID")
    mode: SessionMode = Field(default=SessionMode.NATURAL_LANGUAGE)

    # Shell 状态
    pwd: str = Field(default="", description="当前工作目录")
    env: dict[str, str] = Field(default_factory=dict, description="关键环境变量")
    last_output: str = Field(default="", description="最近终端输出摘要")

    # 交互模式追踪
    interactive_process: Optional[str] = Field(
        default=None,
        description="当前交互进程名 (vim, top, etc.)"
    )

    # 元数据
    created_at: datetime = Field(default_factory=datetime.utcnow)
    updated_at: datetime = Field(default_factory=datetime.utcnow)
```

### MessageContext

```python
class MessageContext(BaseModel):
    """消息处理上下文"""
    message_id: str
    user_id: str
    content: str
    is_command: bool = False          # 是否以 / 开头
    requires_approval: bool = False   # 危险命令标记
    session: Optional[SessionState] = None
```

## TmuxWrapper 核心 API

```python
class TmuxWrapper:
    """tmux 会话封装器"""

    # 交互模式进程名单
    INTERACTIVE_PROCESSES = frozenset([
        "vim", "nvim", "vi",
        "top", "htop", "btop",
        "less", "more", "man",
        "claude", "aider",
        "python", "node", "irb", "ipython",
    ])

    # 快捷键映射
    SPECIAL_KEYS = {
        "C-c", "C-d", "C-z", "C-a", "C-e",
        "Esc", "Tab", "Up", "Down", "Left", "Right",
        "Enter", "Space",
    }

    def create_session(self, user_id: str) -> str:
        """为用户创建独立 tmux session"""

    def session_exists(self, session_name: str) -> bool:
        """检查 session 是否存在"""

    def kill_session(self, session_name: str) -> None:
        """销毁 session"""

    def send_keys(self, session_name: str, keys: str, enter: bool = True) -> None:
        """向 session 发送按键"""

    def send_raw(self, session_name: str, data: str) -> None:
        """发送原始数据（交互模式精确控制）"""

    def capture_pane(self, session_name: str, as_ansi: bool = False) -> str:
        """抓取终端屏幕内容"""

    def get_active_process(self, session_name: str) -> Optional[str]:
        """获取 tmux 窗口中的活跃进程名"""

    def get_pty_state(self, session_name: str) -> PtyState:
        """检测 PTY 是否在等待输入（通过 /proc 状态监控）"""

    def detect_mode(self, session_name: str) -> SessionMode:
        """混合模式检测：进程名单匹配 + PTY 状态检测"""
```

## Inngest 工作流

### handle_im_message

```
Step 1: get-or-create-session → butler_<user_id>
Step 2: load-state → SessionState
Step 3: detect-mode → NATURAL_LANGUAGE | INTERACTIVE

INTERACTIVE 模式:
  └─ send_keys(content, enter=False) → capture_pane() → 返回快照

NATURAL_LANGUAGE 模式:
  ├─ Skill 匹配? → execute Skill → 返回结果
  ├─ 危险命令? → 发送 im/command-needs-approval 事件
  ├─ 特殊指令? → 执行对应逻辑
  └─ send_keys(content, enter=True) → capture_pane() → 返回输出
```

### command_guardrail

```
Step 1: send-confirmation-request → 发送确认请求到 QQ
Step 2: wait-approval → ctx.step.wait_for_event (最长 5 分钟)
Step 3: 处理用户回复 → 执行或取消
```

### context_compaction

```
Step 1: get-pwd → 获取当前工作目录
Step 2: get-env → 获取关键环境变量
Step 3: get-output → 抓取最近终端输出
Step 4: summarize → LLM 总结（可选）
Step 5: 更新状态快照 → ctx.state.set()
```

## 消息处理流程

```
QQ 消息到达
     │
     ▼
NapCatGateway ──→ 转换为 NapCatEvent
     │
     ▼
Inngest Event ──→ 发送 im/message 事件
     │
     ▼
handle_im_message workflow
     │
     ├─ INTERACTIVE 模式 ──→ send_keys + capture_pane
     │
     └─ NATURAL_LANGUAGE 模式
          ├─ Skill 匹配 ──→ execute Skill
          ├─ 危险命令 ──→ command_guardrail
          └─ 正常命令 ──→ execute + capture
     │
     ▼
NapCatGateway ──→ 发送 QQ 回复
```

## 危险命令检测

扩展集合，包括：

| 命令模式 | 说明 |
|----------|------|
| `rm -rf` | 递归强制删除 |
| `chmod 777` | 开放所有权限 |
| `chown` | 修改所有者 |
| `dd` | 磁盘操作 |
| `mkfs` | 格式化 |
| `fdisk`/`parted` | 分区操作 |
| `> /dev/sd*` | 写入磁盘设备 |
| `iptables` | 防火墙规则 |
| `shutdown`/`reboot` | 系统关机/重启 |

## Skill 扩展系统

### 基类 API

```python
class SkillBase(ABC):
    name: str
    description: str
    triggers: list[str]

    @abstractmethod
    async def execute(self, ctx: SkillContext) -> SkillResult:
        pass

    def matches(self, content: str) -> bool:
        """检查是否匹配触发条件"""
```

### 内置 Skills

| Skill | 触发器 | 功能 |
|-------|--------|------|
| LogSkill | `/log`, `/history` | 记录和检索 Shell 交互日志 |
| SystemSkill | `/sys`, `/system`, `/load` | 获取系统负载和资源信息 |

## 错误处理

### 错误分类

| 类型 | 说明 | 处理方式 |
|------|------|----------|
| TRANSIENT | 暂时性错误 | Inngest 自动重试 |
| PERMANENT | 永久性错误 | 返回用户友好消息 |
| USER_ERROR | 用户输入错误 | 提示正确用法 |

### 关键错误类型

- `SessionNotFoundError`: 会话不存在（可恢复）
- `TmuxCommandError`: tmux 命令执行失败（可恢复）
- `ApprovalTimeoutError`: 审批超时

## 测试策略

| 层级 | 测试类型 | 覆盖目标 |
|------|----------|----------|
| TmuxWrapper | 单元测试 | 100% 方法覆盖 |
| 模式检测 | 单元测试 | 所有进程类型 |
| 工作流 | 集成测试 | 主路径 + 错误路径 |
| Skill | 单元测试 | 所有触发场景 |
| 端到端 | E2E 测试 | 完整消息流程 |

## 部署配置

### docker-compose.yml

```yaml
version: '3.8'
services:
  inngest:
    image: inngest/inngest:latest
    ports:
      - "8288:8288"
    command: start --url http://host.docker.internal:8000/api/inngest
    extra_hosts:
      - "host.docker.internal:host-gateway"

  napcat:
    image: mlikiowa/napcat-docker:latest
    ports:
      - "3001:3001"
    volumes:
      - ./napcat-data:/app/napcat/config
```

### 环境变量

```bash
INNGEST_EVENT_KEY=your_event_key
INNGEST_SIGNING_KEY=your_signing_key
NAPCAT_WS_URL=ws://localhost:3001
NAPCAT_TOKEN=your_napcat_token
ANTHROPIC_API_KEY=your_claude_api_key
```

## 实施计划

按以下顺序实施：

1. **Phase 1: 核心会话层**
   - TmuxWrapper 实现
   - 模式检测器实现
   - 单元测试

2. **Phase 2: Inngest 工作流**
   - handle_message 工作流
   - guardrail 审批流程
   - 集成测试

3. **Phase 3: QQ Gateway**
   - NapCatQQ 集成
   - 消息转换
   - 端到端测试

4. **Phase 4: Skills 系统**
   - Skill 基类
   - LogSkill 实现
   - SystemSkill 实现

5. **Phase 5: 完善**
   - 上下文压缩
   - 错误处理优化
   - Mock 测试脚本

## 核心数据模型
-### SessionState
+### SessionMode & PtyState
```python
from enum import Enum

+    BLOCKED = "blocked"     # 阻塞中（如等待 I/O）

"""会话状态快照"""


Good, I added PtyState enum. Now let me add the security section after the architecture section, add mode switching rules, session lifecycle, and other missing pieces.


│              [user_001] [user_002] ...                  │
└─────────────────────────────────────────────────────────┘
```

## 项目结构

│              [user_001] [user_002] ...                  │
└─────────────────────────────────────────────────────────┘
```

## 安全设计

### 认证与授权

| 机制 | 说明 |
|------|------|
| 用户白名单 | 仅允许配置文件中的 QQ 用户 ID 使用系统 |
| 消息签名验证 | 验证 NapCatQQ 消息来源合法性 |
| 会话令牌 | 每个会话生成唯一 UUID，防止会话劫持 |

```python
# 白名单检查
ALLOWED_USERS = os.getenv("BUTLER_ALLOWED_USERS", "").split(",")

def is_authorized(user_id: str) -> bool:
    return user_id in ALLOWED_USERS
```

### 命令安全检测

危险命令检测使用多层防护：

1. **正则匹配**：基础检测
2. **命令解析**：使用 `shlex.split()` 解析命令结构
3. **嵌套检测**：检测命令替换、管道执行等绕过方式

```python
import shlex
import re

DANGEROUS_PATTERNS = [
    # 基础危险命令
    r"\brm\s+(-[rf]+\s+|.*-rf)",
    r"\bchmod\s+(-R\s+)?777",
    r"\bchown\s+",
    r"\bdd\s+",
    r"\bmkfs\b",
    r"\b(fdisk|parted)\b",
    r">\s*/dev/sd",
    r"\biptables\b",
    r"\bshutdown\b",
    r"\breboot\b",
    # 绕过防护
    r"\bsudo\b",                    # sudo 提权
    r"\bsh\s+-c\b",                 # 命令嵌套
    r"\bbash\s+-c\b",               # 命令嵌套
    r"\|.*\b(bash|sh)\b",           # 管道执行
    r"\$\([^)]+\)",                 # 命令替换
    r"`[^`]+`",                     # 反引号命令替换
    r"fork\s*\(\s*\)",              # fork bomb
]

def is_dangerous_command(cmd: str) -> bool:
    # 正则匹配
    if any(re.search(p, cmd, re.IGNORECASE) for p in DANGEROUS_PATTERNS):
        return True

    # 解析命令结构
    try:
        parts = shlex.split(cmd)
        if any(p in DANGEROUS_COMMANDS for p in parts):
            return True
    except ValueError:
        # 解析失败，保守处理
        return True

    return False
```

### 敏感信息处理

- **终端输出过滤**：移除匹配密码、API Key 模式的文本
- **日志脱敏**：Token、密钥等不记录明文
- **会话隔离**：每个用户独立 tmux session

```python
SENSITIVE_PATTERNS = [
    r"(password|passwd|pwd)\s*=\s*\S+",
    r"(api[_-]?key|token)\s*=\s*\S+",
    r"Bearer\s+\S+",
    r"-----BEGIN.*KEY-----",
]

def sanitize_output(output: str) -> str:
    for pattern in SENSITIVE_PATTERNS:
        output = re.sub(pattern, "[REDACTED]", output, flags=re.IGNORECASE)
    return output
```

## 模式切换规则

| 切换方向 | 触发条件 | 用户可干预 |
|----------|----------|------------|
| NL → INTERACTIVE | 检测到交互进程启动 | 否 |
| INTERACTIVE → NL | 检测到进程退出 | 是，发送 `/nl` |
| INTERACTIVE → NL | 发送 `C-d` 或 `:q` | 是 |
| 强制 NL | 发送 `/force-nl` | 是 |

### 模式检测降级

当模式检测失败时（如 `/proc` 访问权限问题）：

- 默认使用 `NATURAL_LANGUAGE` 模式
- 记录 WARN 级别日志
- 提示用户可手动切换：`/interactive` 进入交互模式

## 会话生命周期管理

### 会话策略

| 配置项 | 默认值 | 说明 |
|--------|--------|------|
| `SESSION_IDLE_TIMEOUT` | 3600s | 空闲超时后自动销毁 |
| `MAX_SESSIONS_PER_USER` | 1 | 每用户最大会话数 |
| `MAX_TOTAL_SESSIONS` | 100 | 全局最大会话数 |

### 会话清理

定时任务每小时执行：

1. 扫描所有会话的最后活跃时间
2. 超过 `SESSION_IDLE_TIMEOUT` 的会话标记为待清理
3. 发送通知给用户（可选）
4. 优雅关闭 tmux 会话

```python
import asyncio

async def cleanup_idle_sessions():
    while True:
        await asyncio.sleep(3600)  # 每小时执行
        for session in get_all_sessions():
            if session.idle_seconds > SESSION_IDLE_TIMEOUT:
                await notify_user(session.user_id, "会话已超时，自动关闭")
                tmux.kill_session(session.session_id)
```

## 项目结构


│              [user_001] [user_002] ...                  │
└─────────────────────────────────────────────────────────┘
```

## 安全设计

### 认证与授权

| 机制 | 说明 |
|------|------|
| 用户白名单 | 仅允许配置文件中的 QQ 用户 ID 使用系统 |
| 消息签名验证 | 验证 NapCatQQ 消息来源合法性 |
| 会话令牌 | 每个会话生成唯一 UUID，防止会话劫持 |

```python
# 白名单检查
ALLOWED_USERS = os.getenv("BUTLER_ALLOWED_USERS", "").split(",")

def is_authorized(user_id: str) -> bool:
    return user_id in ALLOWED_USERS
```

### 命令安全检测

危险命令检测使用多层防护：

1. **正则匹配**：基础检测
2. **命令解析**：使用 `shlex.split()` 解析命令结构
3. **嵌套检测**：检测命令替换、管道执行等绕过方式

```python
import shlex
import re

DANGEROUS_PATTERNS = [
    # 基础危险命令
    r"\brm\s+(-[rf]+\s+|.*-rf)",
    r"\bchmod\s+(-R\s+)?777",
    r"\bchown\s+",
    r"\bdd\s+",
    r"\bmkfs\b",
    r"\b(fdisk|parted)\b",
    r">\s*/dev/sd",
    r"\biptables\b",
    r"\bshutdown\b",
    r"\breboot\b",
    # 绕过防护
    r"\bsudo\b",                    # sudo 提权
    r"\bsh\s+-c\b",                 # 命令嵌套
    r"\bbash\s+-c\b",               # 命令嵌套
    r"\|.*\b(bash|sh)\b",           # 管道执行
    r"\$\([^)]+\)",                 # 命令替换
    r"`[^`]+`",                     # 反引号命令替换
    r"fork\s*\(\s*\)",              # fork bomb
]

def is_dangerous_command(cmd: str) -> bool:
    # 正则匹配
    if any(re.search(p, cmd, re.IGNORECASE) for p in DANGEROUS_PATTERNS):
        return True

    # 解析命令结构
    try:
        parts = shlex.split(cmd)
        if any(p in DANGEROUS_COMMANDS for p in parts):
            return True
    except ValueError:
        # 解析失败，保守处理
        return True

    return False
```

### 敏感信息处理

- **终端输出过滤**：移除匹配密码、API Key 模式的文本
- **日志脱敏**：Token、密钥等不记录明文
- **会话隔离**：每个用户独立 tmux session

```python
SENSITIVE_PATTERNS = [
    r"(password|passwd|pwd)\s*=\s*\S+",
    r"(api[_-]?key|token)\s*=\s*\S+",
    r"Bearer\s+\S+",
    r"-----BEGIN.*KEY-----",
]

def sanitize_output(output: str) -> str:
    for pattern in SENSITIVE_PATTERNS:
        output = re.sub(pattern, "[REDACTED]", output, flags=re.IGNORECASE)
    return output
```

## 模式切换规则

| 切换方向 | 触发条件 | 用户可干预 |
|----------|----------|------------|
| NL → INTERACTIVE | 检测到交互进程启动 | 否 |
| INTERACTIVE → NL | 检测到进程退出 | 是，发送 `/nl` |
| INTERACTIVE → NL | 发送 `C-d` 或 `:q` | 是 |
| 强制 NL | 发送 `/force-nl` | 是 |

### 模式检测降级

当模式检测失败时（如 `/proc` 访问权限问题）：

- 默认使用 `NATURAL_LANGUAGE` 模式
- 记录 WARN 级别日志
- 提示用户可手动切换：`/interactive` 进入交互模式

## 会话生命周期管理

### 会话策略

| 配置项 | 默认值 | 说明 |
|--------|--------|------|
| `SESSION_IDLE_TIMEOUT` | 3600s | 空闲超时后自动销毁 |
| `MAX_SESSIONS_PER_USER` | 1 | 每用户最大会话数 |
| `MAX_TOTAL_SESSIONS` | 100 | 全局最大会话数 |

### 会话清理

定时任务每小时执行：

1. 扫描所有会话的最后活跃时间
2. 超过 `SESSION_IDLE_TIMEOUT` 的会话标记为待清理
3. 发送通知给用户（可选）
4. 优雅关闭 tmux 会话

```python
import asyncio

async def cleanup_idle_sessions():
    while True:
        await asyncio.sleep(3600)  # 每小时执行
        for session in get_all_sessions():
            if session.idle_seconds > SESSION_IDLE_TIMEOUT:
                await notify_user(session.user_id, "会话已超时，自动关闭")
                tmux.kill_session(session.session_id)
```

## 项目结构

---
name: Butler-Shell 系统设计
description: 基于 Inngest 与 tmux 的持久化 Shell 助手 (QQ 桥接)
type: project
---

# Butler-Shell 系统设计

## 概述

Butler-Shell 是一个通过 QQ 接收指令、在远程服务器上维护持久化 Shell 会话的系统。核心特性：

- **双模式切换**：自动在自然语言模式与 PTY 透传交互模式间切换
- **命令准入**：危险命令需要用户审批
- **上下文压缩**：`/compact` 指令压缩会话上下文
- **可扩展 Skills**：插件化功能扩展

## 技术栈

| 组件 | 技术选型 |
|------|----------|
| 任务编排 | Inngest (Python SDK) + 自托管 Server |
| PTY 容器 | tmux |
| QQ 接入 | NapCatQQ (WebSocket) |
| 状态存储 | Inngest State |
| 日志存储 | SQLite |
| Agent Logic | Claude API |

## 架构

```
┌─────────────────────────────────────────────────────────┐
│                    QQ Gateway                           │
│              (NapCatQQ WebSocket Handler)               │
└─────────────────────┬───────────────────────────────────┘
                      │ 消息事件
┌─────────────────────▼───────────────────────────────────┐
│                 Inngest Functions                       │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐       │
│  │handle_msg   │ │guardrail    │ │compaction   │       │
│  └─────────────┘ └─────────────┘ └─────────────┘       │
└─────────────────────┬───────────────────────────────────┘
                      │ 状态管理
┌─────────────────────▼───────────────────────────────────┐
│              Session Manager (TmuxWrapper)              │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐    │
│  │create_session│ │capture_pane │ │detect_mode   │    │
│  └──────────────┘ └──────────────┘ └──────────────┘    │
└─────────────────────┬───────────────────────────────────┘
                      │ PTY 控制
┌─────────────────────▼───────────────────────────────────┐
│                    tmux sessions                        │
│              [user_001] [user_002] ...                  │
└─────────────────────────────────────────────────────────┘
```

## 项目结构

```
butler-shell/
├── src/
│   └── butler/
│       ├── __init__.py
│       ├── main.py                    # FastAPI 入口 + Inngest Serve
│       │
│       ├── gateway/                   # QQ 消息接入层
│       │   ├── __init__.py
│       │   ├── napcat.py              # NapCatQQ WebSocket 客户端
│       │   └── events.py              # 消息事件定义
│       │
│       ├── workflows/                 # Inngest 工作流
│       │   ├── __init__.py
│       │   ├── handle_message.py      # 主消息处理流
│       │   ├── guardrail.py           # 命令准入审批
│       │   └── compaction.py          # 上下文压缩
│       │
│       ├── session/                   # Tmux 会话管理
│       │   ├── __init__.py
│       │   ├── wrapper.py             # TmuxWrapper 核心类
│       │   ├── detector.py            # 模式检测器
│       │   └── state.py               # 会话状态模型
│       │
│       ├── skills/                    # Skill 扩展系统
│       │   ├── __init__.py
│       │   ├── base.py                # SkillManager 基类
│       │   ├── log_skill.py           # 日志记录 Skill
│       │   └── system_skill.py        # 系统信息 Skill
│       │
│       └── config.py                  # 配置管理
│
├── tests/
│   ├── conftest.py
│   ├── test_wrapper.py
│   ├── test_detector.py
│   └── test_workflows.py
│
├── docs/
│   └── specs/
│
├── pyproject.toml
├── requirements.txt
└── docker-compose.yml                 # Inngest Server + 依赖
```

## 核心数据模型

### SessionMode & PtyState

```python
from enum import Enum
from pydantic import BaseModel, Field
from datetime import datetime
from typing import Optional

class SessionMode(str, Enum):
    """会话运行模式"""
    NATURAL_LANGUAGE = "nl"      # 自然语言模式 - 通过 LLM 解析
    INTERACTIVE = "interactive"  # 交互模式 - 直接透传到 PTY

class PtyState(str, Enum):
    """PTY 进程状态"""
    IDLE = "idle"           # 空闲，等待输入
    RUNNING = "running"     # 正在执行命令
    BLOCKED = "blocked"     # 阻塞中（如等待 I/O）
    UNKNOWN = "unknown"     # 状态检测失败

class SessionState(BaseModel):
    """会话状态快照"""
    session_id: str = Field(..., description="tmux session 名称")
    user_id: str = Field(..., description="QQ 用户 ID")
    mode: SessionMode = Field(default=SessionMode.NATURAL_LANGUAGE)

    # Shell 状态
    pwd: str = Field(default="", description="当前工作目录")
    env: dict[str, str] = Field(default_factory=dict, description="关键环境变量")
    last_output: str = Field(default="", description="最近终端输出摘要")

    # 交互模式追踪
    interactive_process: Optional[str] = Field(
        default=None,
        description="当前交互进程名 (vim, top, etc.)"
    )

    # 元数据
    created_at: datetime = Field(default_factory=datetime.utcnow)
    updated_at: datetime = Field(default_factory=datetime.utcnow)
```

### MessageContext

```python
class MessageContext(BaseModel):
    """消息处理上下文"""
    message_id: str
    user_id: str
    content: str
    is_command: bool = False          # 是否以 / 开头
    requires_approval: bool = False   # 危险命令标记
    session: Optional[SessionState] = None
```

## TmuxWrapper 核心 API

```python
class TmuxWrapper:
    """tmux 会话封装器"""

    # 交互模式进程名单
    INTERACTIVE_PROCESSES = frozenset([
        "vim", "nvim", "vi",
        "top", "htop", "btop",
        "less", "more", "man",
        "claude", "aider",
        "python", "node", "irb", "ipython",
    ])

    # 快捷键映射
    SPECIAL_KEYS = {
        "C-c", "C-d", "C-z", "C-a", "C-e",
        "Esc", "Tab", "Up", "Down", "Left", "Right",
        "Enter", "Space",
    }

    def create_session(self, user_id: str) -> str:
        """为用户创建独立 tmux session"""

    def session_exists(self, session_name: str) -> bool:
        """检查 session 是否存在"""

    def kill_session(self, session_name: str) -> None:
        """销毁 session"""

    def send_keys(self, session_name: str, keys: str, enter: bool = True) -> None:
        """向 session 发送按键"""

    def send_raw(self, session_name: str, data: str) -> None:
        """发送原始数据（交互模式精确控制）"""

    def capture_pane(self, session_name: str, as_ansi: bool = False) -> str:
        """抓取终端屏幕内容"""

    def get_active_process(self, session_name: str) -> Optional[str]:
        """获取 tmux 窗口中的活跃进程名"""

    def get_pty_state(self, session_name: str) -> PtyState:
        """检测 PTY 是否在等待输入（通过 /proc 状态监控）"""

    def detect_mode(self, session_name: str) -> SessionMode:
        """混合模式检测：进程名单匹配 + PTY 状态检测"""
```

## Inngest 工作流

### handle_im_message

```
Step 1: get-or-create-session → butler_<user_id>
Step 2: load-state → SessionState
Step 3: detect-mode → NATURAL_LANGUAGE | INTERACTIVE

INTERACTIVE 模式:
  └─ send_keys(content, enter=False) → capture_pane() → 返回快照

NATURAL_LANGUAGE 模式:
  ├─ Skill 匹配? → execute Skill → 返回结果
  ├─ 危险命令? → 发送 im/command-needs-approval 事件
  ├─ 特殊指令? → 执行对应逻辑
  └─ send_keys(content, enter=True) → capture_pane() → 返回输出
```

### command_guardrail

```
Step 1: send-confirmation-request → 发送确认请求到 QQ
Step 2: wait-approval → ctx.step.wait_for_event (最长 5 分钟)
Step 3: 处理用户回复 → 执行或取消
```

### context_compaction

```
Step 1: get-pwd → 获取当前工作目录
Step 2: get-env → 获取关键环境变量
Step 3: get-output → 抓取最近终端输出
Step 4: summarize → LLM 总结（可选）
Step 5: 更新状态快照 → ctx.state.set()
```

## 消息处理流程

```
QQ 消息到达
     │
     ▼
NapCatGateway ──→ 转换为 NapCatEvent
     │
     ▼
Inngest Event ──→ 发送 im/message 事件
     │
     ▼
handle_im_message workflow
     │
     ├─ INTERACTIVE 模式 ──→ send_keys + capture_pane
     │
     └─ NATURAL_LANGUAGE 模式
          ├─ Skill 匹配 ──→ execute Skill
          ├─ 危险命令 ──→ command_guardrail
          └─ 正常命令 ──→ execute + capture
     │
     ▼
NapCatGateway ──→ 发送 QQ 回复
```

## 危险命令检测

扩展集合，包括：

| 命令模式 | 说明 |
|----------|------|
| `rm -rf` | 递归强制删除 |
| `chmod 777` | 开放所有权限 |
| `chown` | 修改所有者 |
| `dd` | 磁盘操作 |
| `mkfs` | 格式化 |
| `fdisk`/`parted` | 分区操作 |
| `> /dev/sd*` | 写入磁盘设备 |
| `iptables` | 防火墙规则 |
| `shutdown`/`reboot` | 系统关机/重启 |

## Skill 扩展系统

### 基类 API

```python
class SkillBase(ABC):
    name: str
    description: str
    triggers: list[str]

    @abstractmethod
    async def execute(self, ctx: SkillContext) -> SkillResult:
        pass

    def matches(self, content: str) -> bool:
        """检查是否匹配触发条件"""
```

### 内置 Skills

| Skill | 触发器 | 功能 |
|-------|--------|------|
| LogSkill | `/log`, `/history` | 记录和检索 Shell 交互日志 |
| SystemSkill | `/sys`, `/system`, `/load` | 获取系统负载和资源信息 |

## 错误处理

### 错误分类

| 类型 | 说明 | 处理方式 |
|------|------|----------|
| TRANSIENT | 暂时性错误 | Inngest 自动重试 |
| PERMANENT | 永久性错误 | 返回用户友好消息 |
| USER_ERROR | 用户输入错误 | 提示正确用法 |

### 关键错误类型

- `SessionNotFoundError`: 会话不存在（可恢复）
- `TmuxCommandError`: tmux 命令执行失败（可恢复）
- `ApprovalTimeoutError`: 审批超时

## 测试策略

| 层级 | 测试类型 | 覆盖目标 |
|------|----------|----------|
| TmuxWrapper | 单元测试 | 100% 方法覆盖 |
| 模式检测 | 单元测试 | 所有进程类型 |
| 工作流 | 集成测试 | 主路径 + 错误路径 |
| Skill | 单元测试 | 所有触发场景 |
| 端到端 | E2E 测试 | 完整消息流程 |

## 部署配置

### docker-compose.yml

```yaml
version: '3.8'
services:
  inngest:
    image: inngest/inngest:latest
    ports:
      - "8288:8288"
    command: start --url http://host.docker.internal:8000/api/inngest
    extra_hosts:
      - "host.docker.internal:host-gateway"

  napcat:
    image: mlikiowa/napcat-docker:latest
    ports:
      - "3001:3001"
    volumes:
      - ./napcat-data:/app/napcat/config
```

### 环境变量

```bash
INNGEST_EVENT_KEY=your_event_key
INNGEST_SIGNING_KEY=your_signing_key
NAPCAT_WS_URL=ws://localhost:3001
NAPCAT_TOKEN=your_napcat_token
ANTHROPIC_API_KEY=your_claude_api_key
```

## 实施计划

按以下顺序实施：

1. **Phase 1: 核心会话层**
   - TmuxWrapper 实现
   - 模式检测器实现
   - 单元测试

2. **Phase 2: Inngest 工作流**
   - handle_message 工作流
   - guardrail 审批流程
   - 集成测试

3. **Phase 3: QQ Gateway**
   - NapCatQQ 集成
   - 消息转换
   - 端到端测试

4. **Phase 4: Skills 系统**
   - Skill 基类
   - LogSkill 实现
   - SystemSkill 实现

5. **Phase 5: 完善**
   - 上下文压缩
   - 错误处理优化
   - Mock 测试脚本

+| 机制 | 说明 |

+| 用户白名单 | 仅允许配置文件中的 QQ 用户 ID 使用系统 |

+| 消息签名验证 | 验证 NapCatQQ 消息来源合法性 |

+| 会话令牌 | 每个会话生成唯一 UUID，防止会话劫持 |

+ALLOWED_USERS = os.getenv("BUTLER_ALLOWED_USERS", "").split(",")

+def is_authorized(user_id: str) -> bool:

+危险命令检测使用多层防护：

+1. **正则匹配**：基础检测

+3. **嵌套检测**：检测命令替换、管道执行等绕过方式

+    # 基础危险命令

+    r"\bsudo\b",                    # sudo 提权

+    r"\bsh\s+-c\b",                 # 命令嵌套

+    r"\bbash\s+-c\b",               # 命令嵌套

+    r"fork\s*\(\s*\)",              # fork bomb

+def is_dangerous_command(cmd: str) -> bool:

+    if any(re.search(p, cmd, re.IGNORECASE) for p in DANGEROUS_PATTERNS):

+    # 解析命令结构

+        if any(p in DANGEROUS_COMMANDS for p in parts):

+        # 解析失败，保守处理

+- **终端输出过滤**：移除匹配密码、API Key 模式的文本

+- **日志脱敏**：Token、密钥等不记录明文

+    r"(password|passwd|pwd)\s*=\s*\S+",

+def sanitize_output(output: str) -> str:

+        output = re.sub(pattern, "[REDACTED]", output, flags=re.IGNORECASE)

+| 切换方向 | 触发条件 | 用户可干预 |

+| INTERACTIVE → NL | 检测到进程退出 | 是，发送 `/nl` |

+| INTERACTIVE → NL | 发送 `C-d` 或 `:q` | 是 |

+当模式检测失败时（如 `/proc` 访问权限问题）：

+- 记录 WARN 级别日志

+- 提示用户可手动切换：`/interactive` 进入交互模式

+## 会话生命周期管理

+| 配置项 | 默认值 | 说明 |

+| `SESSION_IDLE_TIMEOUT` | 3600s | 空闲超时后自动销毁 |

+| `MAX_SESSIONS_PER_USER` | 1 | 每用户最大会话数 |

+| `MAX_TOTAL_SESSIONS` | 100 | 全局最大会话数 |

+1. 扫描所有会话的最后活跃时间

+3. 发送通知给用户（可选）

+        await asyncio.sleep(3600)  # 每小时执行

+        for session in get_all_sessions():

+            if session.idle_seconds > SESSION_IDLE_TIMEOUT:

+                await notify_user(session.user_id, "会话已超时，自动关闭")

+                tmux.kill_session(session.session_id)


继续修复其他问题：日志存储 Schema、错误处理详情、可观测性等。


| 任务编排 | Inngest (Python SDK) + 自托管 Server |
| PTY 容器 | tmux |
| QQ 接入 | NapCatQQ (WebSocket) |
| 状态存储 | Inngest State |
| 日志存储 | SQLite |
| Agent Logic | Claude API |

| 任务编排 | Inngest (Python SDK) + 自托管 Server |
| PTY 容器 | tmux |
| QQ 接入 | NapCatQQ (WebSocket) |
| 状态存储 | Inngest State |
| 日志存储 | SQLite |
| Agent Logic | Claude API |

### 日志存储 Schema

```sql
CREATE TABLE shell_logs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id TEXT NOT NULL,
    user_id TEXT NOT NULL,
    command TEXT NOT NULL,
    output TEXT,
    executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    approved BOOLEAN DEFAULT TRUE,
    approval_id TEXT,
    mode TEXT DEFAULT 'nl',
    dangerous BOOLEAN DEFAULT FALSE
);

CREATE INDEX idx_session_logs ON shell_logs(session_id);
CREATE INDEX idx_user_logs ON shell_logs(user_id);
CREATE INDEX idx_timestamp_logs ON shell_logs(executed_at);
```


| 任务编排 | Inngest (Python SDK) + 自托管 Server |
| PTY 容器 | tmux |
| QQ 接入 | NapCatQQ (WebSocket) |
| 状态存储 | Inngest State |
| 日志存储 | SQLite |
| Agent Logic | Claude API |

### 日志存储 Schema

```sql
CREATE TABLE shell_logs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id TEXT NOT NULL,
    user_id TEXT NOT NULL,
    command TEXT NOT NULL,
    output TEXT,
    executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    approved BOOLEAN DEFAULT TRUE,
    approval_id TEXT,
    mode TEXT DEFAULT 'nl',
    dangerous BOOLEAN DEFAULT FALSE
);

CREATE INDEX idx_session_logs ON shell_logs(session_id);
CREATE INDEX idx_user_logs ON shell_logs(user_id);
CREATE INDEX idx_timestamp_logs ON shell_logs(executed_at);
```

---
name: Butler-Shell 系统设计
description: 基于 Inngest 与 tmux 的持久化 Shell 助手 (QQ 桥接)
type: project
---

# Butler-Shell 系统设计

## 概述

Butler-Shell 是一个通过 QQ 接收指令、在远程服务器上维护持久化 Shell 会话的系统。核心特性：

- **双模式切换**：自动在自然语言模式与 PTY 透传交互模式间切换
- **命令准入**：危险命令需要用户审批
- **上下文压缩**：`/compact` 指令压缩会话上下文
- **可扩展 Skills**：插件化功能扩展

## 技术栈

| 组件 | 技术选型 |
|------|----------|
| 任务编排 | Inngest (Python SDK) + 自托管 Server |
| PTY 容器 | tmux |
| QQ 接入 | NapCatQQ (WebSocket) |
| 状态存储 | Inngest State |
| 日志存储 | SQLite |
| Agent Logic | Claude API |

## 架构

```
┌─────────────────────────────────────────────────────────┐
│                    QQ Gateway                           │
│              (NapCatQQ WebSocket Handler)               │
└─────────────────────┬───────────────────────────────────┘
                      │ 消息事件
┌─────────────────────▼───────────────────────────────────┐
│                 Inngest Functions                       │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐       │
│  │handle_msg   │ │guardrail    │ │compaction   │       │
│  └─────────────┘ └─────────────┘ └─────────────┘       │
└─────────────────────┬───────────────────────────────────┘
                      │ 状态管理
┌─────────────────────▼───────────────────────────────────┐
│              Session Manager (TmuxWrapper)              │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐    │
│  │create_session│ │capture_pane │ │detect_mode   │    │
│  └──────────────┘ └──────────────┘ └──────────────┘    │
└─────────────────────┬───────────────────────────────────┘
                      │ PTY 控制
┌─────────────────────▼───────────────────────────────────┐
│                    tmux sessions                        │
│              [user_001] [user_002] ...                  │
└─────────────────────────────────────────────────────────┘
```

## 安全设计

### 认证与授权

| 机制 | 说明 |
|------|------|
| 用户白名单 | 仅允许配置文件中的 QQ 用户 ID 使用系统 |
| 消息签名验证 | 验证 NapCatQQ 消息来源合法性 |
| 会话令牌 | 每个会话生成唯一 UUID，防止会话劫持 |

```python
# 白名单检查
ALLOWED_USERS = os.getenv("BUTLER_ALLOWED_USERS", "").split(",")

def is_authorized(user_id: str) -> bool:
    return user_id in ALLOWED_USERS
```

### 命令安全检测

危险命令检测使用多层防护：

1. **正则匹配**：基础检测
2. **命令解析**：使用 `shlex.split()` 解析命令结构
3. **嵌套检测**：检测命令替换、管道执行等绕过方式

```python
import shlex
import re

DANGEROUS_PATTERNS = [
    # 基础危险命令
    r"\brm\s+(-[rf]+\s+|.*-rf)",
    r"\bchmod\s+(-R\s+)?777",
    r"\bchown\s+",
    r"\bdd\s+",
    r"\bmkfs\b",
    r"\b(fdisk|parted)\b",
    r">\s*/dev/sd",
    r"\biptables\b",
    r"\bshutdown\b",
    r"\breboot\b",
    # 绕过防护
    r"\bsudo\b",                    # sudo 提权
    r"\bsh\s+-c\b",                 # 命令嵌套
    r"\bbash\s+-c\b",               # 命令嵌套
    r"\|.*\b(bash|sh)\b",           # 管道执行
    r"\$\([^)]+\)",                 # 命令替换
    r"`[^`]+`",                     # 反引号命令替换
    r"fork\s*\(\s*\)",              # fork bomb
]

def is_dangerous_command(cmd: str) -> bool:
    # 正则匹配
    if any(re.search(p, cmd, re.IGNORECASE) for p in DANGEROUS_PATTERNS):
        return True

    # 解析命令结构
    try:
        parts = shlex.split(cmd)
        if any(p in DANGEROUS_COMMANDS for p in parts):
            return True
    except ValueError:
        # 解析失败，保守处理
        return True

    return False
```

### 敏感信息处理

- **终端输出过滤**：移除匹配密码、API Key 模式的文本
- **日志脱敏**：Token、密钥等不记录明文
- **会话隔离**：每个用户独立 tmux session

```python
SENSITIVE_PATTERNS = [
    r"(password|passwd|pwd)\s*=\s*\S+",
    r"(api[_-]?key|token)\s*=\s*\S+",
    r"Bearer\s+\S+",
    r"-----BEGIN.*KEY-----",
]

def sanitize_output(output: str) -> str:
    for pattern in SENSITIVE_PATTERNS:
        output = re.sub(pattern, "[REDACTED]", output, flags=re.IGNORECASE)
    return output
```

## 模式切换规则

| 切换方向 | 触发条件 | 用户可干预 |
|----------|----------|------------|
| NL → INTERACTIVE | 检测到交互进程启动 | 否 |
| INTERACTIVE → NL | 检测到进程退出 | 是，发送 `/nl` |
| INTERACTIVE → NL | 发送 `C-d` 或 `:q` | 是 |
| 强制 NL | 发送 `/force-nl` | 是 |

### 模式检测降级

当模式检测失败时（如 `/proc` 访问权限问题）：

- 默认使用 `NATURAL_LANGUAGE` 模式
- 记录 WARN 级别日志
- 提示用户可手动切换：`/interactive` 进入交互模式

## 会话生命周期管理

### 会话策略

| 配置项 | 默认值 | 说明 |
|--------|--------|------|
| `SESSION_IDLE_TIMEOUT` | 3600s | 空闲超时后自动销毁 |
| `MAX_SESSIONS_PER_USER` | 1 | 每用户最大会话数 |
| `MAX_TOTAL_SESSIONS` | 100 | 全局最大会话数 |

### 会话清理

定时任务每小时执行：

1. 扫描所有会话的最后活跃时间
2. 超过 `SESSION_IDLE_TIMEOUT` 的会话标记为待清理
3. 发送通知给用户（可选）
4. 优雅关闭 tmux 会话

```python
import asyncio

async def cleanup_idle_sessions():
    while True:
        await asyncio.sleep(3600)  # 每小时执行
        for session in get_all_sessions():
            if session.idle_seconds > SESSION_IDLE_TIMEOUT:
                await notify_user(session.user_id, "会话已超时，自动关闭")
                tmux.kill_session(session.session_id)
```

## 项目结构

```
butler-shell/
├── src/
│   └── butler/
│       ├── __init__.py
│       ├── main.py                    # FastAPI 入口 + Inngest Serve
│       │
│       ├── gateway/                   # QQ 消息接入层
│       │   ├── __init__.py
│       │   ├── napcat.py              # NapCatQQ WebSocket 客户端
│       │   └── events.py              # 消息事件定义
│       │
│       ├── workflows/                 # Inngest 工作流
│       │   ├── __init__.py
│       │   ├── handle_message.py      # 主消息处理流
│       │   ├── guardrail.py           # 命令准入审批
│       │   └── compaction.py          # 上下文压缩
│       │
│       ├── session/                   # Tmux 会话管理
│       │   ├── __init__.py
│       │   ├── wrapper.py             # TmuxWrapper 核心类
│       │   ├── detector.py            # 模式检测器
│       │   └── state.py               # 会话状态模型
│       │
│       ├── skills/                    # Skill 扩展系统
│       │   ├── __init__.py
│       │   ├── base.py                # SkillManager 基类
│       │   ├── log_skill.py           # 日志记录 Skill
│       │   └── system_skill.py        # 系统信息 Skill
│       │
│       └── config.py                  # 配置管理
│
├── tests/
│   ├── conftest.py
│   ├── test_wrapper.py
│   ├── test_detector.py
│   └── test_workflows.py
│
├── docs/
│   └── specs/
│
├── pyproject.toml
├── requirements.txt
└── docker-compose.yml                 # Inngest Server + 依赖
```

## 核心数据模型

### SessionMode & PtyState

```python
from enum import Enum
from pydantic import BaseModel, Field
from datetime import datetime
from typing import Optional

class SessionMode(str, Enum):
    """会话运行模式"""
    NATURAL_LANGUAGE = "nl"      # 自然语言模式 - 通过 LLM 解析
    INTERACTIVE = "interactive"  # 交互模式 - 直接透传到 PTY

class PtyState(str, Enum):
    """PTY 进程状态"""
    IDLE = "idle"           # 空闲，等待输入
    RUNNING = "running"     # 正在执行命令
    BLOCKED = "blocked"     # 阻塞中（如等待 I/O）
    UNKNOWN = "unknown"     # 状态检测失败

class SessionState(BaseModel):
    """会话状态快照"""
    session_id: str = Field(..., description="tmux session 名称")
    user_id: str = Field(..., description="QQ 用户 ID")
    mode: SessionMode = Field(default=SessionMode.NATURAL_LANGUAGE)

    # Shell 状态
    pwd: str = Field(default="", description="当前工作目录")
    env: dict[str, str] = Field(default_factory=dict, description="关键环境变量")
    last_output: str = Field(default="", description="最近终端输出摘要")

    # 交互模式追踪
    interactive_process: Optional[str] = Field(
        default=None,
        description="当前交互进程名 (vim, top, etc.)"
    )

    # 元数据
    created_at: datetime = Field(default_factory=datetime.utcnow)
    updated_at: datetime = Field(default_factory=datetime.utcnow)
```

### MessageContext

```python
class MessageContext(BaseModel):
    """消息处理上下文"""
    message_id: str
    user_id: str
    content: str
    is_command: bool = False          # 是否以 / 开头
    requires_approval: bool = False   # 危险命令标记
    session: Optional[SessionState] = None
```

## TmuxWrapper 核心 API

```python
class TmuxWrapper:
    """tmux 会话封装器"""

    # 交互模式进程名单
    INTERACTIVE_PROCESSES = frozenset([
        "vim", "nvim", "vi",
        "top", "htop", "btop",
        "less", "more", "man",
        "claude", "aider",
        "python", "node", "irb", "ipython",
    ])

    # 快捷键映射
    SPECIAL_KEYS = {
        "C-c", "C-d", "C-z", "C-a", "C-e",
        "Esc", "Tab", "Up", "Down", "Left", "Right",
        "Enter", "Space",
    }

    def create_session(self, user_id: str) -> str:
        """为用户创建独立 tmux session"""

    def session_exists(self, session_name: str) -> bool:
        """检查 session 是否存在"""

    def kill_session(self, session_name: str) -> None:
        """销毁 session"""

    def send_keys(self, session_name: str, keys: str, enter: bool = True) -> None:
        """向 session 发送按键"""

    def send_raw(self, session_name: str, data: str) -> None:
        """发送原始数据（交互模式精确控制）"""

    def capture_pane(self, session_name: str, as_ansi: bool = False) -> str:
        """抓取终端屏幕内容"""

    def get_active_process(self, session_name: str) -> Optional[str]:
        """获取 tmux 窗口中的活跃进程名"""

    def get_pty_state(self, session_name: str) -> PtyState:
        """检测 PTY 是否在等待输入（通过 /proc 状态监控）"""

    def detect_mode(self, session_name: str) -> SessionMode:
        """混合模式检测：进程名单匹配 + PTY 状态检测"""
```

## Inngest 工作流

### handle_im_message

```
Step 1: get-or-create-session → butler_<user_id>
Step 2: load-state → SessionState
Step 3: detect-mode → NATURAL_LANGUAGE | INTERACTIVE

INTERACTIVE 模式:
  └─ send_keys(content, enter=False) → capture_pane() → 返回快照

NATURAL_LANGUAGE 模式:
  ├─ Skill 匹配? → execute Skill → 返回结果
  ├─ 危险命令? → 发送 im/command-needs-approval 事件
  ├─ 特殊指令? → 执行对应逻辑
  └─ send_keys(content, enter=True) → capture_pane() → 返回输出
```

### command_guardrail

```
Step 1: send-confirmation-request → 发送确认请求到 QQ
Step 2: wait-approval → ctx.step.wait_for_event (最长 5 分钟)
Step 3: 处理用户回复 → 执行或取消
```

### context_compaction

```
Step 1: get-pwd → 获取当前工作目录
Step 2: get-env → 获取关键环境变量
Step 3: get-output → 抓取最近终端输出
Step 4: summarize → LLM 总结（可选）
Step 5: 更新状态快照 → ctx.state.set()
```

## 消息处理流程

```
QQ 消息到达
     │
     ▼
NapCatGateway ──→ 转换为 NapCatEvent
     │
     ▼
Inngest Event ──→ 发送 im/message 事件
     │
     ▼
handle_im_message workflow
     │
     ├─ INTERACTIVE 模式 ──→ send_keys + capture_pane
     │
     └─ NATURAL_LANGUAGE 模式
          ├─ Skill 匹配 ──→ execute Skill
          ├─ 危险命令 ──→ command_guardrail
          └─ 正常命令 ──→ execute + capture
     │
     ▼
NapCatGateway ──→ 发送 QQ 回复
```

## 危险命令检测

扩展集合，包括：

| 命令模式 | 说明 |
|----------|------|
| `rm -rf` | 递归强制删除 |
| `chmod 777` | 开放所有权限 |
| `chown` | 修改所有者 |
| `dd` | 磁盘操作 |
| `mkfs` | 格式化 |
| `fdisk`/`parted` | 分区操作 |
| `> /dev/sd*` | 写入磁盘设备 |
| `iptables` | 防火墙规则 |
| `shutdown`/`reboot` | 系统关机/重启 |

## Skill 扩展系统

### 基类 API

```python
class SkillBase(ABC):
    name: str
    description: str
    triggers: list[str]

    @abstractmethod
    async def execute(self, ctx: SkillContext) -> SkillResult:
        pass

    def matches(self, content: str) -> bool:
        """检查是否匹配触发条件"""
```

### 内置 Skills

| Skill | 触发器 | 功能 |
|-------|--------|------|
| LogSkill | `/log`, `/history` | 记录和检索 Shell 交互日志 |
| SystemSkill | `/sys`, `/system`, `/load` | 获取系统负载和资源信息 |

## 错误处理

### 错误分类

| 类型 | 说明 | 处理方式 |
|------|------|----------|
| TRANSIENT | 暂时性错误 | Inngest 自动重试 |
| PERMANENT | 永久性错误 | 返回用户友好消息 |
| USER_ERROR | 用户输入错误 | 提示正确用法 |

### 关键错误类型

- `SessionNotFoundError`: 会话不存在（可恢复）
- `TmuxCommandError`: tmux 命令执行失败（可恢复）
- `ApprovalTimeoutError`: 审批超时

## 测试策略

| 层级 | 测试类型 | 覆盖目标 |
|------|----------|----------|
| TmuxWrapper | 单元测试 | 100% 方法覆盖 |
| 模式检测 | 单元测试 | 所有进程类型 |
| 工作流 | 集成测试 | 主路径 + 错误路径 |
| Skill | 单元测试 | 所有触发场景 |
| 端到端 | E2E 测试 | 完整消息流程 |

## 部署配置

### docker-compose.yml

```yaml
version: '3.8'
services:
  inngest:
    image: inngest/inngest:latest
    ports:
      - "8288:8288"
    command: start --url http://host.docker.internal:8000/api/inngest
    extra_hosts:
      - "host.docker.internal:host-gateway"

  napcat:
    image: mlikiowa/napcat-docker:latest
    ports:
      - "3001:3001"
    volumes:
      - ./napcat-data:/app/napcat/config
```

### 环境变量

```bash
INNGEST_EVENT_KEY=your_event_key
INNGEST_SIGNING_KEY=your_signing_key
NAPCAT_WS_URL=ws://localhost:3001
NAPCAT_TOKEN=your_napcat_token
ANTHROPIC_API_KEY=your_claude_api_key
```

## 实施计划

按以下顺序实施：

1. **Phase 1: 核心会话层**
   - TmuxWrapper 实现
   - 模式检测器实现
   - 单元测试

2. **Phase 2: Inngest 工作流**
   - handle_message 工作流
   - guardrail 审批流程
   - 集成测试

3. **Phase 3: QQ Gateway**
   - NapCatQQ 集成
   - 消息转换
   - 端到端测试

4. **Phase 4: Skills 系统**
   - Skill 基类
   - LogSkill 实现
   - SystemSkill 实现

5. **Phase 5: 完善**
   - 上下文压缩
   - 错误处理优化
   - Mock 测试脚本

+    id INTEGER PRIMARY KEY AUTOINCREMENT,

+    executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

+CREATE INDEX idx_session_logs ON shell_logs(session_id);

+CREATE INDEX idx_user_logs ON shell_logs(user_id);

+CREATE INDEX idx_timestamp_logs ON shell_logs(executed_at);


Now I need to update the error handling section with more details including retry strategy.


## 错误处理

### 错误分类

| 类型 | 说明 | 处理方式 |
|------|------|----------|
| TRANSIENT | 暂时性错误 | Inngest 自动重试 |
| PERMANENT | 永久性错误 | 返回用户友好消息 |
| USER_ERROR | 用户输入错误 | 提示正确用法 |

### 关键错误类型

## 错误处理

### 错误分类

| 类型 | 说明 | 处理方式 |
|------|------|----------|
| TRANSIENT | 暂时性错误 | Inngest 自动重试 |
| PERMANENT | 永久性错误 | 返回用户友好消息 |
| USER_ERROR | 用户输入错误 | 提示正确用法 |

### 错误分类详情

| 错误类型 | 具体错误 | 分类 | 处理策略 |
|----------|----------|------|----------|
| tmux session 不存在 | `SessionNotFoundError` | TRANSIENT | 自动重建 session |
| tmux 命令超时 | `TmuxTimeoutError` | TRANSIENT | 重试 3 次，指数退避 |
| NapCat 连接断开 | `GatewayDisconnectedError` | TRANSIENT | 重连 + 重试 |
| 用户无权限 | `UnauthorizedError` | PERMANENT | 直接返回错误消息 |
| 命令被拒绝 | `CommandRejectedError` | PERMANENT | 记录日志，通知用户 |
| 审批超时 | `ApprovalTimeoutError` | USER_ERROR | 提示重新发起 |

### 重试策略

```python
RETRY_CONFIG = {
    "max_retries": 3,
    "initial_delay": 1,  # 秒
    "max_delay": 30,
    "multiplier": 2,     # 指数退避
}

async def retry_with_backoff(fn, *args, **kwargs):
    delay = RETRY_CONFIG["initial_delay"]
    for attempt in range(RETRY_CONFIG["max_retries"]):
        try:
            return await fn(*args, **kwargs)
        except TransientError as e:
            if attempt == RETRY_CONFIG["max_retries"] - 1:
                raise
            await asyncio.sleep(delay)
            delay = min(delay * RETRY_CONFIG["multiplier"], RETRY_CONFIG["max_delay"])
```

### 关键错误类型


## 错误处理

### 错误分类

| 类型 | 说明 | 处理方式 |
|------|------|----------|
| TRANSIENT | 暂时性错误 | Inngest 自动重试 |
| PERMANENT | 永久性错误 | 返回用户友好消息 |
| USER_ERROR | 用户输入错误 | 提示正确用法 |

### 错误分类详情

| 错误类型 | 具体错误 | 分类 | 处理策略 |
|----------|----------|------|----------|
| tmux session 不存在 | `SessionNotFoundError` | TRANSIENT | 自动重建 session |
| tmux 命令超时 | `TmuxTimeoutError` | TRANSIENT | 重试 3 次，指数退避 |
| NapCat 连接断开 | `GatewayDisconnectedError` | TRANSIENT | 重连 + 重试 |
| 用户无权限 | `UnauthorizedError` | PERMANENT | 直接返回错误消息 |
| 命令被拒绝 | `CommandRejectedError` | PERMANENT | 记录日志，通知用户 |
| 审批超时 | `ApprovalTimeoutError` | USER_ERROR | 提示重新发起 |

### 重试策略

```python
RETRY_CONFIG = {
    "max_retries": 3,
    "initial_delay": 1,  # 秒
    "max_delay": 30,
    "multiplier": 2,     # 指数退避
}

async def retry_with_backoff(fn, *args, **kwargs):
    delay = RETRY_CONFIG["initial_delay"]
    for attempt in range(RETRY_CONFIG["max_retries"]):
        try:
            return await fn(*args, **kwargs)
        except TransientError as e:
            if attempt == RETRY_CONFIG["max_retries"] - 1:
                raise
            await asyncio.sleep(delay)
            delay = min(delay * RETRY_CONFIG["multiplier"], RETRY_CONFIG["max_delay"])
```

### 关键错误类型

---
name: Butler-Shell 系统设计
description: 基于 Inngest 与 tmux 的持久化 Shell 助手 (QQ 桥接)
type: project
---

# Butler-Shell 系统设计

## 概述

Butler-Shell 是一个通过 QQ 接收指令、在远程服务器上维护持久化 Shell 会话的系统。核心特性：

- **双模式切换**：自动在自然语言模式与 PTY 透传交互模式间切换
- **命令准入**：危险命令需要用户审批
- **上下文压缩**：`/compact` 指令压缩会话上下文
- **可扩展 Skills**：插件化功能扩展

## 技术栈

| 组件 | 技术选型 |
|------|----------|
| 任务编排 | Inngest (Python SDK) + 自托管 Server |
| PTY 容器 | tmux |
| QQ 接入 | NapCatQQ (WebSocket) |
| 状态存储 | Inngest State |
| 日志存储 | SQLite |
| Agent Logic | Claude API |

### 日志存储 Schema

```sql
CREATE TABLE shell_logs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id TEXT NOT NULL,
    user_id TEXT NOT NULL,
    command TEXT NOT NULL,
    output TEXT,
    executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    approved BOOLEAN DEFAULT TRUE,
    approval_id TEXT,
    mode TEXT DEFAULT 'nl',
    dangerous BOOLEAN DEFAULT FALSE
);

CREATE INDEX idx_session_logs ON shell_logs(session_id);
CREATE INDEX idx_user_logs ON shell_logs(user_id);
CREATE INDEX idx_timestamp_logs ON shell_logs(executed_at);
```

## 架构

```
┌─────────────────────────────────────────────────────────┐
│                    QQ Gateway                           │
│              (NapCatQQ WebSocket Handler)               │
└─────────────────────┬───────────────────────────────────┘
                      │ 消息事件
┌─────────────────────▼───────────────────────────────────┐
│                 Inngest Functions                       │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐       │
│  │handle_msg   │ │guardrail    │ │compaction   │       │
│  └─────────────┘ └─────────────┘ └─────────────┘       │
└─────────────────────┬───────────────────────────────────┘
                      │ 状态管理
┌─────────────────────▼───────────────────────────────────┐
│              Session Manager (TmuxWrapper)              │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐    │
│  │create_session│ │capture_pane │ │detect_mode   │    │
│  └──────────────┘ └──────────────┘ └──────────────┘    │
└─────────────────────┬───────────────────────────────────┘
                      │ PTY 控制
┌─────────────────────▼───────────────────────────────────┐
│                    tmux sessions                        │
│              [user_001] [user_002] ...                  │
└─────────────────────────────────────────────────────────┘
```

## 安全设计

### 认证与授权

| 机制 | 说明 |
|------|------|
| 用户白名单 | 仅允许配置文件中的 QQ 用户 ID 使用系统 |
| 消息签名验证 | 验证 NapCatQQ 消息来源合法性 |
| 会话令牌 | 每个会话生成唯一 UUID，防止会话劫持 |

```python
# 白名单检查
ALLOWED_USERS = os.getenv("BUTLER_ALLOWED_USERS", "").split(",")

def is_authorized(user_id: str) -> bool:
    return user_id in ALLOWED_USERS
```

### 命令安全检测

危险命令检测使用多层防护：

1. **正则匹配**：基础检测
2. **命令解析**：使用 `shlex.split()` 解析命令结构
3. **嵌套检测**：检测命令替换、管道执行等绕过方式

```python
import shlex
import re

DANGEROUS_PATTERNS = [
    # 基础危险命令
    r"\brm\s+(-[rf]+\s+|.*-rf)",
    r"\bchmod\s+(-R\s+)?777",
    r"\bchown\s+",
    r"\bdd\s+",
    r"\bmkfs\b",
    r"\b(fdisk|parted)\b",
    r">\s*/dev/sd",
    r"\biptables\b",
    r"\bshutdown\b",
    r"\breboot\b",
    # 绕过防护
    r"\bsudo\b",                    # sudo 提权
    r"\bsh\s+-c\b",                 # 命令嵌套
    r"\bbash\s+-c\b",               # 命令嵌套
    r"\|.*\b(bash|sh)\b",           # 管道执行
    r"\$\([^)]+\)",                 # 命令替换
    r"`[^`]+`",                     # 反引号命令替换
    r"fork\s*\(\s*\)",              # fork bomb
]

def is_dangerous_command(cmd: str) -> bool:
    # 正则匹配
    if any(re.search(p, cmd, re.IGNORECASE) for p in DANGEROUS_PATTERNS):
        return True

    # 解析命令结构
    try:
        parts = shlex.split(cmd)
        if any(p in DANGEROUS_COMMANDS for p in parts):
            return True
    except ValueError:
        # 解析失败，保守处理
        return True

    return False
```

### 敏感信息处理

- **终端输出过滤**：移除匹配密码、API Key 模式的文本
- **日志脱敏**：Token、密钥等不记录明文
- **会话隔离**：每个用户独立 tmux session

```python
SENSITIVE_PATTERNS = [
    r"(password|passwd|pwd)\s*=\s*\S+",
    r"(api[_-]?key|token)\s*=\s*\S+",
    r"Bearer\s+\S+",
    r"-----BEGIN.*KEY-----",
]

def sanitize_output(output: str) -> str:
    for pattern in SENSITIVE_PATTERNS:
        output = re.sub(pattern, "[REDACTED]", output, flags=re.IGNORECASE)
    return output
```

## 模式切换规则

| 切换方向 | 触发条件 | 用户可干预 |
|----------|----------|------------|
| NL → INTERACTIVE | 检测到交互进程启动 | 否 |
| INTERACTIVE → NL | 检测到进程退出 | 是，发送 `/nl` |
| INTERACTIVE → NL | 发送 `C-d` 或 `:q` | 是 |
| 强制 NL | 发送 `/force-nl` | 是 |

### 模式检测降级

当模式检测失败时（如 `/proc` 访问权限问题）：

- 默认使用 `NATURAL_LANGUAGE` 模式
- 记录 WARN 级别日志
- 提示用户可手动切换：`/interactive` 进入交互模式

## 会话生命周期管理

### 会话策略

| 配置项 | 默认值 | 说明 |
|--------|--------|------|
| `SESSION_IDLE_TIMEOUT` | 3600s | 空闲超时后自动销毁 |
| `MAX_SESSIONS_PER_USER` | 1 | 每用户最大会话数 |
| `MAX_TOTAL_SESSIONS` | 100 | 全局最大会话数 |

### 会话清理

定时任务每小时执行：

1. 扫描所有会话的最后活跃时间
2. 超过 `SESSION_IDLE_TIMEOUT` 的会话标记为待清理
3. 发送通知给用户（可选）
4. 优雅关闭 tmux 会话

```python
import asyncio

async def cleanup_idle_sessions():
    while True:
        await asyncio.sleep(3600)  # 每小时执行
        for session in get_all_sessions():
            if session.idle_seconds > SESSION_IDLE_TIMEOUT:
                await notify_user(session.user_id, "会话已超时，自动关闭")
                tmux.kill_session(session.session_id)
```

## 项目结构

```
butler-shell/
├── src/
│   └── butler/
│       ├── __init__.py
│       ├── main.py                    # FastAPI 入口 + Inngest Serve
│       │
│       ├── gateway/                   # QQ 消息接入层
│       │   ├── __init__.py
│       │   ├── napcat.py              # NapCatQQ WebSocket 客户端
│       │   └── events.py              # 消息事件定义
│       │
│       ├── workflows/                 # Inngest 工作流
│       │   ├── __init__.py
│       │   ├── handle_message.py      # 主消息处理流
│       │   ├── guardrail.py           # 命令准入审批
│       │   └── compaction.py          # 上下文压缩
│       │
│       ├── session/                   # Tmux 会话管理
│       │   ├── __init__.py
│       │   ├── wrapper.py             # TmuxWrapper 核心类
│       │   ├── detector.py            # 模式检测器
│       │   └── state.py               # 会话状态模型
│       │
│       ├── skills/                    # Skill 扩展系统
│       │   ├── __init__.py
│       │   ├── base.py                # SkillManager 基类
│       │   ├── log_skill.py           # 日志记录 Skill
│       │   └── system_skill.py        # 系统信息 Skill
│       │
│       └── config.py                  # 配置管理
│
├── tests/
│   ├── conftest.py
│   ├── test_wrapper.py
│   ├── test_detector.py
│   └── test_workflows.py
│
├── docs/
│   └── specs/
│
├── pyproject.toml
├── requirements.txt
└── docker-compose.yml                 # Inngest Server + 依赖
```

## 核心数据模型

### SessionMode & PtyState

```python
from enum import Enum
from pydantic import BaseModel, Field
from datetime import datetime
from typing import Optional

class SessionMode(str, Enum):
    """会话运行模式"""
    NATURAL_LANGUAGE = "nl"      # 自然语言模式 - 通过 LLM 解析
    INTERACTIVE = "interactive"  # 交互模式 - 直接透传到 PTY

class PtyState(str, Enum):
    """PTY 进程状态"""
    IDLE = "idle"           # 空闲，等待输入
    RUNNING = "running"     # 正在执行命令
    BLOCKED = "blocked"     # 阻塞中（如等待 I/O）
    UNKNOWN = "unknown"     # 状态检测失败

class SessionState(BaseModel):
    """会话状态快照"""
    session_id: str = Field(..., description="tmux session 名称")
    user_id: str = Field(..., description="QQ 用户 ID")
    mode: SessionMode = Field(default=SessionMode.NATURAL_LANGUAGE)

    # Shell 状态
    pwd: str = Field(default="", description="当前工作目录")
    env: dict[str, str] = Field(default_factory=dict, description="关键环境变量")
    last_output: str = Field(default="", description="最近终端输出摘要")

    # 交互模式追踪
    interactive_process: Optional[str] = Field(
        default=None,
        description="当前交互进程名 (vim, top, etc.)"
    )

    # 元数据
    created_at: datetime = Field(default_factory=datetime.utcnow)
    updated_at: datetime = Field(default_factory=datetime.utcnow)
```

### MessageContext

```python
class MessageContext(BaseModel):
    """消息处理上下文"""
    message_id: str
    user_id: str
    content: str
    is_command: bool = False          # 是否以 / 开头
    requires_approval: bool = False   # 危险命令标记
    session: Optional[SessionState] = None
```

## TmuxWrapper 核心 API

```python
class TmuxWrapper:
    """tmux 会话封装器"""

    # 交互模式进程名单
    INTERACTIVE_PROCESSES = frozenset([
        "vim", "nvim", "vi",
        "top", "htop", "btop",
        "less", "more", "man",
        "claude", "aider",
        "python", "node", "irb", "ipython",
    ])

    # 快捷键映射
    SPECIAL_KEYS = {
        "C-c", "C-d", "C-z", "C-a", "C-e",
        "Esc", "Tab", "Up", "Down", "Left", "Right",
        "Enter", "Space",
    }

    def create_session(self, user_id: str) -> str:
        """为用户创建独立 tmux session"""

    def session_exists(self, session_name: str) -> bool:
        """检查 session 是否存在"""

    def kill_session(self, session_name: str) -> None:
        """销毁 session"""

    def send_keys(self, session_name: str, keys: str, enter: bool = True) -> None:
        """向 session 发送按键"""

    def send_raw(self, session_name: str, data: str) -> None:
        """发送原始数据（交互模式精确控制）"""

    def capture_pane(self, session_name: str, as_ansi: bool = False) -> str:
        """抓取终端屏幕内容"""

    def get_active_process(self, session_name: str) -> Optional[str]:
        """获取 tmux 窗口中的活跃进程名"""

    def get_pty_state(self, session_name: str) -> PtyState:
        """检测 PTY 是否在等待输入（通过 /proc 状态监控）"""

    def detect_mode(self, session_name: str) -> SessionMode:
        """混合模式检测：进程名单匹配 + PTY 状态检测"""
```

## Inngest 工作流

### handle_im_message

```
Step 1: get-or-create-session → butler_<user_id>
Step 2: load-state → SessionState
Step 3: detect-mode → NATURAL_LANGUAGE | INTERACTIVE

INTERACTIVE 模式:
  └─ send_keys(content, enter=False) → capture_pane() → 返回快照

NATURAL_LANGUAGE 模式:
  ├─ Skill 匹配? → execute Skill → 返回结果
  ├─ 危险命令? → 发送 im/command-needs-approval 事件
  ├─ 特殊指令? → 执行对应逻辑
  └─ send_keys(content, enter=True) → capture_pane() → 返回输出
```

### command_guardrail

```
Step 1: send-confirmation-request → 发送确认请求到 QQ
Step 2: wait-approval → ctx.step.wait_for_event (最长 5 分钟)
Step 3: 处理用户回复 → 执行或取消
```

### context_compaction

```
Step 1: get-pwd → 获取当前工作目录
Step 2: get-env → 获取关键环境变量
Step 3: get-output → 抓取最近终端输出
Step 4: summarize → LLM 总结（可选）
Step 5: 更新状态快照 → ctx.state.set()
```

## 消息处理流程

```
QQ 消息到达
     │
     ▼
NapCatGateway ──→ 转换为 NapCatEvent
     │
     ▼
Inngest Event ──→ 发送 im/message 事件
     │
     ▼
handle_im_message workflow
     │
     ├─ INTERACTIVE 模式 ──→ send_keys + capture_pane
     │
     └─ NATURAL_LANGUAGE 模式
          ├─ Skill 匹配 ──→ execute Skill
          ├─ 危险命令 ──→ command_guardrail
          └─ 正常命令 ──→ execute + capture
     │
     ▼
NapCatGateway ──→ 发送 QQ 回复
```

## 危险命令检测

扩展集合，包括：

| 命令模式 | 说明 |
|----------|------|
| `rm -rf` | 递归强制删除 |
| `chmod 777` | 开放所有权限 |
| `chown` | 修改所有者 |
| `dd` | 磁盘操作 |
| `mkfs` | 格式化 |
| `fdisk`/`parted` | 分区操作 |
| `> /dev/sd*` | 写入磁盘设备 |
| `iptables` | 防火墙规则 |
| `shutdown`/`reboot` | 系统关机/重启 |

## Skill 扩展系统

### 基类 API

```python
class SkillBase(ABC):
    name: str
    description: str
    triggers: list[str]

    @abstractmethod
    async def execute(self, ctx: SkillContext) -> SkillResult:
        pass

    def matches(self, content: str) -> bool:
        """检查是否匹配触发条件"""
```

### 内置 Skills

| Skill | 触发器 | 功能 |
|-------|--------|------|
| LogSkill | `/log`, `/history` | 记录和检索 Shell 交互日志 |
| SystemSkill | `/sys`, `/system`, `/load` | 获取系统负载和资源信息 |

## 错误处理

### 错误分类

| 类型 | 说明 | 处理方式 |
|------|------|----------|
| TRANSIENT | 暂时性错误 | Inngest 自动重试 |
| PERMANENT | 永久性错误 | 返回用户友好消息 |
| USER_ERROR | 用户输入错误 | 提示正确用法 |

### 关键错误类型

- `SessionNotFoundError`: 会话不存在（可恢复）
- `TmuxCommandError`: tmux 命令执行失败（可恢复）
- `ApprovalTimeoutError`: 审批超时

## 测试策略

| 层级 | 测试类型 | 覆盖目标 |
|------|----------|----------|
| TmuxWrapper | 单元测试 | 100% 方法覆盖 |
| 模式检测 | 单元测试 | 所有进程类型 |
| 工作流 | 集成测试 | 主路径 + 错误路径 |
| Skill | 单元测试 | 所有触发场景 |
| 端到端 | E2E 测试 | 完整消息流程 |

## 部署配置

### docker-compose.yml

```yaml
version: '3.8'
services:
  inngest:
    image: inngest/inngest:latest
    ports:
      - "8288:8288"
    command: start --url http://host.docker.internal:8000/api/inngest
    extra_hosts:
      - "host.docker.internal:host-gateway"

  napcat:
    image: mlikiowa/napcat-docker:latest
    ports:
      - "3001:3001"
    volumes:
      - ./napcat-data:/app/napcat/config
```

### 环境变量

```bash
INNGEST_EVENT_KEY=your_event_key
INNGEST_SIGNING_KEY=your_signing_key
NAPCAT_WS_URL=ws://localhost:3001
NAPCAT_TOKEN=your_napcat_token
ANTHROPIC_API_KEY=your_claude_api_key
```

## 实施计划

按以下顺序实施：

1. **Phase 1: 核心会话层**
   - TmuxWrapper 实现
   - 模式检测器实现
   - 单元测试

2. **Phase 2: Inngest 工作流**
   - handle_message 工作流
   - guardrail 审批流程
   - 集成测试

3. **Phase 3: QQ Gateway**
   - NapCatQQ 集成
   - 消息转换
   - 端到端测试

4. **Phase 4: Skills 系统**
   - Skill 基类
   - LogSkill 实现
   - SystemSkill 实现

5. **Phase 5: 完善**
   - 上下文压缩
   - 错误处理优化
   - Mock 测试脚本

| PERMANENT | 永久性错误 | 返回用户友好消息 |

+| 错误类型 | 具体错误 | 分类 | 处理策略 |

+| tmux session 不存在 | `SessionNotFoundError` | TRANSIENT | 自动重建 session |

+| tmux 命令超时 | `TmuxTimeoutError` | TRANSIENT | 重试 3 次，指数退避 |

+| NapCat 连接断开 | `GatewayDisconnectedError` | TRANSIENT | 重连 + 重试 |

+| 用户无权限 | `UnauthorizedError` | PERMANENT | 直接返回错误消息 |

+| 命令被拒绝 | `CommandRejectedError` | PERMANENT | 记录日志，通知用户 |

+| 审批超时 | `ApprovalTimeoutError` | USER_ERROR | 提示重新发起 |

+async def retry_with_backoff(fn, *args, **kwargs):

+    delay = RETRY_CONFIG["initial_delay"]

+    for attempt in range(RETRY_CONFIG["max_retries"]):

+            return await fn(*args, **kwargs)

+            if attempt == RETRY_CONFIG["max_retries"] - 1:

+            delay = min(delay * RETRY_CONFIG["multiplier"], RETRY_CONFIG["max_delay"])


Now I need to add the observability section, E2E test scenarios, and configuration management sections. Let me add these before the deployment configuration section.


| 端到端 | E2E 测试 | 完整消息流程 |

## 部署配置

| 端到端 | E2E 测试 | 完整消息流程 |

### E2E 测试场景

| 场景 | 描述 | 验证点 |
|------|------|--------|
| 新用户首次交互 | 发送消息，创建会话 | 会话创建成功 |
| 正常命令执行 | `ls -la` | 返回正确输出 |
| 危险命令审批 | `rm -rf /tmp/test` | 进入审批流程 |
| 审批超时 | 不响应审批请求 | 5分钟后超时取消 |
| 审批确认 | 回复"确认" | 命令执行成功 |
| 审批拒绝 | 回复"取消" | 命令不执行 |
| 交互模式切换 | 在 vim 中发送按键 | 正确透传 |
| 模式自动恢复 | 退出 vim | 切换回 NL 模式 |
| 手动模式切换 | 发送 `/interactive` | 进入交互模式 |
| 未授权用户 | 白名单外用户发消息 | 返回拒绝消息 |
| 会话超时清理 | 等待超过超时时间 | 会话被清理 |

## 可观测性

### 日志级别

| 级别 | 使用场景 |
|------|----------|
| DEBUG | 详细执行流程、模式检测详情 |
| INFO | 会话创建/销毁、命令执行 |
| WARN | 模式检测失败、降级操作 |
| ERROR | 命令执行失败、外部服务异常 |

### 关键指标

| 指标名 | 类型 | 说明 |
|--------|------|------|
| `butler_sessions_active` | Gauge | 当前活跃会话数 |
| `butler_commands_total` | Counter | 命令执行总数 (按状态分组) |
| `butler_command_duration_seconds` | Histogram | 命令执行耗时 |
| `butler_mode_switches_total` | Counter | 模式切换次数 |
| `butler_approvals_total` | Counter | 审批请求总数 |

### 健康检查端点

| 端点 | 说明 | 检查项 |
|------|------|--------|
| `GET /health` | 服务存活 | 进程运行状态 |
| `GET /ready` | 服务就绪 | NapCat 连接、tmux 可用性 |

## 配置管理

### 配置文件

```yaml
# config.yaml
butler:
  session:
    idle_timeout: 3600
    max_per_user: 1
    max_total: 100

  guardrail:
    enabled: true
    approval_timeout: 300
    danger_patterns:
      - "rm -rf"
      - "sudo"
      - "chmod 777"

  security:
    allowed_users:
      - "12345678"
      - "87654321"

  logging:
    level: INFO
    path: /var/log/butler/shell.log

  napcat:
    ws_url: ws://localhost:3001
    token: ${NAPCAT_TOKEN}
```

### 配置热更新

支持运行时重新加载配置：

- `POST /config/reload` 触发重载
- 监听 `SIGHUP` 信号

## 部署配置