dsh-multi-router 0.6.12 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -430
- package/package.json +5 -81
- package/CHANGELOG.md +0 -243
- package/LICENSE +0 -21
- package/THIRD_PARTY_NOTICES.md +0 -171
- package/cordis.patch.yml +0 -3
- package/lib/accounts.js +0 -19
- package/lib/accounts.js.map +0 -7
- package/lib/api-service.js +0 -11
- package/lib/api-service.js.map +0 -7
- package/lib/chunk-2UM45OXY.js +0 -181
- package/lib/chunk-2UM45OXY.js.map +0 -7
- package/lib/chunk-4L2CBGXN.js +0 -674
- package/lib/chunk-4L2CBGXN.js.map +0 -7
- package/lib/chunk-4V52V5GO.js +0 -105
- package/lib/chunk-4V52V5GO.js.map +0 -7
- package/lib/chunk-DNQPZMPF.js +0 -33
- package/lib/chunk-DNQPZMPF.js.map +0 -7
- package/lib/chunk-FLL3DUVP.js +0 -2017
- package/lib/chunk-FLL3DUVP.js.map +0 -7
- package/lib/chunk-GEBZMIMA.js +0 -287
- package/lib/chunk-GEBZMIMA.js.map +0 -7
- package/lib/chunk-KWXLQJS5.js +0 -1102
- package/lib/chunk-KWXLQJS5.js.map +0 -7
- package/lib/chunk-NZF36O5I.js +0 -5421
- package/lib/chunk-NZF36O5I.js.map +0 -7
- package/lib/chunk-S5KMFAQM.js +0 -50
- package/lib/chunk-S5KMFAQM.js.map +0 -7
- package/lib/chunk-TM5XDMGY.js +0 -332
- package/lib/chunk-TM5XDMGY.js.map +0 -7
- package/lib/chunk-V37VG7MN.js +0 -2101
- package/lib/chunk-V37VG7MN.js.map +0 -7
- package/lib/chunk-WDIT4E2W.js +0 -120
- package/lib/chunk-WDIT4E2W.js.map +0 -7
- package/lib/cli.js +0 -103
- package/lib/cli.js.map +0 -7
- package/lib/client.js +0 -2805
- package/lib/client.js.map +0 -7
- package/lib/clients.js +0 -30
- package/lib/clients.js.map +0 -7
- package/lib/host-v0.6.12.js +0 -414
- package/lib/host.js +0 -414
- package/lib/index.js +0 -414
- package/lib/index.js.map +0 -7
- package/lib/manager.js +0 -27
- package/lib/manager.js.map +0 -7
- package/lib/region-DO3KAV7V.js +0 -15
- package/lib/region-DO3KAV7V.js.map +0 -7
- package/lib/router.js +0 -14
- package/lib/router.js.map +0 -7
- package/lib/shim.js +0 -10
- package/lib/shim.js.map +0 -7
- package/lib/upstream-VP6J7LU2.js +0 -25
- package/lib/upstream-VP6J7LU2.js.map +0 -7
- package/router.example.json +0 -289
- package/upstream-lock.json +0 -17
package/lib/chunk-V37VG7MN.js
DELETED
|
@@ -1,2101 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Checkins,
|
|
3
|
-
jsonCredential,
|
|
4
|
-
qoderPluginDataDir,
|
|
5
|
-
readLimitedText,
|
|
6
|
-
refreshWorkBuddyCheckinCredential,
|
|
7
|
-
supportsCheckin,
|
|
8
|
-
workbuddyToken
|
|
9
|
-
} from "./chunk-NZF36O5I.js";
|
|
10
|
-
import {
|
|
11
|
-
discoverLocalAccounts,
|
|
12
|
-
importAccount,
|
|
13
|
-
localAccountKey,
|
|
14
|
-
normalizeLocalAccountKey,
|
|
15
|
-
readPortableCredential
|
|
16
|
-
} from "./chunk-2UM45OXY.js";
|
|
17
|
-
import {
|
|
18
|
-
modelWithCurrentPromotion
|
|
19
|
-
} from "./chunk-KWXLQJS5.js";
|
|
20
|
-
import {
|
|
21
|
-
ApiService
|
|
22
|
-
} from "./chunk-TM5XDMGY.js";
|
|
23
|
-
import {
|
|
24
|
-
hostIsLoopback
|
|
25
|
-
} from "./chunk-DNQPZMPF.js";
|
|
26
|
-
import {
|
|
27
|
-
Telemetry,
|
|
28
|
-
contextInfo,
|
|
29
|
-
matchModel,
|
|
30
|
-
resolveContext
|
|
31
|
-
} from "./chunk-4L2CBGXN.js";
|
|
32
|
-
import {
|
|
33
|
-
PROVIDERS,
|
|
34
|
-
PROVIDER_NAMES,
|
|
35
|
-
ROUTE_PRESETS,
|
|
36
|
-
beijingDay,
|
|
37
|
-
creditRate,
|
|
38
|
-
isFreeModel,
|
|
39
|
-
routeReasoning,
|
|
40
|
-
validateConfig,
|
|
41
|
-
writePrivateJson
|
|
42
|
-
} from "./chunk-GEBZMIMA.js";
|
|
43
|
-
|
|
44
|
-
// src/manager.mjs
|
|
45
|
-
import { createHash, randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
|
|
46
|
-
|
|
47
|
-
// package.json
|
|
48
|
-
var version = "0.6.12";
|
|
49
|
-
|
|
50
|
-
// src/manager.mjs
|
|
51
|
-
import { readFile as readFile4, mkdir as mkdir2, mkdtemp as mkdtemp2, rm as rm3 } from "node:fs/promises";
|
|
52
|
-
import { watch as watch2 } from "node:fs";
|
|
53
|
-
import { dirname as dirname3, join as join5, relative as relative2, resolve } from "node:path";
|
|
54
|
-
import { withFileLock as withFileLock3 } from "@deepseek-ai/dsh-atomic-write";
|
|
55
|
-
|
|
56
|
-
// upstream/qoder/src/status-paths.ts
|
|
57
|
-
var QODER_STATUS_PATH = "/plugins/dsh-qoder-connect/status";
|
|
58
|
-
var QODER_PROBE_PATH = "/plugins/dsh-qoder-connect/probe";
|
|
59
|
-
var QODER_AUTH_PATH = "/plugins/dsh-qoder-connect/auth";
|
|
60
|
-
var QODER_GLOBAL_STATUS_PATH = "/plugins/dsh-qoder-connect/global/status";
|
|
61
|
-
var QODER_GLOBAL_PROBE_PATH = "/plugins/dsh-qoder-connect/global/probe";
|
|
62
|
-
var QODER_GLOBAL_AUTH_PATH = "/plugins/dsh-qoder-connect/global/auth";
|
|
63
|
-
|
|
64
|
-
// upstream/qoder/src/variants.ts
|
|
65
|
-
var QODER_VARIANTS = [
|
|
66
|
-
{
|
|
67
|
-
id: "qoder",
|
|
68
|
-
displayName: "Qoder",
|
|
69
|
-
appName: "Qoder",
|
|
70
|
-
region: "china",
|
|
71
|
-
ownFilename: ".qoder-auth.json",
|
|
72
|
-
probeFilename: ".qoder-probe.json",
|
|
73
|
-
catalogFilename: ".qoder-catalog.json",
|
|
74
|
-
statusPath: QODER_STATUS_PATH,
|
|
75
|
-
probePath: QODER_PROBE_PATH,
|
|
76
|
-
authPath: QODER_AUTH_PATH
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
id: "qoder-global",
|
|
80
|
-
displayName: "Qoder Global",
|
|
81
|
-
appName: "Qoder Global",
|
|
82
|
-
region: "global",
|
|
83
|
-
ownFilename: ".qoder-global-auth.json",
|
|
84
|
-
probeFilename: ".qoder-global-probe.json",
|
|
85
|
-
catalogFilename: ".qoder-global-catalog.json",
|
|
86
|
-
statusPath: QODER_GLOBAL_STATUS_PATH,
|
|
87
|
-
probePath: QODER_GLOBAL_PROBE_PATH,
|
|
88
|
-
authPath: QODER_GLOBAL_AUTH_PATH
|
|
89
|
-
}
|
|
90
|
-
];
|
|
91
|
-
var CHINA_VARIANT = QODER_VARIANTS[0];
|
|
92
|
-
var GLOBAL_VARIANT = QODER_VARIANTS[1];
|
|
93
|
-
|
|
94
|
-
// upstream/qoder/src/auth.ts
|
|
95
|
-
import { dirname, join } from "node:path";
|
|
96
|
-
import { writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
|
|
97
|
-
function qoderOwnAuthPath(variant) {
|
|
98
|
-
return join(qoderPluginDataDir(), variant.ownFilename);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// src/migration.mjs
|
|
102
|
-
import { randomBytes, scrypt, createCipheriv, createDecipheriv } from "node:crypto";
|
|
103
|
-
import { promisify } from "node:util";
|
|
104
|
-
import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
105
|
-
import { dirname as dirname2, join as join2, relative } from "node:path";
|
|
106
|
-
var derive = promisify(scrypt);
|
|
107
|
-
var FORMAT = "dsh-multi-router";
|
|
108
|
-
var MAX_BYTES = 512 * 1024;
|
|
109
|
-
var pick = (value, keys) => Object.fromEntries(keys.filter((k) => Object.hasOwn(value ?? {}, k)).map((k) => [k, value[k]]));
|
|
110
|
-
var options = ["cooldownMs", "requestTimeoutMs", "catalogTtlMs", "retryServerErrors", "modelVisibility", "usageRetentionDays", "checkinSchedule", "sidebarUsage", "seenRoutePresets"];
|
|
111
|
-
function settings(config) {
|
|
112
|
-
return {
|
|
113
|
-
...pick(config, options),
|
|
114
|
-
models: Object.fromEntries(Object.entries(config.models ?? {}).map(([p, rows]) => [p, Object.fromEntries(Object.entries(rows).map(([id, pref]) => [id, pick(pref, ["name", "visible", "contextWindow", "supportsImages", "reasoningEffort"])]))])),
|
|
115
|
-
accounts: config.accounts.map((a) => pick(a, ["id", "provider", "name", "enabled", "autoCheckin"])),
|
|
116
|
-
routes: Object.fromEntries(Object.entries(config.routes).map(([id, r]) => [id, {
|
|
117
|
-
...pick(r, ["name", "visible", "strategy", "contextMode", "contextWindow", "maxTokens", "reasoningEffort", "preferExpiringCredits", "expiringWithinDays", "freeOnly", "freeModels"]),
|
|
118
|
-
targets: r.targets.map((t) => pick(t, ["provider", "account", "model", "models", "weight", "tokenBudget", "creditBudget"]))
|
|
119
|
-
}]))
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
var problem = (message) => {
|
|
123
|
-
throw Error(message);
|
|
124
|
-
};
|
|
125
|
-
function password(value, exporting = false) {
|
|
126
|
-
if (typeof value !== "string" || value.length > 1024 || value.length < (exporting ? 10 : 1)) problem(exporting ? "\u8FC1\u79FB\u5BC6\u7801\u81F3\u5C11 10 \u4E2A\u5B57\u7B26" : "\u8BF7\u8F93\u5165\u8FC1\u79FB\u5BC6\u7801");
|
|
127
|
-
}
|
|
128
|
-
async function exportConfiguration(manager, input) {
|
|
129
|
-
const payload = { format: FORMAT, version: 1, encrypted: false, createdAt: (/* @__PURE__ */ new Date()).toISOString(), config: settings(manager.config) };
|
|
130
|
-
if (!input.includeCredentials) return payload;
|
|
131
|
-
password(input.password, true);
|
|
132
|
-
payload.credentials = {};
|
|
133
|
-
for (const account of manager.config.accounts) {
|
|
134
|
-
try {
|
|
135
|
-
const credential = await readPortableCredential(account);
|
|
136
|
-
if (credential) Object.defineProperty(payload.credentials, account.id, { value: credential, enumerable: true });
|
|
137
|
-
} catch {
|
|
138
|
-
problem(`\u8D26\u53F7 ${account.id} \u7684\u51ED\u8BC1\u4E0D\u53EF\u8BFB\uFF0C\u8BF7\u4FEE\u590D\u540E\u5BFC\u51FA\u6216\u9009\u62E9\u4EC5\u914D\u7F6E`);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
const salt = randomBytes(16), iv = randomBytes(12), key = await derive(input.password, salt, 32);
|
|
142
|
-
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
143
|
-
const bytes = Buffer.from(JSON.stringify(payload));
|
|
144
|
-
if (bytes.length > MAX_BYTES) problem("\u914D\u7F6E\u8FC7\u5927\uFF0C\u65E0\u6CD5\u5BFC\u51FA");
|
|
145
|
-
const data = Buffer.concat([cipher.update(bytes), cipher.final()]);
|
|
146
|
-
return {
|
|
147
|
-
format: FORMAT,
|
|
148
|
-
version: 1,
|
|
149
|
-
encrypted: true,
|
|
150
|
-
cipher: "aes-256-gcm",
|
|
151
|
-
kdf: "scrypt",
|
|
152
|
-
salt: salt.toString("base64"),
|
|
153
|
-
iv: iv.toString("base64"),
|
|
154
|
-
tag: cipher.getAuthTag().toString("base64"),
|
|
155
|
-
data: data.toString("base64")
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
async function decodeConfiguration(input) {
|
|
159
|
-
let payload;
|
|
160
|
-
try {
|
|
161
|
-
if (typeof input.document !== "string" || Buffer.byteLength(input.document) > 900 * 1024) problem("\u8FC1\u79FB\u6587\u4EF6\u8FC7\u5927");
|
|
162
|
-
payload = JSON.parse(input.document);
|
|
163
|
-
} catch {
|
|
164
|
-
problem("\u8BF7\u9009\u62E9\u6709\u6548\u7684\u63D2\u4EF6\u8FC1\u79FB JSON \u6587\u4EF6\uFF08\u5C0F\u4E8E 900 KB\uFF09");
|
|
165
|
-
}
|
|
166
|
-
if (payload?.format !== FORMAT || payload.version !== 1) problem("\u4E0D\u662F\u53D7\u652F\u6301\u7684\u63D2\u4EF6\u8FC1\u79FB\u6587\u4EF6");
|
|
167
|
-
const encrypted = payload.encrypted === true;
|
|
168
|
-
if (encrypted) {
|
|
169
|
-
password(input.password);
|
|
170
|
-
try {
|
|
171
|
-
if (payload.cipher !== "aes-256-gcm" || payload.kdf !== "scrypt") throw Error();
|
|
172
|
-
const salt = Buffer.from(payload.salt, "base64"), iv = Buffer.from(payload.iv, "base64"), tag = Buffer.from(payload.tag, "base64"), data = Buffer.from(payload.data, "base64");
|
|
173
|
-
if (salt.length !== 16 || iv.length !== 12 || tag.length !== 16 || data.length > MAX_BYTES) throw Error();
|
|
174
|
-
const decipher = createDecipheriv("aes-256-gcm", await derive(input.password, salt, 32), iv);
|
|
175
|
-
decipher.setAuthTag(tag);
|
|
176
|
-
payload = JSON.parse(Buffer.concat([decipher.update(data), decipher.final()]).toString("utf8"));
|
|
177
|
-
} catch {
|
|
178
|
-
problem("\u5BC6\u7801\u9519\u8BEF\u6216\u8FC1\u79FB\u6587\u4EF6\u5DF2\u635F\u574F");
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
if (payload?.format !== FORMAT || payload.version !== 1 || !Array.isArray(payload.config?.accounts) || payload.config.accounts.length > 200 || !payload.config.routes || typeof payload.config.routes !== "object" || Object.keys(payload.config.routes).length > 500) problem("\u8FC1\u79FB\u914D\u7F6E\u683C\u5F0F\u65E0\u6548");
|
|
182
|
-
if (payload.credentials && !encrypted) problem("\u542B\u51ED\u8BC1\u7684\u8FC1\u79FB\u6587\u4EF6\u5FC5\u987B\u52A0\u5BC6");
|
|
183
|
-
if (payload.credentials && (typeof payload.credentials !== "object" || Array.isArray(payload.credentials))) problem("\u8FC1\u79FB\u51ED\u8BC1\u683C\u5F0F\u65E0\u6548");
|
|
184
|
-
const config = settings(payload.config);
|
|
185
|
-
validateConfig({ ...config, accounts: config.accounts.map((a) => ({ ...a, credentialFile: "placeholder.json" })) });
|
|
186
|
-
if (payload.credentials && Object.keys(payload.credentials).some((id) => !config.accounts.some((a) => a.id === id))) problem("\u8FC1\u79FB\u51ED\u8BC1\u5B58\u5728\u672A\u77E5\u8D26\u53F7");
|
|
187
|
-
return { config, credentials: payload.credentials ?? {}, encrypted };
|
|
188
|
-
}
|
|
189
|
-
function migrationSummary(manager, portable, mode) {
|
|
190
|
-
if (!["merge", "replace"].includes(mode)) problem("\u8BF7\u9009\u62E9\u5408\u5E76\u6216\u66FF\u6362");
|
|
191
|
-
for (const a of portable.config.accounts) {
|
|
192
|
-
const old = manager.config.accounts.find((b) => b.id === a.id);
|
|
193
|
-
if (mode === "merge" && old && old.provider !== a.provider) problem(`\u8D26\u53F7 ${a.id} \u7684\u4F9B\u5E94\u5546\u51B2\u7A81\uFF0C\u8BF7\u6539\u7528\u66FF\u6362\u6216\u4FEE\u6539\u5BFC\u5165\u6587\u4EF6`);
|
|
194
|
-
}
|
|
195
|
-
const existing = (id) => mode === "merge" && manager.config.accounts.some((a) => a.id === id);
|
|
196
|
-
return {
|
|
197
|
-
mode,
|
|
198
|
-
accounts: portable.config.accounts.length,
|
|
199
|
-
newAccounts: portable.config.accounts.filter((a) => !existing(a.id)).length,
|
|
200
|
-
credentials: Object.keys(portable.credentials).length,
|
|
201
|
-
needsLogin: portable.config.accounts.filter((a) => !Object.hasOwn(portable.credentials, a.id) && !existing(a.id)).length,
|
|
202
|
-
routes: Object.keys(portable.config.routes).length,
|
|
203
|
-
overwrittenRoutes: Object.keys(portable.config.routes).filter((id) => Object.hasOwn(manager.config.routes, id)).length
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
async function prepareMigration(manager, raw, portable, mode) {
|
|
207
|
-
migrationSummary(manager, portable, mode);
|
|
208
|
-
const base = join2(dirname2(manager.file), "accounts");
|
|
209
|
-
await mkdir(base, { recursive: true, mode: 448 });
|
|
210
|
-
const staging = await mkdtemp(join2(base, "migration-"));
|
|
211
|
-
try {
|
|
212
|
-
const imported = [];
|
|
213
|
-
for (const a of portable.config.accounts) {
|
|
214
|
-
const old = mode === "merge" && raw.accounts.find((b) => b.id === a.id), secret = Object.hasOwn(portable.credentials, a.id) ? portable.credentials[a.id] : void 0;
|
|
215
|
-
if (!secret && old) {
|
|
216
|
-
imported.push({ ...old, ...a });
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
const credentialFile = join2(staging, a.id, "credential.json"), identityFile = join2(staging, a.id, "identity.json");
|
|
220
|
-
if (secret) await importAccount({ provider: a.provider, credentialFile, identityFile, document: secret.document, identityDocument: secret.identity });
|
|
221
|
-
else {
|
|
222
|
-
await writePrivateJson(credentialFile, {});
|
|
223
|
-
if (a.provider.startsWith("trae")) await writePrivateJson(identityFile, {});
|
|
224
|
-
}
|
|
225
|
-
imported.push({
|
|
226
|
-
...a,
|
|
227
|
-
...!secret ? { enabled: false } : {},
|
|
228
|
-
credentialFile: relative(dirname2(manager.file), credentialFile),
|
|
229
|
-
...a.provider.startsWith("trae") ? { identityFile: relative(dirname2(manager.file), identityFile) } : {}
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
const next = mode === "replace" ? { ...portable.config, accounts: imported } : {
|
|
233
|
-
...raw,
|
|
234
|
-
...pick(portable.config, options),
|
|
235
|
-
...raw.seenRoutePresets || portable.config.seenRoutePresets ? { seenRoutePresets: [.../* @__PURE__ */ new Set([...raw.seenRoutePresets ?? [], ...portable.config.seenRoutePresets ?? []])] } : {},
|
|
236
|
-
accounts: [...raw.accounts.map((a) => imported.find((b) => b.id === a.id) ?? a), ...imported.filter((a) => !raw.accounts.some((b) => b.id === a.id))],
|
|
237
|
-
routes: { ...raw.routes, ...portable.config.routes },
|
|
238
|
-
models: Object.fromEntries([.../* @__PURE__ */ new Set([...Object.keys(raw.models ?? {}), ...Object.keys(portable.config.models)])].map((p) => [p, { ...raw.models?.[p], ...portable.config.models[p] }]))
|
|
239
|
-
};
|
|
240
|
-
validateConfig(next, manager.file);
|
|
241
|
-
return { raw: next, staging };
|
|
242
|
-
} catch (error) {
|
|
243
|
-
await rm(staging, { recursive: true, force: true });
|
|
244
|
-
throw error;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// src/plugin-updates.mjs
|
|
249
|
-
import { readFile as readFile2, stat, rm as rm2 } from "node:fs/promises";
|
|
250
|
-
import { join as join4 } from "node:path";
|
|
251
|
-
import { randomUUID } from "node:crypto";
|
|
252
|
-
import { createRequire } from "node:module";
|
|
253
|
-
import { pathToFileURL } from "node:url";
|
|
254
|
-
import { parse as parseYaml, parseDocument, isMap, isSeq, isScalar } from "yaml";
|
|
255
|
-
import { withFileLock, writeFileAtomic as writeFileAtomic2 } from "@deepseek-ai/dsh-atomic-write";
|
|
256
|
-
|
|
257
|
-
// src/plugin-reload.mjs
|
|
258
|
-
import { readFile } from "node:fs/promises";
|
|
259
|
-
import { watch } from "node:fs";
|
|
260
|
-
import { join as join3 } from "node:path";
|
|
261
|
-
import { parse } from "yaml";
|
|
262
|
-
function outsideActivation(hmr, operation) {
|
|
263
|
-
const scope = hmr?.executing;
|
|
264
|
-
return typeof scope?.exit === "function" ? scope.exit(operation) : operation();
|
|
265
|
-
}
|
|
266
|
-
var PluginReload = class {
|
|
267
|
-
constructor({ service, profile, canReload, hmr = () => void 0, busy = () => false, now = Date.now, runningVersion }) {
|
|
268
|
-
Object.assign(this, { service, profile, canReload, hmr, busy, now, runningVersion });
|
|
269
|
-
this.phase = "current";
|
|
270
|
-
this.installedVersion = runningVersion;
|
|
271
|
-
this.retryAt = 0;
|
|
272
|
-
}
|
|
273
|
-
state() {
|
|
274
|
-
return { runningVersion: this.runningVersion, installedVersion: this.installedVersion, phase: this.phase, canReload: !!this.canReload(), message: this.message };
|
|
275
|
-
}
|
|
276
|
-
start() {
|
|
277
|
-
const dir = this.profile()?.dir;
|
|
278
|
-
this.watchers = [];
|
|
279
|
-
if (dir) for (const target of [dir, join3(dir, "node_modules")]) try {
|
|
280
|
-
const watcher = watch(target, () => this.schedule());
|
|
281
|
-
watcher.unref();
|
|
282
|
-
this.watchers.push(watcher);
|
|
283
|
-
} catch {
|
|
284
|
-
}
|
|
285
|
-
this.schedule();
|
|
286
|
-
}
|
|
287
|
-
schedule(delay = 500) {
|
|
288
|
-
if (this.closed) return;
|
|
289
|
-
clearTimeout(this.timer);
|
|
290
|
-
this.timer = setTimeout(() => {
|
|
291
|
-
void this.check().catch(() => {
|
|
292
|
-
});
|
|
293
|
-
}, delay);
|
|
294
|
-
this.timer.unref();
|
|
295
|
-
}
|
|
296
|
-
stop() {
|
|
297
|
-
this.closed = true;
|
|
298
|
-
clearTimeout(this.timer);
|
|
299
|
-
for (const watcher of this.watchers ?? []) watcher.close();
|
|
300
|
-
}
|
|
301
|
-
check() {
|
|
302
|
-
if (this.closed) return Promise.resolve();
|
|
303
|
-
if (this.flight) return this.flight;
|
|
304
|
-
this.flight = this.inspect().finally(() => {
|
|
305
|
-
this.flight = void 0;
|
|
306
|
-
});
|
|
307
|
-
return this.flight;
|
|
308
|
-
}
|
|
309
|
-
async inspect() {
|
|
310
|
-
const dir = this.profile()?.dir;
|
|
311
|
-
if (!dir) return;
|
|
312
|
-
let manifest, installed;
|
|
313
|
-
try {
|
|
314
|
-
manifest = JSON.parse(await readFile(join3(dir, "package.json"), "utf8"));
|
|
315
|
-
installed = JSON.parse(await readFile(join3(dir, "node_modules/dsh-multi-router/package.json"), "utf8"));
|
|
316
|
-
} catch {
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
if (this.closed || !manifest.dsh?.profile?.bundles?.includes("dsh-multi-router")) return;
|
|
320
|
-
if (installed.name !== "dsh-multi-router" || !/^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(installed.version)) return;
|
|
321
|
-
this.installedVersion = installed.version;
|
|
322
|
-
if (installed.version === this.runningVersion) {
|
|
323
|
-
this.phase = "current";
|
|
324
|
-
this.message = void 0;
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
const declared = manifest.dependencies?.["dsh-multi-router"];
|
|
328
|
-
const exact = typeof declared === "string" && /^(?:npm:dsh-multi-router@)?(\d+\.\d+\.\d+(?:-[\w.-]+)?)$/.exec(declared)?.[1];
|
|
329
|
-
let installing = false;
|
|
330
|
-
const pid = Number(await readFile(join3(dir, "package.json.lock"), "utf8").catch(() => ""));
|
|
331
|
-
if (Number.isInteger(pid) && pid > 0) try {
|
|
332
|
-
process.kill(pid, 0);
|
|
333
|
-
installing = true;
|
|
334
|
-
} catch (e) {
|
|
335
|
-
installing = e.code !== "ESRCH";
|
|
336
|
-
}
|
|
337
|
-
if (installing || exact && exact !== installed.version) {
|
|
338
|
-
this.phase = "waiting";
|
|
339
|
-
this.message = "\u5B89\u88C5\u4E8B\u52A1\u5C1A\u672A\u5B8C\u6210\uFF0C\u6216\u5B89\u88C5\u5931\u8D25\u5DF2\u56DE\u6EDA\uFF1B\u7B49\u5F85\u914D\u7F6E\u4E0E\u6587\u4EF6\u7248\u672C\u4E00\u81F4\u540E\u518D\u52A0\u8F7D\u3002";
|
|
340
|
-
if (installing) this.schedule(1e3);
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
if (!this.canReload() || !this.service()?.setBundleEnabled) {
|
|
344
|
-
this.phase = "restart-required";
|
|
345
|
-
this.message = "\u5F53\u524D DSH \u672A\u63D0\u4F9B\u8FD0\u884C\u4E2D\u91CD\u8F7D\uFF0C\u8BF7\u5347\u7EA7 DSH \u6216\u91CD\u542F\u5E94\u7528\u3002";
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
if (this.busy()) {
|
|
349
|
-
this.phase = "waiting";
|
|
350
|
-
this.message = "\u65B0\u7248\u672C\u5DF2\u5B89\u88C5\uFF0C\u7B49\u5F85\u5F53\u524D\u8BF7\u6C42\u548C\u8D26\u53F7\u64CD\u4F5C\u7ED3\u675F\u540E\u81EA\u52A8\u751F\u6548\u3002";
|
|
351
|
-
this.schedule(1e3);
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
const root = join3(dir, "node_modules/dsh-multi-router");
|
|
355
|
-
try {
|
|
356
|
-
const patch = parse(await readFile(join3(root, "cordis.patch.yml"), "utf8"));
|
|
357
|
-
const row = Array.isArray(patch) && patch.flatMap((p) => p?.insert ?? []).find((e) => e?.id === `llm-multi-router-v2-${installed.version}`);
|
|
358
|
-
if (row?.name !== `./lib/host-v${installed.version}.js` || row.disabled) throw Error("legacy entry");
|
|
359
|
-
await readFile(join3(root, `lib/host-v${installed.version}.js`));
|
|
360
|
-
} catch {
|
|
361
|
-
this.phase = "restart-required";
|
|
362
|
-
this.message = "\u5B89\u88C5\u7248\u672C\u7F3A\u5C11\u7248\u672C\u5316\u5165\u53E3\uFF0C\u91CD\u65B0\u5B89\u88C5\u6700\u65B0\u7248\u540E\u518D\u8BD5\u3002";
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
if (this.now() < this.retryAt) return;
|
|
366
|
-
this.phase = "reloading";
|
|
367
|
-
this.message = "\u6B63\u5728\u91CD\u65B0\u52A0\u8F7D\u63D2\u4EF6\u4E0E\u914D\u7F6E\u9762\u677F\u2026";
|
|
368
|
-
this.retryAt = this.now() + 6e4;
|
|
369
|
-
try {
|
|
370
|
-
const result = await outsideActivation(this.hmr(), () => this.service().setBundleEnabled("dsh-multi-router", true));
|
|
371
|
-
if (this.closed) return;
|
|
372
|
-
this.phase = "restart-required";
|
|
373
|
-
this.message = result.application === "applied" ? "DSH \u5C1A\u672A\u5207\u6362\u5230\u65B0\u7248\u672C\uFF0C\u8BF7\u91CD\u65B0\u52A0\u8F7D\u63D2\u4EF6\u6216\u67E5\u770B\u7EC4\u4EF6\u9519\u8BEF\u3002" : "DSH \u672A\u80FD\u91CD\u8F7D\u63D2\u4EF6\uFF0C\u8BF7\u5347\u7EA7 DSH \u6216\u91CD\u542F\u5E94\u7528\u3002";
|
|
374
|
-
} catch {
|
|
375
|
-
if (!this.closed) {
|
|
376
|
-
this.phase = "failed";
|
|
377
|
-
this.message = "\u63D2\u4EF6\u91CD\u8F7D\u672A\u5B8C\u6210\uFF0C\u53EF\u7A0D\u540E\u91CD\u8BD5\uFF1B\u5DF2\u4FDD\u5B58\u7684\u914D\u7F6E\u4FDD\u6301\u4E0D\u53D8\u3002";
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
// upstream/workbuddy/src/update.ts
|
|
384
|
-
var WORKBUDDY_REPOSITORY_URL = "https://github.com/corrinehu/dsh-workbuddy-connect";
|
|
385
|
-
var WORKBUDDY_UPDATE_PACKAGE_NAME = "dsh-workbuddy-connect";
|
|
386
|
-
var WORKBUDDY_UPDATE_NPM_METADATA_URL = `https://registry.npmjs.org/-/package/${WORKBUDDY_UPDATE_PACKAGE_NAME}/dist-tags`;
|
|
387
|
-
var WORKBUDDY_UPDATE_RELEASES_API_URL = `${WORKBUDDY_REPOSITORY_URL.replace("https://github.com", "https://api.github.com/repos")}/releases?per_page=100`;
|
|
388
|
-
var WORKBUDDY_RELEASE_PAGE_BASE = `${WORKBUDDY_REPOSITORY_URL}/releases/tag/`;
|
|
389
|
-
var WORKBUDDY_UPDATE_MAX_METADATA_BYTES = 64 * 1024;
|
|
390
|
-
var WORKBUDDY_UPDATE_MAX_RELEASES_BYTES = 1024 * 1024;
|
|
391
|
-
function parseWorkBuddyVersion(raw) {
|
|
392
|
-
if (typeof raw !== "string") return void 0;
|
|
393
|
-
const normalized = raw.startsWith("v") ? raw.slice(1) : raw;
|
|
394
|
-
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u.exec(normalized);
|
|
395
|
-
if (match === null) return void 0;
|
|
396
|
-
const rawPrerelease = match[4] === void 0 ? [] : match[4].split(".");
|
|
397
|
-
if (rawPrerelease.some((identifier) => /^\d+$/u.test(identifier) && !/^(0|[1-9]\d*)$/u.test(identifier))) return void 0;
|
|
398
|
-
const prerelease = rawPrerelease.map((identifier) => /^(0|[1-9]\d*)$/u.test(identifier) ? Number(identifier) : identifier);
|
|
399
|
-
if (prerelease.some((identifier) => typeof identifier === "number" && !Number.isSafeInteger(identifier))) return void 0;
|
|
400
|
-
const parsed = {
|
|
401
|
-
major: Number(match[1]),
|
|
402
|
-
minor: Number(match[2]),
|
|
403
|
-
patch: Number(match[3]),
|
|
404
|
-
prerelease
|
|
405
|
-
};
|
|
406
|
-
return [parsed.major, parsed.minor, parsed.patch].every(Number.isSafeInteger) ? parsed : void 0;
|
|
407
|
-
}
|
|
408
|
-
function compareIdentifiers(left, right) {
|
|
409
|
-
if (typeof left === "number" && typeof right === "number") return left < right ? -1 : left > right ? 1 : 0;
|
|
410
|
-
if (typeof left === "number") return -1;
|
|
411
|
-
if (typeof right === "number") return 1;
|
|
412
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
413
|
-
}
|
|
414
|
-
function compareWorkBuddyVersions(left, right) {
|
|
415
|
-
const a = parseWorkBuddyVersion(left);
|
|
416
|
-
const b = parseWorkBuddyVersion(right);
|
|
417
|
-
if (a === void 0 || b === void 0) throw new TypeError("invalid WorkBuddy Connect version");
|
|
418
|
-
for (const [aPart, bPart] of [[a.major, b.major], [a.minor, b.minor], [a.patch, b.patch]]) {
|
|
419
|
-
if (aPart !== bPart) return aPart < bPart ? -1 : 1;
|
|
420
|
-
}
|
|
421
|
-
if (a.prerelease.length === 0 && b.prerelease.length !== 0) return 1;
|
|
422
|
-
if (a.prerelease.length !== 0 && b.prerelease.length === 0) return -1;
|
|
423
|
-
for (let index = 0; index < Math.max(a.prerelease.length, b.prerelease.length); index += 1) {
|
|
424
|
-
const aPart = a.prerelease[index];
|
|
425
|
-
const bPart = b.prerelease[index];
|
|
426
|
-
if (aPart === void 0) return -1;
|
|
427
|
-
if (bPart === void 0) return 1;
|
|
428
|
-
const comparison = compareIdentifiers(aPart, bPart);
|
|
429
|
-
if (comparison !== 0) return comparison;
|
|
430
|
-
}
|
|
431
|
-
return 0;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// src/plugin-updates.mjs
|
|
435
|
-
var packageName = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
|
|
436
|
-
var safeRef = /^[a-zA-Z0-9._/-]{1,200}$/;
|
|
437
|
-
function parsePluginSource(name, spec) {
|
|
438
|
-
if (!packageName.test(name) || typeof spec !== "string" || spec.length > 600 || spec.startsWith("-") || spec.trim() !== spec || /[\x00-\x1f]/.test(spec)) return { kind: "unsupported" };
|
|
439
|
-
const git = /^(?:github:|git\+https:\/\/github\.com\/|https:\/\/github\.com\/|git\+ssh:\/\/git@github\.com\/|git@github\.com:)?([\w.-]+)\/([\w.-]+?)(?:\.git)?(?:#(.+))?$/.exec(spec);
|
|
440
|
-
if (git && !spec.startsWith("@") && !spec.startsWith("file:") && !spec.startsWith("link:")) {
|
|
441
|
-
const [, owner, repo, ref] = git;
|
|
442
|
-
if (owner === "." || owner === ".." || repo === "." || repo === ".." || ref && (!safeRef.test(ref) || ref.includes(".."))) return { kind: "unsupported" };
|
|
443
|
-
return { kind: "github", owner, repo, ref, source: `github:${owner}/${repo}${ref ? "#" + ref : ""}` };
|
|
444
|
-
}
|
|
445
|
-
if (/^(?:file:|link:|workspace:|https?:|git|\/|\.)/.test(spec)) return { kind: "unsupported" };
|
|
446
|
-
const alias = /^npm:((?:@[^/]+\/)?[^@]+)(?:@(.+))?$/.exec(spec);
|
|
447
|
-
const actual = alias?.[1] ?? name, range = alias?.[2] ?? spec;
|
|
448
|
-
if (!packageName.test(actual) || !/^[a-zA-Z0-9.*^~<>=|+ -]{1,160}$/.test(range)) return { kind: "unsupported" };
|
|
449
|
-
const channel = /^[a-zA-Z][\w.-]*$/.test(range);
|
|
450
|
-
return { kind: "npm", package: actual, alias: !!alias, channel, tag: channel ? range : "latest", source: spec };
|
|
451
|
-
}
|
|
452
|
-
function githubCommit(value) {
|
|
453
|
-
if (typeof value !== "string") return void 0;
|
|
454
|
-
return /(?:\/tar\.gz\/|\/tarball\/|#)([a-f0-9]{40})(?:\b|\()/i.exec(value)?.[1]?.toLowerCase();
|
|
455
|
-
}
|
|
456
|
-
async function jsonFile(file) {
|
|
457
|
-
try {
|
|
458
|
-
return JSON.parse(await readFile2(file, "utf8"));
|
|
459
|
-
} catch {
|
|
460
|
-
return void 0;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
async function locks(dir) {
|
|
464
|
-
let pnpm;
|
|
465
|
-
try {
|
|
466
|
-
pnpm = parseYaml(await readFile2(join4(dir, "pnpm-lock.yaml"), "utf8"), { maxAliasCount: 20 });
|
|
467
|
-
} catch {
|
|
468
|
-
}
|
|
469
|
-
const npm = await jsonFile(join4(dir, "package-lock.json"));
|
|
470
|
-
const times = await Promise.all(["pnpm-lock.yaml", "package-lock.json"].map((name) => stat(join4(dir, name)).then((s) => s.mtimeMs).catch(() => 0)));
|
|
471
|
-
return { pnpm, npm, preferNpm: times[1] > times[0] };
|
|
472
|
-
}
|
|
473
|
-
async function publicJson(url, fetchImpl, signal) {
|
|
474
|
-
const res = await fetchImpl(url, {
|
|
475
|
-
signal: AbortSignal.any([signal, AbortSignal.timeout(12e3)]),
|
|
476
|
-
// Request only the SHA: the default JSON includes file patches and can be megabytes.
|
|
477
|
-
headers: { Accept: "application/vnd.github.sha", "User-Agent": "dsh-multi-router-plugin-updates" },
|
|
478
|
-
redirect: "error"
|
|
479
|
-
});
|
|
480
|
-
if (!res.ok) throw Error(res.status === 403 || res.status === 429 ? "\u8FDC\u7AEF\u9650\u6D41\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5" : res.status === 404 ? "\u4ED3\u5E93 / \u7248\u672C\u4E0D\u5B58\u5728\uFF0C\u6216\u9700\u8981\u79C1\u6709\u4ED3\u5E93\u6388\u6743" : `\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25\uFF08HTTP ${res.status}\uFF09`);
|
|
481
|
-
let size = 0, chunks = [];
|
|
482
|
-
for await (const chunk of res.body) {
|
|
483
|
-
size += chunk.byteLength;
|
|
484
|
-
if (size > 256 * 1024) throw Error("\u66F4\u65B0\u5143\u6570\u636E\u8FC7\u5927");
|
|
485
|
-
chunks.push(Buffer.from(chunk));
|
|
486
|
-
}
|
|
487
|
-
const text = Buffer.concat(chunks).toString("utf8").trim();
|
|
488
|
-
return /^[a-f0-9]{40}$/i.test(text) ? { sha: text } : JSON.parse(text);
|
|
489
|
-
}
|
|
490
|
-
var short = (sha) => sha?.slice(0, 8);
|
|
491
|
-
var reviewedVersion = (row) => row.source.kind === "npm" && parseWorkBuddyVersion(row.latestVersion) ? `${row.source.package}@${row.latestVersion}` : void 0;
|
|
492
|
-
async function allowReviewedVersion(profile, row) {
|
|
493
|
-
const file = join4(profile.dir, "pnpm-workspace.yaml"), prefix = `${row.source.package}@`;
|
|
494
|
-
try {
|
|
495
|
-
await withFileLock(file, async () => {
|
|
496
|
-
let text;
|
|
497
|
-
try {
|
|
498
|
-
text = await readFile2(file, "utf8");
|
|
499
|
-
} catch (e) {
|
|
500
|
-
if (e.code !== "ENOENT") throw e;
|
|
501
|
-
text = "{}\n";
|
|
502
|
-
}
|
|
503
|
-
const doc = parseDocument(text), entries = doc.get("minimumReleaseAgeExclude");
|
|
504
|
-
if (doc.errors.length || !isMap(doc.contents) || entries !== void 0 && (!isSeq(entries) || entries.anchor || entries.items.some((x) => !isScalar(x) || typeof x.value !== "string" || x.anchor))) throw Error("invalid-policy");
|
|
505
|
-
const values = entries?.items.map((x) => x.value) ?? [];
|
|
506
|
-
const existing = values.filter((x) => x.startsWith(prefix));
|
|
507
|
-
const versions = [.../* @__PURE__ */ new Set([...existing.flatMap((x) => x.slice(prefix.length).split(/\s*\|\|\s*/)), row.latestVersion])];
|
|
508
|
-
const merged = prefix + versions.join(" || ");
|
|
509
|
-
if (existing.length === 1 && existing[0] === merged) return;
|
|
510
|
-
if (!entries) doc.set("minimumReleaseAgeExclude", [merged]);
|
|
511
|
-
else {
|
|
512
|
-
const first = entries.items.findIndex((x) => x.value.startsWith(prefix));
|
|
513
|
-
if (first < 0) entries.add(merged);
|
|
514
|
-
else {
|
|
515
|
-
entries.items[first].value = merged;
|
|
516
|
-
for (let i = entries.items.length - 1; i > first; i--) if (entries.items[i].value.startsWith(prefix)) entries.delete(i);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
await writeFileAtomic2(file, String(doc), { mode: 384 });
|
|
520
|
-
}, { waitMs: 12e4 });
|
|
521
|
-
} catch {
|
|
522
|
-
throw Error("\u65E0\u6CD5\u4FDD\u5B58\u6B64\u7248\u672C\u7684\u7B49\u5F85\u671F\u4F8B\u5916\u914D\u7F6E\uFF1B\u8BF7\u68C0\u67E5\u5F53\u524D DSH \u914D\u7F6E\u76EE\u5F55\u6743\u9650\u53CA pnpm-workspace.yaml \u683C\u5F0F\u3002\u672A\u5173\u95ED\u7B49\u5F85\u671F\u4FDD\u62A4\u3002");
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
var installFailed = (r) => !!(r.error || ["failed", "cancelled"].includes(r.application) || r.packageResult && r.packageResult.exitCode !== 0);
|
|
526
|
-
function installFailure(result) {
|
|
527
|
-
const text = [result.error?.diagnostic, result.packageResult?.output, result.packageResult?.kind].filter((v) => typeof v === "string").join("\n");
|
|
528
|
-
let reason = "unknown", message = "\u5B89\u88C5\u672A\u5B8C\u6210\uFF1B\u8BF7\u5728 DSH \u539F\u751F\u63D2\u4EF6\u9875\u67E5\u770B\u5B89\u88C5\u65E5\u5FD7\uFF0C\u6216\u590D\u5236\u4E0B\u65B9\u8BCA\u65AD\u4FE1\u606F\u3002";
|
|
529
|
-
if (result.application === "cancelled") {
|
|
530
|
-
reason = "cancelled";
|
|
531
|
-
message = "\u5B89\u88C5\u5DF2\u53D6\u6D88\uFF0C\u53EF\u91CD\u65B0\u68C0\u67E5\u540E\u91CD\u8BD5\u3002";
|
|
532
|
-
} else if (/ERR_PNPM_(?:MINIMUM_RELEASE_AGE_VIOLATION|NO_MATURE_MATCHING_VERSION)|minimumReleaseAge/i.test(text)) {
|
|
533
|
-
reason = "release-age";
|
|
534
|
-
message = "\u65B0\u5305\u5C1A\u5728\u5B89\u88C5\u5668\u7B49\u5F85\u671F\u5185\u3002\u53EF\u5728\u300C\u67E5\u770B\u5E76\u66F4\u65B0\u300D\u4E2D\u52FE\u9009\u300C\u5141\u8BB8\u672C\u6B21\u6307\u5B9A\u7248\u672C\u7ACB\u5373\u5B89\u88C5\u300D\u540E\u91CD\u8BD5\uFF1B\u53EA\u653E\u884C\u786E\u8BA4\u7684\u63D2\u4EF6\u7248\u672C\uFF0C\u4F9D\u8D56\u4ECD\u9700\u6EE1\u8DB3\u7B49\u5F85\u671F\u3002\u4E5F\u53EF\u7B49\u5F85\u5230\u671F\u518D\u5B89\u88C5\u3002";
|
|
535
|
-
} else if (result.pendingBuilds?.length) {
|
|
536
|
-
reason = "build";
|
|
537
|
-
message = "\u4F9D\u8D56\u6784\u5EFA\u811A\u672C\u9700\u8981\u6388\u6743\uFF0C\u8BF7\u5230 DSH \u63D2\u4EF6\u5B89\u88C5\u9875\u5141\u8BB8\u8FD9\u4E9B\u811A\u672C\u540E\u91CD\u8BD5\u3002";
|
|
538
|
-
} else if (result.error?.code === "incompatible-version") {
|
|
539
|
-
reason = "compatibility";
|
|
540
|
-
message = "\u63D2\u4EF6\u4E0E\u5F53\u524D DSH \u4E0D\u517C\u5BB9\uFF0C\u8BF7\u5148\u5347\u7EA7 DSH \u6216\u9009\u62E9\u517C\u5BB9\u7248\u672C\u3002";
|
|
541
|
-
} else if (/\b(?:EPERM|EBUSY|EACCES)\b|resource busy|file.*(?:locked|in use)/i.test(text)) {
|
|
542
|
-
reason = "files";
|
|
543
|
-
message = "\u63D2\u4EF6\u6587\u4EF6\u88AB\u5360\u7528\u6216\u76EE\u5F55\u6743\u9650\u4E0D\u8DB3\uFF0C\u5B89\u88C5\u672A\u5B8C\u6210\u3002\u8BF7\u5148\u5728 DSH \u505C\u7528\u8BE5\u63D2\u4EF6\u540E\u91CD\u8BD5\uFF0C\u5E76\u68C0\u67E5\u5B89\u88C5\u76EE\u5F55\u6743\u9650\uFF1B\u4ECD\u88AB\u5360\u7528\u65F6\u9700\u9000\u51FA\u6258\u76D8\u5E94\u7528\u3002";
|
|
544
|
-
} else if (/HMR transactions cannot be nested|activation|did not activate|entry.*activate/i.test(text)) {
|
|
545
|
-
reason = "activation";
|
|
546
|
-
message = "\u63D2\u4EF6\u52A0\u8F7D\u5931\u8D25\u3002\u8BF7\u68C0\u67E5 DSH \u63D2\u4EF6\u9875\u7684\u7EC4\u4EF6\u9519\u8BEF\uFF0C\u518D\u5173\u95ED\u5E76\u91CD\u65B0\u542F\u7528\uFF1B\u5B89\u88C5\u4E0E\u52A0\u8F7D\u662F\u4E24\u4E2A\u72EC\u7ACB\u6B65\u9AA4\u3002";
|
|
547
|
-
} else if (result.packageResult?.timedOut || /\b(?:ETIMEDOUT|ECONNRESET|ENOTFOUND|EAI_AGAIN|ECONNREFUSED)\b|network|fetch|download/i.test(text)) {
|
|
548
|
-
reason = "network";
|
|
549
|
-
message = "\u63D2\u4EF6\u4E0B\u8F7D\u6216\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25\uFF0C\u5B89\u88C5\u672A\u5B8C\u6210\u3002npm \u6765\u6E90\u53EF\u5207\u6362\u5230\u300Cnpm \u5B98\u65B9\u300D\u540E\u91CD\u8BD5\uFF1BGitHub \u6765\u6E90\u8BF7\u68C0\u67E5 GitHub \u7F51\u7EDC\u8FDE\u63A5\u3002";
|
|
550
|
-
}
|
|
551
|
-
const code = /\b(?:ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION|ERR_PNPM_NO_MATURE_MATCHING_VERSION|EPERM|EBUSY|EACCES|ETIMEDOUT|ECONNRESET|ENOTFOUND|EAI_AGAIN|ECONNREFUSED)\b/.exec(text)?.[0] ?? (["operation-error", "incompatible-version"].includes(result.error?.code) ? result.error.code : "unknown");
|
|
552
|
-
return { status: "failed", application: result.application, message, diagnostic: {
|
|
553
|
-
reason,
|
|
554
|
-
code,
|
|
555
|
-
platform: process.platform,
|
|
556
|
-
stage: reason === "activation" ? "activation" : "install",
|
|
557
|
-
...Number.isInteger(result.packageResult?.exitCode) ? { exitCode: result.packageResult.exitCode } : {}
|
|
558
|
-
} };
|
|
559
|
-
}
|
|
560
|
-
async function registryVersion(service, profile, spec, signal, requestedRegistry) {
|
|
561
|
-
const require2 = createRequire(profile.installAnchor);
|
|
562
|
-
const { viewProfilePackage } = await import(pathToFileURL(require2.resolve("@deepseek-ai/dsh-plugin-manager/operations")).href);
|
|
563
|
-
const { registryPlan } = await import(pathToFileURL(require2.resolve("@deepseek-ai/dsh-plugin-manager/registry")).href);
|
|
564
|
-
for (const registry of registryPlan(requestedRegistry, await service.registries())) {
|
|
565
|
-
const answer = await viewProfilePackage(profile.dir, spec, { ...profile.packageManager, registry, timeoutMs: 12e3, signal });
|
|
566
|
-
if (signal.aborted) throw Error("\u68C0\u67E5\u5DF2\u53D6\u6D88");
|
|
567
|
-
if (answer.exitCode !== 0) continue;
|
|
568
|
-
const parsed = JSON.parse(answer.stdout), metadata = Array.isArray(parsed) ? parsed.at(-1) : parsed;
|
|
569
|
-
if (metadata?.version) return metadata;
|
|
570
|
-
}
|
|
571
|
-
throw Error("npm \u67E5\u8BE2\u5931\u8D25\uFF1B\u8BF7\u68C0\u67E5 DSH \u7F51\u7EDC\u3001registry \u8BBE\u7F6E\u6216\u5305\u7684\u8BBF\u95EE\u6743\u9650");
|
|
572
|
-
}
|
|
573
|
-
async function refreshProfilePlugin(service, profile, row, options2, signal) {
|
|
574
|
-
const require2 = createRequire(profile.installAnchor);
|
|
575
|
-
const { runProfilePnpm } = await import(pathToFileURL(require2.resolve("@deepseek-ai/dsh-plugin-manager/operations")).href);
|
|
576
|
-
const { registryPlan, attributeFailure } = await import(pathToFileURL(require2.resolve("@deepseek-ai/dsh-plugin-manager/registry")).href);
|
|
577
|
-
const { classifyInstallFailure } = await import(pathToFileURL(require2.resolve("@deepseek-ai/dsh-plugin-manager")).href);
|
|
578
|
-
const plan = registryPlan(options2.registry, await service.registries());
|
|
579
|
-
return withFileLock(join4(profile.dir, "package.json"), async () => {
|
|
580
|
-
const files = await Promise.all(["package.json", "pnpm-lock.yaml"].map(async (name) => {
|
|
581
|
-
const path = join4(profile.dir, name);
|
|
582
|
-
try {
|
|
583
|
-
return { path, content: await readFile2(path, "utf8") };
|
|
584
|
-
} catch (e) {
|
|
585
|
-
if (e.code !== "ENOENT") throw e;
|
|
586
|
-
return { path };
|
|
587
|
-
}
|
|
588
|
-
}));
|
|
589
|
-
const restore = () => Promise.all(files.map((f) => f.content === void 0 ? rm2(f.path, { force: true }) : writeFileAtomic2(f.path, f.content, { mode: 384 })));
|
|
590
|
-
if (JSON.parse(files[0].content).dependencies?.[row.name] !== row.spec) throw Error("\u5B89\u88C5\u6765\u6E90\u5DF2\u6539\u53D8\uFF0C\u8BF7\u91CD\u65B0\u68C0\u67E5");
|
|
591
|
-
let result;
|
|
592
|
-
try {
|
|
593
|
-
for (const registry of plan) {
|
|
594
|
-
signal.throwIfAborted();
|
|
595
|
-
const run = await runProfilePnpm({ ...profile, profile: profile.name }, ["update", row.source.kind === "github" ? row.name : row.target, ...registry ? [`--registry=${registry}`] : []], {
|
|
596
|
-
execution: "service",
|
|
597
|
-
...profile.packageManager,
|
|
598
|
-
signal,
|
|
599
|
-
outputBytes: 16384,
|
|
600
|
-
idleTimeoutMs: 6e5,
|
|
601
|
-
lookupTimeoutMs: 2e4,
|
|
602
|
-
activateNewBundles: false
|
|
603
|
-
});
|
|
604
|
-
if (run.exitCode === 0 && !run.timedOut && !signal.aborted) return { packageResult: run, application: "restart-required" };
|
|
605
|
-
const kind = classifyInstallFailure({ log: run.output, timedOut: run.timedOut });
|
|
606
|
-
result = {
|
|
607
|
-
packageResult: { ...run, kind },
|
|
608
|
-
application: signal.aborted ? "cancelled" : "failed",
|
|
609
|
-
...run.incompatible?.length ? { error: { code: "incompatible-version" } } : {}
|
|
610
|
-
};
|
|
611
|
-
await restore();
|
|
612
|
-
if (signal.aborted || attributeFailure(kind, run.output, row.source.kind === "github" ? { kind: "git", host: "github.com" } : { kind: "npm" }) !== "registry") break;
|
|
613
|
-
}
|
|
614
|
-
return result;
|
|
615
|
-
} catch (e) {
|
|
616
|
-
await restore();
|
|
617
|
-
throw e;
|
|
618
|
-
}
|
|
619
|
-
}, { waitMs: 12e4 });
|
|
620
|
-
}
|
|
621
|
-
var PluginUpdates = class {
|
|
622
|
-
constructor({ service = () => void 0, profile = () => void 0, hmr = () => void 0, fetchImpl = (...args) => fetch(...args), lookup = registryVersion, refreshPackage = refreshProfilePlugin, onInstalled, stateFile, now = Date.now } = {}) {
|
|
623
|
-
this.stateFile = stateFile;
|
|
624
|
-
this.now = now;
|
|
625
|
-
this.dismissed = {};
|
|
626
|
-
this.lastAttempt = 0;
|
|
627
|
-
this.saveQueue = Promise.resolve();
|
|
628
|
-
this.service = service;
|
|
629
|
-
this.profile = profile;
|
|
630
|
-
this.hmr = hmr;
|
|
631
|
-
this.fetchImpl = fetchImpl;
|
|
632
|
-
this.rows = [];
|
|
633
|
-
this.abort = new AbortController();
|
|
634
|
-
this.lookup = lookup;
|
|
635
|
-
this.refreshPackage = refreshPackage;
|
|
636
|
-
this.onInstalled = onInstalled;
|
|
637
|
-
}
|
|
638
|
-
async load() {
|
|
639
|
-
const saved = this.stateFile && await jsonFile(this.stateFile);
|
|
640
|
-
if (saved?.version === 1) {
|
|
641
|
-
this.lastAttempt = Number.isFinite(saved.lastAttempt) ? saved.lastAttempt : 0;
|
|
642
|
-
this.checkedAt = saved.checkedAt;
|
|
643
|
-
this.dismissed = saved.dismissed && typeof saved.dismissed === "object" && !Array.isArray(saved.dismissed) ? saved.dismissed : {};
|
|
644
|
-
this.rows = Array.isArray(saved.rows) ? saved.rows.filter((r) => ["github", "npm"].includes(r.source?.kind)) : [];
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
persist() {
|
|
648
|
-
if (!this.stateFile) return Promise.resolve();
|
|
649
|
-
const saved = structuredClone({ version: 1, lastAttempt: this.lastAttempt, checkedAt: this.checkedAt, dismissed: this.dismissed, rows: this.rows.filter((r) => ["github", "npm"].includes(r.source.kind)) });
|
|
650
|
-
this.saveQueue = this.saveQueue.catch(() => {
|
|
651
|
-
}).then(() => writePrivateJson(this.stateFile, saved));
|
|
652
|
-
return this.saveQueue;
|
|
653
|
-
}
|
|
654
|
-
reminderKey(row) {
|
|
655
|
-
return JSON.stringify([row.name, row.source, row.version, row.latestVersion ?? row.latestCommit]);
|
|
656
|
-
}
|
|
657
|
-
reminders() {
|
|
658
|
-
return this.rows.filter((r) => r.status === "available" && (!Object.hasOwn(this.dismissed, r.name) || this.dismissed[r.name] !== this.reminderKey(r))).map((r) => ({ key: this.reminderKey(r), name: r.name, version: r.version, latestVersion: r.latestVersion, latestCommit: r.latestCommit }));
|
|
659
|
-
}
|
|
660
|
-
async dismiss(keys) {
|
|
661
|
-
if (!Array.isArray(keys) || keys.length > 200 || keys.some((k) => typeof k !== "string")) throw Error("\u65E0\u6548\u7684\u63D0\u9192\u5217\u8868");
|
|
662
|
-
for (const row of this.reminders()) if (keys.includes(row.key)) Object.defineProperty(this.dismissed, row.name, { value: row.key, enumerable: true, configurable: true, writable: true });
|
|
663
|
-
await this.persist();
|
|
664
|
-
}
|
|
665
|
-
async tick() {
|
|
666
|
-
if (this.flight || !this.state().available) return;
|
|
667
|
-
await this.list();
|
|
668
|
-
if (this.abort.signal.aborted) return;
|
|
669
|
-
if (this.flight || this.now() >= this.lastAttempt && this.now() - this.lastAttempt < 864e5) return;
|
|
670
|
-
this.lastAttempt = this.now();
|
|
671
|
-
await this.persist();
|
|
672
|
-
if (!this.flight && !this.abort.signal.aborted) this.check();
|
|
673
|
-
}
|
|
674
|
-
state() {
|
|
675
|
-
return {
|
|
676
|
-
available: !!this.service()?.listBundles && !!this.profile()?.dir,
|
|
677
|
-
checkedAt: this.checkedAt,
|
|
678
|
-
reminders: this.reminders(),
|
|
679
|
-
autoCheckHours: 24,
|
|
680
|
-
rows: this.rows.map(({ spec, ...row }) => ({ ...row, source: row.source.kind === "unsupported" ? { kind: "unsupported" } : row.source })),
|
|
681
|
-
job: this.job,
|
|
682
|
-
error: this.error
|
|
683
|
-
};
|
|
684
|
-
}
|
|
685
|
-
async list() {
|
|
686
|
-
const service = this.service(), profile = this.profile();
|
|
687
|
-
if (!service?.listBundles || !profile?.dir) {
|
|
688
|
-
this.rows = [];
|
|
689
|
-
return;
|
|
690
|
-
}
|
|
691
|
-
const [manifest, bundles, lock] = await Promise.all([jsonFile(join4(profile.dir, "package.json")), service.listBundles(), locks(profile.dir)]);
|
|
692
|
-
if (!manifest) throw Error("\u5F53\u524D DSH \u914D\u7F6E\u7684\u63D2\u4EF6\u6E05\u5355\u65E0\u6CD5\u8BFB\u53D6");
|
|
693
|
-
this.rows = bundles.filter((b) => (b.installed || b.version) && manifest.dependencies?.[b.name] && b.removable !== false).map((b) => {
|
|
694
|
-
const spec = manifest.dependencies?.[b.name];
|
|
695
|
-
const source = parsePluginSource(b.name, spec);
|
|
696
|
-
const npm = lock.npm?.packages?.[`node_modules/${b.name}`]?.resolved;
|
|
697
|
-
const pnpm = lock.pnpm?.importers?.["."]?.dependencies?.[b.name]?.version;
|
|
698
|
-
const installed = lock.preferNpm ? npm ?? pnpm : pnpm ?? npm;
|
|
699
|
-
const previous = this.rows.find((r) => r.name === b.name && r.spec === spec && r.version === b.version && r.commit === githubCommit(installed));
|
|
700
|
-
return {
|
|
701
|
-
...previous,
|
|
702
|
-
name: b.name,
|
|
703
|
-
version: b.version,
|
|
704
|
-
enabled: b.enabled,
|
|
705
|
-
spec,
|
|
706
|
-
source,
|
|
707
|
-
commit: githubCommit(installed),
|
|
708
|
-
status: previous?.status ?? (source.kind === "unsupported" ? "unsupported" : "unchecked"),
|
|
709
|
-
...source.kind === "unsupported" ? { message: "\u672C\u5730\u6587\u4EF6 / \u94FE\u63A5\u6765\u6E90\uFF0C\u8BF7\u901A\u8FC7\u539F\u5B89\u88C5\u65B9\u5F0F\u66F4\u65B0" } : {}
|
|
710
|
-
};
|
|
711
|
-
});
|
|
712
|
-
}
|
|
713
|
-
async checkRow(row) {
|
|
714
|
-
try {
|
|
715
|
-
if (row.source.kind === "npm") {
|
|
716
|
-
const spec = `${row.source.package}@${row.source.tag}`;
|
|
717
|
-
const found = await this.lookup(this.service(), this.profile(), spec, this.abort.signal);
|
|
718
|
-
if (!found.version || found.problem && found.problem !== "already-installed") throw Error(found.problem === "not-found" ? "npm \u4E2D\u672A\u627E\u5230\u8BE5\u5305\u6216\u9891\u9053" : "npm \u67E5\u8BE2\u5931\u8D25\uFF1B\u8BF7\u68C0\u67E5 DSH \u7684\u7F51\u7EDC\u4E0E registry \u8BBE\u7F6E");
|
|
719
|
-
if (!parseWorkBuddyVersion(found.version) || row.version && !parseWorkBuddyVersion(row.version)) throw Error("\u65E0\u6CD5\u6BD4\u8F83\u7248\u672C\u53F7");
|
|
720
|
-
row.latestVersion = found.version;
|
|
721
|
-
row.target = `${row.name}@${row.source.alias ? `npm:${row.source.package}@` : ""}${row.source.channel ? row.source.tag : found.version}`;
|
|
722
|
-
row.status = !row.version || compareWorkBuddyVersions(found.version, row.version) > 0 ? "available" : "current";
|
|
723
|
-
row.message = !row.version ? "\u5B89\u88C5\u6587\u4EF6\u4E0D\u53EF\u8BFB\uFF0C\u53EF\u91CD\u65B0\u5B89\u88C5\u8BE5\u6765\u6E90\u4FEE\u590D" : row.status === "current" ? "\u5F53\u524D\u7248\u672C\u5DF2\u662F\u8BE5\u9891\u9053\u6700\u65B0\uFF0C\u6216\u9AD8\u4E8E\u8BE5\u9891\u9053" : void 0;
|
|
724
|
-
} else if (row.source.kind === "github") {
|
|
725
|
-
const { owner, repo, ref } = row.source;
|
|
726
|
-
const url = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits/${encodeURIComponent(ref ?? "HEAD")}`;
|
|
727
|
-
const found = await publicJson(url, this.fetchImpl, this.abort.signal);
|
|
728
|
-
if (!/^[a-f0-9]{40}$/i.test(found.sha)) throw Error("GitHub \u8FD4\u56DE\u65E0\u6548\u63D0\u4EA4\u4FE1\u606F");
|
|
729
|
-
row.latestCommit = found.sha.toLowerCase();
|
|
730
|
-
row.target = `${row.name}@${row.spec}`;
|
|
731
|
-
row.status = row.commit === row.latestCommit ? "current" : "available";
|
|
732
|
-
row.message = row.commit ? row.status === "available" ? `\u63D0\u4EA4 ${short(row.commit)} \u2192 ${short(row.latestCommit)}` : "\u8BE5\u5206\u652F / \u6807\u7B7E\u7684\u63D0\u4EA4\u5DF2\u540C\u6B65" : "\u65E0\u6CD5\u8BFB\u53D6\u672C\u5730 Git \u63D0\u4EA4\uFF0C\u53EF\u91CD\u65B0\u5B89\u88C5\u5DF2\u68C0\u67E5\u7684\u6765\u6E90";
|
|
733
|
-
if (ref && /^[a-f0-9]{7,40}$/i.test(ref)) {
|
|
734
|
-
row.status = "pinned";
|
|
735
|
-
row.message = "\u5B89\u88C5\u6765\u6E90\u56FA\u5B9A\u5728 Git \u63D0\u4EA4\uFF1B\u4FDD\u7559\u6B64\u63D0\u4EA4\uFF0C\u4E0D\u81EA\u52A8\u5207\u6362\u5206\u652F";
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
row.checkedAt = Date.now();
|
|
739
|
-
row.checkedEnabled = row.enabled;
|
|
740
|
-
} catch (error) {
|
|
741
|
-
row.status = "failed";
|
|
742
|
-
row.message = error.message;
|
|
743
|
-
delete row.target;
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
start(kind, task) {
|
|
747
|
-
if (this.flight) throw Error("\u5DF2\u6709\u63D2\u4EF6\u64CD\u4F5C\u6B63\u5728\u6267\u884C\uFF0C\u8BF7\u7B49\u5F85\u5B8C\u6210");
|
|
748
|
-
this.job = { id: randomUUID(), kind, state: "running", startedAt: Date.now(), results: [], total: 0 };
|
|
749
|
-
this.error = void 0;
|
|
750
|
-
this.flight = Promise.resolve().then(() => task(this.job)).then(() => {
|
|
751
|
-
this.job.state = this.job.cancelRequested ? "cancelled" : "done";
|
|
752
|
-
}, (e) => {
|
|
753
|
-
this.job.state = this.job.cancelRequested ? "cancelled" : "failed";
|
|
754
|
-
this.job.message = e.message;
|
|
755
|
-
}).finally(async () => {
|
|
756
|
-
this.job.finishedAt = Date.now();
|
|
757
|
-
try {
|
|
758
|
-
await this.persist();
|
|
759
|
-
} catch {
|
|
760
|
-
this.error = "\u66F4\u65B0\u68C0\u67E5\u8BB0\u5F55\u672A\u80FD\u4FDD\u5B58\uFF0C\u8BF7\u68C0\u67E5\u672C\u5730\u76EE\u5F55\u6743\u9650";
|
|
761
|
-
}
|
|
762
|
-
this.flight = void 0;
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
|
-
check() {
|
|
766
|
-
this.start("check", async (job) => {
|
|
767
|
-
this.lastAttempt = this.now();
|
|
768
|
-
await this.persist();
|
|
769
|
-
await this.list();
|
|
770
|
-
const rows = this.rows.filter((r) => ["npm", "github"].includes(r.source.kind));
|
|
771
|
-
job.total = rows.length;
|
|
772
|
-
let next = 0;
|
|
773
|
-
await Promise.all(Array.from({ length: Math.min(3, rows.length) }, async () => {
|
|
774
|
-
while (next < rows.length && !job.cancelRequested) {
|
|
775
|
-
const row = rows[next++];
|
|
776
|
-
await this.checkRow(row);
|
|
777
|
-
job.results.push({ name: row.name, status: row.status, message: row.message });
|
|
778
|
-
}
|
|
779
|
-
}));
|
|
780
|
-
this.checkedAt = Date.now();
|
|
781
|
-
});
|
|
782
|
-
}
|
|
783
|
-
async installRow(row, registry, allowFreshVersions = []) {
|
|
784
|
-
const original = await jsonFile(join4(this.profile().dir, "package.json"));
|
|
785
|
-
if (original?.dependencies?.[row.name] !== row.spec) throw Error("\u5B89\u88C5\u6765\u6E90\u5DF2\u6539\u53D8\uFF0C\u8BF7\u91CD\u65B0\u68C0\u67E5");
|
|
786
|
-
const current = (await this.service().listBundles()).find((b) => b.name === row.name);
|
|
787
|
-
if (!current || current.removable === false || current.enabled !== row.checkedEnabled || current.version !== row.version) throw Error("\u63D2\u4EF6\u7248\u672C\u6216\u542F\u7528\u72B6\u6001\u5DF2\u6539\u53D8\uFF0C\u8BF7\u91CD\u65B0\u68C0\u67E5");
|
|
788
|
-
if (allowFreshVersions.includes(reviewedVersion(row))) await allowReviewedVersion(this.profile(), row);
|
|
789
|
-
const options2 = { enabled: row.enabled, requestId: this.job.id, ...registry ? { registry } : {} };
|
|
790
|
-
let result;
|
|
791
|
-
const refresh = async () => {
|
|
792
|
-
this.gitAbort = new AbortController();
|
|
793
|
-
try {
|
|
794
|
-
return await this.refreshPackage(this.service(), this.profile(), row, options2, AbortSignal.any([this.abort.signal, this.gitAbort.signal]));
|
|
795
|
-
} finally {
|
|
796
|
-
this.gitAbort = void 0;
|
|
797
|
-
}
|
|
798
|
-
};
|
|
799
|
-
if (row.source.kind === "github") result = await refresh();
|
|
800
|
-
else {
|
|
801
|
-
result = await outsideActivation(this.hmr(), () => this.service().installBundle(row.target, options2));
|
|
802
|
-
if (result.error?.code === "already-installed") result = await refresh();
|
|
803
|
-
}
|
|
804
|
-
if (installFailed(result)) return { name: row.name, ...installFailure(result), expectedVersion: row.latestVersion };
|
|
805
|
-
const installed = (await this.service().listBundles()).find((b) => b.name === row.name);
|
|
806
|
-
if (row.source.kind === "npm" && installed?.version !== row.latestVersion) return {
|
|
807
|
-
name: row.name,
|
|
808
|
-
status: "failed",
|
|
809
|
-
message: `\u5B9E\u9645\u5B89\u88C5\u7248\u672C ${installed?.version ?? "\u672A\u77E5"} \u4E0E\u68C0\u67E5\u7248\u672C ${row.latestVersion} \u4E0D\u540C\uFF1B\u8BF7\u7A0D\u540E\u91CD\u65B0\u68C0\u67E5\uFF0C\u53EF\u80FD\u662F\u955C\u50CF\u5C1A\u672A\u540C\u6B65\u6216\u53D1\u884C\u65F6\u95F4\u9650\u5236`
|
|
810
|
-
};
|
|
811
|
-
if (row.source.kind === "github") {
|
|
812
|
-
const lock = await locks(this.profile().dir);
|
|
813
|
-
const pnpm = lock.pnpm?.importers?.["."]?.dependencies?.[row.name]?.version, npm = lock.npm?.packages?.[`node_modules/${row.name}`]?.resolved;
|
|
814
|
-
if (githubCommit(lock.preferNpm ? npm ?? pnpm : pnpm ?? npm) !== row.latestCommit) return {
|
|
815
|
-
name: row.name,
|
|
816
|
-
status: "failed",
|
|
817
|
-
restartRequired: true,
|
|
818
|
-
message: "\u5B9E\u9645 Git \u63D0\u4EA4\u4E0E\u68C0\u67E5\u7ED3\u679C\u4E0D\u4E00\u81F4\uFF0C\u8BF7\u91CD\u65B0\u68C0\u67E5\u540E\u91CD\u8BD5\uFF1B\u4E0D\u628A\u4ECD\u505C\u7559\u5728\u65E7\u9501\u6587\u4EF6\u7684\u5B89\u88C5\u62A5\u544A\u4E3A\u6210\u529F"
|
|
819
|
-
};
|
|
820
|
-
}
|
|
821
|
-
const reloadPending = result.application === "restart-required" && this.onInstalled?.(row.name) === true;
|
|
822
|
-
return { name: row.name, status: "installed", application: reloadPending ? "reload-pending" : result.application, message: reloadPending ? "\u5DF2\u5B89\u88C5\uFF0C\u7B49\u5F85\u5F53\u524D\u64CD\u4F5C\u7ED3\u675F\u540E\u81EA\u52A8\u52A0\u8F7D\uFF1B\u8FD0\u884C\u7248\u672C\u89C1\u9762\u677F\u9876\u90E8" : result.application === "restart-required" ? "\u5DF2\u5B89\u88C5\uFF0C\u5B8C\u5168\u9000\u51FA\u5E76\u91CD\u65B0\u6253\u5F00 DSH \u540E\u751F\u6548" : "\u5B89\u88C5\u5B8C\u6210", restartRequired: !reloadPending && result.application === "restart-required" };
|
|
823
|
-
}
|
|
824
|
-
update(names, registry, allowFreshVersions = []) {
|
|
825
|
-
if (!Array.isArray(names) || !names.length || names.length > 200 || names.some((n) => typeof n !== "string") || new Set(names).size !== names.length) throw Error("\u8BF7\u9009\u62E9\u8981\u66F4\u65B0\u7684\u63D2\u4EF6");
|
|
826
|
-
const selected = names.map((name) => this.rows.find((r) => r.name === name));
|
|
827
|
-
if (selected.some((r) => !r || !["available", "failed"].includes(r.status) || !r.target || Date.now() - r.checkedAt > 6e5)) throw Error("\u8BF7\u5148\u68C0\u67E5\u66F4\u65B0\uFF0C\u518D\u9009\u62E9\u53EF\u66F4\u65B0\u7684\u63D2\u4EF6\uFF08\u68C0\u67E5\u7ED3\u679C\u6709\u6548\u671F 10 \u5206\u949F\uFF09");
|
|
828
|
-
const reviewed = selected.map(reviewedVersion).filter(Boolean);
|
|
829
|
-
if (!Array.isArray(allowFreshVersions) || allowFreshVersions.length > selected.length || new Set(allowFreshVersions).size !== allowFreshVersions.length || allowFreshVersions.some((v) => typeof v !== "string" || !reviewed.includes(v))) throw Error("\u786E\u8BA4\u7684\u63D2\u4EF6\u7248\u672C\u5DF2\u6539\u53D8\uFF0C\u8BF7\u91CD\u65B0\u67E5\u770B\u5E76\u786E\u8BA4\u66F4\u65B0");
|
|
830
|
-
selected.sort((a, b) => Number(a.name === "dsh-multi-router") - Number(b.name === "dsh-multi-router"));
|
|
831
|
-
this.start("update", async (job) => {
|
|
832
|
-
job.total = selected.length;
|
|
833
|
-
for (const row of selected) {
|
|
834
|
-
if (job.cancelRequested) break;
|
|
835
|
-
job.current = row.name;
|
|
836
|
-
let result;
|
|
837
|
-
try {
|
|
838
|
-
result = await this.installRow(row, registry, allowFreshVersions);
|
|
839
|
-
} catch (e) {
|
|
840
|
-
result = { name: row.name, status: "failed", message: e.message };
|
|
841
|
-
}
|
|
842
|
-
job.results.push(result);
|
|
843
|
-
row.status = result.status;
|
|
844
|
-
row.message = result.message;
|
|
845
|
-
}
|
|
846
|
-
delete job.current;
|
|
847
|
-
await this.list();
|
|
848
|
-
});
|
|
849
|
-
}
|
|
850
|
-
install(spec, registry) {
|
|
851
|
-
if (typeof spec !== "string" || spec.length > 600 || spec.startsWith("-") || /[\s\x00-\x1f]/.test(spec)) throw Error("\u8BF7\u8F93\u5165\u4E00\u4E2A npm \u5305\u540D\u6216 GitHub \u5B89\u88C5\u5730\u5740");
|
|
852
|
-
const named = /^((?:@[^/]+\/)?[^@]+)(?:@(.+))?$/.exec(spec);
|
|
853
|
-
const github = parsePluginSource("placeholder", spec);
|
|
854
|
-
const npm = named && parsePluginSource(named[1], named[2] || "latest");
|
|
855
|
-
if (github.kind !== "github" && npm?.kind !== "npm") throw Error("\u4EC5\u652F\u6301 npm \u5305\u540D / \u7248\u672C\u6216 github:\u7528\u6237/\u4ED3\u5E93");
|
|
856
|
-
this.start("install", async (job) => {
|
|
857
|
-
job.total = 1;
|
|
858
|
-
job.current = spec;
|
|
859
|
-
let expected;
|
|
860
|
-
if (npm?.kind === "npm" && github.kind !== "github") {
|
|
861
|
-
const found = await this.lookup(this.service(), this.profile(), `${npm.package}@${named[2] || "latest"}`, this.abort.signal, registry);
|
|
862
|
-
if (!parseWorkBuddyVersion(found.version)) throw Error("npm \u672A\u8FD4\u56DE\u6709\u6548\u7248\u672C\uFF0C\u8BF7\u68C0\u67E5\u5B89\u88C5\u6E90\u540E\u91CD\u8BD5");
|
|
863
|
-
expected = found.version;
|
|
864
|
-
}
|
|
865
|
-
const result = await outsideActivation(this.hmr(), () => this.service().installBundle(spec, { requestId: job.id, ...registry ? { registry } : {} }));
|
|
866
|
-
if (installFailed(result)) {
|
|
867
|
-
job.results.push({ name: result.bundle ?? spec, ...installFailure(result), expectedVersion: expected });
|
|
868
|
-
delete job.current;
|
|
869
|
-
await this.list();
|
|
870
|
-
return;
|
|
871
|
-
}
|
|
872
|
-
const installed = (await this.service().listBundles()).find((b) => b.name === (result.bundle ?? named?.[1]));
|
|
873
|
-
if (expected && installed?.version !== expected) {
|
|
874
|
-
job.results.push({
|
|
875
|
-
name: result.bundle ?? named[1],
|
|
876
|
-
status: "failed",
|
|
877
|
-
expectedVersion: expected,
|
|
878
|
-
installedVersion: installed?.version,
|
|
879
|
-
message: `\u5B9E\u9645\u5B89\u88C5\u7248\u672C ${installed?.version ?? "\u672A\u77E5"} \u4E0E\u68C0\u67E5\u7248\u672C ${expected} \u4E0D\u540C\uFF1B\u8BF7\u5207\u6362\u300Cnpm \u5B98\u65B9\u300D\u540E\u91CD\u65B0\u68C0\u67E5\u5E76\u5B89\u88C5\u6307\u5B9A\u7248\u672C\u3002`
|
|
880
|
-
});
|
|
881
|
-
delete job.current;
|
|
882
|
-
await this.list();
|
|
883
|
-
return;
|
|
884
|
-
}
|
|
885
|
-
const reloadPending = !result.error && result.packageResult?.exitCode === 0 && result.application === "restart-required" && this.onInstalled?.(result.bundle) === true;
|
|
886
|
-
job.results.push({
|
|
887
|
-
name: result.bundle ?? spec,
|
|
888
|
-
status: result.error || ["failed", "cancelled"].includes(result.application) || result.packageResult && result.packageResult.exitCode !== 0 ? "failed" : "installed",
|
|
889
|
-
message: result.error ? `\u5B89\u88C5\u672A\u5B8C\u6210\uFF08${result.error.code}\uFF09\uFF0C\u8BF7\u67E5\u770B DSH \u63D2\u4EF6\u5B89\u88C5\u65E5\u5FD7` : reloadPending ? "\u5DF2\u5B89\u88C5\uFF0C\u7B49\u5F85\u5F53\u524D\u64CD\u4F5C\u7ED3\u675F\u540E\u81EA\u52A8\u52A0\u8F7D" : result.application === "restart-required" ? "\u5DF2\u5B89\u88C5\uFF0C\u8BF7\u5B8C\u5168\u9000\u51FA\u5E76\u91CD\u542F DSH" : "\u5B89\u88C5\u5B8C\u6210",
|
|
890
|
-
application: reloadPending ? "reload-pending" : result.application,
|
|
891
|
-
pendingBuilds: result.pendingBuilds ?? [],
|
|
892
|
-
restartRequired: !reloadPending && result.application === "restart-required"
|
|
893
|
-
});
|
|
894
|
-
delete job.current;
|
|
895
|
-
await this.list();
|
|
896
|
-
});
|
|
897
|
-
}
|
|
898
|
-
async action(input) {
|
|
899
|
-
if (!this.state().available) throw Error("\u5F53\u524D DSH \u672A\u63D0\u4F9B\u63D2\u4EF6\u7BA1\u7406\u63A5\u53E3\uFF0C\u8BF7\u5347\u7EA7 DSH \u540E\u91CD\u8BD5");
|
|
900
|
-
if (input.registry && !["https://registry.npmjs.org/", "https://registry.npmmirror.com/"].includes(input.registry)) throw Error("\u65E0\u6548\u7684 npm \u6765\u6E90");
|
|
901
|
-
if (input.action === "plugins.list") {
|
|
902
|
-
if (!this.flight) await this.list();
|
|
903
|
-
} else if (input.action === "plugins.dismiss") await this.dismiss(input.keys);
|
|
904
|
-
else if (input.action === "plugins.check") this.check();
|
|
905
|
-
else if (input.action === "plugins.update") this.update(input.names, input.registry, input.allowFreshVersions);
|
|
906
|
-
else if (input.action === "plugins.install") this.install(input.spec, input.registry);
|
|
907
|
-
else if (input.action === "plugins.cancel") {
|
|
908
|
-
if (this.job?.state === "running") {
|
|
909
|
-
this.job.cancelRequested = true;
|
|
910
|
-
if (this.gitAbort) this.gitAbort.abort();
|
|
911
|
-
else if (this.job.kind !== "check") await this.service().cancelInstall?.(this.job.id);
|
|
912
|
-
}
|
|
913
|
-
} else throw Error("\u672A\u77E5\u63D2\u4EF6\u64CD\u4F5C");
|
|
914
|
-
}
|
|
915
|
-
};
|
|
916
|
-
|
|
917
|
-
// src/workbuddy-login.mjs
|
|
918
|
-
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
919
|
-
import { readFile as readFile3 } from "node:fs/promises";
|
|
920
|
-
import { withFileLock as withFileLock2 } from "@deepseek-ai/dsh-atomic-write";
|
|
921
|
-
var WorkBuddyLogin = class {
|
|
922
|
-
constructor(file, { fetchImpl = (...args) => fetch(...args), now = Date.now } = {}) {
|
|
923
|
-
this.file = file;
|
|
924
|
-
this.fetchImpl = fetchImpl;
|
|
925
|
-
this.now = now;
|
|
926
|
-
this.abort = new AbortController();
|
|
927
|
-
this.abort.signal.addEventListener("abort", () => this.finish(), { once: true });
|
|
928
|
-
}
|
|
929
|
-
async reserve(field, ms) {
|
|
930
|
-
await withFileLock2(this.file, async () => {
|
|
931
|
-
let state = {};
|
|
932
|
-
try {
|
|
933
|
-
state = JSON.parse(await readFile3(this.file, "utf8"));
|
|
934
|
-
} catch (e) {
|
|
935
|
-
if (e.code !== "ENOENT") throw Error("\u767B\u5F55\u51B7\u5374\u8BB0\u5F55\u4E0D\u53EF\u8BFB\uFF0C\u8BF7\u68C0\u67E5\u672C\u673A\u6587\u4EF6");
|
|
936
|
-
}
|
|
937
|
-
if (state.pausedUntil > this.now() || state[field] > this.now()) throw Error("\u8BF7\u6C42\u51B7\u5374\u4E2D\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\uFF1B\u5E73\u53F0\u9650\u5236\u8BF7\u5728\u5B98\u65B9\u5BA2\u6237\u7AEF\u5904\u7406");
|
|
938
|
-
state[field] = this.now() + ms;
|
|
939
|
-
await writePrivateJson(this.file, state);
|
|
940
|
-
});
|
|
941
|
-
}
|
|
942
|
-
async pause() {
|
|
943
|
-
this.pending = void 0;
|
|
944
|
-
await withFileLock2(this.file, async () => {
|
|
945
|
-
const state = JSON.parse(await readFile3(this.file, "utf8"));
|
|
946
|
-
state.pausedUntil = this.now() + 15 * 6e4;
|
|
947
|
-
await writePrivateJson(this.file, state);
|
|
948
|
-
});
|
|
949
|
-
}
|
|
950
|
-
async post(path, body) {
|
|
951
|
-
let response;
|
|
952
|
-
try {
|
|
953
|
-
response = await this.fetchImpl(`https://www.workbuddy.cn/v2/plugin/login/${path}`, {
|
|
954
|
-
method: "POST",
|
|
955
|
-
headers: { "Content-Type": "application/json", Accept: "application/json", Origin: "https://www.workbuddy.cn", Referer: "https://www.workbuddy.cn/" },
|
|
956
|
-
body: JSON.stringify(body),
|
|
957
|
-
signal: AbortSignal.any([this.abort.signal, AbortSignal.timeout(3e4)]),
|
|
958
|
-
redirect: "error"
|
|
959
|
-
});
|
|
960
|
-
} catch {
|
|
961
|
-
throw Error("\u767B\u5F55\u8BF7\u6C42\u672A\u5B8C\u6210\uFF0C\u8BF7\u7A0D\u540E\u624B\u52A8\u91CD\u8BD5\uFF1B\u4E0D\u4F1A\u81EA\u52A8\u91CD\u53D1\u77ED\u4FE1");
|
|
962
|
-
}
|
|
963
|
-
if ([403, 429].includes(response.status)) {
|
|
964
|
-
await this.pause();
|
|
965
|
-
throw Error("\u5E73\u53F0\u9650\u5236\u6216\u9700\u8981\u9A8C\u8BC1\uFF0C\u5DF2\u6682\u505C\u767B\u5F55\u8BF7\u6C42\uFF0C\u8BF7\u4F7F\u7528\u5B98\u65B9\u5BA2\u6237\u7AEF");
|
|
966
|
-
}
|
|
967
|
-
if (!response.ok) throw Error("\u5B98\u65B9\u767B\u5F55\u670D\u52A1\u6682\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
|
|
968
|
-
let result;
|
|
969
|
-
try {
|
|
970
|
-
result = JSON.parse(await readLimitedText(response, 256 * 1024, "Login"));
|
|
971
|
-
} catch {
|
|
972
|
-
throw Error("\u767B\u5F55\u54CD\u5E94\u65E0\u6CD5\u8BC6\u522B\uFF0C\u8BF7\u4F7F\u7528\u5B98\u65B9\u5BA2\u6237\u7AEF");
|
|
973
|
-
}
|
|
974
|
-
if (result?.code !== 0) {
|
|
975
|
-
if ([403, 429].includes(result?.code) || /频繁|图形验证码|滑块|安全验证|captcha|risk|too many|frequent|验证失败次数/i.test(result?.msg || "")) {
|
|
976
|
-
await this.pause();
|
|
977
|
-
throw Error("\u5E73\u53F0\u9650\u5236\u6216\u9700\u8981\u9A8C\u8BC1\uFF0C\u5DF2\u6682\u505C\u767B\u5F55\u8BF7\u6C42\uFF0C\u8BF7\u4F7F\u7528\u5B98\u65B9\u5BA2\u6237\u7AEF");
|
|
978
|
-
}
|
|
979
|
-
throw Error(path === "token" ? "\u767B\u5F55\u672A\u6210\u529F\uFF0C\u8BF7\u68C0\u67E5\u9A8C\u8BC1\u7801\u662F\u5426\u6B63\u786E\u3001\u8FC7\u671F\uFF1B\u4EC5\u652F\u6301\u5DF2\u6709\u8D26\u53F7" : "\u77ED\u4FE1\u672A\u53D1\u9001\u6210\u529F\uFF0C\u8BF7\u786E\u8BA4\u5DF2\u6709\u8D26\u53F7\u53CA\u624B\u673A\u53F7\uFF0C\u6216\u4F7F\u7528\u5B98\u65B9\u5BA2\u6237\u7AEF");
|
|
980
|
-
}
|
|
981
|
-
return result.data ?? {};
|
|
982
|
-
}
|
|
983
|
-
async send(phone) {
|
|
984
|
-
if (typeof phone !== "string" || !/^1[3-9]\d{9}$/.test(phone)) throw Error("\u8BF7\u8F93\u5165 11 \u4F4D\u4E2D\u56FD\u5927\u9646\u624B\u673A\u53F7");
|
|
985
|
-
if (this.flight) throw Error("\u767B\u5F55\u8BF7\u6C42\u6B63\u5728\u5904\u7406\u4E2D");
|
|
986
|
-
this.flight = (async () => {
|
|
987
|
-
await this.reserve("smsAt", 6e4);
|
|
988
|
-
this.pending = void 0;
|
|
989
|
-
const data = await this.post("send-sms", { phone });
|
|
990
|
-
const expires = Math.min(300, Math.max(1, Number(data.expires_in) || 300)) * 1e3;
|
|
991
|
-
this.pending = { phone, challenge: randomBytes2(24).toString("hex"), expiresAt: this.now() + expires, attempts: 0 };
|
|
992
|
-
clearTimeout(this.expiryTimer);
|
|
993
|
-
this.expiryTimer = setTimeout(() => this.finish(), expires);
|
|
994
|
-
this.expiryTimer.unref();
|
|
995
|
-
return { challenge: this.pending.challenge, expiresAt: this.pending.expiresAt, resendAt: this.now() + 6e4 };
|
|
996
|
-
})();
|
|
997
|
-
try {
|
|
998
|
-
return await this.flight;
|
|
999
|
-
} finally {
|
|
1000
|
-
this.flight = void 0;
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
async complete(challenge, code, verify = true) {
|
|
1004
|
-
if (typeof challenge !== "string" || !this.pending || challenge !== this.pending.challenge || this.pending.expiresAt <= this.now()) throw Error("\u672C\u6B21\u767B\u5F55\u5DF2\u8FC7\u671F\u6216\u63D2\u4EF6\u5DF2\u91CD\u542F\uFF0C\u8BF7\u91CD\u65B0\u83B7\u53D6\u9A8C\u8BC1\u7801");
|
|
1005
|
-
if (this.flight) throw Error("\u767B\u5F55\u8BF7\u6C42\u6B63\u5728\u5904\u7406\u4E2D");
|
|
1006
|
-
if (this.pending.document) return this.pending;
|
|
1007
|
-
if (typeof code !== "string" || !/^\d{4,8}$/.test(code) || typeof verify !== "boolean") throw Error("\u8BF7\u8F93\u5165\u6536\u5230\u7684\u77ED\u4FE1\u9A8C\u8BC1\u7801");
|
|
1008
|
-
if (this.pending.attempts >= 5) throw Error("\u672C\u6B21\u9A8C\u8BC1\u7801\u5C1D\u8BD5\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650\uFF0C\u8BF7\u7A0D\u540E\u91CD\u65B0\u83B7\u53D6");
|
|
1009
|
-
this.flight = (async () => {
|
|
1010
|
-
await this.reserve("loginAt", 5e3);
|
|
1011
|
-
const pending = this.pending;
|
|
1012
|
-
pending.attempts++;
|
|
1013
|
-
const data = await this.post("token", { login_method: "phone", phone: pending.phone, sms_code: code });
|
|
1014
|
-
const accessToken = data.accessToken || data.access_token, refreshToken = data.refreshToken || data.refresh_token;
|
|
1015
|
-
let token;
|
|
1016
|
-
try {
|
|
1017
|
-
token = workbuddyToken(accessToken);
|
|
1018
|
-
} catch {
|
|
1019
|
-
throw Error("\u767B\u5F55\u8FD4\u56DE\u7684\u4EE4\u724C\u4E0D\u5C5E\u4E8E WorkBuddy \u4E2D\u56FD\u7248\uFF0C\u8BF7\u4F7F\u7528\u5B98\u65B9\u5BA2\u6237\u7AEF");
|
|
1020
|
-
}
|
|
1021
|
-
if (typeof refreshToken !== "string" || !refreshToken || !Number.isFinite(token.exp) || token.exp * 1e3 <= this.now()) throw Error("\u767B\u5F55\u54CD\u5E94\u7F3A\u5C11\u6709\u6548\u51ED\u8BC1\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55");
|
|
1022
|
-
let credential = { accessToken, refreshToken, uid: token.sub, expiresAtMs: token.exp * 1e3, domain: "www.workbuddy.cn", source: "dsh" };
|
|
1023
|
-
if (verify) {
|
|
1024
|
-
try {
|
|
1025
|
-
const rotated = await refreshWorkBuddyCheckinCredential(credential, { fetchImpl: this.fetchImpl, signal: AbortSignal.any([this.abort.signal, AbortSignal.timeout(3e4)]) });
|
|
1026
|
-
credential = {
|
|
1027
|
-
...credential,
|
|
1028
|
-
accessToken: rotated.accessToken,
|
|
1029
|
-
refreshToken: rotated.refreshToken || credential.refreshToken,
|
|
1030
|
-
expiresAtMs: Date.now() + rotated.expiresInSec * 1e3
|
|
1031
|
-
};
|
|
1032
|
-
} catch {
|
|
1033
|
-
pending.warning = "\u767B\u5F55\u51ED\u8BC1\u5DF2\u4FDD\u5B58\uFF0C\u4F46\u5237\u65B0\u4EE4\u724C\u9A8C\u8BC1\u672A\u901A\u8FC7\uFF1B\u8BF7\u7A0D\u540E\u5728\u8D26\u53F7\u8BE6\u60C5\u68C0\u67E5\u767B\u5F55\u6709\u6548\u671F\u3002";
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
delete pending.phone;
|
|
1037
|
-
pending.document = { version: 1, credential };
|
|
1038
|
-
return pending;
|
|
1039
|
-
})();
|
|
1040
|
-
try {
|
|
1041
|
-
return await this.flight;
|
|
1042
|
-
} finally {
|
|
1043
|
-
this.flight = void 0;
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
finish() {
|
|
1047
|
-
clearTimeout(this.expiryTimer);
|
|
1048
|
-
this.pending = void 0;
|
|
1049
|
-
}
|
|
1050
|
-
};
|
|
1051
|
-
|
|
1052
|
-
// src/manager.mjs
|
|
1053
|
-
var MANAGEMENT_PATH = "/.well-known/dsh-multi-router";
|
|
1054
|
-
var SUMMARY_PATH = `${MANAGEMENT_PATH}/summary`;
|
|
1055
|
-
var revisionOf = (raw) => createHash("sha256").update(JSON.stringify(raw)).digest("hex");
|
|
1056
|
-
var ManagementError = class extends Error {
|
|
1057
|
-
constructor(status, message) {
|
|
1058
|
-
super(message);
|
|
1059
|
-
this.status = status;
|
|
1060
|
-
}
|
|
1061
|
-
};
|
|
1062
|
-
var bad = (message) => {
|
|
1063
|
-
throw new ManagementError(400, message);
|
|
1064
|
-
};
|
|
1065
|
-
var finite = (value, fallback) => Number.isSafeInteger(value) && value > 0 ? value : fallback;
|
|
1066
|
-
function modelInfo(row) {
|
|
1067
|
-
const billing = row.billing ? {
|
|
1068
|
-
free: row.billing.free === true,
|
|
1069
|
-
rateUnknown: row.billing.rateUnknown === true,
|
|
1070
|
-
...typeof row.billing.credits === "string" ? { credits: row.billing.credits } : {}
|
|
1071
|
-
} : void 0;
|
|
1072
|
-
const reasoning = row.reasoning?.supported ? {
|
|
1073
|
-
supports: true,
|
|
1074
|
-
supportedEfforts: row.reasoning.supported,
|
|
1075
|
-
defaultEffort: row.reasoning.defaultEffort,
|
|
1076
|
-
canDisableThinking: row.reasoning.supported.includes("off")
|
|
1077
|
-
} : row.reasoning;
|
|
1078
|
-
return {
|
|
1079
|
-
id: row.id,
|
|
1080
|
-
name: row.name || row.id,
|
|
1081
|
-
...contextInfo(row),
|
|
1082
|
-
maxTokens: Math.min(finite(row.maxTokens, 32e3), contextInfo(row).maxContextWindow),
|
|
1083
|
-
supportsImages: row.supportsImages === true || row.multimodal === true || row.input?.includes("image") === true,
|
|
1084
|
-
...reasoning ? { reasoning } : {},
|
|
1085
|
-
...billing ? { billing } : {},
|
|
1086
|
-
...typeof row.creditMultiplier === "number" && Number.isFinite(row.creditMultiplier) && row.creditMultiplier >= 0 ? { creditMultiplier: row.creditMultiplier } : {},
|
|
1087
|
-
...Array.isArray(row.promotions) ? { promotions: row.promotions.map((p) => ({ start: p.start, end: p.end, factor: p.factor, priority: p.priority, label: typeof p.label === "string" ? p.label : "" })) } : {}
|
|
1088
|
-
};
|
|
1089
|
-
}
|
|
1090
|
-
var Manager = class {
|
|
1091
|
-
constructor(file, router, onChange = () => {
|
|
1092
|
-
}, pluginAccess = {}) {
|
|
1093
|
-
this.file = file;
|
|
1094
|
-
this.router = router;
|
|
1095
|
-
this.onChange = onChange;
|
|
1096
|
-
this.catalogFile = join5(dirname3(file), "catalogs.json");
|
|
1097
|
-
this.catalogs = /* @__PURE__ */ new Map();
|
|
1098
|
-
this.catalogErrors = /* @__PURE__ */ new Map();
|
|
1099
|
-
this.refreshes = /* @__PURE__ */ new Map();
|
|
1100
|
-
this.closed = false;
|
|
1101
|
-
this.monitorFlights = /* @__PURE__ */ new Map();
|
|
1102
|
-
this.loginInfo = /* @__PURE__ */ new Map();
|
|
1103
|
-
this.telemetry = new Telemetry(join5(dirname3(file), "usage.json"));
|
|
1104
|
-
this.plugins = new PluginUpdates({ ...pluginAccess, stateFile: join5(dirname3(file), "plugin-updates.json"), onInstalled: (name) => {
|
|
1105
|
-
if (name !== "dsh-multi-router" || !this.runtime?.canReload()) return false;
|
|
1106
|
-
this.runtime.schedule();
|
|
1107
|
-
return true;
|
|
1108
|
-
} });
|
|
1109
|
-
this.checkins = new Checkins(join5(dirname3(file), "checkins.json"), this);
|
|
1110
|
-
this.workbuddyLogin = new WorkBuddyLogin(join5(dirname3(file), "workbuddy-login.json"), { fetchImpl: pluginAccess.fetchImpl });
|
|
1111
|
-
this.setupFile = join5(dirname3(file), "initial-setup.json");
|
|
1112
|
-
this.api = new ApiService(join5(dirname3(file), "api-service.json"), this);
|
|
1113
|
-
router.telemetry = this.telemetry;
|
|
1114
|
-
router.monitor = (a) => this.monitorAccount(a, { maxAgeMs: 6e4 });
|
|
1115
|
-
router.prepareFree = () => this.checkFreeModels(true);
|
|
1116
|
-
router.freeModels = (account) => this.freeModels(account);
|
|
1117
|
-
}
|
|
1118
|
-
async load() {
|
|
1119
|
-
this.raw = JSON.parse(await readFile4(this.file, "utf8"));
|
|
1120
|
-
try {
|
|
1121
|
-
this.setup = JSON.parse(await readFile4(this.setupFile, "utf8"));
|
|
1122
|
-
} catch {
|
|
1123
|
-
this.setup = void 0;
|
|
1124
|
-
}
|
|
1125
|
-
this.config = validateConfig(this.raw, this.file);
|
|
1126
|
-
await this.checkins.load();
|
|
1127
|
-
await this.plugins.load();
|
|
1128
|
-
try {
|
|
1129
|
-
const saved = JSON.parse(await readFile4(this.catalogFile, "utf8"));
|
|
1130
|
-
for (const account of this.config.accounts) {
|
|
1131
|
-
const item = saved[account.id];
|
|
1132
|
-
if (item?.credentialFile === account.credentialFile && Array.isArray(item.models)) {
|
|
1133
|
-
this.catalogs.set(account.id, { ...item, ...item.contextVersion !== 1 ? { fetchedAt: 0 } : {}, models: item.models.filter((m) => typeof m.id === "string" && m.id.length > 0).map(modelInfo) });
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
} catch {
|
|
1137
|
-
}
|
|
1138
|
-
this.telemetry.configure(this.config.usageRetentionDays);
|
|
1139
|
-
await this.telemetry.load();
|
|
1140
|
-
await this.api.load();
|
|
1141
|
-
await Promise.all(this.config.accounts.map((a) => this.readLogin(a)));
|
|
1142
|
-
this.publish();
|
|
1143
|
-
}
|
|
1144
|
-
async readLogin(account) {
|
|
1145
|
-
try {
|
|
1146
|
-
const raw = await jsonCredential(account.credentialFile), c = raw.credential;
|
|
1147
|
-
this.loginInfo.set(account.id, account.provider.startsWith("qoder") ? { state: raw.pat ? "configured" : "missing", kind: "PAT", refreshable: false } : {
|
|
1148
|
-
state: !c?.accessToken ? "missing" : c?.expiresAtMs > Date.now() ? "valid" : c?.refreshToken ? "refresh-needed" : "expired",
|
|
1149
|
-
name: c?.nickname || c?.accountName || c?.uid || c?.userId,
|
|
1150
|
-
identity: c?.uid || c?.userId,
|
|
1151
|
-
expiresAt: c?.expiresAtMs,
|
|
1152
|
-
refreshExpiresAt: c?.refreshExpiresAtMs,
|
|
1153
|
-
refreshable: !!c?.refreshToken,
|
|
1154
|
-
kind: "\u5BA2\u6237\u7AEF\u767B\u5F55\u6001",
|
|
1155
|
-
edition: c?.edition
|
|
1156
|
-
});
|
|
1157
|
-
} catch {
|
|
1158
|
-
this.loginInfo.set(account.id, { state: "missing" });
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
async syncLocalAccounts(provider, force = false) {
|
|
1162
|
-
if (this.localScan) return this.localScan;
|
|
1163
|
-
this.localScan = (async () => {
|
|
1164
|
-
const discovered = await discoverLocalAccounts(provider);
|
|
1165
|
-
this.localDiscovery = [
|
|
1166
|
-
...(this.localDiscovery ?? []).filter((s) => !discovered.status.some((n) => n.provider === s.provider)),
|
|
1167
|
-
...discovered.status.map((s) => ({ ...s, checkedAt: Date.now() }))
|
|
1168
|
-
];
|
|
1169
|
-
if (this.closed || !discovered.accounts.length) return;
|
|
1170
|
-
await withFileLock3(this.file, async () => {
|
|
1171
|
-
if (this.closed) return;
|
|
1172
|
-
let raw = JSON.parse(await readFile4(this.file, "utf8"));
|
|
1173
|
-
let changed = false;
|
|
1174
|
-
const saved = /* @__PURE__ */ new Map();
|
|
1175
|
-
for (const item of discovered.accounts) {
|
|
1176
|
-
const key = localAccountKey(item.provider, item.document);
|
|
1177
|
-
if (!force && raw.ignoredLocalAccounts?.some((k) => normalizeLocalAccountKey(k) === key)) continue;
|
|
1178
|
-
const config = validateConfig(raw, this.file);
|
|
1179
|
-
let existing, current;
|
|
1180
|
-
for (const a of config.accounts.filter((a2) => a2.provider === item.provider)) {
|
|
1181
|
-
try {
|
|
1182
|
-
const value = await jsonCredential(a.credentialFile);
|
|
1183
|
-
if (localAccountKey(a.provider, value) === key) {
|
|
1184
|
-
existing = a;
|
|
1185
|
-
current = value;
|
|
1186
|
-
break;
|
|
1187
|
-
}
|
|
1188
|
-
} catch {
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
const id = existing?.id ?? `local_${createHash("sha256").update(key).digest("hex").slice(0, 20)}`;
|
|
1192
|
-
const recordSaved = () => {
|
|
1193
|
-
if (!saved.has(item.provider)) saved.set(item.provider, /* @__PURE__ */ new Set());
|
|
1194
|
-
saved.get(item.provider).add(id);
|
|
1195
|
-
};
|
|
1196
|
-
let sameDevice = true;
|
|
1197
|
-
if (existing && item.identityDocument) {
|
|
1198
|
-
try {
|
|
1199
|
-
const identity = await jsonCredential(existing.identityFile);
|
|
1200
|
-
sameDevice = identity.machineId === item.identityDocument.machineId && identity.deviceId === item.identityDocument.deviceId;
|
|
1201
|
-
} catch {
|
|
1202
|
-
sameDevice = false;
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
if (existing && sameDevice && current.credential.expiresAtMs >= item.document.credential.expiresAtMs) {
|
|
1206
|
-
recordSaved();
|
|
1207
|
-
continue;
|
|
1208
|
-
}
|
|
1209
|
-
const base = join5(dirname3(this.file), "accounts");
|
|
1210
|
-
await mkdir2(base, { recursive: true, mode: 448 });
|
|
1211
|
-
const staging = await mkdtemp2(join5(base, `${id}-`));
|
|
1212
|
-
try {
|
|
1213
|
-
const credentialFile = join5(staging, "credential.json"), identityFile = join5(staging, "identity.json");
|
|
1214
|
-
await importAccount({ ...item, credentialFile, identityFile });
|
|
1215
|
-
const paths = {
|
|
1216
|
-
credentialFile: relative2(dirname3(this.file), credentialFile),
|
|
1217
|
-
...item.provider.startsWith("trae") ? { identityFile: relative2(dirname3(this.file), identityFile) } : {}
|
|
1218
|
-
};
|
|
1219
|
-
const next = structuredClone(raw);
|
|
1220
|
-
if (existing) Object.assign(next.accounts.find((a) => a.id === id), paths);
|
|
1221
|
-
else next.accounts.push({
|
|
1222
|
-
id,
|
|
1223
|
-
provider: item.provider,
|
|
1224
|
-
name: item.document.credential.nickname || item.document.credential.accountName || item.document.credential.uid || item.document.credential.userId,
|
|
1225
|
-
...paths
|
|
1226
|
-
});
|
|
1227
|
-
next.ignoredLocalAccounts = (raw.ignoredLocalAccounts ?? []).filter((k) => normalizeLocalAccountKey(k) !== key);
|
|
1228
|
-
validateConfig(next, this.file);
|
|
1229
|
-
await writePrivateJson(this.file, next);
|
|
1230
|
-
raw = next;
|
|
1231
|
-
changed = true;
|
|
1232
|
-
recordSaved();
|
|
1233
|
-
this.router.clients.delete(id);
|
|
1234
|
-
this.catalogs.delete(id);
|
|
1235
|
-
this.catalogErrors.delete(id);
|
|
1236
|
-
} catch (e) {
|
|
1237
|
-
await rm3(staging, { recursive: true, force: true });
|
|
1238
|
-
this.localDiscovery = this.localDiscovery.map((s) => s.provider === item.provider ? { ...s, state: "unreadable", code: "import_failed" } : s);
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
if (changed) {
|
|
1242
|
-
this.raw = raw;
|
|
1243
|
-
this.config = validateConfig(raw, this.file);
|
|
1244
|
-
await Promise.all(this.config.accounts.map((a) => this.readLogin(a)));
|
|
1245
|
-
this.publish();
|
|
1246
|
-
}
|
|
1247
|
-
this.localDiscovery = this.localDiscovery.map((s) => discovered.status.some((d) => d.provider === s.provider) ? { ...s, savedCount: saved.get(s.provider)?.size ?? 0 } : s);
|
|
1248
|
-
}, { waitMs: 3e4 });
|
|
1249
|
-
})().finally(() => {
|
|
1250
|
-
this.localScan = void 0;
|
|
1251
|
-
});
|
|
1252
|
-
return this.localScan;
|
|
1253
|
-
}
|
|
1254
|
-
async finishInitialSetup() {
|
|
1255
|
-
if (this.setup?.completedAt) return;
|
|
1256
|
-
await withFileLock3(this.setupFile, async () => {
|
|
1257
|
-
try {
|
|
1258
|
-
this.setup = JSON.parse(await readFile4(this.setupFile, "utf8"));
|
|
1259
|
-
} catch {
|
|
1260
|
-
}
|
|
1261
|
-
if (this.setup?.completedAt) return;
|
|
1262
|
-
this.setup = { completedAt: Date.now(), dismissed: false };
|
|
1263
|
-
await writePrivateJson(this.setupFile, this.setup);
|
|
1264
|
-
});
|
|
1265
|
-
}
|
|
1266
|
-
async monitorAccount(account, { force = false, maxAgeMs = 3e5 } = {}) {
|
|
1267
|
-
if (this.monitorFlights.has(account.id)) return this.monitorFlights.get(account.id);
|
|
1268
|
-
const cached = this.telemetry.row("accounts", account.id).balance;
|
|
1269
|
-
const last = cached?.error ? cached.attemptedAt : cached?.fetchedAt;
|
|
1270
|
-
const ttl = cached?.error ? 3e5 : maxAgeMs;
|
|
1271
|
-
if (!force && Number.isFinite(last) && Date.now() >= last && Date.now() - last < ttl) return;
|
|
1272
|
-
const promise = (async () => {
|
|
1273
|
-
try {
|
|
1274
|
-
const client = await this.router.client(account);
|
|
1275
|
-
if (!client.accountInfo) return;
|
|
1276
|
-
const snapshot = await client.accountInfo(AbortSignal.timeout(3e4));
|
|
1277
|
-
if (this.closed || !this.config.accounts.some((a) => a.id === account.id && a.credentialFile === account.credentialFile)) return;
|
|
1278
|
-
this.loginInfo.set(account.id, snapshot.login);
|
|
1279
|
-
await this.telemetry.observe(account.id, snapshot);
|
|
1280
|
-
} catch (e) {
|
|
1281
|
-
if (this.closed) return;
|
|
1282
|
-
const old = this.telemetry.row("accounts", account.id).balance;
|
|
1283
|
-
await this.telemetry.observe(account.id, { ...old, error: { code: "usage_fetch_failed", status: Number(e.status ?? e.failure?.status) || 502 }, attemptedAt: Date.now() });
|
|
1284
|
-
await this.readLogin(account);
|
|
1285
|
-
}
|
|
1286
|
-
})().finally(() => this.monitorFlights.delete(account.id));
|
|
1287
|
-
this.monitorFlights.set(account.id, promise);
|
|
1288
|
-
return promise;
|
|
1289
|
-
}
|
|
1290
|
-
async monitorAll(provider, options2) {
|
|
1291
|
-
await Promise.all(this.config.accounts.filter((a) => (!provider || a.provider === provider) && (options2?.force || a.enabled !== false)).map((a) => this.monitorAccount(a, options2)));
|
|
1292
|
-
}
|
|
1293
|
-
freeAccounts() {
|
|
1294
|
-
const targets = Object.values(this.config.routes).filter((r) => r.freeOnly).flatMap((r) => r.targets);
|
|
1295
|
-
return this.config.accounts.filter((a) => a.enabled !== false && targets.some((t) => t.provider === a.provider && (!t.account || t.account === a.id)));
|
|
1296
|
-
}
|
|
1297
|
-
freeModels(account) {
|
|
1298
|
-
const saved = this.catalogs.get(account.id);
|
|
1299
|
-
if (this.catalogErrors.has(account.id) || !saved || beijingDay(saved.fetchedAt) !== beijingDay(Date.now())) return [];
|
|
1300
|
-
return saved.models.map((m) => modelWithCurrentPromotion(m)).filter((m) => isFreeModel(m));
|
|
1301
|
-
}
|
|
1302
|
-
async checkFreeModels(force = false) {
|
|
1303
|
-
await Promise.all(this.freeAccounts().filter((a) => {
|
|
1304
|
-
const saved = this.catalogs.get(a.id);
|
|
1305
|
-
if (force) return true;
|
|
1306
|
-
if (Date.now() - (this.catalogErrors.get(a.id)?.at ?? 0) < 3e5) return false;
|
|
1307
|
-
return !saved || beijingDay(saved.fetchedAt) !== beijingDay(Date.now()) || saved.pricingVersion !== 1;
|
|
1308
|
-
}).map((a) => this.refreshAccount(a, "pricing")));
|
|
1309
|
-
}
|
|
1310
|
-
freeStatus(row) {
|
|
1311
|
-
const accounts = this.config.accounts.filter((a) => a.enabled !== false && row.targets.some((t) => t.provider === a.provider && (!t.account || t.account === a.id)));
|
|
1312
|
-
return {
|
|
1313
|
-
checking: accounts.some((a) => this.refreshes.has(a.id)),
|
|
1314
|
-
accounts: accounts.map((a) => ({
|
|
1315
|
-
id: a.id,
|
|
1316
|
-
provider: a.provider,
|
|
1317
|
-
name: a.name ?? a.id,
|
|
1318
|
-
checkedAt: this.catalogs.get(a.id)?.fetchedAt,
|
|
1319
|
-
error: this.catalogErrors.get(a.id),
|
|
1320
|
-
models: this.freeModels(a).map((m) => ({ id: m.id, name: m.name }))
|
|
1321
|
-
}))
|
|
1322
|
-
};
|
|
1323
|
-
}
|
|
1324
|
-
freeRoutes(parentId, row) {
|
|
1325
|
-
const groups = /* @__PURE__ */ new Map();
|
|
1326
|
-
for (const account of this.config.accounts.filter((a) => a.enabled !== false)) {
|
|
1327
|
-
for (const model of this.freeModels(account)) {
|
|
1328
|
-
if (!row.targets.some((t) => t.provider === account.provider && (!t.account || t.account === account.id) && (!t.model || t.model === model.id) && (!t.models || t.models.includes(model.id)))) continue;
|
|
1329
|
-
const id = `free_${createHash("sha256").update(JSON.stringify([parentId, account.provider, model.id])).digest("hex").slice(0, 24)}`;
|
|
1330
|
-
if (!groups.has(id)) groups.set(id, { id, models: [], targets: [], accountNames: [], modelName: model.name, provider: account.provider, modelId: model.id });
|
|
1331
|
-
const group = groups.get(id);
|
|
1332
|
-
group.models.push({ model, provider: account.provider, accountId: account.id });
|
|
1333
|
-
group.targets.push({ provider: account.provider, account: account.id, model: model.id });
|
|
1334
|
-
group.accountNames.push(account.name ?? account.id);
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
return [...groups.values()].map(({ models, ...group }) => {
|
|
1338
|
-
const preference = row.freeModels?.[group.id] ?? {};
|
|
1339
|
-
const context = resolveContext(models.map((s) => s.model), preference.contextWindow ?? (row.contextMode === "custom" ? row.contextWindow : null), row.contextWindow);
|
|
1340
|
-
const eligible = models.filter((s) => contextInfo(s.model).maxContextWindow >= context.contextWindow);
|
|
1341
|
-
const eligibleAccountIds = eligible.map((s) => s.accountId);
|
|
1342
|
-
const reasoning = routeReasoning(eligible);
|
|
1343
|
-
const selected = preference.reasoningEffort;
|
|
1344
|
-
return {
|
|
1345
|
-
...group,
|
|
1346
|
-
...context,
|
|
1347
|
-
eligibleAccountIds,
|
|
1348
|
-
targets: group.targets.filter((t) => eligibleAccountIds.includes(t.account)),
|
|
1349
|
-
accountNames: group.accountNames.filter((_, i) => eligibleAccountIds.includes(models[i].accountId)),
|
|
1350
|
-
enforceContext: true,
|
|
1351
|
-
parentId,
|
|
1352
|
-
dynamicFree: true,
|
|
1353
|
-
freeOnly: true,
|
|
1354
|
-
strategy: "failover",
|
|
1355
|
-
preferExpiringCredits: false,
|
|
1356
|
-
name: `Free \xB7 ${group.modelName} \xB7 ${PROVIDER_NAMES[group.provider]}`,
|
|
1357
|
-
reasoning,
|
|
1358
|
-
...selected && reasoning?.supportedEfforts.includes(selected) ? { reasoningEffort: selected } : {},
|
|
1359
|
-
configuredEffort: selected ?? null,
|
|
1360
|
-
effortUnavailable: !!selected && !reasoning?.supportedEfforts.includes(selected),
|
|
1361
|
-
visible: row.visible !== false && preference.visible !== false,
|
|
1362
|
-
selected: preference.visible !== false,
|
|
1363
|
-
supportsImages: eligible.every(({ model }) => model.supportsImages),
|
|
1364
|
-
maxTokens: Math.min(row.maxTokens, context.contextWindow, ...eligible.map(({ model }) => model.maxTokens))
|
|
1365
|
-
};
|
|
1366
|
-
});
|
|
1367
|
-
}
|
|
1368
|
-
adapterModels() {
|
|
1369
|
-
const rows = this.routeModels();
|
|
1370
|
-
return [
|
|
1371
|
-
...rows.filter((r) => r.freeOnly).flatMap((r) => this.freeRoutes(r.id, r)),
|
|
1372
|
-
...rows.filter((r) => !r.freeOnly),
|
|
1373
|
-
...rows.filter((r) => r.freeOnly).map((r) => ({ ...r, visible: false }))
|
|
1374
|
-
];
|
|
1375
|
-
}
|
|
1376
|
-
publish() {
|
|
1377
|
-
this.telemetry.configure(this.config.usageRetentionDays);
|
|
1378
|
-
this.revision = revisionOf(this.raw);
|
|
1379
|
-
const routes = Object.fromEntries(this.routeModels().map((row) => [row.id, row]));
|
|
1380
|
-
for (const model of this.adapterModels().filter((m) => m.dynamicFree)) routes[model.id] = model;
|
|
1381
|
-
for (const provider of PROVIDERS) for (const model of this.providerModels(provider)) {
|
|
1382
|
-
routes[`${provider}::${model.id}`] = { ...model, targets: [{ provider, model: model.id }] };
|
|
1383
|
-
}
|
|
1384
|
-
this.router.updateConfig({ ...this.config, routes });
|
|
1385
|
-
this.onChange();
|
|
1386
|
-
}
|
|
1387
|
-
providerModels(provider) {
|
|
1388
|
-
const rows = /* @__PURE__ */ new Map();
|
|
1389
|
-
for (const account of this.config.accounts.filter((a) => a.provider === provider && a.enabled !== false)) {
|
|
1390
|
-
for (const model of this.catalogs.get(account.id)?.models ?? []) {
|
|
1391
|
-
if (!rows.has(model.id)) rows.set(model.id, []);
|
|
1392
|
-
rows.get(model.id).push(model);
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
return [...rows.values()].map((models) => {
|
|
1396
|
-
const model = models[0], preference = this.config.models[provider]?.[model.id] ?? {};
|
|
1397
|
-
const context = resolveContext(models, preference.contextWindow);
|
|
1398
|
-
const eligible = models.filter((m) => contextInfo(m).maxContextWindow >= context.contextWindow);
|
|
1399
|
-
const reasoning = routeReasoning(eligible.map((model2) => ({ model: model2, provider })));
|
|
1400
|
-
const selected = preference.reasoningEffort;
|
|
1401
|
-
return {
|
|
1402
|
-
...model,
|
|
1403
|
-
...context,
|
|
1404
|
-
enforceContext: true,
|
|
1405
|
-
name: preference.name ?? model.name,
|
|
1406
|
-
reasoning,
|
|
1407
|
-
supportsImages: preference.supportsImages !== false && eligible.every((m) => m.supportsImages),
|
|
1408
|
-
imageCapable: eligible.every((m) => m.supportsImages),
|
|
1409
|
-
...selected && reasoning?.supportedEfforts.includes(selected) ? { reasoningEffort: selected } : {},
|
|
1410
|
-
configuredEffort: selected ?? null,
|
|
1411
|
-
effortUnavailable: !!selected && !reasoning?.supportedEfforts.includes(selected),
|
|
1412
|
-
maxTokens: Math.min(context.contextWindow, ...eligible.map((m) => m.maxTokens))
|
|
1413
|
-
};
|
|
1414
|
-
});
|
|
1415
|
-
}
|
|
1416
|
-
hiddenModels(provider) {
|
|
1417
|
-
return this.providerModels(provider).filter((m) => !this.modelVisible(provider, m.id)).map((m) => m.id);
|
|
1418
|
-
}
|
|
1419
|
-
modelVisible(provider, id) {
|
|
1420
|
-
return this.config.models[provider]?.[id]?.visible ?? this.config.modelVisibility[provider] ?? false;
|
|
1421
|
-
}
|
|
1422
|
-
routeModels(routes = this.config.routes) {
|
|
1423
|
-
return Object.entries(routes).sort(([, a], [, b]) => Number(!!b.freeOnly) - Number(!!a.freeOnly)).map(([id, row]) => {
|
|
1424
|
-
const effectiveTargets = row.freeOnly ? this.freeRoutes(id, row).flatMap((r) => r.targets) : row.targets;
|
|
1425
|
-
const sources = effectiveTargets.flatMap((target) => this.config.accounts.filter((a) => a.enabled !== false && a.provider === target.provider && (!target.account || target.account === a.id)).flatMap((a) => {
|
|
1426
|
-
const catalog = this.catalogs.get(a.id)?.models ?? [];
|
|
1427
|
-
const models2 = target.models ? target.models.map((model) => catalog.find((m) => m.id === model)).filter(Boolean) : [matchModel(id, target, catalog)].filter(Boolean);
|
|
1428
|
-
return models2.map((model) => ({ model, provider: a.provider, accountId: a.id }));
|
|
1429
|
-
}));
|
|
1430
|
-
const context = resolveContext(sources.map((s) => s.model), row.contextMode === "custom" ? row.contextWindow : null, row.contextWindow);
|
|
1431
|
-
const eligible = sources.filter((s) => contextInfo(s.model).maxContextWindow >= context.contextWindow);
|
|
1432
|
-
const models = eligible.map((s) => s.model), reasoning = routeReasoning(eligible);
|
|
1433
|
-
const rates = {};
|
|
1434
|
-
for (const source of eligible) {
|
|
1435
|
-
const rate = creditRate(source.model, this.telemetry.now());
|
|
1436
|
-
if (rate === void 0) continue;
|
|
1437
|
-
rates[source.accountId] = rates[source.accountId] === void 0 ? rate : Math.min(rates[source.accountId], rate);
|
|
1438
|
-
}
|
|
1439
|
-
return {
|
|
1440
|
-
...row,
|
|
1441
|
-
...context,
|
|
1442
|
-
enforceContext: true,
|
|
1443
|
-
supportsImages: models.length > 0 && models.every((m) => m.supportsImages),
|
|
1444
|
-
reasoning,
|
|
1445
|
-
eligibleAccountIds: [...new Set(eligible.map((s) => s.accountId))],
|
|
1446
|
-
creditRates: rates,
|
|
1447
|
-
maxTokens: Math.min(row.maxTokens, context.contextWindow, ...models.map((m) => m.maxTokens)),
|
|
1448
|
-
id,
|
|
1449
|
-
name: row.name ?? id,
|
|
1450
|
-
expiringStatus: { ...this.telemetry.data.expiringRounds[id] },
|
|
1451
|
-
...row.freeOnly ? { freeStatus: { ...this.freeStatus(row), modelRoutes: this.freeRoutes(id, row) } } : {}
|
|
1452
|
-
};
|
|
1453
|
-
});
|
|
1454
|
-
}
|
|
1455
|
-
async ensureCatalog(provider, force = false) {
|
|
1456
|
-
await Promise.all(this.config.accounts.filter((a) => a.provider === provider && a.enabled !== false).map((a) => this.refreshAccount(a, force)));
|
|
1457
|
-
}
|
|
1458
|
-
async refreshAccount(account, force) {
|
|
1459
|
-
const saved = this.catalogs.get(account.id);
|
|
1460
|
-
if (!force && saved && Date.now() - saved.fetchedAt < this.config.catalogTtlMs) return;
|
|
1461
|
-
if (!force && Date.now() - (this.catalogErrors.get(account.id)?.at ?? 0) < 1e4) return;
|
|
1462
|
-
if (this.refreshes.has(account.id)) return this.refreshes.get(account.id);
|
|
1463
|
-
const promise = (async () => {
|
|
1464
|
-
try {
|
|
1465
|
-
if (force === true) this.router.clients.delete(account.id);
|
|
1466
|
-
const client = await this.router.client(account);
|
|
1467
|
-
const models = (await client.models(AbortSignal.timeout(6e4), !!force)).map(modelInfo);
|
|
1468
|
-
if (this.closed || !this.config.accounts.some((a) => a.id === account.id && a.credentialFile === account.credentialFile)) return;
|
|
1469
|
-
this.catalogs.set(account.id, { credentialFile: account.credentialFile, fetchedAt: Date.now(), pricingVersion: 1, contextVersion: 1, models });
|
|
1470
|
-
this.catalogErrors.delete(account.id);
|
|
1471
|
-
await withFileLock3(this.catalogFile, () => writePrivateJson(this.catalogFile, Object.fromEntries(this.catalogs)));
|
|
1472
|
-
} catch (error) {
|
|
1473
|
-
if (this.closed || !this.config.accounts.some((a) => a.id === account.id)) return;
|
|
1474
|
-
this.catalogErrors.set(account.id, { status: Number(error.status ?? error.failure?.status) || 502, code: "catalog_fetch_failed", at: Date.now() });
|
|
1475
|
-
}
|
|
1476
|
-
if (!this.closed) this.publish();
|
|
1477
|
-
})().finally(() => this.refreshes.delete(account.id));
|
|
1478
|
-
this.refreshes.set(account.id, promise);
|
|
1479
|
-
return promise;
|
|
1480
|
-
}
|
|
1481
|
-
summaryState() {
|
|
1482
|
-
const today = new Date(this.telemetry.now()).toLocaleDateString("en-CA", { timeZone: "Asia/Shanghai" });
|
|
1483
|
-
const daily = this.telemetry.data.days[today] ?? {};
|
|
1484
|
-
return {
|
|
1485
|
-
pluginVersion: version,
|
|
1486
|
-
enabled: this.config.sidebarUsage,
|
|
1487
|
-
today,
|
|
1488
|
-
tokens: daily.tokens ?? 0,
|
|
1489
|
-
requests: daily.requests ?? 0,
|
|
1490
|
-
unmetered: daily.unmetered ?? 0,
|
|
1491
|
-
totalTokens: Object.values(this.telemetry.data.accounts).reduce((sum, row) => sum + (row.tokens ?? 0), 0)
|
|
1492
|
-
};
|
|
1493
|
-
}
|
|
1494
|
-
state() {
|
|
1495
|
-
const status = this.router.status();
|
|
1496
|
-
return {
|
|
1497
|
-
pluginVersion: version,
|
|
1498
|
-
startup: this.startup,
|
|
1499
|
-
runtime: this.runtime?.state(),
|
|
1500
|
-
revision: this.revision,
|
|
1501
|
-
localDiscovery: this.localDiscovery ?? [],
|
|
1502
|
-
setup: this.setup,
|
|
1503
|
-
pluginUpdates: this.plugins.state(),
|
|
1504
|
-
checkins: this.checkins.state(),
|
|
1505
|
-
telemetry: this.telemetry.state(),
|
|
1506
|
-
apiService: this.api.state(),
|
|
1507
|
-
accounts: this.config.accounts.map((a) => ({
|
|
1508
|
-
id: a.id,
|
|
1509
|
-
provider: a.provider,
|
|
1510
|
-
name: a.name ?? a.id,
|
|
1511
|
-
enabled: a.enabled !== false,
|
|
1512
|
-
status: status.find((s) => s.id === a.id),
|
|
1513
|
-
login: this.loginInfo.get(a.id),
|
|
1514
|
-
usage: this.telemetry.row("accounts", a.id),
|
|
1515
|
-
modelCount: this.catalogs.get(a.id)?.models.length ?? 0,
|
|
1516
|
-
fetchedAt: this.catalogs.get(a.id)?.fetchedAt,
|
|
1517
|
-
error: this.catalogErrors.get(a.id)
|
|
1518
|
-
})),
|
|
1519
|
-
providers: PROVIDERS.map((id) => ({ id, name: PROVIDER_NAMES[id], models: this.providerModels(id).map((m) => ({
|
|
1520
|
-
...m,
|
|
1521
|
-
visible: this.modelVisible(id, m.id)
|
|
1522
|
-
})) })),
|
|
1523
|
-
presets: Object.entries(ROUTE_PRESETS).map(([id, route]) => ({
|
|
1524
|
-
id,
|
|
1525
|
-
...structuredClone(route),
|
|
1526
|
-
providerCount: route.targets.length,
|
|
1527
|
-
added: Object.hasOwn(this.config.routes, id),
|
|
1528
|
-
availableProviders: [...new Set(route.targets.filter((t) => route.freeOnly ? this.config.accounts.some((a) => a.enabled !== false && a.provider === t.provider && this.freeModels(a).length) : matchModel(id, t, this.providerModels(t.provider))).map((t) => t.provider))]
|
|
1529
|
-
})),
|
|
1530
|
-
routes: this.routeModels(),
|
|
1531
|
-
options: {
|
|
1532
|
-
cooldownMs: this.config.cooldownMs,
|
|
1533
|
-
requestTimeoutMs: this.config.requestTimeoutMs,
|
|
1534
|
-
retryServerErrors: this.config.retryServerErrors,
|
|
1535
|
-
usageRetentionDays: this.config.usageRetentionDays,
|
|
1536
|
-
sidebarUsage: this.config.sidebarUsage
|
|
1537
|
-
}
|
|
1538
|
-
};
|
|
1539
|
-
}
|
|
1540
|
-
async action(input) {
|
|
1541
|
-
if (!input || typeof input !== "object" || Array.isArray(input)) bad("\u8BF7\u6C42\u683C\u5F0F\u65E0\u6548");
|
|
1542
|
-
if (input.action === "route.preview") {
|
|
1543
|
-
if (input.route?.freeOnly) bad("\u514D\u8D39\u6A21\u578B\u8BF7\u9010\u9879\u8BBE\u7F6E\u63A8\u7406\u7B49\u7EA7");
|
|
1544
|
-
let config;
|
|
1545
|
-
try {
|
|
1546
|
-
config = validateConfig({ ...this.raw, routes: { [input.id || "preview"]: input.route } }, this.file);
|
|
1547
|
-
} catch {
|
|
1548
|
-
bad("\u8BF7\u5148\u9009\u62E9\u6709\u6548\u7684\u76EE\u6807\u6A21\u578B\u4E0E\u4E0A\u4E0B\u6587\u5BB9\u91CF");
|
|
1549
|
-
}
|
|
1550
|
-
const { reasoning, contextKnown } = this.routeModels(config.routes)[0];
|
|
1551
|
-
return { routePreview: { reasoning, contextKnown } };
|
|
1552
|
-
}
|
|
1553
|
-
if (input.action === "model.test") {
|
|
1554
|
-
const model = this.providerModels(input.provider).find((m) => m.id === input.id);
|
|
1555
|
-
if (!model) bad("\u8BF7\u5148\u5237\u65B0\u5E76\u9009\u62E9\u5DF2\u53D1\u73B0\u7684\u6A21\u578B");
|
|
1556
|
-
if (this.modelTestFlight || Date.now() - (this.modelTestAt ?? 0) < 1e4) bad("\u8BF7\u7B49\u5F85\u5F53\u524D\u6D4B\u8BD5\u5B8C\u6210\uFF0C\u6D4B\u8BD5\u95F4\u9694\u81F3\u5C11 10 \u79D2");
|
|
1557
|
-
this.modelTestFlight = true;
|
|
1558
|
-
this.modelTestAt = Date.now();
|
|
1559
|
-
this.modelTestAbort = new AbortController();
|
|
1560
|
-
const started = performance.now();
|
|
1561
|
-
let modelTest;
|
|
1562
|
-
try {
|
|
1563
|
-
const { response } = await this.router.chat(
|
|
1564
|
-
{
|
|
1565
|
-
model: `${input.provider}::${input.id}`,
|
|
1566
|
-
stream: true,
|
|
1567
|
-
messages: [{ role: "system", content: "Answer briefly." }, { role: "user", content: "Reply with OK." }],
|
|
1568
|
-
max_tokens: 16
|
|
1569
|
-
},
|
|
1570
|
-
AbortSignal.any([this.modelTestAbort.signal, AbortSignal.timeout(45e3)])
|
|
1571
|
-
);
|
|
1572
|
-
const reader = response.body.getReader();
|
|
1573
|
-
let size = 0, text = "";
|
|
1574
|
-
const decoder = new TextDecoder();
|
|
1575
|
-
try {
|
|
1576
|
-
for (; ; ) {
|
|
1577
|
-
const chunk = await reader.read();
|
|
1578
|
-
if (chunk.done) break;
|
|
1579
|
-
size += chunk.value.byteLength;
|
|
1580
|
-
if (size > 1024 * 1024) throw Error("probe_response_too_large");
|
|
1581
|
-
text += decoder.decode(chunk.value, { stream: true });
|
|
1582
|
-
}
|
|
1583
|
-
text += decoder.decode();
|
|
1584
|
-
let answered = false;
|
|
1585
|
-
for (const line of text.split(/\r?\n/).filter((l) => l.startsWith("data:"))) {
|
|
1586
|
-
if (line.slice(5).trim() === "[DONE]") continue;
|
|
1587
|
-
const payload = JSON.parse(line.slice(5));
|
|
1588
|
-
if (payload.error || payload.choices?.some((c) => c.finish_reason === "error")) throw Error("probe_stream_failed");
|
|
1589
|
-
answered ||= payload.choices?.some((c) => c.delta?.content || c.delta?.reasoning_content || c.delta?.tool_calls?.length) ?? false;
|
|
1590
|
-
}
|
|
1591
|
-
if (!answered || !/^data:\s*\[DONE\]\s*$/m.test(text)) throw Error("probe_incomplete");
|
|
1592
|
-
} finally {
|
|
1593
|
-
await reader.cancel().catch(() => {
|
|
1594
|
-
});
|
|
1595
|
-
reader.releaseLock();
|
|
1596
|
-
}
|
|
1597
|
-
modelTest = { ok: true, elapsedMs: Math.round(performance.now() - started) };
|
|
1598
|
-
} catch (error) {
|
|
1599
|
-
modelTest = {
|
|
1600
|
-
ok: false,
|
|
1601
|
-
status: Number(error.status) || 502,
|
|
1602
|
-
hint: error.hint || "\u6D4B\u8BD5\u672A\u901A\u8FC7\uFF0C\u8BF7\u67E5\u770B\u8D26\u53F7\u72B6\u6001\uFF1B\u4E0D\u4F1A\u81EA\u52A8\u91CD\u8BD5\u3002"
|
|
1603
|
-
};
|
|
1604
|
-
} finally {
|
|
1605
|
-
this.modelTestFlight = false;
|
|
1606
|
-
this.modelTestAbort = void 0;
|
|
1607
|
-
}
|
|
1608
|
-
return { ...this.state(), modelTest };
|
|
1609
|
-
}
|
|
1610
|
-
if (input.action?.startsWith("api.")) {
|
|
1611
|
-
let result;
|
|
1612
|
-
try {
|
|
1613
|
-
result = await this.api.action(input);
|
|
1614
|
-
} catch (e) {
|
|
1615
|
-
bad(e.message);
|
|
1616
|
-
}
|
|
1617
|
-
return { ...this.state(), ...result };
|
|
1618
|
-
}
|
|
1619
|
-
if (input.action === "plugin.reload") {
|
|
1620
|
-
if (!this.runtime) bad("\u5F53\u524D\u540E\u53F0\u4E0D\u652F\u6301\u91CD\u65B0\u52A0\u8F7D");
|
|
1621
|
-
this.runtime.retryAt = 0;
|
|
1622
|
-
this.runtime.schedule(0);
|
|
1623
|
-
return this.state();
|
|
1624
|
-
}
|
|
1625
|
-
if (input.action === "workbuddy.sms") {
|
|
1626
|
-
try {
|
|
1627
|
-
const workbuddyLogin = await this.workbuddyLogin.send(input.phone);
|
|
1628
|
-
return { ...this.state(), workbuddyLogin };
|
|
1629
|
-
} catch (e) {
|
|
1630
|
-
bad(e.message);
|
|
1631
|
-
}
|
|
1632
|
-
}
|
|
1633
|
-
if (input.action === "workbuddy.login") {
|
|
1634
|
-
if (input.revision !== revisionOf(JSON.parse(await readFile4(this.file, "utf8")))) throw new ManagementError(409, "\u914D\u7F6E\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u65B0\u63D0\u4EA4\u767B\u5F55");
|
|
1635
|
-
let login;
|
|
1636
|
-
try {
|
|
1637
|
-
login = await this.workbuddyLogin.complete(input.challenge, input.code, input.verify ?? true);
|
|
1638
|
-
} catch (e) {
|
|
1639
|
-
bad(e.message);
|
|
1640
|
-
}
|
|
1641
|
-
let existing;
|
|
1642
|
-
for (const a of this.config.accounts.filter((a2) => a2.provider === "workbuddy")) {
|
|
1643
|
-
try {
|
|
1644
|
-
if ((await jsonCredential(a.credentialFile)).credential.uid === login.document.credential.uid) {
|
|
1645
|
-
existing = a;
|
|
1646
|
-
break;
|
|
1647
|
-
}
|
|
1648
|
-
} catch {
|
|
1649
|
-
}
|
|
1650
|
-
}
|
|
1651
|
-
if (input.id && input.id !== existing?.id) bad("\u767B\u5F55\u8EAB\u4EFD\u4E0E\u6B63\u5728\u7F16\u8F91\u7684\u8D26\u53F7\u4E0D\u540C\uFF0C\u8BF7\u4ECE\u6DFB\u52A0\u8D26\u53F7\u5165\u53E3\u4FDD\u5B58");
|
|
1652
|
-
const state = await this.action({
|
|
1653
|
-
action: existing ? "account.credentials" : "account.add",
|
|
1654
|
-
revision: input.revision,
|
|
1655
|
-
id: existing?.id ?? `workbuddy_${randomBytes3(8).toString("hex")}`,
|
|
1656
|
-
provider: "workbuddy",
|
|
1657
|
-
name: input.name,
|
|
1658
|
-
document: login.document
|
|
1659
|
-
});
|
|
1660
|
-
this.workbuddyLogin.finish();
|
|
1661
|
-
return { ...state, loginResult: { warning: login.warning, saved: true } };
|
|
1662
|
-
}
|
|
1663
|
-
if (input.action === "checkins.verify") {
|
|
1664
|
-
const account = this.config.accounts.find((a) => a.id === input.id && a.provider === "workbuddy");
|
|
1665
|
-
if (!account) bad("\u8BF7\u9009\u62E9 WorkBuddy \u4E2D\u56FD\u7248\u8D26\u53F7");
|
|
1666
|
-
if (this.checkins.flight || this.checkins.failed) bad("\u8BF7\u7B49\u5F85\u7B7E\u5230\u5B8C\u6210\uFF0C\u6216\u5148\u4FEE\u590D\u7B7E\u5230\u8BB0\u5F55");
|
|
1667
|
-
try {
|
|
1668
|
-
await this.workbuddyLogin.reserve("verifyAt", 3e4);
|
|
1669
|
-
} catch (e) {
|
|
1670
|
-
bad(e.message);
|
|
1671
|
-
}
|
|
1672
|
-
try {
|
|
1673
|
-
const client = await this.router.client(account);
|
|
1674
|
-
await client.checkin.validate(AbortSignal.timeout(3e4));
|
|
1675
|
-
await this.checkins.credentialsUpdated(account.id);
|
|
1676
|
-
await this.readLogin(account);
|
|
1677
|
-
} catch {
|
|
1678
|
-
bad("\u767B\u5F55\u9A8C\u8BC1\u672A\u901A\u8FC7\uFF1A\u8BF7\u5728\u8D26\u53F7\u4E0E\u767B\u5F55\u4E2D\u66F4\u65B0\u51ED\u8BC1\uFF1B\u4E0D\u4F1A\u81EA\u52A8\u91CD\u8BD5\u6216\u9886\u53D6");
|
|
1679
|
-
}
|
|
1680
|
-
return this.state();
|
|
1681
|
-
}
|
|
1682
|
-
if (input.action === "checkins.run") {
|
|
1683
|
-
try {
|
|
1684
|
-
this.checkins.start(input.ids);
|
|
1685
|
-
} catch (e) {
|
|
1686
|
-
bad(e.message);
|
|
1687
|
-
}
|
|
1688
|
-
return this.state();
|
|
1689
|
-
}
|
|
1690
|
-
if (input.action === "checkins.resume") {
|
|
1691
|
-
try {
|
|
1692
|
-
await this.checkins.resume(input.provider);
|
|
1693
|
-
} catch (e) {
|
|
1694
|
-
bad(e.message);
|
|
1695
|
-
}
|
|
1696
|
-
return this.state();
|
|
1697
|
-
}
|
|
1698
|
-
if (typeof input.action === "string" && input.action.startsWith("plugins.")) {
|
|
1699
|
-
try {
|
|
1700
|
-
await this.plugins.action(input);
|
|
1701
|
-
} catch (e) {
|
|
1702
|
-
bad(e.message);
|
|
1703
|
-
}
|
|
1704
|
-
return this.state();
|
|
1705
|
-
}
|
|
1706
|
-
if (input.action === "setup.dismiss") {
|
|
1707
|
-
await withFileLock3(this.setupFile, async () => {
|
|
1708
|
-
this.setup = { ...this.setup, completedAt: this.setup?.completedAt ?? Date.now(), dismissed: true };
|
|
1709
|
-
await writePrivateJson(this.setupFile, this.setup);
|
|
1710
|
-
});
|
|
1711
|
-
return this.state();
|
|
1712
|
-
}
|
|
1713
|
-
if (input.action === "account.export") {
|
|
1714
|
-
if (input.revision !== revisionOf(JSON.parse(await readFile4(this.file, "utf8")))) throw new ManagementError(409, "\u914D\u7F6E\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u540E\u63D0\u53D6\u51ED\u8BC1");
|
|
1715
|
-
const account = this.config.accounts.find((a) => a.id === input.id);
|
|
1716
|
-
if (!account) bad("\u8D26\u53F7\u4E0D\u5B58\u5728");
|
|
1717
|
-
let credential;
|
|
1718
|
-
try {
|
|
1719
|
-
credential = await readPortableCredential(account);
|
|
1720
|
-
} catch {
|
|
1721
|
-
bad("\u8D26\u53F7\u51ED\u8BC1\u4E0D\u53EF\u8BFB\uFF0C\u8BF7\u8865\u5145\u6216\u66F4\u65B0\u540E\u518D\u63D0\u53D6");
|
|
1722
|
-
}
|
|
1723
|
-
if (!credential) bad("\u8D26\u53F7\u5C1A\u672A\u4FDD\u5B58\u51ED\u8BC1\uFF0C\u8BF7\u5148\u8865\u5145\u767B\u5F55\u4FE1\u606F");
|
|
1724
|
-
return { ...this.state(), exported: {
|
|
1725
|
-
filename: `dsh-account-${account.provider}-${account.id}.json`,
|
|
1726
|
-
document: {
|
|
1727
|
-
...credential.document,
|
|
1728
|
-
...credential.identity ? { identity: credential.identity } : {},
|
|
1729
|
-
format: "dsh-multi-router-account",
|
|
1730
|
-
provider: account.provider,
|
|
1731
|
-
name: account.name || account.id
|
|
1732
|
-
}
|
|
1733
|
-
} };
|
|
1734
|
-
}
|
|
1735
|
-
if (input.action === "config.export") {
|
|
1736
|
-
if (input.revision !== revisionOf(JSON.parse(await readFile4(this.file, "utf8")))) throw new ManagementError(409, "\u914D\u7F6E\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u540E\u5BFC\u51FA");
|
|
1737
|
-
try {
|
|
1738
|
-
return { ...this.state(), exported: { filename: `dsh-multi-router-${input.includeCredentials ? "encrypted" : "settings"}-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.json`, document: await exportConfiguration(this, input) } };
|
|
1739
|
-
} catch (e) {
|
|
1740
|
-
bad(e.message);
|
|
1741
|
-
}
|
|
1742
|
-
}
|
|
1743
|
-
if (input.action === "config.preview") {
|
|
1744
|
-
try {
|
|
1745
|
-
const portable = await decodeConfiguration(input);
|
|
1746
|
-
return { ...this.state(), importPreview: migrationSummary(this, portable, input.mode) };
|
|
1747
|
-
} catch (e) {
|
|
1748
|
-
bad(e.message);
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
if (input.action === "monitor.refresh" || input.action === "account.refresh") {
|
|
1752
|
-
if (input.provider && !PROVIDERS.includes(input.provider)) bad("\u672A\u77E5\u4F9B\u5E94\u5546");
|
|
1753
|
-
if (input.action === "account.refresh") {
|
|
1754
|
-
const account = this.config.accounts.find((a) => a.id === input.id);
|
|
1755
|
-
if (!account) bad("\u8D26\u53F7\u4E0D\u5B58\u5728");
|
|
1756
|
-
this.router.clients.delete(account.id);
|
|
1757
|
-
await this.monitorAccount(account, { force: true });
|
|
1758
|
-
} else {
|
|
1759
|
-
await this.syncLocalAccounts(input.provider);
|
|
1760
|
-
await this.monitorAll(input.provider, { force: true });
|
|
1761
|
-
}
|
|
1762
|
-
return this.state();
|
|
1763
|
-
}
|
|
1764
|
-
if (input.action === "accounts.discover") {
|
|
1765
|
-
if (input.provider && !PROVIDERS.includes(input.provider)) bad("\u672A\u77E5\u4F9B\u5E94\u5546");
|
|
1766
|
-
await this.syncLocalAccounts(input.provider, true);
|
|
1767
|
-
await Promise.all([this.monitorAll(input.provider), ...this.config.accounts.filter((a) => !input.provider || a.provider === input.provider).map((a) => this.refreshAccount(a, true))]);
|
|
1768
|
-
return this.state();
|
|
1769
|
-
}
|
|
1770
|
-
if (input.action === "account.capture") {
|
|
1771
|
-
if (!PROVIDERS.includes(input.provider)) bad("\u672A\u77E5\u4F9B\u5E94\u5546");
|
|
1772
|
-
input = { ...input, action: "account.add", id: `${input.provider.replace("-", "_")}_${randomBytes3(4).toString("hex")}`, capture: true };
|
|
1773
|
-
}
|
|
1774
|
-
if (input.action === "refresh") {
|
|
1775
|
-
if (input.provider && !PROVIDERS.includes(input.provider)) bad("\u672A\u77E5\u4F9B\u5E94\u5546");
|
|
1776
|
-
await Promise.all((input.provider ? [input.provider] : PROVIDERS).map((p) => this.ensureCatalog(p, true)));
|
|
1777
|
-
return this.state();
|
|
1778
|
-
}
|
|
1779
|
-
if (input.action === "free.refresh") {
|
|
1780
|
-
await this.checkFreeModels(true);
|
|
1781
|
-
return this.state();
|
|
1782
|
-
}
|
|
1783
|
-
return withFileLock3(this.file, async () => {
|
|
1784
|
-
let raw = JSON.parse(await readFile4(this.file, "utf8"));
|
|
1785
|
-
if (input.revision !== revisionOf(raw)) throw new ManagementError(409, "\u914D\u7F6E\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u540E\u518D\u4FDD\u5B58");
|
|
1786
|
-
raw.routes ??= structuredClone(this.config.routes);
|
|
1787
|
-
let staging;
|
|
1788
|
-
try {
|
|
1789
|
-
const account = raw.accounts.find((a) => a.id === input.id);
|
|
1790
|
-
if (input.action === "config.import") {
|
|
1791
|
-
try {
|
|
1792
|
-
const prepared = await prepareMigration(this, raw, await decodeConfiguration(input), input.mode);
|
|
1793
|
-
staging = prepared.staging;
|
|
1794
|
-
await writePrivateJson(`${this.file}.before-import-${Date.now()}-${randomBytes3(3).toString("hex")}.bak`, raw);
|
|
1795
|
-
raw = prepared.raw;
|
|
1796
|
-
} catch (e) {
|
|
1797
|
-
bad(e.message);
|
|
1798
|
-
}
|
|
1799
|
-
} else if (input.action === "account.add" || input.action === "account.credentials") {
|
|
1800
|
-
const replacing = input.action === "account.credentials";
|
|
1801
|
-
if (replacing) {
|
|
1802
|
-
if (!account) bad("\u8D26\u53F7\u4E0D\u5B58\u5728");
|
|
1803
|
-
input = { ...input, provider: account.provider };
|
|
1804
|
-
}
|
|
1805
|
-
if (!/^[a-zA-Z0-9_-]{1,64}$/.test(input.id ?? "") || !PROVIDERS.includes(input.provider)) bad("\u8D26\u53F7 ID \u6216\u4F9B\u5E94\u5546\u65E0\u6548");
|
|
1806
|
-
if (account && !replacing) bad("\u8D26\u53F7 ID \u5DF2\u5B58\u5728\uFF0C\u8BF7\u4F7F\u7528\u4E0D\u540C ID");
|
|
1807
|
-
const base = join5(dirname3(this.file), "accounts");
|
|
1808
|
-
await mkdir2(base, { recursive: true, mode: 448 });
|
|
1809
|
-
staging = await mkdtemp2(join5(base, `${input.id}-`));
|
|
1810
|
-
const credentialFile = join5(staging, "credential.json"), identityFile = join5(staging, "identity.json");
|
|
1811
|
-
const added = {
|
|
1812
|
-
id: input.id,
|
|
1813
|
-
name: input.name || account?.name || input.id,
|
|
1814
|
-
provider: input.provider,
|
|
1815
|
-
credentialFile: relative2(dirname3(this.file), credentialFile),
|
|
1816
|
-
...input.provider.startsWith("trae") ? { identityFile: relative2(dirname3(this.file), identityFile) } : {}
|
|
1817
|
-
};
|
|
1818
|
-
if (replacing) raw.accounts[raw.accounts.indexOf(account)] = { ...account, ...added, enabled: true };
|
|
1819
|
-
else raw.accounts.push(added);
|
|
1820
|
-
validateConfig(raw, this.file);
|
|
1821
|
-
if (input.document !== void 0) {
|
|
1822
|
-
try {
|
|
1823
|
-
await importAccount({ provider: input.provider, credentialFile, identityFile, document: input.document, identityDocument: input.identityDocument });
|
|
1824
|
-
} catch {
|
|
1825
|
-
bad("\u51ED\u8BC1\u683C\u5F0F\u65E0\u6548\uFF1A\u8BF7\u68C0\u67E5\u5FC5\u586B\u5B57\u6BB5\u3001\u533A\u57DF\u548C Trae \u8BBE\u5907\u8EAB\u4EFD");
|
|
1826
|
-
}
|
|
1827
|
-
} else if (input.provider.startsWith("qoder") && input.pat) {
|
|
1828
|
-
if (typeof input.pat !== "string" || input.pat.length > 8192 || /\s/.test(input.pat.trim())) bad("PAT \u683C\u5F0F\u65E0\u6548");
|
|
1829
|
-
await writePrivateJson(credentialFile, {
|
|
1830
|
-
version: 2,
|
|
1831
|
-
pat: input.pat.trim(),
|
|
1832
|
-
region: input.provider === "qoder" ? "china" : "global",
|
|
1833
|
-
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1834
|
-
});
|
|
1835
|
-
} else {
|
|
1836
|
-
const source = input.source;
|
|
1837
|
-
if (!source && (input.provider.startsWith("workbuddy") || input.provider.startsWith("trae"))) {
|
|
1838
|
-
const discovered = await discoverLocalAccounts(input.provider);
|
|
1839
|
-
const item = discovered.accounts.find((a) => !input.edition || input.edition === "auto" || a.document.credential.edition === input.edition);
|
|
1840
|
-
if (!item) bad(discovered.status.some((s) => s.state === "unreadable") ? "\u627E\u5230\u672C\u673A\u767B\u5F55\u6587\u4EF6\uFF0C\u4F46\u51ED\u8BC1\u6216\u8BBE\u5907\u8EAB\u4EFD\u65E0\u6CD5\u8BFB\u53D6\u3002\u8BF7\u5728\u5B98\u65B9\u5BA2\u6237\u7AEF\u91CD\u65B0\u767B\u5F55\u540E\u91CD\u8BD5\uFF0C\u6216\u624B\u52A8\u5BFC\u5165\u51ED\u8BC1\u3002" : "\u672A\u627E\u5230\u6240\u9009\u533A\u57DF\u7684\u672C\u673A\u767B\u5F55\u3002\u8BF7\u5148\u5728\u5B98\u65B9\u5BA2\u6237\u7AEF\u767B\u5F55\uFF0C\u518D\u70B9\u51FB\u91CD\u65B0\u68C0\u6D4B\u3002");
|
|
1841
|
-
try {
|
|
1842
|
-
await importAccount({ ...item, credentialFile, identityFile });
|
|
1843
|
-
} catch {
|
|
1844
|
-
bad("\u672C\u673A\u51ED\u8BC1\u5BFC\u5165\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u5BA2\u6237\u7AEF\u767B\u5F55\u72B6\u6001\u4E0E\u8BBE\u5907\u8EAB\u4EFD");
|
|
1845
|
-
}
|
|
1846
|
-
} else {
|
|
1847
|
-
const file = source || (input.provider.startsWith("qoder") ? qoderOwnAuthPath(QODER_VARIANTS.find((v) => v.id === input.provider)) : void 0);
|
|
1848
|
-
if (typeof file !== "string" || !file.trim()) bad("\u672A\u627E\u5230\u767B\u5F55\u6587\u4EF6\uFF0C\u8BF7\u586B\u5199\u6765\u6E90\u8DEF\u5F84\u6216 Qoder PAT");
|
|
1849
|
-
try {
|
|
1850
|
-
await importAccount({
|
|
1851
|
-
provider: input.provider,
|
|
1852
|
-
source: resolve(file),
|
|
1853
|
-
credentialFile,
|
|
1854
|
-
identityFile,
|
|
1855
|
-
edition: input.edition,
|
|
1856
|
-
identitySource: input.identity ? resolve(input.identity) : void 0
|
|
1857
|
-
});
|
|
1858
|
-
} catch {
|
|
1859
|
-
bad("\u8D26\u53F7\u5BFC\u5165\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u767B\u5F55\u6587\u4EF6\u3001\u533A\u57DF\u3001Trae edition \u548C\u8BBE\u5907\u8EAB\u4EFD");
|
|
1860
|
-
}
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
const imported = await jsonCredential(credentialFile);
|
|
1864
|
-
const fingerprint = (value) => input.provider.startsWith("qoder") ? value.pat : value.credential?.uid || value.credential?.userId;
|
|
1865
|
-
for (const existing of input.capture || input.document !== void 0 ? this.config.accounts.filter((a) => a.provider === input.provider && a.id !== input.id) : []) {
|
|
1866
|
-
let value;
|
|
1867
|
-
try {
|
|
1868
|
-
value = await jsonCredential(existing.credentialFile);
|
|
1869
|
-
} catch {
|
|
1870
|
-
continue;
|
|
1871
|
-
}
|
|
1872
|
-
if (fingerprint(value) && fingerprint(value) === fingerprint(imported)) bad(`\u6B64\u767B\u5F55\u8D26\u53F7\u5DF2\u5728\u8D26\u53F7\u6C60\u4E2D\uFF1A${existing.name || existing.id}\uFF0C\u8BF7\u5207\u6362\u5BA2\u6237\u7AEF\u8D26\u53F7\u540E\u518D\u83B7\u53D6`);
|
|
1873
|
-
}
|
|
1874
|
-
if (!input.name && !replacing) added.name = imported.credential?.nickname || imported.credential?.accountName || input.id;
|
|
1875
|
-
} else if (input.action === "account.update") {
|
|
1876
|
-
if (!account) bad("\u8D26\u53F7\u4E0D\u5B58\u5728");
|
|
1877
|
-
if (input.autoCheckin !== void 0) {
|
|
1878
|
-
if (typeof input.autoCheckin !== "boolean" || !supportsCheckin(account.provider)) bad("\u6B64\u5E73\u53F0\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u7B7E\u5230");
|
|
1879
|
-
account.autoCheckin = input.autoCheckin;
|
|
1880
|
-
}
|
|
1881
|
-
if (input.name !== void 0) account.name = input.name;
|
|
1882
|
-
if (input.enabled !== void 0) account.enabled = input.enabled;
|
|
1883
|
-
} else if (input.action === "accounts.enabled") {
|
|
1884
|
-
if (input.provider !== void 0 && !PROVIDERS.includes(input.provider) || typeof input.enabled !== "boolean") bad("\u4F9B\u5E94\u5546\u6216\u8DEF\u7531\u53C2\u4E0E\u5F00\u5173\u65E0\u6548");
|
|
1885
|
-
for (const a of raw.accounts.filter((a2) => !input.provider || a2.provider === input.provider)) a.enabled = input.enabled;
|
|
1886
|
-
} else if (input.action === "account.move") {
|
|
1887
|
-
if (!account || ![-1, 1].includes(input.direction)) bad("\u8D26\u53F7\u6216\u987A\u5E8F\u65E0\u6548");
|
|
1888
|
-
const group = raw.accounts.filter((a) => a.provider === account.provider), position = group.indexOf(account) + input.direction;
|
|
1889
|
-
if (position >= 0 && position < group.length) {
|
|
1890
|
-
const index = raw.accounts.indexOf(account), next = raw.accounts.indexOf(group[position]);
|
|
1891
|
-
[raw.accounts[index], raw.accounts[next]] = [raw.accounts[next], raw.accounts[index]];
|
|
1892
|
-
}
|
|
1893
|
-
} else if (input.action === "account.remove") {
|
|
1894
|
-
if (!account) bad("\u8D26\u53F7\u4E0D\u5B58\u5728");
|
|
1895
|
-
if (Object.values(raw.routes).some((r) => r.targets.some((t) => t.account === input.id))) bad("\u8BF7\u5148\u79FB\u9664\u8DEF\u7531\u4E2D\u7ED1\u5B9A\u6B64\u8D26\u53F7\u7684\u76EE\u6807");
|
|
1896
|
-
if (account.provider.startsWith("workbuddy") || account.provider.startsWith("trae")) {
|
|
1897
|
-
try {
|
|
1898
|
-
raw.ignoredLocalAccounts = [.../* @__PURE__ */ new Set([...raw.ignoredLocalAccounts ?? [], localAccountKey(account.provider, await jsonCredential(this.config.accounts.find((a) => a.id === account.id).credentialFile))])];
|
|
1899
|
-
} catch {
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
raw.accounts = raw.accounts.filter((a) => a.id !== input.id);
|
|
1903
|
-
} else if (input.action === "models.visibility") {
|
|
1904
|
-
if (input.provider !== void 0 && !PROVIDERS.includes(input.provider) || typeof input.visible !== "boolean") bad("\u4F9B\u5E94\u5546\u6216\u5C55\u793A\u5F00\u5173\u65E0\u6548");
|
|
1905
|
-
raw.modelVisibility = { ...raw.modelVisibility };
|
|
1906
|
-
raw.models = { ...raw.models };
|
|
1907
|
-
for (const provider of input.provider ? [input.provider] : PROVIDERS) {
|
|
1908
|
-
raw.modelVisibility[provider] = input.visible;
|
|
1909
|
-
raw.models[provider] = Object.fromEntries(Object.entries(raw.models[provider] ?? {}).map(([id, pref]) => [id, { ...pref, visible: input.visible }]));
|
|
1910
|
-
}
|
|
1911
|
-
} else if (input.action === "model.update") {
|
|
1912
|
-
if (!PROVIDERS.includes(input.provider) || typeof input.id !== "string" || !input.id || input.id.length > 256) bad("\u6A21\u578B\u65E0\u6548");
|
|
1913
|
-
const model = this.providerModels(input.provider).find((m) => m.id === input.id);
|
|
1914
|
-
if (input.contextWindow != null && (!Number.isSafeInteger(input.contextWindow) || input.contextWindow <= 0 || !model || input.contextWindow > model.maxContextWindow)) bad("\u4E0A\u4E0B\u6587\u9884\u7B97\u4E0D\u80FD\u8D85\u8FC7\u8BE5\u6E20\u9053\u6A21\u578B\u7684\u6700\u5927\u5BB9\u91CF");
|
|
1915
|
-
if (input.supportsImages === true && !model?.imageCapable) bad("\u8BE5\u6A21\u578B\u672A\u58F0\u660E\u56FE\u7247\u8F93\u5165\u80FD\u529B");
|
|
1916
|
-
if (input.reasoningEffort != null && !model?.reasoning?.supportedEfforts.includes(input.reasoningEffort)) bad("\u6B64\u6A21\u578B\u4E0D\u652F\u6301\u6240\u9009\u63A8\u7406\u7B49\u7EA7");
|
|
1917
|
-
raw.models = { ...raw.models, [input.provider]: {
|
|
1918
|
-
...raw.models?.[input.provider],
|
|
1919
|
-
[input.id]: {
|
|
1920
|
-
...raw.models?.[input.provider]?.[input.id],
|
|
1921
|
-
...Object.fromEntries(["name", "visible", "contextWindow", "supportsImages", "reasoningEffort"].filter((k) => input[k] !== void 0).map((k) => [k, input[k]]))
|
|
1922
|
-
}
|
|
1923
|
-
} };
|
|
1924
|
-
} else if (input.action === "free.model.update") {
|
|
1925
|
-
const parent = raw.routes[input.parentId];
|
|
1926
|
-
if (!parent?.freeOnly || !this.freeRoutes(input.parentId, this.config.routes[input.parentId]).some((m) => m.id === input.id)) bad("\u514D\u8D39\u6A21\u578B\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u9875\u9762");
|
|
1927
|
-
const model = this.freeRoutes(input.parentId, this.config.routes[input.parentId]).find((m) => m.id === input.id);
|
|
1928
|
-
if (input.contextWindow != null && (!Number.isSafeInteger(input.contextWindow) || input.contextWindow <= 0 || input.contextWindow > model.maxContextWindow)) bad("\u4E0A\u4E0B\u6587\u9884\u7B97\u4E0D\u80FD\u8D85\u8FC7\u8BE5\u514D\u8D39\u6A21\u578B\u7684\u6700\u5927\u5BB9\u91CF");
|
|
1929
|
-
if (input.reasoningEffort !== void 0 && input.reasoningEffort !== null && !model.reasoning?.supportedEfforts.includes(input.reasoningEffort)) bad("\u6B64\u514D\u8D39\u6A21\u578B\u4E0D\u652F\u6301\u6240\u9009\u63A8\u7406\u7B49\u7EA7");
|
|
1930
|
-
parent.freeModels = { ...parent.freeModels, [input.id]: {
|
|
1931
|
-
...parent.freeModels?.[input.id],
|
|
1932
|
-
...input.visible !== void 0 ? { visible: input.visible } : {},
|
|
1933
|
-
...input.contextWindow !== void 0 ? { contextWindow: input.contextWindow } : {},
|
|
1934
|
-
...input.reasoningEffort !== void 0 ? { reasoningEffort: input.reasoningEffort } : {}
|
|
1935
|
-
} };
|
|
1936
|
-
} else if (input.action === "route.save") {
|
|
1937
|
-
if (input.id === void 0 || input.id === "") {
|
|
1938
|
-
input.id = input.previousId || `route_${randomBytes3(8).toString("hex")}`;
|
|
1939
|
-
}
|
|
1940
|
-
if (!/^[a-zA-Z0-9_.-]{1,128}$/.test(input.id ?? "")) bad("\u8DEF\u7531 ID \u4EC5\u652F\u6301\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u70B9\u3001\u4E0B\u5212\u7EBF\u548C\u77ED\u6A2A\u7EBF");
|
|
1941
|
-
if (input.previousId && !Object.hasOwn(raw.routes, input.previousId)) bad("\u539F\u8DEF\u7531\u4E0D\u5B58\u5728\uFF0C\u8BF7\u5237\u65B0\u9875\u9762");
|
|
1942
|
-
if (input.previousId !== input.id && Object.hasOwn(raw.routes, input.id)) bad("\u8DEF\u7531 ID \u5DF2\u5B58\u5728");
|
|
1943
|
-
raw.routes = { ...raw.routes, [input.id]: input.route };
|
|
1944
|
-
if (input.previousId && input.previousId !== input.id) delete raw.routes[input.previousId];
|
|
1945
|
-
} else if (input.action === "route.force-expiring") {
|
|
1946
|
-
if (!Object.hasOwn(raw.routes, input.id)) bad("\u8DEF\u7531\u4E0D\u5B58\u5728");
|
|
1947
|
-
raw.routes[input.id].preferExpiringCredits = true;
|
|
1948
|
-
await this.telemetry.restartExpiring(input.id);
|
|
1949
|
-
const route = this.router.config.routes[input.id], strategy = route.strategy ?? "failover";
|
|
1950
|
-
if (["tokens", "credits"].includes(strategy)) await this.telemetry.advance(input.id, this.router.candidates(input.id), JSON.stringify({ strategy, targets: route.targets }));
|
|
1951
|
-
} else if (input.action === "route.remove") {
|
|
1952
|
-
if (!Object.hasOwn(raw.routes, input.id)) bad("\u8DEF\u7531\u4E0D\u5B58\u5728");
|
|
1953
|
-
if (Object.hasOwn(ROUTE_PRESETS, input.id)) raw.seenRoutePresets = [.../* @__PURE__ */ new Set([...raw.seenRoutePresets ?? [], input.id])];
|
|
1954
|
-
delete raw.routes[input.id];
|
|
1955
|
-
} else if (input.action === "preset.add") {
|
|
1956
|
-
const id = input.id ?? "deepseek-v4.1-flash";
|
|
1957
|
-
if (!Object.hasOwn(ROUTE_PRESETS, id)) bad("\u672A\u77E5\u9884\u914D\u7F6E\u8DEF\u7531");
|
|
1958
|
-
if (Object.hasOwn(raw.routes, id)) bad("\u9884\u914D\u7F6E\u8DEF\u7531\u5DF2\u5B58\u5728\uFF0C\u4E0D\u8986\u76D6\u5DF2\u6709\u8BBE\u7F6E");
|
|
1959
|
-
raw.routes[id] = structuredClone(ROUTE_PRESETS[id]);
|
|
1960
|
-
} else if (input.action === "preset.add-all") {
|
|
1961
|
-
for (const [id, route] of Object.entries(ROUTE_PRESETS)) if (!Object.hasOwn(raw.routes, id)) raw.routes[id] = structuredClone(route);
|
|
1962
|
-
} else if (input.action === "checkins.settings") {
|
|
1963
|
-
if (this.checkins.flight) bad("\u8BF7\u7B49\u5F85\u5F53\u524D\u7B7E\u5230\u5B8C\u6210\u540E\u4FEE\u6539\u8BA1\u5212");
|
|
1964
|
-
if (!input.schedule) bad("\u8BF7\u586B\u5199\u7B7E\u5230\u8BA1\u5212");
|
|
1965
|
-
raw.checkinSchedule = input.schedule;
|
|
1966
|
-
} else if (input.action === "options.save") {
|
|
1967
|
-
for (const key of ["cooldownMs", "requestTimeoutMs", "retryServerErrors", "usageRetentionDays", "sidebarUsage"]) if (input[key] !== void 0) raw[key] = input[key];
|
|
1968
|
-
} else bad("\u672A\u77E5\u64CD\u4F5C");
|
|
1969
|
-
let config;
|
|
1970
|
-
try {
|
|
1971
|
-
config = validateConfig(raw, this.file);
|
|
1972
|
-
} catch (error) {
|
|
1973
|
-
bad(error.message);
|
|
1974
|
-
}
|
|
1975
|
-
if (input.action === "route.save" && input.route.reasoningEffort != null) {
|
|
1976
|
-
const previous = this.config.routes[input.previousId], route = config.routes[input.id];
|
|
1977
|
-
const bindings = (r) => JSON.stringify(r.targets.map((t) => ({
|
|
1978
|
-
provider: t.provider,
|
|
1979
|
-
account: t.account,
|
|
1980
|
-
models: t.models ?? (t.model ? [t.model] : [])
|
|
1981
|
-
})));
|
|
1982
|
-
const unchanged = previous?.reasoningEffort === route.reasoningEffort && bindings(previous) === bindings(route) && previous.contextMode === route.contextMode && (route.contextMode === "maximum" || previous.contextWindow === route.contextWindow);
|
|
1983
|
-
if (!unchanged && !this.routeModels({ [input.id]: route })[0].reasoning?.supportedEfforts.includes(route.reasoningEffort)) bad("\u76EE\u6807\u6A21\u578B\u5728\u5F53\u524D\u4E0A\u4E0B\u6587\u5BB9\u91CF\u4E0B\u4E0D\u652F\u6301\u6240\u9009\u601D\u8003\u6DF1\u5EA6\uFF0C\u8BF7\u91CD\u65B0\u9009\u62E9");
|
|
1984
|
-
}
|
|
1985
|
-
await writePrivateJson(this.file, raw);
|
|
1986
|
-
staging = void 0;
|
|
1987
|
-
this.raw = raw;
|
|
1988
|
-
this.config = config;
|
|
1989
|
-
for (const id of this.catalogs.keys()) if (!config.accounts.some((a) => a.id === id && a.credentialFile === this.catalogs.get(id).credentialFile)) {
|
|
1990
|
-
this.catalogs.delete(id);
|
|
1991
|
-
this.catalogErrors.delete(id);
|
|
1992
|
-
}
|
|
1993
|
-
await Promise.all(config.accounts.map((a) => this.readLogin(a)));
|
|
1994
|
-
this.publish();
|
|
1995
|
-
await this.telemetry.persist();
|
|
1996
|
-
} finally {
|
|
1997
|
-
if (staging) await rm3(staging, { recursive: true, force: true });
|
|
1998
|
-
}
|
|
1999
|
-
return this.state();
|
|
2000
|
-
}, { waitMs: 3e4 }).then(async (state) => {
|
|
2001
|
-
if (input.action === "config.import") {
|
|
2002
|
-
void Promise.all([this.monitorAll(), ...PROVIDERS.map((p) => this.ensureCatalog(p, true))]).catch(() => {
|
|
2003
|
-
});
|
|
2004
|
-
}
|
|
2005
|
-
if (input.action === "preset.add" || input.action === "preset.add-all") {
|
|
2006
|
-
await this.checkFreeModels();
|
|
2007
|
-
return this.state();
|
|
2008
|
-
}
|
|
2009
|
-
if (input.action === "account.add" || input.action === "account.credentials") {
|
|
2010
|
-
const account = this.config.accounts.find((a) => a.id === input.id);
|
|
2011
|
-
if (input.action === "account.credentials" && !this.checkins.failed) await this.checkins.credentialsUpdated(input.id);
|
|
2012
|
-
if (account) {
|
|
2013
|
-
await Promise.all([this.refreshAccount(account, true), this.monitorAccount(account)]);
|
|
2014
|
-
return this.state();
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
return state;
|
|
2018
|
-
});
|
|
2019
|
-
}
|
|
2020
|
-
watch(onError) {
|
|
2021
|
-
let timer;
|
|
2022
|
-
const watcher = watch2(dirname3(this.file), (_, filename) => {
|
|
2023
|
-
if (filename && String(filename) !== this.file.split(/[\\/]/).pop()) return;
|
|
2024
|
-
clearTimeout(timer);
|
|
2025
|
-
timer = setTimeout(() => {
|
|
2026
|
-
void withFileLock3(this.file, async () => {
|
|
2027
|
-
const raw = JSON.parse(await readFile4(this.file, "utf8"));
|
|
2028
|
-
if (this.closed || revisionOf(raw) === this.revision) return;
|
|
2029
|
-
const config = validateConfig(raw, this.file);
|
|
2030
|
-
this.raw = raw;
|
|
2031
|
-
this.config = config;
|
|
2032
|
-
this.publish();
|
|
2033
|
-
await this.telemetry.persist();
|
|
2034
|
-
}).catch(() => onError?.());
|
|
2035
|
-
}, 150);
|
|
2036
|
-
});
|
|
2037
|
-
return () => {
|
|
2038
|
-
this.closed = true;
|
|
2039
|
-
clearTimeout(timer);
|
|
2040
|
-
watcher.close();
|
|
2041
|
-
};
|
|
2042
|
-
}
|
|
2043
|
-
};
|
|
2044
|
-
function managementHandler(manager, { summaryOnly = false } = {}) {
|
|
2045
|
-
const key = randomBytes3(32).toString("base64url");
|
|
2046
|
-
const json = (res, status, value) => {
|
|
2047
|
-
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" });
|
|
2048
|
-
res.end(JSON.stringify(value));
|
|
2049
|
-
};
|
|
2050
|
-
return async (req, res) => {
|
|
2051
|
-
manager.requests = (manager.requests ?? 0) + 1;
|
|
2052
|
-
try {
|
|
2053
|
-
const remote = req.socket?.remoteAddress;
|
|
2054
|
-
if (!hostIsLoopback(req.headers.host) || remote && !["127.0.0.1", "::1", "::ffff:127.0.0.1"].includes(remote)) throw new ManagementError(403, "\u4EC5\u5141\u8BB8\u672C\u673A\u8BF7\u6C42");
|
|
2055
|
-
if (req.headers.origin && req.headers.origin !== `http://${req.headers.host}` && req.headers.origin !== `https://${req.headers.host}`) throw new ManagementError(403, "\u6765\u6E90\u65E0\u6548");
|
|
2056
|
-
if (req.headers["sec-fetch-site"] === "cross-site") throw new ManagementError(403, "\u6765\u6E90\u65E0\u6548");
|
|
2057
|
-
if (req.method === "GET") {
|
|
2058
|
-
json(res, 200, summaryOnly ? manager.summaryState() : { ...manager.state(), key });
|
|
2059
|
-
return;
|
|
2060
|
-
}
|
|
2061
|
-
if (summaryOnly) throw new ManagementError(405, "\u65B9\u6CD5\u65E0\u6548");
|
|
2062
|
-
if (req.method !== "POST") throw new ManagementError(405, "\u65B9\u6CD5\u65E0\u6548");
|
|
2063
|
-
const actual = Buffer.from(req.headers["x-multi-router-key"] ?? ""), expected = Buffer.from(key);
|
|
2064
|
-
if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) throw new ManagementError(401, "\u8BF7\u5237\u65B0\u914D\u7F6E\u9875\u9762");
|
|
2065
|
-
if (!/^application\/json(?:\s*;|$)/i.test(req.headers["content-type"] ?? "")) throw new ManagementError(415, "\u9700\u8981 JSON");
|
|
2066
|
-
let size = 0, chunks = [];
|
|
2067
|
-
for await (const chunk of req) {
|
|
2068
|
-
size += chunk.length;
|
|
2069
|
-
if (size > 1024 * 1024) throw new ManagementError(413, "\u8BF7\u6C42\u8FC7\u5927");
|
|
2070
|
-
chunks.push(chunk);
|
|
2071
|
-
}
|
|
2072
|
-
let input;
|
|
2073
|
-
try {
|
|
2074
|
-
input = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
2075
|
-
} catch {
|
|
2076
|
-
throw new ManagementError(400, "JSON \u65E0\u6548");
|
|
2077
|
-
}
|
|
2078
|
-
json(res, 200, { ...await manager.action(input), key });
|
|
2079
|
-
} catch (error) {
|
|
2080
|
-
json(
|
|
2081
|
-
res,
|
|
2082
|
-
error instanceof ManagementError ? error.status : 500,
|
|
2083
|
-
{ error: error instanceof ManagementError ? error.message : "\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u914D\u7F6E\u548C\u6765\u6E90\u6587\u4EF6" }
|
|
2084
|
-
);
|
|
2085
|
-
} finally {
|
|
2086
|
-
manager.requests--;
|
|
2087
|
-
}
|
|
2088
|
-
};
|
|
2089
|
-
}
|
|
2090
|
-
|
|
2091
|
-
export {
|
|
2092
|
-
version,
|
|
2093
|
-
PluginReload,
|
|
2094
|
-
MANAGEMENT_PATH,
|
|
2095
|
-
SUMMARY_PATH,
|
|
2096
|
-
ManagementError,
|
|
2097
|
-
modelInfo,
|
|
2098
|
-
Manager,
|
|
2099
|
-
managementHandler
|
|
2100
|
-
};
|
|
2101
|
-
//# sourceMappingURL=chunk-V37VG7MN.js.map
|