@gehennawu/dsh-service 0.27.0 → 0.28.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 +7 -5
- package/README.md +7 -5
- package/client.js +51 -15
- package/index.js +173 -24
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -10,7 +10,7 @@ A service-control and operations plugin for self-hosted DSH Web. Provides safe r
|
|
|
10
10
|
|
|
11
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
|
|
13
|
+
The plugin also appears under **Plugins → Plugin configuration**, with eight host-level switches enabled by default: **Health diagnostics, 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 and Restart remain available. All eight 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
|
|
|
@@ -33,6 +33,7 @@ The plugin also appears under **Plugins → Plugin configuration**, with seven h
|
|
|
33
33
|
- Full diagnostics check session storage, workspace registry, backup storage, tar availability, file permissions, runtime environment, and Node runtime version; a manual launch is marked with a yellow inline caution (no restart assurance) that does not trigger the health alert banner, the service-control reminder, or the tab ⚠; an unrecognized environment and an empty backup list are informational only — all can be declared explicitly via `DSH_SERVICE_RUNTIME_ENV`
|
|
34
34
|
- Having no backups is an informational note, not a warning, and does not light the Health tab ⚠
|
|
35
35
|
- File-permission deep scan and repair: checks whether the Agent can read/write DSH_HOME and workspaces; repair requires two-step confirmation
|
|
36
|
+
- The **Health diagnostics** switch under Plugins → Plugin configuration hides this whole tab: full diagnostics and file-permission requests stop immediately and the Host rejects the corresponding RPCs; the overview metrics (5-second polling) are unaffected
|
|
36
37
|
|
|
37
38
|
### Model statistics
|
|
38
39
|
|
|
@@ -48,13 +49,14 @@ The plugin also appears under **Plugins → Plugin configuration**, with seven h
|
|
|
48
49
|
|
|
49
50
|

