277 lines
48 KiB
Markdown
277 lines
48 KiB
Markdown
# 工作日志 · 2026-09(第 23 片)
|
||||
|
|
|
|||
|
|
> ⚠️ **本目录日志已按【月】分片**(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-15.md
|
|||
|
|
> **上一片**:`2026-09-下21.md` **下一片**:`2026-09-下23.md`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
## 11:1x–12:0x — **T08 执行:S0 ✅ + S1 ✅**(用户「确认执行」)
|
|||
|
|
|
|||
|
|
- **锁纪律**:`--claim-exec "exec-cluster-1a"` → `mkdir 交接单/.doing-T08` + `OWNER` → 台账行改 🔄 执行中(同一动作完成)。**中途停 ⇒ 已按纪律释放全局锁**(`.doing-T08` + OWNER 保留作续做凭证)。
|
|||
|
|
- **S0 基线冻结**:47 = `/opt/dshs`(**无 .git** ⇒ 用文件 hash:`lib/`=8423ed1c…、`web/`=7e001f0a…、`package.json`=878f7e9a…、unit=545ebe09…、DB 131072 B、用户 6);106 = `/root/dsh-users-platform`(另起 `/opt/dshs-cluster` 隔离)。
|
|||
|
|
- **S0 三处修正**:① 不在 47 跑 smoke(47 的 `/opt/dshs/scripts/` **无 `fake-dsh.mjs`**,且不必生产留痕)② **不建分支**(工作树与其他会话共享 + 已有 11 个别人的未提交改动,建分支会把它们带走)③ 106 测试环境独立(端口 13080/13081)。
|
|||
|
|
- **查清 smoke 性质**:`ci.sh` 明说不纳入;实测**自包含**(进程内 `dbPath:':memory:'` + stand-in `fake-dsh.mjs`);**`DSHS_DB_URL` 优先于 dbPath** ⇒ 同一组 smoke 可跑 PG(S1.4 的可行基础)。
|
|||
|
|
- **S1 完成**:106 装 PG 15.19(**独立数据目录 + `pg_ctl` 手动起 + 端口 15432 + 只绑 127.0.0.1,未建 systemd 单元**,回滚=`rm -rf`)。
|
|||
|
|
- **S1.4 结论:SQLite 6/8 == PG 6/8,失败项完全相同**(`smoke-domain`/`smoke-isolation`)⇒ 两后端行为一致。
|
|||
|
|
- **假阳性已排除**:PG 侧 `dshs_smoke` 有 10 张平台表 + `schema_migrations` 有记录 + smoke 数据真的落库。
|
|||
|
|
- **S1.5 回滚演练 ✅**:加/去 `DSHS_DB_URL` 各起一次服务,`login.html` 均 **200**。
|
|||
|
|
- **S1.2 迁移脚本**:新增 `scripts/migrate-sqlite-to-pg.mjs`(md5 `15fb990a68aaae918128faf0801e9464`)。设计=列清单不写死(PG information_schema ∩ SQLite PRAGMA)+ PG 结构由**平台自己的迁移**建 + identity 列 `OVERRIDING SYSTEM VALUE` + 搬完 `setval`。实测:逐表行数一致|**uid 原样**(100001/100002)|audit id 原样|**迁移后新用户得 100003 不撞主键**。踩坑 1:`schema_migrations` 不能再搬(与平台迁移冲突,事务已回滚)⇒ 已加 `SKIP`。
|
|||
|
|
- **验收三关**:① smoke 两后端一致 6/8 ② 单机形态可跑 ✅ ③ **47 四个 hash 逐字一致 ✅**(DB 字节一致;用户 6→7 是平台在正常服务,非本单动作)。
|
|||
|
|
- 🔴 **新发现(P0 阻塞,影响 S3/S6):106 上 account 隔离模式实例起不来** —— `node: OpenSSL configuration error: Permission denied fopen(/etc/ssl/openssl.cnf)` exitCode 13。**根因(已复现+硬证据)**:bwrap 为绑定 `/etc/pki/tls/certs` **自建的中间目录** `/etc/pki`、`/etc/pki/tls` 权限是 **`drwx------`(0700)** ⇒ uid 无法穿越 ⇒ 而 106 的 `/etc/ssl/openssl.cnf` 是**指向 `/etc/pki/tls/openssl.cnf` 的符号链接** ⇒ **EACCES(非 ENOENT)**。**47 无此文件**故不触发 ⇒ **设计 §14.3「机器基线不能跟着迁移」的首个真实实例**。
|
|||
|
|
- **已验证修法 D(采纳)**:绑定前加 `--perms 0755 --dir /etc/pki` + `--perms 0755 --dir /etc/pki/tls` —— **零权限扩大**。
|
|||
|
|
- **E 被否决**:整绑 `/etc/pki` 也能过,但 `/etc/pki/tls/private/postfix.key` 会暴露给所有实例(违反 R5)。
|
|||
|
|
- **C 不可行**:`bind 不能覆盖悬空符号链接`(`Can't create file at /etc/ssl/openssl.cnf`)。
|
|||
|
|
- ⚠️ 该问题**普适**:任何"宿主配置文件放 X、用符号链接从 Y 指过去"的发行版都会踩。
|
|||
|
|
- **发现 2(待定性)**:`smoke-domain` 打印 `proxy redirect -> /somewhere`,断言期望 `/u/u1/dsh/somewhere` ⇒ legacy 子路径 Location 重写未生效;两侧一致 + 我只读未改码 ⇒ **既有行为**,需单独立项。
|
|||
|
|
- **载荷清单教训**:第一版漏 **`assets/`** ⇒ `proxy.js` 因缺 `assets/inject/recovery.js` **硬失败**(档案 81 R1)。**S6 的 join 脚本必须列全**:`lib scripts package.json assets web poc cordis.patch.yml ensure-role-profile-patch.cjs mksess.cjs README.md STANDARD.md`。
|
|||
|
|
- **改动文件**:`scripts/migrate-sqlite-to-pg.mjs`(本机代码仓,**未 commit**,§4 提交边界);`交接单/T08-*.md §9` 执行记录;`.doing-T08/OWNER`;台账 T08 行。
|
|||
|
|
- **下一步**:**S1.6 先修 bwrap 中间目录 0700**(涉及 `orchestrator.ts` 隔离层,需 **47 回归** + 106 上 `smoke-isolation` 转绿)→ 再回 S2(schema v7 + `lease.ts`)。
|
|||
|
|
|
|||
|
|
## 12:1x–13:2x — **S1.6 完成:bwrap 中间挂载点 0700 已修(双机验证)**
|
|||
|
|
|
|||
|
|
- **用户「确定执行」** ⇒ 抢锁 → 改 `src/supervisor/orchestrator.ts`(**只增不减**):
|
|||
|
|
- 新增模块级 `mountParentDirList(dest, stopAt)` + `mountParentDirArgs(dest, stopAt)`
|
|||
|
|
- 调用点 2 处:① `/etc` 白名单装配 ② `--bind root root` 与共享技能层绑定**之前**(同因)
|
|||
|
|
- 🔴 **救命的一条(回归测试抓到的)**:第一版用 `--perms 0755 --dir` 在 106(bubblewrap **0.11.0**)通过,但 **47 是 bubblewrap 0.4.0 ⇒ `bwrap: Unknown option --perms`** ⇒ **沙箱起不来 = 所有实例全挂**(47 上 `/etc` 可见条目 78 → **0**,一眼可见)。
|
|||
|
|
⇒ **改用 `--tmpfs`**:0.4.0 就支持,且**自带 0755**(本文件注释早已写明)。47 实测 `/etc` 条目 **78 == 78 零变化**。
|
|||
|
|
📌 **通用结论:改 bwrap 参数前必须先看目标机的 bubblewrap 版本**(47 = 0.4.0 这个基线很旧,很多新选项都没有)。
|
|||
|
|
- **修复效果(三机证据)**:① OpenSSL EACCES 出现次数 **1 → 0** ② `/etc` 可见条目 **495==495(106)/ 78==78(47)** ③ bwrap 接受参数 rc=0 ④ 中间目录 0700 → 0755 ⑤ **私钥 `/etc/pki/tls/private` 仍不可达** ⑥ 沙箱内 `/opt` 只有该用户自己的树、平台目录不可见。
|
|||
|
|
- **忠实链探针**(`systemd-run --scope` → bwrap → setpriv → node,cwd 在用户工作区):`cwd=…/ws/proj uid=50001`,**相对路径写成功** ⇒ account 启动链本身是通的。
|
|||
|
|
- ⚠️ **`smoke-isolation` 不能作为 account 模式验收(夹具结构性缺陷,非平台缺陷)**:① 夹具路径必须在沙箱绑定集内(`fake-dsh.mjs` 放 `/opt/...` ⇒ `Cannot find module`;放 `/usr/local/...` 就好)② **数据根用 `mkdtemp(/tmp)` 会与沙箱的 `--bind <userRoot>/tmp /tmp` 冲突** ⇒ `bwrap: Can't chdir to /tmp/dsh-smoke-iso-XXXX/...: No such file or directory`(生产 `dataRoot=/var/lib/dshs` 不撞)。⇒ 遗留小项:修夹具或另写 account 验收脚本。
|
|||
|
|
- **47 未部署**(本步只在 106 部署;47 只做**同参数实测**,在临时目录跑 bwrap,不碰生产进程)。**criterion ③ 终检通过**:`lib/`/`web/`/unit/DB 四项与开工基线**逐字一致**,`dshs` active。
|
|||
|
|
- **产出**:`交接单/T08-*.md §10`(S1.6 执行记录);`orchestrator.ts` md5 记入 OWNER;`docs-audit` rc=0。
|
|||
|
|
- **下一步**:S2(schema v7 + `lease.ts`)。
|
|||
|
|
|
|||
|
|
## 13:3x–14:1x — **S2 ✅ + S3 ✅**(用户「一直执行完毕」)
|
|||
|
|
|
|||
|
|
### S2 · 数据模型 v7 + 租约服务
|
|||
|
|
- **改动**:`db/schema.ts`(v7:`dsh_hosts` + `dsh_instances` 加 `host_id/epoch/heartbeat_at/lease_until` + 两索引,**SQLite 与 PG 两方言同写**)|`db/types.ts`(`DshHost`/`ClaimResult`/`clusterInstanceId()`)|`db/adapter.ts`|`db/repo.ts` + `db/sqlite.ts`(SQLite 侧)|`db/pg.ts`(`UPDATE … RETURNING` 原子抢占)|**`supervisor/lease.ts`(新)**:`InstanceLease` + `stillHolder()` + **`ttl > 2×renew` 硬校验**|**`test/lease.test.mjs`(新,10 例)**。
|
|||
|
|
- **验收**:**SQLite 10/10 == PG 10/10**(PG 侧在 106 上用 `LEASETEST_PG_URL` 跑)。覆盖单写者 / **fencing(旧持有者续租必失败)** / 过期可抢占且 epoch 递增 / epoch 不复用 / 对账视图 / `renewAll` 失权清单。
|
|||
|
|
- 🔴 **踩坑(已修)**:`INSTANCE_COLS` 漏 v7 的 4 个新列 ⇒ 行能匹配但 `hostId` **恒为 null**(3 个用例挂住)⇒ **两方言的列清单必须同步**。
|
|||
|
|
|
|||
|
|
### S3 · Worker agent + RemoteSpawner(**端到端通过**)
|
|||
|
|
- **改动**:**`worker/agent.ts`(新)**(`/healthz` `/instances` `/launch` `/stop` `/status` `/endpoint` `/fence` `/restart-probe` `/watchdog` `/touch`;bearer 鉴权 `timingSafeEqual` + 白名单动作 + **幂等缓存** + **epoch 跟踪**)|**`supervisor/remote-spawner.ts`(新)**(实现 `Spawner` 全接口;**重试 200ms/1s/3s 且全程复用同一 `operationId`**)|`orchestrator.ts` 只加两个 additive 方法(`listUserInstances()`、`launchTokenOf()`)|`config.ts`(`DeployMode` 加 `'cluster'` + 4 个配置)|`web/server.ts`(cluster 分支 + 缺地址 fail-loud)|`cli.ts`(`dshs worker`)|`scripts/verify-cluster-agent.mjs`(新)。
|
|||
|
|
- **关键设计**:**Worker 不碰 DB** —— `apiKey`/`uid` 由 Manager 在 `/launch` 时投递、只存内存(与 k8s per-user Secret 同思路),uid 兜底用纯函数 `hashUid`。
|
|||
|
|
- **验收(106)**:`verify-cluster-agent.mjs` → **OK**,四项保证全绿:路由/代理层零改动 ✅|**launch token 回传(P0-6)**✅|幂等键 ✅|**self-fencing(epoch 1→2 触发)**✅;另证实例确实在 worker 上、代理经远端协议命中 fake-dsh。
|
|||
|
|
- 🔴 **两处踩坑(已修)**:
|
|||
|
|
1. **夹具 `fake-dsh.mjs` 不吐 launch token**(真实 dsh 会打 `dsh web: http://127.0.0.1:<port>/?token=…`)⇒ 平台只能等 **10 s 超时**、`launchToken` 恒 undefined ⇒ **「登录直达会话」在本机测试里根本测不到**。补上后 launch 从 10.2 s 变即时。**连带暴露 `smoke-subdomain:78` 的陈旧断言**(把 URL 写死成不带 token,是"夹具不吐 token"的意外产物)⇒ 已改为 `startsWith(子域)` + 断言不回环端口;**复跑冒烟回到 6/8、失败项与基线完全相同**。
|
|||
|
|
2. **agent 在"实例已在跑"的路径上没记 epoch** ⇒ `/fence` 拿不到 epoch、self-fencing 永不触发 ⇒ 修法 = **epoch 必须在尝试 spawn 之前落**(记的是「Manager 的意图」)。
|
|||
|
|
- ⚠️ **影响后续的重要发现**:**今早 06:26 另一会话把 k8s 后端连同 `src/tcp-bridge.ts` / `src/web/file-service.ts` / `src/fs/k8s-user-fs.ts` 一起删了**(当时按"k8s 专用"处理)⇒ 设计里 S5/S6"复用跨机原语"的假设**失效**。两条出路(S4 后再定):(a) 按 cluster 语义自建 bridge + 每用户 file 服务;(b) **让 agent 直接隧道**(只暴露 1 个端口、`portGuard` 语义完整,需实现 WS 隧道)——**(b) 更优**。S3 的**同机**形态两者都不需要,不影响已完成部分。
|
|||
|
|
- **三关**:① 冒烟 6/8(失败项与基线相同 = 无回归)② 单机形态可跑 ✅ ③ **47 三个 hash 逐字一致 ✅**
|
|||
|
|
- **下一步 S4**:1a 形态(2 台 Manager,一台 `capacity=0` + Worker-01 同机)+ 单活退化 + `dsh_hosts` 注册与心跳。⚠️ 跨机代理需先定 11.3 的 (a)/(b);**1a 同机不受影响,可先做**。
|
|||
|
|
|
|||
|
|
## 13:5x–14:2x — 设计纠正(数据分层)+ **S4 ✅**
|
|||
|
|
|
|||
|
|
### 🔴 设计纠正(用户口径):**Worker 不是"不许有数据库"**
|
|||
|
|
- 用户原话:**「不一定 worker 不连数据库,有些插件有可能有大量数据需要保存和管理、长期使用」** ⇒ 我在 §1.2 写的「Worker ❌ 不碰 DB」**过窄**:本意只是**控制面数据(尤其归属)不能由 Worker 写**;插件长期数据根本不在同一层。
|
|||
|
|
- **已改**:设计文档新增 **§1.3「数据分层」**(四条判据 + 插件三种落点);并同步修正 `agent.ts`/`remote-spawner.ts`/`cli.ts` 三处注释(注释是后来人读代码的唯一线索,必须一致)。
|
|||
|
|
- **三条判据(已写进 MEMORY.md 常驻)**:① 是归属/租约吗 ⇒ **只有 Manager 能写** ② 跟着用户走吗 ⇒ 放**实例 home**(插件库实测 `home/.dsh/mcn-plugin.db`),**别塞控制面 PG** ③ 是本机运维用的吗 ⇒ 放 **Worker 本地库**。
|
|||
|
|
- **插件三种落点**:per-user SQLite 在 home(现状,天然跟迁移)|量大要真 DBMS ⇒ 每用户一库 + 数据目录放位置无关存储|跨用户聚合 ⇒ Manager 侧离线汇总,**不在 Worker 上做**。
|
|||
|
|
|
|||
|
|
### S4 · 归属租约接进启动路径 ✅(端到端五项全绿)
|
|||
|
|
- **改动**:`spawner.ts`(`launch` opts 加 **`epoch`**)|`remote-spawner.ts`(透传 epoch)|**`supervisor/leased-spawner.ts`(新)**(`launch` 前 claim、失败抛 `LeaseBusyError`=退让;心跳 renewAll、失权即向 agent 下发更高 epoch;`stop` 时 release;启动即**注册 host + 起心跳**)|`web/server.ts`(cluster 分支 = `LeasedSpawner(RemoteSpawner)`,租约参数走 env)|**`scripts/verify-cluster-lease.mjs`(新)**。
|
|||
|
|
- **验收(106,两 Manager 共享 PG + 同一 agent,短 TTL 秒级制造过期)**:`dsh_hosts = m-1:up, m-2:up`|① 归属落库 `host_id=m-1 epoch=1`|**② 单写者:m-2 抛 `LeaseBusyError(holder=m-1)`、归属未被改动**|③ 释放即接手 `epoch=2`(单调递增,不用等 TTL)|**④ 失权即 self-fence:worker 实例数 1 → 0**、且归属未被误清。
|
|||
|
|
- **回归**:冒烟 **6/8**(失败项与基线相同)|S3 端到端 **OK**|lease 单测 **SQLite 10/10 == PG 10/10**。
|
|||
|
|
- **编译期两坑(已修)**:① `quotaInfo` 签名被别的会话扩过(多 `baseMb`)⇒ 委托要按**当前**接口对齐 ② 我把心跳停止器命名 `stop()` 与 `Spawner.stop(userId)` **重名** ⇒ 改 `stopHeartbeat()`。
|
|||
|
|
- **47 未变** ✅(三个 hash 逐字一致)。
|
|||
|
|
- **下一步 S5/S6**:按 11.3/(b) **让 agent 直接隧道**(只暴露 1 个端口,`portGuard` 语义完整;需实现 HTTP/WS 转发),再做 join 脚本与计划内迁移。
|
|||
|
|
|
|||
|
|
## 14:0x–15:0x — **S5 ✅ + S6 ✅ + S7(部分)**,用户要求「执行到任务完成」
|
|||
|
|
|
|||
|
|
### S5 · 跨机文件面
|
|||
|
|
- **改动**:agent 加 `/fs/*`(init/list/mkdir/create/upload/read/isdir/plugins/handoff/root,**复用同一个 `LocalUserFs`**)|**`src/fs/remote-user-fs.ts`(新)**(路径安全**复用 `fs-guard.resolveWithinRoot`**,不重写;`resolvePath` 按 **worker 的 dataRoot** 做纯数学)|`provider.ts` 按模式选实现 + `DSHS_CLUSTER_WORKER_DATA_ROOT` + 启动探测不一致就报 |`scripts/verify-cluster-fs.mjs`。
|
|||
|
|
- **验收(关键设计:Manager 的 dataRoot 故意与 worker 不同 ⇒ 才能证明走远端)**:门户 tree/mkdir/upload 全经远端 ✓|文件**落在 worker、不在 manager** ✓|`bad_path` 与本地**同源** ✓|下载回读逐字一致 ✓。
|
|||
|
|
- 🔴 **踩坑(已修)**:agent 的 `/fs/create`、`/fs/upload` 直接 return **字符串** ⇒ Fastify 发 `text/plain` ⇒ 调用方 `JSON.parse` 失败 = **静默 500** ⇒ 必须包 `{name}`。
|
|||
|
|
- **基线约定**:cluster 下**所有 worker 的 dataRoot 必须同路径**(同镜像可满足),不一致启动即报。
|
|||
|
|
|
|||
|
|
### S6 · 多 worker + 容量准入 + 计划内迁移(**方案落点**)
|
|||
|
|
- **改动**:`InstanceLease.held` 记 `{epoch, hostId}`|`Spawner.launch/stop` 支持显式 `hostId`|**`RemoteSpawner` 多 host**(`hosts`/`hostsProvider` **懒加载+30s TTL ⇒ 新增 worker 不用重启 Manager**、`hostIdFor` 按归属路由、**未知 host 直接报错拒绝静默改投**)|**`LeasedSpawner` 选机**(`selectHost` 容量准入 + `agentFor` 让 fence 发给**实例所在那台**;**先选机再认领**)|**管理面** `GET/POST /api/admin/hosts`(**绝不下发 agentToken**)+ `POST /api/admin/users/:id/dsh/migrate`|`scripts/verify-cluster-migrate.mjs`。
|
|||
|
|
- **验收(两 worker 共享 dataRoot)**:① 容量准入把高水位机排除 ⇒ 落 w-a ✓ ② 迁移 w-a→w-b(drain→拉起→**epoch+1**)✓ ③ 迁移后代理 200 + 文件可读(数据不搬家)✓ ④ 边界 `already_there`/`unknown_host` 明确报错 ✓。
|
|||
|
|
- **容量语义(已定)**:`>0` 声明并参与准入(`已用+预留 ≤ 容量`,`DSHS_CLUSTER_RESERVE_MB` 默认 512)|`≤0` 未声明(不设限)|**`-1` 显式禁用承载**(专用 Manager)。新增 **`DSHS_CLUSTER_REGISTER_SELF=0`**:**一个 agent 只应有一条 host 记录**。
|
|||
|
|
- 🔴 **三条教训(都已修)**:① **verify 脚本挂死** —— 测试没收尾实例 ⇒ `fake-dsh` 子进程**继承 stdout** ⇒ 管道不关 ⇒ ssh 一直等;**顺带发现生产问题**:agent 的 `stop()` 只关 HTTP **不收实例** ⇒ worker 停机留孤儿 ⇒ 已改「先 teardown、再关 HTTP」,四个脚本收尾也统一走 `agent.stop()` ② **`selectHost` 改了语义**(不再"谁拉起就归谁")⇒ S4 接管步骤须**显式指定目标机** ③ `holdings()` 结构变了 ⇒ 单测断言同步改(非回归)。
|
|||
|
|
|
|||
|
|
### S7 · 观测面(部分完成)
|
|||
|
|
- 新增 **`dshs doctor [--json]`**(node/cgroup/**swap**/**bwrap 版本(<0.5 提醒 `--perms` 不可用)**/setpriv 降权/dataRoot/DB;**退出码非 0 = 硬失败**,可当 join 门禁)|**`dshs cluster status [--json]`**(worker 目录+水位+实例数+**过期租约**,并明写"需人工确认才可接管 · R9")。
|
|||
|
|
- **自动接管按设计保持关闭**(`expiredAll()` 只报不做;人工路径 = migrate API),与 R9 一致。
|
|||
|
|
- **仍待做**:`join-worker.sh` 一键装机(现在 join = 起 agent + 注册两步手工)|集中日志/metrics|`smoke-domain` 定性。
|
|||
|
|
|
|||
|
|
### 总验收(S0–S7 全量复跑,15:0x)
|
|||
|
|
**冒烟 6/8**(失败项与开工基线**完全相同**,全程无回归)|**四个端到端全 OK**|**lease 单测 SQLite 10/10 == PG 10/10**|**残留进程 0**|**47 三个 hash + unit + dshs active 逐字一致**。
|
|||
|
|
|
|||
|
|
## 15:2x–16:0x — **真实部署形态功能确认**(用户「按最佳决策执行…执行完毕后检查确认功能」)
|
|||
|
|
|
|||
|
|
### 为什么补这一步(缺口)
|
|||
|
|
此前**所有**验证都是"同一进程内起两个 Fastify" ⇒ **部署形态从未验过**(独立进程 + 真 HTTP + 真 PG + 真 `dsh` 子进程)。新增 **`scripts/verify-cluster-live.mjs`**:真起 **2 个 agent 进程 + 1 个 Manager 进程**。
|
|||
|
|
|
|||
|
|
### 结果:**全绿**(①–⑪)
|
|||
|
|
注册→审批→登录 ✓|mkdir 落 worker ✓|拉起真 dsh(URL 带 token)✓|**running=true(account 沙箱内)** ✓|`/enter` 登录直达 ✓|**实例页面 200**(经代理、跟随 303)✓|停止 ✓|w-2 注册(列表**不含 agentToken**)✓|**迁移 w-1→w-2(epoch=3)** ✓|迁移后 enter 给**新 token URL**、页面再 200 ✓|`doctor` rc=0 + `cluster status`(w-1:0 / w-2:1 实例)✓。
|
|||
|
|
|
|||
|
|
### 🔴 真实部署抓到 3 个真 bug(单进程测试**根本测不出**)
|
|||
|
|
1. **我 S1.6 引入的 `--tmpfs` 遮蔽 bug**:`bwrap: Can't chdir to <userRoot>/ws/proj: No such file or directory` —— 用户根与共享技能层**嵌套同前缀**时,技能层的中间目录 `--tmpfs` 插在 `--bind root root` **之后** ⇒ 后挂的 tmpfs **把已绑好的用户根遮掉** ⇒ 实例崩溃循环。**修:所有挂载点的中间目录统一前置 + 去重 + 由外到内**(一处生成,禁止"就近创建")。
|
|||
|
|
2. **`claimInstance` 没记 `folder`/`patch`**:集群模式实例行由它创建(local 模式不写库)⇒ `folder` 恒 NULL ⇒ **迁移复现不了启动参数**(`bwrap: Can't chdir to :` **空路径**)⇒ 崩溃循环。**修**:认领时一并落库 + 迁移无 folder 就 **409 `no_folder_recorded`**(fail-loud)。
|
|||
|
|
3. **检查脚本自身的健壮性**:启动窗口内代理会**中途断连**(`other side closed`,需 try/catch 重试);`/api/dsh/status` 的实例视图**不含 launchToken**(token 要用 `/enter` 判定);dsh 首页 **303** 需跟随重定向。
|
|||
|
|
⇒ 这三条不修会把**真实部署误判为失败** —— 印证设计 §13「组件级测试不能替代部署级验收」。
|
|||
|
|
|
|||
|
|
### 47 侧说明(R11:不隐瞒)
|
|||
|
|
本单在 47 上**零写操作**(`find /opt/dshs -newermt 11:00` 为空);实例 scope 由开工 1 → 0,最可能是**平台自身 idle-reap**(档案 08)或该实例自行退出,**非本单动作**(从未在 47 上 stop/kill)。
|
|||
|
|
|
|||
|
|
## 16:1x–16:4x — **真跨机演练通过**(用户:"重点就是验证跨机,47 当 Manager / 106 当 Worker")
|
|||
|
|
|
|||
|
|
### 关键障碍与绕法(实测)
|
|||
|
|
- 🔴 **106 公网入方向被腾讯云安全组挡住**:106 开 19100,**47 与本机都连不上**;放通只能在控制台点 ⇒ **绕法 = 让 Worker 主动拨 Manager 的 `ssh -R` 反向隧道**(走已开放的 SSH 端口,**两端都不新增监听面**)。
|
|||
|
|
- 🔴 **实例只监听 `127.0.0.1`**(portGuard 设计前提)+ **端口是动态的**(`findFreePort()`)⇒ 用 **ControlMaster + `ssh -O forward/cancel`** 在同一条长连接上**动态加减转发**(新增 `src/worker/tunnel.ts`)。
|
|||
|
|
- **安全**:106 生成专用密钥,47 的 `authorized_keys` 加 **`restrict,port-forwarding`**(禁 shell/pty/ptys,只允许转发)。
|
|||
|
|
|
|||
|
|
### 现场
|
|||
|
|
`47 Manager(127.0.0.1:13080,不公网暴露)` ←── `127.0.0.1:19000/19001` ──ssh -R──▶ `106 agent(w-106/w-106b)`;**控制面 PG 也在 106**(经隧道 15432)。
|
|||
|
|
|
|||
|
|
### 结果:`scripts/verify-cluster-cross.mjs`(在 47 上跑)**九步全绿**
|
|||
|
|
⓪ worker 可达(隧道 ready)|① 注册 w-106(不含 agentToken)|② 用户流程|③ 文件面 mkdir 落 106|④ 拉起 running、worker /instances=1|**⑤ 跨机页面 200**|⑥ 第二台(106 模拟)已注册|**⑦ 跨 worker 迁移 w-106→w-106b(epoch=2)**|⑧ 迁移后新 URL 页面 200|⑨ 停止。
|
|||
|
|
**三方交叉取证**:106 有该用户家目录与 `ws/proj`、两 agent 在跑;47 `cluster status` 显示 w-106/w-106b 均 up;**47 生产逐字未变**(三 hash 一致、`/opt/dshs` 0 改动、13080 无公网监听)。
|
|||
|
|
**顺带照出机器基线差异**:47 = **cgroup v1** + `dsh` 在 `/usr/local/bin/`;106 = **cgroup v2** + `/usr/bin/dsh`。
|
|||
|
|
|
|||
|
|
### 边界(不夸大)
|
|||
|
|
用的是**演练级传输(SSH 反向隧道)**,**不是**设计 §2.3 的最终拓扑(受控网段白名单/正式隧道服务)⇒ 证明的是"**软件在跨机下正确**",不是"生产网络已就绪"。仍待做:① 云控制台放通受控网段(或隧道服务化)② 生产切换演练(drain→切→回滚,需窗口)③ `join-worker.sh` 一键装机。
|
|||
|
|
**现场保留待查验**(拆除三步见 `交接单/T08-§15.5`):47 只新增 `/opt/dshs-cluster/`(新目录)+ `authorized_keys` 一行。
|
|||
|
|
|
|||
|
|
## 16:4x–17:1x — 云安全组取证 + **域名形态访问验证** ✅
|
|||
|
|
|
|||
|
|
### 云端限制取证(用户问"是端口限制还是什么")
|
|||
|
|
- **是腾讯云安全组,不是 106 主机防火墙**:106 上 `firewalld` **inactive**、`iptables` 只有腾讯云「云镜」ipset **黑名单**(只 DROP 已标恶意的源)、`nft` **0 条** ⇒ 主机层面 19100 是允许的;而"106 自测 200 / 47 与本机都连不上"⇒ 拦在**云平台层**。
|
|||
|
|
- **47 的出口 IP = `47.77.182.89`**(106 侧实测对端地址)⇒ 若走 B 方案,安全组写 `源 47.77.182.89/32`。
|
|||
|
|
- 实例端口取自内核临时端口段 **`32768-60999`** ⇒ 要放通必须**先收窄到可声明的端口池**(我建议加 `DSHS_PORT_MIN/MAX`),否则等于摊开 2.7 万端口。
|
|||
|
|
|
|||
|
|
### 域名形态访问(演练环境,不动生产/DNS/证书)
|
|||
|
|
`scripts/verify-cluster-domain.mjs`(在 47 上跑)**全绿**:`https://<user>.test.alotbuy.com/?token=…` → **真 dsh 实例页**(`<base href="/">`)、越权(A 的 cookie 打 root 子域)**403**、实例确实在 106。
|
|||
|
|
🔴 **两次假阳性,都要记住**:① 判据只用状态码 ⇒ **门户/登录页也是 200**;② **`fetch` 会静默丢弃 `Host` 头**(Fetch 规范禁止头,undici 忽略)⇒ 落到"无租户"门户路由、假通过。**⇒ 多租户路由验证必须 `curl -H Host:`(或 node:http),判据要能区分"实例页 vs 门户页"**(MEMORY 里早有这条,本次仍踩了)。
|
|||
|
|
🔴 **另一个重复踩的坑**:`pkill -f ".../lib/cli.js ..."` 匹配不到实际 argv(进程是**相对路径** `node lib/cli.js …`)⇒ **旧进程没死、新进程绑定失败**,看起来"重启了"其实没有(在 47 的 Manager 与 106 的 agent 上各踩一次)⇒ **一律按端口定位 PID 再 kill**(`ss -lntpH 'sport = :PORT' | grep -oP 'pid=\K[0-9]+'`)。
|
|||
|
|
✅ 顺带验证:按端口 kill agent 后 **teardown 确实把实例全收了**(残留 `dsh --profile` = 0)。
|
|||
|
|
|
|||
|
|
### 生产仍未切(明确)
|
|||
|
|
`alotbuy.com` 仍由 **nginx(443) → 生产 `dshs`(`127.0.0.1:3080`,`deployMode` 未设 = local)** 服务,7 个用户 home 在 **47**。要"alotbuy.com 登录 → 实例在 106"需要一次生产切换:① 用户数据面到 106/共享存储 ② 生产库 SQLite→PG(脚本已验证)③ unit 加 cluster env + 重启 ④ 最后一跳(隧道或放通+端口池)⑤ 通配证书/DNS(Nginx 侧)。演练 Manager 在 `127.0.0.1:13080`、**公网不可达**、库是独立的 `dshs_cross` ⇒ **与生产互不影响**。
|
|||
|
|
|
|||
|
|
## 10:2x — 产出交接单 T08(集群化落地 · 全程兼容单例模式)
|
|||
|
|
|
|||
|
|
- **用户要求**:梳理落地方案 → 拆分实现步骤 → 核对无误后逐步执行 → **要兼容现在的单例模式运行**。
|
|||
|
|
- **交付**:`dsh-server-docs/交接单/T08-集群化落地-兼容单例模式.md`(8 段模板齐备 + 步骤 S0–S7);`交接单/README.md §一` 已加 T08 行(🔵 待执行)。
|
|||
|
|
- **硬约束(用户口径落地成机械判据)**:**每步结束必须过三关** —— ① smoke 全绿 ② 单机形态(`deployMode=local`)仍可完整跑通 ③ **47 生产 commit 一行不变**。第 ③ 条是"兼容单例模式"的硬证据(验收 §6 第 2 条)。
|
|||
|
|
- **步骤概览(每步自带验证 + 回滚)**:
|
|||
|
|
- **S0** 前置与基线冻结(抢锁 / 记录 47 与 106 基线 / 47 跑一遍 smoke 存对照基线 / 开分支 `feature/cluster-1a` / 106 建**独立**测试目录 `/opt/dshs-cluster` 端口 13080)
|
|||
|
|
- **S1** SQLite→PG(**不改业务逻辑**;迁移脚本;**同一组 smoke 两侧对比**;回滚 = 去掉 `DSHS_DB_URL`)
|
|||
|
|
- **S2** 数据模型 v7(`dsh_hosts` + 归属/租约列,**SQLite 与 PG 两份同步**;`lease.ts` 纯逻辑 + 单测)
|
|||
|
|
- **S3** Worker agent + `RemoteSpawner`(**同机自连验证,拓扑不变**;含 **launchToken 回传**;回滚 = 切回 LocalSpawner)
|
|||
|
|
- **S4** 1a 形态(2 台 Manager,其中一台 `capacity=0`;单活退化验证)
|
|||
|
|
- **S5** 文件面跨机(`RemoteUserFs` 复用 file-service)
|
|||
|
|
- **S6** 第二台 Worker + `join-worker.sh`(**必须建号** —— §17.3 实测 uid 无 passwd 条目则 `os.userInfo()` 失败)+ 计划内迁移
|
|||
|
|
- **S7** 自愈与观测(心跳用 HTTP **不用 ping**;自动接管开关默认关)
|
|||
|
|
- **决策**:D1–D5 **已定**(双活+支持单活 / 存储可插拔 / 自建 PG / 跨云只当测试床 / **测试环境用 106 另起一套**);⏳ **仅 D6「生产拓扑最终落点」需用户拍板,且不阻塞 S0–S5**;D7(`MemorySwapMax`)**明确不在本单**,只做只读确认。
|
|||
|
|
- **范围纪律**:⛔ 不动 47 生产;⛔ 不动 106 既有 `dsh-users-platform`;⛔ 不动 `orchestrator.ts` 的隔离/配额;⛔ 不加自动化任务。
|
|||
|
|
- **文档库检查(改完必跑)**:`docs-audit.py` rc=0(无 P0、无悬空引用)|`docs-manifest.py` rc=0(已刷新)|`docs-consistency.py` rc=0。
|
|||
|
|
`docs-sync-check.sh` 报 **5 内容不一致 + 1 仅本地** —— 其中 **属于我的是 3 个**(新增 T08、`交接单/README.md`、`docs-manifest.json`);**另 3 个是别的会话的未提交改动**(`BRIEF.md` / `DEPLOY-本部署.md` / `skills/dsh-opensource-release/SKILL.md`)⇒ **我只报不动、不推**(§三.0.⑥ 幽灵文件纪律)。**规划会话不 scp**(§四),服务器镜像留给执行会话。
|
|||
|
|
- **锁**:`--claim-exec "集群化落地规划(09-15)"` → 检查 → `--release-exec` **已释放并复核**(`交接单/.exec-lock` 不存在、无 `.doing-*`)。理由:要等用户核对,不能挂锁空转(§6「持锁期间若要等用户拍板 → 先释放再等」)。
|
|||
|
|
|
|||
|
|
## 06:3x — 源仓下线 K8s 后端 + 删除 @kubernetes/client-node(路线 C)
|
|||
|
|
|
|||
|
|
- 用户指令:「又不是 dsh 主程序的,也不是插件需要的 ⇒ 可以备份起来,从项目代码中删除」⇒ 走影响说明 §9 的**路线 C**。
|
|||
|
|
- ⚠️ 动手前查到的关键事实(已向用户点明):**源仓仍保留 K8s 后端**,`src/supervisor/{k8s-spawner,leader,reconcile}.ts` 三文件 import 该依赖 ⇒ 「回源仓直接 npm uninstall」会打断源仓 tsc(我上一轮的表述有误,已更正)。
|
|||
|
|
另:《集群化改造方案 Manager/Worker》把这些文件列为**要照抄/泛化的模板**(leader.ts 租约 / k8s-spawner.ts stampFencing / k8s-user-fs.ts + file-service.ts → RemoteUserFs / tcp-bridge.ts)⇒ 备份按「模板」组织,不丢。
|
|||
|
|
- 执行(源仓 `D:/github/dsh_shenxian`,基线 HEAD `ed0c3c0`,工作树干净):
|
|||
|
|
1. 9 个文件 `mv` 到 **`D:/github/_dsh_shenxian_K8s后端备份_20260915/`**(含 README.md:操作 / 还原命令 / 模板清单 / 刻意保留项);
|
|||
|
|
2. 脚本 `_retire_k8s_from_source.py`(CRLF 自适应,**预演→落盘→复跑幂等**)改写 4 个文件 14 处:`provider.ts` 整份重写、`server.ts` 换 fail-loud 守卫 + LocalSpawner、`cli.ts` 摘 5 import + 选主块 + 2 子命令 + dispatch + HELP、`package.json` 摘 2 测试入口 + smoke:file-service;
|
|||
|
|
3. `npm uninstall @kubernetes/client-node --package-lock-only`(用 `--package-lock-only` 避开原生依赖重编译;**不手改 lock**);
|
|||
|
|
4. 收尾 3 处**悬空 JSDoc**(`user-fs.ts` / `spawner.ts` ×2,文案**逐字取自导出侧规则** ⇒ 导出物保持逐字节不变);
|
|||
|
|
5. 文档防坑:`docs/k8s-deploy.md` / `k8s-deployment.md` 加**状态横幅**(代码已下线,`dshs file-service`/`tcp-bridge` 已不存在);`README.md` 那句失实的「模式 B 已完整落地」改为「已于 2026-09-15 下线」。
|
|||
|
|
- 验证(最终态):`tsc --noEmit` **exit 0**|`npm test` **36 测试 / 35 通过 / 0 失败 / 1 跳过** + 注入校验全绿|`npm run verify` **exit 0**|依赖 **0 命中**|被删符号 **0 命中**|`node lib/cli.js --help` 只剩 server/bootstrap-admin|源仓差异 **19 文件 / +29 −2690**。
|
|||
|
|
- ⚠️ 刻意保留(非遗漏):`deploy/`·`poc/01–04`·`Dockerfile.dsh`·`docs/k8s*.md`·`config.ts` 的 7 个 K8s 配置字段与 `DeployMode` 联合类型(方案列为未来可选;`server.ts` 仍 fail-loud,填 `k8s` 启动即报错)。
|
|||
|
|
- 导出侧同步:`_build_export.py` 的 `EXCLUDE_FILES` / `REGEX_RULES` ⑦ 段现为**幂等空转(命中 0)→ 注释标明「防御性保留」**;台账/清单/影响说明/`MEMORY.md` 均已更新为「批 3 ✅ 已清,至此清单全部清零」。
|
|||
|
|
- ⚠️ 未提交:源仓 19 个文件改动**未 commit / 未 push**(等你发话)。
|
|||
|
|
|
|||
|
|
## 06:3x — 源仓 K8s 下线改动**已提交**(未推送)
|
|||
|
|
|
|||
|
|
- 提交前取证「不影响当前项目」:生产 `deployMode=local`(`DEFAULT_DEPLOY_MODE='local'`,生产无覆盖)⇒ 被删的 `deployMode === 'k8s'` 分支**本来就不可达**;`file-service` / `tcp-bridge` 子命令**只在 K8s Pod 内**调用;`DeployMode` 联合类型与配置字段**未动**(env/配置兼容)。唯一行为变化:真设 `DEPLOY_MODE=k8s` 时**启动即报错**(原会 new 一个已不存在的类)—— fail-loud,不静默降级。
|
|||
|
|
- 提交:`cf8b7b1` `chore(k8s): 下线 K8s 后端形态,移除依赖 @kubernetes/client-node` —— **19 文件 / +29 −2690**(含 `package-lock.json` −716 行,由 npm 自行重算)。提交后工作树**干净**。
|
|||
|
|
- 只 `git add` 了自己改的 19 个路径(**未用 `git add -A`**,按 R7)。本地 `node_modules` 里该包仍残留(无害;服务器 `npm ci` 会按新 lock 装出干净树)。
|
|||
|
|
- ⏳ **未 push**(项目纪律 §4:push 需用户明确说「推送」)。
|
|||
|
|
|
|||
|
|
## 06:4x — 源仓 K8s 下线改动**已推送**到远端
|
|||
|
|
|
|||
|
|
- `git push -u origin master` 成功:**`ed0c3c0..cf8b7b1 master -> master`**(**快进**,未 force),并顺手补上了 `master` 的上游跟踪。
|
|||
|
|
- 推送前先 fetch 对账(**没盲推**):`FETCH_HEAD` 显示远端 master = `ed0c3c0` = 本次提交的**直接父提交** ⇒ 确认是干净快进。
|
|||
|
|
- 独立复核:`git ls-remote origin refs/heads/master` = `cf8b7b1f5c42c6fb0d17afb025a9d8fddea925c0` = 本地 HEAD ✅
|
|||
|
|
- ⚠️ **新发现的本机怪癖(已写进 MEMORY.md §三)**:该仓 `refs/remotes/**` **写不进去** —— `git fetch origin` 与 `git update-ref refs/remotes/origin/master <sha>` 都**报成功**,但 `git show-ref`/`for-each-ref` 里没有该引用 ⇒ `git status` 显示 `## master...origin/master [gone]`。hooks 目录只有 `.sample`(不是 hook 干的)。
|
|||
|
|
**推送本身不受影响**;与本机的历史结论「源仓 remote-tracking 一直读不到」一致。⇒ **与远端对账改用 `git ls-remote origin refs/heads/master`**。
|
|||
|
|
- ⚠️ 本机 `git` 走 SSH 需加非交互参数(`GIT_SSH_COMMAND="ssh -o BatchMode=yes -o ConnectTimeout=15 -o StrictHostKeyChecking=accept-new"`),否则可能挂在交互提示。
|
|||
|
|
- 锁:`--claim-exec "源仓推送K8s下线-0639"` → 已反序释放(释放前验过 OWNER)。
|
|||
|
|
|
|||
|
|
## 06:5x — 补删 K8s 命名为的死接口 LivePod(用户发现)+ 修正首轮两个副作用
|
|||
|
|
|
|||
|
|
- 用户指出:`src/supervisor/spawner.ts` 的 `export interface LivePod` —— 注释已中性但**名字来自 K8s 的 Pod**,0 引用死代码,`cf8b7b1` 漏了。核实属实(源仓 src 里只有声明那一处)。
|
|||
|
|
- 🔑 **提炼判据(已写进 MEMORY §一)**:K8s 残留要按**三类**扫 —— ① 字面量 `k8s` ② 语义措辞(`Pod`/`sidecar`/`Headless`…)③ **标识符名**(`LivePod`/`K8sSpawner`/`stampFencing`…)。**第三类最易漏**(注释改了、名字还在)。扫法:用 K8s 词汇表捞**非注释行**里的 interface/type/class/函数名。
|
|||
|
|
- 顺带发现**自己首轮留下的两个副作用**并修掉:
|
|||
|
|
1. `docs/k8s-deploy.md` / `k8s-deployment.md` 的状态横幅被**重复插入**(`cf8b7b1` 里每份 2 条、挤在同一行;工作树又变 3 条)—— 根因:横幅规则**不幂等**,每跑一次多插一条。⇒ 补「H1 后已有横幅就不再插」的负向断言 + 一条把重复**收敛成 1 条**的自愈规则。
|
|||
|
|
2. `lib/`(构建产物,gitignore)里留着**已删模块的编译结果**(`k8s-spawner.*`/`leader.*`/`reconcile.*`/`k8s-user-fs.*`/`tcp-bridge.*`/`file-service.*`,共 18 个)⇒ 会让「全仓 grep k8s」出现**假阳性**,已移入备份目录 `_stale_lib/`。(`tsc` 不会自动清已删源文件的产物。)
|
|||
|
|
- 验证:`tsc --noEmit` **exit 0**|`npm test` **36 / 35 通过 / 0 失败 / 1 跳过**|`LivePod` **全仓 0 命中**(含 `lib/`)|两份文档横幅各 1 条。
|
|||
|
|
- 提交并推送:**`68c0a32`** `refactor(k8s): 删除 K8s 命名的死接口 LivePod;修正 K8s 文档横幅重复`(3 文件 / +2 −10)→ `cf8b7b1..68c0a32`;远端 `git ls-remote` 复核 = 本地 HEAD;工作树干净。
|
|||
|
|
- 导出侧同步:`_build_export.py` 的 `K8S_SEMANTIC_RULES` 新增第 ⑫ 条(`LivePod` 整块删除,作兜底);导出物已无 `LivePod`;台账/影响说明/备份 README 均已更新(新增「K8s 残留三类扫」判据与脚本重跑注意事项)。
|
|||
|
|
- 锁:`--claim-exec "删LivePod死代码-0649"` → 已反序释放。
|
|||
|
|
|
|||
|
|
## 07:0x — 多语言(i18n)现状核查 + R5 口径更新(**默认英语**)
|
|||
|
|
|
|||
|
|
- 用户问「项目支持多语言吗(英文/中文)」⇒ 三层核查(全部实测,非推断):
|
|||
|
|
- **官方 dsh 实例内 UI**:✅ 语言开关(Settings → General);**0.1.5-rc.1 覆盖已相当完整**(见下)。
|
|||
|
|
- **自研插件**(`business-plugins`「功能管理」):✅ 已中英双语,走官方 locale。
|
|||
|
|
- **平台自己的门户 6 页 + 注入浮层**:❌ **仍是纯中文**(`<html lang="zh-CN">` + 硬编码,无 i18n 运行时、无切换入口)—— 属 R5 未做项。
|
|||
|
|
- **文档**:开源仓 ✅ 英文为主 + 中文副本;源仓 `docs/` 中文。
|
|||
|
|
- 🔴 **推翻文献旧结论**:档案 81 §10.7 ⑤ 记「官方只迁了 2 个包、20+ 包硬编码中文」—— **只适用 0.1.2-rc.1**。实测 **0.1.5-rc.1**:客户端包 **55** 个中 **36 个已走官方 locale API**(`locale.register/bind`/`addLanguage`/`LOCALE_IDS`),其中 35 个含中文(那是它自带的 `zh` 词条,**不是硬编码**),**不用 API 却仍含中文只剩 1 个**(`dsh-client-connection`)⇒ 官方在后续版本已大面积补齐。
|
|||
|
|
复现:对 `<dsh 包根>/node_modules/@deepseek-ai/*/lib/client.js` 逐包 `grep -cE "locale\.(register|bind)|addLanguage|LOCALE_IDS"` 与 `grep -c '[一-龥]'`。
|
|||
|
|
- 按「**档案只增不改**」处理旧结论:⑤ 处挂同源提示 + 文末追加 **§10.8 修正(2026-09-15)**;§10.5 那句「切英文后官方包仍显示中文」挂一行指针。
|
|||
|
|
- 用户新口径(2026-09-15):「**开发的插件需要沿用 dsh 官方项目的多语言方案,这个项目是全球化的项目,默认选中英语**」⇒ 记入 **§10.9 追加口径**:
|
|||
|
|
- 自研插件**现状已合规**(`poc/business-plugins/lib/client.js`:`locale.register(NS,{zh,en})` + `locale.bind(NS)` + `inject=["slots","locale"]`)⇒ 无需改造。**纪律**:后续插件一律走官方扩展点,**不自造词条运行时**;`register` 要求 zh/en 同时提供。
|
|||
|
|
- **官方兜底即英语**:`FALLBACK_LOCALE="en"` · `LOCALE_IDS=["zh","en"]` · 偏好存 `settings.yaml` 的 `locale.preference`(NS `locale` / field `preference`)⇒ **平台无需改官方代码或打补丁**。
|
|||
|
|
- ⚠️ **唯一例外**:`navigator.language` 仍参与解析(注释原文 “navigator.language trails the ordered …”)⇒ 中文浏览器且无偏好时可能自动落 `zh`。要「**一律默认英语、忽略浏览器**」⇒ **铺实例时把 `locale.preference: en` 写进该用户 `settings.yaml`**(平台侧配置,不触官方代码;用户之后仍可在 Settings 自选)。
|
|||
|
|
- 平台 6 页的 R5 兜底语言由 `zh-CN` **改为 `en`**;是否保留 navigator 跟随一步 = **我的取值(已在档案里标注可推翻)**。
|
|||
|
|
- 文档库流程:抢锁 → 改档案 81 → `docs-audit` / `docs-manifest` / `docs-consistency` 全绿 → scp 同步镜像(600)→ 对账确认 81 已一致。
|
|||
|
|
- ⚠️ **两个旁证(不是我的改动,需留意)**:
|
|||
|
|
1. `docs-shrink-guard.py` 报 **`skills/dsh-decision-method/SKILL.md` 491 → 296 行(-40%)** —— 疑似被**整文件重写抹行**(本库规矩:共享文件只用 Edit 精确替换)。
|
|||
|
|
2. `docs-sync-check.sh` 报 **5 个文件「内容不一致」**:`BRIEF.md` · `docs-manifest.json` · `DEPLOY-本部署.md` · `CODEBUDDY.md` · `skills/dsh-opensource-release/SKILL.md` —— 均为**其他 lane 的在途改动**,镜像未同步(不在我本次清单里)。
|
|||
|
|
- 锁:`--claim-exec "更正R5官方locale覆盖率-0703"` → 已反序释放。
|
|||
|
|
|
|||
|
|
## 07:2x 会话 —— 「用户自传个人技能」支持度核查
|
|||
|
|
|
|||
|
|
- 用户问:「记得之前提过用户可以自己上传、使用、管理个人 skills,看看项目支持吗」。**只读核查**(读文档 + 读源码 + 读源码仓 `git ls-files` + ssh 读 `/opt/dshs` 现状;**未抢锁、未改任何库内文件**)。
|
|||
|
|
- **结论:支持到「后端 + 落盘」两层,用户侧没有界面。** 证据:
|
|||
|
|
- 后端 `src/web/routes/skills.ts`(686 行)6 个路由全 `requireAuth`:`GET/POST /api/skills/mine`、`POST /api/skills/mine/apply`、`POST /:name/{enable,disable}`、`DELETE /:name`;服务器 `/opt/dshs` 实测同一文件存在(`grep -c` = 7 处)。
|
|||
|
|
- 落盘:启用位 `$DSH_HOME/skills/<name>` / 禁用位 `home/skills-library/<name>`(dsh 无 disable 机制的绕法);dsh watch 即时生效,不需重启实例。安全加固(拒 symlink/设备/FIFO、解压 ≤2000 文件 / ≤200MB、`lchown` 跳过 symlink)已在(档案 41)。
|
|||
|
|
- **界面缺失**:门户 `#/skills` 在 `portal.html` 标注 `admin: true`,只调 `/api/skills/shared`(共享层);`web/` 已无 `skills.html`(档案 16 阶段 1 移除「我的技能」+ 加 role 守卫);实例内 `poc/business-plugins/lib/client.js` 只注册 4 个 `settings.section`(`preferences` / `model-settings` / `business-plugins` / `platform-admin`),**没有「我的技能」**。档案 41 文末自己也写「用户目前无界面可用,只有 API」。
|
|||
|
|
- ⇒ 缺口 = 待执行单 **`T01`**(档案 16 阶段 3/4;决策点 1 已于 09-12 拍板 = 扩展 `business-plugins` 加第二个 section,不新建包)。另:档案 11 「待办」里「dsh 会话内 `/技能名` 调用」的 UI 端到端验收**至今未做**(仅源码级确认 watch 即时性)。
|
|||
|
|
- **记忆库维护(系统提示 MEMORY.md 超注入上限被截断)**:`MEMORY.md` **19,979 → 12,054 字节(-40%)**;删的是重复与叙事,**判据零丢失** —— 被压缩的详版(K8s 清理全量判据 / 兼容收口 / 圈选细节)**原样搬到 `PLAYBOOK-实例与插件坑.md` 新增的 §7·§8**(PLAYBOOK 6,682 → 10,842 字节),MEMORY 顶部加了详版索引。
|
|||
|
|
- ⚠️ 顺带纠正两处**过期状态**:① 「归档编号 至 93」→ **至 98**(§一/§四 早已引用 96/97/98,登记行没跟上)② §二 新增「个人技能」行(本次核查结论)。
|
|||
|
|
- ⚠️ 未跑 `docs-shrink-guard.py`(本文件不在受保护根、不属文档库;该脚本按行数骤降报警,本次 `MEMORY.md` 行数 -40% 属**有意的等价搬迁**,非抹行)。
|
|||
|
|
|
|||
|
|
## 07:26–07:35 会话 —— 「技能与插件统一管理」口径适用性分析(已落 T01)
|
|||
|
|
|
|||
|
|
- 用户问:「之前提的把 skill 和插件都看成功能(能力),统一管理,**适用于用户管理个人 skills 吗**」。
|
|||
|
|
- **原文定位**(旧工作区会话目录 `E:\ProgramData\.workbuddy\projects\d-AI技能-aliyun-dsh-server`,用 `extract-user-voice.py --needle 技能 --full` 捞出):
|
|||
|
|
- 2026-09-12 09:18(会话 `ca58e09f`)原话:「…**!业务技能能否打包到插件中 一起安装和使用,不要分开管理!**…」→ 落地 = `archive/交接单-已完成/T03 §4.1「技能随插件包投放」`,判据内核 = 素材库 **U4**。
|
|||
|
|
- 同会话 09:35 追问:「是否需要把技能装到 `$DSH_HOME/skills/` 还是就在插件内目录使用,如果不懂(不同)插件有相同名称技能如何处理」。
|
|||
|
|
- **结论(分两层,方向相反)**:**机制/投放层 ❌ 不适用**(个人技能不是投放物,没有第二条链路可合并;且 `T03 §4.1` 第 4 条「不覆盖用户自建同名技能」正是**刻意**把两向隔离)|**入口/心智层 ✅ 适用**(实例内那一栏已由档案 60 命名为「功能管理」,再另立「我的技能」section = 造反平行概念)。
|
|||
|
|
- **落地(本会话自决,可推翻)**:把 T01 的形态从「新增 `settings.section`」改为**并入既有「功能管理」section 内分组**。
|
|||
|
|
- 改 `交接单/T01-档案16阶段3-4-实例内我的技能.md`:§一 目标 + 完成判定加形态修正横幅;§三 范围表 client bundle 行;**§四 新增决策 6**(含依据与"仅入口层合并"的边界);§六 验收 B 措辞;**新增 §九「口径依据与适用范围」**(原话 / 两层结论表 / 8 维度机制差异对照表 / 相关档案)。
|
|||
|
|
- 改 `03-路线图与待办.md`:待执行行补「形态修正(2026-09-15)」+ 指向 T01 决策 6 与 §九。
|
|||
|
|
- 校验:`docs-audit.py` **无 P0 / exit 0**(交叉引用无悬空);`docs-consistency.py` **全绿**;scp 两文件到 `/opt/dsh/docs`(同相对目录、600)→ `docs-sync-check.sh` **本文件两项已从"内容不一致"消失**(余下 5 项 = 其他 lane 的在途改动,不在我清单,按纪律不动)。
|
|||
|
|
- ⛔ **未跑 `docs-manifest.py` / `docs-archive-index.py --write`**:前者会重写 `docs-manifest.json`(**此刻正是别人的在途改动**,R7 + 幽灵文件纪律);后者会把 `INDEX.md` 的 CR 翻倍(既有实测坑)。**未 commit**(用户未要求)。
|
|||
|
|
- 锁:`--claim-exec "技能插件统一口径分析-0726"` → 已反序释放。
|
|||
|
|
|
|||
|
|
## 08:25–09:1x 会话 —— T01 落地:「我的技能」并入「功能管理」分组(档案 100)
|
|||
|
|
|
|||
|
|
- 用户:「规划一个产品方案(界面布局美观 方便操作)然后实现这个功能」→ 完整走完 **方案 → 实现 → 投放 → 验收 → 归档**。
|
|||
|
|
- **方案**(档案 `04-调整方案/100-实例内我的技能-并入功能管理分组.md`):并入**既有「功能管理」section 内分组**(不新开 section);分组头(竖条标题 + 计数 + 右侧主按钮)/ 上传面板(虚线拖拽 + `label` 包隐藏 `input`)/ 技能行(名称 + 来源徽章 + 状态点 + 事实 `tabular-nums` + 动作,官方卡片行语汇 ⇒ 结构上不换行)/ 两个页内确认弹窗(替换 · 删除)/ zh+en 46 条词条。⛔ 仅**入口层**合并,机制层(两条 API、两套落盘、即时生效 vs 重启)一律独立。
|
|||
|
|
- **实现**:`poc/business-plugins/lib/client.js`(+ 词典 + 状态机 `loadSkills/submitUpload/applyReplace/toggleSkill/removeSkill` + 参数化弹窗 `skillDialog()` + 渲染 + `.bp-*` CSS);`package.json` 0.3.20 → **0.3.21**;**新增 `scripts/verify-my-skills.mjs`**(34 条断言:结构 / 危险操作二次确认 / 锁定行无按钮 / 版式不换行 / 措辞),并挂进 `npm run verify`。
|
|||
|
|
- **投放**:`npm pack`(70,758 B / sha256 `77429d2c…`,与服务器一致)→ scp `/opt/dsh/artifacts/` → `ensure-biz-plugins.cjs --all --restart` → 两实例 0.3.21,`bundles` 7 / 6 均含 `@dsh-local/business-plugins: true`(**未被 `pruneBrokenFileDeps` 摘掉**)。
|
|||
|
|
- **验收全绿**:① `npm run verify`(词典 zh/en 各 366 键、引用键 343 全声明);② 06 §7 三段式(磁盘命中 / 壳页 combo 含该包 + `rev=533537bbc02b` / bundle **200 · 11,363,655 B** 含新特征);③ 端到端 **19/19**(含共享技能 409、非 zip 400);④ **`agent-browser` 全链路零缺陷**(设置→功能管理→展开→选文件→上传 200→删除确认弹窗→删除 200)。
|
|||
|
|
- 🔑 **本轮最可复用的收获 = 实例侧取数四坑**(已写入 `PLAYBOOK §9`):① 抓实例页要带门户 `sid`(否则拿到登录页)② `/plugins/` 只在**实例子域**且仍要 `sid`(否则假 404)③ **新用户 profile 首访才生成** ⇒ 铺 bundle 必须"先 enter → 再 ensure-biz-plugins → 再 restart";且**直改 DB 置 active 会绕过"审批时铺 bundle"钩子** ④ `POST /api/dsh/restart` 要 `{"command":""}`。另:`role` 取值是 **`active`** 不是 `approved`。
|
|||
|
|
- ⚠️ **踩坑(文档侧)**:Edit 工具改 `INDEX.md` 会把**整个文件**判为改动(该文件是历史 `\r\r\n` 混合行尾)⇒ 已按兜底法恢复(镜像 == HEAD 字节)+ 改用**字节级单行插入**(`_tmp_t01/fix_index.py`),最终 diff = **1 增 1 删**。**同类改 INDEX 一律走字节级**。
|
|||
|
|
- **收尾**:T01 填回报 → `git mv` 到 `archive/交接单-已完成/` → 镜像同步(旧路径已删,对账 0 仅本地 / 0 仅服务器)→ `03-路线图` / `交接单/README` / `INDEX §二` 指针同步;`docs-audit` 无 P0、`docs-consistency` 一致。临时用户与 /tmp 脚本**全部清理**(`pocui.txt` / `probe.json` 是 09-11 别人的,按 R7 不动)。**未 commit**(用户未要求)。
|
|||
|
|
- 锁:`--claim-exec "T01我的技能-0825"` → 已释放。
|
|||
|
|
|