@dsh-plugins/dsh-llm-hub 1.5.5 → 1.5.6
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 +25 -0
- package/README.md +15 -8
- package/lib/index.js +31 -6
- package/package.json +2 -2
- package/scripts/readme-gate.mjs +144 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
本项目遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/)。
|
|
4
4
|
|
|
5
|
+
## [1.5.6] - 2026-09-27
|
|
6
|
+
|
|
7
|
+
### 📛 包装升级:首屏补齐居中标题 + 接入 README 门禁
|
|
8
|
+
|
|
9
|
+
- 新增 `<h1 align="center">` 居中标题与双语 slogan
|
|
10
|
+
- 8 个徽章从 `flat-square` 换成 `for-the-badge`
|
|
11
|
+
- 新增 `scripts/readme-gate.mjs`,挂在 `prepublishOnly` 上
|
|
12
|
+
- 真源:`cs rule open_source_project_baseline`(README 首屏金字塔)
|
|
13
|
+
- 4 条契约:居中品牌 / `for-the-badge` 徽章 / 前 80 行有 Why This 对比表 / 有 `README.en.md`
|
|
14
|
+
- 逃生舱:`README_GATE=off`(仅限明确不需要包装的内部包)
|
|
15
|
+
|
|
16
|
+
### 🛣️ 智能路由:`routableStateFor` 把"探测表里没记录"细分成 unknown / unavailable
|
|
17
|
+
|
|
18
|
+
之前一刀切返回 `unavailable`,导致**冷启动 / 用户刚加 provider / 探针刚清空**时被路由跳过,
|
|
19
|
+
即便 settings 里有配、key 也填了,照样不让 smart 接管。典型踩坑:zai-coding-cn 这类
|
|
20
|
+
无 baseURL 的 provider,第一次请求时被 `routableStateFor` 当 unavailable 跳过去。
|
|
21
|
+
|
|
22
|
+
新语义(`lib/index.js:2554-2580`):
|
|
23
|
+
|
|
24
|
+
- 探测表里有 verdict → 用 verdict.state(available / unknown / unavailable)
|
|
25
|
+
- 探测表里没有 + settings 里**有**配 → `unknown`(routable,请求撞真上游,撞完探针补 verdict)
|
|
26
|
+
- 探测表里没有 + settings 里**没**配 → `unavailable`(真不认识,撞 NO_ADAPTER,fail-open)
|
|
27
|
+
|
|
28
|
+
测试新增 1 条(`pickSmartRoute:未探测(unknown)且候选里 → 仍接管`),全 132 项过。
|
|
29
|
+
|
|
5
30
|
## [1.5.5] - 2026-09-27
|
|
6
31
|
|
|
7
32
|
### 🪧 路由卡:每条候选的能力 chip(思考 + 多模态)
|
package/README.md
CHANGED
|
@@ -2,18 +2,25 @@
|
|
|
2
2
|
<img src="https://img.webkubor.online/oss/dsh-llm-hub/banner.png" alt="dsh-llm-hub" width="100%" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
+
|
|
6
|
+
<h1 align="center">🔌 dsh-llm-hub</h1>
|
|
7
|
+
|
|
8
|
+
<p align="center">
|
|
9
|
+
<strong>给 DSH 模型页补上官方适配器缺的那半。</strong><br/>
|
|
10
|
+
<sub>Gateway health, model catalog, balance — the half the built-in adapter leaves out.</sub>
|
|
11
|
+
</p>
|
|
5
12
|
<p align="center">
|
|
6
|
-
<a href="https://www.npmjs.com/package/@dsh-plugins/dsh-llm-hub"><img src="https://img.shields.io/npm/v/%40dsh-plugins%2Fdsh-llm-hub?style=
|
|
7
|
-
<a href="https://www.npmjs.com/package/@dsh-plugins/dsh-llm-hub"><img src="https://img.shields.io/npm/dm/%40dsh-plugins%2Fdsh-llm-hub?style=
|
|
8
|
-
<img src="https://img.shields.io/badge/DSH-%E2%89%A50.1.1--rc.2-4d6bfe?style=
|
|
9
|
-
<img src="https://img.shields.io/badge/runtime_deps-0-5A9E6F?style=
|
|
10
|
-
<img src="https://img.shields.io/badge/license-MIT-777?style=
|
|
13
|
+
<a href="https://www.npmjs.com/package/@dsh-plugins/dsh-llm-hub"><img src="https://img.shields.io/npm/v/%40dsh-plugins%2Fdsh-llm-hub?style=for-the-badge&color=4C7EF3&logo=npm&label=npm" alt="npm" /></a>
|
|
14
|
+
<a href="https://www.npmjs.com/package/@dsh-plugins/dsh-llm-hub"><img src="https://img.shields.io/npm/dm/%40dsh-plugins%2Fdsh-llm-hub?style=for-the-badge&color=6d7f9c&label=downloads" alt="downloads" /></a>
|
|
15
|
+
<img src="https://img.shields.io/badge/DSH-%E2%89%A50.1.1--rc.2-4d6bfe?style=for-the-badge" alt="DSH" />
|
|
16
|
+
<img src="https://img.shields.io/badge/runtime_deps-0-5A9E6F?style=for-the-badge" alt="zero deps" />
|
|
17
|
+
<img src="https://img.shields.io/badge/license-MIT-777?style=for-the-badge" alt="MIT" />
|
|
11
18
|
</p>
|
|
12
19
|
|
|
13
20
|
<p align="center">
|
|
14
|
-
<a href="https://github.com/deepseek-ai/deepseek-harness"><img src="https://img.shields.io/badge/DeepSeek_Harness-Plugin-4d6bfe?style=
|
|
15
|
-
<a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/topic-dsh--plugin-4d6bfe?style=
|
|
16
|
-
<a href="https://github.com/topics/llm-gateway"><img src="https://img.shields.io/badge/topic-llm--gateway-4C7EF3?style=
|
|
21
|
+
<a href="https://github.com/deepseek-ai/deepseek-harness"><img src="https://img.shields.io/badge/DeepSeek_Harness-Plugin-4d6bfe?style=for-the-badge" alt="DSH Plugin" /></a>
|
|
22
|
+
<a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/topic-dsh--plugin-4d6bfe?style=for-the-badge" alt="dsh-plugin" /></a>
|
|
23
|
+
<a href="https://github.com/topics/llm-gateway"><img src="https://img.shields.io/badge/topic-llm--gateway-4C7EF3?style=for-the-badge" alt="llm-gateway" /></a>
|
|
17
24
|
· <a href="README.en.md">English</a> · <a href="CHANGELOG.md">更新日志</a>
|
|
18
25
|
</p>
|
|
19
26
|
|
package/lib/index.js
CHANGED
|
@@ -2563,12 +2563,37 @@ export function apply(ctx) {
|
|
|
2563
2563
|
}
|
|
2564
2564
|
|
|
2565
2565
|
/**
|
|
2566
|
-
*
|
|
2566
|
+
* 一个 provider 是否在配置里"存在":要么 `llm-pi-ai` 段有它的 profile,要么
|
|
2567
|
+
* `llm-deepseek` 段存在(即官方直连适配器挂上了)。
|
|
2567
2568
|
*
|
|
2568
|
-
* `
|
|
2569
|
-
*
|
|
2570
|
-
*
|
|
2571
|
-
*
|
|
2569
|
+
* 与 `routableStateFor` 配合用:探测表里没记录的 provider,
|
|
2570
|
+
* 若在此集合里 → 算 unknown(routable,让请求试一把);
|
|
2571
|
+
* 不在此集合里 → 才算 unavailable(真不认识,路由过去会撞 NO_ADAPTER)。
|
|
2572
|
+
* @returns 已知 provider id 的 Set。
|
|
2573
|
+
*/
|
|
2574
|
+
const knownConfiguredProviders = () => {
|
|
2575
|
+
const ids = new Set()
|
|
2576
|
+
for (const [id, profile] of piaiProviders()) {
|
|
2577
|
+
if (profile !== null && typeof profile === 'object') ids.add(id)
|
|
2578
|
+
}
|
|
2579
|
+
if (sectionOf() !== undefined) ids.add(DEEPSEEK_PROVIDER)
|
|
2580
|
+
return ids
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
/**
|
|
2584
|
+
* 路由目标专线的可用性。
|
|
2585
|
+
*
|
|
2586
|
+
* 判定梯度(决定 `pickSmartRoute` 是跳过还是接管):
|
|
2587
|
+
* · 探测表里有 verdict:用 verdict.state(available / unknown / unavailable)
|
|
2588
|
+
* · 探测表里没有 + provider 在 settings 里配了 → 'unknown'
|
|
2589
|
+
* (冷启动 / 用户刚加 provider / 探针刚清空 —— 不阻塞路由,让请求发出去
|
|
2590
|
+
* 撞真上游;撞完探针会立刻补 verdict,下一轮就准了)
|
|
2591
|
+
* · 探测表里没有 + provider **没在** settings 里 → 'unavailable'
|
|
2592
|
+
* (典型:配置里写错名 / 该 provider 还没注册;路由过去只会撞 NO_ADAPTER,
|
|
2593
|
+
* fail-open 保留原配置,比打挂请求更稳)
|
|
2594
|
+
*
|
|
2595
|
+
* `stateFor` 把前两种情况都折成 'unknown',所以信息面板里仍是"待探明",无歧义;
|
|
2596
|
+
* 区别只在路由**目标**的取与舍 —— 这是 routable vs stateful 的本意。
|
|
2572
2597
|
* @param provider - provider id。
|
|
2573
2598
|
* @returns 'available' | 'unknown' | 'unavailable'。
|
|
2574
2599
|
*/
|
|
@@ -2576,7 +2601,7 @@ export function apply(ctx) {
|
|
|
2576
2601
|
for (const verdict of baseVerdicts.values()) {
|
|
2577
2602
|
if (verdict.provider === provider) return verdict.state
|
|
2578
2603
|
}
|
|
2579
|
-
return 'unavailable'
|
|
2604
|
+
return knownConfiguredProviders().has(provider) ? 'unknown' : 'unavailable'
|
|
2580
2605
|
}
|
|
2581
2606
|
|
|
2582
2607
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-plugins/dsh-llm-hub",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.6",
|
|
4
4
|
"description": "DSH 模型管理层:网关可达性探测、模型目录拉取、DeepSeek + MiniMax/智谱/Moonshot/StepFun 余额与配额、本月用量统计、余额预警、Provider 健康看板,再把本机装好的 codex / claude / agy 注册成子代理(装了才出现)。零运行时依赖,不改动 DSH 安装。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"preview": "npm run deploy && open http://127.0.0.1:3080",
|
|
33
33
|
"check": "for f in lib/*.js; do node --check \"$f\" || exit 1; done",
|
|
34
34
|
"test": "node --test",
|
|
35
|
-
"prepublishOnly": "npm run check && npm test && node scripts/prepublish-gate.mjs"
|
|
35
|
+
"prepublishOnly": "npm run check && npm test && node scripts/prepublish-gate.mjs && node scripts/readme-gate.mjs"
|
|
36
36
|
},
|
|
37
37
|
"keywords": [
|
|
38
38
|
"dsh",
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* readme-gate —— 开源包装的首屏门禁
|
|
4
|
+
*
|
|
5
|
+
* 🔴 这个文件是**拷贝**,真源在 CortexOS:
|
|
6
|
+
* CortexOS/scripts/release-gate/readme-gate.mjs
|
|
7
|
+
* 同步检查:node $CORTEXOS_ROOT/scripts/check-release-gate-sync.mjs
|
|
8
|
+
* 改逻辑请改真源,然后跑同步脚本把各仓拷一遍 —— 不要在各仓就地改。
|
|
9
|
+
*
|
|
10
|
+
* 为什么是门禁而不是文档:cs rule open_source_project_baseline 早就写明了
|
|
11
|
+
* README 首屏金字塔(居中品牌 → for-the-badge → Why This 对比表 → 30 秒上手),
|
|
12
|
+
* 但 2026-09-27 一查四个插件,规则说的和仓库里躺的完全是两回事:
|
|
13
|
+
*
|
|
14
|
+
* · 徽章:四个仓全用 style=flat-square(8/10/8/7 个),规则要求 for-the-badge
|
|
15
|
+
* · 居中标题:bloom-theme / llm-hub 用 <p align="center"> 而非 <h1 align="center">
|
|
16
|
+
* · Why This 对比表:dsh-user-mirror 缺
|
|
17
|
+
* · 英文 README:dsh-env-inspector 缺
|
|
18
|
+
*
|
|
19
|
+
* 共同点还是那句「约定没做成门禁」。所以把判据做成能跑的:
|
|
20
|
+
*
|
|
21
|
+
* 契约 1 居中品牌 —— 存在 <h1 align="center"> 开头且带 emoji/品牌名的标题
|
|
22
|
+
* 契约 2 徽章规格 —— 至少 1 个 for-the-badge;且不得残留 flat-square
|
|
23
|
+
* 契约 3 对比矩阵 —— 首屏(前 80 行)内有 Why This 对比表(Markdown 表格)
|
|
24
|
+
* 契约 4 双语 —— 存在 README.en.md(生态面向全球,英文是默认入场券)
|
|
25
|
+
*
|
|
26
|
+
* 逃生舱:README_GATE=off 跳过全部检查(仅限明确不需要包装的内部包)。
|
|
27
|
+
*/
|
|
28
|
+
import { readFileSync, existsSync, readdirSync } from 'node:fs'
|
|
29
|
+
import { dirname, join } from 'node:path'
|
|
30
|
+
import { fileURLToPath } from 'node:url'
|
|
31
|
+
|
|
32
|
+
const GATE_VERSION = 1
|
|
33
|
+
|
|
34
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
35
|
+
const pkgRoot = dirname(here)
|
|
36
|
+
|
|
37
|
+
const pkg = JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf8'))
|
|
38
|
+
const errors = []
|
|
39
|
+
const notes = []
|
|
40
|
+
|
|
41
|
+
if (String(process.env.README_GATE).toLowerCase() === 'off') {
|
|
42
|
+
console.log('[readme-gate] ↩ README_GATE=off —— 跳过首屏包装检查')
|
|
43
|
+
process.exit(0)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const readmePath = join(pkgRoot, 'README.md')
|
|
47
|
+
if (!existsSync(readmePath)) {
|
|
48
|
+
console.error('[readme-gate] ✖ 找不到 README.md —— 一个没有 README 的包不可能被人用起来')
|
|
49
|
+
process.exit(1)
|
|
50
|
+
}
|
|
51
|
+
const readme = readFileSync(readmePath, 'utf8')
|
|
52
|
+
const lines = readme.split(/\r?\n/)
|
|
53
|
+
|
|
54
|
+
// ── 1. 居中品牌标题 ────────────────────────────────────────────────────────
|
|
55
|
+
// 接受 <h1 align="center">…</h1>;<p align="center"><h1> 也算(有些仓这么写)。
|
|
56
|
+
const h1Centered =
|
|
57
|
+
/<h1[^>]*align=["']center["'][^>]*>/i.test(readme) ||
|
|
58
|
+
/<p[^>]*align=["']center["'][^>]*>\s*<h1/i.test(readme)
|
|
59
|
+
|
|
60
|
+
if (!h1Centered) {
|
|
61
|
+
errors.push(
|
|
62
|
+
'README 缺少居中的 <h1 align="center"> 标题\n' +
|
|
63
|
+
' → 首屏第一眼必须是品牌:<h1 align="center">🔐 项目名</h1>,紧跟一句居中 slogan'
|
|
64
|
+
)
|
|
65
|
+
} else {
|
|
66
|
+
// 标题里带 emoji 或品牌名才像样,光 <h1 align="center"></h1> 是空的
|
|
67
|
+
const m = readme.match(/<h1[^>]*>([\s\S]*?)<\/h1>/i)
|
|
68
|
+
if (m && m[1].replace(/<[^>]+>/g, '').trim().length < 2) {
|
|
69
|
+
errors.push('README 的居中标题是空的 —— 补上 emoji + 项目名')
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── 2. 徽章规格:for-the-badge,且不残留 flat-square ───────────────────────
|
|
74
|
+
const badgeCount = (readme.match(/for-the-badge/g) ?? []).length
|
|
75
|
+
const flatSquare = (readme.match(/flat-square/g) ?? []).length
|
|
76
|
+
|
|
77
|
+
if (badgeCount === 0) {
|
|
78
|
+
errors.push(
|
|
79
|
+
'README 没有任何 style=for-the-badge 徽章\n' +
|
|
80
|
+
' → 规则要求立体大徽章(style=for-the-badge),版本/许可证/DSH 版本/Build 这几个是基本盘'
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
if (flatSquare > 0) {
|
|
84
|
+
errors.push(
|
|
85
|
+
'README 里有 ' + flatSquare + ' 个 style=flat-square 徽章\n' +
|
|
86
|
+
' → 统一换成 style=for-the-badge(把 URL 里的 flat-square 直接替换即可),' +
|
|
87
|
+
'否则首屏是细长方块,视觉重量撑不起品牌'
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── 3. Why This 对比矩阵必须在首屏 ─────────────────────────────────────────
|
|
92
|
+
// 「首屏」= 前 80 行。表格判定:连续的 | ... | 行,至少 3 行(表头+分隔+1 行数据)。
|
|
93
|
+
const HEAD_LINES = 80
|
|
94
|
+
const head = lines.slice(0, HEAD_LINES).join('\n')
|
|
95
|
+
const tableBlocks = []
|
|
96
|
+
let run = 0
|
|
97
|
+
for (const l of lines.slice(0, HEAD_LINES)) {
|
|
98
|
+
if (/^\s*\|.*\|\s*$/.test(l)) { run++; if (run === 3) tableBlocks.push(1) }
|
|
99
|
+
else run = 0
|
|
100
|
+
}
|
|
101
|
+
const hasWhyTable = tableBlocks.length > 0
|
|
102
|
+
|
|
103
|
+
if (!hasWhyTable) {
|
|
104
|
+
errors.push(
|
|
105
|
+
'README 首屏(前 ' + HEAD_LINES + ' 行)没有对比表格\n' +
|
|
106
|
+
' → Why This Table 是转化率的关键:用 ✅ / ❌ 说明「为什么用你这个而不是别的」。' +
|
|
107
|
+
'同类方案、原生能力、手动配置都值得列成列'
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── 4. 英文 README ─────────────────────────────────────────────────────────
|
|
112
|
+
const hasEn = ['README.en.md', 'README.en-US.md', 'README.en_US.md']
|
|
113
|
+
.some((f) => existsSync(join(pkgRoot, f)))
|
|
114
|
+
|
|
115
|
+
if (!hasEn) {
|
|
116
|
+
errors.push(
|
|
117
|
+
'缺少英文 README(README.en.md)\n' +
|
|
118
|
+
' → dsh-plugin 生态面向全球,英文是默认入场券;中文 README 首行加上 [English](README.en.md) | 中文'
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── 额外提示(不拦,但值得知道)───────────────────────────────────────────
|
|
123
|
+
const shotCount = ['assets']
|
|
124
|
+
.flatMap((d) => {
|
|
125
|
+
const p = join(pkgRoot, d)
|
|
126
|
+
if (!existsSync(p)) return []
|
|
127
|
+
return readdirSync(p).filter((f) => /\.(png|jpe?g|webp|gif)$/i.test(f))
|
|
128
|
+
}).length
|
|
129
|
+
if (shotCount === 0) {
|
|
130
|
+
notes.push('assets/ 里没有截图 —— 插件页没图等于没人点,建议至少 1 张真实界面截图')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── 汇总 ───────────────────────────────────────────────────────────────────
|
|
134
|
+
if (errors.length > 0) {
|
|
135
|
+
console.error('[readme-gate] ✖ README 首屏不合规(gate v' + GATE_VERSION + '):')
|
|
136
|
+
for (const e of errors) console.error(' · ' + e)
|
|
137
|
+
console.error('\n 真源:cs rule open_source_project_baseline(README 金字塔标准)')
|
|
138
|
+
console.error(' 逃生舱:README_GATE=off(仅限内部包)')
|
|
139
|
+
process.exit(1)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log('[readme-gate] ✓ README 首屏四条契约通过(gate v' + GATE_VERSION + ')')
|
|
143
|
+
console.log(' · for-the-badge 徽章 ' + badgeCount + ' 个;首屏对比表 ' + tableBlocks.length + ' 个;英文版 ✓')
|
|
144
|
+
for (const n of notes) console.log(' · 提示:' + n)
|