@sqdr2/dsh-fusion 0.3.0 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +148 -0
- package/README.en.md +9 -4
- package/README.md +9 -4
- package/RELEASING.md +129 -0
- package/RELEASING.zh.md +120 -0
- package/dshx.yml +3 -0
- package/lib/client.js +293 -75
- package/lib/client.js.map +1 -1
- package/lib/dsh-fusion.js +609 -237
- package/lib/types/client/FusionPermission.d.ts +15 -0
- package/lib/types/client/FusionSettings.d.ts +9 -1
- package/lib/types/client/status-polling.d.ts +6 -2
- package/lib/types/contracts.d.ts +2 -0
- package/lib/types/host/cache-policy.d.ts +16 -6
- package/lib/types/host/native-role-sandbox.d.ts +10 -2
- package/lib/types/host/native-selection.d.ts +15 -5
- package/lib/types/host/pairs.d.ts +47 -0
- package/lib/types/host/settings.d.ts +7 -3
- package/lib/types/identity.d.ts +16 -14
- package/lib/types/status.d.ts +17 -1
- package/package.json +64 -60
- package/src/client/FusionHint.tsx +11 -4
- package/src/client/FusionPermission.tsx +52 -0
- package/src/client/FusionSettings.tsx +109 -30
- package/src/client/index.ts +10 -5
- package/src/client/status-polling.ts +9 -4
- package/src/contracts.ts +2 -0
- package/src/dsh-fusion.ts +130 -54
- package/src/host/cache-policy.ts +18 -6
- package/src/host/native-role-sandbox.ts +18 -6
- package/src/host/native-scopes.ts +2 -2
- package/src/host/native-selection.ts +65 -21
- package/src/host/native-worker-brief.ts +18 -5
- package/src/host/pairs.ts +178 -0
- package/src/host/settings.ts +9 -5
- package/src/host/status.ts +24 -1
- package/src/identity.ts +39 -16
- package/src/status.ts +17 -1
package/lib/dsh-fusion.js
CHANGED
|
@@ -95,7 +95,7 @@ function recordedProcessId(value) {
|
|
|
95
95
|
return typeof pid === "number" && Number.isSafeInteger(pid) && pid > 0 ? pid : void 0;
|
|
96
96
|
}
|
|
97
97
|
/** Durable state names that mean the row was still open when its process stopped. */
|
|
98
|
-
const OPEN_EFFECT = new Set(["dispatch-started", "outcome-unknown"]);
|
|
98
|
+
const OPEN_EFFECT = /* @__PURE__ */ new Set(["dispatch-started", "outcome-unknown"]);
|
|
99
99
|
/**
|
|
100
100
|
* A row is unfinished when its own state says so. The families disagree on
|
|
101
101
|
* where they record that, so each one is read through its own field and an
|
|
@@ -228,7 +228,7 @@ function addWrites(first, second) {
|
|
|
228
228
|
};
|
|
229
229
|
}
|
|
230
230
|
requireThat$3(first.kind === "details" && second.kind === "details", "CACHE_LAYOUT_CONFLICT");
|
|
231
|
-
const keys = [
|
|
231
|
+
const keys = [.../* @__PURE__ */ new Set([...Object.keys(first.buckets), ...Object.keys(second.buckets)])].sort();
|
|
232
232
|
return {
|
|
233
233
|
kind: "details",
|
|
234
234
|
buckets: Object.fromEntries(keys.map((key) => {
|
|
@@ -519,143 +519,49 @@ const FUSION_API_PATH = "/api/fusion";
|
|
|
519
519
|
const LEGACY_FUSION_API_PATH = "/api/model-fusion";
|
|
520
520
|
/** The plugin id and package name this project used before the rename. */
|
|
521
521
|
const LEGACY_PLUGIN_NAME = "dsh-model-fusion";
|
|
522
|
-
/** Registered provider route for the Fusion model
|
|
522
|
+
/** Registered provider route for the Fusion model entries. */
|
|
523
523
|
const FUSION_PROVIDER = "dsh-fusion";
|
|
524
|
-
/**
|
|
524
|
+
/**
|
|
525
|
+
* The model id of the combo every installation starts with.
|
|
526
|
+
*
|
|
527
|
+
* It is a pair id like any other; keeping this exact spelling for the first
|
|
528
|
+
* combo means a durable `model/selection` written before combos existed still
|
|
529
|
+
* names a combo that exists.
|
|
530
|
+
*/
|
|
525
531
|
const FUSION_MODEL = "auto";
|
|
526
532
|
/**
|
|
527
|
-
* The
|
|
528
|
-
* `
|
|
529
|
-
*
|
|
530
|
-
*
|
|
533
|
+
* The model ids this provider advertises: `auto` plus one generated
|
|
534
|
+
* `pair-xxxxxxxx` per additional combo. The browser bundle inlines this module,
|
|
535
|
+
* so the namespace shape is fixed here rather than read from a settings
|
|
536
|
+
* document at import time.
|
|
531
537
|
*/
|
|
532
|
-
const
|
|
538
|
+
const FUSION_MODEL_PATTERN = /^(?:auto|pair-[a-z0-9]{6,12})$/;
|
|
539
|
+
/** Whether a model id names a Fusion combo on this provider. */
|
|
540
|
+
const isFusionModel = (model) => typeof model === "string" && FUSION_MODEL_PATTERN.test(model);
|
|
533
541
|
/** Whether a provider route belongs to this plugin, under either name. */
|
|
534
542
|
const isFusionProvider = (provider) => provider === "dsh-fusion" || provider === "dsh-model-fusion";
|
|
535
|
-
/** Whether a route names
|
|
536
|
-
const isFusionSelection = (route) => isFusionProvider(route?.provider) && route?.model
|
|
543
|
+
/** Whether a route names a Fusion combo, under either provider spelling. */
|
|
544
|
+
const isFusionSelection = (route) => isFusionProvider(route?.provider) && isFusionModel(route?.model);
|
|
537
545
|
/**
|
|
538
|
-
* How a native model-switch notice names
|
|
546
|
+
* How a native model-switch notice names a combo: `provider/model`, or the bare
|
|
547
|
+
* model when both sides share this provider (switching from one combo to
|
|
548
|
+
* another).
|
|
539
549
|
*
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
*
|
|
550
|
+
* The notice is a pseudo-change: it compares the virtual menu route with the
|
|
551
|
+
* physical Lead route that actually dispatches, so it would otherwise repeat on
|
|
552
|
+
* every Fusion step. Both provider spellings are accepted because a session
|
|
553
|
+
* that switched to Fusion before the rename carries the old one in its durable
|
|
554
|
+
* log; any combo id matches, since combos are not physical models.
|
|
543
555
|
*/
|
|
544
|
-
const
|
|
556
|
+
const FUSION_SELECTION_LABEL_PATTERN = /(?:(?:dsh-fusion|dsh-model-fusion)\/|→ )(?:auto|pair-[a-z0-9]{6,12})$/;
|
|
557
|
+
/** Whether a switch-notice summary describes a Fusion combo as either side. */
|
|
558
|
+
const isFusionSelectionLabel = (summary) => typeof summary === "string" && FUSION_SELECTION_LABEL_PATTERN.test(summary);
|
|
545
559
|
/**
|
|
546
|
-
* Whether a route already names
|
|
547
|
-
* intent that fails this is rewritten before the next request is
|
|
548
|
-
* the legacy spelling is a read-side alias, not a route
|
|
560
|
+
* Whether a route already names a combo under its current provider spelling. A
|
|
561
|
+
* durable intent that fails this is rewritten before the next request is
|
|
562
|
+
* assembled; the legacy spelling is a read-side alias, not a dispatching route.
|
|
549
563
|
*/
|
|
550
|
-
const isCurrentFusionSelection = (route) => route?.provider === "dsh-fusion" && route.model
|
|
551
|
-
//#endregion
|
|
552
|
-
//#region src/host/native-selection.ts
|
|
553
|
-
/** Catalog-only local adapter; the native request is physically routed before dispatch. */
|
|
554
|
-
var FusionCatalogAdapter = class extends LlmAdapter {
|
|
555
|
-
leadInputModalities;
|
|
556
|
-
constructor(leadInputModalities) {
|
|
557
|
-
super();
|
|
558
|
-
this.leadInputModalities = leadInputModalities;
|
|
559
|
-
}
|
|
560
|
-
providerInfo(provider) {
|
|
561
|
-
return {
|
|
562
|
-
id: provider,
|
|
563
|
-
name: "Fusion"
|
|
564
|
-
};
|
|
565
|
-
}
|
|
566
|
-
async listModels(provider) {
|
|
567
|
-
const lead = await this.leadInputModalities?.() ?? [];
|
|
568
|
-
return [{
|
|
569
|
-
provider,
|
|
570
|
-
id: FUSION_MODEL,
|
|
571
|
-
name: "Fusion · 自动",
|
|
572
|
-
description: "Lead 与 Worker 协作。在设置 → Fusion 中配置模型。",
|
|
573
|
-
inputModalities: [...new Set(["text", ...lead])]
|
|
574
|
-
}];
|
|
575
|
-
}
|
|
576
|
-
async resolveModel(provider, model) {
|
|
577
|
-
if (provider !== "dsh-fusion" || model !== "auto") throw new Error("Unknown local Fusion selection");
|
|
578
|
-
return (await this.listModels(provider))[0];
|
|
579
|
-
}
|
|
580
|
-
async *stream(_options) {
|
|
581
|
-
throw new Error("Fusion 尚未就绪。请前往设置 → Fusion 配置 Lead 和 Worker。");
|
|
582
|
-
}
|
|
583
|
-
};
|
|
584
|
-
/** Public native lifecycle integration. No model-menu replacement or Host patch. */
|
|
585
|
-
function installNativeFusionSelection(ctx, input) {
|
|
586
|
-
const pending = /* @__PURE__ */ new Map();
|
|
587
|
-
const failures = /* @__PURE__ */ new Map();
|
|
588
|
-
const desired = /* @__PURE__ */ new Map();
|
|
589
|
-
const selection = (agent) => desired.get(agent.id) ?? input.selection(agent);
|
|
590
|
-
const selected = (agent) => input.coordinator()?.bindings.read(agent.id)?.binding.selected === true;
|
|
591
|
-
const change = (agent) => {
|
|
592
|
-
if (agent.session.header.parentSession || pending.has(agent.id)) return;
|
|
593
|
-
const coordinator = input.coordinator(), profile = input.profile(), route = selection(agent);
|
|
594
|
-
if (!coordinator) return;
|
|
595
|
-
if (isFusionSelection(route)) {
|
|
596
|
-
if (!selected(agent) && profile) coordinator.selectBeforeAssembly(agent, profile);
|
|
597
|
-
if (selected(agent)) {
|
|
598
|
-
if (!isCurrentFusionSelection(agent.session.snapshotEvents().findLast((event) => event.type === "model/selection")?.data)) agent.session.append("model/selection", {
|
|
599
|
-
provider: FUSION_PROVIDER,
|
|
600
|
-
model: FUSION_MODEL
|
|
601
|
-
});
|
|
602
|
-
failures.delete(agent.id);
|
|
603
|
-
}
|
|
604
|
-
return;
|
|
605
|
-
}
|
|
606
|
-
if (!selected(agent)) return;
|
|
607
|
-
const transition = (async () => {
|
|
608
|
-
if (agent.status !== "idle") await coordinator.pause(agent);
|
|
609
|
-
await coordinator.clear(agent);
|
|
610
|
-
failures.delete(agent.id);
|
|
611
|
-
})().catch((error) => {
|
|
612
|
-
failures.set(agent.id, String(error));
|
|
613
|
-
}).finally(() => {
|
|
614
|
-
pending.delete(agent.id);
|
|
615
|
-
});
|
|
616
|
-
pending.set(agent.id, transition);
|
|
617
|
-
};
|
|
618
|
-
const disposers = [
|
|
619
|
-
ctx.on("session/event", (session, event) => {
|
|
620
|
-
if (event.type !== "model/selection") return;
|
|
621
|
-
desired.set(session.id, event.data);
|
|
622
|
-
const agent = ctx.agents.get(SessionId$1(session.id));
|
|
623
|
-
if (agent && (agent.status === "idle" || !isFusionSelection(event.data))) change(agent);
|
|
624
|
-
else if (agent && !selected(agent)) agent.cancel({
|
|
625
|
-
kind: "hook",
|
|
626
|
-
reason: "Fusion selection takes effect after this turn settles"
|
|
627
|
-
}, { keepInbox: true });
|
|
628
|
-
}),
|
|
629
|
-
ctx.on("agent/status", ({ agent, status }) => {
|
|
630
|
-
if (status !== "running" || agent.session.header.parentSession) return;
|
|
631
|
-
if (pending.has(agent.id) && selected(agent)) {
|
|
632
|
-
agent.cancel({
|
|
633
|
-
kind: "hook",
|
|
634
|
-
reason: "Model switch is draining the previous Fusion task"
|
|
635
|
-
}, { keepInbox: true });
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
try {
|
|
639
|
-
change(agent);
|
|
640
|
-
} catch (error) {
|
|
641
|
-
failures.set(agent.id, String(error));
|
|
642
|
-
}
|
|
643
|
-
}),
|
|
644
|
-
ctx.on("agent/request", async (payload, next) => {
|
|
645
|
-
const config = await next(), agent = payload.agent;
|
|
646
|
-
if (agent.session.header.parentSession) return config;
|
|
647
|
-
const route = selection(agent);
|
|
648
|
-
if (pending.has(agent.id) && selected(agent) || failures.has(agent.id)) throw new Error(failures.get(agent.id) ?? "Fusion 正在完成模型切换,请稍后继续");
|
|
649
|
-
if (isFusionSelection(route)) {
|
|
650
|
-
if (!selected(agent)) throw new Error("请先在设置 → Fusion 配置 Lead 和 Worker,再继续此会话。");
|
|
651
|
-
} else if (selected(agent)) throw new Error("先停止当前 Fusion 任务,再切换模型。");
|
|
652
|
-
return config;
|
|
653
|
-
}, { prepend: true })
|
|
654
|
-
];
|
|
655
|
-
return () => {
|
|
656
|
-
for (const dispose of disposers.reverse()) dispose();
|
|
657
|
-
};
|
|
658
|
-
}
|
|
564
|
+
const isCurrentFusionSelection = (route) => route?.provider === "dsh-fusion" && isFusionModel(route.model);
|
|
659
565
|
//#endregion
|
|
660
566
|
//#region src/profile/keepalive.ts
|
|
661
567
|
/** No provider-name heuristic can establish cache support for a DSH route. */
|
|
@@ -701,18 +607,6 @@ function profileRoutes(profile) {
|
|
|
701
607
|
}
|
|
702
608
|
//#endregion
|
|
703
609
|
//#region src/host/settings.ts
|
|
704
|
-
/** Saved Lead route from a raw settings or profile document, or undefined when unconfigured or pointed at Fusion itself. */
|
|
705
|
-
function savedLeadRoute(value) {
|
|
706
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
707
|
-
const route = value.lead;
|
|
708
|
-
if (!route || typeof route !== "object" || Array.isArray(route)) return void 0;
|
|
709
|
-
const provider = route.provider, model = route.model;
|
|
710
|
-
if (typeof provider !== "string" || typeof model !== "string" || isFusionProvider(provider)) return void 0;
|
|
711
|
-
return {
|
|
712
|
-
provider,
|
|
713
|
-
model
|
|
714
|
-
};
|
|
715
|
-
}
|
|
716
610
|
/** Configuration chooses registered physical models; it cannot create adapters. */
|
|
717
611
|
function validatePairChoice(value, catalog) {
|
|
718
612
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(bi("请选择 Lead 和 Worker", "Choose a Lead and a Sidekick"));
|
|
@@ -749,11 +643,11 @@ function validatePairChoice(value, catalog) {
|
|
|
749
643
|
} }
|
|
750
644
|
};
|
|
751
645
|
}
|
|
752
|
-
function profileFromChoice(pair) {
|
|
646
|
+
function profileFromChoice(pair, id = "fusion-auto") {
|
|
753
647
|
const { outputTokens, ...routes } = pair;
|
|
754
648
|
return {
|
|
755
649
|
schemaVersion: 1,
|
|
756
|
-
id
|
|
650
|
+
id,
|
|
757
651
|
version: outputTokens?.worker === 128e3 ? "0.1.0-worker-128k" : "0.1.0-faithful",
|
|
758
652
|
enabled: true,
|
|
759
653
|
quality: "experimental",
|
|
@@ -764,11 +658,15 @@ function profileFromChoice(pair) {
|
|
|
764
658
|
...outputTokens === void 0 ? {} : { context: Object.fromEntries(Object.entries(outputTokens).map(([role, reserveOutputTokens]) => [role, { reserveOutputTokens }])) }
|
|
765
659
|
};
|
|
766
660
|
}
|
|
767
|
-
/**
|
|
768
|
-
|
|
661
|
+
/**
|
|
662
|
+
* Product defaults; explicit legacy/benchmark profiles retain their old contract.
|
|
663
|
+
* `id` names the combo; the first one keeps the historical `fusion-auto` so a
|
|
664
|
+
* profile frozen before combos existed keeps its identity.
|
|
665
|
+
*/
|
|
666
|
+
function modelProfileFromChoice(pair, id = "fusion-auto") {
|
|
769
667
|
const { outputTokens: _output, ...native } = pair;
|
|
770
668
|
return {
|
|
771
|
-
...profileFromChoice(native),
|
|
669
|
+
...profileFromChoice(native, id),
|
|
772
670
|
version: "0.2.0-enforced-v3",
|
|
773
671
|
interactionMode: "model-like",
|
|
774
672
|
workflowPolicy: "enforced-v3"
|
|
@@ -786,6 +684,328 @@ function choiceFromProfile(profile) {
|
|
|
786
684
|
};
|
|
787
685
|
}
|
|
788
686
|
//#endregion
|
|
687
|
+
//#region src/host/pairs.ts
|
|
688
|
+
/** Where the combo list lives; `settings:profile` stays readable for migration. */
|
|
689
|
+
const PAIRS_DOCUMENT = "settings:pairs";
|
|
690
|
+
const LEGACY_PROFILE_DOCUMENT = "settings:profile";
|
|
691
|
+
/** The migrated first combo keeps the label the historical entry had. */
|
|
692
|
+
const DEFAULT_PAIR_NAME = "自动";
|
|
693
|
+
const profileIdOf = (model) => `fusion-${model}`;
|
|
694
|
+
/** The combo a frozen profile belongs to, or undefined for a non-combo profile. */
|
|
695
|
+
function modelOfProfileId(id) {
|
|
696
|
+
const model = id.startsWith("fusion-") ? id.slice(7) : void 0;
|
|
697
|
+
return isFusionModel(model) ? model : void 0;
|
|
698
|
+
}
|
|
699
|
+
const newModel = (taken) => {
|
|
700
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
701
|
+
const model = `pair-${randomUUID().replace(/-/g, "").slice(0, 8)}`;
|
|
702
|
+
if (!taken.includes(model)) return model;
|
|
703
|
+
}
|
|
704
|
+
throw new Error(bi("无法生成新的组合标识", "Could not allocate a new combo id"));
|
|
705
|
+
};
|
|
706
|
+
function object$1(value) {
|
|
707
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
708
|
+
}
|
|
709
|
+
function route(value) {
|
|
710
|
+
const item = object$1(value);
|
|
711
|
+
if (!item || typeof item.provider !== "string" || !item.provider.trim() || typeof item.model !== "string" || !item.model.trim()) return void 0;
|
|
712
|
+
return {
|
|
713
|
+
provider: item.provider,
|
|
714
|
+
model: item.model,
|
|
715
|
+
...typeof item.reasoningEffort === "string" && item.reasoningEffort.trim() ? { reasoningEffort: item.reasoningEffort } : {}
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Read one stored pair without the Host catalog: a combo whose model has since
|
|
720
|
+
* been retired must stay visible (and editable) instead of hiding the list.
|
|
721
|
+
*/
|
|
722
|
+
function structuralPair(value) {
|
|
723
|
+
const raw = object$1(value);
|
|
724
|
+
const lead = route(raw?.lead), worker = route(raw?.worker);
|
|
725
|
+
if (!raw || !lead || !worker) return void 0;
|
|
726
|
+
const compactorRoute = route(object$1(raw.compactor)?.route);
|
|
727
|
+
const maxOutputTokens = object$1(raw.compactor)?.maxOutputTokens;
|
|
728
|
+
let cacheKeepalive;
|
|
729
|
+
try {
|
|
730
|
+
cacheKeepalive = keepaliveChoice(raw.cacheKeepalive);
|
|
731
|
+
} catch {
|
|
732
|
+
cacheKeepalive = void 0;
|
|
733
|
+
}
|
|
734
|
+
let compactor;
|
|
735
|
+
try {
|
|
736
|
+
compactor = compactorRoute ? compactorChoice({
|
|
737
|
+
route: compactorRoute,
|
|
738
|
+
maxOutputTokens
|
|
739
|
+
}) : void 0;
|
|
740
|
+
} catch {
|
|
741
|
+
compactor = void 0;
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
lead,
|
|
745
|
+
worker,
|
|
746
|
+
...compactor === void 0 ? {} : { compactor },
|
|
747
|
+
...cacheKeepalive === void 0 ? {} : { cacheKeepalive }
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
/** A pre-combo profile document as the choice it was saved from. */
|
|
751
|
+
function choiceFromStoredProfile(value) {
|
|
752
|
+
const raw = object$1(value);
|
|
753
|
+
const pair = structuralPair(raw);
|
|
754
|
+
if (!raw || !pair) return void 0;
|
|
755
|
+
if (raw.interactionMode === "model-like") return pair;
|
|
756
|
+
const context = object$1(raw.context);
|
|
757
|
+
const outputTokens = Object.fromEntries(["lead", "worker"].map((role) => [role, object$1(context?.[role])?.reserveOutputTokens]).filter(([, tokens]) => Number.isSafeInteger(tokens) && Number(tokens) > 0 && Number(tokens) !== 8e3).map(([role, tokens]) => [role, Number(tokens)]));
|
|
758
|
+
return Object.keys(outputTokens).length ? {
|
|
759
|
+
...pair,
|
|
760
|
+
outputTokens
|
|
761
|
+
} : pair;
|
|
762
|
+
}
|
|
763
|
+
function parsePairs(value) {
|
|
764
|
+
const raw = object$1(value);
|
|
765
|
+
if (!raw || raw.schemaVersion !== 1 || !Array.isArray(raw.pairs)) return void 0;
|
|
766
|
+
const seen = /* @__PURE__ */ new Set();
|
|
767
|
+
return raw.pairs.flatMap((item) => {
|
|
768
|
+
const entry = object$1(item);
|
|
769
|
+
const pair = structuralPair(entry?.pair);
|
|
770
|
+
if (!entry || !pair || typeof entry.id !== "string" || !isFusionModel(entry.id) || seen.has(entry.id)) return [];
|
|
771
|
+
if (typeof entry.name !== "string" || !entry.name.trim() || entry.name.length > 40) return [];
|
|
772
|
+
seen.add(entry.id);
|
|
773
|
+
return [{
|
|
774
|
+
id: entry.id,
|
|
775
|
+
name: entry.name,
|
|
776
|
+
pair
|
|
777
|
+
}];
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
/** The saved combos; a pre-combo installation migrates to one `auto` combo on read. */
|
|
781
|
+
function readPairs(store) {
|
|
782
|
+
const document = store.readDocument(PAIRS_DOCUMENT);
|
|
783
|
+
if (document) {
|
|
784
|
+
const pairs = parsePairs(document.value);
|
|
785
|
+
if (!pairs?.length) throw new Error(bi("组合配置已损坏,请在 Fusion 页面重新保存", "The combo list is unreadable; save it again from the Fusion page"));
|
|
786
|
+
return {
|
|
787
|
+
revision: document.revision,
|
|
788
|
+
pairs,
|
|
789
|
+
migrated: false
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
const legacy = choiceFromStoredProfile(store.readDocument(LEGACY_PROFILE_DOCUMENT)?.value);
|
|
793
|
+
return legacy ? {
|
|
794
|
+
revision: 0,
|
|
795
|
+
pairs: [{
|
|
796
|
+
id: FUSION_MODEL,
|
|
797
|
+
name: DEFAULT_PAIR_NAME,
|
|
798
|
+
pair: legacy
|
|
799
|
+
}],
|
|
800
|
+
migrated: true
|
|
801
|
+
} : {
|
|
802
|
+
revision: 0,
|
|
803
|
+
pairs: [],
|
|
804
|
+
migrated: false
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
function pairName(value, fallback) {
|
|
808
|
+
if (typeof value !== "string") return fallback;
|
|
809
|
+
const name = value.trim();
|
|
810
|
+
if (!name) return fallback;
|
|
811
|
+
if (name.length > 40) throw new Error(bi(`组合名称最多 40 个字符`, `A combo name is at most 40 characters`));
|
|
812
|
+
return name;
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Validate the list a settings action proposes. Ids are assigned here, never by
|
|
816
|
+
* the client, so a stale tab cannot name a combo that is already frozen
|
|
817
|
+
* elsewhere; reordering only moves the menu rows.
|
|
818
|
+
*/
|
|
819
|
+
function validatePairs(value, catalog, existing) {
|
|
820
|
+
if (!Array.isArray(value) || !value.length) throw new Error(bi("请至少保留一个组合", "Keep at least one combo"));
|
|
821
|
+
if (value.length > 6) throw new Error(bi(`最多保存 6 个组合`, `At most 6 combos can be saved`));
|
|
822
|
+
const known = new Set(existing.map((pair) => pair.id));
|
|
823
|
+
const taken = /* @__PURE__ */ new Set();
|
|
824
|
+
const names = /* @__PURE__ */ new Set();
|
|
825
|
+
return value.map((item, index) => {
|
|
826
|
+
const raw = object$1(item);
|
|
827
|
+
if (!raw) throw new Error(bi("组合配置无效", "Invalid combo entry"));
|
|
828
|
+
const requested = typeof raw.id === "string" ? raw.id : "";
|
|
829
|
+
const id = (known.has(requested) || requested === "auto") && !taken.has(requested) ? requested : newModel([...taken]);
|
|
830
|
+
taken.add(id);
|
|
831
|
+
const name = pairName(raw.name, index === 0 ? DEFAULT_PAIR_NAME : `组合 ${index + 1}`);
|
|
832
|
+
if (names.has(name)) throw new Error(bi(`组合名称 "${name}" 重复`, `The combo name "${name}" is used twice`));
|
|
833
|
+
names.add(name);
|
|
834
|
+
return {
|
|
835
|
+
id,
|
|
836
|
+
name,
|
|
837
|
+
pair: validatePairChoice(raw.pair, catalog)
|
|
838
|
+
};
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
function pairsDocument(pairs) {
|
|
842
|
+
return {
|
|
843
|
+
schemaVersion: 1,
|
|
844
|
+
pairs: pairs.map(({ id, name, pair }) => ({
|
|
845
|
+
id,
|
|
846
|
+
name,
|
|
847
|
+
pair
|
|
848
|
+
}))
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Persist the list under one compare-and-swap revision. The first combo is
|
|
853
|
+
* mirrored into the pre-combo `settings:profile` document so an older tab (or a
|
|
854
|
+
* downgrade) still finds the pair it knows about.
|
|
855
|
+
*/
|
|
856
|
+
function writePairs(store, expectedRevision, pairs) {
|
|
857
|
+
const mirror = pairs.find((pair) => pair.id === "auto") ?? pairs[0];
|
|
858
|
+
const legacy = store.readDocument(LEGACY_PROFILE_DOCUMENT);
|
|
859
|
+
return store.writeDocuments([{
|
|
860
|
+
id: PAIRS_DOCUMENT,
|
|
861
|
+
expectedRevision,
|
|
862
|
+
value: pairsDocument(pairs)
|
|
863
|
+
}, {
|
|
864
|
+
id: LEGACY_PROFILE_DOCUMENT,
|
|
865
|
+
expectedRevision: legacy?.revision ?? 0,
|
|
866
|
+
value: modelProfileFromChoice(mirror.pair, profileIdOf(mirror.id))
|
|
867
|
+
}])[0];
|
|
868
|
+
}
|
|
869
|
+
//#endregion
|
|
870
|
+
//#region src/host/native-selection.ts
|
|
871
|
+
const textOnly = [.../* @__PURE__ */ new Set(["text"])];
|
|
872
|
+
/** Catalog-only local adapter; the native request is physically routed before dispatch. */
|
|
873
|
+
var FusionCatalogAdapter = class extends LlmAdapter {
|
|
874
|
+
models;
|
|
875
|
+
constructor(models) {
|
|
876
|
+
super();
|
|
877
|
+
this.models = models;
|
|
878
|
+
}
|
|
879
|
+
providerInfo(provider) {
|
|
880
|
+
return {
|
|
881
|
+
id: provider,
|
|
882
|
+
name: "Fusion"
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
async listModels(provider) {
|
|
886
|
+
const declared = await this.models?.().catch(() => []) ?? [];
|
|
887
|
+
return (declared.length ? declared : [{
|
|
888
|
+
id: FUSION_MODEL,
|
|
889
|
+
name: "Fusion · 自动",
|
|
890
|
+
inputModalities: textOnly
|
|
891
|
+
}]).map((entry) => ({
|
|
892
|
+
provider,
|
|
893
|
+
id: entry.id,
|
|
894
|
+
name: entry.name,
|
|
895
|
+
description: entry.description ?? "Lead 与 Worker 协作。在设置 → Fusion 中配置模型。",
|
|
896
|
+
inputModalities: [.../* @__PURE__ */ new Set(["text", ...entry.inputModalities])]
|
|
897
|
+
}));
|
|
898
|
+
}
|
|
899
|
+
async resolveModel(provider, model) {
|
|
900
|
+
if (provider !== "dsh-fusion" || !isFusionModel(model)) throw new Error("Unknown local Fusion selection");
|
|
901
|
+
const declared = (await this.listModels(provider)).find((entry) => entry.id === model);
|
|
902
|
+
if (!declared) throw new Error("Unknown local Fusion selection");
|
|
903
|
+
return declared;
|
|
904
|
+
}
|
|
905
|
+
async *stream(_options) {
|
|
906
|
+
throw new Error("Fusion 尚未就绪。请前往设置 → Fusion 配置 Lead 和 Worker。");
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
/** Public native lifecycle integration. No model-menu replacement or Host patch. */
|
|
910
|
+
function installNativeFusionSelection(ctx, input) {
|
|
911
|
+
const pending = /* @__PURE__ */ new Map();
|
|
912
|
+
const failures = /* @__PURE__ */ new Map();
|
|
913
|
+
const desired = /* @__PURE__ */ new Map();
|
|
914
|
+
const selection = (agent) => desired.get(agent.id) ?? input.selection(agent);
|
|
915
|
+
const binding = (agent) => input.coordinator()?.bindings.read(agent.id)?.binding;
|
|
916
|
+
const selected = (agent) => binding(agent)?.selected === true;
|
|
917
|
+
/** The combo a selected session is frozen to, or undefined when it runs no Fusion task. */
|
|
918
|
+
const runningCombo = (agent) => {
|
|
919
|
+
const current = binding(agent);
|
|
920
|
+
return current?.selected ? modelOfProfileId(current.profile.id) ?? "auto" : void 0;
|
|
921
|
+
};
|
|
922
|
+
const blocked = (model) => input.reason?.(model) ?? "请先在设置 → Fusion 配置 Lead 和 Worker,再继续此会话。";
|
|
923
|
+
const change = (agent) => {
|
|
924
|
+
if (agent.session.header.parentSession || pending.has(agent.id)) return;
|
|
925
|
+
const coordinator = input.coordinator(), route = selection(agent);
|
|
926
|
+
if (!coordinator) return;
|
|
927
|
+
if (isFusionSelection(route)) {
|
|
928
|
+
const model = route?.model ?? "auto";
|
|
929
|
+
const profile = input.profile(model), running = runningCombo(agent);
|
|
930
|
+
if (running !== void 0 && running !== model) {
|
|
931
|
+
const transition = (async () => {
|
|
932
|
+
if (agent.status !== "idle") await coordinator.pause(agent);
|
|
933
|
+
await coordinator.clear(agent);
|
|
934
|
+
if (profile) coordinator.selectBeforeAssembly(agent, profile);
|
|
935
|
+
else failures.set(agent.id, blocked(model));
|
|
936
|
+
})().catch((error) => {
|
|
937
|
+
failures.set(agent.id, String(error));
|
|
938
|
+
}).finally(() => {
|
|
939
|
+
pending.delete(agent.id);
|
|
940
|
+
});
|
|
941
|
+
pending.set(agent.id, transition);
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
if (running === void 0) if (profile) coordinator.selectBeforeAssembly(agent, profile);
|
|
945
|
+
else failures.set(agent.id, blocked(model));
|
|
946
|
+
if (selected(agent)) {
|
|
947
|
+
if (!isCurrentFusionSelection(agent.session.snapshotEvents().findLast((event) => event.type === "model/selection")?.data)) agent.session.append("model/selection", {
|
|
948
|
+
provider: FUSION_PROVIDER,
|
|
949
|
+
model
|
|
950
|
+
});
|
|
951
|
+
failures.delete(agent.id);
|
|
952
|
+
}
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
if (!selected(agent)) return;
|
|
956
|
+
const transition = (async () => {
|
|
957
|
+
if (agent.status !== "idle") await coordinator.pause(agent);
|
|
958
|
+
await coordinator.clear(agent);
|
|
959
|
+
failures.delete(agent.id);
|
|
960
|
+
})().catch((error) => {
|
|
961
|
+
failures.set(agent.id, String(error));
|
|
962
|
+
}).finally(() => {
|
|
963
|
+
pending.delete(agent.id);
|
|
964
|
+
});
|
|
965
|
+
pending.set(agent.id, transition);
|
|
966
|
+
};
|
|
967
|
+
const disposers = [
|
|
968
|
+
ctx.on("session/event", (session, event) => {
|
|
969
|
+
if (event.type !== "model/selection") return;
|
|
970
|
+
desired.set(session.id, event.data);
|
|
971
|
+
const agent = ctx.agents.get(SessionId$1(session.id));
|
|
972
|
+
if (agent && (agent.status === "idle" || !isFusionSelection(event.data))) change(agent);
|
|
973
|
+
else if (agent && !selected(agent)) agent.cancel({
|
|
974
|
+
kind: "hook",
|
|
975
|
+
reason: "Fusion selection takes effect after this turn settles"
|
|
976
|
+
}, { keepInbox: true });
|
|
977
|
+
}),
|
|
978
|
+
ctx.on("agent/status", ({ agent, status }) => {
|
|
979
|
+
if (status !== "running" || agent.session.header.parentSession) return;
|
|
980
|
+
if (pending.has(agent.id) && selected(agent)) {
|
|
981
|
+
agent.cancel({
|
|
982
|
+
kind: "hook",
|
|
983
|
+
reason: "Model switch is draining the previous Fusion task"
|
|
984
|
+
}, { keepInbox: true });
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
try {
|
|
988
|
+
change(agent);
|
|
989
|
+
} catch (error) {
|
|
990
|
+
failures.set(agent.id, String(error));
|
|
991
|
+
}
|
|
992
|
+
}),
|
|
993
|
+
ctx.on("agent/request", async (payload, next) => {
|
|
994
|
+
const config = await next(), agent = payload.agent;
|
|
995
|
+
if (agent.session.header.parentSession) return config;
|
|
996
|
+
const route = selection(agent);
|
|
997
|
+
if (pending.has(agent.id) && selected(agent) || failures.has(agent.id)) throw new Error(failures.get(agent.id) ?? "Fusion 正在完成模型切换,请稍后继续");
|
|
998
|
+
if (isFusionSelection(route)) {
|
|
999
|
+
if (!selected(agent)) throw new Error(blocked(route?.model ?? "auto"));
|
|
1000
|
+
} else if (selected(agent)) throw new Error("先停止当前 Fusion 任务,再切换模型。");
|
|
1001
|
+
return config;
|
|
1002
|
+
}, { prepend: true })
|
|
1003
|
+
];
|
|
1004
|
+
return () => {
|
|
1005
|
+
for (const dispose of disposers.reverse()) dispose();
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
//#endregion
|
|
789
1009
|
//#region src/host/cache-defaults.ts
|
|
790
1010
|
/**
|
|
791
1011
|
* Default prompt-cache behaviour for the models most people pair in Fusion, from each provider's official
|
|
@@ -1036,7 +1256,8 @@ function cacheDefaults(provider, model) {
|
|
|
1036
1256
|
source: "generic"
|
|
1037
1257
|
};
|
|
1038
1258
|
}
|
|
1039
|
-
const MIN_INTERVAL = 60
|
|
1259
|
+
const MIN_INTERVAL = 60;
|
|
1260
|
+
const MAX_INTERVAL = 3540;
|
|
1040
1261
|
/** A request reused its prefix when most of its input came from the cache. */
|
|
1041
1262
|
const cacheHit = (cacheRead, input) => cacheRead > 0 && cacheRead / (cacheRead + input) >= .5;
|
|
1042
1263
|
const key = (provider, model) => `${provider}\u0001${model}`;
|
|
@@ -1170,12 +1391,18 @@ var CachePolicy = class {
|
|
|
1170
1391
|
}));
|
|
1171
1392
|
}
|
|
1172
1393
|
};
|
|
1173
|
-
/**
|
|
1394
|
+
/**
|
|
1395
|
+
* One row of the settings page: what applies, why, what the defaults say, and
|
|
1396
|
+
* what was observed. Every saved combo contributes its routes, so a second
|
|
1397
|
+
* combo's models are listed and labelled even before a session uses them.
|
|
1398
|
+
*/
|
|
1174
1399
|
function cacheView(policy, pair, pairChoice) {
|
|
1175
|
-
const
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1400
|
+
const pairs = Array.isArray(pair) ? pair : pair ? [pair] : [];
|
|
1401
|
+
const same = (role, provider, model) => pairs.some((item) => item[role].provider === provider && item[role].model === model);
|
|
1402
|
+
const choiceOf = (role) => pairs.find((item) => item.cacheKeepalive)?.["cacheKeepalive"]?.[role] ?? pairChoice?.[role];
|
|
1403
|
+
return policy.routes(pairs.flatMap((item) => [item.lead, item.worker])).map(({ provider, model }) => {
|
|
1404
|
+
const role = same("lead", provider, model) ? "lead" : same("worker", provider, model) ? "worker" : void 0;
|
|
1405
|
+
const effective = policy.resolve(provider, model, role ?? "lead", role ? choiceOf(role) : void 0);
|
|
1179
1406
|
const stats = policy.stats(provider, model), { family, route } = effective.defaults;
|
|
1180
1407
|
return {
|
|
1181
1408
|
provider,
|
|
@@ -1214,7 +1441,7 @@ function cacheView(policy, pair, pairChoice) {
|
|
|
1214
1441
|
}
|
|
1215
1442
|
//#endregion
|
|
1216
1443
|
//#region src/profile/resolve.ts
|
|
1217
|
-
const PLACEHOLDERS = new Set([
|
|
1444
|
+
const PLACEHOLDERS = /* @__PURE__ */ new Set([
|
|
1218
1445
|
"__AUTHORIZED_PROVIDER__",
|
|
1219
1446
|
"__EXACT_MODEL_ID__",
|
|
1220
1447
|
"__EXACT_WORKER_ID__",
|
|
@@ -1634,20 +1861,20 @@ const ArtifactId = (raw) => asBrand(raw);
|
|
|
1634
1861
|
const SnapshotId = (raw) => asBrand(raw);
|
|
1635
1862
|
const EpochId = (raw) => asBrand(raw);
|
|
1636
1863
|
const UsdDecimal = (raw) => raw;
|
|
1637
|
-
const TERMINAL_PHASES = new Set([
|
|
1864
|
+
const TERMINAL_PHASES = /* @__PURE__ */ new Set([
|
|
1638
1865
|
"COMPLETED",
|
|
1639
1866
|
"CANCELLED",
|
|
1640
1867
|
"FAILED"
|
|
1641
1868
|
]);
|
|
1642
1869
|
//#endregion
|
|
1643
1870
|
//#region src/review/protocol.ts
|
|
1644
|
-
const VALID_DECISIONS = new Set([
|
|
1871
|
+
const VALID_DECISIONS = /* @__PURE__ */ new Set([
|
|
1645
1872
|
"approve",
|
|
1646
1873
|
"accept",
|
|
1647
1874
|
"rework",
|
|
1648
1875
|
"needs-decision"
|
|
1649
1876
|
]);
|
|
1650
|
-
const SUCCESS_REASONS = new Set([
|
|
1877
|
+
const SUCCESS_REASONS = /* @__PURE__ */ new Set([
|
|
1651
1878
|
"stop",
|
|
1652
1879
|
"end_turn",
|
|
1653
1880
|
"end-turn",
|
|
@@ -1656,24 +1883,24 @@ const SUCCESS_REASONS = new Set([
|
|
|
1656
1883
|
"completed",
|
|
1657
1884
|
"success"
|
|
1658
1885
|
]);
|
|
1659
|
-
const TRUNCATED_REASONS = new Set([
|
|
1886
|
+
const TRUNCATED_REASONS = /* @__PURE__ */ new Set([
|
|
1660
1887
|
"length",
|
|
1661
1888
|
"max_tokens",
|
|
1662
1889
|
"max_token"
|
|
1663
1890
|
]);
|
|
1664
|
-
const ABORTED_REASONS = new Set([
|
|
1891
|
+
const ABORTED_REASONS = /* @__PURE__ */ new Set([
|
|
1665
1892
|
"aborted",
|
|
1666
1893
|
"abort",
|
|
1667
1894
|
"cancelled",
|
|
1668
1895
|
"canceled",
|
|
1669
1896
|
"cancel"
|
|
1670
1897
|
]);
|
|
1671
|
-
const ERROR_REASONS = new Set([
|
|
1898
|
+
const ERROR_REASONS = /* @__PURE__ */ new Set([
|
|
1672
1899
|
"error",
|
|
1673
1900
|
"internal_error",
|
|
1674
1901
|
"server_error"
|
|
1675
1902
|
]);
|
|
1676
|
-
const FILTERED_REASONS = new Set([
|
|
1903
|
+
const FILTERED_REASONS = /* @__PURE__ */ new Set([
|
|
1677
1904
|
"content_filter",
|
|
1678
1905
|
"content-filter",
|
|
1679
1906
|
"filtered",
|
|
@@ -1928,7 +2155,7 @@ function assertCurrentSubject(state, ticket, actualSnapshot) {
|
|
|
1928
2155
|
}
|
|
1929
2156
|
//#endregion
|
|
1930
2157
|
//#region src/task/reducer.ts
|
|
1931
|
-
const WAITING = new Set([
|
|
2158
|
+
const WAITING = /* @__PURE__ */ new Set([
|
|
1932
2159
|
"WAITING_APPROVAL",
|
|
1933
2160
|
"WAITING_USER",
|
|
1934
2161
|
"WAITING_BUDGET",
|
|
@@ -2398,7 +2625,7 @@ function applyBoundReviewResult(state, result, event) {
|
|
|
2398
2625
|
function applyApprovalPending(state, event) {
|
|
2399
2626
|
const next = need(state, event);
|
|
2400
2627
|
const approval = event.payload.approval;
|
|
2401
|
-
const pendingApprovalIds = [
|
|
2628
|
+
const pendingApprovalIds = [.../* @__PURE__ */ new Set([...next.pendingApprovalIds, approval.id])];
|
|
2402
2629
|
return {
|
|
2403
2630
|
...waiting(next, "WAITING_APPROVAL"),
|
|
2404
2631
|
pendingApprovalIds,
|
|
@@ -3477,7 +3704,7 @@ var WorkspaceWriteLease = class {
|
|
|
3477
3704
|
const prior = this.#record(row, workspaceId);
|
|
3478
3705
|
if (prior) throw new FusionError(LEASE_BUSY, `workspace is held by ${prior.holder}; reconcile before handoff`);
|
|
3479
3706
|
if (row.generation === Number.MAX_SAFE_INTEGER) throw new FusionError(LEASE_GENERATION, "lease generation exhausted");
|
|
3480
|
-
const refs = [
|
|
3707
|
+
const refs = [.../* @__PURE__ */ new Set([process.pid, ...input.liveProcessRefs ?? []])];
|
|
3481
3708
|
if (refs.some((pid) => !Number.isSafeInteger(pid) || pid <= 0)) throw new FusionError(LEASE_BUSY, "invalid process reference");
|
|
3482
3709
|
const record = {
|
|
3483
3710
|
workspaceId,
|
|
@@ -3538,7 +3765,7 @@ var WorkspaceWriteLease = class {
|
|
|
3538
3765
|
});
|
|
3539
3766
|
const oldTemporary = leasePath(workspaceId, path.join(fs.existsSync("/tmp") ? "/tmp" : os.tmpdir(), "dsh-model-fusion-leases"));
|
|
3540
3767
|
const oldDurable = leasePath(workspaceId, LEGACY_LEASE_ROOT);
|
|
3541
|
-
for (const lock of new Set([
|
|
3768
|
+
for (const lock of /* @__PURE__ */ new Set([
|
|
3542
3769
|
legacy,
|
|
3543
3770
|
oldDurable,
|
|
3544
3771
|
oldTemporary
|
|
@@ -4036,7 +4263,7 @@ function changedPaths(before, after) {
|
|
|
4036
4263
|
if (before.root !== after.root) throw new Error("Workspace root changed");
|
|
4037
4264
|
const old = new Map(before.entries.map((entry) => [entry.path, digestOf(entry)]));
|
|
4038
4265
|
const now = new Map(after.entries.map((entry) => [entry.path, digestOf(entry)]));
|
|
4039
|
-
return [
|
|
4266
|
+
return [.../* @__PURE__ */ new Set([...old.keys(), ...now.keys()])].filter((path) => old.get(path) !== now.get(path)).sort();
|
|
4040
4267
|
}
|
|
4041
4268
|
/**
|
|
4042
4269
|
* Workspace-relative allowlist check. `/` always separates segments; `\` is a
|
|
@@ -4340,7 +4567,7 @@ function record(value) {
|
|
|
4340
4567
|
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
4341
4568
|
}
|
|
4342
4569
|
const execFileAsync = promisify(execFile);
|
|
4343
|
-
const WRAPPERS = new Set([
|
|
4570
|
+
const WRAPPERS = /* @__PURE__ */ new Set([
|
|
4344
4571
|
"env",
|
|
4345
4572
|
"exec",
|
|
4346
4573
|
"time",
|
|
@@ -4365,7 +4592,7 @@ const WRAPPER_VALUE_OPTIONS = {
|
|
|
4365
4592
|
"--output"
|
|
4366
4593
|
]
|
|
4367
4594
|
};
|
|
4368
|
-
const PWSH_KEYWORDS = new Set([
|
|
4595
|
+
const PWSH_KEYWORDS = /* @__PURE__ */ new Set([
|
|
4369
4596
|
"exit",
|
|
4370
4597
|
"return",
|
|
4371
4598
|
"throw",
|
|
@@ -4923,7 +5150,7 @@ var NativeFusionScopes = class {
|
|
|
4923
5150
|
...decision,
|
|
4924
5151
|
messages: [...decision.messages.filter((message) => {
|
|
4925
5152
|
const source = message.source;
|
|
4926
|
-
return !(source?.kind === "model-selection" && source.form === "notice" &&
|
|
5153
|
+
return !(source?.kind === "model-selection" && source.form === "notice" && isFusionSelectionLabel(source.summary));
|
|
4927
5154
|
}).map((message) => this.callbacks.projectMessage?.(agent, entry.binding, role, message) ?? message), ...taskContext ? [taskContext] : []]
|
|
4928
5155
|
};
|
|
4929
5156
|
}, { prepend: true }));
|
|
@@ -5173,9 +5400,16 @@ function captureWorkerBrief(store, taskId, agent, turn, step, revision) {
|
|
|
5173
5400
|
function workerToolBriefProblem(store, taskId, exec, revision, transport) {
|
|
5174
5401
|
const events = exec.agent?.session.snapshotEvents();
|
|
5175
5402
|
const message = events?.findLast((event) => event.type === "assistant/message" && event.data.message.content.some((block) => block.type === "tool-call" && block.id === exec.rootCallId)) ?? (transport === void 0 ? void 0 : events?.findLast((event) => event.type === "assistant/message" && event.data.message.content.some((block) => block.type === "tool-call" && block.name === transport)));
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5403
|
+
let row;
|
|
5404
|
+
if (message && message.type === "assistant/message" && !message.data.interrupted) {
|
|
5405
|
+
row = store.readDocument(`worker-request:${exec.agent.id}:${message.data.turn}:${message.data.step}`)?.value;
|
|
5406
|
+
if (row?.schemaVersion !== 1 || row.taskId !== taskId || row.sessionId !== exec.agent.id || row.turn !== message.data.turn || row.step !== message.data.step) return "Worker tool has no captured brief identity";
|
|
5407
|
+
} else if (transport !== void 0) {
|
|
5408
|
+
const rows = store.listDocumentIds(`worker-request:${exec.agent.id}:`).map((id) => store.readDocument(id)?.value).filter((r) => Boolean(r && r.schemaVersion === 1 && r.taskId === taskId && r.sessionId === exec.agent.id));
|
|
5409
|
+
rows.sort((a, b) => (a.turn ?? 0) - (b.turn ?? 0) || (a.step ?? 0) - (b.step ?? 0));
|
|
5410
|
+
row = rows.at(-1);
|
|
5411
|
+
if (!row) return "Worker tool has no captured brief identity";
|
|
5412
|
+
} else return "Worker tool requires a complete native model request";
|
|
5179
5413
|
if (row.briefRevision !== revision) return "A newer Lead brief superseded this request; process it before further tools or a report";
|
|
5180
5414
|
const order = store.load(taskId)?.currentWorkOrder;
|
|
5181
5415
|
if (row.workOrderId !== order?.id && (row.workOrderId || order?.mode)) return "A newer work order superseded this request; process its plan before further tools";
|
|
@@ -6310,6 +6544,17 @@ function installCompactionRouting(ctx, input) {
|
|
|
6310
6544
|
//#endregion
|
|
6311
6545
|
//#region src/host/native-role-sandbox.ts
|
|
6312
6546
|
const roleSeparated = (binding) => binding.profile.workflowPolicy === "enforced-v3";
|
|
6547
|
+
const intentId = (sessionId) => `sandbox-intent:${sessionId}`;
|
|
6548
|
+
/**
|
|
6549
|
+
* The mode the user chose for a confined Fusion conversation, which is the mode
|
|
6550
|
+
* the Sidekick runs under. Undefined until Fusion has confined the Lead, or after
|
|
6551
|
+
* the conversation left Fusion and its own mode was restored.
|
|
6552
|
+
*/
|
|
6553
|
+
function readSandboxIntent(store, sessionId) {
|
|
6554
|
+
const intent = store.readDocument(intentId(sessionId))?.value;
|
|
6555
|
+
if (!intent || intent.schemaVersion !== 1 || !intent.active) return void 0;
|
|
6556
|
+
return { mode: intent.mode };
|
|
6557
|
+
}
|
|
6313
6558
|
/**
|
|
6314
6559
|
* Role separation enforced below the prompt: the Lead's session runs under the
|
|
6315
6560
|
* Host's own read-only sandbox (bash and filesystem are refused by the OS
|
|
@@ -6330,15 +6575,12 @@ var NativeRoleSandbox = class {
|
|
|
6330
6575
|
get #policy() {
|
|
6331
6576
|
return this.ctx.get("sandboxPolicy");
|
|
6332
6577
|
}
|
|
6333
|
-
#id(sessionId) {
|
|
6334
|
-
return `sandbox-intent:${sessionId}`;
|
|
6335
|
-
}
|
|
6336
6578
|
#intent(sessionId) {
|
|
6337
|
-
return this.store.readDocument(
|
|
6579
|
+
return this.store.readDocument(intentId(sessionId))?.value;
|
|
6338
6580
|
}
|
|
6339
6581
|
#save(intent) {
|
|
6340
|
-
const prior = this.store.readDocument(
|
|
6341
|
-
this.store.writeDocument(
|
|
6582
|
+
const prior = this.store.readDocument(intentId(intent.sessionId));
|
|
6583
|
+
this.store.writeDocument(intentId(intent.sessionId), prior?.revision ?? 0, {
|
|
6342
6584
|
...intent,
|
|
6343
6585
|
ownSeqs: intent.ownSeqs.slice(-32)
|
|
6344
6586
|
});
|
|
@@ -7717,7 +7959,7 @@ var NativeWorkerTransport = class {
|
|
|
7717
7959
|
//#endregion
|
|
7718
7960
|
//#region src/host/native-activity.ts
|
|
7719
7961
|
const prefix = "activity-link:";
|
|
7720
|
-
const visibleTools = new Set([
|
|
7962
|
+
const visibleTools = /* @__PURE__ */ new Set([
|
|
7721
7963
|
"bash",
|
|
7722
7964
|
"pwsh",
|
|
7723
7965
|
"read",
|
|
@@ -8326,7 +8568,7 @@ const optionalStringList = {
|
|
|
8326
8568
|
type: "array",
|
|
8327
8569
|
items: { type: "string" }
|
|
8328
8570
|
};
|
|
8329
|
-
const READ_TOOLS = new Set([
|
|
8571
|
+
const READ_TOOLS = /* @__PURE__ */ new Set([
|
|
8330
8572
|
"fusion_read_state",
|
|
8331
8573
|
"glob",
|
|
8332
8574
|
"grep",
|
|
@@ -8335,7 +8577,7 @@ const READ_TOOLS = new Set([
|
|
|
8335
8577
|
"job_output"
|
|
8336
8578
|
]);
|
|
8337
8579
|
const isRead = (exec) => READ_TOOLS.has(exec.name) || exec.name === "str_replace_editor" && exec.arguments.command === "view";
|
|
8338
|
-
const FUSION_TOOLS = new Set([
|
|
8580
|
+
const FUSION_TOOLS = /* @__PURE__ */ new Set([
|
|
8339
8581
|
"fusion_read_state",
|
|
8340
8582
|
"fusion_delegate_text",
|
|
8341
8583
|
"fusion_explore",
|
|
@@ -8348,7 +8590,7 @@ const FUSION_TOOLS = new Set([
|
|
|
8348
8590
|
"fusion_finish_direct",
|
|
8349
8591
|
"fusion_read_evidence"
|
|
8350
8592
|
]);
|
|
8351
|
-
const WORKER_FUSION_TOOLS = new Set([
|
|
8593
|
+
const WORKER_FUSION_TOOLS = /* @__PURE__ */ new Set([
|
|
8352
8594
|
"fusion_submit_result",
|
|
8353
8595
|
"fusion_read_state",
|
|
8354
8596
|
"fusion_read_evidence"
|
|
@@ -8410,7 +8652,7 @@ const normalizeQuote = (text) => text.replace(/\s+/g, " ").trim();
|
|
|
8410
8652
|
/** Literal code spans (`like this`) a requirement asks for; a candidate that omits them gets flagged. */
|
|
8411
8653
|
const literalSpans = (text) => [...new Set([...text.matchAll(/`([^`\n]{2,120})`/g)].map((match) => match[1]))];
|
|
8412
8654
|
/** The enforced-v3 Lead's fixed catalog (order is part of the cached prompt prefix). */
|
|
8413
|
-
const SEPARATED_LEAD_TOOLS = [
|
|
8655
|
+
const SEPARATED_LEAD_TOOLS = [.../* @__PURE__ */ new Set([
|
|
8414
8656
|
...READ_TOOLS,
|
|
8415
8657
|
nativeShellTool,
|
|
8416
8658
|
...[...FUSION_TOOLS].filter((name) => ![
|
|
@@ -9858,7 +10100,7 @@ var FusionCoordinator = class {
|
|
|
9858
10100
|
...known,
|
|
9859
10101
|
...extra.contents
|
|
9860
10102
|
},
|
|
9861
|
-
unavailable: [
|
|
10103
|
+
unavailable: [.../* @__PURE__ */ new Set([...changeBase.unavailable ?? [], ...extra.unavailable ?? []])]
|
|
9862
10104
|
};
|
|
9863
10105
|
}
|
|
9864
10106
|
this.store.writeDocument(`scope-expansion:${binding.taskId}:${randomUUID()}`, 0, {
|
|
@@ -10965,7 +11207,7 @@ var FusionCoordinator = class {
|
|
|
10965
11207
|
//#endregion
|
|
10966
11208
|
//#region src/host/native-budget.ts
|
|
10967
11209
|
function nativeAuthorization(raw) {
|
|
10968
|
-
const keys = new Set([
|
|
11210
|
+
const keys = /* @__PURE__ */ new Set([
|
|
10969
11211
|
"schemaVersion",
|
|
10970
11212
|
"kind",
|
|
10971
11213
|
"approved",
|
|
@@ -11142,6 +11384,17 @@ const EN = {
|
|
|
11142
11384
|
"继续消息的投递结果尚未确认。检查会话后使用 /fusion recover 提交检查记录。": "Delivery of the continuation is unconfirmed. Check the conversation, then use /fusion recover."
|
|
11143
11385
|
};
|
|
11144
11386
|
const english = (text) => text == null ? text : EN[text] ?? text;
|
|
11387
|
+
/**
|
|
11388
|
+
* The role working right now: an in-flight request names it exactly (requests read
|
|
11389
|
+
* newest first), and between requests the phase does ('DIRECT' is the Lead writing).
|
|
11390
|
+
*/
|
|
11391
|
+
function activeRole(requests, phase) {
|
|
11392
|
+
const running = requests.find((request) => request.purpose === "conversation" && request.outcome === "entered");
|
|
11393
|
+
if (running) return running.role;
|
|
11394
|
+
if (phase === "WORKER_RUNNING" || phase === "REWORK") return "worker";
|
|
11395
|
+
if (phase === "PLANNING" || phase === "REVIEWING" || phase === "DIRECT") return "lead";
|
|
11396
|
+
return null;
|
|
11397
|
+
}
|
|
11145
11398
|
function taskStage(state, checking, workerStepLimitReached = false, workerQuotaExhausted = false) {
|
|
11146
11399
|
const control = state.control;
|
|
11147
11400
|
const value = (stage, detail = null, attention = false) => ({
|
|
@@ -11226,6 +11479,13 @@ function readStatus(store, sessionId, index) {
|
|
|
11226
11479
|
...modelStage,
|
|
11227
11480
|
...pendingDelivery
|
|
11228
11481
|
};
|
|
11482
|
+
const intent = roleSeparated(binding) ? readSandboxIntent(store, sessionId) : void 0;
|
|
11483
|
+
const takeover = runtime?.schemaVersion === 1 && runtime.taskId === taskId && runtime.takeover === true;
|
|
11484
|
+
const permissions = intent && {
|
|
11485
|
+
lead: takeover ? intent.mode : "read-only",
|
|
11486
|
+
worker: intent.mode,
|
|
11487
|
+
active: activeRole(facts.requests, state.phase)
|
|
11488
|
+
};
|
|
11229
11489
|
return {
|
|
11230
11490
|
...base,
|
|
11231
11491
|
task: {
|
|
@@ -11242,6 +11502,7 @@ function readStatus(store, sessionId, index) {
|
|
|
11242
11502
|
worker: modelControl?.routes.worker ?? binding.profile.worker,
|
|
11243
11503
|
compactor: binding.profile.compactor?.route ?? null
|
|
11244
11504
|
},
|
|
11505
|
+
...permissions ? { permissions } : {},
|
|
11245
11506
|
...facts,
|
|
11246
11507
|
automatedChecks: Array.isArray(runtime?.checks) && runtime?.taskId === taskId ? runtime.checks.length : null,
|
|
11247
11508
|
pendingApprovals: state.control.pendingApprovalIds.length,
|
|
@@ -12252,7 +12513,6 @@ async function apply(ctx, config = {}) {
|
|
|
12252
12513
|
const cachePolicy = new CachePolicy(store);
|
|
12253
12514
|
backfillHistory(store);
|
|
12254
12515
|
const budget = new NativeRequestBudget(store, config.authorizationPath);
|
|
12255
|
-
let defaultProfile;
|
|
12256
12516
|
const physicalCatalog = async () => {
|
|
12257
12517
|
const catalog = await ctx.sessionController.modelCatalog();
|
|
12258
12518
|
return {
|
|
@@ -12260,19 +12520,82 @@ async function apply(ctx, config = {}) {
|
|
|
12260
12520
|
groups: catalog.groups.filter((group) => !isFusionProvider(group.id))
|
|
12261
12521
|
};
|
|
12262
12522
|
};
|
|
12263
|
-
|
|
12264
|
-
|
|
12265
|
-
|
|
12266
|
-
|
|
12267
|
-
|
|
12523
|
+
/**
|
|
12524
|
+
* The combos to resolve: the saved list, or the one profile file a benchmark
|
|
12525
|
+
* deployment names. A profile file keeps its legacy contract, so only saved
|
|
12526
|
+
* combos are rebuilt from a choice.
|
|
12527
|
+
*/
|
|
12528
|
+
const comboPlan = () => {
|
|
12529
|
+
const snapshot = readPairs(store);
|
|
12530
|
+
if (snapshot.pairs.length) return {
|
|
12531
|
+
revision: snapshot.revision,
|
|
12532
|
+
items: snapshot.pairs
|
|
12533
|
+
};
|
|
12534
|
+
if (config.profilePath && isAbsolute(config.profilePath)) {
|
|
12535
|
+
const raw = loadJsonObject(config.profilePath);
|
|
12536
|
+
const pair = choiceFromStoredProfile(raw);
|
|
12537
|
+
if (pair) return {
|
|
12538
|
+
revision: snapshot.revision,
|
|
12539
|
+
items: [{
|
|
12540
|
+
id: FUSION_MODEL,
|
|
12541
|
+
name: "自动",
|
|
12542
|
+
pair,
|
|
12543
|
+
raw
|
|
12544
|
+
}]
|
|
12545
|
+
};
|
|
12546
|
+
}
|
|
12547
|
+
return {
|
|
12548
|
+
revision: snapshot.revision,
|
|
12549
|
+
items: []
|
|
12550
|
+
};
|
|
12551
|
+
};
|
|
12552
|
+
let combos;
|
|
12553
|
+
const resolveCombos = async () => {
|
|
12554
|
+
const plan = comboPlan();
|
|
12268
12555
|
const catalog = await physicalCatalog();
|
|
12269
|
-
const
|
|
12270
|
-
const next = saved ? modelProfileFromChoice(pair) : raw;
|
|
12271
|
-
defaultProfile = resolveProfile(next, next.interactionMode === "model-like" ? loadModelPromptBundle() : loadPromptBundle(), { authorizedRoutes: catalog.groups.flatMap((group) => group.models.map((model) => ({
|
|
12556
|
+
const authorizedRoutes = catalog.groups.flatMap((group) => group.models.map((model) => ({
|
|
12272
12557
|
provider: group.id,
|
|
12273
12558
|
model: model.id
|
|
12274
|
-
})))
|
|
12275
|
-
|
|
12559
|
+
})));
|
|
12560
|
+
const profiles = /* @__PURE__ */ new Map(), failures = /* @__PURE__ */ new Map();
|
|
12561
|
+
for (const item of plan.items) try {
|
|
12562
|
+
const pair = validatePairChoice(item.pair, catalog);
|
|
12563
|
+
const next = item.raw ?? modelProfileFromChoice(pair, profileIdOf(item.id));
|
|
12564
|
+
profiles.set(item.id, resolveProfile(next, next.interactionMode === "model-like" ? loadModelPromptBundle() : loadPromptBundle(), { authorizedRoutes }));
|
|
12565
|
+
} catch (error) {
|
|
12566
|
+
failures.set(item.id, error instanceof Error ? error.message : String(error));
|
|
12567
|
+
}
|
|
12568
|
+
combos = {
|
|
12569
|
+
revision: plan.revision,
|
|
12570
|
+
items: plan.items.map(({ id, name, pair }) => ({
|
|
12571
|
+
id,
|
|
12572
|
+
name,
|
|
12573
|
+
pair
|
|
12574
|
+
})),
|
|
12575
|
+
profiles,
|
|
12576
|
+
failures
|
|
12577
|
+
};
|
|
12578
|
+
return combos;
|
|
12579
|
+
};
|
|
12580
|
+
const knownCombos = async () => combos ?? await resolveCombos();
|
|
12581
|
+
let reloading = false;
|
|
12582
|
+
const profileOf = (model) => {
|
|
12583
|
+
const found = combos?.profiles.get(model);
|
|
12584
|
+
if (!found && !reloading) {
|
|
12585
|
+
reloading = true;
|
|
12586
|
+
resolveCombos().catch(() => void 0).finally(() => {
|
|
12587
|
+
reloading = false;
|
|
12588
|
+
});
|
|
12589
|
+
}
|
|
12590
|
+
return found;
|
|
12591
|
+
};
|
|
12592
|
+
/** The profile a plain (unselected) request falls back to: the first usable combo. */
|
|
12593
|
+
const primaryProfile = async () => {
|
|
12594
|
+
const state = await knownCombos();
|
|
12595
|
+
const profile = state.profiles.values().next().value;
|
|
12596
|
+
if (profile) return profile;
|
|
12597
|
+
const [first] = state.failures.values();
|
|
12598
|
+
throw new Error(first ?? bi("请先在设置的 Fusion 页面选择 Lead 和 Worker", "Open Settings → Fusion and choose a Lead and a Sidekick first"));
|
|
12276
12599
|
};
|
|
12277
12600
|
const needsBudget = (profile) => Boolean(config.authorizationPath) || profile.interactionMode !== "model-like";
|
|
12278
12601
|
let runtime;
|
|
@@ -12286,8 +12609,9 @@ async function apply(ctx, config = {}) {
|
|
|
12286
12609
|
if (runtime) return Promise.resolve(runtime);
|
|
12287
12610
|
if (creating) return creating;
|
|
12288
12611
|
creating = (async () => {
|
|
12612
|
+
const profile = frozen ?? await primaryProfile();
|
|
12289
12613
|
runtime = new FusionCoordinator(ctx, store, {
|
|
12290
|
-
profile
|
|
12614
|
+
profile,
|
|
12291
12615
|
workerTools: config.workerTools ?? defaultWorkerTools,
|
|
12292
12616
|
resumeAuthorization: (binding) => needsBudget(binding.profile) ? budget.check(effectiveRoutes(binding)).authorizationId : "native-account",
|
|
12293
12617
|
authorizeRequest: (_agent, binding) => {
|
|
@@ -12320,26 +12644,47 @@ async function apply(ctx, config = {}) {
|
|
|
12320
12644
|
});
|
|
12321
12645
|
return creating;
|
|
12322
12646
|
};
|
|
12323
|
-
const
|
|
12647
|
+
const fusionModels = async () => {
|
|
12648
|
+
let items;
|
|
12324
12649
|
try {
|
|
12325
|
-
|
|
12326
|
-
const lead = savedLeadRoute(saved) ?? (config.profilePath && isAbsolute(config.profilePath) ? savedLeadRoute(loadJsonObject(config.profilePath)) : void 0);
|
|
12327
|
-
if (!lead) return void 0;
|
|
12328
|
-
return (await ctx.llm.resolveModelInfo(lead.provider, lead.model)).inputModalities;
|
|
12650
|
+
items = readPairs(store).pairs;
|
|
12329
12651
|
} catch {
|
|
12330
|
-
return;
|
|
12652
|
+
return [];
|
|
12331
12653
|
}
|
|
12654
|
+
if (!items.length && config.profilePath && isAbsolute(config.profilePath)) {
|
|
12655
|
+
const pair = choiceFromStoredProfile(loadJsonObject(config.profilePath));
|
|
12656
|
+
items = pair ? [{
|
|
12657
|
+
id: FUSION_MODEL,
|
|
12658
|
+
name: "自动",
|
|
12659
|
+
pair
|
|
12660
|
+
}] : [];
|
|
12661
|
+
}
|
|
12662
|
+
return Promise.all(items.map(async (item) => {
|
|
12663
|
+
let modalities = [];
|
|
12664
|
+
try {
|
|
12665
|
+
modalities = (await ctx.llm.resolveModelInfo(item.pair.lead.provider, item.pair.lead.model)).inputModalities ?? [];
|
|
12666
|
+
} catch {
|
|
12667
|
+
modalities = [];
|
|
12668
|
+
}
|
|
12669
|
+
return {
|
|
12670
|
+
id: item.id,
|
|
12671
|
+
name: `Fusion · ${item.name}`,
|
|
12672
|
+
inputModalities: modalities
|
|
12673
|
+
};
|
|
12674
|
+
}));
|
|
12332
12675
|
};
|
|
12333
|
-
ctx.effect(() => ctx.llm.registerAdapter([FUSION_PROVIDER], new FusionCatalogAdapter(
|
|
12676
|
+
ctx.effect(() => ctx.llm.registerAdapter([FUSION_PROVIDER], new FusionCatalogAdapter(fusionModels)));
|
|
12334
12677
|
ctx.effect(() => installNativeFusionSelection(ctx, {
|
|
12335
12678
|
coordinator: () => runtime,
|
|
12336
|
-
profile:
|
|
12679
|
+
profile: profileOf,
|
|
12680
|
+
reason: (model) => combos?.failures.get(model) ?? (combos?.items.length ? bi("该组合已被删除或无法使用,请在模型菜单中选择其他组合。", "That combo was removed or cannot run; pick another one in the model menu.") : void 0),
|
|
12337
12681
|
selection: (agent) => {
|
|
12338
12682
|
const selection = agent.session.snapshotEvents().findLast((event) => event.type === "model/selection");
|
|
12339
12683
|
if (selection?.type === "model/selection") return selection.data;
|
|
12340
|
-
|
|
12684
|
+
const bound = bindings.read(agent.id)?.binding;
|
|
12685
|
+
if (bound?.selected) return {
|
|
12341
12686
|
provider: FUSION_PROVIDER,
|
|
12342
|
-
model:
|
|
12687
|
+
model: modelOfProfileId(bound.profile.id) ?? "auto"
|
|
12343
12688
|
};
|
|
12344
12689
|
const header = agent.session.requestHeader();
|
|
12345
12690
|
return header ? {
|
|
@@ -12387,38 +12732,58 @@ async function apply(ctx, config = {}) {
|
|
|
12387
12732
|
}
|
|
12388
12733
|
if (url.searchParams.get("view") === "catalog") return json(res, 200, await ctx.sessionController.modelCatalog());
|
|
12389
12734
|
if (url.searchParams.get("view") === "cache") {
|
|
12390
|
-
let
|
|
12735
|
+
let pairs = [];
|
|
12391
12736
|
try {
|
|
12392
|
-
|
|
12737
|
+
pairs = [...(await knownCombos()).profiles.values()].map((profile) => ({
|
|
12738
|
+
lead: profile.profile.lead,
|
|
12739
|
+
worker: profile.profile.worker,
|
|
12740
|
+
cacheKeepalive: profile.profile.cacheKeepalive
|
|
12741
|
+
}));
|
|
12393
12742
|
} catch {
|
|
12394
|
-
|
|
12743
|
+
pairs = [];
|
|
12395
12744
|
}
|
|
12396
12745
|
return json(res, 200, {
|
|
12397
12746
|
checked: CACHE_DEFAULTS_CHECKED,
|
|
12398
|
-
models: cacheView(cachePolicy,
|
|
12399
|
-
lead: profile.profile.lead,
|
|
12400
|
-
worker: profile.profile.worker
|
|
12401
|
-
} : void 0, profile?.profile.cacheKeepalive)
|
|
12747
|
+
models: cacheView(cachePolicy, pairs)
|
|
12402
12748
|
});
|
|
12403
12749
|
}
|
|
12404
12750
|
if (url.searchParams.get("view") === "settings") {
|
|
12405
|
-
const
|
|
12406
|
-
|
|
12407
|
-
|
|
12408
|
-
profile
|
|
12409
|
-
|
|
12410
|
-
|
|
12411
|
-
|
|
12412
|
-
|
|
12413
|
-
|
|
12751
|
+
const state = await resolveCombos();
|
|
12752
|
+
const pairs = state.items.map((item) => {
|
|
12753
|
+
const profile = state.profiles.get(item.id);
|
|
12754
|
+
if (!profile) return {
|
|
12755
|
+
id: item.id,
|
|
12756
|
+
name: item.name,
|
|
12757
|
+
pair: item.pair,
|
|
12758
|
+
authorized: false,
|
|
12759
|
+
reason: state.failures.get(item.id)
|
|
12760
|
+
};
|
|
12761
|
+
let authorized = false, reason;
|
|
12762
|
+
try {
|
|
12763
|
+
if (needsBudget(profile.profile)) budget.check(profileRoutes(profile.profile));
|
|
12764
|
+
authorized = true;
|
|
12765
|
+
} catch (error) {
|
|
12766
|
+
reason = error instanceof Error ? error.message : String(error);
|
|
12767
|
+
}
|
|
12768
|
+
return {
|
|
12769
|
+
id: item.id,
|
|
12770
|
+
name: item.name,
|
|
12771
|
+
pair: choiceFromProfile(profile.profile),
|
|
12772
|
+
authorized,
|
|
12773
|
+
...reason ? { reason } : {}
|
|
12774
|
+
};
|
|
12775
|
+
});
|
|
12776
|
+
const blocked = pairs.find((pair) => !pair.authorized);
|
|
12414
12777
|
return json(res, 200, {
|
|
12415
|
-
revision:
|
|
12778
|
+
revision: state.revision,
|
|
12416
12779
|
catalog: await physicalCatalog(),
|
|
12417
|
-
|
|
12418
|
-
|
|
12419
|
-
|
|
12780
|
+
pairs,
|
|
12781
|
+
maxPairs: 6,
|
|
12782
|
+
pair: pairs[0]?.pair ?? null,
|
|
12783
|
+
authorized: pairs.length > 0 && !blocked,
|
|
12784
|
+
...blocked?.reason ? { reason: blocked.reason } : {},
|
|
12420
12785
|
managedAuthorization: Boolean(config.authorizationPath),
|
|
12421
|
-
policy:
|
|
12786
|
+
policy: state.profiles.values().next().value?.profile.interactionMode ?? "model-like",
|
|
12422
12787
|
actualBilledUsd: null,
|
|
12423
12788
|
apiEquivalentUsd: null
|
|
12424
12789
|
});
|
|
@@ -12430,7 +12795,7 @@ async function apply(ctx, config = {}) {
|
|
|
12430
12795
|
const profile = binding && binding.selected ? {
|
|
12431
12796
|
profile: binding.profile,
|
|
12432
12797
|
prompts: binding.prompts
|
|
12433
|
-
} : await
|
|
12798
|
+
} : await primaryProfile();
|
|
12434
12799
|
(await getRuntime(profile)).transport.assertAvailable();
|
|
12435
12800
|
if (needsBudget(profile.profile)) budget.check(profileRoutes(profile.profile));
|
|
12436
12801
|
available = true;
|
|
@@ -12531,9 +12896,17 @@ async function apply(ctx, config = {}) {
|
|
|
12531
12896
|
}
|
|
12532
12897
|
if (body.action === "configure") {
|
|
12533
12898
|
if (!Number.isSafeInteger(body.revision) || Number(body.revision) < 0) throw new Error(bi("配置版本无效,请刷新后重试", "Invalid settings revision; refresh and retry"));
|
|
12534
|
-
const
|
|
12535
|
-
const
|
|
12536
|
-
|
|
12899
|
+
const catalog = await physicalCatalog(), current = readPairs(store);
|
|
12900
|
+
const pairs = validatePairs(Array.isArray(body.pairs) ? body.pairs : [{
|
|
12901
|
+
...current.pairs[0] ?? {
|
|
12902
|
+
id: "auto",
|
|
12903
|
+
name: "自动"
|
|
12904
|
+
},
|
|
12905
|
+
pair: body.pair
|
|
12906
|
+
}, ...current.pairs.slice(1)], catalog, current.pairs);
|
|
12907
|
+
const revision = writePairs(store, Number(body.revision), pairs);
|
|
12908
|
+
const profile = (await resolveCombos()).profiles.values().next().value;
|
|
12909
|
+
if (profile) await getRuntime(profile);
|
|
12537
12910
|
return json(res, 200, {
|
|
12538
12911
|
ok: true,
|
|
12539
12912
|
revision
|
|
@@ -12541,13 +12914,12 @@ async function apply(ctx, config = {}) {
|
|
|
12541
12914
|
}
|
|
12542
12915
|
if (body.action === "authorize") {
|
|
12543
12916
|
if (config.authorizationPath) throw new Error(bi("运行额度由外部授权文件管理,不能在此覆盖", "Run limits are managed by an external authorization file and cannot be overridden here"));
|
|
12544
|
-
|
|
12545
|
-
|
|
12546
|
-
|
|
12547
|
-
if (store.readDocument("settings:profile")?.revision !== body.revision) throw new Error(bi("模型组合已变化,请刷新后再启用额度", "The pair changed; refresh before enabling limits"));
|
|
12917
|
+
if (readPairs(store).revision !== body.revision) throw new Error(bi("组合列表已变化,请刷新后再启用额度", "The combo list changed; refresh before enabling limits"));
|
|
12918
|
+
const state = await resolveCombos();
|
|
12919
|
+
if (readPairs(store).revision !== body.revision) throw new Error(bi("组合列表已变化,请刷新后再启用额度", "The combo list changed; refresh before enabling limits"));
|
|
12548
12920
|
return json(res, 200, {
|
|
12549
12921
|
ok: true,
|
|
12550
|
-
expiresAt: authorizeConfiguredPair(store, profileRoutes(profile.profile), body.limits).expiresAt
|
|
12922
|
+
expiresAt: authorizeConfiguredPair(store, [...state.profiles.values()].flatMap((profile) => profileRoutes(profile.profile)), body.limits).expiresAt
|
|
12551
12923
|
});
|
|
12552
12924
|
}
|
|
12553
12925
|
throw new Error(bi("请使用原模型菜单选择 Fusion 或切换普通模型", "Use the model menu to select Fusion or another model"));
|
|
@@ -12575,7 +12947,7 @@ async function apply(ctx, config = {}) {
|
|
|
12575
12947
|
const binding = bindings.read(invocation.agent.id)?.binding;
|
|
12576
12948
|
if (!binding?.selected) return {
|
|
12577
12949
|
kind: "success",
|
|
12578
|
-
text: bi("当前会话未启用 Fusion
|
|
12950
|
+
text: bi("当前会话未启用 Fusion。请在模型选择器中选择一个 Fusion 组合。", "Fusion is not selected in this conversation. Choose a Fusion combo in the model menu.")
|
|
12579
12951
|
};
|
|
12580
12952
|
if (action === "status" || action === "") {
|
|
12581
12953
|
const controller = await getRuntime({
|
|
@@ -12647,7 +13019,7 @@ async function apply(ctx, config = {}) {
|
|
|
12647
13019
|
store.close();
|
|
12648
13020
|
});
|
|
12649
13021
|
try {
|
|
12650
|
-
await getRuntime(await
|
|
13022
|
+
await getRuntime(await primaryProfile());
|
|
12651
13023
|
} catch {
|
|
12652
13024
|
const selected = bindings.selected()[0];
|
|
12653
13025
|
if (selected) await getRuntime({
|