feat(agents): Phase 8.4-10.5 built-in plugins, bundled skills, coordinator
This commit is contained in:
171
development-doc/plan/code-update/README.md
Normal file
171
development-doc/plan/code-update/README.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# 代码指挥官 (Code Commander) 实施计划索引
|
||||
|
||||
本目录用于存放代码指挥官模块的分阶段规划文档。
|
||||
|
||||
## 文档说明
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `README.md` | 总览、阶段关系、实施顺序 |
|
||||
| `phase-1-infrastructure.md` | 基础设施:State、Prompt、注册 |
|
||||
| `phase-2-execution-engine.md` | 执行引擎:AI Adapter、沙盒、直接执行 |
|
||||
| `phase-3-agent-integration.md` | Agent 集成:Graph 节点、边路由 |
|
||||
| `phase-4-streaming-interaction.md` | 流式交互:PTY 终端、WebSocket |
|
||||
| `phase-5-frontend-integration.md` | 前端集成:Vue 组件、xterm.js |
|
||||
|
||||
## 推荐阅读顺序
|
||||
|
||||
1. 先阅读本 README 了解整体架构
|
||||
2. 再按顺序阅读 phase 1 ~ phase 5
|
||||
3. 实施时严格按阶段推进
|
||||
|
||||
---
|
||||
|
||||
## 总体设计原则
|
||||
|
||||
1. **用户选择式交互** - 不是自动分流,用户显式选择 AI 提供商
|
||||
2. **安全分级执行** - 低风险直接执行,高风险沙盒隔离
|
||||
3. **流式终端体验** - 实时显示 AI 执行过程,支持用户交互
|
||||
4. **临时目录隔离** - 每个任务在独立临时目录执行,执行后清理
|
||||
|
||||
---
|
||||
|
||||
## 阶段总览图
|
||||
|
||||
```
|
||||
Phase 1 ──────────────────────────────────────────────────────────────┐
|
||||
│ 基础设施 (Infrastructure) │
|
||||
│ - State 定义 │
|
||||
│ - Prompt 模板 │
|
||||
│ - 工具注册 │
|
||||
│ - Agent 注册 │
|
||||
│ │
|
||||
│ 核心文件: state.py, prompts.py, tools/__init__.py, builtins.py │
|
||||
└────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
Phase 2 ──────────────────────────────────────────────────────────────┐
|
||||
│ 执行引擎 (Execution Engine) │
|
||||
│ - AI CLI Adapter (统一接口) │
|
||||
│ - Sandbox Executor │
|
||||
│ - Direct Executor │
|
||||
│ - Security Classifier │
|
||||
│ │
|
||||
│ 核心文件: ai_adapter.py, sandbox_executor.py, direct_executor.py, │
|
||||
│ security_classifier.py │
|
||||
└────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
Phase 3 ──────────────────────────────────────────────────────────────┐
|
||||
│ Agent 集成 (Agent Integration) │
|
||||
│ - Graph 节点 │
|
||||
│ - 边路由 │
|
||||
│ - 任务模型 │
|
||||
│ │
|
||||
│ 核心文件: graph.py, schemas/task.py │
|
||||
└────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
Phase 4 ──────────────────────────────────────────────────────────────┐
|
||||
│ 流式交互 (Streaming Interaction) │
|
||||
│ - PTY 终端 │
|
||||
│ - WebSocket 端点 │
|
||||
│ - 流式输出集成 │
|
||||
│ - 交互输入 │
|
||||
│ │
|
||||
│ 核心文件: terminal_engine.py, routers/terminal.py, stream_output.py │
|
||||
└────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
Phase 5 ──────────────────────────────────────────────────────────────┐
|
||||
│ 前端集成 (Frontend Integration) │
|
||||
│ - 页面组件 │
|
||||
│ - 终端显示组件 │
|
||||
│ - WebSocket 服务 │
|
||||
│ - 路由配置 │
|
||||
│ │
|
||||
│ 核心文件: CodeCommander.vue, TerminalDisplay.vue, terminalWs.ts │
|
||||
└────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 架构概览
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Vue 前端 │
|
||||
│ [用户选择: Claude/Gemini/Codex/OpenCode] + [输入需求] │
|
||||
└────────────────────────┬────────────────────────────────────┘
|
||||
│ WebSocket 流式输出
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ FastAPI 后端 │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ 代码指挥官 (Code Commander Agent) │ │
|
||||
│ │ 1. 接收 AI 类型 + 用户需求 │ │
|
||||
│ │ 2. 安全分级判定 │ │
|
||||
│ │ 3. 路由到对应执行器 │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────────────────┼──────────────────┐ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
||||
│ │ 直接执行器 │ │ 沙盒执行器 │ │ 终端引擎 │ │
|
||||
│ │(低风险任务) │ │(高风险任务) │ │ PTY + 流式 │ │
|
||||
│ └────────────┘ └────────────┘ └────────────┘ │
|
||||
└────────────────────────┬────────────────────────────────────┘
|
||||
│ subprocess 调用
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ CLI 进程 (claude/gemini/codex/opencode) │
|
||||
│ 在临时目录中执行 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Demo 项目借鉴映射
|
||||
|
||||
| Demo 项目 | 主要借鉴点 | 对应 Phase |
|
||||
|---------|-----------|-----------|
|
||||
| **golutra** | PTY 终端、多 CLI 适配、工作流隔离 | Phase 2, 4 |
|
||||
| **golutra CLI** | LocalSocket IPC、命令分发 | Phase 2 |
|
||||
| **golutra Shim** | 进程启动、信号处理 | Phase 2 |
|
||||
|
||||
---
|
||||
|
||||
## 实施顺序
|
||||
|
||||
```
|
||||
Phase 1 → Phase 2 → Phase 3 → Phase 4 → Phase 5
|
||||
│ │ │ │ │
|
||||
│ │ │ │ └── 前端 UI + 路由
|
||||
│ │ │ └── PTY + WebSocket
|
||||
│ │ └── Graph 节点 + 边路由
|
||||
│ └── AI Adapter + Sandbox
|
||||
└── State + Prompt + 注册
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 文件变更追踪
|
||||
|
||||
| Phase | 新增文件 | 修改文件 |
|
||||
|-------|---------|---------|
|
||||
| Phase 1 | `tools/__init__.py` (改) | `state.py`, `prompts.py`, `registry/builtins.py` |
|
||||
| Phase 2 | `ai_adapter.py`, `sandbox_executor.py`, `direct_executor.py`, `security_classifier.py` | - |
|
||||
| Phase 3 | `schemas/task.py` (改) | `graph.py` |
|
||||
| Phase 4 | `terminal_engine.py`, `routers/terminal.py`, `stream_output.py`, `interactive_input.py` | - |
|
||||
| Phase 5 | `CodeCommander.vue`, `TerminalDisplay.vue`, `terminalWs.ts` | `router/index.ts` |
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
| 注意事项 | 说明 |
|
||||
|---------|------|
|
||||
| 不要跳过 Phase | 每个阶段都是下一个的基础 |
|
||||
| AI CLI 前置检查 | 确保服务器上已安装对应 CLI |
|
||||
| 临时目录及时清理 | 防止磁盘空间泄漏 |
|
||||
| WebSocket 重连 | 前端实现自动重连机制 |
|
||||
215
development-doc/plan/code-update/checklist.md
Normal file
215
development-doc/plan/code-update/checklist.md
Normal file
@@ -0,0 +1,215 @@
|
||||
# 代码指挥官实施清单(可勾选执行版)
|
||||
|
||||
日期:2026-04-04
|
||||
状态:执行清单
|
||||
适用范围:基于 `phase-1` ~ `phase-5` 整理
|
||||
|
||||
---
|
||||
|
||||
## 使用说明
|
||||
|
||||
- 完成前使用 `- [ ]`
|
||||
- 完成后改成 `- [x]`
|
||||
- Day 1-3 为后端基础设施
|
||||
- Day 4-5 为后端执行引擎
|
||||
- Day 6 为 Agent 集成
|
||||
- Day 7-8 为流式交互
|
||||
- Day 9-10 为前端集成
|
||||
|
||||
---
|
||||
|
||||
## Day 1:State + Prompt + 注册
|
||||
|
||||
Day 1 目标:完成代码指挥官 Agent 的基础架子
|
||||
|
||||
- [ ] 新增 `CODE_COMMANDER = "code_commander"` 到 `AgentRole` 枚举
|
||||
- [ ] 新增 `CodeCommanderState` TypedDict(包含 task_type, ai_provider, sandbox_mode 等)
|
||||
- [ ] 新增 `CODE_COMMANDER_SYSTEM_PROMPT` 系统提示
|
||||
- [ ] 新增 `SANDBOX_EXECUTION_PROMPT` 沙盒执行说明
|
||||
- [ ] 新增 `DIRECT_EXECUTION_PROMPT` 直接执行说明
|
||||
- [ ] 在 `SUB_COMMANDER_TOOLSETS` 中注册 `CODE_COMMANDER_TOOLSET`
|
||||
- [ ] 新增 `CodeCommanderManifest` 到 `AGENT_MANIFESTS`
|
||||
- [ ] 补 Phase 1 单元测试
|
||||
|
||||
**验收:确认 `AgentRole.CODE_COMMANDER` 存在且值正确**
|
||||
|
||||
---
|
||||
|
||||
## Day 2:AI CLI Adapter(统一接口)
|
||||
|
||||
Day 2 目标:实现适配不同 AI CLI 的统一接口
|
||||
|
||||
- [ ] 新增 `AICLIAdapter` 抽象基类
|
||||
- `cli_name` 属性
|
||||
- `requires_workspace` 属性
|
||||
- `build_command()` 方法
|
||||
- `parse_output()` 方法
|
||||
- `is_installed()` 方法
|
||||
- [ ] 新增 `ClaudeAdapter` 实现
|
||||
- [ ] 新增 `GeminiAdapter` 实现
|
||||
- [ ] 新增 `CodexAdapter` 实现
|
||||
- [ ] 新增 `OpenCodeAdapter` 实现
|
||||
- [ ] 新增 `CodeExecutionResult` 数据类
|
||||
- [ ] 补 Day 2 单元测试
|
||||
|
||||
**验收:`AICLIAdapter` 可以正确识别 4 种 CLI**
|
||||
|
||||
---
|
||||
|
||||
## Day 3:Security Classifier + Direct Executor
|
||||
|
||||
Day 3 目标:实现安全分级和直接执行器
|
||||
|
||||
- [ ] 新增 `RiskLevel` 枚举(LOW/HIGH)
|
||||
- [ ] 新增 `SecurityClassifier` 类
|
||||
- `HIGH_RISK_KEYWORDS` 列表
|
||||
- `LOW_RISK_KEYWORDS` 列表
|
||||
- `classify()` 方法实现
|
||||
- `_is_project_path()` 方法实现
|
||||
- [ ] 新增 `DirectExecutor` 类
|
||||
- `execute()` 方法(异步)
|
||||
- 超时控制
|
||||
- `is_installed()` 检查
|
||||
- [ ] 补 Day 3 单元测试
|
||||
|
||||
**验收:`SecurityClassifier` 能正确分类高低风险**
|
||||
|
||||
---
|
||||
|
||||
## Day 4:Sandbox Environment + Sandbox Executor
|
||||
|
||||
Day 4 目标:实现沙盒执行器
|
||||
|
||||
- [ ] 新增 `SandboxEnvironment` 类
|
||||
- `create()` 静态方法(创建临时目录)
|
||||
- `cleanup()` 方法
|
||||
- `workspace_path` 属性
|
||||
- `session_id` 属性
|
||||
- [ ] 新增 `SandboxExecutor` 类
|
||||
- `execute()` 方法(异步,yield 流式输出)
|
||||
- `cleanup_session()` 方法
|
||||
- `_list_created_files()` 方法
|
||||
- [ ] 实现超时控制
|
||||
- [ ] 补 Day 4 单元测试
|
||||
|
||||
**验收:`SandboxExecutor` 能创建、执行、清理沙盒**
|
||||
|
||||
---
|
||||
|
||||
## Day 5:执行引擎集成测试
|
||||
|
||||
Day 5 目标:确保执行引擎各组件协同工作
|
||||
|
||||
- [ ] 集成测试:`SecurityClassifier` + `DirectExecutor`
|
||||
- [ ] 集成测试:`SecurityClassifier` + `SandboxExecutor`
|
||||
- [ ] 集成测试:4 种 `AICLIAdapter` 的 `build_command()`
|
||||
- [ ] 端到端测试:低风险任务直接执行
|
||||
- [ ] 端到端测试:高风险任务沙盒执行
|
||||
- [ ] 确认沙盒目录创建和清理正常
|
||||
|
||||
**验收:所有执行器支持流式输出,且正确路由**
|
||||
|
||||
---
|
||||
|
||||
## Day 6:Graph 节点 + 边路由
|
||||
|
||||
Day 6 目标:将代码指挥官接入 LangGraph
|
||||
|
||||
- [ ] 新增 `code_commander_node` 函数
|
||||
- 获取用户需求和 AI 提供商
|
||||
- 调用 `SecurityClassifier`
|
||||
- 根据风险等级选择执行器
|
||||
- 返回执行结果
|
||||
- [ ] 在 `NODES` 字典中注册 `code_commander`
|
||||
- [ ] 新增 `_should_route_to_code_commander()` 路由函数
|
||||
- [ ] 在 `graph.py` 中添加条件边
|
||||
- [ ] 新增 `CodeTask`, `CodeExecutionResult` 模型到 `schemas/task.py`
|
||||
- [ ] 补 Day 6 单元测试
|
||||
|
||||
**验收:高风险任务路由到沙盒,低风险路由到直接执行**
|
||||
|
||||
---
|
||||
|
||||
## Day 7:PTY Terminal Engine
|
||||
|
||||
Day 7 目标:实现 PTY 终端管理
|
||||
|
||||
- [ ] 新增 `PTYSession` 数据类
|
||||
- [ ] 新增 `PTYManager` 类
|
||||
- `spawn()` 方法
|
||||
- `write()` 方法
|
||||
- `read()` 方法(异步生成器)
|
||||
- `resize()` 方法
|
||||
- `kill()` 方法
|
||||
- [ ] 实现 `asyncio.subprocess` 进程管理
|
||||
- [ ] 实现输出队列
|
||||
- [ ] 补 Day 7 单元测试
|
||||
|
||||
**验收:PTY 会话可以启动、读写、终止**
|
||||
|
||||
---
|
||||
|
||||
## Day 8:WebSocket + 流式输出
|
||||
|
||||
Day 8 目标:实现 WebSocket 端点和流式输出
|
||||
|
||||
- [ ] 新增 `ConnectionManager` 类
|
||||
- [ ] 新增 `/ws/terminal/{session_id}` WebSocket 端点
|
||||
- [ ] 实现连接管理(connect/disconnect)
|
||||
- [ ] 新增 `StreamOutput` 类
|
||||
- [ ] 实现 `stream_execution()` 方法
|
||||
- [ ] 新增 `InteractiveInputHandler` 类
|
||||
- [ ] 实现用户输入传递到 PTY
|
||||
- [ ] 补 Day 8 集成测试
|
||||
|
||||
**验收:WebSocket 连接正常,输出实时推送**
|
||||
|
||||
---
|
||||
|
||||
## Day 9:Vue 页面组件
|
||||
|
||||
Day 9 目标:前端代码指挥官主页面
|
||||
|
||||
- [ ] 新增 `CodeCommander.vue` 页面组件
|
||||
- AI 提供商选择器
|
||||
- 任务输入框
|
||||
- 执行按钮
|
||||
- 终端显示区域
|
||||
- 交互输入框
|
||||
- 下载/清理按钮
|
||||
- [ ] 补 Day 9 组件测试
|
||||
|
||||
**验收:用户可以选择 AI 提供商并输入任务**
|
||||
|
||||
---
|
||||
|
||||
## Day 10:TerminalDisplay + WebSocket 服务 + 路由
|
||||
|
||||
Day 10 目标:完成前端集成
|
||||
|
||||
- [ ] 新增 `TerminalDisplay.vue` 组件(xterm.js)
|
||||
- 终端渲染
|
||||
- ANSI 颜色支持
|
||||
- 用户输入处理
|
||||
- [ ] 新增 `terminalWs.ts` WebSocket 服务
|
||||
- 连接管理
|
||||
- 自动重连
|
||||
- 消息处理
|
||||
- [ ] 在 `router/index.ts` 新增 `/code-commander` 路由
|
||||
- [ ] 端到端测试:完整执行流程
|
||||
- [ ] 确认前端与后端 WebSocket 通信正常
|
||||
|
||||
**验收:用户可以在前端看到实时终端输出并交互**
|
||||
|
||||
---
|
||||
|
||||
## 最终验收
|
||||
|
||||
- [ ] 用户可以选择 AI 提供商(Claude/Gemini/Codex/OpenCode)
|
||||
- [ ] 低风险任务(如贪食蛇 demo)直接执行
|
||||
- [ ] 高风险任务在临时目录沙盒执行
|
||||
- [ ] 终端输出实时流式显示
|
||||
- [ ] 用户可以中途输入交互(如 "y" 确认)
|
||||
- [ ] 临时目录执行后正确清理
|
||||
- [ ] 前端页面正常展示
|
||||
- [ ] 回归测试通过(现有功能不受影响)
|
||||
152
development-doc/plan/code-update/phase-1-infrastructure.md
Normal file
152
development-doc/plan/code-update/phase-1-infrastructure.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# Phase 1:基础设施
|
||||
|
||||
日期:2026-04-04
|
||||
状态:待实施
|
||||
|
||||
---
|
||||
|
||||
## 1. 本阶段目的
|
||||
|
||||
新增代码指挥官 Agent 的基础架子,包括:
|
||||
- State 定义(角色、状态)
|
||||
- Prompt 模板
|
||||
- 工具注册
|
||||
- Agent 注册
|
||||
|
||||
---
|
||||
|
||||
## 2. 详细任务
|
||||
|
||||
### 2.1 State 定义
|
||||
|
||||
**文件**: `backend/app/agents/state.py`
|
||||
|
||||
```python
|
||||
# 新增 AgentRole
|
||||
class AgentRole(str, Enum):
|
||||
# ... 现有角色 ...
|
||||
CODE_COMMANDER = "code_commander"
|
||||
|
||||
# 新增 CodeCommanderState
|
||||
class CodeCommanderState(TypedDict):
|
||||
task_type: str # "demo" | "project" | "modification"
|
||||
ai_provider: str # "claude" | "gemini" | "codex" | "opencode"
|
||||
sandbox_mode: bool # True = 沙盒执行,False = 直接执行
|
||||
workspace_path: str | None # 临时工作目录
|
||||
execution_session_id: str | None # PTY 会话 ID
|
||||
```
|
||||
|
||||
### 2.2 Prompt 模板
|
||||
|
||||
**文件**: `backend/app/agents/prompts.py`
|
||||
|
||||
```python
|
||||
# 代码指挥官系统提示
|
||||
CODE_COMMANDER_SYSTEM_PROMPT = """你是一个代码指挥官,负责协调 AI 写代码助手。
|
||||
|
||||
你的职责:
|
||||
1. 接收用户选择的 AI 提供商(Claude/Gemini/Codex/OpenCode)
|
||||
2. 接收用户的写代码需求
|
||||
3. 进行安全分级判定
|
||||
4. 路由到合适的执行器
|
||||
|
||||
安全分级规则:
|
||||
- 低风险:demo、示例、贪食蛇游戏等独立项目
|
||||
- 高风险:修改现有项目、涉及 Jarvis 项目、路径操作等
|
||||
|
||||
执行模式:
|
||||
- 直接执行:低风险任务,直接运行
|
||||
- 沙盒执行:高风险任务,在临时目录隔离执行"""
|
||||
|
||||
# 沙盒执行说明
|
||||
SANDBOX_EXECUTION_PROMPT = """将在隔离的临时目录中执行任务。
|
||||
任务完成后,工作目录会被保留供下载。"""
|
||||
|
||||
# 直接执行说明
|
||||
DIRECT_EXECUTION_PROMPT = """将直接执行任务。
|
||||
如果需要交互,请等待用户输入。"""
|
||||
```
|
||||
|
||||
### 2.3 工具注册
|
||||
|
||||
**文件**: `backend/app/agents/tools/__init__.py`
|
||||
|
||||
```python
|
||||
# 新增工具集
|
||||
CODE_COMMANDER_TOOLSET = {
|
||||
"code_commander": [
|
||||
"execute_code_task",
|
||||
"get_execution_status",
|
||||
"send_interactive_input",
|
||||
"download_workspace",
|
||||
"cleanup_workspace",
|
||||
]
|
||||
}
|
||||
|
||||
# 在 SUB_COMMANDER_TOOLSETS 中添加
|
||||
SUB_COMMANDER_TOOLSETS: dict[str, list[str]] = {
|
||||
# ... 现有工具集 ...
|
||||
"code_commander": CODE_COMMANDER_TOOLSET["code_commander"],
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 Agent 注册
|
||||
|
||||
**文件**: `backend/app/agents/registry/builtins.py`
|
||||
|
||||
```python
|
||||
# 新增 CodeCommanderManifest
|
||||
CodeCommanderManifest = AgentManifest(
|
||||
id="code_commander",
|
||||
name="代码指挥官",
|
||||
description="协调 AI 写代码助手的指挥官",
|
||||
system_prompt=CODE_COMMANDER_SYSTEM_PROMPT,
|
||||
role=AgentRole.CODE_COMMANDER,
|
||||
sub_commanders=[], # 代码指挥官没有子指挥官
|
||||
tools=["execute_code_task", "get_execution_status",
|
||||
"send_interactive_input", "download_workspace", "cleanup_workspace"],
|
||||
permission_class=PermissionClass.HIGH, # 需要较高权限
|
||||
side_effect_scope=SideEffectScope.WORKSPACE,
|
||||
supports_retry=True,
|
||||
idempotent=False,
|
||||
safe_for_parallel_use=False,
|
||||
requires_confirmation=True,
|
||||
)
|
||||
|
||||
# 注册到 AGENT_MANIFESTS
|
||||
AGENT_MANIFESTS: dict[str, AgentManifest] = {
|
||||
# ... 现有 agent ...
|
||||
"code_commander": CodeCommanderManifest,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心文件清单
|
||||
|
||||
| 文件 | 操作 | 说明 |
|
||||
|------|------|------|
|
||||
| `state.py` | 修改 | 新增 `CODE_COMMANDER` 角色和 `CodeCommanderState` |
|
||||
| `prompts.py` | 修改 | 新增三个 prompt 常量 |
|
||||
| `tools/__init__.py` | 修改 | 新增工具集注册 |
|
||||
| `registry/builtins.py` | 修改 | 新增 `CodeCommanderManifest` |
|
||||
|
||||
---
|
||||
|
||||
## 4. 验收标准
|
||||
|
||||
- [ ] `AgentRole.CODE_COMMANDER` 存在且值正确
|
||||
- [ ] `CODE_COMMANDER_SYSTEM_PROMPT` 包含完整指令
|
||||
- [ ] 工具集已注册且可通过 `SUB_COMMANDER_TOOLSETS` 访问
|
||||
- [ ] `CodeCommanderManifest` 已注册且包含所有必要字段
|
||||
|
||||
---
|
||||
|
||||
## 5. 依赖关系
|
||||
|
||||
```
|
||||
本阶段 → Phase 2(执行引擎)
|
||||
→ Phase 3(Agent 集成)
|
||||
```
|
||||
|
||||
本阶段是后续所有阶段的基础。
|
||||
321
development-doc/plan/code-update/phase-2-execution-engine.md
Normal file
321
development-doc/plan/code-update/phase-2-execution-engine.md
Normal file
@@ -0,0 +1,321 @@
|
||||
# Phase 2:执行引擎
|
||||
|
||||
日期:2026-04-04
|
||||
状态:待实施
|
||||
|
||||
依赖:Phase 1 完成
|
||||
|
||||
---
|
||||
|
||||
## 1. 本阶段目的
|
||||
|
||||
实现代码指挥官的核心执行能力:
|
||||
- AI CLI Adapter:统一接口适配不同 AI CLI
|
||||
- Sandbox Executor:沙盒环境执行
|
||||
- Direct Executor:直接执行低风险任务
|
||||
- Security Classifier:安全分级
|
||||
|
||||
---
|
||||
|
||||
## 2. 详细任务
|
||||
|
||||
### 2.1 AI CLI Adapter
|
||||
|
||||
**新文件**: `backend/app/agents/tools/ai_adapter.py`
|
||||
|
||||
```python
|
||||
from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class CodeExecutionResult:
|
||||
success: bool
|
||||
message: str
|
||||
files_created: list[str]
|
||||
output: str
|
||||
error: str | None
|
||||
|
||||
class AICLIAdapter(ABC):
|
||||
@property
|
||||
@abstractmethod
|
||||
def cli_name(self) -> str:
|
||||
"""CLI 命令名称,如 'claude', 'gemini'"""
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def requires_workspace(self) -> bool:
|
||||
"""是否需要工作目录"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def build_command(self, prompt: str, workspace: Path | None) -> list[str]:
|
||||
"""构建 CLI 命令"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def parse_output(self, output: str) -> CodeExecutionResult:
|
||||
"""解析 CLI 输出"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def is_installed(self) -> bool:
|
||||
"""检查 CLI 是否已安装"""
|
||||
pass
|
||||
|
||||
class ClaudeAdapter(AICLIAdapter):
|
||||
cli_name = "claude"
|
||||
requires_workspace = True
|
||||
|
||||
def build_command(self, prompt: str, workspace: Path | None) -> list[str]:
|
||||
return ["claude", "-p", prompt, "--dangerously-skip-permissions"]
|
||||
|
||||
# ... 其他方法实现
|
||||
|
||||
class GeminiAdapter(AICLIAdapter):
|
||||
cli_name = "gemini"
|
||||
requires_workspace = False
|
||||
# ...
|
||||
|
||||
class CodexAdapter(AICLIAdapter):
|
||||
cli_name = "codex"
|
||||
# ...
|
||||
|
||||
class OpenCodeAdapter(AICLIAdapter):
|
||||
cli_name = "opencode"
|
||||
# ...
|
||||
```
|
||||
|
||||
### 2.2 Security Classifier
|
||||
|
||||
**新文件**: `backend/app/agents/tools/security_classifier.py`
|
||||
|
||||
```python
|
||||
from enum import Enum
|
||||
|
||||
class RiskLevel(Enum):
|
||||
LOW = "low" # 直接执行
|
||||
HIGH = "high" # 沙盒执行
|
||||
|
||||
class SecurityClassifier:
|
||||
HIGH_RISK_KEYWORDS = [
|
||||
"修改", "编辑", "删除", "移动",
|
||||
"Jarvis", "backend", "frontend",
|
||||
"git", "config", ".env",
|
||||
]
|
||||
|
||||
LOW_RISK_KEYWORDS = [
|
||||
"demo", "示例", "贪食蛇", "俄罗斯方块",
|
||||
"小游戏", "独立项目", "新项目",
|
||||
"创建一个", "写一个",
|
||||
]
|
||||
|
||||
def classify(self, task_description: str, target_path: str | None = None) -> RiskLevel:
|
||||
# 1. 检查高风险关键词
|
||||
if any(kw in task_description for kw in self.HIGH_RISK_KEYWORDS):
|
||||
return RiskLevel.HIGH
|
||||
|
||||
# 2. 检查目标路径
|
||||
if target_path and self._is_project_path(target_path):
|
||||
return RiskLevel.HIGH
|
||||
|
||||
# 3. 检查低风险关键词
|
||||
if any(kw in task_description for kw in self.LOW_RISK_KEYWORDS):
|
||||
return RiskLevel.LOW
|
||||
|
||||
# 4. 默认高风险
|
||||
return RiskLevel.HIGH
|
||||
|
||||
def _is_project_path(self, path: str) -> bool:
|
||||
# 检查是否指向 Jarvis 项目路径
|
||||
return "Jarvis" in path or "backend/app" in path
|
||||
```
|
||||
|
||||
### 2.3 Sandbox Executor
|
||||
|
||||
**新文件**: `backend/app/agents/tools/sandbox_executor.py`
|
||||
|
||||
```python
|
||||
import tempfile
|
||||
import shutil
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass, field
|
||||
from typing import AsyncGenerator
|
||||
|
||||
@dataclass
|
||||
class SandboxEnvironment:
|
||||
workspace_path: Path
|
||||
session_id: str
|
||||
|
||||
@staticmethod
|
||||
async def create() -> "SandboxEnvironment":
|
||||
"""创建新的沙盒环境"""
|
||||
temp_dir = tempfile.mkdtemp(prefix="jarvis_code_")
|
||||
session_id = Path(temp_dir).name
|
||||
return SandboxEnvironment(
|
||||
workspace_path=Path(temp_dir),
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
async def cleanup(self):
|
||||
"""清理沙盒环境"""
|
||||
if self.workspace_path.exists():
|
||||
shutil.rmtree(self.workspace_path)
|
||||
|
||||
@dataclass
|
||||
class ExecutionResult:
|
||||
success: bool
|
||||
exit_code: int
|
||||
stdout: str
|
||||
stderr: str
|
||||
files_created: list[str] = field(default_factory=list)
|
||||
|
||||
class SandboxExecutor:
|
||||
def __init__(self, adapter: AICLIAdapter, timeout: int = 300):
|
||||
self.adapter = adapter
|
||||
self.timeout = timeout
|
||||
self._sessions: dict[str, SandboxEnvironment] = {}
|
||||
|
||||
async def execute(
|
||||
self,
|
||||
prompt: str,
|
||||
session_id: str | None = None
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""执行代码任务,yield 实时输出"""
|
||||
# 1. 创建或复用沙盒环境
|
||||
if session_id and session_id in self._sessions:
|
||||
env = self._sessions[session_id]
|
||||
else:
|
||||
env = await SandboxEnvironment.create()
|
||||
self._sessions[env.session_id] = env
|
||||
session_id = env.session_id
|
||||
|
||||
# 2. 构建命令
|
||||
cmd = self.adapter.build_command(prompt, env.workspace_path)
|
||||
|
||||
# 3. 异步执行,实时 yield 输出
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd=str(env.workspace_path),
|
||||
)
|
||||
|
||||
# 4. 实时读取输出
|
||||
while True:
|
||||
line = await process.stdout.readline()
|
||||
if not line:
|
||||
break
|
||||
yield line.decode()
|
||||
|
||||
# 5. 等待完成
|
||||
await process.wait()
|
||||
|
||||
# 6. 收集结果
|
||||
return ExecutionResult(
|
||||
success=process.returncode == 0,
|
||||
exit_code=process.returncode or 0,
|
||||
stdout=...,
|
||||
stderr=...,
|
||||
files_created=self._list_created_files(env.workspace_path),
|
||||
)
|
||||
|
||||
async def cleanup_session(self, session_id: str):
|
||||
"""清理指定会话"""
|
||||
if session_id in self._sessions:
|
||||
await self._sessions[session_id].cleanup()
|
||||
del self._sessions[session_id]
|
||||
```
|
||||
|
||||
### 2.4 Direct Executor
|
||||
|
||||
**新文件**: `backend/app/agents/tools/direct_executor.py`
|
||||
|
||||
```python
|
||||
class DirectExecutor:
|
||||
def __init__(self, adapter: AICLIAdapter, timeout: int = 60):
|
||||
self.adapter = adapter
|
||||
self.timeout = timeout
|
||||
|
||||
async def execute(self, prompt: str) -> ExecutionResult:
|
||||
"""直接执行,不需要沙盒"""
|
||||
if not self.adapter.is_installed():
|
||||
return ExecutionResult(
|
||||
success=False,
|
||||
exit_code=-1,
|
||||
stdout="",
|
||||
stderr=f"{self.adapter.cli_name} is not installed",
|
||||
)
|
||||
|
||||
cmd = self.adapter.build_command(prompt, None)
|
||||
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
|
||||
try:
|
||||
stdout, stderr = await asyncio.wait_for(
|
||||
process.communicate(),
|
||||
timeout=self.timeout,
|
||||
)
|
||||
return ExecutionResult(
|
||||
success=process.returncode == 0,
|
||||
exit_code=process.returncode or 0,
|
||||
stdout=stdout.decode(),
|
||||
stderr=stderr.decode(),
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
process.kill()
|
||||
return ExecutionResult(
|
||||
success=False,
|
||||
exit_code=-1,
|
||||
stdout="",
|
||||
stderr=f"Execution timed out after {self.timeout}s",
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心文件清单
|
||||
|
||||
| 文件 | 操作 | 说明 |
|
||||
|------|------|------|
|
||||
| `ai_adapter.py` | 新增 | 抽象基类 + 4 个具体实现 |
|
||||
| `security_classifier.py` | 新增 | 安全分级器 |
|
||||
| `sandbox_executor.py` | 新增 | 沙盒执行器 |
|
||||
| `direct_executor.py` | 新增 | 直接执行器 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 验收标准
|
||||
|
||||
- [ ] `AICLIAdapter` 可以正确识别 4 种 CLI
|
||||
- [ ] `SecurityClassifier` 能正确分类高低风险
|
||||
- [ ] `SandboxExecutor` 能创建、执行、清理沙盒
|
||||
- [ ] `DirectExecutor` 能直接执行低风险任务
|
||||
- [ ] 所有执行器支持流式输出
|
||||
|
||||
---
|
||||
|
||||
## 5. 风险与缓解
|
||||
|
||||
| 风险 | 缓解 |
|
||||
|------|------|
|
||||
| AI CLI 未安装 | `is_installed()` 检查 + 友好提示 |
|
||||
| 执行超时 | `timeout` 参数控制 |
|
||||
| 沙盒清理遗漏 | 使用 `finally` 块确保清理 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 依赖关系
|
||||
|
||||
```
|
||||
Phase 1(基础设施)
|
||||
↓
|
||||
本阶段 → Phase 3(Agent 集成)
|
||||
→ Phase 4(流式交互)
|
||||
```
|
||||
162
development-doc/plan/code-update/phase-3-agent-integration.md
Normal file
162
development-doc/plan/code-update/phase-3-agent-integration.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# Phase 3:Agent 集成
|
||||
|
||||
日期:2026-04-04
|
||||
状态:待实施
|
||||
|
||||
依赖:Phase 1 + Phase 2 完成
|
||||
|
||||
---
|
||||
|
||||
## 1. 本阶段目的
|
||||
|
||||
将代码指挥官接入 LangGraph:
|
||||
- Graph 节点
|
||||
- 边路由
|
||||
- 任务模型
|
||||
|
||||
---
|
||||
|
||||
## 2. 详细任务
|
||||
|
||||
### 2.1 Graph 节点
|
||||
|
||||
**文件**: `backend/app/agents/graph.py`
|
||||
|
||||
```python
|
||||
# 新增 code_commander_node
|
||||
async def code_commander_node(state: AgentState) -> AgentState:
|
||||
"""代码指挥官节点"""
|
||||
# 1. 获取用户需求和选择的 AI 提供商
|
||||
user_message = state.messages[-1].content
|
||||
ai_provider = state.get("ai_provider", "claude")
|
||||
|
||||
# 2. 安全分级
|
||||
classifier = SecurityClassifier()
|
||||
risk_level = classifier.classify(user_message)
|
||||
|
||||
# 3. 根据风险等级选择执行器
|
||||
adapter = get_adapter(ai_provider)
|
||||
if risk_level == RiskLevel.LOW:
|
||||
executor = DirectExecutor(adapter)
|
||||
result = await executor.execute(user_message)
|
||||
else:
|
||||
sandbox = await SandboxEnvironment.create()
|
||||
executor = SandboxExecutor(adapter)
|
||||
result = await executor.execute(user_message, sandbox.session_id)
|
||||
state["workspace_path"] = str(sandbox.workspace_path)
|
||||
state["execution_session_id"] = sandbox.session_id
|
||||
|
||||
# 4. 更新状态
|
||||
state.messages.append(AIMessage(content=str(result)))
|
||||
state["next_step"] = None # 任务完成
|
||||
|
||||
return state
|
||||
|
||||
# 节点注册到 NODES
|
||||
NODES: dict[str, NodeCallable] = {
|
||||
# ... 现有节点 ...
|
||||
"code_commander": code_commander_node,
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 边路由
|
||||
|
||||
**文件**: `backend/app/agents/graph.py`
|
||||
|
||||
```python
|
||||
def _should_route_to_code_commander(state: AgentState) -> str:
|
||||
"""判断是否路由到代码指挥官"""
|
||||
if state.current_agent == "code_commander":
|
||||
return "code_commander"
|
||||
# ... 其他条件
|
||||
return END
|
||||
|
||||
# 边注册
|
||||
def _build_graph() -> CompiledGraph:
|
||||
# ... 现有边 ...
|
||||
|
||||
# 新增代码指挥官相关边
|
||||
graph.add_conditional_edges(
|
||||
"master",
|
||||
_should_route_to_code_commander,
|
||||
{
|
||||
"code_commander": "code_commander",
|
||||
END: END,
|
||||
}
|
||||
)
|
||||
|
||||
graph.add_edge("code_commander", END)
|
||||
|
||||
return graph.compile()
|
||||
```
|
||||
|
||||
### 2.3 任务模型
|
||||
|
||||
**文件**: `backend/app/agents/schemas/task.py`
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Literal
|
||||
|
||||
class CodeProviderType(str, Enum):
|
||||
CLAUDE = "claude"
|
||||
GEMINI = "gemini"
|
||||
CODEX = "codex"
|
||||
OPENCODE = "opencode"
|
||||
|
||||
class RiskLevelType(str, Enum):
|
||||
LOW = "low"
|
||||
HIGH = "high"
|
||||
|
||||
class CodeTask(BaseModel):
|
||||
"""代码任务"""
|
||||
id: str = Field(default_factory=lambda: f"code_{uuid.uuid4().hex[:8]}")
|
||||
provider: CodeProviderType
|
||||
prompt: str
|
||||
risk_level: RiskLevelType
|
||||
sandbox_mode: bool
|
||||
workspace_path: str | None = None
|
||||
session_id: str | None = None
|
||||
status: Literal["pending", "running", "completed", "failed"] = "pending"
|
||||
created_at: datetime = Field(default_factory=datetime.now)
|
||||
|
||||
class CodeExecutionResult(BaseModel):
|
||||
"""代码执行结果"""
|
||||
task_id: str
|
||||
success: bool
|
||||
exit_code: int
|
||||
stdout: str
|
||||
stderr: str
|
||||
files_created: list[str] = Field(default_factory=list)
|
||||
workspace_path: str | None = None
|
||||
completed_at: datetime = Field(default_factory=datetime.now)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心文件清单
|
||||
|
||||
| 文件 | 操作 | 说明 |
|
||||
|------|------|------|
|
||||
| `graph.py` | 修改 | 新增 `code_commander_node` 和边路由 |
|
||||
| `schemas/task.py` | 修改 | 新增 `CodeTask`, `CodeExecutionResult` 等模型 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 验收标准
|
||||
|
||||
- [ ] `code_commander_node` 正确处理任务
|
||||
- [ ] `SecurityClassifier` 被正确调用
|
||||
- [ ] 高低风险任务路由到正确的执行器
|
||||
- [ ] `CodeTask` 和 `CodeExecutionResult` 模型正确
|
||||
|
||||
---
|
||||
|
||||
## 5. 依赖关系
|
||||
|
||||
```
|
||||
Phase 1 + Phase 2
|
||||
↓
|
||||
本阶段 → Phase 4(流式交互)
|
||||
→ Phase 5(前端集成)
|
||||
```
|
||||
@@ -0,0 +1,298 @@
|
||||
# Phase 4:流式交互
|
||||
|
||||
日期:2026-04-04
|
||||
状态:待实施
|
||||
|
||||
依赖:Phase 3 完成
|
||||
|
||||
---
|
||||
|
||||
## 1. 本阶段目的
|
||||
|
||||
实现 PTY 终端 + WebSocket 流式输出:
|
||||
- PTY 终端管理
|
||||
- WebSocket 端点
|
||||
- 流式输出集成
|
||||
- 交互输入
|
||||
|
||||
---
|
||||
|
||||
## 2. 详细任务
|
||||
|
||||
### 2.1 PTY Terminal Engine
|
||||
|
||||
**新文件**: `backend/app/agents/tools/terminal_engine.py`
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from typing import AsyncGenerator
|
||||
|
||||
@dataclass
|
||||
class PTYSession:
|
||||
session_id: str
|
||||
process: asyncio.subprocess.Process
|
||||
workspace_path: str
|
||||
|
||||
class PTYManager:
|
||||
def __init__(self):
|
||||
self._sessions: dict[str, PTYSession] = {}
|
||||
self._output_queues: dict[str, asyncio.Queue] = {}
|
||||
|
||||
async def spawn(
|
||||
self,
|
||||
cli: str,
|
||||
args: list[str],
|
||||
cwd: str,
|
||||
session_id: str | None = None
|
||||
) -> str:
|
||||
"""启动 PTY 会话"""
|
||||
if session_id is None:
|
||||
session_id = f"pty_{os.urandom(8).hex()}"
|
||||
|
||||
# 创建 PTY 进程
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*([cli] + args),
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd=cwd,
|
||||
env={**os.environ, "TERM": "xterm-256color"},
|
||||
)
|
||||
|
||||
session = PTYSession(
|
||||
session_id=session_id,
|
||||
process=process,
|
||||
workspace_path=cwd,
|
||||
)
|
||||
self._sessions[session_id] = session
|
||||
self._output_queues[session_id] = asyncio.Queue()
|
||||
|
||||
# 启动输出读取协程
|
||||
asyncio.create_task(self._read_output(session_id))
|
||||
|
||||
return session_id
|
||||
|
||||
async def _read_output(self, session_id: str):
|
||||
"""读取 PTY 输出并放入队列"""
|
||||
session = self._sessions.get(session_id)
|
||||
if not session:
|
||||
return
|
||||
|
||||
queue = self._output_queues[session_id]
|
||||
|
||||
while True:
|
||||
line = await session.process.stdout.readline()
|
||||
if not line:
|
||||
break
|
||||
await queue.put(line.decode())
|
||||
|
||||
# 同时推送给所有订阅者
|
||||
await self._broadcast(session_id, line.decode())
|
||||
|
||||
await queue.put(None) # 结束标记
|
||||
|
||||
async def write(self, session_id: str, data: str):
|
||||
"""写入 PTY(用户输入)"""
|
||||
session = self._sessions.get(session_id)
|
||||
if session and session.process.stdin:
|
||||
session.process.stdin.write(data)
|
||||
await session.process.stdin.drain()
|
||||
|
||||
async def read(self, session_id: str) -> AsyncGenerator[str, None]:
|
||||
"""读取 PTY 输出"""
|
||||
queue = self._output_queues.get(session_id)
|
||||
if not queue:
|
||||
return
|
||||
|
||||
while True:
|
||||
line = await queue.get()
|
||||
if line is None:
|
||||
break
|
||||
yield line
|
||||
|
||||
async def resize(self, session_id: str, rows: int, cols: int):
|
||||
"""调整终端大小"""
|
||||
# TODO: 实现 resize
|
||||
pass
|
||||
|
||||
async def kill(self, session_id: str):
|
||||
"""终止 PTY 会话"""
|
||||
if session_id in self._sessions:
|
||||
session = self._sessions[session_id]
|
||||
session.process.terminate()
|
||||
await session.process.wait()
|
||||
del self._sessions[session_id]
|
||||
del self._output_queues[session_id]
|
||||
|
||||
async def _broadcast(self, session_id: str, data: str):
|
||||
"""广播输出到 WebSocket"""
|
||||
# 实际推送由 router 层处理
|
||||
pass
|
||||
```
|
||||
|
||||
### 2.2 WebSocket 端点
|
||||
|
||||
**新文件**: `backend/app/routers/terminal.py`
|
||||
|
||||
```python
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
||||
from typing import dict
|
||||
|
||||
router = APIRouter(prefix="/ws/terminal", tags=["terminal"])
|
||||
|
||||
class ConnectionManager:
|
||||
def __init__(self):
|
||||
self.active_connections: dict[str, WebSocket] = {}
|
||||
|
||||
async def connect(self, session_id: str, websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
self.active_connections[session_id] = websocket
|
||||
|
||||
def disconnect(self, session_id: str):
|
||||
if session_id in self.active_connections:
|
||||
del self.active_connections[session_id]
|
||||
|
||||
async def send(self, session_id: str, data: str):
|
||||
if session_id in self.active_connections:
|
||||
await self.active_connections[session_id].send_text(data)
|
||||
|
||||
manager = ConnectionManager()
|
||||
|
||||
@router.websocket("/{session_id}")
|
||||
async def terminal_websocket(websocket: WebSocket, session_id: str):
|
||||
await manager.connect(session_id, websocket)
|
||||
|
||||
# 获取 PTY Manager 实例
|
||||
from app.agents.tools.terminal_engine import pty_manager
|
||||
|
||||
try:
|
||||
# 订阅该 session 的输出
|
||||
queue = pty_manager._output_queues.get(session_id)
|
||||
if queue:
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
# 接收用户输入
|
||||
await pty_manager.write(session_id, data + "\n")
|
||||
|
||||
except WebSocketDisconnect:
|
||||
manager.disconnect(session_id)
|
||||
```
|
||||
|
||||
### 2.3 流式输出集成
|
||||
|
||||
**新文件**: `backend/app/agents/tools/stream_output.py`
|
||||
|
||||
```python
|
||||
import json
|
||||
from typing import AsyncGenerator
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class StreamEvent:
|
||||
type: str # "output" | "error" | "status" | "complete"
|
||||
session_id: str
|
||||
data: str
|
||||
timestamp: str
|
||||
|
||||
class StreamOutput:
|
||||
def __init__(self, session_id: str, websocket_sender):
|
||||
self.session_id = session_id
|
||||
self.websocket_sender = websocket_sender
|
||||
|
||||
async def push(self, event_type: str, data: str):
|
||||
"""推送事件到 WebSocket"""
|
||||
event = StreamEvent(
|
||||
type=event_type,
|
||||
session_id=self.session_id,
|
||||
data=data,
|
||||
timestamp=datetime.now().isoformat(),
|
||||
)
|
||||
await self.websocket_sender(self.session_id, json.dumps(event.__dict__))
|
||||
|
||||
async def stream_execution(
|
||||
self,
|
||||
executor,
|
||||
prompt: str
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""包装执行器,实现流式输出"""
|
||||
async for line in executor.execute(prompt):
|
||||
await self.push("output", line)
|
||||
yield line
|
||||
|
||||
await self.push("complete", "")
|
||||
```
|
||||
|
||||
### 2.4 交互输入
|
||||
|
||||
**新文件**: `backend/app/agents/tools/interactive_input.py`
|
||||
|
||||
```python
|
||||
class InteractiveInputHandler:
|
||||
def __init__(self, pty_manager: PTYManager):
|
||||
self.pty_manager = pty_manager
|
||||
self._pending_inputs: dict[str, asyncio.Event] = {}
|
||||
|
||||
async def wait_for_input(self, session_id: str, prompt: str) -> str:
|
||||
"""等待用户输入(如 "y" 确认)"""
|
||||
event = asyncio.Event()
|
||||
self._pending_inputs[session_id] = event
|
||||
|
||||
# 发送提示
|
||||
from app.routers.terminal import manager
|
||||
await manager.send(session_id, f"\n{prompt}\n")
|
||||
|
||||
# 等待输入完成
|
||||
await event.wait()
|
||||
del self._pending_inputs[session_id]
|
||||
|
||||
return self._input_cache.get(session_id, "")
|
||||
|
||||
async def send_input(self, session_id: str, data: str):
|
||||
"""用户发送输入"""
|
||||
self._input_cache[session_id] = data
|
||||
if session_id in self._pending_inputs:
|
||||
self._pending_inputs[session_id].set()
|
||||
|
||||
# 同时写入 PTY
|
||||
await self.pty_manager.write(session_id, data + "\n")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心文件清单
|
||||
|
||||
| 文件 | 操作 | 说明 |
|
||||
|------|------|------|
|
||||
| `terminal_engine.py` | 新增 | PTY 终端管理 |
|
||||
| `routers/terminal.py` | 新增 | WebSocket 端点 |
|
||||
| `stream_output.py` | 新增 | 流式输出封装 |
|
||||
| `interactive_input.py` | 新增 | 交互输入处理 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 验收标准
|
||||
|
||||
- [ ] PTY 会话可以启动、读写、终止
|
||||
- [ ] WebSocket 可以建立连接并收发消息
|
||||
- [ ] 执行输出实时推送到前端
|
||||
- [ ] 用户输入可以传递到 PTY
|
||||
|
||||
---
|
||||
|
||||
## 5. 依赖关系
|
||||
|
||||
```
|
||||
Phase 3(Agent 集成)
|
||||
↓
|
||||
本阶段 → Phase 5(前端集成)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 备注
|
||||
|
||||
PTY 实现参考了 golutra 的 `src-tauri/src/runtime/pty.rs`:
|
||||
- 使用 `portable-pty` 库
|
||||
- Windows 路径兼容处理
|
||||
- shim 机制用于信号处理
|
||||
364
development-doc/plan/code-update/phase-5-frontend-integration.md
Normal file
364
development-doc/plan/code-update/phase-5-frontend-integration.md
Normal file
@@ -0,0 +1,364 @@
|
||||
# Phase 5:前端集成
|
||||
|
||||
日期:2026-04-04
|
||||
状态:待实施
|
||||
|
||||
依赖:Phase 4 完成
|
||||
|
||||
---
|
||||
|
||||
## 1. 本阶段目的
|
||||
|
||||
Vue 前端新增代码指挥官 UI:
|
||||
- 页面组件
|
||||
- 终端显示组件
|
||||
- WebSocket 服务
|
||||
- 路由配置
|
||||
|
||||
---
|
||||
|
||||
## 2. 详细任务
|
||||
|
||||
### 2.1 页面组件
|
||||
|
||||
**新文件**: `frontend/src/pages/chat/CodeCommander.vue`
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="code-commander">
|
||||
<!-- AI 提供商选择器 -->
|
||||
<div class="provider-selector">
|
||||
<div class="label">选择 AI 助手</div>
|
||||
<div class="providers">
|
||||
<button
|
||||
v-for="p in providers"
|
||||
:key="p.id"
|
||||
:class="{ active: selectedProvider === p.id }"
|
||||
@click="selectedProvider = p.id"
|
||||
>
|
||||
<img :src="p.icon" :alt="p.name" />
|
||||
{{ p.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务输入 -->
|
||||
<div class="task-input">
|
||||
<textarea
|
||||
v-model="taskPrompt"
|
||||
placeholder="描述你想让 AI 帮你做什么..."
|
||||
rows="4"
|
||||
/>
|
||||
<button @click="executeTask" :disabled="isExecuting">
|
||||
{{ isExecuting ? '执行中...' : '开始执行' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 终端输出 -->
|
||||
<TerminalDisplay
|
||||
ref="terminalRef"
|
||||
:session-id="currentSessionId"
|
||||
@input="handleUserInput"
|
||||
/>
|
||||
|
||||
<!-- 交互输入框 -->
|
||||
<div v-if="isWaitingForInput" class="interactive-input">
|
||||
<span>{{ inputPrompt }}</span>
|
||||
<input v-model="userInput" @keyup.enter="sendUserInput" />
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="actions">
|
||||
<button @click="downloadFiles" :disabled="!canDownload">
|
||||
下载文件
|
||||
</button>
|
||||
<button @click="cleanup" :disabled="!canCleanup">
|
||||
清理
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import TerminalDisplay from '@/components/TerminalDisplay.vue'
|
||||
import { terminalWsService } from '@/services/terminalWs'
|
||||
|
||||
const providers = [
|
||||
{ id: 'claude', name: 'Claude', icon: '/icons/claude.png' },
|
||||
{ id: 'gemini', name: 'Gemini', icon: '/icons/gemini.png' },
|
||||
{ id: 'codex', name: 'Codex', icon: '/icons/codex.png' },
|
||||
{ id: 'opencode', name: 'OpenCode', icon: '/icons/opencode.png' },
|
||||
]
|
||||
|
||||
const selectedProvider = ref('claude')
|
||||
const taskPrompt = ref('')
|
||||
const isExecuting = ref(false)
|
||||
const currentSessionId = ref<string | null>(null)
|
||||
const isWaitingForInput = ref(false)
|
||||
const inputPrompt = ref('')
|
||||
const userInput = ref('')
|
||||
const terminalRef = ref<InstanceType<typeof TerminalDisplay> | null>(null)
|
||||
|
||||
const canDownload = computed(() => currentSessionId.value !== null)
|
||||
const canCleanup = computed(() => currentSessionId.value !== null)
|
||||
|
||||
async function executeTask() {
|
||||
if (!taskPrompt.value.trim()) return
|
||||
|
||||
isExecuting.value = true
|
||||
currentSessionId.value = await terminalWsService.connect(selectedProvider.value)
|
||||
|
||||
// 订阅消息
|
||||
terminalWsService.onMessage((msg) => {
|
||||
if (msg.type === 'output') {
|
||||
terminalRef.value?.write(msg.data)
|
||||
} else if (msg.type === 'waiting_input') {
|
||||
isWaitingForInput.value = true
|
||||
inputPrompt.value = msg.data
|
||||
} else if (msg.type === 'complete') {
|
||||
isExecuting.value = false
|
||||
}
|
||||
})
|
||||
|
||||
// 发送任务
|
||||
await terminalWsService.sendTask(currentSessionId.value, taskPrompt.value)
|
||||
}
|
||||
|
||||
function handleUserInput(data: string) {
|
||||
terminalWsService.sendInput(currentSessionId.value!, data)
|
||||
}
|
||||
|
||||
function sendUserInput() {
|
||||
terminalWsService.sendInput(currentSessionId.value!, userInput.value)
|
||||
userInput.value = ''
|
||||
isWaitingForInput.value = false
|
||||
}
|
||||
|
||||
async function downloadFiles() {
|
||||
// TODO: 调用下载 API
|
||||
}
|
||||
|
||||
async function cleanup() {
|
||||
if (currentSessionId.value) {
|
||||
await terminalWsService.disconnect(currentSessionId.value)
|
||||
currentSessionId.value = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 2.2 终端显示组件
|
||||
|
||||
**新文件**: `frontend/src/components/TerminalDisplay.vue`
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="terminal-display" ref="containerRef">
|
||||
<div class="terminal-output" ref="outputRef"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { Terminal } from 'xterm'
|
||||
import { FitAddon } from 'xterm-addon-fit'
|
||||
import 'xterm/css/xterm.css'
|
||||
|
||||
const props = defineProps<{
|
||||
sessionId: string | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
input: [data: string]
|
||||
}>()
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const outputRef = ref<HTMLElement | null>(null)
|
||||
let terminal: Terminal | null = null
|
||||
let fitAddon: FitAddon | null = null
|
||||
|
||||
onMounted(() => {
|
||||
terminal = new Terminal({
|
||||
theme: { background: '#1e1e1e' },
|
||||
cursorBlink: true,
|
||||
})
|
||||
|
||||
fitAddon = new FitAddon()
|
||||
terminal.loadAddon(fitAddon)
|
||||
|
||||
terminal.open(outputRef.value!)
|
||||
fitAddon.fit()
|
||||
|
||||
// 用户输入
|
||||
terminal.onData((data) => {
|
||||
emit('input', data)
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
terminal?.dispose()
|
||||
})
|
||||
|
||||
function write(data: string) {
|
||||
terminal?.write(data)
|
||||
}
|
||||
|
||||
function clear() {
|
||||
terminal?.clear()
|
||||
}
|
||||
|
||||
defineExpose({ write, clear })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.terminal-display {
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.terminal-output {
|
||||
padding: 12px;
|
||||
min-height: 400px;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### 2.3 WebSocket 服务
|
||||
|
||||
**新文件**: `frontend/src/services/terminalWs.ts`
|
||||
|
||||
```typescript
|
||||
type MessageHandler = (msg: StreamMessage) => void
|
||||
|
||||
interface StreamMessage {
|
||||
type: 'output' | 'error' | 'status' | 'waiting_input' | 'complete'
|
||||
session_id: string
|
||||
data: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
class TerminalWsService {
|
||||
private ws: WebSocket | null = null
|
||||
private sessionId: string | null = null
|
||||
private handlers: MessageHandler[] = []
|
||||
private reconnectAttempts = 0
|
||||
private maxReconnectAttempts = 5
|
||||
|
||||
async connect(provider: string): Promise<string> {
|
||||
// 创建会话
|
||||
const response = await fetch('/api/code-commander/sessions', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ provider }),
|
||||
})
|
||||
const { session_id } = await response.json()
|
||||
|
||||
// 建立 WebSocket
|
||||
this.ws = new WebSocket(`ws://localhost:8000/ws/terminal/${session_id}`)
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
const msg: StreamMessage = JSON.parse(event.data)
|
||||
this.handlers.forEach((h) => h(msg))
|
||||
}
|
||||
|
||||
this.ws.onclose = () => {
|
||||
this.attemptReconnect()
|
||||
}
|
||||
|
||||
this.sessionId = session_id
|
||||
return session_id
|
||||
}
|
||||
|
||||
async sendTask(sessionId: string, prompt: string) {
|
||||
await fetch(`/api/code-commander/sessions/${sessionId}/task`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ prompt }),
|
||||
})
|
||||
}
|
||||
|
||||
sendInput(sessionId: string, input: string) {
|
||||
this.ws?.send(JSON.stringify({ type: 'input', data: input }))
|
||||
}
|
||||
|
||||
onMessage(handler: MessageHandler) {
|
||||
this.handlers.push(handler)
|
||||
}
|
||||
|
||||
removeHandler(handler: MessageHandler) {
|
||||
this.handlers = this.handlers.filter((h) => h !== handler)
|
||||
}
|
||||
|
||||
async disconnect(sessionId: string) {
|
||||
await fetch(`/api/code-commander/sessions/${sessionId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
this.ws?.close()
|
||||
this.ws = null
|
||||
this.sessionId = null
|
||||
}
|
||||
|
||||
private async attemptReconnect() {
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
return
|
||||
}
|
||||
this.reconnectAttempts++
|
||||
await new Promise((r) => setTimeout(r, 1000 * this.reconnectAttempts))
|
||||
// 重新连接
|
||||
}
|
||||
}
|
||||
|
||||
export const terminalWsService = new TerminalWsService()
|
||||
```
|
||||
|
||||
### 2.4 路由配置
|
||||
|
||||
**文件**: `frontend/src/router/index.ts`
|
||||
|
||||
```typescript
|
||||
{
|
||||
path: '/code-commander',
|
||||
name: 'CodeCommander',
|
||||
component: () => import('@/pages/chat/CodeCommander.vue'),
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心文件清单
|
||||
|
||||
| 文件 | 操作 | 说明 |
|
||||
|------|------|------|
|
||||
| `CodeCommander.vue` | 新增 | 主页面组件 |
|
||||
| `TerminalDisplay.vue` | 新增 | 终端显示组件(xterm.js) |
|
||||
| `terminalWs.ts` | 新增 | WebSocket 服务 |
|
||||
| `router/index.ts` | 修改 | 新增路由 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 验收标准
|
||||
|
||||
- [ ] 用户可以选择 AI 提供商
|
||||
- [ ] 可以输入任务描述并执行
|
||||
- [ ] 终端实时显示 AI 输出
|
||||
- [ ] 用户可以输入交互(如 "y")
|
||||
- [ ] 可以下载和清理文件
|
||||
|
||||
---
|
||||
|
||||
## 5. 依赖
|
||||
|
||||
| 依赖 | 版本 | 用途 |
|
||||
|------|------|------|
|
||||
| xterm | ^5.x | 终端渲染 |
|
||||
| xterm-addon-fit | ^0.8.x | 自适应大小 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 依赖关系
|
||||
|
||||
```
|
||||
Phase 4(流式交互)
|
||||
↓
|
||||
本阶段(前端集成)→ 端到端测试
|
||||
```
|
||||
Reference in New Issue
Block a user