461 lines
49 KiB
Markdown
461 lines
49 KiB
Markdown
# 工作日志 · 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 的核对入口。
|
|||
|
|
|
|||
|
|
**未改动任何文件**(纯只读核查,等用户裁定是否在路线图关闭这三项)。
|