zcbot/RUN.md

17 KiB
Raw Blame History

运行手册

怎么把 zcbot 跑起来。env / 常用命令 / 故障兜底。设计看 DESIGN.md,进度看 PROGRESS.md

最后更新:2026-05-19(dev SPA 登录撤回 邮箱+密码 — 复用 0001 schema 的 users.email/password_hash,0005 加 UNIQUE(email);短命 invites 表 + /v1/auth/login_invite 撤;新路由 POST /v1/auth/login_password;main.py user add CLI 建用户;/v1/auth/login (platform 机器对机器) 不动;SENTINEL user 撤)


环境

  • 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
    # main.py web 必填(probe/db 用不到,只在起 web 时校验)
    PLATFORM_KEY=<至少 16 字符的随机串,platform 服务端持有,机器对机器入口校验>
    JWT_SECRET=<≥32 字符随机串,HS256 签 session token;泄漏 = 任意伪造,与 PLATFORM_KEY 同级保护>
    # 可选:覆盖默认 7d JWT TTL
    # ZCBOT_JWT_TTL_SECONDS=604800
    

    邀请码方案已撤(05-19,一日游),改回 users.email/password_hash(0001 schema 原列 + 0005 加 UNIQUE)。 litellm 在 import 时副作用加载 .env;入口走 main.py,.env 会自动生效。直跑 python -c "from core.storage import ..." 不经 litellm 链路时记得自己 import litellm 触发,或手动 export ZCBOT_DB_URL=...

  • 依赖:pip install -r requirements.txt(已在 .venv 里;含 bcrypt 给密码哈希)。
  • PG:ZCBOT_DB_URL 必填。本地 docker compose 起 / 远端 dev / 生产任选。未设置时启动会清晰报错,不引导 docker(§7.4)。
  • Auth env(main.py web 必填):PLATFORM_KEY + JWT_SECRET,任一缺失 web 启动会 fail-fast。生成随机串可用 python -c "import secrets; print(secrets.token_urlsafe(48))"main.py db / probe / user 不验,不要这两个 env 也能跑。
  • 邮箱密码(users.email/password_hash,0005 加 UNIQUE):dev SPA 登录后端。一行 = 一个用户,password_hashbcrypt(cost=12)。发用户走 .venv/Scripts/python.exe main.py user add --email X --password Y(详见下方 user 命令段);撤用户直接 DELETE FROM users WHERE email=...(先 DELETE 该 user 的 tasks,messages 通过 FK CASCADE 自动)。同事拿到邮箱密码即可登录,不接触 PLATFORM_KEY。改密 / 改邮箱目前手动 SQL(UPDATE users SET password_hash=<bcrypt hash> / email=... WHERE ...)或先 DELETE 再 add。

一次性初始化

# 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 main.py db upgrade head
.venv/Scripts/python.exe main.py db current   # 应输出 0004 (head)

日常命令

