dsh-theone 0.3.9 → 0.3.12

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.
Files changed (44) hide show
  1. package/README.md +77 -53
  2. package/README.zh.md +82 -58
  3. package/cordis.patch.yml +0 -0
  4. package/dist/catalog-types.d.ts +0 -0
  5. package/dist/catalog-types.js +0 -0
  6. package/dist/client-locales.d.ts +11 -0
  7. package/dist/client-locales.js +22 -0
  8. package/dist/client-navigation.d.ts +0 -0
  9. package/dist/client-navigation.js +0 -0
  10. package/dist/client.d.ts +0 -0
  11. package/dist/client.js +130 -10
  12. package/dist/gateway-compaction.d.ts +0 -0
  13. package/dist/gateway-compaction.js +0 -0
  14. package/dist/history-catalog.d.ts +0 -0
  15. package/dist/history-catalog.js +0 -0
  16. package/dist/index.d.ts +13 -3
  17. package/dist/index.js +102 -22
  18. package/dist/linkage.d.ts +0 -0
  19. package/dist/linkage.js +0 -0
  20. package/dist/llm-router.d.ts +0 -0
  21. package/dist/llm-router.js +0 -0
  22. package/dist/model-json.d.ts +0 -0
  23. package/dist/model-json.js +0 -0
  24. package/dist/router.d.ts +0 -0
  25. package/dist/router.js +0 -0
  26. package/dist/routing-policy.d.ts +0 -0
  27. package/dist/routing-policy.js +0 -0
  28. package/dist/run.d.ts +0 -0
  29. package/dist/run.js +0 -0
  30. package/dist/settings-types.d.ts +0 -0
  31. package/dist/settings-types.js +0 -0
  32. package/dist/settings.d.ts +0 -0
  33. package/dist/settings.js +0 -0
  34. package/dist/store.d.ts +0 -0
  35. package/dist/store.js +0 -0
  36. package/dist/types.d.ts +0 -0
  37. package/dist/types.js +0 -0
  38. package/dist/update.d.ts +55 -0
  39. package/dist/update.js +121 -0
  40. package/docs/plugin-v0.1.md +0 -0
  41. package/docs/plugin-v0.1.zh.md +0 -0
  42. package/docs/plugin-v0.2.md +0 -0
  43. package/docs/plugin-v0.2.zh.md +0 -0
  44. package/package.json +1 -1
package/dist/client.js CHANGED
@@ -88,6 +88,17 @@ var zh = {
88
88
  "gateway.opening": "\u6B63\u5728\u6253\u5F00 TheOne \u4E3B\u804A\u5929\u2026",
89
89
  "gateway.error": "\u4E3B\u804A\u5929\u6682\u65F6\u65E0\u6CD5\u6253\u5F00\uFF0C\u8BF7\u68C0\u67E5 DSH \u8FDE\u63A5\u548C TheOne \u63D2\u4EF6\u72B6\u6001\u3002",
90
90
  "retry": "\u91CD\u8BD5",
91
+ "update.available": "\u66F4\u65B0",
92
+ "update.installing": "\u66F4\u65B0\u4E2D\u2026",
93
+ "update.restart": "\u91CD\u542F\u751F\u6548",
94
+ "update.reloading": "\u6B63\u5728\u91CD\u65B0\u52A0\u8F7D\u2026",
95
+ "update.reloadingHint": "TheOne \u6B63\u5728\u7528\u65B0\u7248\u672C\u91CD\u65B0\u52A0\u8F7D\uFF0CDSH \u4E0D\u9700\u8981\u91CD\u542F\uFF1B\u5B8C\u6210\u540E\u9875\u9762\u4F1A\u81EA\u52A8\u5237\u65B0\u3002",
96
+ "update.busy": "\u6709\u56DE\u590D\u6B63\u5728\u8FDB\u884C\uFF0C\u7B49\u5B83\u7ED3\u675F\u540E\u518D\u70B9\u66F4\u65B0\u3002",
97
+ "update.failed": "\u66F4\u65B0\u5931\u8D25",
98
+ "update.hint": "\u5F53\u524D {current}\uFF0C\u53EF\u66F4\u65B0\u5230 {latest}\u3002\u70B9\u51FB\u4E00\u952E\u66F4\u65B0\uFF0C\u4E0D\u7528\u91CD\u542F DSH\u3002",
99
+ "update.manualHint": "\u5F53\u524D {current}\uFF0C\u6700\u65B0 {latest}\u3002\u8FD9\u4EFD\u63D2\u4EF6\u4E0D\u662F\u4ECE GitHub \u6216 npm \u5B89\u88C5\u7684\uFF0C\u8BF7\u5728\u300C\u63D2\u4EF6\u300D\u9875\u9762\u91CD\u65B0\u5B89\u88C5\u3002",
100
+ "update.restartHint": "\u5DF2\u66F4\u65B0\uFF0C\u91CD\u542F DSH \u540E\u751F\u6548\u3002",
101
+ "update.failedHint": "\u66F4\u65B0\u6CA1\u6709\u5B8C\u6210\uFF08{error}\uFF09\u3002\u70B9\u51FB\u91CD\u8BD5\uFF0C\u6216\u5728\u300C\u63D2\u4EF6\u300D\u9875\u9762\u91CD\u65B0\u5B89\u88C5\u3002",
91
102
  "settings.title": "TheOne \u8BBE\u7F6E",
92
103
  "settings.menu": "\u8BBE\u7F6E",
93
104
  "settings.subtitle": "\u67E5\u770B\u5F53\u524D\u751F\u6548\u7684\u914D\u7F6E\u548C\u5404\u9879\u7528\u9014\u3002",
@@ -257,6 +268,17 @@ var en = {
257
268
  "gateway.opening": "Opening TheOne main chat\u2026",
258
269
  "gateway.error": "Main chat could not open. Check your DSH connection and TheOne plugin status.",
259
270
  "retry": "Retry",
271
+ "update.available": "Update",
272
+ "update.installing": "Updating\u2026",
273
+ "update.restart": "Restart to apply",
274
+ "update.reloading": "Reloading\u2026",
275
+ "update.reloadingHint": "TheOne is reloading with the new version; DSH keeps running, and the page refreshes when it is done.",
276
+ "update.busy": "A reply is in progress. Update once it finishes.",
277
+ "update.failed": "Update failed",
278
+ "update.hint": "You have {current}; {latest} is available. Click to update without restarting DSH.",
279
+ "update.manualHint": "You have {current}; {latest} is available. This copy was not installed from GitHub or npm, so reinstall it from the Plugins page.",
280
+ "update.restartHint": "Updated. Restart DSH to apply it.",
281
+ "update.failedHint": "The update did not finish ({error}). Click to retry, or reinstall from the Plugins page.",
260
282
  "settings.title": "TheOne settings",
261
283
  "settings.menu": "Settings",
262
284
  "settings.subtitle": "Review active configuration and what each option does.",
@@ -497,9 +519,94 @@ function apply(ctx) {
497
519
  });
498
520
  });
