dsh-qoder-connect 0.1.9 → 0.2.1

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
@@ -64,6 +64,8 @@ Each variant card's *Context window* tab lists capacities per model under the *
64
64
 
65
65
  Both variant cards provide a *Model Toggles* tab (located between *Context window* and *Credit details*): search models on the fly, toggle individual models on or off, and use *Enable all* / *Disable all* batch buttons; hidden models will not appear in the session model picker.
66
66
 
67
+ ![Model toggles & batch controls](assets/8.png)
68
+
67
69
  ### Daily Auto Check-In & Check-In Log
68
70
 
69
71
  Enable daily automatic check-in in the settings card and set each variant's own moment (10:00 UTC+8 by default), then inspect check-in history logs, trigger a manual check-in, or clear logs in the *Check-in log* tab:
@@ -74,19 +76,30 @@ Enable daily automatic check-in in the settings card and set each variant's own
74
76
 
75
77
  Prerequisites:
76
78
 
77
- - DSH core `0.1.5-rc.1` or newer (this plugin peers on the `@deepseek-ai/*` `^0.1.5-rc.1` line);
79
+ - DSH core `0.1.7-rc.1` or newer (this plugin peers on the `@deepseek-ai/*` `>=0.1.7-rc.1` line);
78
80
  - Node.js `^22.19.0 || >=24.0.0` (per `package.json` engines);
79
81
  - your own Qoder account and at least one Personal Access Token.
80
82
 
81
83
  ```sh
82
84
  # From GitHub (recommended)
83
85
  dsh plugin --profile web add github:masknull/dsh-qoder-connect
84
- # Or from npm, once published
86
+ # Or from npm
85
87
  dsh plugin --profile web add dsh-qoder-connect
86
88
  ```
87
89
 
88
90
  Swap the `--profile` value for the profile you use (`web` / `desktop` / `tui`, e.g. `dsh plugin --profile desktop add dsh-qoder-connect`) — data stays inside that profile, so web / desktop / tui never collide. On a terminal-only profile (TUI) there is no browser card: manage the PAT and check status through the bundled CLI instead; the settings card and sidebar widgets require Web or Desktop.
89
91
 
