@gehennawu/dsh-service 2.0.8 → 2.0.10

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/icon.svg ADDED
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#4778f5" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
2
+ <rect x="2" y="2" width="20" height="8" rx="2"/>
3
+ <rect x="2" y="14" width="20" height="8" rx="2"/>
4
+ <path d="M6 6h.01M6 18h.01"/>
5
+ </svg>
package/index.js CHANGED
@@ -4035,6 +4035,7 @@ async function listSubagentModels(llm, agentDefaultModel) {
4035
4035
 
4036
4036
  /** 子代理路由配置的空档(inherit):与未安装本功能的原生行为完全一致。 */
4037
4037
  function createEmptySubagentRoute() {
4038
+ // 不含 preset 键:空预设不落盘,磁盘形状与 v2.0.8 及之前逐字节兼容(注入端按 absent = 跟随父代)。
4038
4039
  return { version: SUBAGENT_ROUTE_VERSION, mode: 'inherit' }
4039
4040
  }
4040
4041
 
@@ -4073,12 +4074,16 @@ function parseSubagentRouteText(text) {
4073
4074
  if (parsed.mode === 'custom' && (provider === '' || model === '')) return fallback
4074
4075
  // reasoningEffort 是可选的 adapter 自有等级 ID:只有字符串、trim 后非空才保留(空串视为默认)。
4075
4076
  const reasoningEffort = provider !== '' && model !== '' && typeof parsed.reasoningEffort === 'string' ? parsed.reasoningEffort.trim().slice(0, MAX_SUBAGENT_ROUTE_FIELD) : ''
4077
+ // 独立 Agent 预设(v2.0.9):与模式无关的全局字段;空串 = 跟随父代。存在性校验在保存端做
4078
+ // (磁盘配置容忍失效 id——旧宿主/预设被删时注入端按 provider 能力门跳过,不阻塞派生)。
4079
+ const preset = typeof parsed.preset === 'string' ? parsed.preset.trim().slice(0, MAX_SUBAGENT_ROUTE_FIELD) : ''
4076
4080
  return {
4077
4081
  version: SUBAGENT_ROUTE_VERSION,
4078
4082
  mode: parsed.mode,
4079
4083
  ...(provider !== '' && model !== '' ? { provider, model } : {}),
4080
4084
  ...(reasoningEffort !== '' ? { reasoningEffort } : {}),
4081
4085
  ...(fallbacks.length > 0 ? { fallbacks } : {}),
4086
+ ...(preset !== '' ? { preset } : {}),
4082
4087
  }
4083
4088
  }
4084
4089
 
@@ -4343,6 +4348,8 @@ function buildSubagentDispatchRecord(agent, parent, dispatch, options = {}) {
4343
4348
  }
4344
4349
  if (typeof turn === 'number' && Number.isFinite(turn)) record.turn = turn
4345
4350
  if (reasoningEffort !== undefined) record.reasoningEffort = reasoningEffort
4351
+ // 独立 Agent 预设(v2.0.9):仅记录插件注入的值;调用方显式携带时不记(来源不是本插件)。
4352
+ if (typeof dispatch?.preset === 'string' && dispatch.preset !== '') record.preset = dispatch.preset
4346
4353
  return record
4347
4354
  }
4348
4355
 
