Ctrl K
ComunidadGuías…

docs/plan/2026-06-03-shell-unification-design.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Shell 工具统一方案:移除 bash,改进 shell

## 背景

当前系统同时存在 `bash` 和 `shell` 两个命令执行工具,能力重叠:

- `bash`:无状态一次性执行,支持 `async` 后台任务
- `shell`:有状态持久会话,支持 `exec`/`write_stdin`/`close`/`list`

`shell` 的 `exec` action 在不传 `session_id` 时等价于 `bash` 的一次性执行。两者合并后 Agent 只需学习一个工具的 schema,审批模型也更合理(创建会话审批一次,后续命令放行)。

本方案描述最终形态:**移除 `bash`,将 `shell` 改进为唯一的命令执行工具**。

---

## 一、Shell 工具最终形态

### 1.1 工具 Schema(面向 LLM)

```json
{
  "name": "shell",
  "description": "Execute shell commands. Supports one-shot execution and persistent sessions.\nActions:\n- exec: Execute a command (creates session if session_id omitted)\n- write_stdin: Write input to a running session's stdin\n- close: Close a shell session\n- list: List active sessions",
  "input_schema": {
    "type": "object",
    "properties": {
      "action": {
        "type": "string",
        "enum": ["exec", "write_stdin", "close", "list"],
        "description": "Action to perform"
      },
      "command": {
        "type": "string",
        "description": "Command to execute (for exec action)"
      },
      "session_id": {
        "type": "string",
        "description": "Session ID. Omit for one-shot execution, provide to reuse existing session."
      },
      "input": {
        "type": "string",
        "description": "Input to write to stdin (for write_stdin action)"
      },
      "workdir": {
        "type": "string",
        "description": "Working directory (relative to workspace)"
      },
      "yield_time_ms": {
        "type": "integer",
        "description": "Max wait time for output in milliseconds (default 10000)"
      },
      "async": {
        "type": "boolean",
        "description": "Run in background and return task_id immediately (default false, exec action only)"
      }
    },
    "required": ["action"]
  }
}
```

### 1.2 输出捕获机制(核心改进)

**现状问题**:当前后端和 Electron 都用 `sleep → 非阻塞读` 的方式捕获输出,不可靠——命令可能在 sleep 期间产生输出后又继续运行,或者 sleep 结束时命令还没开始输出。

**最终方案:Marker 检测**

每条命令执行后,shell 进程自动写入一个唯一标记行。读取输出时持续读直到看到这个标记,就知道命令执行完了。

```
# 执行流程:
1. 生成 marker: __CMD_DONE_<uuid>__
2. 向 stdin 写入: <command>\n echo __CMD_DONE_<uuid>__\n
3. 逐行读取 stdout,直到看到 marker 行
4. marker 之前的所有行即为命令输出
5. 超时兜底:yield_time_ms 到期仍未看到 marker,返回已读取的输出 + truncated: true
```

**优势**:
- 不依赖 sleep 时间,命令结束立即返回
- 不需要 PTY,`TERM=dumb` 即可(marker 是纯文本,不依赖终端控制序列)
- 与后端 `OutputManager` 的 `sleep → reader.ready()` 相比,更可靠且延迟更低

### 1.3 Async 后台执行

沿用 `BackgroundTaskManager` 机制,从 `bash` 工具迁移到 `shell` 工具:

- `exec` action 新增 `async` 参数(boolean,默认 false)
- `async=true` 时:创建后台任务,立即返回 `{ async: true, task_id, message }`
- 后台任务完成后,结果在下一轮 AgentLoop 中通过 `<background-task-results>` 注入

### 1.4 会话生命周期

对齐后端 `ShellSessionManager` 的管理策略:

| 参数 | 值 | 说明 |
|------|---|------|
| 空闲超时 | 30 分钟 | 无命令执行的会话自动关闭 |
| 最大存活 | 2 小时 | 无论活跃与否,超时强制关闭 |
| 每会话上限 | 5 个 | 每个 AgentSession 最多 5 个并发 shell 会话 |
| 清理周期 | 60 秒 | 定时扫描过期会话 |

### 1.5 审批模型

- `exec` action(无 `session_id`):每次执行前审批(一次性命令)
- `exec` action(有 `session_id`,会话已存在):**不审批**(会话创建时已审批)
- `exec` action(有 `session_id`,会话不存在):审批后创建会话
- `write_stdin`:不审批(会话已存在,写 stdin 不构成独立风险)
- `close`/`list`:不审批

### 1.6 输出截断

对齐后端 `OutputManager` 的截断策略:

| 参数 | 默认值 | 说明 |
|------|--------|------|
| maxPreviewLines | 100 | 预览最多保留尾部 N 行 |
| maxPreviewChars | 10000 | 预览最多 M 字符 |
| outputFile | `{workspace}/shellOutput/{sessionId}_{N}.out` | 完整输出落盘 |

返回格式:
```
exit_code: 0
session_id: sh-123-abc
elapsed_ms: 156
current_workdir: /Users/foo/project
output_lines: 42
truncated: true
output_file: /path/to/shellOutput/sh-123-abc_3.out
---
... (预览内容)
```

---

## 二、具体改动点

### 2.1 后端 — 删除 BashTool

| 文件 | 改动 |
|------|------|
| `harness/tool/impl/BashTool.java` | **删除整个文件** |

无需修改 `ToolRegistry`、`PromptEngine`、`ToolDispatcher`——它们都是动态发现 `@Component` 的,删除 `BashTool` 后自动从注册列表消失。

