fix: stabilize conversation loading
This commit is contained in:
parent
dd32db7677
commit
09654b7d4b
228
CLAUDE.md
Normal file
228
CLAUDE.md
Normal file
@ -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** 使用特殊的流式输出格式:
|
||||||
|
|
||||||
|
```
|
||||||
|
<<<APPEND:path/to/file>>>
|
||||||
|
文件内容...
|
||||||
|
<<<END_APPEND>>>
|
||||||
|
|
||||||
|
<<<MODIFY:path/to/file>>>
|
||||||
|
[replace:1]
|
||||||
|
<<OLD>>原文内容<<END>>
|
||||||
|
<<NEW>>新内容<<END>>
|
||||||
|
[/replace]
|
||||||
|
<<<END_MODIFY>>>
|
||||||
|
```
|
||||||
|
|
||||||
|
处理逻辑在 `web_server.py` 的 `handle_task_with_sender` 中,通过检测标记并在流式输出中即时执行。
|
||||||
|
|
||||||
|
### Container Architecture
|
||||||
|
|
||||||
|
每个用户登录时:
|
||||||
|
1. `UserContainerManager` 创建专属容器
|
||||||
|
2. 容器挂载 `users/<username>/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_<tool_name>` 方法
|
||||||
|
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`
|
||||||
@ -1003,14 +1003,15 @@ const appOptions = {
|
|||||||
if (!this.currentConversationId) {
|
if (!this.currentConversationId) {
|
||||||
this.skipConversationHistoryReload = true;
|
this.skipConversationHistoryReload = true;
|
||||||
this.currentConversationId = statusConversationId;
|
this.currentConversationId = statusConversationId;
|
||||||
}
|
|
||||||
// 如果有当前对话,尝试获取标题和Token统计
|
// 如果有当前对话,尝试获取标题和历史
|
||||||
try {
|
try {
|
||||||
const convResponse = await fetch(`/api/conversations/current`);
|
const convResponse = await fetch(`/api/conversations/current`);
|
||||||
const convData = await convResponse.json();
|
const convData = await convResponse.json();
|
||||||
if (convData.success && convData.data) {
|
if (convData.success && convData.data) {
|
||||||
this.currentConversationTitle = convData.data.title;
|
this.currentConversationTitle = convData.data.title;
|
||||||
}
|
}
|
||||||
|
// 初始化时调用一次,因为 skipConversationHistoryReload 会阻止 watch 触发
|
||||||
await this.fetchAndDisplayHistory();
|
await this.fetchAndDisplayHistory();
|
||||||
// 获取当前对话的Token统计
|
// 获取当前对话的Token统计
|
||||||
this.fetchConversationTokenStatistics();
|
this.fetchConversationTokenStatistics();
|
||||||
@ -1019,6 +1020,7 @@ const appOptions = {
|
|||||||
console.warn('获取当前对话标题失败:', e);
|
console.warn('获取当前对话标题失败:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await this.loadToolSettings(true);
|
await this.loadToolSettings(true);
|
||||||
|
|
||||||
@ -1167,16 +1169,12 @@ const appOptions = {
|
|||||||
this.subAgentFetch();
|
this.subAgentFetch();
|
||||||
this.fetchTodoList();
|
this.fetchTodoList();
|
||||||
|
|
||||||
// 4. 延迟获取并显示历史对话内容(关键功能)
|
// 4. 历史对话内容和Token统计由后端的 conversation_loaded 事件触发
|
||||||
setTimeout(() => {
|
// 不在此处重复调用,避免双重加载
|
||||||
this.fetchAndDisplayHistory();
|
// Socket.IO 的 conversation_loaded 事件会处理:
|
||||||
}, 300);
|
// - fetchAndDisplayHistory()
|
||||||
|
// - fetchConversationTokenStatistics()
|
||||||
// 5. 获取Token统计(重点:加载历史累计统计+当前上下文)
|
// - updateCurrentContextTokens()
|
||||||
setTimeout(() => {
|
|
||||||
this.fetchConversationTokenStatistics();
|
|
||||||
this.updateCurrentContextTokens();
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.error('对话加载失败:', result.message);
|
console.error('对话加载失败:', result.message);
|
||||||
|
|||||||
@ -242,7 +242,8 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
ctx.chatCompleteTextAction(finalText || '');
|
ctx.chatCompleteTextAction(finalText || '');
|
||||||
completeActiveTextAction(finalText || '');
|
completeActiveTextAction(finalText || '');
|
||||||
ctx.$forceUpdate();
|
ctx.$forceUpdate();
|
||||||
ctx.streamingMessage = false;
|
// 注意:不在这里重置 streamingMessage,因为可能还有工具调用在进行
|
||||||
|
// streamingMessage 只在 task_complete 事件时重置
|
||||||
logStreamingDebug('finalizeStreamingText:complete', snapshotStreamingState());
|
logStreamingDebug('finalizeStreamingText:complete', snapshotStreamingState());
|
||||||
streamingState.activeMessageIndex = null;
|
streamingState.activeMessageIndex = null;
|
||||||
streamingState.activeTextAction = null;
|
streamingState.activeTextAction = null;
|
||||||
@ -604,6 +605,7 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
typeof ctx.currentMessageIndex === 'number' ? ctx.currentMessageIndex : null;
|
typeof ctx.currentMessageIndex === 'number' ? ctx.currentMessageIndex : null;
|
||||||
streamingState.activeTextAction = null;
|
streamingState.activeTextAction = null;
|
||||||
ctx.stopRequested = false;
|
ctx.stopRequested = false;
|
||||||
|
ctx.streamingMessage = true; // 确保设置为流式状态
|
||||||
ctx.chatEnableAutoScroll();
|
ctx.chatEnableAutoScroll();
|
||||||
ctx.scrollToBottom();
|
ctx.scrollToBottom();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -189,6 +189,9 @@ export const useChatStore = defineStore('chat', {
|
|||||||
},
|
},
|
||||||
startTextAction() {
|
startTextAction() {
|
||||||
const msg = this.ensureAssistantMessage();
|
const msg = this.ensureAssistantMessage();
|
||||||
|
if (!msg) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
msg.streamingText = '';
|
msg.streamingText = '';
|
||||||
msg.currentStreamingType = 'text';
|
msg.currentStreamingType = 'text';
|
||||||
const action = {
|
const action = {
|
||||||
@ -204,6 +207,12 @@ export const useChatStore = defineStore('chat', {
|
|||||||
appendTextChunk(content: string) {
|
appendTextChunk(content: string) {
|
||||||
if (this.currentMessageIndex < 0) return null;
|
if (this.currentMessageIndex < 0) return null;
|
||||||
const msg = this.messages[this.currentMessageIndex];
|
const msg = this.messages[this.currentMessageIndex];
|
||||||
|
if (!msg) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (typeof msg.streamingText !== 'string') {
|
||||||
|
msg.streamingText = '';
|
||||||
|
}
|
||||||
msg.streamingText += content;
|
msg.streamingText += content;
|
||||||
const lastAction = msg.actions[msg.actions.length - 1];
|
const lastAction = msg.actions[msg.actions.length - 1];
|
||||||
if (lastAction && lastAction.type === 'text') {
|
if (lastAction && lastAction.type === 'text') {
|
||||||
|
|||||||
@ -561,14 +561,7 @@
|
|||||||
|
|
||||||
// 初始化WebSocket连接
|
// 初始化WebSocket连接
|
||||||
async function initWebSocket() {
|
async function initWebSocket() {
|
||||||
const usePollingOnly = window.location.hostname !== 'localhost' &&
|
const socketOptions = {
|
||||||
window.location.hostname !== '127.0.0.1';
|
|
||||||
|
|
||||||
const socketOptions = usePollingOnly ? {
|
|
||||||
transports: ['polling'],
|
|
||||||
upgrade: false,
|
|
||||||
autoConnect: false
|
|
||||||
} : {
|
|
||||||
transports: ['websocket', 'polling'],
|
transports: ['websocket', 'polling'],
|
||||||
autoConnect: false
|
autoConnect: false
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user