# 开发参考 API、SSE、扩展方式和框架约束。运行、调试、打包与部署见 [项目 README](../README.md)。以下源码命令均在项目根目录执行,使用项目虚拟环境。 ## API 主入口与 1.5 一致,额外新增了若干辅助接口。 ### 对话 | 方法 | 路径 | 说明 | |---|---|---| | POST | `/api/chat` | 主入口,SSE 流式 | | POST | `/api/chat/resume` | 恢复被中断挂起的对话 | | GET | `/api/chat/interrupt` | 查挂起的中断(刷新页面补渲染卡片);有则 200,无则 204 | | GET | `/api/chat/history/{session_id}` | 会话历史 | 不提供 `/api/chat/only`,统一使用 `/api/chat`。 `POST /api/chat` 请求体是 `multipart/form-data`: | 字段 | 必填 | 说明 | |---|---|---| | `message` | 是 | 用户消息,支持 `#skill:` 与 `#session:` 前缀 | | `session_id` | 否 | 缺省则新建会话 | | `thread_id` | 否 | **必须等于 `session_id`**,否则 404 | | `mode` | 否 | `plan`(先出计划待批)或 `full`(直接执行) | | `file` | 否 | 附件,落到 `/workspace/uploads/` | | `request_id` | 否 | 客户端生成的请求唯一键;同一用户重复提交返回 409 和原 `session_id`、`run_id` | | `user_name` | 否 | 归属用户,缺省 `default`(**2.0 新增,替代 1.5 的鉴权依赖**) | 响应是 `text/event-stream`,头含 `X-Session-Id`、`X-Trace-Id` 与 `X-Run-Id`。 ### 会话与用量 | 方法 | 路径 | 说明 | |---|---|---| | GET | `/api/sessions` | 会话列表(置顶优先) | | GET | `/api/sessions/search?q=` | 标题模糊搜索 | | DELETE | `/api/sessions/{id}` | 删除会话(**同时清图状态**) | | DELETE | `/api/messages/{id}` | 删除单条消息 | | GET/PUT | `/api/sessions/{id}/mode` | 权限模式 | | PUT | `/api/sessions/{id}/pin` · `/title` | 置顶 · 重命名 | | GET | `/api/sessions/{id}/runs` | 最近 50 次执行的状态与错误 | | GET | `/api/sessions/{id}/context` | 上下文用量明细与压缩存档索引 | | GET | `/api/usage/today` | 当日用量与额度 | | GET | `/api/usage/recent` | 最近用量流水 | | PUT | `/api/usage/quota` | 调整每日额度(0 = 不限) | | GET | `/api/usage/pricing` | 模型单价表 | | GET | `/api/preferences` · PUT · DELETE | 用户偏好(每轮自动注入) | ### 模型与工具 | 方法 | 路径 | 说明 | |---|---|---| | GET | `/api/model` | 模型配置与 agent 组装摘要 | | GET | `/api/model/tools` | 工具清单(按分组) | | POST | `/api/model/thinking` | 切换思考级别(触发 agent 重建) | | POST | `/api/model/reload` | 强制重建 agent | ### 定时任务 | 方法 | 路径 | 说明 | |---|---|---| | GET/POST | `/api/schedules` | 列表 / 新建 | | GET/PUT/DELETE | `/api/schedules/{id}` | 详情 / 更新 / 删除 | | POST | `/api/schedules/{id}/pause` · `/resume` · `/run` | 暂停 / 恢复 / 立即执行 | | GET | `/api/schedules/{id}/runs` | 执行记录 | ### 运维 | 方法 | 路径 | 说明 | |---|---|---| | GET | `/health` | 逐子系统健康状态 | | GET | `/api/health/billing` | 计费自检(单价是否覆盖当前模型) | | GET | `/` | 重定向到测试页 | | GET | `/static/index.html` | 极简测试页 | --- ## SSE 事件契约 14 种业务事件 + 1 种通用错误。**字段名是从 1.5 前端逐行反查出来的**, 不是猜的。 | 事件 | 前端读取的字段 | |---|---| | `agent_start` | `agent` · `cn_agent` | | `thinking` | `message` · `cn_agent` | | `thinking_token` | `content` | | `executing` | `message` · `cn_agent` · `tools[]` · `cn_tools[]` | | `tool_call` | `tool` · `cn_tool` · `args` · `agent_id` | | `tool_result` | `tool` · `cn_tool` · `content` · `error` · `agent_id` | | `token` | `content` | | `updated_todo_list` | `todos` | | `progress` | `message` | | `interrupt` | `tool_approval{tool,args,reason,side_effect}` 或 `ask_user{questions[]}` 或 `message` | | `citations` | `items[]` · `model_only` | | `agent_done` | `agent` · `cn_agent` · `duration_ms` · `progress` · `context_tokens` | | `agent_error` | `agent` · `cn_agent` · `message` · `duration_ms` | | `done` | `duration_ms` · `usage{}` | | `error` | `message` · `code` · `retry_after` | 帧格式:`data: {json}\n\n`,每帧恒带 `session_id`,绑定 trace 时带 `trace_id`。 HTTP 错误语义:额度超限 429;并发超限 429(带 `retry_after`);上传 400/413; 会话不存在或越权 404;恢复时无挂起中断 409。 **契约的唯一出口是 `api/sse_events.py`**,其他任何地方都不得手拼 `data: ` 字符串。改字段必须同步改 `tests/test_sse_contract.py`。 --- ## 四项核心能力(相对 1.5 的做法改进) 这是 2.0 与 1.5 的主要差异所在。 ### 子智能体跟踪 | | 做法 | |---|---| | 1.5 | 从子图命名空间字符串 `tools:` **反查** `subagent_type`;id 被上下文压缩裁掉后反查落空,子智能体身份丢失 | | 2.0 | 收到 task 调用的**那一刻**就建 `tool_call_id → {类型, 描述}` 表,之后只按 id 查表;表由跟踪器持有,不受历史窗口影响 | 见 `api/subagent_tracker.py`。 ### 上下文计量 | | 做法 | |---|---| | 1.5 | tiktoken **本地估算**优先,为此专门开了 `fix/context-usage-accounting` 分支修口径 | | 2.0 | **真实 usage 优先**(`usage_metadata`),provider 不给才回落估算并标 `usage_source=estimate` | 两个量刻意分开:`input_tokens` 累加值是计费口径,`context_tokens`(最近一次 调用的 input)是当前上下文占用。 见 `api/context_meter.py`。 ### 自动压缩 | | 做法 | |---|---| | 1.5 | 自研 `auto_compact.py`,默认**关闭**,未被信任到敢开 | | 2.0 | 直接用 deepagents 的 `SummarizationMiddleware`:85% 窗口触发,**把被淘汰的历史追加落盘**到 `/conversation_history/{thread_id}.md` 并把路径嵌进摘要,agent 能自己读回来 | 调节旋钮是 `CONTEXT_LIMIT`(声明窗口),不是阈值比例——阈值由框架固定为 85%。压缩存档位置可在 `GET /api/sessions/{id}/context` 的 `archives` 里查。 ### 来源标注 | | 做法 | |---|---| | 1.5 | 靠提示词让**模型自觉**在正文写引用,再用启发式规则去正文里刮 | | 2.0 | **工具侧结构化产出**:工具返回 `sourced(data, sources)`,SSE 层从 ToolMessage 抽取、聚合、去重 | 模型漏写和编造这两种失败模式,在读工具返回值的路径上根本不存在。 见 `api/source_tracker.py` 与 `tools/registry.py`。 --- ## 扩展方式 ### 加一个业务工具 ```python # tools/my_tools.py from tools.registry import register, source, sourced @register(group="我的分组", label="中文显示名", summary="一句话说明") def my_tool(query: str, limit: int = 5) -> dict: """做某事。 Args: query: 查询词。 limit: 最多返回几条。 Returns: 含 data 与 sources 的结构;sources 会被渲染成引用标注。 """ return sourced( data=[...], sources=[source("标题", "https://...", "摘要", score=0.9)], ) ``` 然后把模块名加进 `tools/registry.py` 的 `_BUILTIN_MODULES`。工具名全局唯一, 重名会在启动时抛错。 **docstring 是给模型看的**:第一行说清做什么,正文说清何时用、参数含义、 返回什么。模型选错工具多半是因为 docstring 写得含糊。 ### 加一个技能 在 `skills//SKILL.md` 写带 frontmatter 的操作手册: ```markdown --- name: my-skill description: 什么时候该用这个技能(这段会被模型看到,要写具体) --- # 标题 ## 步骤 1. ... ``` agent 启动时只读 frontmatter,按需才 `read_file` 读全文(渐进披露)。 用户用 `#skill:my-skill` 前缀显式指定。 ### 加一个子智能体 改 `agents/agent_factory.py` 的 `_build_subagents()`。注意 `tools` 必须是 `BaseTool` 列表(用 `registry.get_tools_by_name(...)`),`system_prompt` 不继承父级。 --- ## 配置 全部配置项见 `.env.example`,每项都有默认值。几个容易搞错的: | 变量 | 说明 | |---|---| | `DEEP_AGENT_ENV_PRIORITY` | `.env` 与进程环境谁优先。`dotenv`(默认)/ `process` / `none`。**跑变体实例用 `process`**:`DEEP_AGENT_ENV_PRIORITY=process DB_DIALECT=mysql PORT=8081 python main.py` | | `DB_DIALECT` | `mysql`(生产)/ `sqlite`(本地开发与测试,无需数据库服务) | | `DB_NAME` | MySQL 库名;库不存在时启动会自动建库,不需要手工 `CREATE DATABASE` | | `CONTEXT_LIMIT` | 声明的上下文窗口,**压缩的唯一调节旋钮**。0 = 用模型登记值 | | `CTX_COMPACT_ENABLED` | 关掉会同时失去历史落盘能力,长会话会超窗报错 | | `CHECKPOINTER` | `sqlite`(默认,落盘)/ `memory`(重启丢图状态) | | `HITL_TOOLS` | 追加需要人工审批的工具名,逗号分隔。如 `calculate` 便于自测中断闭环 | | `BILLING_ALLOW_UNPRICED` | 模型未定价时是否按 0 计费。**默认 0 = 直接报错**(fail-closed) | | `TFAGENTS_DAILY_TOKEN_LIMIT` | 每用户每日 token 限额,0 = 不限 | **配置冲突会被大声报出来**:若 `.env` 与进程环境里有同名但不同的值,启动 日志会打 WARNING(含脱敏后的生效值),`/health` 的 `config_conflicts` 也能 看到。这解决的是一个很费时间的现象:改了配置却不生效,却没有任何提示。 --- ## 验证 ```bash # 离线单元 + SSE 契约测试(不需要密钥,不需要数据库服务) python -m pytest # 结构自检:导入、建表、agent 编译、工具注册 python scripts/selfcheck.py # HTTP 层端到端(需服务已启动 + 可用密钥) python scripts/http_e2e.py ``` `http_e2e.py` 覆盖:SSE 帧格式与响应头、落库与顺序、错误语义 (404/400/409)、**中断/恢复闭环**、定时任务的手动触发与 cron 校验。 --- ## 目录结构 ``` main.py FastAPI 入口(lifespan / 健康检查 / 静态挂载) config.py 配置唯一入口(.env 读取与冲突检测) api/ chat · sse_core · session · model · usage · schedule · concurrency sse_events.py ★ SSE 契约唯一事实源 sse_core.py ★ 流式内核(读流 → 派事件 → 收尾) subagent_tracker.py 子智能体生命周期跟踪 context_meter.py 上下文计量(真实 usage 优先) source_tracker.py 来源标注抽取与聚合 agents/ agent_factory · agent_registry · model_factory · prompts · middleware · agent_memory tools/ registry(工具唯一事实源)· demo_tools(示例工具) skills/demo-skill/ 示例技能 db/ db_conn(双方言)· mysql_schema.sql · schema_sqlite.sql · 三个 store billing/ pricing · guard(预检)· meter(入账) scheduler/ scheduler(APScheduler)· runner(独立会话执行) obs/ log_setup · tracing(trace_id) static/index.html 极简测试页 tests/ pytest(离线单元、HTTP 与真实图引擎回归) scripts/ selfcheck.py · http_e2e.py · start_server.sh ``` 标 ★ 的两个文件是核心,改动时同步检查 SSE 契约与流式执行测试。 --- ## 执行可靠性与隔离 - **统一准入**:聊天、恢复和定时执行共用额度预检、并发闸、会话互斥、总超时、计量与收尾。并发满返回 429;同会话冲突返回 409。 - **运行状态**:`execution_runs` 保存 `running / waiting_input / success / failed / cancelled / interrupted`。中断只表示等待输入,不发送完成事件;启动时将崩溃遗留的 running 标记为 interrupted。 - **请求去重**:聊天可提交 `request_id`(1–128 字符)。相同用户的相同键不会再次运行,返回 409 和原会话、运行 ID;不重放旧 SSE。新任务应使用新键。 - **取消收尾**:断连时停止消费图,保存已收到的部分回答、结算已观测用量,并释放占位。定时任务停机时等待取消收尾后再关闭 checkpoint。 - **持久化结算**:同一流水使用稳定 ID,写账与累计额度在事务中完成。待结算数据保存在 `billing_pending`,服务启动后及每分钟重试。数据库完全不可写时仍只能日志告警;进程被强杀前尚未观测到的 provider 用量需要外部账单对账。 - **文件隔离**:虚拟 `/workspace/`、`/conversation_history/` 映射到 `data/workspaces/<用户哈希>/<会话哈希>/`。子智能体共享其父会话目录,其他会话不可见;`/skills/` 共享只读。 - **定时任务**:采用同一执行流程,注入模式和用户偏好;cron 与手动触发共用计划互斥,等待审批的运行记录为 `waiting_input`,从聊天恢复后同步结果状态。cron 显式使用 Asia/Shanghai。 ### Agent 人机交互 - `ask_user(questions)`:提出一至三个必要问题并暂停,前端显示输入框;刷新页面可恢复问题卡。 - `request_plan_approval(plan)`:提交计划等待确认。plan 模式提示词会准确指向这个工具。 - 批量工具审批卡完整展示每项工具及参数,批准按钮明确写出批次数量。 - 审批卡带 `interrupt_token`;恢复接口必须原样携带。服务端在会话锁内核对当前中断,过期、缺失和重复确认返回 409。 - 业务工具可在 `@register(..., requires_approval=True)` 声明强制审批,组装时自动进入框架 HITL。plan 提示词本身不构成所有工具的硬权限边界,真实写操作应声明强制审批。 - 子智能体和摘要模型的用量计入总费用,其内部正文不混入主回答,也不覆盖主线程上下文占用。 ### 升级已有部署 启动自动增建 `execution_runs`、`billing_pending`、`request_keys`,不会删除旧表。 同步发布 `static/app.js`;自定义客户端需补传 `interrupt_token`。定时任务的详情、修改、删除、运行接口需携带其归属 `user_name`。 旧的共享 `workspace/`、`conversation_history/` 原样保留,**不会自动猜测文件归属或复制到新目录**。 旧会话需要的附件可重新上传;需要迁移的历史文件应先确认用户和会话归属。 未登记的新模型现在会在执行前拒绝请求,不再自动写入零价;既有单价记录不会被覆盖。 ## 已知缺口 **当前只适合内网或开发环境**,主要因为: 1. **没有鉴权** —— `user_name` 由客户端自报,可伪造。计费与会话归属都建立在 这个值上。需要为所有路由接入统一身份依赖,并区分普通用户与管理操作。 2. **单进程** —— SQLite checkpointer、并发计数、调度器都是进程内的。多 worker 会导致会话状态分裂、定时任务重复触发。 3. **并发计数不跨进程** —— 多进程部署需挪到 Redis/MySQL。 尚未提供业务工具/技能、MCP 接入及 `remember`;默认标题使用文本截断。