|
|
50
51
|
|
|
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
|
|
52
|
+
- 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, Xiaomi MiMo console usage page) 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
|
|
52
53
|
- 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
|
|
53
54
|
- 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
|
|
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
|
|
55
|
+
- 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), **Xiaomi MiMo Token Plan** (subscription plan quota, see below); 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, token-plan-cn.xiaomimimo.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
|
|
55
56
|
- **DeepSeek official balance with peak/off-peak hint (deepseek)**: calls DeepSeek Platform's official `GET /user/balance` and shows the total balance per currency (CNY→¥, USD→$), plus a separate "Granted balance" line whenever unexpired granted credit is positive; each DeepSeek card embeds a **peak/off-peak block** — a status badge on top (orange = "Peak now · standard price", green = "Off-peak now · half price") with a countdown to the next switch in Beijing time (e.g. "Peak pricing from 09:00 (in 12 h 41 min)", correctly spanning weekends to Monday's morning peak), below it a two-segment ribbon: the first block is the remainder of the current period and the second is the next opposite period (spanning days when needed — e.g. Friday evening draws straight to Monday's morning peak), widths proportional to actual durations with the left edge marking "now"; blocks are tagged inline as Peak / Off-peak, advancing every 30 seconds, and a caption stating the official rule "off-peak price is half the peak price"; the ring panel shows a compact version without the caption. Credentials resolve via the `DEEPSEEK_API_KEY` hint from the DSH credential store or environment variables, or can be written directly through the card's credential form. DSH's built-in `@deepseek-ai/dsh-llm-deepseek` official channel (the DeepSeek V4 models in the model picker, route id `deepseek-official`) needs **no extra settings route** — the quota lookup merges that runtime channel automatically and adapts it to the official balance endpoint, so the ring also appears when you switch to one of its models
|
|
56
57
|
- **CLIProxyAPI accounts (cliproxy)**: queries the **official remaining quota** of each OAuth upstream account inside a CLIProxyAPI (CPA) deployment (Codex 5-hour/weekly windows, per-model quotas for GeminiCLI and Antigravity) — not proxy-key usage. Prerequisites: CPA's `remote-management.secret-key` must be set (the whole management API is unavailable without it), remote access requires `allow-remote-management`, and the management secret goes into the DSH credential store as `CPA_MANAGEMENT_KEY` or an environment variable of the same name — it is independent of the proxy API key, and the plugin never sends the proxy key to the management plane. When you save the "CLIProxyAPI accounts" adaptation, the plugin pins the hostname of that provider's settings baseURL into the config file and afterwards only sends requests to that pinned domain (re-save the adaptation after changing baseURL); a single refresh queries at most 8 accounts (disabled accounts and account types without a supported quota endpoint are skipped automatically), and failures on some accounts do not affect the rest. Note that CPA's management plane has built-in key throttling — 5 wrong attempts ban your IP for 30 minutes — so make sure the secret is correct before testing
|
|
57
|
-
-
|
|
58
|
+
- **Xiaomi MiMo Token Plan quota (xiaomi-token-plan-cn)**: queries the "plan usage" of a Xiaomi MiMo Open Platform Token Plan subscription — plan total quota, compensation credits and other buckets, straight from the same source as the console; each row shows used percentage, an absolute figure abbreviation (e.g. `12% · 1.4B / 11B`) and a subscription-validity countdown. Xiaomi has no API-key-based quota endpoint (the inference gateway serves `/v1` inference paths only); the data lives behind console-origin APIs, so the credential is your **web session cookie**: log into platform.xiaomimimo.com in your browser, open DevTools, copy the full `Cookie:` request header from any `/api/v1/tokenPlan/` call, then click **"Set console cookie (web session)"** on the card and paste it (stored in the DSH credential store under `XIAOMI_MIMO_CONSOLE_COOKIE` or an environment variable of the same name; the `Cookie:` prefix is optional). The cookie is sent only to two fixed endpoints, `platform.xiaomimimo.com/api/v1/tokenPlan/detail` and `/usage`; the `tp-…` inference key is never sent to the console plane. When the cookie expires (logout or session expiry) the card shows "Console cookie expired" — copy it again. Providers auto-detect from baseURL (token-plan-cn.xiaomimimo.com); when you reach MiMo through a relay domain, pick "Xiaomi MiMo Token Plan" in "Manual adapt" instead (the queried plane is fixed and unaffected by baseURL). Note that a web cookie grants full console access — it is stored only in your local credential store and sent only to those two fixed endpoints; never hand it to environments you do not trust
|
|
59
|
+
- Credential fill-in form: when an adapted provider shows "credential missing", its card offers an inline form — regular adaptations show **"Set API credential"**, the CLIProxyAPI adaptation shows **"Set management key (web login key)"** (the remote-management secret you use to log into CPA's web management UI — not the proxy API key, which the management plane bans wrong attempts for), and the Xiaomi Token Plan adaptation shows **"Set console cookie (web session)"**; the credential name comes from a host-derived whitelist per adaptation type (CLIProxyAPI only ever offers `CPA_MANAGEMENT_KEY`/`CLIPROXY_MANAGEMENT_KEY` — these are **alias slots for the same secret; storing one is enough**, discovery takes the first configured value in order; Xiaomi likewise offers `XIAOMI_MIMO_CONSOLE_COOKIE`/`MIMO_CONSOLE_COOKIE`; the primary name is marked and the form defaults to a configured slot. Never the proxy key or the tp-… inference key); saving writes the value into the DSH credential provider (the `refs:` section of `$DSH_HOME/.credentials.yaml`, hot-effective without restarts) and then force-refreshes that provider; the form can also clear a stored file-layer credential in one click. If a process environment variable is currently shadowing that name, the host refuses the write (credential-store contract) — change the environment variable itself instead
|
|
58
60
|
- Anti-rate-limit pacing is enforced by the host: successful results are cached for 60 seconds (shared across tabs), failures back off exponentially (30 s doubling, capped at 15 minutes), upstream timeout is 15 s; the panel can set auto query to manual only / 1 / 2 / 5 / 10 minutes (manual only by default), paused automatically while the page is hidden
|
|
59
61
|
- API keys are resolved and used only inside the host process — the browser receives normalized window data (percentages or balance text) only; data flows through the plugin's own loopback RPC with no webServer routes exposed
|
|
60
62
|
|
|
@@ -73,7 +75,7 @@ The plugin also appears under **Plugins → Plugin configuration**, with seven h
|
|
|
73
75
|
- 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
|
|
74
76
|
- 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
|
|
75
77
|
- 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
|
|
76
|
-
- "Fill with AI" (✨): pick any configured model (last choice remembered); the Host calls it with a fixed template to draft description and usage **in Simplified Chinese
|
|
78
|
+
- "Fill with AI" (✨): pick any configured model (last choice remembered); the Host calls it with a fixed template to draft description and usage **in the DSH UI language** (Simplified Chinese in Chinese environments, English in English ones); the draft is previewed old-vs-new and saved after explicit confirmation as an "AI note". Notes live only in the plugin sidecar index `DSH_HOME/dsh-service-skills-index.json` and **never modify SKILL.md** — they render under their entry on the Skills tab only (with a remove button); a body change marks the note stale for refilling
|
|
77
79
|
- 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
|
|
78
80
|
- 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
|
|
79
81
|
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
设置页「服务控制」面板包含九个顶部标签:**概览、通知、健康诊断、模型统计、额度查询、备份维护、技能、子代理、重启**;重启、额度查询、技能与子代理标签还可在设置页左侧标签列底部开启快捷入口(默认关闭)。
|
|
12
12
|
|
|
13
|
-
插件同时出现在「插件 →
|
|
13
|
+
插件同时出现在「插件 → 插件配置」,提供八个默认开启的宿主级开关:**健康诊断、模型统计、额度查询、备份维护、任务通知、技能管理、子代理模型、`/healthz` 探活端点**。关闭后不仅隐藏对应界面,也会停止相关轮询/订阅并由宿主拒绝对应能力;概览与重启固定保留。设置写入 DSH settings,八项均为热生效:关闭或重新开启都无需刷新页面或重启 DSH Web。已在途的统计刷新、额度请求或备份操作允许完成;额度重新开启时保留既有缓存、TTL 与退避状态,因此可立即恢复界面和调用,但不保证立刻重新请求上游。
|
|
14
14
|
|
|
15
15
|
### 版本与更新
|
|
16
16
|
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
- 完整诊断检查会话存储、工作区注册表、备份目录、tar 可用性、文件权限、运行环境和 Node 运行时版本;手动启动环境以黄色行内警示标注重启无保障(不触发健康提醒横幅、服务控制提醒和标签 ⚠),未识别环境与空备份为信息级提示(不算警告),均可通过 `DSH_SERVICE_RUNTIME_ENV` 显式声明
|
|
34
34
|
- 没有备份属于信息级提示,不算警告,也不点亮健康诊断标签的 ⚠
|
|
35
35
|
- 文件权限深检与修复:检查 Agent 是否能读写 DSH_HOME 和工作区,修复需两段式确认
|
|
36
|
+
- 「插件 → 插件配置」的「健康诊断」开关可整体关闭本标签:关闭后完整诊断与文件权限请求立即停止、宿主拒绝对应 RPC;概览的运行指标(5 秒轮询)不受影响
|
|
36
37
|
|
|
37
38
|
### 模型统计
|
|
38
39
|
|
|
@@ -48,13 +49,14 @@
|
|
|
48
49
|
|
|
49
50
|

|
|
50
51
|
|
|
51
|
-
- 独立的「额度查询」标签:以卡片分区展示已适配的供应商,每个窗口显示百分比、独立进度条,重置时间单独一行;卡片头部更新时间旁有刷新图标,点击即强制重拉该供应商(不受轮询间隔限制);已适配卡片的标题链接到对应官网用量页(DeepSeek 平台、智谱 GLM Coding Plan、OpenCode Go
|
|
52
|
+
- 独立的「额度查询」标签:以卡片分区展示已适配的供应商,每个窗口显示百分比、独立进度条,重置时间单独一行;卡片头部更新时间旁有刷新图标,点击即强制重拉该供应商(不受轮询间隔限制);已适配卡片的标题链接到对应官网用量页(DeepSeek 平台、智谱 GLM Coding Plan、OpenCode Go、小米 MiMo 控制台用量页),点击新标签页打开;卡片列表右上角有「调整排序」开关(至少两张卡时显示),点击后各卡头部出现 ↑/↓ 按钮(首末卡对应方向自动禁用),再点一次收起;顺序记忆在本浏览器,新出现的供应商排在末尾;未适配的不占位置,统一收进底部「手动适配」行选择类型启用,卡片脚部可随时切换适配类型、回退自动识别或停用查询
|
|
52
53
|
- 对话输入框内一枚额度圆环,跟随当前会话所选模型的供应商,显示最紧预算窗口的已用百分比(<80% 绿色、≥80% 黄色);点击弹出面板——头部标明供应商,各窗口带独立进度条与已用百分比、重置时间单独一行;手机等窄屏上面板自动切换为视口居中的浮层(完整可见、超高时内部滚动),旋转或拖宽窗口即时切回圆环上方锚定
|
|
53
54
|
- 可在「额度查询」标签开启「设置页左列显示入口」开关(默认关闭),开启后在设置页左侧标签列底部显示「额度查询」快捷入口(与「重启」入口同模式)
|
|
54
|
-
- 内置适配:**OpenCode Go**(`{baseURL}/usage`)、**智谱 GLM Coding Plan / zai-coding-cn**(官方监控端点 `quota/limit`,含 5 小时滚动 Token、每周 Token、MCP 月度配额三个窗口,5 小时窗口空闲时与官网一致地不显示重置时间)、**OpenRouter**(credits 已用%)、**Kimi/Moonshot** 与 **硅基流动**(人民币余额)、**DeepSeek
|
|
55
|
+
- 内置适配:**OpenCode Go**(`{baseURL}/usage`)、**智谱 GLM Coding Plan / zai-coding-cn**(官方监控端点 `quota/limit`,含 5 小时滚动 Token、每周 Token、MCP 月度配额三个窗口,5 小时窗口空闲时与官网一致地不显示重置时间)、**OpenRouter**(credits 已用%)、**Kimi/Moonshot** 与 **硅基流动**(人民币余额)、**DeepSeek 开放平台**(官方余额,见下一条)、**小米 MiMo Token Plan**(订阅套餐额度,见下一条);原生报「剩余百分比」的方言会自动把面板头部切换为「剩余」并把预警阈值反向;上游瞬时网络错误自动重试,智谱双域候选链自动切换;供应商与适配类型的对应关系保存在 `DSH_HOME/dsh-service-quota.json`,已知服务商按 baseURL 自动识别适配(如 opencode.ai、bigmodel.cn、api.deepseek.com、token-plan-cn.xiaomimimo.com),无需手选;未适配或已停用的供应商不会被发起任何上游请求,停用可在卡片脚部选「停用查询」或在配置文件写 `"<provider>": null`(两者等价)。智谱的重置卡暂无 API Key 可查的接口,可在「额度查询」标签内点各供应商卡片的「添加重置卡」填写名称与到期时间(可精确到分钟),可连续添加多条、每条独立移除;圆环面板同步显示(数据存入同一配置文件),过期自动标注
|
|
55
56
|
- **DeepSeek 官方余额与峰谷提示(deepseek)**:调用 DeepSeek 开放平台官方 `GET /user/balance`,按币种显示总额余额(CNY→¥、USD→$),未过期赠金大于 0 时追加一行「赠送余额」;卡片内嵌**峰谷时段提示块**——顶部为当前状态徽标(橙=「当前高峰时段 · 标准价」、绿=「当前空闲时段 · 半价计费」)和下一次换挡的北京时间倒计时(如「09:00 转高峰(12 小时 41 分钟后)」,跨周末也能算到周一早高峰),下方是一条**两段式**峰谷色带:第一段是当前时段的剩余部分,第二段是下一个相反时段(可跨天,如周五晚直接画到周一早高峰),宽度按实际时长比例、左缘细标线即当前时刻;段内只标「忙时 / 闲时」,每 30 秒自动前进,底部说明行写明官方规则「空闲时段价格为高峰时段的一半」;圆环面板内同步显示紧凑版(无说明行)。凭据按 `DEEPSEEK_API_KEY` 线索从 DSH 凭据库或环境变量解析,也可直接在卡片上用「填写 API 密钥」表单写入。DSH 内置的 `@deepseek-ai/dsh-llm-deepseek` 官方渠道(模型选择器里的 DeepSeek V4 系列,路由名 `deepseek-official`)**无需在 settings 另建路由**——额度查询会自动并入这个运行时渠道并按官方余额适配,切换到它的模型时圆环同样出现
|
|
56
57
|
- **CLIProxyAPI 账号额度(cliproxy)**:查询 CLIProxyAPI(CPA)部署内各 OAuth 上游账号的**官方剩余额度**(Codex 的 5 小时/本周窗口、GeminiCLI 与 Antigravity 的各模型配额),而不是代理 key 的用量。前置条件:CPA 配置 `remote-management.secret-key` 非空(为空时管理面整体不可用)、远程访问需开启 `allow-remote-management`;管理密钥放入 DSH 凭据库 `CPA_MANAGEMENT_KEY` 或同名环境变量——它与代理用的 API key 相互独立,插件绝不会把代理 key 发往管理面。在「手动适配」中选择「CLIProxyAPI 账号额度」保存时,插件会把该供应商 settings baseURL 的域名钉进配置文件,此后外呼只发往这个域(修改 baseURL 后需重新保存适配);单次刷新最多查询 8 个账号(已禁用或无受支持配额端点的账号自动跳过),部分账号失败不影响其余账号的结果。请注意 CPA 管理面对错误密钥有内建封禁(连续错 5 次封 IP 30 分钟),务必填对再测
|
|
57
|
-
-
|
|
58
|
+
- **小米 MiMo Token Plan 额度(xiaomi-token-plan-cn)**:查询小米 MiMo 开放平台 Token Plan 订阅的「套餐使用情况」——套餐总额度、补偿积分等额度桶,与控制台同源,每行显示已用百分比、绝对数缩写(如 `12% · 1.4B / 11B`)和订阅有效期倒计时。小米没有可用 API Key 查询额度的接口(推理网关只有 `/v1` 推理路径),数据来自控制台同源 API,凭据是**网页登录态 Cookie**:浏览器登录 platform.xiaomimimo.com 后打开开发者工具,从任意 `/api/v1/tokenPlan/` 请求复制完整 `Cookie:` 请求头的值,点卡片上的**「填写控制台 Cookie(网页登录态)」**粘贴保存(写入 DSH 凭据库 `XIAOMI_MIMO_CONSOLE_COOKIE` 或同名环境变量;带不带 `Cookie:` 前缀都行)。Cookie 只发往 `platform.xiaomimimo.com/api/v1/tokenPlan/detail` 与 `/usage` 两个固定端点;`tp-` 开头的推理密钥绝不会发往控制台平面。Cookie 失效(退出登录或过期)时卡片显示「控制台 Cookie 已失效」,重新复制一次即可。供应商按 baseURL 自动识别(token-plan-cn.xiaomimimo.com);经中转域接入推理时在「手动适配」中选择「小米 MiMo Token Plan」即可(查询平面固定、不受 baseURL 影响)。请注意网页 Cookie 等同整个控制台的访问权限——它只保存在本机凭据库、只用于上述两个固定端点,请不要把 Cookie 交给不信任的环境
|
|
59
|
+
- 凭据填写窗口:已适配供应商因缺凭据显示「凭据未配置」时,卡片上会出现内联表单——普通适配显示**「填写 API 密钥」**,CLIProxyAPI 适配显示**「填写管理密钥(网页登录的 key)」**(即登录 CPA 网页管理面所用的 remote-management 密钥,不是代理 API key——管理面对错密钥有封禁,别填错),小米 Token Plan 适配显示**「填写控制台 Cookie(网页登录态)」**;凭据名称由宿主按适配类型派生白名单(CLIProxyAPI 只会出现 `CPA_MANAGEMENT_KEY`/`CLIPROXY_MANAGEMENT_KEY`——两者是同一密钥的**别名存放槽,二选一即可**,查找按顺序取第一个已配置的值;小米只出现 `XIAOMI_MIMO_CONSOLE_COOKIE`/`MIMO_CONSOLE_COOKIE`,同理二选一;主名带「主名」标记且表单默认选中已配置的槽位。绝不混入代理 key 或 tp- 推理密钥),输入密钥值保存即写入 DSH 凭据提供方(`$DSH_HOME/.credentials.yaml` 的 `refs:` 分节,热生效无需重启),随后自动强制重拉该供应商;表单里还能一键清除已存的文件层凭据。进程环境变量正在遮蔽该名字时宿主会拒绝写入(凭据库契约),此时请改环境变量本身
|
|
58
60
|
- 防风控节律由宿主统一控制:成功结果缓存 60 秒(多标签共享)、失败指数退避(30 秒起 ×2、封顶 15 分钟)、上游超时 15 秒;面板可把自动查询调成仅手动 / 1 / 2 / 5 / 10 分钟(默认仅手动),页面不可见时自动暂停
|
|
59
61
|
- API key 只在宿主进程内解析使用,浏览器只会收到归一化后的窗口数据(百分比或余额文本);数据走插件自有 loopback RPC,不在 webServer 上暴露任何路由
|
|
60
62
|
|
|
@@ -73,7 +75,7 @@
|
|
|
73
75
|
- 「技能」标签按**自动加载 / 仅手动调用 / 完全停用**三区展示全部本地技能(项目 `.dsh`、项目 `.agents`、用户 `~/.dsh/skills`、`$DSH_AGENTS_HOME` 与 `$DSH_BUNDLED_SKILL_DIR` 根,一层深度),支持名称过滤与来源徽标;同名遮蔽(低 rank 优先)与被遮蔽副本均有标注,内置目录只读展示;同一物理目录被多条规则命中时只计一次
|
|
74
76
|
- 每条目双开关直接改写 SKILL.md frontmatter:`disable-model-invocation` 控制「对模型可见」,`user-invocable` 控制「可被 / 调用」;开关采用两段式点击确认,改动约 200ms 内热生效,活跃会话下一步自动收到目录更新提示;往返切换不会在文件中累积残留
|
|
75
77
|
- 带 camelCase 旧版调用键(如 `disableModelInvocation`)的条目会被官方解析器整条剔除:面板给出 ⚠ 提示,两段式确认后按语义换算修复为规范键
|
|
76
|
-
- ✨「AI
|
|
78
|
+
- ✨「AI 补全说明」:从已配置模型中任选一个(记住上次选择),宿主用固定模板调用该模型生成描述与用法草稿,**输出语言跟随 DSH 设置的界面语言**(中文环境出简体中文、英文环境出英文);草稿以新旧对照预览,显式确认后保存为「AI 注释」。注释**只存在插件的侧车索引 `DSH_HOME/dsh-service-skills-index.json` 里、绝不改写 SKILL.md**,仅在技能标签页该条目下方单独展示(带移除按钮);正文变更后注释自动标记过期,重新补全即可刷新
|
|
77
79
|
- 一键批量补全:自动圈出未注释或正文有变的技能(含只读目录,跳过无效与被遮蔽副本),先展示候选数/预计发送量与可展开的逐条跳过清单,确认后顺序执行并显示进度,单条失败不影响批次,可随时取消(取消会立即中断在途模型调用);全程零文件改动,也绝不自动发起任何模型调用
|
|
78
80
|
- 批量任务在宿主后台持续运行:切换标签、关闭设置面板甚至刷新页面都不会中断;回到技能页自动恢复进度与取消按钮,批量进行中「技能」标签标题实时显示 `⟳已完成/总数` 角标,运行中重复生成计划会被明确拒绝
|
|
79
81
|
|
package/client.js
CHANGED
|
@@ -15,6 +15,7 @@ window.__ModuleLoader__.load({
|
|
|
15
15
|
'features.cardHint': '控制可选功能和外部能力。开关立即生效,无需重启;详细状态与操作位于左侧「服务控制」。',
|
|
16
16
|
'features.optional': '可选功能',
|
|
17
17
|
'features.external': '外部能力',
|
|
18
|
+
'features.healthDiagnostics': '健康诊断',
|
|
18
19
|
'features.modelUsage': '模型统计',
|
|
19
20
|
'features.quotaLookup': '额度查询',
|
|
20
21
|
'features.backupMaintenance': '备份维护',
|
|
@@ -443,6 +444,13 @@ window.__ModuleLoader__.load({
|
|
|
443
444
|
'quota.kind.siliconflow': '硅基流动',
|
|
444
445
|
'quota.kind.deepseek': 'DeepSeek 开放平台',
|
|
445
446
|
'quota.kind.cliproxy': 'CLIProxyAPI 账号额度',
|
|
447
|
+
'quota.kind.xiaomi-token-plan-cn': '小米 MiMo Token Plan',
|
|
448
|
+
'quota.window.total_token': '套餐总额度',
|
|
449
|
+
'quota.window.compensation_total_token': '补偿积分',
|
|
450
|
+
'quota.window.plan-name': '订阅套餐',
|
|
451
|
+
'quota.error.no-subscription': '当前账号没有生效中的 Token Plan 订阅',
|
|
452
|
+
'quota.error.credential-rejected': '控制台 Cookie 已失效,请重新从浏览器复制',
|
|
453
|
+
'quota.credential.editCookie': '填写控制台 Cookie(网页登录态)',
|
|
446
454
|
'quota.kindAuto': '自动识别',
|
|
447
455
|
'quota.noAdapted': '暂无已适配的供应商,可在下方手动适配',
|
|
448
456
|
'quota.disable': '停用查询',
|
|
@@ -498,6 +506,7 @@ window.__ModuleLoader__.load({
|
|
|
498
506
|
'features.cardHint': 'Control optional features and external capabilities. Changes take effect immediately without a restart; detailed status and actions remain in Service Control.',
|
|
499
507
|
'features.optional': 'Optional features',
|
|
500
508
|
'features.external': 'External capabilities',
|
|
509
|
+
'features.healthDiagnostics': 'Health diagnostics',
|
|
501
510
|
'features.modelUsage': 'Model statistics',
|
|
502
511
|
'features.quotaLookup': 'Quota lookup',
|
|
503
512
|
'features.backupMaintenance': 'Backup maintenance',
|
|
@@ -926,6 +935,13 @@ window.__ModuleLoader__.load({
|
|
|
926
935
|
'quota.kind.siliconflow': 'SiliconFlow',
|
|
927
936
|
'quota.kind.cliproxy': 'CLIProxyAPI accounts',
|
|
928
937
|
'quota.kind.deepseek': 'DeepSeek Platform',
|
|
938
|
+
'quota.kind.xiaomi-token-plan-cn': 'Xiaomi MiMo Token Plan',
|
|
939
|
+
'quota.window.total_token': 'Plan total quota',
|
|
940
|
+
'quota.window.compensation_total_token': 'Compensation credits',
|
|
941
|
+
'quota.window.plan-name': 'Subscription plan',
|
|
942
|
+
'quota.error.no-subscription': 'No active Token Plan subscription on this account',
|
|
943
|
+
'quota.error.credential-rejected': 'Console cookie expired; copy it from the browser again',
|
|
944
|
+
'quota.credential.editCookie': 'Set console cookie (web session)',
|
|
929
945
|
'quota.kindAuto': 'Auto-detected',
|
|
930
946
|
'quota.noAdapted': 'No adapted providers yet — adapt manually below',
|
|
931
947
|
'quota.disable': 'Disable',
|
|
@@ -1063,7 +1079,19 @@ window.__ModuleLoader__.load({
|
|
|
1063
1079
|
ctx.effect(() => () => { if (svcStyle) svcStyle.remove() }, 'dsh-service theme styles')
|
|
1064
1080
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-service dictionaries')
|
|
1065
1081
|
const t = ctx.locale.bind(NS)
|
|
1066
|
-
|
|
1082
|
+
// 当前生效界面语言(显式设置 > 浏览器语言 > en 兜底,locale 快照已折算):'zh' | 'en'。
|
|
1083
|
+
// 供 AI 补全等宿主侧语言相关动作取用;宿主只收枚举,不收自由文本。
|
|
1084
|
+
const currentUiLocale = () => ((ctx.locale?.getSnapshot?.()?.active) === 'zh' ? 'zh' : 'en')
|
|
1085
|
+
/** 十进制数量缩写共用实现:模型统计沿用 K/M,额度绝对数可额外启用 B;非法值由调用方指定兜底。 */
|
|
1086
|
+
const formatCompactCount = (value, options = {}) => {
|
|
1087
|
+
const number = Number(value)
|
|
1088
|
+
if (!Number.isFinite(number)) return options.invalid ?? ''
|
|
1089
|
+
if (options.billions === true && number >= 1e9) return `${Math.round(number / 1e8) / 10}B`
|
|
1090
|
+
if (number >= 1e6) return `${Math.round(number / 1e5) / 10}M`
|
|
1091
|
+
if (number >= 1e3) return `${Math.round(number / 1e2) / 10}K`
|
|
1092
|
+
return number.toLocaleString()
|
|
1093
|
+
}
|
|
1094
|
+
const DEFAULT_FEATURES = { healthDiagnostics: true, modelUsage: true, quotaLookup: true, backupMaintenance: true, taskNotifications: true, healthz: true, skillManager: true, subagentRoute: true }
|
|
1067
1095
|
const featureScope = ctx.settingsScope.bind({ namespace: NS })
|
|
1068
1096
|
const featureSnapshot = () => featureScope.getSnapshot()
|
|
1069
1097
|
const featureValue = () => Object.assign({}, DEFAULT_FEATURES, featureSnapshot().value || {})
|
|
@@ -1234,7 +1262,7 @@ window.__ModuleLoader__.load({
|
|
|
1234
1262
|
}
|
|
1235
1263
|
const startSkillsBatchShared = async () => {
|
|
1236
1264
|
if (skillsBatchPlan === null || skillsBatchState === null) return false
|
|
1237
|
-
const res = await ctx.connection.rpc.call('/dsh-service', 'skills-batch-run', { planId: skillsBatchPlan.planId })
|
|
1265
|
+
const res = await ctx.connection.rpc.call('/dsh-service', 'skills-batch-run', { planId: skillsBatchPlan.planId, lang: currentUiLocale() })
|
|
1238
1266
|
if (!res.ok) { skillsBatchError = res.error || 'unknown'; publishSkillsBatch(); return false }
|
|
1239
1267
|
skillsBatchState = { ...skillsBatchState, phase: 'running' }
|
|
1240
1268
|
syncSkillsBatchPolling()
|
|
@@ -1758,7 +1786,7 @@ window.__ModuleLoader__.load({
|
|
|
1758
1786
|
const QUOTA_POLL_KEY = 'dsh-service-quota-poll'
|
|
1759
1787
|
const QUOTA_POLL_CHOICES = [0, 1, 2, 5, 10]
|
|
1760
1788
|
// 适配类型下拉选项:与宿主 QUOTA_KINDS 白名单保持一致(词典键 quota.kind.<kind>)。
|
|
1761
|
-
const QUOTA_KIND_OPTIONS = ['opencode-go', 'zai-coding-cn', 'openrouter', 'kimi', 'siliconflow', 'deepseek', 'cliproxy']
|
|
1789
|
+
const QUOTA_KIND_OPTIONS = ['opencode-go', 'zai-coding-cn', 'openrouter', 'kimi', 'siliconflow', 'deepseek', 'xiaomi-token-plan-cn', 'cliproxy']
|
|
1762
1790
|
function readQuotaPollMinutes() {
|
|
1763
1791
|
try {
|
|
1764
1792
|
const raw = Number.parseInt(localStorage.getItem(QUOTA_POLL_KEY), 10)
|
|
@@ -1961,6 +1989,15 @@ window.__ModuleLoader__.load({
|
|
|
1961
1989
|
const account = typeof window.label === 'string' ? window.label.trim() : ''
|
|
1962
1990
|
return account !== '' ? `${account} · ${kindText}` : kindText
|
|
1963
1991
|
}
|
|
1992
|
+
/** 百分比窗口的数值文本:percent 必显;窗口带 used/limit(原始数值)时追加「已用 / 总量」figure,
|
|
1993
|
+
* 与控制台「{{used}} / {{limit}}」口径一致——只有比例没有绝对数会丢掉最关键的剩余信息。 */
|
|
1994
|
+
function quotaWindowValueText(window) {
|
|
1995
|
+
const percent = `${window.percent}%`
|
|
1996
|
+
const used = Number(window.used)
|
|
1997
|
+
const limit = Number(window.limit)
|
|
1998
|
+
if (!Number.isFinite(used) || !Number.isFinite(limit) || limit <= 0) return percent
|
|
1999
|
+
return `${percent} · ${formatCompactCount(used, { billions: true })} / ${formatCompactCount(limit, { billions: true })}`
|
|
2000
|
+
}
|
|
1964
2001
|
function humanizeDuration(ms, translate) {
|
|
1965
2002
|
// 官网口径:取最显着的两个非零单位(28 天 22 小时 / 4 小时 1 分钟),不足 1 分钟显示 0 分钟。
|
|
1966
2003
|
const totalMinutes = Math.max(0, Math.floor(ms / 60000))
|
|
@@ -2191,7 +2228,7 @@ window.__ModuleLoader__.load({
|
|
|
2191
2228
|
},
|
|
2192
2229
|
React.createElement('div', { 'data-value': window.percent, style: { display: 'flex', justifyContent: 'space-between', gap: '10px', fontSize: '12px', lineHeight: '18px' } },
|
|
2193
2230
|
React.createElement('span', { style: labelStyle }, quotaWindowDisplayLabel(window, translate)),
|
|
2194
|
-
React.createElement('span', { style: valueStyle },
|
|
2231
|
+
React.createElement('span', { style: valueStyle }, quotaWindowValueText(window))),
|
|
2195
2232
|
quotaBar(inCard ? `quota-card-bar-${provider}-${window.id}` : `quota-window-bar-${window.id}`, window.percent, '4px', window.remaining === true),
|
|
2196
2233
|
resetNode)
|
|
2197
2234
|
}
|
|
@@ -2433,7 +2470,7 @@ window.__ModuleLoader__.load({
|
|
|
2433
2470
|
}, React.createElement('path', { d: 'M3 5.25 7 9l4-3.75', fill: 'none', stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round', strokeLinejoin: 'round' }))),
|
|
2434
2471
|
open ? React.createElement('div', { style: { margin: '0 16px', padding: '12px 0 8px', borderTop: '1px solid var(--dsw-alias-border-l2)' } },
|
|
2435
2472
|
React.createElement('div', { style: { fontSize: '12px', fontWeight: 700 } }, translate('features.optional')),
|
|
2436
|
-
['modelUsage', 'quotaLookup', 'backupMaintenance', 'taskNotifications', 'skillManager', 'subagentRoute'].map(row),
|
|
2473
|
+
['healthDiagnostics', 'modelUsage', 'quotaLookup', 'backupMaintenance', 'taskNotifications', 'skillManager', 'subagentRoute'].map(row),
|
|
2437
2474
|
React.createElement('div', { style: { fontSize: '12px', fontWeight: 700, marginTop: '8px', paddingTop: '10px', borderTop: '1px solid var(--dsw-alias-border-l1)' } }, translate('features.external')),
|
|
2438
2475
|
row('healthz'),
|
|
2439
2476
|
!writable ? React.createElement('p', { style: { margin: '6px 0 0', fontSize: '11px', color: 'var(--dsw-alias-label-tertiary)' } }, translate('features.readOnly')) : null) : null)
|
|
@@ -2774,7 +2811,7 @@ window.__ModuleLoader__.load({
|
|
|
2774
2811
|
if (describe === null || describe.modelItem === null) return
|
|
2775
2812
|
const { entry, modelItem } = describe
|
|
2776
2813
|
setDescribe((prev) => ({ ...prev, busy: true, error: '' }))
|
|
2777
|
-
const res = await SKILL_RPC('skills-describe', { id: entry.id, provider: modelItem.provider, model: modelItem.id })
|
|
2814
|
+
const res = await SKILL_RPC('skills-describe', { id: entry.id, provider: modelItem.provider, model: modelItem.id, lang: currentUiLocale() })
|
|
2778
2815
|
if (res.ok) {
|
|
2779
2816
|
try { localStorage.setItem(SKILLS_MODEL_STORAGE_KEY, JSON.stringify({ provider: modelItem.provider, model: modelItem.id })) } catch (_) {}
|
|
2780
2817
|
setDescribe((prev) => ({ ...prev, draft: res.value.draft, busy: false }))
|
|
@@ -3353,7 +3390,8 @@ window.__ModuleLoader__.load({
|
|
|
3353
3390
|
'data-testid': `quota-cred-edit-${row.provider}`,
|
|
3354
3391
|
onClick: () => openCredEditor(row),
|
|
3355
3392
|
style: { fontSize: '12px', lineHeight: '20px', padding: '4px 14px', borderRadius: 999, border: '1px solid var(--dsw-alias-brand-primary)', background: 'transparent', color: 'var(--dsw-alias-brand-primary)', cursor: 'pointer', width: 'auto', minWidth: 0, overflow: 'visible', flex: '0 0 auto', whiteSpace: 'nowrap' },
|
|
3356
|
-
|
|
3393
|
+
// 宿主按 kind registry 下发凭据入口语义键;客户端只本地化,避免新增 kind 时复制分支。
|
|
3394
|
+
}, translate(`quota.credential.${typeof row.credentialEntryKey === 'string' && row.credentialEntryKey !== '' ? row.credentialEntryKey : 'edit'}`)))]
|
|
3357
3395
|
})()
|
|
3358
3396
|
: []),
|
|
3359
3397
|
...resetCardNodes,
|
|
@@ -3480,6 +3518,8 @@ window.__ModuleLoader__.load({
|
|
|
3480
3518
|
return () => { active = false }
|
|
3481
3519
|
}, [])
|
|
3482
3520
|
useEffect(() => {
|
|
3521
|
+
// 健康诊断开关关闭时权限浅检查属于被门禁功能:不发起请求,也不落错误态。
|
|
3522
|
+
if (!featureEnabled('healthDiagnostics')) return () => {}
|
|
3483
3523
|
let active = true
|
|
3484
3524
|
ctx.connection.rpc.call('/dsh-service', 'permissions-plan', {}).then((res) => {
|
|
3485
3525
|
if (!active) return
|
|
@@ -3489,7 +3529,7 @@ window.__ModuleLoader__.load({
|
|
|
3489
3529
|
if (active) setPermissionError(translate('permissions.error'))
|
|
3490
3530
|
})
|
|
3491
3531
|
return () => { active = false }
|
|
3492
|
-
}, [])
|
|
3532
|
+
}, [features.healthDiagnostics])
|
|
3493
3533
|
useEffect(() => {
|
|
3494
3534
|
if (!featureEnabled('modelUsage')) return () => {}
|
|
3495
3535
|
let active = true
|
|
@@ -3543,6 +3583,7 @@ window.__ModuleLoader__.load({
|
|
|
3543
3583
|
}, [])
|
|
3544
3584
|
|
|
3545
3585
|
const runDiagnostics = async (force = false) => {
|
|
3586
|
+
if (!featureEnabled('healthDiagnostics')) return
|
|
3546
3587
|
if (!force && diagnosticsLoadedAt > 0 && Date.now() - diagnosticsLoadedAt <= 30000) return
|
|
3547
3588
|
setDiagnosticsBusy(true)
|
|
3548
3589
|
try {
|
|
@@ -3845,12 +3886,7 @@ window.__ModuleLoader__.load({
|
|
|
3845
3886
|
const usageValue = (totals, metricName) => metricName === 'cacheTokens'
|
|
3846
3887
|
? totals.cacheReadTokens + totals.cacheWriteTokens
|
|
3847
3888
|
: totals[metricName] || 0
|
|
3848
|
-
const formatTokenValue = (value) => {
|
|
3849
|
-
const number = Number(value) || 0
|
|
3850
|
-
if (number >= 1000000) return `${Math.round(number / 100000) / 10}M`
|
|
3851
|
-
if (number >= 1000) return `${Math.round(number / 100) / 10}K`
|
|
3852
|
-
return number.toLocaleString()
|
|
3853
|
-
}
|
|
3889
|
+
const formatTokenValue = (value) => formatCompactCount(value, { invalid: '0' })
|
|
3854
3890
|
const formatUsageValue = (value, metricName) => metricName === 'cacheHitRate'
|
|
3855
3891
|
? (Number(value) * 100).toFixed(1) + '%'
|
|
3856
3892
|
: metricName === 'steps'
|
|
@@ -4294,7 +4330,7 @@ window.__ModuleLoader__.load({
|
|
|
4294
4330
|
const tabs = [
|
|
4295
4331
|
['overview', 'tabs.overview'],
|
|
4296
4332
|
...(features.taskNotifications !== false ? [['notify', 'tabs.notify']] : []),
|
|
4297
|
-
['health', 'tabs.health'],
|
|
4333
|
+
...(features.healthDiagnostics !== false ? [['health', 'tabs.health']] : []),
|
|
4298
4334
|
...(features.modelUsage !== false ? [['usage', 'tabs.usage']] : []),
|
|
4299
4335
|
...(features.quotaLookup !== false ? [['quota', 'tabs.quota']] : []),
|
|
4300
4336
|
...(features.backupMaintenance !== false ? [['backup', 'tabs.backup']] : []),
|
package/index.js
CHANGED
|
@@ -18,6 +18,7 @@ const DSH_PACKAGE = '@deepseek-ai/dsh'
|
|
|
18
18
|
const PLUGIN_PACKAGE = '@gehennawu/dsh-service'
|
|
19
19
|
const SETTINGS_NAMESPACE = 'dsh-service'
|
|
20
20
|
const DEFAULT_FEATURE_SETTINGS = Object.freeze({
|
|
21
|
+
healthDiagnostics: true,
|
|
21
22
|
modelUsage: true,
|
|
22
23
|
quotaLookup: true,
|
|
23
24
|
backupMaintenance: true,
|
|
@@ -27,6 +28,7 @@ const DEFAULT_FEATURE_SETTINGS = Object.freeze({
|
|
|
27
28
|
subagentRoute: true,
|
|
28
29
|
})
|
|
29
30
|
const FeatureSettingsSchema = z.object({
|
|
31
|
+
healthDiagnostics: z.boolean().default(true),
|
|
30
32
|
modelUsage: z.boolean().default(true),
|
|
31
33
|
quotaLookup: z.boolean().default(true),
|
|
32
34
|
backupMaintenance: z.boolean().default(true),
|
|
@@ -124,15 +126,29 @@ const KIND_REGISTRY = {
|
|
|
124
126
|
// CLIProxyAPI(router-for-me/CLIProxyAPI)管理面:查它托管的 OAuth 上游账号官方额度。
|
|
125
127
|
// 无固定 endpoints / 全局 hosts——每个部署一个域名,保存适配时把 settings baseURL 的
|
|
126
128
|
// hostname 钉进配置 allowedHosts[provider](见 quota-config 写入口),外呼前复验精确命中。
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
129
|
+
// credentialPolicy 把「凭据从哪里取、是否包 Bearer、客户端入口文案」收进 kind 注册;
|
|
130
|
+
// configEndpointPolicy:'pinned' 表示保存适配时把 settings baseURL 主机钉入 allowedHosts。
|
|
131
|
+
// CLIProxyAPI 的 remote-management.secret-key 绝不使用 settings apiKeyEnv(代理 key),避免撞 fail2ban。
|
|
130
132
|
cliproxy: {
|
|
131
133
|
fetcher: fetchCliproxyUsage,
|
|
132
134
|
fetcherGuard: cliproxyFetchGuard,
|
|
133
135
|
keyHints: ['CPA_MANAGEMENT_KEY', 'CLIPROXY_MANAGEMENT_KEY'],
|
|
134
136
|
hosts: [],
|
|
135
|
-
|
|
137
|
+
credentialPolicy: { includeProfileHint: false, format: 'bearer', entryKey: 'editManagement' },
|
|
138
|
+
configEndpointPolicy: 'pinned',
|
|
139
|
+
},
|
|
140
|
+
// 小米 MiMo Token Plan(v0.29):推理网关(token-plan-cn.xiaomimimo.com)只有 /v1 推理路径,
|
|
141
|
+
// 无任何 API-key 形态的额度查询端点(逐路径探测 404;OpenClaw 官方插件的 fetchUsageSnapshot
|
|
142
|
+
// 是空窗口占位)。额度数据只在控制台同源 API(platform.xiaomimimo.com/api/v1/tokenPlan/*),
|
|
143
|
+
// 认证是网页登录态 Cookie:凭据按裸值交给 fetcher,settings apiKeyEnv(tp- 推理密钥)绝不进入
|
|
144
|
+
// 控制台凭据链;configEndpointPolicy:'fixed' 表示查询平面由宿主常量固定、与 baseURL 无关。
|
|
145
|
+
'xiaomi-token-plan-cn': {
|
|
146
|
+
fetcher: fetchXiaomiTokenPlanUsage,
|
|
147
|
+
keyHints: ['XIAOMI_MIMO_CONSOLE_COOKIE', 'MIMO_CONSOLE_COOKIE'],
|
|
148
|
+
hosts: ['token-plan-cn.xiaomimimo.com'],
|
|
149
|
+
usageUrl: 'https://platform.xiaomimimo.com/console/usage',
|
|
150
|
+
credentialPolicy: { includeProfileHint: false, format: 'raw', entryKey: 'editCookie' },
|
|
151
|
+
configEndpointPolicy: 'fixed',
|
|
136
152
|
},
|
|
137
153
|
}
|
|
138
154
|
const QUOTA_KINDS = Object.keys(KIND_REGISTRY)
|
|
@@ -932,12 +948,25 @@ function normalizeResetTimestamp(value) {
|
|
|
932
948
|
* 凭据填写窗口(quota-credential-set/unset)只接受这份清单里的名字——零输入拼接:浏览器传来的
|
|
933
949
|
* 名字必须命中宿主派生的白名单,顺带保证 CredentialRef 文法(POSIX shell 标识符)合法。
|
|
934
950
|
*/
|
|
951
|
+
function quotaCredentialPolicy(kind) {
|
|
952
|
+
const registered = KIND_REGISTRY[kind]
|
|
953
|
+
return {
|
|
954
|
+
includeProfileHint: registered?.credentialPolicy?.includeProfileHint !== false,
|
|
955
|
+
format: registered?.credentialPolicy?.format === 'raw' ? 'raw' : 'bearer',
|
|
956
|
+
entryKey: typeof registered?.credentialPolicy?.entryKey === 'string' && registered.credentialPolicy.entryKey !== ''
|
|
957
|
+
? registered.credentialPolicy.entryKey
|
|
958
|
+
: 'edit',
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
935
962
|
function quotaCredentialHintNames(kind, profile) {
|
|
936
963
|
const names = []
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
964
|
+
const registered = KIND_REGISTRY[kind]
|
|
965
|
+
const policy = quotaCredentialPolicy(kind)
|
|
966
|
+
// 独立管理面/控制台面的 kind 通过 policy 排除 settings apiKeyEnv;那通常是代理/推理 key,
|
|
967
|
+
// 不属于额度查询凭据,错误回退既无效也扩大泄露面。
|
|
968
|
+
if (policy.includeProfileHint && profile.apiKeyEnv !== '') names.push(profile.apiKeyEnv)
|
|
969
|
+
for (const name of registered?.keyHints ?? []) {
|
|
941
970
|
if (!names.includes(name)) names.push(name)
|
|
942
971
|
}
|
|
943
972
|
return names
|
|
@@ -951,13 +980,17 @@ function quotaCredentialHintNames(kind, profile) {
|
|
|
951
980
|
*/
|
|
952
981
|
async function discoverQuotaCredential(ctx, kind, profile) {
|
|
953
982
|
const attempted = quotaCredentialHintNames(kind, profile)
|
|
983
|
+
const policy = quotaCredentialPolicy(kind)
|
|
984
|
+
const formatCredential = (value) => policy.format === 'raw' ? value : `Bearer ${value}`
|
|
954
985
|
const envHas = (name) => typeof process.env[name] === 'string' && process.env[name].trim() !== ''
|
|
955
986
|
const credentials = ctx.get('credentials')
|
|
956
987
|
if (credentials !== undefined && typeof credentials.resolve === 'function') {
|
|
957
988
|
for (const name of attempted) {
|
|
958
989
|
try {
|
|
959
990
|
const hit = await Promise.resolve(credentials.resolve(name))
|
|
960
|
-
if (hit !== undefined && typeof hit.value === 'string' && hit.value !== '')
|
|
991
|
+
if (hit !== undefined && typeof hit.value === 'string' && hit.value !== '') {
|
|
992
|
+
return formatCredential(hit.value)
|
|
993
|
+
}
|
|
961
994
|
} catch (_) {}
|
|
962
995
|
}
|
|
963
996
|
} else if (profile.apiKeyEnv !== '' && !attempted.some(envHas)) {
|
|
@@ -965,7 +998,7 @@ async function discoverQuotaCredential(ctx, kind, profile) {
|
|
|
965
998
|
}
|
|
966
999
|
for (const name of attempted) {
|
|
967
1000
|
const value = process.env[name]
|
|
968
|
-
if (typeof value === 'string' && value.trim() !== '') return
|
|
1001
|
+
if (typeof value === 'string' && value.trim() !== '') return formatCredential(value.trim())
|
|
969
1002
|
}
|
|
970
1003
|
return undefined
|
|
971
1004
|
}
|
|
@@ -1082,6 +1115,90 @@ function deepseekMoneyText(amount, currency) {
|
|
|
1082
1115
|
return symbol === '' ? `${amount} ${currency}` : `${symbol}${amount}`
|
|
1083
1116
|
}
|
|
1084
1117
|
|
|
1118
|
+
// ─── 小米 MiMo Token Plan(xiaomi-token-plan-cn)控制台查询 ─────────────────
|
|
1119
|
+
// 数据源是控制台同源 API(platform.xiaomimimo.com SPA bundle 核实):前端统一走
|
|
1120
|
+
// `/api/v1` 前缀 + same-origin Cookie,无任何 API-key 查询端点。两个 GET 都是宿主常量,
|
|
1121
|
+
// Cookie 凭据只发往这两个固定地址。「套餐使用情况」页 = detail(套餐名/有效期)+ usage(额度桶)。
|
|
1122
|
+
const XIAOMI_TOKEN_PLAN_DETAIL_URL = 'https://platform.xiaomimimo.com/api/v1/tokenPlan/detail'
|
|
1123
|
+
const XIAOMI_TOKEN_PLAN_USAGE_URL = 'https://platform.xiaomimimo.com/api/v1/tokenPlan/usage'
|
|
1124
|
+
|
|
1125
|
+
/** 控制台信封解包:{code,message,data},code∈{0,200} 视为成功(与控制台前端同一口径)。
|
|
1126
|
+
* code=401(登录态失效)→ 稳定错误码 credential-rejected;其余非零码 → bad-payload 并透出 message。 */
|
|
1127
|
+
function unwrapXiaomiConsoleEnvelope(payload) {
|
|
1128
|
+
if (payload === null || typeof payload !== 'object') throw new Error('bad-payload')
|
|
1129
|
+
const code = Number(payload.code)
|
|
1130
|
+
if (!Number.isFinite(code)) throw new Error('bad-payload')
|
|
1131
|
+
if (code === 0 || code === 200) return payload.data !== null && typeof payload.data === 'object' ? payload.data : {}
|
|
1132
|
+
const detail = sanitizeQuotaErrorDetail(typeof payload.message === 'string' ? payload.message : undefined)
|
|
1133
|
+
const error = new Error(code === 401 ? 'credential-rejected' : 'bad-payload')
|
|
1134
|
+
if (detail !== undefined) error.detail = detail
|
|
1135
|
+
throw error
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* usage.items[] → 统一窗口:每项 {name, percent(0..1 小数), used, limit},与控制台
|
|
1140
|
+
* 「套餐使用情况」进度条同源。percent×100 截断(控制台同款 min(100,max(0,100p))),
|
|
1141
|
+
* 缺 percent 的桶跳过(控制台对非法 percent 显示「—」,我们宁可少一行也不显示假进度);
|
|
1142
|
+
* compensation_total_token 在 limit===0 时控制台不渲染(账号无补偿积分),同样丢弃。
|
|
1143
|
+
* used/limit 原始数值随窗口下发、由客户端做 K/M/B 缩写——数字不是文案,宿主不拼句子。
|
|
1144
|
+
* detail.planName 有值时输出一个文本窗口置顶;currentPeriodEnd 解析成功且未过期时作为
|
|
1145
|
+
* resetsAt 附到百分比窗口(订阅有效期即额度清零点;已失效套餐不再挂未来时刻误导倒计时)。
|
|
1146
|
+
*/
|
|
1147
|
+
function normalizeXiaomiTokenPlanUsage(detailData, usageData) {
|
|
1148
|
+
const windows = []
|
|
1149
|
+
const planName = typeof detailData?.planName === 'string' ? detailData.planName.trim().slice(0, MAX_QUOTA_PROVIDER_NAME) : ''
|
|
1150
|
+
if (planName !== '') windows.push({ id: 'plan', kindKey: 'plan-name', text: planName })
|
|
1151
|
+
const periodEnd = detailData?.expired === true ? undefined : normalizeResetTimestamp(detailData?.currentPeriodEnd)
|
|
1152
|
+
const items = Array.isArray(usageData?.usage?.items) ? usageData.usage.items : []
|
|
1153
|
+
const seenNames = new Set()
|
|
1154
|
+
for (const item of items) {
|
|
1155
|
+
if (item === null || typeof item !== 'object') continue
|
|
1156
|
+
const name = typeof item.name === 'string' ? item.name.trim() : ''
|
|
1157
|
+
if (name === '' || seenNames.has(name)) continue
|
|
1158
|
+
if (name === 'compensation_total_token' && Number(item.limit) === 0) continue
|
|
1159
|
+
const fraction = Number(item.percent)
|
|
1160
|
+
if (!Number.isFinite(fraction)) continue
|
|
1161
|
+
seenNames.add(name)
|
|
1162
|
+
const used = Number(item.used)
|
|
1163
|
+
const limit = Number(item.limit)
|
|
1164
|
+
windows.push({
|
|
1165
|
+
id: name,
|
|
1166
|
+
kindKey: name,
|
|
1167
|
+
percent: Math.max(0, Math.min(100, Math.round(fraction * 100))),
|
|
1168
|
+
...(Number.isFinite(used) && used >= 0 ? { used } : {}),
|
|
1169
|
+
...(Number.isFinite(limit) && limit > 0 ? { limit } : {}),
|
|
1170
|
+
...(periodEnd !== undefined ? { resetsAt: periodEnd } : {}),
|
|
1171
|
+
})
|
|
1172
|
+
}
|
|
1173
|
+
return windows
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/**
|
|
1177
|
+
* 小米 Token Plan 编排:detail + usage 两次固定 GET,Cookie 认证(无候选链、无钉住域——
|
|
1178
|
+
* 查询平面与 settings baseURL 完全无关)。credential 是 discoverQuotaCredential 按 kind policy
|
|
1179
|
+
* 下发的裸 Cookie;容错处理粘贴带入的 `Cookie:` 前缀;HTTP 401/403 与信封 code:401 都归一为
|
|
1180
|
+
* credential-rejected(Cookie 失效是唯一常见故障,别让用户猜状态码)。
|
|
1181
|
+
*/
|
|
1182
|
+
async function fetchXiaomiTokenPlanUsage({ credential, signal }) {
|
|
1183
|
+
const cookie = String(credential ?? '').trim().replace(/^cookie:\s*/i, '')
|
|
1184
|
+
if (cookie === '') throw new Error('credential-missing')
|
|
1185
|
+
const fetchEnvelope = async (url) => {
|
|
1186
|
+
try {
|
|
1187
|
+
return unwrapXiaomiConsoleEnvelope(await requestQuotaJson(url, { cookie, signal }))
|
|
1188
|
+
} catch (error) {
|
|
1189
|
+
if (error?.message === 'http-status:401' || error?.message === 'http-status:403') throw new Error('credential-rejected')
|
|
1190
|
+
throw error
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
const detailData = await fetchEnvelope(XIAOMI_TOKEN_PLAN_DETAIL_URL)
|
|
1194
|
+
const usageData = await fetchEnvelope(XIAOMI_TOKEN_PLAN_USAGE_URL)
|
|
1195
|
+
const hasPlan = typeof detailData?.planCode === 'string' && detailData.planCode.trim() !== ''
|
|
1196
|
+
const windows = normalizeXiaomiTokenPlanUsage(detailData, usageData)
|
|
1197
|
+
if (!hasPlan && windows.length === 0) throw new Error('no-subscription')
|
|
1198
|
+
return windows
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
|
|
1085
1202
|
// ─── CLIProxyAPI(cliproxy)管理面编排 ──────────────────────────────────────
|
|
1086
1203
|
// 上游官方额度端点注册表(经 CPA api-call 代调,header 的 $TOKEN$ 由 CPA 替换为对应账号凭据)。
|
|
1087
1204
|
// 形状来源:muyouzhi6/astrbot_plugin_cliproxy_stats + CPA main 分支源码(2026-08 核实);
|
|
@@ -1241,7 +1358,8 @@ function parseCliproxyUpstream(provider, payload) {
|
|
|
1241
1358
|
* 预算:账号 ≤8、api-call 总次数 ≤12、并发 3、窗口总数 ≤32;部分账号失败不拖垮整行
|
|
1242
1359
|
* (有成功窗口即返回),全部失败抛首个稳定错误码。守卫与凭据由 kickQuotaRefresh 先行完成。
|
|
1243
1360
|
*/
|
|
1244
|
-
async function fetchCliproxyUsage({ profile, config,
|
|
1361
|
+
async function fetchCliproxyUsage({ profile, config, credential, signal }) {
|
|
1362
|
+
const authorization = credential
|
|
1245
1363
|
const pinned = Array.isArray(config?.allowedHosts?.[profile.name]) ? config.allowedHosts[profile.name] : []
|
|
1246
1364
|
const origin = safeCliproxyOrigin(profile.baseURL, pinned)
|
|
1247
1365
|
if (origin === undefined) throw new Error('host-not-pinned')
|
|
@@ -1450,6 +1568,7 @@ async function fetchProviderUsage(endpoint, authorization, options = {}) {
|
|
|
1450
1568
|
/**
|
|
1451
1569
|
* 单次 JSON 请求(GET/POST,POST 带 JSON body):CLIProxyAPI 管理面专用(GET auth-files +
|
|
1452
1570
|
* POST api-call)。与 GET 版同样的 15s 超时、64KB 上限、瞬时错误白名单;body 只接受已序列化字符串。
|
|
1571
|
+
* options.cookie(v0.29 小米控制台):按 Cookie 头直发登录态,与 Authorization 互斥使用。
|
|
1453
1572
|
*/
|
|
1454
1573
|
function requestQuotaJsonOnce(endpoint, options = {}) {
|
|
1455
1574
|
return new Promise((resolve, reject) => {
|
|
@@ -1472,6 +1591,7 @@ function requestQuotaJsonOnce(endpoint, options = {}) {
|
|
|
1472
1591
|
'user-agent': `dsh-service/${pluginVersion} (DeepSeek Harness plugin)`,
|
|
1473
1592
|
...(body === '' ? {} : { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }),
|
|
1474
1593
|
...(options.authorization === undefined || options.authorization === '' ? {} : { Authorization: options.authorization }),
|
|
1594
|
+
...(typeof options.cookie === 'string' && options.cookie !== '' ? { Cookie: options.cookie } : {}),
|
|
1475
1595
|
},
|
|
1476
1596
|
}, (response) => {
|
|
1477
1597
|
const status = response.statusCode || 0
|
|
@@ -2734,12 +2854,23 @@ async function mutateSkillEntryById(ctx, dshHome, index, id, allowInvalid, mutat
|
|
|
2734
2854
|
return { ok: true, entry: publicSkillEntry(fresh, index), entryPath: entry.path }
|
|
2735
2855
|
}
|
|
2736
2856
|
|
|
2737
|
-
|
|
2857
|
+
// 补全输出语言白名单:只认 'zh',其余一切输入(含缺省/伪造)一律按 'en' 处理——
|
|
2858
|
+
// 与 DSH locale 服务的 en 兜底语义一致;语言是枚举开关,浏览器永远送不进自由文本。
|
|
2859
|
+
function normalizeSkillDescribeLang(value) {
|
|
2860
|
+
return value === 'zh' ? 'zh' : 'en'
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
function skillDescribeSystemPrompt(lang) {
|
|
2864
|
+
// 输出语言跟随 DSH 界面语言(客户端下发 effective locale 枚举):中文环境出简体中文,
|
|
2865
|
+
// 英文环境出英文;无论哪种,都不跟随技能正文自身的语言。
|
|
2866
|
+
const languageRule = lang === 'zh'
|
|
2867
|
+
? '"description" and "whenToUse" MUST be written in Simplified Chinese regardless of the skill body language'
|
|
2868
|
+
: '"description" and "whenToUse" MUST be written in English regardless of the skill body language'
|
|
2738
2869
|
return [
|
|
2739
2870
|
'You write catalog metadata for agent-harness skills.',
|
|
2740
2871
|
'Reply with STRICT JSON only, no markdown fences, no extra keys, no commentary:',
|
|
2741
2872
|
'{"description":"...","whenToUse":"..."}',
|
|
2742
|
-
'Rules:
|
|
2873
|
+
'Rules: ' + languageRule + '; "description" is ONE routing sentence (max ' + SKILL_DESCRIPTION_MAX_CHARS + ' characters) saying what the skill does and when to pick it; "whenToUse" is short usage guidance (max ' + SKILL_USAGE_MAX_CHARS + ' characters); never use line breaks inside either value.',
|
|
2743
2874
|
].join(' ')
|
|
2744
2875
|
}
|
|
2745
2876
|
|
|
@@ -2784,7 +2915,7 @@ async function collectLlmText(llm, options) {
|
|
|
2784
2915
|
const stream = llm.stream({
|
|
2785
2916
|
provider: options.provider,
|
|
2786
2917
|
model: options.model,
|
|
2787
|
-
system: skillDescribeSystemPrompt(),
|
|
2918
|
+
system: skillDescribeSystemPrompt(normalizeSkillDescribeLang(options.lang)),
|
|
2788
2919
|
messages: [createSkillDescribeMessage(options.prompt)],
|
|
2789
2920
|
maxTokens: options.maxTokens ?? SKILL_DESCRIBE_MAX_TOKENS,
|
|
2790
2921
|
signal: controller.signal,
|
|
@@ -2848,7 +2979,7 @@ async function describeSkillDraft(llm, entryName, rawContent, provider, model, o
|
|
|
2848
2979
|
try {
|
|
2849
2980
|
onEvent?.('attempt', { n: attempt + 1, total: SKILL_DESCRIBE_ATTEMPTS, route: provider + '/' + model })
|
|
2850
2981
|
// 重试时逐级放大输出预算:推理型模型可能耗尽配额却产不出正文。
|
|
2851
|
-
const text = await collectLlmText(llm, { provider, model, prompt, onEvent, maxTokens: SKILL_DESCRIBE_MAX_TOKENS * Math.pow(4, attempt), signal: options.signal })
|
|
2982
|
+
const text = await collectLlmText(llm, { provider, model, prompt, onEvent, maxTokens: SKILL_DESCRIBE_MAX_TOKENS * Math.pow(4, attempt), signal: options.signal, lang: options.lang })
|
|
2852
2983
|
onEvent?.('received', { chars: text.length })
|
|
2853
2984
|
const draft = extractSkillDraftJson(text)
|
|
2854
2985
|
onEvent?.('parsed')
|
|
@@ -3187,14 +3318,14 @@ function apply(ctx) {
|
|
|
3187
3318
|
// 编排类 kind(cliproxy):端点守卫 → 凭据 → fetcher 合并多账号窗口,无候选链。
|
|
3188
3319
|
const guard = registered.fetcherGuard?.(profile, config) ?? null
|
|
3189
3320
|
if (guard !== null) throw new Error(guard)
|
|
3190
|
-
const
|
|
3191
|
-
if (
|
|
3321
|
+
const credential = await discoverQuotaCredential(ctx, kind, profile)
|
|
3322
|
+
if (credential === undefined) throw new Error('credential-missing')
|
|
3192
3323
|
const controller = new AbortController()
|
|
3193
3324
|
quotaAbortControllers.add(controller)
|
|
3194
3325
|
const deadline = setTimeout(() => controller.abort(), QUOTA_PROVIDER_DEADLINE_MS)
|
|
3195
3326
|
let produced
|
|
3196
3327
|
try {
|
|
3197
|
-
produced = await registered.fetcher({ profile, config,
|
|
3328
|
+
produced = await registered.fetcher({ profile, config, credential, signal: controller.signal })
|
|
3198
3329
|
} finally {
|
|
3199
3330
|
clearTimeout(deadline)
|
|
3200
3331
|
quotaAbortControllers.delete(controller)
|
|
@@ -3395,6 +3526,7 @@ function apply(ctx) {
|
|
|
3395
3526
|
}
|
|
3396
3527
|
|
|
3397
3528
|
if (endpoint === 'diagnostics') {
|
|
3529
|
+
if (!featureEnabled('healthDiagnostics')) return { ok: false, error: 'feature-disabled' }
|
|
3398
3530
|
try {
|
|
3399
3531
|
return { ok: true, value: await collectDiagnostics(ctx, dshHome, runtimeEnv) }
|
|
3400
3532
|
} catch (error) {
|
|
@@ -3424,6 +3556,7 @@ function apply(ctx) {
|
|
|
3424
3556
|
}
|
|
3425
3557
|
|
|
3426
3558
|
if (endpoint === 'permissions-plan') {
|
|
3559
|
+
if (!featureEnabled('healthDiagnostics')) return { ok: false, error: 'feature-disabled' }
|
|
3427
3560
|
try {
|
|
3428
3561
|
return { ok: true, value: await permissionSnapshot(ctx, dshHome, permissionPlans) }
|
|
3429
3562
|
} catch (error) {
|
|
@@ -3432,6 +3565,7 @@ function apply(ctx) {
|
|
|
3432
3565
|
}
|
|
3433
3566
|
|
|
3434
3567
|
if (endpoint === 'permissions-deep') {
|
|
3568
|
+
if (!featureEnabled('healthDiagnostics')) return { ok: false, error: 'feature-disabled' }
|
|
3435
3569
|
try {
|
|
3436
3570
|
const value = await deepCheckPermissions(dshHome, permissionPlans, payload?.planId)
|
|
3437
3571
|
if (value === undefined) return { ok: false, error: 'unknown-permission-plan' }
|
|
@@ -3442,6 +3576,7 @@ function apply(ctx) {
|
|
|
3442
3576
|
}
|
|
3443
3577
|
|
|
3444
3578
|
if (endpoint === 'permissions-repair') {
|
|
3579
|
+
if (!featureEnabled('healthDiagnostics')) return { ok: false, error: 'feature-disabled' }
|
|
3445
3580
|
try {
|
|
3446
3581
|
const value = await repairPermissions(ctx, dshHome, permissionPlans, payload?.planId)
|
|
3447
3582
|
if (value === undefined) return { ok: false, error: 'unknown-permission-plan' }
|
|
@@ -3595,7 +3730,7 @@ function apply(ctx) {
|
|
|
3595
3730
|
// 注册进活动调用表:Fiber 销毁时立即中断,不再僵尸到 90s 超时。
|
|
3596
3731
|
const call = registerSkillCall()
|
|
3597
3732
|
try {
|
|
3598
|
-
const draft = await describeSkillDraft(llm, entry.name ?? '', raw, provider, model, (code, params) => job.push(code, params), { signal: call.signal })
|
|
3733
|
+
const draft = await describeSkillDraft(llm, entry.name ?? '', raw, provider, model, (code, params) => job.push(code, params), { signal: call.signal, lang: normalizeSkillDescribeLang(payload?.lang) })
|
|
3599
3734
|
return { ok: true, value: { draft } }
|
|
3600
3735
|
} finally {
|
|
3601
3736
|
call.done()
|
|
@@ -3682,6 +3817,8 @@ function apply(ctx) {
|
|
|
3682
3817
|
if (skillsBatch.running || skillsBatch.phase === 'done' || skillsBatch.phase === 'cancelled') return { ok: false, error: 'batch-already-' + (skillsBatch.running ? 'running' : skillsBatch.phase) }
|
|
3683
3818
|
skillsBatch.phase = 'running'
|
|
3684
3819
|
skillsBatch.running = true
|
|
3820
|
+
// 补全语言在 run 时刻定格(而非 plan 时刻):计划确认前切换界面语言,按新语言补全。
|
|
3821
|
+
skillsBatch.lang = normalizeSkillDescribeLang(payload?.lang)
|
|
3685
3822
|
// 批量级 AbortController:取消/销毁时立即中断在途 LLM 调用(不只等当前条目自然结束)。
|
|
3686
3823
|
const batchCall = registerSkillCall()
|
|
3687
3824
|
// 有意不 await:批量在后台顺序执行,客户端轮询 skills-batch-status 取进度。
|
|
@@ -3715,7 +3852,7 @@ function apply(ctx) {
|
|
|
3715
3852
|
const evaluated = evaluateSkillFile(raw)
|
|
3716
3853
|
if (evaluated.invalid !== undefined) throw new Error('entry-changed')
|
|
3717
3854
|
const located = locateSkillFrontmatter(raw)
|
|
3718
|
-
const draft = await describeSkillDraft(llm, entry.name ?? '', raw, skillsBatch.provider, skillsBatch.model, batchLog, { signal: batchCall.signal })
|
|
3855
|
+
const draft = await describeSkillDraft(llm, entry.name ?? '', raw, skillsBatch.provider, skillsBatch.model, batchLog, { signal: batchCall.signal, lang: skillsBatch.lang })
|
|
3719
3856
|
// 注释只进侧车索引:文件零改动,正文哈希取当前内容(正文再变更即自动回到待补全)。
|
|
3720
3857
|
await serializeSkillsIndexWrite((current) => {
|
|
3721
3858
|
current[entry.path] = { bodyHash: bodyHashOf(raw, located?.bodyStart ?? 0), note: { description: draft.description, usage: draft.usage }, model: skillsBatch.provider + '/' + skillsBatch.model, at: Date.now() }
|
|
@@ -3832,6 +3969,8 @@ function apply(ctx) {
|
|
|
3832
3969
|
nextAllowedAt: view.nextAllowedAt,
|
|
3833
3970
|
...(providerResetCards.length > 0 ? { resetCards: providerResetCards } : {}),
|
|
3834
3971
|
...(credentialHints !== undefined ? { credentialHints } : {}),
|
|
3972
|
+
// 凭据入口语义由 kind registry 下发稳定键,客户端只负责本地化,不再对 kind 重复分支。
|
|
3973
|
+
credentialEntryKey: quotaCredentialPolicy(kind).entryKey,
|
|
3835
3974
|
// 官网用户页余额网址(宿主常量白名单随 kind 下发;无则缺省)。
|
|
3836
3975
|
...(typeof KIND_REGISTRY[kind].usageUrl === 'string' && KIND_REGISTRY[kind].usageUrl !== '' ? { usageUrl: KIND_REGISTRY[kind].usageUrl } : {}),
|
|
3837
3976
|
})
|
|
@@ -3880,8 +4019,9 @@ function apply(ctx) {
|
|
|
3880
4019
|
} else {
|
|
3881
4020
|
const kind = payload?.kind
|
|
3882
4021
|
if (kind !== null && !QUOTA_KINDS.includes(kind)) return { save: false, value: { ok: false, error: 'unknown-kind' } }
|
|
3883
|
-
|
|
3884
|
-
|
|
4022
|
+
const endpointPolicy = kind === null ? 'dynamic' : (KIND_REGISTRY[kind]?.configEndpointPolicy ?? 'dynamic')
|
|
4023
|
+
if (endpointPolicy === 'pinned') {
|
|
4024
|
+
// 钉住查询面 kind:把 settings baseURL 的 host 钉进本条目(服务端派生,浏览器零输入)。
|
|
3885
4025
|
// 外呼时复验同一张表——之后改 baseURL 必须重新保存适配才恢复外呼。
|
|
3886
4026
|
const host = cliproxyPinHostFromBaseURL(profileForProvider.baseURL)
|
|
3887
4027
|
if (host === undefined) {
|
|
@@ -3889,10 +4029,13 @@ function apply(ctx) {
|
|
|
3889
4029
|
}
|
|
3890
4030
|
config.allowedHosts[providerName] = [host]
|
|
3891
4031
|
} else {
|
|
3892
|
-
|
|
4032
|
+
// fixed 查询面由宿主常量决定、与 baseURL 无关;dynamic 则必须能从注册表/baseURL 解出安全端点。
|
|
4033
|
+
if (kind !== null && endpointPolicy !== 'fixed'
|
|
4034
|
+
&& Array.isArray(KIND_REGISTRY[kind]?.endpoints) === false
|
|
4035
|
+
&& quotaEndpointFor(kind, profileForProvider.baseURL).length === 0) {
|
|
3893
4036
|
return { save: false, value: { ok: false, error: 'unsafe-provider-endpoint' } }
|
|
3894
4037
|
}
|
|
3895
|
-
//
|
|
4038
|
+
// 非 pinned kind(含显式停用/清除)不保留钉住记录,避免陈旧域残留。
|
|
3896
4039
|
delete config.allowedHosts[providerName]
|
|
3897
4040
|
}
|
|
3898
4041
|
config.kinds[providerName] = kind
|
|
@@ -4073,6 +4216,7 @@ export {
|
|
|
4073
4216
|
extractSkillDraftJson,
|
|
4074
4217
|
fetchCliproxyUsage,
|
|
4075
4218
|
fetchProviderUsage,
|
|
4219
|
+
fetchXiaomiTokenPlanUsage,
|
|
4076
4220
|
fixLegacySkillInvocationKeys,
|
|
4077
4221
|
inferQuotaKind,
|
|
4078
4222
|
inject,
|
|
@@ -4086,6 +4230,7 @@ export {
|
|
|
4086
4230
|
normalizeOpencodeUsage,
|
|
4087
4231
|
normalizeOpenRouterCredits,
|
|
4088
4232
|
normalizeSiliconFlowInfo,
|
|
4233
|
+
normalizeXiaomiTokenPlanUsage,
|
|
4089
4234
|
normalizeZaiCodingUsage,
|
|
4090
4235
|
parseQuotaConfigText,
|
|
4091
4236
|
parseSkillFrontmatterData,
|
|
@@ -4103,6 +4248,7 @@ export {
|
|
|
4103
4248
|
setSkillInvocationKey,
|
|
4104
4249
|
skillIdFor,
|
|
4105
4250
|
unwrapCliproxyApiCallEnvelope,
|
|
4251
|
+
unwrapXiaomiConsoleEnvelope,
|
|
4106
4252
|
}
|
|
4107
4253
|
export default {
|
|
4108
4254
|
SKILL_SOURCE_RANK,
|
|
@@ -4117,6 +4263,7 @@ export default {
|
|
|
4117
4263
|
extractSkillDraftJson,
|
|
4118
4264
|
fetchCliproxyUsage,
|
|
4119
4265
|
fetchProviderUsage,
|
|
4266
|
+
fetchXiaomiTokenPlanUsage,
|
|
4120
4267
|
fixLegacySkillInvocationKeys,
|
|
4121
4268
|
inferQuotaKind,
|
|
4122
4269
|
inject,
|
|
@@ -4130,6 +4277,7 @@ export default {
|
|
|
4130
4277
|
normalizeOpencodeUsage,
|
|
4131
4278
|
normalizeOpenRouterCredits,
|
|
4132
4279
|
normalizeSiliconFlowInfo,
|
|
4280
|
+
normalizeXiaomiTokenPlanUsage,
|
|
4133
4281
|
normalizeZaiCodingUsage,
|
|
4134
4282
|
parseQuotaConfigText,
|
|
4135
4283
|
parseSkillFrontmatterData,
|
|
@@ -4147,4 +4295,5 @@ export default {
|
|
|
4147
4295
|
setSkillInvocationKey,
|
|
4148
4296
|
skillIdFor,
|
|
4149
4297
|
unwrapCliproxyApiCallEnvelope,
|
|
4298
|
+
unwrapXiaomiConsoleEnvelope,
|
|
4150
4299
|
}
|