### 2.2 后端 — 改进 ShellSessionTool

**文件**:`harness/tool/impl/ShellSessionTool.java`

改动:
1. `handleExec` 中注入 marker,逐行读取直到 marker 或超时
2. 新增 `async` 参数处理,调用 `BackgroundTaskManager.submit()`
3. `handleExec` 中传递 `sessionId` 给 `requestToolApproval`(无 session_id 时审批)

**文件**:`harness/shell/ShellSessionManager.java`

改动:
1. 新增 `cleanupExpiredSessions()` 定时任务(`@Scheduled(fixedRate = 60000)`)
2. `ShellSession` 增加 `commandCount` 递增逻辑(当前已存在但未在本地模式使用)

**文件**:`harness/shell/OutputManager.java`

改动:
1. 新增 `readUntilMarker(BufferedReader, String marker, Duration timeout)` 方法
2. 保留 `readOutput` 用于非 marker 场景(或直接移除,统一用 marker)

### 2.3 后端 — 业务层适配

**文件**:`session/util/ToolResultSummarizer.java`

- `case "bash"` → `case "shell"`,方法名 `summarizeBash` → `summarizeShell`
- 解析逻辑适配 shell 的输出格式(增加 `session_id`、`action` 字段提取)

**文件**:`session/activity/ActivityTypeMapper.java`

- `case "bash" -> "RUN"` → `case "shell" -> "RUN"`

**文件**:`session/ws/WsStreamingEventListener.java`

- `case "bash"` → `case "shell"`,提取逻辑适配 shell 的参数结构(`command` 在 `exec` action 的参数中)

**文件**:`harness/core/BackgroundTaskManager.java`

- 更新 javadoc 注释(移除 "bash" 字样)

**文件**:`harness/local/LocalToolExecutor.java`

- 更新 javadoc 注释

### 2.4 后端 — 数据库迁移

**新建迁移**:`V0XX__remove_bash_tool.sql`

```sql
-- 删除 bash 工具
DELETE FROM skill WHERE name = 'bash';
-- 更新 agent_tool 关联(如果 agent 之前绑定了 bash,改为绑定 shell)
UPDATE agent_tool SET tool_id = (SELECT id FROM skill WHERE name = 'shell')
WHERE tool_id = (SELECT id FROM skill WHERE name = 'bash');
```

**修改已有迁移**(仅影响新环境初始化):
- `V004__add_builtin_skills.sql`:删除 bash 的 INSERT 语句
- `V011__reset_agents.sql`:将 tool_id=7(bash)改为 shell 的 tool_id

### 2.5 Electron — main.cjs

**删除**:
- `local-execute-bash` IPC handler(lines 203-231)
- `handleBashFromWebSocket` 函数(lines 328-359)
- `executeToolByName` 中 `case 'bash'` 分支

**改进 `handleShellFromWebSocket`**:
1. `exec` action:注入 marker,逐行读取直到 marker 或超时
2. `write_stdin` action:同样用 marker 检测输出结束
3. `exec` action 支持 `async` 参数(通过 IPC 调用后端的 `BackgroundTaskManager`,或在本地用 `child_process` 后台运行)
4. 会话数据结构增加 `commandCount`、`createdAt`、`lastActiveAt`
5. 新增定时清理过期会话逻辑

### 2.6 Electron — preload.cjs

**删除**:
- `localExecuteBash` IPC bridge

### 2.7 前端 — ToolApprovalBar.vue

- 从 `titleMap` 和 `descMap` 中移除 `bash` 条目(shell 已有对应条目)

### 2.8 前端 — ToolCallCard.vue

- `name.includes('bash')` → `name.includes('shell')`(保留 `execute`/`terminal` 的判断)

### 2.9 文档

以下文档包含 bash 引用,需要更新或标记为过时:
- `docs/shell-session-design.md` — 核心设计文档,需要重写
- `docs/best-agent-spec/s01-the-agent-loop.md` — "One loop & Bash is all you need" → 更新为 shell
- `docs/best-agent-spec/s02-tool-use.md` — 工具使用示例
- 其余 12+ 文档中的 bash 引用更新为 shell

---

## 三、改动影响范围汇总

| 层 | 文件 | 改动类型 |
|----|------|---------|
| 后端 | `BashTool.java` | 删除 |
| 后端 | `ShellSessionTool.java` | 改进:marker 输出捕获 + async |
| 后端 | `ShellSessionManager.java` | 改进:会话生命周期管理 |
| 后端 | `OutputManager.java` | 改进:新增 `readUntilMarker` |
| 后端 | `ToolResultSummarizer.java` | 适配:bash → shell |
| 后端 | `ActivityTypeMapper.java` | 适配:bash → shell |
| 后端 | `WsStreamingEventListener.java` | 适配:bash → shell |
| 后端 | `BackgroundTaskManager.java` | 注释更新 |
| 后端 | `LocalToolExecutor.java` | 注释更新 |
| 后端 | 新迁移 `V0XX__remove_bash_tool.sql` | 新增 |
| 后端 | `V004`、`V011` 迁移 | 修改(仅新环境) |
| Electron | `main.cjs` | 删除 bash 相关 + 改进 shell |
| Electron | `preload.cjs` | 删除 `localExecuteBash` |
| 前端 | `ToolApprovalBar.vue` | 移除 bash 条目 |
| 前端 | `ToolCallCard.vue` | bash → shell |
| 文档 | 15+ docs 文件 | 更新引用 |