499
521
  });
522
+ let update;
523
+ const updateListeners = /* @__PURE__ */ new Set();
524
+ const setUpdate = (value) => {
525
+ update = value;
526
+ for (const listener of updateListeners) listener();
527
+ };
528
+ const subscribeUpdate = (listener) => {
529
+ updateListeners.add(listener);
530
+ return () => {
531
+ updateListeners.delete(listener);
532
+ };
533
+ };
534
+ const readUpdate = async (method = "GET") => {
535
+ try {
536
+ const response = await fetch("/api/theone/update", { method, signal: lifetime.signal, cache: "no-store" });
537
+ if (response.ok || response.status === 409) setUpdate(await response.json());
538
+ if (update?.state === "reloading") void awaitReload(update.current);
539
+ } catch {
540
+ }
541
+ };
542
+ async function awaitReload(previous) {
543
+ for (const started = Date.now(); Date.now() - started < 12e4; ) {
544
+ await new Promise((resolve) => setTimeout(resolve, 1500));
545
+ try {
546
+ const response = await fetch("/api/theone/update", { cache: "no-store" });
547
+ if (response.ok && (await response.json()).current !== previous) {
548
+ window.location.reload();
549
+ return;
550
+ }
551
+ } catch {
552
+ }
553
+ }
554
+ if (update) setUpdate({ ...update, state: "restart" });
555
+ }
556
+ ctx.effect(() => {
557
+ void readUpdate();
558
+ const timer = setInterval(() => {
559
+ void readUpdate();
560
+ }, 6 * 36e5);
561
+ return () => clearInterval(timer);
562
+ });
563
+ function UpdateButton() {
564
+ const t2 = useText();
565
+ const status = (0, import_react.useSyncExternalStore)(subscribeUpdate, () => update);
566
+ if (!status || !status.available && !status.state) return null;
567
+ const label = status.state === "installing" ? t2("update.installing") : status.state === "reloading" ? t2("update.reloading") : status.state === "restart" ? t2("update.restart") : status.state === "failed" ? t2("update.failed") : t2("update.available");
568
+ const title = status.error === "GATEWAY_BUSY" ? t2("update.busy") : status.state === "reloading" ? t2("update.reloadingHint") : status.state === "restart" ? t2("update.restartHint") : status.state === "failed" ? t2("update.failedHint", { error: status.error ?? "" }) : status.installable ? t2("update.hint", { current: status.current, latest: status.latest ?? "" }) : t2("update.manualHint", { current: status.current, latest: status.latest ?? "" });
569
+ const act = (event) => {
570
+ event.preventDefault();
571
+ event.stopPropagation();
572
+ if (status.state === "installing" || status.state === "reloading" || status.state === "restart") return;
573
+ if (!status.installable) {
574
+ window.open("https://github.com/YunongDai2005/dsh-theone#readme", "_blank", "noopener");
575
+ return;
576
+ }
577
+ setUpdate({ ...status, state: "installing" });
578
+ void readUpdate("POST");
579
+ };
580
+ return (0, import_react.createElement)(
581
+ "span",
582
+ {
583
+ className: "theone-update",
584
+ role: "button",
585
+ tabIndex: 0,
586
+ title,
587
+ "aria-label": title,
588
+ "data-state": status.state ?? "available",
589
+ onClick: act,
590
+ onPointerDown: (event) => event.stopPropagation(),
591
+ onKeyDown: (event) => {
592
+ if (event.key === "Enter" || event.key === " ") act(event);
593
+ }
594
+ },
595
+ status.state === "installing" || status.state === "reloading" ? (0, import_react.createElement)("span", { className: "theone-update-spin", "aria-hidden": true }) : null,
596
+ label,
597
+ !status.state && status.latest ? (0, import_react.createElement)("small", null, `v${status.latest}`) : null
598
+ );
599
+ }
600
+ const symbol = () => (0, import_react.createElement)(
601
+ "svg",
602
+ { className: "theone-symbol", viewBox: "0 0 20 20", width: 20, height: 20, "aria-hidden": true, focusable: false },
603
+ (0, import_react.createElement)("circle", { cx: 10, cy: 10, r: 8.6, fill: "none", stroke: "currentColor", strokeWidth: 1.4 }),
604
+ (0, import_react.createElement)("circle", { cx: 10, cy: 10, r: 2.4, fill: "currentColor" })
605
+ );
500
606
  function SidebarEntry({ size }) {
501
607
  const t2 = useText();
502
608
  const marker = (0, import_react.useRef)(null);
609
+ const pending = (0, import_react.useSyncExternalStore)(subscribeUpdate, () => !!update?.available && !update.state);
503
610
  const id = (0, import_react.useSyncExternalStore)(navigation.subscribe, navigation.getSnapshot);
504
611
  const sessions = (0, import_react.useSyncExternalStore)(ctx.sessions.list.subscribe, ctx.sessions.list.getSnapshot);
505
612
  const panel = (0, import_react.useSyncExternalStore)(ctx.layout.panelInfo.subscribe, ctx.layout.panelInfo.getSnapshot);
@@ -606,7 +713,7 @@ function apply(ctx) {
606
713
  return (0, import_react.createElement)(
607
714
  "span",
608
715
  { ref: marker, className: "theone-nav", translate: "no", "data-wide": size === 16, "data-active": active },
609
- (0, import_react.createElement)("span", { className: "theone-symbol" }),
716
+ (0, import_react.createElement)("span", { className: "theone-symbol-wrap" }, symbol(), size !== 16 && pending ? (0, import_react.createElement)("span", { className: "theone-update-dot", "aria-hidden": true }) : null),
610
717
  size === 16 && (0, import_react.createElement)(
611
718
  "span",
612
719
  { className: "theone-entry-copy" },
@@ -622,7 +729,8 @@ function apply(ctx) {
622
729
  (0, import_react.createElement)("span", { className: "theone-entry-label" }, t2("gateway.label"))
623
730
  ),
624
731
  (0, import_react.createElement)("span", { className: "theone-entry-sub" }, t2("gateway.subtitle"))
625
- )
732
+ ),
733
+ size === 16 && (0, import_react.createElement)(UpdateButton)
626
734
  );
627
735
  }
