dsh-plugin-modelwatch
DSH (DeepSeek Harness) OpenRouter 模型监控插件:新上模型 / 热门周榜 / 变化流水。全程只读 GET,不推外部通知。
- Stars
- 0
- Language
- JavaScript
- Created
- Oct 2, 2026
- Updated
- Oct 7, 2026
Introduction
dsh-plugin-modelwatch
DSH(deepseek HARNESS)的 OpenRouter 模型监控插件:在侧边栏「插件」下方增加一个入口,盯两件事 —— 哪些模型新上了、本周热门榜怎么变。变化只记录在本插件面板里,不推任何外部通知。
全程只读 GET(每轮三次,点开模型详情时再按需一次):不带 key、不 POST 到 openrouter、不推外部通知。
它是什么
- 新上模型:由清单的
created时间戳直接算近 N 天(首轮建档就有东西可看,不依赖 diff),表格给出 name + slug / 上架时间 / 上下文 / 输入价 / 输出价 - 热门周榜:按 token 用量的周榜,取前
topN(默认 15,可设 5~20),名次用金/银/铜徽标,量级条按榜首归一 —— 纯文本列看不出第 1 名与第 15 名差 20 倍 - 免费榜单:免费变体(
:free)模型的周榜,独立源、独立闸门,默认同样 15 条(真页可排 27 个免费模型) - 变化流水:追加式时间线,事件种类为
new_model / removed_model / top_enter / top_exit / top_move / source_error(中文标签定义在宿主侧lib/domain.js,界面只查表);位次挪动 ≥3 才记top_move,周榜按天滚动更新,日间小幅挪动视为噪声不记 - 模型详情:页面里六处模型名都可点,点开是居中弹框,列该模型的上架时间、上下文、价格与逐条端点(按需向官方清单 API 的
/models/<slug>/endpoints现取,10 分钟内同模型不重复取;不进定时链路、不落 KV) - 设置:监测频率(1 / 6 / 12 / 24 小时)、榜单条数、事件保留条数(默认 500,范围 50~2000),外加一张只读的「运行环境」能力表
它不是什么
- 不是 OpenRouter 官方工具,与 openrouter.ai 无隶属关系;榜单数字是第三方口径的用量估算。
- 不做自动切换模型、不做调用、不做成本告警、不推通知。要通知请用宿主自己的机制,本插件不代作决定。
一个必须先说清的口径
热门周榜的来源是 GET https://openrouter.ai/rankings 的 HTML 页面内嵌 react-query 水合数据,属于非官方接口:解析路径是把 flight 字符串逐段反转义 → 取 {"dehydratedAt"…} 对象 → 找 queryKey 含 "rankings","models" 的那条 → state.data。对方改版即失效。
免费榜单走另一个非官方读接口:GET https://openrouter.ai/api/frontend/v1/rankings/models?view=week(榜单页自己的前端数据接口,返回每模型一行的周汇总)。为什么不从上面那 20 行里筛 —— 真页周榜里只有 1 行是 :free 变体,成不了榜;换源后有 27 个免费模型可排。名次口径 = rankingMetricValue(= prompt+completion 周总量,与 SSR 页逐值核对过);端点行序不是站点名次序,所以免费榜按 token 量自己排,这一点在卡片的来源注里如实写明。热门周榜仍然只读 SSR 那 20 行 —— 那才是站点页面上显示的名次。
因此:
| 情况 | 面板表现 |
|---|---|
| 清单源(官方 API)失败 | 状态卡就地写「清单源不可用:<原因>」,保留上次快照 |
| 榜单源解析失败 | 榜单卡就地写「榜单源结构变化,解析失败」+ 保留上次榜单;卡标题旁常驻小字「来源:榜单页内嵌数据,非官方接口」 |
| 免费榜源失败 | 免费榜卡就地写「本轮榜单源故障:<原因>;下面显示的是上次成功数据」—— 不连坐周榜卡,反之亦然(三源三道独立闸门) |
| 详情源失败 | 弹框内写「详情拉取失败:<原因>」并说明这是按需源、坏了不影响监测;失败不缓存,下次点真重试(监测链路完全不受影响) |
| 任一轮失败 | 原因进事件表 kind:'source_error'(只在坏好的翻转那轮记,连坏不刷屏),让你能回看「哪天开始坏的」 |
源故障但留有旧数据时,表上方就地写「下面显示的是上次成功数据」—— 不把陈旧数据当实时数据念。
安装
方式 A:官方 CLI
dsh plugin --profile web add github:gh-gongjin/dsh-plugin-modelwatch
CLI 内部走 pnpm,需要 pnpm 在 PATH 里。本地目录同样可装:dsh plugin --profile web add <本仓库的绝对路径>。
方式 B:手工两步(不需要 pnpm)
-
编辑 profile 的
package.json(默认~/.dsh/profiles/web/package.json,Windows 为%USERPROFILE%\.dsh\profiles\web\package.json):dependencies增加:"dsh-plugin-modelwatch": "link:<本仓库的绝对路径>"dsh.profile.bundles数组末尾追加"dsh-plugin-modelwatch"
-
在 profile 的
node_modules下建目录联接:New-Item -ItemType Junction ` -Path "$env:USERPROFILE\.dsh\profiles\web\node_modules\dsh-plugin-modelwatch" ` -Target "<本仓库的绝对路径>" -
重启 dsh(宿主半边在启动时加载)。只改
client.js的话刷新页面即可。
停用 / 卸载
临时停用:在 ~/.dsh/profiles/web/cordis.patch.yml 里加
- id: modelwatch
disabled: true
界面
页头 → 页签栏(6) → 当前面板,同一时刻只渲染一个面板。
页头左侧是标题;右侧是数据源状态 pill(「数据源全部在位 / X 故障 / 部分源未就位 / 等待首帧」,悬停 title 落故障原因)+ 检查时刻与模型总数 + 唯一的主操作「立即检查」(跑时禁用并显示「检查中…」)。一个功能只留一个入口,总览页不再重复放这颗按钮。
| 页签 | id | 计数徽标 | 内容 |
|---|---|---|---|
| 总览 | overview | — | 数据源状态 → 本周前三领奖台 → 新上 / 变化各取前几条(默认页) |
| 热门周榜 | top | 榜内行数 | 完整榜单表(# / 模型 / 周 token / 量级 / 较上轮) |
| 免费榜单 | free | 榜内行数 | 与热门周榜共用一张表,数据来自独立的免费榜源,各挂各的故障闸门 |
| 新上模型 | new | 近 N 天条数 | 新上表(5 列) |
| 变化记录 | events | 事件条数 | 时间线,限高内滚 |
| 设置 | settings | — | 三项偏好 + 只读运行环境 |
计数徽标只在有数据时渲染,数字跟着快照走,不写死。相对时间一律以本轮检查时刻为基准(fmtAge(x, snap.at)),不是浏览器当前时刻。
点模型名看详情(v1.12):页面里六处模型名都可点 —— 总览的「本周前三」slug、「近 N 天新上」名称、「最近变化」里带标识的行,新上模型表、热门周榜表、免费榜单表的模型列。点开居中的详情弹框(Esc / 点遮罩 / 「关闭」退出),显示上架日期、最大上下文与输出、模态、分词器、端点数、描述原文,以及逐条端点的上下文 / 最大输出 / 输入输出价 / 近 1 天可用率 / 量化(最多 12 条,其余写「另有 N 个端点未列出」)。
弹框标题本身就是出口(v1.14):标题点开后是这一模型在 OpenRouter 上的详情页(新标签,rel="noopener noreferrer"),旁边一枚 ↗ 独立成格 —— 长模型名被省略号截断时,它是唯一的「点了会离开本页」信号。地址由宿主按源返回的 id 拼好随详情送出(detail.pageUrl),跟的不是所点的榜单 slug(perma-slug 在详情页上是 404);加载中与拉取失败两态没有这个链接,因为那时 pageUrl 还不存在。
「端点」= 这个模型在 OpenRouter 上的一个上游供给(供应商 × 区域 × 部署),各自定价、各自报可用性 —— 源字段是 endpoints,v1.12 曾译作「线路」,用户 2026-10-07 真机截图指出这一列既看不懂又三行同名,故改口并把区分量画进列里:显示名取 provider_name + tag 斜杠后半段(Amazon Bedrock · eu-west-1),短名仍撞车的那几行改带完整 tag,源没给 tag 就只留供应商名(不编区分量);每格 title 保留原始 tag 作出处。列宽 200px 由无头实测钉(真夹具最长一条不截断),两侧同值由 client-31 对账。
详情是按需源:点开才向官方清单 API 的 /models/<slug>/endpoints 取一次,进程内缓存 10 分钟,同模型连点不再打上游;不进定时检查链路、不落 KV。源里没说明语义的字段(状态、时延、吞吐、折扣)一律不显示 —— 不猜。~ 开头的别名标识会写明「指向当前版本、可能查不到公开端点」,与「这个模型没有公开端点」是两句不同的实话。
浮层只有详情这一处(v1 的「零浮层」为此改判):全树 position:fixed 有且只有 .mw-mask 一条规则,由 client-15 在 client.js 与原型两侧各数一遍条数。其余故障与状态说明照旧各占一行落在触发它的那张卡内,toast / 抽屉 / 浮动 tooltip 仍然禁止。
节拍与存储
默认 60 分钟一轮,档位是封闭集合 [60, 360, 720, 1440];宿主 ctx.timer 优先,缺了退 setInterval(unref + dispose 显式清)。手动「立即检查」与定时走同一条代码路径,正在跑时返回 409 CHECK_BUSY。
存储走宿主 KV(domain modelwatch,三张表):
state(key=latest):本轮快照 + 上轮榜单 + 近 30 天新模型行events(key=毫秒+序号):追加式流水,超出keepEvents裁老prefs(key=prefs):{ intervalMin, topN, keepEvents },写入即生效(重挂定时器)
KV 不可用时监控照跑,但状态卡写「存储不可用:本轮变化无法留痕」,事件不落库 —— 不静默假装记录成功。
对外 HTTP 面挂在前缀 /modelwatch 下:GET /api/snapshot、GET /api/stream(SSE,首帧 snapshot、每轮 broadcast update)、POST /api/check、GET /api/events、GET /api/model?slug=(单模型详情,浏览器不直连 openrouter)、GET/POST /api/prefs。回环闸门只拦 POST(本插件对系统零破坏面,GET 留给你从浏览器直接看数据)。详情路由是本插件第一次把外部源的字符串拼进上游 URL,所以 slug 先过白名单闸门(不合法 400 且一次上游都不打),放行后才按段编码拼路径。
测试
零依赖:node:assert/strict + 自研 check(name, fn),node test/<x>.test.mjs 直跑,无测试框架、无 npm script。用例名含「真机」者在 SKIP_LOCAL=1 下跳过并打印 SKIP。
for f in test/*.test.mjs; do node "$f"; done
当前读数(2026-10-07 本机):SKIP_LOCAL=1 165 过 / 0 挂 / 1 跳(跳的是 host-compat 那条只声明口径、不碰本机的真机声明);带联网「真机」用例一起跑 166 过 / 0 挂 / 0 跳。
| 文件 | 项数 | 覆盖 |
|---|---|---|
api.test.mjs | 27 | 路由表、回环闸门、SSE、错误码翻译、快照 free 切片与 sources.free 闸门、GET /api/model(命真夹具 / 脏 slug 400 且上游零请求 / 上游坏 502 且不进缓存 / dispose 清缓存 / 读操作不受回环闸 / 出口带 pageUrl) |
check.test.mjs | 21 | 变化判定(diff / created 现算 / 位次阈值)、三源各自独立闸门与翻转记账、单飞与 409 |
client.test.mjs | 32 | 页签骨架与顺序、tab→组件路由各挂各的源闸门、计数跟快照走、版式契约(含金/银/铜徽标回落、box-sizing 根上必须在场、禁 container-type)、原型同构断言、表头与列值逐列同向(.mw-tbl th 的 text-align:left 特异性压过 .mw-r,必须有 th.mw-r 点名覆盖)、列模板(模型列封顶 340 + 末尾兜底列吃余量 + 1000px 窄屏回退,client 渲染树与原型逐列 deepEqual)、六处模型名都可点、详情弹框三态与封顶/别名/尾注、端点列显示名四步改法(拆 tag 后缀 / 短名撞车回落完整 tag / 不撞名不硬塞 / 空后缀不画,外加判重吃全量与每格 title 留源 tag)、标题外链址=宿主给的 pageUrl 且加载/失败态不画链接、弹框接线钉源码、原型弹框与真组件逐格同构(行、小结、列模板、表头类、端点列宽、标题区 CSS、两个外链属性 七张对账单) |
host-compat.test.mjs | 11(+1 跳) | 缺 storageDomain / timer 时加载不抛、状态如实报 |
models.test.mjs | 32 | 清单解析与规范化行;详情源:slug 闸门四类放行与逃逸拦截、逐段编码、规范化与四个语义未证字段不抽、BAD_SHAPE 两守卫、空壳端点行丢弃、四档失败折成人话、按需缓存(只存成功 / TTL / 插入序淘汰)、详情页址 pageUrl(按段转义、跟源 id 不跟所点 slug、空 id 不产出) |
rankings.test.mjs | 26 | SSR:flight 反转义 → 平衡花括号扫描 → queryKey 定位,改版即失败的降级;免费榜源:JSON 筛选/口径回落/取最新日/截断 |
store.test.mjs | 16 | 三张表读写语义、裁剪、clamp 用宿主常量不在客户端重写、freeOk/freeError 可选字段(旧档照读) |
除用例外的两件验收工具(tmp/ 不入库,脚本按需在本地留存):详情弹框的无头几何与截图核对 tmp/verify-detail.mjs(43 项判据,红 0 项);变异电池 tmp/mut_mw01.mjs(136 条全红、0 漏网,跑完按字节还原 allRestored:true)。
目录结构
.
├── package.json 插件清单(dsh.bundle.patch / dsh.client),零第三方依赖
├── cordis.yml bundle patch:- insert: [{id: modelwatch, name: dsh-plugin-modelwatch}]
├── index.js 宿主半边:能力探测、建服务、注路由、注入浏览器半边所需载荷
├── client.js 浏览器半边:侧边栏入口 + 主面板(React,零构建,CSS 以字符串注入)
├── lib/
│ ├── kv-schema.js 自造记录校验器(宿主只调 parse/safeParse)
│ ├── kv-records-base.js domain 建域 / 写链 / 关停公共基座
│ ├── domain.js 事件 kind 标签、档位与边界常量的唯一来源
│ ├── stores.js state / events / prefs 三张表
│ ├── services/ models(清单)、rankings(榜单解析)
│ ├── check.js 变化判定(宿主单点)
│ ├── caps.js 宿主能力探测
│ └── api.js 路由表 + SSE + 回环闸门
├── docs/design-spec.md 设计规格(含 §9 每轮验收台账与踩坑记录)
├── prototype/index.html 可点原型(与 client.js 同构,改设计先改它)
└── test/ 见上
挂载时的两个坑(踩过,写在这里省你时间)
- 宿主半边不许 import
@deepseek-ai/*:link:挂载会解析出第二份模块实例。本仓库零第三方依赖,schema 与 KV 基座都是自造的最小实现。 client.js里 ModuleLoader factory 必须return module.exports:宿主把 factory 的返回值当模块导出。写成return module;会在宿主启动时报invalid plugin, expect function or object with an "apply" method, received object—— 这个错从 renderer 抛出、转成主进程 crash 日志,排查要去前端产物里找同文案。
另外版式上几条硬约束(由 client.test.mjs 钉住,别改回去):.mw-root 必须自己带 box-sizing:border-box(.mw-root * 盖不到根自己,宿主前端没有任何通配 reset,漏了就整条右边界被裁出可视区);禁用 container-type / @container,断点只用 @media(现役 1080px 管两栏落单栏、1000px 管表格窄屏回退;容器查询在宿主 flex 主区下会把根算成 0 宽,整页塌成竖线);表格列模板 = 模型列封顶 340px + 末尾兜底空列吃余量(table-layout:fixed 里没写宽的自动列会吸走 ~7 成表宽,把数值列推到天边 —— 真机大空隙事故;定宽列必须配 1000px 窄屏回退)。
许可
MIT。见 LICENSE。
榜单是第三方页面的非官方接口,随时可能因改版失效;插件不会假装数据是实时准确的。