# NanoClaw 后端设计文档 ## 架构概览 ```mermaid graph TB subgraph Frontend[前端] UI[Vue 3 UI] end subgraph Backend[后端] API[Flask Routes] SVC[Services] TOOLS[Tool System] DB[(Database)] end subgraph External[外部服务] LLM[LLM API] WEB[Web Resources] end UI -->|REST/SSE| API API --> SVC API --> TOOLS SVC --> LLM TOOLS --> WEB SVC --> DB TOOLS --> DB ``` --- ## 项目结构 ``` backend/ ├── __init__.py # 应用工厂,数据库初始化 ├── models.py # SQLAlchemy 模型 ├── run.py # 入口文件 ├── config.py # 配置加载器 │ ├── routes/ # API 路由 │ ├── __init__.py │ ├── conversations.py # 会话 CRUD │ ├── messages.py # 消息 CRUD + 聊天 │ ├── models.py # 模型列表 │ ├── projects.py # 项目管理 │ ├── stats.py # Token 统计 │ └── tools.py # 工具列表 │ ├── services/ # 业务逻辑 │ ├── __init__.py │ ├── chat.py # 聊天补全服务 │ └── glm_client.py # GLM API 客户端 │ ├── tools/ # 工具系统 │ ├── __init__.py │ ├── core.py # 核心类 │ ├── factory.py # 工具装饰器 │ ├── executor.py # 工具执行器 │ ├── services.py # 辅助服务 │ └── builtin/ # 内置工具 │ ├── crawler.py # 网页搜索、抓取 │ ├── data.py # 计算器、文本、JSON │ ├── weather.py # 天气查询 │ ├── file_ops.py # 文件操作(需要 project_id) │ └── code.py # 代码执行 │ ├── utils/ # 辅助函数 │ ├── __init__.py │ ├── helpers.py # 通用函数 │ └── workspace.py # 工作目录工具 │ └── migrations/ # 数据库迁移 └── add_project_support.py ``` --- ## 类图 ### 核心数据模型 ```mermaid classDiagram direction TB class User { +Integer id +String username +String password +String phone +relationship conversations +relationship projects } class Project { +String id +Integer user_id +String name +String path +String description +DateTime created_at +DateTime updated_at +relationship conversations } class Conversation { +String id +Integer user_id +String project_id +String title +String model +String system_prompt +Float temperature +Integer max_tokens +Boolean thinking_enabled +DateTime created_at +DateTime updated_at +relationship messages } class Message { +String id +String conversation_id +String role +LongText content +Integer token_count +DateTime created_at } class TokenUsage { +Integer id +Integer user_id +Date date +String model +Integer prompt_tokens +Integer completion_tokens +Integer total_tokens +DateTime created_at } User "1" --> "*" Conversation : 拥有 User "1" --> "*" Project : 拥有 Project "1" --> "*" Conversation : 包含 Conversation "1" --> "*" Message : 包含 User "1" --> "*" TokenUsage : 消耗 ``` ### Message Content JSON 结构 `content` 字段统一使用 JSON 格式存储: **User 消息:** ```json { "text": "用户输入的文本内容", "attachments": [ {"name": "utils.py", "extension": "py", "content": "def hello()..."} ] } ``` **Assistant 消息:** ```json { "text": "AI 回复的文本内容", "thinking": "思考过程(可选)", "tool_calls": [ { "id": "call_xxx", "type": "function", "function": { "name": "file_read", "arguments": "{\"path\": \"...\"}" }, "result": "{\"content\": \"...\"}", "success": true, "skipped": false, "execution_time": 0.5 } ] } ``` ### 服务层 ```mermaid classDiagram direction TB class ChatService { -GLMClient glm_client -ToolExecutor executor +Integer MAX_ITERATIONS +sync_response(conv, tools_enabled, project_id) Response +stream_response(conv, tools_enabled, project_id) Response -_build_tool_calls_json(calls, results) list -_message_to_dict(msg) dict -_process_tool_calls_delta(delta, list) list } class GLMClient { -str api_url -str api_key +call(model, messages, kwargs) Response } class ToolExecutor { -ToolRegistry registry -dict _cache -list _call_history +process_tool_calls(calls, context) list +build_request(messages, model, tools) dict +clear_history() void } ChatService --> GLMClient : 使用 ChatService --> ToolExecutor : 使用 ``` ### 工具系统 ```mermaid classDiagram direction TB class ToolDefinition { <> +str name +str description +dict parameters +Callable handler +str category +to_openai_format() dict } class ToolRegistry { -dict _tools +register(ToolDefinition) void +get(str name) ToolDefinition? +list_all() list~dict~ +list_by_category(str) list~dict~ +execute(str name, dict args) dict +remove(str name) bool +has(str name) bool } class ToolExecutor { -ToolRegistry registry -dict _cache -list _call_history +process_tool_calls(list, dict) list +clear_history() void } class ToolResult { <> +bool success +Any data +str? error +to_dict() dict +ok(Any)$ ToolResult +fail(str)$ ToolResult } ToolRegistry "1" --> "*" ToolDefinition : 管理 ToolExecutor "1" --> "1" ToolRegistry : 使用 ToolDefinition ..> ToolResult : 返回 ``` --- ## 工作目录系统 ### 概述 工作目录系统为文件操作工具提供安全隔离,确保所有文件操作都在项目目录内执行。 ### 核心函数 ```python # backend/utils/workspace.py def get_workspace_root() -> Path: """获取工作区根目录""" def get_project_path(project_id: str, project_path: str) -> Path: """获取项目绝对路径""" def validate_path_in_project(path: str, project_dir: Path) -> Path: """验证路径在项目目录内(核心安全函数)""" def create_project_directory(name: str, user_id: int) -> tuple: """创建项目目录""" def delete_project_directory(project_path: str) -> bool: """删除项目目录""" def copy_folder_to_project(source_path: str, project_dir: Path, project_name: str) -> dict: """复制文件夹到项目目录""" ``` ### 安全机制 `validate_path_in_project()` 是核心安全函数: ```python def validate_path_in_project(path: str, project_dir: Path) -> Path: p = Path(path) # 相对路径转换为绝对路径 if not p.is_absolute(): p = project_dir / p p = p.resolve() # 安全检查:确保路径在项目目录内 try: p.relative_to(project_dir.resolve()) except ValueError: raise ValueError(f"Path '{path}' is outside project directory") return p ``` 即使传入恶意路径,后端也会拒绝: ```python "../../../etc/passwd" # 尝试跳出项目目录 -> ValueError "/etc/passwd" # 绝对路径攻击 -> ValueError ``` ### project_id 自动注入 工具执行器自动为文件工具注入 `project_id`: ```python # backend/tools/executor.py def process_tool_calls(self, tool_calls, context=None): for call in tool_calls: name = call["function"]["name"] args = json.loads(call["function"]["arguments"]) # 自动注入 project_id if context and name.startswith("file_") and "project_id" in context: args["project_id"] = context["project_id"] result = self.registry.execute(name, args) ``` --- ## API 总览 ### 会话管理 | 方法 | 路径 | 说明 | |------|------|------| | `POST` | `/api/conversations` | 创建会话 | | `GET` | `/api/conversations` | 获取会话列表(游标分页) | | `GET` | `/api/conversations/:id` | 获取会话详情 | | `PATCH` | `/api/conversations/:id` | 更新会话 | | `DELETE` | `/api/conversations/:id` | 删除会话 | ### 消息管理 | 方法 | 路径 | 说明 | |------|------|------| | `GET` | `/api/conversations/:id/messages` | 获取消息列表(游标分页) | | `POST` | `/api/conversations/:id/messages` | 发送消息(支持 SSE 流式) | | `DELETE` | `/api/conversations/:id/messages/:mid` | 删除消息 | | `POST` | `/api/conversations/:id/regenerate/:mid` | 重新生成消息 | ### 项目管理 | 方法 | 路径 | 说明 | |------|------|------| | `GET` | `/api/projects` | 获取项目列表 | | `POST` | `/api/projects` | 创建项目 | | `GET` | `/api/projects/:id` | 获取项目详情 | | `PUT` | `/api/projects/:id` | 更新项目 | | `DELETE` | `/api/projects/:id` | 删除项目 | | `POST` | `/api/projects/upload` | 上传文件夹作为项目 | | `GET` | `/api/projects/:id/files` | 列出项目文件 | ### 其他 | 方法 | 路径 | 说明 | |------|------|------| | `GET` | `/api/models` | 获取模型列表 | | `GET` | `/api/tools` | 获取工具列表 | | `GET` | `/api/stats/tokens` | Token 使用统计 | --- ## SSE 事件 | 事件 | 说明 | |------|------| | `thinking_start` | 新一轮思考开始,前端应清空之前的思考缓冲 | | `thinking` | 思维链增量内容(启用时) | | `message` | 回复内容的增量片段 | | `tool_calls` | 工具调用信息 | | `tool_result` | 工具执行结果 | | `process_step` | 处理步骤(按顺序:thinking/tool_call/tool_result),支持交替显示 | | `error` | 错误信息 | | `done` | 回复结束,携带 message_id 和 token_count | ### process_step 事件格式 ```json // 思考过程 {"index": 0, "type": "thinking", "content": "完整思考内容..."} // 工具调用 {"index": 1, "type": "tool_call", "id": "call_abc123", "name": "web_search", "arguments": "{\"query\": \"...\"}"} // 工具返回 {"index": 2, "type": "tool_result", "id": "call_abc123", "name": "web_search", "content": "{\"success\": true, ...}", "skipped": false} ``` 字段说明: - `index`: 步骤序号,确保按正确顺序显示 - `type`: 步骤类型(thinking/tool_call/tool_result) - `id`: 工具调用唯一标识,用于匹配工具调用和返回结果 - `name`: 工具名称 - `content`: 内容或结果 - `skipped`: 工具是否被跳过(失败后跳过) --- ## 数据模型 ### User(用户) | 字段 | 类型 | 说明 | |------|------|------| | `id` | Integer | 自增主键 | | `username` | String(50) | 用户名(唯一) | | `password` | String(255) | 密码(可为空,支持第三方登录) | | `phone` | String(20) | 手机号 | ### Project(项目) | 字段 | 类型 | 说明 | |------|------|------| | `id` | String(64) | UUID 主键 | | `user_id` | Integer | 外键关联 User | | `name` | String(255) | 项目名称(用户内唯一) | | `path` | String(512) | 相对路径(如 user_1/my_project) | | `description` | Text | 项目描述 | | `created_at` | DateTime | 创建时间 | | `updated_at` | DateTime | 更新时间 | ### Conversation(会话) | 字段 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `id` | String(64) | UUID | 主键 | | `user_id` | Integer | - | 外键关联 User | | `project_id` | String(64) | null | 外键关联 Project(可选) | | `title` | String(255) | "" | 会话标题 | | `model` | String(64) | "glm-5" | 模型名称 | | `system_prompt` | Text | "" | 系统提示词 | | `temperature` | Float | 1.0 | 采样温度 | | `max_tokens` | Integer | 65536 | 最大输出 token | | `thinking_enabled` | Boolean | False | 是否启用思维链 | | `created_at` | DateTime | now | 创建时间 | | `updated_at` | DateTime | now | 更新时间 | ### Message(消息) | 字段 | 类型 | 说明 | |------|------|------| | `id` | String(64) | UUID 主键 | | `conversation_id` | String(64) | 外键关联 Conversation | | `role` | String(16) | user/assistant/system/tool | | `content` | LongText | JSON 格式内容(见上方结构说明) | | `token_count` | Integer | Token 数量 | | `created_at` | DateTime | 创建时间 | ### TokenUsage(Token 使用统计) | 字段 | 类型 | 说明 | |------|------|------| | `id` | Integer | 自增主键 | | `user_id` | Integer | 外键关联 User | | `date` | Date | 统计日期 | | `model` | String(64) | 模型名称 | | `prompt_tokens` | Integer | 输入 token | | `completion_tokens` | Integer | 输出 token | | `total_tokens` | Integer | 总 token | | `created_at` | DateTime | 创建时间 | --- ## 分页机制 所有列表接口使用**游标分页**: ``` GET /api/conversations?limit=20&cursor=conv_abc123 ``` 响应: ```json { "code": 0, "data": { "items": [...], "next_cursor": "conv_def456", "has_more": true } } ``` - `limit`:每页数量(会话默认 20,消息默认 50,最大 100) - `cursor`:上一页最后一条的 ID --- ## 错误码 | Code | 说明 | |------|------| | `0` | 成功 | | `400` | 请求参数错误 | | `404` | 资源不存在 | | `500` | 服务器错误 | 错误响应: ```json { "code": 404, "message": "conversation not found" } ``` --- ## 配置文件 配置文件:`config.yml` ```yaml # 服务端口 backend_port: 3000 frontend_port: 4000 # LLM API api_key: your-api-key api_url: https://open.bigmodel.cn/api/paas/v4/chat/completions # 工作区根目录 workspace_root: ./workspaces # 数据库 db_type: mysql # mysql, sqlite, postgresql db_host: localhost db_port: 3306 db_user: root db_password: "" db_name: nano_claw db_sqlite_file: app.db # SQLite 时使用 ```