Files
dsh_ai1net_server/.workbuddy/memory/2026-09-下.md
T
admin ce8e6ceed9 chore(工作区): 纳入版本控制基线(回收 411 MB 过程产物)
回收 411 MB(470 M → 58.8 M),全部经回收站,可恢复:
- 待清理/(146.2 M,含 relay 分片 128 M 与 42 项过程目录)
- tmp/(32.4 M,按接续棒命名的过程临时区)
- .workbuddy/tmp/(39.5 M)
- 4 份 workbuddy.db 冗余副本(101 M,09-23 事故的坏副本 / 抢救产物)
- tmp/im16/gw/centrifugo 二进制(63.9 M,可重下)+ 缓存残留

入库范围:常驻规则(CODEBUDDY.md / README.md / state.py)、在途接续入口与
接续包、docs/、交付物/、交接单/、归档/、scripts/、.codebuddy/、
.workbuddy/memory/;共 398 件,其中 >60 KB 的 26 件全为文档。

排除(.gitignore):tmp/、待清理/、运行态日志与缓存、*.db 与 DB 备份整目录、
打包二进制(*.tar.gz / *.tgz)、记忆修复前备份。
2026-09-24 07:51:03 +08:00

462 lines
49 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 工作日志 · 2026-09(第 2 片)
> ⚠️ **本目录日志已按【月】分片**(2026-09-15 用户定,单文件 ≤50 KB):同月续片 `2026-09.md` / `2026-09-下.md` / `2026-09-下2.md` / `2026-09-下3.md` / `2026-09-下4.md` / `2026-09-下5.md` / `2026-09-下6.md` / `2026-09-下7.md` / `2026-09-下8.md` / `2026-09-下9.md` / `2026-09-下10.md` / `2026-09-下11.md` / `2026-09-下12.md` / `2026-09-下13.md` / `2026-09-下14.md` / `2026-09-下15.md` / `2026-09-下16.md` / `2026-09-下17.md` / `2026-09-下18.md` / `2026-09-下19.md` / `2026-09-下20.md` / `2026-09-下21.md` / `2026-09-下22.md` / `2026-09-下23.md`
> **写入约定**:一律 append 到**当月最后一片**;该片超 50 KB ⇒ 新建 `2026-09-下N.md`;⛔ **不要再按日新建 `2026-09-DD.md`**。
> **覆盖来源**:2026-09-10.md、2026-09-11.md
> **上一片**:`2026-09.md` **下一片**:`2026-09-下2.md`
---
## 档案 21 续二:刷新载入慢 = 930KB bundle 未压缩(23:12-23:20)
**新症状**:刷新后载入历史会话要**几分钟**,体感网络卡。
**取证**(nginx 日志 4000 行窗口内 guest 子域 823 请求):
- **单次刷新要下载 `GET /plugins/??<45 个 client.js 拼接>&rev=<hash>` = 实发 980 KB / 930 KB**(含 `dsh-client-hmr`、`typert-registry`、`api-gateway`、~30 个 `dsh-client-ui-*`),另有 `/assets/vendor-*.js` 209 KB
- 小 API 轮询密集:agentPresets/list 131、commands/list 105、modelCatalog 78、syncInspectManifest 76、subagents/list 73、session/list 68…
**根因**:① 全局 `gzip_proxied` 只列了 `expired no-cache no-store private auth`,**不含 `any`** → 反代响应多数不压缩(930 KB 原样下发;若压缩应是 ~250 KB 量级)② 哈希资源(`/assets/*`)无长缓存 → 每次刷新重下 ~1.2 MB。叠加 200+ 次小请求 RTT,在差链路上就是"几分钟"。
**优化(已执行)**:两个 443 站点块的 `location /` 加 **`gzip_proxied any;`**;新增 `location ~* ^/assets/` → 同代理参数 + `gzip_proxied any` + **`expires 30d`**。备份 `*.bak-YYYYMMDD-HHMM-pre-gzip`;`nginx -t` 通过 → reload → 生效(`gzip_proxied any` ×4、assets 块 ×2)→ 主域 200。
**预期**:930 KB → ~250 KB;重复刷新走缓存。**验证**:下次该请求的 `$body_bytes_sent`。
**踩坑(工具)**:nginx 日志用 awk 按空格切字段会被 UA 里的空格/数字带偏(得出"耗时=0"的假结论);改用 python 正则解析器 `_中间产物_待清理/nginx-slow.py`(按 `"请求行" 状态 字节` 抓)。
**待**:用户硬刷新(Cmd/Ctrl+Shift+R)后复验体感;若仍慢 → 查该机链路(测速/VPN)与客户端轮询放大效应。文档双端 **47/47** 一致 → 提交 `ede8156` 推送。
## 域名切换请求侦察:evertwins.com(23:04-23:12,未改动任何配置)
**用户请求**:把访问域名改为 evertwins.com。**侦察结论:3 个前置条件在服务器之外,暂不能执行**。
**硬事实(实测)**:
1. **evertwins.com 现解析到 AWS**(76.223.54.146 / 13.248.169.48,AWS Global Accelerator 段),**不指向本机 47.77.182.89** → 之前的 "Host: evertwins.com 880 次" 是扫描器伪造 Host 打到本机,不是该域名真指向我们
2. **CF token 仅覆盖 alotbuy.com 单 zone**(token 有效,zones 列表只有 alotbuy.com;查 `?name=evertwins.com` 命中 0)→ **无法用现有凭据签发 `*.evertwins.com` 通配证书**(通配必须 DNS-01)
3. 现证书仅 `*.dsh.alotbuy.com` + apex(Let's Encrypt,有效至 2026-12-07);certbot 走 `authenticator = dns-cloudflare`,凭据 `/etc/cloudflare.ini`(600)
4. 编排器 `DSHS_BASE_DOMAIN` / `COOKIE_DOMAIN` 是**单值**;用户子域 = `<username>.<baseDomain>`(`subdomainForUser`),CORS 白名单亦基于它(`server.ts:47 isAllowedOrigin`)→ **不支持双域名并存**,并存需改代码
5. ⚠️ **中国大陆 Aliyun ECS 关键约束**:域名解析到国内机器并走 80/443 需 **ICP 备案**(alotbuy.com 显然已备),evertwins.com 未确认备案 → 解析过来也可能被拦
**需用户先办**:① 确认拥有/可改 evertwins.com DNS;② 确认备案(或接入阿里云);③ 证书路径三选一(加入同一 CF 账号 / 提供当前 DNS 商 API 凭据 / 非通配 HTTP-01(不推荐))
**待用户选**:Q1 切换方式(完全替换 / 双域名并存(需改码) / 仅门户试水);Q2 子域形态(`<user>.evertwins.com` 推荐 / 路径式(需改码+nginx))
**可立即准备**:新 vhost 模板(含本次 `proxy_buffering off` 修复项)、certbot 命令、切换与回滚脚本、切换前检查清单
> **用户 23:09 表示:还没准备好 → 域名切换暂缓(parked),不再推进,等用户后续主动提起。** 侦察结论保留在本文件,可直接复用。
## 档案 22:服务域名迁移 → alotbuy.com(23:25-23:45,已完成上线)
**用户裁定**:路径 **②走 Cloudflare 代理**(橙云,零 DNS 改动)。
**前置实测**:alotbuy.com A 已指向本机(经 CF);根域当时 nginx 404 空站 → 接管无冲突;`*.alotbuy.com` 通配已存在;`ss` 实测 CF 回源**全走 443**(非 Flexible,无明文风险);CF token **仅 DNS 权限**(改不了 zone 设置)。
**执行**:
1. `certbot certonly --dns-cloudflare --dns-cloudflare-propagation-seconds 60 -d alotbuy.com -d '*.alotbuy.com'` → 成功(至 2026-12-09)
2. 新增 vhost `alotbuy.com.conf`:`server_name alotbuy.com www.alotbuy.com *.alotbuy.com`;80 段代理(兼容任一 CF 模式);继承 `proxy_buffering off`/`gzip_proxied any`/assets 缓存;**CF 回源段 `set_real_ip_from` + `real_ip_header CF-Connecting-IP`(放在 server 块内)**
3. `/opt/dsh/switch-domain-alotbuy.sh`:改 env(BASE_DOMAIN=alotbuy.com、COOKIE_DOMAIN=.alotbuy.com,备份 `env.bak-20260910-2332`)→ drain(**按 pid 结束**,不用 pkill -f 自匹配)→ 重启 → 残留 0、服务 active
4. 旧域改**纯 301**:`map $host $alotbuy_new_host` + 通配 server_name → 用户名映射正确
**踩坑 4 个(已写进档案 22)**:① DNS-01 默认等 10 s 不够(CF 上实测 ~10 s 才可见)→ 须 `--dns-cloudflare-propagation-seconds 60`;**certbot 只把该参数写进新证书的 renewal conf,旧证书要手工补**(已补两个)② `set_real_ip_from` 写 vhost 文件顶层=http 上下文全局生效 → 必须放 server 块内 ③ **正则 server_name `~^(?<sub>...)\.dsh\.alotbuy\.com$` 实测不匹配**(SNI 命中默认 vhost,服务的是 alotbuy 证书)→ 改用 `map` + 通配 ④ inline shell 在 Windows→ssh 边界多次被引号吃掉 → 一律「本地写文件 + scp + 远端执行」。
**验证**:门户经 CF `200`;`admin.alotbuy.com` → `401`(未登录,证明已被识别为用户子域);旧域 301 映射正确(apex→apex,admin.dsh.*→admin.alotbuy.com);源站证书校验 0。
**遗留/待办**:① CF SSL 模式建议人工确认 **Full (Strict)** + 开 Always Use HTTPS(token 无该权限)② **全员需重新登录**(Cookie 域变化)③ Cookie 域覆盖 `.alotbuy.com` 全域(gao/mao/work 也会收到,如需收窄要改码)④ 宝塔保存站点会重写 vhost(可能抹掉我们的自定义指令)⑤ 档案 21 的 A1 禁 HMR / A2 catch-all 仍未做。
**产出**:档案 `22-服务域名迁移到alotbuy.com.md`;INDEX 场景表新增「访问入口」行;双端 **48/48** 一致 → 提交 `7b2d268` 推送;`MEMORY.md` 已更新访问入口与 CF/证书事实。
# 2026-09-11 工作日志
## 按方案执行:代码入库 + CI + 文档补齐 + 归档(00:07-00:25)
**用户指令**:「按照方案执行」;并反馈**换域名后速度明显变快**;**档案 21 整体暂缓**(A1 禁 HMR / A2 catch-all / 流式与刷新复验均不做)。
### 1. 代码:提交 + 双仓库同步(完成)
- 服务器 `/opt/dshs` 提交 **`7cba3e3`**(前 HEAD `8355e99`),内容:
- 档案 20:`crash-policy.ts`(新增)+ `orchestrator.ts`/`spawner.ts`/`config.ts`/`routes/dsh.ts`/`ensure-role-profile-patch.cjs`/`package.json`(改)
- 档案 18:`poc/workspace-scoped-picker/`(插件源码 + README + 自检)
- 档案 19 §C3:`scripts/ci.sh`(新增)、删 `smoke-plugins.mjs`(目标路由已在档案 16 阶段 0 移除)、删 `smoke-watchdog.mjs`(watchdog 从不启动)
- 仓库根污染清理:**10 个 `bak-*`** 移入 `/opt/dsh/backups/repo-bak-20260911-0018/`(非删除)
- 同步路径:服务器 `git bundle master --not 8355e99` → scp → 本机 `D:\github\dsh_shenxian` fetch + `merge --ff-only` → **push Gitea `8355e99..7cba3e3`** ✅
### 2. 档案 20 的 live 自愈实测(顺带完成)
kill admin main 后 journald 出现:
```
[crash-restart] {"event":"instance-restart","userId":"cce6d1cd-…","attempt":1,"delayMs":1000,"restartsInWindow":1,"restarts":1,...}
```
→ 1 s 退避重启、计数与结构化日志均如设计 ✅(熔断 live 实测仍待(需连续 6 次 kill),策略已由 7 项单测覆盖)
### 3. 档案 18 机制修正(重要实证结论)
用户截图证实**第一次覆盖未生效**(admin 仍能看到整个文件系统)。取证:
- admin profile `cordis.patch.yml` 的覆盖行**确实存在**,但 `--dump-config` 仍显示官方 `-auto` 行、本包出现 0 次;插件本身从 profile 目录可正常 `import` ✓
- ⇒ **本 dsh 构建下「同 id 换 name」的覆盖写法不生效**(bundle 层与 profile 层都试过)。档案 09 的先例是 **client 行 + `disabled: true`**,不是换 name。
- 另确认:**`--dump-config` 不反映 bundle/profile patch 层**(连已生效的 portal-entry/business-plugins 行、档案 09 的禁用行都不显示)→ 不能作为验收口径。
- 改为 v2 写法(写入 admin profile patch 并已重启实例):
```yaml
- insert:
- id: workspace-scoped-picker
name: "@dsh-local/workspace-scoped-picker"
- id: directory-picker
name: "@deepseek-ai/dsh-host-directory-picker-auto"
disabled: true
```
- 插件源码同步更新:`cordis.patch.yml` 改 insert 形态 + 新增 `README.md`(记录机制结论与安装要点);文档库片段同步订正。
- **待用户浏览器验证**:重载后点「添加工作区」是否只剩自有目录。
### 4. 文档补齐(档案 19 §D1/D2/D3)+ 运维归档
- `01-规划与架构`:追加「附一 机制索引」表(机制 → 生效位置 → 档案号,12 行)
- `02-运维手册`:追加「附录 C」——域名与证书(含 DNS-01 必须 60 s 传播)、nginx 四行关键配置及其**失效症状**、平台脚本族、排障速查(含 bwrap 包装进程假象提醒)
- `03-路线图`:刷新头部状态、已完成清单补 14-22(共 22 项)、待办表重写(picker 全量 / H2 / CF Strict / Cookie 域 / 熔断实测 / 档案21 暂缓)
- 归档新增:`ops/nginx/alotbuy.com.conf`、`ops/nginx/dsh.alotbuy.com.conf(旧域301)`、`ops/scripts/switch-domain-alotbuy.sh`、`scripts/{cf-dns.py,cf-probe.py,nginx-slow.py}`
- 文档双端 **54/54 一致** → 提交 `3e6cf98`、`b5b599c` 推送
### 5. 本机整理
`_中间产物_待清理/` 已清空删除;插件源码保留在工作区根 `poc-dsh-local/workspace-scoped-picker/`(唯一真源)。
**待办**:① admin 浏览器验证 picker(若仍不行 → 转 H3 自建 host 插件覆盖 seam 或 bwrap 层方案)② 通过后写 `install-workspace-picker.cjs` 全量 ③ H2 写保护 ④ CF SSL 改 Full (Strict)(人工)⑤ 档案 19 剩余批次(C5 死代码、C6 扫描规则、C8-C10、D6-D8)
## 档案 23:实例内 AI 能力与权限限制核查(00:25-00:45)
**用户提供**:guest 对话里 AI 遇到的 9 条"平台/环境限制",要求判定正常/不正常。
**判定结论**:
| # | 项 | 判定 |
|---|---|---|
| 1 | 沙箱后端缺失 → bash 工具被整体拒绝 | 🔴 **P1 真 bug(已修,待重启生效)** |
| 2 | 只读根文件系统(/etc 等 4 处) | ✅ 设计使然 |
| 3 | 无 sudo | ✅ 设计使然(bwrap 默认 `no_new_privs` → setuid 失效;宿主 sudo 本身是 setuid root) |
| 4 | 他人目录 Permission denied | ✅ 隔离生效(`users/` 711、用户目录 700) |
| 5 | rg / jq / ffmpeg MISSING | ⚠️ 可优化(宿主装即可,`/usr` ro-bind 直接可见) |
| 6 | 无浏览器 / 无 playwright | ⚠️ 预期内但影响抖音类任务(受 512M 内存上限约束) |
| 7 | python 3.6.8 + pip 9.0.3 | ⚠️ 可优化(EOL;宿主系统 python) |
| 8 | 无 DeepSeek key → curl 401 | ⚠️ 半正常:**实例 env 确实含 `DEEPSEEK_API_KEY`**(实测键名),401 因未带鉴权头;**但统一 key 注入实例 = 租户 agent 可外泄平台 key(P1 风险)** |
| 9 | api.vvhan.com 解析失败 | ✅ 与平台无关(公共 DNS 亦 **NXDOMAIN**,`vvhan.com` 本身可解析) |
**第 1 条根因链(关键发现)**:`dsh-sandbox-local` 在 Linux 的 runner 链 = **bwrap → Landlock**;Landlock 需内核 ≥5.13(本机 al8 为 **4.19**,不可用);而 bwrap 功能探针在我们构造的**合成根**(只 bind `/usr` `/lib64` `/etc` `/dev` `/proc` `/tmp` + 用户根)里 **execvp 失败(缺 `/bin`、`/sbin`、`/lib`)** → `SANDBOX_UNAVAILABLE` → `dsh-sandbox` **拒绝无沙箱运行**("refusing to run the command unconfined")→ 实例内 bash 工具**完全不可用**。
**修复**:`spawnAsUser` bwrap 参数补 3 个符号链接(不扩大可见面):`--symlink usr/bin /bin`、`usr/sbin /sbin`、`usr/lib /lib`。实测嵌套 bwrap 补链后 `NESTED-SHELL-OK`;`ci.sh` 通过。
**提交**:服务器 `32a9ad8` → bundle → 本机 merge → **push Gitea `7cba3e3..32a9ad8`**。**生效需重启 `dshs`(drain 实例)**。
**产出**:档案 `23-实例内AI能力与权限限制核查.md`(含逐条判定、根因链、建议项成本表、红线声明);双端 **55/55** 一致 → 提交 `023ba48` 推送。
**待用户**:① 授权重启使沙箱修复生效 ② 是否装 ffmpeg/python3.11/rg/jq ③ 是否评估 per-user key 或内网 LLM 网关(消除统一 key 外泄面)
## 档案 24:实例侧 401(token 过期)自动恢复(00:32-01:00)
**用户报障**:刷新实例子域页面显示 `dsh web authentication required; reopen the URL printed by dsh web.`,**不跳转也不重启**。
**成因**:实例**重启后 launch token 轮换**(本次由我 00:14 为验证 picker 重启实例引起;23:32 域名迁移 drain 同理),旧标签页 URL 带旧 token → dsh 返回 401 + 该文案;而 `proxy.ts` **只处理了「门户侧 401」**(`resolveSubdomainAccess` 的 unauthorized → 302 login.html),**上游(实例)401 被原样透传**;`not_running` 自动重启分支不适用(实例在跑)。
**修复**(`src/supervisor/proxy.ts`):
- `proxyHttp` 增加 `freshAuthUrl?: () => Promise<string|undefined>` 参数
- 上游 401 + 浏览器导航(GET + `accept: text/html`)→ `upRes.resume()` + **302 到 `freshAuthUrl()`**(取 `supervisor.status(userId).main.launchToken` → `https://<同Host>/?token=<新token>`,拿不到则回门户)
- `SubdomainAccess` 成功分支补 `userId`;路径式 `/u/:slug/dsh/` 路由同样接入
- 编译 + `ci.sh`(38 pass / 0 fail)通过
**同批上线**:档案 23 的沙箱修复(bwrap 补 `/bin` `/sbin` `/lib` 符号链接)→ 实例内 **bash 工具恢复可用**。
**重启**:drain + `systemctl start` → active / 残留实例 0 / 门户 200;`lib/` 产物确认 `symlink`×3 与 `freshAuthUrl`×3。
**提交**:服务器 `d7ab5b4` → bundle → 本机 merge → **push Gitea `32a9ad8..d7ab5b4`**;文档档案 24 + INDEX/README → 双端 **56/56** 一致 → 推送 `102ffda`。
**回滚**:`/opt/dsh/backups/proxy.ts.bak-<TS>` 或 `git revert d7ab5b4` + build + 重启。
**待用户**:重新进入会话(drain 后需重进)并实测:① 实例重启后刷新旧页应自动恢复 ② agent 的 bash 工具应可用。
## 档案 24 后续:预置默认工作区 + picker 友好化(00:43-00:55)
**用户反馈(附截图)**:编辑器要求"必须选择一个工作区"才能开始会话;要求「只允许在自己的目录下创建工作区」「不要给用户看完整路径」。
**关键发现**:工作区列表持久化在 `<实例 home>/storages/workspace.json`(schema: `unit{name:workspace,version:2}` + `global{initialized,workspaceIds[],archivedSessionIds[]}` + `tables.workspaces{<uuid>:{path,title,sessionIds[],createdAt,updatedAt}}`)。实测:**admin 的列表为空**(`workspaceIds: []` → 所以一直被要求选工作区);**guest 有**(path=`ws/MCN短视频创作`,title 是短名 → 本来就不显示完整路径)。UI 侧 `label` 用法远多于 `.path`(108 vs 2)→ 展示层可收敛。
**已实施(两项,均已上线)**:
1. **预置默认工作区**(编排器 `seedDefaultWorkspace`,`spawnInstance` 内 isMain 时调用):仅当 `workspaceIds` 为空时,把 `<userRoot>/ws` 以短标题 **「我的工作区」** 写入 `workspace.json`,并 `chown` 给该 uid;**不覆盖**用户已有工作区;失败只记日志不阻断启动 → 用户开箱即用,不必手动选。
2. **插件 v0.1.1**:① 面包屑根节点显示 **「我的工作区」**(可用 `DSH_WORKSPACE_LABEL` 覆盖),不再暴露 `/var/lib/.../users/<uuid>/ws`;② **新增加载标记** `[workspace-scoped-picker] loaded root=...`(启动日志可直接确认是否生效,解决"无法验证"问题)。
- 安装:重建 tgz(`package/` 前缀)→ 两个 profile `pnpm add file:...tgz`(**guest 的 profile 是 pnpm workspace 根,需 `-w`**,否则 `ERR_PNPM_ADDING_TO_ROOT`)→ 软链指向 0.1.1 ✓
- 自检 26/26 通过;`ci.sh` 通过;服务已重启(active / 0 实例 / 门户 200)
**待用户实测**:① 进入会话后编辑器应直接可用(默认工作区已种)② 点 `+` 添加工作区时面包屑应显示「我的工作区」且看不到完整路径、出不去自己目录 ③ 我可在日志里查 `[workspace-scoped-picker] loaded` 与 `[seed-workspace]` 验证。
## 档案 25:事故 + not_running 兜底(00:49-01:05)
**用户报障**:刷新后显示裸 JSON `{"error":"not_running"}`,无自动跳转。
**取证(journald)**:`dsh: plugin tree failed to load ... duplicate loader entry id: workspace-scoped-picker` → 实例启动即退出 → **崩溃循环 5 次(1→2→4→8→16s)→ 熔断 `crash-loop-circuit-open`** → 用户刷新落到 `reply.code(404).send({error:'not_running'})` 显示裸 JSON。
**根因(自造回归)**:我把插件包内 `cordis.patch.yml` 从"覆盖行"改成 `insert: workspace-scoped-picker`,而 profile 层也 insert 同一 id → 双写 → loader 视为致命错误。
**正面收获**:档案 20 的熔断按设计工作(拦住无限重启),并把实例标 failed 后允许重新进入。
**修复(已上线,均推送)**:
1. 包内 bundle patch 改 **空补丁 `[]`**(单点插入只在 profile 层);插件版本 0.1.1→**0.1.2**
2. `proxy.ts`:not_running **浏览器导航永不吐 JSON**;并发进场(AlreadyRunningError)→ 等待在飞实例 token(≤20s)→ 302 带 token;仍拿不到 → 302 回门户
3. 同批:`seedDefaultWorkspace`(预置工作区)+ 插件友好面包屑 + 加载标记
**安装踩坑**:`pnpm add` 需 `HOME=<ws>`;**`-w` 仅在 profile 是 pnpm workspace 根时需要**(guest 需要、admin 不需要,否则 `--workspace-root may only be used inside a workspace`);换版本号以避开 pnpm 缓存。
**验证**:ci.sh 38 pass;两 profile 装 0.1.2(包内 patch 确认为 `[]`);自检 26/26;服务重启 active/0/门户 200。
**提交**:代码 `ce1b3ac`(push `d7ab5b4..ce1b3ac`);文档档案 25 → 双端 **57/57** → push `86e662f`。
**待验证**:用户进入会话后核对日志 `[workspace-scoped-picker] loaded` 与 `[seed-workspace]`(确认插件生效 + 默认工作区已种)。
## 验证结果:插件生效 ✅ / seed 兜底不可靠 ⚠️(01:00)
**① 插件已确认生效**(首次确定性证明,靠新加的加载标记):
```
00:53:33 [dsh-child main] [workspace-scoped-picker] loaded root=/var/lib/.../users/cce6d1cd-.../ws (cwd)
```
→ `disabled` 官方行 + insert 自建行 的组合**成功**;scoped picker 生效(只看自有目录 + 友好面包屑)。**用户"能自己建/选工作区且不见系统路径"的需求已满足**。
**② seed 默认工作区不可靠**:日志显示种子**写入成功**(`00:49:29 [seed-workspace] … → …/ws`),但 admin 的 `workspace.json` 随后被**实例自身的注册表规范化重写为空列表**(mtime 00:54、`workspaceIds: []`、`tables.workspaces: {}`,仅保留 `archivedSessionIds`)。
→ 推论:dsh 启动时会加载并**重写**该存储,对我们的条目做了校验/清理(`dsh-workspace` 有 `order`(显示顺序)与不变量 `initialized && order.size !== table.size`,且会报 `order unknown workspace '…'`)——**预置条目会在启动时被丢弃**,因此不能作为"开箱即用"的可靠手段。
→ 结论:**改为依赖用户经 `+` 自行创建**(已可用);若要做"开箱即用",应改为**实例启动后经 dsh 自身 API(`workspace/create`)创建**(需探针确认方法名,列为后续项)。
**用户顾虑已确认成立**:用户可删除工作区 → 删空后回到"必须选一个工作区";但**现在他们能自己建**(`+` → 只在自己目录内),所以不再卡死。
## 档案 26:dsh 升级耦合点与回归清单(01:05-01:15)
**用户问**:这些改动是否影响 dsh 原始包?dsh 更新是否会覆盖?
**实测证据(只读)**:
- 官方包目录(`/usr/local/lib/node_modules/@deepseek-ai/dsh`,版本 **0.1.2-rc.1**)内**零改动**:grep 我们的标识(workspace-scoped-picker / dshs / freshAuthUrl / crash-restart)**无命中**;包内**无 2026-09-10 之后被修改的文件**
- 所有改动都在 dsh 之外四层:① 编排器 `/opt/dshs`(6 提交)② profile 层(dataRoot 内 `cordis.patch.yml`/bundles/`node_modules/@dsh-local/{portal-entry,business-plugins,workspace-scoped-picker}`)③ 实例环境(bwrap 参数,编排器代码)④ 系统层(nginx vhost、systemd `dshs`/`dsh-egress`/`dsh-provision`、env、certbot)
- **结论:升级不会"覆盖"我们的改动(无可覆盖之物);真正风险 = 官方契约变化导致失效。**
**六类耦合点(升级必查,已写入档案 26)**:
- C1 profile patch 引用的官方 client 行 id(`ui-settings-models`/`-plugins`/`-plugin-inventory`/`ui-cordis`)→ 检查设置面板是否仍隐藏
- C2 `directory-picker` 行(**已回滚**,现为官方默认 `[]`);双面性教训:disable 该行会同时失去客户端对话框
- C3 自建插件继承官方 seam 基类(`dsh-host-directory-picker`)→ **当前未挂载,风险 0**;靠加载标记可验
- C4 自建 client bundle 契约(portal-entry / business-plugins)
- C5 编排器 bwrap 合成根(`/bin /sbin /lib` symlink ↔ dsh 沙箱后端 bwrap/Landlock)
- C6 proxy 的 401 / not_running 假设(dsh 文案、响应码、launch token)
**建议**:升级前 `git tag pre-dsh-upgrade-<版本>` + 备份(nft/env/vhost/profile patch);升级后逐项回归;任一失败先 pin 回旧版。**并给 portal-entry / business-plugins 各加一行加载标记**(下次改造顺手做),把 C4 回归从"看 UI"变成"看日志"。
**产出**:档案 `26-dsh升级耦合点与回归清单.md`;双端 **58/58** 一致 → 提交 `c443da9` 推送。
## 档案 18 v3:官方对话框 + 受限 host(用户已验证)+ 隐藏改路径入口(01:05-01:25)
**关键机制(实测)**:官方 `directory-picker` 行(@…-auto) 在启动时**连带挂载客户端对话框** `@deepseek-ai/dsh-client-ui-directory-picker-browse`(该 client 包自带 `dsh.client` 元数据、且是 `dsh-web-app` 的依赖)。disable 那一行 → 对话框消失(点不动,客户端连 RPC 都不发)。
**v3 解法(配置层,不改官方包)**:
```yaml
- insert:
- id: workspace-scoped-picker # host 面:自建受限实现(根=<userRoot>/ws)
name: "@dsh-local/workspace-scoped-picker"
- id: ui-directory-picker-browse # client 面:官方对话框 UI 单独插回
name: "@deepseek-ai/dsh-client-ui-directory-picker-browse"
- id: directory-picker
name: "@deepseek-ai/dsh-host-directory-picker-auto"
disabled: true
```
→ **用户实测:对话框能开 ✓,路径框里手输 `/` 被拒 ✓**(host 侧越界校验生效)。
**v0.1.4 增量:隐藏"改路径"入口**——插件新增 **client 面**(`lib/client.js`),以 `window.__ModuleLoader__.load({factory})` 形态只导出 `apply+inject`(**禁 `exports.default`**,红线 R3),注入 CSS 隐藏官方对话框的 `crumbEditZone/crumbEditGlyph`(用 `[class*=…]` 模糊匹配抗 CSS-module 哈希),并挂 MutationObserver 兜懒挂载。
**全量铺开(已有工具)**:
- `poc/workspace-scoped-picker/ensure-workspace-picker-patch.cjs`:以 `# >>> platform: workspace-scoped-picker` 标记**幂等追加**平台段(保留角色 patch),支持 `--dry-run/--restart/指定用户`
- `/opt/dsh/install-wsp.sh <tgz>`:按用户正确切换 `pnpm add` 的 `-w`(**guest 的 profile 是 pnpm workspace 根需 `-w`;admin 不需要**)并校验 `lib/`
**本轮踩坑(3 个,都要记)**:
1. **pnpm 同版本缓存**:同名同版本 tgz 即使内容变了也复用旧包 → 必须**升版本号**(0.1.3→0.1.4)才生效;
2. **scp 报错被我 grep 过滤** → `lib/client.js` 没传上服务器却发现得晚 → 传输后必须显式 `ls` 校验;
3. **脚本追加平台段前未做"整文件去重"** → admin 的 patch 出现**两段**(手写 v3 + 追加段)→ 又是 duplicate id 隐患;已重写为单段。教训:**平台段必须以标记包裹,且写入前先检测整份文件是否已含这些 id**。
**当前状态**:admin/guest 两个 profile 均已装 0.1.4(`lib/{index.js,client.js}`)、平台段均为单份;实例已重启(kill→自愈拉起),日志 `[workspace-scoped-picker] loaded root=…/ws` ✓、无 duplicate 报错;ws 里历史 tgz 已清理。
**待用户**:硬刷新后验证「选择工作区」顶部**不再出现可编辑路径框/铅笔图标**。
## 续:紧急修复 + 全量铺开 + B1 写保护(07:44-08:02)
**① 紧急修复:重复平台段(我的脚本 bug)**
- 现象:`ensure-workspace-picker.cjs` 初版 marker 检测写成 `…picker.cjs`,与历史 `…picker-patch.cjs` 不匹配 → 二次追加 → **两份 profile 各 2 份平台段**(duplicate loader id 隐患,下次实例启动会崩)。
- 修复:① `normalize-picker-patch.py` 一次性归一化(已执行,各 id 恰好 1 份;guest 角色 patch 完整保留 ✓)② 脚本改为**正则宽松匹配** marker + **写入前剥离所有旧段**再比对 → 天然自愈(幂等已验证:两用户均 skip)。
**② 新用户自动铺开(第一层,已上线)**
- `ensure-workspace-picker.cjs`:全量幂等(① 写平台段 ② 逐用户 pnpm add 插件,含版本比对;`--dry-run/--restart/指定用户`)
- 已接入 `/usr/local/bin/provision-new-users.sh` 尾部(新用户注册自动补齐);插件产物固定 `/opt/dsh/artifacts/workspace-scoped-picker-0.1.4.tgz`
**③ B1 / H2 写保护(P1,已实现 + 双重验证 + 已激活)**
- 威胁:用户把 `<userRoot>/home/profiles/web` 加为工作区 → 用 bash 改写 `cordis.patch.yml`(去掉平台段)或 `package.json`(挂任意 bundle)→ 绕过平台策略
- 实现:`spawnAsUser` 增 `role` 形参;在 `--bind root root` **之后**追加 `--ro-bind-try`(cordis.patch.yml / package.json / pnpm-lock.yaml);**不动 cordis.yml**(dsh 启动会写它)
- 验证:手工 bwrap 写测试(读✅/写策略文件被拒✅/cordis.yml 可写✅/ws 可写✅)+ **真实 dsh 启动**(guest profile + 新参数)正常打印 launch token ✅ + ci.sh 通过
- 已重启服务激活(active / 0 残留 / 门户 200);lib/ 含 ro-bind-try ✓
**④ 其他**
- A1 已验证:CF Always Use HTTPS 生效(http→301、子域同、https 200)
- A2 结论:Cookie 域 `.alotbuy.com` **不影响** gao/mao/work 功能;属隐私边界取舍;当前架构不能改 host-only;彻底隔离需专用子域(成本高)→ 记已知取舍
- B5 前置不满足:`portal-entry`/`business-plugins` 源码不在仓库(仅 /tmp 遗留)→ 加加载标记暂缓
- 运维要点:本地 shell PATH 曾损坏(dirname/grep/ssh 全找不到)→ 需显式 `export PATH=<PortableGit>/usr/bin:/c/Windows/System32:$PATH`;`git fetch` 引用 bundle 必须用 **D:/…** 路径(MSYS 的 /d/… 会失败)
**提交**:`277d817`(脚本+归一化)、`34190b1`(B1)→ 推送 Gitea **be3d3f6..34190b1** ✓
## B6 文档批次 + B4/B2/B3 实现并激活(08:02-08:30)
**B6(档案 19 §C8/C9/C10 + §D6/D7/D8)全部完成**(纯文档,双端 59/59)
- **C9**:新增 `DEPLOY-本部署.md` —— 拓扑/目录布局/env 键(8 个)/依赖版本(实测 Node 22.23.2、pnpm 9.15.9、bwrap 0.4.0、nginx 1.28.3、sqlite3 3.26)/平台脚本族/构建·部署·回滚三步/默认红线 5 条
- **C10**:两处 `docs/` 互指(文档库 README ↔ 代码仓库 README,均标注"上游 7 篇勿混")
- **C8**:02-运维手册 附录 C.5 列出本部署未使用/未验证子系统(k8s-spawner/pg/leader/reconcile)
- **D6**:档案 12(VoxEMW)标注**已冻结**;**D7**:档案 18 头部加"与 17 同主线"互指(保持独立编号避免断链);**D8**:06-工作台UI规范 加"同步时间戳 + 回拷四步"
- INDEX 登记 DEPLOY;03 路线图刷新
**B4(档案 19 §C6 安全扫描分级,已实现+已激活+已冒烟)**
- `src/web/security-scan.ts`:规则拆 **P0 阻断**(原 7 条 + `nc -e` 反弹 / `/dev/tcp` 反弹 / base64 解码后执行 / 给系统二进制加 setuid / fork 炸弹)与 **P1 告警**(动态执行子进程、vm/eval/new Function、敏感 env、超长 base64、外部网络、隐藏目录/.git)
- 覆盖面:扩展名 19→31、无扩展名按 shebang、二进制按 0x00 跳过、上限 2MB→8MB;P1 以 findings 返回并写 journald(`upload-scan-warnings`)
- **冒烟实测**:P1 样例(child_process + DEEPSEEK_API_KEY)→ 2 条 findings 且**不阻断** ✅;P0 样例(nc -e)→ **阻断** ✅;良性样例 → 零误报 ✅
**B2(档案 18 v3 第二层自愈,已实现+已激活)**
- `orchestrator.ensurePickerProfile(userId)`:异步 fire-and-forget 调 `ensure-workspace-picker.cjs --user-id <id>`;调用点 = `launch()` 前 + main spawn 成功后 → 覆盖 profile 被清空/重建/平台段被删/插件被卸
- 脚本新增 `--user-id` 过滤(实测命中 admin 并 skip;md5 双端一致)
**B3(§C5)子集**:删除 `src/runtime.ts`(全仓无引用)→ 并清掉 tsc 遗留的 `lib/runtime.js` 过期产物。
`folder_plugins`/`enablePatch`/`renderPatch`/`patch?` 链路跨 10 文件 → **留专项**(不在本轮)。
**验证与激活**:`ci.sh` 全绿;drain + 重启后 `lib/` 含 WARN_RULES/BLOCK_PATTERNS/ensurePickerProfile/ro-bind-try,`lib/runtime.js` 已清;服务 active / 0 残留 / 门户 200。
**提交**:代码 `277d817`、`34190b1`(B1)、`a9c065d`(B4+B2+B3)→ 推送 **至 `a9c065d`**;文档 `6940c8b`、`859ed06` → 59/59 一致。
**运维踩坑(再次出现,务必记住)**:
1. 本地 shell 的 PATH 会莫名丢失(`dirname/grep/cat/ls` 全找不到)→ **每条命令都要显式 export PATH**(PortableGit/usr/bin + System32);
2. **scp 相对路径 + 命令链中段失败会让文件没传上去**(表现为服务器脚本仍是旧版)→ 一律用**绝对本地路径** scp 并当场比对 **md5**;
3. `git fetch` 引用 bundle 必须用 **`D:/…`** 路径;
4. ssh 内联 python/node 一遇括号/引号就炸 → **一律"本地写脚本文件 + scp + 执行"**
## 档案 27:MCN 工作台插件平台化评估 + B3/B5 收尾(08:17-08:35)
**用户问**:MCN 工作台插件能否作为业务插件投放到平台?多用户?数据/产物如何存?插件自带 DB 有无风险?业务插件调用 skill 应整合还是外置?
**实测结论(档案 27)**:
- 插件形态:`dsh-plugin-mcn`(569K)标准 bundle(`dsh.client` inject sidebar/runtime + `cordis.patch.yml` insert `mcn-nav`);host 面 `lib/{index,db,data,http,mcp,imports,video-tools,workshop,config}.js`
- **DB**:`node:sqlite`(`DatabaseSync`),路径 `homedir()/.dsh/mcn-plugin.db`;10 张表**按 `account_id` 组织、无 userId**(单租户)
- **技能调用方式**:插件**不执行技能** —— UI 收集参数后**拼提示词**要求 agent 调用具名技能(如「请调用 mcn-dou-analysis 技能(路径 `~/.dsh/skills/mcn-dou-analysis/`)」);**硬编码技能路径 13 处**(mcn-dou-analysis×6、script-review×3、short-video-script×2、storyboard-prompt×1、mcn-data-insight×1)
- **能力来源 = agent preset**:`<DSH_HOME>/.agent-presets/mcn/{preset.yml,agent.cordis.yml}`(挂 persona/tool-bash/tool-fs…),插件经 `ctx.get("agentPresets").resolve('mcn')` + `mount()` 加载
- **MCP**:自带 stdio 客户端 `npx myai-mcp`(`NPX_CMD/MCP_PACKAGE/MCP_ENV`);`mcp.js` **已优先读 DSH_HOME**(正确写法,config.js 漏改)
**结论**:① 可作业务插件投放,但 **3 处 P0 必改**:`homedir()/.dsh`→`DSH_HOME`、技能路径改只报技能名、**agent preset 随包投放**(平台每用户独立 DSH_HOME,无人写就没 preset→退化为默认,能力缺失);② 多用户**天然成立**(每用户独立实例 → 每用户一份 DB,无需加 userId);③ 产物写用户工作区 ✓ 隔离;④ `node:sqlite` 平台 Node 22.23.2 **实测可用**(ExperimentalWarning);风险=实验 API 随 Node 变动(应补档案 26)、用户可损坏自己的库(建议重建兜底);⑤ **技能=外置**(平台共享技能层,一份 vs 每用户一份、可独立更新、复用平台技能管理面),preset 仍需随插件投放,MCP 建议预装(避免每次 npx 拉包)
**B3 便宜版完成 / 完整版关闭**:`src/runtime.ts` 已删(上轮);本轮给 `folder_plugins`/`workspaces` 加废弃注释(schema sqlite+pg 各 2 处,共 4 处)+ 档案 16 标注"表保留、无业务调用、勿再加功能"。**决策:C5 完整版关闭**(跨 10 文件 + 含 k8s/PG 未验证路径,删除风险不对称)。
**踩坑**:注释里含**反引号会截断 TS 模板字符串**(schema 是模板字面量)→ 改无引号写法。
**B5 降级为"顺手做"**(不解决当前问题 + 源码不在仓库 + 仅提升升级排查速度)。
**提交**:代码 `57a7d34`(推送 `a9c065d..57a7d34`);文档 `1452541`(`859ed06..1452541`,双端 **60/60**)。
**待办**:MCN 平台化改造(3 项 P0 + 3 项 P1)待启动;其余为触发式(会话 GC、档案16 阶段3-4)。
## 档案 28:用户数据清理策略(工作区/会话/回收站)+ 修掉污染 ws 的平台 bug(08:32-09:00)
**用户口径**:AI 生成的代码/脚本**按对话任务一次性产生、无复用价值** → 是定期清理的重点;会话记录达到某大小阈值后可清 90 天前的。
**关键发现(平台自身 bug)**:安装脚本原用 `HOME=<用户 ws> pnpm add …` → pnpm 把 **store**(`$HOME/.local/share/pnpm/store`)与 **cache**(`$HOME/.cache/pnpm`)写进**用户工作区**,再叠加我们复制进去的 `*.tgz`、`poc/`、`.poc-backup/`。
实测:**admin ws 18MB/2045 文件(17.6MB 全是平台产物;真内容仅 92KB)**、guest 2.5MB/30 文件(2.0MB 平台产物;真内容 504KB)。→ 这才是"ws 里一堆文件"的真凶,与 AI 无关。
**修复与清理(已执行 + 已实测)**:
1. `ensure-workspace-picker.cjs` 改为 `--store-dir <home>/.pnpm-store --cache-dir <home>/.pnpm-cache`,并直接用 `/opt/dsh/artifacts/` 的 tgz(不再复制进 ws);
2. 存量污染用 `scripts/clean-ws-pollution.cjs`(白名单精确匹配)`mv` 到 `<userRoot>/trash/2026-09-11-ws-pollution/` → **admin 12KB/0 文件、guest 480KB/8 文件**;
3. **实测清理后真实 dsh 正常启动 + 插件正常加载**(`[workspace-scoped-picker] loaded…` + token)→ 证明清 store/cache 无副作用。
**新增三件套(档案 28)**:
- `scripts/ws-cleanup.cjs`:三档 —— **T1** 平台产物/明确垃圾(`.local/.cache/poc/.poc-backup/__pycache__/node_modules/.pytest_cache` + `*.tgz/.tmp/.log/.bak/.part`)直接清;**T2** ws **顶层**一次性脚本(`.py/.js/.sh/.ps1/.ts/.ipynb/.sql` 等)**>90 天**才清;**T3** 其余(含**所有目录**)**永不自动删**,只统计。阈值 **ws ≥ 2048MB** 才动手;阈值/天数可 `--threshold/--days` 调。
- `scripts/session-gc.cjs`:会话保留期回收(`sessions/<slug>/<session-id>/session.jsonl.zstd`;**sessions ≥ 500MB** 才触发,清 **>90 天**;结构与"无索引文件"已实测)。
- `scripts/purge-trash.sh`:回收站 **超 30 天真删**(唯一执行删除的组件)。
- **cron**(`/etc/cron.d/dsh-maintenance`):每天 04:10 ws-cleanup / 04:20 session-gc / 04:40 purge-trash;日志 `/var/log/dsh-{ws-cleanup,session-gc,trash-purge}.log`。
- **安全设计**:所有清理一律 **`mv` 到 `<userRoot>/trash/<日期>-<任务>/`**(同盘秒级、可整目录移回),30 天后才真删。
- 三脚本 dry-run 与 `--apply`(阈值未到时正确跳过)均实测通过。
**未做/待细化**:`session_projcache` 同步回收(KB 级暂不动);T2 保留标记(`.keep`/项目内 `scripts/` 豁免);门户用量可见性;可选硬配额(XFS project quota)。
**提交**:代码 `3f6481b`(`57a7d34..3f6481b`);文档 `3f40de8`(`1452541..3f40de8`,双端 **61/61**)。
## 档案 28 续:清理策略"一次性优化到位"(08:46-09:00)
**用户要求**:一次性优化到位;**AI 对话记录(sessions)保留时间可以长些**。
**本轮全部落地(5 项)**:
1. **会话保留期加长**:90 → **365 天**;阈值 500 → **1024 MB**(cron 已更新)
2. **`.keep` 豁免**:`ws/.keep` 存在 → 跳过 T2(一次性脚本不清理),T1 仍清
3. **projcache 同步回收**:`session-gc.cjs` 删会话时按 session-id 精确回收 `storages/session_projcache/sessions/<id>.json`
4. **用量可见**:新增 `scripts/storage-report.cjs`(每小时快照 `/var/run/dsh-storage-report.json`)+ 门户 `GET /api/admin/storage`(`requireAdmin`,只读快照不做实时 du)+ `admin.html` 新增「存储用量」区块(工作区/会话/回收站/可清理项数,超阈值标红)
5. **硬配额评估结论 = 不做**:根 fs 实测 **ext4**(未启用 prjquota)→ 强行上需重挂载/重建(停机与数据风险不对称);当前用量 24%(8.6G/40G)无压力 → 以"软化阈值 + 小时快照 + 门户可视 + 每日清理"替代
**cron(`/etc/cron.d/dsh-maintenance` 已更新)**:每小时 05 分 storage-report;04:10 ws-cleanup(2048MB/90d);04:20 session-gc(1024MB/**365d**);04:40 purge-trash(30d)。
**验证**:`ci.sh` 通过;storage-report 首跑成功(admin ws=0 sessions=0.7M trash=17.6M;guest ws=0.5M sessions=1.1M trash=2.0M);服务重启后 **`/api/admin/storage` 未登录返回 401**(证明路由生效且受保护);`admin.html` 已含用量区块。
**提交**:代码 `b048dd9`(`3f6481b..b048dd9`);文档 `fe87fc5`(`3f40de8..fe87fc5`),双端 **61/61**。
**踩坑**:`git commit -m "多行消息含 '+' 开头行"` 会被 shell 拆成 pathspec → **改用 `git commit -F <消息文件>`**(已验证有效)。
## 备份修复 + 全量同步确认(09:43-09:50)
**核查发现(真问题)**:`/opt/dsh/backup.sh` **无执行权限**(`-rw-------`)且**未接任何调度** → 最后一次全量备份停在 **2026-09-08**(之后 3 天含域名迁移/插件/清理策略等大改动,全部无备份)。
**修复与加固**:
- `chmod +x`;脚本加固为 **① `sqlite3 .backup` 一致性快照**(避免只拷到 `-wal` 导致库不一致)② 归档纳入 `opt/dsh/artifacts` + `opt/dshs/scripts` + `etc/cron.d/dsh-*` + `nftables-dsh-egress.nft` + `www/server/panel/vhost/nginx`(可重建)
- 调度 `/etc/cron.d/dsh-backup`:**每周日 05:00 全量** + 05:30 清超 60 天旧备份;日志 `/var/log/dsh-backup.log`
- 入库副本 `scripts/backup-platform.sh`
- **实测**:首跑全量 **8.7MB / 6919 条目**(users 6858 + DB + env + artifacts + scripts 20 + cron 2 + nginx vhost 22 + systemd 3)+ DB 快照 7.8K;**恢复演练**:解出 DB → `select count(*) from users` = **2 行** ✓
- 文档:02-运维手册 附录 **C.8 备份与恢复**(含恢复命令与演练证据)+ DEPLOY **6.5 定时维护**(三张 cron 表汇总)
**同步状态确认(三端一致)**:
- 文档库双端 **61/61** 一致(md5 对账);文档 git HEAD `a77de58` 已推送
- 代码:服务器 `709b359`(工作树干净)= 本机镜像 `709b359` = `origin/master` `709b359` ✓
- **发现并修复**:本机镜像曾出现 **57 项未提交删除**(`src/**` 45 + `scripts/**` 12,本地误删;服务器与远端完好)→ `git restore .` 恢复,现 0 项未提交
- 路线图刷新:清理陈旧行(picker v3 / H2 写保护 / 备份核查 → 已完成);修复待办表空行断表
**提交**:代码 `709b359`;文档 `a77de58`。
## A/B 同步(并行会话成果回收)+ C 待确认清单(21:05-21:35)
**背景**:21:05 拉状态发现**另一位会话在过去 11 小时并行推进到档案 52**(代码 `3b82d31`,29 个提交),文档库服务器侧 88 文件 / 本机 61;且**我上午留下的两个 bug 已被对方修复**。
**A 完成:文档库反向拉取(服务器 → 本机 → Gitea)**
- 按内容比对(mtime 不可靠:scp 会改服务器 mtime)确定清单 → 从 `/opt/dsh/docs` 拉取 **42 个文件**(27 新增「档案 29-52」+ 15 更新)+ 2 个 `.bak` → **本机文件数 61 → 88**
- **抽查确认我 09:00 的内容未丢**:02 附录 C.5/C.6/C.7/C.8、01 机制索引、06 同步规则、档案 18 主线说明、DEPLOY 6.5 定时维护 ✓
- **合并 `03-路线图与待办.md`**:对方基于较早快照重写,丢了我加的条目(14~22 已完成、C5 关闭结论、备份核查)→ 已补回 7 条并保留对方新决策(MCN 改造**暂缓**、白名单源码安装)
- 期间踩坑:`tar -T -` 走 stdin 无效(45 字节空包);列表文件是 **CRLF**(python `open(...,'w')` 默认翻译)→ 服务器侧 `tr -d "\r"` 解决
- 结果:**双端 88/88 一致** ✅;提交推送 `a77de58..e3380d9`
**B 完成:代码同步(服务器 → 本机镜像 → Gitea)**
- 服务器 `git bundle master --not 709b359`(110KB / **29 个提交**)→ 本机镜像 merge → push
- 结果:**三端一致 `3b82d31`**(服务器工作树 0 未提交)✅
**对方(并行会话)已修复的、我引入的缺陷**:
- **档案 35**:`ws-cleanup.cjs` T1 规则**无条件删除 ws 顶层 `*.tgz`** → 误删平台自己的 bundle 包(`.keep` 豁免不了 T1)→ 已修(`70302e6`)
- **档案 52**:新用户实例无法启动 —— 根因之一是我的 `stripPlatformSegments()`:profile 模板是「3 行注释 + `[]`」,剥段后 `body` ≠ `'[]'` → 判定失效 → **写出非法 YAML(`[]` + 平台段)** → 实例崩溃循环;另两处为 `ro-bind` 遮蔽 `/usr`、以及 new user 缺少插件(正是 071d678a 那个用户)
- 另有档案 30(孤儿实例清理)、43(root 污染用户 ws 与属主自愈)、46(共享 jq/ripgrep/ffmpeg)等,覆盖了我此前列的待办
**C 待用户详细确认(清单已按对方进展更新)**:MCN 改造(对方已暂缓)、档案 16 阶段 3/4(疑似被档案 41 覆盖)、Cookie 域、熔断实测、档案 21 后续、白名单源码安装(对方新列 P2)
**协作建议**:两会话并行改同一批文件已造成一次内容丢失(我已在 03-路线图 合并修复);后续应约「改前先拉取 / 分工到目录」。
## P2 两项处理完毕(21:38-21:52)
**P2-1 熔断 live 实测 → ✅ 完成(无需人为 kill)**
- 实证来源:真实故障用户(档案 52 的 `071d678a`)崩溃循环日志
- 退避序列 attempt1→4:`delayMs` **1000 → 2000 → 4000 → 8000**(base×2);`restartsInWindow` 1→4
- 熔断:第 **5** 次触发 `crash-loop-circuit-open`(`windowMs=600000`、`restartsInWindow=5/max=5`),当日 **2** 次开断
- 结论:退避 + 计数 + 开断 + 结构化日志四要素齐全 → **实测通过**;归档到 **档案 20 附录「熔断 live 实测记录」**
**P2-2 白名单源码安装支持 → ✅ 评估关闭(不做)**
- 档案 29 §六.1 原本就写明代价(以 root 跑第三方构建脚本:慢/需工具链/易失败/供应链风险)并"当前决策:不做"
- 本轮正式定稿:**不做**。理由 = 与「平台不执行第三方构建脚本」红线冲突(供应链 + 可复现性 + 性能)
- **替代路径**:admin 本地构建 → 走「上传 tgz」通道(已含 P0/P1 扫描,档案 19 §C6)✓ 覆盖源码类真实需求
- 打开条件清单(沙箱构建 / `--no-... ignore-scripts` / 仅 admin / 审计 / 全量扫描)已作为升级条件写入 **档案 29 §七**
**同步与踩坑**
- 文档库:修正后 **双端 88/88 一致 ✅**;提交推送 `e3380d9`、`ea431b1`
- **踩坑(Windows 非法文件名)**:服务器侧编辑器备份 `INDEX.md.bak-20260911111003.`(**结尾点号**)在 Windows 上无法创建 → tar 解包留下"幽灵文件",导致 `git add -A` 报 `unable to index file`
→ 处置:`.gitignore` 加 `*.bak-*`/`*.bak`(避免 -A 触碰)+ 用 **`\?\` 扩展路径**把该文件**读出**(37550 B)并**回传服务器**恢复双端对称;删除该文件因只读属性 + 尾点号在 Windows 上无法完成(`os.chmod(S_IWRITE)` 也失败)
→ 教训:**服务器侧生成备份文件时不要用结尾点号**(跨 Windows 不兼容)
## 核查:档案 05 PoC-2 三项待办是否还有必要(21:55-22:15)
用户问「管理类插件化 / 插件↔门户鉴权令牌 / portal_ping 端到端 还有必要做吗」。只读核查(10 次探针),结论如下。
**① 管理类插件化(KEY/用户管理搬进会话窗口)→ 无必要**
- 门户 `portal.html` 已是完整管理门户,导航含:**服务管理 / 密钥管理 / 用户管理 / 技能管理 / 插件管理 / 运行环境**
- admin 在会话内已可一键直达:`portal-entry` v0.4.3 提供「平台管理 / 打开管理台」(**整页跳转**,其 client 注释写明"浏览器侧跨子域调用门户会被拦截")
- 端点数:KEY = `GET/POST /api/me/keys`、`/api/me/keys/:id/select`、`DELETE /api/me/keys/:id`(**requireAdmin**);用户 = `/api/admin/users*`(ADMIN);插件投放 = `/api/plugins/business*`(ADMIN)
- 反向论点:把 admin 能力调用搬进**实例子域**,会扩大 admin 权限执行面(该域内用户可装插件、agent 可写文件)→ 与档案 39「收窄可见面/网络面」方向相反
**② 插件↔门户鉴权令牌 → 已完成(但不是用令牌实现的)→ 可关闭**
- 实际机制 = **共享会话 Cookie(`Domain=.alotbuy.com`,HttpOnly)+ 门户 CORS 白名单**
- `src/web/server.ts` 有 `isAllowedOrigin(origin, baseDomain)`:允许 baseDomain 及其子域,回 `Access-Control-Allow-Origin` + `Allow-Credentials: true`,并处理 OPTIONS;注释原文说明就是为「功能插件启停 section 在 `<user>.alotbuy.com` 上调门户 API」而加
- `business-plugins` v0.2.1 client 用 `portalHost()`(去掉实例子域 label)+ `credentials:"include"` 调 `/api/plugins/mine`,**已在生产跑通**(档案 36/37)
- 结论:再做独立令牌 = 重复建设,且无额外安全收益(同一会话、同一信任边界)
**③ portal_ping 端到端 → 原验收项已失效(安全收紧导致)**
- 工具**仍在**:`portal-entry` v0.4.3 host 面 `defineTool({name:"portal_ping"})` + `ctx.tools.register`(boot 时注册)
- 但其实现是 `fetch(process.env.DSHS_URL ?? "http://127.0.0.1:3080")`
- 档案 39 已封 `127.0.0.0/8`(实测 loopback `:3080` **BLOCKED**,"实例进程自身不需要任何 loopback 连接")→ **原验收动作现在注定失败**
- 如实补充:该机制(实例内 host 插件给 agent 注册工具)**目前仍无生产验证**——`business-plugins` 只注册 settings section(`tools.register` = 0),`workspace-scoped-picker` 不注册工具 → 若要保留这条验证,应**换用不依赖 loopback 的探针**(属重新设计,不是完成原待办)
**顺带发现(待核)**:`/api/skills/mine*`(10 条中的 6 条)已是 **auth** 守卫(普通用户可用),但现存三个 client 插件**都不含「技能」UI** → 档案 16 阶段 3 可能只落地了后端;门户「技能管理」偏 admin 共享技能。→ 可作为 C2 的核对入口。
**未改动任何文件**(纯只读核查,等用户裁定是否在路线图关闭这三项)。