# dsh-workbuddy-bridge 架构说明 本文是插件结构与契约的唯一权威文档:为什么这样分层、哪些行为是兼容性契约、浏览器半边依赖官方的哪些接口。使用方式见 [README](../README.md);`docs/` 另有一份活层的发版手册,以及 `archive/` 里按日期归档的一次性调查记录,都不承担结构说明职责。 ## 总览 插件分为两半,编译为两个独立产物,运行在两个进程里: ```text src/ ├── index.ts 宿主入口:两个变体的组装与生命周期 ├── variants.ts 变体描述符(宿主半,含文件系统路径) ├── credential/ 凭据:读取桌面 App 登录文件、自留副本、静态加密 ├── catalog/ 模型目录:上游抓取、降级链、模型显隐 ├── protocol/ 上游协议:chat / 目录 / 账单 / 错误分类 / 身份 ├── llm/ 接入 DSH:adapter、shim(loopback 端点)、附件 ├── probe/ 推理档位探测:发请求、存档、服务判定 ├── web/ 宿主暴露的 HTTP 路由:status / probe 控制路由 / 心跳 ├── shared/paths.ts 两半共用的路由路径与文档类型 └── client/ 浏览器半边:设置页、两张卡片、composer 控件 ``` 两半之间唯一的桥是 `shared/paths.ts` 里的路由路径和 JSON 文档形状。浏览器半边**永远拿不到**文件路径、凭据、平台事实——变体的浏览器可见面是 `src/client/variants.ts` 里的独立描述符,不是宿主的 `variants.ts`。 ## 宿主半(src/index.ts) 两个产品变体(`workbuddy` 国内版、`workbuddy-ai` 国际版)由同一工厂组装,只差 `WorkBuddyVariant` 描述符:各自的凭据存储、目录、上游客户端、shim、探测状态、路由。任何一个变体启动失败都不影响另一个注册——只装一个 App 的用户只看到那一个分组。 **目录降级链**(每个变体独立):`live`(本次抓取)→ `saved`(该账号上次成功目录,重启可恢复)→ `fallback`(编译进插件的名单)。凭据消失时分组隐藏,账号切换时按身份键(`uid:enterpriseId`)切换各自的数据。 **启动分三相,顺序是契约**:先读凭据并采纳身份 → 再注册 provider → 最后抓目录。第二相必须晚于第一相:注册时 `catalog.current()` 已经有内容,所以 **provider 一旦出现在 `listProviders()` 里就已经带着模型**。反过来(先注册再揭示,且揭示是 fire-and-forget)会留一个空窗——provider 可见但 `resolveModelInfo` 对它的每个模型都答 `UNKNOWN_MODEL`;picker 里看不出来,但一个已选中该模型的会话(恢复的历史会话、或 `agent-default-model`)首条消息就失败。任一相失败都回退到「以隐藏态注册」,随后由轮询揭示,即与今天相同的降级。判据见 [决策记录](../.agents/notes/2026-10-02-startup-window-fix-direction.md),守卫在 `tests/catalog-lifecycle.spec.ts`。 **轮询**:30 秒一次凭据存在性检查(`DSH_WORKBUDDY_POLL_MS` 可覆盖,仅测试/诊断用,不是产品设置)。同身份的令牌轮换不触发目录重抓;失败的抓取按退避重试,成功落地后停止。 ## 浏览器半(src/client) 官方契约按宿主声明走,没有宿主版本探测、没有防御性包装: | 契约 | 用法 | |---|---| | `plugins.bundle.config` | 本插件在插件页的配置入口,key = 包名 | | `conversation.input.right` | 输入区紧凑控件行的推理等级控件 + 剩余积分标签。**必须选 `kind: 'list'` 的槽**——`conversation.input.model` 是 `single`,已被宿主自带的 `ModelSelect` 占用,同 priority 再注册会抛错并顶掉宿主的模型选择器(判据见 [PUBLISHING.md](PUBLISHING.md) 的「平台契约的取真源方式」,经过见 [.agents/notes/2026-09-25-composer-seat-single-occupancy.md](../.agents/notes/2026-09-25-composer-seat-single-occupancy.md)) | | `conversation.chat.assistant-actions` | 已定稿助手回复的操作行(宿主 Like/Dislike 也在这一行)里的「共消耗 X」标签。`kind: 'list'`、`scope: 'session'`,owner currency 是 `{ messageId }`,由 `dsh-client-ui-chat` 声明 | | `ctx.configForms.whileServed` | 宿主真正在服务本条目时才注册页面——不可写的部署上不会出现一个存不了的表单 | | `SettingsForm` / `SettingsFormModel` | 与官方设置页同一套表单栈,一次 revision 封装的 mutation 保存全部字段 | | `ctx.locale` | `settings.workbuddy` 命名空间,zh/en 双语 | | 官方 primitives | 卡片壳 `DisclosureRow` + `StateDot`,面板用 `SegmentedTabs` / `Tag` / `Checkbox` / `Button` / `Tooltip`,插件不自带任何手搓控件 | | CSS Modules | `*.module.css` 经 lightningcss 编译为 `[hash]_[local]` 类名并注入 `