dsh-qoder-connect 0.2.0 → 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 +16 -3
- package/README.md +19 -6
- package/assets/3.png +0 -0
- package/assets/5.png +0 -0
- package/assets/6.png +0 -0
- package/assets/8.png +0 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +4 -17
- package/lib/index.d.ts +8 -10
- package/lib/index.js +32 -175
- package/lib/{variants-DuEgaUad.js → variants-CjWW4lVI.js} +93 -36
- package/package.json +25 -28
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
|
+

|
|
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.
|
|
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
|
|
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 →
|
|
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
|
+

|
|
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.
|
|
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
|
-
#
|
|
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 → 设置 →
|
|
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 #
|
|
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
|
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-
|
|
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 = (
|
|
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
|
-
|
|
4175
|
-
name:
|
|
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(
|
|
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
|
|
@@ -1448,16 +1448,14 @@ declare const QUOTA_POLL_DEFAULT_MS = 300000;
|
|
|
1448
1448
|
declare const QUOTA_POLL_MIN_MS = 60000;
|
|
1449
1449
|
declare const Config: z<Config>;
|
|
1450
1450
|
/**
|
|
1451
|
-
*
|
|
1452
|
-
* configuration has to carry through.
|
|
1451
|
+
* The field groups the configuration is merged from, one list per card.
|
|
1453
1452
|
*
|
|
1454
|
-
*
|
|
1455
|
-
*
|
|
1456
|
-
* `
|
|
1457
|
-
*
|
|
1458
|
-
*
|
|
1459
|
-
*
|
|
1460
|
-
* 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.
|
|
1461
1459
|
*/
|
|
1462
1460
|
declare const CN_SECTION_KEYS: readonly ["probeConsent", "useMaximumContextWindowCN", "modelContextWindowsCN", "disabledModelsCN"];
|
|
1463
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-
|
|
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
|
|
@@ -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
|
|
1747
|
-
* file holds the user's live edits" — see the seed rule in ./index.ts.
|
|
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
|
-
*
|
|
1769
|
-
*
|
|
1770
|
-
*
|
|
1771
|
-
*
|
|
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
|
-
*
|
|
2021
|
-
*
|
|
2022
|
-
*
|
|
2023
|
-
*
|
|
2024
|
-
*
|
|
2025
|
-
* `
|
|
2026
|
-
*
|
|
2027
|
-
*
|
|
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
|
*
|
|
@@ -2314,15 +2232,7 @@ async function startVariant(ctx, runtime, seedCatalog) {
|
|
|
2314
2232
|
let releaseDirectory;
|
|
2315
2233
|
try {
|
|
2316
2234
|
releaseAdapter = ctx.llm.registerAdapter([variant.id], qoder.adapter);
|
|
2317
|
-
const
|
|
2318
|
-
try {
|
|
2319
|
-
return ctx.get?.("configEditor") !== void 0;
|
|
2320
|
-
} catch {
|
|
2321
|
-
return false;
|
|
2322
|
-
}
|
|
2323
|
-
})();
|
|
2324
|
-
const entryId = ctx.fiber?.entry?.options?.id;
|
|
2325
|
-
const settingsNs = host017 && entryId !== void 0 ? entryId : settingsNamespaceFor(variant);
|
|
2235
|
+
const settingsNs = ctx.fiber?.entry?.options?.id ?? variant.id;
|
|
2326
2236
|
releaseDirectory = ctx.llm.registerConfigurableProviders([{
|
|
2327
2237
|
provider: variant.id,
|
|
2328
2238
|
displayName: variant.displayName,
|
|
@@ -2558,43 +2468,22 @@ function validateSettingsPatch(patch) {
|
|
|
2558
2468
|
}
|
|
2559
2469
|
}
|
|
2560
2470
|
/**
|
|
2561
|
-
* When the profile tree last recomposed, module-level so every apply sees it.
|
|
2562
|
-
*
|
|
2563
|
-
* The legacy settings.yaml import writes the profile patch one section at a
|
|
2564
|
-
* time for seconds after the Loader settles, emitting this event on every
|
|
2565
|
-
* write. Cleaning up our row in that window is futile — the import's next
|
|
2566
|
-
* section simply writes it back — so the cleanup waits for the tree to fall
|
|
2567
|
-
* quiet first.
|
|
2568
|
-
*/
|
|
2569
|
-
let lastConfigReloadAt = Date.now();
|
|
2570
|
-
/** Wait until the profile tree has been quiet (the legacy import finished). */
|
|
2571
|
-
async function waitForProfileQuiet() {
|
|
2572
|
-
const deadline = Date.now() + 12e4;
|
|
2573
|
-
for (;;) {
|
|
2574
|
-
if (Date.now() - lastConfigReloadAt >= 5e3) return;
|
|
2575
|
-
if (Date.now() >= deadline) return;
|
|
2576
|
-
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
2577
|
-
}
|
|
2578
|
-
}
|
|
2579
|
-
/**
|
|
2580
2471
|
* Delete this plugin's own fields from the profile entry config, leaving every
|
|
2581
2472
|
* other key of the row untouched.
|
|
2582
2473
|
*
|
|
2583
2474
|
* One-time, right after the settings file has been seeded: the entry returns to
|
|
2584
|
-
* its shipped state, so no second source of truth remains.
|
|
2585
|
-
*
|
|
2586
|
-
*
|
|
2587
|
-
* the
|
|
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.
|
|
2588
2479
|
*
|
|
2589
2480
|
* @param ctx - plugin context.
|
|
2590
2481
|
* @param ownKeys - this plugin's declared config fields.
|
|
2591
|
-
* @param namespaces - the 0.1.5 section namespaces this plugin owns.
|
|
2592
2482
|
*/
|
|
2593
|
-
async function cleanupEntryConfig(ctx, ownKeys
|
|
2483
|
+
async function cleanupEntryConfig(ctx, ownKeys) {
|
|
2594
2484
|
const attempts = 10;
|
|
2595
2485
|
for (let attempt = 0; attempt < attempts; attempt++) try {
|
|
2596
|
-
if (attempt
|
|
2597
|
-
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));
|
|
2598
2487
|
const probe = ctx;
|
|
2599
2488
|
const editor = (typeof probe.get === "function" ? (() => {
|
|
2600
2489
|
try {
|
|
@@ -2626,16 +2515,6 @@ async function cleanupEntryConfig(ctx, ownKeys, namespaces) {
|
|
|
2626
2515
|
});
|
|
2627
2516
|
return;
|
|
2628
2517
|
}
|
|
2629
|
-
await new Promise((resolve) => {
|
|
2630
|
-
ctx.inject(["settings"], (settingsCtx) => {
|
|
2631
|
-
const settings = settingsCtx.settings;
|
|
2632
|
-
if (typeof settings?.replace !== "function" || typeof settings.installSection !== "function") {
|
|
2633
|
-
resolve(void 0);
|
|
2634
|
-
return;
|
|
2635
|
-
}
|
|
2636
|
-
Promise.all(namespaces.map((ns) => Promise.resolve(settings.replace(ns, {})).catch(() => void 0))).then(() => resolve(void 0));
|
|
2637
|
-
});
|
|
2638
|
-
});
|
|
2639
2518
|
return;
|
|
2640
2519
|
} catch (error) {
|
|
2641
2520
|
if (attempt === 9) {
|
|
@@ -2672,63 +2551,41 @@ function apply(ctx, config) {
|
|
|
2672
2551
|
...store.values()
|
|
2673
2552
|
});
|
|
2674
2553
|
/**
|
|
2675
|
-
* One-time
|
|
2676
|
-
*
|
|
2677
|
-
* NON-DEFAULT value → take the entry
|
|
2678
|
-
*
|
|
2679
|
-
* seed taken inside that window can hold a stale or mis-encoded value); the
|
|
2680
|
-
* 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.
|
|
2681
2558
|
*
|
|
2682
2559
|
* That last clause is load-bearing: a bundle layer's insert config does NOT
|
|
2683
2560
|
* reach the composition (verified with `dsh --dump-config`), so once the
|
|
2684
2561
|
* one-time cleanup has emptied the entry row the entry answers pure defaults —
|
|
2685
2562
|
* treating those as authoritative would erase the user's values on the next
|
|
2686
|
-
* boot. Re-checked on every apply, which is also what closes the
|
|
2687
|
-
*
|
|
2563
|
+
* boot. Re-checked on every apply, which is also what closes the volatile
|
|
2564
|
+
* commit timing window.
|
|
2688
2565
|
*
|
|
2689
2566
|
* When the entry is authoritative, its own fields are then deleted from the
|
|
2690
2567
|
* profile row, so no second source of truth remains.
|
|
2691
2568
|
*/
|
|
2692
2569
|
const migrateOwnSettings = () => {
|
|
2693
|
-
const legacy = readLegacySections([
|
|
2694
|
-
QODER_SETTINGS_NS,
|
|
2695
|
-
QODER_GLOBAL_SETTINGS_NS,
|
|
2696
|
-
QODER_QUOTA_SETTINGS_NS
|
|
2697
|
-
]);
|
|
2698
2570
|
const seeded = {};
|
|
2699
2571
|
for (const key of CONFIG_KEYS) {
|
|
2700
2572
|
const entryValue = readField(config, key);
|
|
2701
|
-
const legacyValue = legacy === void 0 ? void 0 : legacy[key];
|
|
2702
2573
|
if (!Object.hasOwn(store.user, key)) {
|
|
2703
2574
|
if (entryValue !== void 0 && JSON.stringify(entryValue) !== JSON.stringify(DEFAULT_FOR_FIELD[key])) seeded[key] = entryValue;
|
|
2704
|
-
else if (legacyValue !== void 0 && JSON.stringify(legacyValue) !== JSON.stringify(DEFAULT_FOR_FIELD[key])) seeded[key] = legacyValue;
|
|
2705
2575
|
else if (entryValue !== void 0) seeded[key] = entryValue;
|
|
2706
|
-
else if (legacyValue !== void 0) seeded[key] = legacyValue;
|
|
2707
2576
|
continue;
|
|
2708
2577
|
}
|
|
2709
2578
|
if (store.edited) continue;
|
|
2710
|
-
if (entryValue !== void 0 && JSON.stringify(entryValue) !== JSON.stringify(DEFAULT_FOR_FIELD[key]))
|
|
2711
|
-
seeded[key] = entryValue;
|
|
2712
|
-
continue;
|
|
2713
|
-
}
|
|
2714
|
-
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;
|
|
2715
2580
|
}
|
|
2716
2581
|
if (Object.keys(seeded).length === 0) return;
|
|
2717
2582
|
store.patch(seeded);
|
|
2718
|
-
cleanupEntryConfig(ctx, CONFIG_KEYS
|
|
2719
|
-
QODER_SETTINGS_NS,
|
|
2720
|
-
QODER_GLOBAL_SETTINGS_NS,
|
|
2721
|
-
QODER_QUOTA_SETTINGS_NS
|
|
2722
|
-
]);
|
|
2583
|
+
cleanupEntryConfig(ctx, CONFIG_KEYS);
|
|
2723
2584
|
};
|
|
2724
2585
|
migrateOwnSettings();
|
|
2725
|
-
|
|
2726
|
-
eventSink.on("loader/volatile-update", () => {
|
|
2586
|
+
ctx.on("loader/volatile-update", () => {
|
|
2727
2587
|
migrateOwnSettings();
|
|
2728
2588
|
});
|
|
2729
|
-
eventSink.on("app-boot/config-reload", () => {
|
|
2730
|
-
lastConfigReloadAt = Date.now();
|
|
2731
|
-
});
|
|
2732
2589
|
/** Timers and in-flight work belonging to this plugin instance. */
|
|
2733
2590
|
let stopped = false;
|
|
2734
2591
|
const timers = [];
|
|
@@ -7,6 +7,8 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
8
8
|
import { EMPTY_RESPONSE_CODE, LlmError, ProviderRequestId, ReasoningEffortId, ToolCallId, attributionHeaders, contentHasImage, createAssistantMessage, createToolResultMessage, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
|
+
import { brotliDecompressSync, gunzipSync, inflateSync } from "node:zlib";
|
|
11
|
+
import { requestImageDimensions } from "@deepseek-ai/dsh-attachment";
|
|
10
12
|
import { execFileSync } from "node:child_process";
|
|
11
13
|
//#region src/paths.ts
|
|
12
14
|
/**
|
|
@@ -1035,10 +1037,40 @@ function getQoderClaimCampaignUrl(region = "global", campaignId) {
|
|
|
1035
1037
|
}
|
|
1036
1038
|
//#endregion
|
|
1037
1039
|
//#region src/qoder/transport/machine-id.ts
|
|
1040
|
+
/**
|
|
1041
|
+
* The machine id Qoder's own desktop CLI keeps at
|
|
1042
|
+
* `$HOME/.qoder/.auth/machine_id`.
|
|
1043
|
+
*
|
|
1044
|
+
* Strictly a read source for this plugin: when the official client generated
|
|
1045
|
+
* one, the transport reuses it so the same device presents one stable
|
|
1046
|
+
* identity upstream. The plugin never writes there — that tree belongs to
|
|
1047
|
+
* the official CLI.
|
|
1048
|
+
*/
|
|
1049
|
+
function officialMachineIdPath() {
|
|
1050
|
+
return join(homedir(), ".qoder", ".auth", "machine_id");
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* The trusted locations consulted when a caller nominates no chain: the
|
|
1054
|
+
* official Qoder CLI's id first, this plugin's own seed second.
|
|
1055
|
+
*
|
|
1056
|
+
* A freshly created id is saved ONLY at the end of the chain, which is now
|
|
1057
|
+
* always inside the plugin's data directory. An earlier default ended the
|
|
1058
|
+
* chain at `~/.dsh/qoder/machine_id`, derived from `os.homedir()` rather
|
|
1059
|
+
* than the resolved harness home, so any host with `DSH_HOME` pointed at
|
|
1060
|
+
* another location got a device credential scattered into the wrong `$HOME`
|
|
1061
|
+
* — outside every profile, with no owner deleting it. That fallback is
|
|
1062
|
+
* gone.
|
|
1063
|
+
*/
|
|
1038
1064
|
function defaultMachineIdPaths() {
|
|
1039
|
-
return [
|
|
1065
|
+
return [officialMachineIdPath(), qoderMachineIdPath()];
|
|
1040
1066
|
}
|
|
1041
|
-
/**
|
|
1067
|
+
/**
|
|
1068
|
+
* Read Qoder's machine id, or create this plugin's seed at the chain's end.
|
|
1069
|
+
*
|
|
1070
|
+
* `paths` is the read chain; its last entry doubles as the create point.
|
|
1071
|
+
* Passing no chain defaults to {@link defaultMachineIdPaths}; passing an
|
|
1072
|
+
* empty array nominates no save location, which keeps the id process-local.
|
|
1073
|
+
*/
|
|
1042
1074
|
function getMachineId(paths = defaultMachineIdPaths()) {
|
|
1043
1075
|
for (const path of paths) {
|
|
1044
1076
|
if (!existsSync(path)) continue;
|
|
@@ -1049,7 +1081,8 @@ function getMachineId(paths = defaultMachineIdPaths()) {
|
|
|
1049
1081
|
}
|
|
1050
1082
|
const machineId = crypto.randomUUID();
|
|
1051
1083
|
const savePath = paths.at(-1);
|
|
1052
|
-
if (savePath
|
|
1084
|
+
if (savePath === void 0) return machineId;
|
|
1085
|
+
try {
|
|
1053
1086
|
mkdirSync(dirname(savePath), { recursive: true });
|
|
1054
1087
|
writeFileSync(savePath, machineId, {
|
|
1055
1088
|
encoding: "utf8",
|
|
@@ -1262,22 +1295,34 @@ async function readLimitedText(response, maxBytes, label) {
|
|
|
1262
1295
|
}
|
|
1263
1296
|
if (!response.body) return "";
|
|
1264
1297
|
const reader = response.body.getReader();
|
|
1265
|
-
const
|
|
1298
|
+
const chunks = [];
|
|
1266
1299
|
let bytes = 0;
|
|
1267
|
-
let text = "";
|
|
1268
1300
|
try {
|
|
1269
1301
|
while (true) {
|
|
1270
1302
|
const { done, value } = await reader.read();
|
|
1271
1303
|
if (done) break;
|
|
1272
1304
|
bytes += value.byteLength;
|
|
1273
1305
|
if (bytes > maxBytes) throw new QoderLlmError(`${label} exceeded its response size limit.`, "MALFORMED_RESPONSE");
|
|
1274
|
-
|
|
1306
|
+
chunks.push(value);
|
|
1275
1307
|
}
|
|
1276
|
-
return text + decoder.decode();
|
|
1277
1308
|
} finally {
|
|
1278
1309
|
await reader.cancel().catch(() => void 0);
|
|
1279
1310
|
reader.releaseLock();
|
|
1280
1311
|
}
|
|
1312
|
+
let payload = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)));
|
|
1313
|
+
const encoding = (response.headers.get("content-encoding") ?? "").toLowerCase();
|
|
1314
|
+
const looksGzipped = payload.length >= 2 && payload[0] === 31 && payload[1] === 139;
|
|
1315
|
+
if (encoding !== "" || looksGzipped) {
|
|
1316
|
+
try {
|
|
1317
|
+
if (encoding.includes("gzip") || encoding === "" && looksGzipped) payload = gunzipSync(payload);
|
|
1318
|
+
else if (encoding.includes("br")) payload = brotliDecompressSync(payload);
|
|
1319
|
+
else if (encoding.includes("deflate")) payload = inflateSync(payload);
|
|
1320
|
+
} catch {
|
|
1321
|
+
throw new QoderLlmError(`${label} arrived compressed but could not be decompressed.`, "MALFORMED_RESPONSE");
|
|
1322
|
+
}
|
|
1323
|
+
if (payload.byteLength > maxBytes) throw new QoderLlmError(`${label} exceeded its response size limit.`, "MALFORMED_RESPONSE");
|
|
1324
|
+
}
|
|
1325
|
+
return new TextDecoder().decode(payload);
|
|
1281
1326
|
}
|
|
1282
1327
|
var SingleFlight = class {
|
|
1283
1328
|
flights = /* @__PURE__ */ new Map();
|
|
@@ -1330,6 +1375,7 @@ async function openApiJsonRequest(fetchImpl, options) {
|
|
|
1330
1375
|
try {
|
|
1331
1376
|
const headers = {
|
|
1332
1377
|
accept: "application/json",
|
|
1378
|
+
"accept-encoding": "identity",
|
|
1333
1379
|
"user-agent": options.userAgent ?? defaultUserAgent,
|
|
1334
1380
|
"cosy-version": "1.0.1",
|
|
1335
1381
|
"cosy-clienttype": "5",
|
|
@@ -1647,6 +1693,7 @@ async function fetchQoderModels(credentials, options = {}) {
|
|
|
1647
1693
|
method: "GET",
|
|
1648
1694
|
headers: {
|
|
1649
1695
|
accept: "application/json",
|
|
1696
|
+
"accept-encoding": "identity",
|
|
1650
1697
|
...buildAuthHeaders(null, url, credentials)
|
|
1651
1698
|
},
|
|
1652
1699
|
signal: deadline.signal
|
|
@@ -1686,9 +1733,16 @@ async function fetchQoderModels(credentials, options = {}) {
|
|
|
1686
1733
|
function unsupported(message) {
|
|
1687
1734
|
return new QoderLlmError(message, "UNSUPPORTED_CONTENT");
|
|
1688
1735
|
}
|
|
1689
|
-
|
|
1736
|
+
/**
|
|
1737
|
+
* Flatten one tool-result message's content to wire text.
|
|
1738
|
+
*
|
|
1739
|
+
* DSH 0.1.7 models a tool result as a first-class `role: 'tool'` message
|
|
1740
|
+
* (before 0.1.6 it was a `tool-result` block inside a user message); either
|
|
1741
|
+
* way the payload is a content array whose text/图像 blocks this flattens.
|
|
1742
|
+
*/
|
|
1743
|
+
function toolResultText(content) {
|
|
1690
1744
|
let text = "";
|
|
1691
|
-
for (const nested of
|
|
1745
|
+
for (const nested of content) {
|
|
1692
1746
|
if (nested.type === "image") continue;
|
|
1693
1747
|
if (nested.type !== "text") throw unsupported(`Qoder tool results support text only; received nested ${String(nested.type)} content.`);
|
|
1694
1748
|
text += nested.text;
|
|
@@ -1699,16 +1753,19 @@ function toolResultText(block) {
|
|
|
1699
1753
|
* Check message shapes without performing any provider I/O.
|
|
1700
1754
|
*
|
|
1701
1755
|
* Callers run this before resolving credentials so an invalid request never
|
|
1702
|
-
* consumes a Qoder subscription.
|
|
1756
|
+
* consumes a Qoder subscription. The input is DSH's request shape — durable
|
|
1757
|
+
* messages and one-shot user inputs alike; only `role` and `content` are read.
|
|
1703
1758
|
*/
|
|
1704
1759
|
function validateMessageShapes(messages) {
|
|
1705
1760
|
for (const message of messages) {
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1761
|
+
if (message.role === "tool") {
|
|
1762
|
+
for (const block of message.content) {
|
|
1763
|
+
if (block.type === "text" || block.type === "image") continue;
|
|
1764
|
+
throw unsupported(`Qoder tool results support text and image content only; received ${String(block.type)}.`);
|
|
1765
|
+
}
|
|
1710
1766
|
continue;
|
|
1711
1767
|
}
|
|
1768
|
+
if (message.role === "developer") throw unsupported("Qoder transport does not support developer messages (dynamic tool addition/removal).");
|
|
1712
1769
|
for (const block of message.content) {
|
|
1713
1770
|
if (block.type === "text") continue;
|
|
1714
1771
|
if (block.type === "image") {
|
|
@@ -1737,10 +1794,12 @@ async function resolveImagePart(block, context) {
|
|
|
1737
1794
|
if (attachments === void 0) throw new QoderLlmError("Qoder image input requires the DSH attachment service.", "ATTACHMENT");
|
|
1738
1795
|
let image;
|
|
1739
1796
|
try {
|
|
1740
|
-
const
|
|
1797
|
+
const { width: sourceWidth, height: sourceHeight } = block.attachment;
|
|
1798
|
+
const projected = requestImageDimensions(sourceWidth, sourceHeight, attachments.imageLimits.maxImagePixels);
|
|
1741
1799
|
image = await attachments.readImageRequest(block.attachment, {
|
|
1742
|
-
|
|
1743
|
-
|
|
1800
|
+
width: projected.width,
|
|
1801
|
+
height: projected.height,
|
|
1802
|
+
maxBytes: attachments.imageLimits.maxImageBytes
|
|
1744
1803
|
}, signal);
|
|
1745
1804
|
} catch (error) {
|
|
1746
1805
|
if (signal?.aborted) throw new QoderLlmError("Qoder image preparation was aborted.", "ABORTED", { cause: error });
|
|
@@ -1781,26 +1840,22 @@ async function validateAndTranslateMessages(messages, systemPrompt, attachments,
|
|
|
1781
1840
|
content: systemPrompt
|
|
1782
1841
|
});
|
|
1783
1842
|
for (const message of messages) {
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1843
|
+
if (message.role === "tool") {
|
|
1844
|
+
output.push({
|
|
1845
|
+
role: "tool",
|
|
1846
|
+
tool_call_id: String(message.toolCallId),
|
|
1847
|
+
content: toolResultText(message.content)
|
|
1848
|
+
});
|
|
1849
|
+
const images = message.content.filter((block) => block.type === "image");
|
|
1850
|
+
if (images.length > 0) {
|
|
1851
|
+
if (attachments !== void 0) enforceImageLimits(images, attachments);
|
|
1788
1852
|
output.push({
|
|
1789
|
-
role: "
|
|
1790
|
-
|
|
1791
|
-
|
|
1853
|
+
role: "user",
|
|
1854
|
+
content: [{
|
|
1855
|
+
type: "text",
|
|
1856
|
+
text: `[${images.length} image${images.length === 1 ? "" : "s"} returned by the previous tool call]`
|
|
1857
|
+
}, ...await Promise.all(images.map((image) => resolveImagePart(image, context)))]
|
|
1792
1858
|
});
|
|
1793
|
-
const images = result.content.filter((block) => block.type === "image");
|
|
1794
|
-
if (images.length > 0) {
|
|
1795
|
-
if (attachments !== void 0) enforceImageLimits(images, attachments);
|
|
1796
|
-
output.push({
|
|
1797
|
-
role: "user",
|
|
1798
|
-
content: [{
|
|
1799
|
-
type: "text",
|
|
1800
|
-
text: `[${images.length} image${images.length === 1 ? "" : "s"} returned by the previous tool call]`
|
|
1801
|
-
}, ...await Promise.all(images.map((image) => resolveImagePart(image, context)))]
|
|
1802
|
-
});
|
|
1803
|
-
}
|
|
1804
1859
|
}
|
|
1805
1860
|
continue;
|
|
1806
1861
|
}
|
|
@@ -1843,6 +1898,7 @@ async function validateAndTranslateMessages(messages, systemPrompt, attachments,
|
|
|
1843
1898
|
if (message.role === "assistant") reasoningText += block.text;
|
|
1844
1899
|
continue;
|
|
1845
1900
|
}
|
|
1901
|
+
throw unsupported(`Qoder transport encountered unsupported block type: ${String(block.type)}`);
|
|
1846
1902
|
}
|
|
1847
1903
|
if (message.role === "assistant") {
|
|
1848
1904
|
const hasReasoning = preserveThinking && reasoningText.length > 0;
|
|
@@ -1861,6 +1917,7 @@ async function validateAndTranslateMessages(messages, systemPrompt, attachments,
|
|
|
1861
1917
|
userContent[pending.slot] = await resolveImagePart(pending.block, context);
|
|
1862
1918
|
}));
|
|
1863
1919
|
}
|
|
1920
|
+
if (message.role === "developer") continue;
|
|
1864
1921
|
output.push({
|
|
1865
1922
|
role: message.role,
|
|
1866
1923
|
content: hasImage ? userContent.filter((part) => part !== void 0) : text
|
|
@@ -4388,7 +4445,7 @@ const DEFAULT_MAX_TOKENS = 32768;
|
|
|
4388
4445
|
const DEFAULT_CONTEXT_WINDOW = 18e4;
|
|
4389
4446
|
//#endregion
|
|
4390
4447
|
//#region src/version.ts
|
|
4391
|
-
const QODER_CONNECT_VERSION = "0.2.
|
|
4448
|
+
const QODER_CONNECT_VERSION = "0.2.1";
|
|
4392
4449
|
//#endregion
|
|
4393
4450
|
//#region src/host-heartbeat.ts
|
|
4394
4451
|
/**
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-qoder-connect",
|
|
3
3
|
"displayName": "DSH Qoder Connect",
|
|
4
4
|
"description": "将 Qoder(国内版 / 国际版)的模型接入 DeepSeek Harness — bring Qoder (CN & international) models into DeepSeek Harness.",
|
|
5
|
-
"version": "0.2.
|
|
5
|
+
"version": "0.2.1",
|
|
6
6
|
"author": "corrinehu",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"dsh-plugin",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
],
|
|
14
14
|
"type": "module",
|
|
15
15
|
"engines": {
|
|
16
|
-
"dsh": ">=0.1.
|
|
16
|
+
"dsh": ">=0.1.7-rc.1",
|
|
17
17
|
"node": "^22.19.0 || >=24.0.0"
|
|
18
18
|
},
|
|
19
19
|
"main": "lib/index.js",
|
|
@@ -76,35 +76,35 @@
|
|
|
76
76
|
},
|
|
77
77
|
"peerDependencies": {
|
|
78
78
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
79
|
-
"@deepseek-ai/dsh-atomic-write": "
|
|
80
|
-
"@deepseek-ai/dsh-attachment": "
|
|
81
|
-
"@deepseek-ai/dsh-home-paths": "
|
|
82
|
-
"@deepseek-ai/dsh-host-webserver": "
|
|
83
|
-
"@deepseek-ai/dsh-llm": "
|
|
84
|
-
"@deepseek-ai/dsh-llm-pi-ai": "
|
|
85
|
-
"@deepseek-ai/dsh-settings": "
|
|
79
|
+
"@deepseek-ai/dsh-atomic-write": ">=0.1.7-rc.1",
|
|
80
|
+
"@deepseek-ai/dsh-attachment": ">=0.1.7-rc.1",
|
|
81
|
+
"@deepseek-ai/dsh-home-paths": ">=0.1.7-rc.1",
|
|
82
|
+
"@deepseek-ai/dsh-host-webserver": ">=0.1.7-rc.1",
|
|
83
|
+
"@deepseek-ai/dsh-llm": ">=0.1.7-rc.1",
|
|
84
|
+
"@deepseek-ai/dsh-llm-pi-ai": ">=0.1.7-rc.1",
|
|
85
|
+
"@deepseek-ai/dsh-settings": ">=0.1.7-rc.1",
|
|
86
86
|
"@deepseek-ai/schemastery": "^3.18.2",
|
|
87
87
|
"@earendil-works/pi-ai": "^0.85.1",
|
|
88
88
|
"react": "^18.2.0"
|
|
89
89
|
},
|
|
90
90
|
"devDependencies": {
|
|
91
91
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
92
|
-
"@deepseek-ai/dsh-atomic-write": "
|
|
93
|
-
"@deepseek-ai/dsh-attachment": "
|
|
94
|
-
"@deepseek-ai/dsh-client-locale": "
|
|
95
|
-
"@deepseek-ai/dsh-client-ui-conversation": "0.1.
|
|
96
|
-
"@deepseek-ai/dsh-client-ui-model-selection": "0.1.
|
|
97
|
-
"@deepseek-ai/dsh-client-ui-renderer": "
|
|
98
|
-
"@deepseek-ai/dsh-client-ui-settings": "
|
|
99
|
-
"@deepseek-ai/dsh-client-ui-settings-plugins": "
|
|
100
|
-
"@deepseek-ai/dsh-client-ui-sidebar": "
|
|
101
|
-
"@deepseek-ai/dsh-client-ui-slots": "
|
|
102
|
-
"@deepseek-ai/dsh-credentials": "
|
|
103
|
-
"@deepseek-ai/dsh-home-paths": "
|
|
104
|
-
"@deepseek-ai/dsh-host-webserver": "
|
|
105
|
-
"@deepseek-ai/dsh-llm": "
|
|
106
|
-
"@deepseek-ai/dsh-llm-pi-ai": "
|
|
107
|
-
"@deepseek-ai/dsh-settings": "
|
|
92
|
+
"@deepseek-ai/dsh-atomic-write": ">=0.1.7-rc.1",
|
|
93
|
+
"@deepseek-ai/dsh-attachment": ">=0.1.7-rc.1",
|
|
94
|
+
"@deepseek-ai/dsh-client-locale": ">=0.1.7-rc.1",
|
|
95
|
+
"@deepseek-ai/dsh-client-ui-conversation": ">=0.1.7-rc.1",
|
|
96
|
+
"@deepseek-ai/dsh-client-ui-model-selection": ">=0.1.7-rc.1",
|
|
97
|
+
"@deepseek-ai/dsh-client-ui-renderer": ">=0.1.7-rc.1",
|
|
98
|
+
"@deepseek-ai/dsh-client-ui-settings": ">=0.1.7-rc.1",
|
|
99
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins": ">=0.1.7-rc.1",
|
|
100
|
+
"@deepseek-ai/dsh-client-ui-sidebar": ">=0.1.7-rc.1",
|
|
101
|
+
"@deepseek-ai/dsh-client-ui-slots": ">=0.1.7-rc.1",
|
|
102
|
+
"@deepseek-ai/dsh-credentials": ">=0.1.7-rc.1",
|
|
103
|
+
"@deepseek-ai/dsh-home-paths": ">=0.1.7-rc.1",
|
|
104
|
+
"@deepseek-ai/dsh-host-webserver": ">=0.1.7-rc.1",
|
|
105
|
+
"@deepseek-ai/dsh-llm": ">=0.1.7-rc.1",
|
|
106
|
+
"@deepseek-ai/dsh-llm-pi-ai": ">=0.1.7-rc.1",
|
|
107
|
+
"@deepseek-ai/dsh-settings": ">=0.1.7-rc.1",
|
|
108
108
|
"@deepseek-ai/schemastery": "^3.18.2",
|
|
109
109
|
"@earendil-works/pi-ai": "^0.85.1",
|
|
110
110
|
"@types/node": "^22.20.0",
|
|
@@ -116,8 +116,5 @@
|
|
|
116
116
|
"typescript": "^5.9.3",
|
|
117
117
|
"vitest": "^4.1.8"
|
|
118
118
|
},
|
|
119
|
-
"dependencies": {
|
|
120
|
-
"js-yaml": "^4.1.0"
|
|
121
|
-
},
|
|
122
119
|
"icon": "./assets/icon.svg"
|
|
123
120
|
}
|