# 运行手册 > 怎么把 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。 --- ## 一次性初始化 ```bash # 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 服务 ```bash # 默认 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 ```bash # 模型能力对账(费 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= # 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 # 撤用户:先清 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 `。 ```bash # 路径 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":"","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_model` 是 `config/media/doubao.yaml` image 段的 variant key(空 → 沿用 yaml 第一个),仅本 run 装配 SeedreamTool 时使用,不入 DB;UI 应 disable send 直到 SSE `done` | 必填 | | `GET /v1/tasks/{id}/events` | SSE 流(`event: ` + `data: `);订阅 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 上传到 `//`;路径不存在自动 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: ` + `data: `):`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。 ### 一次性 ```bash 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` ```ini [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 ``` 启用 + 日常: ```bash 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 次仍失败 → 同上提示,用户重发即可。 ```ini 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`) ```bash # 在服务器上 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=`(§7.5 #1 PG 单独 block 一遍) > 4. lifespan 启动失败会 fail-fast(`RuntimeError: sandbox init failed`),不静默退到 host ### 镜像构建 容器内 uid/gid 与 host `zcbot` 用户必须对齐(bind mount 保 host owner;错配导致 EACCES): ```bash # 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` 消息): ```bash # 启动 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-,STATUS = Up ... # 5 分钟无新消息后 reaper 自动 rm ``` 也可直接起一个测试容器单验 hardening(不依赖 web 进程): ```bash 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` 自动化跑。 ### 部署前置对账 切 `ZCBOT_SANDBOX_BACKEND=docker` 之前跑一次: ```bash sudo -u zcbot .venv/bin/python main.py sandbox check ``` 输出形如 `[ok] / [warn] / [err]` × 5 项 + 汇总 `N/5 passed`,exit code 0=可启动 / 1=有 err 要修。5 项对应:① docker daemon 可达 ② `zcbot-sandbox:latest` 镜像存在 ③ `zcbot-sandbox-net` network 存在(缺也能跑,lifespan 自动 ensure)④ 镜像内 zcbot uid 与 host uid 对齐(错配 → exec 写 `/workspace` 全 EACCES)⑤ `workspace/users/` 所在 fs 类型可 quota。 lifespan 启动时同样会打第 ⑤ 项的 WARN 到 stdout(`[startup] [warn] fs quota ...`), 应用层周期扫描仍生效;**仅外部用户开放前必须把 ⑤ 升级到 OS 层 quota**。 ### 配额硬化(§7.5 #4,外部开放前必做) 应用层磁盘配额能挡常规超额,**但扫描间隙打满共享 fs 拖死同节点**这条硬要 OS 层 quota。`sandbox check` 第 ⑤ 项会探测当前 fs 状态: | 探测结果 | 含义 | 处理 | |---|---|---| | `fs quota: xfs with prjquota on ...` | ok,可直接 `xfs_quota -x` 给 user 加配额 | (无需处理) | | `fs quota: ext4 with project quota on ...` | ok,可 `quota -P` | (无需处理) | | `fs quota: zfs on ...` | ok,在 dataset 层 `zfs set quota=` | (无需处理) | | `fs quota: xfs ... NO prjquota mount option` | fs 支持但 mount 时没启 | 见下方 xfs 步骤 | | `fs quota: ext4 ... NO project quota option` | 同上 | `sudo tune2fs -O project,quota ` + remount | | `fs quota: btrfs ...` | qgroup 配置复杂 | 生产推荐换 xfs 单独分区,或自行验 `btrfs qgroup` | | `fs quota: tmpfs/overlay/... ` | 通常 Docker-in-Docker 或本地 dev | 生产必须挂独立分区 | **xfs 升级步骤(推荐方案)**: ```bash # 1) 确认 workspace 在哪个 mount(假设 /opt 是独立 xfs 分区) findmnt --target /opt/zcbot/workspace # 2) 启用 prjquota(写入 /etc/fstab 让 reboot 后保留) sudo mount -o remount,prjquota /opt # 3) 给某 user 加 project quota( 自定义整数 id,与 user_id 映射建表跟踪) echo "1001 /opt/zcbot/workspace/users/" | sudo tee -a /etc/projects echo "zcbot_:1001" | sudo tee -a /etc/projid sudo xfs_quota -x -c "project -s zcbot_" /opt sudo xfs_quota -x -c "limit -p bhard=10g zcbot_" /opt ``` `` ↔ `user_uuid` 映射手工维护(`/etc/projects` 是数字 id,zcbot 侧需建表追踪; **首期外部开放前补一个 `main.py sandbox quota-set --user-id --gb 10` 子命令** 读写 /etc/projects + 调 xfs_quota,这是 Step 4 / 5 之后真上线前一步,当前不做)。 不做这步等于"软配额 + 信任用户不写满" -- dogfood + 信任同事白名单阶段够用, **外部用户开放是 hard prereq**。 --- ## 故障兜底 | 现象 | 原因 / 处理 | |---|---| | `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。用 `[OK]` / `[ng]` 等 ASCII 标签(见 memory) | | `db upgrade` 报 `column 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 ` 清;② 外部 `--working-dir`(DB 存绝对路径)— 不自动清,避免误删用户外部项目。ROOT 内 + 同 working_dir 无其他 task 引用 + FS 空 → DELETE task 时已自动 rmdir | | Sandbox 容器内 `touch /workspace/x` 报 `Permission 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` | | 启动报 `ZCBOT_SANDBOX_BACKEND=docker but sandbox init failed: ...` | docker daemon 没起 / 用户不在 docker group / network create 失败。先跑 `main.py sandbox check` 看哪一项 err | | `[startup] [warn] fs quota: on ...` | workspace 所在 fs 没启 OS 层 quota。dogfood 阶段忽略;外部用户开放前必须升级 xfs prjquota / ext4 project / zfs(详 RUN.md「配额硬化」段) | | `docker run zcbot-sandbox:latest` 报 `Unable to find image` | 镜像没 build。`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 .` | | 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_PROXY` 或 `curl --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_status` 卡 `running`/`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.yaml` 的 `price_cny_per_image` 一行 → 重启 web。**历史 usage_events 不受影响**(units jsonb 里有当时单价 snapshot,聚合查仍按旧价);新写入按新价。涨价瞬间到改 YAML 中间这段记账偏低,开发期接受 | | `kill -HUP ` 后 `/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 add` 报 `IntegrityError ... uq_users_email` | 邮箱已存在,改 email 或先 `DELETE FROM users WHERE email=...`(先清该 user 的 tasks) | | `main.py user add` 报 `IntegrityError ... users_pkey` | `--user-id` 撞已有 UUID,换一个或不传让随机生成 | | 登录页"+ 管理员添加用户"提交后 503 `admin create_user disabled` | `ZCBOT_ADMIN_TOKEN` env 未设,功能默关。设了 env 重启 web 即可;或临时回退 `main.py user add` | | 登录页"+ 管理员添加用户"返 403 `invalid admin_token` | 弹窗里管理员口令栏填错或没复制完整。跟 `.env` 里 `ZCBOT_ADMIN_TOKEN` 比对(注意末尾空格 / 引号) | | 改了用户邮箱 / 密码后他登不上 | `UPDATE users SET email=...` 不影响 user_id(行同一行,task 仍归属),用新邮箱登即可;DB 里应存小写(后端 lower() 后查)。改密 `UPDATE users SET password_hash=` 同理 | | `/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//.memory/{core.md, extended/}` — 跨 task 记忆,FS 永久,dotfile 隔离 - `workspace/users///` — 工作目录,用户起名,同 working_dir 多 task 共享 --- ## 维护约定 - **改对外行为(CLI 选项 / env / 文件布局)→ 同步本文档**。bug 修不动这个,只动 PROGRESS。 - 故障兜底新增:用过一次的真实坑,写一行,不预测。 - 跟 DESIGN/PROGRESS 边界:DESIGN 写"为什么",PROGRESS 写"做到哪",RUN 写"怎么跑"。