@gehennawu/dsh-service 0.25.0 → 0.27.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/README.en.md CHANGED
@@ -8,9 +8,9 @@ A service-control and operations plugin for self-hosted DSH Web. Provides safe r
8
8
 
9
9
  ## Features
10
10
 
11
- The Settings panel "Service Control" page has eight top-level tabs: **Overview, Notifications, Health, Model stats, Quota lookup, Backups, Skills, Restart**; the Restart, Quota lookup, and Skills tabs can each enable a quick entry at the bottom of the settings left navigation (off by default).
11
+ The Settings panel "Service Control" page has nine top-level tabs: **Overview, Notifications, Health, Model stats, Quota lookup, Backups, Skills, Subagents, Restart**; the Restart, Quota lookup, Skills, and Subagents tabs can each enable a quick entry at the bottom of the settings left navigation (off by default).
12
12
 
13
- The plugin also appears under **Plugins → Plugin configuration**, with six host-level switches enabled by default: **Model statistics, Quota lookup, Backup maintenance, Task notifications, Skill manager, and the `/healthz` liveness endpoint**. Disabling one hides its UI, stops the associated polling/subscriptions, and makes the Host reject that capability; Overview, Health diagnostics, and Restart remain available. All six switches are live settings: disabling or re-enabling them requires neither a page reload nor a DSH Web restart. Statistics refreshes, quota requests, or backup operations already in flight are allowed to finish; quota re-enablement preserves existing cache, TTL, and backoff state, so its UI and calls return immediately but a new upstream request is not guaranteed at once.
13
+ The plugin also appears under **Plugins → Plugin configuration**, with seven host-level switches enabled by default: **Model statistics, Quota lookup, Backup maintenance, Task notifications, Skill manager, Subagent model, and the `/healthz` liveness endpoint**. Disabling one hides its UI, stops the associated polling/subscriptions, and makes the Host reject that capability; Overview, Health diagnostics, and Restart remain available. All seven switches are live settings: disabling or re-enabling them requires neither a page reload nor a DSH Web restart. Statistics refreshes, quota requests, or backup operations already in flight are allowed to finish; quota re-enablement preserves existing cache, TTL, and backoff state, so its UI and calls return immediately but a new upstream request is not guaranteed at once.
14
14
 
15
15
  ### Version and updates
16
16
 
@@ -36,6 +36,8 @@ The plugin also appears under **Plugins → Plugin configuration**, with six hos
36
36
 
37
37
  ### Model statistics
38
38
 
39
+ ![Model statistics](./screenshots/model-usage_en.png)
40
+
39
41
  - 7-day stacked bar chart of input/output/cache tokens with blue/orange/teal legend
40
42
  - Filter by project; hover for exact values
41
43
  - Model breakdown as horizontal stacked bars (same legend colors) with a "Today / Last 7 days / All time" toggle on the right of the list header (default: last 7 days): Today aggregates only the current day, Last 7 days ranks within the chart's window, and All time covers every date in the index (bounded by session retention); each row keeps `x times · Cache hit x% · Input xM token · Output xM token`
@@ -44,7 +46,9 @@ The plugin also appears under **Plugins → Plugin configuration**, with six hos
44
46
 
45
47
  ### Quota lookup
46
48
 
47
- - A dedicated "Quota lookup" tab shows adapted providers as separate cards, each window rendered as a percentage with its own bar and a reset countdown on its own line, plus a refresh icon beside the updated time that force-refreshes that provider on click (bypassing the poll interval); each card header also has ↑/↓ buttons to reorder cards manually (auto-disabled at the top/bottom of the list) — the order is remembered in this browser and newly seen providers append at the end; unadapted providers take no space — a "Manual adapt" row at the bottom enables one by picking its type, and each card's footer can switch type, fall back to auto-detect, or disable lookup at any time
49
+ ![Quota lookup](./screenshots/quota-lookup_en.png)
50
+
51
+ - A dedicated "Quota lookup" tab shows adapted providers as separate cards, each window rendered as a percentage with its own bar and a reset countdown on its own line, plus a refresh icon beside the updated time that force-refreshes that provider on click (bypassing the poll interval); adapted card titles link to the provider's official usage page (DeepSeek Platform, Zhipu GLM Coding Plan, OpenCode Go) in a new tab; a "Reorder" toggle at the top-right of the card list (shown with two or more cards) reveals ↑/↓ buttons on each card header (auto-disabled at the ends), and clicking it again hides them; the order is remembered in this browser and newly seen providers append at the end; unadapted providers take no space — a "Manual adapt" row at the bottom enables one by picking its type, and each card's footer can switch type, fall back to auto-detect, or disable lookup at any time
48
52
  - A quota ring inside the conversation composer follows the provider selected by the current session and shows the tightest budget window as a percentage (green below 80%, amber at or above); clicking opens a panel headed by the provider name, with a bar and used percentage for each window and reset times on their own lines; on narrow viewports (phones) the panel switches to a viewport-centered floating layer — fully visible, scrolling internally when too tall — and rotates or resizing wider flips it back to the anchored placement above the ring
49
53
  - A "Show a remote quota entry in the settings left navigation" switch lives in the Quota lookup tab (off by default), mirroring the Restart entry
50
54
  - Built-in adaptations: **OpenCode Go** (`{baseURL}/usage`), **Zhipu GLM Coding Plan / zai-coding-cn** (official monitor `quota/limit` endpoint with three windows — 5-hour rolling tokens, weekly tokens, monthly MCP quota; an idle 5-hour window hides its reset time, matching the official console), **OpenRouter** (credits used %), **Kimi/Moonshot** and **SiliconFlow** (CNY balance text), **DeepSeek Platform** (official balance, see next bullet); dialects that natively report remaining percentage flip the panel word to "Remaining" and invert the warn threshold; transient network errors retry automatically and the Zhipu dual-domain candidate chain switches automatically; the provider-to-kind mapping lives in `DSH_HOME/dsh-service-quota.json`, and known services are auto-detected from their baseURL (e.g. opencode.ai, bigmodel.cn, api.deepseek.com) with no manual picking; no upstream request is ever made for unadapted or disabled providers; disabling can be done via "Disable" in a card's footer or by writing `"<provider>": null` in the config file (both are equivalent). Zhipu reset cards have no API-key-queryable endpoint yet — you can add any number of them via each provider card's "Add reset card" button on the tab (name plus expiry time, down to the minute), and remove each one independently — the ring panel shows them too (stored in the same config file); expired cards are flagged automatically
@@ -64,6 +68,8 @@ The plugin also appears under **Plugins → Plugin configuration**, with six hos
64
68
 
65
69
  ### Skills management
66
70
 
71
+ ![Skills management](./screenshots/skill-manager_en.png)
72
+
67
73
  - The "Skills" tab lists every local skill under three sections — **auto-loaded / manual-only / fully disabled** — scanning project `.dsh`, project `.agents`, user `~/.dsh/skills`, `$DSH_AGENTS_HOME`, and `$DSH_BUNDLED_SKILL_DIR` roots one level deep, with a name filter and source badges; same-name shadowing (lower rank wins) marks both winner and loser copies, bundled directories are shown read-only, and a physical directory hit by multiple root rules is counted once
68
74
  - Two per-entry switches edit the SKILL.md frontmatter directly: `disable-model-invocation` for "visible to model", `user-invocable` for "invocable via /"; switches use a two-click confirmation, changes go live within ~200 ms, active sessions receive a catalog-update notice on their next step, and toggling back and forth leaves no residue in the file
69
75
  - Entries carrying legacy camelCase invocation keys (e.g. `disableModelInvocation`) are dropped entirely by the official parser: the panel shows a ⚠ warning and a two-click confirmed fix that converts them to canonical keys semantically
@@ -71,8 +77,18 @@ The plugin also appears under **Plugins → Plugin configuration**, with six hos
71
77
  - One-click batch fill: automatically collects unannotated skills or ones whose body changed (read-only roots included; invalid entries and shadowed copies are skipped), shows candidate count / estimated payload and an expandable per-entry skip list first, then runs sequentially with live progress; individual failures never block the batch, and cancelling immediately interrupts the in-flight model call. Zero file modifications throughout, and the plugin never issues model calls autonomously
72
78
  - Batch runs in the Host background: switching tabs, closing the settings panel, or even refreshing the page never interrupts it; returning to the Skills tab restores progress and the cancel button, the Skills tab title shows a live `⟳done/total` badge while running, and planning a second batch mid-run is explicitly rejected
73
79
 
80
+ ### Subagent model
81
+
82
+ ![Subagent model](./screenshots/subagent-model_en.png)
83
+
84
+ - The "Subagents" tab controls routing for delegations that **did not explicitly specify a model**, with three modes: **Default (no override)** injects nothing and preserves DSH's native inheritance; **Follow main model** reads the provider/model actually used by the main conversation's latest request at delegation time; **Custom** pins every unspecified subagent to the selected provider and model
85
+ - A provider or model explicitly carried by the delegation always wins, so the plugin never overrides a pinned preset, call arguments, or a route already injected by another plugin. Custom choices come exclusively from the Host's live model catalog; if that provider is later removed, delegation safely falls back to native inheritance instead of failing
86
+ - Configuration is stored in `$DSH_HOME/dsh-service-subagent-route.json` (atomic writes, file mode `0600`); "Reset to default" clears the custom route and restores zero intervention. An optional "Subagents" quick entry can be enabled for the settings left navigation (off by default)
87
+
74
88
  ### Task notifications
75
89
 
