@sqdr2/dsh-fusion 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL.md +89 -0
- package/INSTALL.zh.md +86 -0
- package/LICENSE +201 -0
- package/README.en.md +191 -0
- package/README.md +180 -2
- package/contracts/fusion.ts +279 -0
- package/contracts/storage.sql +86 -0
- package/cordis.patch.yml +3 -0
- package/docs/COMPATIBILITY.md +162 -0
- package/docs/EVIDENCE.md +141 -0
- package/dshx.yml +39 -0
- package/lib/client.js +1920 -0
- package/lib/client.js.map +1 -0
- package/lib/dsh-fusion.js +12661 -0
- package/lib/types/activity.d.ts +20 -0
- package/lib/types/approval/logical.d.ts +10 -0
- package/lib/types/benchmark/naive.d.ts +37 -0
- package/lib/types/benchmark/native-run.d.ts +68 -0
- package/lib/types/benchmark/output-limits.d.ts +19 -0
- package/lib/types/bilingual.d.ts +2 -0
- package/lib/types/client/ActivityRows.d.ts +8 -0
- package/lib/types/client/FusionCache.d.ts +4 -0
- package/lib/types/client/FusionHint.d.ts +9 -0
- package/lib/types/client/FusionHistory.d.ts +5 -0
- package/lib/types/client/FusionPanelIcon.d.ts +13 -0
- package/lib/types/client/FusionRecovery.d.ts +5 -0
- package/lib/types/client/FusionSettings.d.ts +9 -0
- package/lib/types/client/FusionStatus.d.ts +5 -0
- package/lib/types/client/FusionTool.d.ts +4 -0
- package/lib/types/client/activity-model.d.ts +14 -0
- package/lib/types/client/i18n.d.ts +15 -0
- package/lib/types/client/index.d.ts +22 -0
- package/lib/types/client/settings-events.d.ts +2 -0
- package/lib/types/client/status-polling.d.ts +9 -0
- package/lib/types/context/guard.d.ts +21 -0
- package/lib/types/context/handoff.d.ts +17 -0
- package/lib/types/contracts.d.ts +449 -0
- package/lib/types/digest.d.ts +5 -0
- package/lib/types/dsh-fusion.d.ts +13 -0
- package/lib/types/errors.d.ts +28 -0
- package/lib/types/evidence/gate.d.ts +23 -0
- package/lib/types/evidence/receipts.d.ts +65 -0
- package/lib/types/execution/write-lease.d.ts +42 -0
- package/lib/types/host/bindings.d.ts +30 -0
- package/lib/types/host/cache-defaults.d.ts +48 -0
- package/lib/types/host/cache-policy.d.ts +155 -0
- package/lib/types/host/change-evidence.d.ts +38 -0
- package/lib/types/host/compaction-routing.d.ts +15 -0
- package/lib/types/host/coordinator.d.ts +169 -0
- package/lib/types/host/evidence-page.d.ts +13 -0
- package/lib/types/host/history.d.ts +88 -0
- package/lib/types/host/http.d.ts +4 -0
- package/lib/types/host/model-output.d.ts +13 -0
- package/lib/types/host/native-activity.d.ts +26 -0
- package/lib/types/host/native-approvals.d.ts +29 -0
- package/lib/types/host/native-auxiliary.d.ts +17 -0
- package/lib/types/host/native-budget.d.ts +33 -0
- package/lib/types/host/native-checks.d.ts +104 -0
- package/lib/types/host/native-compaction.d.ts +8 -0
- package/lib/types/host/native-context.d.ts +36 -0
- package/lib/types/host/native-effects.d.ts +75 -0
- package/lib/types/host/native-evidence.d.ts +4 -0
- package/lib/types/host/native-exploration.d.ts +15 -0
- package/lib/types/host/native-keepalive.d.ts +82 -0
- package/lib/types/host/native-model-control.d.ts +99 -0
- package/lib/types/host/native-on-demand-context.d.ts +39 -0
- package/lib/types/host/native-request.d.ts +5 -0
- package/lib/types/host/native-review.d.ts +11 -0
- package/lib/types/host/native-role-sandbox.d.ts +49 -0
- package/lib/types/host/native-scopes.d.ts +49 -0
- package/lib/types/host/native-selection.d.ts +29 -0
- package/lib/types/host/native-task-context.d.ts +39 -0
- package/lib/types/host/native-usage.d.ts +46 -0
- package/lib/types/host/native-worker-brief.d.ts +74 -0
- package/lib/types/host/native-worker-notices.d.ts +15 -0
- package/lib/types/host/native-worker.d.ts +44 -0
- package/lib/types/host/native-workflow.d.ts +41 -0
- package/lib/types/host/route-transport.d.ts +22 -0
- package/lib/types/host/settings.d.ts +39 -0
- package/lib/types/host/shell.d.ts +11 -0
- package/lib/types/host/status.d.ts +7 -0
- package/lib/types/host/subagent-seam.d.ts +10 -0
- package/lib/types/host/tool-table.d.ts +35 -0
- package/lib/types/host/worker-terminal.d.ts +68 -0
- package/lib/types/host/workspace-guard.d.ts +13 -0
- package/lib/types/host/workspace.d.ts +52 -0
- package/lib/types/identity.d.ts +64 -0
- package/lib/types/index.d.ts +34 -0
- package/lib/types/message-source.d.ts +29 -0
- package/lib/types/profile/compactor.d.ts +5 -0
- package/lib/types/profile/keepalive.d.ts +3 -0
- package/lib/types/profile/resolve.d.ts +17 -0
- package/lib/types/prompts.d.ts +8 -0
- package/lib/types/recovery/reconcile.d.ts +28 -0
- package/lib/types/review/dispatch.d.ts +20 -0
- package/lib/types/review/protocol.d.ts +51 -0
- package/lib/types/review/ticket.d.ts +32 -0
- package/lib/types/status.d.ts +88 -0
- package/lib/types/store-migration.d.ts +29 -0
- package/lib/types/task/deletion.d.ts +64 -0
- package/lib/types/task/reducer.d.ts +6 -0
- package/lib/types/task/sqlite-store.d.ts +71 -0
- package/lib/types/task/state.d.ts +51 -0
- package/lib/types/task/status-index.d.ts +28 -0
- package/lib/types/task/store.d.ts +48 -0
- package/lib/types/usage/budget.d.ts +27 -0
- package/lib/types/usage/ledger.d.ts +66 -0
- package/lib/types/usage/normalize.d.ts +50 -0
- package/lib/types/usage/pricing.d.ts +18 -0
- package/package.json +176 -4
- package/profiles/fusion-auto.example.json +45 -0
- package/prompts/compact.md +7 -0
- package/prompts/lead.md +42 -0
- package/prompts/model-lead.md +13 -0
- package/prompts/model-worker.md +5 -0
- package/prompts/worker.md +21 -0
- package/src/activity.ts +23 -0
- package/src/approval/logical.ts +44 -0
- package/src/benchmark/naive.ts +216 -0
- package/src/benchmark/native-run.ts +215 -0
- package/src/benchmark/output-limits.ts +33 -0
- package/src/bilingual.ts +2 -0
- package/src/client/ActivityRows.tsx +59 -0
- package/src/client/FusionCache.tsx +130 -0
- package/src/client/FusionHint.tsx +39 -0
- package/src/client/FusionHistory.tsx +247 -0
- package/src/client/FusionPanelIcon.tsx +39 -0
- package/src/client/FusionRecovery.tsx +69 -0
- package/src/client/FusionSettings.tsx +205 -0
- package/src/client/FusionStatus.tsx +103 -0
- package/src/client/FusionTool.tsx +66 -0
- package/src/client/activity-model.ts +31 -0
- package/src/client/i18n.ts +30 -0
- package/src/client/index.ts +50 -0
- package/src/client/settings-events.ts +3 -0
- package/src/client/status-polling.ts +88 -0
- package/src/context/guard.ts +81 -0
- package/src/context/handoff.ts +28 -0
- package/src/contracts.ts +449 -0
- package/src/digest.ts +28 -0
- package/src/dsh-fusion.ts +321 -0
- package/src/errors.ts +34 -0
- package/src/evidence/gate.ts +290 -0
- package/src/evidence/receipts.ts +175 -0
- package/src/execution/write-lease.ts +187 -0
- package/src/host/bindings.ts +91 -0
- package/src/host/cache-defaults.ts +103 -0
- package/src/host/cache-policy.ts +168 -0
- package/src/host/change-evidence.ts +156 -0
- package/src/host/compaction-routing.ts +46 -0
- package/src/host/coordinator.ts +2160 -0
- package/src/host/evidence-page.ts +25 -0
- package/src/host/history.ts +114 -0
- package/src/host/http.ts +30 -0
- package/src/host/model-output.ts +22 -0
- package/src/host/native-activity.ts +199 -0
- package/src/host/native-approvals.ts +196 -0
- package/src/host/native-auxiliary.ts +19 -0
- package/src/host/native-budget.ts +112 -0
- package/src/host/native-checks.ts +474 -0
- package/src/host/native-compaction.ts +34 -0
- package/src/host/native-context.ts +204 -0
- package/src/host/native-effects.ts +260 -0
- package/src/host/native-evidence.ts +31 -0
- package/src/host/native-exploration.ts +114 -0
- package/src/host/native-keepalive.ts +292 -0
- package/src/host/native-model-control.ts +298 -0
- package/src/host/native-on-demand-context.ts +74 -0
- package/src/host/native-request.ts +12 -0
- package/src/host/native-review.ts +51 -0
- package/src/host/native-role-sandbox.ts +171 -0
- package/src/host/native-scopes.ts +203 -0
- package/src/host/native-selection.ts +110 -0
- package/src/host/native-task-context.ts +264 -0
- package/src/host/native-usage.ts +121 -0
- package/src/host/native-worker-brief.ts +122 -0
- package/src/host/native-worker-notices.ts +72 -0
- package/src/host/native-worker.ts +215 -0
- package/src/host/native-workflow.ts +103 -0
- package/src/host/route-transport.ts +54 -0
- package/src/host/settings.ts +93 -0
- package/src/host/shell.ts +13 -0
- package/src/host/status.ts +112 -0
- package/src/host/subagent-seam.ts +20 -0
- package/src/host/tool-table.ts +56 -0
- package/src/host/worker-terminal.ts +91 -0
- package/src/host/workspace-guard.ts +17 -0
- package/src/host/workspace.ts +184 -0
- package/src/identity.ts +76 -0
- package/src/index.ts +50 -0
- package/src/message-source.ts +31 -0
- package/src/profile/compactor.ts +26 -0
- package/src/profile/keepalive.ts +13 -0
- package/src/profile/resolve.ts +131 -0
- package/src/prompts.ts +32 -0
- package/src/recovery/reconcile.ts +38 -0
- package/src/review/dispatch.ts +48 -0
- package/src/review/protocol.ts +212 -0
- package/src/review/ticket.ts +166 -0
- package/src/status.ts +55 -0
- package/src/store-migration.ts +65 -0
- package/src/task/deletion.ts +105 -0
- package/src/task/reducer.ts +679 -0
- package/src/task/sqlite-store.ts +386 -0
- package/src/task/state.ts +72 -0
- package/src/task/status-index.ts +198 -0
- package/src/task/store.ts +287 -0
- package/src/usage/budget.ts +88 -0
- package/src/usage/ledger.ts +218 -0
- package/src/usage/normalize.ts +308 -0
- package/src/usage/pricing.ts +95 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,1920 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@sqdr2/dsh-fusion",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
10
|
+
//#region src/client/i18n.ts
|
|
11
|
+
let source;
|
|
12
|
+
const listeners$1 = /* @__PURE__ */ new Set();
|
|
13
|
+
const notify = () => {
|
|
14
|
+
for (const listener of listeners$1) listener();
|
|
15
|
+
};
|
|
16
|
+
/** Called from the client entry when DSH's locale service is available; follows the user's language setting. */
|
|
17
|
+
function bindLocale(next) {
|
|
18
|
+
source = next;
|
|
19
|
+
notify();
|
|
20
|
+
const unsubscribe = next.subscribe(notify);
|
|
21
|
+
return () => {
|
|
22
|
+
unsubscribe();
|
|
23
|
+
if (source === next) {
|
|
24
|
+
source = void 0;
|
|
25
|
+
notify();
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const current = () => {
|
|
30
|
+
return (source?.getSnapshot().active ?? (typeof navigator === "undefined" ? "en" : navigator.language)).toLowerCase().startsWith("zh") ? "zh" : "en";
|
|
31
|
+
};
|
|
32
|
+
/** The UI language: Chinese for any zh locale, English otherwise (DSH's own fallback). */
|
|
33
|
+
function useLang() {
|
|
34
|
+
return (0, react.useSyncExternalStore)((listener) => {
|
|
35
|
+
listeners$1.add(listener);
|
|
36
|
+
return () => {
|
|
37
|
+
listeners$1.delete(listener);
|
|
38
|
+
};
|
|
39
|
+
}, current, () => "en");
|
|
40
|
+
}
|
|
41
|
+
/** Pick the text for the active language. */
|
|
42
|
+
const tr = (lang, zh, en) => lang === "zh" ? zh : en;
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/identity.ts
|
|
45
|
+
/**
|
|
46
|
+
* The identity this plugin registers under.
|
|
47
|
+
*
|
|
48
|
+
* Both halves import this module, so the provider route, the model entry and
|
|
49
|
+
* the durable producers keep one spelling. It deliberately has no imports: the
|
|
50
|
+
* browser bundle inlines it and must not pull a Host module in.
|
|
51
|
+
*/
|
|
52
|
+
/** The HTTP path the panel's own views and actions are served from. */
|
|
53
|
+
const FUSION_API_PATH = "/api/fusion";
|
|
54
|
+
/** Whether a provider route belongs to this plugin, under either name. */
|
|
55
|
+
const isFusionProvider = (provider) => provider === "dsh-fusion" || provider === "dsh-model-fusion";
|
|
56
|
+
/** Whether a route names the Fusion entry, under either name. */
|
|
57
|
+
const isFusionSelection = (route) => isFusionProvider(route?.provider) && route?.model === "auto";
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/client/FusionHistory.tsx
|
|
60
|
+
function formatCompact(tokens) {
|
|
61
|
+
if (tokens < 1e3) return `${tokens}`;
|
|
62
|
+
if (tokens < 1e6) {
|
|
63
|
+
const k = tokens / 1e3;
|
|
64
|
+
return `${k >= 100 ? Math.round(k) : Number(k.toFixed(1))}K`;
|
|
65
|
+
}
|
|
66
|
+
const m = tokens / 1e6;
|
|
67
|
+
return `${m >= 100 ? Math.round(m) : Number(m.toFixed(m >= 10 ? 1 : 2))}M`;
|
|
68
|
+
}
|
|
69
|
+
function outputTokens(lang, usage, calls) {
|
|
70
|
+
if (!calls) return "—";
|
|
71
|
+
const total = usage.totalOutput;
|
|
72
|
+
const hasTotal = Boolean(total && total.reported > 0);
|
|
73
|
+
const hasVisible = Boolean(usage.output && usage.output.reported > 0);
|
|
74
|
+
const hasReasoning = Boolean(usage.reasoning && usage.reasoning.reported > 0);
|
|
75
|
+
if (!hasTotal) {
|
|
76
|
+
if (hasVisible) {
|
|
77
|
+
const vis = formatCompact(usage.output.known);
|
|
78
|
+
const cov = usage.output.reported < calls ? tr(lang, `(${usage.output.reported}/${calls} 次有报告)`, ` (${usage.output.reported}/${calls} reported)`) : "";
|
|
79
|
+
return `${tr(lang, `至少 ${vis},推理未提供`, `at least ${vis}, reasoning unavailable`)}${cov}`;
|
|
80
|
+
}
|
|
81
|
+
return tr(lang, "未提供", "not reported");
|
|
82
|
+
}
|
|
83
|
+
const totalStr = formatCompact(total.known);
|
|
84
|
+
const visibleStr = hasVisible ? formatCompact(usage.output.known) : tr(lang, "未提供", "not reported");
|
|
85
|
+
const reasoningStr = hasReasoning ? formatCompact(usage.reasoning.known) : tr(lang, "未提供", "not reported");
|
|
86
|
+
const coverageStr = total.reported < calls ? tr(lang, `(${total.reported}/${calls} 次有报告)`, ` (${total.reported}/${calls} reported)`) : "";
|
|
87
|
+
return `${totalStr} ${tr(lang, `(可见 ${visibleStr} + 推理 ${reasoningStr})`, ` (visible ${visibleStr} + reasoning ${reasoningStr})`)}${coverageStr}`;
|
|
88
|
+
}
|
|
89
|
+
const takeoverReason = (lang, reason) => ({
|
|
90
|
+
"checks-still-failing": tr(lang, "返工后检查仍失败", "checks still failing after rework"),
|
|
91
|
+
"worker-step-limit": tr(lang, "Sidekick 步数用尽", "Sidekick step limit"),
|
|
92
|
+
"worker-stalled": tr(lang, "Sidekick 反复停滞", "Sidekick stalled repeatedly")
|
|
93
|
+
})[reason] ?? reason;
|
|
94
|
+
const tokens$1 = (lang, field, calls) => !calls ? "—" : !field.reported ? tr(lang, "未提供", "not reported") : `${formatCompact(field.known)} (${field.known.toLocaleString()})${field.reported < calls ? tr(lang, `(${field.reported}/${calls} 次有报告)`, ` (${field.reported}/${calls} reported)`) : ""}`;
|
|
95
|
+
const purpose = (lang, value) => ({
|
|
96
|
+
conversation: tr(lang, "任务", "task"),
|
|
97
|
+
compaction: tr(lang, "压缩", "compaction"),
|
|
98
|
+
"cache-keepalive": tr(lang, "保活", "keepalive"),
|
|
99
|
+
"session-title": tr(lang, "标题", "title")
|
|
100
|
+
})[value] ?? value;
|
|
101
|
+
/** What still holds the task, so a refusal says why instead of only that it refused. */
|
|
102
|
+
const blockerText = (lang, blocker) => ({
|
|
103
|
+
agent: tr(lang, "Lead 或 Sidekick 仍在运行", "its Lead or Sidekick is still running"),
|
|
104
|
+
tool: tr(lang, "有原生工具调用尚未返回", "a native tool call has not returned"),
|
|
105
|
+
job: tr(lang, "有后台任务尚未停止", "a background job has not stopped"),
|
|
106
|
+
lease: tr(lang, "仍持有工作区写入租约", "it still holds the workspace write lease"),
|
|
107
|
+
"foreign-process": tr(lang, "另一个 Host 进程仍在处理它", "another Host process is still working on it")
|
|
108
|
+
})[blocker ?? ""] ?? tr(lang, "仍有活动写入方", "something is still writing to it");
|
|
109
|
+
function FusionHistory() {
|
|
110
|
+
const lang = useLang();
|
|
111
|
+
const [page, setPage] = (0, react.useState)();
|
|
112
|
+
const [cursor, setCursor] = (0, react.useState)("");
|
|
113
|
+
const [error, setError] = (0, react.useState)("");
|
|
114
|
+
const [refresh, setRefresh] = (0, react.useState)(0);
|
|
115
|
+
const [loading, setLoading] = (0, react.useState)(false);
|
|
116
|
+
const [confirmingId, setConfirmingId] = (0, react.useState)(null);
|
|
117
|
+
const [deletingId, setDeletingId] = (0, react.useState)(null);
|
|
118
|
+
const [rowError, setRowError] = (0, react.useState)(null);
|
|
119
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
120
|
+
const [tasksOpen, setTasksOpen] = (0, react.useState)(false);
|
|
121
|
+
(0, react.useEffect)(() => {
|
|
122
|
+
const abort = new AbortController();
|
|
123
|
+
setLoading(true);
|
|
124
|
+
setError("");
|
|
125
|
+
fetch(`${FUSION_API_PATH}?view=history&cursor=${encodeURIComponent(cursor)}`, {
|
|
126
|
+
credentials: "same-origin",
|
|
127
|
+
cache: "no-store",
|
|
128
|
+
signal: abort.signal
|
|
129
|
+
}).then(async (response) => {
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
const body = await response.json().catch(() => null);
|
|
132
|
+
if (cursor && (response.status === 409 || body?.error?.includes("cursor") || body?.error?.includes("Cursor"))) {
|
|
133
|
+
setCursor("");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
throw new Error(body?.error || tr(lang, "历史记录暂时无法读取", "History is unavailable right now"));
|
|
137
|
+
}
|
|
138
|
+
return response.json();
|
|
139
|
+
}).then((data) => {
|
|
140
|
+
if (data) setPage(data);
|
|
141
|
+
}).catch((error) => {
|
|
142
|
+
if (!abort.signal.aborted) setError(String(error));
|
|
143
|
+
}).finally(() => {
|
|
144
|
+
if (!abort.signal.aborted) setLoading(false);
|
|
145
|
+
});
|
|
146
|
+
return () => abort.abort();
|
|
147
|
+
}, [
|
|
148
|
+
cursor,
|
|
149
|
+
refresh,
|
|
150
|
+
lang
|
|
151
|
+
]);
|
|
152
|
+
const handleDelete = async (taskId) => {
|
|
153
|
+
setDeletingId(taskId);
|
|
154
|
+
setRowError(null);
|
|
155
|
+
setError("");
|
|
156
|
+
try {
|
|
157
|
+
const res = await fetch(`${FUSION_API_PATH}?taskId=${encodeURIComponent(taskId)}`, {
|
|
158
|
+
method: "DELETE",
|
|
159
|
+
credentials: "same-origin"
|
|
160
|
+
});
|
|
161
|
+
if (!res.ok) {
|
|
162
|
+
const data = await res.json().catch(() => null);
|
|
163
|
+
const raw = data?.error || "";
|
|
164
|
+
const msg = raw === "Cannot delete a running task" ? tr(lang, `当前任务仍在运行或尚未安全终止,无法清除:${blockerText(lang, data?.blocker)}`, `Cannot delete while it still runs: ${blockerText(lang, data?.blocker)}`) : raw || tr(lang, "清除任务失败", "Failed to delete task");
|
|
165
|
+
throw new Error(msg);
|
|
166
|
+
}
|
|
167
|
+
setPage((prev) => prev ? {
|
|
168
|
+
...prev,
|
|
169
|
+
totalTasks: Math.max(0, prev.totalTasks - 1),
|
|
170
|
+
tasks: prev.tasks.filter((task) => task.id !== taskId)
|
|
171
|
+
} : prev);
|
|
172
|
+
setConfirmingId(null);
|
|
173
|
+
setRowError(null);
|
|
174
|
+
if (cursor === taskId) setCursor("");
|
|
175
|
+
setRefresh((v) => v + 1);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
setRowError({
|
|
178
|
+
id: taskId,
|
|
179
|
+
message: err instanceof Error ? err.message : String(err)
|
|
180
|
+
});
|
|
181
|
+
} finally {
|
|
182
|
+
setDeletingId(null);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
186
|
+
className: "fusion-history",
|
|
187
|
+
"aria-label": tr(lang, "Fusion 历史与统计", "Fusion history and statistics"),
|
|
188
|
+
"data-ud-check": "fusion-history",
|
|
189
|
+
...open ? { open: true } : {},
|
|
190
|
+
onToggle: (event) => {
|
|
191
|
+
if (!event.target || event.target === event.currentTarget) setOpen(event.currentTarget.open);
|
|
192
|
+
},
|
|
193
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", {
|
|
194
|
+
className: "fusion-history-summary",
|
|
195
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
196
|
+
className: "fusion-history-title",
|
|
197
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: tr(lang, "历史与统计", "History and statistics") })
|
|
198
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
199
|
+
className: "fusion-history-indicator",
|
|
200
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: open ? tr(lang, "点击收起", "Collapse") : tr(lang, "点击展开", "Expand") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
201
|
+
className: "fusion-history-chevron",
|
|
202
|
+
"aria-hidden": "true"
|
|
203
|
+
})]
|
|
204
|
+
})]
|
|
205
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
206
|
+
className: "fusion-history-content",
|
|
207
|
+
children: [
|
|
208
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
209
|
+
className: "fusion-history-header",
|
|
210
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: tr(lang, "按实际模型请求汇总,包含任务、压缩、保活及已有标题请求。未报告的用量不算作 0;金额与节省率尚无可靠数据。", "Totals of actual model requests: tasks, compaction, keepalive and title requests. Unreported usage is not counted as 0; no reliable billed amount or saving rate is available.") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
211
|
+
disabled: loading,
|
|
212
|
+
onClick: () => setRefresh((value) => value + 1),
|
|
213
|
+
children: tr(lang, "刷新记录", "Refresh")
|
|
214
|
+
})]
|
|
215
|
+
}),
|
|
216
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
217
|
+
role: "alert",
|
|
218
|
+
children: error
|
|
219
|
+
}),
|
|
220
|
+
loading && !page && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
221
|
+
role: "status",
|
|
222
|
+
children: tr(lang, "正在读取记录…", "Loading…")
|
|
223
|
+
}),
|
|
224
|
+
page && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
225
|
+
className: "fusion-history-panel fusion-history-usage",
|
|
226
|
+
"aria-label": tr(lang, "用量统计", "Usage statistics"),
|
|
227
|
+
children: [
|
|
228
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: tr(lang, "用量统计", "Usage statistics") }),
|
|
229
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: tr(lang, `${page.totalTasks} 项任务 · ${page.totals.calls} 次原生请求 · ${page.totals.final} 次用量定稿 · ${page.totals.provisional} 次暂报 · ${page.totals.unreported} 次未提供`, `${page.totalTasks} tasks · ${page.totals.calls} native requests · ${page.totals.final} final usage · ${page.totals.provisional} provisional · ${page.totals.unreported} unreported`) }),
|
|
230
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
|
|
231
|
+
tr(lang, "未缓存输入", "Uncached input"),
|
|
232
|
+
" ",
|
|
233
|
+
tokens$1(lang, page.totals.input, page.totals.calls),
|
|
234
|
+
" · ",
|
|
235
|
+
tr(lang, "缓存读取", "cache read"),
|
|
236
|
+
" ",
|
|
237
|
+
tokens$1(lang, page.totals.cacheRead, page.totals.calls),
|
|
238
|
+
" · ",
|
|
239
|
+
tr(lang, "总输出", "Total output"),
|
|
240
|
+
" ",
|
|
241
|
+
outputTokens(lang, page.totals, page.totals.calls),
|
|
242
|
+
" Token"
|
|
243
|
+
] }),
|
|
244
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
|
|
245
|
+
tr(lang, `Lead 接手改代码 ${page.takeovers?.total ?? 0} 次(${page.takeovers?.tasks ?? 0} 项任务)`, `Lead takeovers: ${page.takeovers?.total ?? 0} (${page.takeovers?.tasks ?? 0} tasks)`),
|
|
246
|
+
page.takeovers?.total ? `: ${Object.entries(page.takeovers.reasons).map(([reason, count]) => `${takeoverReason(lang, reason)} ${count}`).join(", ")}` : "",
|
|
247
|
+
tr(lang, "。只有 Sidekick 确实完成不了时,程序才会解锁接手。", ". The Host unlocks a takeover only when the Sidekick demonstrably cannot finish.")
|
|
248
|
+
] }),
|
|
249
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
250
|
+
style: { overflowX: "auto" },
|
|
251
|
+
tabIndex: 0,
|
|
252
|
+
"aria-label": tr(lang, "按实际模型统计", "By actual model"),
|
|
253
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", { children: [
|
|
254
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("caption", { children: tr(lang, "按角色、实际模型和请求用途", "By role, actual model and purpose") }),
|
|
255
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
256
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
257
|
+
scope: "col",
|
|
258
|
+
children: tr(lang, "角色 / 模型", "Role / model")
|
|
259
|
+
}),
|
|
260
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
261
|
+
scope: "col",
|
|
262
|
+
children: tr(lang, "用途", "Purpose")
|
|
263
|
+
}),
|
|
264
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
265
|
+
scope: "col",
|
|
266
|
+
children: tr(lang, "请求", "Requests")
|
|
267
|
+
}),
|
|
268
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
269
|
+
scope: "col",
|
|
270
|
+
children: tr(lang, "总输出 Token(可见 / 推理)", "Total output tokens (visible / reasoning)")
|
|
271
|
+
})
|
|
272
|
+
] }) }),
|
|
273
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: page.groups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
274
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("th", {
|
|
275
|
+
scope: "row",
|
|
276
|
+
children: [
|
|
277
|
+
group.role === "lead" ? "Lead" : "Sidekick",
|
|
278
|
+
" · ",
|
|
279
|
+
group.model,
|
|
280
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: group.provider })
|
|
281
|
+
]
|
|
282
|
+
}),
|
|
283
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: purpose(lang, group.purpose) }),
|
|
284
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: group.calls }),
|
|
285
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: outputTokens(lang, group, group.calls) })
|
|
286
|
+
] }, JSON.stringify([
|
|
287
|
+
group.role,
|
|
288
|
+
group.provider,
|
|
289
|
+
group.model,
|
|
290
|
+
group.purpose
|
|
291
|
+
]))) })
|
|
292
|
+
] })
|
|
293
|
+
}),
|
|
294
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: tr(lang, `更新于 ${new Date(page.observedAt).toLocaleString()}。供应商内部重试次数未知。`, `Updated ${new Date(page.observedAt).toLocaleString()}. Provider-internal retries are unknown.`) }) })
|
|
295
|
+
]
|
|
296
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
297
|
+
className: "fusion-history-panel fusion-history-tasks",
|
|
298
|
+
...tasksOpen ? { open: true } : {},
|
|
299
|
+
onToggle: (event) => setTasksOpen(event.currentTarget.open),
|
|
300
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", {
|
|
301
|
+
className: "fusion-history-tasks-summary",
|
|
302
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
303
|
+
className: "fusion-history-tasks-title",
|
|
304
|
+
children: [
|
|
305
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: tr(lang, "任务记录", "Tasks") }),
|
|
306
|
+
" ",
|
|
307
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
308
|
+
className: "fusion-history-count",
|
|
309
|
+
children: [
|
|
310
|
+
"(",
|
|
311
|
+
page.totalTasks,
|
|
312
|
+
")"
|
|
313
|
+
]
|
|
314
|
+
})
|
|
315
|
+
]
|
|
316
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
317
|
+
className: "fusion-history-tasks-indicator",
|
|
318
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: tasksOpen ? tr(lang, "点击收起", "Collapse") : tr(lang, "点击展开", "Expand") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
319
|
+
className: "fusion-history-tasks-chevron",
|
|
320
|
+
"aria-hidden": "true"
|
|
321
|
+
})]
|
|
322
|
+
})]
|
|
323
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
324
|
+
className: "fusion-history-tasks-content",
|
|
325
|
+
children: [
|
|
326
|
+
!page.tasks.length && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: tr(lang, "还没有 Fusion 任务。", "No Fusion tasks yet.") }),
|
|
327
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", { children: page.tasks.map((task) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
|
|
328
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
329
|
+
className: "fusion-history-item-header",
|
|
330
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: task.title }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("small", { children: [
|
|
331
|
+
new Date(task.createdAt).toLocaleString(),
|
|
332
|
+
" · ",
|
|
333
|
+
task.mode === "completed" ? tr(lang, "已结束", "finished") : task.phase,
|
|
334
|
+
" · ",
|
|
335
|
+
tr(lang, `${task.usage.calls} 次请求`, `${task.usage.calls} requests`),
|
|
336
|
+
task.takeovers ? tr(lang, ` · Lead 接手 ${task.takeovers} 次`, ` · ${task.takeovers} Lead takeovers`) : ""
|
|
337
|
+
] })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
338
|
+
className: "fusion-history-actions",
|
|
339
|
+
children: confirmingId === task.id ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
340
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
341
|
+
className: "fusion-history-confirm-text",
|
|
342
|
+
children: tr(lang, "确定清除?", "Delete?")
|
|
343
|
+
}),
|
|
344
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
345
|
+
type: "button",
|
|
346
|
+
disabled: Boolean(deletingId),
|
|
347
|
+
onClick: () => void handleDelete(task.id),
|
|
348
|
+
children: deletingId === task.id ? tr(lang, "正在清除…", "Deleting…") : tr(lang, "确认", "Confirm")
|
|
349
|
+
}),
|
|
350
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
351
|
+
type: "button",
|
|
352
|
+
className: "fusion-btn-subtle",
|
|
353
|
+
disabled: Boolean(deletingId),
|
|
354
|
+
onClick: () => {
|
|
355
|
+
setConfirmingId(null);
|
|
356
|
+
setRowError(null);
|
|
357
|
+
},
|
|
358
|
+
children: tr(lang, "取消", "Cancel")
|
|
359
|
+
})
|
|
360
|
+
] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
361
|
+
type: "button",
|
|
362
|
+
disabled: Boolean(deletingId),
|
|
363
|
+
onClick: () => {
|
|
364
|
+
setError("");
|
|
365
|
+
setRowError(null);
|
|
366
|
+
setConfirmingId(task.id);
|
|
367
|
+
},
|
|
368
|
+
"aria-label": tr(lang, `清除任务 ${task.title}`, `Delete task ${task.title}`),
|
|
369
|
+
children: tr(lang, "清除记录", "Clear")
|
|
370
|
+
})
|
|
371
|
+
})]
|
|
372
|
+
}),
|
|
373
|
+
rowError?.id === task.id && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
374
|
+
role: "alert",
|
|
375
|
+
className: "fusion-history-row-error",
|
|
376
|
+
style: {
|
|
377
|
+
color: "var(--dsw-alias-state-error-primary, #e5484d)",
|
|
378
|
+
margin: "6px 0 0",
|
|
379
|
+
fontSize: "12px"
|
|
380
|
+
},
|
|
381
|
+
children: rowError.message
|
|
382
|
+
}),
|
|
383
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", { children: [
|
|
384
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: tr(lang, "记录详情", "Details") }),
|
|
385
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
|
|
386
|
+
tr(lang, "任务", "Task"),
|
|
387
|
+
": ",
|
|
388
|
+
task.id
|
|
389
|
+
] }),
|
|
390
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
|
|
391
|
+
tr(lang, "会话", "Session"),
|
|
392
|
+
": ",
|
|
393
|
+
task.sessionId
|
|
394
|
+
] }),
|
|
395
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: tr(lang, `验证状态:${task.verification}。历史运行状态不代表当前仍在执行。`, `Verification: ${task.verification}. A recorded state does not mean it is still running.`) }),
|
|
396
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
|
|
397
|
+
tr(lang, "输入", "Input"),
|
|
398
|
+
" ",
|
|
399
|
+
tokens$1(lang, task.usage.input, task.usage.calls),
|
|
400
|
+
" · ",
|
|
401
|
+
tr(lang, "缓存读取", "cache read"),
|
|
402
|
+
" ",
|
|
403
|
+
tokens$1(lang, task.usage.cacheRead, task.usage.calls),
|
|
404
|
+
" · ",
|
|
405
|
+
tr(lang, "总输出", "Total output"),
|
|
406
|
+
" ",
|
|
407
|
+
outputTokens(lang, task.usage, task.usage.calls),
|
|
408
|
+
" Token"
|
|
409
|
+
] })
|
|
410
|
+
] })
|
|
411
|
+
] }, task.id)) }),
|
|
412
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
413
|
+
className: "fusion-actions",
|
|
414
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
415
|
+
disabled: loading || !cursor,
|
|
416
|
+
onClick: () => setCursor(""),
|
|
417
|
+
children: tr(lang, "最新记录", "Newest")
|
|
418
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
419
|
+
disabled: loading || !page.nextCursor,
|
|
420
|
+
onClick: () => setCursor(page.nextCursor),
|
|
421
|
+
children: tr(lang, "更早记录", "Older")
|
|
422
|
+
})]
|
|
423
|
+
})
|
|
424
|
+
]
|
|
425
|
+
})]
|
|
426
|
+
})] })
|
|
427
|
+
]
|
|
428
|
+
})]
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
//#endregion
|
|
432
|
+
//#region src/client/FusionCache.tsx
|
|
433
|
+
const tokens = (value) => value >= 1e6 ? `${(value / 1e6).toFixed(1)}M` : value >= 1e3 ? `${Math.round(value / 1e3)}k` : String(value);
|
|
434
|
+
const minutes = (seconds) => seconds % 60 ? `${(seconds / 60).toFixed(1)} min` : `${seconds / 60} min`;
|
|
435
|
+
const modeName = (lang, mode) => ({
|
|
436
|
+
auto: tr(lang, "自动", "Auto"),
|
|
437
|
+
on: tr(lang, "开启", "On"),
|
|
438
|
+
off: tr(lang, "关闭", "Off")
|
|
439
|
+
})[mode];
|
|
440
|
+
function modeSource(lang, row) {
|
|
441
|
+
switch (row.modeSource) {
|
|
442
|
+
case "user": return tr(lang, "你的设置", "your setting");
|
|
443
|
+
case "pair": return tr(lang, "旧版组合设置", "older pair setting");
|
|
444
|
+
case "role": return tr(lang, "Sidekick 默认关闭(它很少长时间等待)", "Sidekick default off (it rarely waits long)");
|
|
445
|
+
case "route": return `${row.defaults.route.label}: ${lang === "zh" ? row.defaults.route.reason.zh : row.defaults.route.reason.en}`;
|
|
446
|
+
case "family": return tr(lang, `${row.defaults.family.label} 官方默认`, `${row.defaults.family.label} documented default`);
|
|
447
|
+
default: return tr(lang, "通用默认", "generic default");
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function intervalSource(lang, row) {
|
|
451
|
+
if (row.intervalSource === "user") return tr(lang, "你的设置", "your setting");
|
|
452
|
+
if (row.intervalSource === "learned") return tr(lang, "自动学习:保活多次未命中,已缩短", "learned: pings kept missing, shortened");
|
|
453
|
+
return tr(lang, "默认", "default");
|
|
454
|
+
}
|
|
455
|
+
function ModelCard({ row, onSaved }) {
|
|
456
|
+
const lang = useLang();
|
|
457
|
+
const [interval, setInterval] = (0, react.useState)(String(row.setting.intervalSeconds ? row.setting.intervalSeconds / 60 : ""));
|
|
458
|
+
const [busy, setBusy] = (0, react.useState)(false), [error, setError] = (0, react.useState)();
|
|
459
|
+
const save = async (body) => {
|
|
460
|
+
setBusy(true);
|
|
461
|
+
setError(void 0);
|
|
462
|
+
try {
|
|
463
|
+
const response = await fetch(FUSION_API_PATH, {
|
|
464
|
+
method: "POST",
|
|
465
|
+
credentials: "same-origin",
|
|
466
|
+
headers: { "content-type": "application/json" },
|
|
467
|
+
body: JSON.stringify({
|
|
468
|
+
action: "cache-setting",
|
|
469
|
+
provider: row.provider,
|
|
470
|
+
model: row.model,
|
|
471
|
+
...body
|
|
472
|
+
})
|
|
473
|
+
});
|
|
474
|
+
const result = await response.json();
|
|
475
|
+
if (!response.ok) throw new Error(result.error ?? tr(lang, "保存失败", "Save failed"));
|
|
476
|
+
onSaved();
|
|
477
|
+
} catch (reason) {
|
|
478
|
+
setError(reason instanceof Error ? reason.message : String(reason));
|
|
479
|
+
} finally {
|
|
480
|
+
setBusy(false);
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
const t = row.totals;
|
|
484
|
+
const family = row.defaults.family;
|
|
485
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
486
|
+
className: "fusion-cache-model",
|
|
487
|
+
children: [
|
|
488
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("strong", { children: [row.model, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("small", { children: [row.provider, row.role ? ` · ${row.role === "lead" ? "Lead" : "Sidekick"}` : ""] })] }),
|
|
489
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
|
|
490
|
+
tr(lang, "现在", "Now"),
|
|
491
|
+
": ",
|
|
492
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: modeName(lang, row.mode) }),
|
|
493
|
+
row.mode !== "off" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
494
|
+
tr(lang, ",每 ", ", every "),
|
|
495
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: minutes(row.intervalSeconds) }),
|
|
496
|
+
tr(lang, " 续一次", "")
|
|
497
|
+
] }),
|
|
498
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("small", { children: [
|
|
499
|
+
tr(lang, "开关来源", "Mode"),
|
|
500
|
+
": ",
|
|
501
|
+
modeSource(lang, row),
|
|
502
|
+
" · ",
|
|
503
|
+
tr(lang, "间隔来源", "Interval"),
|
|
504
|
+
": ",
|
|
505
|
+
intervalSource(lang, row)
|
|
506
|
+
] })
|
|
507
|
+
] }),
|
|
508
|
+
family && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("small", { children: [
|
|
509
|
+
tr(lang, "官方文档", "Documented"),
|
|
510
|
+
": ",
|
|
511
|
+
lang === "zh" ? family.lifetime.zh : family.lifetime.en,
|
|
512
|
+
";",
|
|
513
|
+
tr(lang, "缓存价约为全价", "cached price ≈"),
|
|
514
|
+
" ",
|
|
515
|
+
family.discount,
|
|
516
|
+
" · ",
|
|
517
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
518
|
+
href: family.docs,
|
|
519
|
+
target: "_blank",
|
|
520
|
+
rel: "noreferrer",
|
|
521
|
+
children: tr(lang, "来源", "source")
|
|
522
|
+
})
|
|
523
|
+
] }) }),
|
|
524
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
525
|
+
className: "fusion-cache-controls",
|
|
526
|
+
children: [
|
|
527
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [tr(lang, "保活", "Keepalive"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
528
|
+
"aria-label": `${row.model} keepalive`,
|
|
529
|
+
disabled: busy,
|
|
530
|
+
value: row.setting.mode ?? "",
|
|
531
|
+
onChange: (event) => {
|
|
532
|
+
save({
|
|
533
|
+
mode: event.target.value || null,
|
|
534
|
+
intervalSeconds: row.setting.intervalSeconds ?? null
|
|
535
|
+
});
|
|
536
|
+
},
|
|
537
|
+
children: [
|
|
538
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
539
|
+
value: "",
|
|
540
|
+
children: tr(lang, `默认(${modeName(lang, row.defaults.mode)})`, `Default (${modeName(lang, row.defaults.mode)})`)
|
|
541
|
+
}),
|
|
542
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
543
|
+
value: "auto",
|
|
544
|
+
children: tr(lang, "自动:该模型返回过缓存命中后才开启", "Auto: only after this model reports cache hits")
|
|
545
|
+
}),
|
|
546
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
547
|
+
value: "on",
|
|
548
|
+
children: tr(lang, "开启", "On")
|
|
549
|
+
}),
|
|
550
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
551
|
+
value: "off",
|
|
552
|
+
children: tr(lang, "关闭(按请求次数计费的套餐选这个)", "Off (choose for plans billed per request)")
|
|
553
|
+
})
|
|
554
|
+
]
|
|
555
|
+
})] }),
|
|
556
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [tr(lang, "间隔(分钟)", "Interval (min)"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
557
|
+
type: "number",
|
|
558
|
+
min: "1",
|
|
559
|
+
max: "59",
|
|
560
|
+
step: "0.5",
|
|
561
|
+
disabled: busy,
|
|
562
|
+
value: interval,
|
|
563
|
+
placeholder: String(+(row.intervalSeconds / 60).toFixed(2)),
|
|
564
|
+
onChange: (event) => setInterval(event.target.value),
|
|
565
|
+
onBlur: () => {
|
|
566
|
+
if (interval !== String(row.setting.intervalSeconds ? row.setting.intervalSeconds / 60 : "")) save({
|
|
567
|
+
mode: row.setting.mode ?? null,
|
|
568
|
+
intervalSeconds: interval ? Math.round(Number(interval) * 60) : null
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
})] }),
|
|
572
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
573
|
+
type: "button",
|
|
574
|
+
disabled: busy || !row.setting.mode && !row.setting.intervalSeconds,
|
|
575
|
+
onClick: () => {
|
|
576
|
+
setInterval("");
|
|
577
|
+
save({ reset: true });
|
|
578
|
+
},
|
|
579
|
+
children: tr(lang, "恢复默认", "Reset")
|
|
580
|
+
})
|
|
581
|
+
]
|
|
582
|
+
}),
|
|
583
|
+
row.suggestion && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
584
|
+
role: "status",
|
|
585
|
+
children: tr(lang, `观察到等待 ${minutes(row.suggestion)} 以上缓存仍然命中,可以把间隔放宽到 ${minutes(row.suggestion)}(不会自动修改)。`, `Cache still hit after waits over ${minutes(row.suggestion)}; you can lengthen the interval to ${minutes(row.suggestion)} (not changed automatically).`)
|
|
586
|
+
}),
|
|
587
|
+
t ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
|
|
588
|
+
tr(lang, "效果", "Effect"),
|
|
589
|
+
": ",
|
|
590
|
+
tr(lang, `等待后命中 ${t.waitHits}/${t.waits} 次`, `hit after ${t.waitHits}/${t.waits} waits`),
|
|
591
|
+
t.waits ? ` (${Math.round(100 * t.waitHits / t.waits)}%)` : "",
|
|
592
|
+
";",
|
|
593
|
+
tr(lang, ` 保活 ${t.pings} 次(${t.pingHits} 次命中,读取 ${tokens(t.pingTokens)} token,多为缓存价${t.pingOutputTokens != null ? `,输出 ${tokens(t.pingOutputTokens)} token` : ""});`, ` ${t.pings} pings (${t.pingHits} hit, ${tokens(t.pingTokens)} tokens read, mostly at cache price${t.pingOutputTokens != null ? `, ${tokens(t.pingOutputTokens)} output` : ""}); `),
|
|
594
|
+
tr(lang, ` 等待后仍从缓存读取 ${tokens(t.keptWarmTokens)} token,按全价重发 ${tokens(t.resentTokens)} token。`, ` after waits ${tokens(t.keptWarmTokens)} tokens served from cache, ${tokens(t.resentTokens)} resent at full price.`),
|
|
595
|
+
t.pings >= 5 && t.pingHits / t.pings < .5 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: tr(lang, " 保活大多没命中,建议关闭或缩短间隔。", " Most pings missed: turn it off or shorten the interval.") })
|
|
596
|
+
] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: tr(lang, "还没有数据:Lead 等待 Sidekick 之后才会产生记录。", "No data yet: records appear after the Lead waits for the Sidekick.") }),
|
|
597
|
+
!!row.recent.length && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("small", { children: [
|
|
598
|
+
tr(lang, "最近", "Recent"),
|
|
599
|
+
": ",
|
|
600
|
+
row.recent.map((item, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
601
|
+
item.ping ? "↻" : "",
|
|
602
|
+
minutes(item.gapSeconds),
|
|
603
|
+
item.hit ? " ✓" : " ✗",
|
|
604
|
+
i < row.recent.length - 1 ? " · " : ""
|
|
605
|
+
] }, i))
|
|
606
|
+
] }) }),
|
|
607
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
608
|
+
role: "alert",
|
|
609
|
+
children: error
|
|
610
|
+
})
|
|
611
|
+
]
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
/** Per-model cache keepalive: what applies and why, the documented default, and what was observed. */
|
|
615
|
+
function FusionCache({ refresh }) {
|
|
616
|
+
const lang = useLang();
|
|
617
|
+
const [rows, setRows] = (0, react.useState)(), [checked, setChecked] = (0, react.useState)(), [error, setError] = (0, react.useState)();
|
|
618
|
+
const [reload, setReload] = (0, react.useState)(0);
|
|
619
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
620
|
+
(0, react.useEffect)(() => {
|
|
621
|
+
const abort = new AbortController();
|
|
622
|
+
fetch(`${FUSION_API_PATH}?view=cache`, {
|
|
623
|
+
credentials: "same-origin",
|
|
624
|
+
cache: "no-store",
|
|
625
|
+
signal: abort.signal
|
|
626
|
+
}).then(async (response) => {
|
|
627
|
+
if (!response.ok) throw new Error(tr(lang, "无法读取缓存设置", "Cannot read cache settings"));
|
|
628
|
+
return response.json();
|
|
629
|
+
}).then((value) => {
|
|
630
|
+
setRows(value.models);
|
|
631
|
+
setChecked(value.checked);
|
|
632
|
+
}).catch((reason) => {
|
|
633
|
+
if (!abort.signal.aborted) setError(String(reason));
|
|
634
|
+
});
|
|
635
|
+
return () => abort.abort();
|
|
636
|
+
}, [
|
|
637
|
+
refresh,
|
|
638
|
+
reload,
|
|
639
|
+
lang
|
|
640
|
+
]);
|
|
641
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
642
|
+
className: "fusion-cache",
|
|
643
|
+
"aria-label": tr(lang, "缓存保活", "Cache keepalive"),
|
|
644
|
+
"data-ud-check": "fusion-cache",
|
|
645
|
+
...open ? { open: true } : {},
|
|
646
|
+
onToggle: (event) => {
|
|
647
|
+
if (!event.target || event.target === event.currentTarget) setOpen(event.currentTarget.open);
|
|
648
|
+
},
|
|
649
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", {
|
|
650
|
+
className: "fusion-cache-summary",
|
|
651
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
652
|
+
className: "fusion-cache-title",
|
|
653
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: tr(lang, "缓存保活", "Cache keepalive") })
|
|
654
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
655
|
+
className: "fusion-cache-indicator",
|
|
656
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: open ? tr(lang, "点击收起", "Collapse") : tr(lang, "点击展开", "Expand") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
657
|
+
className: "fusion-cache-chevron",
|
|
658
|
+
"aria-hidden": "true"
|
|
659
|
+
})]
|
|
660
|
+
})]
|
|
661
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
662
|
+
className: "fusion-cache-content",
|
|
663
|
+
children: [
|
|
664
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: tr(lang, "Lead 等 Sidekick 干活时可能要等几分钟,模型的输入缓存过期后,下一次请求会按全价重读整段对话。保活在等待期间定时把 Lead 上一次的请求原样再发一遍,末尾只加一句“Reply OK”,模型只回一个 OK,缓存就不会过期。默认值来自各家官方文档,每个模型都可以单独调整。", "While the Lead waits for the Sidekick, the model's prompt cache can expire and the next request re-reads the whole conversation at full price. Keepalive resends the Lead's previous request at intervals with only “Reply OK” appended; the model answers OK and the cache stays warm. Defaults come from each provider's documentation; every model can be adjusted.") }),
|
|
665
|
+
checked && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: tr(lang, `官方默认值核对日期:${checked}。`, `Documented defaults checked on ${checked}.`) }) }),
|
|
666
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
667
|
+
role: "alert",
|
|
668
|
+
children: error
|
|
669
|
+
}),
|
|
670
|
+
rows?.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelCard, {
|
|
671
|
+
row,
|
|
672
|
+
onSaved: () => setReload((value) => value + 1)
|
|
673
|
+
}, `${row.provider}/${row.model}/${row.setting.mode ?? ""}/${row.setting.intervalSeconds ?? ""}`))
|
|
674
|
+
]
|
|
675
|
+
})]
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
//#endregion
|
|
679
|
+
//#region src/client/settings-events.ts
|
|
680
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
681
|
+
const onSettingsChanged = (listener) => {
|
|
682
|
+
listeners.add(listener);
|
|
683
|
+
return () => {
|
|
684
|
+
listeners.delete(listener);
|
|
685
|
+
};
|
|
686
|
+
};
|
|
687
|
+
const settingsChanged = () => {
|
|
688
|
+
for (const listener of listeners) listener();
|
|
689
|
+
};
|
|
690
|
+
//#endregion
|
|
691
|
+
//#region src/client/FusionSettings.tsx
|
|
692
|
+
const styles$1 = `
|
|
693
|
+
.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
|
+
.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
|
+
.fusion-settings h2{margin:0;font-size:22px;line-height:30px;font-weight:600}
|
|
696
|
+
.fusion-settings p{margin:0;font-size:13px;line-height:21px;color:var(--dsw-alias-label-secondary)}
|
|
697
|
+
.fusion-settings header{display:flex;flex-direction:column;gap:8px;padding-top:28px}
|
|
698
|
+
[data-platform='darwin'] .fusion-settings header{padding-top:calc(28px + var(--dsh-frame-top-clearance,0px))}
|
|
699
|
+
.fusion-settings .fusion-card{padding:20px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;display:flex;flex-direction:column;gap:20px;background:var(--dsw-alias-bg-module-platform)}
|
|
700
|
+
.fusion-settings fieldset{padding:0;margin:0;border:0;min-inline-size:0;display:flex;flex-direction:column;gap:10px}
|
|
701
|
+
.fusion-settings legend{font-size:15px;font-weight:600;padding:0 0 4px}
|
|
702
|
+
.fusion-settings label{display:grid;grid-template-columns:85px minmax(0,1fr);align-items:center;gap:12px;font-size:13px}
|
|
703
|
+
.fusion-settings :is(select,option,optgroup){background-color:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);color-scheme:inherit}
|
|
704
|
+
.fusion-settings select{box-sizing:border-box;min-width:0;width:100%;min-height:36px;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;padding:5px 9px;font:inherit}
|
|
705
|
+
.fusion-settings .fusion-actions{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
|
|
706
|
+
.fusion-settings button{border:1px solid var(--dsw-alias-border-l2);border-radius:18px;background:var(--dsw-alias-button-primary-fill,var(--dsw-alias-brand-primary));color:var(--dsw-alias-label-primary-foreground,#fff);padding:7px 18px;font:inherit;font-size:13px;cursor:pointer}
|
|
707
|
+
.fusion-settings button:disabled{opacity:.5;cursor:default}
|
|
708
|
+
.fusion-settings :is(button,select):focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
|
|
709
|
+
.fusion-settings [role=alert]{color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere}
|
|
710
|
+
.fusion-settings details{font-size:13px;line-height:21px}
|
|
711
|
+
.fusion-settings summary{cursor:pointer;font-weight:500}
|
|
712
|
+
.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}
|
|
714
|
+
.fusion-settings .fusion-consent{display:flex;align-items:flex-start;gap:8px;font-size:12px;line-height:18px}
|
|
715
|
+
.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
|
+
.fusion-cache h3{margin:0}
|
|
717
|
+
.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}
|
|
718
|
+
.fusion-cache-summary::-webkit-details-marker{display:none}
|
|
719
|
+
.fusion-cache-summary:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;border-radius:4px}
|
|
720
|
+
.fusion-cache-title{display:inline-flex;align-items:center;gap:6px}
|
|
721
|
+
.fusion-cache-title h3{margin:0;font-size:15px;font-weight:600}
|
|
722
|
+
.fusion-cache-indicator{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:400;color:var(--dsw-alias-label-secondary)}
|
|
723
|
+
.fusion-cache-chevron{display:inline-block;width:7px;height:7px;border-right:2px solid var(--dsw-alias-label-secondary);border-bottom:2px solid var(--dsw-alias-label-secondary);transform:rotate(-45deg);transition:transform .15s ease;margin-left:2px}
|
|
724
|
+
.fusion-cache[open] .fusion-cache-chevron{transform:rotate(45deg)}
|
|
725
|
+
.fusion-cache-content{display:flex;flex-direction:column;gap:12px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l2);margin-top:4px}
|
|
726
|
+
.fusion-cache-model{display:flex;flex-direction:column;gap:8px;padding:14px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px}
|
|
727
|
+
.fusion-cache-model small{display:block;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}
|
|
728
|
+
.fusion-cache-controls{display:grid;grid-template-columns:minmax(0,2fr) minmax(0,1fr) auto;gap:10px;align-items:end}
|
|
729
|
+
.fusion-cache-controls label{grid-template-columns:1fr;gap:4px}
|
|
730
|
+
.fusion-cache a{color:var(--dsw-alias-brand-primary)}
|
|
731
|
+
@media(max-width:560px){.fusion-cache-controls{grid-template-columns:1fr}}
|
|
732
|
+
.fusion-history{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}
|
|
733
|
+
.fusion-history-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}
|
|
734
|
+
.fusion-history-summary::-webkit-details-marker{display:none}
|
|
735
|
+
.fusion-history-summary:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;border-radius:4px}
|
|
736
|
+
.fusion-history-title{display:inline-flex;align-items:center;gap:6px}
|
|
737
|
+
.fusion-history-title h3{margin:0;font-size:15px;font-weight:600}
|
|
738
|
+
.fusion-history-indicator{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:400;color:var(--dsw-alias-label-secondary)}
|
|
739
|
+
.fusion-history-chevron{display:inline-block;width:7px;height:7px;border-right:2px solid var(--dsw-alias-label-secondary);border-bottom:2px solid var(--dsw-alias-label-secondary);transform:rotate(-45deg);transition:transform .15s ease;margin-left:2px}
|
|
740
|
+
.fusion-history[open] .fusion-history-chevron{transform:rotate(45deg)}
|
|
741
|
+
.fusion-history-content{display:flex;flex-direction:column;gap:12px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l2);margin-top:4px}
|
|
742
|
+
.fusion-history-header{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap}
|
|
743
|
+
.fusion-history-panel{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}
|
|
744
|
+
.fusion-history-usage h3{margin:0;font-size:14px;font-weight:600}
|
|
745
|
+
.fusion-history table{width:100%;border-collapse:collapse;font-size:12px;text-align:left}
|
|
746
|
+
.fusion-history :is(th,td){padding:8px;border-bottom:1px solid var(--dsw-alias-border-l2);font-weight:400;vertical-align:top}
|
|
747
|
+
.fusion-history caption{text-align:left;font-size:13px;margin-bottom:8px}
|
|
748
|
+
.fusion-history small{display:block;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}
|
|
749
|
+
.fusion-history ol{padding-left:20px;margin:0}
|
|
750
|
+
.fusion-history li{padding:10px 0;border-bottom:1px solid var(--dsw-alias-border-l2)}
|
|
751
|
+
.fusion-history li:last-child{border-bottom:0}
|
|
752
|
+
.fusion-history h3{margin:0;font-size:14px;font-weight:600}
|
|
753
|
+
.fusion-history p{overflow-wrap:anywhere}
|
|
754
|
+
.fusion-history-tasks-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}
|
|
755
|
+
.fusion-history-tasks-summary::-webkit-details-marker{display:none}
|
|
756
|
+
.fusion-history-tasks-summary:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;border-radius:4px}
|
|
757
|
+
.fusion-history-tasks-title{display:inline-flex;align-items:center;gap:6px}
|
|
758
|
+
.fusion-history-count{font-size:13px;font-weight:400;color:var(--dsw-alias-label-secondary)}
|
|
759
|
+
.fusion-history-tasks-indicator{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:400;color:var(--dsw-alias-label-secondary)}
|
|
760
|
+
.fusion-history-tasks-chevron{display:inline-block;width:7px;height:7px;border-right:2px solid var(--dsw-alias-label-secondary);border-bottom:2px solid var(--dsw-alias-label-secondary);transform:rotate(-45deg);transition:transform .15s ease;margin-left:2px}
|
|
761
|
+
.fusion-history-tasks[open] .fusion-history-tasks-chevron{transform:rotate(45deg)}
|
|
762
|
+
.fusion-history-tasks-content{display:flex;flex-direction:column;gap:12px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l2);margin-top:4px}
|
|
763
|
+
.fusion-history-item-header{display:flex;align-items:center;justify-content:space-between;gap:12px}
|
|
764
|
+
.fusion-history-item-header h3{margin:0;font-size:14px;font-weight:600}
|
|
765
|
+
.fusion-history-actions{display:flex;align-items:center;gap:8px;flex-shrink:0}
|
|
766
|
+
.fusion-history-actions button{padding:3px 12px;font-size:12px;border-radius:14px}
|
|
767
|
+
.fusion-history-actions button.fusion-btn-subtle{background:transparent;color:var(--dsw-alias-label-primary)}
|
|
768
|
+
.fusion-history-confirm-text{font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
769
|
+
@media(max-width:480px){.fusion-page{padding:0 16px 32px}.fusion-settings label{grid-template-columns:1fr;gap:6px}.fusion-settings .fusion-card{padding:16px}}
|
|
770
|
+
`;
|
|
771
|
+
const key = (route) => route ? JSON.stringify([route.provider, route.model]) : "";
|
|
772
|
+
function ModelChoice({ role, route, catalog, onChange }) {
|
|
773
|
+
const lang = useLang();
|
|
774
|
+
const name = role === "compactor" ? tr(lang, "压缩", "Compaction") : role;
|
|
775
|
+
const model = catalog.groups.find((group) => group.id === route?.provider)?.models.find((model) => model.id === route?.model);
|
|
776
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", { children: [
|
|
777
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", { children: name }),
|
|
778
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: role === "Lead" ? tr(lang, "回答问题、制定方案并审查结果。", "Answers, plans and reviews.") : role === "Sidekick" ? tr(lang, "执行命令和修改,接收反馈后继续。", "Runs commands and makes changes, then continues with feedback.") : tr(lang, "整理较早的上下文,供 Lead 或 Worker 继续工作。", "Summarises older context so the Lead or Sidekick can continue.") }),
|
|
779
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [tr(lang, "模型", "Model"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
780
|
+
"aria-label": tr(lang, `${name} 模型`, `${name} model`),
|
|
781
|
+
value: key(route),
|
|
782
|
+
onChange: (event) => {
|
|
783
|
+
if (!event.target.value) {
|
|
784
|
+
onChange(void 0);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
const [provider, model] = JSON.parse(event.target.value);
|
|
788
|
+
onChange({
|
|
789
|
+
provider,
|
|
790
|
+
model
|
|
791
|
+
});
|
|
792
|
+
},
|
|
793
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
794
|
+
value: "",
|
|
795
|
+
disabled: role !== "compactor",
|
|
796
|
+
children: role === "compactor" ? tr(lang, "使用当前角色模型", "Use the role's own model") : tr(lang, "选择已连接的模型", "Choose a connected model")
|
|
797
|
+
}), catalog.groups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("optgroup", {
|
|
798
|
+
label: group.name,
|
|
799
|
+
children: group.models.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
800
|
+
value: key({
|
|
801
|
+
provider: group.id,
|
|
802
|
+
model: model.id
|
|
803
|
+
}),
|
|
804
|
+
children: model.name
|
|
805
|
+
}, model.id))
|
|
806
|
+
}, group.id))]
|
|
807
|
+
})] }),
|
|
808
|
+
model?.reasoning && route && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [tr(lang, "推理强度", "Reasoning"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
809
|
+
"aria-label": tr(lang, `${name} 推理强度`, `${name} reasoning`),
|
|
810
|
+
value: route.reasoningEffort ?? "",
|
|
811
|
+
onChange: (event) => {
|
|
812
|
+
const { reasoningEffort: _old, ...physical } = route;
|
|
813
|
+
onChange({
|
|
814
|
+
...physical,
|
|
815
|
+
...event.target.value ? { reasoningEffort: event.target.value } : {}
|
|
816
|
+
});
|
|
817
|
+
},
|
|
818
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
|
|
819
|
+
value: "",
|
|
820
|
+
children: [tr(lang, "模型默认", "Model default"), model.reasoning.defaultEffort ? ` · ${model.reasoning.defaultEffort}` : ""]
|
|
821
|
+
}), model.reasoning.efforts.map((effort) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
822
|
+
value: effort.id,
|
|
823
|
+
children: effort.name
|
|
824
|
+
}, effort.id))]
|
|
825
|
+
})] })
|
|
826
|
+
] });
|
|
827
|
+
}
|
|
828
|
+
function FusionSettings() {
|
|
829
|
+
const lang = useLang();
|
|
830
|
+
const [state, setState] = (0, react.useState)();
|
|
831
|
+
const [draft, setDraft] = (0, react.useState)({});
|
|
832
|
+
const [error, setError] = (0, react.useState)();
|
|
833
|
+
const [notice, setNotice] = (0, react.useState)();
|
|
834
|
+
const [saving, setSaving] = (0, react.useState)(false);
|
|
835
|
+
const chooseModel = (role, route) => setDraft((value) => ({
|
|
836
|
+
...value,
|
|
837
|
+
[role]: route,
|
|
838
|
+
...value.cacheKeepalive ? { cacheKeepalive: {
|
|
839
|
+
...value.cacheKeepalive,
|
|
840
|
+
[role]: role === "lead" ? "auto" : false
|
|
841
|
+
} } : {}
|
|
842
|
+
}));
|
|
843
|
+
(0, react.useEffect)(() => {
|
|
844
|
+
const abort = new AbortController();
|
|
845
|
+
fetch(`${FUSION_API_PATH}?view=settings`, {
|
|
846
|
+
credentials: "same-origin",
|
|
847
|
+
cache: "no-store",
|
|
848
|
+
signal: abort.signal
|
|
849
|
+
}).then(async (response) => {
|
|
850
|
+
if (!response.ok) throw new Error(tr(lang, "无法读取 Fusion 设置", "Cannot read Fusion settings"));
|
|
851
|
+
return response.json();
|
|
852
|
+
}).then((value) => {
|
|
853
|
+
setState(value);
|
|
854
|
+
setDraft(value.pair ?? {});
|
|
855
|
+
}).catch((error) => {
|
|
856
|
+
if (!abort.signal.aborted) setError(String(error));
|
|
857
|
+
});
|
|
858
|
+
return () => abort.abort();
|
|
859
|
+
}, []);
|
|
860
|
+
const save = async () => {
|
|
861
|
+
if (!state || !draft.lead || !draft.worker) return;
|
|
862
|
+
setSaving(true);
|
|
863
|
+
setError(void 0);
|
|
864
|
+
setNotice(void 0);
|
|
865
|
+
try {
|
|
866
|
+
const { outputTokens, ...rest } = draft;
|
|
867
|
+
const limits = Object.fromEntries(Object.entries(outputTokens ?? {}).filter(([, tokens]) => Number.isSafeInteger(tokens) && Number(tokens) > 0));
|
|
868
|
+
const pair = {
|
|
869
|
+
...rest,
|
|
870
|
+
...Object.keys(limits).length ? { outputTokens: limits } : {}
|
|
871
|
+
};
|
|
872
|
+
const response = await fetch(FUSION_API_PATH, {
|
|
873
|
+
method: "POST",
|
|
874
|
+
credentials: "same-origin",
|
|
875
|
+
headers: { "content-type": "application/json" },
|
|
876
|
+
body: JSON.stringify({
|
|
877
|
+
action: "configure",
|
|
878
|
+
revision: state.revision,
|
|
879
|
+
pair
|
|
880
|
+
})
|
|
881
|
+
});
|
|
882
|
+
const result = await response.json();
|
|
883
|
+
if (!response.ok) throw new Error(result.error ?? tr(lang, "保存失败,请刷新后重试", "Save failed; refresh and try again"));
|
|
884
|
+
const current = await fetch(`${FUSION_API_PATH}?view=settings`, {
|
|
885
|
+
credentials: "same-origin",
|
|
886
|
+
cache: "no-store"
|
|
887
|
+
});
|
|
888
|
+
if (!current.ok) throw new Error(tr(lang, "配置已提交,但暂时无法确认,请刷新", "Saved, but not confirmed yet; refresh"));
|
|
889
|
+
const confirmed = await current.json();
|
|
890
|
+
setState(confirmed);
|
|
891
|
+
setDraft(confirmed.pair ?? {});
|
|
892
|
+
settingsChanged();
|
|
893
|
+
setNotice(tr(lang, "组合已保存并启用。可以在模型菜单中选择 Fusion · 自动。", "Pair saved and enabled. Choose Fusion · auto in the model menu."));
|
|
894
|
+
} catch (error) {
|
|
895
|
+
setError(error instanceof Error ? error.message : String(error));
|
|
896
|
+
} finally {
|
|
897
|
+
setSaving(false);
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
901
|
+
className: "fusion-page",
|
|
902
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: styles$1 }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
903
|
+
className: "fusion-settings",
|
|
904
|
+
children: [
|
|
905
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
906
|
+
"data-window-drag": true,
|
|
907
|
+
children: [
|
|
908
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: "Fusion" }),
|
|
909
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: tr(lang, "选择 Lead 和 Sidekick。Lead 处理日常请求、安排工作并审查结果;Sidekick 执行委派的任务。", "Choose a Lead and a Sidekick. The Lead handles requests, plans and reviews; the Sidekick does the delegated work.") }),
|
|
910
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
|
|
911
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: tr(lang, "搭配建议", "Pairing tip") }),
|
|
912
|
+
": ",
|
|
913
|
+
tr(lang, "Fusion 省钱靠的是 Sidekick 比 Lead 便宜得多。选一个强的前沿模型当 Lead,再选一个价格低很多(最好 5 倍以上)的模型当 Sidekick;两者价格越接近,越省不了钱。", "Fusion saves money because the Sidekick is much cheaper than the Lead. Pick a strong frontier Lead and a Sidekick that costs far less (ideally 5× or more); the closer their prices, the smaller the saving.")
|
|
914
|
+
] })
|
|
915
|
+
]
|
|
916
|
+
}),
|
|
917
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
918
|
+
role: "alert",
|
|
919
|
+
children: error
|
|
920
|
+
}),
|
|
921
|
+
!state && !error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
922
|
+
role: "status",
|
|
923
|
+
children: tr(lang, "正在读取模型…", "Loading models…")
|
|
924
|
+
}),
|
|
925
|
+
state && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
926
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
|
|
927
|
+
className: "fusion-card",
|
|
928
|
+
disabled: saving,
|
|
929
|
+
children: [
|
|
930
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelChoice, {
|
|
931
|
+
role: "Lead",
|
|
932
|
+
route: draft.lead,
|
|
933
|
+
catalog: state.catalog,
|
|
934
|
+
onChange: (route) => {
|
|
935
|
+
if (route) chooseModel("lead", route);
|
|
936
|
+
}
|
|
937
|
+
}),
|
|
938
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelChoice, {
|
|
939
|
+
role: "Sidekick",
|
|
940
|
+
route: draft.worker,
|
|
941
|
+
catalog: state.catalog,
|
|
942
|
+
onChange: (route) => {
|
|
943
|
+
if (route) chooseModel("worker", route);
|
|
944
|
+
}
|
|
945
|
+
}),
|
|
946
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: tr(lang, "上下文压缩模型 · 可选", "Compaction model · optional") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
947
|
+
className: "fusion-limits",
|
|
948
|
+
children: [
|
|
949
|
+
/* @__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
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelChoice, {
|
|
951
|
+
role: "compactor",
|
|
952
|
+
route: draft.compactor?.route,
|
|
953
|
+
catalog: state.catalog,
|
|
954
|
+
onChange: (route) => setDraft((value) => {
|
|
955
|
+
const { compactor, ...rest } = value;
|
|
956
|
+
return route ? {
|
|
957
|
+
...rest,
|
|
958
|
+
compactor: {
|
|
959
|
+
route,
|
|
960
|
+
maxOutputTokens: compactor?.maxOutputTokens ?? 8192
|
|
961
|
+
}
|
|
962
|
+
} : rest;
|
|
963
|
+
})
|
|
964
|
+
}),
|
|
965
|
+
draft.compactor && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [tr(lang, "输出 Token 上限", "Output token limit"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
966
|
+
"aria-label": tr(lang, "压缩输出 Token 上限", "Compaction output token limit"),
|
|
967
|
+
type: "number",
|
|
968
|
+
min: "1",
|
|
969
|
+
max: "128000",
|
|
970
|
+
value: draft.compactor.maxOutputTokens,
|
|
971
|
+
onChange: (event) => {
|
|
972
|
+
const maxOutputTokens = Number(event.target.value);
|
|
973
|
+
setDraft((value) => value.compactor ? {
|
|
974
|
+
...value,
|
|
975
|
+
compactor: {
|
|
976
|
+
...value.compactor,
|
|
977
|
+
maxOutputTokens
|
|
978
|
+
}
|
|
979
|
+
} : value);
|
|
980
|
+
}
|
|
981
|
+
})] }),
|
|
982
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: tr(lang, "已选中的会话仍使用原配置。", "Conversations already using Fusion keep their setup.") })
|
|
983
|
+
]
|
|
984
|
+
})] })
|
|
985
|
+
]
|
|
986
|
+
}),
|
|
987
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
988
|
+
className: "fusion-actions",
|
|
989
|
+
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 || !draft.lead || !draft.worker || JSON.stringify(draft) === JSON.stringify(state.pair),
|
|
991
|
+
onClick: () => {
|
|
992
|
+
save();
|
|
993
|
+
},
|
|
994
|
+
children: saving ? tr(lang, "正在保存…", "Saving…") : tr(lang, "保存组合", "Save pair")
|
|
995
|
+
})]
|
|
996
|
+
}),
|
|
997
|
+
notice && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
998
|
+
role: "status",
|
|
999
|
+
children: notice
|
|
1000
|
+
}),
|
|
1001
|
+
/* @__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
|
+
/* @__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
|
+
state.pair && !state.authorized && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
1004
|
+
role: "status",
|
|
1005
|
+
children: [tr(lang, "组合需要重新保存后才能使用", "Save the pair again before use"), state.reason ? `: ${state.reason}` : ""]
|
|
1006
|
+
}),
|
|
1007
|
+
state.managedAuthorization && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: tr(lang, "运行额度由外部授权配置管理。", "Run limits are managed by an external authorization file.") })
|
|
1008
|
+
] }),
|
|
1009
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FusionCache, { refresh: state?.revision ?? 0 }),
|
|
1010
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FusionHistory, {})
|
|
1011
|
+
]
|
|
1012
|
+
})]
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
//#endregion
|
|
1016
|
+
//#region src/client/FusionPanelIcon.tsx
|
|
1017
|
+
/**
|
|
1018
|
+
* Compact, accessible icon for the Fusion sidebar entry.
|
|
1019
|
+
* Renders a dual-node convergence (Lead + Sidekick models fusing into one) SVG glyph.
|
|
1020
|
+
*/
|
|
1021
|
+
function FusionPanelIcon({ size = 16, active = false, className }) {
|
|
1022
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
1023
|
+
role: "img",
|
|
1024
|
+
"aria-label": "Fusion",
|
|
1025
|
+
"aria-hidden": "true",
|
|
1026
|
+
focusable: "false",
|
|
1027
|
+
viewBox: "0 0 16 16",
|
|
1028
|
+
width: size,
|
|
1029
|
+
height: size,
|
|
1030
|
+
fill: "none",
|
|
1031
|
+
stroke: "currentColor",
|
|
1032
|
+
strokeWidth: active ? 1.5 : 1.2,
|
|
1033
|
+
strokeLinecap: "round",
|
|
1034
|
+
strokeLinejoin: "round",
|
|
1035
|
+
className,
|
|
1036
|
+
children: [
|
|
1037
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
1038
|
+
cx: "4",
|
|
1039
|
+
cy: "4.5",
|
|
1040
|
+
r: "2"
|
|
1041
|
+
}),
|
|
1042
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
1043
|
+
cx: "4",
|
|
1044
|
+
cy: "11.5",
|
|
1045
|
+
r: "2"
|
|
1046
|
+
}),
|
|
1047
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
1048
|
+
cx: "12",
|
|
1049
|
+
cy: "8",
|
|
1050
|
+
r: "2"
|
|
1051
|
+
}),
|
|
1052
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M6 4.5h1.5a3 3 0 0 1 2.7 1.7L10.5 6.8" }),
|
|
1053
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M6 11.5h1.5a3 3 0 0 0 2.7-1.7l.3-.6" })
|
|
1054
|
+
]
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
//#endregion
|
|
1058
|
+
//#region src/client/FusionRecovery.tsx
|
|
1059
|
+
function FusionRecovery({ sessionId, task }) {
|
|
1060
|
+
const lang = useLang();
|
|
1061
|
+
const control = task.modelControl;
|
|
1062
|
+
const [role, setRole] = (0, react.useState)("lead");
|
|
1063
|
+
const [route, setRoute] = (0, react.useState)();
|
|
1064
|
+
const [catalog, setCatalog] = (0, react.useState)();
|
|
1065
|
+
const [dueAt, setDueAt] = (0, react.useState)("");
|
|
1066
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
1067
|
+
const [notice, setNotice] = (0, react.useState)("");
|
|
1068
|
+
const [error, setError] = (0, react.useState)("");
|
|
1069
|
+
const action = async (name) => {
|
|
1070
|
+
setBusy(true);
|
|
1071
|
+
setError("");
|
|
1072
|
+
setNotice("");
|
|
1073
|
+
try {
|
|
1074
|
+
const response = await fetch(FUSION_API_PATH, {
|
|
1075
|
+
method: "POST",
|
|
1076
|
+
credentials: "same-origin",
|
|
1077
|
+
headers: { "content-type": "application/json" },
|
|
1078
|
+
body: JSON.stringify({
|
|
1079
|
+
action: name,
|
|
1080
|
+
sessionId,
|
|
1081
|
+
taskId: task.id,
|
|
1082
|
+
revision: control.revision,
|
|
1083
|
+
role,
|
|
1084
|
+
route,
|
|
1085
|
+
...dueAt ? { dueAt: new Date(dueAt).toISOString() } : {}
|
|
1086
|
+
})
|
|
1087
|
+
});
|
|
1088
|
+
const result = await response.json();
|
|
1089
|
+
if (!response.ok) throw new Error(result.error ?? tr(lang, "操作未完成,请刷新状态", "Not completed; refresh the status"));
|
|
1090
|
+
setNotice(name === "schedule" ? tr(lang, "已设置一次定时继续。Host 需要运行;新消息会取消安排。", "One scheduled continuation is set. The Host must be running; a new message cancels it.") : name === "cancel-schedule" ? tr(lang, "已取消定时继续。", "Scheduled continuation cancelled.") : tr(lang, "已提交继续操作,状态将自动更新。", "Continuation submitted; the status updates automatically."));
|
|
1091
|
+
} catch (error) {
|
|
1092
|
+
setError(error instanceof Error ? error.message : String(error));
|
|
1093
|
+
} finally {
|
|
1094
|
+
setBusy(false);
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
const loadCatalog = async () => {
|
|
1098
|
+
setError("");
|
|
1099
|
+
try {
|
|
1100
|
+
const response = await fetch(`${FUSION_API_PATH}?view=catalog`, {
|
|
1101
|
+
credentials: "same-origin",
|
|
1102
|
+
cache: "no-store"
|
|
1103
|
+
});
|
|
1104
|
+
if (!response.ok) throw new Error(tr(lang, "无法读取模型列表", "Cannot read the model list"));
|
|
1105
|
+
const value = await response.json();
|
|
1106
|
+
setCatalog({
|
|
1107
|
+
...value,
|
|
1108
|
+
groups: value.groups.filter((group) => !isFusionProvider(group.id))
|
|
1109
|
+
});
|
|
1110
|
+
} catch (error) {
|
|
1111
|
+
setError(String(error));
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1115
|
+
className: "fusion-recovery",
|
|
1116
|
+
"aria-label": tr(lang, "Fusion 恢复操作", "Fusion recovery"),
|
|
1117
|
+
children: [
|
|
1118
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: tr(lang, "继续当前任务", "Continue this task") }),
|
|
1119
|
+
Object.entries(control.waits).filter(([, wait]) => wait?.taskId === task.id).map(([key, wait]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
|
|
1120
|
+
key === "lead" ? "Lead" : "Sidekick",
|
|
1121
|
+
" · ",
|
|
1122
|
+
wait.code,
|
|
1123
|
+
" · ",
|
|
1124
|
+
wait.code === "NO_PROGRESS" ? tr(lang, "重复操作没有进展,请调整做法", "Repeated steps made no progress; change the approach") : wait.code === "WORKFLOW_INCOMPLETE" ? tr(lang, "报告、委派或审查尚未完成,请检查任务后继续", "A report, handoff or review is unfinished; check the task, then continue") : wait.code.includes("QUOTA") ? tr(lang, "额度重置时间未知", "Quota reset time unknown") : tr(lang, "需要处理模型错误", "A model error needs attention"),
|
|
1125
|
+
wait.retryNotBefore && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("small", { children: [
|
|
1126
|
+
tr(lang, "服务商建议重试时间", "Provider retry hint"),
|
|
1127
|
+
": ",
|
|
1128
|
+
new Date(wait.retryNotBefore).toLocaleString(),
|
|
1129
|
+
" ",
|
|
1130
|
+
tr(lang, "(不代表额度重置)", "(not a quota reset)")
|
|
1131
|
+
] })
|
|
1132
|
+
] }, key)),
|
|
1133
|
+
control.schedule && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
1134
|
+
role: "status",
|
|
1135
|
+
children: [
|
|
1136
|
+
tr(lang, "定时继续", "Scheduled continuation"),
|
|
1137
|
+
": ",
|
|
1138
|
+
new Date(control.schedule.dueAt).toLocaleString(),
|
|
1139
|
+
" · ",
|
|
1140
|
+
{
|
|
1141
|
+
scheduled: tr(lang, "已安排", "scheduled"),
|
|
1142
|
+
cancelled: tr(lang, "已取消", "cancelled"),
|
|
1143
|
+
fired: tr(lang, "已执行一次", "ran once"),
|
|
1144
|
+
failed: tr(lang, "未能执行", "failed")
|
|
1145
|
+
}[control.schedule.state],
|
|
1146
|
+
control.schedule.reason ? ` · ${control.schedule.reason}` : ""
|
|
1147
|
+
]
|
|
1148
|
+
}),
|
|
1149
|
+
control.operation && ["prepared", "dispatching"].includes(control.operation.state) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1150
|
+
role: "alert",
|
|
1151
|
+
children: tr(lang, "上次继续操作的投递结果尚未确认。请检查会话;系统不会自动重发。", "Delivery of the last continuation is unconfirmed. Check the conversation; nothing is resent automatically.")
|
|
1152
|
+
}),
|
|
1153
|
+
(task.recovering || task.outcomeUnknown) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1154
|
+
role: "alert",
|
|
1155
|
+
children: tr(lang, "中断的操作尚未核对。继续和更换模型都不会生效,本次也不会切换模型。请先用 /fusion recover <检查记录> 提交核对,再继续或更换 Sidekick。设置里保存的新组合不会改写这个会话。", "Interrupted operations are not inspected yet. Continue and model switching do nothing, and this attempt does not change the model. Record the inspection with /fusion recover <note>, then continue or switch the Sidekick. A new pair saved in Settings does not rewrite this conversation.")
|
|
1156
|
+
}),
|
|
1157
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1158
|
+
role: "alert",
|
|
1159
|
+
children: error
|
|
1160
|
+
}),
|
|
1161
|
+
notice && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1162
|
+
role: "status",
|
|
1163
|
+
children: notice
|
|
1164
|
+
}),
|
|
1165
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
|
|
1166
|
+
disabled: busy || task.unsettledTools > 0 || task.pendingApprovals > 0 || task.recovering || task.outcomeUnknown,
|
|
1167
|
+
children: [
|
|
1168
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1169
|
+
onClick: () => {
|
|
1170
|
+
action("continue");
|
|
1171
|
+
},
|
|
1172
|
+
children: tr(lang, "继续一次", "Continue once")
|
|
1173
|
+
}),
|
|
1174
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", { children: [
|
|
1175
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", {
|
|
1176
|
+
onClick: () => {
|
|
1177
|
+
if (!catalog) loadCatalog();
|
|
1178
|
+
},
|
|
1179
|
+
children: tr(lang, "更换角色模型并继续", "Switch a role model and continue")
|
|
1180
|
+
}),
|
|
1181
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [
|
|
1182
|
+
tr(lang, "角色", "Role"),
|
|
1183
|
+
" ",
|
|
1184
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
1185
|
+
value: role,
|
|
1186
|
+
onChange: (event) => {
|
|
1187
|
+
setRole(event.target.value);
|
|
1188
|
+
setRoute(void 0);
|
|
1189
|
+
},
|
|
1190
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1191
|
+
value: "lead",
|
|
1192
|
+
children: "Lead"
|
|
1193
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1194
|
+
value: "worker",
|
|
1195
|
+
children: "Sidekick"
|
|
1196
|
+
})]
|
|
1197
|
+
})
|
|
1198
|
+
] }),
|
|
1199
|
+
catalog && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelChoice, {
|
|
1200
|
+
role: role === "lead" ? "Lead" : "Sidekick",
|
|
1201
|
+
route,
|
|
1202
|
+
catalog,
|
|
1203
|
+
onChange: setRoute
|
|
1204
|
+
}),
|
|
1205
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: tr(lang, "仅更换当前会话的角色模型,保留任务与 Sidekick 会话。后续请求使用新模型的账号。", "Changes this conversation's role model only; the task and the Sidekick session are kept. Later requests use the new model's account.") }),
|
|
1206
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1207
|
+
disabled: !route,
|
|
1208
|
+
onClick: () => {
|
|
1209
|
+
action("switch-role");
|
|
1210
|
+
},
|
|
1211
|
+
children: tr(lang, "更换并继续一次", "Switch and continue once")
|
|
1212
|
+
})
|
|
1213
|
+
] }),
|
|
1214
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", { children: [
|
|
1215
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: tr(lang, "定时继续一次", "Continue once at a time") }),
|
|
1216
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [
|
|
1217
|
+
tr(lang, "本地时间", "Local time"),
|
|
1218
|
+
" ",
|
|
1219
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1220
|
+
"aria-label": tr(lang, "定时继续时间", "Continuation time"),
|
|
1221
|
+
type: "datetime-local",
|
|
1222
|
+
value: dueAt,
|
|
1223
|
+
onChange: (event) => setDueAt(event.target.value)
|
|
1224
|
+
})
|
|
1225
|
+
] }),
|
|
1226
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: tr(lang, "按你选择的时间尝试一次;再次遇到额度错误会停止。此时间不代表服务商承诺的恢复时间。", "Tries once at the time you choose and stops on another quota error. It is not a provider-promised reset time.") }),
|
|
1227
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1228
|
+
disabled: !dueAt,
|
|
1229
|
+
onClick: () => {
|
|
1230
|
+
action("schedule");
|
|
1231
|
+
},
|
|
1232
|
+
children: tr(lang, "设置时间", "Set time")
|
|
1233
|
+
}),
|
|
1234
|
+
control.schedule?.state === "scheduled" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1235
|
+
onClick: () => {
|
|
1236
|
+
action("cancel-schedule");
|
|
1237
|
+
},
|
|
1238
|
+
children: tr(lang, "取消安排", "Cancel")
|
|
1239
|
+
})
|
|
1240
|
+
] })
|
|
1241
|
+
]
|
|
1242
|
+
})
|
|
1243
|
+
]
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
//#endregion
|
|
1247
|
+
//#region src/client/status-polling.ts
|
|
1248
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1249
|
+
const visible = () => document.visibilityState !== "hidden";
|
|
1250
|
+
const running = new Set([
|
|
1251
|
+
"分析",
|
|
1252
|
+
"执行",
|
|
1253
|
+
"修改",
|
|
1254
|
+
"审查",
|
|
1255
|
+
"验证",
|
|
1256
|
+
"正在停止"
|
|
1257
|
+
]);
|
|
1258
|
+
function subscribe(sessionId, listener, expanded) {
|
|
1259
|
+
let entry = entries.get(sessionId);
|
|
1260
|
+
if (!entry) {
|
|
1261
|
+
entry = {
|
|
1262
|
+
listeners: /* @__PURE__ */ new Map(),
|
|
1263
|
+
busy: false,
|
|
1264
|
+
visibility: () => {}
|
|
1265
|
+
};
|
|
1266
|
+
entries.set(sessionId, entry);
|
|
1267
|
+
const own = entry;
|
|
1268
|
+
const active = () => entries.get(sessionId) === own && own.listeners.size > 0;
|
|
1269
|
+
const publish = (snapshot) => {
|
|
1270
|
+
if (!active()) return;
|
|
1271
|
+
own.snapshot = snapshot;
|
|
1272
|
+
for (const notify of own.listeners.keys()) notify();
|
|
1273
|
+
};
|
|
1274
|
+
const refresh = async () => {
|
|
1275
|
+
if (!active() || own.busy || !visible()) return;
|
|
1276
|
+
clearTimeout(own.timer);
|
|
1277
|
+
own.busy = true;
|
|
1278
|
+
const abort = new AbortController();
|
|
1279
|
+
own.abort = abort;
|
|
1280
|
+
const deadline = setTimeout(() => {
|
|
1281
|
+
if (active() && visible()) publish({
|
|
1282
|
+
sessionId,
|
|
1283
|
+
failed: true
|
|
1284
|
+
});
|
|
1285
|
+
abort.abort();
|
|
1286
|
+
}, 1e4);
|
|
1287
|
+
try {
|
|
1288
|
+
const response = await fetch(`${FUSION_API_PATH}?view=status&sessionId=${encodeURIComponent(sessionId)}`, {
|
|
1289
|
+
credentials: "same-origin",
|
|
1290
|
+
cache: "no-store",
|
|
1291
|
+
signal: abort.signal
|
|
1292
|
+
});
|
|
1293
|
+
if (!response.ok) throw new Error("unavailable");
|
|
1294
|
+
const status = await response.json();
|
|
1295
|
+
if (status.schemaVersion !== 1 || status.sessionId !== sessionId) throw new Error("mismatched status");
|
|
1296
|
+
if (!abort.signal.aborted) publish({
|
|
1297
|
+
sessionId,
|
|
1298
|
+
status,
|
|
1299
|
+
failed: false
|
|
1300
|
+
});
|
|
1301
|
+
} catch {
|
|
1302
|
+
if (!abort.signal.aborted) publish({
|
|
1303
|
+
sessionId,
|
|
1304
|
+
failed: true
|
|
1305
|
+
});
|
|
1306
|
+
} finally {
|
|
1307
|
+
clearTimeout(deadline);
|
|
1308
|
+
own.busy = false;
|
|
1309
|
+
if (active() && visible()) {
|
|
1310
|
+
const fast = [...own.listeners.values()].some((value) => value()) && running.has(own.snapshot?.status?.task?.stage ?? "");
|
|
1311
|
+
own.timer = setTimeout(() => {
|
|
1312
|
+
refresh();
|
|
1313
|
+
}, fast ? 2e3 : 15e3);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
};
|
|
1317
|
+
own.visibility = () => {
|
|
1318
|
+
clearTimeout(own.timer);
|
|
1319
|
+
if (!visible()) own.abort?.abort();
|
|
1320
|
+
else refresh();
|
|
1321
|
+
};
|
|
1322
|
+
document.addEventListener("visibilitychange", own.visibility);
|
|
1323
|
+
}
|
|
1324
|
+
entry.listeners.set(listener, expanded);
|
|
1325
|
+
if (entry.listeners.size === 1 || expanded()) entry.visibility();
|
|
1326
|
+
const own = entry;
|
|
1327
|
+
return () => {
|
|
1328
|
+
own.listeners.delete(listener);
|
|
1329
|
+
if (own.listeners.size) return;
|
|
1330
|
+
entries.delete(sessionId);
|
|
1331
|
+
clearTimeout(own.timer);
|
|
1332
|
+
own.abort?.abort();
|
|
1333
|
+
document.removeEventListener("visibilitychange", own.visibility);
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
/** One in-flight read per mounted session; collapsed hints still discover new attention states. */
|
|
1337
|
+
function useFusionStatus(sessionId, expandedTask, attentionOnly = false) {
|
|
1338
|
+
const expandedRef = (0, react.useRef)(expandedTask), attentionRef = (0, react.useRef)(attentionOnly);
|
|
1339
|
+
expandedRef.current = expandedTask;
|
|
1340
|
+
attentionRef.current = attentionOnly;
|
|
1341
|
+
(0, react.useEffect)(() => {
|
|
1342
|
+
if (expandedTask) entries.get(sessionId)?.visibility();
|
|
1343
|
+
}, [sessionId, expandedTask]);
|
|
1344
|
+
return (0, react.useSyncExternalStore)((0, react.useCallback)((listener) => subscribe(sessionId, listener, () => {
|
|
1345
|
+
const task = entries.get(sessionId)?.snapshot?.status?.task;
|
|
1346
|
+
return expandedRef.current !== void 0 && task?.id === expandedRef.current && (!attentionRef.current || task.attention);
|
|
1347
|
+
}), [sessionId]), (0, react.useCallback)(() => entries.get(sessionId)?.snapshot, [sessionId]), () => void 0);
|
|
1348
|
+
}
|
|
1349
|
+
//#endregion
|
|
1350
|
+
//#region src/client/FusionStatus.tsx
|
|
1351
|
+
const styles = `
|
|
1352
|
+
.fusion-status{margin:8px 12px;color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5;overflow-wrap:anywhere}
|
|
1353
|
+
.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
|
+
.fusion-status>summary::before{content:'›';font-size:18px;flex:none}.fusion-status[open]>summary::before{content:'⌄'}
|
|
1355
|
+
.fusion-status>summary:focus-visible{outline:2px solid var(--dsw-alias-brand-primary)}
|
|
1356
|
+
.fusion-status .fusion-status-label{font-weight:600}.fusion-status .fusion-status-count{margin-left:auto;color:var(--dsw-alias-label-secondary);white-space:nowrap}
|
|
1357
|
+
.fusion-status .fusion-status-detail{max-height:320px;overflow:auto;padding:4px 4px 12px 0;scrollbar-gutter:stable}
|
|
1358
|
+
.fusion-status .fusion-recovery fieldset{border:0;padding:0;display:flex;flex-direction:column;gap:10px}.fusion-recovery :is(button,select,input){font:inherit;color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:7px;max-width:100%}.fusion-recovery :is(select,input,option,optgroup){background-color:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);color-scheme:inherit}.fusion-recovery button{cursor:pointer;background-color:var(--dsw-alias-button-elevated-fill)}.fusion-recovery :is(button,input,select):focus-visible{outline:2px solid var(--dsw-alias-brand-primary)}.fusion-recovery [role=alert]{color:var(--dsw-alias-state-error-primary)}
|
|
1359
|
+
.fusion-status:has(.fusion-recovery) .fusion-status-detail{max-height:min(65vh,640px)}.fusion-recovery details>summary{padding:8px 0;cursor:pointer}
|
|
1360
|
+
.fusion-status p{margin:4px 0 12px}.fusion-status h3{font-size:13px;margin:16px 0 4px;font-weight:600}
|
|
1361
|
+
.fusion-status dl{margin:0;display:grid;grid-template-columns:minmax(72px,auto) minmax(0,1fr);gap:4px 12px}
|
|
1362
|
+
.fusion-status dt{color:var(--dsw-alias-label-secondary)}.fusion-status dd{margin:0;min-width:0}
|
|
1363
|
+
.fusion-status ul{list-style:none;margin:0;padding:0}.fusion-status li{padding:6px 0;border-bottom:1px solid var(--dsw-alias-border-l2)}
|
|
1364
|
+
.fusion-status small{display:block;color:var(--dsw-alias-label-secondary);font-size:12px}
|
|
1365
|
+
@media(max-width:400px){.fusion-status>summary{gap:8px}.fusion-status .fusion-status-detail{max-height:280px}}
|
|
1366
|
+
`;
|
|
1367
|
+
const role = (value) => value === "lead" ? "Lead" : "Sidekick";
|
|
1368
|
+
const purposeOf = (lang, value) => ({
|
|
1369
|
+
conversation: tr(lang, "任务", "task"),
|
|
1370
|
+
compaction: tr(lang, "压缩", "compaction"),
|
|
1371
|
+
"cache-keepalive": tr(lang, "保活", "keepalive"),
|
|
1372
|
+
"session-title": tr(lang, "标题", "title")
|
|
1373
|
+
})[value] ?? value;
|
|
1374
|
+
const outcomeOf = (lang, value) => ({
|
|
1375
|
+
entered: tr(lang, "请求中", "in progress"),
|
|
1376
|
+
stop: tr(lang, "已返回", "returned"),
|
|
1377
|
+
"tool-calls": tr(lang, "调用工具", "tool calls"),
|
|
1378
|
+
"max-tokens": tr(lang, "输出截断", "output truncated"),
|
|
1379
|
+
aborted: tr(lang, "已中断", "aborted"),
|
|
1380
|
+
error: tr(lang, "请求失败", "failed"),
|
|
1381
|
+
unknown: tr(lang, "结束状态未知", "unknown end")
|
|
1382
|
+
})[value] ?? value;
|
|
1383
|
+
const toolStateOf = (lang, value) => ({
|
|
1384
|
+
"dispatch-started": tr(lang, "执行未收尾", "unfinished"),
|
|
1385
|
+
returned: tr(lang, "已返回", "returned"),
|
|
1386
|
+
"outcome-unknown": tr(lang, "结果未知", "outcome unknown"),
|
|
1387
|
+
inspected: tr(lang, "已人工检查", "inspected")
|
|
1388
|
+
})[value] ?? value;
|
|
1389
|
+
const countOf = (lang, value) => value.totalRequests === 0 ? "—" : value.reportedRequests === 0 ? tr(lang, "未提供", "not reported") : `${value.knownTokens.toLocaleString()}${value.reportedRequests < value.totalRequests ? tr(lang, `(${value.reportedRequests}/${value.totalRequests} 次有报告)`, ` (${value.reportedRequests}/${value.totalRequests} reported)`) : ""}`;
|
|
1390
|
+
const noteStyle = {
|
|
1391
|
+
margin: "8px 12px",
|
|
1392
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
1393
|
+
fontSize: 12,
|
|
1394
|
+
lineHeight: 1.5
|
|
1395
|
+
};
|
|
1396
|
+
/** Visible-session polling reads the plugin ledger; it never sends a chat message. */
|
|
1397
|
+
function FusionStatus({ sessionId, attentionOnly = false }) {
|
|
1398
|
+
const lang = useLang();
|
|
1399
|
+
const purpose = (value) => purposeOf(lang, value), outcome = (value) => outcomeOf(lang, value);
|
|
1400
|
+
const toolState = (value) => toolStateOf(lang, value), count = (value) => countOf(lang, value);
|
|
1401
|
+
const [expandedTask, setExpandedTask] = (0, react.useState)();
|
|
1402
|
+
const snapshot = useFusionStatus(sessionId, expandedTask, attentionOnly);
|
|
1403
|
+
if (!snapshot || snapshot.sessionId !== sessionId) return attentionOnly ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1404
|
+
style: noteStyle,
|
|
1405
|
+
role: "status",
|
|
1406
|
+
children: tr(lang, "正在读取 Fusion 状态…", "Loading Fusion status…")
|
|
1407
|
+
});
|
|
1408
|
+
if (snapshot.failed) return attentionOnly ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1409
|
+
style: noteStyle,
|
|
1410
|
+
role: "status",
|
|
1411
|
+
children: tr(lang, "Fusion 状态暂时无法更新,恢复连接后会重试。可用 /fusion status 查看。", "Fusion status cannot update right now; it retries when reconnected. Use /fusion status.")
|
|
1412
|
+
});
|
|
1413
|
+
const task = snapshot.status?.task;
|
|
1414
|
+
if (!snapshot.status?.selected || !task) return null;
|
|
1415
|
+
if (attentionOnly && !task.attention) return null;
|
|
1416
|
+
const { usage } = task;
|
|
1417
|
+
const verification = task.automatedChecks === 0 && task.verification === "verified" ? tr(lang, "仅经 Lead 审查(未运行自动检查)", "Lead review only (no automated checks ran)") : {
|
|
1418
|
+
verified: tr(lang, "验证通过", "verified"),
|
|
1419
|
+
partial: tr(lang, "部分验证", "partly verified"),
|
|
1420
|
+
unverified: tr(lang, "尚未验证", "unverified")
|
|
1421
|
+
}[task.verification];
|
|
1422
|
+
const stage = lang === "en" ? task.stageEn ?? task.stage : task.stage, detail = lang === "en" && task.detailEn !== void 0 ? task.detailEn : task.detail;
|
|
1423
|
+
const share = (role) => {
|
|
1424
|
+
const total = usage.byRole.lead.output.knownTokens + usage.byRole.worker.output.knownTokens;
|
|
1425
|
+
return total ? tr(lang, ` · 输出占 ${Math.round(usage.byRole[role].output.knownTokens / total * 100)}%`, ` · ${Math.round(usage.byRole[role].output.knownTokens / total * 100)}% of output`) : "";
|
|
1426
|
+
};
|
|
1427
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
1428
|
+
className: "fusion-status",
|
|
1429
|
+
onToggle: (event) => setExpandedTask(event.currentTarget.open ? task.id : void 0),
|
|
1430
|
+
children: [
|
|
1431
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: styles }),
|
|
1432
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", {
|
|
1433
|
+
"aria-label": tr(lang, `Fusion · ${stage},任务详情`, `Fusion · ${stage}, task details`),
|
|
1434
|
+
"data-ud-check": "fusion-status-summary",
|
|
1435
|
+
children: [
|
|
1436
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1437
|
+
className: "fusion-status-label",
|
|
1438
|
+
role: "status",
|
|
1439
|
+
children: ["Fusion · ", stage]
|
|
1440
|
+
}),
|
|
1441
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1442
|
+
className: "fusion-status-count",
|
|
1443
|
+
children: tr(lang, `${usage.calls} 次请求`, `${usage.calls} requests`)
|
|
1444
|
+
}),
|
|
1445
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tr(lang, "详情", "Details") })
|
|
1446
|
+
]
|
|
1447
|
+
}),
|
|
1448
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1449
|
+
className: "fusion-status-detail",
|
|
1450
|
+
tabIndex: 0,
|
|
1451
|
+
"aria-label": tr(lang, "Fusion 任务详情", "Fusion task details"),
|
|
1452
|
+
"data-ud-check": "fusion-status-detail",
|
|
1453
|
+
children: [
|
|
1454
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: detail ?? (task.stage === "完成" ? tr(lang, `本轮任务已结束 · ${verification}`, `This task has finished · ${verification}`) : tr(lang, `本轮任务 · ${verification}`, `This task · ${verification}`)) }),
|
|
1455
|
+
task.modelControl && task.stage !== "完成" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FusionRecovery, {
|
|
1456
|
+
sessionId,
|
|
1457
|
+
task
|
|
1458
|
+
}),
|
|
1459
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", { children: [
|
|
1460
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: "Lead" }),
|
|
1461
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", { children: [task.models.lead.model, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: task.models.lead.provider })] }),
|
|
1462
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: "Sidekick" }),
|
|
1463
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", { children: [task.models.worker.model, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("small", { children: [task.models.worker.provider, task.workerId ? tr(lang, " · 已建立持续会话", " · persistent session") : tr(lang, " · 尚未委派", " · not delegated yet")] })] }),
|
|
1464
|
+
task.models.compactor && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: tr(lang, "压缩模型", "Compaction") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", { children: [task.models.compactor.model, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: task.models.compactor.provider })] })] }),
|
|
1465
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: tr(lang, "待确认", "Pending approvals") }),
|
|
1466
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: task.pendingApprovals }),
|
|
1467
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: tr(lang, "未收尾工具", "Unsettled tools") }),
|
|
1468
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: task.unsettledTools })
|
|
1469
|
+
] }),
|
|
1470
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: tr(lang, "本轮用量", "Usage this task") }),
|
|
1471
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", { children: [
|
|
1472
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: tr(lang, "模型请求", "Model requests") }),
|
|
1473
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", { children: [usage.calls, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: tr(lang, `含压缩 ${usage.compactionCalls} 次、保活 ${usage.keepaliveCalls} 次`, `incl. ${usage.compactionCalls} compaction, ${usage.keepaliveCalls} keepalive`) })] }),
|
|
1474
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: tr(lang, "用量报告", "Usage reports") }),
|
|
1475
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: tr(lang, `${usage.finalCalls} 次已定稿 · ${usage.provisionalCalls} 次暂报 · ${usage.unreportedCalls} 次未提供`, `${usage.finalCalls} final · ${usage.provisionalCalls} provisional · ${usage.unreportedCalls} unreported`) }),
|
|
1476
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: tr(lang, "输入 Token", "Input tokens") }),
|
|
1477
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", { children: [count(usage.input), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: tr(lang, "已报告的未缓存输入", "reported uncached input") })] }),
|
|
1478
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: tr(lang, "缓存读取", "Cache read") }),
|
|
1479
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: count(usage.cacheRead) }),
|
|
1480
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: tr(lang, "输出 Token", "Output tokens") }),
|
|
1481
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: count(usage.output) }),
|
|
1482
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: tr(lang, "实际费用", "Actual cost") }),
|
|
1483
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", { children: [tr(lang, "未知", "unknown"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: tr(lang, "当前账号未提供可靠账单金额", "the account reports no reliable billed amount") })] })
|
|
1484
|
+
] }),
|
|
1485
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: tr(lang, "按角色", "By role") }),
|
|
1486
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dl", { children: ["lead", "worker"].map((role) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: role === "lead" ? "Lead" : "Sidekick" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", { children: [
|
|
1487
|
+
tr(lang, `${usage.byRole[role].calls} 次`, `${usage.byRole[role].calls} requests`),
|
|
1488
|
+
" · ",
|
|
1489
|
+
tr(lang, "输出", "output"),
|
|
1490
|
+
" ",
|
|
1491
|
+
count(usage.byRole[role].output),
|
|
1492
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("small", { children: [
|
|
1493
|
+
tr(lang, "输入", "input"),
|
|
1494
|
+
" ",
|
|
1495
|
+
count(usage.byRole[role].input),
|
|
1496
|
+
" · ",
|
|
1497
|
+
tr(lang, "缓存读取", "cache read"),
|
|
1498
|
+
" ",
|
|
1499
|
+
count(usage.byRole[role].cacheRead),
|
|
1500
|
+
share(role)
|
|
1501
|
+
] })
|
|
1502
|
+
] })] }, role)) }),
|
|
1503
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: tr(lang, "Token 数包含暂报数据;缺失字段不计为 0。请求数来自 DSH,供应商内部重试次数未知。", "Token counts include provisional data; missing fields are not counted as 0. Request counts come from DSH; provider-internal retries are unknown.") }),
|
|
1504
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: tr(lang, "最近上下文检查", "Recent context checks") }),
|
|
1505
|
+
task.contexts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: task.contexts.map((context) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
|
|
1506
|
+
role(context.role),
|
|
1507
|
+
" · ",
|
|
1508
|
+
purpose(context.purpose),
|
|
1509
|
+
" · ",
|
|
1510
|
+
context.admitted ? tr(lang, "可发送", "admitted") : tr(lang, "超出预算", "over budget"),
|
|
1511
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("small", { children: [
|
|
1512
|
+
context.inputTokens.toLocaleString(),
|
|
1513
|
+
" / ",
|
|
1514
|
+
context.budget.toLocaleString(),
|
|
1515
|
+
" Token · ",
|
|
1516
|
+
context.quality === "exact" ? tr(lang, "精确计数", "exact") : tr(lang, "估算", "estimated")
|
|
1517
|
+
] })
|
|
1518
|
+
] }, context.role)) }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: tr(lang, "尚无上下文检查记录。", "No context checks yet.") }),
|
|
1519
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: tr(lang, "最近模型请求", "Recent model requests") }),
|
|
1520
|
+
task.requests.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: task.requests.map((request) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
|
|
1521
|
+
role(request.role),
|
|
1522
|
+
" · ",
|
|
1523
|
+
purpose(request.purpose),
|
|
1524
|
+
" · ",
|
|
1525
|
+
outcome(request.outcome),
|
|
1526
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("small", { children: [
|
|
1527
|
+
request.model,
|
|
1528
|
+
" · ",
|
|
1529
|
+
request.provider,
|
|
1530
|
+
" · ",
|
|
1531
|
+
request.authority === "final" ? tr(lang, "用量已定稿", "usage final") : request.authority === "provisional" ? tr(lang, "用量暂报", "usage provisional") : tr(lang, "用量未提供", "usage not reported")
|
|
1532
|
+
] })
|
|
1533
|
+
] }, request.id)) }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: tr(lang, "尚未发出模型请求。", "No model requests yet.") }),
|
|
1534
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: tr(lang, "最近命令与写入", "Recent commands and writes") }),
|
|
1535
|
+
task.tools.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: task.tools.map((tool) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
|
|
1536
|
+
role(tool.role),
|
|
1537
|
+
" · ",
|
|
1538
|
+
tool.name,
|
|
1539
|
+
" · ",
|
|
1540
|
+
tool.failed ? tr(lang, "返回错误", "error") : toolState(tool.state)
|
|
1541
|
+
] }, tool.id)) }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: tr(lang, "尚无工具执行记录。", "No tool runs yet.") }),
|
|
1542
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: tr(lang, "显示最近 12 次请求、8 项命令或写入操作;用量汇总覆盖本轮全部请求。读取操作仍可在会话中查看。", "Shows the last 12 requests and 8 commands or writes; usage totals cover all requests of this task. Reads remain visible in the conversation.") })
|
|
1543
|
+
]
|
|
1544
|
+
})
|
|
1545
|
+
]
|
|
1546
|
+
}, task.id);
|
|
1547
|
+
}
|
|
1548
|
+
//#endregion
|
|
1549
|
+
//#region src/client/FusionHint.tsx
|
|
1550
|
+
/** Additive composer hint. The original selector and its selection state stay native. */
|
|
1551
|
+
function FusionHint({ sessionId, directory, load }) {
|
|
1552
|
+
const selected = (0, react.useSyncExternalStore)(directory.subscribe, directory.getSnapshot).current;
|
|
1553
|
+
const fusion = isFusionSelection(selected ?? void 0);
|
|
1554
|
+
const lang = useLang();
|
|
1555
|
+
const [message, setMessage] = (0, react.useState)();
|
|
1556
|
+
(0, react.useEffect)(() => {
|
|
1557
|
+
load();
|
|
1558
|
+
}, [load]);
|
|
1559
|
+
(0, react.useEffect)(() => {
|
|
1560
|
+
if (!fusion) {
|
|
1561
|
+
setMessage(void 0);
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
let active = true;
|
|
1565
|
+
const abort = new AbortController();
|
|
1566
|
+
const refresh = () => {
|
|
1567
|
+
fetch(`${FUSION_API_PATH}?view=settings`, {
|
|
1568
|
+
credentials: "same-origin",
|
|
1569
|
+
cache: "no-store",
|
|
1570
|
+
signal: abort.signal
|
|
1571
|
+
}).then(async (response) => {
|
|
1572
|
+
if (!response.ok) throw new Error("unavailable");
|
|
1573
|
+
return response.json();
|
|
1574
|
+
}).then((state) => {
|
|
1575
|
+
if (active) setMessage(!state.pair ? tr(lang, "Fusion 尚未配置模型,请前往设置 → Fusion 选择 Lead 和 Sidekick。", "Fusion has no models yet: open Settings → Fusion and choose a Lead and a Sidekick.") : !state.authorized ? tr(lang, "Fusion 当前配置未就绪,请前往设置 → Fusion 查看。", "The Fusion setup is not ready: see Settings → Fusion.") : void 0);
|
|
1576
|
+
}).catch(() => {
|
|
1577
|
+
if (active) setMessage(tr(lang, "暂时无法确认 Fusion 配置,请前往设置 → Fusion 查看。", "Cannot confirm the Fusion setup right now: see Settings → Fusion."));
|
|
1578
|
+
});
|
|
1579
|
+
};
|
|
1580
|
+
refresh();
|
|
1581
|
+
const off = onSettingsChanged(refresh);
|
|
1582
|
+
return () => {
|
|
1583
|
+
active = false;
|
|
1584
|
+
abort.abort();
|
|
1585
|
+
off();
|
|
1586
|
+
};
|
|
1587
|
+
}, [fusion, lang]);
|
|
1588
|
+
if (!fusion) return null;
|
|
1589
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [message && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1590
|
+
role: "status",
|
|
1591
|
+
style: {
|
|
1592
|
+
margin: "8px 12px",
|
|
1593
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
1594
|
+
fontSize: 12,
|
|
1595
|
+
lineHeight: "18px"
|
|
1596
|
+
},
|
|
1597
|
+
children: message
|
|
1598
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FusionStatus, {
|
|
1599
|
+
sessionId,
|
|
1600
|
+
attentionOnly: true
|
|
1601
|
+
})] });
|
|
1602
|
+
}
|
|
1603
|
+
//#endregion
|
|
1604
|
+
//#region src/activity.ts
|
|
1605
|
+
/** Namespaced display identity; the unchanged native identity remains in source. */
|
|
1606
|
+
function activityCallId(activity) {
|
|
1607
|
+
const event = activity.source;
|
|
1608
|
+
const id = event.type === "tool/call" ? event.data.callId : event.data.message.source.callId;
|
|
1609
|
+
return `fusion:${activity.childSessionId}:${id}`;
|
|
1610
|
+
}
|
|
1611
|
+
//#endregion
|
|
1612
|
+
//#region src/client/activity-model.ts
|
|
1613
|
+
function activityRows(events) {
|
|
1614
|
+
const rows = /* @__PURE__ */ new Map();
|
|
1615
|
+
for (const event of events) {
|
|
1616
|
+
const id = activityCallId(event);
|
|
1617
|
+
if (event.source.type === "tool/call") rows.set(id, {
|
|
1618
|
+
id,
|
|
1619
|
+
call: event.source
|
|
1620
|
+
});
|
|
1621
|
+
else {
|
|
1622
|
+
const row = rows.get(id);
|
|
1623
|
+
if (row) rows.set(id, {
|
|
1624
|
+
...row,
|
|
1625
|
+
result: event.source
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
return [...rows.values()];
|
|
1630
|
+
}
|
|
1631
|
+
/** Only actual result-time metadata is labelled an applied diff. */
|
|
1632
|
+
function activityDiffs(row) {
|
|
1633
|
+
if (!row.result || row.result.data.message.isError) return [];
|
|
1634
|
+
const meta = row.result.data.meta;
|
|
1635
|
+
if (!Array.isArray(meta?.diffs)) return [];
|
|
1636
|
+
return meta.diffs.filter((hunk) => hunk && typeof hunk === "object" && typeof hunk.path === "string" && (typeof hunk.oldText === "string" || hunk.oldText === null) && typeof hunk.newText === "string");
|
|
1637
|
+
}
|
|
1638
|
+
//#endregion
|
|
1639
|
+
//#region src/client/ActivityRows.tsx
|
|
1640
|
+
const labelsFor = (lang) => ({
|
|
1641
|
+
codeLabel: tr(lang, "代码", "Code"),
|
|
1642
|
+
wrapLabel: tr(lang, "自动换行", "Wrap"),
|
|
1643
|
+
unwrapLabel: tr(lang, "取消自动换行", "No wrap"),
|
|
1644
|
+
copy: tr(lang, "复制", "Copy"),
|
|
1645
|
+
copied: tr(lang, "已复制", "Copied"),
|
|
1646
|
+
collapseAria: tr(lang, "收起内容", "Collapse"),
|
|
1647
|
+
collapse: tr(lang, "收起", "Collapse"),
|
|
1648
|
+
expandAria: (n) => tr(lang, `展开其余 ${n} 行`, `Show ${n} more lines`),
|
|
1649
|
+
expand: (n) => tr(lang, `展开其余 ${n} 行`, `Show ${n} more lines`)
|
|
1650
|
+
});
|
|
1651
|
+
const terminalLabelsFor = (lang) => ({
|
|
1652
|
+
...labelsFor(lang),
|
|
1653
|
+
commandLine: (n) => tr(lang, `命令行第 ${n} 行`, `command line ${n}`),
|
|
1654
|
+
signal: (s) => tr(lang, `信号 ${s}`, `signal ${s}`),
|
|
1655
|
+
exitCode: (n) => tr(lang, `退出码 ${n}`, `exit code ${n}`),
|
|
1656
|
+
running: tr(lang, "执行中", "Running"),
|
|
1657
|
+
failed: tr(lang, "执行失败", "Failed"),
|
|
1658
|
+
done: tr(lang, "已返回", "Done"),
|
|
1659
|
+
noOutput: tr(lang, "无输出", "No output"),
|
|
1660
|
+
noExitCode: tr(lang, "未记录退出码", "No exit code")
|
|
1661
|
+
});
|
|
1662
|
+
const titleFor = (lang) => ({
|
|
1663
|
+
bash: tr(lang, "终端", "Terminal"),
|
|
1664
|
+
pwsh: tr(lang, "终端", "Terminal"),
|
|
1665
|
+
read: tr(lang, "读取", "Read"),
|
|
1666
|
+
write: tr(lang, "写入", "Write"),
|
|
1667
|
+
edit: tr(lang, "编辑", "Edit"),
|
|
1668
|
+
glob: tr(lang, "查找文件", "Find files"),
|
|
1669
|
+
grep: tr(lang, "搜索", "Search"),
|
|
1670
|
+
job_output: tr(lang, "命令输出", "Command output"),
|
|
1671
|
+
job_kill: tr(lang, "停止命令", "Stop command"),
|
|
1672
|
+
str_replace_editor: tr(lang, "编辑", "Edit")
|
|
1673
|
+
});
|
|
1674
|
+
function ActivityCard({ row, done, cwd, openFile }) {
|
|
1675
|
+
const lang = useLang();
|
|
1676
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
1677
|
+
let args = {};
|
|
1678
|
+
try {
|
|
1679
|
+
const parsed = JSON.parse(row.call.data.arguments);
|
|
1680
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) args = parsed;
|
|
1681
|
+
} catch {}
|
|
1682
|
+
const result = row.result?.data.message;
|
|
1683
|
+
const output = result?.content.flatMap((part) => part.type === "text" ? [part.text] : []).join("\n") ?? "";
|
|
1684
|
+
const path = typeof args.file_path === "string" ? args.file_path : typeof args.path === "string" ? args.path : void 0;
|
|
1685
|
+
const command = typeof args.command === "string" ? args.command : void 0;
|
|
1686
|
+
const exit = /\n\[exit code: (\d+)\]\s*$/.exec(output);
|
|
1687
|
+
const failed = result?.isError || exit && Number(exit[1]) !== 0;
|
|
1688
|
+
const diffs = activityDiffs(row);
|
|
1689
|
+
const label = titleFor(lang)[row.call.data.name] ?? row.call.data.name;
|
|
1690
|
+
const summary = path ?? (typeof args.description === "string" ? args.description : command) ?? (typeof args.pattern === "string" ? args.pattern : label);
|
|
1691
|
+
const status = failed ? tr(lang, "失败", "Failed") : !row.result ? done ? tr(lang, "结果未记录", "No result recorded") : tr(lang, "执行中", "Running") : "";
|
|
1692
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1693
|
+
className: "fusion-activity-row",
|
|
1694
|
+
"data-fusion-tool": row.call.data.name,
|
|
1695
|
+
"data-fusion-call": row.id,
|
|
1696
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1697
|
+
type: "button",
|
|
1698
|
+
"aria-expanded": open,
|
|
1699
|
+
onClick: () => setOpen(!open),
|
|
1700
|
+
className: "fusion-activity-toggle",
|
|
1701
|
+
children: [
|
|
1702
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1703
|
+
"aria-hidden": true,
|
|
1704
|
+
children: open ? "⌄" : "›"
|
|
1705
|
+
}),
|
|
1706
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }),
|
|
1707
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1708
|
+
className: "fusion-activity-summary",
|
|
1709
|
+
children: summary
|
|
1710
|
+
}),
|
|
1711
|
+
status && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", {
|
|
1712
|
+
role: "status",
|
|
1713
|
+
"data-failed": Boolean(failed),
|
|
1714
|
+
children: status
|
|
1715
|
+
})
|
|
1716
|
+
]
|
|
1717
|
+
}), open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1718
|
+
className: "fusion-activity-body",
|
|
1719
|
+
children: [path && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1720
|
+
type: "button",
|
|
1721
|
+
className: "fusion-activity-file",
|
|
1722
|
+
onClick: () => openFile(path),
|
|
1723
|
+
children: [
|
|
1724
|
+
tr(lang, "打开", "Open"),
|
|
1725
|
+
" ",
|
|
1726
|
+
path
|
|
1727
|
+
]
|
|
1728
|
+
}), diffs.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DiffBlock, {
|
|
1729
|
+
diffs,
|
|
1730
|
+
labels: labelsFor(lang),
|
|
1731
|
+
maxLines: 16
|
|
1732
|
+
}) : (row.call.data.name === "bash" || row.call.data.name === "pwsh") && command ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.TerminalBlock, {
|
|
1733
|
+
command,
|
|
1734
|
+
cwd,
|
|
1735
|
+
output,
|
|
1736
|
+
running: !row.result && !done,
|
|
1737
|
+
exitCode: exit ? Number(exit[1]) : void 0,
|
|
1738
|
+
labels: terminalLabelsFor(lang)
|
|
1739
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", { children: output || (row.result ? tr(lang, "已返回,无文本输出。", "Returned without text output.") : row.call.data.arguments) })]
|
|
1740
|
+
})]
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
const activityStyles = `
|
|
1744
|
+
.fusion-activity-row{font-size:13px;line-height:1.5;margin:4px 0;min-width:0;color:var(--dsw-alias-label-secondary)}
|
|
1745
|
+
.fusion-activity-toggle{display:flex;align-items:center;gap:8px;width:100%;padding:6px 0;background:none;border:0;color:inherit;text-align:left;cursor:pointer;font:inherit}
|
|
1746
|
+
.fusion-activity-toggle:focus-visible,.fusion-activity-file:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
|
|
1747
|
+
.fusion-activity-summary{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary)}
|
|
1748
|
+
.fusion-activity-toggle small{white-space:nowrap}.fusion-activity-toggle small[data-failed=true]{color:var(--dsw-alias-state-error-primary)}
|
|
1749
|
+
.fusion-activity-body{padding:4px 0 10px;overflow:hidden}.fusion-activity-body pre{white-space:pre-wrap;overflow-wrap:anywhere;max-height:360px;overflow:auto;font-size:12px}
|
|
1750
|
+
.fusion-activity-file{font:inherit;border:0;background:none;color:var(--dsw-alias-brand-primary);padding:0 0 6px;cursor:pointer}
|
|
1751
|
+
`;
|
|
1752
|
+
//#endregion
|
|
1753
|
+
//#region src/client/FusionTool.tsx
|
|
1754
|
+
const fusionControlTools = [
|
|
1755
|
+
"fusion_read_state",
|
|
1756
|
+
"fusion_delegate_text",
|
|
1757
|
+
"fusion_explore",
|
|
1758
|
+
"fusion_delegate",
|
|
1759
|
+
"fusion_rework",
|
|
1760
|
+
"fusion_wait",
|
|
1761
|
+
"fusion_review_result",
|
|
1762
|
+
"fusion_submit_result",
|
|
1763
|
+
"fusion_takeover",
|
|
1764
|
+
"fusion_finish_direct",
|
|
1765
|
+
"fusion_read_evidence"
|
|
1766
|
+
];
|
|
1767
|
+
/** Keep internal coordination out of the normal conversation. Errors remain visible. */
|
|
1768
|
+
function FusionTool({ block, sessionId, callId, toolName, cwd, openFile }) {
|
|
1769
|
+
const lang = useLang();
|
|
1770
|
+
const settled = "kind" in block;
|
|
1771
|
+
const tracksActivity = toolName === "fusion_delegate_text" || toolName === "fusion_explore" || toolName === "fusion_delegate" || toolName === "fusion_rework";
|
|
1772
|
+
const [snapshot, setSnapshot] = (0, react.useState)();
|
|
1773
|
+
const key = `${sessionId}:${callId}`;
|
|
1774
|
+
(0, react.useEffect)(() => {
|
|
1775
|
+
if (!tracksActivity) return;
|
|
1776
|
+
let active = true, busy = false, cursor = -1, timer;
|
|
1777
|
+
const events = /* @__PURE__ */ new Map();
|
|
1778
|
+
let abort;
|
|
1779
|
+
const visible = () => document.visibilityState !== "hidden";
|
|
1780
|
+
const refresh = async () => {
|
|
1781
|
+
if (!active || busy || !visible()) return;
|
|
1782
|
+
busy = true;
|
|
1783
|
+
abort = new AbortController();
|
|
1784
|
+
const deadline = setTimeout(() => abort?.abort(), 1e4);
|
|
1785
|
+
let again = true, delay = 2e3;
|
|
1786
|
+
try {
|
|
1787
|
+
const response = await fetch(`${FUSION_API_PATH}?view=activity&sessionId=${encodeURIComponent(sessionId)}&callId=${encodeURIComponent(callId)}&after=${cursor}`, {
|
|
1788
|
+
credentials: "same-origin",
|
|
1789
|
+
cache: "no-store",
|
|
1790
|
+
signal: abort.signal
|
|
1791
|
+
});
|
|
1792
|
+
if (!response.ok) throw new Error("Activity unavailable");
|
|
1793
|
+
const page = await response.json();
|
|
1794
|
+
if (!Array.isArray(page.events) || !Number.isSafeInteger(page.cursor) || page.cursor < cursor) throw new Error("Invalid activity page");
|
|
1795
|
+
for (const event of page.events) if (event.parentCallId === callId) events.set(event.source.seq, event);
|
|
1796
|
+
cursor = page.cursor;
|
|
1797
|
+
again = page.more || !page.done || !settled;
|
|
1798
|
+
delay = page.more ? 0 : 2e3;
|
|
1799
|
+
if (active) setSnapshot({
|
|
1800
|
+
key,
|
|
1801
|
+
events: [...events.values()].sort((a, b) => a.source.seq - b.source.seq),
|
|
1802
|
+
done: page.done
|
|
1803
|
+
});
|
|
1804
|
+
} catch {
|
|
1805
|
+
if (active) setSnapshot((previous) => ({
|
|
1806
|
+
key,
|
|
1807
|
+
events: previous?.key === key ? previous.events : [],
|
|
1808
|
+
done: false,
|
|
1809
|
+
failed: true
|
|
1810
|
+
}));
|
|
1811
|
+
} finally {
|
|
1812
|
+
clearTimeout(deadline);
|
|
1813
|
+
busy = false;
|
|
1814
|
+
if (active && again && visible()) timer = setTimeout(() => {
|
|
1815
|
+
refresh();
|
|
1816
|
+
}, delay);
|
|
1817
|
+
}
|
|
1818
|
+
};
|
|
1819
|
+
const visibility = () => {
|
|
1820
|
+
if (timer) clearTimeout(timer);
|
|
1821
|
+
if (document.visibilityState === "hidden") abort?.abort();
|
|
1822
|
+
else refresh();
|
|
1823
|
+
};
|
|
1824
|
+
refresh();
|
|
1825
|
+
document.addEventListener("visibilitychange", visibility);
|
|
1826
|
+
return () => {
|
|
1827
|
+
active = false;
|
|
1828
|
+
abort?.abort();
|
|
1829
|
+
if (timer) clearTimeout(timer);
|
|
1830
|
+
document.removeEventListener("visibilitychange", visibility);
|
|
1831
|
+
};
|
|
1832
|
+
}, [
|
|
1833
|
+
sessionId,
|
|
1834
|
+
callId,
|
|
1835
|
+
key,
|
|
1836
|
+
tracksActivity,
|
|
1837
|
+
settled
|
|
1838
|
+
]);
|
|
1839
|
+
const current = tracksActivity && snapshot?.key === key ? snapshot : void 0;
|
|
1840
|
+
const rows = (0, react.useMemo)(() => activityRows(current?.events ?? []), [current?.events]);
|
|
1841
|
+
const text = settled ? block.content.flatMap((part) => part.type === "text" ? [part.text] : []).join("\n") : "";
|
|
1842
|
+
let problem = settled && block.isError ? text : "";
|
|
1843
|
+
if (!problem) try {
|
|
1844
|
+
const result = JSON.parse(text);
|
|
1845
|
+
if (result.status === "needs-decision") problem = result.reason ?? tr(lang, "任务需要进一步处理。", "The task needs attention.");
|
|
1846
|
+
} catch {}
|
|
1847
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1848
|
+
rows.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: activityStyles }),
|
|
1849
|
+
rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActivityCard, {
|
|
1850
|
+
row,
|
|
1851
|
+
done: current?.done ?? false,
|
|
1852
|
+
cwd,
|
|
1853
|
+
openFile
|
|
1854
|
+
}, row.id)),
|
|
1855
|
+
current?.failed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1856
|
+
role: "status",
|
|
1857
|
+
children: tr(lang, "执行记录暂时无法加载,正在重试。", "Activity is unavailable; retrying.")
|
|
1858
|
+
}),
|
|
1859
|
+
problem && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1860
|
+
role: "status",
|
|
1861
|
+
style: {
|
|
1862
|
+
fontSize: 13,
|
|
1863
|
+
whiteSpace: "pre-wrap"
|
|
1864
|
+
},
|
|
1865
|
+
children: problem
|
|
1866
|
+
})
|
|
1867
|
+
] });
|
|
1868
|
+
}
|
|
1869
|
+
//#endregion
|
|
1870
|
+
//#region src/client/index.ts
|
|
1871
|
+
const name = "dsh-fusion/client";
|
|
1872
|
+
const inject = ["slots"];
|
|
1873
|
+
const FUSION_PANEL_ID = "fusion";
|
|
1874
|
+
function apply(ctx) {
|
|
1875
|
+
ctx.inject(["locale"], (scope) => {
|
|
1876
|
+
scope.effect(() => bindLocale(scope.locale));
|
|
1877
|
+
});
|
|
1878
|
+
for (const tool of fusionControlTools) ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
|
|
1879
|
+
name: "tool.call.toolview",
|
|
1880
|
+
key: tool
|
|
1881
|
+
}, FusionTool));
|
|
1882
|
+
ctx.slots.inject("main", () => ctx.slots.register({
|
|
1883
|
+
name: "main",
|
|
1884
|
+
key: FUSION_PANEL_ID
|
|
1885
|
+
}, FusionSettings));
|
|
1886
|
+
ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
|
|
1887
|
+
name: "sidebar.panellist",
|
|
1888
|
+
id: FUSION_PANEL_ID,
|
|
1889
|
+
order: 40,
|
|
1890
|
+
label: () => "Fusion"
|
|
1891
|
+
}, FusionPanelIcon));
|
|
1892
|
+
ctx.inject(["modelDirectories"], (scope) => {
|
|
1893
|
+
scope.slots.inject("conversation.input.dock", () => scope.slots.register({
|
|
1894
|
+
name: "conversation.input.dock",
|
|
1895
|
+
id: "fusion-configuration",
|
|
1896
|
+
order: 20,
|
|
1897
|
+
inject: (sessionId) => {
|
|
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
|
+
}
|
|
1907
|
+
}, FusionHint));
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
//#endregion
|
|
1911
|
+
exports.FUSION_PANEL_ID = FUSION_PANEL_ID;
|
|
1912
|
+
exports.FusionPanelIcon = FusionPanelIcon;
|
|
1913
|
+
exports.apply = apply;
|
|
1914
|
+
exports.inject = inject;
|
|
1915
|
+
exports.name = name;
|
|
1916
|
+
return module.exports;
|
|
1917
|
+
}
|
|
1918
|
+
});
|
|
1919
|
+
|
|
1920
|
+
//# sourceMappingURL=client.js.map
|