docs(wiki): 需求分析改为使用者视角重写
CI / ci (push) Canceled after 0s
CI / ci-windows (push) Canceled after 0s
CI / plugin-mount (push) Canceled after 0s

原稿按「现象/根因(含证据)/需求/验收标准」组织,实质是在讲我们怎么修的;
现在从「用户为什么会需要」写起:谁在用 → 他会怎么说 → 为什么难受 → 他想要什么
→ 怎样算满意(全部是用户能亲手点到、亲眼看到的行为)。

- 五条实现导向的需求 → U1~U6 用户问题(U5 拆成两套侧边栏 / 弹窗被切)
- 新增:不做什么(省得期待错位)、已知的暂时满足不了(应用层 feed、完整子代理树、
  沙箱态相对资源),如实交代边界
- 新增:验收总表(动作 → 应看到)与优先级/状态表
- 实现机制与提交号全部移入附录 A「根因档案」索引,指向 02/03/05/06
This commit is contained in:
figmar
2026-10-04 01:55:35 +08:00
parent 63bcb47ed0
commit 2224991047
2 changed files with 138 additions and 93 deletions
+137 -92
View File
@@ -1,97 +1,142 @@
# 01 · 需求分析
> 本篇回答三个问题:**为什么要做这个 fork、要解决什么、做到什么程度算完成**。
> 每条需求都给出「现象 / 根因(含证据)/ 需求 / 验收标准 / 状态」,便于回溯与验收。
> 证据中的提交号均在本仓库 `main` 或 `sync/v0.24.1` 上。
> **本篇只回答一个问题:使用者为什么会需要它。**
> 每条需求都从「用户会怎么说」写起——他遇到什么处境、他想要什么结果、他怎样才算满意。
> **不写我们是怎么实现的**:实现机制与故障根因在 [02-系统架构](02-系统架构) 与 [06-故障排查](06-故障排查),
> 本篇末尾只留一份索引供开发/排障查阅。
> 验收标准一律写成**用户能亲手点到、亲眼看到**的东西,不写测试用例名、不写提交号。
## 一、背景
## 一、谁在用
DSH(DeepSeek Harness)宿主自身提供会话、任务、模型与一个**原生右侧栏**(`ctx.sidebarRight` /
`sidebarRightTabs`)。上游 `dsh-better-sidebar` 在此基础上提供了一套「侧边栏工作台」:
文件树与编辑器、文件变动、任务、侧边对话、终端、浏览器,并把
`ctx.betterSidebar` 服务开放给其他插件注册标签页与文件预览器。
**本 fork 的定位**:不重复造工作台,而是在其之上补齐宿主与上游都没有的那一层——
**「信息层」**:这个会话的智能体拓扑、所属应用、以及会话画像。同时,在 fork 过程中暴露出
若干与 DSH 桌面壳(Electron 壳)交互的**边界缺陷**,一并作为需求纳入。
## 二、核心需求
### R1 — 侧边栏只应存在一套
| 项 | 内容 |
| --- | --- |
| 现象 | 窗口里同时出现**两套右侧边栏**与**两套开合按钮**(一套自绘浮窗控件、一套 DSH 原生) |
| 根因 | fork 基线 v0.18.0 时,插件**自绘**右侧面板与开关(`toggleCluster`);DSH 0.1.5 起宿主自己也提供原生右侧栏,二者并存 |
| 需求 | 右侧内容全部改为**注册进 DSH 原生右侧栏**;插件不再自绘右侧面板 |
| 验收标准 | ① 产物中 `toggleCluster` 引用数为 **0**;② 右列只有宿主那一个展开控件;③ 每个 tab 类型与 tab 体都经由原生注册路径(`src/client/native/`)出现 |
| 状态 | ✅ 已完成(基线升级到上游 v0.19.1:`5a7e213` → 合并 `e5d34ac`;产物核对 `toggleCluster` = 0) |
### R2 — 补齐「信息层」标签页
| 项 | 内容 |
| --- | --- |
| 现象 | 宿主有会话树、上游有工作台,但**没有**「这个会话的智能体拓扑 / 应用 / 画像」这一层信息 |
| 根因 | 上游聚焦文件工作台,信息层属于使用方(本 fork)的业务诉求 |
| 需求 | 提供三个信息层标签页:`apes:agents`(智能体)、`apes:apps`(应用)、`apes:profile`(画像),按 DSH 标准 `TabDescriptor` 注册 |
| 验收标准 | ① 三个 tab 进入原生 guide(带图标、可开合分栏);② `order` 取 60/65/70,与原生 10–50 不冲突;③ 单实例(single);④ 文案走 i18n(zh/en 双语齐全)|
| 状态 | ✅ 已完成(`src/client/apes/`,随基线同步进 v0.19.1 与 v0.24.1 两条线) |
**R2.1(收敛)** — 最初的 `apes:tasks` 已移除:它与原生会话树信息重叠,属重复建设。当前信息层为三 tab。
### R3 — HTML 预览必须能在桌面壳里显示(且不牺牲沙箱)
| 项 | 内容 |
| --- | --- |
| 现象 | 预览 HTML 文件时 iframe 只显示一行 `forbidden`(DSH Desktop 扩展模式) |
| 根因 | 三层因果:① 桌面壳给**所有**路由套一层围栏,扩展模式下 `ordinaryBrowserEnabled` 恒为 false,不带 Electron 注入的 `x-dsh-desktop-renderer` 头一律 403;② 该头只发给 **mainFrame** 与**同源**子框架;③ 预览 iframe 的 sandbox 令牌不含 `allow-same-origin`,请求时源为 `null` → 拿不到头 → 403。**与本次基线升级无关**(v0.18.0 起行为一致) |
| 需求 | 在**保持不透明源沙箱边界**的前提下让预览可用 |
| 验收标准 | ① 沙箱态下自包含 HTML(含内联脚本/样式)完整可见;② `sandbox` 令牌一个字符不变(不加 `allow-same-origin`);③ 解锁态下多文件 HTML 的相对资源也能加载;④ 取文档失败时仍能把路由的错误答复透出,而不是白屏 |
| 状态 | ✅ 已完成(`4c242ac`:主框架 `fetch` 取文档 → `srcDoc` 交付,见 [03-功能说明](03-功能说明)) |
### R4 — 插件面板不得盖住宿主与其他插件的弹窗
| 项 | 内容 |
| --- | --- |
| 现象 | 打开「社区插件市场」(宿主自带的 `dsh-community-market`)时,弹窗左下角被本插件的文件面板**切掉** |
| 根因 | 桌面壳给上游 `#root` 打标 `data-dsh-desktop-content-viewport` 并施加 `isolation: isolate`——`#root` 成为层叠上下文,宿主的 overlay 层(`.dshDesktopOverlay`,`z-index: 1000`)就在其中;而插件的面板宿主挂在 `document.body`(在该上下文**之外**)且是带 `z-index: 25` 的定位元素,于是整块画在其上,宿主 overlay 的 1000 根本没参与竞争 |
| 需求 | 遵守宿主的层叠契约:**在 app 内容之上、在宿主 overlay 层之下** |
| 验收标准 | ① 插件市场弹窗完整盖住本插件面板;② 本插件面板仍在对话内容之上;③ 非桌面壳部署(纯网页)行为不变 |
| 状态 | ✅ 已完成(`4422001`:宿主挂载点改为 `#root[data-dsh-desktop-content-viewport]`,无标记时回退 `document.body`) |
### R5 — 可运维:版本线可预期、升级可回滚
| 项 | 内容 |
| --- | --- |
| 现象 | 跟随上游新版本时,插件可能**静默失效**(不报错、不加载、无日志),排查成本高 |
| 根因 | 宿主启动时的兼容性预检 `semver.satisfies(host, peerRange, { includePrerelease: true })`:caret 范围跨 minor 必为 false → 整行禁用 |
| 需求 | ① 同时维护两条互相独立的版本线(旧宿主线 / 新宿主线),各自钉住可用的上游基线与 peer 范围;② 每次升级都有可回滚锚点(分支 + tag);③ 文档写清「哪条线配哪个宿主」 |
| 验收标准 | ① `main`(1.2.0 / 上游 v0.19.1 / peer `^0.1.5-rc.1`)与 `sync/v0.24.1`(1.3.0 / 上游 v0.24.1 / peer `^0.2.0-rc.1`)均可独立构建并通过门禁;② 远端有 `backup/*` 分支与 `apes-v*` tag 作为锚点;③ 部署指南给出选线决策 |
| 状态 | ✅ 已完成(见 [05-运维手册](05-运维手册)) |
## 三、非目标(scope lock)
| 不做 | 原因 |
| --- | --- |
| 新增上游之外的工作台面板(文件 / 终端 / 浏览器) | 属上游与宿主职责;本 fork 只用它们,不重造 |
| 团队 / 房间 / 多智能体协作面板 | 属宿主 `dsh-background-agents` 一类能力的职责 |
| 为「能用」而放宽沙箱(例如给预览 iframe 加 `allow-same-origin`) | **安全底线**:不拿安全边界换便利(R3 明确要求保住不透明源) |
| 直接 merge 上游提交 | 冲突面大、易引入无关变更;改用测量式 re-base(见 [07-开发与测试](07-开发与测试)) |
## 四、约束条件
1. **安全边界不可降级**:HTML 预览与浏览器类内容一律在不透明源沙箱中渲染;新增能力不得要求放宽它。
2. **KISS / DRY**:一处逻辑只写一遍(例如两处 HTML 预览共用 `HtmlPreviewFrame`)。
3. **双语**:所有面向用户的文案必须同时有 `zh` 与 `en`(本次会话新增文案遵循此约定)。
4. **可回滚**:任何升级前先留分支/tag 锚点,改动都能一条命令回退。
5. **不污染运行环境**:不把未验证的新线同步进正在使用的 DSH profile。
## 五、需求 → 实现 → 锚点
| 需求 | 主要实现位置 | 关键提交 |
| 使用者 | 他在干什么 | 他对右侧栏的期待 |
| --- | --- | --- |
| R1 单一侧边栏 | `src/client/native/`(原生侧边栏适配层) | `5a7e213` / `e5d34ac` |
| R2 信息层三 tab | `src/client/apes/`(`tabs.tsx` / `icons.tsx` / `locales.ts`) | 随 `5a7e213`、`e5bbef1` 同步 |
| R3 HTML 预览 | `src/client/HtmlPreviewFrame.tsx` + `src/client/html-preview.ts` | `4c242ac` |
| R4 层叠契约 | `src/client/desktop-env.ts`(`panelHostParent`)+ `src/client/index.tsx` | `4422001` |
| R5 版本线可运维 | 分支 `main` / `sync/v0.24.1`、tag `apes-v1.1.0-pre-sync`、`apes-v1.2.0` | `4422001` / `e5bbef1` |
| 在 DSH Desktop 里做项目的开发者(主要) | 一边跟模型对话干活,一边开着文件、终端、预览 | 这是他一天里看得最多的那块地方:**别挡道、别自相矛盾、点开就有用** |
| 同时放出去好几个子代理的「指挥者」 | 主会话里派活,子代理并行跑 | 不用翻聊天记录就知道**谁在跑、谁停了** |
| 接手这套环境的同事 / 运维 | 装插件、升级、出事定位 | 装得上、看得出在跑、能退回去、文档照着做就行 |
**一句话场景**:他打开 DSH,右列就是他的工作台;他希望在一个地方同时管住「文件、终端、预览」以及**这场会话的全局态势**。
## 二、使用者的需求
### U1 「我看不见谁在替我干活」
| | |
| --- | --- |
| **他会怎么说** | 「我刚派了四个子代理,现在聊天记录刷得飞快——还剩几个在跑?哪个已经停了?」 |
| **为什么难受** | 在聊天流里回溯是**唯一**的办法:往上翻,翻到就丢了上下文。原生会话树只回答"有哪些会话",不回答"这场会话里现在有谁、在干什么"。用户被迫在两个地方来回找同一条信息。 |
| **他想要** | 点一下就有一张**这场会话的队伍表**:主代理在最上,它的直系子代理一人一行,谁在跑、谁停了,写在脸上。 |
| **怎样算满意** | ① 不离开右列就能看到;② 人数与状态一眼可辨(运行中 / 已结束、一次性 / 可续);③ 没有子代理时**明确告诉他"没有"**,而不是留一片空白让他怀疑是不是坏了;④ 不用等刷新,开面板就是最新的。 |
| **状态** | ✅ 已满足(`apes:agents`) |
### U2 「我不知道这活儿是在哪儿干的」
| | |
| --- | --- |
| **他会怎么说** | 「同时开着三个项目,会话标题长得都差不多——我现在这个是哪个目录下的?等下写文件会不会写错地方?」 |
| **为什么难受** | 工作区是他**唯一**能确认"我在哪儿"的锚点,但它只存在于终端提示符里。切回一个会话时,他得先开个终端敲 `pwd` 才能安心。 |
| **他想要** | 面板直接告诉他这场会话锚在哪个工作区;更好的是还告诉他这属于哪个「应用」。 |
| **怎样算满意** | ① 打开就能看到工作区路径(不用他自己去敲命令);② 拿不到应用层信息时**如实写"宿主现在没有应用信息"**,而不是留空让他猜哪里出了问题。 |
| **状态** | ◑ 部分满足:工作区 ✅(`apes:apps`)/应用层信息受宿主限制(见 §四) |
### U3 「这个会话现在是什么状态、值不值得等」
| | |
| --- | --- |
| **他会怎么说** | 「它还在跑吗?还是刚才就停了?我刚回来,这个会话是哪一次来着?」 |
| **为什么难受** | 会话的"身份"和"当前状态"散落在界面细节里,要靠猜;离开一会儿回来,就要重新建立"我在看什么"的心智模型。 |
| **他想要** | 一张**会话画像**:这次会话是什么、现在在跑还是空闲、工作区在哪儿。 |
| **怎样算满意** | ① 运行 / 空闲有明确文字,不用看动画猜;② 标题或工作区缺失时给出替代信息或明确提示。 |
| **状态** | ✅ 已满足(`apes:profile`:标题 / 运行态 / cwd) |
### U4 「我点开一个 HTML,结果只有一行 forbidden」
| | |
| --- | --- |
| **他会怎么说** | 「模型给我生成了一份报表 / 图表 / 原型页面,我点开——就一行 `forbidden`。文件是坏了吗?」 |
| **为什么难受** | HTML 是最直观的**交付物**,也是他最想直接验收的东西。看到的却是一行技术错误:他既不知道文件是好是坏,也不知道该找谁。信心成本很高——一旦怀疑"预览不可信",他下次就直接绕开功能,回到最原始的"下载下来用浏览器打开"。 |
| **他想要** | 点开就能看到内容。**而且不希望为了让他看见,就把安全边界拆掉。** |
| **怎样算满意** | ① 自包含 HTML(内联样式/脚本)完整显示;② 多文件 HTML 至少正文可读,需要外链资源时由**他本人**点「临时解锁」这个显式动作来放宽;③ 真取不到文档时,显示真实原因,不是白屏。 |
| **状态** | ✅ 已满足(默认沙箱态可读;多文件需显式解锁,见 [03-功能说明](03-功能说明) §3.1) |
### U5 「我的界面自己乱套了」
**a. 右边有两套侧边栏,我不知道该点哪个**
| | |
| --- | --- |
| **他会怎么说** | 「窗口右边出现了两个侧边栏、两个开合按钮——哪个是真的?」 |
| **为什么难受** | 两个入口、两套内容,用户无法判断该用哪个;他会同时试两个,再怀疑是不是装重了。**不可信的工具会先被弃用**,然后才被排障。 |
| **他想要** | 右列只有一套——DSH 自己那套;插件的东西**住在里面**,而不是另起一套。 |
| **怎样算满意** | 右列只有一个展开/收起入口;插件每个面板都能从原生入口进入;不会出现"关掉一个还剩一个"。 |
| **状态** | ✅ 已满足 |
**b. 弹窗被插件面板切掉一角**
| | |
| --- | --- |
| **他会怎么说** | 「我打开插件市场,弹窗左下角被切了一块,按钮点不到。」 |
| **为什么难受** | 他遇到的不是"功能缺失",而是**功能互相打架**:明明能用的东西被自己的插件挡住。这类问题最伤使用意愿,因为用户会归咎于"这个插件不可靠"。 |
| **他想要** | 弹窗是更上层的东西,应该完整盖住插件面板;同时插件面板仍要盖在对话正文之上。 |
| **怎样算满意** | 弹窗完整可见、按钮可点;面板不回退到被对话正文压住。 |
| **状态** | ✅ 已满足 |
### U6 「我不敢升级——也不知道该装哪一版」
| | |
| --- | --- |
| **他会怎么说** | 「升了一版,插件就像没装一样,没报错也没日志。我这台该装哪版?出事怎么退回去?」 |
| **为什么难受** | 宿主对版本不匹配是**静默禁用**——没有错误提示,用户只会得出"这东西坏了/不可信"的结论,而真正的原因是版本配错。加上换机器、多环境,选线成本全压在用户的记忆里。 |
| **他想要** | ① 一份"哪个宿主配哪个版本"的对照,照着选就能装上;② 每步升级都留可回滚的锚点;③ 万一装错,文档能告诉他"现象就是像没装"以及怎么换回来。 |
| **怎样算满意** | ① 按 [04-部署指南](04-部署指南) 的对照表选线即装得上;② 升级前后有分支/tag,一条命令回退;③ 能在 [06-故障排查](06-故障排查) 里按症状自查。 |
| **状态** | ✅ 已满足(两条版本线 + 锚点;新宿主线待实机验证,见 [05-运维手册](05-运维手册)) |
## 三、我们不做的事(先说清,省得期待错位)
| 不做 | 对使用者意味着 |
| --- | --- |
| 不重造「文件 / 终端 / 浏览器」 | 这些上游与宿主已经有了,再造一遍只会让你的界面更乱 |
| 不做团队 / 房间 / 多智能体协作面板 | 那是宿主另一类能力的职责,不该塞进右列 |
| 不为「看得见」放宽沙箱 | 想加载沙箱外资源,必须由**你本人**点一次显式解锁;插件不会替你默默降级 |
| 不承诺"装上看不见变化"就是坏的 | 宿主对版本不匹配是静默禁用,先按 [06-故障排查](06-故障排查) 自查宿主版本 |
## 四、已知的「暂时满足不了」(如实交代)
| 使用者想要的 | 现状 | 为什么 | 何时会好 |
| --- | --- | --- | --- |
| 应用级信息(这场会话属于哪个「应用」、有哪些入口) | `apes:apps` 只显示工作区 cwd | 宿主当前没有向插件提供应用清单的通道 | 宿主提供 feed 后即可接上 |
| 一次摊开完整子代理树(含子代理的子代理) | 只显示**直系**子代理 | 宿主投影只给一层 catalog | 同上 |
| 沙箱态下多文件 HTML 的相对资源 | 需点「临时解锁」 | 不透明源沙箱里子请求拿不到宿主信任头——这是安全边界的必然结果 | 刻意保留,不计划改变 |
## 五、验收总表(你可以自己动手验)
| 你做什么 | 你应该看到 |
| --- | --- |
| 展开右列 | 只有**一套**侧边栏、**一个**开合入口 |
| 点「智能体」 | 主代理 + 直系子代理,带运行态;没有子代理时明确提示"没有" |
| 点「应用」 | 当前工作区路径;宿主无应用 feed 时写明原因 |
| 点「画像」 | 会话标题 / 运行或空闲 / 工作区 |
| 打开一个自包含 HTML | 内容完整显示在面板里,不是 `forbidden` |
| 打开一个多文件 HTML | 正文可读;需要外链资源时由你点「临时解锁」 |
| 打开插件市场 | 弹窗完整盖在面板之上,按钮可点 |
| 打开某个 HTML 后 ⌘Q 重启 DSH | 面板仍在、开合入口仍只有一个 |
## 六、优先级与当前状态
| 需求 | 优先级 | 状态 |
| --- | --- | --- |
| U1 看得见队伍(谁在替我干活) | P0 | ✅ `apes:agents` |
| U4 HTML 看得见、且不降级安全 | P0 | ✅ 默认沙箱可读;多文件需显式解锁 |
| U5 界面别乱(单一侧边栏 + 层叠正确) | P0 | ✅ |
| U6 敢升级、能回滚 | P1 | ✅ 两条版本线 + 锚点;新宿主线待实机验证 |
| U2 知道在哪儿干活 | P1 | ◑ 工作区 ✅/应用层等宿主 |
| U3 会话画像 | P2 | ✅ `apes:profile` |
## 附录 A · 根因档案(实现视角,开发与排障看,其他读者可跳过)
| 需求 | 机制 / 证据 | 详细页 |
| --- | --- | --- |
| U1 / U2 / U3 信息层三 tab | 按 DSH 标准 `TabDescriptor` 注册(`order` 60/65/70、`single`);数据来自 `ctx.sessions.list` 的 `byId` 与宿主会话投影;随基线同步进两条版本线 | [02](02-系统架构) · [03](03-功能说明) |
| U4 HTML 预览 | 桌面壳对**所有**路由做前置围栏,只信任带 Electron 注入头的请求;沙箱 iframe 源为 `null` 因而发不出合规请求。解法:主框架 `fetch` 取文档 → 注入 `<base href>` → `srcDoc` 交付,**沙箱令牌一字未改** | [06](06-故障排查) · [03](03-功能说明) §3.1 |
| U5-a 两套侧边栏 | fork 基线 v0.18.0 自绘右列与开关(`toggleCluster`),而 DSH 自带原生右列;升级到上游 v0.19.1(原生注册路径)后仅剩一套 | [02](02-系统架构) |
| U5-b 弹窗被切 | 桌面壳给 `#root` 加 `isolation: isolate` 形成层叠上下文,宿主 overlay(`z-index: 1000`)被封在其中;插件面板宿主当时挂在 `document.body`(上下文之外)且带 `z-index: 25`。解法:把面板宿主挂进 `#root`,无标记时回退 `document.body` | [02](02-系统架构) · [06](06-故障排查) |
| U6 静默禁用 | 宿主启动预检 `semver.satisfies(host, peerRange, { includePrerelease: true })`,caret 跨 minor 为 false → 整行跳过,无日志。对策:两条独立版本线 + 分支/tag 锚点 | [05](05-运维手册) · [04](04-部署指南) |