dsh-multi-router 0.3.8 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -34,13 +34,13 @@ import {
34
34
  import { createHash, randomBytes as randomBytes2, timingSafeEqual } from "node:crypto";
35
35
 
36
36
  // package.json
37
- var version = "0.3.8";
37
+ var version = "0.4.1";
38
38
 
39
39
  // src/manager.mjs
40
- import { readFile, mkdir as mkdir2, mkdtemp as mkdtemp2, rm as rm2 } from "node:fs/promises";
40
+ import { readFile as readFile2, mkdir as mkdir2, mkdtemp as mkdtemp2, rm as rm3 } from "node:fs/promises";
41
41
  import { watch } from "node:fs";
42
- import { dirname as dirname3, join as join3, relative as relative2, resolve } from "node:path";
43
- import { withFileLock } from "@deepseek-ai/dsh-atomic-write";
42
+ import { dirname as dirname3, join as join4, relative as relative2, resolve } from "node:path";
43
+ import { withFileLock as withFileLock2 } from "@deepseek-ai/dsh-atomic-write";
44
44
 
45
45
  // upstream/qoder/src/status-paths.ts
46
46
  var QODER_STATUS_PATH = "/plugins/dsh-qoder-connect/status";
@@ -233,6 +233,400 @@ async function prepareMigration(manager, raw, portable, mode) {
233
233
  }
234
234
  }
235
235
 
