zcbot/RUN.md

28 KiB

运行手册

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

最后更新:2026-05-22(dev SPA 加 iframe embed 模式 — ?embed=1&parent_origin=...,对接见 EMBED.md)


环境

  • Python:虚拟环境 .venv/,所有依赖装在里面。一律用 .venv/Scripts/python.exe ...(Windows)/.venv/bin/python ...(Unix),不要全局 python(litellm/python-pptx 等会 ModuleNotFoundError)。
  • 配置文件 .env(项目根,git 忽略,litellm 自动加载):
    DEEPSEEK_API_KEY=sk-...
    # 用 GLM 的话再加一条;国际站 z.ai 用 ZAI_API_KEY,国内站 bigmodel.cn 用 ZHIPUAI_API_KEY(对应 config/models/glm.yaml 的 api_key_env 字段)
    ZHIPUAI_API_KEY=...
    # 豆包(火山方舟)图像/视频生成:可选。设了同时挂 seedream tool(0.22 元/张)与 seedance tool
    # (Seedance 2.0 Fast,文生视频,480p 4s ¥1.86 ~ 720p 15s ¥12+,异步等 30-90s);未设两个 tool 都不出现
    ARK_API_KEY=...
    # documents skill(内部知识库 document_search API):可选。设了 documents skill 才能用,未设调用立即抛 RuntimeError
    DOCUMENT_SEARCH_API_KEY=...
    # 可选:覆盖默认 base_url(默认 https://ai.ctc-zc.com:8100/api)
    # DOCUMENT_SEARCH_URL=https://ai.ctc-zc.com:8100/api
    ZCBOT_DB_URL=postgresql://user:pass@host:5432/zcbot
    # main.py web 必填(probe/db/user 不验)
    PLATFORM_KEY=<≥16 字符随机串,platform 机器对机器入口校验>
    JWT_SECRET=<≥32 字符随机串,HS256 签 session token;泄漏 = 任意伪造>
    # 可选:覆盖默认 7d JWT TTL
    # ZCBOT_JWT_TTL_SECONDS=604800
    # 可选:设了之后登录页右下角"+ 管理员添加用户"入口才工作(未设 → 接口返 503)
    # ZCBOT_ADMIN_TOKEN=<≥32 字符随机串,管理员发用户共享口令>
    

    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:PLATFORM_KEY + JWT_SECRET 任一缺失 web 启动 fail-fast。生成随机串:python -c "import secrets; print(secrets.token_urlsafe(48))"
  • 用户管理(users.email/password_hash,0005 UNIQUE(email)):dev SPA 登录后端。发用户两条路径任选:CLI main.py user add(下方),或在登录页右下角"+ 管理员添加用户"链接(需先设 ZCBOT_ADMIN_TOKEN env,弹窗输入 email/密码/管理员口令)。撤用户 DELETE FROM users WHERE email=...(先 DELETE 该 user 的 tasks)。改密 / 改邮箱手动 SQL 或先 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   # 应输出 0007 (head)

日常命令