90
+ ![Task notifications](./screenshots/task-notifications_en.png)
91
+
76
92
  - Notification settings live in the top-level Notifications tab: browser notification when a session finishes its turn, or when your approval, plan review, or answer is needed
77
93
  - Clicking a notification focuses the DSH page and closes the popup
78
94
  - Three toggle switches: master switch, task completion, and approvals & questions, each controlled independently
package/README.md CHANGED
@@ -8,9 +8,9 @@
8
8
 
9
9
  ## 功能
10
10
 
11
- 设置页「服务控制」面板包含八个顶部标签:**概览、通知、健康诊断、模型统计、额度查询、备份维护、技能、重启**;重启、额度查询与技能标签还可在设置页左侧标签列底部开启快捷入口(默认关闭)。
11
+ 设置页「服务控制」面板包含九个顶部标签:**概览、通知、健康诊断、模型统计、额度查询、备份维护、技能、子代理、重启**;重启、额度查询、技能与子代理标签还可在设置页左侧标签列底部开启快捷入口(默认关闭)。
12
12
 
13
- 插件同时出现在「插件 → 插件配置」,提供六个默认开启的宿主级开关:**模型统计、额度查询、备份维护、任务通知、技能管理、`/healthz` 探活端点**。关闭后不仅隐藏对应界面,也会停止相关轮询/订阅并由宿主拒绝对应能力;概览、健康诊断与重启固定保留。设置写入 DSH settings,六项均为热生效:关闭或重新开启都无需刷新页面或重启 DSH Web。已在途的统计刷新、额度请求或备份操作允许完成;额度重新开启时保留既有缓存、TTL 与退避状态,因此可立即恢复界面和调用,但不保证立刻重新请求上游。
13
+ 插件同时出现在「插件 → 插件配置」,提供七个默认开启的宿主级开关:**模型统计、额度查询、备份维护、任务通知、技能管理、子代理模型、`/healthz` 探活端点**。关闭后不仅隐藏对应界面,也会停止相关轮询/订阅并由宿主拒绝对应能力;概览、健康诊断与重启固定保留。设置写入 DSH settings,七项均为热生效:关闭或重新开启都无需刷新页面或重启 DSH Web。已在途的统计刷新、额度请求或备份操作允许完成;额度重新开启时保留既有缓存、TTL 与退避状态,因此可立即恢复界面和调用,但不保证立刻重新请求上游。
14
14
 
15
15
  ### 版本与更新
16
16
 
@@ -36,6 +36,8 @@
36
36
 
37
37
  ### 模型统计
38
38
 
39
+ ![模型统计](./screenshots/model-usage.png)
40
+
39
41
  - 近 7 天输入/输出/缓存 token 堆叠柱图,蓝/橙/青图例
40
42
  - 按项目筛选,鼠标悬停显示精确数值
41
43
  - 模型明细为横向堆叠柱形图(沿用主图图例配色),列表头部右侧提供「今日 / 近 7 天 / 累计」切换标签(默认近 7 天):今日只聚合当天,近 7 天按主图同窗口排序,累计覆盖索引内全部日期(受会话持久化留存范围限制);每行附「x次 · 缓存命中 x% · 输入 xM token · 输出 xM token」明细
@@ -44,7 +46,9 @@
44
46
 
45
47
  ### 额度查询
46
48
 
47
- - 独立的「额度查询」标签:以卡片分区展示已适配的供应商,每个窗口显示百分比、独立进度条,重置时间单独一行;卡片头部更新时间旁有刷新图标,点击即强制重拉该供应商(不受轮询间隔限制);卡片头部另有 ↑/↓ 排序按钮(首末卡对应方向自动禁用),顺序记忆在本浏览器,新出现的供应商排在末尾;未适配的不占位置,统一收进底部「手动适配」行选择类型启用,卡片脚部可随时切换适配类型、回退自动识别或停用查询
49
+ ![额度查询](./screenshots/quota-lookup.png)
50
+
51
+ - 独立的「额度查询」标签:以卡片分区展示已适配的供应商,每个窗口显示百分比、独立进度条,重置时间单独一行;卡片头部更新时间旁有刷新图标,点击即强制重拉该供应商(不受轮询间隔限制);已适配卡片的标题链接到对应官网用量页(DeepSeek 平台、智谱 GLM Coding Plan、OpenCode Go),点击新标签页打开;卡片列表右上角有「调整排序」开关(至少两张卡时显示),点击后各卡头部出现 ↑/↓ 按钮(首末卡对应方向自动禁用),再点一次收起;顺序记忆在本浏览器,新出现的供应商排在末尾;未适配的不占位置,统一收进底部「手动适配」行选择类型启用,卡片脚部可随时切换适配类型、回退自动识别或停用查询
48
52
  - 对话输入框内一枚额度圆环,跟随当前会话所选模型的供应商,显示最紧预算窗口的已用百分比(<80% 绿色、≥80% 黄色);点击弹出面板——头部标明供应商,各窗口带独立进度条与已用百分比、重置时间单独一行;手机等窄屏上面板自动切换为视口居中的浮层(完整可见、超高时内部滚动),旋转或拖宽窗口即时切回圆环上方锚定
49
53
  - 可在「额度查询」标签开启「设置页左列显示入口」开关(默认关闭),开启后在设置页左侧标签列底部显示「额度查询」快捷入口(与「重启」入口同模式)
50
54
  - 内置适配:**OpenCode Go**(`{baseURL}/usage`)、**智谱 GLM Coding Plan / zai-coding-cn**(官方监控端点 `quota/limit`,含 5 小时滚动 Token、每周 Token、MCP 月度配额三个窗口,5 小时窗口空闲时与官网一致地不显示重置时间)、**OpenRouter**(credits 已用%)、**Kimi/Moonshot** 与 **硅基流动**(人民币余额)、**DeepSeek 开放平台**(官方余额,见下一条);原生报「剩余百分比」的方言会自动把面板头部切换为「剩余」并把预警阈值反向;上游瞬时网络错误自动重试,智谱双域候选链自动切换;供应商与适配类型的对应关系保存在 `DSH_HOME/dsh-service-quota.json`,已知服务商按 baseURL 自动识别适配(如 opencode.ai、bigmodel.cn、api.deepseek.com),无需手选;未适配或已停用的供应商不会被发起任何上游请求,停用可在卡片脚部选「停用查询」或在配置文件写 `"<provider>": null`(两者等价)。智谱的重置卡暂无 API Key 可查的接口,可在「额度查询」标签内点各供应商卡片的「添加重置卡」填写名称与到期时间(可精确到分钟),可连续添加多条、每条独立移除;圆环面板同步显示(数据存入同一配置文件),过期自动标注
@@ -64,6 +68,8 @@
64
68
 
65
69
  ### 技能管理
66
70
 
71
+ ![技能管理](./screenshots/skill-manager.png)
72
+
67
73
  - 「技能」标签按**自动加载 / 仅手动调用 / 完全停用**三区展示全部本地技能(项目 `.dsh`、项目 `.agents`、用户 `~/.dsh/skills`、`$DSH_AGENTS_HOME` 与 `$DSH_BUNDLED_SKILL_DIR` 根,一层深度),支持名称过滤与来源徽标;同名遮蔽(低 rank 优先)与被遮蔽副本均有标注,内置目录只读展示;同一物理目录被多条规则命中时只计一次
68
74
  - 每条目双开关直接改写 SKILL.md frontmatter:`disable-model-invocation` 控制「对模型可见」,`user-invocable` 控制「可被 / 调用」;开关采用两段式点击确认,改动约 200ms 内热生效,活跃会话下一步自动收到目录更新提示;往返切换不会在文件中累积残留
69
75
  - 带 camelCase 旧版调用键(如 `disableModelInvocation`)的条目会被官方解析器整条剔除:面板给出 ⚠ 提示,两段式确认后按语义换算修复为规范键
@@ -71,8 +77,18 @@
71
77
  - 一键批量补全:自动圈出未注释或正文有变的技能(含只读目录,跳过无效与被遮蔽副本),先展示候选数/预计发送量与可展开的逐条跳过清单,确认后顺序执行并显示进度,单条失败不影响批次,可随时取消(取消会立即中断在途模型调用);全程零文件改动,也绝不自动发起任何模型调用
72
78
  - 批量任务在宿主后台持续运行:切换标签、关闭设置面板甚至刷新页面都不会中断;回到技能页自动恢复进度与取消按钮,批量进行中「技能」标签标题实时显示 `⟳已完成/总数` 角标,运行中重复生成计划会被明确拒绝
73
79
 
80
+ ### 子代理模型
81
+
82
+ ![子代理模型](./screenshots/subagent-model.png)
83
+
84
+ - 「子代理」标签控制**未显式指定模型**的子代理委派路由,支持三种模式:**初始(不干预)**不注入任何配置并完整保留 DSH 原生继承;**跟随主模型**在每次派生时读取主对话最近一次请求实际使用的 provider/model;**自定义**把所有未指定路由的子代理固定到所选供应商与模型
85
+ - 派生请求自身显式指定 provider 或 model 时始终优先,插件不会覆盖预设钉死、调用参数或其他插件已经注入的路由。自定义模型只能从宿主实时模型清单中选择;已配置供应商后来被卸载时安全回落原生继承,不会让子代理创建失败
86
+ - 配置保存在 `$DSH_HOME/dsh-service-subagent-route.json`(原子写入、文件权限 `0600`);「重置回初始配置」会清除自定义路由并恢复零干预。可在该标签开启设置页左列「子代理」快捷入口(默认关闭)
87
+
74
88
  ### 任务通知
