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.
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-部署指南)。