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