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 线。
155 lines
9.8 KiB
Markdown
155 lines
9.8 KiB
Markdown
# 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 注入)
|
||
|
||
```ts
|
||
// 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](../blob/main/docs/external-plugin-guide.md)。
|
||
服务版本常量 `SIDEBAR_SERVICE_VERSION` 必须与 `package.json` / `dsh.plugin.json` 的版本三方一致
|
||
(有单元测试断言这一对)。
|
||
|
||
## 四、面板宿主与层叠契约(重点雷区)
|
||
|
||
插件的所有面板渲染在一个 **body 级(或壳层 viewport 级)的宿主层**里:
|
||
|
||
```css
|
||
[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 里的弹窗(如社区插件市场)盖住。
|
||
|
||
```ts
|
||
// 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-部署指南](04-部署指南))。
|