From 63bcb47ed0015c94455e4714d5dc4c7eb64175ef Mon Sep 17 00:00:00 2001 From: figmar Date: Sun, 4 Oct 2026 01:19:31 +0800 Subject: [PATCH] =?UTF-8?q?docs(wiki):=20=E5=BB=BA=E7=AB=8B=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E7=99=BE=E7=A7=91=EF=BC=88=E9=9C=80=E6=B1=82=E5=88=86?= =?UTF-8?q?=E6=9E=90=20=E2=86=92=20=E6=9E=B6=E6=9E=84=20=E2=86=92=20?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=20=E2=86=92=20=E9=83=A8=E7=BD=B2=20=E2=86=92?= =?UTF-8?q?=20=E8=BF=90=E7=BB=B4=20=E2=86=92=20=E6=8E=92=E9=9A=9C=20?= =?UTF-8?q?=E2=86=92=20=E5=BC=80=E5=8F=91=E4=B8=8E=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 线。 --- README.md | 3 + docs/wiki/01-需求分析.md | 97 +++++++++++++++++ docs/wiki/02-系统架构.md | 154 +++++++++++++++++++++++++++ docs/wiki/03-功能说明.md | 109 +++++++++++++++++++ docs/wiki/04-部署指南.md | 163 +++++++++++++++++++++++++++++ docs/wiki/05-运维手册.md | 133 ++++++++++++++++++++++++ docs/wiki/06-故障排查.md | 208 +++++++++++++++++++++++++++++++++++++ docs/wiki/07-开发与测试.md | 164 +++++++++++++++++++++++++++++ docs/wiki/Home.md | 82 +++++++++++++++ scripts/push-wiki.sh | 113 ++++++++++++++++++++ 10 files changed, 1226 insertions(+) create mode 100644 docs/wiki/01-需求分析.md create mode 100644 docs/wiki/02-系统架构.md create mode 100644 docs/wiki/03-功能说明.md create mode 100644 docs/wiki/04-部署指南.md create mode 100644 docs/wiki/05-运维手册.md create mode 100644 docs/wiki/06-故障排查.md create mode 100644 docs/wiki/07-开发与测试.md create mode 100644 docs/wiki/Home.md create mode 100755 scripts/push-wiki.sh diff --git a/README.md b/README.md index 7cfa0b2..18aa74b 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ > `apes:profile` 画像),按 DSH 标准 `TabDescriptor` 接口注册——因此它们会自动获得上游原生适配层 > 的全部待遇(进原生 guide、tab 芯片图标、开合与分栏)。 > - 版本线独立于上游:`1.2.0`(fork)。上游历史版本见其 CHANGELOG。 +> - **📚 项目百科**(需求分析 → 系统架构 → 功能说明 → 部署指南 → 运维手册 → 故障排查 → 开发与测试): +>  源文件在仓库 `docs/wiki/`, +> 改完用 `bash scripts/push-wiki.sh` 一条命令发布。 > [!IMPORTANT] > **已适配 DSH 原生侧边栏 API**(v0.19.0 起,DSH `0.1.5-rc.1+`):右列就是 DSH 自己的右侧栏——插件的每个 tab 类型与 tab 体通过 `ctx.sidebarRightTabs` / `ctx.sidebarRight` 注册与打开,聊天里的文件打开统一走 `ctx.sidebarRight.openResource('dsh-resource://file/…')`,插件**不再自绘右侧面板**(旧的浮窗能力同步移除)。自绘的底部工作台与开放给其他插件的 `ctx.betterSidebar` 服务保持不变,接入方式见[插件接入指南](docs/external-plugin-guide.md)。 diff --git a/docs/wiki/01-需求分析.md b/docs/wiki/01-需求分析.md new file mode 100644 index 0000000..6483db7 --- /dev/null +++ b/docs/wiki/01-需求分析.md @@ -0,0 +1,97 @@ +# 01 · 需求分析 + +> 本篇回答三个问题:**为什么要做这个 fork、要解决什么、做到什么程度算完成**。 +> 每条需求都给出「现象 / 根因(含证据)/ 需求 / 验收标准 / 状态」,便于回溯与验收。 +> 证据中的提交号均在本仓库 `main` 或 `sync/v0.24.1` 上。 + +## 一、背景 + +DSH(DeepSeek Harness)宿主自身提供会话、任务、模型与一个**原生右侧栏**(`ctx.sidebarRight` / +`sidebarRightTabs`)。上游 `dsh-better-sidebar` 在此基础上提供了一套「侧边栏工作台」: +文件树与编辑器、文件变动、任务、侧边对话、终端、浏览器,并把 +`ctx.betterSidebar` 服务开放给其他插件注册标签页与文件预览器。 + +**本 fork 的定位**:不重复造工作台,而是在其之上补齐宿主与上游都没有的那一层—— +**「信息层」**:这个会话的智能体拓扑、所属应用、以及会话画像。同时,在 fork 过程中暴露出 +若干与 DSH 桌面壳(Electron 壳)交互的**边界缺陷**,一并作为需求纳入。 + +## 二、核心需求 + +### R1 — 侧边栏只应存在一套 + +| 项 | 内容 | +| --- | --- | +| 现象 | 窗口里同时出现**两套右侧边栏**与**两套开合按钮**(一套自绘浮窗控件、一套 DSH 原生) | +| 根因 | fork 基线 v0.18.0 时,插件**自绘**右侧面板与开关(`toggleCluster`);DSH 0.1.5 起宿主自己也提供原生右侧栏,二者并存 | +| 需求 | 右侧内容全部改为**注册进 DSH 原生右侧栏**;插件不再自绘右侧面板 | +| 验收标准 | ① 产物中 `toggleCluster` 引用数为 **0**;② 右列只有宿主那一个展开控件;③ 每个 tab 类型与 tab 体都经由原生注册路径(`src/client/native/`)出现 | +| 状态 | ✅ 已完成(基线升级到上游 v0.19.1:`5a7e213` → 合并 `e5d34ac`;产物核对 `toggleCluster` = 0) | + +### R2 — 补齐「信息层」标签页 + +| 项 | 内容 | +| --- | --- | +| 现象 | 宿主有会话树、上游有工作台,但**没有**「这个会话的智能体拓扑 / 应用 / 画像」这一层信息 | +| 根因 | 上游聚焦文件工作台,信息层属于使用方(本 fork)的业务诉求 | +| 需求 | 提供三个信息层标签页:`apes:agents`(智能体)、`apes:apps`(应用)、`apes:profile`(画像),按 DSH 标准 `TabDescriptor` 注册 | +| 验收标准 | ① 三个 tab 进入原生 guide(带图标、可开合分栏);② `order` 取 60/65/70,与原生 10–50 不冲突;③ 单实例(single);④ 文案走 i18n(zh/en 双语齐全)| +| 状态 | ✅ 已完成(`src/client/apes/`,随基线同步进 v0.19.1 与 v0.24.1 两条线) | + +**R2.1(收敛)** — 最初的 `apes:tasks` 已移除:它与原生会话树信息重叠,属重复建设。当前信息层为三 tab。 + +### R3 — HTML 预览必须能在桌面壳里显示(且不牺牲沙箱) + +| 项 | 内容 | +| --- | --- | +| 现象 | 预览 HTML 文件时 iframe 只显示一行 `forbidden`(DSH Desktop 扩展模式) | +| 根因 | 三层因果:① 桌面壳给**所有**路由套一层围栏,扩展模式下 `ordinaryBrowserEnabled` 恒为 false,不带 Electron 注入的 `x-dsh-desktop-renderer` 头一律 403;② 该头只发给 **mainFrame** 与**同源**子框架;③ 预览 iframe 的 sandbox 令牌不含 `allow-same-origin`,请求时源为 `null` → 拿不到头 → 403。**与本次基线升级无关**(v0.18.0 起行为一致) | +| 需求 | 在**保持不透明源沙箱边界**的前提下让预览可用 | +| 验收标准 | ① 沙箱态下自包含 HTML(含内联脚本/样式)完整可见;② `sandbox` 令牌一个字符不变(不加 `allow-same-origin`);③ 解锁态下多文件 HTML 的相对资源也能加载;④ 取文档失败时仍能把路由的错误答复透出,而不是白屏 | +| 状态 | ✅ 已完成(`4c242ac`:主框架 `fetch` 取文档 → `srcDoc` 交付,见 [03-功能说明](03-功能说明)) | + +### R4 — 插件面板不得盖住宿主与其他插件的弹窗 + +| 项 | 内容 | +| --- | --- | +| 现象 | 打开「社区插件市场」(宿主自带的 `dsh-community-market`)时,弹窗左下角被本插件的文件面板**切掉** | +| 根因 | 桌面壳给上游 `#root` 打标 `data-dsh-desktop-content-viewport` 并施加 `isolation: isolate`——`#root` 成为层叠上下文,宿主的 overlay 层(`.dshDesktopOverlay`,`z-index: 1000`)就在其中;而插件的面板宿主挂在 `document.body`(在该上下文**之外**)且是带 `z-index: 25` 的定位元素,于是整块画在其上,宿主 overlay 的 1000 根本没参与竞争 | +| 需求 | 遵守宿主的层叠契约:**在 app 内容之上、在宿主 overlay 层之下** | +| 验收标准 | ① 插件市场弹窗完整盖住本插件面板;② 本插件面板仍在对话内容之上;③ 非桌面壳部署(纯网页)行为不变 | +| 状态 | ✅ 已完成(`4422001`:宿主挂载点改为 `#root[data-dsh-desktop-content-viewport]`,无标记时回退 `document.body`) | + +### R5 — 可运维:版本线可预期、升级可回滚 + +| 项 | 内容 | +| --- | --- | +| 现象 | 跟随上游新版本时,插件可能**静默失效**(不报错、不加载、无日志),排查成本高 | +| 根因 | 宿主启动时的兼容性预检 `semver.satisfies(host, peerRange, { includePrerelease: true })`:caret 范围跨 minor 必为 false → 整行禁用 | +| 需求 | ① 同时维护两条互相独立的版本线(旧宿主线 / 新宿主线),各自钉住可用的上游基线与 peer 范围;② 每次升级都有可回滚锚点(分支 + tag);③ 文档写清「哪条线配哪个宿主」 | +| 验收标准 | ① `main`(1.2.0 / 上游 v0.19.1 / peer `^0.1.5-rc.1`)与 `sync/v0.24.1`(1.3.0 / 上游 v0.24.1 / peer `^0.2.0-rc.1`)均可独立构建并通过门禁;② 远端有 `backup/*` 分支与 `apes-v*` tag 作为锚点;③ 部署指南给出选线决策 | +| 状态 | ✅ 已完成(见 [05-运维手册](05-运维手册)) | + +## 三、非目标(scope lock) + +| 不做 | 原因 | +| --- | --- | +| 新增上游之外的工作台面板(文件 / 终端 / 浏览器) | 属上游与宿主职责;本 fork 只用它们,不重造 | +| 团队 / 房间 / 多智能体协作面板 | 属宿主 `dsh-background-agents` 一类能力的职责 | +| 为「能用」而放宽沙箱(例如给预览 iframe 加 `allow-same-origin`) | **安全底线**:不拿安全边界换便利(R3 明确要求保住不透明源) | +| 直接 merge 上游提交 | 冲突面大、易引入无关变更;改用测量式 re-base(见 [07-开发与测试](07-开发与测试)) | + +## 四、约束条件 + +1. **安全边界不可降级**:HTML 预览与浏览器类内容一律在不透明源沙箱中渲染;新增能力不得要求放宽它。 +2. **KISS / DRY**:一处逻辑只写一遍(例如两处 HTML 预览共用 `HtmlPreviewFrame`)。 +3. **双语**:所有面向用户的文案必须同时有 `zh` 与 `en`(本次会话新增文案遵循此约定)。 +4. **可回滚**:任何升级前先留分支/tag 锚点,改动都能一条命令回退。 +5. **不污染运行环境**:不把未验证的新线同步进正在使用的 DSH profile。 + +## 五、需求 → 实现 → 锚点 + +| 需求 | 主要实现位置 | 关键提交 | +| --- | --- | --- | +| R1 单一侧边栏 | `src/client/native/`(原生侧边栏适配层) | `5a7e213` / `e5d34ac` | +| R2 信息层三 tab | `src/client/apes/`(`tabs.tsx` / `icons.tsx` / `locales.ts`) | 随 `5a7e213`、`e5bbef1` 同步 | +| R3 HTML 预览 | `src/client/HtmlPreviewFrame.tsx` + `src/client/html-preview.ts` | `4c242ac` | +| R4 层叠契约 | `src/client/desktop-env.ts`(`panelHostParent`)+ `src/client/index.tsx` | `4422001` | +| R5 版本线可运维 | 分支 `main` / `sync/v0.24.1`、tag `apes-v1.1.0-pre-sync`、`apes-v1.2.0` | `4422001` / `e5bbef1` | diff --git a/docs/wiki/02-系统架构.md b/docs/wiki/02-系统架构.md new file mode 100644 index 0000000..29fd976 --- /dev/null +++ b/docs/wiki/02-系统架构.md @@ -0,0 +1,154 @@ +# 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('#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// (路由负责路径围栏与体积上限) + 主框架 fetch 取回文档文本 → 注入 + → 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-部署指南))。 diff --git a/docs/wiki/03-功能说明.md b/docs/wiki/03-功能说明.md new file mode 100644 index 0000000..7ce4ad3 --- /dev/null +++ b/docs/wiki/03-功能说明.md @@ -0,0 +1,109 @@ +# 03 · 功能说明 + +> 本篇是**功能清单与行为契约**:有哪些面板、数据从哪来、文案与配色怎么走、安全边界在哪、能配什么。 + +## 一、面板与标签页总表 + +「来源」列:**上游** = 来自 `dsh-better-sidebar`;**宿主** = DSH 自身提供并占用该槽位;**本 fork** = 本项目的增量。 + +| id | 名称 | order | 来源 | 数据 / 行为 | +| --- | --- | --- | --- | --- | +| `editor` | 文件 | 10 | 上游 | 文件树 + 编辑器(CodeMirror 系,懒加载 `client-editor`) | +| `git` | 文件变动 | 20 | 上游 | 工作区变更、单个 diff 视图 | +| `subagent` | 子代理 | 30 | 上游 | 会话的子代理视图 | +| `sidechat` | 侧边对话 | 35 | 上游 | 会话内旁路对话 | +| `terminal` | 终端 | 40 | 上游 | PTY 终端(懒加载 `client-terminal`;v0.24.1 线起由宿主内置终端承担) | +| `browser` | 浏览器 | 50 | 上游 | 内嵌浏览器/HTML 预览宿主 | +| `diff` | (内部) | -1 | 上游 | 非用户可见,供内部打开 diff | +| **`apes:agents`** | **智能体** | **60** | **本 fork** | 主代理 + 直系子代理拓扑 | +| **`apes:apps`** | **应用** | **65** | **本 fork** | 当前会话所属工作区(宿主暂无「应用」feed) | +| **`apes:profile`** | **画像** | **70** | **本 fork** | 当前会话摘要(标题 / cwd / 运行态) | + +> order 10–50 是上游/宿主既定带宽,apos 信息层取 60/65/70 留出余量,避免与原生面板抢占位置。 + +## 二、信息层三 tab(本 fork 的增量) + +三个 tab 全部是**单实例**(`single: true`),且都按 DSH 标准 `TabDescriptor` 注册, +因此自动获得原生侧边栏的全部待遇:进入原生 guide、tab 芯片带图标、可开合与分栏。 + +| tab | 展示内容 | 数据来源 | 空态 | +| --- | --- | --- | --- | +| `apes:agents` | 根(主代理)卡片 + 其直系子代理卡片;每行显示标签、`one-shot`/`continuable`、运行态圆点 | ① `ctx.sessions.list` 的 `byId`(durable 会话摘要);② 宿主的会话投影 `projectionsBySession[sessionId].values.subagentCatalog`(直系子代理清单) | 「当前会话没有子代理」提示 | +| `apes:apps` | 当前会话的工作区(cwd) | `byId[scope.sessionId].cwd`,回退 `scope.cwd` | 明确写出「宿主今日无应用 feed」 | +| `apes:profile` | 会话标题、运行/空闲、cwd | `byId[scope.sessionId]` | 「无 cwd」提示 | + +**实现要点**:两个来源被归一化成同一种行形状 `{ id, label, mode }`,**活动态统一从 `byId` 读**, +于是「投影已就绪」与「仅有 durable 摘要」两种情况下渲染一致;这也是上游 0.1.6→0.1.7 会话 feed +迁移(`subagentsByParent` → `projectionsBySession`、`list.current` 删除)后的落地方式。 + +## 三、文件预览器 + +| id | 适用 | 说明 | +| --- | --- | --- | +| `image` | 图片 | 走媒体路由,受 `mediaLimit` 限制 | +| `pdf` | PDF | 内嵌查看 | +| `markdown` | Markdown | 渲染(Mermaid 走懒加载 chunk,可手动刷新) | +| `html` | HTML | **不透明源沙箱 iframe**,见下节 | +| `code` | 代码 | 高亮查看(编辑器懒加载) | +| `binary-download` | 二进制 | 只提供下载 | + +### 3.1 HTML 预览的交付方式(重要) + +``` +路由 /sidebar/html// ← 路径围栏 + 体积上限(readLimit) + │ + ├─ 主框架 fetch(唯一同时被宿主围栏信任、又能带上 Electron 头的角色) + │ + ├─ 注入 ← 让 ./style.css 等相对引用解析回本文件所在目录 + │ + └─ iframe srcDoc=<文档> + sandbox=HTML_IFRAME_SANDBOX +``` + +| 状态 | 自包含 HTML | 多文件 HTML(外链 CSS/JS/图片) | +| --- | --- | --- | +| 沙箱态(默认) | ✅ 完整可用 | 正文可用,**相对资源不可用**(iframe 源为 `null`,子请求同样被宿主围栏拒绝) | +| 临时解锁(无 sandbox 属性) | ✅ | ✅ **相对资源可用**(iframe 继承 GUI 源,成为普通同源请求) | + +设计取舍:**沙箱令牌一个字符都不放宽**。多文件 HTML 想要完整效果需用户显式点「临时解锁」—— +这是把「放宽边界」的决定权留给用户的显式动作,而不是插件默认降级。 + +## 四、安全边界(三层) + +| 层 | 机制 | 说明 | +| --- | --- | --- | +| 预览内容 | `sandbox="allow-scripts allow-popups allow-downloads allow-modals"` | **不含 `allow-same-origin`**:预览文档运行在不透明源里。宿主路由另有 CSP `sandbox` 兜底 | +| 插件路由 | 请求信任围栏(`src/trust-fence.ts`) | 校验 Host 头/令牌,拒绝跨站或非授权来源对 `/sidebar/*` 的访问 | +| 桌面壳 | Electron 注入的 `x-dsh-desktop-renderer` 头 | 扩展模式下所有路由的前置围栏;只发给主框架与同源子框架。这正是「沙箱 iframe 发不出请求」的根源 | + +## 五、i18n 与主题 + +- **语言包**:`src/client/locales.ts` + 19 个 `src/client/locales-*.ts` 覆盖文件;默认 `zh`,缺省回退 `en`。 +- **本 fork 文案**:集中在 `src/client/apes/locales.ts`,每个 key 同时提供 `zh` / `en`。 +- **硬性约定**:界面里**不得出现裸字符串**,一律 `t('key')`。 +- **配色**:只用宿主设计令牌 `--dsw-alias-*`(bg / border / label / state / interactive 系列), + 写法为 `var(--dsw-alias-xxx, <字面回退>)`,保证在非 DSH 环境(单元测试)也能渲染。 + **无公开的 `--ds-*` 几何令牌**,字号/间距/圆角内联即可。 + +## 六、配置面(挂载行 `config`) + +配置写在插件挂载行上(profile 的 `cordis.patch.yml` 或 bundle patch 的 insert 行),设置页写的也是它: + +| 字段 | 默认 | 含义 | +| --- | --- | --- | +| `readLimit` | `512 KiB` | 单文件读取上限(超出截断并标记 `truncated`) | +| `mediaLimit` | `20 MiB` | 媒体(图片等)读取上限 | +| `uploadLimit` | `128 MiB` | 上传上限 | +| `listLimit` | `1000` | 目录列举条目上限 | +| `terminalsPerSession` | `3` | 每会话终端数上限 | +| `shell` / `shellArgs` | 自动解析 | 终端 shell 与其启动参数(`shellArgs` 非空时替换默认 `-l`) | + +例:在挂载行里指定 shell + +```yaml +- insert: + - id: better-sidebar + name: 'dsh-apes-sidebar' + config: + shell: /bin/zsh + shellArgs: ['--noprofile', '--no-rc'] +``` diff --git a/docs/wiki/04-部署指南.md b/docs/wiki/04-部署指南.md new file mode 100644 index 0000000..1be180f --- /dev/null +++ b/docs/wiki/04-部署指南.md @@ -0,0 +1,163 @@ +# 04 · 部署指南 + +> 目标:在一台机器上把本插件挂进 DSH,并确认它**真的在跑**。 +> 关键前提:**先确认宿主版本,再选版本线**——选错线的唯一症状是「静默失效」。 + +## 一、前置条件 + +| 项 | 要求 | +| --- | --- | +| Node | ≥ 20(`engines.node`);本机验证用 22.x | +| pnpm | 11.x(`pnpm install` 会自动触发 `prepare: tsdown` 构建) | +| DSH | 见下面的版本矩阵 | +| 终端可用性 | 仅 v0.19.1 线需要额外放行 `node-pty` 原生构建(见 §五) | + +### 1.1 宿主版本矩阵(先读这张表) + +| 版本线 | 分支 | 插件版本 | peer 声明 | 适用宿主 | +| --- | --- | --- | --- | --- | +| 旧宿主线 | `main` | 1.2.0 | `^0.1.5-rc.1` | DSH `0.1.5-rc.1+`(含 `0.1.5-rc.2`,即 **DSH Desktop 2.0.10** 内置版本) | +| 新宿主线 | `sync/v0.24.1` | 1.3.0 | `^0.2.0-rc.1` | DSH `0.2.0-rc.1+` | + +**怎么查宿主版本**(扩展模式桌面版): + +```bash +# 内置的 dsh 版本 +grep -m1 '"version"' "/Applications/DSH Desktop.app/Contents/Resources/app/node_modules/@deepseek-ai/dsh/package.json" +# 或看 profile 里装的宿主包 +grep -m1 -A2 '"@deepseek-ai/dsh"' ~/.dsh/profiles/desktop/package.json +``` + +**选线决策** + +``` +宿主 ≥ 0.2.0-rc.1 ? ── 是 ──▶ sync/v0.24.1(1.3.0) + └─ 否 ──▶ main(1.2.0) +``` + +> ⚠️ **peer 不匹配不会报错。** 宿主以 `semver.satisfies(host, peerRange, { includePrerelease: true })` 预检, +> caret 跨 minor 判 false 后**整行静默禁用**:不加载、无日志、界面上「像没装」。 +> 因此「装完没反应」的第一件事永远是核对宿主版本 ↔ peer 范围。 + +## 二、安装方式(本 fork 未发布 npm 包) + +`npm view dsh-apes-sidebar` 返回 404 —— **本 fork 不在 npm 上**(README 里的 npm 徽章/命令是沿用上游文档)。 +实际部署使用「本地源码 + `file:` 链接依赖」: + +### 步骤 1:构建 + +```bash +cd <插件仓库绝对路径> +git checkout main # 或 sync/v0.24.1(按宿主版本选) +pnpm install # 会顺带跑 prepare: tsdown +pnpm typecheck && pnpm build && pnpm test # 门禁三连 +``` + +产物(`lib/`):`index.js` / `invariant.js`(host)+ `client.js` + 懒加载 chunk +(`client-registry` / `client-editor` / `client-locale` / `client-mermaid` / `client-terminal`)+ `types/`。 + +### 步骤 2:接进 profile + +编辑 `~/.dsh/profiles//package.json`(桌面版为 `desktop`,网页版通常为 `web`): + +```jsonc +{ + "dependencies": { + "dsh-apes-sidebar": "file:/绝对路径/dsh-apes-sidebar" // ← 必须是绝对路径 + }, + "dsh": { + "profile": { + "bundles": [ + "…", + "dsh-apes-sidebar" // ← 加入 bundle 栈 + ] + } + } +} +``` + +> 也可以在 profile 自己的 `cordis.patch.yml` 里手写挂载行(老方式)。**两种方式不要同时用**: +> 会双挂载成两个 Node 半、两个侧边栏。包内自带的 `cordis.patch.yml` 是 `dsh.bundle.patch` 层, +> 由官方 CLI 安装时自动追加 bundle,并带「聚合包双挂载退让」守卫。 + +### 步骤 3:让 profile 重新安装依赖 + +```bash +cd ~/.dsh/profiles/ && pnpm install +``` + +**为什么必须重装**:pnpm 对 `file:` 依赖是**复制**语义(不是软链)。源码 `pnpm build` 之后, +profile 的 `node_modules/dsh-apes-sidebar/lib` **仍是旧副本**——这就是「改了代码但界面没变」的头号原因。 +DSH 启动时也会 reconcile profile 的 pnpm workspace,但不要假定它一定帮你刷新。 + +### 步骤 4:完整重启 + +**⌘Q 完全退出 DSH,再重新启动**(不是关窗口)。client 半有热加载,但 host 半与依赖变更必须走完整重启。 + +## 三、验收清单 + +重启后逐条核对(每条都对应一个可观察事实): + +- [ ] **右列只有一套侧边栏**、一个开合控件(不再出现自绘的第二套开关) +- [ ] 原生 guide 里能看到本插件的 tab 芯片,且**带图标** +- [ ] 三个信息层 tab 存在:**智能体 / 应用 / 画像**(order 在原生面板之后) +- [ ] `apes:agents` 打开后:有主代理卡片;有子代理时列出直系子代理(标签 + 模式 + 运行态) +- [ ] 打开一个 HTML 文件:**沙箱态下自包含 HTML 能正常渲染**(不再是 `forbidden`) +- [ ] 打开「社区插件市场」:弹窗**完整盖在**本插件面板之上(面板不切弹窗角) +- [ ] 页面左下角**没有**红色诊断条(有的话说明 client 生命周期某处抛错) +- [ ] 开发者控制台**没有** `[dsh-apes-sidebar] … error` 前缀的错误 + +## 四、网页 profile 与桌面 profile 的差异 + +| 项 | 桌面版(DSH Desktop) | 网页版 | +| --- | --- | --- | +| profile 目录 | `~/.dsh/profiles/desktop` | 通常 `~/.dsh/profiles/web` | +| 外壳 | Electron 壳,有「扩展模式/兼容模式」与请求围栏 | 浏览器直连宿主端口 | +| 预览 iframe | 受渲染器围栏约束(因此必须走 `srcDoc` 交付) | 无该围栏,但仍受路由 CSP/沙箱约束 | +| 面板宿主层级 | 壳层会给 `#root` 打 content-viewport 标记 → 宿主挂进 `#root` | 无标记 → 宿主挂 `document.body` | +| 刷新方式 | ⌘Q 完整退出再启动 | 硬刷新(Cmd/Ctrl+Shift+R) | + +## 五、v0.19.1 线特有:放行 node-pty 原生构建 + +`main`(上游 v0.19.1 基线)在 `package.json` 中仍有 `node-pty` 依赖;pnpm 11 默认**拦截依赖的构建脚本**, +于是首次安装会失败或终端不可用: + +```bash +cd ~/.dsh/profiles/ +pnpm approve-builds # 选中 node-pty,回车确认 +pnpm rebuild node-pty # 必要时 +``` + +`sync/v0.24.1`(1.3.0)**不含** `node-pty`(终端交由宿主内置承担),无需此步。 +(`main` 与 `sync/v0.24.1` 的 `node-pty` 引用数:1 / 0。) + +## 六、切换与卸载 + +同一条线只能装一个(两条线都注册 `ctx.betterSidebar`,entry id 冲突,第二个被丢弃)。 +仓库自带切换脚本: + +```bash +bash scripts/switch.sh --status # 只看当前是哪条线 +bash scripts/switch.sh aps # 切到本 fork 线(本地 file: 安装) +bash scripts/switch.sh upstream # 切回上游 dsh-better-sidebar +bash scripts/switch.sh upstream 0.18.0 # 指定上游版本 +bash scripts/switch.sh aps --profile web # 指定 profile +``` + +**完全卸载**:从 profile 的 `dependencies` 与 `dsh.profile.bundles` 中移除 `dsh-apes-sidebar`, +`pnpm install`,⌘Q 完整重启。若曾手写过 `cordis.patch.yml` 挂载行,一并删除(否则会留下悬空 entry)。 + +> 若同时装了聚合包(如 `@linxin666/dsh-web-ui-all`)且它内部也挂载了本包, +> 包内 bundle patch 的守卫会让**本行退让**;此时请确认聚合包在 bundle 栈中位于本行之前。 + +## 七、变更后的重新部署 + +```bash +# 插件仓库 +pnpm typecheck && pnpm build && pnpm test +# profile +cd ~/.dsh/profiles/ && pnpm install +# ⌘Q 退出 DSH 再启动 +``` + +改完代码**必须**走完这三步:产物构建非字节确定性,profile 的 `file:` 副本也不会自动跟随。 diff --git a/docs/wiki/05-运维手册.md b/docs/wiki/05-运维手册.md new file mode 100644 index 0000000..8af85d9 --- /dev/null +++ b/docs/wiki/05-运维手册.md @@ -0,0 +1,133 @@ +# 05 · 运维手册 + +> 本篇是**日常运维、升级与回滚**的操作手册。所有命令都以「先留锚点、再动手、改完回读验证」为原则。 + +## 一、版本线与分支策略(当前远端实况) + +| ref | 指向 | 含义 / 用途 | +| --- | --- | --- | +| `main` | `4422001` | **旧宿主线**:插件 1.2.0,上游基线 v0.19.1,peer `^0.1.5-rc.1`。本机 DSH Desktop 2.0.10 正在跑这条 | +| `sync/v0.24.1` | `e5bbef1` | **新宿主线**:插件 1.3.0,上游基线 v0.24.1,peer `^0.2.0-rc.1`。待 ≥0.2.0 宿主实机验证 | +| `backup/pre-sync-v0.19.1` | `96cc334` | v0.19.1 re-base **之前**的状态(旧 fork 三角色 tab 版) | +| `backup/pre-sync-v0.24.1` | `4422001` | v0.24.1 re-base 之前的 `main`(等价于当前 main,作为下次升级的锚点) | +| `tags/apes-v1.1.0-pre-sync` | `96cc334` | 与上面第一支 backup 同一提交,tag 形式的锚点 | +| `tags/apes-v1.2.0` | `4422001` | 当前已验收的 1.2.0 线 | + +**纪律**:任何 re-base / 大改之前,先建 `backup/pre-sync-<新基线>` 分支 + `apes-v<版本>` tag, +并**推送**到远端(本地锚点在多人/多机场景等于不存在)。 + +## 二、回滚剧本 + +### 场景 A:某次提交引入回归(单点回退) + +```bash +cd <插件仓库> +git log --oneline -5 # 定位可疑提交 +git revert # 保留历史的回退(推荐) +# 或整体丢弃:git reset --hard <上一提交> +pnpm typecheck && pnpm build && pnpm test +cd ~/.dsh/profiles/ && pnpm install # file: 是复制语义,必须重装 +# ⌘Q 完全退出 DSH → 重启 +``` + +### 场景 B:整条线回退(例如新线在真机上不可用) + +```bash +git checkout main # 或 backup/pre-sync-v0.24.1 +pnpm install && pnpm build +cd ~/.dsh/profiles/ && pnpm install +# ⌘Q 重启 +``` + +### 场景 C:profile 依赖/config 被改坏 + +```bash +cd ~/.dsh/profiles/ +cp package.json package.json.bak.$(date +%s) # 先备份 +bash <插件仓库>/scripts/switch.sh aps # 或 upstream,脚本会重写依赖与 bundle 行 +pnpm install +# ⌘Q 重启 +``` + +**回滚验收**:重启后按 [04-部署指南](04-部署指南) §三 的验收清单逐条核对(至少「右列一套侧边栏」 +与「三个信息层 tab 存在」两条)。 + +## 三、发布 / 变更纪律 + +1. **版本三元组必须一致**: + - `package.json` 的 `version` + - `dsh.plugin.json` 的 `version` + - `src/client/service.ts` 的 `SIDEBAR_SERVICE_VERSION` + 三者不一致会被单元测试(`tests/service.spec.ts`)直接拦下——**不要改其中一个而忘了另一个**。 +2. **门禁三连必过**:`pnpm typecheck && pnpm build && pnpm test`。当前基线:`main` 131 个测试文件 / + 1367 通过;`sync/v0.24.1` 141 个文件 / 1515 通过(另有 9 个 skip)。 +3. **产物非字节确定性**:两次构建的字节数可能相同但 **sha 不同**。因此「改完代码 → 让 profile 用旧副本」 + 永远不成立;必须重新 `pnpm build` + 让 profile 重新安装。 +4. **发布方式**:本 fork 未发布 npm 包,发布 = 推分支/tag + 目标机器 `git fetch && checkout && pnpm build` + + profile 重装。发布前把 `README.md` 与 `docs/wiki/` 一并更新(wiki 见 §六)。 +5. **不要顺手改无关文件**:re-base 时保持「只带自己的增量」,避免下次升级冲突面扩大。 + +## 四、日常巡检清单 + +| 频率 | 检查项 | 命令 / 判据 | +| --- | --- | --- | +| 每次部署后 | 版本三元组一致 | 见 §三.1 | +| 每次部署后 | 产物标记齐全 | `grep -c 'apes:agents' lib/client.js`(>0)、`grep -c 'dsh-desktop-content-viewport' lib/client.js`(>0) | +| 每次部署后 | profile 副本已刷新 | 比对 `lib/client.js` 与 `~/.dsh/profiles/

