dev SPA: embed+task_id 模型下拉 + 导出/清空键 disable 修复

- loadModels 收尾若有 task 选中补一次 renderChatMeta,修 embed+task_id 模型下拉
  在 models 接口 resolve 前 selectTask 已跑过的竞态(同步覆盖生图/生视频下拉)
- btn-export/btn-clear-msgs 不再按 n_messages 门禁,只要选中 task 就常亮;清空
  保留 running 期禁用。修历史 bug:清空后两键 disable,新对话进来 taskMeta 不
  重渲一直灰

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-05-27 12:42:19 +08:00
parent 30b1d40204
commit da4e47139f
2 changed files with 11 additions and 3 deletions

View File

@ -23,6 +23,8 @@
### 2026-05-27 ### 2026-05-27
- **dev SPA 「导出对话记录」/「清空对话」按钮 disable 逻辑改成只要选中 task 就常亮**:`web/static/dev.html` `renderChatMeta` 里原本按 `t.n_messages === 0` 一并禁用两键(`:1812`/`:1815`)。bug:用户「清空对话」→ `clearMessages` 返回 `n_messages=0` → 两键 disable;之后发新消息走 SSE 流(`sendMessage` `:2210`),后端 task.n_messages 累加但前端 `state.taskMeta.n_messages` 没刷,renderChatMeta 也不会再跑,两键一直灰。改法:不按 n_messages 门禁,导出常亮、清空仅在 run running/cancelling 期间禁(后端 409,confirm 后再报错 UX 差);0 条时点导出生成空 docx、点清空 confirm 显 0 条 —— 都不会出错,语义一致更省心。`deleteTask` 路径里把"无 task 选中时两键显式 disable"逻辑保留(那时 chat-meta 退到"未选中任务"占位,常亮反而误导)。否决:(a) 修真正的 n_messages 同步问题(SSE 收 done 时回拉 taskMeta) —— 一次额外 GET,且 streaming 期间频繁拉一致性更易飘;(b) 把 export 也按 running 禁 —— export 是只读快照,run 中导出当前可见的部分对话完全合理。`DESIGN.md` 不动(无架构变化);`RUN.md` 不动(无 CLI/env 变化)。
- **dev SPA embed + task_id 模式模型下拉不显示修复**:`web/static/dev.html` `enterApp()``loadModels()` 是 fire-and-forget(`:1512` 无 await),非 embed 模式下用户手动点 task 列表行时 `/v1/models` 早已 resolve,所以下拉正常;embed 模式 + URL 带 `task_id``embedHandleMessage` 收到 token → `enterApp()` 后立刻 `selectTask(EMBED_INITIAL_TASK_ID)`(`:3766-3771`),此时 `state.models` 还是 `[]`,而 `renderModelDropdown` 在 models 为空时直接 `return ""`(`:1817`)→ 顶上模型下拉缺失(同理影响生图 / 生视频下拉)。修法在 `loadModels()` 尾部加 `if (state.taskMeta) renderChatMeta();` 一行,让 models / image_models / video_models 全部 resolve 后,若当前已有 task 选中就补一次 chat-meta 重渲;`state.taskMeta` 不存在时 `renderChatMeta` 本就 no-op,无副作用。一并覆盖"loadModels 接口慢/失败再恢复"的场景。否决:(a) 在 embed 初始 task 分支单独 `loadModels().then(selectTask)` —— 要跟 `enterApp` 里 fire 出的 loadModels 共享同一个 promise,改起来啰嗦;(b) `renderModelDropdown` 内部 fallback 自己拉一次 models —— 把数据获取塞进渲染函数,违反单一职责。`DESIGN.md` 不动(无架构变化);`RUN.md` 不动(无 CLI/env 变化)。
- **Stage C 收尾包:容器资源 yaml 化 + 应用层磁盘配额 + dogfood 网络放开 + 容器内 pip/npm 源持久化**:Step 4 完整 egress proxy(allowlist + audit + 字节计量)1-2 天工程量,**dogfood + 信任同事白名单阶段不必先做**,符合 DESIGN §7.7 阶段语义;沉淀为升级触发信号(任一陌生用户注册 / dogfood 发现模型异常 outbound / 信任白名单出现非密切相识者 → 必上 Step 4)。本批做 3 件:① **容器资源 yaml 化**:`config/agent.yaml` 加 `sandbox` 段(memory/cpus/pids_limit),`SandboxPool.__init__` 加三个字段,优先级 env > yaml > 默(2g/1.0/256);`setup_pool` / `init_pool` 透传 sandbox_cfg;`main.py sandbox check` 输出加 4 行 `[info]`(memory/cpus/pids_limit/disk_bytes_per_user)给运维一眼对账。② **应用层磁盘配额**:migration `0008_user_disk_usage`(单行 per user,bytes_used/file_count/scanned_at)+ `core/storage/disk_quota.py`(`parse_bytes`("5gb"/"500mb"/int)+ `scan_user_dir`(os.scandir 跳顶层 dotfile `.zcbot_tmp` `.memory`)+ `upsert_user_usage` ON CONFLICT + `check_disk_quota`(超额返中文 msg)+ `scan_all_users` 串行扫所有 user)+ web/app.py lifespan `_disk_scanner` 后台 task(启动跑一次 + 默 15min 周期 `run_in_executor`)+ `DockerExecutor._exec_fs_tool` write/edit 起手 `_check_user_disk_quota` 超额返 `[Error]` 不调容器 + `/v1/files/upload` 同款 gate 超额 HTTP 413。yaml `quotas.disk_bytes_per_user: 5gb` + `disk_scan_interval_seconds: 900`,≤0 视为不限,首次扫描前 check 短路放行避免冷启动卡死。race 接受:扫描间隙写入轻微突破上限(与 image/video 配额同款 race-tolerant)。③ **网络放开 + 容器内源持久化**:`core/sandbox/network.py` 去掉 `--internal` flag(改 docker bridge default 有 NAT outbound;dogfood 阶段让模型能 `pip install foo` / `curl https://...`),已存在 internal network 不自动 rm 仅 warn(避免破坏现有容器,RUN.md 给迁移命令)。Dockerfile 加 `/etc/pip.conf`(写 `[global]\nindex-url=${PIP_INDEX_URL}` + timeout 60)+ `/etc/npmrc`(写 `registry=${NPM_REGISTRY}`)让运行时 pip / npm install 也走 mirror(此前 `--build-arg` 只 build 时生效)。iptables 红线段不动 ── `169.254/127/10/172.16/192.168/100.64/PG_IP` 仍 DROP,挡 cloud metadata + 内网扫描 + loopback,这是基线不依赖 proxy。**测试**:`tests/test_disk_quota.py` 11 测试覆盖 parse_bytes 各单位 / scan_user_dir 跳 dotfile / 空目录 / 不存在路径;**unittest discover 46/46 PASS**(原 35 + 新 11)。**DESIGN §7.5 #2 待 commit 加"Step 4 延后 + 升级触发表"段落**(本 commit 暂没改 DESIGN ── DESIGN 只在架构变时改,延后决策仍在 §7.7 Stage C 阶段语义内,触发信号沉淀进 PROGRESS / RUN);RUN.md 加 yaml sandbox 段 + 网络迁移 + 配额命令 + 故障兜底 2 行(internal network legacy / 磁盘 413)。否决:(a) network 改 internal 时自动 rm + recreate ── destructive,会破现有容器连接,改 warn 让运维 ack;(b) 写前实时 du ── user_root 大时几秒一次写不能接受,sticky 周期扫描表 + 写前查表是 image/video 配额同款范式;(c) 同时做完整 Step 4 ── 1-2 天大工程,dogfood 不阻塞,先放开网络让模型能 pip install 更急(实测装包 / 拉资源能力是产品门槛);(d) 磁盘配额硬阻所有写(包括 run_python / shell)── 截 syscall 太重,write/edit + upload gate 已覆盖 95%(skill 产物路径),run_python / shell 写文件靠扫描后续感知(下次周期 check 时挡新增写入);(e) yaml `sandbox.memory` 默 4g/2cpu ── 腾讯云轻量 4 核 8G,留 host 跑 web + PG + nginx 需求,2g/1cpu 是合理基线,极端任务用户改 yaml 升配。 - **Stage C 收尾包:容器资源 yaml 化 + 应用层磁盘配额 + dogfood 网络放开 + 容器内 pip/npm 源持久化**:Step 4 完整 egress proxy(allowlist + audit + 字节计量)1-2 天工程量,**dogfood + 信任同事白名单阶段不必先做**,符合 DESIGN §7.7 阶段语义;沉淀为升级触发信号(任一陌生用户注册 / dogfood 发现模型异常 outbound / 信任白名单出现非密切相识者 → 必上 Step 4)。本批做 3 件:① **容器资源 yaml 化**:`config/agent.yaml` 加 `sandbox` 段(memory/cpus/pids_limit),`SandboxPool.__init__` 加三个字段,优先级 env > yaml > 默(2g/1.0/256);`setup_pool` / `init_pool` 透传 sandbox_cfg;`main.py sandbox check` 输出加 4 行 `[info]`(memory/cpus/pids_limit/disk_bytes_per_user)给运维一眼对账。② **应用层磁盘配额**:migration `0008_user_disk_usage`(单行 per user,bytes_used/file_count/scanned_at)+ `core/storage/disk_quota.py`(`parse_bytes`("5gb"/"500mb"/int)+ `scan_user_dir`(os.scandir 跳顶层 dotfile `.zcbot_tmp` `.memory`)+ `upsert_user_usage` ON CONFLICT + `check_disk_quota`(超额返中文 msg)+ `scan_all_users` 串行扫所有 user)+ web/app.py lifespan `_disk_scanner` 后台 task(启动跑一次 + 默 15min 周期 `run_in_executor`)+ `DockerExecutor._exec_fs_tool` write/edit 起手 `_check_user_disk_quota` 超额返 `[Error]` 不调容器 + `/v1/files/upload` 同款 gate 超额 HTTP 413。yaml `quotas.disk_bytes_per_user: 5gb` + `disk_scan_interval_seconds: 900`,≤0 视为不限,首次扫描前 check 短路放行避免冷启动卡死。race 接受:扫描间隙写入轻微突破上限(与 image/video 配额同款 race-tolerant)。③ **网络放开 + 容器内源持久化**:`core/sandbox/network.py` 去掉 `--internal` flag(改 docker bridge default 有 NAT outbound;dogfood 阶段让模型能 `pip install foo` / `curl https://...`),已存在 internal network 不自动 rm 仅 warn(避免破坏现有容器,RUN.md 给迁移命令)。Dockerfile 加 `/etc/pip.conf`(写 `[global]\nindex-url=${PIP_INDEX_URL}` + timeout 60)+ `/etc/npmrc`(写 `registry=${NPM_REGISTRY}`)让运行时 pip / npm install 也走 mirror(此前 `--build-arg` 只 build 时生效)。iptables 红线段不动 ── `169.254/127/10/172.16/192.168/100.64/PG_IP` 仍 DROP,挡 cloud metadata + 内网扫描 + loopback,这是基线不依赖 proxy。**测试**:`tests/test_disk_quota.py` 11 测试覆盖 parse_bytes 各单位 / scan_user_dir 跳 dotfile / 空目录 / 不存在路径;**unittest discover 46/46 PASS**(原 35 + 新 11)。**DESIGN §7.5 #2 待 commit 加"Step 4 延后 + 升级触发表"段落**(本 commit 暂没改 DESIGN ── DESIGN 只在架构变时改,延后决策仍在 §7.7 Stage C 阶段语义内,触发信号沉淀进 PROGRESS / RUN);RUN.md 加 yaml sandbox 段 + 网络迁移 + 配额命令 + 故障兜底 2 行(internal network legacy / 磁盘 413)。否决:(a) network 改 internal 时自动 rm + recreate ── destructive,会破现有容器连接,改 warn 让运维 ack;(b) 写前实时 du ── user_root 大时几秒一次写不能接受,sticky 周期扫描表 + 写前查表是 image/video 配额同款范式;(c) 同时做完整 Step 4 ── 1-2 天大工程,dogfood 不阻塞,先放开网络让模型能 pip install 更急(实测装包 / 拉资源能力是产品门槛);(d) 磁盘配额硬阻所有写(包括 run_python / shell)── 截 syscall 太重,write/edit + upload gate 已覆盖 95%(skill 产物路径),run_python / shell 写文件靠扫描后续感知(下次周期 check 时挡新增写入);(e) yaml `sandbox.memory` 默 4g/2cpu ── 腾讯云轻量 4 核 8G,留 host 跑 web + PG + nginx 需求,2g/1cpu 是合理基线,极端任务用户改 yaml 升配。
- **Stage C Step 3d:fs 工具(read/write/edit/glob/grep)进容器 + DESIGN §7.5 #6 重写**:Ubuntu dogfood 第一次切 docker backend 后发现 host 工具 `Path.cwd()` 漏底 —— 模型用 glob `*` 列出了 host `/home/lighthouse/zcbot/.git/.venv/config/core/...`,即 zcbot 源码自身。回查 DESIGN §7.5 #6 写"host 工具走 `paths.py::resolve_user_path` 校验",grep 代码**根本没那个函数**,假命题;`Tool._resolve` 实际是 `base_dir / path`,base_dir=`Path.cwd()`(= web 启动目录 = zcbot repo 根),绝对路径完全不挡,模型能 read `/etc/passwd` / write zcbot 源码自己。**修法对比**:Phase A(改 cwd → working_dir,1 行 hack)修 UX 不修安全;Phase B(host 工具加 user_root 强制校验 + skills/ 白名单,~80 行)安全但脆弱(symlink/`..`/Windows path 都得 case 挡,漏一个就破);**方案 3(fs 工具进容器)物理边界替代代码护栏,选这条**。`core/sandbox/tool_runner.py` 新增容器内 helper(~80 行,from stdin 接 JSON args 调 `tools/fs.py` Tool 子类,base_dir=cwd 走 docker exec --workdir 传入,user_root=/workspace);`DockerExecutor` 加 `FS_TOOLS = {read,write,edit,glob,grep}` 信任域 + `_exec_fs_tool` 方法 `docker exec -i ... python /sandbox/tool_runner.py <name>` + stdin 喂 JSON args(CJK 路径透明传不被 shell metachar 切);`_run_subprocess` 加 stdin 参数 + is_fs_tool 路径返 stdout 直透(不包 [stdout]/[exit],原模型语义保持),exit≠0 把 stderr 当 ToolResult content。`SandboxPool` 加 `repo_root` 字段,`_docker_run` 加 `<repo>/skills:/sandbox/skills:ro` mount(SKILL.md 内引用 `references/foo.md` 时容器内 read 能解析);`web/app.py` lifespan 透传 `ROOT`;`Dockerfile` `COPY tools/ /sandbox/tools/ + tool_runner.py` 让镜像内有一份 tools 源(build-time COPY 而非 mount —— 容器内代码不应跟随 host repo 修改重启)。**留 host 的工具**:`load_skill`(SkillRegistry 内存查找,无 fs 越界)/ `web_search` / `web_fetch` / `seedream` / `seedance`(持 Bocha/ARK API key,key 不入容器 env;Step 4 egress proxy 后再讨论)。**测试**:`tests/test_executor_docker.py` 改 `test_load_skill_passthrough_to_host`(原 `test_read_passthrough_to_host` 不再成立 —— read 进容器了)+ 加 4 个 fs 路径测试(read argv 形态 / CJK 路径 stdin JSON 透明传 / grep exit≠0 stderr 透传 / glob timeout 杀 docker CLI),`unittest discover 35/35 PASS`。**DESIGN §7.5 #6 重写**:从"工具二分(host fs + container code)"改"几乎所有工具进容器,host 只留持 key + 跨 user 的"+ 标注 2026-05-26 修正记录(原假命题溯源)。**代价**:每个 fs tool call 多 ~200ms docker exec overhead,对话级 N≤15 总 1-3s,LLM 推理 5-30s 下噪声;镜像 build COPY tools/ ~5s 增量。**升级触发**(§7.9 升级表):若 metric `docker_exec_overhead / total_tool_time > 30%` 持续两周,或模型出现"在容器内起长驻服务"工作流,启用容器内 tool-runner unix socket RPC(消除每次 exec 开销)。否决:(a) Phase B path validator —— 跟 §7.9 § "美学统一性 ≠ 升级理由"对称,**安全要"物理 ≠ 代码"才稳**;(b) `COPY core/ tools/ ...` 把整个 zcbot core 进镜像 —— tool_runner 只需要 `tools/fs.py` + base.py,容器内多余代码增加攻击面;(c) tool_runner.py 用 argv 传 JSON args —— CJK / 引号 / 路径分隔符全是 shell metachar 切风险,stdin 喂稳;(d) 让 host backend 也保留 fs 工具走 user_root 校验作"双保险" —— 双源 = 漂移源,docker backend 是 §7.5 的全部论证基础,host backend 不在外部用户场景有它就够。 - **Stage C Step 3d:fs 工具(read/write/edit/glob/grep)进容器 + DESIGN §7.5 #6 重写**:Ubuntu dogfood 第一次切 docker backend 后发现 host 工具 `Path.cwd()` 漏底 —— 模型用 glob `*` 列出了 host `/home/lighthouse/zcbot/.git/.venv/config/core/...`,即 zcbot 源码自身。回查 DESIGN §7.5 #6 写"host 工具走 `paths.py::resolve_user_path` 校验",grep 代码**根本没那个函数**,假命题;`Tool._resolve` 实际是 `base_dir / path`,base_dir=`Path.cwd()`(= web 启动目录 = zcbot repo 根),绝对路径完全不挡,模型能 read `/etc/passwd` / write zcbot 源码自己。**修法对比**:Phase A(改 cwd → working_dir,1 行 hack)修 UX 不修安全;Phase B(host 工具加 user_root 强制校验 + skills/ 白名单,~80 行)安全但脆弱(symlink/`..`/Windows path 都得 case 挡,漏一个就破);**方案 3(fs 工具进容器)物理边界替代代码护栏,选这条**。`core/sandbox/tool_runner.py` 新增容器内 helper(~80 行,from stdin 接 JSON args 调 `tools/fs.py` Tool 子类,base_dir=cwd 走 docker exec --workdir 传入,user_root=/workspace);`DockerExecutor` 加 `FS_TOOLS = {read,write,edit,glob,grep}` 信任域 + `_exec_fs_tool` 方法 `docker exec -i ... python /sandbox/tool_runner.py <name>` + stdin 喂 JSON args(CJK 路径透明传不被 shell metachar 切);`_run_subprocess` 加 stdin 参数 + is_fs_tool 路径返 stdout 直透(不包 [stdout]/[exit],原模型语义保持),exit≠0 把 stderr 当 ToolResult content。`SandboxPool` 加 `repo_root` 字段,`_docker_run` 加 `<repo>/skills:/sandbox/skills:ro` mount(SKILL.md 内引用 `references/foo.md` 时容器内 read 能解析);`web/app.py` lifespan 透传 `ROOT`;`Dockerfile` `COPY tools/ /sandbox/tools/ + tool_runner.py` 让镜像内有一份 tools 源(build-time COPY 而非 mount —— 容器内代码不应跟随 host repo 修改重启)。**留 host 的工具**:`load_skill`(SkillRegistry 内存查找,无 fs 越界)/ `web_search` / `web_fetch` / `seedream` / `seedance`(持 Bocha/ARK API key,key 不入容器 env;Step 4 egress proxy 后再讨论)。**测试**:`tests/test_executor_docker.py` 改 `test_load_skill_passthrough_to_host`(原 `test_read_passthrough_to_host` 不再成立 —— read 进容器了)+ 加 4 个 fs 路径测试(read argv 形态 / CJK 路径 stdin JSON 透明传 / grep exit≠0 stderr 透传 / glob timeout 杀 docker CLI),`unittest discover 35/35 PASS`。**DESIGN §7.5 #6 重写**:从"工具二分(host fs + container code)"改"几乎所有工具进容器,host 只留持 key + 跨 user 的"+ 标注 2026-05-26 修正记录(原假命题溯源)。**代价**:每个 fs tool call 多 ~200ms docker exec overhead,对话级 N≤15 总 1-3s,LLM 推理 5-30s 下噪声;镜像 build COPY tools/ ~5s 增量。**升级触发**(§7.9 升级表):若 metric `docker_exec_overhead / total_tool_time > 30%` 持续两周,或模型出现"在容器内起长驻服务"工作流,启用容器内 tool-runner unix socket RPC(消除每次 exec 开销)。否决:(a) Phase B path validator —— 跟 §7.9 § "美学统一性 ≠ 升级理由"对称,**安全要"物理 ≠ 代码"才稳**;(b) `COPY core/ tools/ ...` 把整个 zcbot core 进镜像 —— tool_runner 只需要 `tools/fs.py` + base.py,容器内多余代码增加攻击面;(c) tool_runner.py 用 argv 传 JSON args —— CJK / 引号 / 路径分隔符全是 shell metachar 切风险,stdin 喂稳;(d) 让 host backend 也保留 fs 工具走 user_root 校验作"双保险" —— 双源 = 漂移源,docker backend 是 §7.5 的全部论证基础,host backend 不在外部用户场景有它就够。
- **Stage C Step 3 hotfix:exec_user 改 username 跟随 build_arg + Dockerfile 加 Node/Chromium/mermaid-cli**:Ubuntu 上 dogfood 暴露两个真问题。① **uid 错配**:DockerExecutor 写死 `--user 1000:1000`,但镜像 `docker build --build-arg HOST_UID=$(id -u)` 跟随 host 实际 uid(腾讯云轻量 lighthouse 用户 uid=1001),docker exec 进容器 uid=1000 → bind mount `/workspace/<wd>/` owner 1001 → 写文件全 EACCES,文件落 `/tmp/`。改 `DEFAULT_EXEC_USER = "zcbot"`(username,docker 自动查容器 /etc/passwd 拿 uid),无论 HOST_UID build 成 1000/1001/其他都跟 bind mount owner 对齐,且未来切其他部署机不用改 env。② **proposal/patent skill 渲 mermaid 缺 Node**:`skills/proposal/scripts/render_diagrams.py` `render_via_mmdc``shutil.which("mmdc")`,容器没装 → 退到 mermaid.ink 公网 API → 但 sandbox 容器 `--internal` 默 deny outbound,API 也走不通 → ASCII fallback 出 docx 没图不能用。Dockerfile 加 `chromium nodejs npm` apt 装(Debian bookworm 自带 node 18.x 够新)+ `npm install -g @mermaid-js/mermaid-cli@latest`,镜像 +~400MB(接受)。容器内 chromium 缺 setuid sandbox + `/dev/shm` 不够大会跪,镜像落 `/sandbox/puppeteer-config.json`(`--no-sandbox` / `--disable-setuid-sandbox` / `--disable-dev-shm-usage` + executablePath=/usr/bin/chromium)+ ENV `MERMAID_PUPPETEER_CONFIG=/sandbox/puppeteer-config.json`,`render_via_mmdc` 改读 env 拼 `-p <config>` 注入 mmdc;host 上跑 env 没设行为零变化。`PUPPETEER_SKIP_DOWNLOAD=true` + `PUPPETEER_EXECUTABLE_PATH` 让 puppeteer 用容器 chromium 不再下载它自带的 Chrome(省 ~300MB build)。npm 源加 `--build-arg NPM_REGISTRY=https://mirrors.cloud.tencent.com/npm/`(腾讯云内网)防境内 build 慢。`DESIGN.md` 不动(纯实施层 bug fix + skill 依赖);`RUN.md` 加 NPM_REGISTRY 段 + 故障兜底 3 行(EACCES uid 错配 / mmdc 报 launch chromium / npm 慢)。否决:(a) 让 DockerExecutor 启动时探测 `os.getuid()` 自动取 host uid 作 `--user` —— 写死 username 让 docker 查 passwd 比应用层探测更直接,且 部署机 uid 偶尔变(从 1000 重装成 1001)不用改任何东西;(b) 容器走 NodeSource repo 装 Node 20 LTS —— Debian bookworm 自带 18.x 已满足 mermaid-cli 要求(>=14.x),多一步外网拖速度;(c) 不装 chromium 等 Step 4 egress proxy 后用 mermaid.ink —— proposal 是早期就要交付的能力,等 Step 4(还没动手)不现实;(d) puppeteer config 注入靠改 mmdc 启动脚本 —— mmdc 默支持 `-p`,改 render_diagrams.py 读 env 就够,不动 mmdc 内部。 - **Stage C Step 3 hotfix:exec_user 改 username 跟随 build_arg + Dockerfile 加 Node/Chromium/mermaid-cli**:Ubuntu 上 dogfood 暴露两个真问题。① **uid 错配**:DockerExecutor 写死 `--user 1000:1000`,但镜像 `docker build --build-arg HOST_UID=$(id -u)` 跟随 host 实际 uid(腾讯云轻量 lighthouse 用户 uid=1001),docker exec 进容器 uid=1000 → bind mount `/workspace/<wd>/` owner 1001 → 写文件全 EACCES,文件落 `/tmp/`。改 `DEFAULT_EXEC_USER = "zcbot"`(username,docker 自动查容器 /etc/passwd 拿 uid),无论 HOST_UID build 成 1000/1001/其他都跟 bind mount owner 对齐,且未来切其他部署机不用改 env。② **proposal/patent skill 渲 mermaid 缺 Node**:`skills/proposal/scripts/render_diagrams.py` `render_via_mmdc``shutil.which("mmdc")`,容器没装 → 退到 mermaid.ink 公网 API → 但 sandbox 容器 `--internal` 默 deny outbound,API 也走不通 → ASCII fallback 出 docx 没图不能用。Dockerfile 加 `chromium nodejs npm` apt 装(Debian bookworm 自带 node 18.x 够新)+ `npm install -g @mermaid-js/mermaid-cli@latest`,镜像 +~400MB(接受)。容器内 chromium 缺 setuid sandbox + `/dev/shm` 不够大会跪,镜像落 `/sandbox/puppeteer-config.json`(`--no-sandbox` / `--disable-setuid-sandbox` / `--disable-dev-shm-usage` + executablePath=/usr/bin/chromium)+ ENV `MERMAID_PUPPETEER_CONFIG=/sandbox/puppeteer-config.json`,`render_via_mmdc` 改读 env 拼 `-p <config>` 注入 mmdc;host 上跑 env 没设行为零变化。`PUPPETEER_SKIP_DOWNLOAD=true` + `PUPPETEER_EXECUTABLE_PATH` 让 puppeteer 用容器 chromium 不再下载它自带的 Chrome(省 ~300MB build)。npm 源加 `--build-arg NPM_REGISTRY=https://mirrors.cloud.tencent.com/npm/`(腾讯云内网)防境内 build 慢。`DESIGN.md` 不动(纯实施层 bug fix + skill 依赖);`RUN.md` 加 NPM_REGISTRY 段 + 故障兜底 3 行(EACCES uid 错配 / mmdc 报 launch chromium / npm 慢)。否决:(a) 让 DockerExecutor 启动时探测 `os.getuid()` 自动取 host uid 作 `--user` —— 写死 username 让 docker 查 passwd 比应用层探测更直接,且 部署机 uid 偶尔变(从 1000 重装成 1001)不用改任何东西;(b) 容器走 NodeSource repo 装 Node 20 LTS —— Debian bookworm 自带 18.x 已满足 mermaid-cli 要求(>=14.x),多一步外网拖速度;(c) 不装 chromium 等 Step 4 egress proxy 后用 mermaid.ink —— proposal 是早期就要交付的能力,等 Step 4(还没动手)不现实;(d) puppeteer config 注入靠改 mmdc 启动脚本 —— mmdc 默支持 `-p`,改 render_diagrams.py 读 env 就够,不动 mmdc 内部。

