14 KiB
运行手册
怎么把 zcbot 跑起来。env / 常用命令 / 故障兜底。设计看
DESIGN.md,进度看PROGRESS.md。
最后更新:2026-05-18(POST /v1/tasks/{id}/runs/{rid}/cancel 协作式 cancel + cancelled SSE 事件 + dev SPA stop 按钮;gate 扩到 cancelling)
环境
- Python:虚拟环境
.venv/,所有依赖装在里面。一律用.venv/Scripts/python.exe ...(Windows)/.venv/bin/python ...(Unix),不要全局python(litellm/python-pptx 等会 ModuleNotFoundError)。 - 配置文件
.env(项目根,git 忽略,litellm 自动加载):DEEPSEEK_API_KEY=sk-... ZCBOT_DB_URL=postgresql://user:pass@host:5432/zcbot # cli.py web 必填(纯 CLI 用不到,只在起 web 时校验) PLATFORM_KEY=<至少 16 字符的随机串,platform 服务端 / dev 浏览器持有,登录时校验> JWT_SECRET=<≥32 字符随机串,HS256 签 session token;泄漏 = 任意伪造,与 PLATFORM_KEY 同级保护> # 可选:覆盖默认 7d # ZCBOT_JWT_TTL_SECONDS=604800litellm 在 import 时副作用加载 .env;CLI 入口直接走
cli.py,.env会自动生效。直跑python -c "from core.storage import ..."不经 litellm 链路时记得自己import litellm触发,或手动export ZCBOT_DB_URL=...。 - 依赖:
pip install -r requirements.txt(已在.venv里)。 - PG:
ZCBOT_DB_URL必填。本地 docker compose 起 / 远端 dev / 生产任选。未设置时启动会清晰报错,不引导 docker(§7.4)。 - Auth env(
cli.py web必填):PLATFORM_KEY+JWT_SECRET,任一缺失 web 启动会 fail-fast。生成随机串可用python -c "import secrets; print(secrets.token_urlsafe(48))"。CLI(chat / tasks / probe / db)不验,不要这两个 env 也能跑。
一次性初始化
# 1) 装依赖(若 .venv 不在)
python -m venv .venv
.venv/Scripts/python.exe -m pip install -r requirements.txt
# 2) 准备 .env(见上)
# 3) DB schema 上车
.venv/Scripts/python.exe cli.py db upgrade head
.venv/Scripts/python.exe cli.py db current # 应输出 0003 (head)
日常命令
聊天 / 任务
# 新建 task —— `--name` 必填(任务显示名),`--working-dir` 可选(目录名,留空 → 用 --name)
.venv/Scripts/python.exe cli.py chat --name "初稿大纲" --working-dir proposal_v3
# 只给 name → working_dir fallback 用 name
.venv/Scripts/python.exe cli.py chat --name proposal_v3
# 带 skill + 描述(便于后续 list 识别)
.venv/Scripts/python.exe cli.py chat --name "修登录 401" --working-dir fix_login_bug --skill coding --desc "登录返回 401 排查"
# 同 working_dir 多 task(共享 workspace/users/<sentinel>/proposal_v3/ 目录,name 各不同)
.venv/Scripts/python.exe cli.py chat --name "补充资料" --working-dir proposal_v3
# 恢复最近一个 task(resume 时 --name / --working-dir 都忽略)
.venv/Scripts/python.exe cli.py chat --resume last
# 恢复指定 task(UUID 完整或 ≥8 字符前缀)
.venv/Scripts/python.exe cli.py chat --resume 76c6bd25
# 切模型
.venv/Scripts/python.exe cli.py chat --name x --model deepseek_v4.pro
REPL 内命令:/exit /reset /new [<name>] /resume [last|<id>] /id /status /done /abandon /desc <文本> /export [<id>](/new <name> 用新任务名 + 沿用当前 working_dir;/new 无参 → 自动 gen 新任务_HH-MM-SS)
列表 / 导出
# 看最近 20 个 task
.venv/Scripts/python.exe cli.py tasks
# 只看 active
.venv/Scripts/python.exe cli.py tasks --status active --limit 50
# 导出某 task 的对话为 .docx(自动从 PG 找 task_dir 作为输出目录)
.venv/Scripts/python.exe cli.py export 76c6bd25
# 导出最近的
.venv/Scripts/python.exe cli.py export last -o /tmp/chat.docx
能力探测 / DB 管理
# 实测对账模型 yaml 声称的能力(费 token,有 API 开销)
.venv/Scripts/python.exe cli.py probe --model deepseek_v4.flash
# DB migration
.venv/Scripts/python.exe cli.py db upgrade head
.venv/Scripts/python.exe cli.py db downgrade -1
.venv/Scripts/python.exe cli.py db current
Web API(§7 D + D' 过渡 auth)
# 默认 127.0.0.1:8765 启;dev SPA 在 /,Swagger UI 在 /docs
.venv/Scripts/python.exe cli.py web
# 自定义端口 / 监听 0.0.0.0(慎用,部署形态走反代不直暴)
.venv/Scripts/python.exe cli.py web --port 9000
# dev:文件改动自动重启(uvicorn 工厂模式 reload)
.venv/Scripts/python.exe cli.py web --reload
Auth:所有 /v1/tasks* 需 Authorization: Bearer <jwt>;先走 /v1/auth/login 拿 token:
# 登录 → 拿 token(本地默 user_id = sentinel 全 0)
curl --noproxy '*' -s -X POST http://127.0.0.1:8765/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"user_id":"00000000-0000-0000-0000-000000000000","platform_key":"<value of $PLATFORM_KEY>"}'
# → {"token":"eyJ...","expires_at":"...","user_id":"...","ttl_seconds":604800}
# 用 token 调 /v1/*
TOKEN="eyJ..."
curl --noproxy '*' -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8765/v1/tasks
dev SPA:打开 http://127.0.0.1:8765/(自动 302 → /static/dev.html),login 表单填 user_id(默 sentinel)+ PLATFORM_KEY 进入 3 栏(task 列表 / chat / files)。仅给开发自验,不发布给真用户。
路由表(全 JSON,CORS allow_origins=["*"];详细 schema 见 http://127.0.0.1:8765/docs):
| 方法 + 路径 | 用途 | Auth |
|---|---|---|
GET /healthz |
{"status":"ok"} 健康检查 |
豁免 |
GET / |
302 → /static/dev.html dev SPA |
豁免 |
GET /docs /openapi.json |
Swagger UI / OpenAPI schema | 豁免 |
GET /static/* |
dev.html 等静态文件 | 豁免 |
POST /v1/auth/login |
body {user_id, platform_key} → {token,expires_at,user_id,ttl_seconds} |
豁免 |
POST /v1/tasks |
创建 task,body {name(req), working_dir?, description?, skill?} |
必填 |
GET /v1/tasks?page=&page_size=&status=&skill=&working_dir=&q=&ordering= |
列任务,默认 -created_at;响应 {page, page_size, count, results};page 1-based,page_size 1–100;working_dir 末段名;q ILIKE name+desc;ordering DRF 风格逗号分隔 -field 倒序,allowlist created_at/updated_at/name/status |
必填 |
GET /v1/tasks/{id} |
单 task meta + n_messages;跨 user → 404 |
必填 |
PATCH /v1/tasks/{id} |
{status?,description?,name?,skill?} 部分更新;active 走 CLI 切回 |
必填 |
DELETE /v1/tasks/{id} |
硬删 DB 行(messages CASCADE),FS working_dir 保留 | 必填 |
GET /v1/folders |
列当前 user 工作目录 + n_tasks + last_used(供创建 task 自动补全用) | 必填 |
GET /v1/tasks/{id}/messages |
LiteLLM payload 透传 | 必填 |
POST /v1/tasks/{id}/messages |
{content} 发消息;返 {run_id, events_url};同 task 已有 running / cancelling run → 409(单活 run 保护,UI 应 disable send 按钮直到 SSE done) |
必填 |
GET /v1/tasks/{id}/runs/{rid}/events |
SSE 流(event: <type> + data: <json>) |
必填 |
POST /v1/tasks/{id}/runs/{rid}/cancel |
协作式 cancel 当前 run;返 {ok, run_id, status:"cancelling"};run.status != running → 409;LLM 同步 call 本身不可中断,最坏等当前一轮跑完 |
必填 |
GET /v1/tasks/{id}/files?path= |
列子目录条目 + 面包屑 | 必填 |
GET /v1/tasks/{id}/files/download?path= |
下单文件 | 必填 |
POST /v1/tasks/{id}/files/upload |
multipart 上传,path 走 form |
必填 |
POST /v1/tasks/{id}/files/delete |
body {path};文件或空目录 |
必填 |
GET /v1/tasks/{id}/export |
对话导出 .docx | 必填 |
SSE 事件 schema(每帧 event: <type> + data: <JSON>):run_start{} → llm_start{} → text{content} / tool_call{name,args,args_preview} / tool_result{name,preview,truncated} → llm_end{prompt_tokens,completion_tokens} → done{};cancel 命中走 cancelled{} 后随 done{} 收流;异常路径走 error{msg}。30s 无 event 服务端发 : ping 注释心跳。SSE 经 nginx 反代记得关 buffering(响应头已带 X-Accel-Buffering: no 默认起效)。
SSE 客户端注意:浏览器原生 EventSource 不支持自定义 header,无法塞 Bearer token。要么走 fetch + ReadableStream 自解 SSE 帧(dev.html 走的就是这条),要么后端日后加 ?token=... query 路径(目前不支持,避免 token 进 access log)。
原 Phase G Jinja2 + HTMX Web UI 路线撤(DESIGN §7.9 取舍说明)—— UI 改 platform 端实现,本仓库只维护 API + 一个 dev SPA。
cli.py web跑的是 API + Swagger + dev.html。
故障兜底
| 现象 | 原因 / 处理 |
|---|---|
ZCBOT_DB_URL is not set |
.env 没写 / litellm 链路没触发。直跑脚本时 import litellm,或 export ZCBOT_DB_URL=... |
ModuleNotFoundError: litellm |
用了全局 python,改 .venv/Scripts/python.exe ... |
| Windows 控制台 emoji 崩 | Python stdout 是 GBK,emoji 不能直 print。用 [OK] / [ng] 等 ASCII 标签(见 memory) |
db upgrade 报 column already exists |
DB 已被改过,先 db current 确认 revision,必要时手 ALTER 或 db downgrade base 重来 |
| Resume 找不到 task | cli.py tasks 看 task_id 是否在;前缀冲突报 ambiguous 时给完整 UUID |
--working-dir 指定后 /exit 没清目录 |
设计如此 —— 工作目录绝不 rmtree(同 working_dir 多 task 共享);DB 行该删还是删。要彻底删手动 rm -rf <dir> |
| Export 报 "无可导出内容" | task 没 messages(只 system 不算);先在 REPL 发条消息再 export |
NoSubtaskError: working_dir ... 与已有 task ... 前缀嵌套 |
§7.4 no-subtask:同 user 不允许 working_dir 嵌套(child 或 parent)。同项目多对话请传完全相同的 --working-dir;否则改路径成 sibling(平级) |
cli.py web 启动后 curl 连不上 |
检查 proxy(HTTP_PROXY / HTTPS_PROXY):本地服务在 127.0.0.1,系统 proxy 拦截会 502。临时 unset HTTP_PROXY HTTPS_PROXY 或加 curl --noproxy '*'。验通:curl --noproxy '*' http://127.0.0.1:8765/healthz → {"status":"ok"} |
| SSE 卡住不流(经 nginx) | 反代要关 buffering — 后端响应头已带 X-Accel-Buffering: no,nginx ≥ 1.5.6 默认认。仍卡看 nginx 配 proxy_buffering off; proxy_read_timeout 3600s; |
| platform 端 CORS preflight 失败 | 本地 dev allow_origins=["*"] 应该没事;部署后看是否按 platform 域名收紧过头(access-control-allow-origin 响应头要含 platform 域名 或 *) |
POST /v1/tasks/{id}/messages 返 409 task already has an active run |
上一条消息的 BG run 还没跑完(SSE 没 done)。等流式跑完;或点 dev SPA 的 stop / POST /runs/{rid}/cancel;服务异常下 Run 行卡 running/cancelling,启动 reaper 会清 |
POST /v1/tasks/{id}/runs/{rid}/cancel 返 409 run not running |
run 已结束(ok/error/cancelled)或已被 cancel 进入 cancelling,不能重复 cancel;dev SPA 自动忽略不报错 |
| 点 stop 后流式没立刻停 | LLM 同步调用本身不可中断,最坏等当前一轮跑完(通常几十秒)。loop 进入下个 check 点(每轮 LLM 前 / 每个 tool_call 前)就退,emit cancelled → SSE done → UI 收回 stop 按钮 |
[startup] reaped N stale active run(s) |
上次 cli.py web 进程未正常 finish 留下 N 个 running / cancelling Run 行,启动 lifespan 自动标 error。无需处理,info 级 |
cli.py web 启动报 PLATFORM_KEY env not set / JWT_SECRET env not set |
D' 过渡 auth 强制双 env 必填。生成 python -c "import secrets;print(secrets.token_urlsafe(48))" 各填一,写进 .env 重起 |
/v1/* 全返 401 missing Authorization: Bearer |
没拿 token 或没带 header。先 POST /v1/auth/login 拿 token,curl 加 -H "Authorization: Bearer $TOKEN" |
/v1/* 返 401 token expired |
JWT 默 7d TTL 到期,重 login。要更长改 ZCBOT_JWT_TTL_SECONDS env |
| dev.html SSE 收不到流(消息发出去但 UI 没动) | EventSource 不支持 header,dev.html 走 fetch + ReadableStream。看浏览器 devtools Network,POST /messages 是否 202 + Network 看 events_url GET 是否 200 + Content-Type 是 text/event-stream;若 401,token 过期了 — logout 重 login |
| dev.html 显示 "load failed" 且立刻回登录页 | token 过期或 JWT_SECRET 服务端变了,localStorage 旧 token 失效。已自动跳登录页,重新填 platform_key 即可 |
关键路径与文件
- 入口:
cli.py(REPL +chat / tasks / probe / db / web子命令)→main.py::build_agent(装配) - 核心:
core/loop.py(ReAct)/core/session.py(PG messages)/core/task.py(PG tasks)/core/llm.py(LiteLLM 封装) - 工具:
tools/{fs,shell,run_python,skill_tool}.py - 存储:
core/storage/{engine,models,utils}.py(SQLAlchemy 2.x ORM)+db/migrations/(alembic) - Web:
web/{app.py, auth.py, broker.py, sinks.py}(FastAPI + /v1 JSON API + SSE + PLATFORM_KEY→JWT)+web/static/dev.html(dev SPA,单文件 vanilla JS) - 配置:
config/agent.yaml(全局)/config/models/*.yaml(模型档案,§3.2 Model Profile) - Skill:
skills/{coding,ppt,proposal}/SKILL.md(渐进披露,§3.5) - Workspace(per-user 子树,本地 CLI sentinel =
00000000-0000-0000-0000-000000000000,web/JWT 用 sub):workspace/users/<user_id>/.memory/{core.md, extended/}—— 跨 task 记忆,FS 永久,dotfile 隔离workspace/users/<user_id>/<working_dir>/—— 工作目录,用户起的目录名(cli chat --working-dir或留空 fallback--name/ APIPOST /v1/tasks {working_dir?}),同 working_dir 多 task 共享
维护约定
- 每改一个对外行为(CLI 选项 / REPL 命令 / env 变量 / 文件布局)→ 同步更新本文档。bug 修不动这个,只动 PROGRESS。
- 故障兜底表新增条目:用过一次的真实坑,写一行(现象 + 处理),不预测。
- 跟 DESIGN/PROGRESS 的边界:DESIGN 写"为什么",PROGRESS 写"做到哪",RUN 写"怎么跑"。