bowenliang123/dsh-context
bowenliang123/dsh-contextDSH 上下文洞察面板:Context 仪表盘 + /context命令 + Context 浏览器,查看 Context的分类组成、内容详情、演进趋势、压缩/注入事件、统计等一站式 Context 全生命周期管理。
在 DSH 侧栏显示 Our Free Model 网关各渠道的剩余额度:一张常驻卡片覆盖网关支持的渠道(未配置的默认隐藏),折叠时显示当前对话所用渠道及其剩余百分比,展开后每个渠道一行含进度条与余量,并可在插件详情页按渠道开关显示。
折叠态 — 显示当前对话使用的渠道及其剩余百分比 |
展开态 — 每个渠道一行:进度条 · 剩余量 · 百分比 |
渠道名 · 百分比。
找不到当前渠道时就退回到剩余最少的那个渠道——那才是最该看的一个。OFM 把十来个渠道(CodeArts、TRAE、Qoder……)挂在同一个网关后面。每个渠道各有自己的配额, 而网关默认只在你主动去查的时候才告诉你。于是很常见的两个问题:
这张卡片把这两件事都放进侧栏,不占对话、不用点击、不打断你。
它同时支持两套 Our Free Model。 「Our Free Model」这个名字下有两个产品:装在 DSH 里的 官方插件,以及独立的桌面应用。它们各自维护账号池, 互不相通。默认读插件——它跑在 DSH 里,账号就是在 DSH 里登录的那些;插件不在时自动回退读桌面应用。 详见 数据来源。
默认只显示「已登录账号」的渠道,其余按目录列出但隐藏,可在设置里手动打开。
判据是数据源自己的账号清单(provider.status → accounts.total > 0),不是 DSH profile 里
声明的模型 id。这一点在 0.7.0 改过,原因是旧判据在两个方向都会错:
账号清单是实时真值,也是 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 层面即可排到最前;两者都不依赖宿主的内部排序实现。宿主若改变布局,面板会退回插槽给的位置,而不会报错。
也可以直接编辑 $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 构建授权这一步。
手动安装:
package.json → dependenciesdsh-ofm-balance 追加到 dsh.profile.bundlespnpm 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键),优先级高于配置项——避免配置项与用户点击互相打架。
全部限制为本机回环访问(非 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,只在宿主进程内使用。
官方插件有意把它的管理接口锁在 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。若已选的数据源后来不可用(应用关了、插件卸了),会自动回退到另一套,而不是把卡片钉在空结果上。
注意两套的账号池互不相通。 卡片「正在使用的渠道」只认当前所读那一套的路由: 读插件时,走桌面网关的对话不会被点亮(反之亦然),因为那两套同名渠道背后是不同的账号, 点亮它会显示另一个账号的数字。
宿主启动时,插件会在凭据服务就绪之前挂载。本插件对此的处理:
结果:打开 DSH 后无需手动刷新,余额会自行出现。
网关没有批量接口,每次探测按渠道并发请求;隐藏的渠道不请求。
gemini 的配额查询需要本机能访问 Google。未开代理时该渠道返回 balance: null 与
配额查询网络失败,插件显示为「不可用」,不影响其它渠道;代理可用后下一轮自动恢复。
网关返回的积分包有两处会算错百分比,都以实测数据修正:
used。同一个 used 会同时出现在聚合包与子包里(codearts 的「总积分包」与
「赠送积分包」都是 349.31),把每个包的 used 相加会把分母从真实的 7500 抬到 7849.31。
判别方式是找到 remaining 等于 balance.total 的那个包(即聚合包),用它自己的 used。active: false(buddy 有 4 个失效裂变包,
total 100 / used 100 / remaining 0)。它们已不属于本周期额度,计入分母会把
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)
测试覆盖渠道分类策略与设置持久化的边界情况(缺失文件、损坏文件、原子写、未知形状容忍), 不依赖网络,也不消耗任何额度。
session/event 的回调是 (session, event),会话参数此前被忽略(写作 _session)。于是同时开两个
会话时,卡片显示的是「最近发过请求的那个会话」的渠道,不是用户正在看的那个——实测卡片显示 buddy
而当前对话在 codearts。
现在按会话记录:session/event 传入的 session.id 逐会话保存渠道;客户端从宿主 sessions 服务的
retainedBy.mainView 读出「主视图会话」,经 POST /view-session 报告给宿主,折叠态只显示该会话的渠道。
该会话尚未发过请求时显示空值,而不是顶替别的会话。客户端拿不到 sessions 服务时回退到旧的全局行为,
不会变成空白(sessions 故意不写进 inject —— Cordis 视之为必需依赖,缺了会导致插件不激活)。
新增 6 个测试(共 102 个)。source 默认值 auto → plugin)。插件跑在 DSH 里,账号就是在
DSH 里登录的那些,读它不需要另开进程,因此首次安装不再询问「用哪一套」。插件不在时仍自动回退读桌面
应用,不会显示空卡片;auto 保留为显式选项(想被问的人可以设)。同时修复由此暴露的一个回归:判据写成
source !== 'auto' 时,默认值变成 plugin 会让用户存盘的选择被跳过,选了桌面应用重启后又读回插件。
现在优先级为「设置里的显式非默认值 → 界面存盘的选择 → 插件默认」。
另外跨数据源不再点亮渠道:两套产品的同名渠道背后是不同账号池,读插件时走桌面网关的对话(反之亦然)
会明确清空高亮,而不是显示另一个账号的数字;切换数据源时也一并清空记住的路由。新增 7 个测试(共 96 个)。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 个测试)。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 主体」这层意义上是同一个契约。sourcePromptSeen),但「关闭」与「选择」是两种状态——关闭不会钉死某个源,之后仍按 uto 自动选可用的一套;提示里注明后续在「插件详情页 → 数据源」切换。单复数单位合并(credit/credits → credits);文案去掉「仅可查看」这类废话。provider.status → accounts.total > 0),不再依据 DSH profile 声明的模型 id。旧判据两头都错:profile 是快照,账号没了还一直声明,卡片堆满读不到数字的行;刚登录的渠道又可能还没写进 profile,真实账号反被隐藏。两套数据源的 provider.status 结构一致,因此一条规则覆盖两边。删除 profile 读取与 60 秒轮询等死代码。127.0.0.1:18900;
装了 DSH 官方插件后,两边的账号池互不相通,卡片显示的是另一套账号。现在会先探测
plugin(官方插件,走 DSH 内核的 channel-pack RPC)与 desktop(桌面应用,走 HTTP),
只探测到一套就直接用,两套都在才询问一次,并可在插件详情页随时切换;已选项失效时自动回退。
两套来源共用同一份归一化与分母修正逻辑。新增 lib/sources.js、lib/plugin-source.js
与 23 个测试(共 56 个)。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,用真实网关返回值锁住这些算法。order,但该槽位是追加到
官方区块之后的,order 只在槽位内部生效、改不了与官方区块的相对位置;改用 CSS order: -1
(详情页正文是 flex column)并附 JS 兜底前插。plugins.detail.section);修复冷启动「缺少凭据」
不自动恢复的问题;渠道名称改由宿主下发;文档重构。