From 09654b7d4b1e5d4e5b3628c31c35a17919ee28d3 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Sat, 29 Nov 2025 23:13:11 +0800 Subject: [PATCH] fix: stabilize conversation loading --- CLAUDE.md | 228 ++++++++++++++++++++++ static/src/app.ts | 46 +++-- static/src/composables/useLegacySocket.ts | 4 +- static/src/stores/chat.ts | 9 + static/terminal.html | 9 +- 5 files changed, 263 insertions(+), 33 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1675512 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,228 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +多用户 AI Agent 系统,为每个登录用户提供独立的 Docker 容器环境。支持终端交互、文件操作、对话管理和实时监控。主要用于学习和实验"智能体 + 真实 Dev Workflow",代码大量由 AI 生成。 + +## Core Architecture + +### Entry Points +- **CLI模式**: `python main.py` - 启动命令行交互式 Agent +- **Web模式**: `python web_server.py` - 启动 Flask + Socket.IO Web 服务器 (默认端口 8091) + +### Key Components + +**core/** +- `MainTerminal` - CLI 主终端,处理用户输入和 AI 对话循环 +- `WebTerminal` - Web 终端,继承 MainTerminal,增加实时推送和对话持久化 +- `tool_config.py` - 定义所有工具的配置和分类 + +**modules/** +- `user_container_manager.py` - 单用户-单容器管理,负责容器生命周期 +- `container_file_proxy.py` - 容器内文件代理,通过 `docker exec` 沙箱化文件操作 +- `terminal_manager.py` - 管理多个持久化终端会话 +- `file_manager.py` - 文件 CRUD,包含搜索、替换和 append/modify streaming 功能 +- `conversation_manager.py` - 对话历史持久化 +- `context_manager.py` - 上下文构建、token统计、对话管理 +- `memory_manager.py` - 长期记忆管理 +- `todo_manager.py` - 待办事项管理 +- `sub_agent_manager.py` - 子智能体任务调度 +- `user_manager.py` - 用户认证和工作区管理 +- `upload_security.py` - 上传文件隔离扫描 + +**utils/** +- `api_client.py` - 与 OpenAI-compatible API 交互,支持 thinking mode(首次思考,后续快速) +- `terminal_factory.py` - 终端类型工厂 + +**static/** +- Vue + Socket.IO 前端,包含对话流、终端输出、资源监控面板 + +**docker/** +- `terminal.Dockerfile` - 终端容器镜像构建配置 + +## Development Commands + +### Build & Run +```bash +# 安装依赖(需要在项目根目录创建 requirements.txt) +pip install flask flask-socketio flask-cors openai python-dotenv tiktoken lxml + +# 构建终端镜像 +docker build -f docker/terminal.Dockerfile -t my-agent-shell:latest . + +# CLI 模式 +python main.py + +# Web 模式 +python web_server.py +# 或指定端口和思考模式 +python web_server.py --port 8091 --thinking-mode +``` + +### Testing & Debugging +```bash +# 查看容器状态 +docker ps -a | grep agent-term + +# 查看调试日志 +tail -f logs/debug_stream.log + +# 查看容器统计 +tail -f logs/container_stats.log +``` + +## Configuration + +主要配置文件:`.env`(复制 `.env.example`) + +**必填变量**: +- `AGENT_API_BASE_URL` - OpenAI-compatible API 端点 +- `AGENT_API_KEY` - API 密钥 +- `AGENT_MODEL_ID` - 模型 ID (例如 deepseek-chat) +- `WEB_SECRET_KEY` - Flask session 密钥 + +**容器配置** (`config/terminal.py`): +- `TERMINAL_SANDBOX_MODE` - docker/host +- `TERMINAL_SANDBOX_IMAGE` - 容器镜像 (默认 python:3.11-slim) +- `TERMINAL_SANDBOX_NETWORK` - 网络模式 (bridge/none/host) +- `TERMINAL_SANDBOX_CPUS` - CPU 限制 +- `TERMINAL_SANDBOX_MEMORY` - 内存限制 + +**资源限制**: +- `PROJECT_MAX_STORAGE_MB` - 单用户磁盘配额 (默认 2048MB) +- `MAX_ACTIVE_USER_CONTAINERS` - 并发容器数量 (默认 8) + +## Important Implementation Details + +### File Operations with Streaming + +**append_to_file** 和 **modify_file** 使用特殊的流式输出格式: + +``` +<<>> +文件内容... +<<>> + +<<>> +[replace:1] +<>原文内容<> +<>新内容<> +[/replace] +<<>> +``` + +处理逻辑在 `web_server.py` 的 `handle_task_with_sender` 中,通过检测标记并在流式输出中即时执行。 + +### Container Architecture + +每个用户登录时: +1. `UserContainerManager` 创建专属容器 +2. 容器挂载 `users//project/` 到 `/workspace` +3. 所有文件操作通过 `ContainerFileProxy` 在容器内执行 +4. 终端会话通过 `docker exec` 运行 +5. 空闲超时或登出后自动销毁容器 + +### Context Management + +`ContextManager` 负责: +- 动态构建上下文(系统提示 + 工具 + 对话历史 + 聚焦文件 + 记忆) +- Token 统计和限制检查 +- 对话历史保存和加载 +- 自动保存机制(每次 API 调用后增量保存) + +### Thinking Mode + +支持"思考模式"(需配置 `AGENT_THINKING_MODEL_ID`): +- 首次调用使用 reasoning 模型(如 deepseek-reasoner) +- 后续调用使用快速模型 +- 可通过 `THINKING_FAST_INTERVAL` 控制切换频率 +- 失败时自动回退到思考模式 + +### Web Socket Events + +主要事件: +- `send_message` - 发送用户消息 +- `thinking_start/chunk/end` - 思考过程 +- `text_start/chunk/end` - 文本响应 +- `tool_preparing/start/update_action` - 工具执行状态 +- `conversation_list_update` - 对话列表更新 +- `terminal_output` - 终端输出 + +## Common Patterns + +### Adding New Tools + +1. 在 `MainTerminal` 添加 `handle_` 方法 +2. 在 `core/tool_config.py` 的 `define_tools()` 中注册工具 +3. 如果需要在流式输出中处理,在 `web_server.py` 添加特殊逻辑 + +### Working with Containers + +使用 `ContainerHandle`: +```python +# 文件操作 +result = container_handle.execute_file_op("read", path="/workspace/file.txt") + +# 执行命令 +result = container_handle.run_command("ls -la /workspace") + +# 终端会话 +result = container_handle.start_terminal(session_name="main", working_dir="/workspace") +``` + +### Managing Conversations + +```python +# 创建新对话 +terminal.create_new_conversation(thinking_mode=True) + +# 加载对话 +terminal.load_conversation(conversation_id) + +# 搜索对话 +terminal.search_conversations(query="bug fix") + +# 删除对话 +terminal.delete_conversation(conversation_id) +``` + +## Security Considerations + +- 所有用户操作在独立容器中执行,与宿主机隔离 +- 文件操作路径验证在 `FileManager._validate_path` +- 上传文件经过 `UploadQuarantineManager` 隔离和扫描 +- CSRF 保护在 Web 端启用 +- 登录失败次数限制和临时锁定 + +## Known Limitations + +- 代码大量由 AI 生成,未达到生产级别 +- 容器资源配额依赖 Docker cgroups +- 前端处于改造中,部分功能可能不稳定 +- Token 统计基于 tiktoken,可能与实际 API 有偏差 + +## Directory Structure + +``` +agents/ +├── core/ # 核心终端和工具配置 +├── modules/ # 独立功能模块 +├── utils/ # 辅助工具 +├── config/ # 配置文件 +├── static/ # Web 前端 +├── docker/ # 容器镜像 +├── prompts/ # 系统提示词 +├── users/ # 用户工作区 +├── data/ # 全局数据 +└── logs/ # 日志文件 +``` + +## Development Tips + +- 修改系统提示词:编辑 `prompts/main_system.txt` +- 调整工具限制:修改 `config/limits.py` +- 容器镜像定制:编辑 `docker/terminal.Dockerfile` 并重新构建 +- 前端开发:`npm install && npm run dev` (在 `static/` 目录) +- 查看详细执行流程:监控 `logs/debug_stream.log` diff --git a/static/src/app.ts b/static/src/app.ts index 2df258b..cb4719f 100644 --- a/static/src/app.ts +++ b/static/src/app.ts @@ -1003,20 +1003,22 @@ const appOptions = { if (!this.currentConversationId) { this.skipConversationHistoryReload = true; this.currentConversationId = statusConversationId; - } - // 如果有当前对话,尝试获取标题和Token统计 - try { - const convResponse = await fetch(`/api/conversations/current`); - const convData = await convResponse.json(); - if (convData.success && convData.data) { - this.currentConversationTitle = convData.data.title; + + // 如果有当前对话,尝试获取标题和历史 + try { + const convResponse = await fetch(`/api/conversations/current`); + const convData = await convResponse.json(); + if (convData.success && convData.data) { + this.currentConversationTitle = convData.data.title; + } + // 初始化时调用一次,因为 skipConversationHistoryReload 会阻止 watch 触发 + await this.fetchAndDisplayHistory(); + // 获取当前对话的Token统计 + this.fetchConversationTokenStatistics(); + this.updateCurrentContextTokens(); + } catch (e) { + console.warn('获取当前对话标题失败:', e); } - await this.fetchAndDisplayHistory(); - // 获取当前对话的Token统计 - this.fetchConversationTokenStatistics(); - this.updateCurrentContextTokens(); - } catch (e) { - console.warn('获取当前对话标题失败:', e); } } @@ -1166,17 +1168,13 @@ const appOptions = { this.resetAllStates(`loadConversation:${conversationId}`); this.subAgentFetch(); this.fetchTodoList(); - - // 4. 延迟获取并显示历史对话内容(关键功能) - setTimeout(() => { - this.fetchAndDisplayHistory(); - }, 300); - - // 5. 获取Token统计(重点:加载历史累计统计+当前上下文) - setTimeout(() => { - this.fetchConversationTokenStatistics(); - this.updateCurrentContextTokens(); - }, 500); + + // 4. 历史对话内容和Token统计由后端的 conversation_loaded 事件触发 + // 不在此处重复调用,避免双重加载 + // Socket.IO 的 conversation_loaded 事件会处理: + // - fetchAndDisplayHistory() + // - fetchConversationTokenStatistics() + // - updateCurrentContextTokens() } else { console.error('对话加载失败:', result.message); diff --git a/static/src/composables/useLegacySocket.ts b/static/src/composables/useLegacySocket.ts index f9ebb77..508ff33 100644 --- a/static/src/composables/useLegacySocket.ts +++ b/static/src/composables/useLegacySocket.ts @@ -242,7 +242,8 @@ export async function initializeLegacySocket(ctx: any) { ctx.chatCompleteTextAction(finalText || ''); completeActiveTextAction(finalText || ''); ctx.$forceUpdate(); - ctx.streamingMessage = false; + // 注意:不在这里重置 streamingMessage,因为可能还有工具调用在进行 + // streamingMessage 只在 task_complete 事件时重置 logStreamingDebug('finalizeStreamingText:complete', snapshotStreamingState()); streamingState.activeMessageIndex = null; streamingState.activeTextAction = null; @@ -604,6 +605,7 @@ export async function initializeLegacySocket(ctx: any) { typeof ctx.currentMessageIndex === 'number' ? ctx.currentMessageIndex : null; streamingState.activeTextAction = null; ctx.stopRequested = false; + ctx.streamingMessage = true; // 确保设置为流式状态 ctx.chatEnableAutoScroll(); ctx.scrollToBottom(); }); diff --git a/static/src/stores/chat.ts b/static/src/stores/chat.ts index 6407a4e..6f2bc31 100644 --- a/static/src/stores/chat.ts +++ b/static/src/stores/chat.ts @@ -189,6 +189,9 @@ export const useChatStore = defineStore('chat', { }, startTextAction() { const msg = this.ensureAssistantMessage(); + if (!msg) { + return null; + } msg.streamingText = ''; msg.currentStreamingType = 'text'; const action = { @@ -204,6 +207,12 @@ export const useChatStore = defineStore('chat', { appendTextChunk(content: string) { if (this.currentMessageIndex < 0) return null; const msg = this.messages[this.currentMessageIndex]; + if (!msg) { + return null; + } + if (typeof msg.streamingText !== 'string') { + msg.streamingText = ''; + } msg.streamingText += content; const lastAction = msg.actions[msg.actions.length - 1]; if (lastAction && lastAction.type === 'text') { diff --git a/static/terminal.html b/static/terminal.html index 1089012..3aaaf50 100644 --- a/static/terminal.html +++ b/static/terminal.html @@ -561,14 +561,7 @@ // 初始化WebSocket连接 async function initWebSocket() { - const usePollingOnly = window.location.hostname !== 'localhost' && - window.location.hostname !== '127.0.0.1'; - - const socketOptions = usePollingOnly ? { - transports: ['polling'], - upgrade: false, - autoConnect: false - } : { + const socketOptions = { transports: ['websocket', 'polling'], autoConnect: false };