预览
折叠态 — 显示当前对话使用的渠道及其剩余百分比 |
展开态 — 每个渠道一行:进度条 · 剩余量 · 百分比 |
- 折叠态:标题右侧显示当前对话使用的渠道及其剩余百分比,格式
渠道名 · 百分比。 找不到当前渠道时就退回到剩余最少的那个渠道——那才是最该看的一个。 - 展开态:每个渠道一行,含进度条、剩余量与百分比;数据来自网关的真实配额接口。
- 刷新:点击「刷新」立即探测;页面可见时按周期自动轮询。
它解决什么问题
OFM 把十来个渠道(CodeArts、TRAE、Qoder……)挂在同一个网关后面。每个渠道各有自己的配额, 而网关默认只在你主动去查的时候才告诉你。于是很常见的两个问题:
- 「我刚才是走哪个渠道花的额度?」——折叠态直接回答。
- 「哪个渠道快用完了?」——按剩余量排序,一眼扫过去。
这张卡片把这两件事都放进侧栏,不占对话、不用点击、不打断你。
它同时支持两套 Our Free Model。 「Our Free Model」这个名字下有两个产品:装在 DSH 里的 官方插件,以及独立的桌面应用。它们各自维护账号池, 互不相通。默认读插件——它跑在 DSH 里,账号就是在 DSH 里登录的那些;插件不在时自动回退读桌面应用。 详见 数据来源。
显示哪些渠道
默认只显示「已登录账号」的渠道,其余按目录列出但隐藏,可在设置里手动打开。
判据是数据源自己的账号清单(provider.status → accounts.total > 0),不是 DSH profile 里
声明的模型 id。这一点在 0.7.0 改过,原因是旧判据在两个方向都会错:
- profile 是快照:账号早就没了,它还一直声明那个渠道,卡片于是堆满永远读不到数字的行;
- 刚登录的渠道可能还没写进 profile,真实账号反而被藏起来。
账号清单是实时真值,也是 credits.balances 唯一肯回答的前提。两个产品(插件 / 桌面应用)
的 provider.status 返回结构完全一致,因此一条规则覆盖两边。
| 来源 | 数据出处 | 含义 |
|---|---|---|
| 已登录 | 数据源 provider.status → accounts.total > 0 |
你在这个渠道登录了账号 |
| 目录 | 插件内置的渠道清单(与网关控制台一致,当前 13 个) | 该产品支持哪些渠道 |
| 已提供 | GET /v1/models → data[].channel |
此刻有模型可用 |
| 可读取 | credits.balances 返回了数字 |
此刻余额读得到 |
目录不是封闭的。 如果数据源提供了目录里没有的渠道,它依然会被列出来并使用标题化的显示名。 上游新增渠道后,本插件无需更新即可工作——这一点有单元测试守着:
a freshly logged-in channel outside the catalogue still earns a row。
默认规则
| 情况 | 默认行为 |
|---|---|
| 已登录账号 | 显示。即使此刻读不到余额,也保留该行并说明原因 |
| 目录中有、但没登录账号 | 隐藏。可手动打开,用于盯一个以后可能会登录的渠道 |
| 已登录但读不到 | 显示,并如实标注(如「网关未提供该渠道」) |
| 渠道未登录账号 | 查询成功但返回空账号列表,显示 「未在该渠道登录账号」 |
| 无账号但手动打开 | 显示,并提示「尚未登录该渠道的账号,仅供查看」 |
未登录账号的渠道会如实标注,而不是报错或留空——查得到和查不到是两件事,卡片把它们分开说。
如何调整(图形界面)
在 DSH 的 插件 页打开本插件(dsh-ofm-balance)详情页,正文顶部的 数据源 与
渠道显示 区块即控制面板。渠道分成两组:
- 已登录账号 — 数据源里确实有账号的渠道
- 其余渠道(未登录账号) — 目录中有、但还没有账号的
每个渠道一行,包含:
- 左侧复选框控制是否显示
- 渠道名与一行说明(已登录 / 未登录账号 / 当前未提供)
- 右侧状态标签
勾选未登录的渠道时会弹窗确认——那种渠道没有账号可读,现在不会有任何数字。
改动即时生效并写入 $DSH_HOME/dsh-ofm-balance/settings.json。
为什么不放在宿主自动生成的配置表单里:DSH 会从插件的
Configschema 自动生成设置页, 但它只渲染标量字段;array类型的渠道列表在那里渲染不出可用的复选框,也无法在 「显示未配置渠道」时给出确认对话。所以本插件自带面板。
为什么这个区块能排在「来源信息」之上
面板通过 plugins.detail.section 插槽注册,而该插槽是追加在官方页面自己的区块
(来源信息、当前版本、更新)之后的——注册时给的 order 只在槽位内部排序,改不了与官方区块的相对位置。
所以位置由两件事共同保证:
- 详情页正文是
flex-direction: column,面板根节点设order: -1,CSS 层面即可排到最前; - 渲染回调里再做一次保守的前插(仅当父容器确实是 flex column 时才动),作为 CSS 不可用时的兜底。
两者都不依赖宿主的内部排序实现。宿主若改变布局,面板会退回插槽给的位置,而不会报错。
如何调整(配置文件)
也可以直接编辑 $DSH_HOME/dsh-ofm-balance/settings.json:
{
"version": 1,
"hidden": ["qoder"],
"revealed": ["moonshot"]
}
| 字段 | 作用 |
|---|---|
hidden |
要隐藏的已登录账号渠道 |
revealed |
要额外显示的无账号渠道 |
source |
记住的数据源选择(plugin / desktop) |
删除该文件即恢复默认(已登录的全显示、无账号的全隐藏)。被隐藏的渠道不参与探测,因此不浪费请求。
颜色
进度条与百分比按剩余比例连续渐变——不是已用比例,也不是分档跳变。
色相经过实测调整。第一版把色相线性铺满整段,结果在健康区间显示一片看不出差别的绿色 (80% → 100% 只跨 23°)。现在改用锚点,把高位推向青色:
| 剩余 | 色相 | 观感 |
|---|---|---|
| 100% | 178° | 青 |
| 90% | 147° | 春绿 |
| 80% | 123° | 绿 |
| 60% | 78° | 黄绿 |
| 40% | 48° | 琥珀 |
| 20% | 25° | 橙 |
| 0% | 0° | 红 |
饱和度与亮度也随数值变化,使整条刻度读起来是一条连续量表,而非色轮。 语义不可翻转:100% 永远是健康的青,0% 永远是红。
折叠态摘要里,渠道名保持默认文字色,只有百分比着色——两者都上色会让名字看起来像状态标签,与数字争夺注意力。
安装
dsh plugin --profile <profile> add github:Shen-18/dsh-ofm-balance
也可以使用仓库地址或本地路径:
dsh plugin --profile <profile> add https://github.com/Shen-18/dsh-ofm-balance
dsh plugin --profile <profile> add link:/绝对路径/dsh-ofm-balance
本机 profile 名不一定是
web,请替换成自己的(例如桌面端常见的是desktop)。
预构建安装包(免构建授权)
每个版本都附带一个预构建的安装包,供插件市场直接提供:
https://github.com/Shen-18/dsh-ofm-balance/releases/latest/download/dsh-ofm-balance.tgz
该文件名不含版本号,因此 latest/download/ 在每次发版后仍然有效,不会像
「资产名带版本号」那样在下一个版本发布时静默 404。从该 tgz 安装时不需要源码构建,
因此也免去 allowBuilds 构建授权这一步。
手动安装:
- 把本目录加入 profile 的
package.json→dependencies - 把包名
dsh-ofm-balance追加到dsh.profile.bundles - 在 profile 目录执行
pnpm install - 重启宿主
宿主半区改动需重启;客户端半区改动刷新页面即可。
配置
宿主侧的部署级配置(在 profile 补丁或插件配置里):
| 键 | 默认 | 说明 |
|---|---|---|
enabled |
true |
总开关;关闭后不探测、不注册路由 |
source |
plugin |
数据源:plugin 读 DSH 插件(默认主源),desktop 读桌面应用,auto 自动判定并在两套都在时询问 |
baseUrl |
http://127.0.0.1:18900 |
桌面应用的网关地址(source=plugin 时不用) |
credentialRoute |
ourfreemodel |
凭据与配置所属的 provider 路由名 |
pollIntervalSec |
60 |
轮询间隔(30–3600 秒) |
timeoutMs |
20000 |
单渠道超时。冷启动实测约 10.5 秒,不宜调低 |
hiddenChannels |
[] |
(初始值)隐藏的已登录渠道 |
revealedChannels |
[] |
(初始值)额外显示的无账号渠道 |
渠道开关的实际值存在
$DSH_HOME/dsh-ofm-balance/settings.json,由插件详情页的面板维护。 配置里的hiddenChannels/revealedChannels只作为首次运行的初始值。 数据源的实际选择也存在同一个文件里(source键),优先级高于配置项——避免配置项与用户点击互相打架。
HTTP 接口
全部限制为本机回环访问(非 127.0.0.1 / ::1 一律 403)。API Key 由宿主解析,不会下发到浏览器。
| 方法 | 路径 | 用途 |
|---|---|---|
GET |
/api/dsh-ofm-balance/overview |
当前快照与探测报告 |
POST |
/api/dsh-ofm-balance/refresh |
立即探测并返回新快照 |
GET |
/api/dsh-ofm-balance/channels |
渠道分类结果(含显示名称),供设置面板使用 |
POST |
/api/dsh-ofm-balance/visibility |
保存渠道开关,请求体 { hidden, revealed } |
GET |
/api/dsh-ofm-balance/sources |
数据源探测结果、当前选择、是否需要询问 |
POST |
/api/dsh-ofm-balance/sources |
切换数据源,请求体 { source: "plugin" | "desktop" } |
POST |
/api/dsh-ofm-balance/view-session |
报告侧栏正在显示哪个会话,请求体 { sessionId };用于让折叠态显示该会话的渠道 |
数据来源
有两套互不相通的 Our Free Model,名字相同、账号各自独立,本插件会先探测再决定读哪一套:
| 数据源 | 是什么 | 怎么读 |
|---|---|---|
plugin |
安装在 DSH 里的 dsh-our-free-model 插件 |
内核 RPC 通道 channel-pack,方法 credits.balances |
desktop |
独立的桌面应用(127.0.0.1:18900) |
POST {baseUrl}/api/management/channels/rpc |
桌面应用还用它自己的接口发现渠道:GET {baseUrl}/v1/models → data[].channel;桌面路径的凭据取自宿主凭据服务的
OURFREEMODEL_API_KEY,只在宿主进程内使用。
为什么插件源必须走内核 RPC
官方插件有意把它的管理接口锁在 DSH 内核里:/api/our-free-model/* 与 /api/channel-pack 都要经过
connection.admit() 的浏览器令牌校验。实测从外部调用(包括回环地址、匹配的 Origin、以及从凭据库里取出
browser-session 密钥)一律返回 401。
但它把同一套接口注册成了进程内的 Fetch 路由,因此同一个进程里的插件可以合法调用——本插件的宿主半身
正是这样读取的:取 connection 服务、用共享的 owner Peer、向 /api/channel-pack 投递内核信封。
两套来源返回的积分结构完全相同,因此共用同一份归一化与分母修正逻辑,不存在两套算法漂移的问题。
首次运行的询问规则
默认读插件(plugin)——它跑在 DSH 里,账号就是在 DSH 里登录的那些,读它不需要另开进程。
因此正常情况下不会问你:
- 插件在 → 直接读插件,不打扰;
- 插件不在、桌面应用在 → 自动回退读桌面应用(不会显示空卡片);
- 两套都在但你想自己选 → 把
source设成auto,首次运行时会问一次,之后记住; - 在插件详情页可随时切换,选择写入
settings.json。
若已选的数据源后来不可用(应用关了、插件卸了),会自动回退到另一套,而不是把卡片钉在空结果上。
注意两套的账号池互不相通。 卡片「正在使用的渠道」只认当前所读那一套的路由: 读插件时,走桌面网关的对话不会被点亮(反之亦然),因为那两套同名渠道背后是不同的账号, 点亮它会显示另一个账号的数字。
冷启动行为
宿主启动时,插件会在凭据服务就绪之前挂载。本插件对此的处理:
- 探测拿不到凭据时不缓存失败结果,而是把渠道标记为「读取中」,1 秒后自动重试;
- 连续 3 次仍拿不到,才判定为真的未配置,并在卡片上给出可操作的提示(告诉你该配置哪个环境变量);
- 网关的首次冷探测约需 10.5 秒,因此首次请求会立即返回带渠道名的占位行,数字随后填入——不会出现空白卡片。
结果:打开 DSH 后无需手动刷新,余额会自行出现。
已知限制
网关没有批量接口,每次探测按渠道并发请求;隐藏的渠道不请求。
gemini的配额查询需要本机能访问 Google。未开代理时该渠道返回balance: null与配额查询网络失败,插件显示为「不可用」,不影响其它渠道;代理可用后下一轮自动恢复。网关返回的积分包有两处会算错百分比,都以实测数据修正:
- 重叠的
used。同一个used会同时出现在聚合包与子包里(codearts 的「总积分包」与 「赠送积分包」都是 349.31),把每个包的used相加会把分母从真实的 7500 抬到 7849.31。 判别方式是找到remaining等于balance.total的那个包(即聚合包),用它自己的used。 - 失效的包。网关把已过期的包标为
active: false(buddy 有 4 个失效裂变包,total100 /used100 /remaining0)。它们已不属于本周期额度,计入分母会把 4539 抬到 4939。
两者叠加后,codearts 从 91.1% 修正为 95.3%,buddy 从 78.9% 修正为 85.8%。 当网关完全没有返回积分包明细、或返回值形状无法解释时,插件不显示百分比—— 没有可信的分母就不该编造一个数字(尤其是 100%,那会把「已用掉大半」显示成「从未使用」)。
- 重叠的
未在某个渠道登录账号时,余额接口仍可查询,只是返回空账号列表;插件如实标注,不算失败。
设计说明
| 决策 | 原因 |
|---|---|
| 宿主 / 浏览器半区分离 | API Key 只在宿主进程内使用,浏览器只见归一化后的快照 |
Promise.allSettled 扇出 |
单个渠道慢或失败不能拖垮其它渠道 |
| 缓存优先 + 预热态 | 冷启动约 10.5 秒;首次请求立即返回占位行,避免首屏空白 |
| 凭据缺失时不缓存结果 | 冷启动时凭据服务晚于插件就绪,缓存「没有 key」会把一次瞬时状态变成永久错误 |
分类规则抽到 lib/channels.js |
纯函数、无 I/O,是策略所在,也最值得测试 |
渠道开关走 plugins.detail.section |
宿主自动表单渲染不了数组字段,也给不出「未配置渠道」的确认对话 |
| 开关存自己的 JSON 而非 profile 补丁 | 插件由 bundle 补丁挂载,profile 里没有它的行可改;写自己的文件不会破坏用户配置 |
| 显示名称由宿主下发 | 插件在提供未知渠道的网关部署上也能给出正确名称与标题化兜底 |
文件结构
| 文件 | 职责 |
|---|---|
lib/gateway.js |
网关协议:发现渠道、查询余额、归一化 |
lib/channels.js |
渠道分类与可见性策略(纯函数) |
lib/service.js |
缓存、轮询、凭据就绪重试、构建快照 |
lib/patch.js |
渠道开关的持久化(原子写:先写临时文件再 rename) |
lib/index.js |
宿主挂载、路由、配置、会话事件 |
lib/client.js |
侧栏卡片 + 插件详情页的渠道开关面板 |
零运行时依赖。@deepseek-ai/* 全部由宿主提供,不作为 dependencies 声明。
支持的渠道
插件内置的目录(与网关控制台一致):
| id | 名称 | 所属 |
|---|---|---|
codearts |
CodeArts | 华为云 |
buddy |
WorkBuddy 国内版 | 腾讯 |
workbuddy |
WorkBuddy 国际版 | 腾讯 |
lobsterai |
LobsterAI | 有道 |
qoder |
Qoder | 阿里系 |
qodercn |
Qoder 中国版 | 阿里系 |
trae |
TRAE | 字节 |
cline |
Cline | — |
loomy |
Loomy | — |
raccoon |
Raccoon | — |
minimax |
MiniMax Code | — |
zcode |
ZCode | — |
gemini |
Gemini |
这份目录是下限而非过滤条件:未登录账号的渠道会如实显示,目录之外的渠道也会照常出现。
开发
npm run check # 语法检查(node --check)
npm test # 单测:渠道分类 + 设置持久化(node --test)
测试覆盖渠道分类策略与设置持久化的边界情况(缺失文件、损坏文件、原子写、未知形状容忍), 不依赖网络,也不消耗任何额度。
版本
- 0.8.4 — 修复折叠态显示的是别的会话的渠道。侧栏卡片是所有会话共用的一个元素,而路由记忆只有一个
全局值:
session/event的回调是(session, event),会话参数此前被忽略(写作_session)。于是同时开两个 会话时,卡片显示的是「最近发过请求的那个会话」的渠道,不是用户正在看的那个——实测卡片显示buddy而当前对话在codearts。 现在按会话记录:session/event传入的session.id逐会话保存渠道;客户端从宿主sessions服务的retainedBy.mainView读出「主视图会话」,经POST /view-session报告给宿主,折叠态只显示该会话的渠道。 该会话尚未发过请求时显示空值,而不是顶替别的会话。客户端拿不到sessions服务时回退到旧的全局行为, 不会变成空白(sessions故意不写进inject—— Cordis 视之为必需依赖,缺了会导致插件不激活)。 新增 6 个测试(共 102 个)。 - 0.8.3 — 插件成为默认主数据源(
source默认值auto→plugin)。插件跑在 DSH 里,账号就是在 DSH 里登录的那些,读它不需要另开进程,因此首次安装不再询问「用哪一套」。插件不在时仍自动回退读桌面 应用,不会显示空卡片;auto保留为显式选项(想被问的人可以设)。同时修复由此暴露的一个回归:判据写成source !== 'auto'时,默认值变成plugin会让用户存盘的选择被跳过,选了桌面应用重启后又读回插件。 现在优先级为「设置里的显式非默认值 → 界面存盘的选择 → 插件默认」。 另外跨数据源不再点亮渠道:两套产品的同名渠道背后是不同账号池,读插件时走桌面网关的对话(反之亦然) 会明确清空高亮,而不是显示另一个账号的数字;切换数据源时也一并清空记住的路由。新增 7 个测试(共 96 个)。 - 0.8.2 — 修复折叠态在插件数据源下认错渠道。两套产品上报路由的形状不同:桌面应用是
provider: ourfreemodel+model: buddy/deepseek-v4.1-flash(渠道在 model 前缀里),而官方 插件为每个渠道注册独立的 LLM provider 路由,provider: workbuddy+model: deepseek-v4.1-flash(model 里没有渠道前缀)。旧代码只按 model 前缀取,在插件模式下取到deepseek-v4.1-flash、匹配不到任何行,currentChannel恒为 undefined,折叠态于是回退到 「快用完的那个渠道」,与当前对话不符。现在 provider 本身是已知渠道时优先取 provider(仅限plugin源,避免用户自配的同名 provider 点亮渠道行);插件自有的our-free-model免费通道 明确清空而不沿用上一个渠道;其余仍按 model 前缀取。新增lib/route.test.js(8 个测试)。 - 0.8.1 — 修复 DSH 0.2.0 上读不到插件余额:
connection服务里「owner Peer」的字段名在 DSH 0.2.1 从operator改成了browserPeer,而本插件只读新名字,于是在 0.2.0 上恒定抛connection service has no browser peer,整个plugin数据源被判定为不可用,卡片落到active: none(「未检测到可用的 Our Free Model」)。现在两个名字都接受,并新增 5 个测试钉住 两套内核形状。engines.dsh由>=0.2.1-rc.2放宽为>=0.2.0-rc.2,与实际支持范围一致。 两版内核的 Peer 并不等价:0.2.0 的fetch(request)只收一个参数、全文没有principal概念,路由无法因 Peer 而 403;0.2.1 的fetch(request, peer)会检查peer.principal.kind,因此两者只在「字段名指向同一个 owner 主体」这层意义上是同一个契约。 - 0.8.0 — 数据源询问改为只问一次:关闭提示也算已回答(写入
sourcePromptSeen),但「关闭」与「选择」是两种状态——关闭不会钉死某个源,之后仍按 uto 自动选可用的一套;提示里注明后续在「插件详情页 → 数据源」切换。单复数单位合并(credit/credits → credits);文案去掉「仅可查看」这类废话。 - 0.7.0 — 渠道显示判据改为**「是否已登录账号」**(
provider.status→accounts.total > 0),不再依据 DSH profile 声明的模型 id。旧判据两头都错:profile 是快照,账号没了还一直声明,卡片堆满读不到数字的行;刚登录的渠道又可能还没写进 profile,真实账号反被隐藏。两套数据源的 provider.status 结构一致,因此一条规则覆盖两边。删除 profile 读取与 60 秒轮询等死代码。 - 0.6.0 — 支持两套 Our Free Model 数据源。此前卡片固定读桌面应用的
127.0.0.1:18900; 装了 DSH 官方插件后,两边的账号池互不相通,卡片显示的是另一套账号。现在会先探测plugin(官方插件,走 DSH 内核的channel-packRPC)与desktop(桌面应用,走 HTTP), 只探测到一套就直接用,两套都在才询问一次,并可在插件详情页随时切换;已选项失效时自动回退。 两套来源共用同一份归一化与分母修正逻辑。新增lib/sources.js、lib/plugin-source.js与 23 个测试(共 56 个)。 - 0.5.1 — 修正百分比分母的两处错误。①同一个
used会同时出现在聚合包与子包里 (codearts 的「总积分包」与「赠送积分包」都是 349.31),相加会把分母算大;改为找出remaining等于balance.total的聚合包、用它自己的used。②已过期的active:false包不属于本周期额度(buddy 有 4 个),计入分母会把 4539 抬到 4939。 codearts 由 91.1% 修正为 95.3%,buddy 由 78.9% 修正为 85.8%。网关未返回积分包明细时 不显示百分比,而不是编造 100%。新增lib/gateway.test.js,用真实网关返回值锁住这些算法。 - 0.5.0 — 列出网关支持的全部渠道(而不是只有当前有模型路由的几个),通过内置目录补齐; 已登录账号的渠道正常查余额,未登录的如实标注。已配置渠道的识别增加文件回退 (某些宿主不向插件暴露路由设置命名空间,此前会把所有渠道误判为「未配置」)。 设置面板改为分组布局,行高与说明文字放宽。
- 0.4.3 — 设置区块真正移到「来源信息」之上。上一版依赖注册的
order,但该槽位是追加到 官方区块之后的,order只在槽位内部生效、改不了与官方区块的相对位置;改用 CSSorder: -1(详情页正文是 flex column)并附 JS 兜底前插。 - 0.4.2 — 侧栏卡片改为完全透明(侧栏本身即透明,任何底色都会显出「贴上去的框」), 仅在悬停时给出极淡反馈;圆角收到 8px。
- 0.4.1 — 侧栏卡片去掉边框、背景改用侧栏同色系,降低视觉突兀感。
- 0.4.0 — 插件详情页新增渠道开关面板(
plugins.detail.section);修复冷启动「缺少凭据」 不自动恢复的问题;渠道名称改由宿主下发;文档重构。 - 0.3.0 — 支持网关提供的目录外渠道。
- 0.2.0 — 折叠态改为「渠道名 · 剩余百分比」(仅百分比着色);颜色改为连续渐变;折叠态显示当前对话渠道。
- 0.1.0 — 首个版本。
No comments yet. Be the first to write one.