/node_modules/dsh-apes-sidebar/lib/client.js` 的 sha256 | +| 每次升级宿主/插件 | 宿主版本 ↔ peer 范围 | 见 [04-部署指南](04-部署指南) §1.1(不匹配 = 静默禁用) | +| 每月/大改前 | 锚点是否已推远端 | `git ls-remote \| grep -E 'heads/(main\|sync)\|tags/apes'` | +| 异常时 | 诊断条与控制台 | 页面左下角红条;控制台 `[dsh-apes-sidebar]` 前缀 | + +## 五、备份与配置铁律 + +- **改任何 profile 配置前先备份**,改完**回读验证**(不要假定写入即生效): + + ```bash + cp ~/.dsh/profiles//package.json{,.bak.$(date +%s)} + # 改完: + python3 -c "import json;json.load(open('$HOME/.dsh/profiles//package.json'));print('JSON OK')" + ``` + +- **DSH 应用配置的类型陷阱**:部分字段(如材质/透明度类的 `macosMaterial`)必须是**字符串** + (`"off"` / `"transparent"`)。若被写成 YAML 布尔(`false`),DSH 会启动进**恢复模式**。 + 改完务必回读校验类型: + + ```bash + python3 -c "import yaml;d=yaml.safe_load(open('$HOME/.dsh/settings.yaml'));v=d.get('macosMaterial');print(v, type(v).__name__)" + ``` + +- **不要回滚/手工编辑** profile 的 `pnpm-lock.yaml`:由 pnpm 重建;手工改会导致解析错乱。 +- **凭据不进仓库**:远端推送用系统凭据助手(`git credential fill`),仓库内不写 token。 + +## 六、百科(wiki)本身的维护 + +百科源文件版本化在仓库 `docs/wiki/`,通过脚本推送到 Gitea wiki: + +```bash +bash scripts/push-wiki.sh # 推送/更新全部页面(幂等:有则 PATCH,无则 POST) +bash scripts/push-wiki.sh 05-运维手册 # 只推一页 +``` + +凭据来自 `git credential fill`(与推送代码同一套,不落盘)。修改文档的推荐流程: +**改 `docs/wiki/*.md` → 提交到仓库 → 跑脚本推送**,这样 wiki 与代码同源可回溯。 + +## 七、诊断与日志 + +| 信号 | 含义 | 位置 | +| --- | --- | --- | +| 左下角红色诊断条 `[dsh-apes-sidebar] error: …` | client 生命周期某阶段抛错(mount / native / 懒加载等) | 页面左下角(`z-index: 2147483000`),同时 `console.error` 打前缀 | +| 控制台 `[dsh-apes-sidebar] …` | 同上,且带上具体阶段名 | 开发者工具 Console | +| 「插件像没装」且**无任何日志** | 宿主兼容性预检失败(peer 不匹配)→ **静默禁用** | 只能靠版本矩阵排查,见 [06-故障排查](06-故障排查) §1 | +| 界面正常但面板层级错乱 | 面板宿主挂载点/层叠上下文问题 | 见 [02-系统架构](02-系统架构) §四 | + +## 八、性能与容量 + +| 项 | 观察值 | 说明 | +| --- | --- | --- | +| `lib/client.js` | ≈ 0.9 MB | 主 bundle,始终加载 | +| `lib/client-mermaid.js` | ≈ 7 MB(+12 MB map) | 懒加载,仅在需要渲染 Mermaid 时拉取 | +| `lib/client-editor.js` | ≈ 2 MB | 懒加载,仅打开编辑器时 | +| `lib/client-terminal.js` | ≈ 0.5 MB | 懒加载,仅打开终端时 | +| 全量测试 | 约 3–4 分钟 | 变更后按需跑全量,日常可只跑相关 spec | + +主 bundle 不应静态引入上述重型库;新增重型依赖一律走 `src/client/chunks/`(懒加载 chunk)。 diff --git a/docs/wiki/06-故障排查.md b/docs/wiki/06-故障排查.md new file mode 100644 index 0000000..8f384ed --- /dev/null +++ b/docs/wiki/06-故障排查.md @@ -0,0 +1,208 @@ +# 06 · 故障排查 + +> 每条都给出**症状 → 根因(机制)→ 处置 → 验证**。标注 ✅ 的条目是在真机上定位并验证过的案例。 + +## 速查表 + +| 症状 | 最可能根因 | 跳到 | +| --- | --- | --- | +| 装完「像没装」,界面无变化且**无任何日志** | peer 范围与宿主版本不匹配 → 静默禁用 | §1 | +| 页面出现**两个侧边栏** / 两套开合按钮 | 双挂载(两条线同装,或残留挂载行) | §2 | +| HTML 预览只显示 `forbidden` | 桌面壳渲染器围栏 + 不透明源 iframe | §3 | +| 插件面板盖住宿主/第三方弹窗(如插件市场) | 面板宿主挂在层叠上下文之外 | §4 | +| 终端打不开 / `node-pty` 加载失败 | pnpm 拦截原生构建(仅 v0.19.1 线) | §5 | +| 改了代码,界面没变 | 产物未重建 / profile 副本未刷新 / 浏览器缓存 | §6 | +| DSH 拒绝加载插件、entry pending | 插件声明(`dsh.plugin.json` / bundle patch)不合法 | §7 | +| DSH 启动进**恢复模式** | profile 配置被写成错误类型(如 YAML 布尔) | §8 | + +--- + +## §1 装完「像没装」:静默禁用 ✅ + +**症状**:profile 里依赖装了、文件也在,但界面没有任何变化;日志里也没有插件相关报错。 + +**根因**:宿主启动时做兼容性预检,判定 `peerDependencies` 与自身版本是否满足: + +``` +semver.satisfies(hostVersion, peerRange, { includePrerelease: true }) +``` + +caret 范围**跨 minor 必然为 false**(例如 `^0.1.5-rc.1` 对 `0.2.0`)。判定失败后宿主的处理是 +**跳过这一行**——不抛错、不打日志、不加载。 + +**处置**:核对宿主版本,改用匹配的版本线(见 [04-部署指南](04-部署指南) §1.1)。 + +```bash +grep -m1 '"version"' "/Applications/DSH Desktop.app/Contents/Resources/app/node_modules/@deepseek-ai/dsh/package.json" +grep -n 'dsh-client-ui-sidebar-right' <插件仓库>/package.json # 看 peer 范围 +``` + +**验证**:重启后右列出现插件 tab;`lib/client.js` 里能 grep 到 `apes:agents`。 + +**教训**:这一条没有日志可查,因此**部署前先看版本矩阵**是唯一可靠的预防手段。 + +## §2 两个侧边栏 / 两套开合按钮 ✅ + +**症状**:窗口里出现两套右侧面板与两组开合控件。 + +**根因**:两个方向都可能: +1. **旧版遗留**:v0.18.0 基线时插件自绘右侧面板与开关(`toggleCluster`)。升级后自绘部分应已移除。 +2. **双挂载**:同一条线被挂两次(profile 自己的 `cordis.patch.yml` 里还留着旧挂载行, + 与 bundle patch 同时生效),或者本 fork 线与上游线同时安装(两者都注册 `ctx.betterSidebar`, + entry id 冲突,第二个被丢弃)。 + +**处置**: + +```bash +# 确认产物里已经没有自绘开关 +grep -c toggleCluster <插件仓库>/lib/client.js # 期望 0 + +# 检查是否双挂载:profile 里是否同时存在 bundle 行 与 手写挂载行 +grep -n "dsh-apes-sidebar\|dsh-better-sidebar" ~/.dsh/profiles//cordis.patch.yml ~/.dsh/profiles//package.json +``` + +- 有残留手写挂载行 → 删除它,只保留一条通道。 +- 两条线同装 → `bash scripts/switch.sh aps`(或 `upstream`)二选一。 +- 装了聚合包(如 `@linxin666/dsh-web-ui-all`)→ 确认聚合包在本包之前,让包内守卫生效。 + +**验证**:右列只剩一套面板与一个开合控件。 + +## §3 HTML 预览显示 `forbidden` ✅ + +**症状**:预览 HTML 文件时 iframe 里只有一行裸文本 `forbidden`;而「临时解锁」后又能看。 + +**根因(三层)**: + +1. DSH Desktop 在**扩展模式**下给所有路由套了一层请求围栏:`ordinaryBrowserEnabled` 恒为 false, + 不带 Electron 注入的 `x-dsh-desktop-renderer` 头的请求一律 `403 + "forbidden"`; +2. 该头只注入给 **mainFrame** 与**同源**子框架; +3. 预览 iframe 的 sandbox 令牌不含 `allow-same-origin`,请求时其源为 `null` → 拿不到头 → 403。 + +与「是否升级了插件基线」无关(v0.18.0 起行为一致)。 + +**处置**:插件已在 1.2.0+ 用 **`srcDoc` 交付**修复(主框架取文档 → 注入 `` → 注入 iframe), +`main` 上对应提交 `4c242ac`。**不要**通过给 iframe 加 `allow-same-origin` 来"修"——那是安全降级。 + +**验证**: +- 沙箱态下自包含 HTML(含内联脚本)能正常渲染; +- 有外链资源的 HTML 在沙箱态下缺资源属**已知限制**,点「临时解锁」后应完整显示。 + +**排查命令**(确认宿主围栏在起作用): + +```bash +lsof -nP -iTCP -sTCP:LISTEN | grep -i dsh # 找宿主端口(每次启动可能不同) +curl -s -o /dev/null -w '%{http_code}\n' "http://127.0.0.1:<端口>/" # 无渲染器头 → 403 +``` + +## §4 插件面板盖住宿主/第三方弹窗 ✅ + +**症状**:打开「社区插件市场」(宿主自带 `dsh-community-market`)时,弹窗左下角被本插件的 +文件面板整齐切掉一块。 + +**根因**:桌面壳的有框模式会给上游 `#root` 打标 `data-dsh-desktop-content-viewport` 并施加 +`isolation: isolate`,使 `#root` 成为**层叠上下文**;宿主的 overlay 层(`.dshDesktopOverlay`, +`z-index: 1000`)位于其中。插件面板宿主若挂在 `document.body`(该上下文之外)且是定位元素, +就会画在整个上下文之上,把 overlay 里的弹窗盖住。 + +**处置**:已修(`4422001`)——面板宿主改为挂进 `#root[data-dsh-desktop-content-viewport]`, +无标记时回退 `document.body`。**不要**通过调大 `z-index` 解决(25 的取义见 +[02-系统架构](02-系统架构) §四;乱调会破坏与宿主其他层级的关系)。 + +**验证**:插件市场弹窗完整盖住本插件面板,同时本插件面板仍在对话内容之上。 + +## §5 终端不可用 / `node-pty` 加载失败(仅 v0.19.1 线)✅ + +**症状**:终端面板报 shell 启动失败或提示 `node-pty` 加载失败;`pnpm install` 阶段报 +"ignored build scripts"。 + +**根因**:pnpm 11 默认**拦截依赖的构建脚本**,`node-pty` 的原生二进制没被编译。 +(`main` 依赖 `node-pty`;`sync/v0.24.1` 已移除该依赖,终端由宿主内置承担。) + +**处置**: + +```bash +cd ~/.dsh/profiles/ +pnpm approve-builds # 选中 node-pty +pnpm rebuild node-pty # 必要时 +# ⌘Q 完整退出 DSH 再启动 +``` + +**验证**:终端能开出 shell;`node_modules/node-pty/build/Release/*.node` 存在。 + +## §6 改了代码,界面没变 ✅ + +**症状**:源码已改、`pnpm build` 也跑了,但界面行为仍是旧的。 + +**根因(三个叠加因素)**: + +1. 产物**非字节确定性**,且 profile 的 `file:` 依赖是**复制**语义 → profile 里仍是旧副本; +2. `lib/client.js` 是浏览器的 `window.__ModuleLoader__.load({...})` bundle,**不是 CJS/ESM**, + 在 Node 里 `require` 会因 `window is not defined` 报错——因此不能用「在 Node 里试跑」来验证前端改动; +3. host 半的改动必须重启进程(client 半有热加载,host 半没有)。 + +**处置**: + +```bash +cd <插件仓库> && pnpm build +cd ~/.dsh/profiles/ && pnpm install # 刷新 file: 副本 +# ⌘Q 完整退出 DSH → 重启 +``` + +**验证**:比对两边产物 sha: + +```bash +shasum -a 256 <插件仓库>/lib/client.js \ + ~/.dsh/profiles//node_modules/dsh-apes-sidebar/lib/client.js +``` + +## §7 DSH 拒绝加载插件 / entry pending + +**症状**:插件目录存在,但宿主不加载,或 loader 报 entry pending / 恢复模式。 + +**根因**:插件的声明面不合法或不被接受,常见于: +- `dsh.plugin.json` 的 `engines.dsh` 与宿主不符; +- bundle patch / 挂载行的 `insert` 结构不合法(例如把声明写成宿主无法解析的形态); +- 同 id 重复挂载(`duplicate loader entry id` / `duplicate prefix route`)。 + +**处置**: + +```bash +cat <插件仓库>/dsh.plugin.json # 检查 id / engines / main +cat <插件仓库>/cordis.patch.yml # 检查 insert 结构与守卫表达式 +grep -n "cordis.patch\|dsh-apes-sidebar" ~/.dsh/profiles//package.json +``` + +确保只有一条挂载通道、`name` 与实际包名一致(本 fork 为 `dsh-apes-sidebar`), +`engines.dsh` 与宿主版本相容。改完 `pnpm install` + **完整重启**。 + +**验证**:启动日志无 loader 报错;右列出现插件 tab。 + +## §8 DSH 启动进「恢复模式」 + +**症状**:DSH 启动后进入恢复/安全模式,插件与侧边栏都不正常。 + +**根因**:profile 或应用配置被写成**错误类型**。典型是材质/透明度字段要求**字符串** +(`"off"` / `"transparent"`),但被 YAML 序列化成了布尔 `false` —— 宿主按类型校验失败即进恢复模式。 + +**处置与验证**: + +```bash +python3 -c "import yaml;d=yaml.safe_load(open('$HOME/.dsh/settings.yaml'));v=d.get('macosMaterial');print(repr(v), type(v).__name__)" +# 期望:'off' str(或 'transparent' str);若为 False bool → 改回字符串 +``` + +改配置前**先备份**,改完**回读校验类型**(见 [05-运维手册](05-运维手册) §五)。 + +--- + +## 附:定位手法(本次会话验证有效) + +1. **先按用户可见文案 grep 找归属**:`grep -rl "<界面上的中文短语>" /lib /node_modules`。 + 若两边都搜不到,说明它来自**运行时下载/缓存**(例如市场插件),再按 `NSUserData` 缓存或 + `desktop-market/state.json` 之类线索去找提供方。 +2. **再读提供方自己的 CSS/DOM 结构**:拿到它声明的 z-index、定位方式与所在槽位 + (如 `renderSlot("shell.overlay")`),据此判断是「它太高」还是「我们太高」。 +3. **层叠问题先找 `isolation` / `transform` / `filter` / `contain`**:这些才是层叠上下文与包含块的真正来源, + 比 z-index 数值更关键。 +4. **像素证据要用原图量**:交给视觉模型的截图会被缩放(1920→1280),裁剪坐标 ≠ 屏幕坐标; + 用 PIL 在原图上量边界、比对 RGB 剖面,再下结论。 diff --git a/docs/wiki/07-开发与测试.md b/docs/wiki/07-开发与测试.md new file mode 100644 index 0000000..b8c7029 --- /dev/null +++ b/docs/wiki/07-开发与测试.md @@ -0,0 +1,164 @@ +# 07 · 开发与测试 + +> 面向改代码的人(与 agent)。**先读 [02-系统架构](02-系统架构) §六「关键不变量」**, +> 再动手改,能省掉大部分回退。 + +## 一、工程约定 + +| 项 | 约定 | +| --- | --- | +| 语言 | TypeScript **strict**(含 `noUncheckedIndexedAccess`)、ESM、导入写 `.ts` 扩展名 | +| 构建 | `tsdown`(rolldown),host/client 双产物;懒加载 chunk 走 `src/client/chunks/` | +| 包管理 | `pnpm`(11.x);`pnpm install` 会触发 `prepare` 构建 | +| 测试 | `vitest`;默认 node 环境,需要 DOM 的 spec 用 `// @vitest-environment jsdom` | +| 宿主 peer | `dsh-better-sidebar`(本仓库已改名)**永不打包**,必须留在 peer/neverBundle 一侧 | +| 命名 | 信息层 tab id 前缀 `apes:`;插件名 `dsh-apes-sidebar` | +| 颜色 | 只用 `--dsw-alias-*` 设计令牌(`var(--token, <回退>)`),不写死色值 | + +## 二、目录结构(`main` 实况) + +``` +src/ + index.ts HOST 入口:name / inject / apply(ctx, config),路由与围栏装配 + invariant.ts 零依赖不变量面(host/client 共用) + context-types.ts cordis 服务面契约(ctx.sessions / sidebarRight / …) + trust-fence.ts 请求信任围栏(Host 头 / 令牌) + html-route.ts /sidebar/html 路由(路径围栏 + 体积上限) + config.ts 挂载行配置 schema 与默认值(readLimit / mediaLimit / …) + fs-*.ts git.ts … 文件系统、Git、会话、任务、终端等 host 能力 + client/ + index.tsx CLIENT 入口(apply):装配 store/service/native/apes + service.ts ctx.betterSidebar 服务实现(对外 API + 能力清单) + state.ts 侧边栏状态存储(store) + native/ 原生右侧栏适配(tab 记录、tab 体、tab 芯片标题) + builtins/ 内置面板:editor / git / subagent / sidechat / terminal / browser + 预览器 + sidebar/ 自绘底部工作台与开合控件 + apes/ 信息层三 tab(本 fork 增量:tabs.tsx / icons.tsx / locales.ts) + changes/ diff/ 文件变动与 diff 视图 + chunks/ 懒加载 chunk:editor / locale / mermaid / terminal + locales*.ts i18n(`locales.ts` + 19 个语言覆盖文件) + *.module.css 组件样式(含 sidebar.module.css:面板宿主层契约) +tests/ vitest specs(131 个文件,含 e2e/ 的挂载脚本型用例) +scripts/ install / switch / e2e 挂载 / 发布辅助 +docs/ external-plugin-guide.md(对外契约)+ plans/(上游历史设计)+ wiki/(本百科源文件) +``` + +## 三、门禁(提交前必跑) + +```bash +pnpm typecheck # tsc --noEmit(strict) +pnpm build # rm -rf lib && tsc(d.ts) && tsdown +pnpm test # vitest run +``` + +当前基线(本次会话实测):`main` = 131 文件 / **1367 通过** + 9 skip; +`sync/v0.24.1` = 141 文件 / **1515 通过** + 9 skip。全量 test 约 3–4 分钟。 +**任何一条不过就不要提交**;三条全绿才算完成。 + +## 四、测试体系(按层次) + +| 层次 | 环境 | 手法 | 代表 spec | +| --- | --- | --- | --- | +| 宿主插件形状 / 对外契约 | node | 断言导出面与语义(含与 `package.json` 的版本一致性) | `smoke.spec.ts` · `plugin-shape.spec.ts` · `service.spec.ts` · `consumer-types.ts` | +| 原生侧边栏适配 | node | 用「类型为真、运行时为假」的 mock 注册表 | `native-surface.spec.ts` | +| 面板宿主与层叠契约 | node + jsdom | CSS 断言 + DOM 挂载点解析 | `panel-host-css.spec.ts` · `panel-host-parent.spec.ts` | +| 预览交付 | node + jsdom | 纯逻辑(`` 注入)+ 客户端挂载(stub `fetch`,`createRoot` + `act`) | `html-preview.spec.ts` · `html-preview-frame.spec.tsx` | +| 沙箱契约 | node | 对渲染结果做**字符串断言**(服务端渲染把 `srcDoc` 输出为 camelCase `srcDoc=""`,与客户端设置的真实属性 `srcdoc` 不同 —— 断言方式随之不同) | `sandbox-views.spec.tsx` | +| 信息层 tab | node | mock `ctx.sessions.list` 与投影 | `apes-tabs.spec.ts` | +| 桌面环境探测 | node | stub 全局(URL stamp / preload 标记) | `desktop-env.spec.ts` | + +约定: +- **不引入 jsdom 到默认环境**:需要 DOM 的 spec 在文件头写 `// @vitest-environment jsdom`。 +- **不依赖真实 DSH 宿主**:需要真宿主的用例归到 `tests/e2e/`(脚本型挂载),不进单元门禁。 +- 新增「契约」必须**同时新增 pin 它的 spec**(例如本文档提到的 25 层级、沙箱令牌、`srcDoc` 交付方式, + 都有对应断言)。改契约 = 改代码 + 改 spec,两者必须一起提交。 + +## 五、扩展点(对外服务) + +其他插件通过 `ctx.betterSidebar` 接入: + +```ts +// 注册一个 tab 类型 +await ctx.betterSidebar.registerTab(descriptor) +// 注册一个文件预览器 +await ctx.betterSidebar.registerFileViewer(viewer) +``` + +- 服务按**能力清单**做前向兼容门控(能力只增不减),消费方应据此判断可用性而非版本号比较。 +- 完整契约、字段表与示例见仓库内 [docs/external-plugin-guide.md](../blob/main/docs/external-plugin-guide.md)。 +- 改服务面时:**只能新增**;删除或改语义需同步更新能力清单、指南与本百科。 + +## 六、上游同步:测量式 re-base(本项目的方法论) + +**不 merge 上游**。上游每个版本差距可达数百文件,直接合并会引入无关变更并放大冲突面; +本项目改用「先测量、再重建、后补增量」: + +### 步骤 + +**① 测出自己的增量(把改名归一化掉)** + +```python +# 对每个变更文件:取出两边 blob,把改名归一化后比较 +old = git('show', f'{base}:{path}'); new = git('show', f'HEAD:{path}') +rename_only = old.replace('dsh-better-sidebar', 'dsh-apes-sidebar') == new +``` +→ 得到三张清单:**仅改名**(自动处理)/ **真改动** / **新增**。数量级参考:v0.19.1→v0.24.1 为 +78 / 11 / 10。 + +**② 确定改名的精确范围(不要重新推断)** + +```bash +# 仍保留上游名字的文件 = 有意保留(本仓库为 README.md、package.json、docs/plans/*) +comm -12 <(git grep -l 'dsh-better-sidebar' <旧基线> | cut -d: -f2 | sort) \ + <(git grep -l 'dsh-better-sidebar' HEAD | cut -d: -f2 | sort) +``` +其余文件只做子串替换 `dsh-better-sidebar` → `dsh-apes-sidebar`;**camelCase 服务名 `betterSidebar` 必须保留**。 + +**③ 造一个"改名归一化基座",让增量与改名解耦** + +```bash +git checkout -B tmp/norm-<旧基线> <旧基线 tag> +# → 套同一口径改名 → commit +git diff tmp/norm-<旧基线> HEAD -- . ':(exclude)pnpm-lock.yaml' > /tmp/apes-delta.patch +# 校验:这条 diff 的文件清单必须恰好等于 ① 的「真改动 + 新增」 +``` + +**④ 在新基线上重建 + 三点合并** + +```bash +git checkout -B sync/<新基线 tag> <新基线 tag> +# → 套同一口径改名 → commit("rename only") +git apply -3 /tmp/apes-delta.patch # 冲突会以 <<<<<<< ours / >>>>>>> theirs 留存(命令本身不失败) +``` + +**⑤ 逐条判读冲突(只有五种形态)** + +| 形态 | 判定 | +| --- | --- | +| 版本/身份 JSON | 取**新基线**侧,再改成本 fork 的版本;两侧各自新增的元数据取**并集**(依赖、files 条目、`forkedFrom` 块) | +| 上游搬走了某个 import | 取上游的新路径,保留本 fork 的新 import | +| 上游新环境里的注册块 | 保留本 fork 的块,放进上游的新上下文 | +| 同一函数两边都定义(上游上移/下移了它) | 删除本 fork 那份;用 `grep -c 'const '` 断言只剩 1 处 | +| 长文档(README) | 整体取上游原文,再机械套上本 fork 的两处增量:顶部 fork notice + 包名替换(**跳过** `github.com/<上游 org>` 与徽章域名行) | + +**⑥ 迁移检查(API 漂移)+ 门禁** + +- 先**解完所有冲突标记再** `pnpm install`(`prepare: tsdown` 会因为语法错误直接失败), + 并**删掉 lockfile 重新生成**(不要把旧 lockfile 带过来)。 +- 逐条跑 `pnpm typecheck` 修类型漂移。经验:漂移通常集中在**本 fork 自己新增的模块**里。 + 上游会在 `context-types.ts` 的注释里写清迁移路径(例:0.1.6 的 `subagentsByParent` + → 0.1.7 的 `projectionsBySession[id].values.subagentCatalog`,`list.current` 被删除),照注释做即可。 +- 最后跑门禁三连,并且**用 `grep` 核对关键标记**(tab id、`srcDoc`、`dsh-desktop-content-viewport` 等)。 + +**⑦ 收尾(纪律)** + +- 安全锚点:`backup/pre-sync-<新基线>` 分支 + `apes-v<版本>` tag,**推送远端**。 +- **不要把新线同步进正在使用的 profile**:peer 范围可能已跨 minor,同步过去只会「静默失效」, + 同时把本机环境弄脏。保持 profile 在已验证的线上。 + +## 七、提交与分支纪律 + +- 提交信息用 `type(scope): 摘要`(`feat` / `fix` / `chore` / `docs` / `refactor`),正文说明**为什么**与验证方式。 +- 一个提交一件事;re-base 类改动拆成「rename only」+「增量移植」两个提交,便于回溯与回滚。 +- 分支命名:`sync/<上游 tag>`(跟随上游)、`backup/pre-sync-<基线>`(锚点)、其余按需。 +- 提交前:门禁三连 + 关键标记 grep + (若涉及部署)提醒 profile 重装与完整重启。 diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md new file mode 100644 index 0000000..07b5063 --- /dev/null +++ b/docs/wiki/Home.md @@ -0,0 +1,82 @@ +# apes-sidebar 项目百科 + +> DeepSeek Harness(DSH)的**服务化右侧边栏工作台** + **apes 信息层**。 +> 本项目是 [`dsh-better-sidebar`](https://github.com/omdsh-dev/DSH-better-sidebar)(MIT)的 fork, +> 在其「文件 / 文件变动 / 任务 / 侧边对话 / 终端 / 浏览器」工作台之上,追加三个信息层标签页: +> **智能体(agents)/ 应用(apps)/ 画像(profile)**。 + +## 一、项目速览 + +| 项 | 值 | +| --- | --- | +| 仓库 | `figmar/apes-sidebar`(公开) | +| 默认分支 | `main` | +| 许可证 | MIT(fork 自 `omdsh-dev/DSH-better-sidebar`) | +| 语言 / 构建 | TypeScript(strict)· `tsdown`(rolldown)· `pnpm` | +| 宿主 | DSH Desktop(Electron 壳)或 DSH Web profile | +| 上游跟随方式 | 不合并上游提交,而是**测量式 re-base**(见 [07-开发与测试](07-开发与测试)) | + +## 二、两条版本线(重要) + +本仓库同时维护两条互相独立、**不能同时挂载**的版本线(两条线都注册 `ctx.betterSidebar`, +cordis entry id 冲突,第二个会被丢弃): + +| 分支 | 版本 | 上游基线 | peer / 宿主要求 | 状态 | +| --- | --- | --- | --- | --- | +| **`main`** | 1.2.0 | `dsh-better-sidebar` **v0.19.1**(`8753096`) | `^0.1.5-rc.1`(DSH **0.1.5-rc.1+**) | 已在本机 DSH Desktop 2.0.10(`dsh 0.1.5-rc.2`)实机运行 | +| **`sync/v0.24.1`** | 1.3.0 | `dsh-better-sidebar` **v0.24.1**(`a2751cf`) | `^0.2.0-rc.1`(DSH **0.2.0-rc.1+**) | 构建与测试全绿,**待具备 0.2.0 宿主实机验证** | + +> ⚠️ **宿主的兼容性预检是「静默禁用」,不报错、不加载、无日志。** +> 宿主用 `semver.satisfies(host, peerRange, { includePrerelease: true })` 判定;caret 范围跨 minor +> 不成立(`^0.1.5-rc.1` 在 `0.2.0` 宿主上为 false),于是整行被跳过。 +> **现象 = 插件"好像没装"**。选线请先看宿主版本,见 [04-部署指南](04-部署指南)。 + +## 三、文档地图 + +| 页面 | 内容 | +| --- | --- | +| [01-需求分析](01-需求分析) | 背景、五条核心需求(含验收标准与状态)、非目标与约束 | +| [02-系统架构](02-系统架构) | host/client 拆分、三层接口面、面板宿主层级契约、关键不变量 | +| [03-功能说明](03-功能说明) | 全部标签页与预览器、信息层数据来源、i18n、主题令牌、安全边界 | +| [04-部署指南](04-部署指南) | 环境矩阵、选线、profile 接线、构建、验收清单、卸载 | +| [05-运维手册](05-运维手册) | 版本线与分支策略、回滚剧本、发布纪律、巡检、备份铁律 | +| [06-故障排查](06-故障排查) | 症状 → 根因 → 处置(含全部实机验证过的案例) | +| [07-开发与测试](07-开发与测试) | 工程约定、目录、门禁、测试体系、扩展点、上游 re-base 方法 | + +## 四、快速开始(TL;DR) + +```bash +# 1) 构建(Node ≥ 20,pnpm) +pnpm install && pnpm typecheck && pnpm build && pnpm test + +# 2) 接到 DSH profile(以桌面 profile 为例,使用 file: 链接依赖) +# 在 ~/.dsh/profiles/desktop/package.json 中: +# "dsh-apes-sidebar": "file:/绝对路径/dsh-apes-sidebar" +# 并在 dsh.profile.bundles 列表里加入 "dsh-apes-sidebar" + +# 3) 让 profile 重新安装依赖,然后 ⌘Q 完整退出 DSH 再启动 +``` + +完整步骤与坑见 [04-部署指南](04-部署指南)。 + +## 五、仓库内的事实源 + +| 位置 | 说明 | +| --- | --- | +| `README.md` | 面向用户的功能与安装说明(主体沿用上游文档,顶部有 fork notice) | +| `AGENTS.md` | 面向 agent/协作者的工程约定(构建、布局、硬性规则、测试约定)⚠️ 注意:本文件当前描述的是**另一套 from-scratch 实现**,与 `main` 的 fork 树不一致,以源码为准 | +| `docs/external-plugin-guide.md` | 插件接入指南:`ctx.betterSidebar.registerTab / registerFileViewer` 的对外契约 | +| `docs/plans/` | 上游历史设计文档(保持上游原样,未随 fork 改名) | +| `docs/wiki/` | **本百科的源文件**(Gitea wiki 页面的可版本化副本,由 `scripts/push-wiki.sh` 推送) | +| `scripts/switch.sh` | 两线切换脚本(`aps` ↔ `upstream`),含 `--status` | + +## 六、谱系说明(如实交代) + +`figmar/apes-sidebar` 这个仓库先后承载过同一产品族的三代实现: + +1. **v1**:早期 JS bundle 版侧边栏插件(仓库描述里提到的「可配置显示名 / 预设 feyOS」即出自那一代); +2. **v2**:from-scratch 重写(`src/index.ts` + `src/client/index.tsx` 结构,五个 `apes:*` tab), + 即 `AGENTS.md` 所描述的那一套,其产物不在当前 `main` 上; +3. **当前线(v0.19.1 / v0.24.1 fork)**:即 `main` 与 `sync/v0.24.1` 上的代码——本百科描述的对象。 + +若需要把本百科改写成描述 v2 重写版,请告知,源文件在 `docs/wiki/` 可直接替换。 diff --git a/scripts/push-wiki.sh b/scripts/push-wiki.sh new file mode 100755 index 0000000..07fc2f3 --- /dev/null +++ b/scripts/push-wiki.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +# ============================================================================= +# push-wiki.sh — 把 docs/wiki/*.md 推送/更新到本仓库的 Gitea wiki +# +# 为什么需要它:本项目的百科源文件**版本化在仓库里**(docs/wiki/), +# Gitea wiki 只是它的发布面。改文档 = 改仓库文件 + 跑本脚本,日志与回溯都在 git 里。 +# +# 行为: +# · 幂等:页面不存在则 POST 新建,已存在则 PATCH 覆盖(标题 = 文件名去掉 .md) +# · 凭据来自 `git credential fill`(与推代码同一套),脚本自身不存任何 token +# · 凭据经临时 netrc 文件传给 curl(600 权限,退出即删),不进入 argv +# +# 用法: +# bash scripts/push-wiki.sh # 推送全部页面 +# bash scripts/push-wiki.sh 05-运维手册 # 只推指定页面(按文件名/片段匹配) +# WIKI_REMOTE=origin bash scripts/push-wiki.sh # 指定 remote(默认 gitea,缺失则 origin) +# ============================================================================= +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +WIKI_DIR="$ROOT/docs/wiki" +FILTER="${1:-}" + +# ---- 解析 remote / owner / repo ------------------------------------------- +pick_remote() { + for r in "${WIKI_REMOTE:-}" gitea origin; do + [ -z "$r" ] && continue + if git remote get-url "$r" >/dev/null 2>&1; then echo "$r"; return 0; fi + done + echo "无法找到可用的 git remote(试过: ${WIKI_REMOTE:-<空>} gitea origin)" >&2 + return 1 +} +REMOTE="$(pick_remote)" +URL="$(git remote get-url "$REMOTE")" +case "$URL" in + https://*) ;; + *) echo "本脚本只支持 https remote(当前: $URL)" >&2; exit 1 ;; +esac +HOST="$(printf '%s' "$URL" | sed -E 's#^https://([^/]+)/.*#\1#')" +SLUG="$(printf '%s' "$URL" | sed -E 's#^https://[^/]+/##; s#\.git$##')" +OWNER="${SLUG%%/*}" +REPO="${SLUG#*/}" +API="https://$HOST/api/v1/repos/$OWNER/$REPO/wiki" + +echo "wiki 目标 : $OWNER/$REPO @ $HOST (remote: $REMOTE)" +echo "源目录 : docs/wiki" +echo + +# ---- 凭据 ----------------------------------------------------------------- +CRED="$(printf 'protocol=https\nhost=%s\n\n' "$HOST" | git credential fill 2>/dev/null || true)" +USER="$(printf '%s\n' "$CRED" | sed -n 's/^username=//p')" +PASS="$(printf '%s\n' "$CRED" | sed -n 's/^password=//p')" +if [ -z "$PASS" ]; then + echo "未从 git credential helper 取到 $HOST 的凭据(先确保 git push 能成功)" >&2 + exit 1 +fi +NETRC="$(mktemp)" +chmod 600 "$NETRC" +trap 'rm -f "$NETRC"' EXIT +printf 'machine %s\nlogin %s\npassword %s\n' "$HOST" "$USER" "$PASS" > "$NETRC" + +json_body() { # $1 = file + python3 - "$1" <<'PY' +import base64, json, sys, pathlib +p = pathlib.Path(sys.argv[1]) +print(json.dumps({ + "title": p.stem, + "content_base64": base64.b64encode(p.read_text(encoding="utf-8").encode("utf-8")).decode("ascii"), +}, ensure_ascii=False)) +PY +} + +api() { # $1 = method, $2 = path, $3 = body(optional) -> 输出 HTTP 码 + local method="$1" path="$2" body="${3:-}" + if [ -n "$body" ]; then + curl -s --netrc-file "$NETRC" -o /tmp/push-wiki-resp.json -w '%{http_code}' \ + -X "$method" -H 'Content-Type: application/json' --data-binary "$body" "$API$path" + else + curl -s --netrc-file "$NETRC" -o /tmp/push-wiki-resp.json -w '%{http_code}' -X "$method" "$API$path" + fi +} + +# ---- 逐页推送 ------------------------------------------------------------- +shopt -s nullglob +files=("$WIKI_DIR"/*.md) +[ ${#files[@]} -gt 0 ] || { echo "docs/wiki 下没有 .md 文件" >&2; exit 1; } + +ok=0; fail=0 +for f in "${files[@]}"; do + title="$(basename "$f" .md)" + if [ -n "$FILTER" ] && [[ "$title" != *"$FILTER"* ]]; then continue; fi + body="$(json_body "$f")" + + code="$(api POST /new "$body")" + if [ "$code" = "201" ]; then + echo " + $title (新建)"; ok=$((ok+1)); continue + fi + enc="$(python3 -c 'import sys,urllib.parse;print(urllib.parse.quote(sys.argv[1], safe=""))' "$title")" + code="$(api PATCH "/page/$enc" "$body")" + if [ "$code" = "200" ]; then + echo " ~ $title (更新)"; ok=$((ok+1)) + else + echo " ! $title 失败(HTTP $code): $(head -c 200 /tmp/push-wiki-resp.json 2>/dev/null)" >&2 + fail=$((fail+1)) + fi +done + +echo +echo "完成:成功 $ok 页,失败 $fail 页" +[ "$fail" -eq 0 ] || exit 1 +echo "查看: https://$HOST/$OWNER/$REPO/wiki"