@gehennawu/dsh-service 2.0.7 → 2.0.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gehennawu/dsh-service",
3
- "version": "2.0.7",
3
+ "version": "2.0.8",
4
4
  "description": "DSH Web 运维面板:安全重启、健康监控、备份和 Linux 权限维护。",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -25,6 +25,13 @@
25
25
  "quota-routes.js",
26
26
  "subagent-routes.js",
27
27
  "backup-routes.js",
28
+ "model-workbench.js",
29
+ "model-workbench-routes.js",
30
+ "model-workbench-settings.js",
31
+ "model-config-routes.js",
32
+ "provider-availability.js",
33
+ "model-config-schema.js",
34
+ "model-smart-fill.js",
28
35
  "plugin-health.js",
29
36
  "client.js",
30
37
  "cordis.patch.yml",
@@ -64,7 +71,8 @@
64
71
  "client": {
65
72
  "inject": [
66
73
  "@deepseek-ai/dsh-client-connection",
67
- "@deepseek-ai/dsh-api-remotes"
74
+ "@deepseek-ai/dsh-api-remotes",
75
+ "@deepseek-ai/dsh-client-ui-primitives"
68
76
  ],
69
77
  "platform": "web"
70
78
  }
@@ -0,0 +1,84 @@
1
+ // Profile-owned policy: configuration and credentials never move or disappear.
2
+ export function createProviderAvailability(store) {
3
+ let disabled = new Set()
4
+ let disabledModels = new Set()
5
+ const modelKey = (provider, model) => JSON.stringify([provider, model])
6
+ let ready = false
7
+ const loading = store.load().then((state) => {
8
+ disabled = new Set(Object.entries(state.disabledProviders ?? {}).filter(([, value]) => value === true).map(([id]) => id))
9
+ disabledModels = new Set(Object.entries(state.disabledModels ?? {}).filter(([, value]) => value === true).map(([key]) => key))
10
+ ready = true
11
+ })
12
+ const enabled = (provider) => !disabled.has(provider)
13
+ const modelEnabled = (provider, model) => !disabledModels.has(modelKey(provider, model))
14
+ const assertEnabled = (provider, model) => {
15
+ if (!ready || !enabled(provider) || model !== undefined && !modelEnabled(provider, model)) {
16
+ const code = !ready ? 'provider-availability-loading' : !enabled(provider) ? 'provider-disabled' : 'model-disabled'
17
+ const error = new Error(code)
18
+ error.code = code.toUpperCase().replaceAll('-', '_')
19
+ throw error
20
+ }
21
+ }
22
+ let changes = Promise.resolve()
23
+ const set = (provider, value, expected) => {
24
+ const result = changes.then(async () => {
25
+ await loading
26
+ if (enabled(provider) !== expected) throw new Error('conflict')
27
+ await store.mutate((state) => {
28
+ state.disabledProviders ??= {}
29
+ if (value) delete state.disabledProviders[provider]
30
+ else Object.defineProperty(state.disabledProviders, provider, { value: true, enumerable: true, configurable: true, writable: true })
31
+ })
32
+ if (value) disabled.delete(provider); else disabled.add(provider)
33
+ return { route: provider, enabled: value }
34
+ })
35
+ changes = result.catch(() => {})
36
+ return result
37
+ }
38
+ const setModel = (provider, model, value, expected) => {
39
+ const result = changes.then(async () => {
40
+ await loading
41
+ if (modelEnabled(provider, model) !== expected) throw new Error('conflict')
42
+ const key = modelKey(provider, model)
43
+ await store.mutate((state) => {
44
+ state.disabledModels ??= {}
45
+ if (value) delete state.disabledModels[key]
46
+ else Object.defineProperty(state.disabledModels, key, { value: true, enumerable: true, configurable: true, writable: true })
47
+ })
48
+ if (value) disabledModels.delete(key); else disabledModels.add(key)
49
+ return { route: provider, modelId: model, enabled: value }
50
+ })
51
+ changes = result.catch(() => {})
52
+ return result
53
+ }
54
+ return { loading, enabled, modelEnabled, assertEnabled, set, setModel }
55
+ }
56
+
57
+ // Filter the shared catalog and guard exact routing, including inherited routes.
58
+ // Keep every original method and restore it when the owning Fiber is disposed.
59
+ export function installProviderAvailability(llm, policy, notify = () => {}) {
60
+ const originals = new Map()
61
+ const wrappers = new Map()
62
+ const wrap = (key, factory) => {
63
+ if (typeof llm[key] !== 'function') return
64
+ const original = llm[key]
65
+ const wrapper = factory(original.bind(llm))
66
+ originals.set(key, original); wrappers.set(key, wrapper); llm[key] = wrapper
67
+ }
68
+ wrap('listProviders', (original) => () => original().filter((row) => policy.enabled(row.id)))
69
+ wrap('listModels', (original) => async (provider, ...args) => { await policy.loading; return policy.enabled(provider) ? (await original(provider, ...args)).filter((row) => policy.modelEnabled(provider, row.id)) : [] })
70
+ wrap('resolveModelInfo', (original) => async (provider, ...args) => { await policy.loading; policy.assertEnabled(provider, args[0]); return original(provider, ...args) })
71
+ wrap('resolveCallConfig', (original) => async (config, ...args) => { await policy.loading; policy.assertEnabled(config.provider, config.model); return original(config, ...args) })
72
+ wrap('prepareCall', (original) => async (config, ...args) => {
73
+ await policy.loading; policy.assertEnabled(config.provider, config.model)
74
+ const prepared = await original(config, ...args)
75
+ return { ...prepared, stream: async function* (options) {
76
+ policy.assertEnabled(config.provider, config.model)
77
+ yield* prepared.stream(options)
78
+ } }
79
+ })
80
+ wrap('stream', (original) => async function* (options) { await policy.loading; policy.assertEnabled(options.provider, options.model); yield* original(options) })
81
+ let disposed = false
82
+ policy.loading.then(() => { if (!disposed) notify() }, () => {})
83
+ return () => { disposed = true; for (const [key, original] of originals) if (llm[key] === wrappers.get(key)) llm[key] = original; notify() }
84
+ }
@@ -17,11 +17,13 @@ export function createSubagentRoutes({
17
17
  listSubagentDispatches,
18
18
  listSubagentModels,
19
19
  rpcTechnicalFailure,
20
+ providerAvailability,
20
21
  }) {
21
22
  return {
22
23
  'subagent-route': { feature: 'subagentRoute', handle: async (payload, rpcEndpoint) => {
23
24
  try {
24
25
  await subagentRouteLoadPromise
26
+ if (providerAvailability) await providerAvailability.loading
25
27
  const llm = ctx.get('llm')
26
28
  // 模型清单沿用 skills-models 的白名单口径(llm.listProviders × listModels,单渠道失败跳过),
27
29
  // 并对每个精确模型附加 adapter 的 reasoning metadata(resolveModelInfo 缺席时保留原目录项);
@@ -128,6 +130,11 @@ export function createSubagentRoutes({
128
130
  }
129
131
  }
130
132
  return await serializeSubagentRouteWrite(async (config) => {
133
+ if (providerAvailability) {
134
+ await providerAvailability.loading
135
+ if (primary.provider) providerAvailability.assertEnabled(primary.provider, primary.model)
136
+ for (const fallback of fallbacks) providerAvailability.assertEnabled(fallback.provider, fallback.model)
137
+ }
131
138
  config.mode = mode
132
139
  if (mode === 'custom') {
133
140
  config.provider = primary.provider