chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进

1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
   保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
   工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
   必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
   + ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
   ⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
   验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
This commit is contained in:
admin committed 2026-09-15 18:47:13 +08:00
1 parent c70d5d860e
commit 5ad755116e
173 files changed
+27632

No files matched your search

@@ -0,0 +1,123 @@
# 56 · 实例助手:我的文件 / 能力清单 / 档位提示(+ 文件下载端点)
- 日期:2026-09-11
- 状态:✅ **已实施并端到端验证**(commit `ebe8075`;双端文档 99/99)
- 触发:① 用户要求「按照建议处理」= 落地档案 55 §四.1(老会话档位提示)与 §四.2(实例能力清单);② 用户追加问题「**AI 生成的文件看不到、下不了,本机地址浏览器打不开**」
- 关联:档案 55(取证与结论)、50(实例页注入先例)、39(loopback 封锁)、33(权限档位默认值)、19 §C8(k8s 未验证路径)
---
## 一、三个问题与根因(都已解决)
| # | 用户可感知的问题 | 根因 | 本次处置 |
|---|---|---|---|
| 1 | 老会话里 AI「bash 不可用」,但没人告诉他为什么、怎么修 | 权限档位**会话级播种**:平台默认已改(档案 33),既有会话仍 `workspace-write`;本机沙箱后端不可用 → dsh **fail-closed 拒绝任何 shell** | 实例页**顶部提示条**:说明原因 + 给出"切档位 / 新建会话"两条路 |
| 2 | 用户反复问「能力有变化吗」;AI 每会话都要现场试一遍(实测同批探测重复 3 轮) | 平台没有**权威能力说明**,只有"试出来的"结论 | **能力清单**(人看的实例页面板 + agent 看的共享技能,**同源生成**) |
| 3 | **AI 产出的文件看不到、下不了**;AI 给的是 `/var/lib/.../ws/...` 或 `127.0.0.1:<port>` | 平台**根本没有读取/下载端点**(`/api/desktop/tree` 只列目录);且实例内不可访问宿主 loopback(档案 39)→ 127.0.0.1 链接必失效 | 新增 `GET /api/fs/download` + 实例页右下「**我的文件**」面板(浏览 + 下载) |
---
## 二、实现
### ① 档位提示
| 改动 | 说明 |
|---|---|
| **新增 `src/supervisor/session-preset.ts`** | 解析会话事件流(**多帧 zstd**,按 `28 b5 2f fd` 切分逐帧解压)取**最后一条** `permission/preset`;跨工作区取**最近修改**的会话 |
| **新增 `GET /api/dsh/session-permission`** | 返回 `{ expected, session, stale }`;`stale` 仅在「会话档位 = `workspace-write` 且 ≠ 平台默认」时为真(这是唯一真正挡住用户的组合) |
| 实例页注入脚本 | `stale` 时顶部横幅:说明「沙箱不可用 → AI 无法执行命令」+ 两条修法;可关闭(按 sessionId 记 `sessionStorage`) |
### ② 能力清单(人机同源)
| 改动 | 说明 |
|---|---|
| **新增 `scripts/gen-capabilities.cjs`** | 探测工具链版本 + 技能清单,写出 ① `/opt/dsh/state/capabilities.json`(机读) ② `bundled-skills/platform-capabilities/SKILL.md`(**平台首个共享技能**,agent 可直接读) |
| 内容 | 权限档位含义、可读/可写范围、**网络边界(明确 `127.0.0.1` 不可达)**、工具版本表、**已注册技能清单**、**"把文件交给用户"的正确方式** |
| **新增 `GET /api/capabilities`** | 给实例页「能力」面板读同一份 JSON |
| **cron** | 每日 05:05 重新生成(平台变化后清单自动跟上) |
### ③ 文件查看 / 下载
| 改动 | 说明 |
|---|---|
| `UserFs` 接口 + `LocalUserFs.readFile` | 目录 → `not_a_file`;>32MB → `too_large`;路径逃逸 → `bad_path`;**符号链接逃逸同样拒绝**(复用既有 `assertNoLinks`) |
| `K8sUserFs.readFile` | **显式抛 `unsupported`**(sidecar 尚无 read 端点)—— 属档案 19 §C8 的未验证路径,**不假装支持** |
| **新增 `GET /api/fs/download`** | `requireAuth`;限定在调用者自己的根内;带 `content-type` / `content-disposition` / `no-store` |
| 实例页「我的文件」面板 | 右下角 **📁 我的文件**:目录可下钻、文件可**一键下载**、显示大小;走平台代理,**不暴露宿主路径** |
**注入方式**:复用档案 50 的 HTML 注入点(`proxy.ts` 的 `injectRecovery`),新脚本与「401 自愈」**并存**,不改官方包、不落盘(R2)。
---
## 三、验证(实测,非推断)
**CI**:`typecheck + build` 通过;单测 **42 通过 / 0 失败**(新增 4 项 `readFile` 安全测试:目录/缺失/超限/路径逃逸/符号链接)。
**端到端**(铸造临时 session → 实测后即删):
| 检查 | 结果 |
|---|---|
| `GET /api/dsh/session-permission` | `200`,`stale=true`(admin 最近会话仍是 `workspace-write`)✓ |
| `GET /api/capabilities` | `200`,工具 10 项、含 `python3=Python 3.12.14`、网络边界写明 `127.0.0.1` 不可用 ✓ |
| `GET /api/fs/download?path=<文件>` | `200`,`content-type: text/markdown; charset=utf-8`,内容正确 ✓ |
| **越界** `../../etc/passwd` | **`400 bad_path`**(未读到)✓ |
| 目录(空路径) | `400 not_a_file` ✓ |
| 实例首页 HTML(35KB) | 含 `__dshAssist`(新助手)**且**含 `__dshRecover`(原 401 自愈未破坏)✓ |
| 三个新端点未登录 | 均 `401`(已注册且受 `requireAuth` 保护)✓ |
**产物**:`/opt/dsh/state/capabilities.json`(2.5KB)、`bundled-skills/platform-capabilities/SKILL.md`(2.9KB)。
---
## 四、怎么用
**用户(在会话页右下角)**
- **📁 我的文件** → 浏览自己的工作区、**下载任意文件**(AI 产出的报告/表格/脚本都在这里,不再需要服务器路径)。
- **🧭 能力** → 看到权威的"能做什么/不能做什么"(含工具版本与技能清单)。
- 若顶部出现黄色提示条 → 说明当前会话档位过时,按提示**切换档位或新建会话**即可让 AI 能执行命令。
**AI(实例内)**
- 新会话会看到共享技能 `platform-capabilities` → **开工前读它即可**,不必再现场试探;
- 交付文件时按其中《把文件交给用户》一节:**用工作区相对路径**,并提示用户点右下角「我的文件」下载。
---
## 五、边界与已知限制
1. **档位切换仍需用户在 dsh 界面操作**:平台**不会**自动改既有会话的档位(那等于把受限会话静默提升为完全权限,属安全语义变更,必须用户知情)→ 提示条 + 新建会话是最稳的路径。
2. **`/api/fs/download` 仅 local 部署可用**:k8s 的 sidecar 尚无 read 端点(抛 `unsupported`),已在接口注释与档案 19 §C8 标注。
3. **单文件 32MB 上限**;只允许访问**自己的工作区**(根外一律 `bad_path`)。
4. 能力清单里的技能列表当前为**空**(平台尚未投放业务技能)——这正是档案 55 §四.3 待你裁定的事项;清单已把"空"如实写出来。
---
## 六、回滚
| 对象 | 回滚方式 |
|---|---|
| 代码 | `git revert ebe8075` + `npm run build` + 重启(备份:`/opt/dsh/backups/p56-20260911-2251/`) |
| 注入脚本 | 摘掉 `proxy.ts` 里 `SESSION_ASSIST_JS` 的拼接即可(自愈脚本独立) |
| 能力清单 | 删除 `/opt/dsh/state/capabilities.json` 与 `bundled-skills/platform-capabilities/`;cron 行删除 |
| 下载端点 | 删除 `desktop.ts` 的 `/api/fs/download` 路由(`readFile` 保留无副作用) |
---
## 七、后续(可选,待裁定)
1. **门户侧下载入口**:门户「服务管理 → 浏览文件」目前只能列表/上传(`portal.html`),可补"下载"按钮复用同一端点。
2. **技能投放**:是否投放业务技能(决定能力清单里"技能"一节是否长期为空)。
3. **档位一键切换**:若你希望连"点一下切换"也由平台代劳,需要进一步评估(改会话状态有风险,建议保持现状)。
---
## 修正(2026-09-13 08:3x):**注入脚本一直是坏的,今天才真正跑起来**
2026-09-13 排查另一处注入脚本事故时发现:本档的 `SESSION_ASSIST_JS`(`src/supervisor/proxy.ts`)
**在浏览器里从来没有执行过** —— 脚本内 8 处 `'\n'` 写成**单反斜杠**,而它位于 TS **模板字面量**内,
`\n` 在模板求值时就变成**裸换行**,注入后直接 SyntaxError,整段脚本作废。
⇒ 本档的「我的文件 / 能力清单」面板在前端**实际未生效**;当时的验收只 grep 了
页面 HTML 里是否出现 `__dshAssist` 字样(**验的是"文本在不在",不是"脚本跑不跑"**),所以是假绿。
**2026-09-13 08:3x 已修**(按源文件补成 `\\n`),并新增常设校验 `scripts/verify-inject.cjs`
(先模板求值再 `node --check`,已接入 `npm test`)防复发。
**后续**:需要在真实浏览器里重跑一遍本档的面板验收(原本验收结论需重新判定)。