diff --git a/PROGRESS.md b/PROGRESS.md
index 78c8656..d2db827 100644
--- a/PROGRESS.md
+++ b/PROGRESS.md
@@ -23,6 +23,8 @@
### 2026-06-02
+- **`resolve_workspace` 加 env 覆盖 `ZCBOT_WORKSPACE_DIR`(per-host 部署,不碰共用 yaml)**:prod 想把重写入的 `workspace/users/` 落到独立数据盘(1T xfs prjquota,空间 + OS 层配额一步到位),但 `config/agent.yaml` 的 `workspace_dir` 是 dev/prod 共用提交的,改成绝对路径会带歪 dev。改法:`core/agent_builder.py:resolve_workspace` 优先级改为 **显式 arg > env `ZCBOT_WORKSPACE_DIR` > cfg `workspace_dir` > 默 `workspace`**,env/cfg 值都 `ROOT / ws`(POSIX 上绝对右操作数覆盖左 → 绝对路径直接生效,相对挂 repo 根)。prod systemd 设 `Environment=ZCBOT_WORKSPACE_DIR=/data/zcbot-workspace`,dev 不设照旧。PG 暂不迁(元数据库小,留默认 `/var/lib/postgresql` 少坑,等真涨到 30–40G 再说)。**对外行为(env 变量)变化 → 更 RUN.md**:env 段加 `ZCBOT_WORKSPACE_DIR`、新增「workspace 落独立数据盘」段(整盘 mkfs.xfs + fstab prjquota + rsync 迁移 + systemd env)、故障表加一行。DESIGN 不动(无架构/schema 变化)。
+
- **修 embed 模式"登录页一闪而过"(绘制时机,非鉴权)**:`web/static/dev.html` 的 `#login` 默认 `display:flex` 且带 `login-in .35s` 动画,而加 `body.embed-mode`(→ CSS 隐藏 `#login`)的 `embedInit()` 在 body 末尾才跑;单文件 3800+ 行,浏览器常在解析到底部脚本前就先把登录卡画出来 → 闪一下。改法:在 `
` 第一行加一段同步内联脚本,`?embed=1` 时立即 `document.body.classList.add("embed-mode")`,赶在 `#login` 解析/绘制之前隐藏它 → 根本不绘制。只是"绘制闸门",底部 `embedInit()`(postMessage 握手 / `embed-waiting` 覆盖层 / token 分支)完全不动,`embed-mode` 幂等。未提前加 `embed-waiting`(有 stored token 时 `embedInit` 走 `enterApp` 不移除等待层会卡死,故等待层决定仍留底部按 token 判)。**bug 修复,DESIGN 不动;URL 参数/命令/env 无变化,RUN 不动**。
### 2026-06-01
diff --git a/RUN.md b/RUN.md
index 2cf294f..e8f6c41 100644
--- a/RUN.md
+++ b/RUN.md
@@ -372,6 +372,14 @@ sudo -u zcbot docker network create --internal zcbot-sandbox-net
# PG 实际 IP,逗号分隔。defense-in-depth ── 即便落内网三段(§7.5 #1),
# init.sh 再加一遍 DROP 规则。生产部署必填。
ZCBOT_PG_IPS=10.1.2.3,10.1.2.4
+
+# workspace 根目录(per-host 覆盖)。来源只有两个:这条 env,或 yaml `workspace_dir`
+# (默值 = `workspace`);env 设了就用 env,没设就用 yaml 那条。两者都按 `ROOT/<值>` 解析
+# (绝对路径直接生效,相对路径挂 repo 根下)。所以**不设这条 env → 走 yaml 的
+# workspace_dir=workspace → ROOT/workspace(即 zcbot 下的 workspace)**,dev 维持原样。
+# prod 设成数据盘绝对路径,把重写入的 user 子树落过去(下方「workspace 落独立数据盘」段),
+# 不碰提交进仓库的 agent.yaml,两边不抢同一份配置。
+# ZCBOT_WORKSPACE_DIR=/data/zcbot-workspace
```
### 验证
@@ -544,6 +552,45 @@ uid 与 host uid 对齐(错配 → exec 写 `/workspace` 全 EACCES)⑤ `workspa
lifespan 启动时同样会打第 ⑤ 项的 WARN 到 stdout(`[startup] [warn] fs quota ...`),
应用层周期扫描仍生效;**仅外部用户开放前必须把 ⑤ 升级到 OS 层 quota**。
+### workspace 落独立数据盘(prod,大空间 + quota fs)
+
+prod 的 `workspace/users//` 是重写入区(报告 / 图 / pptx 等大件落这,DB 只存元数据)。
+推荐挂一块独立数据盘(xfs prjquota),空间和 OS 层配额一步到位。**dev 不动** —— 走提交进
+仓库的相对 `workspace_dir`(= `ROOT/workspace`);prod 在 systemd unit 里设 env
+`ZCBOT_WORKSPACE_DIR` 指到数据盘,两边不抢同一份 `agent.yaml`。
+
+PG 不必跟着搬:它是元数据库,长期个位数~几十 G,根盘够用;留默认 `/var/lib/postgresql//main`
+更省坑(`pg_ctlcluster` / AppArmor 按标准路径来)。等 `pg_database_size` 真奔 30–40G、根盘紧了
+再迁,那时 `/data` 下加个 `postgresql/` 子目录布局照样兼容。
+
+```bash
+# 假设新盘是整块裸盘 /dev/vdb(lsblk 看,无分区表直接整盘格,数据盘惯例)
+# 0) 停服务(迁移时别再写 workspace)
+sudo systemctl stop zcbot
+
+# 1) 整盘格 xfs
+sudo mkfs.xfs /dev/vdb
+
+# 2) 写 fstab(UUID + prjquota),挂 /data
+sudo mkdir -p /data
+UUID=$(sudo blkid -s UUID -o value /dev/vdb)
+echo "UUID=$UUID /data xfs defaults,prjquota 0 0" | sudo tee -a /etc/fstab
+sudo mount -a
+findmnt -no FSTYPE,OPTIONS /data # 期望:xfs ... prjquota
+
+# 3) 迁现有 workspace 数据(prod 若 workspace 还空则跳过 rsync),owner 对齐跑服务的账号
+sudo rsync -aXS /home/ubuntu/zcbot/workspace/ /data/zcbot-workspace/
+sudo chown -R ubuntu:ubuntu /data/zcbot-workspace # uid 必须 == 容器内 zcbot(HOST_UID)
+
+# 4) systemd unit 加 env 指过去(Environment= 或 EnvironmentFile 的 .env),重启
+# Environment=ZCBOT_WORKSPACE_DIR=/data/zcbot-workspace
+sudo systemctl daemon-reload && sudo systemctl start zcbot
+python3 main.py sandbox check # fs quota 那项应变 [ok]
+```
+
+确认 `sandbox check` 通过、新 task 文件确实落 `/data/zcbot-workspace/users/...` 后,再删旧
+`/home/ubuntu/zcbot/workspace`。
+
### 配额硬化(§7.5 #4,外部开放前必做)
应用层磁盘配额能挡常规超额,**但扫描间隙打满共享 fs 拖死同节点**这条硬要 OS 层
@@ -598,6 +645,7 @@ sudo xfs_quota -x -c "limit -p bhard=10g zcbot_" /opt
| 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「配额硬化」段) |
+| prod 想把 workspace 落独立数据盘,但 `agent.yaml` 是 dev/prod 共用提交的 | 别改 `workspace_dir`(会带歪 dev)。prod systemd 设 env `ZCBOT_WORKSPACE_DIR=/data/zcbot-workspace`(优先级 env > yaml > 默)。详「workspace 落独立数据盘」段 |
| `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 .` |
| 镜像 build pip 报 `ReadTimeoutError: HTTPSConnectionPool(host='files.pythonhosted.org', ...)` | 境内访问 PyPI 抖动。加 `--build-arg PIP_INDEX_URL=https://mirrors.cloud.tencent.com/pypi/simple/`(腾讯云内网)或阿里云 / 清华源,详 RUN.md「镜像构建」段。Dockerfile 已把 pip timeout 拉到 60s,主因仍是源不通而非超时 |
| pip 报 `Could not find a version that satisfies the requirement litellm>=1.83.0`(伴随一串 `Ignored ... yanked versions: 0.1.xxxx`) | 用的镜像源同步滞后,没有该新版本。**阿里 PyPI 一度只到 litellm 1.82.6** —— update.sh 默认已改腾讯源(到 1.88)。若手动 build 撞到:换 `PIP_INDEX_URL=https://mirrors.cloud.tencent.com/pypi/simple/` 或清华源。那串 `0.1.xxxx` 是 litellm 远古版本,纯干扰信息 |
diff --git a/core/agent_builder.py b/core/agent_builder.py
index d59f43b..ba5fa46 100644
--- a/core/agent_builder.py
+++ b/core/agent_builder.py
@@ -17,6 +17,7 @@ state.json 已删除(元数据全在 PG)。
"""
from __future__ import annotations
+import os
from datetime import datetime
from pathlib import Path
from typing import Callable, Optional, Tuple
@@ -94,8 +95,19 @@ def _resolve_executor(
def resolve_workspace(workspace: Optional[str], cfg: Optional[dict] = None) -> Path:
+ """workspace 根解析,优先级:显式 arg > env `ZCBOT_WORKSPACE_DIR` > cfg `workspace_dir` > "workspace"。
+
+ env 覆盖给 per-host 部署用 —— prod 在 systemd 里设 `ZCBOT_WORKSPACE_DIR=/data/zcbot-workspace`
+ 把重写入的 user 子树落到独立数据盘(xfs prjquota),dev 不设就吃提交进仓库的相对
+ `workspace_dir`(= ROOT/workspace),两边不抢同一份 yaml。env / cfg 值绝对相对都行:
+ `ROOT / "/abs"` 在 POSIX 上即 "/abs"(绝对右操作数覆盖左),相对则挂在 repo 根下。
+ """
cfg = cfg or load_config()
- p = Path(workspace) if workspace else ROOT / cfg.get("workspace_dir", "workspace")
+ if workspace:
+ p = Path(workspace)
+ else:
+ ws = os.getenv("ZCBOT_WORKSPACE_DIR") or cfg.get("workspace_dir", "workspace")
+ p = ROOT / ws
p.mkdir(parents=True, exist_ok=True)
return p