入口 main.py {web, db, probe, user}。所有 task / 消息 / 文件交互走 main.py web 起服务后浏览器(dev SPA)或 platform 端 / curl 调 /v1/*

Web 服务

# 默认 127.0.0.1:8765;dev SPA 在 /,Swagger UI 在 /docs
.venv/Scripts/python.exe main.py web
.venv/Scripts/python.exe main.py web --port 9000
.venv/Scripts/python.exe main.py web --reload   # 文件改动自动重启

DB / probe / user

# 模型能力对账(费 token)
.venv/Scripts/python.exe main.py probe --model deepseek_v4.flash
.venv/Scripts/python.exe main.py probe --model glm.pro          # 智谱 GLM-5.1(走 litellm zai provider + 国内站 bigmodel.cn)

# 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

# 发用户(两条路径,任选其一)
# a) CLI:
.venv/Scripts/python.exe main.py user add --email alice@example.com --password "atLeast6"
# → [ok] user added  email=alice@example.com  user_id=<uuid>
# b) 登录页右下角"+ 管理员添加用户":需先在 .env 里设 `ZCBOT_ADMIN_TOKEN`,
#    弹窗输入 email/密码/管理员口令,POST /v1/auth/admin/create_user 落库。
#    没设 env → 接口直接返 503,UI 入口会报"admin create_user disabled"。

# 可选:把已有 user_id(platform_key 入口创的)接到邮箱密码路径
.venv/Scripts/python.exe main.py user add --email bob@x.com --password "s3cret" --user-id <UUID>

# 撤用户:先清 tasks(messages CASCADE)再 DELETE user
# 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 + curl 用 token 调 /v1/*

两条 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"}'

# 路径 2:platform_key + 指定 user_id(机器对机器)
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>"}'

# 调 /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",last-used 持久化 LS)进入 3 栏(task / chat / files)。给同事试用:main.py user add 发用户,不用重启 web(每次 login 都查 DB),把 URL + 邮箱密码分别发给同事。

iframe 嵌入(platform 主页内嵌):URL 加 ?embed=1&parent_origin=<父页面 origin>,触发 embed 模式 —— 藏左上 brand / 退出按钮,登录页不显示,新建任务挪到任务面板;父页面通过 postMessage 协议推 JWT(zcbot-ready / zcbot-token / zcbot-401)。完整对接手册见 EMBED.md(URL 参数 / 协议 / 后端 SSO 示例 / 父端前端示例 / 安全 / 故障兜底)。

路由表

全 JSON,CORS allow_origins=["*"];详细 schema 见 /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,...,email,...};邮箱不存在 / 密码错 / 未设密码统一 403 豁免
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};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 不让从 web 切回 必填
DELETE /v1/tasks/{id} 硬删 DB 行(messages CASCADE);若 working_dir 已无其他 task 引用且 FS 目录为空 → 顺手 rmdir 清孤儿(非空 / 外部 --working-dir 静默跳过) 必填
GET /v1/folders 列当前 user 工作目录 + n_tasks + last_used 必填
GET /v1/tasks/{id}/messages LiteLLM payload 透传 必填
POST /v1/tasks/{id}/messages {content, image_model?=""} 发消息;返 {events_url};run_status 是 running/cancelling → 409(单活 run;error 起新 run 时清);image_modelconfig/media/doubao.yaml image 段的 variant key(空 → 沿用 yaml 第一个),仅本 run 装配 SeedreamTool 时使用,不入 DB;UI 应 disable send 直到 SSE done 必填
GET /v1/tasks/{id}/events SSE 流(event: <type> + data: <json>);订阅 task 当前活动 必填
POST /v1/tasks/{id}/cancel 协作式 cancel;run_status != running → 409;LLM 走 streaming,chunk 间 poll cancel — 延迟 100ms 级,基本秒退 必填
POST /v1/tasks/{id}/clear 清空当前 task 全部 messages + reset tasks.tokens_prompt/completion/cost_cny 三列累计 + run_status='idle';usage_events(账单记账)不动,只 message_id 列变 NULL;run 活跃中(running/cancelling)→ 409(先 cancel);FS 文件保留 必填
POST /v1/tasks/{id}/optimize_prompt body {text(req, ≤4000), image_model?=""};同步调当前 task model 润色草稿,返 {optimized, model_profile, tokens_in, tokens_out, cost_cny};写 messages、累计 task 三列(顶栏数字不污染),只在 usage_events 写一行 kind="prompt_optimize"(对账可见);不与主对话 run 互斥(允许 streaming 中并行润色) 必填
GET /v1/files?path= 列 user_root 下条目 + 面包屑;dotfile 隐藏 必填
GET /v1/files/download?path= 下单文件 必填
POST /v1/files/upload multipart 上传到 <user_root>/<path>/;路径不存在自动 mkdir,重名覆盖 必填
POST /v1/files/delete {path, recursive?=false};recursive=false 文件或空目录(非空 → 400);recursive=true shutil.rmtree —— 顶层目录被 task 引用 → 409(先 DELETE task);空目录两种模式都可删,task.working_dir 字段不动,下次 build_agent 按需 mkdir 重建 必填
POST /v1/files/rename {path, new_name};sibling 已存在 → 409;path 顶层目录 → 同事务 UPDATE tasks.working_dir + FOR UPDATE 锁;有 running/cancelling → 409;check_no_subtask 防嵌套 → 409 必填
GET /v1/tasks/{id}/export 对话导出 .docx 必填
GET /v1/models 列 chat LLM 模型清单(扫 config/models/*.yaml),前端顶栏切换 / 新建对话框下拉用 必填
GET /v1/image_models 列图像生成 variant 清单(扫 config/media/doubao.yaml image 段),前端"生图"下拉用;yaml 无 image variant → 空列表 → UI 隐藏下拉 必填

SSE 事件(每帧 event: <type> + data: <JSON>):run_start{}llm_start{}text{delta} / 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 心跳。nginx 反代记得关 buffering(响应头已带 X-Accel-Buffering: no 默认起效)。

SSE 客户端注意:浏览器原生 EventSource 不支持自定义 header,无法塞 Bearer token。要么 fetch + ReadableStream 自解 SSE 帧(dev.html 走的就是这条),要么后端日后加 ?token=... query(目前不支持,避免 token 进 access log)。


部署(Ubuntu / systemd)

单机最小拓扑:/opt/zcbot/(代码 + .venv + .env)+ systemd unit。按需改路径 / user / port。

一次性

sudo useradd -r -s /sbin/nologin -d /opt/zcbot zcbot   # 跑服务的非 root 用户
sudo chown -R zcbot:zcbot /opt/zcbot
# 把 .env 权限收紧(含 JWT_SECRET / PLATFORM_KEY)
sudo chmod 600 /opt/zcbot/.env
sudo chown zcbot:zcbot /opt/zcbot/.env

unit 文件 /etc/systemd/system/zcbot.service

[Unit]
Description=zcbot web (FastAPI/uvicorn)
After=network-online.target postgresql.service
Wants=network-online.target

[Service]
Type=simple
User=zcbot
WorkingDirectory=/opt/zcbot
# 显式让 systemd 装载 .env(KEY=value 行,不展开 ${...},不留 shell 引号)
EnvironmentFile=/opt/zcbot/.env
ExecStart=/opt/zcbot/.venv/bin/python main.py web --host 0.0.0.0 --port 8765
Restart=on-failure
RestartSec=2
KillSignal=SIGTERM
# uvicorn graceful shutdown 会等 in-flight 请求(含 SSE 长连接);
# 10s 后 systemd 兜底 SIGKILL,避免 SSE 拖住 restart 卡死
TimeoutStopSec=10
KillMode=mixed
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

启用 + 日常:

sudo systemctl daemon-reload
sudo systemctl enable --now zcbot
sudo systemctl status zcbot | head
sudo journalctl -u zcbot -f                   # 实时日志
sudo systemctl restart zcbot                  # 重启(REST 抖动 ~2s,SSE 连接断)
sudo systemctl stop zcbot

不要再用 kill -HUP:uvicorn 不响应 SIGHUP(没装 handler,落 Python 默认),也不会 reload 代码。Ubuntu 上要么 systemctl restart,要么用下面 --reload 自动模式。

无感更新(对 SSE 也尽量不抖)

zcbot 现在 5 人级 + SSE 长连接,严格"零中断"(蓝绿 + nginx + SSE 客户端 reconnect 设计)代价高,不值得。有性价比的两挡:

A. 简易档:--reload(推荐当前规模) ExecStart 加 --reload,git pull 后 uvicorn 监听到文件变动自动重起子进程,REST 抖动 < 1s。代价:SSE 连接被切断,dev.html 自动重连 3 次(1s/2s/4s 退避);若新进程已被启动 reaper 标 run_status=error,重连立即收 done,卡片末尾追加红色"连接已断开,请重发"。期间 LLM 吐的 delta 丢失(broker 不持久化 event,接受)。3 次仍失败 → 同上提示,用户重发即可。

ExecStart=/opt/zcbot/.venv/bin/python main.py web --host 0.0.0.0 --port 8765 --reload

sudo systemctl restart zcbot 一次生效。之后git pull 即可,不用再 restart;改 unit 文件本身才需 daemon-reload + restart。

B. 真无感档:nginx + 蓝绿双实例(将来流量上来再上) 两个 systemd 实例 zcbot@blue / zcbot@green(模板 unit,--port 8765 / --port 8766),nginx upstream 在中间切。流程:

  1. 部署到空闲实例(假设 green):sudo systemctl restart zcbot@green
  2. curl 127.0.0.1:8766/healthz 验新版起来
  3. 改 nginx upstream 指向 green,nginx -s reload新 REST 走 green,旧 SSE 还连在 blue 不断
  4. 等 blue SSE 自然清空(ss -tnp | grep :8765 为空)再关 blue

zcbot 端额外要做的事:消息 broker 当前在 task 进程内(web/broker.py),蓝绿期间同 task 不同进程会丢事件。nginx 侧用 hash $arg_task_id consistent 保同 task 落同实例可以缓解,但 task 创建分布是另一回事。要做这条得先把 broker 改成 Redis pub/sub。10 人内不推,留到真有需要再上。

部署 SOP(目前推荐:方案 A --reload)

# 在服务器上
cd /opt/zcbot
sudo -u zcbot git pull --ff-only                    # 拉新代码
sudo -u zcbot .venv/bin/python -m pip install -r requirements.txt   # 依赖有变才需要
sudo -u zcbot .venv/bin/python main.py db upgrade head              # migration 有新版才需要
# 这一步通常不用做:--reload 监听到 .py 文件变动会自动重起
# 但 .env / unit 改了 → 手动:
# sudo systemctl restart zcbot
sudo systemctl status zcbot | head
sudo journalctl -u zcbot -n 50                      # 看新进程起没起干净

Sandbox(Stage C,Ubuntu)

为外部用户开放前必须完成。当前 dogfood + 信任同事白名单阶段可跳过 ── 默 backend = host, shell / run_python 仍走 subprocess(未隔离)。Step 3 已接入 DockerExecutor: ZCBOT_SANDBOX_BACKEND=docker 切容器执行;host(默)保留本地 Windows / 同事 dogfood。

启用 docker backend 的前置条件:

  1. 部署机有 docker daemon,zcbot 用户在 docker group
  2. zcbot-sandbox:latest 镜像已 build(HOST_UID/GID 对齐)
  3. .env 至少有 ZCBOT_PG_IPS=<PG实际IP>(§7.5 #1 PG 单独 block 一遍)
  4. lifespan 启动失败会 fail-fast(RuntimeError: sandbox init failed),不静默退到 host

镜像构建

容器内 uid/gid 与 host zcbot 用户必须对齐(bind mount 保 host owner;错配导致 EACCES):

# 1) 确保 zcbot 用户存在,uid 拿出来
id -u zcbot      # 期望:整数,后面用作 HOST_UID
id -g zcbot      # 期望:整数

# 2) 构建镜像(build context = repo 根)
cd /opt/zcbot
sudo -u zcbot docker build \
    -f deploy/sandbox/Dockerfile \
    --build-arg HOST_UID=$(id -u zcbot) \
    --build-arg HOST_GID=$(id -g zcbot) \
    -t zcbot-sandbox:latest .

# 3) 创建 sandbox 网络(--internal,默认无 outbound)
sudo -u zcbot docker network create --internal zcbot-sandbox-net
# 或 SandboxPool.setup_pool() 自动 ensure

Sandbox 相关 env(.env 加)

# Backend 选择(默 host):
#   host   = shell/run_python 走 host subprocess(本地 Windows / dogfood)
#   docker = shell/run_python 走 per-user 容器 docker exec(部署机 / 外部用户)
# ZCBOT_SANDBOX_BACKEND=docker

# 容器内 exec 用户(默 1000:1000;Dockerfile 的 HOST_UID/HOST_GID build-arg 同步对齐)
# ZCBOT_SANDBOX_EXEC_USER=1000:1000

# 容器镜像 tag(默 zcbot-sandbox:latest)
# ZCBOT_SANDBOX_IMAGE=zcbot-sandbox:latest
# 容器 runtime(切 gVisor 用 runsc,Firecracker 用 kata;默 runc)
# ZCBOT_SANDBOX_RUNTIME=
# 空闲多少秒回收(默 300)
# ZCBOT_SANDBOX_IDLE_TTL=300
# PG 实际 IP,逗号分隔。defense-in-depth ── 即便落内网三段(§7.5 #1),
# init.sh 再加一遍 DROP 规则。生产部署必填。
ZCBOT_PG_IPS=10.1.2.3,10.1.2.4

验证

Step 3 之后,推荐用集成验证(web 起 docker backend + dev SPA 发 shell / run_python 消息):

# 启动 web 时切 docker backend(.env 已设 PG_IPS / SANDBOX_BACKEND=docker)
ZCBOT_SANDBOX_BACKEND=docker .venv/bin/python main.py web

# 触发任一 shell / run_python 消息后,容器应已起
sudo -u zcbot docker ps --filter label=zcbot.product=sandbox
# 应看到 zcbot-sandbox-<your-uid>,STATUS = Up ...
# 5 分钟无新消息后 reaper 自动 rm

也可直接起一个测试容器单验 hardening(不依赖 web 进程):

USER_ID=00000000-0000-0000-0000-000000000001
sudo -u zcbot docker run -d \
    --name zcbot-sandbox-$USER_ID \
    --label zcbot.product=sandbox \
    --label zcbot.user_id=$USER_ID \
    --network zcbot-sandbox-net \
    --read-only --tmpfs /tmp:exec,size=512m,mode=1777 \
    --cap-drop=ALL --cap-add=NET_ADMIN \
    --security-opt=no-new-privileges \
    --pids-limit=256 --memory=2g --cpus=1.0 \
    -v /opt/zcbot/workspace/users/$USER_ID:/workspace \
    -e ZCBOT_PG_IPS=10.1.2.3 \
    zcbot-sandbox:latest

# 看 iptables 规则确实 apply 了(应 6+1 条 DROP)
sudo -u zcbot docker exec zcbot-sandbox-$USER_ID iptables -L OUTPUT -n --line-numbers

# 看 non-root 用户(uid 应 = host zcbot uid)
sudo -u zcbot docker exec --user $(id -u zcbot):$(id -g zcbot) \
    zcbot-sandbox-$USER_ID id

# 看 rootfs read-only(应报 Read-only file system)
sudo -u zcbot docker exec --user $(id -u zcbot):$(id -g zcbot) \
    zcbot-sandbox-$USER_ID touch /badtest

# 销毁
sudo -u zcbot docker rm -f zcbot-sandbox-$USER_ID

Step 4 引入 egress proxy 后,完整 5 条红队用例(metadata / loopback / 跨 user / nohup 残留 / allowlist 外 403)进 tests/test_sandbox_redteam.py 自动化跑。

配额硬化(§7.5 #4,外部开放前必做)

应用层磁盘配额(Step 5 引入)能挡常规超额,但扫描间隙打满共享 fs 拖死同节点这条 硬要 xfs / ext4 project quota 或 zfs dataset quota。部署到独立服务器 + 多租户开放前:

# 示例(xfs project quota):
sudo mount -o remount,prjquota /opt
sudo xfs_quota -x -c "project -s -p /opt/zcbot/workspace/users/<uid> <pid>" /opt
sudo xfs_quota -x -c "limit -p bhard=10g <pid>" /opt

具体方案视部署 fs 选择(xfs 推荐)── 不做这步等于"软配额 + 信任用户不写满"。


故障兜底

现象 原因 / 处理
ZCBOT_DB_URL is not set .env 没写 / litellm 链路没触发。直跑脚本时 import litellmexport ZCBOT_DB_URL=...
ModuleNotFoundError: litellm 用了全局 python,改 .venv/Scripts/python.exe ...
Windows 控制台 emoji 崩 Python stdout 是 GBK。用 [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 指定后 task 删了目录还在 两种情况:① 目录非空(有用户文件) — 设计如此,绝不 rmtree,手动 rm -rf <dir> 清;② 外部 --working-dir(DB 存绝对路径)— 不自动清,避免误删用户外部项目。ROOT 内 + 同 working_dir 无其他 task 引用 + FS 空 → DELETE task 时已自动 rmdir
Sandbox 容器内 touch /workspace/xPermission denied 容器 uid 1000 与 host zcbot 用户 uid 不一致(bind mount 保 host owner)。docker build --build-arg HOST_UID=$(id -u zcbot) 重建镜像
Sandbox 容器 build 完起不来,docker logs 显示 iptables 报错 缺 NET_ADMIN cap(--cap-add=NET_ADMIN 漏了)或 kernel 不支持(WSL2 / OpenVZ 环境不能跑)。Ubuntu 物理 / KVM 正常。验:docker exec ... iptables -V
Export 报 "无可导出内容" task 没 messages(只 system 不算);先发条消息再 export
NoSubtaskError: working_dir ... 前缀嵌套 §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_PROXYcurl --noproxy '*'。验通:curl --noproxy '*' http://127.0.0.1:8765/healthz
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 域名收紧过头
POST /v1/tasks/{id}/messages 返 409 task already has an active run 上一条消息的 BG run 还没跑完;等流式 done 或点 stop / POST .../cancel;服务异常下 run_statusrunning/cancelling,启动 reaper 会清
POST /v1/tasks/{id}/cancel 返 409 task not running run_status 不是 running(idle / cancelling / error 都不能 cancel);dev SPA 自动忽略不报错
POST /v1/tasks/{id}/clear 返 409 task has an active run 当前 run 还没跑完;先点停止 / POST .../cancel 等流式 done 再清空
点 stop 后流式没立刻停 streaming 改造后正常路径秒退;若仍卡可能是 ① httpx 连接 close 没立刻关(GC 时机)/ ② 模型 thinking 阶段长时间不吐 chunk,等下一个 chunk 到达才能 poll cancel(罕见)
[startup] reaped N stale active run(s) 上次 web 进程未正常 finish 留下 N 个孤儿 run,启动 lifespan 自动标 error。info 级,无需处理
seedream tool 没出现在对话里 .env 没设 ARK_API_KEY,build_agent 跳过注册。设了重启 web 即可;无需迁移、无需 DB 改动
豆包调价了 config/media/doubao.yamlprice_cny_per_image 一行 → 重启 web。历史 usage_events 不受影响(units jsonb 里有当时单价 snapshot,聚合查仍按旧价);新写入按新价。涨价瞬间到改 YAML 中间这段记账偏低,开发期接受
kill -HUP <pid>/openapi.json 没新接口 uvicorn 不响应 SIGHUP(没装 handler,落 Python 默认终止;Windows 上信号本身无效)。Ubuntu 上用 systemctl restart zcbot,或 unit 加 --reload 让 uvicorn 监听文件自动重起(见"部署"段)。验证:curl -s http://127.0.0.1:8765/openapi.json | python3 -c 'import sys,json;print([p for p in json.load(sys.stdin)["paths"] if "auth" in p])'
systemctl restart zcbot 卡 10s 才退 有 SSE 长连接,uvicorn graceful shutdown 等 in-flight。unit 已设 TimeoutStopSec=10 兜 SIGKILL,正常现象;真急用 systemctl kill -s KILL zcbot
POST /v1/files/rename 返 409 folder has active run(s) 顶层目录被某 running/cancelling 的 task 占用;先 cancel 等流式 done 再 rename
POST /v1/files/rename 返 409 ... 前缀嵌套 改名后会与其他 task 的 working_dir 形成嵌套;换不冲突的 new_name
启动报 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 邮箱不存在 / 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=...(用 .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 邮箱已存在,改 email 或先 DELETE FROM users WHERE email=...(先清该 user 的 tasks)
main.py user addIntegrityError ... users_pkey --user-id 撞已有 UUID,换一个或不传让随机生成
登录页"+ 管理员添加用户"提交后 503 admin create_user disabled ZCBOT_ADMIN_TOKEN env 未设,功能默关。设了 env 重启 web 即可;或临时回退 main.py user add
登录页"+ 管理员添加用户"返 403 invalid admin_token 弹窗里管理员口令栏填错或没复制完整。跟 .envZCBOT_ADMIN_TOKEN 比对(注意末尾空格 / 引号)
改了用户邮箱 / 密码后他登不上 UPDATE users SET email=... 不影响 user_id(行同一行,task 仍归属),用新邮箱登即可;DB 里应存小写(后端 lower() 后查)。改密 UPDATE users SET password_hash=<bcrypt> 同理
/v1/* 全返 401 missing Authorization: Bearer 没拿 token 或没带 header。先 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 + events_url GET 是否 200 + Content-Type 是 text/event-stream;401 → token 过期,logout 重 login
dev.html 显示 "load failed" 立刻回登录页 token 过期或 JWT_SECRET 服务端变了。已自动跳登录页,按上次 tab 重登
dev.html 顶栏出现"连接断开,重连中…(N/3)" SSE 流被切(--reload 重启 / nginx 切换 / 网络抖)。客户端自动重连,1s/2s/4s 退避;新进程已 reaper 标 error 则立即收 done + 卡片末尾"请重发"提示;若服务端还活着会继续看后续 delta(断开期间的丢失,broker 不持久化)

关键路径与文件

  • 入口:main.py(web / db / probe / user)→ core/agent_builder.py::build_agent
  • 核心:core/{agent_builder, loop, session, task, llm, memory, paths}.py + core/storage/{engine,models,utils}.py + db/migrations/
  • 工具:tools/{fs, shell, run_python, skill_tool}.py
  • Web:web/{app.py, auth.py, broker.py, sinks.py} + web/static/dev.html(dev SPA)+ web/static/vendor/(office 预览 jszip/docx-preview/xlsx)
  • 配置: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):
    • workspace/users/<user_id>/.memory/{core.md, extended/} — 跨 task 记忆,FS 永久,dotfile 隔离
    • workspace/users/<user_id>/<working_dir>/ — 工作目录,用户起名,同 working_dir 多 task 共享

维护约定

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