92
+ > **DSH older than `0.1.7-rc.1`?** Use **[v0.2.0](https://github.com/masknull/dsh-qoder-connect/releases/tag/v0.2.0)** instead — the last release supporting the 0.1.5 line (DSH ≥ `0.1.5-rc.2`):
93
+ >
94
+ > ```sh
95
+ > # From GitHub (recommended, pin the tag)
96
+ > dsh plugin --profile web add github:masknull/dsh-qoder-connect#v0.2.0
97
+ > # Or from npm
98
+ > dsh plugin --profile web add dsh-qoder-connect@0.2.0
99
+ > ```
100
+ >
101
+ > Upgrading from 0.1.5? This release removed the automatic migration of the old host's `settings.yaml`. To carry your old configuration over, migrate through **[v0.1.9](https://github.com/masknull/dsh-qoder-connect/releases/tag/v0.1.9)** (the first release with the data migration) first, then upgrade to this release.
102
+
90
103
  ## Configuration
91
104
 
92
105
  ### Mint a PAT
@@ -98,7 +111,7 @@ Swap the `--profile` value for the profile you use (`web` / `desktop` / `tui`, e
98
111
 
99
112
  ### Save it on the card
100
113
 
101
- DSH → Settings → Plugins → the variant's card: paste the PAT into the password field and press *Save* (*“Validating and saving…”* while it runs). The token is validated against that variant's region first — a refusal saves nothing and the card says *“That PAT was rejected — generate a new one in your account settings and try again.”*. Do this on both cards to run both groups side by side.
114
+ DSH → Settings → Plugin Settings (插件设置) → the Qoder card: paste the PAT into the password field and press *Save* (*“Validating and saving…”* while it runs). The token is validated against that variant's region first — a refusal saves nothing and the card says *“That PAT was rejected — generate a new one in your account settings and try again.”*. Do this on both cards to run both groups side by side.
102
115
 
103
116
  ### Environment-variable fallback
104
117
 
package/README.md CHANGED
@@ -64,6 +64,8 @@
64
64
 
65
65
  两张变体卡片均提供「模型开关」页(位于「上下文窗口」与「额度明细」之间):支持即时搜索模型、单独开启/关闭指定模型,以及全选后一键「批量开启」或「批量关闭」;关闭的模型不会出现在模型选择器中。
66
66
 
67
+ ![模型开关与批量控制](assets/8.png)
68
+
67
69
  ### 每日自动签到与签到日志
68
70
 
69
71
  支持在设置卡片中开启每日自动签到,并自定义各版本的签到时刻(默认 10:00,按 UTC+8);在卡片底部的「签到日志」面板中查看签到流水、立即签到或清空日志:
@@ -74,19 +76,30 @@
74
76
 
75
77
  前置条件:
76
78
 
77
- - DSH 核心 `0.1.5-rc.1` 及以上(本插件 peer 依赖 `@deepseek-ai/*` `^0.1.5-rc.1` 线);
79
+ - DSH 核心 `0.1.7-rc.1` 及以上(本插件 peer 依赖 `@deepseek-ai/*` `>=0.1.7-rc.1` 线);
78
80
  - Node.js `^22.19.0 || >=24.0.0`(`package.json` engines);
79
81
  - 你自己的 Qoder 账号,以及至少一枚个人访问令牌。
80
82
 
81
83
  ```sh
82
84
  # 从 GitHub 安装(推荐)
83
85
  dsh plugin --profile web add github:masknull/dsh-qoder-connect
84
- # 或已发布 npm 后从 npm 安装
86
+ # 或从 npm 安装
85
87
  dsh plugin --profile web add dsh-qoder-connect
86
88
  ```
87
89
 
88
90
  按你使用的 profile 换 `--profile` 的值(`web` / `desktop` / `tui`,如 `dsh plugin --profile desktop add dsh-qoder-connect`)。装到哪个 profile,数据就落在哪个 profile 目录里,web / desktop / tui 各自独立。没有浏览器卡片的终端环境(TUI)下依旧可用 CLI 保存 PAT 并驱动模型;设置卡片与侧栏额度卡需要 Web/Desktop 界面。
89
91
 
92
+ > **DSH 低于 `0.1.7-rc.1`?** 请改用最后一个支持 0.1.5 线的版本 **[v0.2.0](https://github.com/masknull/dsh-qoder-connect/releases/tag/v0.2.0)**(要求 DSH ≥ `0.1.5-rc.2`):
93
+ >
94
+ > ```sh
95
+ > # GitHub(推荐,指定 tag)
96
+ > dsh plugin --profile web add github:masknull/dsh-qoder-connect#v0.2.0
97
+ > # 或从 npm 安装
98
+ > dsh plugin --profile web add dsh-qoder-connect@0.2.0
99
+ > ```
100
+ >
101
+ > 从 0.1.5 升级上来的老用户:本版本已移除旧宿主 `settings.yaml` 的自动迁移;如需保留旧配置,请先经 **[v0.1.9](https://github.com/masknull/dsh-qoder-connect/releases/tag/v0.1.9)**(首个带数据迁移的版本)完成迁移后,再升级本版本。
102
+
90
103
  ## 配置与使用
91
104
 
92
105
  ### 生成 PAT
@@ -98,7 +111,7 @@ dsh plugin --profile web add dsh-qoder-connect
98
111
 
99
112
  ### 在卡片上保存
100
113
 
101
- DSH → 设置 → 插件 → 对应卡片,展开后把 PAT 粘贴进密码输入框,点「保存」(期间显示「校验并保存中…」)。保存动作会先经 Qoder 校验令牌对该变体的区域有效,失败则不落盘并提示「PAT 无效或已过期 — 请到账号设置重新生成后再试。」两版各填各的卡片即可两组并存。
114
+ DSH → 设置 → 插件设置 → Qoder 卡片,展开后把 PAT 粘贴进密码输入框,点「保存」(期间显示「校验并保存中…」)。保存动作会先经 Qoder 校验令牌对该变体的区域有效,失败则不落盘并提示「PAT 无效或已过期 — 请到账号设置重新生成后再试。」两版各填各的卡片即可两组并存。
102
115
 
103
116
  ### 环境变量兜底
104
117
 
@@ -119,13 +132,13 @@ DSH → 设置 → 插件 → 对应卡片,展开后把 PAT 粘贴进密码输
119
132
 
120
133
  ```text
121
134
  <profile>/.dsh-qoder-connect/
122
- ├── .qoder-auth.json # 中国版 PAT({version:2, pat, region, savedAt})
135
+ ├── .qoder-auth.json # 国内版 PAT({version:2, pat, region, savedAt})
123
136
  ├── .qoder-global-auth.json # 国际版 PAT
124
137
  ├── checkin-status.json # 签到状态与历史日志记录
125
138
  └── state/
126
- ├── .qoder-catalog.json # 中国版按账号保存的模型目录
139
+ ├── .qoder-catalog.json # 国内版按账号保存的模型目录
127
140
  ├── .qoder-global-catalog.json
128
- ├── .qoder-probe.json # 中国版推理档位探测记录
141
+ ├── .qoder-probe.json # 国内版推理档位探测记录
129
142
  ├── .qoder-global-probe.json
130
143
  ├── .qoder-host-heartbeat.json # host 心跳(doctor 判断宿主是否在跑)
131
144
  └── .qoder-machine-id # 传输层机器标识种子(不落 ~/.qoder)
package/assets/3.png CHANGED
Binary file
package/assets/5.png CHANGED
Binary file
package/assets/6.png CHANGED
Binary file
package/assets/8.png ADDED
Binary file
@@ -0,0 +1,6 @@
1
+ <svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect x="1" y="1" width="34" height="34" rx="8" fill="url(#g)" />
3
+ <path d="M19.6 7.5 11 20h5.6l-1.4 8.5L24.5 16h-5.6l.7-8.5Z" fill="#FFFFFF"/>
4
+ <circle cx="25.5" cy="25.5" r="3.2" fill="#45D9E7"/>
5
+ <defs><linearGradient id="g" x1="18" y1="1" x2="18" y2="35" gradientUnits="userSpaceOnUse"><stop stop-color="#7CB7FF"/><stop offset="1" stop-color="#145AF3"/></linearGradient></defs>
6
+ </svg>
package/lib/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { D as FALLBACK_QODER_MODELS, E as qoderCatalogPath, T as QoderCatalogStore, V as qoderCredentialIdentity, b as createQoderTransport, f as QODER_CONNECT_VERSION, i as variantFor, l as qoderHostHeartbeatPath, m as QoderUpstreamClient, r as QODER_VARIANTS, s as isHeartbeatProcessAlive, t as CHINA_VARIANT, u as readHostHeartbeat, y as validateApiKey, z as QoderCredentialStore } from "./variants-C8ezztjV.js";
2
+ import { D as FALLBACK_QODER_MODELS, E as qoderCatalogPath, T as QoderCatalogStore, V as qoderCredentialIdentity, b as createQoderTransport, f as QODER_CONNECT_VERSION, i as variantFor, l as qoderHostHeartbeatPath, m as QoderUpstreamClient, r as QODER_VARIANTS, s as isHeartbeatProcessAlive, t as CHINA_VARIANT, u as readHostHeartbeat, y as validateApiKey, z as QoderCredentialStore } from "./variants-CjWW4lVI.js";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { realpathSync } from "node:fs";
5
5
  import { fileURLToPath } from "node:url";
package/lib/client.js CHANGED
@@ -4047,13 +4047,6 @@ window.__ModuleLoader__.load({
4047
4047
  * (`settings.plugin.item`) is declared by
4048
4048
  * `@deepseek-ai/dsh-client-ui-settings-plugins`. Those packages are named in
4049
4049
  * the package's `dsh.client.inject` list.
4050
- *
4051
- * NOT declared here: the settings services. `settingsScope` (0.1.5) does not
4052
- * exist at all on 0.1.7 — a static service dependency on it is what left this
4053
- * plugin's client activation pending forever — and `configForms` (0.1.7) does
4054
- * not exist on 0.1.5. Both are waited for with `ctx.inject([...], callback)`
4055
- * below, where a missing service simply never calls back instead of holding
4056
- * activation.
4057
4050
  */
4058
4051
  const inject = [
4059
4052
  "slots",
@@ -4164,21 +4157,15 @@ window.__ModuleLoader__.load({
4164
4157
  applySnapshot();
4165
4158
  scope.subscribe(applySnapshot);
4166
4159
  };
4167
- const registerUnifiedCard = (slot) => {
4160
+ const registerUnifiedCard = () => {
4168
4161
  const inject = () => ({
4169
4162
  t,
4170
4163
  scope: quotaScope,
4171
4164
  signedIn: () => quotaSignInState(),
4172
4165
  unified: true
4173
4166
  });
4174
- if (slot === "settings.plugin.item") return ctx.slots.inject(slot, () => ctx.slots.register({
4175
- name: slot,
4176
- key: "qoder",
4177
- priority: 50,
4178
- inject
4179
- }, QoderPluginCard));
4180
- return ctx.slots.inject(slot, () => ctx.slots.register({
4181
- name: slot,
4167
+ return ctx.slots.inject(SHARED_ITEM_SLOT, () => ctx.slots.register({
4168
+ name: SHARED_ITEM_SLOT,
4182
4169
  id: SHARED_ITEM_ID,
4183
4170
  order: 30,
4184
4171
  inject
@@ -4214,7 +4201,7 @@ window.__ModuleLoader__.load({
4214
4201
  } catch (error) {
4215
4202
  console.error("[dsh-qoder-connect] shared plugin-settings block lost the race; attaching to the winner:", error);
4216
4203
  }
4217
- const disposeItem = registerUnifiedCard(SHARED_ITEM_SLOT);
4204
+ const disposeItem = registerUnifiedCard();
4218
4205
  return () => {
4219
4206
  disposeItem();
4220
4207
  disposeContainer?.();
package/lib/index.d.ts CHANGED
@@ -2,10 +2,10 @@ import z from "@deepseek-ai/schemastery";
2
2
  import "@earendil-works/pi-ai";
3
3
  import { GenerateOptions, StreamChunk } from "@deepseek-ai/dsh-llm";
4
4
  import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
5
+ import { AttachmentStore } from "@deepseek-ai/dsh-attachment";
5
6
  import { IncomingMessage, ServerResponse } from "node:http";
6
7
  import { Context } from "@deepseek-ai/cordis";
7
8
  import { SettingsNamespace } from "@deepseek-ai/dsh-settings";
8
- import { AttachmentStore } from "@deepseek-ai/dsh-attachment";
9
9
  //#region src/paths.d.ts
10
10
  /**
11
11
  * The plugin's data directory — the ONE place every file the plugin owns
@@ -834,6 +834,12 @@ declare function classifyUpstreamError(status: number, body: string): UpstreamEr
834
834
  * The transport's taxonomy (`src/qoder/errors.ts`) codes an HTTP 402 as
835
835
  * `INVALID_REQUEST`, so the status gets the last say for the quota and auth
836
836
  * families before the code-based defaults apply.
837
+ *
838
+ * `RATE_LIMIT` is tested before the auth family because the upstream's queue
839
+ * answer keeps its 401/403 status: a saturated qfmodel queue reads
840
+ * `code: RATE_LIMIT, status: 403` (body carries 10605/isQueued/
841
+ * retryAfterSeconds), and letting the status arm claim it reported a dead
842
+ * credential — which the host never retries — instead of a throttle it would.
837
843
  */
838
844
  declare function kindFromQoderFailure(failure: {
839
845
  code: string;
@@ -1442,16 +1448,14 @@ declare const QUOTA_POLL_DEFAULT_MS = 300000;
1442
1448
  declare const QUOTA_POLL_MIN_MS = 60000;
1443
1449
  declare const Config: z<Config>;
1444
1450
  /**
1445
- * Every field each settings section owns, and therefore every field the live
1446
- * configuration has to carry through.
1451
+ * The field groups the configuration is merged from, one list per card.
1447
1452
  *
1448
- * One list per section, shared by the merge and by the test that pins it to
1449
- * the schema. Writing the merge out by hand is what broke auto check-in: the
1450
- * `qoder-quota` section grew four fields (`autoCheckInCN`, `autoCheckInGlobal`,
1451
- * `checkInMinuteCN`, `checkInMinuteGlobal`) while the merge kept copying only
1452
- * the three that predated them, so `current().autoCheckInCN` read `undefined`
1453
- * forever and the scheduler saw the toggle as permanently off. The card saved
1454
- * it, the file held it, and nothing ever acted on it.
1453
+ * Writing the merge out by hand is what broke auto check-in: the quota group
1454
+ * grew four fields (`autoCheckInCN`, `autoCheckInGlobal`, `checkInMinuteCN`,
1455
+ * `checkInMinuteGlobal`) while the merge kept copying only the three that
1456
+ * predated them, so `current().autoCheckInCN` read `undefined` forever and the
1457
+ * scheduler saw the toggle as permanently off. The card saved it, the file
1458
+ * held it, and nothing ever acted on it.
1455
1459
  */
1456
1460
  declare const CN_SECTION_KEYS: readonly ["probeConsent", "useMaximumContextWindowCN", "modelContextWindowsCN", "disabledModelsCN"];
1457
1461
  declare const GLOBAL_SECTION_KEYS: readonly ["useMaximumContextWindow", "modelContextWindows", "disabledModels"];
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as QODER_GLOBAL_AUTH_PATH, B as patTail, C as probeModel, D as FALLBACK_QODER_MODELS, E as qoderCatalogPath, F as QODER_STATUS_PATH, G as qoderMachineIdPath, H as qoderOwnAuthPath, I as QODER_AUTH_FILENAME, K as qoderPluginDataDir, L as QODER_PAT_ENV_CN, M as QODER_GLOBAL_STATUS_PATH, N as QODER_PROBE_PATH, O as QoderCatalog, P as QODER_SETTINGS_FACE_PATH, R as QODER_PAT_ENV_GLOBAL, S as PROBE_EFFORT_CANDIDATES, T as QoderCatalogStore, U as QODER_DATA_DIR_ENV, V as qoderCredentialIdentity, W as QODER_DATA_DIR_NAME, _ as modelInfoOf, a as QODER_HOST_HEARTBEAT_FILENAME, b as createQoderTransport, c as processStartTimeMs, d as writeHostHeartbeat, f as QODER_CONNECT_VERSION, g as kindFromQoderFailure, h as classifyUpstreamError, i as variantFor, j as QODER_GLOBAL_PROBE_PATH, k as QODER_AUTH_PATH, l as qoderHostHeartbeatPath, m as QoderUpstreamClient, n as GLOBAL_VARIANT, o as clearHostHeartbeat, p as KIND_STATUS, q as qoderStateDir, r as QODER_VARIANTS, s as isHeartbeatProcessAlive, t as CHINA_VARIANT, u as readHostHeartbeat, v as normalizeCredits, w as randomSentinel, x as getMachineId, y as validateApiKey, z as QoderCredentialStore } from "./variants-C8ezztjV.js";
1
+ import { A as QODER_GLOBAL_AUTH_PATH, B as patTail, C as probeModel, D as FALLBACK_QODER_MODELS, E as qoderCatalogPath, F as QODER_STATUS_PATH, G as qoderMachineIdPath, H as qoderOwnAuthPath, I as QODER_AUTH_FILENAME, K as qoderPluginDataDir, L as QODER_PAT_ENV_CN, M as QODER_GLOBAL_STATUS_PATH, N as QODER_PROBE_PATH, O as QoderCatalog, P as QODER_SETTINGS_FACE_PATH, R as QODER_PAT_ENV_GLOBAL, S as PROBE_EFFORT_CANDIDATES, T as QoderCatalogStore, U as QODER_DATA_DIR_ENV, V as qoderCredentialIdentity, W as QODER_DATA_DIR_NAME, _ as modelInfoOf, a as QODER_HOST_HEARTBEAT_FILENAME, b as createQoderTransport, c as processStartTimeMs, d as writeHostHeartbeat, f as QODER_CONNECT_VERSION, g as kindFromQoderFailure, h as classifyUpstreamError, i as variantFor, j as QODER_GLOBAL_PROBE_PATH, k as QODER_AUTH_PATH, l as qoderHostHeartbeatPath, m as QoderUpstreamClient, n as GLOBAL_VARIANT, o as clearHostHeartbeat, p as KIND_STATUS, q as qoderStateDir, r as QODER_VARIANTS, s as isHeartbeatProcessAlive, t as CHINA_VARIANT, u as readHostHeartbeat, v as normalizeCredits, w as randomSentinel, x as getMachineId, y as validateApiKey, z as QoderCredentialStore } from "./variants-CjWW4lVI.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
4
4
  import { dirname, join, resolve } from "node:path";
@@ -7,10 +7,8 @@ import { createProvider } from "@earendil-works/pi-ai";
7
7
  import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
8
8
  import { resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
9
9
  import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
10
- import { homedir } from "node:os";
11
10
  import { createServer } from "node:http";
12
11
  import { Readable } from "node:stream";
13
- import yamlModule from "js-yaml";
14
12
  //#region src/loopback.ts
15
13
  /**
16
14
  * Shared loopback gates for the plugin's local HTTP surfaces: the loopback
@@ -628,7 +626,7 @@ function createQoderShim(options) {
628
626
  req.on("close", () => controller.abort());
629
627
  const result = await client.chatStream(raw, controller.signal);
630
628
  if (!result.ok) {
631
- writeOpenAIError(res, KIND_STATUS[result.kind], result.kind, `${providerId} upstream ${result.kind} (http ${result.status}): ${result.message.slice(0, 400)}`);
629
+ writeOpenAIError(res, KIND_STATUS[result.kind], result.kind, `${providerId} upstream ${result.kind} (http ${KIND_STATUS[result.kind]}): ${result.message.slice(0, 400)}`);
632
630
  return;
633
631
  }
634
632
  res.writeHead(200, {
@@ -1660,8 +1658,6 @@ function appendHintRow(agent, kind, text) {
1660
1658
  *
1661
1659
  * @module dsh-qoder-connect/settings-store
1662
1660
  */
1663
- const yaml = yamlModule;
1664
- const loadYaml = typeof yaml.load === "function" ? yaml.load : yaml.default?.load;
1665
1661
  /** Settings file name inside the plugin data directory. */
1666
1662
  const SETTINGS_FILE_NAME = "settings.json";
1667
1663
  /** This plugin's data directory (shared with the credential and caches). */
@@ -1691,61 +1687,11 @@ function writeSettings(values) {
1691
1687
  writeFileSync(tmp, `${JSON.stringify(values, null, 2)}\n`, "utf8");
1692
1688
  renameSync(tmp, path);
1693
1689
  }
1694
- /** Resolve `$DSH_HOME` the way the Host does, without importing a Host package. */
1695
- function dshHome() {
1696
- const fromEnv = process.env.DSH_HOME?.trim() || void 0;
1697
- if (fromEnv !== void 0) return fromEnv;
1698
- return join(homedir(), ".dsh");
1699
- }
1700
- /**
1701
- * Read this plugin's sections out of the legacy settings document.
1702
- *
1703
- * The 0.1.5 host kept every plugin's settings in `$DSH_HOME/settings.yaml`;
1704
- * 0.1.7 renames it to `settings.yaml.imported` once, on the first boot, and
1705
- * imports only the sections whose names match a profile entry id. This
1706
- * plugin's 0.1.5 sections were keyed by its SERVED NAMESPACES (`qoder`,
1707
- * `qoder-global`, `qoder-quota`), which name no entry in the profile — the row
1708
- * is `llm-qoder` — so the platform refuses them and their data survives only
1709
- * in the renamed document. Reading it here recovers that data independently of
1710
- * the platform's import, its section-name mapping, and its timing.
1711
- *
1712
- * @param namespaces - the section names the 0.1.5 host keyed this plugin's
1713
- * settings under (one per served section).
1714
- * @returns the union of those sections' fields, or undefined when the document
1715
- * is absent, unreadable, or holds none of them.
1716
- */
1717
- function readLegacySections(namespaces) {
1718
- let path;
1719
- for (const name of ["settings.yaml", "settings.yaml.imported"]) {
1720
- const candidate = join(dshHome(), name);
1721
- if (existsSync(candidate)) {
1722
- path = candidate;
1723
- break;
1724
- }
1725
- }
1726
- if (path === void 0 || loadYaml === void 0) return void 0;
1727
- let document;
1728
- try {
1729
- document = loadYaml(readFileSync(path, "utf8"));
1730
- } catch {
1731
- return;
1732
- }
1733
- if (document === null || typeof document !== "object") return void 0;
1734
- const merged = {};
1735
- let found = false;
1736
- for (const ns of namespaces) {
1737
- const section = document[ns];
1738
- if (section === null || typeof section !== "object" || Array.isArray(section)) continue;
1739
- Object.assign(merged, section);
1740
- found = true;
1741
- }
1742
- return found ? merged : void 0;
1743
- }
1744
1690
  /**
1745
1691
  * Reserved bookkeeping key: how many writes the settings card has made through
1746
- * this store. Its presence separates "the file holds migration seed" from "the
1747
- * file holds the user's live edits" — see the seed rule in ./index.ts. Field
1748
- * readers address their fields by name and never collide with it.
1692
+ * this store. Its presence separates "the file holds the startup seed" from
1693
+ * "the file holds the user's live edits" — see the seed rule in ./index.ts.
1694
+ * Field readers address their fields by name and never collide with it.
1749
1695
  */
1750
1696
  const WRITE_MARK = "__writes";
1751
1697
  /**
@@ -1765,12 +1711,10 @@ var SettingsStore = class {
1765
1711
  /**
1766
1712
  * Whether the settings card has ever written through this store.
1767
1713
  *
1768
- * False during the migration window — the window where a 0.1.5 settings.yaml
1769
- * import (which lands only after the Loader settles, i.e. AFTER this plugin's
1770
- * first apply) can still deliver the user's real values, and where a first
1771
- * seed may have been taken before that import. While false the entry config
1772
- * stays authoritative; once the card writes, the file is — a runtime edit
1773
- * must never be regressed by a stale profile row.
1714
+ * Until it has, the entry config stays the fallback layer (a hand-edited
1715
+ * profile row can still seed a fresh install); once the card writes, the
1716
+ * file is authoritative — a runtime edit must never be regressed by a stale
1717
+ * profile row.
1774
1718
  */
1775
1719
  get edited() {
1776
1720
  return typeof this.user[WRITE_MARK] === "number" && this.user[WRITE_MARK] > 0;
@@ -1996,37 +1940,15 @@ const Config = z.object({
1996
1940
  checkInMinuteGlobal: CHECK_IN_MINUTE_FIELD,
1997
1941
  quotaPollMs: QUOTA_POLL_FIELD
1998
1942
  });
1999
- z.object({
2000
- probeConsent: PROBE_CONSENT_FIELD,
2001
- useMaximumContextWindowCN: MAXIMUM_CONTEXT_WINDOW_CN_FIELD,
2002
- modelContextWindowsCN: MODEL_CONTEXT_WINDOWS_FIELD,
2003
- disabledModelsCN: DISABLED_MODELS_FIELD
2004
- });
2005
- z.object({
2006
- useMaximumContextWindow: MAXIMUM_CONTEXT_WINDOW_FIELD,
2007
- modelContextWindows: MODEL_CONTEXT_WINDOWS_FIELD,
2008
- disabledModels: DISABLED_MODELS_FIELD
2009
- });
2010
- z.object({
2011
- sidebarQuotaCN: QUOTA_TOGGLE_FIELD,
2012
- sidebarQuotaGlobal: QUOTA_TOGGLE_FIELD,
2013
- autoCheckInCN: AUTO_CHECK_IN_FIELD,
2014
- autoCheckInGlobal: AUTO_CHECK_IN_FIELD,
2015
- checkInMinuteCN: CHECK_IN_MINUTE_FIELD,
2016
- checkInMinuteGlobal: CHECK_IN_MINUTE_FIELD,
2017
- quotaPollMs: QUOTA_POLL_FIELD
2018
- });
2019
1943
  /**
2020
- * Every field each settings section owns, and therefore every field the live
2021
- * configuration has to carry through.
2022
- *
2023
- * One list per section, shared by the merge and by the test that pins it to
2024
- * the schema. Writing the merge out by hand is what broke auto check-in: the
2025
- * `qoder-quota` section grew four fields (`autoCheckInCN`, `autoCheckInGlobal`,
2026
- * `checkInMinuteCN`, `checkInMinuteGlobal`) while the merge kept copying only
2027
- * the three that predated them, so `current().autoCheckInCN` read `undefined`
2028
- * forever and the scheduler saw the toggle as permanently off. The card saved
2029
- * it, the file held it, and nothing ever acted on it.
1944
+ * The field groups the configuration is merged from, one list per card.
1945
+ *
1946
+ * Writing the merge out by hand is what broke auto check-in: the quota group
1947
+ * grew four fields (`autoCheckInCN`, `autoCheckInGlobal`, `checkInMinuteCN`,
1948
+ * `checkInMinuteGlobal`) while the merge kept copying only the three that
1949
+ * predated them, so `current().autoCheckInCN` read `undefined` forever and the
1950
+ * scheduler saw the toggle as permanently off. The card saved it, the file
1951
+ * held it, and nothing ever acted on it.
2030
1952
  */
2031
1953
  const CN_SECTION_KEYS = [
2032
1954
  "probeConsent",
@@ -2081,10 +2003,6 @@ function readConfig(config) {
2081
2003
  }
2082
2004
  /** Stable identity key used by credentials, probe records, and catalog entries. */
2083
2005
  const credentialIdentity = qoderCredentialIdentity;
2084
- /** The settings namespace a variant's card and provider directory entry use. */
2085
- function settingsNamespaceFor(variant) {
2086
- return variant.id === CHINA_VARIANT.id ? QODER_SETTINGS_NS : QODER_GLOBAL_SETTINGS_NS;
2087
- }
2088
2006
  /**
2089
2007
  * The static catalog a variant serves before its first successful fetch.
2090
2008
  *
@@ -2113,9 +2031,16 @@ function jobTokenHintText(at) {
2113
2031
  * fresh token is not necessarily an authorization problem at all, and the real
2114
2032
  * reason travels in the failure message itself (see the SSE envelope body).
2115
2033
  * Claiming "quota" or "revoked" here would be a guess presented as a finding.
2034
+ *
2035
+ * It also must not point the reader at "the error above": the failed heal does
2036
+ * not always belong to a chat whose failure renders a card in this
2037
+ * conversation. A heal inside the session-title request — same transport,
2038
+ * same shared notice state — fails silently there, so the row used to send
2039
+ * users looking for an error card that does not exist. The row states only
2040
+ * what is known: the retry ran, it did not recover, the request failed.
2116
2041
  */
2117
2042
  function jobTokenRefreshFailedHintText(at) {
2118
- return `jobToken 已重换但仍被上游拒绝(${new Date(at).toLocaleTimeString("zh-CN", { hour12: false })})— 自愈未恢复,请查看上方错误详情`;
2043
+ return `jobToken 已重换但仍被上游拒绝(${new Date(at).toLocaleTimeString("zh-CN", { hour12: false })})— 自愈未恢复,该请求已失败`;
2119
2044
  }
2120
2045
  /** Build one variant's stores, transport, and probe state. */
2121
2046
  function createVariantRuntime(ctx, config, variant, current, identityOf) {
@@ -2307,15 +2232,7 @@ async function startVariant(ctx, runtime, seedCatalog) {
2307
2232
  let releaseDirectory;
2308
2233
  try {
2309
2234
  releaseAdapter = ctx.llm.registerAdapter([variant.id], qoder.adapter);
2310
- const host017 = (() => {
2311
- try {
2312
- return ctx.get?.("configEditor") !== void 0;
2313
- } catch {
2314
- return false;
2315
- }
2316
- })();
2317
- const entryId = ctx.fiber?.entry?.options?.id;
2318
- const settingsNs = host017 && entryId !== void 0 ? entryId : settingsNamespaceFor(variant);
2235
+ const settingsNs = ctx.fiber?.entry?.options?.id ?? variant.id;
2319
2236
  releaseDirectory = ctx.llm.registerConfigurableProviders([{
2320
2237
  provider: variant.id,
2321
2238
  displayName: variant.displayName,
@@ -2551,43 +2468,22 @@ function validateSettingsPatch(patch) {
2551
2468
  }
2552
2469
  }
2553
2470
  /**
2554
- * When the profile tree last recomposed, module-level so every apply sees it.
2555
- *
2556
- * The legacy settings.yaml import writes the profile patch one section at a
2557
- * time for seconds after the Loader settles, emitting this event on every
2558
- * write. Cleaning up our row in that window is futile — the import's next
2559
- * section simply writes it back — so the cleanup waits for the tree to fall
2560
- * quiet first.
2561
- */
2562
- let lastConfigReloadAt = Date.now();
2563
- /** Wait until the profile tree has been quiet (the legacy import finished). */
2564
- async function waitForProfileQuiet() {
2565
- const deadline = Date.now() + 12e4;
2566
- for (;;) {
2567
- if (Date.now() - lastConfigReloadAt >= 5e3) return;
2568
- if (Date.now() >= deadline) return;
2569
- await new Promise((resolve) => setTimeout(resolve, 1e3));
2570
- }
2571
- }
2572
- /**
2573
2471
  * Delete this plugin's own fields from the profile entry config, leaving every
2574
2472
  * other key of the row untouched.
2575
2473
  *
2576
2474
  * One-time, right after the settings file has been seeded: the entry returns to
2577
- * its shipped state, so no second source of truth remains. Both write APIs are
2578
- * probed — `configEditor` (0.1.7) first, then the settings service's namespace
2579
- * `replace` (0.1.5), which rebuilds each of this plugin's three sections from
2580
- * the foreign keys alone.
2475
+ * its shipped state, so no second source of truth remains. The write rides
2476
+ * `configEditor` — the 0.1.7 profile-patch editor — probed on the plugin
2477
+ * context first and on the settings service's owner context second, because
2478
+ * that is where the host root mounts it.
2581
2479
  *
2582
2480
  * @param ctx - plugin context.
2583
2481
  * @param ownKeys - this plugin's declared config fields.
2584
- * @param namespaces - the 0.1.5 section namespaces this plugin owns.
2585
2482
  */
2586
- async function cleanupEntryConfig(ctx, ownKeys, namespaces) {
2483
+ async function cleanupEntryConfig(ctx, ownKeys) {
2587
2484
  const attempts = 10;
2588
2485
  for (let attempt = 0; attempt < attempts; attempt++) try {
2589
- if (attempt === 0) await waitForProfileQuiet();
2590
- else await new Promise((resolve) => setTimeout(resolve, 2e3 + Math.random() * 1e3));
2486
+ if (attempt > 0) await new Promise((resolve) => setTimeout(resolve, 2e3 + Math.random() * 1e3));
2591
2487
  const probe = ctx;
2592
2488
  const editor = (typeof probe.get === "function" ? (() => {
2593
2489
  try {
@@ -2619,16 +2515,6 @@ async function cleanupEntryConfig(ctx, ownKeys, namespaces) {
2619
2515
  });
2620
2516
  return;
2621
2517
  }
2622
- await new Promise((resolve) => {
2623
- ctx.inject(["settings"], (settingsCtx) => {
2624
- const settings = settingsCtx.settings;
2625
- if (typeof settings?.replace !== "function" || typeof settings.installSection !== "function") {
2626
- resolve(void 0);
2627
- return;
2628
- }
2629
- Promise.all(namespaces.map((ns) => Promise.resolve(settings.replace(ns, {})).catch(() => void 0))).then(() => resolve(void 0));
2630
- });
2631
- });
2632
2518
  return;
2633
2519
  } catch (error) {
2634
2520
  if (attempt === 9) {
@@ -2665,63 +2551,41 @@ function apply(ctx, config) {
2665
2551
  ...store.values()
2666
2552
  });
2667
2553
  /**
2668
- * One-time migration: per field — the file never held it → take the entry;
2669
- * the card HAS written this file → keep the file; the entry carries a
2670
- * NON-DEFAULT value → take the entry (the legacy settings.yaml import lands
2671
- * only after the Loader settles, i.e. after this plugin's first apply, and a
2672
- * seed taken inside that window can hold a stale or mis-encoded value); the
2673
- * entry only carries the schema default → keep the file.
2554
+ * One-time seed: per field — the file never held it → take the entry; the
2555
+ * card HAS written this file → keep the file; the entry carries a
2556
+ * NON-DEFAULT value → take the entry; the entry only carries the schema
2557
+ * default → keep the file.
2674
2558
  *
2675
2559
  * That last clause is load-bearing: a bundle layer's insert config does NOT
2676
2560
  * reach the composition (verified with `dsh --dump-config`), so once the
2677
2561
  * one-time cleanup has emptied the entry row the entry answers pure defaults —
2678
2562
  * treating those as authoritative would erase the user's values on the next
2679
- * boot. Re-checked on every apply, which is also what closes the legacy
2680
- * import's timing window.
2563
+ * boot. Re-checked on every apply, which is also what closes the volatile
2564
+ * commit timing window.
2681
2565
  *
2682
2566
  * When the entry is authoritative, its own fields are then deleted from the
2683
2567
  * profile row, so no second source of truth remains.
2684
2568
  */
2685
2569
  const migrateOwnSettings = () => {
2686
- const legacy = readLegacySections([
2687
- QODER_SETTINGS_NS,
2688
- QODER_GLOBAL_SETTINGS_NS,
2689
- QODER_QUOTA_SETTINGS_NS
2690
- ]);
2691
2570
  const seeded = {};
2692
2571
  for (const key of CONFIG_KEYS) {
2693
2572
  const entryValue = readField(config, key);
2694
- const legacyValue = legacy === void 0 ? void 0 : legacy[key];
2695
2573
  if (!Object.hasOwn(store.user, key)) {
2696
2574
  if (entryValue !== void 0 && JSON.stringify(entryValue) !== JSON.stringify(DEFAULT_FOR_FIELD[key])) seeded[key] = entryValue;
2697
- else if (legacyValue !== void 0 && JSON.stringify(legacyValue) !== JSON.stringify(DEFAULT_FOR_FIELD[key])) seeded[key] = legacyValue;
2698
2575
  else if (entryValue !== void 0) seeded[key] = entryValue;
2699
- else if (legacyValue !== void 0) seeded[key] = legacyValue;
2700
2576
  continue;
2701
2577
  }
2702
2578
  if (store.edited) continue;
2703
- if (entryValue !== void 0 && JSON.stringify(entryValue) !== JSON.stringify(DEFAULT_FOR_FIELD[key])) {
2704
- seeded[key] = entryValue;
2705
- continue;
2706
- }
2707
- if (legacyValue !== void 0 && JSON.stringify(legacyValue) !== JSON.stringify(DEFAULT_FOR_FIELD[key])) seeded[key] = legacyValue;
2579
+ if (entryValue !== void 0 && JSON.stringify(entryValue) !== JSON.stringify(DEFAULT_FOR_FIELD[key])) seeded[key] = entryValue;
2708
2580
  }
2709
2581
  if (Object.keys(seeded).length === 0) return;
2710
2582
  store.patch(seeded);
2711
- cleanupEntryConfig(ctx, CONFIG_KEYS, [
2712
- QODER_SETTINGS_NS,
2713
- QODER_GLOBAL_SETTINGS_NS,
2714
- QODER_QUOTA_SETTINGS_NS
2715
- ]);
2583
+ cleanupEntryConfig(ctx, CONFIG_KEYS);
2716
2584
  };
2717
2585
  migrateOwnSettings();
2718
- const eventSink = ctx;
2719
- eventSink.on("loader/volatile-update", () => {
2586
+ ctx.on("loader/volatile-update", () => {
2720
2587
  migrateOwnSettings();
2721
2588
  });
2722
- eventSink.on("app-boot/config-reload", () => {
2723
- lastConfigReloadAt = Date.now();
2724
- });
2725
2589
  /** Timers and in-flight work belonging to this plugin instance. */
2726
2590
  let stopped = false;
2727
2591
  const timers = [];