Files
apes-sidebar/docs/wiki/02-系统架构.md
T
figmar 66ee3953c9 feat(apes): revive the task/message tabs in the info layer
C6 removed apes:tasks as a duplicate of the host session panel; in practice the two answer different questions (host panel = switch workspaces, plugin tab = who is running right now), so the tab is restored and the info layer goes back to five types.

tasks: live session list (current / running / subagent markers) + showInactive toggle via pluginSettings['apes:tasks']; messages: placeholder that states the host has no message feed yet; both get badge counters, and the whole layer now honours the visible gate (auto-collapse) so hidden tabs stop waking on session events.

zh/en/ja dictionaries in sync (AGENTS 5); wiki 02/03/07 reverted from three to five tabs; decision record in docs/plans/2026-10-11-apes-tab-revival.md.
2026-10-11 21:18:08 +08:00

9.8 KiB
Raw 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-部署指南)。