Files
figmar 63bcb47ed0
CI / ci (push) Canceled after 0s
CI / ci-windows (push) Canceled after 0s
CI / plugin-mount (push) Canceled after 0s
docs(wiki): 建立项目百科(需求分析 → 架构 → 功能 → 部署 → 运维 → 排障 → 开发与测试)
Gitea 的 wiki 此前是空的。本次把百科的**源文件版本化进仓库**(docs/wiki/),
再用脚本发布到 wiki——文档因此与代码同源、可 review、可回溯,而不是只存在于网页上。

八页(Home 作为导航与项目速览):

  Home          项目速览 / 两条版本线的宿主兼容性差异 / 文档地图 / 谱系说明
  01-需求分析    五条核心需求(现象·根因·验收标准·状态)+ 非目标与约束 + 需求→实现→锚点
  02-系统架构    host/client 分层与产物、三层接口面、面板宿主层叠契约与 isolate 陷阱、关键不变量
  03-功能说明    面板/预览器总表、信息层三 tab 数据来源、HTML 预览交付方式、i18n 与令牌、配置面
  04-部署指南    宿主版本矩阵与选线决策、profile 接线(file: + bundles)、构建、验收清单、卸载与切换
  05-运维手册    分支/tag 策略与回滚剧本、发布纪律(版本三元组/非确定性产物)、巡检、备份铁律、诊断
  06-故障排查    8 类症状 → 根因 → 处置(含本会话实机定位过的全部案例)+ 定位手法
  07-开发与测试  工程约定、目录、门禁基线、测试分层与契约 spec、扩展点、测量式 re-base 方法

脚本 scripts/push-wiki.sh:幂等发布(POST 新建 / PATCH 覆盖),凭据走 git credential helper
(临时 netrc,600,退出即删),不落盘、不进 argv。已实测:8 页全部发布成功,复推为更新。

内容纪律:只写**本次会话实证过**的事实(提交号、行数、层级取值、产物大小、宿主版本矩阵、
静默禁用机制、re-base 步骤),未验证的部分明确标注为待验证或已知限制;并如实说明本仓库
历代承载过 v1 / v2 / 当前 fork 三代实现,本百科描述的是 main 上的 fork 线。
2026-10-04 01:19:31 +08:00

9.8 KiB
Raw Permalink Blame History

02 · 系统架构

本篇说明插件的分层、接口面、层叠契约与关键不变量。 与源码一一对应,可直接当作改动前的「雷区地图」使用。

一、总体分层