628
736
  function GatewayPanel() {
@@ -1317,17 +1425,17 @@ function apply(ctx) {
1317
1425
  ctx.effect(() => {
1318
1426
  const style = document.createElement("style");
1319
1427
  style.dataset.plugin = "dsh-theone-gateway-row";
1320
- const update = () => {
1428
+ const update2 = () => {
1321
1429
  const id = navigation.getSnapshot();
1322
1430
  style.textContent = id ? `[role="treeitem"][data-row-key="${CSS.escape(`session:${id}`)}"]{display:none!important}` : "";
1323
1431
  };
1324
- update();
1325
- const unsubscribe = navigation.subscribe(update);
1326
- window.addEventListener("storage", update);
1432
+ update2();
1433
+ const unsubscribe = navigation.subscribe(update2);
1434
+ window.addEventListener("storage", update2);
1327
1435
  document.head.append(style);
1328
1436
  return () => {
1329
1437
  unsubscribe();
1330
- window.removeEventListener("storage", update);
1438
+ window.removeEventListener("storage", update2);
1331
1439
  style.remove();
1332
1440
  };
1333
1441
  });
@@ -1355,9 +1463,21 @@ button:has(.theone-nav[data-wide=true])>span:not(:has(.theone-nav)){display:none
1355
1463
  button:has(.theone-nav):hover{background:var(--one-tint);border-color:color-mix(in srgb,var(--one-accent) 40%,var(--one-line))}
1356
1464
  button:has(.theone-nav[data-active=true]){border-color:color-mix(in srgb,var(--one-accent) 40%,var(--one-line))}
1357
1465
  .theone-nav{display:flex;align-items:center;gap:10px;color:var(--one-accent);font-family:inherit;position:relative;z-index:1}
1358
- .theone-symbol{width:18px;height:18px;border:1px solid currentColor;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;flex:none}
1359
- .theone-symbol:after{content:'';width:4px;height:4px;border-radius:50%;background:currentColor}
1360
- .theone-nav[data-wide=true] .theone-symbol{margin:0 3px}
1466
+ .theone-symbol-wrap{position:relative;display:inline-flex;flex:none;width:20px;height:20px}
1467
+ .theone-nav .theone-symbol{display:block;width:20px;height:20px;flex:none;overflow:visible;border:0;border-radius:0;background:none}
1468
+ .theone-nav[data-wide=true] .theone-symbol-wrap{margin:0 2px}
1469
+ .theone-update-dot{position:absolute;top:-2px;right:-2px;width:7px;height:7px;border-radius:50%;background:#e8590c;box-shadow:0 0 0 2px var(--one-tint)}
1470
+ button:has(.theone-nav[data-wide=true])>span:has(.theone-nav){flex:1;min-width:0}
1471
+ .theone-nav[data-wide=true]{width:100%}
1472
+ .theone-update{margin-left:auto;align-self:center;display:inline-flex;align-items:center;gap:5px;padding:5px 10px;border-radius:999px;border:1px solid color-mix(in srgb,var(--one-accent) 45%,transparent);background:color-mix(in srgb,var(--one-accent) 12%,transparent);color:var(--one-accent);font-size:12px;line-height:16px;white-space:nowrap;cursor:pointer;transition:background 150ms ease}
1473
+ .theone-update:hover{background:color-mix(in srgb,var(--one-accent) 22%,transparent)}
1474
+ .theone-update:focus-visible{outline:2px solid var(--one-accent);outline-offset:2px}
1475
+ .theone-update small{font-size:11px;opacity:.75}
1476
+ .theone-update[data-state=installing],.theone-update[data-state=reloading],.theone-update[data-state=restart]{cursor:default}
1477
+ .theone-update[data-state=failed]{color:#d9480f;border-color:#d9480f66;background:#d9480f14}
1478
+ .theone-update-spin{width:10px;height:10px;border-radius:50%;border:1.5px solid currentColor;border-right-color:transparent;animation:theone-spin 800ms linear infinite}
1479
+ @keyframes theone-spin{to{transform:rotate(360deg)}}
1480
+ @media(prefers-reduced-motion:reduce){.theone-update-spin{animation:none}}
1361
1481
  .theone-entry-copy{display:flex;flex-direction:column;align-items:flex-start;gap:2px}
1362
1482
  .theone-entry-title{display:flex;align-items:center;gap:9px;line-height:22px}
1363
1483
  .theone-wordmark{display:inline-flex;align-items:baseline;gap:1px;white-space:nowrap}
File without changes
File without changes
File without changes
File without changes
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Context, Service } from '@deepseek-ai/cordis';
2
2
  import z from '@deepseek-ai/schemastery';
3
- import type { GenerateOptions, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm';
3
+ import type { GenerateOptions, LlmModelInfo, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm';
4
+ import type { ModelSelection } from '@deepseek-ai/dsh-agent';
4
5
  import type { SessionEventWindow } from '@deepseek-ai/dsh-session-query';
5
6
  import { HistoryCatalog } from './history-catalog.ts';
6
7
  import { ContextStore } from './store.ts';
@@ -8,6 +9,7 @@ import type { RouterReceipt } from './llm-router.ts';
8
9
  import type { LinkageSnapshot } from './catalog-types.ts';
9
10
  import { type SettingsSnapshot } from './settings-types.ts';
10
11
  import { type LinkScope } from './linkage.ts';
12
+ import { Updater } from './update.ts';
11
13
  export interface Config {
12
14
  databasePath?: string;
13
15
  contextsPath?: string;
@@ -54,6 +56,8 @@ declare module '@deepseek-ai/dsh-llm' {
54
56
  };
55
57
  }
56
58
  }
59
+ export declare function viaModel(selection: ModelSelection): string;
60
+ export declare function parseVia(id: string | undefined): ModelSelection | undefined;
57
61
  export default class TheOne extends Service {
58
62
  private config;
59
63
  static inject: string[];
@@ -72,6 +76,8 @@ export default class TheOne extends Service {
72
76
  private readonly closing;
73
77
  /** A model the user picked in main chat's own model selector; it answers through the Workers. */
74
78
  private pickedModel?;
79
+ /** Update checks and one-click install through DSH's plugin manager. */
80
+ readonly updater: Updater;
75
81
  /** Sessions whose current step answers through TheOne; only these refuse to run tools themselves. */
76
82
  private readonly throughTheOne;
77
83
  constructor(ctx: Context, config: Config);
@@ -89,8 +95,12 @@ export default class TheOne extends Service {
89
95
  /** Capture before Web saves the gateway itself as DSH's new default. */
90
96
  captureDefaultModel(): void;
91
97
  private backingModel;
92
- /** The entry has exactly the configured backing model's capacity, including DSH overrides. */
93
- gatewayModelInfo(provider?: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
98
+ /** Every model DSH offers besides TheOne; a provider that cannot list its models in time offers none. */
99
+ private offeredModels;
100
+ /** Main chat's model menu: plain TheOne (follow DSH), then "TheOne · <model>" for every model DSH offers. */
101
+ gatewayModels(provider: string): Promise<LlmModelInfo[]>;
102
+ /** The entry has exactly the backing model's capacity, including DSH overrides. */
103
+ gatewayModelInfo(provider?: string, signal?: AbortSignal, model?: string): Promise<LlmResolvedModelInfo>;
94
104
  /** This main chat's recent text turns, each attributed to the topic it was routed to. */
95
105
  private recentMainChat;
96
106
  /** Recover bounded routing context from DSH references after the Gateway is rebuilt. */
package/dist/index.js CHANGED
@@ -19,10 +19,26 @@ import { EDITABLE_SETTINGS_KEYS, RESTART_SETTINGS_KEYS } from "./settings-types.
19
19
  import { validateSettings } from "./settings.js";
20
20
  import { RESTART_CODE, WorkerRun } from "./run.js";
21
21
  import { buildBriefing, LINK_SIGNAL, relatedTopics } from "./linkage.js";
22
+ import { PACKAGE_NAME, Updater } from "./update.js";
22
23
  function redactDescriptor(text) {
23
24
  return text.replace(/\bsk-[a-zA-Z0-9_-]{16,}|\bBearer\s+\S+/gi, '[REDACTED]')
24
25
  .replace(/((?:api[_ -]?key|password|密码|密钥)\s*[:=:]\s*)\S+/gi, '$1[REDACTED]');
25
26
  }
27
+ /**
28
+ * Main chat's model entries besides plain "TheOne": "TheOne · <model>" still answers through TheOne,
29
+ * with that model doing the routing and the background work.
30
+ */
31
+ const VIA = 'via:';
32
+ export function viaModel(selection) { return `${VIA}${selection.provider}/${selection.model}`; }
33
+ export function parseVia(id) {
34
+ if (!id?.startsWith(VIA))
35
+ return undefined;
36
+ const rest = id.slice(VIA.length);
37
+ const slash = rest.indexOf('/');
38
+ if (slash <= 0 || slash === rest.length - 1 || rest.slice(0, slash) === 'theone')
39
+ return undefined;
40
+ return { provider: rest.slice(0, slash), model: rest.slice(slash + 1) };
41
+ }
26
42
  /** Gateway provider delegates each accepted input to its context's DSH worker. */
27
43
  class GatewayAdapter extends LlmAdapter {
28
44
  service;
@@ -31,15 +47,11 @@ class GatewayAdapter extends LlmAdapter {
31
47
  this.service = service;
32
48
  }
33
49
  providerInfo(provider) { return { id: provider, name: 'TheOne' }; }
34
- async listModels(provider) {
35
- // The entry accepts whatever the backing model accepts (e.g. images).
36
- const info = await this.service.gatewayModelInfo(provider).catch(() => undefined);
37
- return [{ provider, id: 'gateway', name: 'TheOne', inputModalities: [...(info?.inputModalities ?? ['text'])] }];
38
- }
50
+ listModels(provider) { return this.service.gatewayModels(provider); }
39
51
  resolveModel(provider, model, signal) {
40
- if (model !== 'gateway')
41
- throw new Error('TheOne only exposes the gateway model');
42
- return this.service.gatewayModelInfo(provider, signal);
52
+ if (model !== 'gateway' && !parseVia(model))
53
+ throw new Error('TheOne only exposes its gateway models');
54
+ return this.service.gatewayModelInfo(provider, signal, model);
43
55
  }
44
56
  providerRetryPolicy() { return resolveRetryPolicy({ mode: 'normal', maxRetries: 0 }, 'theone.retry'); }
45
57
  stream(options) { return this.service.answer(options); }
@@ -85,6 +97,28 @@ function reasonLabel(reason) {
85
97
  function settingsConfig(values) {
86
98
  return { ...values, workerProvider: values.workerProvider ?? undefined, workerModel: values.workerModel ?? undefined, contextsPath: values.contextsPath ?? undefined };
87
99
  }
100
+ /**
101
+ * Switch an installed bundle off and on so DSH loads its new version. This disposes the running
102
+ * TheOne, so it runs after the current request has answered and outlives this instance.
103
+ */
104
+ function reloadBundle(manager, bundle) {
105
+ setTimeout(() => {
106
+ void (async () => {
107
+ const off = await manager.setBundleEnabled(bundle, false);
108
+ if (off.application === 'failed' || off.application === 'cancelled')
109
+ return;
110
+ await manager.setBundleEnabled(bundle, true);
111
+ })().catch(error => console.warn('TheOne could not reload itself after updating; restart DSH to apply the update.', error));
112
+ }, 300);
113
+ }
114
+ function readOwnVersion() {
115
+ try {
116
+ return String(JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version ?? '0.0.0');
117
+ }
118
+ catch {
119
+ return '0.0.0';
120
+ }
121
+ }
88
122
  function readDescriptors(path) {
89
123
  const value = JSON.parse(readFileSync(path, 'utf8'));
90
124
  if (!Array.isArray(value))
@@ -132,6 +166,8 @@ export default class TheOne extends Service {
132
166
  closing = new Map();
133
167
  /** A model the user picked in main chat's own model selector; it answers through the Workers. */
134
168
  pickedModel;
169
+ /** Update checks and one-click install through DSH's plugin manager. */
170
+ updater;
135
171
  /** Sessions whose current step answers through TheOne; only these refuse to run tools themselves. */
136
172
  throughTheOne = new WeakMap();
137
173
  constructor(ctx, config) {
@@ -164,6 +200,18 @@ export default class TheOne extends Service {
164
200
  }
165
201
  descriptors ??= config.contextsPath ? readDescriptors(config.contextsPath) : [];
166
202
  this.gatewayDirectory = resolve(dirname(databasePath), 'gateway');
203
+ this.updater = new Updater(readOwnVersion(), () => {
204
+ // The profile's package.json records how this plugin was installed (GitHub, npm or a local path).
205
+ const dir = ctx.profileContext?.dir;
206
+ if (!dir)
207
+ return undefined;
208
+ try {
209
+ return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).dependencies?.[PACKAGE_NAME];
210
+ }
211
+ catch {
212
+ return undefined;
213
+ }
214
+ });
167
215
  ctx.effect(() => async () => {
168
216
  try {
169
217
  await Promise.all([...this.workers.values()].map(handle => handle.dispose()));
@@ -264,8 +312,8 @@ export default class TheOne extends Service {
264
312
  return config;
265
313
  }
266
314
  this.throughTheOne.set(payload.agent, true);
267
- // The resolved selection is authoritative; TheOne itself means "follow DSH's selected model".
268
- this.pickedModel = config.provider === 'theone' ? undefined : { provider: config.provider, model: config.model };
315
+ // The resolved selection is authoritative; plain TheOne means "follow DSH's selected model".
316
+ this.pickedModel = config.provider === 'theone' ? parseVia(config.model) : { provider: config.provider, model: config.model };
269
317
  return config.provider === 'theone' ? config : { ...config, provider: 'theone', model: 'gateway' };
270
318
  }, { prepend: true });
271
319
  ctx.on('agent/pre-step', async ({ agent, signal }, next) => {
@@ -278,7 +326,7 @@ export default class TheOne extends Service {
278
326
  // Know main chat's model choice before routing, so this very message is classified with it.
279
327
  const pickedNow = selected?.signal === signal ? selected.model : undefined;
280
328
  if (provider === 'theone')
281
- this.pickedModel = undefined;
329
+ this.pickedModel = parseVia(pickedNow ?? agent.options.model);
282
330
  else if (provider && pickedNow)
283
331
  this.pickedModel = { provider, model: pickedNow };
284
332
  const users = decision.messages.filter(message => message.source.kind === 'user');
@@ -566,6 +614,18 @@ export default class TheOne extends Service {
566
614
  return Response.json({ error: code }, { status: code === 'GATEWAY_BUSY' ? 409 : 400 });
567
615
  }
568
616
  } }));
617
+ // Is a newer TheOne out, and install it with DSH's plugin manager (it loads after a restart).
618
+ child.effect(() => connection.fetch.register({ path: '/api/theone/update', methods: ['GET', 'POST'], requestBody: 'buffered', fetch: async (request) => {
619
+ if (request.method === 'POST' && (this.active || this.reservedGateway))
620
+ return Response.json({ ...await this.updater.status(), error: 'GATEWAY_BUSY' }, { status: 409 });
621
+ const manager = this.ctx.get('pluginManager');
622
+ // With DSH's hot reload, switching the bundle off and on loads the new version without a DSH restart.
623
+ const live = !!this.ctx.get('hmr') && typeof manager?.setBundleEnabled === 'function';
624
+ const status = request.method === 'POST'
625
+ ? await this.updater.install(manager, live ? bundle => reloadBundle(manager, bundle) : undefined)
626
+ : await this.updater.status(new URL(request.url).searchParams.has('force'));
627
+ return Response.json(status, { headers: { 'cache-control': 'no-store' } });
628
+ } }));
569
629
  child.effect(() => connection.fetch.register({ path: '/api/theone/sessions', methods: ['GET'], requestBody: 'buffered', fetch: async () => Response.json({ sessions: await this.attachableSessions() }, { headers: { 'cache-control': 'no-store' } }) }));
570
630
  // The user's corrections to topic linking always take precedence over what was learned.
571
631
  child.effect(() => connection.fetch.register({ path: '/api/theone/links', methods: ['POST'], requestBody: 'buffered', fetch: async (request) => {
@@ -701,7 +761,7 @@ export default class TheOne extends Service {
701
761
  const defaultSelection = this.ctx.agentDefaultModel.currentSelection();
702
762
  const selection = c.workerProvider && c.workerModel
703
763
  ? { provider: c.workerProvider, model: c.workerModel }
704
- : defaultSelection.provider !== 'theone' ? defaultSelection : this.store.rememberedModel(c.gatewayKey);
764
+ : defaultSelection.provider !== 'theone' ? defaultSelection : parseVia(defaultSelection.model) ?? this.store.rememberedModel(c.gatewayKey);
705
765
  let model = selection ? { provider: selection.provider, model: selection.model } : null;
706
766
  let modelUnavailable = !model;
707
767
  if (selection) {
@@ -729,10 +789,7 @@ export default class TheOne extends Service {
729
789
  savedValues = validateSettings(saved.values, { contextsPath: c.contextsPath ?? null });
730
790
  }
731
791
  catch { /* Keep the current usable form. */ }
732
- // A provider that cannot list its models in time offers none here.
733
- const listed = await Promise.all(this.ctx.llm.listProviders().filter(provider => provider.id !== 'theone').map(provider => Promise.race([this.ctx.llm.listModels(provider.id), new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000).unref())])
734
- .then(models => models.map(model => ({ provider: provider.id, id: model.id, name: model.name })), () => [])));
735
- const models = listed.flat();
792
+ const models = (await this.offeredModels()).map(model => ({ provider: model.provider, id: model.id, name: model.name }));
736
793
  return { values, model, models, modelUnavailable, savedValues, revision: saved?.revision ?? 0,
737
794
  restartRequired: RESTART_SETTINGS_KEYS.some(key => savedValues[key] !== activeEditable[key]) };
738
795
  }
@@ -749,8 +806,9 @@ export default class TheOne extends Service {
749
806
  }
750
807
  /** Capture before Web saves the gateway itself as DSH's new default. */
751
808
  captureDefaultModel() {
752
- const selection = this.ctx.agentDefaultModel.currentSelection();
753
- if (selection.provider && selection.model && selection.provider !== 'theone')
809
+ const current = this.ctx.agentDefaultModel.currentSelection();
810
+ const selection = current.provider === 'theone' ? parseVia(current.model) : current;
811
+ if (selection?.provider && selection.model)
754
812
  this.store.rememberModel(this.config.gatewayKey, selection);
755
813
  }
756
814
  backingModel() {
@@ -764,12 +822,32 @@ export default class TheOne extends Service {
764
822
  throw new RouterFailure('ROUTER_MODEL_MISSING');
765
823
  return selection;
766
824
  }
767
- /** The entry has exactly the configured backing model's capacity, including DSH overrides. */
768
- async gatewayModelInfo(provider = 'theone', signal) {
769
- const info = { provider, id: 'gateway', name: 'TheOne Gateway', inputModalities: ['text'] };
825
+ /** Every model DSH offers besides TheOne; a provider that cannot list its models in time offers none. */
826
+ async offeredModels() {
827
+ const listed = await Promise.all(this.ctx.llm.listProviders().filter(provider => provider.id !== 'theone').map(provider => Promise.race([this.ctx.llm.listModels(provider.id), new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000).unref())])
828
+ .catch(() => [])));
829
+ return listed.flat();
830
+ }
831
+ /** Main chat's model menu: plain TheOne (follow DSH), then "TheOne · <model>" for every model DSH offers. */
832
+ async gatewayModels(provider) {
833
+ // The entry accepts whatever the backing model accepts (e.g. images).
834
+ const info = await this.gatewayModelInfo(provider).catch(() => undefined);
835
+ const models = [{ provider, id: 'gateway', name: 'TheOne', inputModalities: [...(info?.inputModalities ?? ['text'])] }];
836
+ // A model pinned in TheOne's settings decides for the Workers, so choices here would do nothing.
837
+ if (this.config.workerProvider && this.config.workerModel)
838
+ return models;
839
+ for (const model of await this.offeredModels())
840
+ models.push({ provider, id: viaModel({ provider: model.provider, model: model.id }), name: `TheOne · ${model.name}`,
841
+ ...(model.description ? { description: model.description } : {}), ...(model.inputModalities ? { inputModalities: [...model.inputModalities] } : {}) });
842
+ return models;
843
+ }
844
+ /** The entry has exactly the backing model's capacity, including DSH overrides. */
845
+ async gatewayModelInfo(provider = 'theone', signal, model = 'gateway') {
846
+ const chosen = parseVia(model);
847
+ const info = { provider, id: model, name: 'TheOne Gateway', inputModalities: ['text'] };
770
848
  let selection;
771
849
  try {
772
- selection = this.backingModel();
850
+ selection = chosen ?? this.backingModel();
773
851
  }
774
852
  catch (error) {
775
853
  if (error instanceof RouterFailure && error.code === 'ROUTER_MODEL_MISSING')
@@ -777,6 +855,8 @@ export default class TheOne extends Service {
777
855
  throw error;
778
856
  }
779
857
  const backing = await this.ctx.llm.resolveModelInfo(selection.provider, selection.model, signal);
858
+ if (chosen)
859
+ info.name = `TheOne · ${backing.name}`;
780
860
  // Image input and the thinking-effort choices are the backing model's, so main chat offers the same controls.
781
861
  return { ...info, ...(backing.inputModalities ? { inputModalities: [...backing.inputModalities] } : {}),
782
862
  ...(backing.reasoning ? { reasoning: backing.reasoning } : {}),
package/dist/linkage.d.ts CHANGED
File without changes
package/dist/linkage.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
package/dist/router.d.ts CHANGED
File without changes
package/dist/router.js CHANGED
File without changes
File without changes
File without changes
package/dist/run.d.ts CHANGED
File without changes
package/dist/run.js CHANGED
File without changes
File without changes
File without changes
File without changes
package/dist/settings.js CHANGED
File without changes
package/dist/store.d.ts CHANGED
File without changes
package/dist/store.js CHANGED
File without changes
package/dist/types.d.ts CHANGED
File without changes
package/dist/types.js CHANGED
File without changes
@@ -0,0 +1,55 @@
1
+ export declare const REPOSITORY = "YunongDai2005/dsh-theone";
2
+ export declare const PACKAGE_NAME = "dsh-theone";
3
+ /** Where this copy was installed from, which decides where its updates come from. */
4
+ export type UpdateSource = 'github' | 'npm' | 'other';
5
+ export interface UpdateStatus {
6
+ current: string;
7
+ latest?: string;
8
+ /** A newer version exists. */
9
+ available: boolean;
10
+ /** It can be installed from here (otherwise reinstall by hand). */
11
+ installable: boolean;
12
+ source: UpdateSource;
13
+ /** installing → reloading (TheOne restarts itself; DSH keeps running), restart (applies after DSH restarts) or failed. */
14
+ state?: 'installing' | 'reloading' | 'restart' | 'failed';
15
+ error?: string;
16
+ }
17
+ /** The DSH plugin manager's install call, as TheOne uses it. */
18
+ export interface PluginInstaller {
19
+ installBundle(spec: string, options?: {
20
+ enabled?: boolean;
21
+ }): Promise<{
22
+ application: string;
23
+ bundle?: string;
24
+ error?: {
25
+ code?: string;
26
+ message?: string;
27
+ } | unknown;
28
+ }>;
29
+ }
30
+ /** Compare dotted versions numerically; a pre-release sorts before its release. */
31
+ export declare function compareVersions(a: string, b: string): number;
32
+ /** Read the dependency spec DSH saved for this plugin: GitHub, npm, or something else (a local path, a tarball). */
33
+ export declare function installSource(spec: string | undefined): UpdateSource;
34
+ /** Checks for a newer TheOne and installs it through DSH's own plugin manager. */
35
+ export declare class Updater {
36
+ readonly current: string;
37
+ private readonly spec;
38
+ private readonly fetcher;
39
+ private readonly now;
40
+ private checked?;
41
+ private state?;
42
+ private error?;
43
+ private pending?;
44
+ constructor(current: string, spec: () => string | undefined, fetcher?: typeof fetch, now?: () => number);
45
+ get source(): UpdateSource;
46
+ /** The latest known status; checks again at most every six hours (or now, when forced). */
47
+ status(force?: boolean): Promise<UpdateStatus>;
48
+ /**
49
+ * Install the newer version. With `reload`, TheOne then restarts itself so the new version runs
50
+ * without restarting DSH; otherwise it is loaded the next time DSH starts.
51
+ */
52
+ install(installer: PluginInstaller | undefined, reload?: (bundle: string) => void): Promise<UpdateStatus>;
53
+ private snapshot;
54
+ private check;
55
+ }