Files
dsh_shenxian/dsh-server-docs/04-调整方案/56-实例助手与文件下载端点.md
T
admin 5ad755116e 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 一律写「远程服务器」。
2026-09-15 18:47:13 +08:00

124 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
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.
# 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`)防复发。
**后续**:需要在真实浏览器里重跑一遍本档的面板验收(原本验收结论需重新判定)。