┌──────────────────────────────────────────────────────────────────────────┐
│ DSH 宿主(Desktop Electron 壳 / Web profile)                              │
│  ├─ 原生右侧栏   ctx.sidebarRight / sidebarRightTabs(tab 类型注册表)       │
│  ├─ 会话与任务   ctx.sessions.list(会话摘要、子代理、投影)、ctx.tools       │
│  ├─ webServer    路由挂载点(插件用 /sidebar/* 前缀注册)                    │
│  └─ 桌面壳层     webserver 外层的请求围栏 + #root 的 content-viewport 标记   │
└───────────────┬──────────────────────────────────────────────────────────┘
                │ ① 宿主 → 插件:cordis 服务注入(inject)
                │ ② 插件 → 宿主:原生 tab / 资源打开 / 路由
┌───────────────┴──────────────────────────────────────────────────────────┐
│ 插件 HOST 半(Node,lib/index.js)                                         │
│  · src/index.ts      路由注册(/sidebar/*)、信任围栏、会话/文件数据、HTML 交付 │
│  · src/trust-fence.ts 请求信任围栏(Host 头 / 令牌)                         │
│  · src/invariant.ts  依赖零的断言与 semver 工具(host/client 共用)           │
└───────────────┬──────────────────────────────────────────────────────────┘
                │ 浏览器侧模块(__ModuleLoader__ bundle,非 CJS)
┌───────────────┴──────────────────────────────────────────────────────────┐
│ 插件 CLIENT 半(浏览器,lib/client.js + 懒加载 chunk)                      │
│  · src/client/index.tsx    应用入口:装配 store/service/原生适配/信息层       │
│  · src/client/native/      原生右侧栏适配(tab 记录、tab 体、tab 芯片标题)    │
│  · src/client/builtins/    内置面板:文件 / 文件变动 / 子代理 / 侧边对话 /     │
│                            终端 / 浏览器 + 文件预览器                        │
│  · src/client/sidebar/     自绘底部工作台与开合控件(原生侧边栏之外的部分)     │
│  · src/client/apes/        信息层三 tab(本 fork 的增量)                     │
│  · src/client/service.ts   ctx.betterSidebar 服务(供其他插件注册)            │
└──────────────────────────────────────────────────────────────────────────┘

二、构建产物

产物 角色 备注
lib/index.js host 半(ESM,Node) 路由、围栏、数据面
lib/invariant.js 不变量面(零依赖) host/client 都要 import,不得引入 Node 内置或宿主包
lib/client.js client 主 bundle 浏览器 window.__ModuleLoader__.load({...}) 形态,不是 CJS/ESM,Node 里 require 会因 window is not defined 抛错
lib/client-registry.js 懒加载 chunk 高开销能力(图标/预览库)按需加载
lib/client-editor.js 懒加载 chunk 编辑器(CodeMirror 系)
lib/client-locale.js 懒加载 chunk 语言包
lib/client-mermaid.js 懒加载 chunk Mermaid 渲染
lib/client-terminal.js 懒加载 chunk 终端
lib/types/**/*.d.ts 类型声明 供消费方类型检查

懒加载动机:主 bundle 不应静态 import 重型预览库;chunk 通过宿主 ctx.modules 解析外部依赖 (入口在 src/client/index.tsx 里先 setChunkModuleSystem(ctx.modules))。

三、三层接口面

3.1 宿主 → 插件(cordis 注入)

// src/index.ts
export const name = 'dsh-apes-sidebar'
export const inject = ['webServer', 'sessions', 'webRuntime', 'tools']
export function apply(ctx: Context, config?: SidebarConfig): void

客户端侧读取的宿主面(结构化镜像,运行时由宿主提供): ctx.sessions.list(getSnapshot/subscribe)、ctx.sidebarRight(原生右列与 openResource)、 ctx.slots、ctx.settings(挂载行 config)、ctx.modules(懒加载 chunk 的模块系统)。

3.2 插件 → 宿主

方向 手段
注册 tab 类型与 tab 体 src/client/native/registerNativeSurface() → 原生右侧栏注册表
打开会话内文件 ctx.sidebarRight.openResource('dsh-resource://file/…')
暴露 HTTP 能力 /sidebar/ 前缀路由:api · file · html · upload · ws/terminal · ws/agent-opens · ws/agent-terminals

3.3 插件 → 其他插件(对外服务)

ctx.betterSidebar(src/client/service.ts)对外开放 registerTab / registerFileViewer 等, 并按「能力清单」做前向兼容门控(能力只增不减)。对外契约见仓库内 docs/external-plugin-guide.md。 服务版本常量 SIDEBAR_SERVICE_VERSION 必须与 package.json / dsh.plugin.json 的版本三方一致 (有单元测试断言这一对)。

四、面板宿主与层叠契约(重点雷区)

插件的所有面板渲染在一个 body 级(或壳层 viewport 级)的宿主层里:

[data-dsh-panel-host] { position: fixed; inset: 0; z-index: 25; pointer-events: none }

z-index: 25 不是随手取的,它要同时满足:在 app 内容与壳层 overlay 之上、在宿主交给插件用的 overlay 槽之下。完整取义:

层级 对象
z-index: auto app 的流内内容、.dshDesktopFrame
25 本插件的面板宿主
30 宿主的动态插件面板(ui-cordis)
100+ app 的 overlay 栈(Toast / onBoarding 等)
1000 .dshDesktopOverlay[data-shell-overlay](宿主给 shell.overlay 槽的容器,第三方弹窗在此)

陷阱(已修,4422001):DSH 桌面壳的有框模式会给上游 #root 打标 data-dsh-desktop-content-viewport 并施加 isolation: isolate,使 #root 成为一个层叠上下文。 宿主自己的 overlay 层就在这个上下文内;若插件宿主挂在 document.body(上下文之外), 它是「带正 z-index 的定位元素」,会画在整个上下文之上——把宿主 overlay 里的弹窗(如社区插件市场)盖住。

// src/client/desktop-env.ts —— 修法:挂进壳层的 content viewport
export function panelHostParent(doc: Document = document): HTMLElement {
  return doc.querySelector<HTMLElement>('#root[data-dsh-desktop-content-viewport]') ?? doc.body
}

进入该上下文后,25 恢复原意(在 app frame 之上、在 overlay 层 1000 之下);无该标记的部署 (纯网页 / 未打标的壳)仍挂 document.body,行为不变。重新锚定守卫同时监听 document.body 的 data-dsh-desktop-mode(壳层打标与它是同一个 effect),因此壳层在插件挂载之后 才框住 #root 的情况也能被搬到正确位置。

五、数据流示例

会话 → 信息层

ctx.sessions.list.getSnapshot()      →  { byId, projectionsBySession? }
  useSyncExternalStore(subscribe)        (uSES 安全订阅)
  ├─ apes:agents  → rootAncestor(byId, scope.sessionId) 找主代理
  │                 → projectionsBySession[rootId].values.subagentCatalog 取直系子代理
  │                   (无投影时回退 byId 里的 durable 子会话)
  └─ apes:profile / apes:apps → byId[scope.sessionId](标题 / cwd / running)

文件 → 预览(HTML 为例)

编辑器/diff 面板 → /sidebar/html/<session>/<path>    (路由负责路径围栏与体积上限)
  主框架 fetch 取回文档文本 → 注入 <base href="<该文件的路由目录>">
  → iframe srcDoc + sandbox(不透明源)
  取失败时回退为 iframe src=路由 URL,把路由的答复原样透出

六、关键不变量(改动前必读)

  1. host / client 拆分是结构性的:client bundle 在浏览器运行,不得 import Node 内置; src/invariant.ts 两侧共用,必须保持零依赖。
  2. dsh-better-sidebar 是 peer,永不打包:一旦被内联,client 会带上第二份侧边栏注册表, tab 注册进死实例,界面上永不出现。
  3. tab id 前缀归本 fork:信息层用 apes:*;不得占用上游/宿主内部 id。
  4. HTML 预览的沙箱令牌不得放宽(不加 allow-same-origin 等);交付方式只能是「主框架取文档 + srcDoc」。
  5. 颜色只用宿主设计令牌(--dsw-alias-*),不写硬编码色值;几何量(字号/间距/圆角)无公开令牌,内联即可。
  6. 所有面向用户的字符串走 i18n,zh / en 必须同时存在。
  7. 变更后必须重新同步到 profile:构建产物非字节确定性,且 profile 的 file: 依赖是复制语义, 不能假定旧副本可用(见 04-部署指南)。