@dmh2002/dsh-connect-qoder 0.0.0-stage → 0.4.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/CHANGELOG.md +428 -0
- package/LICENSE +21 -0
- package/README.en.md +266 -0
- package/README.md +265 -2
- package/THIRD_PARTY_NOTICES.md +60 -0
- package/cordis.patch.yml +12 -0
- package/docs/DESIGN.md +153 -0
- package/docs/PAT.md +181 -0
- package/docs/assets/dsh-connect-qoder-composer-credits.png +0 -0
- package/docs/assets/dsh-connect-qoder-model-management.png +0 -0
- package/docs/assets/dsh-connect-qoder-model-picker.png +0 -0
- package/docs/assets/dsh-connect-qoder-usage-card.png +0 -0
- package/icons/dsh-connect-qoder-128.png +0 -0
- package/icons/dsh-connect-qoder-64.png +0 -0
- package/lib/bin.d.ts +26 -0
- package/lib/bin.js +219 -0
- package/lib/client.js +4872 -0
- package/lib/host-heartbeat-BNuj6Lt_.js +1985 -0
- package/lib/index.d.ts +126 -0
- package/lib/index.js +3093 -0
- package/lib/status-paths-w2WDUPVS.js +218 -0
- package/lib/transfer-CUNaVvmi.js +172 -0
- package/package.json +135 -4
- package/screenshots.json +6 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
//#region src/status-paths.ts
|
|
2
|
+
/**
|
|
3
|
+
* Node-free constants and types shared by the Host and browser halves.
|
|
4
|
+
*
|
|
5
|
+
* 参考:dsh-connect-workbuddy(MIT,Copyright (c) 2026 LaoDing)的
|
|
6
|
+
* `status-paths.ts` ——「同源路由常量 + 区域参数 + 与浏览器共享的
|
|
7
|
+
* node-free DTO + 配置读取 helper(volatile 解包)」的 host↔client
|
|
8
|
+
* 桥梁形态整体沿用;其又上承 dingminhua/dsh-connect-trae(MIT)与
|
|
9
|
+
* corrinehu/dsh-workbuddy-connect(MIT)的 status-route 模式。
|
|
10
|
+
* 改动(按 Qoder 上游的实际能力):
|
|
11
|
+
* 1. 凭据是 PAT,不是桌面端登录文件——没有 searched-paths 诊断,
|
|
12
|
+
* 账号来源是 settings / 环境变量 / 插件自有保险库三种;
|
|
13
|
+
* 2. 每日签到走 Qoder 的 campaigns 端点(CN 与国际版都已接通),池的批量动作
|
|
14
|
+
* 因此是 checkin / test 两个;`checkinSupported` 仍按区域可判,字段保留;
|
|
15
|
+
* 3. 扫码登录换成「添加 PAT」:accounts 路由的 add/remove 动作;
|
|
16
|
+
* 4. 积分模型是「基础/附加/组织」三个额度桶,聚合成与家族一致的
|
|
17
|
+
* packages 形状;模型字段是倍率/上下文档位/推理与多模态标记。
|
|
18
|
+
*
|
|
19
|
+
* @module dsh-connect-qoder/status-paths
|
|
20
|
+
*/
|
|
21
|
+
/** Plugin-owned usage endpoint consumed by its browser half. */
|
|
22
|
+
const QODER_USAGE_PATH = "/plugins/dsh-connect-qoder/usage";
|
|
23
|
+
/**
|
|
24
|
+
* Per-account credit balances for one region, read ONLY while the composer
|
|
25
|
+
* panel is open.
|
|
26
|
+
*
|
|
27
|
+
* Separate from {@link QODER_USAGE_PATH} because its cost is different in
|
|
28
|
+
* kind: the usage document is one upstream read for the selected account,
|
|
29
|
+
* while this is one read PER account in the region. Folding it into the usage
|
|
30
|
+
* route would make every 5-minute refresh pay for a table nobody is looking
|
|
31
|
+
* at, so it is fetched on demand instead — the same principle the pool
|
|
32
|
+
* applies to its per-account figures.
|
|
33
|
+
*/
|
|
34
|
+
const QODER_ACCOUNT_CREDITS_PATH = "/plugins/dsh-connect-qoder/account-credits";
|
|
35
|
+
/**
|
|
36
|
+
* The project's public repository.
|
|
37
|
+
*
|
|
38
|
+
* Lives here rather than beside one component because TWO sections link to it
|
|
39
|
+
* — the model section and the account pool — and a second copy of a URL is a
|
|
40
|
+
* second thing to forget when it moves. Node-free and side-effect free, so it
|
|
41
|
+
* costs nothing on the Host side either.
|
|
42
|
+
*/
|
|
43
|
+
const QODER_GITHUB_URL = "https://github.com/dingminhua/dsh-connect-qoder";
|
|
44
|
+
/** Plugin-owned live model refresh endpoint. */
|
|
45
|
+
const QODER_MODELS_REFRESH_PATH = "/plugins/dsh-connect-qoder/models/refresh";
|
|
46
|
+
/** Plugin-owned account rescan endpoint (re-reads settings / env / vault). */
|
|
47
|
+
const QODER_ACCOUNTS_REFRESH_PATH = "/plugins/dsh-connect-qoder/accounts/refresh";
|
|
48
|
+
/**
|
|
49
|
+
* Plugin-owned account mutation endpoint: add or remove one account.
|
|
50
|
+
*
|
|
51
|
+
* `?action=add` POSTs `{ pat, name? }`: the Host validates the PAT against
|
|
52
|
+
* the region's gateway (a jobToken exchange doubles as the cheapest
|
|
53
|
+
* authenticated check and yields the account's display name) and stores it in
|
|
54
|
+
* the plugin-owned vault. `?action=remove` POSTs `{ accountId }` and deletes
|
|
55
|
+
* the vault entry. POST + loopback-only: both carry token material or mutate
|
|
56
|
+
* stored credentials.
|
|
57
|
+
*/
|
|
58
|
+
const QODER_ACCOUNTS_PATH = "/plugins/dsh-connect-qoder/accounts";
|
|
59
|
+
/**
|
|
60
|
+
* Plugin-owned model probe endpoint.
|
|
61
|
+
*
|
|
62
|
+
* Sends one minimal chat request per named model so the card can answer "is
|
|
63
|
+
* this model usable right now, and if it is limited, when can it be used
|
|
64
|
+
* again?". Lives on the Host because the credential must never reach the
|
|
65
|
+
* browser half.
|
|
66
|
+
*/
|
|
67
|
+
const QODER_PROBE_PATH = "/plugins/dsh-connect-qoder/probe";
|
|
68
|
+
/**
|
|
69
|
+
* Plugin-owned account-pool endpoint: one batch action per call.
|
|
70
|
+
*
|
|
71
|
+
* `?action=test` probes every checked account against the region's target
|
|
72
|
+
* model; `?action=remove` drops one account's membership. Both are POST and
|
|
73
|
+
* loopback-only because a probe spends credits and removal mutates a
|
|
74
|
+
* preference.
|
|
75
|
+
*
|
|
76
|
+
* Deliberately ONE endpoint rather than two: the actions share every guard
|
|
77
|
+
* (method, origin, region) and differ only in the work, so splitting them
|
|
78
|
+
* would duplicate the guards and invite them to drift apart.
|
|
79
|
+
*/
|
|
80
|
+
const QODER_POOL_PATH = "/plugins/dsh-connect-qoder/pool";
|
|
81
|
+
/**
|
|
82
|
+
* Plugin-owned account transfer endpoint: the batch export / import of PAT
|
|
83
|
+
* credentials between machines.
|
|
84
|
+
*
|
|
85
|
+
* `?action=export` POSTs account ids, answers a JSON ARRAY of credential
|
|
86
|
+
* records; `?action=preview` POSTs file text, answers a token-free listing;
|
|
87
|
+
* `?action=import` POSTs file text plus selected indexes, merges into the
|
|
88
|
+
* vault. POST + loopback-only like every other mutation: the file content is
|
|
89
|
+
* arbitrary user data and export carries token material.
|
|
90
|
+
*/
|
|
91
|
+
const QODER_TRANSFER_PATH = "/plugins/dsh-connect-qoder/transfer";
|
|
92
|
+
/**
|
|
93
|
+
* Plugin-owned settings write fallback: the card's writes go through the
|
|
94
|
+
* settings mirror when it is healthy, and through this Host-side endpoint
|
|
95
|
+
* when the mirror refuses or lags (a measured 0.1.7 behaviour the sibling
|
|
96
|
+
* plugin hit: `settings/mutate` from the browser can leave the mirror stale
|
|
97
|
+
* while the Host accepted the write).
|
|
98
|
+
*/
|
|
99
|
+
const QODER_SAVE_PATH = "/plugins/dsh-connect-qoder/__save";
|
|
100
|
+
/** Query parameter naming the region a card request addresses. */
|
|
101
|
+
const QODER_REGION_PARAM = "region";
|
|
102
|
+
/** Every region, in card tab order. */
|
|
103
|
+
const QODER_REGIONS = ["cn", "global"];
|
|
104
|
+
/**
|
|
105
|
+
* Read the region parameter off a status-route URL. Absent means the domestic
|
|
106
|
+
* tab (`cn`); a present-but-unknown value returns undefined so the route can
|
|
107
|
+
* answer 400 instead of guessing.
|
|
108
|
+
*/
|
|
109
|
+
function regionOfStatusUrl(url) {
|
|
110
|
+
const at = url.indexOf("?");
|
|
111
|
+
const value = at === -1 ? null : new URLSearchParams(url.slice(at + 1)).get(QODER_REGION_PARAM);
|
|
112
|
+
if (value === null || value === "") return "cn";
|
|
113
|
+
return QODER_REGIONS.includes(value) ? value : void 0;
|
|
114
|
+
}
|
|
115
|
+
/** Query parameter selecting which action a request means. */
|
|
116
|
+
const QODER_ACTION_PARAM = "action";
|
|
117
|
+
/** Read the pool action off a request URL; unknown/absent means undefined. */
|
|
118
|
+
function poolActionOf(url) {
|
|
119
|
+
const value = actionParamOf(url);
|
|
120
|
+
return value === "checkin" || value === "test" || value === "remove" ? value : void 0;
|
|
121
|
+
}
|
|
122
|
+
/** Read the account action off a request URL; unknown/absent means undefined. */
|
|
123
|
+
function accountActionOf(url) {
|
|
124
|
+
const value = actionParamOf(url);
|
|
125
|
+
return value === "add" || value === "remove" ? value : void 0;
|
|
126
|
+
}
|
|
127
|
+
/** Read the transfer action off a request URL; unknown/absent means undefined. */
|
|
128
|
+
function transferActionOf(url) {
|
|
129
|
+
const value = actionParamOf(url);
|
|
130
|
+
return value === "export" || value === "preview" || value === "import" ? value : void 0;
|
|
131
|
+
}
|
|
132
|
+
/** Read the shared action parameter; unknown/absent means undefined. */
|
|
133
|
+
function actionParamOf(url) {
|
|
134
|
+
const at = url.indexOf("?");
|
|
135
|
+
if (at === -1) return void 0;
|
|
136
|
+
const value = new URLSearchParams(url.slice(at + 1)).get(QODER_ACTION_PARAM);
|
|
137
|
+
return value === null || value === "" ? void 0 : value;
|
|
138
|
+
}
|
|
139
|
+
/** Query parameter naming ONE pool account a per-row request addresses. */
|
|
140
|
+
const QODER_POOL_ACCOUNT_PARAM = "accountId";
|
|
141
|
+
/** Read the per-row account id off a request URL, when the action takes one. */
|
|
142
|
+
function poolAccountIdOf(url) {
|
|
143
|
+
const at = url.indexOf("?");
|
|
144
|
+
if (at === -1) return void 0;
|
|
145
|
+
const value = new URLSearchParams(url.slice(at + 1)).get(QODER_POOL_ACCOUNT_PARAM);
|
|
146
|
+
return value !== null && value !== "" ? value : void 0;
|
|
147
|
+
}
|
|
148
|
+
/** All region keys, in display order. The single source for region loops. */
|
|
149
|
+
const QODER_WEB_REGIONS = ["cn", "global"];
|
|
150
|
+
/** Every domain {@link knownQoderRegionOfDomain} admits, i.e. the transfer domains. */
|
|
151
|
+
const TRANSFER_DOMAINS = ["qoder.com.cn", "qoder.sh"];
|
|
152
|
+
/**
|
|
153
|
+
* The login domain a transfer record must carry to belong to a region.
|
|
154
|
+
*
|
|
155
|
+
* Qoder has two gateways with distinct hosts, so a transferred PAT identifies
|
|
156
|
+
* its region by the host it was minted against — `qoder.com.cn` is the CN
|
|
157
|
+
* gateway's domain, `qoder.sh` the international one.
|
|
158
|
+
*/
|
|
159
|
+
function knownQoderRegionOfDomain(domain) {
|
|
160
|
+
if (domain === "qoder.com.cn") return "cn";
|
|
161
|
+
if (domain === "qoder.sh") return "global";
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* 该区域的**传输域名**:导出账号时写进 transfer 文件的 `domain` 字段。
|
|
165
|
+
*
|
|
166
|
+
* 与 {@link knownQoderRegionOfDomain}(域名 → 区域)是同一组事实的**反向读法**,
|
|
167
|
+
* 所以在这里定义、并从那张反查表**推导**出来,而不是再手抄一遍域名 —— 手抄才是
|
|
168
|
+
* 这两处漂移过一次就很难被发现的原因。推导失败即抛错,让漏登记的区域在编译期
|
|
169
|
+
* 与首屏就暴露,而不是等到用户导出才发现。
|
|
170
|
+
*
|
|
171
|
+
* 注意这与 {@link QODER_CONSOLE_DOMAIN} **不是同一个域名**:那边是用户浏览器里
|
|
172
|
+
* 申请 PAT 的控制台(国际版是 `qoder.com`),这边是 PAT 实际发证方所在的网关域
|
|
173
|
+
* (国际版是 `qoder.sh`),导入时的区域门只认后者。
|
|
174
|
+
*/
|
|
175
|
+
const QODER_REGION_TRANSFER_DOMAIN = (() => {
|
|
176
|
+
const table = {};
|
|
177
|
+
for (const region of QODER_WEB_REGIONS) table[region] = "";
|
|
178
|
+
for (const domain of TRANSFER_DOMAINS) {
|
|
179
|
+
const region = knownQoderRegionOfDomain(domain);
|
|
180
|
+
if (region === void 0) throw new Error(`transfer domain is not a known Qoder region: ${domain}`);
|
|
181
|
+
table[region] = domain;
|
|
182
|
+
}
|
|
183
|
+
for (const region of QODER_WEB_REGIONS) if (table[region] === "") throw new Error(`region has no transfer domain: ${region}`);
|
|
184
|
+
return table;
|
|
185
|
+
})();
|
|
186
|
+
/**
|
|
187
|
+
* Deep copy of a settings value with every `{get(): T}` live reference
|
|
188
|
+
* replaced by the value it resolves to.
|
|
189
|
+
*
|
|
190
|
+
* `regions` is declared `asVolatile(...)`, and schemastery resolves a volatile
|
|
191
|
+
* field to a live reference. A live reference is still `typeof === 'object'`,
|
|
192
|
+
* so `{ ...reference }` does NOT read the field: it produces
|
|
193
|
+
* `{ get: <function> }`. Any caller that spreads the resolved field to
|
|
194
|
+
* preserve its siblings would otherwise DROP every sibling and leak a
|
|
195
|
+
* function into the document. Both halves therefore unwrap before touching a
|
|
196
|
+
* resolved field.
|
|
197
|
+
*/
|
|
198
|
+
function unwrapVolatileDeep(value) {
|
|
199
|
+
if (value === null || typeof value !== "object") return value;
|
|
200
|
+
if (typeof value.get === "function") return unwrapVolatileDeep(value.get());
|
|
201
|
+
if (Array.isArray(value)) return value.map((entry) => unwrapVolatileDeep(entry));
|
|
202
|
+
const source = value;
|
|
203
|
+
const out = {};
|
|
204
|
+
for (const key of Object.keys(source)) out[key] = unwrapVolatileDeep(source[key]);
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* The plugin's DECLARED settings namespace — the fallback name, not the value
|
|
209
|
+
* the host necessarily serves.
|
|
210
|
+
*
|
|
211
|
+
* On 0.1.7 the settings service keys every form by the plugin's Loader entry
|
|
212
|
+
* id (`describe()` returns `ns: entry.options.id`), and the harness resolves
|
|
213
|
+
* a provider's namespace by EXACT match. The entry id is chosen by the
|
|
214
|
+
* profile patch (`cordis.patch.yml`), so the declared id mirrors it.
|
|
215
|
+
*/
|
|
216
|
+
const QODER_SETTINGS_NS = "dsh-connect-qoder";
|
|
217
|
+
//#endregion
|
|
218
|
+
export { regionOfStatusUrl as _, QODER_MODELS_REFRESH_PATH as a, QODER_REGIONS as c, QODER_SETTINGS_NS as d, QODER_TRANSFER_PATH as f, poolActionOf as g, poolAccountIdOf as h, QODER_GITHUB_URL as i, QODER_REGION_TRANSFER_DOMAIN as l, accountActionOf as m, QODER_ACCOUNTS_REFRESH_PATH as n, QODER_POOL_PATH as o, QODER_USAGE_PATH as p, QODER_ACCOUNT_CREDITS_PATH as r, QODER_PROBE_PATH as s, QODER_ACCOUNTS_PATH as t, QODER_SAVE_PATH as u, transferActionOf as v, unwrapVolatileDeep as y };
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { g as qoderAccountId } from "./host-heartbeat-BNuj6Lt_.js";
|
|
2
|
+
import { l as QODER_REGION_TRANSFER_DOMAIN } from "./status-paths-w2WDUPVS.js";
|
|
3
|
+
//#region src/transfer.ts
|
|
4
|
+
/**
|
|
5
|
+
* Account transfer: the export / import file format shared with sibling
|
|
6
|
+
* credential managers for the same upstream.
|
|
7
|
+
*
|
|
8
|
+
* 参考:dsh-connect-workbuddy(MIT,Copyright (c) 2026 LaoDing)的
|
|
9
|
+
* `src/transfer.ts` —— 传输文件格式(JSON 数组、snake_case 字段、去重、
|
|
10
|
+
* 预览不回明文、解析不依赖文件系统、区域门放在纯函数里以便无 UI 单测)
|
|
11
|
+
* 的形态整体沿用;工作清单上又是家族内各 DSH 插件的既有互操作约定。
|
|
12
|
+
* 改动(按 Qoder 上游的实际能力):
|
|
13
|
+
* 1. 凭据是 PAT,不是桌面端登录文件——记录只承载 `access_token`(即
|
|
14
|
+
* PAT)、`uid`(PAT 派生的账号 id)、`nickname`、`email`、`domain`,
|
|
15
|
+
* 没有 refresh_token / expiresAt 一族字段(PAT 一般无过期);
|
|
16
|
+
* 2. domain 只取两个上游区域值——`qoder.com.cn`(cn)/ `qoder.sh`
|
|
17
|
+
* (global)——区域门用白名单正匹配:值不属于目标区域的记录一律跳过,
|
|
18
|
+
* 不做「当作哪个区域都行」的默认,避免跨区导入交叉计费或收进伪装域;
|
|
19
|
+
* 3. 账号 id 由 PAT 单向散列派生(`credentials.ts` 的 `qoderAccountId`),
|
|
20
|
+
* 合并按 id 去重、以同 id 覆盖;import 落库由 Host 侧的
|
|
21
|
+
* `QoderAccountStore.add(pat, accountName)` 完成,`nickname` → accountName。
|
|
22
|
+
*
|
|
23
|
+
* @module dsh-connect-qoder/transfer
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Parse transfer-file text: a JSON array, every element an object.
|
|
27
|
+
*
|
|
28
|
+
* The error strings carry the position (1-based item number) because a file
|
|
29
|
+
* from another tool can be big, and "第 N 项" is what lets the user find the
|
|
30
|
+
* broken record in their editor.
|
|
31
|
+
*/
|
|
32
|
+
function parseTransferFile(text) {
|
|
33
|
+
if (text.trim() === "") throw new Error("transfer file is empty");
|
|
34
|
+
let parsed;
|
|
35
|
+
try {
|
|
36
|
+
parsed = JSON.parse(text);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
throw new Error("transfer file is not valid JSON: " + (error instanceof Error ? error.message : String(error)));
|
|
39
|
+
}
|
|
40
|
+
if (!Array.isArray(parsed)) throw new Error("transfer file must be a JSON array of account records");
|
|
41
|
+
for (const [index, item] of parsed.entries()) if (typeof item !== "object" || item === null || Array.isArray(item)) throw new Error("transfer file item " + (index + 1) + " is not an account object");
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
/** One string field of a record, tolerating the sibling format's odd shapes. */
|
|
45
|
+
function displayString(value) {
|
|
46
|
+
if (typeof value === "string") return value;
|
|
47
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
/** Whether a record carries a usable token, envelope-shaped or plain. */
|
|
51
|
+
function hasToken(record) {
|
|
52
|
+
const token = record["access_token"];
|
|
53
|
+
return typeof token === "string" && token !== "";
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The transfer file's desensitized preview: display fields only.
|
|
57
|
+
*
|
|
58
|
+
* Fields come from EXTERNAL files, so a display field may hold an object or an
|
|
59
|
+
* array (a sibling tool can store the desktop app's encrypted-wrapper nickname
|
|
60
|
+
* verbatim); every preview field is normalized to a string or emptied rather
|
|
61
|
+
* than letting a non-renderable value reach the card.
|
|
62
|
+
*/
|
|
63
|
+
function previewTransferFile(text) {
|
|
64
|
+
const records = parseTransferFile(text);
|
|
65
|
+
return {
|
|
66
|
+
accounts: records.map((record, index) => ({
|
|
67
|
+
index,
|
|
68
|
+
uid: displayString(record["uid"]),
|
|
69
|
+
nickname: displayString(record["nickname"]),
|
|
70
|
+
email: displayString(record["email"]),
|
|
71
|
+
hasToken: hasToken(record)
|
|
72
|
+
})),
|
|
73
|
+
total: records.length
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Project one stored vault account into the shared transfer record shape.
|
|
78
|
+
*
|
|
79
|
+
* `regionDomain` is the target region's login domain (`qoder.com.cn` for cn,
|
|
80
|
+
* `qoder.sh` for global); the export caller passes it so the imported file
|
|
81
|
+
* self-describes which region each record belongs to. `uid` is the PAT-derived
|
|
82
|
+
* account id so a re-import converges on the SAME vault key. `nickname` is
|
|
83
|
+
* dropped — never set to empty/undefined — when the account has no name, and
|
|
84
|
+
* `access_token` carries the PAT itself.
|
|
85
|
+
*/
|
|
86
|
+
function transferRecordOf(account, regionDomain) {
|
|
87
|
+
return {
|
|
88
|
+
access_token: account.pat,
|
|
89
|
+
uid: account.id,
|
|
90
|
+
...account.accountName !== "" ? { nickname: account.accountName } : {},
|
|
91
|
+
domain: regionDomain
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Project a region's whole vault into the exported transfer document.
|
|
96
|
+
*
|
|
97
|
+
* This is the ONE place the export path turns vault accounts into records;
|
|
98
|
+
* the `exportAccounts` dep (`index.ts`) MUST go through it. It used to be
|
|
99
|
+
* written inline in `exportAccounts` as a raw `{pat, accountName, id}` map,
|
|
100
|
+
* which is what produced files that could never be re-imported: the region
|
|
101
|
+
* gate (`mergeTransferRecords`) reads `domain`, and the raw vault shape has
|
|
102
|
+
* none. Keeping the projection here — rather than in the Host wiring — is what
|
|
103
|
+
* lets a test export through it and immediately import the result.
|
|
104
|
+
*/
|
|
105
|
+
function transferRecordsFor(region, accounts) {
|
|
106
|
+
const domain = QODER_REGION_TRANSFER_DOMAIN[region];
|
|
107
|
+
return accounts.map((account) => transferRecordOf(account, domain));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Positive-match a record's login domain to a region.
|
|
111
|
+
*
|
|
112
|
+
* Distinct from any upstream-side registry: `domain` is whatever the FILE'S
|
|
113
|
+
* author wrote, so only an exactly-known Qoder domain counts. `qoder.com.cn`
|
|
114
|
+
* and `qoder.sh` are the plugin's two per-region domains; an empty, misspelled,
|
|
115
|
+
* spoofed, or registrable-domain-lookalike value classifies as undecidable and
|
|
116
|
+
* is refused rather than being filed under either region.
|
|
117
|
+
*/
|
|
118
|
+
function knownRegionOfDomain(domain) {
|
|
119
|
+
if (domain === "qoder.com.cn") return "cn";
|
|
120
|
+
if (domain === "qoder.sh") return "global";
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Pure merge: fold the selected records of a transfer file into a CHANGES map.
|
|
124
|
+
*
|
|
125
|
+
* Mirrors the sibling merger's contract: a record with the same account id
|
|
126
|
+
* OVERWRITES the stored entry (the imported PAT replaces what is stored —
|
|
127
|
+
* re-importing a rotated PAT must refresh it; the write layer stores the record
|
|
128
|
+
* atomically, so "overwrite" needs no knowledge of the incumbent), and a
|
|
129
|
+
* record without a token is skipped. The region gate lives HERE rather than in
|
|
130
|
+
* the route so it is testable without a Host: a record whose login domain maps
|
|
131
|
+
* to the other region's tab is skipped, because the two regions' providers are
|
|
132
|
+
* parallel stacks and a cross-region import would cross-bill.
|
|
133
|
+
*
|
|
134
|
+
* The caller persists each map entry via `QoderAccountStore.add`; an importing
|
|
135
|
+
* selection that is already stored rewrites those entries with identical
|
|
136
|
+
* content — idempotent, and the steady state of importing the same file twice.
|
|
137
|
+
*/
|
|
138
|
+
function mergeTransferRecords(region, text, indexes) {
|
|
139
|
+
const records = parseTransferFile(text);
|
|
140
|
+
const changes = /* @__PURE__ */ new Map();
|
|
141
|
+
let imported = 0;
|
|
142
|
+
let skipped = 0;
|
|
143
|
+
for (const index of indexes) {
|
|
144
|
+
const record = records[index];
|
|
145
|
+
if (record === void 0) {
|
|
146
|
+
skipped += 1;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const accessToken = record["access_token"];
|
|
150
|
+
if (typeof accessToken !== "string" || accessToken === "") {
|
|
151
|
+
skipped += 1;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (knownRegionOfDomain(record["domain"]) !== region) {
|
|
155
|
+
skipped += 1;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const id = qoderAccountId(accessToken);
|
|
159
|
+
changes.set(id, {
|
|
160
|
+
pat: accessToken,
|
|
161
|
+
accountName: displayString(record["nickname"])
|
|
162
|
+
});
|
|
163
|
+
imported += 1;
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
changes,
|
|
167
|
+
imported,
|
|
168
|
+
skipped
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
//#endregion
|
|
172
|
+
export { mergeTransferRecords, previewTransferFile, transferRecordsFor };
|
package/package.json
CHANGED
|
@@ -1,6 +1,137 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dmh2002/dsh-connect-qoder",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"displayName": "DSH Connect Qoder",
|
|
4
|
+
"description": "把 Qoder(通义灵码国内版 / 国际版)接入 DeepSeek Harness:PAT 多账号池、每日签到、积分读数与模型管理。",
|
|
5
|
+
"version": "0.4.0",
|
|
6
|
+
"author": "dingminhua",
|
|
7
|
+
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"dsh-plugin",
|
|
11
|
+
"deepseek-harness",
|
|
12
|
+
"qoder",
|
|
13
|
+
"lingma",
|
|
14
|
+
"model-provider",
|
|
15
|
+
"credits",
|
|
16
|
+
"account-pool",
|
|
17
|
+
"check-in"
|
|
18
|
+
],
|
|
19
|
+
"type": "module",
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
25
|
+
},
|
|
26
|
+
"main": "lib/index.js",
|
|
27
|
+
"types": "lib/index.d.ts",
|
|
28
|
+
"bin": {
|
|
29
|
+
"dsh-connect-qoder": "lib/bin.js"
|
|
30
|
+
},
|
|
31
|
+
"icon": "icons/dsh-connect-qoder-128.png",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./lib/index.d.ts",
|
|
35
|
+
"default": "./lib/index.js"
|
|
36
|
+
},
|
|
37
|
+
"./client": "./lib/client.js",
|
|
38
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
39
|
+
"./package.json": "./package.json"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@deepseek-ai/schemastery": "3.18.4"
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"lib",
|
|
46
|
+
"icons",
|
|
47
|
+
"docs/assets",
|
|
48
|
+
"docs/PAT.md",
|
|
49
|
+
"docs/DESIGN.md",
|
|
50
|
+
"screenshots.json",
|
|
51
|
+
"cordis.patch.yml",
|
|
52
|
+
"README.md",
|
|
53
|
+
"README.en.md",
|
|
54
|
+
"CHANGELOG.md",
|
|
55
|
+
"THIRD_PARTY_NOTICES.md",
|
|
56
|
+
"LICENSE"
|
|
57
|
+
],
|
|
58
|
+
"scripts": {
|
|
59
|
+
"prepack": "npm run build",
|
|
60
|
+
"build": "tsdown",
|
|
61
|
+
"test": "vitest run",
|
|
62
|
+
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.client.json",
|
|
63
|
+
"check": "pnpm run typecheck && pnpm run test && pnpm run build",
|
|
64
|
+
"probe:cosy": "node scripts/probe-cosy.mjs",
|
|
65
|
+
"probe:vault": "node scripts/dump-vault.mjs"
|
|
66
|
+
},
|
|
67
|
+
"dsh": {
|
|
68
|
+
"bundle": {
|
|
69
|
+
"patch": "./cordis.patch.yml"
|
|
70
|
+
},
|
|
71
|
+
"client": {
|
|
72
|
+
"inject": [
|
|
73
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
74
|
+
"@deepseek-ai/dsh-client-ui-slots",
|
|
75
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins",
|
|
76
|
+
"@deepseek-ai/dsh-client-locale"
|
|
77
|
+
],
|
|
78
|
+
"platform": "web"
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"repository": {
|
|
82
|
+
"type": "git",
|
|
83
|
+
"url": "git+https://github.com/dingminhua/dsh-connect-qoder.git"
|
|
84
|
+
},
|
|
85
|
+
"homepage": "https://github.com/dingminhua/dsh-connect-qoder#readme",
|
|
86
|
+
"bugs": {
|
|
87
|
+
"url": "https://github.com/dingminhua/dsh-connect-qoder/issues"
|
|
88
|
+
},
|
|
89
|
+
"peerDependencies": {
|
|
90
|
+
"@deepseek-ai/cordis": ">=4.0.1-rc.1 <5.0.0 || >=4.0.2 <5.0.0",
|
|
91
|
+
"@deepseek-ai/dsh-atomic-write": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
92
|
+
"@deepseek-ai/dsh-attachment": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
93
|
+
"@deepseek-ai/dsh-client-locale": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
94
|
+
"@deepseek-ai/dsh-client-ui-primitives": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
95
|
+
"@deepseek-ai/dsh-client-ui-renderer": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
96
|
+
"@deepseek-ai/dsh-client-ui-settings": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
97
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
98
|
+
"@deepseek-ai/dsh-client-ui-slots": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
99
|
+
"@deepseek-ai/dsh-home-paths": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
100
|
+
"@deepseek-ai/dsh-host-webserver": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
101
|
+
"@deepseek-ai/dsh-llm": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
102
|
+
"@deepseek-ai/dsh-llm-pi-ai": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
103
|
+
"@deepseek-ai/dsh-settings": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
104
|
+
"@deepseek-ai/schemastery": ">=3.18.1-rc.1 <4.0.0 || >=3.18.2 <4.0.0",
|
|
105
|
+
"@earendil-works/pi-ai": ">=0.85.0 <0.88.0",
|
|
106
|
+
"react": "^18.2.0"
|
|
107
|
+
},
|
|
108
|
+
"devDependencies": {
|
|
109
|
+
"@deepseek-ai/cordis": ">=4.0.1 <5.0.0 || >=4.0.2 <5.0.0",
|
|
110
|
+
"@deepseek-ai/dsh-atomic-write": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
111
|
+
"@deepseek-ai/dsh-attachment": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
112
|
+
"@deepseek-ai/dsh-client-locale": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
113
|
+
"@deepseek-ai/dsh-client-ui-primitives": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
114
|
+
"@deepseek-ai/dsh-client-ui-renderer": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
115
|
+
"@deepseek-ai/dsh-client-ui-settings": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
116
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
117
|
+
"@deepseek-ai/dsh-client-ui-slots": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
118
|
+
"@deepseek-ai/dsh-home-paths": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
119
|
+
"@deepseek-ai/dsh-host-webserver": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
120
|
+
"@deepseek-ai/dsh-llm": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
121
|
+
"@deepseek-ai/dsh-llm-pi-ai": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
122
|
+
"@deepseek-ai/dsh-settings": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
123
|
+
"@deepseek-ai/schemastery": ">=3.18.1 <4.0.0 || >=3.18.2 <4.0.0",
|
|
124
|
+
"@earendil-works/pi-ai": ">=0.85.0 <0.88.0",
|
|
125
|
+
"@types/node": "^22.20.0",
|
|
126
|
+
"@types/react": "~18.3.31",
|
|
127
|
+
"@types/react-dom": "18.3.7",
|
|
128
|
+
"@types/semver": "^7.8.0",
|
|
129
|
+
"jsdom": "^29.1.1",
|
|
130
|
+
"react": "^18.3.1",
|
|
131
|
+
"react-dom": "18.3.1",
|
|
132
|
+
"semver": "^7.8.5",
|
|
133
|
+
"tsdown": "^0.22.2",
|
|
134
|
+
"typescript": "^5.9.3",
|
|
135
|
+
"vitest": "^4.1.8"
|
|
136
|
+
}
|
|
137
|
+
}
|