Repository navigation
Releases
v1.0.2
1.0.2 (2026-09-20)
CI 修复:2 条用例依赖运行机器时区 ——
testworkflow 自 2026-09-08 起
6/6 次全失败,根因即此。无生产逻辑改动(仅测试与 CI;lib/index.js与
client.js的改动全部是文件头注释与版本号,无行为变化)。发布后补丁(同日):外部审核发现
assets/DSH-model-router-v1.0.2.zip为过期产物
—— 包在 11:20 打出,而 CHANGELOG 在 14:28 被改写为扁平标准后包未重打,
于是包内 CHANGELOG 自述「904.6 KB / 34 文件 / 顶层单一目录 / 含scripts/」,
与实际内容「251.0 KB / 29 条目 / 扁平 / 不含」自相矛盾(详见下「发布后补丁」节)。
缺陷
2 条用例用硬编码时刻断言时段命中,却没给 Router 指定 timeZone
(缺省 null = 系统时区):
// unit.test.mjs —— 注释写「上海 00:30」,但 config.timeZone 是 null
router._now = new Date('2026-09-04T16:30:00Z');
assert.deepEqual(router.candidates(...), [{ provider: 'q-off', model: 'n-off' }]);2026-09-04T16:30:00Z 在 +8 机器上是本地 00:30(命中谷时窗 ✓),
在 UTC 机器上是本地 16:30(不命中 ✗)→ 同一断言结果相反。
症状极具误导性:本地(+8)全绿,CI(UTC)全红,
且 6 个矩阵 job 全失败(含与本地配置相同的 macos-latest, 22)。
修复
makeRouter()新增可选opts.timeZone(不传则维持原行为null)- 2 条用例显式传入
timeZone: 'Asia/Shanghai'(或new Router({ timeZone })) - CI 的测试步骤显式声明
env: TZ: UTC—— 把「跨时区可复现」钉成持续约束,
而非依赖 runner 镜像的默认时区
多时区实测(6 个时区全部通过):
| TZ | 结果 |
|---|---|
UTC |
✓ |
Asia/Shanghai |
✓ |
Asia/Tokyo |
✓ |
America/New_York |
✓ |
Europe/London |
✓ |
Pacific/Auckland |
✓ |
同时修复:1 条用例在 Windows 上必然失败
修完时区后 CI 变为 5 绿 2 红 —— 仅剩 windows-latest(Node 22/24)。
根因是同一测试里的场景 3:
chmodSync(roDir, 0o555); // 造只读目录
assert.ok(thrown, '只读目录写入应抛错'); // 期望 EACCESWindows 的 chmod 是空操作(无 Unix 权限位语义)→ 目录仍可写 →
writeAtomic 成功 → 断言崩。
修复:把场景 3 拆成独立测试并加 { skip: process.platform === 'win32' },
场景 1/2(跨平台)留在原测试中继续在 Windows 上跑。
刻意跳过而非改成假断言 —— 权限语义只在类 Unix 上可测,保 coverage 诚实。
测试数 290 → 291(拆分后新增 1 条)。
新增 · v1.0.2 安装包(assets/DSH-model-router-v1.0.2.zip)
打 tag 发 Release 时由 release.yml 上传。251.0 KB / 29 条目,
按项目既有打包标准(扁平布局,无顶层目录前缀)。
package.json client.js cordis.patch.yml screenshots.json
README.md README.zh.md CHANGELOG.md LICENSE
lib/(19 个 js,含 lib/wrapper/)
排除:node_modules / .git / test/ / docs/ / scripts/ / assets/ /
SUBMISSION.md / .github/ / 其他 *.zip / .DS_Store / *.bak。
验证(解压后实测,非只看列表):
package.jsonversion =1.0.2- 27 个文件与仓库当前状态逐字节一致
- 不含
node_modules|test/|docs/|scripts/|assets/(计数 = 0) - 20 个 js 文件语法检查通过
- 无
zi??????/XX??????临时文件残留
说明(第一版走了弯路):本版首次打包时按「自包含」思路加入了
scripts/+assets/(904.6 KB / 34 文件),理由是 README「方式二」的
deploy.sh需要 scripts。但用户指出应沿用既有标准 —— 该标准面向
「拷贝进node_modules」的场景,脚本与截图均不需要。已改回标准形态。
「方式二」的脚本按 README 原文取自本仓库,不由 Release 资产提供。
修复 · 仓库内版本声明不统一(7 处)
统一前,同一仓库并存 0.8 / 0.9.5 / 1.0.2 三种版本口径:
| 位置 | 原状 | 现状 |
|---|---|---|
README.md / README.zh.md |
## v0.8 范围声明(含「86 项测试」「五页签」「F 段计划 v0.9.0」) |
## v1.0.2 范围声明(291 项测试 / 8 页签 / F 段已实现) |
README.md / README.zh.md |
## 阶段交付速览(v0.9.10–v0.9.21) |
(v0.9.10–v1.0.2),补 v0.9.20–v1.0.2 三行 |
client.js 头注释 |
插件浏览器半(client)v0.8.0 + 「五页签」 |
v1.0.2 + 「8 页签」(L1/L2/L3 结构按实际重写) |
client.js 页签定义注释 |
「七页签」 | 「八页签」(+ 测试档案) |
README.zh.md 状态接口示例 |
端口 3080 |
3081(与 README.md 及实机一致) |
SUBMISSION.md |
多处 v1.0.1 / 8775a3b / 「lib/index.js 注释已滞后」 |
v1.0.2 / 3ebd7d6 / 「已更新」 |
assets/ |
部署包仅 v0.8.0 | 补 v1.0.2 |
⚠ 未改动:根 commit
43460d6「v0.9.5 实机状态基线锚点」的提交说明。
那是历史事实记录(记录仓库建立时的实机状态),不是版本声明;
改写它需要重写 git 历史,风险远大于收益。
同理CHANGELOG.md中历史版本条目里的「五页签」等表述保持原样。
修复 · release.yml 按 tag 精确选包(原为 assets/*.zip)
原逻辑上传 assets/ 下所有 zip。现在该目录长期存有历史版本的包
(v0.8.0 + v1.0.2),打 v1.0.2 tag 会把 v0.8.0 的包也附上 —— 用户可能下错版本。
files: assets/DSH-model-router-${{ steps.tag.outputs.value }}.ziptag v1.0.2 → 只上传 assets/DSH-model-router-v1.0.2.zip ✓
缺包时不失败(fail_on_unmatched_files: false),仅告警 —— 便于先发版后补包。
更新 · lib/index.js 顶部能力注释(滞后于实现)
原注释写「能力(v0.2.0)」且有多处与实际不符:
| 原注释 | 实际 |
|---|---|
| 能力(v0.2.0) | v1.0.2 |
rules/propose 支持面板热更新 |
propose 已是 v0.9.1 前的遗留兼容字段,新客户端不再提交、不参与路由 |
commit-on-first-chunk |
v0.6.0 已改为 commit-on-substantive |
详见 lib/wrapper.js |
实现已迁至 lib/wrapper/index.js(wrapper.js 仅 20 行 re-export 薄桥) |
补齐 v0.9/v1.0 的能力:纯选择驱动、每日报告、模型测试与档案、峰谷段场景、
窗口限额与重置规则、free-tier 节流、8 页签面板。
「四种候选扩展策略」经核实仍成立,保留。
同时:CI 失败可诊断化
job 日志需仓库 admin 权限才能通过 API 下载
(GET .../actions/jobs/{id}/logs → 403 Must have admin rights),
在无权限环境下无法定位失败。现改为:测试失败时把「测试名 + 错误详情」
发成 GitHub annotation(::error::),公共仓库的 check-run annotations
可匿名读取(GET .../check-runs/{id}/annotations)。
同时:CI 增强
concurrency—— 同分支新推送取消旧运行(tag 运行不取消),省 CI 分钟timeout-minutes—— 防挂死任务烧分钟workflow_dispatch—— 手动触发便于排查changelog-guardjob —— 校验「package.json 版本须在 CHANGELOG 有对应段落」。
release.yml依赖 CHANGELOG 段落,缺段会在打完 tag 后才失败;提前到 PR 阶段拦截。npm test由硬编码文件名改为 globtest/*.test.mjs
—— 此前新增测试文件会静默漏跑;manual-suite-automated.mjs(需实机实例)
由命名约定自然排除。
排查过程中的弯路(如实记录)
首轮误判为「CI 缺 @deepseek-ai/dsh-llm(peerDependency 不在公共源)」,
据此加了 test/stubs/dsh-llm/ 测试替身 + CI 安装步骤。
该诊断是错的:我的复现脚本跳过了 CI 的 npm install 步骤,
所以复现出的是个假故障。实际 npm install 能正常从公共 npm 源装上
@deepseek-ai/dsh-llm@0.0.1-rc.1(38 个导出)。
教训:复现 CI 失败时,必须完整复现 CI 的每一步(含依赖安装),
否则会对着假故障修出真复杂度。替身已删除。
发布后补丁 · 产物与源码不一致(外部审核发现,同日修复)
缺陷:assets/DSH-model-router-v1.0.2.zip 是过期产物 —— 包在 11:20 打出,
CHANGELOG 在 14:28 被改写为扁平标准后包未重打。于是包内 CHANGELOG 自述:
| 包内 CHANGELOG 自述 | 包的实际内容 |
|---|---|
| 904.6 KB / 34 文件 | 251.0 KB / 29 条目 |
「顶层单一目录 DSH-model-router-v1.0.2/」 |
扁平(package.json 在根) |
「含 scripts/」 |
不含 |
「含 assets/(2 张截图)」 |
不含 |
下载者读包内 CHANGELOG 会以为「应该有 34 个文件、应该有 scripts」,实际没有 →
怀疑下载损坏或版本错误,产生无效排查。
为什么能静默通过发布:release.yml 只校验「assets/ 里的包存在」,
不校验「内容来自本 tag 的源码」;且 fail_on_unmatched_files: false
让「包缺失」也只告警不失败。
修复(三层):
- 重打并覆盖:
assets/DSH-model-router-v1.0.2.zip按当前标准重建;
Release 上的资产同步替换。 - 打包逻辑收进
scripts/pack.sh(单一来源)—— 人工与 CI 共用同一份,
避免「人工打包 / CHANGELOG 规格描述 / CI 上传」三处独立维护再次分叉。
脚本内含自检:版本一致、扁平、不含排除目录、条目名全 ASCII。 release.yml加硬门禁:checkout(已带ref: tag)后用当次源码重建包,
与assets/里那个做条目级 + 内容级比对,不一致即::error::+ fail。
原则:assets/只作留档,Release 资产必须来自当次源码。
验证:用修复前的过期包试跑门禁 → 条目级与内容级均报红(差异正是上述规格矛盾),
证明该门禁确实能拦住此类缺陷。
门禁自身的假阳性(2026-09-21 首次上线即踩到,共 2 处):
- 日期列 = mtime —— 条目级比对最初用
unzip -l的大小 + 日期 + 名称三列,
其中日期列是源文件 mtime:CI 检出时全变成检出时刻,本地打包用编辑时刻,必然不一致。
→ 已去掉日期列,只比「未压缩大小 + 条目名」。 - 条目顺序 = 文件系统顺序 ——
zip -r lib按文件系统顺序写入条目
(macOS/APFS 与 Linux/ext4 不同),不排序则diff必然不一致。
→ 已在比对前sort。
两次都导致 tag v1.0.2 的发布被误判为「条目清单不一致」而失败。
修正后双向验证:① mtime 不同 / 条目顺序不同但内容相同 → pass;
② 注入一处内容差异 → fail(未丧失判别力)。
与本项目审核报告 §7.2 记录的两处「校验器假阳性」同源:未经校验的校验器会制造假缺陷,
比不校验更危险。判据必须避开与被测对象无关的变量 —— 此处是 mtime 与条目顺序。
发布后补丁 · 上架材料与安装指引(外部审核发现,同日修复)
SUBMISSION.md测试计数过期:写「290/290(单测 277)」→ 实测 291(单测 278 + 契约 13)。
根因是一致性检查清单未覆盖SUBMISSION.md(已补进 skill §I 第 ⑧ 项)。- README 安装指引:
README.md/README.zh.md的「方式一:插件市场」原先
被列为推荐,但 npm 包尚未发布、awesome-dsh-plugin条目尚未提交 ——
按指引操作的用户第一步就会失败。现标注「⚠️ 上架中,暂不可用」并把
「方式二:本仓库脚本」提为当前推荐。
v0.8.0
0.8.0 (2026-09-07)
- G1 每日 API 报告:日账本 NDJSON 按天追加 + 每日凌晨 1 点调度生成昨日报告(可配
reports.hour)- 稳定性评级 S/A/B/C/D/N/A;按供应商×模型聚合(calls/失败/切换/token/时延/错误码分布)
- 三渠道交付:文件(
~/.deepseek-harness/home/model-router-reports/)+ 面板「每日报告」页签 + API - 接口:
GET /reports、GET /reports?l1=1(面板轻量摘要)、GET /reports?day=YYYY-MM-DD、POST /reports/generate - 默认
reports.enabled=false,零记账零调度,独立可回退
- G2 面板信息分级:五页签三级结构(概览摘要卡 → 明细页签 → 工具折叠区)
- 概览 5 张摘要卡:健康总览 / 今日运行 / 昨日报告 / 熔断速览 / 规则速览
- 轮询分级:概览态 5s 轮询
?l1=1轻量接口,明细页才全量拉取
- G3 Windows 适配:
deploy.ps1/undeploy.ps1(PS5.1 兼容、无 BOM UTF-8、id: model-router幂等)+ CI 三平台 matrix - A-1:
failoverSignals基于实际 10 项末尾追加INVALID_REQUEST(DSH httpErrorCode(400, 非 quota/context)),严禁整段替换 - A-2:新增
providerMeta配置域(quotaGroup/tier,自家配置域不跨插件读 settings);route hop 内联优先 - A-3:
maxRetries自动调优——未显式声明时回填max(quotaGroupCount×2, 5);显式声明(含显式 2)一律尊重 - B-1/B-2:按需负载测试
lib/loadtest.js(4-phase:probe / rpm / context / quota-group),复用 probe 原语- 端点
GET/POST/DELETE /api/model-router/loadtest;默认只测 free tier;rpm 触发限流后自动等待恢复 - 所有探针请求带 PROBE_MARK 直透——跑完 cooldown/metrics/quota/daily 零污染
- 端点
- C-1:
router.candidates()按quotaGroup去重(同组保留链序第一个) - C-2:registry
registeredPairs()/metaSnapshot()携带quotaGroup/tier元数据 - 附带修复:
wrapper.jssessionId 未赋值 bug;透传路径 try/finally 兜底记账;statusversion去硬编码 - 测试:86 项全过(基线 61 + 新增 25:A-2/A-3/C-1/C-2 8 项、G1 12 项、B-1/B-2 5 项)