236
+ // src/plugin-updates.mjs
237
+ import { readFile, stat, rm as rm2 } from "node:fs/promises";
238
+ import { join as join3 } from "node:path";
239
+ import { randomUUID } from "node:crypto";
240
+ import { createRequire } from "node:module";
241
+ import { pathToFileURL } from "node:url";
242
+ import { parse as parseYaml } from "yaml";
243
+ import { withFileLock, writeFileAtomic as writeFileAtomic2 } from "@deepseek-ai/dsh-atomic-write";
244
+
245
+ // upstream/workbuddy/src/update.ts
246
+ var WORKBUDDY_REPOSITORY_URL = "https://github.com/corrinehu/dsh-workbuddy-connect";
247
+ var WORKBUDDY_UPDATE_PACKAGE_NAME = "dsh-workbuddy-connect";
248
+ var WORKBUDDY_UPDATE_NPM_METADATA_URL = `https://registry.npmjs.org/-/package/${WORKBUDDY_UPDATE_PACKAGE_NAME}/dist-tags`;
249
+ var WORKBUDDY_UPDATE_RELEASES_API_URL = `${WORKBUDDY_REPOSITORY_URL.replace("https://github.com", "https://api.github.com/repos")}/releases?per_page=100`;
250
+ var WORKBUDDY_RELEASE_PAGE_BASE = `${WORKBUDDY_REPOSITORY_URL}/releases/tag/`;
251
+ var WORKBUDDY_UPDATE_MAX_METADATA_BYTES = 64 * 1024;
252
+ var WORKBUDDY_UPDATE_MAX_RELEASES_BYTES = 1024 * 1024;
253
+ function parseWorkBuddyVersion(raw) {
254
+ if (typeof raw !== "string") return void 0;
255
+ const normalized = raw.startsWith("v") ? raw.slice(1) : raw;
256
+ 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);
257
+ if (match === null) return void 0;
258
+ const rawPrerelease = match[4] === void 0 ? [] : match[4].split(".");
259
+ if (rawPrerelease.some((identifier) => /^\d+$/u.test(identifier) && !/^(0|[1-9]\d*)$/u.test(identifier))) return void 0;
260
+ const prerelease = rawPrerelease.map((identifier) => /^(0|[1-9]\d*)$/u.test(identifier) ? Number(identifier) : identifier);
261
+ if (prerelease.some((identifier) => typeof identifier === "number" && !Number.isSafeInteger(identifier))) return void 0;
262
+ const parsed = {
263
+ major: Number(match[1]),
264
+ minor: Number(match[2]),
265
+ patch: Number(match[3]),
266
+ prerelease
267
+ };
268
+ return [parsed.major, parsed.minor, parsed.patch].every(Number.isSafeInteger) ? parsed : void 0;
269
+ }
270
+ function compareIdentifiers(left, right) {
271
+ if (typeof left === "number" && typeof right === "number") return left < right ? -1 : left > right ? 1 : 0;
272
+ if (typeof left === "number") return -1;
273
+ if (typeof right === "number") return 1;
274
+ return left < right ? -1 : left > right ? 1 : 0;
275
+ }
276
+ function compareWorkBuddyVersions(left, right) {
277
+ const a = parseWorkBuddyVersion(left);
278
+ const b = parseWorkBuddyVersion(right);
279
+ if (a === void 0 || b === void 0) throw new TypeError("invalid WorkBuddy Connect version");
280
+ for (const [aPart, bPart] of [[a.major, b.major], [a.minor, b.minor], [a.patch, b.patch]]) {
281
+ if (aPart !== bPart) return aPart < bPart ? -1 : 1;
282
+ }
283
+ if (a.prerelease.length === 0 && b.prerelease.length !== 0) return 1;
284
+ if (a.prerelease.length !== 0 && b.prerelease.length === 0) return -1;
285
+ for (let index = 0; index < Math.max(a.prerelease.length, b.prerelease.length); index += 1) {
286
+ const aPart = a.prerelease[index];
287
+ const bPart = b.prerelease[index];
288
+ if (aPart === void 0) return -1;
289
+ if (bPart === void 0) return 1;
290
+ const comparison = compareIdentifiers(aPart, bPart);
291
+ if (comparison !== 0) return comparison;
292
+ }
293
+ return 0;
294
+ }
295
+
296
+ // src/plugin-updates.mjs
297
+ var packageName = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
298
+ var safeRef = /^[a-zA-Z0-9._/-]{1,200}$/;
299
+ function parsePluginSource(name, spec) {
300
+ if (!packageName.test(name) || typeof spec !== "string" || spec.length > 600 || spec.startsWith("-") || spec.trim() !== spec || /[\x00-\x1f]/.test(spec)) return { kind: "unsupported" };
301
+ const git = /^(?:github:|git\+https:\/\/github\.com\/|https:\/\/github\.com\/|git\+ssh:\/\/git@github\.com\/|git@github\.com:)?([\w.-]+)\/([\w.-]+?)(?:\.git)?(?:#(.+))?$/.exec(spec);
302
+ if (git && !spec.startsWith("@") && !spec.startsWith("file:") && !spec.startsWith("link:")) {
303
+ const [, owner, repo, ref] = git;
304
+ if (owner === "." || owner === ".." || repo === "." || repo === ".." || ref && (!safeRef.test(ref) || ref.includes(".."))) return { kind: "unsupported" };
305
+ return { kind: "github", owner, repo, ref, source: `github:${owner}/${repo}${ref ? "#" + ref : ""}` };
306
+ }
307
+ if (/^(?:file:|link:|workspace:|https?:|git|\/|\.)/.test(spec)) return { kind: "unsupported" };
308
+ const alias = /^npm:((?:@[^/]+\/)?[^@]+)(?:@(.+))?$/.exec(spec);
309
+ const actual = alias?.[1] ?? name, range = alias?.[2] ?? spec;
310
+ if (!packageName.test(actual) || !/^[a-zA-Z0-9.*^~<>=|+ -]{1,160}$/.test(range)) return { kind: "unsupported" };
311
+ const channel = /^[a-zA-Z][\w.-]*$/.test(range);
312
+ return { kind: "npm", package: actual, alias: !!alias, channel, tag: channel ? range : "latest", source: spec };
313
+ }
314
+ function githubCommit(value) {
315
+ if (typeof value !== "string") return void 0;
316
+ return /(?:\/tar\.gz\/|\/tarball\/|#)([a-f0-9]{40})(?:\b|\()/i.exec(value)?.[1]?.toLowerCase();
317
+ }
318
+ async function jsonFile(file) {
319
+ try {
320
+ return JSON.parse(await readFile(file, "utf8"));
321
+ } catch {
322
+ return void 0;
323
+ }
324
+ }
325
+ async function locks(dir) {
326
+ let pnpm;
327
+ try {
328
+ pnpm = parseYaml(await readFile(join3(dir, "pnpm-lock.yaml"), "utf8"), { maxAliasCount: 20 });
329
+ } catch {
330
+ }
331
+ const npm = await jsonFile(join3(dir, "package-lock.json"));
332
+ const times = await Promise.all(["pnpm-lock.yaml", "package-lock.json"].map((name) => stat(join3(dir, name)).then((s) => s.mtimeMs).catch(() => 0)));
333
+ return { pnpm, npm, preferNpm: times[1] > times[0] };
334
+ }
335
+ async function publicJson(url, fetchImpl, signal) {
336
+ const res = await fetchImpl(url, {
337
+ signal: AbortSignal.any([signal, AbortSignal.timeout(12e3)]),
338
+ headers: { Accept: "application/json", "User-Agent": "dsh-multi-router-plugin-updates" },
339
+ redirect: "error"
340
+ });
341
+ 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`);
342
+ let size = 0, chunks = [];
343
+ for await (const chunk of res.body) {
344
+ size += chunk.byteLength;
345
+ if (size > 256 * 1024) throw Error("\u66F4\u65B0\u5143\u6570\u636E\u8FC7\u5927");
346
+ chunks.push(Buffer.from(chunk));
347
+ }
348
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
349
+ }
350
+ var short = (sha) => sha?.slice(0, 8);
351
+ async function registryVersion(service, profile, spec, signal) {
352
+ const require2 = createRequire(profile.installAnchor);
353
+ const { viewProfilePackage } = await import(pathToFileURL(require2.resolve("@deepseek-ai/dsh-plugin-manager/operations")).href);
354
+ const { registryPlan } = await import(pathToFileURL(require2.resolve("@deepseek-ai/dsh-plugin-manager/registry")).href);
355
+ for (const registry of registryPlan(void 0, await service.registries())) {
356
+ const answer = await viewProfilePackage(profile.dir, spec, { ...profile.packageManager, registry, timeoutMs: 12e3, signal });
357
+ if (signal.aborted) throw Error("\u68C0\u67E5\u5DF2\u53D6\u6D88");
358
+ if (answer.exitCode !== 0) continue;
359
+ const parsed = JSON.parse(answer.stdout), metadata = Array.isArray(parsed) ? parsed.at(-1) : parsed;
360
+ if (metadata?.version) return metadata;
361
+ }
362
+ throw Error("npm \u67E5\u8BE2\u5931\u8D25\uFF1B\u8BF7\u68C0\u67E5 DSH \u7F51\u7EDC\u3001registry \u8BBE\u7F6E\u6216\u5305\u7684\u8BBF\u95EE\u6743\u9650");
363
+ }
364
+ async function refreshGitPlugin(service, profile, row, options2, signal) {
365
+ const require2 = createRequire(profile.installAnchor);
366
+ const { runProfilePnpm } = await import(pathToFileURL(require2.resolve("@deepseek-ai/dsh-plugin-manager/operations")).href);
367
+ const { registryPlan, attributeFailure } = await import(pathToFileURL(require2.resolve("@deepseek-ai/dsh-plugin-manager/registry")).href);
368
+ const { classifyInstallFailure } = await import(pathToFileURL(require2.resolve("@deepseek-ai/dsh-plugin-manager")).href);
369
+ const plan = registryPlan(options2.registry, await service.registries());
370
+ return withFileLock(join3(profile.dir, "package.json"), async () => {
371
+ const files = await Promise.all(["package.json", "pnpm-lock.yaml"].map(async (name) => {
372
+ const path = join3(profile.dir, name);
373
+ try {
374
+ return { path, content: await readFile(path, "utf8") };
375
+ } catch (e) {
376
+ if (e.code !== "ENOENT") throw e;
377
+ return { path };
378
+ }
379
+ }));
380
+ const restore = () => Promise.all(files.map((f) => f.content === void 0 ? rm2(f.path, { force: true }) : writeFileAtomic2(f.path, f.content, { mode: 384 })));
381
+ 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");
382
+ let result;
383
+ try {
384
+ for (const registry of plan) {
385
+ signal.throwIfAborted();
386
+ const run = await runProfilePnpm({ ...profile, profile: profile.name }, ["update", row.name, ...registry ? [`--registry=${registry}`] : []], {
387
+ execution: "service",
388
+ ...profile.packageManager,
389
+ signal,
390
+ outputBytes: 16384,
391
+ idleTimeoutMs: 6e5,
392
+ lookupTimeoutMs: 2e4,
393
+ activateNewBundles: false
394
+ });
395
+ if (run.exitCode === 0 && !run.timedOut && !signal.aborted) return { packageResult: run, application: "restart-required" };
396
+ const kind = classifyInstallFailure({ log: run.output, timedOut: run.timedOut });
397
+ result = {
398
+ packageResult: { ...run, kind },
399
+ application: signal.aborted ? "cancelled" : "failed",
400
+ ...run.incompatible?.length ? { error: { code: "incompatible-version" } } : {}
401
+ };
402
+ await restore();
403
+ if (signal.aborted || attributeFailure(kind, run.output, { kind: "git", host: "github.com" }) !== "registry") break;
404
+ }
405
+ return result;
406
+ } catch (e) {
407
+ await restore();
408
+ throw e;
409
+ }
410
+ }, { waitMs: 12e4 });
411
+ }
412
+ var PluginUpdates = class {
413
+ constructor({ service = () => void 0, profile = () => void 0, fetchImpl = (...args) => fetch(...args), lookup = registryVersion, refreshGit = refreshGitPlugin } = {}) {
414
+ this.service = service;
415
+ this.profile = profile;
416
+ this.fetchImpl = fetchImpl;
417
+ this.rows = [];
418
+ this.abort = new AbortController();
419
+ this.lookup = lookup;
420
+ this.refreshGit = refreshGit;
421
+ }
422
+ state() {
423
+ return {
424
+ available: !!this.service()?.listBundles && !!this.profile()?.dir,
425
+ checkedAt: this.checkedAt,
426
+ rows: this.rows.map(({ spec, ...row }) => ({ ...row, source: row.source.kind === "unsupported" ? { kind: "unsupported" } : row.source })),
427
+ job: this.job,
428
+ error: this.error
429
+ };
430
+ }
431
+ async list() {
432
+ const service = this.service(), profile = this.profile();
433
+ if (!service?.listBundles || !profile?.dir) {
434
+ this.rows = [];
435
+ return;
436
+ }
437
+ const [manifest, bundles, lock] = await Promise.all([jsonFile(join3(profile.dir, "package.json")), service.listBundles(), locks(profile.dir)]);
438
+ if (!manifest) throw Error("\u5F53\u524D DSH \u914D\u7F6E\u7684\u63D2\u4EF6\u6E05\u5355\u65E0\u6CD5\u8BFB\u53D6");
439
+ this.rows = bundles.filter((b) => b.installed || b.version).map((b) => {
440
+ const spec = manifest.dependencies?.[b.name];
441
+ const source = !spec || b.removable === false && !b.readOnlyReason ? { kind: "builtin" } : parsePluginSource(b.name, spec);
442
+ const npm = lock.npm?.packages?.[`node_modules/${b.name}`]?.resolved;
443
+ const pnpm = lock.pnpm?.importers?.["."]?.dependencies?.[b.name]?.version;
444
+ const installed = lock.preferNpm ? npm ?? pnpm : pnpm ?? npm;
445
+ const previous = this.rows.find((r) => r.name === b.name && r.spec === spec && r.version === b.version && r.commit === githubCommit(installed));
446
+ return {
447
+ ...previous,
448
+ name: b.name,
449
+ version: b.version,
450
+ enabled: b.enabled,
451
+ spec,
452
+ source,
453
+ commit: githubCommit(installed),
454
+ status: previous?.status ?? (source.kind === "builtin" ? "builtin" : source.kind === "unsupported" ? "unsupported" : "unchecked"),
455
+ ...source.kind === "builtin" ? { message: "DSH \u81EA\u5E26 / \u53D7\u4FDD\u62A4\u7EC4\u4EF6\uFF0C\u8BF7\u901A\u8FC7\u66F4\u65B0 DeepSeek Harness \u5347\u7EA7" } : {},
456
+ ...source.kind === "unsupported" ? { message: "\u672C\u5730\u6587\u4EF6 / \u94FE\u63A5\u6765\u6E90\uFF0C\u8BF7\u901A\u8FC7\u539F\u5B89\u88C5\u65B9\u5F0F\u66F4\u65B0" } : {}
457
+ };
458
+ });
459
+ }
460
+ async checkRow(row) {
461
+ try {
462
+ if (row.source.kind === "npm") {
463
+ const spec = `${row.source.package}@${row.source.tag}`;
464
+ const found = await this.lookup(this.service(), this.profile(), spec, this.abort.signal);
465
+ 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");
466
+ if (!parseWorkBuddyVersion(found.version) || row.version && !parseWorkBuddyVersion(row.version)) throw Error("\u65E0\u6CD5\u6BD4\u8F83\u7248\u672C\u53F7");
467
+ row.latestVersion = found.version;
468
+ row.target = `${row.name}@${row.source.alias ? `npm:${row.source.package}@` : ""}${row.source.channel ? row.source.tag : found.version}`;
469
+ row.status = !row.version || compareWorkBuddyVersions(found.version, row.version) > 0 ? "available" : "current";
470
+ 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;
471
+ } else if (row.source.kind === "github") {
472
+ const { owner, repo, ref } = row.source;
473
+ const url = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits/${encodeURIComponent(ref ?? "HEAD")}`;
474
+ const found = await publicJson(url, this.fetchImpl, this.abort.signal);
475
+ if (!/^[a-f0-9]{40}$/i.test(found.sha)) throw Error("GitHub \u8FD4\u56DE\u65E0\u6548\u63D0\u4EA4\u4FE1\u606F");
476
+ row.latestCommit = found.sha.toLowerCase();
477
+ row.target = `${row.name}@${row.spec}`;
478
+ row.status = row.commit === row.latestCommit ? "current" : "available";
479
+ 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";
480
+ if (ref && /^[a-f0-9]{7,40}$/i.test(ref)) {
481
+ row.status = "pinned";
482
+ 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";
483
+ }
484
+ }
485
+ row.checkedAt = Date.now();
486
+ row.checkedEnabled = row.enabled;
487
+ } catch (error) {
488
+ row.status = "failed";
489
+ row.message = error.message;
490
+ delete row.target;
491
+ }
492
+ }
493
+ start(kind, task) {
494
+ if (this.flight) throw Error("\u5DF2\u6709\u63D2\u4EF6\u64CD\u4F5C\u6B63\u5728\u6267\u884C\uFF0C\u8BF7\u7B49\u5F85\u5B8C\u6210");
495
+ this.job = { id: randomUUID(), kind, state: "running", startedAt: Date.now(), results: [], total: 0 };
496
+ this.error = void 0;
497
+ this.flight = Promise.resolve().then(() => task(this.job)).then(() => {
498
+ this.job.state = this.job.cancelRequested ? "cancelled" : "done";
499
+ }, (e) => {
500
+ this.job.state = this.job.cancelRequested ? "cancelled" : "failed";
501
+ this.job.message = e.message;
502
+ }).finally(() => {
503
+ this.job.finishedAt = Date.now();
504
+ this.flight = void 0;
505
+ });
506
+ }
507
+ check() {
508
+ this.start("check", async (job) => {
509
+ await this.list();
510
+ const rows = this.rows.filter((r) => ["npm", "github"].includes(r.source.kind));
511
+ job.total = rows.length;
512
+ let next = 0;
513
+ await Promise.all(Array.from({ length: Math.min(3, rows.length) }, async () => {
514
+ while (next < rows.length && !job.cancelRequested) {
515
+ const row = rows[next++];
516
+ await this.checkRow(row);
517
+ job.results.push({ name: row.name, status: row.status, message: row.message });
518
+ }
519
+ }));
520
+ this.checkedAt = Date.now();
521
+ });
522
+ }
523
+ async installRow(row, registry) {
524
+ const original = await jsonFile(join3(this.profile().dir, "package.json"));
525
+ if (original?.dependencies?.[row.name] !== row.spec) throw Error("\u5B89\u88C5\u6765\u6E90\u5DF2\u6539\u53D8\uFF0C\u8BF7\u91CD\u65B0\u68C0\u67E5");
526
+ const current = (await this.service().listBundles()).find((b) => b.name === row.name);
527
+ if (!current || 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");
528
+ const options2 = { enabled: row.enabled, requestId: this.job.id, ...registry ? { registry } : {} };
529
+ let result;
530
+ if (row.source.kind === "github") {
531
+ this.gitAbort = new AbortController();
532
+ try {
533
+ result = await this.refreshGit(this.service(), this.profile(), row, options2, AbortSignal.any([this.abort.signal, this.gitAbort.signal]));
534
+ } finally {
535
+ this.gitAbort = void 0;
536
+ }
537
+ } else result = await this.service().installBundle(row.target, options2);
538
+ if (result.packageResult && result.packageResult.exitCode !== 0 || ["failed", "cancelled"].includes(result.application) || result.error) {
539
+ return {
540
+ name: row.name,
541
+ status: "failed",
542
+ application: result.application,
543
+ message: result.pendingBuilds?.length ? "\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" : result.error?.code === "incompatible-version" ? "\u63D2\u4EF6\u4E0E\u5F53\u524D DSH \u4E0D\u517C\u5BB9\uFF0C\u8BF7\u5148\u5347\u7EA7 DSH \u6216\u9009\u62E9\u517C\u5BB9\u7248\u672C" : `\u5B89\u88C5\u672A\u5B8C\u6210\uFF08${result.error?.code ?? result.packageResult?.kind ?? "unknown"}\uFF09\uFF1B\u8BF7\u5230 DSH \u63D2\u4EF6\u9875\u67E5\u770B\u5B89\u88C5\u65E5\u5FD7`,
544
+ pendingBuilds: result.pendingBuilds ?? [],
545
+ logPath: result.packageResult?.logPath
546
+ };
547
+ }
548
+ const installed = (await this.service().listBundles()).find((b) => b.name === row.name);
549
+ if (row.source.kind === "npm" && installed?.version !== row.latestVersion) return {
550
+ name: row.name,
551
+ status: "failed",
552
+ 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`
553
+ };
554
+ if (row.source.kind === "github") {
555
+ const lock = await locks(this.profile().dir);
556
+ const pnpm = lock.pnpm?.importers?.["."]?.dependencies?.[row.name]?.version, npm = lock.npm?.packages?.[`node_modules/${row.name}`]?.resolved;
557
+ if (githubCommit(lock.preferNpm ? npm ?? pnpm : pnpm ?? npm) !== row.latestCommit) return {
558
+ name: row.name,
559
+ status: "failed",
560
+ restartRequired: true,
561
+ 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"
562
+ };
563
+ }
564
+ return { name: row.name, status: "installed", application: result.application, message: 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: result.application === "restart-required" };
565
+ }
566
+ update(names, registry) {
567
+ 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");
568
+ const selected = names.map((name) => this.rows.find((r) => r.name === name));
569
+ 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");
570
+ selected.sort((a, b) => Number(a.name === "dsh-multi-router") - Number(b.name === "dsh-multi-router"));
571
+ this.start("update", async (job) => {
572
+ job.total = selected.length;
573
+ for (const row of selected) {
574
+ if (job.cancelRequested) break;
575
+ job.current = row.name;
576
+ let result;
577
+ try {
578
+ result = await this.installRow(row, registry);
579
+ } catch (e) {
580
+ result = { name: row.name, status: "failed", message: e.message };
581
+ }
582
+ job.results.push(result);
583
+ row.status = result.status;
584
+ row.message = result.message;
585
+ }
586
+ delete job.current;
587
+ await this.list();
588
+ });
589
+ }
590
+ install(spec, registry) {
591
+ 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");
592
+ const named = /^((?:@[^/]+\/)?[^@]+)(?:@(.+))?$/.exec(spec);
593
+ const github = parsePluginSource("placeholder", spec);
594
+ const npm = named && parsePluginSource(named[1], named[2] || "latest");
595
+ if (github.kind !== "github" && npm?.kind !== "npm") throw Error("\u4EC5\u652F\u6301 npm \u5305\u540D / \u7248\u672C\u6216 github:\u7528\u6237/\u4ED3\u5E93");
596
+ this.start("install", async (job) => {
597
+ job.total = 1;
598
+ job.current = spec;
599
+ const result = await this.service().installBundle(spec, { requestId: job.id, ...registry ? { registry } : {} });
600
+ job.results.push({
601
+ name: result.bundle ?? spec,
602
+ status: result.error || ["failed", "cancelled"].includes(result.application) || result.packageResult && result.packageResult.exitCode !== 0 ? "failed" : "installed",
603
+ message: result.error ? `\u5B89\u88C5\u672A\u5B8C\u6210\uFF08${result.error.code}\uFF09\uFF0C\u8BF7\u67E5\u770B DSH \u63D2\u4EF6\u5B89\u88C5\u65E5\u5FD7` : result.application === "restart-required" ? "\u5DF2\u5B89\u88C5\uFF0C\u8BF7\u5B8C\u5168\u9000\u51FA\u5E76\u91CD\u542F DSH" : "\u5B89\u88C5\u5B8C\u6210",
604
+ application: result.application,
605
+ pendingBuilds: result.pendingBuilds ?? [],
606
+ restartRequired: result.application === "restart-required"
607
+ });
608
+ delete job.current;
609
+ await this.list();
610
+ });
611
+ }
612
+ async action(input) {
613
+ 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");
614
+ if (input.registry && !["https://registry.npmjs.org/", "https://registry.npmmirror.com/"].includes(input.registry)) throw Error("\u65E0\u6548\u7684 npm \u6765\u6E90");
615
+ if (input.action === "plugins.list") {
616
+ if (!this.flight) await this.list();
617
+ } else if (input.action === "plugins.check") this.check();
618
+ else if (input.action === "plugins.update") this.update(input.names, input.registry);
619
+ else if (input.action === "plugins.install") this.install(input.spec, input.registry);
620
+ else if (input.action === "plugins.cancel") {
621
+ if (this.job?.state === "running") {
622
+ this.job.cancelRequested = true;
623
+ if (this.gitAbort) this.gitAbort.abort();
624
+ else if (this.job.kind !== "check") await this.service().cancelInstall?.(this.job.id);
625
+ }
626
+ } else throw Error("\u672A\u77E5\u63D2\u4EF6\u64CD\u4F5C");
627
+ }
628
+ };
629
+
236
630
  // src/manager.mjs
237
631
  var MANAGEMENT_PATH = "/.well-known/dsh-multi-router";
238
632
  var revisionOf = (raw) => createHash("sha256").update(JSON.stringify(raw)).digest("hex");
@@ -272,28 +666,35 @@ function modelInfo(row) {
272
666
  }
273
667
  var Manager = class {
274
668
  constructor(file, router, onChange = () => {
275
- }) {
669
+ }, pluginAccess = {}) {
276
670
  this.file = file;
277
671
  this.router = router;
278
672
  this.onChange = onChange;
279
- this.catalogFile = join3(dirname3(file), "catalogs.json");
673
+ this.catalogFile = join4(dirname3(file), "catalogs.json");
280
674
  this.catalogs = /* @__PURE__ */ new Map();
281
675
  this.catalogErrors = /* @__PURE__ */ new Map();
282
676
  this.refreshes = /* @__PURE__ */ new Map();
283
677
  this.closed = false;
284
678
  this.monitorFlights = /* @__PURE__ */ new Map();
285
679
  this.loginInfo = /* @__PURE__ */ new Map();
286
- this.telemetry = new Telemetry(join3(dirname3(file), "usage.json"));
680
+ this.telemetry = new Telemetry(join4(dirname3(file), "usage.json"));
681
+ this.plugins = new PluginUpdates(pluginAccess);
682
+ this.setupFile = join4(dirname3(file), "initial-setup.json");
287
683
  router.telemetry = this.telemetry;
288
684
  router.monitor = (a) => this.monitorAccount(a);
289
685
  router.prepareFree = () => this.checkFreeModels(true);
290
686
  router.freeModels = (account) => this.freeModels(account);
291
687
  }
292
688
  async load() {
293
- this.raw = JSON.parse(await readFile(this.file, "utf8"));
689
+ this.raw = JSON.parse(await readFile2(this.file, "utf8"));
690
+ try {
691
+ this.setup = JSON.parse(await readFile2(this.setupFile, "utf8"));
692
+ } catch {
693
+ this.setup = void 0;
694
+ }
294
695
  this.config = validateConfig(this.raw, this.file);
295
696
  try {
296
- const saved = JSON.parse(await readFile(this.catalogFile, "utf8"));
697
+ const saved = JSON.parse(await readFile2(this.catalogFile, "utf8"));
297
698
  for (const account of this.config.accounts) {
298
699
  const item = saved[account.id];
299
700
  if (item?.credentialFile === account.credentialFile && Array.isArray(item.models)) {
@@ -333,10 +734,11 @@ var Manager = class {
333
734
  ...discovered.status.map((s) => ({ ...s, checkedAt: Date.now() }))
334
735
  ];
335
736
  if (this.closed || !discovered.accounts.length) return;
336
- await withFileLock(this.file, async () => {
737
+ await withFileLock2(this.file, async () => {
337
738
  if (this.closed) return;
338
- let raw = JSON.parse(await readFile(this.file, "utf8"));
739
+ let raw = JSON.parse(await readFile2(this.file, "utf8"));
339
740
  let changed = false;
741
+ const saved = /* @__PURE__ */ new Map();
340
742
  for (const item of discovered.accounts) {
341
743
  const key = localAccountKey(item.provider, item.document);
342
744
  if (!force && raw.ignoredLocalAccounts?.includes(key)) continue;
@@ -353,6 +755,11 @@ var Manager = class {
353
755
  } catch {
354
756
  }
355
757
  }
758
+ const id = existing?.id ?? `local_${createHash("sha256").update(key).digest("hex").slice(0, 20)}`;
759
+ const recordSaved = () => {
760
+ if (!saved.has(item.provider)) saved.set(item.provider, /* @__PURE__ */ new Set());
761
+ saved.get(item.provider).add(id);
762
+ };
356
763
  let sameDevice = true;
357
764
  if (existing && item.identityDocument) {
358
765
  try {
@@ -362,13 +769,15 @@ var Manager = class {
362
769
  sameDevice = false;
363
770
  }
364
771
  }
365
- if (existing && sameDevice && current.credential.expiresAtMs >= item.document.credential.expiresAtMs) continue;
366
- const id = existing?.id ?? `local_${createHash("sha256").update(key).digest("hex").slice(0, 20)}`;
367
- const base = join3(dirname3(this.file), "accounts");
772
+ if (existing && sameDevice && current.credential.expiresAtMs >= item.document.credential.expiresAtMs) {
773
+ recordSaved();
774
+ continue;
775
+ }
776
+ const base = join4(dirname3(this.file), "accounts");
368
777
  await mkdir2(base, { recursive: true, mode: 448 });
369
- const staging = await mkdtemp2(join3(base, `${id}-`));
778
+ const staging = await mkdtemp2(join4(base, `${id}-`));
370
779
  try {
371
- const credentialFile = join3(staging, "credential.json"), identityFile = join3(staging, "identity.json");
780
+ const credentialFile = join4(staging, "credential.json"), identityFile = join4(staging, "identity.json");
372
781
  await importAccount({ ...item, credentialFile, identityFile });
373
782
  const paths = {
374
783
  credentialFile: relative2(dirname3(this.file), credentialFile),
@@ -387,11 +796,12 @@ var Manager = class {
387
796
  await writePrivateJson(this.file, next);
388
797
  raw = next;
389
798
  changed = true;
799
+ recordSaved();
390
800
  this.router.clients.delete(id);
391
801
  this.catalogs.delete(id);
392
802
  this.catalogErrors.delete(id);
393
803
  } catch (e) {
394
- await rm2(staging, { recursive: true, force: true });
804
+ await rm3(staging, { recursive: true, force: true });
395
805
  this.localDiscovery = this.localDiscovery.map((s) => s.provider === item.provider ? { ...s, state: "unreadable", code: "import_failed" } : s);
396
806
  }
397
807
  }
@@ -401,12 +811,25 @@ var Manager = class {
401
811
  await Promise.all(this.config.accounts.map((a) => this.readLogin(a)));
402
812
  this.publish();
403
813
  }
814
+ this.localDiscovery = this.localDiscovery.map((s) => discovered.status.some((d) => d.provider === s.provider) ? { ...s, savedCount: saved.get(s.provider)?.size ?? 0 } : s);
404
815
  }, { waitMs: 3e4 });
405
816
  })().finally(() => {
406
817
  this.localScan = void 0;
407
818
  });
408
819
  return this.localScan;
409
820
  }
821
+ async finishInitialSetup() {
822
+ if (this.setup?.completedAt) return;
823
+ await withFileLock2(this.setupFile, async () => {
824
+ try {
825
+ this.setup = JSON.parse(await readFile2(this.setupFile, "utf8"));
826
+ } catch {
827
+ }
828
+ if (this.setup?.completedAt) return;
829
+ this.setup = { completedAt: Date.now(), dismissed: false };
830
+ await writePrivateJson(this.setupFile, this.setup);
831
+ });
832
+ }
410
833
  async monitorAccount(account) {
411
834
  if (this.monitorFlights.has(account.id)) return this.monitorFlights.get(account.id);
412
835
  const promise = (async () => {
@@ -581,7 +1004,7 @@ var Manager = class {
581
1004
  if (this.closed || !this.config.accounts.some((a) => a.id === account.id && a.credentialFile === account.credentialFile)) return;
582
1005
  this.catalogs.set(account.id, { credentialFile: account.credentialFile, fetchedAt: Date.now(), pricingVersion: 1, models });
583
1006
  this.catalogErrors.delete(account.id);
584
- await withFileLock(this.catalogFile, () => writePrivateJson(this.catalogFile, Object.fromEntries(this.catalogs)));
1007
+ await withFileLock2(this.catalogFile, () => writePrivateJson(this.catalogFile, Object.fromEntries(this.catalogs)));
585
1008
  } catch (error) {
586
1009
  if (this.closed || !this.config.accounts.some((a) => a.id === account.id)) return;
587
1010
  this.catalogErrors.set(account.id, { status: Number(error.status ?? error.failure?.status) || 502, code: "catalog_fetch_failed", at: Date.now() });
@@ -597,6 +1020,8 @@ var Manager = class {
597
1020
  pluginVersion: version,
598
1021
  revision: this.revision,
599
1022
  localDiscovery: this.localDiscovery ?? [],
1023
+ setup: this.setup,
1024
+ pluginUpdates: this.plugins.state(),
600
1025
  telemetry: this.telemetry.state(),
601
1026
  accounts: this.config.accounts.map((a) => ({
602
1027
  id: a.id,
@@ -631,8 +1056,23 @@ var Manager = class {
631
1056
  }
632
1057
  async action(input) {
633
1058
  if (!input || typeof input !== "object" || Array.isArray(input)) bad("\u8BF7\u6C42\u683C\u5F0F\u65E0\u6548");
1059
+ if (typeof input.action === "string" && input.action.startsWith("plugins.")) {
1060
+ try {
1061
+ await this.plugins.action(input);
1062
+ } catch (e) {
1063
+ bad(e.message);
1064
+ }
1065
+ return this.state();
1066
+ }
1067
+ if (input.action === "setup.dismiss") {
1068
+ await withFileLock2(this.setupFile, async () => {
1069
+ this.setup = { ...this.setup, completedAt: this.setup?.completedAt ?? Date.now(), dismissed: true };
1070
+ await writePrivateJson(this.setupFile, this.setup);
1071
+ });
1072
+ return this.state();
1073
+ }
634
1074
  if (input.action === "account.export") {
635
- if (input.revision !== revisionOf(JSON.parse(await readFile(this.file, "utf8")))) throw new ManagementError(409, "\u914D\u7F6E\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u540E\u63D0\u53D6\u51ED\u8BC1");
1075
+ if (input.revision !== revisionOf(JSON.parse(await readFile2(this.file, "utf8")))) throw new ManagementError(409, "\u914D\u7F6E\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u540E\u63D0\u53D6\u51ED\u8BC1");
636
1076
  const account = this.config.accounts.find((a) => a.id === input.id);
637
1077
  if (!account) bad("\u8D26\u53F7\u4E0D\u5B58\u5728");
638
1078
  let credential;
@@ -654,7 +1094,7 @@ var Manager = class {
654
1094
  } };
655
1095
  }
656
1096
  if (input.action === "config.export") {
657
- if (input.revision !== revisionOf(JSON.parse(await readFile(this.file, "utf8")))) throw new ManagementError(409, "\u914D\u7F6E\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u540E\u5BFC\u51FA");
1097
+ if (input.revision !== revisionOf(JSON.parse(await readFile2(this.file, "utf8")))) throw new ManagementError(409, "\u914D\u7F6E\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u540E\u5BFC\u51FA");
658
1098
  try {
659
1099
  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) } };
660
1100
  } catch (e) {
@@ -701,8 +1141,8 @@ var Manager = class {
701
1141
  await this.checkFreeModels(true);
702
1142
  return this.state();
703
1143
  }
704
- return withFileLock(this.file, async () => {
705
- let raw = JSON.parse(await readFile(this.file, "utf8"));
1144
+ return withFileLock2(this.file, async () => {
1145
+ let raw = JSON.parse(await readFile2(this.file, "utf8"));
706
1146
  if (input.revision !== revisionOf(raw)) throw new ManagementError(409, "\u914D\u7F6E\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u540E\u518D\u4FDD\u5B58");
707
1147
  raw.routes ??= structuredClone(this.config.routes);
708
1148
  let staging;
@@ -725,10 +1165,10 @@ var Manager = class {
725
1165
  }
726
1166
  if (!/^[a-zA-Z0-9_-]{1,64}$/.test(input.id ?? "") || !PROVIDERS.includes(input.provider)) bad("\u8D26\u53F7 ID \u6216\u4F9B\u5E94\u5546\u65E0\u6548");
727
1167
  if (account && !replacing) bad("\u8D26\u53F7 ID \u5DF2\u5B58\u5728\uFF0C\u8BF7\u4F7F\u7528\u4E0D\u540C ID");
728
- const base = join3(dirname3(this.file), "accounts");
1168
+ const base = join4(dirname3(this.file), "accounts");
729
1169
  await mkdir2(base, { recursive: true, mode: 448 });
730
- staging = await mkdtemp2(join3(base, `${input.id}-`));
731
- const credentialFile = join3(staging, "credential.json"), identityFile = join3(staging, "identity.json");
1170
+ staging = await mkdtemp2(join4(base, `${input.id}-`));
1171
+ const credentialFile = join4(staging, "credential.json"), identityFile = join4(staging, "identity.json");
732
1172
  const added = {
733
1173
  id: input.id,
734
1174
  name: input.name || account?.name || input.id,
@@ -888,7 +1328,7 @@ var Manager = class {
888
1328
  this.publish();
889
1329
  await this.telemetry.persist();
890
1330
  } finally {
891
- if (staging) await rm2(staging, { recursive: true, force: true });
1331
+ if (staging) await rm3(staging, { recursive: true, force: true });
892
1332
  }
893
1333
  return this.state();
894
1334
  }, { waitMs: 3e4 }).then(async (state) => {
@@ -916,8 +1356,8 @@ var Manager = class {
916
1356
  if (filename && String(filename) !== this.file.split(/[\\/]/).pop()) return;
917
1357
  clearTimeout(timer);
918
1358
  timer = setTimeout(() => {
919
- void withFileLock(this.file, async () => {
920
- const raw = JSON.parse(await readFile(this.file, "utf8"));
1359
+ void withFileLock2(this.file, async () => {
1360
+ const raw = JSON.parse(await readFile2(this.file, "utf8"));
921
1361
  if (this.closed || revisionOf(raw) === this.revision) return;
922
1362
  const config = validateConfig(raw, this.file);
923
1363
  this.raw = raw;
@@ -984,4 +1424,4 @@ export {
984
1424
  Manager,
985
1425
  managementHandler
986
1426
  };
987
- //# sourceMappingURL=chunk-WIDXMBQ2.js.map
1427
+ //# sourceMappingURL=chunk-DZYRIZW2.js.map