入口统一在 main.py,只剩三个子命令:web / db / probe。所有 task / 消息 / 文件交互 走 main.py web 起服务后浏览器(dev SPA)或 platform 端 / curl 调 /v1/*(下方 Web API 段)。

Web 服务(§7 D + D' 过渡 auth)

# 默认 127.0.0.1:8765 启;dev SPA 在 /,Swagger UI 在 /docs
.venv/Scripts/python.exe main.py web

# 自定义端口 / 监听 0.0.0.0(慎用,部署形态走反代不直暴)
.venv/Scripts/python.exe main.py web --port 9000

# dev:文件改动自动重启(uvicorn 工厂模式 reload)
.venv/Scripts/python.exe main.py web --reload

能力探测 / DB 管理 / 用户管理

# 实测对账模型 yaml 声称的能力(费 token,有 API 开销)
.venv/Scripts/python.exe main.py probe --model deepseek_v4.flash

# DB migration
.venv/Scripts/python.exe main.py db upgrade head
.venv/Scripts/python.exe main.py db downgrade -1
.venv/Scripts/python.exe main.py db current

# 发用户 — dev SPA 邮箱密码登录后端 bootstrap
.venv/Scripts/python.exe main.py user add --email alice@example.com --password "atLeast6"
# → [ok] user added  email=alice@example.com  user_id=<uuid>
# 可选把已有 user_id(platform_key 入口创的)接到邮箱密码路径
.venv/Scripts/python.exe main.py user add --email bob@x.com --password "s3cret" --user-id <UUID>
# 撤用户(先清该 user 的 tasks,messages CASCADE)
# psql> DELETE FROM tasks WHERE user_id=(SELECT user_id FROM users WHERE email='alice@example.com');
# psql> DELETE FROM users WHERE email='alice@example.com';

Auth:两条 login 路径,签同款 JWT。所有 /v1/tasks*Authorization: Bearer <jwt>

# 路径 1:邮箱密码(dev SPA 给同事 / 自己试用 — 推荐)
curl --noproxy '*' -s -X POST http://127.0.0.1:8765/v1/auth/login_password \
  -H "Content-Type: application/json" \
  -d '{"email":"alice@example.com","password":"atLeast6"}'
# → {"token":"eyJ...","expires_at":"...","user_id":"...","email":"alice@example.com","ttl_seconds":604800}

# 路径 2:platform_key + 指定 user_id(platform 服务端机器对机器入口)
curl --noproxy '*' -s -X POST http://127.0.0.1:8765/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"user_id":"<UUID>","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),登录页两 tab(默认"邮箱密码",备用"UUID + PLATFORM_KEY")进入 3 栏(task 列表 / chat / files);last-used tab 持久化在 localStorage。给同事试用的最简发用户流程:

  1. .venv/Scripts/python.exe main.py user add --email <每人> --password <每人>
  2. 不用重启 web(每次 login 都查 DB),把 URL + 各自邮箱密码分别发给同事

路由表(全 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 platform 机器对机器入口;body {user_id, platform_key}{token,expires_at,user_id,ttl_seconds} 豁免
POST /v1/auth/login_password dev SPA 邮箱密码入口;body {email, password}{token,expires_at,user_id,email,ttl_seconds};邮箱不存在 / 密码错 / 未设密码统一 403 invalid email or password 豁免
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 1100;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} 发消息;返 {events_url};tasks.run_status 是 running / cancelling → 409(单活 run 保护;error 状态视为可重启,起新 run 时清);UI 应 disable send 按钮直到 SSE done 必填
GET /v1/tasks/{id}/events SSE 流(event: <type> + data: <json>);订阅 task 当前活动 — 单活 run 形态下无歧义 必填
POST /v1/tasks/{id}/cancel 协作式 cancel 当前活跃 run;返 {ok, task_id, run_status:"cancelling"};run_status != "running" → 409;LLM 同步 call 本身不可中断,最坏等当前一轮跑完 必填
GET /v1/files?path= 列 user_root 下子目录条目 + 面包屑(user-rooted,不绑 task);dotfile 隐藏 必填
GET /v1/files/download?path= 下单文件(user_root 下) 必填
POST /v1/files/upload multipart 上传到 <user_root>/<path>/;路径不存在自动 mkdir,重名覆盖 必填
POST /v1/files/delete body {path};文件或空目录;path 是顶层目录(user_root 直接子项,且为目录)且仍被 task 引用 working_dir → 409,先 DELETE 关联 task 必填
POST /v1/files/rename body {path, new_name};new_name 是新 leaf 名(校验同 task_name);sibling 已存在 → 409;path 是顶层目录 → 同步 UPDATE tasks.working_dir(同事务 + FOR UPDATE 锁;有 running/cancelling task → 409;check_no_subtask 防嵌套 → 409);非顶层(子目录 / 文件)纯 FS rename 必填
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。main.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 upgradecolumn already exists DB 已被改过,先 db current 确认 revision,必要时手 ALTER 或 db downgrade base 重来
Resume 找不到 task dev SPA 左侧 task 列表看 task_id 是否在;或 curl /v1/tasks
--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(平级)
main.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 /v1/tasks/{id}/cancel;服务异常下 tasks.run_statusrunning/cancelling,启动 reaper 会清
POST /v1/tasks/{id}/cancel 返 409 task not running run_status 不是 running(idle / cancelling / error 都不能 cancel,error 只能起新 run 顶掉);dev SPA 自动忽略不报错
点 stop 后流式没立刻停 LLM 同步调用本身不可中断,最坏等当前一轮跑完(通常几十秒)。loop 进入下个 check 点(每轮 LLM 前 / 每个 tool_call 前)就退,emit cancelled → SSE done → UI 收回 stop 按钮
[startup] reaped N stale active run(s) 上次 main.py web 进程未正常 finish 留下 N 个 running / cancelling Run 行,启动 lifespan 自动标 error。无需处理,info 级
POST /v1/files/delete 返 409 folder ... 仍被 N 个 task 引用 顶层目录(user_root 直接子项)被 task 引用 working_dir;先 DELETE /v1/tasks/{id} 删完所有关联 task 再删目录。子目录不受此限,可直接删空目录
POST /v1/files/rename 返 409 folder has active run(s) 顶层目录被某 running/cancelling 的 task 占用;先点 stop / POST /v1/tasks/{id}/cancel 等流式 done 再 rename
POST /v1/files/rename 返 409 ... 前缀嵌套 改名后会与其他 task 的 working_dir 形成嵌套(§7.4 no-subtask)。换一个不冲突的 new_name
main.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/auth/login_password 返 403 invalid email or password 邮箱在 users 表里不存在 / password_hash 列为空(platform_key 入口建的 user) / 密码错。SELECT user_id, email, password_hash IS NOT NULL AS has_pw FROM users WHERE email=... 核对;无行 → main.py user add 发新;有行无密码 → UPDATE users SET password_hash=... 手补 bcrypt(用 .venv/Scripts/python.exe -c "from web.auth import hash_password;print(hash_password('xxx'))" 算)或直接 user add --user-id 接到现有 user_id
main.py user addIntegrityError ... uq_users_email 邮箱已在 users 表里,改 email 或先 DELETE FROM users WHERE email=...(先清该 user 的 tasks);允许同邮箱不同 user 是漏
main.py user addIntegrityError ... users_pkey --user-id 撞已有 UUID,换一个或不传 --user-id 让随机生成
改了某用户邮箱后他登不上 单纯改 UPDATE users SET email=... 不影响 user_id(行还是同一行,task 仍归属),用户用新邮箱登即可;若 lowercase 不一致(后端 lower() 后查)→ DB 里就该存小写。改密同理 UPDATE users SET password_hash=<bcrypt>
/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 失效。已自动跳登录页,按上次用的 tab(邮箱密码 / UUID+PLATFORM_KEY)重登即可

关键路径与文件

  • 入口:main.py(web / db / probe / user 四子命令)→ core/agent_builder.py::build_agent(装配 lib)
  • 核心:core/agent_builder.py(build_agent / system prompt / validate_task_name 等装配 lib)/ 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 子树,user_id 来自 JWT sub — 邮箱密码登录从 users 表直读、platform 登录直传):
    • workspace/users/<user_id>/.memory/{core.md, extended/} —— 跨 task 记忆,FS 永久,dotfile 隔离
    • workspace/users/<user_id>/<working_dir>/ —— 工作目录,用户起的目录名(API POST /v1/tasks {working_dir?},留空 fallback name),同 working_dir 多 task 共享

维护约定

  • 每改一个对外行为(CLI 选项 / REPL 命令 / env 变量 / 文件布局)→ 同步更新本文档。bug 修不动这个,只动 PROGRESS。
  • 故障兜底表新增条目:用过一次的真实坑,写一行(现象 + 处理),不预测。
  • 跟 DESIGN/PROGRESS 的边界:DESIGN 写"为什么",PROGRESS 写"做到哪",RUN 写"怎么跑"。