# Shell 会话系统技术方案 > Note: The `bash` tool has been removed. `shell` is now the only command execution tool. See `2026-06-03-shell-unification-design.md`. ## 一、背景与目标 ### 1.1 当前实现的局限 原有 `BashTool`(已移除)采用无状态设计,每次调用都创建新进程: ```java ProcessBuilder pb = new ProcessBuilder("bash", "-c", command); ``` 这导致: - **状态丢失**:`cd /tmp` 后再 `ls` 无法感知目录变化 - **环境隔离**:无法维持环境变量、虚拟环境等 - **交互缺失**:不支持 `top`、`python -i` 等交互式程序 - **输出截断**:超过 50000 字符直接丢弃,无法回溯 ### 1.2 设计目标 将 Shell 从"一次性命令执行器"升级为"有状态的交互式子系统": 1. **会话持久化**:每个 Agent 对话拥有独立 Shell 会话,状态持续存在 2. **双模式支持**:普通命令(同步)+ 交互式程序(PTY) 3. **输出管理**:截断预览 + 完整落盘,不丢失信息 4. **向后兼容**:原有 BashTool 调用方式已迁移至 ShellSessionTool --- ## 二、架构设计 ### 2.1 整体架构 ``` ┌─────────────────────────────────────────────────────────────┐ │ AgentLoop │ │ │ │ │ ToolDispatcher │ │ │ │ │ ┌───────────────┴───────────────┐ │ │ │ │ │ │ BashTool (已移除) ShellSessionTool (当前) │ │ │ │ │ ShellSessionManager │ │ │ │ │ ┌────────────────┼────────────────┐ │ │ │ │ │ │ │ ShellSession ShellSession ShellSession │ │ (普通模式) (PTY模式) (...) │ │ │ │ │ │ │ Process PTY Process ... │ └─────────────────────────────────────────────────────────────┘ ``` ### 2.2 核心组件 | 组件 | 职责 | |------|------| | `ShellSessionManager` | 会话生命周期管理,会话池 | | `ShellSession` | 单个会话的状态封装(进程、工作目录、环境变量) | | `ShellSessionTool` | 新的 Tool 实现,暴露给 LLM | | `OutputManager` | 输出截断、落盘、格式化 | | `PathSandbox` | 复用现有路径安全校验 | --- ## 三、核心组件详细设计 ### 3.1 ShellSession — 会话实体 ```java public class ShellSession { private final String sessionId; // 会话唯一标识 private final Long conversationId; // 关联的 Agent 对话 ID private final boolean ptyMode; // 是否 PTY 模式 private final Process process; // bash 进程 private final BufferedWriter stdin; // 标准输入流 private final BufferedReader stdout; // 标准输出流 private final Path workspaceDir; // 工作目录 private final Path outputFile; // 输出落盘文件 private final Instant createdAt; // 创建时间 private Instant lastActiveAt; // 最后活跃时间 private volatile boolean alive = true; // 会话是否存活 // 状态信息 private String currentWorkdir; // 当前工作目录(通过 pwd 追踪) private Map envVars; // 环境变量快照 } ``` **关键设计点**: 1. **进程复用**:启动 `bash --norc --noprofile` 保持运行,通过 stdin/stdout 交互 2. **工作目录追踪**:每次命令执行后自动注入 `pwd` 获取当前目录 3. **输出落盘**:所有输出 append 到文件,返回时截断预览 ### 3.2 ShellSessionManager — 会话管理器 ```java @Component public class ShellSessionManager { // 会话池:sessionId -> ShellSession private final ConcurrentHashMap sessions = new ConcurrentHashMap<>(); // 按 conversationId 分组索引 private final ConcurrentHashMap> conversationSessions = new ConcurrentHashMap<>(); // 配置 private static final int MAX_SESSIONS_PER_CONVERSATION = 5; private static final Duration SESSION_IDLE_TIMEOUT = Duration.ofMinutes(30); private static final Duration SESSION_MAX_LIFETIME = Duration.ofHours(2); /** * 获取或创建会话 */ public ShellSession getOrCreate(Long conversationId, String sessionId, boolean ptyMode) { // 1. 如果 sessionId 已存在且存活,直接返回 // 2. 如果 sessionId 不存在,创建新会话 // 3. 检查会话数限制 // 4. 启动 bash 进程 } /** * 关闭指定会话 */ public void close(String sessionId) { // 1. 关闭进程 // 2. 从 sessions 移除 // 3. 从 conversationSessions 索引移除 } /** * 关闭某个对话的所有会话 */ public void closeByConversation(Long conversationId) { // 遍历 conversationSessions.get(conversationId) 逐个关闭 } /** * 清理过期会话(定时任务调用) */ @Scheduled(fixedRate = 60000) public void cleanupExpiredSessions() { // 遍历 sessions,关闭 idle > 30min 或 lifetime > 2h 的会话 } } ``` ### 3.3 OutputManager — 输出管理器 ```java @Component public class OutputManager { private static final int MAX_PREVIEW_LINES = 100; private static final int MAX_PREVIEW_CHARS = 10000; private static final String OUTPUT_DIR = "shellOutput"; /** * 读取进程输出,返回截断预览 + 完整落盘 */ public OutputResult readOutput(BufferedReader reader, Path outputFile, Duration yieldTime) { // 1. 等待 yieldTime(默认 10s),让命令有时间产生输出 // 2. 非阻塞读取所有可用输出 // 3. 写入 outputFile(append 模式) // 4. 如果超过阈值,只返回尾部 N 行/字符 // 5. 返回 OutputResult(preview, fullWritten, totalLines) } /** * 格式化工具返回结果 */ public String formatToolResult(int exitCode, String sessionId, long elapsedMs, OutputResult output, String currentWorkdir) { return String.format(""" exit_code: %d session_id: %s elapsed_ms: %d current_workdir: %s output_lines: %d truncated: %s --- %s""", exitCode, sessionId, elapsedMs, currentWorkdir, output.totalLines(), output.truncated(), output.preview()); } } ``` ### 3.4 ShellSessionTool — 新工具实现 ```java @Component public class ShellSessionTool implements Tool { private final ShellSessionManager sessionManager; private final OutputManager outputManager; private final PathSandbox pathSandbox; private final ObjectMapper objectMapper; @Override public String getName() { return "shell"; // 新工具名,与原有 bash 区分 } @Override public String getDescription() { return """ Interactive shell tool with session persistence. Actions: - exec: Execute a command in a shell session - write_stdin: Write input to an interactive session - close: Close a shell session - list: List active sessions for current conversation """; } @Override public Map getInputSchema() { // 返回 JSON Schema,包含 action、session_id、command 等参数 } @Override public String execute(String arguments, Long sessionId, String workspace) { JsonNode args = objectMapper.readTree(arguments); String action = args.get("action").asText(); return switch (action) { case "exec" -> handleExec(args, sessionId, workspace); case "write_stdin" -> handleWriteStdin(args, sessionId, workspace); case "close" -> handleClose(args, sessionId); case "list" -> handleList(sessionId); default -> errorJson("Unknown action: " + action); }; } private String handleExec(JsonNode args, Long conversationId, String workspace) { // 1. 解析参数:session_id(可选)、command、workdir(可选)、pty(可选) // 2. 获取或创建 ShellSession // 3. 如果有 workdir,先执行 cd // 4. 写入命令到 stdin // 5. 读取输出(带 yieldTime) // 6. 追踪当前工作目录(注入 pwd) // 7. 格式化返回 } } ``` --- ## 四、交互模式设计 ### 4.1 普通模式(pty=false) 适用场景:大多数命令,直接获取结果 ``` Agent 调用: shell(action="exec", command="ls -la", session_id="s1") ↓ ShellSession: stdin.write("ls -la\n") stdin.write("pwd\n") // 自动注入,追踪目录 stdout.readWithTimeout(10s) ↓ 返回: {exit_code: 0, current_workdir: "/tmp", output: "..."} ``` ### 4.2 PTY 模式(pty=true) 适用场景:交互式程序(top、python -i、vim 等) ``` Agent 调用: shell(action="exec", command="python3 -i", session_id="s2", pty=true) ↓ ShellSession: 启动 PTY 进程 stdout.readWithTimeout(5s) // 读取初始输出(Python banner) ↓ 返回: {session_id: "s2", output: "Python 3.11.0\n>>>"} Agent 调用: shell(action="write_stdin", session_id="s2", input="print('hello')\n") ↓ ShellSession: stdin.write("print('hello')\n") stdout.readWithTimeout(5s) ↓ 返回: {session_id: "s2", output: "hello\n>>>"} ``` ### 4.3 命令执行流程对比 | 步骤 | BashTool (已移除) | ShellSessionTool (当前) | |------|--------------|---------------------| | 1. 接收命令 | `execute(command)` (已移除) | `exec(command, session_id)` | | 2. 创建进程 | 每次新建 | 复用已有会话 | | 3. 执行命令 | `bash -c command` | `stdin.write(command)` | | 4. 读取输出 | `process.getInputStream()` | `stdout.readWithTimeout()` | | 5. 返回结果 | 截断到 50000 字符 | 预览 + 落盘 | | 6. 清理 | `process.destroy()` | 保持会话存活 | --- ## 五、输出管理设计 ### 5.1 输出截断策略 ``` 原始输出(可能 100000+ 字符) ↓ ┌───┴───┐ │ │ 预览 完整输出 (返回给AI) (落盘到文件) │ ├── 尾部 100 行 └── 最多 10000 字符 ``` ### 5.2 文件落盘结构 ``` workspace/ └── shellOutput/ ├── {sessionId}_1.out # 第 1 次命令输出 ├── {sessionId}_2.out # 第 2 次命令输出 └── {sessionId}_N.out # 第 N 次命令输出 ``` Agent 可通过 `read_file` 工具查看完整输出: ``` read_file(path="shellOutput/s1_3.out") ``` ### 5.3 返回格式 ```json { "exit_code": 0, "session_id": "s1", "elapsed_ms": 156, "current_workdir": "/tmp/project", "output_lines": 245, "truncated": true, "output_file": "shellOutput/s1_3.out", "output": "(尾部 100 行预览)..." } ``` --- ## 六、数据库设计 ### 6.1 新增表:shell_session ```sql CREATE TABLE IF NOT EXISTS `shell_session` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `session_id` VARCHAR(64) NOT NULL UNIQUE COMMENT '会话唯一标识', `conversation_id` BIGINT NOT NULL COMMENT '关联的会话 ID', `pty_mode` TINYINT DEFAULT 0 COMMENT '是否 PTY 模式 0-否 1-是', `pid` INT COMMENT '进程 PID', `current_workdir` VARCHAR(512) COMMENT '当前工作目录', `status` VARCHAR(20) DEFAULT 'ACTIVE' COMMENT 'ACTIVE/CLOSED/EXPIRED', `command_count` INT DEFAULT 0 COMMENT '已执行命令数', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `last_active_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `closed_at` DATETIME, INDEX `idx_conversation` (`conversation_id`), INDEX `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` ### 6.2 新增表:shell_command_log ```sql CREATE TABLE IF NOT EXISTS `shell_command_log` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `session_id` VARCHAR(64) NOT NULL COMMENT '会话 ID', `conversation_id` BIGINT NOT NULL COMMENT '会话 ID', `command` TEXT NOT NULL COMMENT '执行的命令', `exit_code` INT COMMENT '退出码', `output_lines` INT COMMENT '输出行数', `output_file` VARCHAR(512) COMMENT '输出文件路径', `truncated` TINYINT DEFAULT 0 COMMENT '是否截断', `elapsed_ms` INT COMMENT '执行耗时(毫秒)', `executed_at` DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX `idx_session` (`session_id`), INDEX `idx_conversation` (`conversation_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` --- ## 七、API 设计 ### 7.1 Tool Schema(供 LLM 调用) ```json { "name": "shell", "description": "Interactive shell tool with session persistence. Supports multi-step operations and interactive programs.", "input_schema": { "type": "object", "properties": { "action": { "type": "string", "enum": ["exec", "write_stdin", "close", "list"], "description": "Action to perform" }, "session_id": { "type": "string", "description": "Session ID. Omit to create new session, or provide to reuse existing." }, "command": { "type": "string", "description": "Shell command to execute (for exec action)" }, "input": { "type": "string", "description": "Input to write to stdin (for write_stdin action)" }, "workdir": { "type": "string", "description": "Working directory (relative to workspace)" }, "pty": { "type": "boolean", "description": "Use PTY mode for interactive programs (default false)" }, "timeout": { "type": "integer", "description": "Timeout in seconds (default 30)" }, "yield_time_ms": { "type": "integer", "description": "Initial wait time for output (default 10000)" } }, "required": ["action"] } } ``` ### 7.2 调用示例 **普通命令**: ```json { "action": "exec", "command": "npm install", "workdir": "frontend" } ``` **交互式程序**: ```json // 第一次调用:启动 Python {"action": "exec", "command": "python3 -i", "pty": true} // 后续调用:发送输入 {"action": "write_stdin", "session_id": "s1", "input": "import pandas as pd\n"} {"action": "write_stdin", "session_id": "s1", "input": "df = pd.read_csv('data.csv')\n"} {"action": "write_stdin", "session_id": "s1", "input": "df.head()\n"} // 关闭会话 {"action": "close", "session_id": "s1"} ``` --- ## 八、安全设计 ### 8.1 路径安全 复用现有 `PathSandbox`,所有路径操作必须经过校验: ```java Path resolvedWorkdir = pathSandbox.resolve(workdir, workspace); ``` ### 8.2 命令白名单(可选) 可配置允许/禁止的命令模式: ```yaml app: harness: shell: allowed-commands: - "npm *" - "git *" - "python3 *" blocked-commands: - "rm -rf /" - "sudo *" ``` ### 8.3 资源限制 | 资源 | 限制 | |------|------| | 每对话最大会话数 | 5 | | 会话空闲超时 | 30 分钟 | | 会话最大生命周期 | 2 小时 | | 单命令最大输出 | 100 MB(落盘) | | 预览输出大小 | 10000 字符 | ### 8.4 会话清理 ```java @Scheduled(fixedRate = 60000) // 每分钟检查 public void cleanupExpiredSessions() { sessions.forEach((id, session) -> { if (session.isExpired() || session.isIdleTimeout()) { session.close(); sessions.remove(id); } }); } ``` --- ## 九、与现有系统的集成 ### 9.1 ToolDispatcher 路由 ```java public String dispatch(String toolName, String arguments, String executionMode, Long sessionId, String workspace) { // 新增 shell 工具路由 if ("shell".equals(toolName)) { Tool tool = toolRegistry.getTool("shell"); return tool.execute(arguments, sessionId, workspace); } // 原有逻辑保持不变 if ("LOCAL".equals(executionMode)) { return localToolExecutor.execute(sessionId, toolName, arguments, workspace); } Tool tool = toolRegistry.getTool(toolName); return tool.execute(arguments, sessionId, workspace); } ``` ### 9.2 AgentLoop 集成 在 AgentLoop 的会话结束时清理 Shell 会话: ```java public void execute(AgentExecutionContext context, AgentEventListener listener, MessagePersistenceCallback persistenceCallback) { try { // ... 原有逻辑 ... } finally { // 清理该对话的所有 Shell 会话 shellSessionManager.closeByConversation(context.getSessionId()); } } ``` ### 9.3 PromptEngine 集成 在系统提示词中注入 Shell 工具使用指南: ```java public String buildSystemPrompt(AgentExecutionContext context) { StringBuilder sb = new StringBuilder(); // ... 原有逻辑 ... // 如果 Agent 配置了 shell 工具,注入使用指南 if (context.getToolNames().contains("shell")) { sb.append("\n## Shell Tool Usage\n"); sb.append(shellUsageGuide); // 从 skill 文档加载 } return sb.toString(); } ``` --- ## 十、改造清单 ### 10.1 新增文件 | 文件路径 | 说明 | |---------|------| | `backend/.../harness/shell/ShellSession.java` | 会话实体类 | | `backend/.../harness/shell/ShellSessionManager.java` | 会话管理器 | | `backend/.../harness/shell/OutputManager.java` | 输出管理器 | | `backend/.../harness/tool/impl/ShellSessionTool.java` | 新 Tool 实现 | | `backend/.../harness/shell/ShellUsageGuide.md` | 使用指南(Skill 文档) | | `backend/src/main/resources/db/migration/V011__shell_session.sql` | 数据库迁移 | ### 10.2 修改文件 | 文件路径 | 修改内容 | |---------|---------| | `backend/.../harness/tool/ToolRegistry.java` | 注册 ShellSessionTool | | `backend/.../harness/init/ToolDataInitializer.java` | 初始化 shell 工具数据 | | `backend/.../harness/core/AgentLoop.java` | 会话结束时清理 Shell 会话 | | `backend/.../harness/core/PromptEngine.java` | 注入 Shell 使用指南 | | `backend/.../harness/tool/ToolDispatcher.java` | 添加 shell 工具路由(可选) | ### 10.3 配置文件 ```yaml # application.yml 新增配置 app: harness: shell: enabled: true max-sessions-per-conversation: 30 session-idle-timeout-minutes: 30 session-max-lifetime-hours: 2 default-yield-time-ms: 10000 output: max-preview-lines: 100 max-preview-chars: 10000 dir: shellOutput ``` --- ## 十一、实施步骤 ### Phase 1:基础框架(1-2 天) 1. 创建 `ShellSession` 实体类 2. 实现 `ShellSessionManager` 基础会话管理 3. 实现 `OutputManager` 输出截断和落盘 4. 编写数据库迁移脚本 ### Phase 2:工具实现(2-3 天) 5. 实现 `ShellSessionTool` 的 `exec` action(普通模式) 6. 实现 `write_stdin` 和 `close` action 7. 集成到 `ToolRegistry` 和 `ToolDataInitializer` 8. 编写单元测试 ### Phase 3:高级特性(2-3 天) 9. 实现 PTY 模式支持 10. 实现会话过期清理定时任务 11. 集成到 `AgentLoop` 会话生命周期 12. 编写 Skill 使用指南文档 ### Phase 4:测试与优化(1-2 天) 13. 集成测试:多轮对话、会话复用、输出落盘 14. 性能测试:并发会话、长时间运行 15. 安全测试:路径逃逸、命令注入 16. 文档更新 --- ## 十二、风险与注意事项 ### 12.1 风险点 | 风险 | 影响 | 缓解措施 | |------|------|---------| | 进程泄漏 | 内存/文件描述符耗尽 | 严格的超时和清理机制 | | PTY 资源消耗 | 高并发下资源不足 | 限制每对话会话数 | | 状态不可见 | Agent 忘记当前目录 | 每次返回 current_workdir | | 输出过大 | 磁盘空间耗尽 | 限制单命令输出大小 | ### 12.2 向后兼容 - `BashTool` 已移除,`shell` 工具是唯一的命令执行工具 - 现有 Agent 已迁移到 `shell` 工具 ### 12.3 性能考量 - 普通模式:与原有 BashTool 性能相当 - PTY 模式:每个会话占用一个 PTY 文件描述符 - 输出落盘:异步写入,不阻塞返回 --- ## 十三、后续扩展 ### 13.1 可能的增强 1. **命令历史**:记录会话内的命令历史,支持上下文感知 2. **环境变量管理**:支持设置/获取环境变量 3. **后台任务集成**:长时间命令自动转为后台任务 4. **会话持久化**:支持跨对话的会话恢复(需评估安全性) 5. **多用户隔离**:不同用户的会话完全隔离 ### 13.2 与 LOCAL 模式的集成 ShellSessionTool 也可支持 LOCAL 模式,将命令转发到桌面客户端执行: ``` ShellSessionTool → LocalToolExecutor → WebSocket → Electron ``` 这需要在桌面客户端实现对应的 Shell 会话管理逻辑。