# dsh-workbuddy2api-panel 二期需求(M2 / M3) > 一期(已交付)解决的是「能不能用」:嗅探连接、模型同步(倍率 / 国内国际 / 思考档位)、额度与用量展示、容错。 > 本文档写的是「用起来省不省心」和「敢不敢发布」。一期功能说明见 [REQUIREMENTS.md](./REQUIREMENTS.md)。 > **状态(1.0.0)**:M2-1 ~ M2-6、M3-1 / M3-2 / M3-4 均已实现;M3-3(本 DSH 消耗估算) > 经评估不做(插件看不到 DSH 的推理请求)。1.0.0 只做兼容承诺,没有新功能,见 [CHANGELOG.md](../CHANGELOG.md)。 ## 0. 一期遗留与本文档的关系 一期代码里已经落地、本文档不重复的部分: - 差异写入(配置没变不写)、revision 冲突重读重试、连续失败指数退避; - 首次启动从 `providers.workbuddy2api` 迁移地址与 `apiKeyEnv`; - 用量口径说明(面板统计的是整个账号池); - HMR 事务冲突时把同步推迟到下一次 HTTP 请求(`pendingSync`)。 下面每一条都给出**验收标准**,写成可执行的形式,方便直接转成测试用例。 --- ## M2(第二阶段:可信赖) ### M2-1 告警:余额不足 / 积分包到期 / Key 失效 **问题**:现在额度不够、积分包快过期、Key 被换掉,都只在设置页里静默变红,用户不看就不知道。 **需求**: | 触发条件 | 默认阈值 | 可配置 | | --- | --- | --- | | 国内版剩余积分低于阈值 | 总额的 10% 或绝对值 50 积分(取大者) | 是 | | 国际版剩余积分低于阈值 | 同上,独立阈值 | 是 | | 有积分包在 N 天内到期 | 3 天 | 是(1/3/7 天) | | 面板鉴权失败(401/403) | 立即 | 否 | | 面板连续不可达超过 M 分钟 | 10 分钟 | 是 | **告警通道(按优先级降级)**: 1. DSH 客户端若有通知/Toast 槽位 → 弹一次(同一事件去重,24 小时内不重复); 2. 设置页卡片顶部出现一条可关闭的提示条; 3. 状态栏(若 M3-1 落地)图标变红。 **验收标准**: - 把阈值调到高于当前余额,下次轮询后(≤ 2 分钟)设置页出现提示条,且提示文案里带具体数字; - 同一条告警在 24 小时内只出现一次,面板恢复正常后告警自动消失; - Key 失效时提示条文案是「API Key 无效或缺失」,不是原始的 HTTP 状态码。 **待确认**:DSH 客户端插件是否有 Toast / 通知槽位(`dsh-client-ui-primitives` 导出了 `Toast`,但需要确认它在 settings 页面之外是否可用、以及是否有全局通知服务)。若没有,直接走 2、3 两级。 --- ### M2-2 面板下架模型:宽限期而不是立即消失 **问题**:一期是「面板不返回 → 下一轮同步直接删掉」。正在用该模型的会话会立刻报「模型不存在」。 **需求**: - 模型从面板消失后,先标记为 `已下架`,**保留一个宽限期**(默认 24 小时)后真正从配置里移除; - 宽限期内该模型仍然可选、可调用(面板侧可能仍然可用,或返回明确错误); - 设置页模型列表里用灰色 + `已下架` 标签展示,并提供「立即移除」; - 如果被下架的模型正好是 `agent-default-model` 指向的模型,额外提示一次(不自动改默认模型)。 **验收标准**: - 在 mock panel 里移除一个模型,DSH 模型列表在下一轮同步后仍能看到它,且名字带 `已下架`; - 超过宽限期后自动消失; - 点「立即移除」后下一轮同步就不再写入它。 --- ### M2-3 新模型上架策略 **问题**:一期是「默认全勾」,面板上新 10 个模型,选择器里立刻多 10 行,多数是噪音。 **需求**: - 设置项「新模型默认」:`自动勾选`(当前行为)/ `自动不勾选` / `只提示`; - 选 `只提示` 时,模型区顶部显示「面板新增 N 个模型」+ 一键全选; - 无论哪种策略,**都不静默丢模型**:没勾的模型在插件列表里可见,只是不进 DSH 选择器。 **验收标准**: - 切到「自动不勾选」,mock panel 新增一个模型,下一轮同步后 DSH 选择器数量不变,插件列表里多一行且未勾选; - 切回「自动勾选」,同一模型在下一轮同步后出现在 DSH 选择器里。 --- ### M2-4 额度耗尽联动 **需求**: - 某个域(国内/国际)剩余积分 ≤ 0 时,该域模型默认标记为不可用: - 方案 A:从 DSH 配置里移除(选择器里消失); - 方案 B:保留但在显示名后加 `[额度不足]`。 - 默认选 **B**(保留但标注),因为余额可能随时被充值;A 作为可选开关; - 恢复余额后自动还原,不需要用户干预。 **验收标准**: - mock panel 把国际版余额设为 0,下一轮同步后国际版模型名字带 `[额度不足]`,DSH 选择器仍能看到; - 余额恢复后标签消失。 --- ### M2-5 清除插件写入的配置 **问题**:卸载插件后,`providers.workbuddy2api` 会留在配置里,模型列表还在但没人更新。 **需求**: - 设置页底部提供「清除插件写入的配置」按钮,二次确认; - 清除内容:`providers.workbuddy2api` 整条路由(用 `unset` 操作,不是写空对象); - 清除后插件状态回到「未连接」,不再自动写入,直到用户重新配置; - 卸载时**不自动清除**(避免误删用户手写过的配置),只在 README 里提示手动清理。 **验收标准**: - 点清除后 `settings.describe()` 里不再有 `providers.workbuddy2api`,DSH 模型选择器里对应模型消失; - 清除后后台轮询不再产生写入(断言 mutate 调用次数为 0)。 --- ### M2-6 可离线运行的 mock panel **问题**:现在假面板写在 `tests/host.test.mjs` 里,只能跑测试,不能手动点。 **需求**: - 独立脚本 `scripts/mock-panel.mjs`,`node scripts/mock-panel.mjs --port 7863` 启动; - 支持 `--models N`、`--credits x0.05`、`--realm cn|global`、`--fail-auth`、`--empty` 等参数; - 提供 `/panel/api/models|overview|usage|packages` 四个接口,数据可热改(比如 `POST /mock/models/remove`)方便手工验证「下架」「余额归零」这些场景; - 不引入任何依赖。 **验收标准**: - 断网、没有真面板的情况下,`node scripts/mock-panel.mjs` 之后在插件里填 `127.0.0.1:7863` 能完整走通连接 → 同步 → 展示; - 用 `--fail-auth` 启动,插件显示「面板可达,但 API Key 无效或缺失」。 --- ## M3(第三阶段:可发布) ### M3-1 状态栏摘要 **需求**:DSH 状态栏显示 `WB ××× 积分`(国内/国际合计或最小者),点击展开设置页。 **前置**:需确认 DSH 客户端是否有状态栏槽位(`dsh-client-ui-slots` 里是否存在类似 `statusbar.item` 的注册点)。**没有就砍掉这条**,不硬凑。 **验收标准**:状态栏显示的数字与设置页「剩余额度」一致,且不产生额外的面板请求(复用 `/state` 快照)。 --- ### M3-2 多地址与反向代理 **需求**: - 地址列表而不是单个地址:按顺序尝试,选第一个能连通的(例如 `内网 IP` → `Tailscale 域名` → `公网域名`); - 每个地址可以单独设置超时; - 面板挂在子路径(如 `https://nas.example.com/wb/`)时,`baseURL` 由用户显式填写,不由拼接推导; - 连接成功后记住「哪个地址可用」,下次优先用它。 **验收标准**: - 配置两个地址,第一个不通、第二个通 → 插件自动用第二个,设置页显示当前用的是哪个; - 面板地址填 `https://nas.example.com/wb/`,写进 DSH 的 `baseURL` 是 `https://nas.example.com/wb/v1`,不被剥成 `https://nas.example.com/v1`。 --- ### M3-3 本 DSH 的消耗估算 **问题**:面板的用量是整个账号池的,用户想知道「DSH 自己用了多少」。 **需求**: - 插件在本地累计自己发起的请求数(按模型),与面板总量并列展示; - 明确标注这是**插件侧统计**,与面板统计口径不同(面板按积分,插件只知道请求次数和 token); - 若能做到,用面板返回的 `credits_per_1m_tokens` 估算积分消耗,并标注「估算」。 **验收标准**:用 DSH 跑 N 次请求后,设置页显示本 DSH 的请求次数 ≥ N,且与面板总量分开显示。 --- ### M3-4 发布准备 - 去掉所有个人化默认值(举例 IP、默认地址),只保留 `127.0.0.1:7863` / `localhost:7863` 作为候选; - 包名建议 `dsh-workbuddy2api`(比 `dsh-workbuddy-panel` 更容易被搜到),并在 README 里写明与 panel 项目的关系; - README 顶部加一段「适用面板版本」:记录测试通过的 panel 版本(当前 `1.13.0-panel`); - 补 LICENSE 与最小 CI(`node --test`)。 --- ## 开放问题(需要拍板) 1. **告警通道**:DSH 客户端插件到底能不能弹全局通知?不能的话,M2-1 只能退化成「设置页提示条 + 状态栏」。 2. **下架宽限期**:24 小时是否合理?如果面板下架是「永久删除」,宽限期只是让用户有时间换模型,那 1 小时也许够。 3. **额度不足**:默认「标注」还是「隐藏」?标注更保守,隐藏更省事。 4. **本 DSH 消耗统计**:值不值得做?面板侧其实分不出是哪个客户端,只能在插件里自己数。 ## 已实现清单(v0.2.0) | 条目 | 实现位置 | 验收方式 | | --- | --- | --- | | M2-1 告警 | `computeAlerts` / `summarizePackages`(lib/panel.js),设置页顶部提示条 + 24h 去重 | `tests/panel.test.mjs`、`tests/host.test.mjs`「某个域额度为 0 时标注额度不足并产生告警」 | | M2-2 下架宽限 | `reconcileModels` + `retireNow`,默认 24 小时、可改(0–168) | 「模型下架后进宽限期,过期才真删」 | | M2-3 新模型策略 | `newModelPolicy`(select/ignore/notify)+ 新增模型提示条 | 「新模型策略为 ignore 时,后上架的模型默认不勾选」 | | M2-4 额度耗尽联动 | `decorate()` 打 `lowCredits`,显示名加「额度不足」,可关 | 同上第一条 | | M2-5 清除配置 | `clearConfig()` 用 `unset` 移除整条路由并停同步,两次点击确认 | 「清除配置用 unset 移除整条路由并停止同步」 | | M2-6 mock panel | `scripts/mock-panel.mjs`(零依赖,含 `/mock/*` 控制接口) | `node scripts/mock-panel.mjs --port 7863` 手工跑通 | ## 开放问题的决定 1. **告警通道**:不做全局弹窗。DSH 客户端插件没有公开的全局通知服务,`ui-primitives` 的 `Toast` 是页面内的局部组件; 跨页面弹窗要依赖宿主内部实现,不稳。定为「设置页顶部提示条 + 24 小时去重」,状态栏(M3-1)确认槽位后再补。 2. **下架宽限期**:24 小时。理由是面板下架多数是「这个模型不卖了」,但用户可能在跑长任务; 1 小时太短,一周太长。做成可配置(`retireHours`,0–168),想立刻删就设 0 或点「立即移除」。 3. **额度不足**:默认「标注」而不是「隐藏」。余额随时可能充值,隐藏会让用户以为模型被删了; 标注既保留可用性又说明原因。提供开关。 4. **本 DSH 消耗统计**:不做。面板侧根本区分不出请求来自哪个客户端,插件自己只能数「请求次数」, 与面板的「积分」口径不可比,做出来容易误导。 5. **新模型默认策略**:默认保持 `自动勾选`(一期行为),避免老用户升级后模型突然消失; 想要清爽的可以切到 `不勾选` 或 `只提示`。首次导入永远全勾,不受策略影响。 ## 优先级建议 | 优先级 | 条目 | 理由 | | --- | --- | --- | | P0 | M2-5 清除配置、M2-6 mock panel | 前者是卸载后不留垃圾,后者是所有后续验证的地基 | | P1 | M2-2 下架宽限、M2-3 新模型策略 | 直接影响日常使用体验 | | P1 | M2-1 告警(设置页提示条部分) | 不依赖外部槽位,成本低收益高 | | P2 | M2-4 额度耗尽联动 | 边界场景,但发生时体验很差 | | P2 | M3-1 状态栏、M3-2 多地址 | 依赖 DSH 槽位确认 / 使用场景变化 | | P3 | M3-3 消耗估算、M3-4 发布准备 | 锦上添花 | ## 已实现清单(v0.3.0 / M3) > 版本号现状:这些条目随 v0.3.0 落地,v0.4.x 只做徽标显示方式,**v0.5.0 是审计修复版** > (4 条 P0 + 10 条 P1),**v0.5.1 是 P2 收尾**(ETag、告警国际化、Esc/外点关闭), > **v1.0.0 只加兼容承诺**(无功能变化)。详见 `CHANGELOG.md`。 | 条目 | 实现位置 | 验收方式 | | --- | --- | --- | | M3-1 侧边栏徽标 | 客户端 `WorkBuddyFooter`,挂 `sidebar.footer.action`(id `workbuddy-quota`,order 50) | `tests/client.test.mjs`「侧边栏徽标:宽窄两种形态都能渲染」 | | M3-2 多地址 | `parseAddressLines` / `formatAddressLines`(lib/panel.js)、`applyAddresses` / `get timeoutMs` / `parseAddressEntries`(lib/index.js) | `tests/panel.test.mjs`「parseAddressLines…」、`tests/host.test.mjs`「多地址:第一个连不上就自动用第二个」「子路径反代:baseURL 保留 /wb 前缀」「所有地址都连不上时保存地址再报错」 | | M3-4 发布准备 | `LICENSE`、`.github/workflows/ci.yml`、README 顶部「适用面板版本」、去掉个人地址默认值 | `node --test` 在 Node 20/22 下跑通 | ### M3-1 的落点说明 需求里写的是「状态栏」。查了宿主(`/Applications/DeepSeek Harness.app/Contents/Resources/app.asar`) 的插槽清单,**没有 statusbar 这类插槽**,可用且语义最接近的是 `sidebar.footer.action` (侧边栏底部、设置按钮旁边,`kind: list`,owner props 只有 `{ wide }`)。 限制:这个插槽拿不到「打开设置」的句柄(`openSettings` 只在 `settings.launcher` 的 owner props 里, 而那是账号启动器的单选插槽,被宿主占着),所以徽标点开是**就地浮层**,不是跳转。 按需求里「没有就砍掉,不硬凑」的口径,这已经是能拿到的最接近状态栏的位置,且没有伪造能力。 ### M3-2 的实现要点 - 地址写成多行文本,每行 `地址 [超时秒]`,按顺序试连,用第一个通的;全不通时**先存下来再报错**, 用户不用重敲; - 401/403 视为「地址对、Key 不对」,照样选中该地址(让后续同步去报错),否则用户连地址都存不下来; - 子路径反代直接填整条 `https://nas.example.com/wb/`,`normalizeBaseUrl` 只剥 `/v1`、`/panel` 后缀和行尾斜杠,不会把 `/wb` 吃掉;写进 DSH 的是 `https://nas.example.com/wb/v1`; - 记住 `activeUrl`,下次启动优先用它。 ### M3-3 为什么没做 面板侧根本区分不出请求来自哪个客户端;插件也**看不到** DSH 的推理请求(那些请求走 `llm-pi-ai`,不经过插件),只能在插件自己发起的面板 API 调用上计数,跟「积分」口径完全不可比。 做出来只会误导,所以这条明确不做。 ### M3-4 关于包名 需求建议改名 `dsh-workbuddy2api`。查证后**那个名字已经被占**: | 位置 | 名字 | 占用者 | | --- | --- | --- | | npm | `dsh-workbuddy2api` | FireOpalus,v0.6.8,靠本机 WorkBuddy 登录做账号池 | | GitHub | `dsh-workbuddy2api` | tearslee,同名不同项目,托管本地网关进程 | | npm | `dsh-workbuddy` | dushaobindoudou,0.0.1 占名 | | npm | `dsh-connect-workbuddy` | 他人,单账号连接 | 最终改为 **`dsh-workbuddy2api-panel`**(npm 与 GitHub 均无占用,且与上游仓库 `linguo2625469/workbuddy2api-panel` 逐词对应,搜索「workbuddy2api」能命中)。 改名时保持不变的:cordis 条目 id 与插件导出的 `name` 仍是 `workbuddy-panel`, 状态文件仍是 `workbuddy-panel.json`(因此地址、模型选择、策略设置**不随改名丢失**)。 变动的只有包身份:`package.json` 的 `name`、`cordis.patch.yml` 的 `name:`、 profile 的 `dependencies` 与 `dsh.profile.bundles`、`node_modules` 符号链接、客户端 bundle id。