← Back to home@gh-gongjin

dsh-plugin-modelwatch

DSH (DeepSeek Harness) OpenRouter 模型监控插件:新上模型 / 热门周榜 / 变化流水。全程只读 GET,不推外部通知。

Stars
0
Language
JavaScript
Created
Oct 2, 2026
Updated
Oct 7, 2026
GitHub repo

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)

  1. 编辑 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"
  2. 在 profile 的 node_modules 下建目录联接:

    New-Item -ItemType Junction `
      -Path "$env:USERPROFILE\.dsh\profiles\web\node_modules\dsh-plugin-modelwatch" `
      -Target "<本仓库的绝对路径>"
    
  3. 重启 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.mjs27路由表、回环闸门、SSE、错误码翻译、快照 free 切片与 sources.free 闸门、GET /api/model(命真夹具 / 脏 slug 400 且上游零请求 / 上游坏 502 且不进缓存 / dispose 清缓存 / 读操作不受回环闸 / 出口带 pageUrl)
check.test.mjs21变化判定(diff / created 现算 / 位次阈值)、三源各自独立闸门与翻转记账、单飞与 409
client.test.mjs32页签骨架与顺序、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.mjs11(+1 跳)缺 storageDomain / timer 时加载不抛、状态如实报
models.test.mjs32清单解析与规范化行;详情源:slug 闸门四类放行与逃逸拦截、逐段编码、规范化与四个语义未证字段不抽、BAD_SHAPE 两守卫、空壳端点行丢弃、四档失败折成人话、按需缓存(只存成功 / TTL / 插入序淘汰)、详情页址 pageUrl(按段转义、跟源 id 不跟所点 slug、空 id 不产出)
rankings.test.mjs26SSR:flight 反转义 → 平衡花括号扫描 → queryKey 定位,改版即失败的降级;免费榜源:JSON 筛选/口径回落/取最新日/截断
store.test.mjs16三张表读写语义、裁剪、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/                     见上

挂载时的两个坑(踩过,写在这里省你时间)

  1. 宿主半边不许 import @deepseek-ai/*:link: 挂载会解析出第二份模块实例。本仓库零第三方依赖,schema 与 KV 基座都是自造的最小实现。
  2. 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。

榜单是第三方页面的非官方接口,随时可能因改版失效;插件不会假装数据是实时准确的。