@@ -5162,7 +5169,9 @@ async function quotaCredentialEndpoint(ctx, refreshQuotaConfig, throttle, payloa
5162
5169
  try {
5163
5170
  await credentials.set(name, value)
5164
5171
  } catch (error) {
5165
- // 典型拒绝:进程环境层正在遮蔽该名字(describe().writable=false)——seam 契约的显式报错。
5172
+ // 可写性由宿主凭据服务裁决后回传:0.2.1-alpha.3 起托管存储优先、env 层不再只读,
5173
+ // 旧版「进程环境层遮蔽 ⇒ describe().writable=false 且 set 必拒」的成因已不适用;
5174
+ // 这里不预判成因,宿主报什么错就透传什么(seam 契约的显式报错)。
5166
5175
  return { ok: false, error: 'credential-write-failed', detail: sanitizeQuotaErrorDetail(error?.message) }
5167
5176
  }
5168
5177
  // 新凭据落库即清掉该 provider 的退避/冷却闸门:旧失败是旧凭据造成的,
@@ -5574,6 +5583,45 @@ function apply(ctx, featureConfig) {
5574
5583
  const effort = typeof reasoningEffort === 'string' && reasoningEffort !== '' ? reasoningEffort : undefined
5575
5584
  return { request: decorated, dispatch: { parent: request?.parent, provider: injected.provider, model: injected.model, ...(effort !== undefined ? { reasoningEffort: effort } : {}), routeCandidates: eligibleRoutes } }
5576
5585
  }
5586
+ // 独立 Agent 预设(v2.0.9):官方 0.2.1-alpha.3 起 startActivation 的请求面新增
5587
+ // request.agentPreset(统一设置子代理角色与工具;省略 = 跟随父代当前预设)。注入门完全
5588
+ // 对齐官方 tool-subagent 的 assertProvider:provider.capabilities.agentPreset === true 且
5589
+ // !provider.inheritsParentContext(fork 类继承父代上下文的 provider 不支持)。调用方显式
5590
+ // 携带的 agentPreset 永远赢。其余一切不明朗情形(服务缺席、清单读不到、预设已删/broken、
5591
+ // 能力不明)一律 fail-closed 不注入——给不支持的 provider 注入该字段会被宿主
5592
+ // assertCapabilities 拒绝、派生直接失败,「不生效」永远好过「失败」。每次派发现查清单
5593
+ // (预设集极小、变更低频),保证预设被删后自动回落跟随父代。仅 startActivation 路径装饰:
5594
+ // 旧版 start/startContinuable 的宿主请求类型没有该字段,其 provider 能力面也不含 agentPreset。
5595
+ const resolvePresetForSpec = async (spec, request) => {
5596
+ const configured = typeof subagentRouteRef.current.preset === 'string' ? subagentRouteRef.current.preset : ''
5597
+ if (configured === '') return undefined
5598
+ if (typeof request?.agentPreset === 'string' && request.agentPreset !== '') return undefined
5599
+ let declared
5600
+ try {
5601
+ const registry = ctx.get('agentPresets')
5602
+ if (registry === undefined || registry === null || typeof registry.list !== 'function') return undefined
5603
+ declared = await registry.list()
5604
+ } catch (_) {
5605
+ return undefined
5606
+ }
5607
+ const hit = (Array.isArray(declared) ? declared : []).find((entry) => entry !== null && typeof entry === 'object' && entry.id === configured)
5608
+ if (hit === undefined || (typeof hit.broken === 'string' && hit.broken !== '')) {
5609
+ ctx.logger?.info?.(`dsh-service: subagent preset "${configured}" skipped: not in the host preset list`)
5610
+ return undefined
5611
+ }
5612
+ let provider
5613
+ try {
5614
+ provider = typeof subagents.getProvider === 'function' ? subagents.getProvider(spec?.provider) : undefined
5615
+ } catch (_) {
5616
+ provider = undefined
5617
+ }
5618
+ if (provider === null || typeof provider !== 'object') return undefined
5619
+ if (provider.capabilities?.agentPreset !== true || provider.inheritsParentContext === true) {
5620
+ ctx.logger?.info?.(`dsh-service: subagent preset "${configured}" skipped: provider "${spec?.provider ?? ''}" does not support independent presets`)
5621
+ return undefined
5622
+ }
5623
+ return configured
5624
+ }
5577
5625
  const runWithDispatch = (dispatch, work) => {
5578
5626
  if (dispatch === undefined) return work()
5579
5627
  return pendingDispatchStorage.run(dispatch, work)
@@ -5586,7 +5634,10 @@ function apply(ctx, featureConfig) {
5586
5634
  await providerAvailability.loading
5587
5635
  const { request: decorated, dispatch } = applyInjection(spec.request)
5588
5636
  if (dispatch === undefined) return originalStartActivation.call(subagents, spec)
5589
- return runWithDispatch(dispatch, () => originalStartActivation.call(subagents, { ...spec, request: decorated }))
5637
+ const preset = await resolvePresetForSpec(spec, decorated)
5638
+ const finalRequest = preset !== undefined ? { ...decorated, agentPreset: preset } : decorated
5639
+ if (preset !== undefined) dispatch.preset = preset
5640
+ return runWithDispatch(dispatch, () => originalStartActivation.call(subagents, { ...spec, request: finalRequest }))
5590
5641
  }
5591
5642
  } else {
5592
5643
  subagents.start = async (name, request) => {
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "DSH Operations Panel",
4
+ "description": "DSH Web operations panel: safe restarts, health monitoring, backups, and Linux permission maintenance."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "DSH 运维面板",
4
+ "description": "DSH Web 运维面板:安全重启、健康监控、备份和 Linux 权限维护。"
5
+ }
6
+ }
@@ -279,6 +279,9 @@ export function createModelConfigRoutes({ ctx, workbenchStore, loadPiCatalog = l
279
279
  name: defaultEntry?.name ?? id,
280
280
  api: defaultEntry?.api ?? 'openai-completions',
281
281
  baseURL: defaultEntry?.baseUrl ?? '',
282
+ // 混合协议目录(v2.0.9):pi-ai 1.1.0 起 azure 等渠道单目录含多种 api。路由级 `api`
283
+ // 会覆盖该路由全部模型(官方语义),客户端据此提示「钉协议前先确认全部模型同协议」。
284
+ apis: [...new Set(records.map((x) => x.api).filter(Boolean))],
282
285
  models: records,
283
286
  }
284
287
  })
