@dsh-plugins/dsh-llm-hub 1.5.6 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.en.md +4 -2
- package/README.md +21 -11
- package/lib/client.js +142 -101
- package/package.json +1 -1
- package/scripts/gate-lib.mjs +88 -0
- package/scripts/prepublish-gate.mjs +22 -38
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
本项目遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/)。
|
|
4
4
|
|
|
5
|
+
## [1.6.1] - 2026-09-28
|
|
6
|
+
|
|
7
|
+
### 🧹 去掉模型页底部的「热门大模型一键装配」,改为添加时按厂商匹配
|
|
8
|
+
|
|
9
|
+
- 删掉页脚常驻的预设抽屉:它和用户正在配哪一家无关,读起来像一块广告
|
|
10
|
+
- 改为在「添加提供方」里**选中某一家时**,草稿卡上只给匹配到的那一家一行提示:名称 · 推荐模型 · 获取 Key ↗;没匹配到的厂商不显示任何东西
|
|
11
|
+
- 不再提供 YAML 模板:草稿卡本身就是配置表单,而模板根键(`minimax`)与目录 id(`minimax-cn`)对不上,复制过去反而误导
|
|
12
|
+
- 修复:添加提供方时,只是选了下拉框就出现红字「llm-pi-ai 段里没有 "xxx" 这个 provider」。草稿态不再渲染原卡内容
|
|
13
|
+
|
|
14
|
+
## [1.6.0] - 2026-09-28
|
|
15
|
+
|
|
16
|
+
### ✨ 智能路由:组可排序 + 折叠摘要
|
|
17
|
+
|
|
18
|
+
- 路由组(公司 / 个人 …)新增 ↑↓ 上下移。组的先后决定智能路由先用哪个池子,此前只能手改 `settings.yaml`
|
|
19
|
+
- 折叠态直接显示「主力模型 + N 条兜底」,不用展开也知道这一组会按什么顺序试;空组显示「还没加候选」
|
|
20
|
+
- 序号与上下移按钮在折叠态也可用;首组 ↑ / 末组 ↓ 自动禁用
|
|
21
|
+
|
|
5
22
|
## [1.5.6] - 2026-09-27
|
|
6
23
|
|
|
7
24
|
### 📛 包装升级:首屏补齐居中标题 + 接入 README 门禁
|
|
@@ -27,6 +44,20 @@
|
|
|
27
44
|
|
|
28
45
|
测试新增 1 条(`pickSmartRoute:未探测(unknown)且候选里 → 仍接管`),全 132 项过。
|
|
29
46
|
|
|
47
|
+
### 🛣️ 智能路由:`routableStateFor` 把"探测表里没记录"细分成 unknown / unavailable
|
|
48
|
+
|
|
49
|
+
之前一刀切返回 `unavailable`,导致**冷启动 / 用户刚加 provider / 探针刚清空**时被路由跳过,
|
|
50
|
+
即便 settings 里有配、key 也填了,照样不让 smart 接管。典型踩坑:zai-coding-cn 这类
|
|
51
|
+
无 baseURL 的 provider,第一次请求时被 `routableStateFor` 当 unavailable 跳过去。
|
|
52
|
+
|
|
53
|
+
新语义(`lib/index.js:2554-2580`):
|
|
54
|
+
|
|
55
|
+
- 探测表里有 verdict → 用 verdict.state(available / unknown / unavailable)
|
|
56
|
+
- 探测表里没有 + settings 里**有**配 → `unknown`(routable,请求撞真上游,撞完探针补 verdict)
|
|
57
|
+
- 探测表里没有 + settings 里**没**配 → `unavailable`(真不认识,撞 NO_ADAPTER,fail-open)
|
|
58
|
+
|
|
59
|
+
测试新增 1 条(`pickSmartRoute:未探测(unknown)且候选里 → 仍接管`),全 132 项过。
|
|
60
|
+
|
|
30
61
|
## [1.5.5] - 2026-09-27
|
|
31
62
|
|
|
32
63
|
### 🪧 路由卡:每条候选的能力 chip(思考 + 多模态)
|
package/README.en.md
CHANGED
|
@@ -27,11 +27,13 @@ On DSH's Models page, the official adapters leave half the job undone. This plug
|
|
|
27
27
|
|
|
28
28
|
## Install
|
|
29
29
|
|
|
30
|
+
> Only Node.js is required — **no global dsh install**; upstream's official entry is `npx @deepseek-ai/dsh`. If dsh is installed globally, use `dsh` instead.
|
|
31
|
+
|
|
30
32
|
```sh
|
|
31
|
-
dsh plugin --profile web add @dsh-plugins/dsh-llm-hub
|
|
33
|
+
npx @deepseek-ai/dsh plugin --profile web add @dsh-plugins/dsh-llm-hub
|
|
32
34
|
```
|
|
33
35
|
|
|
34
|
-
Add `@dsh-plugins/dsh-llm-hub` to `dsh.profile.bundles` in `~/.dsh/profiles/web/package.json`, then
|
|
36
|
+
Add `@dsh-plugins/dsh-llm-hub` to `dsh.profile.bundles` in `~/.dsh/profiles/web/package.json`, then restart DSH (stop it and rerun `npx @deepseek-ai/dsh web`).
|
|
35
37
|
Open **Settings → Models** — a new row appears under the provider cards.
|
|
36
38
|
|
|
37
39
|
<sub>A boot-graph change requires a restart; hot reload won't pick it up. `cordis.patch.yml` is inserted automatically by the bundle mechanism.</sub>
|
package/README.md
CHANGED
|
@@ -45,12 +45,14 @@ DSH 的模型页上,官方适配器有一半事情没做。这个插件把它
|
|
|
45
45
|
|
|
46
46
|
## 装
|
|
47
47
|
|
|
48
|
+
> 前提:装好 Node.js 即可,**无需全局安装 dsh** —— 官方入口就是 `npx @deepseek-ai/dsh`。已全局安装的可把它换成 `dsh`。
|
|
49
|
+
|
|
48
50
|
```sh
|
|
49
|
-
dsh plugin --profile web add @dsh-plugins/dsh-llm-hub
|
|
51
|
+
npx @deepseek-ai/dsh plugin --profile web add @dsh-plugins/dsh-llm-hub
|
|
50
52
|
```
|
|
51
53
|
|
|
52
54
|
再把 `@dsh-plugins/dsh-llm-hub` 加进 `~/.dsh/profiles/web/package.json` 的 `dsh.profile.bundles` 数组,
|
|
53
|
-
|
|
55
|
+
然后重启 DSH(结束当前进程后重新运行 `npx @deepseek-ai/dsh web`)。打开**设置 → 模型**,provider 卡片下方会多出一行。
|
|
54
56
|
|
|
55
57
|
<sub>boot graph 变了必须重启,热载不生效;`cordis.patch.yml` 由 bundle 机制自动 insert。</sub>
|
|
56
58
|
|
|
@@ -82,7 +84,7 @@ dsh plugin --profile web add @dsh-plugins/dsh-llm-hub
|
|
|
82
84
|
升级:
|
|
83
85
|
|
|
84
86
|
```sh
|
|
85
|
-
dsh plugin --profile web update @dsh-plugins/dsh-llm-hub@^1.5.1
|
|
87
|
+
npx @deepseek-ai/dsh plugin --profile web update @dsh-plugins/dsh-llm-hub@^1.5.1
|
|
86
88
|
# 重启 DSH
|
|
87
89
|
```
|
|
88
90
|
|
|
@@ -484,16 +486,24 @@ publish。Release 的存在性单独探测,所以「npm 发成功但 Release
|
|
|
484
486
|
`name` 完全一致**,否则 DSH 拒绝注册。React 由 `factory(require)` 提供,不打包进产物。
|
|
485
487
|
当前体量无需构建步骤;若将来拆多文件,再加 esbuild(`format: 'iife'`,React 等标 external)。
|
|
486
488
|
|
|
487
|
-
## 🧩
|
|
489
|
+
## 🧩 DSH 插件全家桶
|
|
490
|
+
|
|
491
|
+
<p align="center">
|
|
492
|
+
<img src="https://img.webkubor.online/projects/dsh-plugins/dsh-plugins-family.png" alt="DSH 插件全家桶:主题美化 / 模型管理 / 用户记忆 / 电脑环境" width="100%" />
|
|
493
|
+
</p>
|
|
494
|
+
|
|
495
|
+
<p align="center">
|
|
496
|
+
<a href="https://github.com/webkubor/dsh-bloom-theme">🎨 主题美化</a> ·
|
|
497
|
+
<a href="https://github.com/webkubor/dsh-llm-hub">⚡ 模型管理</a> ·
|
|
498
|
+
<a href="https://github.com/webkubor/dsh-mirror">🪞 用户记忆</a> ·
|
|
499
|
+
<a href="https://github.com/webkubor/dsh-env-inspector">🖥️ 电脑环境</a>
|
|
500
|
+
</p>
|
|
488
501
|
|
|
489
|
-
|
|
502
|
+
一行装齐(只需 Node.js),装完重启 DSH 即可:
|
|
490
503
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
| [⚡ **dsh-llm-hub**](https://github.com/webkubor/dsh-llm-hub) | 智能路由 | 多模型厂商聚合、秒级切换与故障智能重试 | `dsh plugin install @dsh-plugins/dsh-llm-hub` |
|
|
495
|
-
| [🪞 **dsh-user-mirror**](https://github.com/webkubor/dsh-mirror) | 角色记忆 | 用户数字画像、习惯偏好与记忆沉淀网络 | `dsh plugin install @dsh-plugins/dsh-user-mirror` |
|
|
496
|
-
| [🖥️ **dsh-env-inspector**](https://github.com/webkubor/dsh-env-inspector) | 运行环境 | 活跃端口一键释放、CLI 工具链与开发凭据大屏 | `dsh plugin install @dsh-plugins/dsh-env-inspector` |
|
|
504
|
+
```bash
|
|
505
|
+
npx -y @deepseek-ai/dsh plugin --profile web add dsh-bloom-theme @dsh-plugins/dsh-llm-hub @dsh-plugins/dsh-user-mirror @dsh-plugins/dsh-env-inspector && node -e 'const f=(process.env.DSH_HOME||require("os").homedir()+"/.dsh")+"/profiles/web/package.json",p=require(f),b=p.dsh.profile.bundles;for(const n of Object.keys(p.dependencies))if(/^(dsh-bloom-theme|@dsh-plugins\/)/.test(n)&&!b.includes(n))b.push(n);require("fs").writeFileSync(f,JSON.stringify(p,null,2)+"\n")'
|
|
506
|
+
```
|
|
497
507
|
|
|
498
508
|
---
|
|
499
509
|
|
package/lib/client.js
CHANGED
|
@@ -192,6 +192,8 @@ window.__ModuleLoader__.load({
|
|
|
192
192
|
routeDeleteGroup: '删除该组',
|
|
193
193
|
routeUp: '上移',
|
|
194
194
|
routeDown: '下移',
|
|
195
|
+
routeNoCandidate: '(还没加候选)',
|
|
196
|
+
routeFallbackCount: '条兜底',
|
|
195
197
|
routeRemove: '移除',
|
|
196
198
|
routeGroupLabelPlaceholder: '组名(公司 / 个人)',
|
|
197
199
|
routeActiveTag: '当前组',
|
|
@@ -211,10 +213,6 @@ window.__ModuleLoader__.load({
|
|
|
211
213
|
heroFastest: '最优延迟',
|
|
212
214
|
heroSecurity: '凭据防护',
|
|
213
215
|
heroSecSafe: '零明文脱敏中',
|
|
214
|
-
presetsTitle: '⚡ 热门大模型一键装配',
|
|
215
|
-
presetsSub: '点击主流服务商快速复制 YAML 配置模板与直达控制台',
|
|
216
|
-
presetsCopied: '已复制配置片段',
|
|
217
|
-
presetsCopyBtn: '复制 YAML',
|
|
218
216
|
presetsDocsBtn: '获取 Key ↗',
|
|
219
217
|
usageTitle: '本月用量',
|
|
220
218
|
usageCalls: '次调用',
|
|
@@ -344,6 +342,8 @@ window.__ModuleLoader__.load({
|
|
|
344
342
|
routeDeleteGroup: 'Delete group',
|
|
345
343
|
routeUp: 'Move up',
|
|
346
344
|
routeDown: 'Move down',
|
|
345
|
+
routeNoCandidate: '(no candidates yet)',
|
|
346
|
+
routeFallbackCount: 'fallbacks',
|
|
347
347
|
routeRemove: 'Remove',
|
|
348
348
|
routeGroupLabelPlaceholder: 'Group name (work / personal)',
|
|
349
349
|
routeActiveTag: 'active',
|
|
@@ -363,10 +363,6 @@ window.__ModuleLoader__.load({
|
|
|
363
363
|
heroFastest: 'Fastest ping',
|
|
364
364
|
heroSecurity: 'Key security',
|
|
365
365
|
heroSecSafe: 'Masked in memory',
|
|
366
|
-
presetsTitle: '⚡ Popular LLM Quick Presets',
|
|
367
|
-
presetsSub: 'One-click YAML config snippets and official key links',
|
|
368
|
-
presetsCopied: 'YAML snippet copied',
|
|
369
|
-
presetsCopyBtn: 'Copy YAML',
|
|
370
366
|
presetsDocsBtn: 'Get Key ↗',
|
|
371
367
|
usageTitle: 'This month',
|
|
372
368
|
usageCalls: 'calls',
|
|
@@ -613,7 +609,7 @@ window.__ModuleLoader__.load({
|
|
|
613
609
|
'border:.5px solid var(--dsw-alias-state-success-primary);',
|
|
614
610
|
'color:var(--dsw-alias-state-success-primary)}',
|
|
615
611
|
'.dsh-llm-hub-routing__suggestion{color:var(--dsw-alias-state-warn-primary);padding-top:2px}',
|
|
616
|
-
'.dsh-llm-hub-routing__reason{color:var(--dsw-alias-
|
|
612
|
+
'.dsh-llm-hub-routing__reason{font-size:10.5px;line-height:1.5;color:var(--dsw-alias-label-tertiary);padding-top:2px}',
|
|
617
613
|
// composer 快切 chip(conversation.input.left)
|
|
618
614
|
'.dsh-llm-hub-route{position:relative;display:inline-flex}',
|
|
619
615
|
'.dsh-llm-hub-route__btn{box-sizing:border-box;display:inline-flex;align-items:center;gap:4px;height:22px;',
|
|
@@ -629,8 +625,9 @@ window.__ModuleLoader__.load({
|
|
|
629
625
|
'background:var(--dsw-alias-bg-layer-1,rgba(15,15,15,.96));backdrop-filter:blur(14px);',
|
|
630
626
|
'box-shadow:0 8px 24px rgba(0,0,0,.28);display:flex;flex-direction:column;gap:8px;font-size:11px}',
|
|
631
627
|
'.dsh-llm-hub-route__modes{display:flex;gap:4px}',
|
|
632
|
-
'.dsh-llm-hub-route__mode{flex:
|
|
633
|
-
'background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;font:inherit;font-size:10.5px;
|
|
628
|
+
'.dsh-llm-hub-route__mode{flex:0 0 auto;padding:4px 9px;border-radius:5px;border:.5px solid var(--dsw-alias-border-l1);',
|
|
629
|
+
'background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;font:inherit;font-size:10.5px;',
|
|
630
|
+
'transition:all .15s ease;white-space:nowrap;line-height:1.5}',
|
|
634
631
|
'.dsh-llm-hub-route__mode.is-on{border-color:var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-state-warn-primary);',
|
|
635
632
|
'background:var(--dsw-alias-bg-layer-2)}',
|
|
636
633
|
'.dsh-llm-hub-route__group{display:flex;flex-direction:column;gap:3px;padding:6px 7px;border-radius:6px;',
|
|
@@ -655,9 +652,18 @@ window.__ModuleLoader__.load({
|
|
|
655
652
|
'.dsh-llm-hub-routing__group.is-active{border-color:var(--dsw-alias-state-warn-primary)}',
|
|
656
653
|
'.dsh-llm-hub-routing__group-top{display:flex;align-items:center;gap:6px;flex-wrap:wrap}',
|
|
657
654
|
'.dsh-llm-hub-routing__group-toggle{border:0;background:0 0;padding:0;cursor:pointer;font:inherit;font-size:11px;color:var(--dsw-alias-label-secondary)}',
|
|
658
|
-
'.dsh-llm-hub-routing__group-
|
|
655
|
+
'.dsh-llm-hub-routing__group-order{display:flex;align-items:center;gap:2px;flex:none}',
|
|
656
|
+
'.dsh-llm-hub-routing__group-move{border:0;background:0 0;padding:0 2px;cursor:pointer;font:inherit;font-size:10px;',
|
|
657
|
+
'color:var(--dsw-alias-label-tertiary);line-height:1}',
|
|
658
|
+
'.dsh-llm-hub-routing__group-move:hover:not(:disabled){color:var(--dsw-alias-label-primary)}',
|
|
659
|
+
'.dsh-llm-hub-routing__group-move:disabled{opacity:.25;cursor:default}',
|
|
660
|
+
'.dsh-llm-hub-routing__group-state b{font-weight:600;color:var(--dsw-alias-label-primary)}',
|
|
661
|
+
'.dsh-llm-hub-routing__group-more{color:var(--dsw-alias-label-tertiary)}',
|
|
662
|
+
'.dsh-llm-hub-routing__group-would{color:var(--dsw-alias-state-warn-primary);margin-left:5px}',
|
|
663
|
+
'.dsh-llm-hub-routing__group-empty{font-style:normal;color:var(--dsw-alias-label-tertiary)}',
|
|
664
|
+
'.dsh-llm-hub-routing__group-label{flex:0 0 128px;width:128px;box-sizing:border-box;height:22px;padding:0 6px;border-radius:4px;',
|
|
659
665
|
'border:.5px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;font-size:11px}',
|
|
660
|
-
'.dsh-llm-hub-routing__group-state{font-size:10.5px;color:var(--dsw-alias-label-secondary);
|
|
666
|
+
'.dsh-llm-hub-routing__group-state{flex:1;min-width:0;font-size:10.5px;color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
661
667
|
'.dsh-llm-hub-routing__candidates{display:flex;flex-direction:column;gap:3px;padding-top:2px}',
|
|
662
668
|
'.dsh-llm-hub-routing__add{display:flex;align-items:center;gap:5px;flex-wrap:wrap;font-size:10.5px;color:var(--dsw-alias-label-secondary);padding-top:4px}',
|
|
663
669
|
'.dsh-llm-hub-routing__select{box-sizing:border-box;height:22px;padding:0 4px;border-radius:4px;border:.5px solid var(--dsw-alias-border-l1);',
|
|
@@ -671,17 +677,12 @@ window.__ModuleLoader__.load({
|
|
|
671
677
|
'.dsh-llm-hub-hero__card{display:flex;flex-direction:column;gap:2px;padding:8px 10px;border-radius:6px;border:.5px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2,rgba(255,255,255,.02));backdrop-filter:blur(6px)}',
|
|
672
678
|
'.dsh-llm-hub-hero__num{font-size:15px;font-weight:700;color:var(--dsw-alias-label-primary);font-family:ui-monospace,SFMono-Regular,monospace;letter-spacing:-0.3px;line-height:1.2}',
|
|
673
679
|
'.dsh-llm-hub-hero__label{font-size:10.5px;color:var(--dsw-alias-label-secondary);opacity:.85}',
|
|
674
|
-
//
|
|
675
|
-
'.dsh-llm-hub-
|
|
676
|
-
'
|
|
677
|
-
'.dsh-llm-hub-
|
|
678
|
-
'.dsh-llm-hub-
|
|
679
|
-
'.dsh-llm-hub-
|
|
680
|
-
'.dsh-llm-hub-presets__item:hover{border-color:var(--dsw-alias-brand-primary,#3b82f6);transform:translateY(-1px);background:var(--dsw-alias-bg-layer-3,rgba(255,255,255,.05))}',
|
|
681
|
-
'.dsh-llm-hub-presets__name{font-weight:600;font-size:11.5px;color:var(--dsw-alias-label-primary)}',
|
|
682
|
-
'.dsh-llm-hub-presets__desc{font-size:10px;color:var(--dsw-alias-label-secondary);opacity:.75;line-height:1.3}',
|
|
683
|
-
'.dsh-llm-hub-presets__detail{margin-top:6px;padding:8px;border-radius:4px;background:var(--dsw-alias-bg-layer-3,rgba(0,0,0,.2));border:.5px solid var(--dsw-alias-border-l1);font-size:11px}',
|
|
684
|
-
'.dsh-llm-hub-presets__code{font-family:ui-monospace,SFMono-Regular,monospace;font-size:10px;white-space:pre-wrap;color:var(--dsw-alias-label-secondary);margin:4px 0}',
|
|
680
|
+
// 添加提供方草稿卡上的匹配提示(1.6.0)
|
|
681
|
+
'.dsh-llm-hub-preset-hint{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 8px;margin:0 0 8px;padding:6px 10px;border-radius:6px;',
|
|
682
|
+
'border:.5px solid var(--dsw-alias-border-l1);font-size:11px;line-height:1.5}',
|
|
683
|
+
'.dsh-llm-hub-preset-hint__name{font-weight:600;color:var(--dsw-alias-label-primary)}',
|
|
684
|
+
'.dsh-llm-hub-preset-hint__desc{color:var(--dsw-alias-label-secondary);flex:1 1 auto;min-width:0}',
|
|
685
|
+
'.dsh-llm-hub-preset-hint__link{color:var(--dsw-alias-brand-primary,#3b82f6);text-decoration:none;white-space:nowrap}',
|
|
685
686
|
// 延迟高亮胶囊
|
|
686
687
|
'.dsh-llm-hub-latency-badge{display:inline-flex;align-items:center;gap:3.5px;padding:1px 5px;border-radius:3px;font-size:10.5px;font-variant-numeric:tabular-nums;line-height:1}',
|
|
687
688
|
'.dsh-llm-hub-latency-badge.is-fast{background:rgba(16,185,129,.08);color:var(--dsw-alias-state-success-primary,#10b981);border:.5px solid rgba(16,185,129,.2)}',
|
|
@@ -1529,45 +1530,45 @@ window.__ModuleLoader__.load({
|
|
|
1529
1530
|
const PRESETS = [
|
|
1530
1531
|
{
|
|
1531
1532
|
id: 'deepseek',
|
|
1533
|
+
match: ['deepseek', 'llm-deepseek'],
|
|
1532
1534
|
name: '🇨🇳 DeepSeek 官方',
|
|
1533
1535
|
desc: 'V4.1 Flash / V4 Pro 极速官方直连',
|
|
1534
|
-
docUrl: 'https://platform.deepseek.com/api_keys'
|
|
1535
|
-
yaml: 'llm-deepseek:\n keyPool:\n llm-deepseek:\n - name: "主账号"\n env: DEEPSEEK_API_KEY\n models:\n - id: deepseek-flash\n name: DeepSeek-V4.1-Flash\n - id: deepseek-v4-pro\n name: DeepSeek-V4-Pro'
|
|
1536
|
+
docUrl: 'https://platform.deepseek.com/api_keys'
|
|
1536
1537
|
},
|
|
1537
1538
|
{
|
|
1538
1539
|
id: 'minimax',
|
|
1540
|
+
match: ['minimax', 'minimax-cn'],
|
|
1539
1541
|
name: '⚡ MiniMax',
|
|
1540
1542
|
desc: 'MiniMax-M3 百万上下文超强大模型',
|
|
1541
|
-
docUrl: 'https://platform.minimaxi.com/user-center/basic-information/interface-key'
|
|
1542
|
-
yaml: 'minimax:\n displayName: MiniMax\n api: openai-completions\n baseURL: https://api.minimaxi.com/v1\n apiKeyEnv: MINIMAX_API_KEY\n models:\n - id: MiniMax-M3\n name: MiniMax-M3\n - id: MiniMax-M2.7\n name: MiniMax-M2.7'
|
|
1543
|
+
docUrl: 'https://platform.minimaxi.com/user-center/basic-information/interface-key'
|
|
1543
1544
|
},
|
|
1544
1545
|
{
|
|
1545
1546
|
id: 'zhipu',
|
|
1547
|
+
match: ['zai-coding-cn', 'zai', 'zhipu'],
|
|
1546
1548
|
name: '🧠 智谱清言 GLM',
|
|
1547
1549
|
desc: 'GLM-5 / GLM-4.7 国内 Coding 利器',
|
|
1548
|
-
docUrl: 'https://open.bigmodel.cn/usercenter/apikeys'
|
|
1549
|
-
yaml: 'zai-coding-cn:\n displayName: 智谱 GLM\n api: openai-completions\n baseURL: https://open.bigmodel.cn/api/paas/v4\n apiKeyEnv: ZHIPU_API_KEY\n models:\n - id: glm-5-turbo\n - id: glm-4.7'
|
|
1550
|
+
docUrl: 'https://open.bigmodel.cn/usercenter/apikeys'
|
|
1550
1551
|
},
|
|
1551
1552
|
{
|
|
1552
1553
|
id: 'volc',
|
|
1554
|
+
match: ['volcengine', 'volc', 'ark'],
|
|
1553
1555
|
name: '🌋 火山方舟 Ark',
|
|
1554
1556
|
desc: '字节豆包 Doubao-1.5 多模态矩阵',
|
|
1555
|
-
docUrl: 'https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey'
|
|
1556
|
-
yaml: 'volcengine:\n displayName: 火山方舟\n api: openai-completions\n baseURL: https://ark.cn-beijing.volces.com/api/v3\n apiKeyEnv: ARK_API_KEY\n models:\n - id: doubao-1.5-pro-32k\n - id: doubao-1.5-lite-32k'
|
|
1557
|
+
docUrl: 'https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey'
|
|
1557
1558
|
},
|
|
1558
1559
|
{
|
|
1559
1560
|
id: 'siliconflow',
|
|
1561
|
+
match: ['siliconflow'],
|
|
1560
1562
|
name: '🚀 硅基流动',
|
|
1561
1563
|
desc: 'SiliconFlow 聚合开源高性价比模型',
|
|
1562
|
-
docUrl: 'https://cloud.siliconflow.cn/account/ak'
|
|
1563
|
-
yaml: 'siliconflow:\n displayName: 硅基流动\n api: openai-completions\n baseURL: https://api.siliconflow.cn/v1\n apiKeyEnv: SILICONFLOW_API_KEY\n models:\n - id: deepseek-ai/DeepSeek-V3\n - id: Qwen/Qwen2.5-72B-Instruct'
|
|
1564
|
+
docUrl: 'https://cloud.siliconflow.cn/account/ak'
|
|
1564
1565
|
},
|
|
1565
1566
|
{
|
|
1566
1567
|
id: 'openrouter',
|
|
1568
|
+
match: ['openrouter'],
|
|
1567
1569
|
name: '🌐 OpenRouter',
|
|
1568
1570
|
desc: '聚合全球 Claude / GPT / Gemini 全模型',
|
|
1569
|
-
docUrl: 'https://openrouter.ai/keys'
|
|
1570
|
-
yaml: 'openrouter:\n displayName: OpenRouter\n api: openai-completions\n baseURL: https://openrouter.ai/api/v1\n apiKeyEnv: OPENROUTER_API_KEY\n models:\n - id: anthropic/claude-3.7-sonnet\n - id: deepseek/deepseek-r1'
|
|
1571
|
+
docUrl: 'https://openrouter.ai/keys'
|
|
1571
1572
|
}
|
|
1572
1573
|
]
|
|
1573
1574
|
|
|
@@ -1607,58 +1608,52 @@ window.__ModuleLoader__.load({
|
|
|
1607
1608
|
}
|
|
1608
1609
|
|
|
1609
1610
|
/**
|
|
1610
|
-
*
|
|
1611
|
+
* 添加提供方时的匹配提示(1.6.0 起取代页脚的「热门大模型一键装配」抽屉)。
|
|
1612
|
+
*
|
|
1613
|
+
* 为什么改:抽屉常驻在模型页底部,和用户正在配哪一家毫无关系,读起来像一块广告。
|
|
1614
|
+
* 该出现的时机是「用户在添加提供方里选中了某一家」—— 宿主此时渲染草稿卡,并以
|
|
1615
|
+
* `configured: false` 派发 settings.models.provider-card(见宿主 slot-contract.d.ts)。
|
|
1616
|
+
* 于是只在这张卡上、只给匹配到的那一家一行:名称 · 推荐模型 · 获取 Key。
|
|
1617
|
+
* 不再给 YAML:草稿卡本身就是配置表单,而模板根键(minimax)与目录 id(minimax-cn)
|
|
1618
|
+
* 对不上,复制过去反而误导。没匹配到的厂商不渲染任何东西。
|
|
1619
|
+
*
|
|
1620
|
+
* @param {unknown} provider - 宿主传入的 entry 对象或 provider id 字符串。
|
|
1621
|
+
* @returns {object | undefined} 命中的预设。
|
|
1611
1622
|
*/
|
|
1612
|
-
|
|
1613
|
-
const
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
const [copied, setCopied] = React.useState(false)
|
|
1623
|
+
const presetFor = (provider) => {
|
|
1624
|
+
const id = typeof provider === 'string' ? provider : (provider && typeof provider.provider === 'string' ? provider.provider : '')
|
|
1625
|
+
return id === '' ? undefined : PRESETS.find((preset) => preset.match.includes(id))
|
|
1626
|
+
}
|
|
1617
1627
|
|
|
1628
|
+
function PresetHint(props) {
|
|
1629
|
+
const t = typeof props.t === 'function' ? props.t : fallbackT
|
|
1630
|
+
const preset = presetFor(props.provider)
|
|
1631
|
+
if (preset === undefined) return null
|
|
1618
1632
|
ensureStyle()
|
|
1633
|
+
return h('div', { className: 'dsh-llm-hub-preset-hint' },
|
|
1634
|
+
h('span', { className: 'dsh-llm-hub-preset-hint__name' }, preset.name),
|
|
1635
|
+
h('span', { className: 'dsh-llm-hub-preset-hint__desc' }, preset.desc),
|
|
1636
|
+
h('a', {
|
|
1637
|
+
href: preset.docUrl,
|
|
1638
|
+
target: '_blank',
|
|
1639
|
+
rel: 'noreferrer',
|
|
1640
|
+
className: 'dsh-llm-hub-preset-hint__link'
|
|
1641
|
+
}, t('presetsDocsBtn')))
|
|
1642
|
+
}
|
|
1619
1643
|
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
h('span', { className: 'dsh-llm-hub-presets__title' },
|
|
1634
|
-
t('presetsTitle'),
|
|
1635
|
-
h('span', { style: { fontSize: '10px', opacity: 0.6 } }, expanded ? ' ▲ 收起' : ' ▼ 展开模版')),
|
|
1636
|
-
h('span', { className: 'dsh-llm-hub-subtext' }, t('presetsSub'))),
|
|
1637
|
-
expanded ? h('div', { className: 'dsh-llm-hub-presets__grid' },
|
|
1638
|
-
PRESETS.map((p) => h('div', {
|
|
1639
|
-
key: p.id,
|
|
1640
|
-
className: 'dsh-llm-hub-presets__item',
|
|
1641
|
-
onClick: () => setSelected(selected?.id === p.id ? null : p)
|
|
1642
|
-
},
|
|
1643
|
-
h('span', { className: 'dsh-llm-hub-presets__name' }, p.name),
|
|
1644
|
-
h('span', { className: 'dsh-llm-hub-presets__desc' }, p.desc)))) : null,
|
|
1645
|
-
expanded && selected !== null ? h('div', { className: 'dsh-llm-hub-presets__detail' },
|
|
1646
|
-
h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
1647
|
-
h('span', { style: { fontWeight: '600' } }, `${selected.name} 配置模版`),
|
|
1648
|
-
h('div', { style: { display: 'flex', gap: '6px' } },
|
|
1649
|
-
h('button', {
|
|
1650
|
-
type: 'button',
|
|
1651
|
-
className: 'dsh-llm-hub-btn',
|
|
1652
|
-
onClick: (e) => copyYaml(e, selected.yaml)
|
|
1653
|
-
}, copied ? t('presetsCopied') : t('presetsCopyBtn')),
|
|
1654
|
-
h('a', {
|
|
1655
|
-
href: selected.docUrl,
|
|
1656
|
-
target: '_blank',
|
|
1657
|
-
rel: 'noreferrer',
|
|
1658
|
-
className: 'dsh-llm-hub-btn',
|
|
1659
|
-
style: { textDecoration: 'none' }
|
|
1660
|
-
}, t('presetsDocsBtn')))),
|
|
1661
|
-
h('pre', { className: 'dsh-llm-hub-presets__code' }, selected.yaml)) : null)
|
|
1644
|
+
/**
|
|
1645
|
+
* 给 provider-card 组件套上草稿态行为。直接调用原组件(而不是 h(Card)),
|
|
1646
|
+
* 保证原组件的 hooks 挂在同一个组件上、调用顺序不变 —— 草稿保存后 configured
|
|
1647
|
+
* 翻成 true,同一个组件实例继续渲染,hook 数量不能变。
|
|
1648
|
+
*
|
|
1649
|
+
* 草稿态(configured === false)只给匹配提示、丢掉原卡输出:原卡此时去查一个
|
|
1650
|
+
* 还没写进配置的 provider,必然渲染红字「llm-pi-ai 段里没有 "xxx" 这个 provider」,
|
|
1651
|
+
* 用户只是选了一下下拉框,看到的却像报错(2026-09-28 浏览器实测)。
|
|
1652
|
+
*/
|
|
1653
|
+
const withPresetHint = (Card) => (props) => {
|
|
1654
|
+
const card = Card(props)
|
|
1655
|
+
if (props.configured !== false) return card
|
|
1656
|
+
return PresetHint(props)
|
|
1662
1657
|
}
|
|
1663
1658
|
|
|
1664
1659
|
/**
|
|
@@ -1886,6 +1881,25 @@ window.__ModuleLoader__.load({
|
|
|
1886
1881
|
next.splice(target, 0, item)
|
|
1887
1882
|
return { ...group, candidates: next }
|
|
1888
1883
|
})
|
|
1884
|
+
/**
|
|
1885
|
+
* 组之间上下移。
|
|
1886
|
+
*
|
|
1887
|
+
* 为什么需要:候选(组内的 provider/model)早就能用 ↑↓ 排,但组本身
|
|
1888
|
+
* (公司 / 个人 …)此前没有任何排序入口 —— 顺序只能靠手改 settings.yaml。
|
|
1889
|
+
* 而组的先后是有语义的:activeGroup 之外的组在智能路由下也会按顺序兜底,
|
|
1890
|
+
* 「先用哪个池子」必须在页面上能配。
|
|
1891
|
+
*
|
|
1892
|
+
* @param {number} index - 当前组下标。
|
|
1893
|
+
* @param {number} delta - -1 上移 / +1 下移。
|
|
1894
|
+
*/
|
|
1895
|
+
const moveGroup = (index, delta) => {
|
|
1896
|
+
const target = index + delta
|
|
1897
|
+
if (target < 0 || target >= groups.length) return
|
|
1898
|
+
const next = [...groups]
|
|
1899
|
+
const [item] = next.splice(index, 1)
|
|
1900
|
+
next.splice(target, 0, item)
|
|
1901
|
+
setDraft(next)
|
|
1902
|
+
}
|
|
1889
1903
|
const removeCandidate = (id, index) => editGroup(id, (group) => ({ ...group, candidates: group.candidates.filter((_, i) => i !== index) }))
|
|
1890
1904
|
const addCandidate = (id) => {
|
|
1891
1905
|
const provider = picker.provider
|
|
@@ -1974,20 +1988,54 @@ window.__ModuleLoader__.load({
|
|
|
1974
1988
|
const candidates = group.candidates
|
|
1975
1989
|
return h('div', { key: group.id, className: `dsh-llm-hub-routing__group${activeGroup === group.id ? ' is-active' : ''}` },
|
|
1976
1990
|
h('div', { className: 'dsh-llm-hub-routing__group-top' },
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1991
|
+
// 顺序即优先级,所以序号 + 上下移都得露在折叠态 —— 折叠着也能改顺序。
|
|
1992
|
+
h('div', { className: 'dsh-llm-hub-routing__group-order' },
|
|
1993
|
+
h('button', {
|
|
1994
|
+
type: 'button',
|
|
1995
|
+
className: 'dsh-llm-hub-routing__group-toggle',
|
|
1996
|
+
onClick: () => setExpanded(isOpen ? '' : group.id)
|
|
1997
|
+
}, `${isOpen ? '▾' : '▸'} ${groupIndex + 1}`),
|
|
1998
|
+
h('button', {
|
|
1999
|
+
type: 'button',
|
|
2000
|
+
className: 'dsh-llm-hub-routing__group-move',
|
|
2001
|
+
title: t('routeUp'),
|
|
2002
|
+
disabled: groupIndex === 0,
|
|
2003
|
+
onClick: () => moveGroup(groupIndex, -1)
|
|
2004
|
+
}, '↑'),
|
|
2005
|
+
h('button', {
|
|
2006
|
+
type: 'button',
|
|
2007
|
+
className: 'dsh-llm-hub-routing__group-move',
|
|
2008
|
+
title: t('routeDown'),
|
|
2009
|
+
disabled: groupIndex === groups.length - 1,
|
|
2010
|
+
onClick: () => moveGroup(groupIndex, 1)
|
|
2011
|
+
}, '↓')),
|
|
1982
2012
|
h('input', {
|
|
1983
2013
|
className: 'dsh-llm-hub-routing__group-label',
|
|
1984
2014
|
value: group.label ?? '',
|
|
1985
2015
|
placeholder: t('routeGroupLabelPlaceholder'),
|
|
1986
2016
|
onChange: (event) => editGroup(group.id, (current) => ({ ...current, label: event.target.value }))
|
|
1987
2017
|
}),
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
2018
|
+
// 折叠态给出「这一组会按什么顺序试」的摘要:主力 + 兜底条数。
|
|
2019
|
+
// 之前顺序完全藏在折叠区里,用户看不到自己在排什么。
|
|
2020
|
+
h('span', { className: 'dsh-llm-hub-routing__group-state' },
|
|
2021
|
+
candidates.length === 0
|
|
2022
|
+
? h('em', { className: 'dsh-llm-hub-routing__group-empty' }, t('routeNoCandidate'))
|
|
2023
|
+
: [
|
|
2024
|
+
h('b', { key: 'p' }, `${candidates[0].provider}/${candidates[0].model}`),
|
|
2025
|
+
candidates.length > 1
|
|
2026
|
+
? h('span', { key: 'n', className: 'dsh-llm-hub-routing__group-more' },
|
|
2027
|
+
` + ${candidates.length - 1} ${t('routeFallbackCount')}`)
|
|
2028
|
+
: null,
|
|
2029
|
+
// 只在「实际会切到非主模型」时才提示 —— 跟主模型相同的箭头是纯冗余,
|
|
2030
|
+
// 还会把摘要挤到省略号里。真正有信息量的只有「兜底被选上了」。
|
|
2031
|
+
live?.wouldRoute && `${live.wouldRoute.provider}/${live.wouldRoute.model}` !== `${candidates[0].provider}/${candidates[0].model}`
|
|
2032
|
+
? h('span', { key: 'w', className: 'dsh-llm-hub-routing__group-would' },
|
|
2033
|
+
`→ ${live.wouldRoute.provider}/${live.wouldRoute.model}`)
|
|
2034
|
+
: null,
|
|
2035
|
+
live?.reason
|
|
2036
|
+
? h('span', { key: 'r', className: 'dsh-llm-hub-routing__group-would' }, live.reason)
|
|
2037
|
+
: null,
|
|
2038
|
+
]),
|
|
1991
2039
|
activeGroup === group.id
|
|
1992
2040
|
? h('span', { className: 'dsh-llm-hub-routing__role' }, t('routeActiveTag'))
|
|
1993
2041
|
: h('button', {
|
|
@@ -2564,13 +2612,13 @@ window.__ModuleLoader__.load({
|
|
|
2564
2612
|
key: CARD_KEY,
|
|
2565
2613
|
locale: NS,
|
|
2566
2614
|
inject: () => ({ t, availability })
|
|
2567
|
-
}, BalanceCard))
|
|
2615
|
+
}, withPresetHint(BalanceCard)))
|
|
2568
2616
|
ctx.slots.inject('settings.models.provider-card', () => ctx.slots.register({
|
|
2569
2617
|
name: 'settings.models.provider-card',
|
|
2570
2618
|
key: PIAI_CARD_KEY,
|
|
2571
2619
|
locale: NS,
|
|
2572
2620
|
inject: () => ({ t, availability })
|
|
2573
|
-
}, PiAiCard))
|
|
2621
|
+
}, withPresetHint(PiAiCard)))
|
|
2574
2622
|
// settings.models.footer 的契约是 kind:'list'(见宿主 dsh-client-ui-settings-models
|
|
2575
2623
|
// 的 children 声明),list 类 slot 注册**必须带 id** —— 参照宿主自己的
|
|
2576
2624
|
// settings.onboarding 注册(id:'welcome-notice', order:-100)。
|
|
@@ -2584,13 +2632,6 @@ window.__ModuleLoader__.load({
|
|
|
2584
2632
|
locale: NS,
|
|
2585
2633
|
inject: () => ({ t, availability })
|
|
2586
2634
|
}, HeroOverviewCard))
|
|
2587
|
-
ctx.slots.inject('settings.models.footer', () => ctx.slots.register({
|
|
2588
|
-
name: 'settings.models.footer',
|
|
2589
|
-
id: 'dsh-llm-hub-presets',
|
|
2590
|
-
order: 65,
|
|
2591
|
-
locale: NS,
|
|
2592
|
-
inject: () => ({ t })
|
|
2593
|
-
}, QuickPresetsCard))
|
|
2594
2635
|
ctx.slots.inject('settings.models.footer', () => ctx.slots.register({
|
|
2595
2636
|
name: 'settings.models.footer',
|
|
2596
2637
|
id: 'dsh-llm-hub-health',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-plugins/dsh-llm-hub",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "DSH 模型管理层:网关可达性探测、模型目录拉取、DeepSeek + MiniMax/智谱/Moonshot/StepFun 余额与配额、本月用量统计、余额预警、Provider 健康看板,再把本机装好的 codex / claude / agy 注册成子代理(装了才出现)。零运行时依赖,不改动 DSH 安装。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 发版门禁的纯判据 —— 从 prepublish-gate.mjs 抽出来的可测部分。
|
|
3
|
+
*
|
|
4
|
+
* 为什么要抽:门禁脚本顶层会读 package.json、跑 npm pack、连 registry,
|
|
5
|
+
* 没法直接 import 进来做单元测试;而契约 4 的「纯文档版放行」判据恰恰是
|
|
6
|
+
* 最需要测的逻辑(判错的两个方向都很难看:误拦文档版 → 逼人用逃生舱;
|
|
7
|
+
* 误放代码版 → 契约 4 形同虚设)。
|
|
8
|
+
*
|
|
9
|
+
* 这些函数**不含任何副作用**,只做字符串/文件内容比对。
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs'
|
|
12
|
+
import { join } from 'node:path'
|
|
13
|
+
import { createHash } from 'node:crypto'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 「运行期载荷」= tarball 里**除了文档/元数据/开发工具之外**的每一个文件。
|
|
17
|
+
*
|
|
18
|
+
* 判据必须能机械执行 —— 不用「CHANGELOG 里有没有 feat/fix 关键字」这种启发式
|
|
19
|
+
* (自省段落里出现一个「修」字就会误放行)。详见 docs/rules/release-semantics.md。
|
|
20
|
+
*/
|
|
21
|
+
export const EXCLUDE = [
|
|
22
|
+
/^readme(\.[^/]*)?$/i,
|
|
23
|
+
/^changelog(\.[^/]*)?$/i,
|
|
24
|
+
/^(license|notice|contributing)(\.[^/]*)?$/i,
|
|
25
|
+
/^docs\//i,
|
|
26
|
+
/^assets\//i,
|
|
27
|
+
/^scripts\//i,
|
|
28
|
+
/^\.github\//i,
|
|
29
|
+
/^\.githooks\//i,
|
|
30
|
+
/^tsconfig[^/]*\.json$/i,
|
|
31
|
+
/^(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?)$/i,
|
|
32
|
+
/^(screenshots\.json|llms\.txt|dev_notes[^/]*)$/i,
|
|
33
|
+
/^(\.npmignore|\.gitignore|\.gitattributes)$/i
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
export const isRuntime = (p) => !EXCLUDE.some((re) => re.test(p))
|
|
37
|
+
|
|
38
|
+
/** 递归收集目录下所有文件(相对路径)。 */
|
|
39
|
+
export function collectAllFiles(root) {
|
|
40
|
+
const out = []
|
|
41
|
+
const walk = (rel) => {
|
|
42
|
+
const abs = rel ? join(root, rel) : root
|
|
43
|
+
let st
|
|
44
|
+
try { st = statSync(abs) } catch { return }
|
|
45
|
+
if (st.isDirectory()) {
|
|
46
|
+
for (const e of readdirSync(abs)) walk(rel ? join(rel, e) : e)
|
|
47
|
+
} else {
|
|
48
|
+
out.push(rel)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
walk('')
|
|
52
|
+
return out
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 找出「本次包里内容与上一已发布版不同」的运行期文件。
|
|
57
|
+
*
|
|
58
|
+
* 只服务于契约 4 的「载荷指纹相同」分支:指纹相同说明集合级没变,
|
|
59
|
+
* 这时要再问一句「那到底动了什么」——
|
|
60
|
+
* · 动的是运行期文件 → 说明内容变了但指纹没体现(改注释 / 重构不生效分支),拦
|
|
61
|
+
* · 全是文档 → 纯文档版,放行
|
|
62
|
+
*
|
|
63
|
+
* 比对策略:逐文件 sha256。新增/删除也算「变了」。
|
|
64
|
+
*/
|
|
65
|
+
export function detectChangedRuntime(prevRoot, curRoot, curFiles) {
|
|
66
|
+
const changed = []
|
|
67
|
+
const prevFiles = new Set(collectAllFiles(prevRoot))
|
|
68
|
+
|
|
69
|
+
const hashOf = (root, rel) => {
|
|
70
|
+
try { return createHash('sha256').update(readFileSync(join(root, rel))).digest('hex') }
|
|
71
|
+
catch { return null }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const rel of curFiles) {
|
|
75
|
+
if (rel === 'package.json') continue
|
|
76
|
+
if (!isRuntime(rel)) continue
|
|
77
|
+
const cur = hashOf(curRoot, rel)
|
|
78
|
+
if (cur === null) continue
|
|
79
|
+
const prev = prevFiles.has(rel) ? hashOf(prevRoot, rel) : null
|
|
80
|
+
if (prev !== cur) changed.push(rel)
|
|
81
|
+
}
|
|
82
|
+
for (const rel of prevFiles) {
|
|
83
|
+
if (rel === 'package.json') continue
|
|
84
|
+
if (!isRuntime(rel)) continue
|
|
85
|
+
if (!curFiles.includes(rel)) changed.push(rel + ' (本次已移除)')
|
|
86
|
+
}
|
|
87
|
+
return changed
|
|
88
|
+
}
|
|
@@ -31,6 +31,7 @@ import { dirname, join, relative } from 'node:path'
|
|
|
31
31
|
import { fileURLToPath } from 'node:url'
|
|
32
32
|
import { tmpdir } from 'node:os'
|
|
33
33
|
import { createHash } from 'node:crypto'
|
|
34
|
+
import { EXCLUDE, isRuntime, collectAllFiles, detectChangedRuntime } from './gate-lib.mjs'
|
|
34
35
|
|
|
35
36
|
const GATE_VERSION = 1
|
|
36
37
|
|
|
@@ -138,21 +139,6 @@ if (existsSync(changelogPath)) {
|
|
|
138
139
|
// feat/fix 关键字」这种启发式(自省段落里出现一个「修」字就会误放行)。
|
|
139
140
|
//
|
|
140
141
|
// 详见 docs/rules/release-semantics.md。
|
|
141
|
-
const EXCLUDE = [
|
|
142
|
-
/^readme(\.[^/]*)?$/i,
|
|
143
|
-
/^changelog(\.[^/]*)?$/i,
|
|
144
|
-
/^(license|notice|contributing)(\.[^/]*)?$/i,
|
|
145
|
-
/^docs\//i,
|
|
146
|
-
/^assets\//i,
|
|
147
|
-
/^scripts\//i,
|
|
148
|
-
/^\.github\//i,
|
|
149
|
-
/^\.githooks\//i,
|
|
150
|
-
/^tsconfig[^/]*\.json$/i,
|
|
151
|
-
/^(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?)$/i,
|
|
152
|
-
/^(screenshots\.json|llms\.txt|dev_notes[^/]*)$/i,
|
|
153
|
-
/^(\.npmignore|\.gitignore|\.gitattributes)$/i
|
|
154
|
-
]
|
|
155
|
-
const isRuntime = (p) => !EXCLUDE.some(re => re.test(p))
|
|
156
142
|
|
|
157
143
|
const RUNTIME_PKG_FIELDS = [
|
|
158
144
|
'name', 'type', 'main', 'module', 'types', 'exports', 'bin', 'dsh', 'files',
|
|
@@ -210,12 +196,27 @@ if (process.env.ALLOW_METADATA_ONLY === '1') {
|
|
|
210
196
|
const before = runtimeFingerprint(prevRoot, collectAllFiles(prevRoot))
|
|
211
197
|
const after = runtimeFingerprint(pkgRoot, packFiles)
|
|
212
198
|
if (JSON.stringify(before) === JSON.stringify(after)) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
)
|
|
199
|
+
// 载荷相同有两种情况,必须分开处理(2026-09-27 补):
|
|
200
|
+
//
|
|
201
|
+
// (a) 本次改动**全是文档**(README / LICENSE / CHANGELOG / assets)——
|
|
202
|
+
// 这正是 README 包装升级要发版的场景,拦下来只会逼人用逃生舱,
|
|
203
|
+
// 而 CI 跑 `npm publish` 时根本传不了环境变量。
|
|
204
|
+
// (b) 本次动了**代码但载荷碰巧没变**(改了注释、重构了不生效的分支)——
|
|
205
|
+
// 这才是契约 4 要拦的:「diff 看着有东西,用户其实感知不到」。
|
|
206
|
+
//
|
|
207
|
+
// 判据:拿「本次 tarball 里与上一版内容不同的文件」去比对 isRuntime。
|
|
208
|
+
// 有任何一个是运行期文件 → 走 (b) 拦下;全是文档 → 走 (a) 放行。
|
|
209
|
+
const changedRuntime = detectChangedRuntime(prevRoot, pkgRoot, packFiles)
|
|
210
|
+
if (changedRuntime.length > 0) {
|
|
211
|
+
errors.push(
|
|
212
|
+
`运行期载荷与上一个已发布版本 ${previous} **完全相同** —— 这个 semver 号没有承载任何用户可感知的变化\n` +
|
|
213
|
+
` → 本次改动的运行期文件: ${changedRuntime.slice(0, 6).join(', ')}\n` +
|
|
214
|
+
` → 内容变了但载荷指纹没变(改了注释 / 重构了不生效的分支?),用户感知不到任何差异。\n` +
|
|
215
|
+
` → semver 是对用户的契约: 流程修补 / 元数据修正 / 自省笔记不该占版本号(docs/rules/release-semantics.md)。`
|
|
216
|
+
)
|
|
217
|
+
} else {
|
|
218
|
+
console.log(`[prepublish-gate] · 契约 4: 本次为纯文档版(载荷相对 ${previous} 无运行期变化,放行)`)
|
|
219
|
+
}
|
|
219
220
|
} else {
|
|
220
221
|
console.log(`[prepublish-gate] · 契约 4: 运行期载荷相对 ${previous} 有变化(${before.length} 项比对)`)
|
|
221
222
|
}
|
|
@@ -229,23 +230,6 @@ if (process.env.ALLOW_METADATA_ONLY === '1') {
|
|
|
229
230
|
}
|
|
230
231
|
}
|
|
231
232
|
|
|
232
|
-
/** 递归收集目录下所有文件(相对路径),用于对上一版本解包目录做指纹。 */
|
|
233
|
-
function collectAllFiles(root) {
|
|
234
|
-
const out = []
|
|
235
|
-
const walk = (rel) => {
|
|
236
|
-
const abs = rel ? join(root, rel) : root
|
|
237
|
-
let st
|
|
238
|
-
try { st = statSync(abs) } catch { return }
|
|
239
|
-
if (st.isDirectory()) {
|
|
240
|
-
for (const e of readdirSync(abs)) walk(rel ? join(rel, e) : e)
|
|
241
|
-
} else {
|
|
242
|
-
out.push(rel)
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
walk('')
|
|
246
|
-
return out
|
|
247
|
-
}
|
|
248
|
-
|
|
249
233
|
// ── 报告 ──────────────────────────────────────────────────────────────────
|
|
250
234
|
if (errors.length > 0) {
|
|
251
235
|
console.error(`\n[prepublish-gate] ${errors.length} 条契约不满足, 拒绝发版:\n`)
|