@dsh-plugins/dsh-llm-hub 1.7.0 → 1.8.0
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/lib/index.js +76 -52
- package/package.json +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
本项目遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/)。
|
|
4
4
|
|
|
5
|
+
## [1.8.0] - 2026-09-29
|
|
6
|
+
|
|
7
|
+
### 🔧 适配 DSH 0.1.7 的 settings 架构(升级到 0.1.7 后配置读不到的那版)
|
|
8
|
+
|
|
9
|
+
**症状**:升到 DSH 0.1.7 后,路由组「凭空消失」—— chip 不渲染,设置页路由卡是空的,
|
|
10
|
+
写接口一律拒绝。数据其实好好躺在配置里,只是没人读。
|
|
11
|
+
|
|
12
|
+
**根因**:0.1.7 把 settings 架构整个换掉了。`settings.register(ns, schema)` 和
|
|
13
|
+
`settings.get(ns)` 双双消失(0.1.5 的 `dsh-settings` 里有 20 处 `register` 引用,
|
|
14
|
+
0.1.7 是 0 处)。老代码里 `settings.register(...)` 抛 `TypeError` 被 catch 吞掉,
|
|
15
|
+
`hubRegistered` 恒为 false,于是整个 `dsh-llm-hub` 段的读写全部短路。
|
|
16
|
+
|
|
17
|
+
**改造**
|
|
18
|
+
- 导出 `Config`,配置改由 `apply(ctx, config)` 注入(0.1.7 的官方姿势,
|
|
19
|
+
`@deepseek-ai/dsh-llm-pi-ai` 的 providers 段同款)
|
|
20
|
+
- 读自己的段走 `config.routing.get()`;读**别人**的段(`llm-pi-ai` / `llm-deepseek`)
|
|
21
|
+
走 `settings.describe()` —— 0.1.7 里那是唯一的跨插件读法
|
|
22
|
+
- 拆掉 `hubRegistered` 守卫(该概念已不存在)
|
|
23
|
+
- 三个字段全部标 `.volatile()`:`settings.update()` 拒绝「没有 volatile 字段的条目」
|
|
24
|
+
(原话:`Plugin entry "dsh-llm-hub" has no volatile fields`),而 volatile 的语义
|
|
25
|
+
正是本插件要的 —— 路由组/预警/别名都是每次请求惰性重读的,切一次路由不该重跑 apply
|
|
26
|
+
- `@deepseek-ai/schemastery` 加为 **devDependency**(静态 import 需要它才能加载;
|
|
27
|
+
运行时依赖仍是零 —— 它由 DSH 宿主提供)
|
|
28
|
+
|
|
29
|
+
**配置迁移**:0.1.7 启动时把全局 `settings.yaml` 迁进 profile 的 `cordis.patch.yml`
|
|
30
|
+
并把原文件改名为 `settings.yaml.imported`。本插件的段需要一条 `dsh-llm-hub` entry;
|
|
31
|
+
未迁移时该段读为空,也就是上面那个症状。
|
|
32
|
+
|
|
33
|
+
**验证**:157 个单测 + 门禁全过;真机确认路由组、设置页路由卡、跨插件读(balance 能
|
|
34
|
+
解析出真实 key)、写路径(`routing/select` 200)全部恢复。
|
|
35
|
+
|
|
5
36
|
## [1.7.0] - 2026-09-29
|
|
6
37
|
|
|
7
38
|
### 🧭 路由设置卡:始终折叠,展开才编辑
|
package/lib/index.js
CHANGED
|
@@ -33,12 +33,14 @@
|
|
|
33
33
|
/** 官方直连适配器拥有的 settings 命名空间。 */
|
|
34
34
|
import { readFileSync } from 'node:fs'
|
|
35
35
|
import { fileURLToPath } from 'node:url'
|
|
36
|
+
import z from '@deepseek-ai/schemastery'
|
|
36
37
|
import { applyHarness, harnessReport } from './harness.js'
|
|
37
38
|
import { detectBillingMode, sortProviders } from './sorter.js'
|
|
38
39
|
import { AvailabilityStore, isDropDeadHidden, classifyFailure } from './availability-store.js'
|
|
39
40
|
|
|
40
41
|
export { detectBillingMode, sortProviders } from './sorter.js'
|
|
41
42
|
export { AvailabilityStore, isDropDeadHidden, classifyFailure } from './availability-store.js'
|
|
43
|
+
export { Config }
|
|
42
44
|
|
|
43
45
|
/**
|
|
44
46
|
* 用量统计 domain 的 spec。
|
|
@@ -657,66 +659,70 @@ export const name = 'dsh-llm-hub'
|
|
|
657
659
|
* 用量统计走的是**局部注入**:`storageDomain` 缺席时不报错,本插件其余功能照常工作
|
|
658
660
|
* —— 没有 storageDomain 也能用余额/可用性/harness,只是没法跨会话记 token 用量。
|
|
659
661
|
*/
|
|
662
|
+
/**
|
|
663
|
+
* 本插件的配置形态(DSH 0.1.7 的 settings 契约)。
|
|
664
|
+
*
|
|
665
|
+
* 0.1.7 起配置不再运行时 `settings.register()` 注册,而是由 Loader 按**条目 id**
|
|
666
|
+
* 从 profile 的 `cordis.patch.yml` 读取;插件导出这个 `Config`,`apply(ctx, config)`
|
|
667
|
+
* 收下 `config`,页面的表单与 `settings.update(ns, patch)` 也都按它生成。
|
|
668
|
+
* 字段与 1.5.0 起的段结构一一对应,所以老 settings.yaml 里的段能原样迁进 patch。
|
|
669
|
+
*
|
|
670
|
+
* note / notes:页面上写的备注。YAML 注释在页面保存时会被整段替换冲掉,
|
|
671
|
+
* 所以说明要落成字段才留得住。notes 以 'provider/model' 为键。
|
|
672
|
+
*/
|
|
673
|
+
const groupItem = z.object({
|
|
674
|
+
id: z.string().required(),
|
|
675
|
+
label: z.string(),
|
|
676
|
+
note: z.string(),
|
|
677
|
+
primary: z.string(),
|
|
678
|
+
fallbacks: z.array(z.string()),
|
|
679
|
+
notes: z.dict(z.string())
|
|
680
|
+
})
|
|
681
|
+
// ⚠️ 三个字段都必须 .volatile():0.1.7 的 settings.update() 拒绝「没有 volatile
|
|
682
|
+
// 字段的条目」(原话:Plugin entry "dsh-llm-hub" has no volatile fields)。
|
|
683
|
+
// volatile 的语义正是本插件要的行为 —— 配置改动只换值、不重跑 apply:路由组、预警
|
|
684
|
+
// 阈值、别名全是**每次请求惰性重读**的(见 hubSectionOf 的注释),让切一次路由就去
|
|
685
|
+
// 重新 apply 整个插件是白付代价。官方 dsh-llm-pi-ai 的 providers 段同样标了 volatile。
|
|
686
|
+
const Config = z.object({
|
|
687
|
+
routing: z.object({
|
|
688
|
+
mode: z.union(['manual', 'smart']).default('manual'),
|
|
689
|
+
activeGroup: z.string().default(''),
|
|
690
|
+
groups: z.array(groupItem).default([])
|
|
691
|
+
}).default({}).volatile(),
|
|
692
|
+
warning: z.object({ cashCNY: z.number().default(10), planPercent: z.number().default(10) }).default({}).volatile(),
|
|
693
|
+
aliases: z.dict(z.string()).default({}).volatile()
|
|
694
|
+
})
|
|
695
|
+
|
|
660
696
|
export const inject = ['llm', 'webServer']
|
|
661
697
|
|
|
662
698
|
/**
|
|
663
699
|
* 注册模型发现与余额路由。
|
|
664
700
|
* @param ctx - 插件上下文。
|
|
665
701
|
*/
|
|
666
|
-
export function apply(ctx) {
|
|
702
|
+
export function apply(ctx, config) {
|
|
667
703
|
|
|
668
704
|
// ── 插件自有 settings 命名空间(dsh-llm-hub)────────────────────────────
|
|
669
705
|
//
|
|
670
|
-
// 1.
|
|
671
|
-
//
|
|
672
|
-
//
|
|
673
|
-
//
|
|
674
|
-
//
|
|
675
|
-
//
|
|
676
|
-
// (schemastery 接受原生构造器作为 schema)。注册失败不 fatal:配置读取退
|
|
677
|
-
// 回 legacy 路径,智能路由降级为「只建议」。
|
|
678
|
-
let hubRegistered = false
|
|
679
|
-
ctx.inject(['settings'], (settingsCtx) => {
|
|
680
|
-
void (async () => {
|
|
681
|
-
let schema
|
|
682
|
-
try {
|
|
683
|
-
const mod = await import('@deepseek-ai/schemastery')
|
|
684
|
-
const z = (mod && typeof mod === 'object' ? mod.default ?? mod : mod)
|
|
685
|
-
// note / notes(1.7.0):页面上写的备注。YAML 注释在页面保存时会被整段替换冲掉,
|
|
686
|
-
// 所以说明要落成字段才留得住。notes 以 'provider/model' 为键。
|
|
687
|
-
const groupItem = z.object({ id: z.string().required(), label: z.string(), note: z.string(), primary: z.string(), fallbacks: z.array(z.string()), notes: z.dict(z.string()) })
|
|
688
|
-
schema = z.object({
|
|
689
|
-
routing: z.object({
|
|
690
|
-
mode: z.union(['manual', 'smart']).default('manual'),
|
|
691
|
-
activeGroup: z.string().default(''),
|
|
692
|
-
groups: z.array(groupItem).default([])
|
|
693
|
-
}).default({}),
|
|
694
|
-
warning: z.object({ cashCNY: z.number().default(10), planPercent: z.number().default(10) }).default({}),
|
|
695
|
-
aliases: z.dict(z.string()).default({})
|
|
696
|
-
})
|
|
697
|
-
} catch {
|
|
698
|
-
// 零依赖环境:schemastery 没解析到 —— 用原生构造器兜底。
|
|
699
|
-
schema = Object
|
|
700
|
-
}
|
|
701
|
-
try {
|
|
702
|
-
settingsCtx.settings.register(HUB_NS, schema)
|
|
703
|
-
hubRegistered = true
|
|
704
|
-
} catch (error) {
|
|
705
|
-
ctx.logger.warn(`dsh-llm-hub: settings ns "dsh-llm-hub" register failed: ${error?.message ?? error}`)
|
|
706
|
-
}
|
|
707
|
-
})()
|
|
708
|
-
})
|
|
706
|
+
// DSH 0.1.7 把 settings 架构整个换了:`settings.register(ns, schema)` 与
|
|
707
|
+
// `settings.get(ns)` 双双消失,配置改由 Loader 按**条目 id**从 profile 的
|
|
708
|
+
// cordis.patch.yml 读,插件自己导出 `Config`,再由 `apply(ctx, config)` 收下。
|
|
709
|
+
// 老写法在 0.1.7 上是 `TypeError: settings.register is not a function`,
|
|
710
|
+
// 被下面那个 catch 吞掉 → hubRegistered 恒 false → 整个 dsh-llm-hub 段读写
|
|
711
|
+
// 全死。对外表现是「路由组凭空消失」:数据好好躺在 patch 里,只是没人读。
|
|
709
712
|
|
|
710
713
|
/**
|
|
711
714
|
* 读 `dsh-llm-hub` 设置段(1.5.0 起的真源)。
|
|
712
715
|
* @returns 已解析设置段;未注册 / 未配置时为 `undefined`。
|
|
713
716
|
*/
|
|
714
717
|
const hubSectionOf = () => {
|
|
715
|
-
|
|
716
|
-
|
|
718
|
+
// 0.1.7:配置由 `apply(ctx, config)` 注入,惰性 .get() 每次拿当前修订。
|
|
719
|
+
// 不再走 settings.get(HUB_NS) —— 那个方法在 0.1.7 已被移除。
|
|
720
|
+
if (config === undefined || config === null) return undefined
|
|
717
721
|
try {
|
|
718
|
-
const
|
|
719
|
-
|
|
722
|
+
const routing = config.routing?.get?.()
|
|
723
|
+
const warning = config.warning?.get?.()
|
|
724
|
+
const aliases = config.aliases?.get?.()
|
|
725
|
+
return { routing, warning, aliases }
|
|
720
726
|
} catch {
|
|
721
727
|
return undefined
|
|
722
728
|
}
|
|
@@ -729,13 +735,31 @@ export function apply(ctx) {
|
|
|
729
735
|
* @returns 已解析的设置段;不可用时为 `undefined`。
|
|
730
736
|
*/
|
|
731
737
|
const sectionOf = () => {
|
|
738
|
+
// 0.1.7:读**别的插件**的配置不再有 settings.get(ns)。改走
|
|
739
|
+
// settings.describe() —— 它返回所有条目的 ns + 实时 value。
|
|
740
|
+
const settings = ctx.get('settings')
|
|
741
|
+
if (settings === undefined) return undefined
|
|
742
|
+
try {
|
|
743
|
+
const row = settings.describe().find((item) => item.ns === NS)
|
|
744
|
+
const value = row?.value
|
|
745
|
+
return value !== null && typeof value === 'object' ? value : undefined
|
|
746
|
+
} catch {
|
|
747
|
+
return undefined
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* 读任意命名空间(跨插件)在 0.1.7 的唯一途径。
|
|
753
|
+
* @param {string} ns - profile 条目 id。
|
|
754
|
+
* @returns {object | undefined} 该条目的实时配置值。
|
|
755
|
+
*/
|
|
756
|
+
const sectionOfNs = (ns) => {
|
|
732
757
|
const settings = ctx.get('settings')
|
|
733
758
|
if (settings === undefined) return undefined
|
|
734
759
|
try {
|
|
735
|
-
const value = settings.
|
|
760
|
+
const value = settings.describe().find((item) => item.ns === ns)?.value
|
|
736
761
|
return value !== null && typeof value === 'object' ? value : undefined
|
|
737
762
|
} catch {
|
|
738
|
-
// 命名空间尚未注册时会抛 —— 那是正常启动竞态,不是失败。
|
|
739
763
|
return undefined
|
|
740
764
|
}
|
|
741
765
|
}
|
|
@@ -1125,7 +1149,7 @@ export function apply(ctx) {
|
|
|
1125
1149
|
const settings = ctx.get('settings')
|
|
1126
1150
|
if (settings === undefined) return []
|
|
1127
1151
|
try {
|
|
1128
|
-
const section =
|
|
1152
|
+
const section = sectionOfNs(PIAI_NS)
|
|
1129
1153
|
const providers = section !== null && typeof section === 'object' ? section.providers : undefined
|
|
1130
1154
|
if (providers === null || typeof providers !== 'object') return []
|
|
1131
1155
|
return Object.entries(providers).filter(([, profile]) => profile !== null && typeof profile === 'object')
|
|
@@ -1145,7 +1169,7 @@ export function apply(ctx) {
|
|
|
1145
1169
|
const settings = ctx.get('settings')
|
|
1146
1170
|
if (settings === undefined) return undefined
|
|
1147
1171
|
try {
|
|
1148
|
-
const section =
|
|
1172
|
+
const section = sectionOfNs(PIAI_NS)
|
|
1149
1173
|
const providers = section !== null && typeof section === 'object' ? section.providers : undefined
|
|
1150
1174
|
const profile = providers !== null && typeof providers === 'object' ? providers[provider] : undefined
|
|
1151
1175
|
return profile !== null && typeof profile === 'object' ? profile : undefined
|
|
@@ -2204,10 +2228,10 @@ export function apply(ctx) {
|
|
|
2204
2228
|
// modelgo 这种 pi-ai 风格 provider 在 'llm-pi-ai' namespace 下;
|
|
2205
2229
|
// dsh-llm-hub 自己的 NS 是 'llm-deepseek',但写回属于跨 namespace 兜底:
|
|
2206
2230
|
// plugin 不是 pi-ai 的 owner,只是借用 settings 服务给 pi-ai 段补一份目录。
|
|
2207
|
-
const source =
|
|
2231
|
+
const source = sectionOfNs(PIAI_NS)
|
|
2208
2232
|
const updates = await populateEmptyProviderModels(source)
|
|
2209
2233
|
if (Object.keys(updates).length === 0) return
|
|
2210
|
-
const current =
|
|
2234
|
+
const current = sectionOfNs(PIAI_NS)
|
|
2211
2235
|
for (const provider of Object.keys(updates)) {
|
|
2212
2236
|
const live = current?.providers?.[provider]
|
|
2213
2237
|
if (Array.isArray(live?.models) && live.models.length > 0) {
|
|
@@ -2812,7 +2836,7 @@ export function apply(ctx) {
|
|
|
2812
2836
|
/** 读 settings 的 routing 段并按需覆写 mode / activeGroup,持久化。 */
|
|
2813
2837
|
const applyRoutingSelect = async (patch) => {
|
|
2814
2838
|
const settings = ctx.get('settings')
|
|
2815
|
-
if (settings === undefined
|
|
2839
|
+
if (settings === undefined) {
|
|
2816
2840
|
return { ok: false, error: 'settings 命名空间 dsh-llm-hub 尚未就绪 —— 稍后重试' }
|
|
2817
2841
|
}
|
|
2818
2842
|
const current = routingObjOf()
|
|
@@ -2930,7 +2954,7 @@ export function apply(ctx) {
|
|
|
2930
2954
|
/** 写回整份路由组(设置页编辑器保存路径),并返回最新状态。 */
|
|
2931
2955
|
const applyRoutingConfig = async (payload) => {
|
|
2932
2956
|
const settings = ctx.get('settings')
|
|
2933
|
-
if (settings === undefined
|
|
2957
|
+
if (settings === undefined) {
|
|
2934
2958
|
return { ok: false, error: 'settings 命名空间 dsh-llm-hub 尚未就绪 —— 稍后重试' }
|
|
2935
2959
|
}
|
|
2936
2960
|
const normalized = normalizeRoutingGroupsInput(payload?.groups)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-plugins/dsh-llm-hub",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "DSH 模型管理层:网关可达性探测、模型目录拉取、DeepSeek + MiniMax/智谱/Moonshot/StepFun 余额与配额、本月用量统计、余额预警、Provider 健康看板,再把本机装好的 codex / claude / agy 注册成子代理(装了才出现)。零运行时依赖,不改动 DSH 安装。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -79,5 +79,8 @@
|
|
|
79
79
|
"lib",
|
|
80
80
|
"scripts",
|
|
81
81
|
"types"
|
|
82
|
-
]
|
|
82
|
+
],
|
|
83
|
+
"devDependencies": {
|
|
84
|
+
"@deepseek-ai/schemastery": "3.18.4"
|
|
85
|
+
}
|
|
83
86
|
}
|