@@ -317,6 +320,11 @@ export function createModelConfigRoutes({ ctx, workbenchStore, loadPiCatalog = l
317
320
  }
318
321
  } catch (_) { /* Missing status must not prevent configuration editing. */ }
319
322
  if (builtin(catalog, id)) dto.catalogModels = [...catalog.byModelId.entries()].flatMap(([modelId, records]) => records.filter((x) => x.channel === id).map((x) => ({ id: modelId, api: x.api, ...displayProfile({ models: [{ id: modelId, ...x }] }).models[0] })))
323
+ // 混合协议目录(v2.0.9):≥2 种 api 才下发,客户端据它在路由编辑器里提示路由级 api 的覆盖语义。
324
+ if (Array.isArray(dto.catalogModels)) {
325
+ const apis = [...new Set(dto.catalogModels.map((entry) => entry.api).filter(Boolean))]
326
+ if (apis.length > 1) dto.catalogApis = apis
327
+ }
320
328
  dto.resolved = {
321
329
  displayName: effectiveRaw.displayName ?? dto.effective.displayName ?? defaultEntry?.name ?? id,
322
330
  api: dto.resolvedApi,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gehennawu/dsh-service",
3
- "version": "2.0.8",
3
+ "version": "2.0.10",
4
4
  "description": "DSH Web 运维面板:安全重启、健康监控、备份和 Linux 权限维护。",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -14,7 +14,9 @@
14
14
  "exports": {
15
15
  ".": "./index.js",
16
16
  "./client": "./client.js",
17
- "./package.json": "./package.json"
17
+ "./package.json": "./package.json",
18
+ "./locale/*.json": "./locale/*.json",
19
+ "./icon": "./icon.svg"
18
20
  },
19
21
  "files": [
20
22
  "index.js",
@@ -35,6 +37,8 @@
35
37
  "plugin-health.js",
36
38
  "client.js",
37
39
  "cordis.patch.yml",
40
+ "icon.svg",
41
+ "locale/*.json",
38
42
  "README.md",
39
43
  "README.en.md",
40
44
  "screenshots",
@@ -59,7 +63,7 @@
59
63
  "homepage": "https://github.com/gehennawu/dsh-service#readme",
60
64
  "engines": {
61
65
  "node": ">=22",
62
- "dsh": ">=0.1.1-rc.2 <0.2.1-alpha.3"
66
+ "dsh": ">=0.1.1-rc.2"
63
67
  },
64
68
  "dependencies": {
65
69
  "@deepseek-ai/schemastery": "^3.18.3"
package/quota-adapters.js CHANGED
@@ -1270,6 +1270,37 @@ async function fetchCliproxyUsage({ profile, config, credential, signal, request
1270
1270
  return windows
1271
1271
  }
1272
1272
 
1273
+ // Non-monetary points retain decimal strings instead of implying a currency or quota denominator.
1274
+ function normalizePoePoints(payload) {
1275
+ const raw = payload?.current_point_balance
1276
+ const text = typeof raw === 'string' ? raw : typeof raw === 'number' && Number.isFinite(raw) ? String(raw) : ''
1277
+ if (text.length > 64 || !/^-?\d+(?:\.\d+)?$/.test(text)) return { windows: [] }
1278
+ return { windows: [{ id: 'points', kindKey: 'points', text: `${text} points`, unit: 'points' }] }
1279
+ }
1280
+
1281
+ function normalizeVercelCredits(payload) {
1282
+ // The API declares USD decimal strings. Lifetime spend is not a quota denominator.
1283
+ const balance = payload?.balance
1284
+ if (typeof balance !== 'string' || balance.length > 64 || !/^\d+(?:\.\d+)?$/.test(balance)
1285
+ || !Number.isFinite(Number(balance)) || Number(balance) > Number.MAX_SAFE_INTEGER) return { windows: [] }
1286
+ return { windows: [{ id: 'balance', kindKey: 'balance', text: `$${balance}`, unit: 'usd' }] }
1287
+ }
1288
+
1289
+ function normalizeClinePassUsage(payload) {
1290
+ if (payload?.success !== true || !Array.isArray(payload?.data?.limits)) return { windows: [] }
1291
+ const windows = []
1292
+ for (const [type, id] of [['five_hour', 'five-hour'], ['weekly', 'weekly'], ['monthly', 'monthly']]) {
1293
+ const matches = payload.data.limits.filter((entry) => entry?.type === type)
1294
+ if (matches.length > 1) return { windows: [] }
1295
+ const entry = matches[0]
1296
+ if (typeof entry?.percentUsed !== 'number' || !Number.isFinite(entry.percentUsed) || entry.percentUsed < 0) continue
1297
+ const resetsAt = typeof entry.resetsAt === 'string' && /^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$/.test(entry.resetsAt)
1298
+ ? normalizeResetTimestamp(entry.resetsAt) : undefined
1299
+ windows.push({ id, percent: Math.min(100, Math.round(entry.percentUsed)), ...(resetsAt !== undefined ? { resetsAt } : {}) })
1300
+ }
1301
+ return { windows }
1302
+ }
1303
+
1273
1304
  function createQuotaAdapterCatalog() {
1274
1305
  return Object.freeze([
1275
1306
  createEndpointAdapter({
@@ -1330,6 +1361,30 @@ function createQuotaAdapterCatalog() {
1330
1361
  keyHints: ['SILICONFLOW_API_KEY'],
1331
1362
  hosts: ['siliconflow.cn'],
1332
1363
  }),
1364
+ createEndpointAdapter({
1365
+ kind: 'vercel-ai-gateway',
1366
+ normalize: normalizeVercelCredits,
1367
+ endpoints: ['https://ai-gateway.vercel.sh/v1/credits'],
1368
+ keyHints: ['AI_GATEWAY_API_KEY'],
1369
+ hosts: ['ai-gateway.vercel.sh'],
1370
+ routeIds: ['vercel-ai-gateway'],
1371
+ }),
1372
+ createEndpointAdapter({
1373
+ kind: 'poe',
1374
+ normalize: normalizePoePoints,
1375
+ endpoints: ['https://api.poe.com/usage/current_balance'],
1376
+ keyHints: ['POE_API_KEY'],
1377
+ hosts: ['api.poe.com'],
1378
+ routeIds: ['poe'],
1379
+ }),
1380
+ createEndpointAdapter({
1381
+ kind: 'clinepass',
1382
+ normalize: normalizeClinePassUsage,
1383
+ endpoints: ['https://api.cline.bot/api/v1/users/me/plan/usage-limits'],
1384
+ keyHints: ['CLINEPASS_API_KEY'],
1385
+ hosts: ['api.cline.bot'],
1386
+ routeIds: ['clinepass'],
1387
+ }),
1333
1388
  createEndpointAdapter({
1334
1389
  kind: 'deepseek',
1335
1390
  normalize: normalizeDeepseekBalance,
@@ -16,9 +16,10 @@ README 使用的中英文界面截图。文件名与对应功能如下:
16
16
  | `session-manager.png` | `session-manager_en.png` | 会话管理 |
17
17
  | `mobile-adaptation.png` | `mobile-adaptation_en.png` | 移动端适配 |
18
18
  | `plugin-config.png` | `plugin-config_en.png` | 插件配置 |
19
- | `maintenance.png` | `maintenance_en.png` | 六页导航的「维护」聚合页 |
20
- | `configuration.png` | `configuration_en.png` | 六页导航的「配置」聚合页 |
19
+ | `maintenance.png` | `maintenance_en.png` | 维护聚合页 |
20
+ | `configuration.png` | `configuration_en.png` | 配置聚合页 |
21
+ | `model-configuration.png` | `model-configuration_en.png` | 模型配置:渠道与模型 |
21
22
 
22
23
  截图宽度建议 800–1200px,PNG 格式。README 新增截图引用时,必须同时加入本目录并通过 `test/docs-static.test.js`;本目录由 `package.json#files` 打入 npm 包。
23
24
 
24
- 本目录截图已按 v0.40 六页信息架构更新。后续替换时保持现有文件名不变,并确保截图不暴露 API key、Cookie、Oasis-Token、管理密钥、会话正文或其他凭据/隐私数据。
25
+ 后续替换保持现有文件名,并确保截图不暴露 API key、Cookie、Oasis-Token、管理密钥、会话正文或其他凭据/隐私数据。
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -36,6 +36,24 @@ export function createSubagentRoutes({
36
36
  current = catalog.current
37
37
  }
38
38
  const config = subagentRouteRef.current
39
+ // 独立 Agent 预设(v2.0.9):清单来自宿主 agentPresets 服务(0.2.1-alpha.3+;旧宿主缺席
40
+ // ⇒ 空清单 + presetSupported:false,客户端禁用选择器)。broken 的声明照常下发但带标记,
41
+ // 保存端拒绝选中它。
42
+ let presets = []
43
+ let presetSupported = false
44
+ const presetRegistry = ctx.get('agentPresets')
45
+ if (presetRegistry !== undefined && presetRegistry !== null && typeof presetRegistry.list === 'function') {
46
+ presetSupported = true
47
+ try {
48
+ const declared = await presetRegistry.list()
49
+ presets = (Array.isArray(declared) ? declared : [])
50
+ .filter((entry) => entry !== null && typeof entry === 'object' && typeof entry.id === 'string' && entry.id !== '')
51
+ .map((entry) => ({ id: entry.id, ...(typeof entry.name === 'string' && entry.name !== '' ? { name: entry.name } : {}), ...(typeof entry.broken === 'string' && entry.broken !== '' ? { broken: entry.broken } : {}) }))
52
+ .sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
53
+ } catch (_) {
54
+ presets = []
55
+ }
56
+ }
39
57
  // 自定义路由草稿(v1.4.12):三模式统一下发——客户端据此回填表单,切换模式不丢配置。
40
58
  const routePresent = typeof config.provider === 'string' && config.provider !== '' && typeof config.model === 'string' && config.model !== ''
41
59
  return {
@@ -49,6 +67,9 @@ export function createSubagentRoutes({
49
67
  ...(typeof config.reasoningEffort === 'string' && config.reasoningEffort !== '' ? { reasoningEffort: config.reasoningEffort } : {}),
50
68
  } : {}),
51
69
  ...(Array.isArray(config.fallbacks) && config.fallbacks.length > 0 ? { fallbacks: config.fallbacks } : {}),
70
+ ...(typeof config.preset === 'string' && config.preset !== '' ? { preset: config.preset } : {}),
71
+ presetSupported,
72
+ presets,
52
73
  models,
53
74
  ...(current !== undefined ? { current } : {}),
54
75
  },
@@ -129,6 +150,36 @@ export function createSubagentRoutes({
129
150
  fallbacks.push(normalized)
130
151
  }
131
152
  }
153
+ // 独立 Agent 预设(v2.0.9):与模式无关,payload 携带 `preset` 键才生效(缺省 = 保留原值,
154
+ // 与 fallbacks 的草稿语义一致);空串 = 清除(跟随父代)。非空值必须在 agentPresets 清单内
155
+ // 且未 broken——宿主在派生端还会按 provider 能力二次把关,这里只挡「明显无效」的引用。
156
+ let preset
157
+ if (Object.prototype.hasOwnProperty.call(payload ?? {}, 'preset')) {
158
+ const rawPreset = payload.preset
159
+ if (rawPreset === undefined || rawPreset === null || rawPreset === '') {
160
+ preset = ''
161
+ } else if (typeof rawPreset !== 'string') {
162
+ return { ok: false, error: 'invalid-preset' }
163
+ } else {
164
+ const candidate = rawPreset.trim().slice(0, MAX_SUBAGENT_ROUTE_FIELD)
165
+ if (candidate === '') {
166
+ preset = ''
167
+ } else {
168
+ const registry = ctx.get('agentPresets')
169
+ if (registry === undefined || registry === null || typeof registry.list !== 'function') return { ok: false, error: 'preset-unavailable' }
170
+ let declared = []
171
+ try {
172
+ declared = await registry.list()
173
+ } catch (_) {
174
+ declared = []
175
+ }
176
+ const hit = (Array.isArray(declared) ? declared : []).find((entry) => entry !== null && typeof entry === 'object' && entry.id === candidate)
177
+ if (hit === undefined) return { ok: false, error: 'invalid-preset' }
178
+ if (typeof hit.broken === 'string' && hit.broken !== '') return { ok: false, error: 'invalid-preset' }
179
+ preset = candidate
180
+ }
181
+ }
182
+ }
132
183
  return await serializeSubagentRouteWrite(async (config) => {
133
184
  if (providerAvailability) {
134
185
  await providerAvailability.loading
@@ -152,12 +203,19 @@ export function createSubagentRoutes({
152
203
  if (fallbacks.length > 0) config.fallbacks = fallbacks
153
204
  else delete config.fallbacks
154
205
  }
206
+ // 预设:与模式无关;只有本轮 payload 携带了 `preset` 键才写/清。空值一律不落盘
207
+ // (磁盘形状与不认识该字段的旧版本逐字节兼容;注入端 absent = 跟随父代)。
208
+ if (preset !== undefined) {
209
+ if (preset === '') delete config.preset
210
+ else config.preset = preset
211
+ }
212
+ if (config.preset === '') delete config.preset
155
213
  const routePresent = typeof config.provider === 'string' && config.provider !== '' && typeof config.model === 'string' && config.model !== ''
156
214
  return { value: { ok: true, mode: config.mode, ...(routePresent ? {
157
215
  provider: config.provider,
158
216
  model: config.model,
159
217
  ...(typeof config.reasoningEffort === 'string' && config.reasoningEffort !== '' ? { reasoningEffort: config.reasoningEffort } : {}),
160
- } : {}), ...(Array.isArray(config.fallbacks) && config.fallbacks.length > 0 ? { fallbacks: config.fallbacks } : {}) } }
218
+ } : {}), ...(Array.isArray(config.fallbacks) && config.fallbacks.length > 0 ? { fallbacks: config.fallbacks } : {}), ...(typeof config.preset === 'string' && config.preset !== '' ? { preset: config.preset } : {}) } }
161
219
  })
162
220
  } catch (error) {
163
221
  return rpcTechnicalFailure(error)