75
89
 
90
+ ![任务通知](./screenshots/task-notifications.png)
91
+
76
92
  - 通知设置位于服务控制顶部的「通知」标签:会话完成一轮任务、或需要你授权/审阅计划/选择答案时发送浏览器通知
77
93
  - 点击系统通知弹窗会聚焦到 DSH 页面并关闭该通知
78
94
  - 三档开关条:总开关、任务结束通知、授权与提问通知,各自独立控制
package/client.js CHANGED
@@ -23,6 +23,33 @@ window.__ModuleLoader__.load({
23
23
  'features.skillManager': '技能管理',
24
24
  'features.readOnly': '当前设置不可写。',
25
25
  'tabs.skills': '技能',
26
+ 'tabs.subagent': '子代理',
27
+ 'features.subagentRoute': '子代理模型',
28
+ 'subagent.title': '子代理模型',
29
+ 'subagent.hint': '控制「未显式指定模型」的子代理委派使用哪个模型;显式指定了模型的派生(预设钉死、其他插件注入、调用参数携带)不受影响。',
30
+ 'subagent.mode.label': '模式',
31
+ 'subagent.mode.inherit': '初始(不干预)',
32
+ 'subagent.mode.inherit.desc': '不注入任何路由,保持宿主原生继承行为:子代理使用会话创建时烘焙的默认模型。',
33
+ 'subagent.mode.follow': '跟随主模型',
34
+ 'subagent.mode.follow.desc': '每次派生时读取主对话当前实际使用的模型(最近一次请求的渠道)并注入。',
35
+ 'subagent.mode.custom': '自定义',
36
+ 'subagent.mode.custom.desc': '所有未显式指定模型的子代理固定使用下方选择的模型。',
37
+ 'subagent.provider': '供应商',
38
+ 'subagent.model': '模型',
39
+ 'subagent.modelsEmpty': '模型清单为空:无法解析宿主 LLM 渠道。',
40
+ 'subagent.save': '保存',
41
+ 'subagent.saved': '已保存',
42
+ 'subagent.reset': '重置回初始配置',
43
+ 'subagent.saving': '保存中…',
44
+ 'subagent.unavailable': '宿主未提供子代理注册表(subagents 服务缺席),配置不会生效。',
45
+ 'subagent.navToggle': '设置页左列显示「子代理」入口',
46
+ 'subagent.navToggleHint': '默认关闭;开启后在设置页左侧标签列底部显示子代理模型快捷入口',
47
+ 'subagent.error': '操作失败:{error}',
48
+ 'subagent.error.feature-disabled': '子代理模型功能已在设置中关闭',
49
+ 'subagent.error.llm-unavailable': '宿主 LLM 服务不可用',
50
+ 'subagent.error.unknown-mode': '未知模式',
51
+ 'subagent.error.invalid-model-route': '供应商或模型不在宿主清单内',
52
+ 'subagent.error.network': '网络错误:无法连接宿主',
26
53
  'skills.error': '操作失败:{error}',
27
54
  'skills.error.feature-disabled': '技能管理功能已在设置中关闭',
28
55
  'skills.error.network': '网络错误,请稍后重试',
@@ -406,6 +433,8 @@ window.__ModuleLoader__.load({
406
433
  'quota.refresh': '刷新',
407
434
  'quota.card.moveUp': '上移',
408
435
  'quota.card.moveDown': '下移',
436
+ 'quota.reorder': '调整排序',
437
+ 'quota.usageLink': '打开官网用量页',
409
438
  'quota.adapt': '适配',
410
439
  'quota.kind.opencode-go': 'OpenCode Go',
411
440
  'quota.kind.zai-coding-cn': '智谱 GLM Coding Plan',
@@ -477,6 +506,33 @@ window.__ModuleLoader__.load({
477
506
  'features.skillManager': 'Skill manager',
478
507
  'features.readOnly': 'These settings are read-only.',
479
508
  'tabs.skills': 'Skills',
509
+ 'tabs.subagent': 'Subagents',
510
+ 'features.subagentRoute': 'Subagent model',
511
+ 'subagent.title': 'Subagent model',
512
+ 'subagent.hint': 'Controls which model a subagent delegation uses when no model was explicitly specified; delegations with an explicit route (pinned preset, another plugin, call arguments) are unaffected.',
513
+ 'subagent.mode.label': 'Mode',
514
+ 'subagent.mode.inherit': 'Default (no override)',
515
+ 'subagent.mode.inherit.desc': 'Injects nothing and keeps the native inheritance: subagents use the model baked in when the session was created.',
516
+ 'subagent.mode.follow': 'Follow main model',
517
+ 'subagent.mode.follow.desc': 'Each delegation reads the model the main conversation actually uses right now (route of its latest request) and injects it.',
518
+ 'subagent.mode.custom': 'Custom',
519
+ 'subagent.mode.custom.desc': 'Every delegation without an explicit model uses the model selected below.',
520
+ 'subagent.provider': 'Provider',
521
+ 'subagent.model': 'Model',
522
+ 'subagent.modelsEmpty': 'Model list is empty: host LLM channels cannot be resolved.',
523
+ 'subagent.save': 'Save',
524
+ 'subagent.saved': 'Saved',
525
+ 'subagent.reset': 'Reset to default',
526
+ 'subagent.saving': 'Saving…',
527
+ 'subagent.unavailable': 'The host exposes no subagents registry (service missing); this configuration has no effect.',
528
+ 'subagent.navToggle': 'Show "Subagents" entry in settings left nav',
529
+ 'subagent.navToggleHint': 'Off by default; when enabled, a subagent-model entry appears at the bottom of the settings left navigation',
530
+ 'subagent.error': 'Operation failed: {error}',
531
+ 'subagent.error.feature-disabled': 'Subagent model is switched off in settings',
532
+ 'subagent.error.llm-unavailable': 'Host LLM service is unavailable',
533
+ 'subagent.error.unknown-mode': 'Unknown mode',
534
+ 'subagent.error.invalid-model-route': 'Provider or model is not in the host catalog',
535
+ 'subagent.error.network': 'Network error: cannot reach the host',
480
536
  'skills.error': 'Operation failed: {error}',
481
537
  'skills.error.feature-disabled': 'Skill manager is switched off in settings',
482
538
  'skills.error.network': 'Network error, try again later',
@@ -860,6 +916,8 @@ window.__ModuleLoader__.load({
860
916
  'quota.refresh': 'Refresh',
861
917
  'quota.card.moveUp': 'Move up',
862
918
  'quota.card.moveDown': 'Move down',
919
+ 'quota.reorder': 'Reorder',
920
+ 'quota.usageLink': 'Open the official usage page',
863
921
  'quota.adapt': 'Adapt',
864
922
  'quota.kind.opencode-go': 'OpenCode Go',
865
923
  'quota.kind.zai-coding-cn': 'Zhipu GLM Coding Plan',
@@ -932,6 +990,8 @@ window.__ModuleLoader__.load({
932
990
  const NAV_ICON_BODY_RESTART = '%3Cpath d=%27M12 2v10%27/%3E%3Cpath d=%27M18.4 6.6a9 9 0 1 1-12.77.04%27/%3E'
933
991
  // 技能 = 书本轮廓(lucide book 风格,16px 下可读)
934
992
  const NAV_ICON_BODY_SKILLS = '%3Cpath d=%27M4 19.5A2.5 2.5 0 0 1 6.5 17H20%27/%3E%3Cpath d=%27M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z%27/%3E'
993
+ // 子代理 = 机器人头(lucide bot 风格,16px 下可读)
994
+ const NAV_ICON_BODY_SUBAGENT = '%3Cpath d=%27M12 8V4H8%27/%3E%3Crect width=%2716%27 height=%2712%27 x=%274%27 y=%278%27 rx=%272%27/%3E%3Cpath d=%27M2 14h2%27/%3E%3Cpath d=%27M20 14h2%27/%3E%3Cpath d=%27M15 13v2%27/%3E%3Cpath d=%27M9 13v2%27/%3E'
935
995
 
936
996
  function markSettingsNavRows(rows) {
937
997
  if (typeof document === 'undefined' || !document.body) return () => {}
@@ -990,11 +1050,12 @@ window.__ModuleLoader__.load({
990
1050
  // 设置页导航行图标:外壳按 id 硬编码(第三方一律兜底齿轮)且协议无 icon 字段,
991
1051
  // 由 markSettingsNavRows 打的 data 标记接住——藏齿轮 SVG、mask SVG 画各自图标,
992
1052
  // currentColor 跟随主题文字色(hover/active 高亮自动继承)。
993
- '[data-dsh-service-nav]>svg:first-child,[data-dsh-service-quota-nav]>svg:first-child,[data-dsh-service-restart-nav]>svg:first-child,[data-dsh-service-skills-nav]>svg:first-child{display:none}',
994
- '[data-dsh-service-nav]::before,[data-dsh-service-quota-nav]::before,[data-dsh-service-restart-nav]::before,[data-dsh-service-skills-nav]::before{content:\'\';flex:none;width:16px;height:16px;background:currentColor}',
1053
+ '[data-dsh-service-nav]>svg:first-child,[data-dsh-service-quota-nav]>svg:first-child,[data-dsh-service-restart-nav]>svg:first-child,[data-dsh-service-skills-nav]>svg:first-child,[data-dsh-service-subagent-nav]>svg:first-child{display:none}',
1054
+ '[data-dsh-service-nav]::before,[data-dsh-service-quota-nav]::before,[data-dsh-service-restart-nav]::before,[data-dsh-service-skills-nav]::before,[data-dsh-service-subagent-nav]::before{content:\'\';flex:none;width:16px;height:16px;background:currentColor}',
995
1055
  '[data-dsh-service-nav]::before{' + navIconMask(NAV_ICON_BODY_SERVICE) + '}',
996
1056
  '[data-dsh-service-quota-nav]::before{' + navIconMask(NAV_ICON_BODY_QUOTA) + '}',
997
1057
  '[data-dsh-service-skills-nav]::before{' + navIconMask(NAV_ICON_BODY_SKILLS) + '}',
1058
+ '[data-dsh-service-subagent-nav]::before{' + navIconMask(NAV_ICON_BODY_SUBAGENT) + '}',
998
1059
  '[data-dsh-service-restart-nav]::before{' + navIconMask(NAV_ICON_BODY_RESTART) + '}',
999
1060
  ].join('')
1000
1061
  document.head.appendChild(svcStyle)
@@ -1002,7 +1063,7 @@ window.__ModuleLoader__.load({
1002
1063
  ctx.effect(() => () => { if (svcStyle) svcStyle.remove() }, 'dsh-service theme styles')
1003
1064
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-service dictionaries')
1004
1065
  const t = ctx.locale.bind(NS)
1005
- const DEFAULT_FEATURES = { modelUsage: true, quotaLookup: true, backupMaintenance: true, taskNotifications: true, healthz: true, skillManager: true }
1066
+ const DEFAULT_FEATURES = { modelUsage: true, quotaLookup: true, backupMaintenance: true, taskNotifications: true, healthz: true, skillManager: true, subagentRoute: true }
1006
1067
  const featureScope = ctx.settingsScope.bind({ namespace: NS })
1007
1068
  const featureSnapshot = () => featureScope.getSnapshot()
1008
1069
  const featureValue = () => Object.assign({}, DEFAULT_FEATURES, featureSnapshot().value || {})
@@ -1019,6 +1080,7 @@ window.__ModuleLoader__.load({
1019
1080
  { attr: 'data-dsh-service-quota-nav', label: () => t('tabs.quota') },
1020
1081
  { attr: 'data-dsh-service-restart-nav', label: () => t('nav.restart') },
1021
1082
  { attr: 'data-dsh-service-skills-nav', label: () => t('tabs.skills') },
1083
+ { attr: 'data-dsh-service-subagent-nav', label: () => t('tabs.subagent') },
1022
1084
  ]),
1023
1085
  'dsh-service settings nav icons',
1024
1086
  )
@@ -1092,6 +1154,7 @@ window.__ModuleLoader__.load({
1092
1154
  const restartNavToggle = createNavEntryToggle({ storageKey: 'dsh-service-restart-nav', sectionId: 'dsh-service-restart', order: 499, labelKey: 'nav.restart', renderContent: () => React.createElement(RestartSection, null) })
1093
1155
  const quotaNavToggle = createNavEntryToggle({ storageKey: 'dsh-service-quota-nav', sectionId: 'dsh-service-quota', order: 498, labelKey: 'tabs.quota', feature: 'quotaLookup', renderContent: () => React.createElement(QuotaSection, null) })
1094
1156
  const skillsNavToggle = createNavEntryToggle({ storageKey: 'dsh-service-skills-nav', sectionId: 'dsh-service-skills', order: 497, labelKey: 'tabs.skills', feature: 'skillManager', renderContent: () => React.createElement(SkillsSection, null) })
1157
+ const subagentNavToggle = createNavEntryToggle({ storageKey: 'dsh-service-subagent-nav', sectionId: 'dsh-service-subagent', order: 496, labelKey: 'tabs.subagent', feature: 'subagentRoute', renderContent: () => React.createElement(SubagentSection, null) })
1095
1158
  // ── 批量补全共享状态:跨标签/设置面板开关存活(宿主任务本身不随 UI 停止)──
1096
1159
  let skillsBatchState = null // 宿主状态快照
1097
1160
  let skillsBatchPlan = null // 本端计划(含所选模型)
@@ -2370,12 +2433,153 @@ window.__ModuleLoader__.load({
2370
2433
  }, React.createElement('path', { d: 'M3 5.25 7 9l4-3.75', fill: 'none', stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round', strokeLinejoin: 'round' }))),
2371
2434
  open ? React.createElement('div', { style: { margin: '0 16px', padding: '12px 0 8px', borderTop: '1px solid var(--dsw-alias-border-l2)' } },
2372
2435
  React.createElement('div', { style: { fontSize: '12px', fontWeight: 700 } }, translate('features.optional')),
2373
- ['modelUsage', 'quotaLookup', 'backupMaintenance', 'taskNotifications', 'skillManager'].map(row),
2436
+ ['modelUsage', 'quotaLookup', 'backupMaintenance', 'taskNotifications', 'skillManager', 'subagentRoute'].map(row),
2374
2437
  React.createElement('div', { style: { fontSize: '12px', fontWeight: 700, marginTop: '8px', paddingTop: '10px', borderTop: '1px solid var(--dsw-alias-border-l1)' } }, translate('features.external')),
2375
2438
  row('healthz'),
2376
2439
  !writable ? React.createElement('p', { style: { margin: '6px 0 0', fontSize: '11px', color: 'var(--dsw-alias-label-tertiary)' } }, translate('features.readOnly')) : null) : null)
2377
2440
  }
2378
2441
 
2442
+ // ─── 子代理模型(v0.27):三态路由配置 ────────────────────────────────
2443
+ function mapSubagentError(translate, code) {
2444
+ if (code === 'feature-disabled') return translate('subagent.error.feature-disabled')
2445
+ if (code === 'llm-unavailable') return translate('subagent.error.llm-unavailable')
2446
+ if (code === 'unknown-mode') return translate('subagent.error.unknown-mode')
2447
+ if (code === 'invalid-model-route') return translate('subagent.error.invalid-model-route')
2448
+ if (code === 'network') return translate('subagent.error.network')
2449
+ return code
2450
+ }
2451
+
2452
+ const SUBAGENT_MODES = ['inherit', 'follow', 'custom']
2453
+ function SubagentSection() {
2454
+ const translate = useTranslation()
2455
+ const { useState, useEffect } = React
2456
+ const [navEnabled, setNavEnabled] = subagentNavToggle.useEnabled()
2457
+ const [snapshot, setSnapshot] = useState(null)
2458
+ const [mode, setMode] = useState('inherit')
2459
+ const [provider, setProvider] = useState('')
2460
+ const [model, setModel] = useState('')
2461
+ const [loading, setLoading] = useState(true)
2462
+ const [saving, setSaving] = useState(false)
2463
+ const [savedTick, setSavedTick] = useState(0)
2464
+ const [error, setError] = useState('')
2465
+ const hintStyle = { color: 'var(--dsw-alias-label-secondary)', fontSize: '12px', marginTop: '8px', lineHeight: 1.5 }
2466
+ const selectStyle = { fontSize: '12px', padding: '4px 8px', borderRadius: '6px', border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-primary)', maxWidth: '100%' }
2467
+
2468
+ const load = async () => {
2469
+ setLoading(true)
2470
+ try {
2471
+ const res = await ctx.connection.rpc.call('/dsh-service', 'subagent-route', {})
2472
+ if (res.ok) {
2473
+ setSnapshot(res.value)
2474
+ setMode(res.value.mode)
2475
+ if (res.value.mode === 'custom') {
2476
+ setProvider(typeof res.value.provider === 'string' ? res.value.provider : '')
2477
+ setModel(typeof res.value.model === 'string' ? res.value.model : '')
2478
+ }
2479
+ setError('')
2480
+ } else {
2481
+ setError(res.error || 'unknown')
2482
+ }
2483
+ } catch (_) {
2484
+ setError('network')
2485
+ } finally {
2486
+ setLoading(false)
2487
+ }
2488
+ }
2489
+ useEffect(() => { void load() }, [])
2490
+
2491
+ const models = snapshot !== null && Array.isArray(snapshot.models) ? snapshot.models : []
2492
+ // 供应商分组:清单顺序去重;模型下拉随供应商切换。
2493
+ const providers = []
2494
+ const providerName = {}
2495
+ for (const item of models) {
2496
+ if (providerName[item.provider] === undefined) {
2497
+ providerName[item.provider] = item.providerName
2498
+ providers.push(item.provider)
2499
+ }
2500
+ }
2501
+ const providerModels = models.filter((item) => item.provider === provider)
2502
+ // 换供应商时若当前模型不属于它,回落到该供应商首个模型。
2503
+ useEffect(() => {
2504
+ if (provider !== '' && !providerModels.some((item) => item.id === model)) {
2505
+ setModel(providerModels[0]?.id ?? '')
2506
+ }
2507
+ }, [provider])
2508
+ const effectiveProvider = providers.includes(provider) ? provider : providers[0] ?? ''
2509
+ useEffect(() => { if (provider !== effectiveProvider) setProvider(effectiveProvider) }, [effectiveProvider, provider])
2510
+
2511
+ const save = async (nextMode) => {
2512
+ setSaving(true)
2513
+ setError('')
2514
+ try {
2515
+ const payload = nextMode === 'custom'
2516
+ ? { mode: 'custom', provider: effectiveProvider, model }
2517
+ : { mode: nextMode }
2518
+ const res = await ctx.connection.rpc.call('/dsh-service', 'subagent-route-save', payload)
2519
+ if (res.ok) {
2520
+ setSavedTick((tick) => tick + 1)
2521
+ await load()
2522
+ } else {
2523
+ setError(res.error || 'unknown')
2524
+ }
2525
+ } catch (_) {
2526
+ setError('network')
2527
+ } finally {
2528
+ setSaving(false)
2529
+ }
2530
+ }
2531
+
2532
+ const cardStyle = { padding: '4px 0 14px', marginBottom: '12px', color: 'var(--dsw-alias-label-primary)' }
2533
+ const modeButton = (candidate) => React.createElement('button', {
2534
+ type: 'button',
2535
+ key: candidate,
2536
+ 'data-testid': 'subagent-mode-' + candidate,
2537
+ 'aria-pressed': String(mode === candidate),
2538
+ disabled: loading,
2539
+ onClick: () => setMode(candidate),
2540
+ style: {
2541
+ fontSize: '12.5px', padding: '5px 12px', borderRadius: '7px', cursor: loading ? 'default' : 'pointer',
2542
+ border: mode === candidate ? '1px solid var(--dsw-alias-state-brand-primary)' : '1px solid var(--dsw-alias-border-l2)',
2543
+ background: mode === candidate ? 'var(--dsh-svc-surface-bg)' : 'var(--dsw-alias-bg-layer-3)',
2544
+ color: 'var(--dsw-alias-label-primary)',
2545
+ opacity: loading ? 0.55 : 1,
2546
+ },
2547
+ }, translate('subagent.mode.' + candidate))
2548
+
2549
+ return React.createElement('div', { 'data-testid': 'subagent-section', style: cardStyle },
2550
+ React.createElement('div', { style: { fontSize: '14px', fontWeight: 700 } }, translate('subagent.title')),
2551
+ React.createElement('p', { style: hintStyle }, translate('subagent.hint')),
2552
+ snapshot !== null && snapshot.available === false ? React.createElement('p', { 'data-testid': 'subagent-unavailable', style: { ...hintStyle, color: 'var(--dsw-alias-state-warn-primary)' } }, translate('subagent.unavailable')) : null,
2553
+ React.createElement('div', { 'data-testid': 'subagent-modes', style: { display: 'flex', gap: '8px', marginTop: '12px', flexWrap: 'wrap' } },
2554
+ SUBAGENT_MODES.map(modeButton)),
2555
+ React.createElement('p', { 'data-testid': 'subagent-mode-desc', style: { ...hintStyle, marginTop: '8px' } }, translate('subagent.mode.' + mode + '.desc')),
2556
+ mode === 'custom' ? React.createElement('div', { 'data-testid': 'subagent-custom', style: { marginTop: '10px', display: 'flex', alignItems: 'center', gap: '10px', flexWrap: 'wrap' } },
2557
+ React.createElement('span', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } }, translate('subagent.provider')),
2558
+ React.createElement('select', { 'data-testid': 'subagent-provider', value: effectiveProvider, disabled: providers.length === 0 || saving, onChange: (event) => setProvider(event.target.value), style: selectStyle },
2559
+ providers.map((id) => React.createElement('option', { key: id, value: id }, providerName[id] ?? id))),
2560
+ React.createElement('span', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } }, translate('subagent.model')),
2561
+ React.createElement('select', { 'data-testid': 'subagent-model', value: model, disabled: providerModels.length === 0 || saving, onChange: (event) => setModel(event.target.value), style: selectStyle },
2562
+ providerModels.map((item) => React.createElement('option', { key: item.id, value: item.id }, item.name ?? item.id)))) : null,
2563
+ mode === 'custom' && models.length === 0 && !loading ? React.createElement('p', { 'data-testid': 'subagent-models-empty', style: { ...hintStyle, color: 'var(--dsw-alias-state-warn-primary)' } }, translate('subagent.modelsEmpty')) : null,
2564
+ error !== '' ? React.createElement('p', { 'data-testid': 'subagent-error', style: { ...hintStyle, color: 'var(--dsw-alias-state-error-primary)' } }, mapSubagentError(translate, error)) : null,
2565
+ savedTick > 0 && error === '' ? React.createElement('p', { 'data-testid': 'subagent-saved', style: { ...hintStyle, color: 'var(--dsw-alias-state-success-primary)' } }, '✓ ' + translate('subagent.saved')) : null,
2566
+ React.createElement('div', { style: { display: 'flex', gap: '10px', marginTop: '12px', flexWrap: 'wrap' } },
2567
+ React.createElement('button', { type: 'button', 'data-testid': 'subagent-save', disabled: saving || loading || (mode === 'custom' && (effectiveProvider === '' || model === '')), onClick: () => void save(mode), style: { fontSize: '12.5px', padding: '6px 16px', borderRadius: '7px', border: '1px solid transparent', background: 'var(--dsw-alias-state-success-primary)', color: '#fff', cursor: saving ? 'default' : 'pointer', opacity: saving || loading || (mode === 'custom' && (effectiveProvider === '' || model === '')) ? 0.55 : 1 } }, saving ? translate('subagent.saving') : translate('subagent.save')),
2568
+ mode !== 'inherit' ? React.createElement('button', { type: 'button', 'data-testid': 'subagent-reset', disabled: saving || loading, onClick: () => { setMode('inherit'); void save('inherit') }, style: { fontSize: '12.5px', padding: '6px 14px', borderRadius: '7px', border: '1px solid var(--dsw-alias-state-error-primary)', background: 'transparent', color: 'var(--dsw-alias-state-error-primary)', cursor: saving ? 'default' : 'pointer', opacity: saving || loading ? 0.55 : 1 } }, translate('subagent.reset')) : null),
2569
+ React.createElement('div', { style: { marginTop: '12px', paddingTop: '10px', borderTop: '1px solid var(--dsw-alias-border-l1)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px' } },
2570
+ React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '2px' } },
2571
+ React.createElement('span', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-primary)' } }, translate('subagent.navToggle')),
2572
+ React.createElement('span', { style: hintStyle }, translate('subagent.navToggleHint'))),
2573
+ React.createElement('button', {
2574
+ type: 'button',
2575
+ role: 'switch',
2576
+ 'data-testid': 'subagent-nav-switch',
2577
+ 'aria-checked': String(navEnabled),
2578
+ onClick: () => setNavEnabled(!navEnabled),
2579
+ style: { width: '34px', height: '20px', borderRadius: '10px', padding: 0, flexShrink: 0, position: 'relative', border: `1px solid ${navEnabled ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-border-l2)'}`, background: navEnabled ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-bg-layer-2)', cursor: 'pointer', lineHeight: 0 },
2580
+ }, React.createElement('span', { style: { position: 'absolute', top: '1px', left: navEnabled ? '15px' : '1px', width: '16px', height: '16px', borderRadius: '50%', background: navEnabled ? '#fff' : 'var(--dsw-alias-label-tertiary)' } }))))
2581
+ }
2582
+
2379
2583
  // ─── 技能管理(v0.22):三区列表 / 启停 / AI 补全 / 批量 ────────────────
2380
2584
  const SKILLS_MODEL_STORAGE_KEY = 'dsh-service-skills-model'
2381
2585
  const SKILL_RPC = (endpoint, payload) => ctx.connection.rpc.call('/dsh-service', endpoint, payload)
@@ -2929,6 +3133,8 @@ window.__ModuleLoader__.load({
2929
3133
  const clearAdaptedKind = (providerName) => requestQuotaConfig({ provider: providerName, clear: true })
2930
3134
  // 卡片手动排序:localStorage 名单驱动展示序(纯客户端,宿主契约不动);新供应商自然排末尾。
2931
3135
  const [cardOrder, setCardOrder] = useState(readQuotaCardOrder())
3136
+ // 排序模式开关(用户点名):平时不显示 ↑↓,点「调整排序」才出现,避免卡片头部常驻小钮。
3137
+ const [reorderMode, setReorderMode] = useState(false)
2932
3138
  // 卡片分区(v0.20):只展示已适配供应商;未适配/已停用的不渲染灰行,统一收进底部「手动适配」行。
2933
3139
  const adaptedRows = applyQuotaCardOrder(providers.filter((row) => row.adapted === true), cardOrder)
2934
3140
  const candidateRows = providers.filter((row) => row.adapted !== true)
@@ -2981,12 +3187,34 @@ window.__ModuleLoader__.load({
2981
3187
  style: { width: '34px', height: '20px', borderRadius: '10px', padding: 0, flexShrink: 0, position: 'relative', border: `1px solid ${quotaNav ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-border-l2)'}`, background: quotaNav ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-bg-layer-2)', cursor: 'pointer', lineHeight: 0 },
2982
3188
  }, React.createElement('span', { style: { position: 'absolute', top: '1px', left: quotaNav ? '15px' : '1px', width: '16px', height: '16px', borderRadius: '50%', background: quotaNav ? '#fff' : 'var(--dsw-alias-label-tertiary)' } }))),
2983
3189
  configError !== '' ? React.createElement('p', { 'data-testid': 'quota-config-error', style: Object.assign({}, hint, { color: 'var(--dsw-alias-state-error-primary)' }) }, configError) : null,
3190
+ // 「调整排序」开关(≥2 张卡才有意义):进入后卡片头部出现 ↑↓,再点一次收起。
3191
+ ...(adaptedRows.length >= 2
3192
+ ? [React.createElement('div', { key: 'quota-reorder-row', style: { display: 'flex', justifyContent: 'flex-end', margin: '2px 0 8px' } },
3193
+ React.createElement('button', {
3194
+ type: 'button',
3195
+ 'data-testid': 'quota-reorder-toggle',
3196
+ 'aria-pressed': String(reorderMode),
3197
+ title: translate('quota.reorder'),
3198
+ onClick: () => setReorderMode(!reorderMode),
3199
+ style: { fontSize: '12px', lineHeight: '20px', padding: '2px 12px', borderRadius: 999, border: `1px solid ${reorderMode ? 'var(--dsw-alias-brand-primary)' : 'var(--dsw-alias-border-l2)'}`, background: 'transparent', color: reorderMode ? 'var(--dsw-alias-brand-primary)' : 'var(--dsw-alias-label-secondary)', cursor: 'pointer' },
3200
+ }, translate('quota.reorder')))]
3201
+ : []),
2984
3202
  adaptedRows.length === 0
2985
3203
  ? React.createElement('p', { style: hint }, translate('quota.noAdapted'))
2986
3204
  : React.createElement('div', { 'data-testid': 'quota-card-list', style: { display: 'flex', flexDirection: 'column', gap: '10px' } },
2987
3205
  adaptedRows.map((row, index) => {
2988
3206
  const nameNode = React.createElement('span', { style: { fontWeight: 600, fontSize: '12px', overflowWrap: 'anywhere' } },
2989
- row.displayName || row.provider,
3207
+ // 官网用量页链接(用户点名):宿主按 kind 下发 usageUrl 时,展示名本身即外链。
3208
+ typeof row.usageUrl === 'string' && row.usageUrl !== ''
3209
+ ? React.createElement('a', {
3210
+ 'data-testid': `quota-usage-link-${row.provider}`,
3211
+ href: row.usageUrl,
3212
+ target: '_blank',
3213
+ rel: 'noreferrer',
3214
+ title: translate('quota.usageLink'),
3215
+ style: { color: 'var(--dsw-alias-brand-primary)', textDecoration: 'underline' },
3216
+ }, row.displayName || row.provider)
3217
+ : (row.displayName || row.provider),
2990
3218
  row.kindSource === 'auto'
2991
3219
  ? React.createElement('span', {
2992
3220
  'data-testid': `quota-auto-tag-${row.provider}`,
@@ -3048,25 +3276,28 @@ window.__ModuleLoader__.load({
3048
3276
  React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: '10px' } },
3049
3277
  nameNode,
3050
3278
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '5px' } },
3051
- // 手动排序(用户点名):↑↓ 与相邻卡换位,首/末卡对应方向禁用;顺序存本浏览器。
3052
- React.createElement('button', {
3053
- type: 'button',
3054
- 'data-testid': `quota-move-up-${row.provider}`,
3055
- 'aria-label': translate('quota.card.moveUp'),
3056
- title: translate('quota.card.moveUp'),
3057
- disabled: index === 0,
3058
- onClick: () => moveQuotaCard(row.provider, -1),
3059
- style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '16px', height: '16px', padding: 0, border: 'none', background: 'transparent', color: index === 0 ? 'var(--dsw-alias-label-tertiary)' : 'var(--dsw-alias-label-secondary)', cursor: index === 0 ? 'default' : 'pointer', opacity: index === 0 ? 0.45 : 1, fontSize: '12px', lineHeight: '16px' },
3060
- }, '↑'),
3061
- React.createElement('button', {
3062
- type: 'button',
3063
- 'data-testid': `quota-move-down-${row.provider}`,
3064
- 'aria-label': translate('quota.card.moveDown'),
3065
- title: translate('quota.card.moveDown'),
3066
- disabled: index === adaptedRows.length - 1,
3067
- onClick: () => moveQuotaCard(row.provider, 1),
3068
- style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '16px', height: '16px', padding: 0, border: 'none', background: 'transparent', color: index === adaptedRows.length - 1 ? 'var(--dsw-alias-label-tertiary)' : 'var(--dsw-alias-label-secondary)', cursor: index === adaptedRows.length - 1 ? 'default' : 'pointer', opacity: index === adaptedRows.length - 1 ? 0.45 : 1, fontSize: '12px', lineHeight: '16px' },
3069
- }, '↓'),
3279
+ // 手动排序(用户点名):仅在「调整排序」模式下出现;↑↓ 与相邻卡换位,
3280
+ // 首/末卡对应方向禁用;顺序存本浏览器。
3281
+ ...(reorderMode ? [
3282
+ React.createElement('button', {
3283
+ type: 'button',
3284
+ 'data-testid': `quota-move-up-${row.provider}`,
3285
+ 'aria-label': translate('quota.card.moveUp'),
3286
+ title: translate('quota.card.moveUp'),
3287
+ disabled: index === 0,
3288
+ onClick: () => moveQuotaCard(row.provider, -1),
3289
+ style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '16px', height: '16px', padding: 0, border: 'none', background: 'transparent', color: index === 0 ? 'var(--dsw-alias-label-tertiary)' : 'var(--dsw-alias-label-secondary)', cursor: index === 0 ? 'default' : 'pointer', opacity: index === 0 ? 0.45 : 1, fontSize: '12px', lineHeight: '16px' },
3290
+ }, ''),
3291
+ React.createElement('button', {
3292
+ type: 'button',
3293
+ 'data-testid': `quota-move-down-${row.provider}`,
3294
+ 'aria-label': translate('quota.card.moveDown'),
3295
+ title: translate('quota.card.moveDown'),
3296
+ disabled: index === adaptedRows.length - 1,
3297
+ onClick: () => moveQuotaCard(row.provider, 1),
3298
+ style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '16px', height: '16px', padding: 0, border: 'none', background: 'transparent', color: index === adaptedRows.length - 1 ? 'var(--dsw-alias-label-tertiary)' : 'var(--dsw-alias-label-secondary)', cursor: index === adaptedRows.length - 1 ? 'default' : 'pointer', opacity: index === adaptedRows.length - 1 ? 0.45 : 1, fontSize: '12px', lineHeight: '16px' },
3299
+ }, '↓'),
3300
+ ] : []),
3070
3301
  // 手动刷新:SVG 图标按钮,点击强制该 provider 重拉上游;在途时置灰防重入。
3071
3302
  React.createElement('button', {
3072
3303
  type: 'button',
@@ -4068,6 +4299,7 @@ window.__ModuleLoader__.load({
4068
4299
  ...(features.quotaLookup !== false ? [['quota', 'tabs.quota']] : []),
4069
4300
  ...(features.backupMaintenance !== false ? [['backup', 'tabs.backup']] : []),
4070
4301
  ...(features.skillManager !== false ? [['skills', 'tabs.skills']] : []),
4302
+ ...(features.subagentRoute !== false ? [['subagent', 'tabs.subagent']] : []),
4071
4303
  ['restart', 'tabs.restart'],
4072
4304
  ]
4073
4305
  const warningTabs = tabs.filter(([id]) => tabWarnings[id]).map(([, label]) => translate(label))
@@ -4086,6 +4318,8 @@ window.__ModuleLoader__.load({
4086
4318
  ? maintenanceBlock
4087
4319
  : visibleActiveTab === 'skills'
4088
4320
  ? React.createElement(SkillsSection, null)
4321
+ : visibleActiveTab === 'subagent'
4322
+ ? React.createElement(SubagentSection, null)
4089
4323
  : restartBlock
4090
4324
  return React.createElement('div', null,
4091
4325
  warningTabs.length > 0 ? React.createElement('div', { style: { marginBottom: '12px', padding: '11px 13px', borderRadius: '8px', background: 'rgba(198,128,0,0.16)', border: '1px solid rgba(198,128,0,0.48)', boxShadow: '0 2px 8px rgba(0,0,0,0.08)' } },
@@ -4148,19 +4382,23 @@ window.__ModuleLoader__.load({
4148
4382
  { name: 'settings.section', id: 'dsh-service', order: 99, label: () => t('nav.label') },
4149
4383
  () => React.createElement(ServicePanel, null),
4150
4384
  )
4151
- // 左列「重启」「额度查询」「技能」入口由各自标签内的开关控制,默认不注册
4385
+ // 左列「重启」「额度查询」「技能」「子代理」入口由各自标签内的开关控制,默认不注册
4152
4386
  restartNavToggle.sync()
4153
4387
  quotaNavToggle.sync()
4154
4388
  skillsNavToggle.sync()
4389
+ subagentNavToggle.sync()
4155
4390
  const unsubscribeFeatures = featureScope.subscribe(quotaNavToggle.sync)
4156
4391
  const unsubscribeFeaturesSkills = featureScope.subscribe(skillsNavToggle.sync)
4392
+ const unsubscribeFeaturesSubagent = featureScope.subscribe(subagentNavToggle.sync)
4157
4393
  return () => {
4158
4394
  unsubscribeFeatures()
4159
4395
  unsubscribeFeaturesSkills()
4396
+ unsubscribeFeaturesSubagent()
4160
4397
  disposePanel()
4161
4398
  restartNavToggle.disposeEntry()
4162
4399
  quotaNavToggle.disposeEntry()
4163
4400
  skillsNavToggle.disposeEntry()
4401
+ subagentNavToggle.disposeEntry()
4164
4402
  }
4165
4403
  })
4166
4404
  ctx.slots.inject('shell.overlay', () => ctx.slots.register(
package/index.js CHANGED
@@ -24,6 +24,7 @@ const DEFAULT_FEATURE_SETTINGS = Object.freeze({
24
24
  taskNotifications: true,
25
25
  healthz: true,
26
26
  skillManager: true,
27
+ subagentRoute: true,
27
28
  })
28
29
  const FeatureSettingsSchema = z.object({
29
30
  modelUsage: z.boolean().default(true),
@@ -32,6 +33,7 @@ const FeatureSettingsSchema = z.object({
32
33
  taskNotifications: z.boolean().default(true),
33
34
  healthz: z.boolean().default(true),
34
35
  skillManager: z.boolean().default(true),
36
+ subagentRoute: z.boolean().default(true),
35
37
  })
36
38
  const NPM_REGISTRY = 'https://registry.npmjs.org/'
37
39
  const MAX_NPM_RESPONSE_BYTES = 256 * 1024
@@ -79,6 +81,8 @@ const KIND_REGISTRY = {
79
81
  parser: normalizeOpencodeUsage,
80
82
  keyHints: ['OPENCODE_GO_API_KEY', 'OPENCODE_API_KEY'],
81
83
  hosts: ['opencode.ai'],
84
+ // 官网用户页(卡片标题外链;宿主常量,零浏览器输入)。
85
+ usageUrl: 'https://opencode.ai/',
82
86
  },
83
87
  'zai-coding-cn': {
84
88
  parser: normalizeZaiCodingUsage,
@@ -88,6 +92,7 @@ const KIND_REGISTRY = {
88
92
  ],
89
93
  keyHints: ['ZAI_CODING_CN_API_KEY', 'ZAI_API_KEY', 'BIGMODEL_API_KEY'],
90
94
  hosts: ['open.bigmodel.cn', 'bigmodel.cn'],
95
+ usageUrl: 'https://open.bigmodel.cn/coding-plan/personal/usage',
91
96
  },
92
97
  openrouter: {
93
98
  parser: normalizeOpenRouterCredits,
@@ -114,6 +119,7 @@ const KIND_REGISTRY = {
114
119
  endpoints: ['https://api.deepseek.com/user/balance'],
115
120
  keyHints: ['DEEPSEEK_API_KEY'],
116
121
  hosts: ['api.deepseek.com', 'deepseek.com'],
122
+ usageUrl: 'https://platform.deepseek.com/usage',
117
123
  },
118
124
  // CLIProxyAPI(router-for-me/CLIProxyAPI)管理面:查它托管的 OAuth 上游账号官方额度。
119
125
  // 无固定 endpoints / 全局 hosts——每个部署一个域名,保存适配时把 settings baseURL 的
@@ -153,6 +159,15 @@ const MAX_QUOTA_CPA_CALLS = 12
153
159
  const QUOTA_CPA_CONCURRENCY = 3
154
160
  const MAX_QUOTA_CPA_WINDOWS = 32
155
161
 
162
+ // 子代理路由(v0.27):三态配置的常量。inherit=不干预(原生继承),
163
+ // follow=派生时读父会话最近一次请求路由注入,custom=固定 provider/model。
164
+ // 数值只在此处与 TODO.md 里程碑两处出现。
165
+ const SUBAGENT_ROUTE_VERSION = 1
166
+ const SUBAGENT_ROUTE_FILE = 'dsh-service-subagent-route.json'
167
+ const SUBAGENT_ROUTE_MODES = ['inherit', 'follow', 'custom']
168
+ const MAX_SUBAGENT_ROUTE_BYTES = 64 * 1024
169
+ const MAX_SUBAGENT_ROUTE_FIELD = 256
170
+
156
171
  // 升级目标白名单:命令与包名全部来自宿主常量,浏览器不传任何输入。
157
172
  // TARGET_RE 与 dsh-market 同源:只放行「包名@版本」这一种形状的字符集。
158
173
  const TARGET_RE = /^[A-Za-z0-9@:./_#+~^=-]+$/
@@ -2875,6 +2890,88 @@ async function listSkillModels(llm, agentDefaultModel) {
2875
2890
  return { models, ...(current !== undefined ? { current } : {}) }
2876
2891
  }
2877
2892
 
2893
+ /** 子代理路由配置的空档(inherit):与未安装本功能的原生行为完全一致。 */
2894
+ function createEmptySubagentRoute() {
2895
+ return { version: SUBAGENT_ROUTE_VERSION, mode: 'inherit' }
2896
+ }
2897
+
2898
+ /** 解析磁盘上的子代理路由配置:损坏/版本不符/未知模式回退 inherit(零侵入,不因坏配置破坏派生)。 */
2899
+ function parseSubagentRouteText(text) {
2900
+ const fallback = createEmptySubagentRoute()
2901
+ let parsed
2902
+ try {
2903
+ parsed = JSON.parse(text)
2904
+ } catch (_) {
2905
+ return fallback
2906
+ }
2907
+ if (parsed?.version !== SUBAGENT_ROUTE_VERSION || !SUBAGENT_ROUTE_MODES.includes(parsed.mode)) return fallback
2908
+ if (parsed.mode !== 'custom') return { version: SUBAGENT_ROUTE_VERSION, mode: parsed.mode }
2909
+ const provider = typeof parsed.provider === 'string' ? parsed.provider.trim().slice(0, MAX_SUBAGENT_ROUTE_FIELD) : ''
2910
+ const model = typeof parsed.model === 'string' ? parsed.model.trim().slice(0, MAX_SUBAGENT_ROUTE_FIELD) : ''
2911
+ if (provider === '' || model === '') return fallback
2912
+ return { version: SUBAGENT_ROUTE_VERSION, mode: 'custom', provider, model }
2913
+ }
2914
+
2915
+ async function loadSubagentRoute(dshHome) {
2916
+ try {
2917
+ const target = join(dshHome, SUBAGENT_ROUTE_FILE)
2918
+ const info = await stat(target)
2919
+ if (info.size > MAX_SUBAGENT_ROUTE_BYTES) return createEmptySubagentRoute()
2920
+ return parseSubagentRouteText(await readFile(target, 'utf8'))
2921
+ } catch (_) {
2922
+ return createEmptySubagentRoute()
2923
+ }
2924
+ }
2925
+
2926
+ async function saveSubagentRoute(dshHome, config) {
2927
+ await mkdir(dshHome, { recursive: true })
2928
+ const target = join(dshHome, SUBAGENT_ROUTE_FILE)
2929
+ const temporary = `${target}.${randomUUID()}.tmp`
2930
+ try {
2931
+ await writeFile(temporary, JSON.stringify(config), { mode: 0o600 })
2932
+ await rename(temporary, target)
2933
+ } finally {
2934
+ await rm(temporary, { force: true })
2935
+ }
2936
+ }
2937
+
2938
+ /**
2939
+ * 计算一次子代理派生应注入的 agentOptions(seam 核心,纯函数便于测试):
2940
+ * - 派生请求已显式携带 provider 或 model → 不干预(显式永远赢,含预设钉死与其他插件注入);
2941
+ * - follow → 读父会话最近一次 request header 的路由(= 主对话当前实际在用的渠道;空白
2942
+ * 会话无 header 时回落继承,不注入);
2943
+ * - custom → 配置的 provider/model,且 llm 注册表当前可路由才注入(渠道被卸载后回落继承
2944
+ * 而不是让派生失败);
2945
+ * - inherit / 其余一切 → undefined(原生继承父代理创建 options)。
2946
+ * @param {object|null} request SubagentStartRequest(读 parent / agentOptions)。
2947
+ * @param {object|null} config 当前生效的路由配置。
2948
+ * @param {object} options isRoutable(provider) 与 readParentHeader(parent) 注入点(测试替身用)。
2949
+ */
2950
+ function resolveSubagentInjection(request, config, options = {}) {
2951
+ const agentOptions = request?.agentOptions
2952
+ const explicitProvider = typeof agentOptions?.provider === 'string' && agentOptions.provider !== ''
2953
+ const explicitModel = typeof agentOptions?.model === 'string' && agentOptions.model !== ''
2954
+ if (explicitProvider || explicitModel) return undefined
2955
+ if (config?.mode === 'follow') {
2956
+ let header
2957
+ try {
2958
+ header = options.readParentHeader?.(request?.parent)
2959
+ } catch (_) {
2960
+ header = undefined
2961
+ }
2962
+ if (typeof header?.provider === 'string' && header.provider !== '' && typeof header?.model === 'string' && header.model !== '') {
2963
+ return { provider: header.provider, model: header.model }
2964
+ }
2965
+ return undefined
2966
+ }
2967
+ if (config?.mode === 'custom' && typeof config.provider === 'string' && config.provider !== '' && typeof config.model === 'string' && config.model !== '') {
2968
+ if (options.isRoutable !== undefined && !options.isRoutable(config.provider)) return undefined
2969
+ return { provider: config.provider, model: config.model }
2970
+ }
2971
+ return undefined
2972
+ }
2973
+
2974
+
2878
2975
  function apply(ctx) {
2879
2976
  const dshHome = resolveDshHome()
2880
2977
  let featureSettings = DEFAULT_FEATURE_SETTINGS
@@ -2950,6 +3047,64 @@ function apply(ctx) {
2950
3047
  }
2951
3048
  skillsActiveControllers.clear()
2952
3049
  }, 'dsh-service skills batch teardown')
3050
+ // ── 子代理路由(v0.27):三态配置 + subagents seam ─────────────────────
3051
+ // 内存态即 seam 读取的事实源:保存端点落盘成功后原地替换,派生路径零读盘。
3052
+ let subagentRouteConfig = createEmptySubagentRoute()
3053
+ const subagentRouteLoadPromise = Promise.resolve().then(async () => {
3054
+ subagentRouteConfig = await loadSubagentRoute(dshHome)
3055
+ })
3056
+ // 配置写串行化(quota-config 同款):所有写先等首次加载完成再从同一内存快照复制,
3057
+ // 避免与启动加载竞态把磁盘回退成 inherit。
3058
+ let subagentRouteWrites = Promise.resolve()
3059
+ const serializeSubagentRouteWrite = (work) => {
3060
+ const result = subagentRouteWrites.then(async () => {
3061
+ await subagentRouteLoadPromise
3062
+ const current = { ...subagentRouteConfig }
3063
+ const outcome = await work(current)
3064
+ if (outcome?.save === false) return outcome.value
3065
+ await saveSubagentRoute(dshHome, current)
3066
+ subagentRouteConfig = current
3067
+ return outcome.value
3068
+ })
3069
+ subagentRouteWrites = result.then(() => undefined, () => undefined)
3070
+ return result
3071
+ }
3072
+ // seam 是否已挂上(宿主 subagents 服务存在时由下面的 inject 置真):快照端点据此告知客户端。
3073
+ let subagentSeamInstalled = false
3074
+ // 只包装宿主 subagents 注册表的两个入口(start / startContinuable,spawn/fork/acp 全走这
3075
+ // 两个口):未显式指定模型的派生按配置注入 agentOptions,其余原样透传。Fiber 销毁还原
3076
+ // 原方法——包装挂在服务实例上,disposer 期间新派生恢复原生行为。
3077
+ ctx.inject(['subagents'], (scope) => {
3078
+ const subagents = scope.subagents
3079
+ if (subagents === undefined || typeof subagents.start !== 'function' || typeof subagents.startContinuable !== 'function') return
3080
+ const isRoutable = (provider) => {
3081
+ const llm = ctx.get('llm')
3082
+ if (llm === undefined || typeof llm.listProviders !== 'function') return false
3083
+ try {
3084
+ return llm.listProviders().some((entry) => entry?.id === provider)
3085
+ } catch (_) {
3086
+ return false
3087
+ }
3088
+ }
3089
+ const readParentHeader = (parent) => parent?.session?.requestHeader?.()?.config
3090
+ const decorate = (request) => {
3091
+ if (!featureEnabled('subagentRoute')) return request
3092
+ const injected = resolveSubagentInjection(request, subagentRouteConfig, { isRoutable, readParentHeader })
3093
+ if (injected === undefined) return request
3094
+ ctx.logger?.info?.(`dsh-service: subagent route seam applied ${injected.provider}/${injected.model} to a subagent without an explicit route`)
3095
+ return { ...request, agentOptions: { ...(request?.agentOptions ?? {}), ...injected } }
3096
+ }
3097
+ const originalStart = subagents.start
3098
+ const originalStartContinuable = subagents.startContinuable
3099
+ subagents.start = (name, request) => originalStart.call(subagents, name, decorate(request))
3100
+ subagents.startContinuable = (spec) => originalStartContinuable.call(subagents, { ...spec, request: decorate(spec.request) })
3101
+ subagentSeamInstalled = true
3102
+ scope.effect(() => () => {
3103
+ subagents.start = originalStart
3104
+ subagents.startContinuable = originalStartContinuable
3105
+ subagentSeamInstalled = false
3106
+ }, 'dsh-service subagent route seam teardown')
3107
+ })
2953
3108
  let quotaConfig = createEmptyQuotaConfig()
2954
3109
  let quotaConfigLoaded = false
2955
3110
  let quotaConfigLoadPromise
@@ -3677,6 +3832,8 @@ function apply(ctx) {
3677
3832
  nextAllowedAt: view.nextAllowedAt,
3678
3833
  ...(providerResetCards.length > 0 ? { resetCards: providerResetCards } : {}),
3679
3834
  ...(credentialHints !== undefined ? { credentialHints } : {}),
3835
+ // 官网用户页余额网址(宿主常量白名单随 kind 下发;无则缺省)。
3836
+ ...(typeof KIND_REGISTRY[kind].usageUrl === 'string' && KIND_REGISTRY[kind].usageUrl !== '' ? { usageUrl: KIND_REGISTRY[kind].usageUrl } : {}),
3680
3837
  })
3681
3838
  }
3682
3839
  return { ok: true, value: { providers: rows, serverTime: Date.now() } }
@@ -3835,6 +3992,70 @@ function apply(ctx) {
3835
3992
  }
3836
3993
  }
3837
3994
 
3995
+ // ── 子代理路由(v0.27):快照 + 三态保存 ─────────────────────────────
3996
+ if (endpoint === 'subagent-route') {
3997
+ if (!featureEnabled('subagentRoute')) return { ok: false, error: 'feature-disabled' }
3998
+ try {
3999
+ await subagentRouteLoadPromise
4000
+ const llm = ctx.get('llm')
4001
+ // 模型清单沿用 skills-models 的白名单口径(llm.listProviders × listModels,单渠道失败跳过);
4002
+ // llm 服务缺席时清单为空——自定义模式在保存端也会被拒(llm-unavailable),快照仍可下发。
4003
+ let models = []
4004
+ let current
4005
+ if (llm !== undefined && typeof llm.listProviders === 'function') {
4006
+ const catalog = await listSkillModels(llm, ctx.get('agentDefaultModel'))
4007
+ models = catalog.models
4008
+ current = catalog.current
4009
+ }
4010
+ const config = subagentRouteConfig
4011
+ return {
4012
+ ok: true,
4013
+ value: {
4014
+ available: subagentSeamInstalled,
4015
+ mode: config.mode,
4016
+ ...(config.mode === 'custom' ? { provider: config.provider, model: config.model } : {}),
4017
+ models,
4018
+ ...(current !== undefined ? { current } : {}),
4019
+ },
4020
+ }
4021
+ } catch (error) {
4022
+ return { ok: false, error: error?.message || String(error) }
4023
+ }
4024
+ }
4025
+
4026
+ if (endpoint === 'subagent-route-save') {
4027
+ if (!featureEnabled('subagentRoute')) return { ok: false, error: 'feature-disabled' }
4028
+ const mode = payload?.mode
4029
+ if (!SUBAGENT_ROUTE_MODES.includes(mode)) return { ok: false, error: 'unknown-mode' }
4030
+ try {
4031
+ if (mode === 'custom') {
4032
+ const provider = typeof payload?.provider === 'string' ? payload.provider.trim() : ''
4033
+ const model = typeof payload?.model === 'string' ? payload.model.trim() : ''
4034
+ if (provider === '' || model === '') return { ok: false, error: 'invalid-model-route' }
4035
+ const llm = ctx.get('llm')
4036
+ if (llm === undefined || typeof llm.stream !== 'function') return { ok: false, error: 'llm-unavailable' }
4037
+ // 与 skills-describe 同一道闸:provider/model 必须命中运行时清单白名单。
4038
+ const whitelist = await listSkillModels(llm, ctx.get('agentDefaultModel'))
4039
+ if (!whitelist.models.some((item) => item.provider === provider && item.id === model)) return { ok: false, error: 'invalid-model-route' }
4040
+ }
4041
+ return await serializeSubagentRouteWrite(async (config) => {
4042
+ if (mode === 'custom') {
4043
+ config.mode = 'custom'
4044
+ config.provider = typeof payload?.provider === 'string' ? payload.provider.trim().slice(0, MAX_SUBAGENT_ROUTE_FIELD) : ''
4045
+ config.model = typeof payload?.model === 'string' ? payload.model.trim().slice(0, MAX_SUBAGENT_ROUTE_FIELD) : ''
4046
+ } else {
4047
+ // inherit / follow:不保留 custom 字段,重置回干净形状。
4048
+ config.mode = mode
4049
+ delete config.provider
4050
+ delete config.model
4051
+ }
4052
+ return { value: { ok: true, mode: config.mode, ...(config.mode === 'custom' ? { provider: config.provider, model: config.model } : {}) } }
4053
+ })
4054
+ } catch (error) {
4055
+ return { ok: false, error: error?.message || String(error) }
4056
+ }
4057
+ }
4058
+
3838
4059
  return { ok: false, error: 'unknown endpoint: ' + String(endpoint) }
3839
4060
  }, { authority: 'loopback' })
3840
4061
  }
@@ -3868,11 +4089,13 @@ export {
3868
4089
  normalizeZaiCodingUsage,
3869
4090
  parseQuotaConfigText,
3870
4091
  parseSkillFrontmatterData,
4092
+ parseSubagentRouteText,
3871
4093
  quotaCredentialHintNames,
3872
4094
  quotaEndpointFor,
3873
4095
  quotaErrorCode,
3874
4096
  readLlmProviders,
3875
4097
  resolveSkillInvocationState,
4098
+ resolveSubagentInjection,
3876
4099
  runtimeEnvCheck,
3877
4100
  safeCliproxyOrigin,
3878
4101
  sanitizeSkillDraftText,
@@ -3910,11 +4133,13 @@ export default {
3910
4133
  normalizeZaiCodingUsage,
3911
4134
  parseQuotaConfigText,
3912
4135
  parseSkillFrontmatterData,
4136
+ parseSubagentRouteText,
3913
4137
  quotaCredentialHintNames,
3914
4138
  quotaEndpointFor,
3915
4139
  quotaErrorCode,
3916
4140
  readLlmProviders,
3917
4141
  resolveSkillInvocationState,
4142
+ resolveSubagentInjection,
3918
4143
  runtimeEnvCheck,
3919
4144
  safeCliproxyOrigin,
3920
4145
  sanitizeSkillDraftText,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gehennawu/dsh-service",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "description": "DSH Web 自托管运维面板:安全重启、健康监控、备份和 Linux 权限维护。",
5
5
  "type": "module",
6
6
  "scripts": {