@spzxg/cli-ui 0.6.7 → 0.6.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -1
- package/README.md +14 -4
- package/dist/{cli-ui-embed-CB7ibAtF.js → cli-ui-embed-CtZDwP8y.js} +15517 -15389
- package/dist/{cli-ui-index-BmwOLWFI.js → cli-ui-index-D9uwe5kv.js} +2 -2
- package/dist/{cli-ui-jszip.min-Cq0WjNYb.js → cli-ui-jszip.min-BkjzNeOa.js} +2 -2
- package/dist/{cli-ui-jszip.min-BKNOJnhm.js → cli-ui-jszip.min-D-YVhdLU.js} +1 -1
- package/dist/{cli-ui-xlsx-Sl7ypvSe.js → cli-ui-xlsx-c5AN8lBq.js} +1 -1
- package/dist/cli-ui.css +1 -1
- package/dist/cli-ui.js +1 -1
- package/package.json +1 -1
- package/src/chat/CliChat.tsx +35 -5
- package/src/chat/Composer.tsx +7 -7
- package/src/chat/RunControls.tsx +15 -8
- package/src/chat/draft.ts +7 -2
- package/src/chat/useExecutionConfig.ts +93 -0
- package/src/components.tsx +1 -1
- package/src/core/client.ts +21 -4
- package/src/core/config.ts +6 -2
- package/src/core/execution.ts +24 -0
- package/src/core/store.ts +10 -6
- package/src/core/types.ts +2 -0
- package/src/index.ts +1 -0
- package/src/operator/node/Chat.tsx +6 -3
- package/src/operator/node/Composer.tsx +16 -14
- package/src/operator/node/SessionView.tsx +7 -4
- package/src/operator.base.css +21 -19
- package/src/operator.css +21 -19
- package/src/styles.css +16 -15
- package/src/ui/QueuedMessages.tsx +0 -1
package/dist/cli-ui.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as a, b as i, d as r, e as t, f as o, h as C, i as l, j as n, k as m, m as d, l as u, n as w, o as B, p as f } from "./cli-ui-embed-
|
|
1
|
+
import { C as a, b as i, d as r, e as t, f as o, h as C, i as l, j as n, k as m, m as d, l as u, n as w, o as B, p as f } from "./cli-ui-embed-CtZDwP8y.js";
|
|
2
2
|
export {
|
|
3
3
|
a as CliBoard,
|
|
4
4
|
i as CliBrowserView,
|
package/package.json
CHANGED
package/src/chat/CliChat.tsx
CHANGED
|
@@ -28,6 +28,9 @@ import { QueuedMessages } from "../ui/QueuedMessages";
|
|
|
28
28
|
import { ReferenceCard } from "../ui/ReferenceCard";
|
|
29
29
|
import type { UploadDocument } from "../core/documentUpload";
|
|
30
30
|
import { GroupChat, type GroupChatAdapter } from "./GroupChat";
|
|
31
|
+
import { RunControls } from "./RunControls";
|
|
32
|
+
import { useExecutionConfig } from "./useExecutionConfig";
|
|
33
|
+
import { CenterError } from "../core/client";
|
|
31
34
|
|
|
32
35
|
/** onTurnEnd / onConversationEnd:浏览器里从实时流算出的结果通知,不依赖 center 的 callback.url。 */
|
|
33
36
|
export interface CliChatProps extends CliStoreOptions, ResultHandlers {
|
|
@@ -229,7 +232,17 @@ function ChatInner({ slots, passiveContext = true, configurationBusy, onClear, j
|
|
|
229
232
|
const task = (shownTaskId ? job?.tasks?.find((t) => t.id === shownTaskId) : undefined) ?? (shownTaskId || batch ? undefined : job?.tasks?.[0]);
|
|
230
233
|
const info = taskId ? job?.conversation?.conversations?.find((c) => c.task_id === taskId) : undefined;
|
|
231
234
|
const agent = task?.agent ?? conv?.turns[conv.turns.length - 1]?.agent ?? null;
|
|
232
|
-
const
|
|
235
|
+
const draftScope = store.draftScope ? `${store.draftScope}:${key ?? `new:${instanceId}`}` : key ?? `new:${instanceId}`;
|
|
236
|
+
const initialOptions = newConversation ?? store.getNewConversationOptions();
|
|
237
|
+
const execution = useExecutionConfig({ client: store.client, scope: draftScope,
|
|
238
|
+
target: key && !isNew ? key : null, running: running || batchPending,
|
|
239
|
+
enabled: showComposer && (cfg.composer.agentSelector || cfg.composer.executionMode),
|
|
240
|
+
initial: { ...(initialOptions?.agent ? { agent: initialOptions.agent } : {}), ...initialOptions?.executionConfig },
|
|
241
|
+
});
|
|
242
|
+
const agentText = labelOf(execution.config?.agent ?? agent);
|
|
243
|
+
const executionAgent = execution.config?.agents.find(item => item.id === execution.config?.agent);
|
|
244
|
+
const executionReadOnly = !!key && !isNew && !store.client.setExecutionConfig;
|
|
245
|
+
const settingsBusy = !!configurationBusy || execution.blocked;
|
|
233
246
|
const jobStatus = conv?.job?.status ?? job?.status ?? null;
|
|
234
247
|
// 对话式子任务没有「结束」:大任务结束了,再发一条会重新打开它。
|
|
235
248
|
// 支持续派的普通子任务同理:大任务结束不挡输入框。
|
|
@@ -306,7 +319,11 @@ function ChatInner({ slots, passiveContext = true, configurationBusy, onClear, j
|
|
|
306
319
|
// 本组件的新建参数随每次发送带上:放进共享 Provider 时也用自己的,不依赖仓库是谁建的。
|
|
307
320
|
const sendOpts = useRef({ newConversation, createConversation, businessIdFor, select: !controlled });
|
|
308
321
|
useLayoutEffect(() => { sendOpts.current = { newConversation, createConversation, businessIdFor, select: !controlled }; });
|
|
322
|
+
const prepareExecution = useRef(execution.prepareSend);
|
|
323
|
+
const reloadExecution = useRef(execution.reload);
|
|
324
|
+
useLayoutEffect(() => { prepareExecution.current = execution.prepareSend; reloadExecution.current = execution.reload; });
|
|
309
325
|
const send = useCallback(async (text: string) => {
|
|
326
|
+
const executionOptions = prepareExecution.current();
|
|
310
327
|
// 一次性任务:发给正在看的那个子任务(接着它聊)。
|
|
311
328
|
const id = latest.current.conv?.batch ? latest.current.runKey : latest.current.key;
|
|
312
329
|
// 看板里引用到这个对话的东西:合成一个「看板上下文」块跟着这条消息发出去;发失败就放回去。
|
|
@@ -325,9 +342,10 @@ function ChatInner({ slots, passiveContext = true, configurationBusy, onClear, j
|
|
|
325
342
|
let target: string;
|
|
326
343
|
try {
|
|
327
344
|
if(charCount(full)>MAX_MESSAGE_CHARS)throw new Error("加上看板背景后消息过长,请精简消息内容后重试。");
|
|
328
|
-
target = await store.send(id ?? null, full, sendOpts.current);
|
|
345
|
+
target = await store.send(id ?? null, full, { ...sendOpts.current, ...executionOptions });
|
|
329
346
|
} catch (error) {
|
|
330
347
|
if (refKey) { store.restoreRefs(refKey, refs); store.restoreViewing(refKey, viewing); }
|
|
348
|
+
if (error instanceof CenterError && error.code === "execution_config_conflict" && latest.current.key === refKey) await reloadExecution.current();
|
|
331
349
|
throw error;
|
|
332
350
|
}
|
|
333
351
|
if (target !== id && latest.current.key === refKey) go(target);
|
|
@@ -814,21 +832,33 @@ function ChatInner({ slots, passiveContext = true, configurationBusy, onClear, j
|
|
|
814
832
|
</div>
|
|
815
833
|
)}
|
|
816
834
|
{slots?.beforeComposer}
|
|
835
|
+
{showComposer && execution.error && <div className="control-error" role="status">{execution.error} {store.client.getExecutionConfig && <button type="button" className="btn btn-ghost btn-sm" disabled={execution.loading || execution.busy} onClick={() => void execution.reload()}>重新读取</button>}</div>}
|
|
817
836
|
{!showComposer && <QueuedMessages scopeKey={key ?? "new"} messages={queued} onCancel={handlers.onCancelQueued} attachmentUrl={handlers.attachmentUrl} />}
|
|
818
837
|
{showComposer && (
|
|
819
838
|
<Composer
|
|
820
839
|
uploadDocument={uploadDocument}
|
|
821
840
|
key={key ?? "new"}
|
|
822
|
-
draft={
|
|
841
|
+
draft={draftScope}
|
|
823
842
|
context={(!!shownRefs?.length || !!viewing) && <div className="cli-refs composer-references" aria-label="下一条消息的引用">
|
|
824
843
|
{viewing && <ReferenceCard key={`${key}:viewing:${viewing.id}`} className="reference-viewing" title={`正在看${viewing.label}`} text={viewing.text} disabled={composerDisabled || !!configurationBusy} removeLabel={`不带上 ${viewing.label}`} onRemove={() => { if (key) store.dismissViewing(key); chatRef.current?.querySelector<HTMLTextAreaElement>(".composer textarea")?.focus({ preventScroll: true }); }} />}
|
|
825
844
|
{(shownRefs ?? []).map(ref => <ReferenceCard key={`${key}:${ref.id}`} title={ref.kind === "message" && ref.label === "引用你的消息" ? "你" : ref.kind === "message" && ref.label === "引用智能体消息" ? "智能体" : ref.label} text={ref.text} disabled={composerDisabled || !!configurationBusy} removeLabel={`不带上 ${ref.label}`} onRemove={() => { if (key) store.removeRef(key, ref.id); chatRef.current?.querySelector<HTMLTextAreaElement>(".composer textarea")?.focus({ preventScroll: true }); }} />)}
|
|
826
845
|
</div>}
|
|
827
846
|
placeholder={fillTemplate(key && !isNew ? cfg.composer.placeholder : cfg.composer.placeholderNew, { title: convTitle, agent: agentText })}
|
|
828
|
-
disabled={composerDisabled ||
|
|
847
|
+
disabled={composerDisabled || settingsBusy}
|
|
829
848
|
disabledHint={cfg.composer.placeholderDisabled ? fillTemplate(cfg.composer.placeholderDisabled, { title: convTitle, agent: agentText }) : disabledHint}
|
|
830
849
|
running={running || batchPending}
|
|
831
|
-
toolbar={
|
|
850
|
+
toolbar={<>
|
|
851
|
+
<RunControls task={{ id: draftScope, agent: execution.config?.agent ?? agent ?? initialOptions?.agent ?? undefined,
|
|
852
|
+
model: execution.config?.model, reasoning: execution.config?.reasoning, permission_mode: execution.config?.permission_mode }}
|
|
853
|
+
agents={execution.config?.agents} caps={executionAgent?.capabilities} defaults={executionAgent?.defaults}
|
|
854
|
+
showAgent={cfg.composer.agentSelector} showPermission={cfg.composer.executionMode}
|
|
855
|
+
unavailableReason={executionReadOnly ? "当前接入仅支持读取执行设置。" : execution.error ?? (execution.loading ? "正在读取执行设置…" : undefined)}
|
|
856
|
+
disabled={composerDisabled || running || batchPending || !!configurationBusy || execution.loading || executionReadOnly}
|
|
857
|
+
onSetAgent={agent => execution.save({ agent, model: null, reasoning: null, permission_mode: null })}
|
|
858
|
+
onSetModel={model => execution.save({ model })} onSetReasoning={reasoning => execution.save({ reasoning })}
|
|
859
|
+
onSetPermission={permission_mode => execution.save({ permission_mode })} />
|
|
860
|
+
{slots?.composerToolbar}
|
|
861
|
+
</>}
|
|
832
862
|
queued={<QueuedMessages scopeKey={key ?? "new"} messages={queued} onCancel={handlers.onCancelQueued} attachmentUrl={handlers.attachmentUrl} />}
|
|
833
863
|
onSend={async text => { if (text.trim() === "/clear" && onClear) { onClear(); return; } const sentFrom = key; await send(text); if (!running && !batchPending && latest.current.key === sentFrom) scrollToBottom("auto"); }}
|
|
834
864
|
onStop={cfg.composer.stop && key && conv && runningTurn(conv) ? stop : undefined}
|
package/src/chat/Composer.tsx
CHANGED
|
@@ -109,10 +109,11 @@ export function Composer({ draft, placeholder, disabled, disabledHint, running,
|
|
|
109
109
|
const tooLong = total > MAX_MESSAGE_CHARS;
|
|
110
110
|
const reading = pending.some((p) => p.status === "reading");
|
|
111
111
|
// 运行中不允许排队时,等这一轮结束再发。
|
|
112
|
-
const canSend = !!message.trim() && !pending.length && !reading && !tooLong && !sending && (!running || options.queue);
|
|
112
|
+
const canSend = !!message.trim() && !pending.length && !reading && !tooLong && !sending && !stopping && (!running || options.queue);
|
|
113
|
+
const showStop = running && !!onStop && !sending && (stopping || disabled || !options.queue || !message.trim());
|
|
113
114
|
|
|
114
115
|
const submit = async () => {
|
|
115
|
-
if (!canSend || disabled || readComposerDraft(draft).sending) return;
|
|
116
|
+
if (!canSend || disabled || stopLock.current || readComposerDraft(draft).sending) return;
|
|
116
117
|
const submitted = JSON.stringify([val, blocks]);
|
|
117
118
|
setSending(true);
|
|
118
119
|
setError(null);
|
|
@@ -138,7 +139,7 @@ export function Composer({ draft, placeholder, disabled, disabledHint, running,
|
|
|
138
139
|
stopLock.current = true; setStopping(true); setError(null);
|
|
139
140
|
try { await onStop(); } catch (error) { stopLock.current = false; setStopping(false); setError(error instanceof Error ? error.message : "停止失败,请重试"); }
|
|
140
141
|
}}
|
|
141
|
-
disabled={stopping} aria-busy={stopping} aria-label={stopping ? "正在停止" : "停止当前轮次"} title={stopping ? "正在停止…" : "停止当前轮次"}>{Icon.stop()}</button>
|
|
142
|
+
disabled={stopping} aria-busy={stopping} aria-label={stopping ? "正在停止" : "停止当前轮次"} title={stopping ? "正在停止…" : "停止当前轮次"}>{stopping ? <span className="spinner" /> : Icon.stop()}</button>
|
|
142
143
|
);
|
|
143
144
|
|
|
144
145
|
return (
|
|
@@ -224,12 +225,11 @@ export function Composer({ draft, placeholder, disabled, disabledHint, running,
|
|
|
224
225
|
onClick={async () => { if (controlLock.current) return; controlLock.current = true; setControlling(true); try { await taskControl.onToggle(); } catch (error) { setError(error instanceof Error ? error.message : "执行控制失败,请重试"); } finally { controlLock.current = false; setControlling(false); } }}>
|
|
225
226
|
{taskControl.paused ? Icon.play() : Icon.pause()}<span>{controlling ? "处理中…" : taskControl.paused ? "继续" : "暂停"}</span>
|
|
226
227
|
</button>}
|
|
227
|
-
{(!running || options.queue) && <button type="button" className={`send cli-send${running ? " queue" : ""}`} disabled={disabled || !canSend} aria-busy={sending}
|
|
228
|
+
{showStop ? stopButton : (!running || options.queue) && <button type="button" className={`send cli-send${running ? " queue" : ""}`} disabled={disabled || !canSend} aria-busy={sending}
|
|
228
229
|
onClick={() => void submit()} aria-label={sending ? "正在发送" : running ? "排队发送" : "发送消息"}
|
|
229
|
-
title={taskControl?.paused ? "排队发送,继续任务后依次执行" : running ? "排队发送,当前轮次结束后执行" : "发送消息(Enter)"}>
|
|
230
|
-
{
|
|
230
|
+
title={sending ? "发送中…" : taskControl?.paused ? "排队发送,继续任务后依次执行" : running ? "排队发送,当前轮次结束后执行" : "发送消息(Enter)"}>
|
|
231
|
+
{sending ? <span className="spinner" /> : Icon.send()}
|
|
231
232
|
</button>}
|
|
232
|
-
{stopButton}
|
|
233
233
|
</div>
|
|
234
234
|
</div>
|
|
235
235
|
</div>
|
package/src/chat/RunControls.tsx
CHANGED
|
@@ -21,6 +21,9 @@ export type RunControlProps = {
|
|
|
21
21
|
caps?: RunCapabilities;
|
|
22
22
|
defaults?: { model?: string | null; reasoning?: string | null; permission_mode?: string | null };
|
|
23
23
|
disabled: boolean;
|
|
24
|
+
showAgent?: boolean;
|
|
25
|
+
showPermission?: boolean;
|
|
26
|
+
unavailableReason?: string;
|
|
24
27
|
onSetModel: (value: string | null) => Promise<void>;
|
|
25
28
|
onSetReasoning: (value: string | null) => Promise<void>;
|
|
26
29
|
onSetPermission: (value: string | null) => Promise<void>;
|
|
@@ -29,7 +32,7 @@ export type RunControlProps = {
|
|
|
29
32
|
|
|
30
33
|
// Both the pre-start view and composer use the same capability-driven controls.
|
|
31
34
|
// Await persistence before allowing Send so the next turn uses the chosen value.
|
|
32
|
-
export function RunControls({ task, agents, onSetAgent, caps, defaults, disabled, onSetModel, onSetReasoning, onSetPermission, onBusyChange }: RunControlProps) {
|
|
35
|
+
export function RunControls({ task, agents, onSetAgent, caps, defaults, disabled, showAgent = true, showPermission = true, unavailableReason, onSetModel, onSetReasoning, onSetPermission, onBusyChange }: RunControlProps) {
|
|
33
36
|
const [open, setOpen] = useState<"model" | "permission" | null>(null);
|
|
34
37
|
const [modelPage, setModelPage] = useState<"agents" | "settings" | "models">("agents");
|
|
35
38
|
const [switching, setSwitching] = useState<{ agent: string; saved: boolean } | null>(null);
|
|
@@ -79,6 +82,10 @@ export function RunControls({ task, agents, onSetAgent, caps, defaults, disabled
|
|
|
79
82
|
finally { release(); onBusyChange?.(false); }
|
|
80
83
|
};
|
|
81
84
|
const locked = disabled || busy || !!switching || !caps;
|
|
85
|
+
const permissionAvailable = !!caps?.permissionModes.length;
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if ((open === "model" && (!showAgent || !caps)) || (open === "permission" && (!showPermission || !permissionAvailable))) setOpen(null);
|
|
88
|
+
}, [open, showAgent, showPermission, caps, permissionAvailable]);
|
|
82
89
|
const pickAgent = async (agent: string) => {
|
|
83
90
|
if (locked || !onSetAgent) return;
|
|
84
91
|
setError(""); setCustom("");
|
|
@@ -108,11 +115,11 @@ export function RunControls({ task, agents, onSetAgent, caps, defaults, disabled
|
|
|
108
115
|
return groups;
|
|
109
116
|
}, []);
|
|
110
117
|
return <>
|
|
111
|
-
{
|
|
112
|
-
<button className={`comp-setting permission${effectivePermission === "bypassPermissions" ? " elevated" : ""}`} disabled={locked} aria-label="权限模式" aria-expanded={open === "permission"} aria-haspopup="dialog" title={disabled ? "本轮结束后可修改权限" : permission?.sub} onClick={(e) => { e.stopPropagation(); setError(""); setOpen(open === "permission" ? null : "permission"); }}>
|
|
118
|
+
{showPermission && <div className="comp-control">
|
|
119
|
+
<button className={`comp-setting permission${effectivePermission === "bypassPermissions" ? " elevated" : ""}`} disabled={locked || !permissionAvailable} aria-label="权限模式" aria-expanded={open === "permission"} aria-haspopup="dialog" title={unavailableReason || (!permissionAvailable ? "当前智能体暂未提供执行模式设置" : disabled ? "本轮结束后可修改权限" : permission?.sub)} onClick={(e) => { e.stopPropagation(); setError(""); setOpen(open === "permission" ? null : "permission"); }}>
|
|
113
120
|
{Icon.lock()} <span>{permission?.value ? permission.label : "默认权限"}</span>{Icon.chevDown()}
|
|
114
121
|
</button>
|
|
115
|
-
{open === "permission" && <Popover onClose={() => setOpen(null)} className="composer-popover composer-permission-popover" ariaLabel="权限模式" placement="top" align="start">
|
|
122
|
+
{open === "permission" && permissionAvailable && <Popover onClose={() => setOpen(null)} className="composer-popover composer-permission-popover" ariaLabel="权限模式" placement="top" align="start">
|
|
116
123
|
<div className="composer-menu-heading">权限模式</div>
|
|
117
124
|
<p className="composer-menu-note">保存后用于下一轮</p>
|
|
118
125
|
{permissions.map((p) => <button key={p.value ?? "default"} className="pop-item composer-option run-permission-option" disabled={locked} aria-pressed={(task.permission_mode ?? null) === p.value} onClick={() => void change(() => onSetPermission(p.value))}>
|
|
@@ -124,11 +131,11 @@ export function RunControls({ task, agents, onSetAgent, caps, defaults, disabled
|
|
|
124
131
|
{error && <div className="control-error" role="alert">{error}</div>}
|
|
125
132
|
</Popover>}
|
|
126
133
|
</div>}
|
|
127
|
-
<div className="comp-control comp-model">
|
|
128
|
-
<button className="comp-setting" disabled={locked} aria-label="智能体设置" aria-expanded={open === "model"} aria-haspopup="dialog" title={disabled ? "本轮结束后可修改智能体设置" : `当前智能体:${currentAgent}`} onClick={(e) => { e.stopPropagation(); setError(""); setCustom(""); setModelPage(hasAgentPicker ? "agents" : "settings"); setOpen(open === "model" ? null : "model"); }}>
|
|
134
|
+
{showAgent && <div className="comp-control comp-model">
|
|
135
|
+
<button className="comp-setting" disabled={locked} aria-label="智能体设置" aria-expanded={open === "model"} aria-haspopup="dialog" title={unavailableReason || (!caps ? "当前服务暂未提供智能体设置" : disabled ? "本轮结束后可修改智能体设置" : `当前智能体:${currentAgent}`)} onClick={(e) => { e.stopPropagation(); setError(""); setCustom(""); setModelPage(hasAgentPicker ? "agents" : "settings"); setOpen(open === "model" ? null : "model"); }}>
|
|
129
136
|
<span className="comp-model-name">{currentAgent}</span>{Icon.chevDown()}
|
|
130
137
|
</button>
|
|
131
|
-
{open === "model" && <Popover onClose={() => setOpen(null)} className="composer-popover composer-agent-popover" ariaLabel={modelPage === "agents" ? "选择智能体" : modelPage === "models" ? "选择模型" : "模型与推理设置"} placement="top" align="end">
|
|
138
|
+
{open === "model" && caps && <Popover onClose={() => setOpen(null)} className="composer-popover composer-agent-popover" ariaLabel={modelPage === "agents" ? "选择智能体" : modelPage === "models" ? "选择模型" : "模型与推理设置"} placement="top" align="end">
|
|
132
139
|
{modelPage === "agents" ? <div ref={agentListRef} className="run-agent-list">
|
|
133
140
|
<div className="composer-menu-heading">选择智能体</div>
|
|
134
141
|
{availableAgents.map(agent => <button key={agent.id} className="pop-item composer-option run-agent-option" disabled={locked || agent.disabled} aria-pressed={task.agent === agent.id} onClick={() => void pickAgent(agent.id)}>
|
|
@@ -184,6 +191,6 @@ export function RunControls({ task, agents, onSetAgent, caps, defaults, disabled
|
|
|
184
191
|
{(busy || switching) && <div className="composer-menu-note" role="status">{switching ? "正在切换智能体…" : "正在保存…"}</div>}
|
|
185
192
|
{error && <div className="control-error" role="alert">{error}</div>}
|
|
186
193
|
</Popover>}
|
|
187
|
-
</div>
|
|
194
|
+
</div>}
|
|
188
195
|
</>;
|
|
189
196
|
}
|
package/src/chat/draft.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { useSyncExternalStore } from "react";
|
|
2
2
|
import type { TextBlock } from "../core/blocks";
|
|
3
3
|
import type { UploadPhase } from "../core/documentUpload";
|
|
4
|
+
import type { ExecutionOptions } from "../core/execution";
|
|
4
5
|
export type PendingFile = { key: string; name: string; status: "reading" | "error"; error?: string; progress?: string; phase?: UploadPhase; bytes?: number; size?: number };
|
|
5
|
-
export interface ComposerDraft { text: string; blocks: Array<TextBlock & { key: string }>; pending: PendingFile[]; sending: boolean; error: string | null; revision: number }
|
|
6
|
+
export interface ComposerDraft { text: string; blocks: Array<TextBlock & { key: string }>; pending: PendingFile[]; sending: boolean; error: string | null; revision: number; executionConfig?: ExecutionOptions; executionUncertain?: boolean }
|
|
6
7
|
const empty = (): ComposerDraft => ({ text: "", blocks: [], pending: [], sending: false, error: null, revision: 0 });
|
|
7
8
|
const server = empty();
|
|
8
9
|
const entries = new Map<string, { state: ComposerDraft; stamp: string }>();
|
|
@@ -18,6 +19,10 @@ export function readComposerDraft(id: string): ComposerDraft {
|
|
|
18
19
|
state.text = localStorage.getItem(textKey(id)) ?? "";
|
|
19
20
|
const data = JSON.parse(localStorage.getItem(dataKey(id)) ?? "null");
|
|
20
21
|
if (data?.version === 1) {
|
|
22
|
+
if (data.executionUncertain === true) state.executionUncertain = true;
|
|
23
|
+
if (data.executionConfig && typeof data.executionConfig === "object") state.executionConfig = Object.fromEntries(
|
|
24
|
+
Object.entries(data.executionConfig).filter(([key, value]) => ["agent", "model", "reasoning", "permission_mode"].includes(key) && (typeof value === "string" || value === null)),
|
|
25
|
+
);
|
|
21
26
|
state.blocks = Array.isArray(data.blocks) ? data.blocks.filter((b: TextBlock & { key: string }) => typeof b?.text === "string" && typeof b?.key === "string") : [];
|
|
22
27
|
state.pending = Array.isArray(data.pending) ? data.pending.map((p: PendingFile) => ({ ...p, status: "error", error: p.phase
|
|
23
28
|
? `${p.error || "页面重载中断了上传。"}${p.error?.includes("重新选择文件") ? "" : " 请重新选择文件。"}`
|
|
@@ -32,7 +37,7 @@ export function updateComposerDraft(id: string, update: (state: ComposerDraft) =
|
|
|
32
37
|
const state = { ...update(previous), revision: previous.revision + 1 };
|
|
33
38
|
try {
|
|
34
39
|
if (state.text) localStorage.setItem(textKey(id), state.text); else localStorage.removeItem(textKey(id));
|
|
35
|
-
if (state.blocks.length || state.pending.length) localStorage.setItem(dataKey(id), JSON.stringify({ version: 1, blocks: state.blocks, pending: state.pending })); else localStorage.removeItem(dataKey(id));
|
|
40
|
+
if (state.blocks.length || state.pending.length || state.executionConfig || state.executionUncertain) localStorage.setItem(dataKey(id), JSON.stringify({ version: 1, blocks: state.blocks, pending: state.pending, executionConfig: state.executionConfig, executionUncertain: state.executionUncertain })); else localStorage.removeItem(dataKey(id));
|
|
36
41
|
} catch { /* Keep the in-memory draft even if storage is full. */ }
|
|
37
42
|
entries.set(id, { state, stamp: stamp(id) });
|
|
38
43
|
listeners.forEach(listener => listener());
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
2
|
+
import { CenterError, type ConversationBackend } from "../core/client";
|
|
3
|
+
import type { ExecutionConfiguration, ExecutionOptions } from "../core/execution";
|
|
4
|
+
import { isConfigurationBusy, useConfigurationBusy } from "./configuration";
|
|
5
|
+
import { readComposerDraft, updateComposerDraft, useComposerDraft } from "./draft";
|
|
6
|
+
|
|
7
|
+
const errorText = (error: unknown) => error instanceof CenterError && [404, 405, 501].includes(error.status)
|
|
8
|
+
? "当前服务尚未支持执行设置,请升级配套服务。"
|
|
9
|
+
: error instanceof Error ? error.message : "执行设置读取失败,请重试。";
|
|
10
|
+
|
|
11
|
+
function validate(result: ExecutionConfiguration) {
|
|
12
|
+
if (!result || typeof result.agent !== "string" || !Number.isSafeInteger(result.revision) || result.revision < 0 || !Array.isArray(result.agents)
|
|
13
|
+
|| result.agents.some(agent => typeof agent?.id !== "string" || typeof agent.label !== "string" || !agent.capabilities
|
|
14
|
+
|| [agent.capabilities.models, agent.capabilities.reasoningOptions, agent.capabilities.permissionModes].some(options => !Array.isArray(options)
|
|
15
|
+
|| options.some(option => typeof option?.value !== "string" || typeof option.label !== "string")))) {
|
|
16
|
+
throw new Error("服务未返回有效的执行设置,请检查配套服务版本。");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 按对话隔离读取与保存,不轮询;换对话后的迟到响应不覆盖当前选择。 */
|
|
21
|
+
export function useExecutionConfig({ client, scope, target, enabled, initial, running }: {
|
|
22
|
+
client: ConversationBackend; scope: string; target: string | null; enabled: boolean;
|
|
23
|
+
initial?: ExecutionOptions; running: boolean;
|
|
24
|
+
}) {
|
|
25
|
+
const [view, setView] = useState<{ scope: string; config?: ExecutionConfiguration; error?: string; loading: boolean; saveFailed?: boolean }>({ scope, loading: false });
|
|
26
|
+
const current = useRef(scope);
|
|
27
|
+
const request = useRef(0);
|
|
28
|
+
const mounted = useRef(true);
|
|
29
|
+
useLayoutEffect(() => { current.current = scope; request.current++; }, [scope]);
|
|
30
|
+
useEffect(() => { mounted.current = true; return () => { mounted.current = false; request.current++; }; }, []);
|
|
31
|
+
const draft = useComposerDraft(scope);
|
|
32
|
+
const busy = useConfigurationBusy(scope);
|
|
33
|
+
const config = view.scope === scope ? view.config : undefined;
|
|
34
|
+
const reload = useCallback(async () => {
|
|
35
|
+
const serial = ++request.current;
|
|
36
|
+
const active = () => mounted.current && current.current === scope && request.current === serial;
|
|
37
|
+
if (!enabled && !readComposerDraft(scope).executionUncertain) return;
|
|
38
|
+
if (!client.getExecutionConfig) {
|
|
39
|
+
setView({ scope, loading: false, error: "当前接入尚未提供执行设置。" });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
setView(value => ({ ...(value.scope === scope ? value : {}), scope, loading: true }));
|
|
43
|
+
try {
|
|
44
|
+
const result = await client.getExecutionConfig(target);
|
|
45
|
+
validate(result);
|
|
46
|
+
if (active()) {
|
|
47
|
+
if (!isConfigurationBusy(scope) && readComposerDraft(scope).executionUncertain) updateComposerDraft(scope, draft => ({ ...draft, executionUncertain: false }));
|
|
48
|
+
setView({ scope, config: result, loading: false });
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (active()) setView(value => ({ ...(value.scope === scope ? value : {}), scope, error: errorText(error), loading: false }));
|
|
52
|
+
}
|
|
53
|
+
}, [client, scope, target, enabled]);
|
|
54
|
+
useEffect(() => { void reload(); return () => { request.current++; }; }, [reload]);
|
|
55
|
+
const wasRunning = useRef(running);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (wasRunning.current && !running && !isConfigurationBusy(scope)) void reload();
|
|
58
|
+
wasRunning.current = running;
|
|
59
|
+
}, [running, reload, scope]);
|
|
60
|
+
|
|
61
|
+
const local = target === null ? { ...initial, ...draft.executionConfig } : undefined;
|
|
62
|
+
const selected = config ? { ...config, ...local } : undefined;
|
|
63
|
+
const save = async (changes: ExecutionOptions) => {
|
|
64
|
+
if (!config || !enabled || running) throw new Error("当前无法修改执行设置,请稍后重试。");
|
|
65
|
+
if (target === null) {
|
|
66
|
+
updateComposerDraft(scope, value => ({ ...value, executionConfig: { agent: config.agent, ...initial, ...value.executionConfig, ...changes } }));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (!client.setExecutionConfig) throw new Error("当前接入尚未提供保存执行设置的接口。");
|
|
70
|
+
// An earlier GET must not overwrite the response to this save.
|
|
71
|
+
const serial = ++request.current;
|
|
72
|
+
updateComposerDraft(scope, draft => ({ ...draft, executionUncertain: true }));
|
|
73
|
+
try {
|
|
74
|
+
const result = await client.setExecutionConfig(target, changes, config.revision);
|
|
75
|
+
validate(result);
|
|
76
|
+
updateComposerDraft(scope, draft => ({ ...draft, executionUncertain: false }));
|
|
77
|
+
if (mounted.current && current.current === scope && request.current === serial) setView({ scope, config: result, loading: false });
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (mounted.current && current.current === scope && request.current === serial) setView({ scope, config, loading: false, saveFailed: true, error: `${errorText(error)} 请重新读取设置后再发送。` });
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
config: selected, busy, loading: view.scope === scope && view.loading,
|
|
85
|
+
error: view.scope === scope && (enabled || view.saveFailed || draft.executionUncertain) ? view.error ?? (draft.executionUncertain && !busy ? "执行设置尚未确认,请重新读取后再发送。" : undefined) : undefined,
|
|
86
|
+
blocked: busy || !!draft.executionUncertain || (view.scope === scope && !!view.saveFailed), reload, save,
|
|
87
|
+
/** Read at submit time, including changes committed before React paints. */
|
|
88
|
+
prepareSend: () => {
|
|
89
|
+
if (isConfigurationBusy(scope) || readComposerDraft(scope).executionUncertain || (view.scope === scope && view.saveFailed)) throw new Error("请等待执行设置保存或重新读取设置后再发送。");
|
|
90
|
+
return target === null ? { executionConfig: readComposerDraft(scope).executionConfig } : { expectedExecutionRevision: config?.revision };
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
package/src/components.tsx
CHANGED
|
@@ -9,5 +9,5 @@ export interface CliMsgListProps extends CenterMsgListProps { operator?: Compone
|
|
|
9
9
|
export interface CliChatProps extends CenterChatProps { operator?: ComponentProps<typeof SessionView> }
|
|
10
10
|
export interface CliBoardProps extends CenterBoardProps { operator?: ComponentProps<typeof SessionRail> }
|
|
11
11
|
export function CliMsgList(props: CliMsgListProps) { return props.operator ? <TasksColumn {...props.operator}/> : <CenterMsgList {...props}/>; }
|
|
12
|
-
export function CliChat(props: CliChatProps) { return props.operator ? <SessionView {...props.operator} board={{...props.operator.board,autoContext:props.operator.board?.autoContext??props.passiveContext,onFilesChange:props.operator.board?.onFilesChange??props.onFilesChange,onFilesWatch:props.operator.board?.onFilesWatch??props.onFilesWatch,onBoardContextChange:props.operator.board?.onBoardContextChange??props.onBoardContextChange}}/> : <CenterChat {...props}/>; }
|
|
12
|
+
export function CliChat(props: CliChatProps) { return props.operator ? <SessionView {...props.operator} composer={props.config?.composer ?? props.operator.composer} board={{...props.operator.board,autoContext:props.operator.board?.autoContext??props.passiveContext,onFilesChange:props.operator.board?.onFilesChange??props.onFilesChange,onFilesWatch:props.operator.board?.onFilesWatch??props.onFilesWatch,onBoardContextChange:props.operator.board?.onBoardContextChange??props.onBoardContextChange}}/> : <CenterChat {...props}/>; }
|
|
13
13
|
export function CliBoard(props: CliBoardProps) { return props.operator ? <SessionRail key={props.operator.task.id} {...props.operator} tabs={props.operator.tabs ?? props.tabs} availableTabs={props.operator.availableTabs ?? props.availableTabs} defaultTab={props.operator.defaultTab ?? props.defaultTab} autoContext={props.operator.autoContext ?? props.autoQuoteOpenFile} watchFiles={props.operator.watchFiles ?? props.watchFiles} fileWatchIntervalMs={props.operator.fileWatchIntervalMs ?? props.fileWatchIntervalMs} onFilesChange={props.operator.onFilesChange ?? props.onFilesChange} onFilesWatch={props.operator.onFilesWatch ?? props.onFilesWatch} onBoardContextChange={props.operator.onBoardContextChange ?? props.onBoardContextChange} onEvent={props.operator.onEvent ?? props.onEvent}/> : <CenterBoard {...props}/>; }
|
package/src/core/client.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { decodeText } from "./textDecode";
|
|
|
8
8
|
import { createDocumentUploader, extractDocumentReferences, documentRequest, isTaskUploadPath, type UploadDocument, type DocumentUploadOptions, type DocumentReceipt } from "./documentUpload";
|
|
9
9
|
import { syncFile, type FileSyncTransport } from "../board/fileSync";
|
|
10
10
|
import { createFileSyncTransport } from "../board/fileSyncTransport";
|
|
11
|
+
import type { ExecutionConfiguration, ExecutionOptions } from "./execution";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* center 服务(cli-task-center)对话任务接口的浏览器端客户端。组件的数据全部从这里来。
|
|
@@ -95,6 +96,10 @@ const ERROR_TEXT: Record<string, string> = {
|
|
|
95
96
|
* 带 ? 的是可选能力:后端不提供,界面就按 Operator 在「没有这个能力」时的样子显示(例如提问的回答改走下一条消息)。
|
|
96
97
|
*/
|
|
97
98
|
export interface ConversationBackend {
|
|
99
|
+
/** 无 key 时读取新对话可用设置;有 key 时读取该对话的已保存设置。 */
|
|
100
|
+
getExecutionConfig?(key?: string | null): Promise<ExecutionConfiguration>;
|
|
101
|
+
/** 保存成功才切换界面;服务端用 revision 防止并发覆盖。 */
|
|
102
|
+
setExecutionConfig?(key: string, changes: ExecutionOptions, expectedRevision: number): Promise<ExecutionConfiguration>;
|
|
98
103
|
/** Upload original HTML/Markdown without embedding document bytes in a message. */
|
|
99
104
|
uploadDocument?: UploadDocument;
|
|
100
105
|
baseUrl: string;
|
|
@@ -109,7 +114,7 @@ export interface ConversationBackend {
|
|
|
109
114
|
history?(jobId: string, remote: string, before: string, limit?: number): Promise<{ messages: Message[]; older: number }>;
|
|
110
115
|
createConversation(firstMessage: string, opts?: NewConversationOptions & { businessId?: string }): Promise<{ id: string; job_id: string; status: string; task_ids?: string[]; turn: CenterTurn }>;
|
|
111
116
|
/** taskId:发给对话任务里的哪个对话,不给 = 第一个。 */
|
|
112
|
-
sendMessage(jobId: string, text: string, opts?: { businessId?: string; timeoutSeconds?: number; taskId?: string }): Promise<SendResult>;
|
|
117
|
+
sendMessage(jobId: string, text: string, opts?: { businessId?: string; timeoutSeconds?: number; taskId?: string; expectedExecutionRevision?: number }): Promise<SendResult>;
|
|
113
118
|
/**
|
|
114
119
|
* 一次性任务里的对话式子任务接着聊:续派一条 mode=conversation、continue_from 这个子任务的子任务(接在同一个 Agent 会话里)。
|
|
115
120
|
* 返回新子任务的 id。
|
|
@@ -119,7 +124,7 @@ export interface ConversationBackend {
|
|
|
119
124
|
pauseTask?(taskId: string): Promise<CenterTask>;
|
|
120
125
|
resumeTask?(taskId: string): Promise<CenterTask>;
|
|
121
126
|
/** 在对话任务里新开一个对话(同一节点、同一工作区),text 是它的第一条消息。 */
|
|
122
|
-
openConversation?(jobId: string, text: string, opts?: { title?: string; agent?: string; browser?: boolean; businessId?: string; timeoutSeconds?: number }): Promise<SendResult>;
|
|
127
|
+
openConversation?(jobId: string, text: string, opts?: { title?: string; agent?: string; executionConfig?: ExecutionOptions; browser?: boolean; businessId?: string; timeoutSeconds?: number }): Promise<SendResult>;
|
|
123
128
|
cancelTurn(jobId: string, turnId: string): Promise<CenterTurn>;
|
|
124
129
|
closeConversation(jobId: string): Promise<{ id: string; status: string }>;
|
|
125
130
|
/** 只关掉对话任务里的一个对话(别的对话和对话任务照常)。 */
|
|
@@ -297,9 +302,18 @@ export function createCenterClient(options: CenterClientOptions = {}): Conversat
|
|
|
297
302
|
|
|
298
303
|
const post = <T>(path: string, body: unknown = {}) => request<T>(path, { method: "POST", body: JSON.stringify(body) });
|
|
299
304
|
const id = encodeURIComponent;
|
|
305
|
+
const executionPath = (key?: string | null) => {
|
|
306
|
+
if (!key) return "/api/execution-config";
|
|
307
|
+
const ref = parseConvKey(key);
|
|
308
|
+
return `/api/jobs/${id(ref.jobId)}/execution-config${ref.taskId && ref.taskId !== NEW_TASK ? `?task_id=${id(ref.taskId)}` : ""}`;
|
|
309
|
+
};
|
|
300
310
|
|
|
301
311
|
return {
|
|
302
312
|
baseUrl: base,
|
|
313
|
+
getExecutionConfig: key => request<ExecutionConfiguration>(executionPath(key)),
|
|
314
|
+
setExecutionConfig: (key, changes, expectedRevision) => request<ExecutionConfiguration>(executionPath(key), {
|
|
315
|
+
method: "PATCH", body: JSON.stringify({ ...changes, expected_revision: expectedRevision }),
|
|
316
|
+
}),
|
|
303
317
|
uploadDocument: createDocumentUploader(doFetch, `${base}/api/file-sync`, headers),
|
|
304
318
|
getJob: (jobId: string) => request<CenterJob>(`/api/jobs/${id(jobId)}`),
|
|
305
319
|
listTurns: (jobId: string, query: { afterSeq?: number; limit?: number; taskId?: string } = {}) => {
|
|
@@ -345,6 +359,7 @@ export function createCenterClient(options: CenterClientOptions = {}): Conversat
|
|
|
345
359
|
...(extractDocumentReferences(firstMessage).attachments ? { attachments: extractDocumentReferences(firstMessage).attachments } : {}),
|
|
346
360
|
...(opts.businessId ? { business_id: opts.businessId } : {}),
|
|
347
361
|
...(opts.agent ? { agent: opts.agent } : {}),
|
|
362
|
+
...(opts.executionConfig ? { execution_config: opts.executionConfig } : {}),
|
|
348
363
|
...(opts.timeoutSeconds ? { timeout_seconds: opts.timeoutSeconds } : {}),
|
|
349
364
|
...(opts.browser !== undefined ? { browser: opts.browser } : {}),
|
|
350
365
|
...(opts.browserPlaybook !== undefined ? { browser_playbook: opts.browserPlaybook } : {}),
|
|
@@ -353,10 +368,11 @@ export function createCenterClient(options: CenterClientOptions = {}): Conversat
|
|
|
353
368
|
});
|
|
354
369
|
},
|
|
355
370
|
/** 带 business_id 时 center 按它去重,网络超时可以放心重发同一条。 */
|
|
356
|
-
sendMessage: (jobId: string, text: string, opts: { businessId?: string; timeoutSeconds?: number; taskId?: string } = {}) =>
|
|
371
|
+
sendMessage: (jobId: string, text: string, opts: { businessId?: string; timeoutSeconds?: number; taskId?: string; expectedExecutionRevision?: number } = {}) =>
|
|
357
372
|
post<SendResult>(`/api/jobs/${id(jobId)}/messages`, {
|
|
358
373
|
...extractDocumentReferences(text),
|
|
359
374
|
...(opts.taskId ? { task_id: opts.taskId } : {}),
|
|
375
|
+
...(opts.expectedExecutionRevision !== undefined ? { expected_execution_revision: opts.expectedExecutionRevision } : {}),
|
|
360
376
|
...(opts.businessId ? { business_id: opts.businessId } : {}),
|
|
361
377
|
...(opts.timeoutSeconds ? { timeout_seconds: opts.timeoutSeconds } : {}),
|
|
362
378
|
}),
|
|
@@ -370,11 +386,12 @@ export function createCenterClient(options: CenterClientOptions = {}): Conversat
|
|
|
370
386
|
pauseTask: (taskId: string) => post<CenterTask>(`/api/tasks/${id(taskId)}/pause`, {}),
|
|
371
387
|
resumeTask: (taskId: string) => post<CenterTask>(`/api/tasks/${id(taskId)}/resume`, {}),
|
|
372
388
|
/** 同一个 business_id 重发只开一个对话。 */
|
|
373
|
-
openConversation: (jobId: string, text: string, opts: { title?: string; agent?: string; browser?: boolean; businessId?: string; timeoutSeconds?: number } = {}) =>
|
|
389
|
+
openConversation: (jobId: string, text: string, opts: { title?: string; agent?: string; executionConfig?: ExecutionOptions; browser?: boolean; businessId?: string; timeoutSeconds?: number } = {}) =>
|
|
374
390
|
post<SendResult>(`/api/jobs/${id(jobId)}/conversations`, {
|
|
375
391
|
...extractDocumentReferences(text),
|
|
376
392
|
...(opts.title ? { title: opts.title } : {}),
|
|
377
393
|
...(opts.agent ? { agent: opts.agent } : {}),
|
|
394
|
+
...(opts.executionConfig ? { execution_config: opts.executionConfig } : {}),
|
|
378
395
|
...(opts.browser !== undefined ? { browser: opts.browser } : {}),
|
|
379
396
|
...(opts.businessId ? { business_id: opts.businessId } : {}),
|
|
380
397
|
...(opts.timeoutSeconds ? { timeout_seconds: opts.timeoutSeconds } : {}),
|
package/src/core/config.ts
CHANGED
|
@@ -45,6 +45,10 @@ export interface CliChatConfig {
|
|
|
45
45
|
charCount?: boolean;
|
|
46
46
|
/** 文件上传(按钮、拖拽、粘贴文件)。只收文档:Word、Excel、Markdown、TXT、HTML、CSV。 */
|
|
47
47
|
fileUpload?: boolean;
|
|
48
|
+
/** 智能体选择入口,默认显示;缺少服务端能力时禁用。 */
|
|
49
|
+
agentSelector?: boolean;
|
|
50
|
+
/** 执行模式选择入口,默认显示;缺少服务端能力时禁用。 */
|
|
51
|
+
executionMode?: boolean;
|
|
48
52
|
/** 「文件」按钮的文字。 */
|
|
49
53
|
fileButtonText?: string;
|
|
50
54
|
/** 执行控制:普通对话停止当前轮次,一次性子任务暂停 / 继续。 */
|
|
@@ -87,7 +91,7 @@ export interface ResolvedChatConfig {
|
|
|
87
91
|
header: { show: boolean; crumb: boolean; export: boolean; title: boolean; agent: boolean; usage: boolean; model: boolean; status: boolean; close: boolean };
|
|
88
92
|
composer: {
|
|
89
93
|
show: boolean; placeholder: string; placeholderNew: string; placeholderRunning: string; placeholderDisabled: string | null;
|
|
90
|
-
hints: boolean; charCount: boolean; fileUpload: boolean; fileButtonText: string; stop: boolean; queue: boolean;
|
|
94
|
+
hints: boolean; charCount: boolean; fileUpload: boolean; agentSelector: boolean; executionMode: boolean; fileButtonText: string; stop: boolean; queue: boolean;
|
|
91
95
|
};
|
|
92
96
|
transcript: { nav: boolean; sessionLabel: boolean; timestamps: boolean; initialPrompt: boolean; initialBadge: boolean; tools: boolean; runningIndicator: boolean; status: boolean };
|
|
93
97
|
labels: { user: string; agent: string };
|
|
@@ -108,7 +112,7 @@ export const DEFAULT_CHAT_CONFIG: ResolvedChatConfig = {
|
|
|
108
112
|
placeholderNew: "输入消息,开始新对话…",
|
|
109
113
|
placeholderRunning: "排队一条后续消息…(本轮结束后发送)",
|
|
110
114
|
placeholderDisabled: null,
|
|
111
|
-
hints:
|
|
115
|
+
hints: false, charCount: false, fileUpload: true, agentSelector: true, executionMode: true, fileButtonText: "", stop: true, queue: true,
|
|
112
116
|
},
|
|
113
117
|
transcript: { nav: true, sessionLabel: true, timestamps: true, initialPrompt: false, initialBadge: true, tools: true, runningIndicator: true, status: false },
|
|
114
118
|
labels: { user: "你", agent: "智能体" },
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { RunCapabilities } from "../chat/RunControls";
|
|
2
|
+
|
|
3
|
+
/** 用户选择的下一轮执行设置;不包含账号或凭据。null 表示使用服务端默认值。 */
|
|
4
|
+
export interface ExecutionOptions {
|
|
5
|
+
agent?: string;
|
|
6
|
+
model?: string | null;
|
|
7
|
+
reasoning?: string | null;
|
|
8
|
+
permission_mode?: string | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ExecutionConfiguration {
|
|
12
|
+
revision: number;
|
|
13
|
+
agent: string;
|
|
14
|
+
model: string | null;
|
|
15
|
+
reasoning: string | null;
|
|
16
|
+
permission_mode: string | null;
|
|
17
|
+
agents: Array<{
|
|
18
|
+
id: string;
|
|
19
|
+
label: string;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
capabilities: RunCapabilities;
|
|
22
|
+
defaults?: Omit<ExecutionOptions, "agent">;
|
|
23
|
+
}>;
|
|
24
|
+
}
|
package/src/core/store.ts
CHANGED
|
@@ -153,7 +153,7 @@ export interface CliStoreOptions extends CenterClientOptions {
|
|
|
153
153
|
*/
|
|
154
154
|
newConversation?: NewConversationOptions;
|
|
155
155
|
/** 自己建对话(例如经业务后端建,以便把回调密钥留在服务端):返回 center 的 job_id。 */
|
|
156
|
-
createConversation?: (firstMessage: string) => Promise<string>;
|
|
156
|
+
createConversation?: (firstMessage: string, options?: NewConversationOptions) => Promise<string>;
|
|
157
157
|
/**
|
|
158
158
|
* 每条消息的 business_id(center 按它去重,回调里原样带回)。缺省 `cli-<uuid>`。
|
|
159
159
|
* kind = "create" 是第一条(新建对话),"message" 是之后的每条。
|
|
@@ -163,10 +163,12 @@ export interface CliStoreOptions extends CenterClientOptions {
|
|
|
163
163
|
|
|
164
164
|
/** 发一条消息时可以单独指定的新建参数(组件自己的 props),优先于仓库的。 */
|
|
165
165
|
export interface SendOptions {
|
|
166
|
+
expectedExecutionRevision?: number;
|
|
167
|
+
executionConfig?: import("./execution").ExecutionOptions;
|
|
166
168
|
/** 新建对话后是否改仓库的选中(缺省改)。 */
|
|
167
169
|
select?: boolean;
|
|
168
170
|
newConversation?: NewConversationOptions;
|
|
169
|
-
createConversation?: (firstMessage: string) => Promise<string>;
|
|
171
|
+
createConversation?: (firstMessage: string, options?: NewConversationOptions) => Promise<string>;
|
|
170
172
|
businessIdFor?: (text: string, kind: "create" | "message") => string;
|
|
171
173
|
}
|
|
172
174
|
|
|
@@ -601,7 +603,8 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
601
603
|
|
|
602
604
|
async function sendWithText(key: string | null, text: string, overrides: SendOptions): Promise<string> {
|
|
603
605
|
// 发消息的组件自己的参数优先(同一个仓库里可以有几个对话组件,各带各的新建参数),没有再用仓库的。
|
|
604
|
-
const
|
|
606
|
+
const originalOptions = overrides.newConversation ?? options.newConversation;
|
|
607
|
+
const newConversation = overrides.executionConfig ? { ...originalOptions, agent: overrides.executionConfig.agent ?? originalOptions?.agent, executionConfig: overrides.executionConfig } : originalOptions;
|
|
605
608
|
const createConversation = overrides.createConversation ?? options.createConversation;
|
|
606
609
|
const businessIdFor = overrides.businessIdFor ?? options.businessIdFor;
|
|
607
610
|
const ref = key ? parseConvKey(key) : null;
|
|
@@ -610,7 +613,7 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
610
613
|
if (ref?.taskId === NEW_TASK) {
|
|
611
614
|
// 在对话任务里新开一个对话:标题取第一行,智能体 / 浏览器模式沿用对话任务的第一个对话(center 决定)。
|
|
612
615
|
if (!client.openConversation) throw new CenterError("当前后端不支持在对话任务里新开对话", "UNSUPPORTED", 0);
|
|
613
|
-
const result = await retryOnce(() => client.openConversation!(ref.jobId, text, { title: firstLine(text, 60), businessId }));
|
|
616
|
+
const result = await retryOnce(() => client.openConversation!(ref.jobId, text, { title: firstLine(text, 60), businessId, agent: newConversation?.agent, executionConfig: newConversation?.executionConfig, browser: newConversation?.browser, timeoutSeconds: newConversation?.timeoutSeconds }));
|
|
614
617
|
const target = convKey(result.job_id, result.task_id ?? result.turn.task_id);
|
|
615
618
|
patchConv(target, (conv) => ({ ...conv, turns: addTurnsIfAbsent(conv.turns, [{ ...result.turn, queue_position: result.queue_position }]) }));
|
|
616
619
|
addJobIfMissing(result.job_id);
|
|
@@ -623,7 +626,7 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
623
626
|
const selectedBeforeCreate = state.selected;
|
|
624
627
|
let newId: string;
|
|
625
628
|
if (createConversation) {
|
|
626
|
-
newId = await createConversation(text);
|
|
629
|
+
newId = newConversation?.executionConfig ? await createConversation(text, newConversation) : await createConversation(text);
|
|
627
630
|
} else {
|
|
628
631
|
// 新建只发一次:center 的 POST /api/jobs 没有幂等,建好了却没收到响应时重发会建出第二个对话。
|
|
629
632
|
const created = await client.createConversation(text, { ...newConversation, title: newConversation?.title || firstLine(text), businessId: newConversation?.businessId || businessId });
|
|
@@ -653,7 +656,7 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
653
656
|
void refreshJob(ref.jobId, true);
|
|
654
657
|
return key;
|
|
655
658
|
}
|
|
656
|
-
const result = await retryOnce(() => client.sendMessage(ref.jobId, text, { businessId, ...(ref.taskId ? { taskId: ref.taskId } : {}) }));
|
|
659
|
+
const result = await retryOnce(() => client.sendMessage(ref.jobId, text, { businessId, ...(ref.taskId ? { taskId: ref.taskId } : {}), ...(overrides.expectedExecutionRevision !== undefined ? { expectedExecutionRevision: overrides.expectedExecutionRevision } : {}) }));
|
|
657
660
|
const jobTarget = result.job_id || ref.jobId;
|
|
658
661
|
// 指定了对话的:接续后跟到新对话任务里对应的那个对话;只有 job_id 的照旧只换 job。
|
|
659
662
|
const target = ref.taskId ? convKey(jobTarget, result.task_id ?? result.turn.task_id ?? ref.taskId) : jobTarget;
|
|
@@ -959,6 +962,7 @@ export function createCliStore(options: CliStoreOptions = {}) {
|
|
|
959
962
|
|
|
960
963
|
return {
|
|
961
964
|
client,
|
|
965
|
+
getNewConversationOptions: () => options.newConversation,
|
|
962
966
|
draftScope: connectionDraftScope(options),
|
|
963
967
|
getState: () => state,
|
|
964
968
|
/** 在看板里打开文件 / 浏览器 / 改动。返回有没有看板接住。 */
|
package/src/core/types.ts
CHANGED
|
@@ -131,6 +131,8 @@ export interface BrowserPacing {
|
|
|
131
131
|
|
|
132
132
|
/** 新建对话时可带的参数(POST /api/jobs,mode=conversation)。 */
|
|
133
133
|
export interface NewConversationOptions {
|
|
134
|
+
/** 首轮执行配置。由服务端校验、持久化,并用于后续派发和重试。 */
|
|
135
|
+
executionConfig?: import("./execution").ExecutionOptions;
|
|
134
136
|
title?: string;
|
|
135
137
|
/** 业务自己的 ID(tasks[0].business_id):第一轮的回调里原样带回,用来和业务数据对应。 */
|
|
136
138
|
businessId?: string;
|
package/src/index.ts
CHANGED
|
@@ -43,6 +43,7 @@ export {
|
|
|
43
43
|
type TextBlock, type MessagePart,
|
|
44
44
|
} from "./core/blocks";
|
|
45
45
|
export type * from "./core/types";
|
|
46
|
+
export type { ExecutionOptions, ExecutionConfiguration } from "./core/execution";
|
|
46
47
|
|
|
47
48
|
|
|
48
49
|
export { RunControls, type RunControlProps, type RunCapabilities, type RunTask } from "./chat/RunControls";
|