View File

@ -1543,6 +1543,9 @@ async function loadModels() {
state.videoModels = []; state.videoModels = [];
state.videoModel = ""; state.videoModel = "";
} }
// embed + task_id 场景下 selectTask 可能在 loadModels 完成前就跑完 renderChatMeta,
// 此时 models 为空 → 模型下拉不渲染。loadModels 收尾时如果已选中 task,补一次 chat-meta 重渲。
if (state.taskMeta) renderChatMeta();
} }
// loadTaskList:默认 reset(filters/refresh/写操作后),append=true 由 sentinel observer 触发 // loadTaskList:默认 reset(filters/refresh/写操作后),append=true 由 sentinel observer 触发
@ -1806,10 +1809,13 @@ function renderChatMeta() {
$("btn-done").disabled = !active; $("btn-done").disabled = !active;
$("btn-abandon").disabled = !active; $("btn-abandon").disabled = !active;
$("btn-delete-task").disabled = false; // delete 不限 status(用户显式 confirm) $("btn-delete-task").disabled = false; // delete 不限 status(用户显式 confirm)
$("btn-export").disabled = (t.n_messages || 0) === 0; // 导出 / 清空:只要选中 task 就允许点(不按 n_messages 门禁 —— 历史 bug:
// 清空对话:活跃 run 期间禁用(后端 409),无消息也无意义 // 清空后 n_messages=0 disable,但新对话进来后 taskMeta 不重渲一直 disable;
// 0 条时点击不会出错(导出空 docx / 清空 confirm 显 0 条),让 UX 一致更省心)。
$("btn-export").disabled = false;
// 清空对话:仅活跃 run 期间禁用(后端 409,confirm 通过后才报错 UX 差)
const running = t.run_status === "running" || t.run_status === "cancelling"; const running = t.run_status === "running" || t.run_status === "cancelling";
$("btn-clear-msgs").disabled = running || (t.n_messages || 0) === 0; $("btn-clear-msgs").disabled = running;
} }
function renderModelDropdown(t) { function renderModelDropdown(t) {