@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/client.js
CHANGED
|
@@ -51,10 +51,19 @@ window.__ModuleLoader__.load({
|
|
|
51
51
|
*/
|
|
52
52
|
/** The HTTP path the panel's own views and actions are served from. */
|
|
53
53
|
const FUSION_API_PATH = "/api/fusion";
|
|
54
|
+
/**
|
|
55
|
+
* The model ids this provider advertises: `auto` plus one generated
|
|
56
|
+
* `pair-xxxxxxxx` per additional combo. The browser bundle inlines this module,
|
|
57
|
+
* so the namespace shape is fixed here rather than read from a settings
|
|
58
|
+
* document at import time.
|
|
59
|
+
*/
|
|
60
|
+
const FUSION_MODEL_PATTERN = /^(?:auto|pair-[a-z0-9]{6,12})$/;
|
|
61
|
+
/** Whether a model id names a Fusion combo on this provider. */
|
|
62
|
+
const isFusionModel = (model) => typeof model === "string" && FUSION_MODEL_PATTERN.test(model);
|
|
54
63
|
/** Whether a provider route belongs to this plugin, under either name. */
|
|
55
64
|
const isFusionProvider = (provider) => provider === "dsh-fusion" || provider === "dsh-model-fusion";
|
|
56
|
-
/** Whether a route names
|
|
57
|
-
const isFusionSelection = (route) => isFusionProvider(route?.provider) && route?.model
|
|
65
|
+
/** Whether a route names a Fusion combo, under either provider spelling. */
|
|
66
|
+
const isFusionSelection = (route) => isFusionProvider(route?.provider) && isFusionModel(route?.model);
|
|
58
67
|
//#endregion
|
|
59
68
|
//#region src/client/FusionHistory.tsx
|
|
60
69
|
function formatCompact(tokens) {
|
|
@@ -689,7 +698,7 @@ window.__ModuleLoader__.load({
|
|
|
689
698
|
};
|
|
690
699
|
//#endregion
|
|
691
700
|
//#region src/client/FusionSettings.tsx
|
|
692
|
-
const styles$
|
|
701
|
+
const styles$2 = `
|
|
693
702
|
.fusion-page{box-sizing:border-box;width:100%;height:100%;min-width:0;min-height:0;flex:1 1 auto;overflow:auto;display:flex;flex-direction:column;align-items:center;scrollbar-gutter:stable;padding:0 clamp(24px,4vw,48px) 48px;--dsh-scrollbar-width:9px;--dsh-scrollbar-thumb-border:2px}
|
|
694
703
|
.fusion-settings{box-sizing:border-box;width:100%;max-width:680px;margin:0 auto;color:var(--dsw-alias-label-primary);display:flex;flex-direction:column;gap:18px}
|
|
695
704
|
.fusion-settings h2{margin:0;font-size:22px;line-height:30px;font-weight:600}
|
|
@@ -710,8 +719,27 @@ window.__ModuleLoader__.load({
|
|
|
710
719
|
.fusion-settings details{font-size:13px;line-height:21px}
|
|
711
720
|
.fusion-settings summary{cursor:pointer;font-weight:500}
|
|
712
721
|
.fusion-settings .fusion-limits{display:flex;flex-direction:column;gap:12px;padding-top:14px}
|
|
713
|
-
.fusion-settings input[type=number]{min-width:0;width:100%;box-sizing:border-box;background-color:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:7px;padding:8px;font:inherit}
|
|
722
|
+
.fusion-settings :is(input[type=number],input[type=text]){min-width:0;width:100%;box-sizing:border-box;background-color:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:7px;padding:8px;font:inherit}
|
|
714
723
|
.fusion-settings .fusion-consent{display:flex;align-items:flex-start;gap:8px;font-size:12px;line-height:18px}
|
|
724
|
+
.fusion-combos{display:flex;flex-direction:column;gap:8px}
|
|
725
|
+
.fusion-settings .fusion-combo{display:flex;align-items:stretch;padding:0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background-color:var(--dsw-alias-bg-layer-1);font-size:13px;overflow:hidden;transition:border-color .15s ease,background-color .15s ease}
|
|
726
|
+
.fusion-settings .fusion-combo:hover{background-color:var(--dsw-alias-interactive-bg-hover)}
|
|
727
|
+
.fusion-settings .fusion-combo[data-active=true]{border-color:var(--dsw-alias-brand-primary);background-color:var(--dsw-alias-interactive-bg-hover-accent)}
|
|
728
|
+
.fusion-settings .fusion-combo-main{flex:1 1 auto;display:flex;align-items:center;gap:10px;min-width:0;padding:11px 12px;background:none;border:0;border-radius:0;color:inherit;font:inherit;text-align:left;cursor:pointer}
|
|
729
|
+
.fusion-settings .fusion-combo-main:hover{background:none}
|
|
730
|
+
.fusion-settings .fusion-combo :is(.fusion-combo-main,.fusion-combo-actions):focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
|
|
731
|
+
.fusion-settings .fusion-combo-check{position:relative;flex:none;width:14px;height:14px;border:1.5px solid var(--dsw-alias-border-l2);border-radius:50%;transition:border-color .15s ease}
|
|
732
|
+
.fusion-settings .fusion-combo:hover .fusion-combo-check{border-color:var(--dsw-alias-label-tertiary)}
|
|
733
|
+
.fusion-settings .fusion-combo[data-active=true] .fusion-combo-check{border-color:var(--dsw-alias-brand-primary)}
|
|
734
|
+
.fusion-settings .fusion-combo[data-active=true] .fusion-combo-check::after{content:'';position:absolute;inset:2.5px;border-radius:50%;background:var(--dsw-alias-brand-primary)}
|
|
735
|
+
.fusion-settings .fusion-combo-text{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
736
|
+
.fusion-settings .fusion-combo-name{font-weight:600;overflow-wrap:anywhere}
|
|
737
|
+
.fusion-settings .fusion-combo-routes{color:var(--dsw-alias-label-secondary);font-size:12px;overflow-wrap:anywhere}
|
|
738
|
+
.fusion-settings .fusion-combo-actions{flex:none;align-self:stretch;display:flex;align-items:center;padding:0 14px;border:0;border-left:1px solid var(--dsw-alias-border-l1);border-radius:0;background:none;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;cursor:pointer;transition:color .15s ease,background-color .15s ease}
|
|
739
|
+
.fusion-settings .fusion-combo-actions:hover:not(:disabled){color:var(--dsw-alias-state-error-primary);background-color:var(--dsw-alias-interactive-bg-hover-danger)}
|
|
740
|
+
.fusion-settings .fusion-combo-actions:disabled{opacity:.4;cursor:default}
|
|
741
|
+
.fusion-settings .fusion-combo-add{align-self:flex-start;padding:8px 16px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:none;color:var(--dsw-alias-label-secondary);font-size:13px}
|
|
742
|
+
.fusion-settings .fusion-combo-add:hover:not(:disabled){background-color:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
715
743
|
.fusion-cache{box-sizing:border-box;width:100%;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-module-platform);padding:16px;display:flex;flex-direction:column;gap:12px}
|
|
716
744
|
.fusion-cache h3{margin:0}
|
|
717
745
|
.fusion-cache-summary{cursor:pointer;font-weight:600;font-size:14px;display:flex;align-items:center;justify-content:space-between;list-style:none;user-select:none;padding:2px 0}
|
|
@@ -825,20 +853,34 @@ window.__ModuleLoader__.load({
|
|
|
825
853
|
})] })
|
|
826
854
|
] });
|
|
827
855
|
}
|
|
856
|
+
let draftKeys = 0;
|
|
857
|
+
const toDraft = (pairs) => pairs.map(({ id, name, pair }) => ({
|
|
858
|
+
key: id,
|
|
859
|
+
id,
|
|
860
|
+
name,
|
|
861
|
+
pair
|
|
862
|
+
}));
|
|
828
863
|
function FusionSettings() {
|
|
829
864
|
const lang = useLang();
|
|
830
865
|
const [state, setState] = (0, react.useState)();
|
|
831
|
-
const [draft, setDraft] = (0, react.useState)(
|
|
866
|
+
const [draft, setDraft] = (0, react.useState)([]);
|
|
867
|
+
const [active, setActive] = (0, react.useState)(0);
|
|
832
868
|
const [error, setError] = (0, react.useState)();
|
|
833
869
|
const [notice, setNotice] = (0, react.useState)();
|
|
834
870
|
const [saving, setSaving] = (0, react.useState)(false);
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
871
|
+
const current = draft[Math.min(active, draft.length - 1)];
|
|
872
|
+
const routeName = (route) => route ? state?.catalog.groups.find((group) => group.id === route.provider)?.models.find((model) => model.id === route.model)?.name ?? `${route.provider}/${route.model}` : "—";
|
|
873
|
+
const update = (change) => setDraft((value) => value.map((combo, index) => combo.key === current?.key ? change(combo) : combo));
|
|
874
|
+
const chooseModel = (role, route) => update((combo) => ({
|
|
875
|
+
...combo,
|
|
876
|
+
pair: {
|
|
877
|
+
...combo.pair,
|
|
878
|
+
[role]: route,
|
|
879
|
+
...combo.pair.cacheKeepalive ? { cacheKeepalive: {
|
|
880
|
+
...combo.pair.cacheKeepalive,
|
|
881
|
+
[role]: role === "lead" ? "auto" : false
|
|
882
|
+
} } : {}
|
|
883
|
+
}
|
|
842
884
|
}));
|
|
843
885
|
(0, react.useEffect)(() => {
|
|
844
886
|
const abort = new AbortController();
|
|
@@ -851,24 +893,49 @@ window.__ModuleLoader__.load({
|
|
|
851
893
|
return response.json();
|
|
852
894
|
}).then((value) => {
|
|
853
895
|
setState(value);
|
|
854
|
-
setDraft(value.
|
|
896
|
+
setDraft(toDraft(value.pairs));
|
|
897
|
+
setActive(0);
|
|
855
898
|
}).catch((error) => {
|
|
856
899
|
if (!abort.signal.aborted) setError(String(error));
|
|
857
900
|
});
|
|
858
901
|
return () => abort.abort();
|
|
859
902
|
}, []);
|
|
903
|
+
const addCombo = () => {
|
|
904
|
+
draftKeys += 1;
|
|
905
|
+
setDraft((value) => [...value, {
|
|
906
|
+
key: `new-${draftKeys}`,
|
|
907
|
+
name: tr(lang, `组合 ${value.length + 1}`, `Combo ${value.length + 1}`),
|
|
908
|
+
pair: {}
|
|
909
|
+
}]);
|
|
910
|
+
setActive(draft.length);
|
|
911
|
+
};
|
|
912
|
+
const removeCombo = (index) => {
|
|
913
|
+
setDraft((value) => value.filter((_, position) => position !== index));
|
|
914
|
+
setActive(Math.max(0, Math.min(active > index ? active - 1 : active, draft.length - 2)));
|
|
915
|
+
};
|
|
860
916
|
const save = async () => {
|
|
861
|
-
if (!state
|
|
917
|
+
if (!state) return;
|
|
918
|
+
const invalid = draft.findIndex((combo) => !combo.pair.lead || !combo.pair.worker || !combo.name.trim());
|
|
919
|
+
if (invalid >= 0) {
|
|
920
|
+
setError(tr(lang, `第 ${invalid + 1} 个组合还不完整:需要名称、Lead 和 Sidekick。`, `Combo ${invalid + 1} is incomplete: it needs a name, a Lead and a Sidekick.`));
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
862
923
|
setSaving(true);
|
|
863
924
|
setError(void 0);
|
|
864
925
|
setNotice(void 0);
|
|
865
926
|
try {
|
|
866
|
-
const {
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
927
|
+
const pairs = draft.map(({ id, name, pair }) => {
|
|
928
|
+
const { outputTokens, ...rest } = pair;
|
|
929
|
+
const limits = Object.fromEntries(Object.entries(outputTokens ?? {}).filter(([, tokens]) => Number.isSafeInteger(tokens) && Number(tokens) > 0));
|
|
930
|
+
return {
|
|
931
|
+
...id ? { id } : {},
|
|
932
|
+
name: name.trim(),
|
|
933
|
+
pair: {
|
|
934
|
+
...rest,
|
|
935
|
+
...Object.keys(limits).length ? { outputTokens: limits } : {}
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
});
|
|
872
939
|
const response = await fetch(FUSION_API_PATH, {
|
|
873
940
|
method: "POST",
|
|
874
941
|
credentials: "same-origin",
|
|
@@ -876,37 +943,49 @@ window.__ModuleLoader__.load({
|
|
|
876
943
|
body: JSON.stringify({
|
|
877
944
|
action: "configure",
|
|
878
945
|
revision: state.revision,
|
|
879
|
-
|
|
946
|
+
pairs
|
|
880
947
|
})
|
|
881
948
|
});
|
|
882
949
|
const result = await response.json();
|
|
883
950
|
if (!response.ok) throw new Error(result.error ?? tr(lang, "保存失败,请刷新后重试", "Save failed; refresh and try again"));
|
|
884
|
-
const
|
|
951
|
+
const confirmed = await fetch(`${FUSION_API_PATH}?view=settings`, {
|
|
885
952
|
credentials: "same-origin",
|
|
886
953
|
cache: "no-store"
|
|
887
954
|
});
|
|
888
|
-
if (!
|
|
889
|
-
const
|
|
890
|
-
setState(
|
|
891
|
-
setDraft(
|
|
955
|
+
if (!confirmed.ok) throw new Error(tr(lang, "配置已提交,但暂时无法确认,请刷新", "Saved, but not confirmed yet; refresh"));
|
|
956
|
+
const next = await confirmed.json();
|
|
957
|
+
setState(next);
|
|
958
|
+
setDraft(toDraft(next.pairs));
|
|
959
|
+
setActive(0);
|
|
892
960
|
settingsChanged();
|
|
893
|
-
setNotice(tr(lang, "
|
|
961
|
+
setNotice(tr(lang, "组合已保存。模型菜单中每个组合各占一项,不同会话可以同时使用不同组合。", "Combos saved. Each one is its own model-menu entry, so different conversations can use different combos at the same time."));
|
|
894
962
|
} catch (error) {
|
|
895
963
|
setError(error instanceof Error ? error.message : String(error));
|
|
896
964
|
} finally {
|
|
897
965
|
setSaving(false);
|
|
898
966
|
}
|
|
899
967
|
};
|
|
968
|
+
const dirty = state ? JSON.stringify(draft.map(({ id, name, pair }) => ({
|
|
969
|
+
id,
|
|
970
|
+
name,
|
|
971
|
+
pair
|
|
972
|
+
}))) !== JSON.stringify(state.pairs.map(({ id, name, pair }) => ({
|
|
973
|
+
id,
|
|
974
|
+
name,
|
|
975
|
+
pair
|
|
976
|
+
}))) : false;
|
|
977
|
+
const blocked = state?.pairs.filter((pair) => !pair.authorized) ?? [];
|
|
900
978
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
901
979
|
className: "fusion-page",
|
|
902
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: styles$
|
|
980
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: styles$2 }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
903
981
|
className: "fusion-settings",
|
|
904
982
|
children: [
|
|
905
983
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
906
984
|
"data-window-drag": true,
|
|
907
985
|
children: [
|
|
908
986
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: "Fusion" }),
|
|
909
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: tr(lang, "
|
|
987
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: tr(lang, "每个组合是一对 Lead 和 Sidekick,会作为单独的一项出现在模型菜单里。开启多个会话时,可以为每个会话选择不同的组合。", "Each combo is a Lead and a Sidekick pair, listed as its own entry in the model menu. With several conversations open, each one can use a different combo.") }),
|
|
988
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: tr(lang, "Lead 处理日常请求、安排工作并审查结果;Sidekick 执行委派的任务。", "The Lead handles requests, plans and reviews; the Sidekick does the delegated work.") }),
|
|
910
989
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
|
|
911
990
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: tr(lang, "搭配建议", "Pairing tip") }),
|
|
912
991
|
": ",
|
|
@@ -923,13 +1002,77 @@ window.__ModuleLoader__.load({
|
|
|
923
1002
|
children: tr(lang, "正在读取模型…", "Loading models…")
|
|
924
1003
|
}),
|
|
925
1004
|
state && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
926
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
1005
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1006
|
+
className: "fusion-card",
|
|
1007
|
+
"aria-label": tr(lang, "组合列表", "Combo list"),
|
|
1008
|
+
children: [
|
|
1009
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1010
|
+
className: "fusion-combos",
|
|
1011
|
+
children: [draft.map((combo, index) => {
|
|
1012
|
+
const selected = combo.key === current?.key;
|
|
1013
|
+
const label = combo.name.trim() || tr(lang, "未命名组合", "Untitled combo");
|
|
1014
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1015
|
+
className: "fusion-combo",
|
|
1016
|
+
"data-active": selected,
|
|
1017
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1018
|
+
type: "button",
|
|
1019
|
+
className: "fusion-combo-main",
|
|
1020
|
+
"aria-pressed": selected,
|
|
1021
|
+
onClick: () => setActive(index),
|
|
1022
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1023
|
+
className: "fusion-combo-check",
|
|
1024
|
+
"aria-hidden": "true"
|
|
1025
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1026
|
+
className: "fusion-combo-text",
|
|
1027
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1028
|
+
className: "fusion-combo-name",
|
|
1029
|
+
children: label
|
|
1030
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1031
|
+
className: "fusion-combo-routes",
|
|
1032
|
+
children: `${routeName(combo.pair.lead)} → ${routeName(combo.pair.worker)}`
|
|
1033
|
+
})]
|
|
1034
|
+
})]
|
|
1035
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1036
|
+
type: "button",
|
|
1037
|
+
className: "fusion-combo-actions",
|
|
1038
|
+
disabled: saving || draft.length <= 1,
|
|
1039
|
+
"aria-label": tr(lang, `删除组合 ${label}`, `Delete combo ${label}`),
|
|
1040
|
+
onClick: (event) => {
|
|
1041
|
+
event.stopPropagation();
|
|
1042
|
+
removeCombo(index);
|
|
1043
|
+
},
|
|
1044
|
+
children: tr(lang, "删除", "Delete")
|
|
1045
|
+
})]
|
|
1046
|
+
}, combo.key);
|
|
1047
|
+
}), !draft.length && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: tr(lang, "还没有组合。新增一个,选择 Lead 和 Sidekick 后保存。", "No combos yet. Add one, choose a Lead and a Sidekick, then save.") })]
|
|
1048
|
+
}),
|
|
1049
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1050
|
+
type: "button",
|
|
1051
|
+
className: "fusion-combo-add",
|
|
1052
|
+
disabled: saving || draft.length >= state.maxPairs,
|
|
1053
|
+
onClick: addCombo,
|
|
1054
|
+
children: tr(lang, "+ 新增组合", "+ Add combo")
|
|
1055
|
+
}),
|
|
1056
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: tr(lang, `最多保存 ${state.maxPairs} 个组合;模型菜单中的名称是「Fusion · 组合名称」。`, `Up to ${state.maxPairs} combos; the model menu shows them as “Fusion · name”.`) })
|
|
1057
|
+
]
|
|
1058
|
+
}),
|
|
1059
|
+
current && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
|
|
927
1060
|
className: "fusion-card",
|
|
928
1061
|
disabled: saving,
|
|
929
1062
|
children: [
|
|
1063
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [tr(lang, "名称", "Name"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1064
|
+
type: "text",
|
|
1065
|
+
maxLength: 40,
|
|
1066
|
+
"aria-label": tr(lang, "组合名称", "Combo name"),
|
|
1067
|
+
value: current.name,
|
|
1068
|
+
onChange: (event) => update((combo) => ({
|
|
1069
|
+
...combo,
|
|
1070
|
+
name: event.target.value
|
|
1071
|
+
}))
|
|
1072
|
+
})] }),
|
|
930
1073
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelChoice, {
|
|
931
1074
|
role: "Lead",
|
|
932
|
-
route:
|
|
1075
|
+
route: current.pair.lead,
|
|
933
1076
|
catalog: state.catalog,
|
|
934
1077
|
onChange: (route) => {
|
|
935
1078
|
if (route) chooseModel("lead", route);
|
|
@@ -937,7 +1080,7 @@ window.__ModuleLoader__.load({
|
|
|
937
1080
|
}),
|
|
938
1081
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelChoice, {
|
|
939
1082
|
role: "Sidekick",
|
|
940
|
-
route:
|
|
1083
|
+
route: current.pair.worker,
|
|
941
1084
|
catalog: state.catalog,
|
|
942
1085
|
onChange: (route) => {
|
|
943
1086
|
if (route) chooseModel("worker", route);
|
|
@@ -949,37 +1092,42 @@ window.__ModuleLoader__.load({
|
|
|
949
1092
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: tr(lang, "默认由当前角色模型压缩。也可以单独选择已连接的模型;请求由所选账号计费。", "By default each role compacts with its own model. You can pick another connected model; its account is billed.") }),
|
|
950
1093
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelChoice, {
|
|
951
1094
|
role: "compactor",
|
|
952
|
-
route:
|
|
1095
|
+
route: current.pair.compactor?.route,
|
|
953
1096
|
catalog: state.catalog,
|
|
954
|
-
onChange: (route) =>
|
|
955
|
-
const { compactor, ...rest } =
|
|
956
|
-
return
|
|
957
|
-
...
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1097
|
+
onChange: (route) => update((combo) => {
|
|
1098
|
+
const { compactor, ...rest } = combo.pair;
|
|
1099
|
+
return {
|
|
1100
|
+
...combo,
|
|
1101
|
+
pair: route ? {
|
|
1102
|
+
...rest,
|
|
1103
|
+
compactor: {
|
|
1104
|
+
route,
|
|
1105
|
+
maxOutputTokens: compactor?.maxOutputTokens ?? 8192
|
|
1106
|
+
}
|
|
1107
|
+
} : rest
|
|
1108
|
+
};
|
|
963
1109
|
})
|
|
964
1110
|
}),
|
|
965
|
-
|
|
1111
|
+
current.pair.compactor && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [tr(lang, "输出 Token 上限", "Output token limit"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
966
1112
|
"aria-label": tr(lang, "压缩输出 Token 上限", "Compaction output token limit"),
|
|
967
1113
|
type: "number",
|
|
968
1114
|
min: "1",
|
|
969
1115
|
max: "128000",
|
|
970
|
-
value:
|
|
1116
|
+
value: current.pair.compactor.maxOutputTokens,
|
|
971
1117
|
onChange: (event) => {
|
|
972
1118
|
const maxOutputTokens = Number(event.target.value);
|
|
973
|
-
|
|
974
|
-
...
|
|
975
|
-
|
|
976
|
-
...
|
|
977
|
-
|
|
1119
|
+
update((combo) => combo.pair.compactor ? {
|
|
1120
|
+
...combo,
|
|
1121
|
+
pair: {
|
|
1122
|
+
...combo.pair,
|
|
1123
|
+
compactor: {
|
|
1124
|
+
...combo.pair.compactor,
|
|
1125
|
+
maxOutputTokens
|
|
1126
|
+
}
|
|
978
1127
|
}
|
|
979
|
-
} :
|
|
1128
|
+
} : combo);
|
|
980
1129
|
}
|
|
981
|
-
})] })
|
|
982
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: tr(lang, "已选中的会话仍使用原配置。", "Conversations already using Fusion keep their setup.") })
|
|
1130
|
+
})] })
|
|
983
1131
|
]
|
|
984
1132
|
})] })
|
|
985
1133
|
]
|
|
@@ -987,11 +1135,11 @@ window.__ModuleLoader__.load({
|
|
|
987
1135
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
988
1136
|
className: "fusion-actions",
|
|
989
1137
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: tr(lang, "已选中的会话保留原组合;新选择使用新组合。额度耗尽后要换当前任务的 Sidekick,用该会话的 Fusion 恢复面板,而不是只改这里。", "Conversations already using Fusion keep their pair; new selections use the new one. After a quota stop, switch this conversation's Sidekick from its Fusion recovery panel, not only here.") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
990
|
-
disabled: saving || !
|
|
1138
|
+
disabled: saving || !dirty || !draft.length,
|
|
991
1139
|
onClick: () => {
|
|
992
1140
|
save();
|
|
993
1141
|
},
|
|
994
|
-
children: saving ? tr(lang, "正在保存…", "Saving…") : tr(lang, "保存组合", "Save
|
|
1142
|
+
children: saving ? tr(lang, "正在保存…", "Saving…") : tr(lang, "保存组合", "Save combos")
|
|
995
1143
|
})]
|
|
996
1144
|
}),
|
|
997
1145
|
notice && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -1000,10 +1148,10 @@ window.__ModuleLoader__.load({
|
|
|
1000
1148
|
}),
|
|
1001
1149
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: tr(lang, "输出参数跟随原生模型设置,Fusion 不额外限制请求次数或任务轮数。", "Output settings follow the native model settings; Fusion adds no request or round limits.") }),
|
|
1002
1150
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: tr(lang, "保存即表示允许 Fusion 使用所选模型的现有账号;费用由这些账号计入,当前未提供可靠的实际账单金额。", "Saving lets Fusion use the chosen models' existing accounts; they are billed there, and no reliable billed amount is available here.") }),
|
|
1003
|
-
|
|
1151
|
+
blocked.map((pair) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1004
1152
|
role: "status",
|
|
1005
|
-
children:
|
|
1006
|
-
}),
|
|
1153
|
+
children: `${pair.name}: ${tr(lang, "尚未就绪,需要重新保存这个组合", "not ready yet; save this combo again")}${pair.reason ? `: ${pair.reason}` : ""}`
|
|
1154
|
+
}, pair.id)),
|
|
1007
1155
|
state.managedAuthorization && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: tr(lang, "运行额度由外部授权配置管理。", "Run limits are managed by an external authorization file.") })
|
|
1008
1156
|
] }),
|
|
1009
1157
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FusionCache, { refresh: state?.revision ?? 0 }),
|
|
@@ -1247,7 +1395,7 @@ window.__ModuleLoader__.load({
|
|
|
1247
1395
|
//#region src/client/status-polling.ts
|
|
1248
1396
|
const entries = /* @__PURE__ */ new Map();
|
|
1249
1397
|
const visible = () => document.visibilityState !== "hidden";
|
|
1250
|
-
const running = new Set([
|
|
1398
|
+
const running = /* @__PURE__ */ new Set([
|
|
1251
1399
|
"分析",
|
|
1252
1400
|
"执行",
|
|
1253
1401
|
"修改",
|
|
@@ -1333,22 +1481,28 @@ window.__ModuleLoader__.load({
|
|
|
1333
1481
|
document.removeEventListener("visibilitychange", own.visibility);
|
|
1334
1482
|
};
|
|
1335
1483
|
}
|
|
1336
|
-
/**
|
|
1337
|
-
|
|
1338
|
-
|
|
1484
|
+
/**
|
|
1485
|
+
* One in-flight read per mounted session; collapsed hints still discover new attention states.
|
|
1486
|
+
* `live` asks for the fast cadence whenever the task is running, for a view whose whole
|
|
1487
|
+
* purpose is the current stage (the role and permission of the model working now).
|
|
1488
|
+
*/
|
|
1489
|
+
function useFusionStatus(sessionId, expandedTask, attentionOnly = false, live = false) {
|
|
1490
|
+
const expandedRef = (0, react.useRef)(expandedTask), attentionRef = (0, react.useRef)(attentionOnly), liveRef = (0, react.useRef)(live);
|
|
1339
1491
|
expandedRef.current = expandedTask;
|
|
1340
1492
|
attentionRef.current = attentionOnly;
|
|
1493
|
+
liveRef.current = live;
|
|
1341
1494
|
(0, react.useEffect)(() => {
|
|
1342
1495
|
if (expandedTask) entries.get(sessionId)?.visibility();
|
|
1343
1496
|
}, [sessionId, expandedTask]);
|
|
1344
1497
|
return (0, react.useSyncExternalStore)((0, react.useCallback)((listener) => subscribe(sessionId, listener, () => {
|
|
1498
|
+
if (liveRef.current) return true;
|
|
1345
1499
|
const task = entries.get(sessionId)?.snapshot?.status?.task;
|
|
1346
1500
|
return expandedRef.current !== void 0 && task?.id === expandedRef.current && (!attentionRef.current || task.attention);
|
|
1347
1501
|
}), [sessionId]), (0, react.useCallback)(() => entries.get(sessionId)?.snapshot, [sessionId]), () => void 0);
|
|
1348
1502
|
}
|
|
1349
1503
|
//#endregion
|
|
1350
1504
|
//#region src/client/FusionStatus.tsx
|
|
1351
|
-
const styles = `
|
|
1505
|
+
const styles$1 = `
|
|
1352
1506
|
.fusion-status{margin:8px 12px;color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5;overflow-wrap:anywhere}
|
|
1353
1507
|
.fusion-status>summary{cursor:pointer;min-height:44px;display:flex;align-items:center;gap:12px;list-style:none;border-top:1px solid var(--dsw-alias-border-l2);outline-offset:2px}
|
|
1354
1508
|
.fusion-status>summary::before{content:'›';font-size:18px;flex:none}.fusion-status[open]>summary::before{content:'⌄'}
|
|
@@ -1428,7 +1582,7 @@ window.__ModuleLoader__.load({
|
|
|
1428
1582
|
className: "fusion-status",
|
|
1429
1583
|
onToggle: (event) => setExpandedTask(event.currentTarget.open ? task.id : void 0),
|
|
1430
1584
|
children: [
|
|
1431
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: styles }),
|
|
1585
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: styles$1 }),
|
|
1432
1586
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", {
|
|
1433
1587
|
"aria-label": tr(lang, `Fusion · ${stage},任务详情`, `Fusion · ${stage}, task details`),
|
|
1434
1588
|
"data-ud-check": "fusion-status-summary",
|
|
@@ -1572,7 +1726,12 @@ window.__ModuleLoader__.load({
|
|
|
1572
1726
|
if (!response.ok) throw new Error("unavailable");
|
|
1573
1727
|
return response.json();
|
|
1574
1728
|
}).then((state) => {
|
|
1575
|
-
if (active)
|
|
1729
|
+
if (!active) return;
|
|
1730
|
+
const combo = state.pairs.find((pair) => pair.id === selected?.model);
|
|
1731
|
+
if (!state.pairs.length) setMessage(tr(lang, "Fusion 尚未配置模型,请前往设置 → Fusion 选择 Lead 和 Sidekick。", "Fusion has no models yet: open Settings → Fusion and choose a Lead and a Sidekick."));
|
|
1732
|
+
else if (!combo) setMessage(tr(lang, "当前组合已被删除,请在模型菜单中选择其他组合。", "This combo was removed; pick another one in the model menu."));
|
|
1733
|
+
else if (!combo.authorized) setMessage(tr(lang, "Fusion 当前配置未就绪,请前往设置 → Fusion 查看。", "The Fusion setup is not ready: see Settings → Fusion."));
|
|
1734
|
+
else setMessage(void 0);
|
|
1576
1735
|
}).catch(() => {
|
|
1577
1736
|
if (active) setMessage(tr(lang, "暂时无法确认 Fusion 配置,请前往设置 → Fusion 查看。", "Cannot confirm the Fusion setup right now: see Settings → Fusion."));
|
|
1578
1737
|
});
|
|
@@ -1584,7 +1743,11 @@ window.__ModuleLoader__.load({
|
|
|
1584
1743
|
abort.abort();
|
|
1585
1744
|
off();
|
|
1586
1745
|
};
|
|
1587
|
-
}, [
|
|
1746
|
+
}, [
|
|
1747
|
+
fusion,
|
|
1748
|
+
lang,
|
|
1749
|
+
selected?.model
|
|
1750
|
+
]);
|
|
1588
1751
|
if (!fusion) return null;
|
|
1589
1752
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [message && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1590
1753
|
role: "status",
|
|
@@ -1601,6 +1764,54 @@ window.__ModuleLoader__.load({
|
|
|
1601
1764
|
})] });
|
|
1602
1765
|
}
|
|
1603
1766
|
//#endregion
|
|
1767
|
+
//#region src/client/FusionPermission.tsx
|
|
1768
|
+
const styles = `
|
|
1769
|
+
.fusion-permission{display:inline-flex;align-items:center;gap:4px;height:28px;min-width:0;max-width:220px;padding:0 6px;border-radius:var(--dsw-radius-sm);color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;font-weight:500}
|
|
1770
|
+
.fusion-permission svg{width:14px;height:14px;flex:none}
|
|
1771
|
+
.fusion-permission .fusion-permission-role{flex:none;color:var(--dsw-alias-label-caption)}
|
|
1772
|
+
.fusion-permission .fusion-permission-mode{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1773
|
+
`;
|
|
1774
|
+
const modeOf = (lang, mode) => mode === "read-only" ? tr(lang, "仅可查看", "Read Only") : mode === "workspace-write" ? tr(lang, "工作区内修改", "Workspace Write") : tr(lang, "完全权限", "Full access");
|
|
1775
|
+
const iconOf = (mode) => mode === "read-only" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.PermissionIconReadOnlyRegular, {}) : mode === "workspace-write" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.PermissionIconWorkspaceWriteRegular, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.PermissionIconFullAccessRegular, {});
|
|
1776
|
+
/**
|
|
1777
|
+
* The permission of the model working now. The composer's native permission control
|
|
1778
|
+
* belongs to the conversation (Lead) session, which Fusion confines to read-only, and
|
|
1779
|
+
* that confinement matches no configured preset — so the control reads Custom while a
|
|
1780
|
+
* task runs. This additive chip names the role and its mode, so the tool row never
|
|
1781
|
+
* shows an unexplained Custom alone.
|
|
1782
|
+
*/
|
|
1783
|
+
function FusionPermission({ sessionId, directory, load }) {
|
|
1784
|
+
const selected = (0, react.useSyncExternalStore)(directory.subscribe, directory.getSnapshot).current;
|
|
1785
|
+
const fusion = isFusionSelection(selected ?? void 0);
|
|
1786
|
+
const lang = useLang();
|
|
1787
|
+
const snapshot = useFusionStatus(sessionId, void 0, false, true);
|
|
1788
|
+
(0, react.useEffect)(() => {
|
|
1789
|
+
load();
|
|
1790
|
+
}, [load]);
|
|
1791
|
+
const permissions = fusion ? snapshot?.status?.task?.permissions : void 0;
|
|
1792
|
+
if (!permissions) return null;
|
|
1793
|
+
const role = permissions.active === "worker" ? "Sidekick" : "Lead";
|
|
1794
|
+
const mode = permissions.active === "worker" ? permissions.worker : permissions.lead;
|
|
1795
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1796
|
+
className: "fusion-permission",
|
|
1797
|
+
title: tr(lang, `Fusion 按角色分离权限:Lead ${modeOf(lang, permissions.lead)},Sidekick ${modeOf(lang, permissions.worker)}。输入框的权限控件属于对话会话(Lead),运行期间可能显示 Custom。`, `Fusion separates permissions by role: Lead ${modeOf(lang, permissions.lead)}, Sidekick ${modeOf(lang, permissions.worker)}. The permission control belongs to the conversation (Lead) session, so it may read Custom while a task runs.`),
|
|
1798
|
+
"aria-label": `${role} · ${modeOf(lang, mode)}`,
|
|
1799
|
+
"data-ud-check": "fusion-permission",
|
|
1800
|
+
children: [
|
|
1801
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: styles }),
|
|
1802
|
+
iconOf(mode),
|
|
1803
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1804
|
+
className: "fusion-permission-role",
|
|
1805
|
+
children: role
|
|
1806
|
+
}),
|
|
1807
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1808
|
+
className: "fusion-permission-mode",
|
|
1809
|
+
children: modeOf(lang, mode)
|
|
1810
|
+
})
|
|
1811
|
+
]
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1814
|
+
//#endregion
|
|
1604
1815
|
//#region src/activity.ts
|
|
1605
1816
|
/** Namespaced display identity; the unchanged native identity remains in source. */
|
|
1606
1817
|
function activityCallId(activity) {
|
|
@@ -1890,21 +2101,28 @@ window.__ModuleLoader__.load({
|
|
|
1890
2101
|
label: () => "Fusion"
|
|
1891
2102
|
}, FusionPanelIcon));
|
|
1892
2103
|
ctx.inject(["modelDirectories"], (scope) => {
|
|
2104
|
+
const sessionProps = (sessionId) => {
|
|
2105
|
+
const directory = scope.modelDirectories.directoryFor(sessionId);
|
|
2106
|
+
return {
|
|
2107
|
+
sessionId,
|
|
2108
|
+
directory: directory.store,
|
|
2109
|
+
load: () => {
|
|
2110
|
+
directory.load().catch(() => void 0);
|
|
2111
|
+
}
|
|
2112
|
+
};
|
|
2113
|
+
};
|
|
1893
2114
|
scope.slots.inject("conversation.input.dock", () => scope.slots.register({
|
|
1894
2115
|
name: "conversation.input.dock",
|
|
1895
2116
|
id: "fusion-configuration",
|
|
1896
2117
|
order: 20,
|
|
1897
|
-
inject:
|
|
1898
|
-
const directory = scope.modelDirectories.directoryFor(sessionId);
|
|
1899
|
-
return {
|
|
1900
|
-
sessionId,
|
|
1901
|
-
directory: directory.store,
|
|
1902
|
-
load: () => {
|
|
1903
|
-
directory.load().catch(() => void 0);
|
|
1904
|
-
}
|
|
1905
|
-
};
|
|
1906
|
-
}
|
|
2118
|
+
inject: sessionProps
|
|
1907
2119
|
}, FusionHint));
|
|
2120
|
+
scope.slots.inject("conversation.input.left", () => scope.slots.register({
|
|
2121
|
+
name: "conversation.input.left",
|
|
2122
|
+
id: "fusion-permission",
|
|
2123
|
+
order: 10,
|
|
2124
|
+
inject: sessionProps
|
|
2125
|
+
}, FusionPermission));
|
|
1908
2126
|
});
|
|
1909
2127
|
}
|
|
1910
2128
|
//#endregion
|