@zhin.js/adapter-sandbox 7.0.12 → 7.0.15
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 +40 -0
- package/adapters/sandbox.js +2 -1
- package/adapters/sandbox.ts +2 -1
- package/lib/endpoint.d.ts +18 -2
- package/lib/endpoint.js +86 -12
- package/lib/protocol.d.ts +3 -0
- package/lib/protocol.js +18 -2
- package/lib/run-config.d.ts +10 -0
- package/lib/run-config.js +30 -0
- package/package.json +16 -16
- package/pages/RichTextEditor.js +23 -8
- package/pages/RichTextEditor.tsx +20 -8
- package/pages/SandboxChat.js +315 -63
- package/pages/SandboxChat.tsx +693 -179
- package/pages/agentTrace.js +559 -0
- package/pages/agentTrace.test.js +235 -0
- package/pages/agentTrace.test.ts +265 -0
- package/pages/agentTrace.ts +646 -0
- package/pages/index.js +2 -2
- package/pages/index.tsx +2 -2
- package/pages/playgroundState.js +126 -0
- package/pages/playgroundState.test.js +92 -0
- package/pages/playgroundState.test.ts +105 -0
- package/pages/playgroundState.ts +172 -0
- package/src/endpoint.ts +98 -14
- package/src/protocol.ts +25 -2
- package/src/run-config.ts +41 -0
package/pages/SandboxChat.js
CHANGED
|
@@ -1,42 +1,79 @@
|
|
|
1
1
|
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
3
|
-
import React, { useState, useEffect, useRef } from 'react';
|
|
4
|
-
import { cn, resolveMediaSrc, pickMediaRawUrl, } from '@zhin.js/client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
|
4
|
+
import { cn, CodeBlock, MarkdownContent, resolveMediaSrc, pickMediaRawUrl, } from '@zhin.js/client';
|
|
5
5
|
import { buildSandboxWebSocketUrl } from './sandboxTransport';
|
|
6
|
-
import { User, Bot, Users, Trash2, Send, Hash, MessageSquare, Wifi, WifiOff, Smile, Image, X, Check, Info, Search,
|
|
6
|
+
import { User, Bot, Users, Trash2, Send, Hash, MessageSquare, Wifi, WifiOff, Smile, Image, X, Check, Info, Search, Video, Music, Plus, PanelRight, ExternalLink, RefreshCw, Sparkles, Activity, Wrench, Coins, CircleAlert, Gauge, SlidersHorizontal, FolderOpen, ShieldCheck, Network, Square, RotateCcw, FileDiff, Terminal, FlaskConical, ChevronDown, FileDown, ListChecks, } from 'lucide-react';
|
|
7
7
|
import RichTextEditor, {} from './RichTextEditor';
|
|
8
|
+
import { agentStudioPath, buildAgentRunReport, buildSandboxSessionKey, cancelAgentTask, deriveAgentRunSteps, deriveTaskRuns, deriveWorkbenchArtifacts, fetchAgentTrace, loadCachedAgentTrace, mergeTraceSnapshot, presentTraceEvent, summarizeTrace, saveCachedAgentTrace, } from './agentTrace.js';
|
|
9
|
+
import { createDefaultPlaygroundState, loadPlaygroundState, savePlaygroundState, } from './playgroundState.js';
|
|
8
10
|
export default function Sandbox() {
|
|
9
|
-
const [
|
|
10
|
-
const [
|
|
11
|
-
|
|
12
|
-
{ id: 'group_2001', name: '测试群组', type: 'group', unread: 0 },
|
|
13
|
-
{ id: 'channel_3001', name: '测试频道', type: 'channel', unread: 0 }
|
|
14
|
-
]);
|
|
11
|
+
const [initialState] = useState(() => loadPlaygroundState());
|
|
12
|
+
const [messages, setMessages] = useState(() => [...initialState.messages]);
|
|
13
|
+
const [channels, setChannels] = useState(() => [...initialState.sessions]);
|
|
15
14
|
const [faceList, setFaceList] = useState([]);
|
|
16
|
-
const [activeChannel, setActiveChannel] = useState(
|
|
15
|
+
const [activeChannel, setActiveChannel] = useState(() => (initialState.sessions.find((session) => session.id === initialState.activeSessionId && (initialState.activeSessionType === undefined || session.type === initialState.activeSessionType))
|
|
16
|
+
?? initialState.sessions[0]
|
|
17
|
+
?? createDefaultPlaygroundState().sessions[0]));
|
|
17
18
|
const [inputText, setInputText] = useState('');
|
|
18
|
-
const [endpointId, setBotName] = useState('
|
|
19
|
+
const [endpointId, setBotName] = useState('sandbox-bot');
|
|
19
20
|
const [connected, setConnected] = useState(false);
|
|
21
|
+
const [canExecute, setCanExecute] = useState(true);
|
|
22
|
+
const [persistenceStatus, setPersistenceStatus] = useState('saved');
|
|
23
|
+
const [transportNotice, setTransportNotice] = useState(null);
|
|
24
|
+
const [shellIsolation, setShellIsolation] = useState(null);
|
|
25
|
+
const [inspectorView, setInspectorView] = useState('runs');
|
|
26
|
+
const [expandedArtifact, setExpandedArtifact] = useState(null);
|
|
27
|
+
const [stoppingTask, setStoppingTask] = useState(false);
|
|
28
|
+
const [inlineRunExpanded, setInlineRunExpanded] = useState(false);
|
|
20
29
|
const [showFacePicker, setShowFacePicker] = useState(false);
|
|
21
30
|
/** 输入区:插入图片 / 视频 / 音频 URL */
|
|
22
31
|
const [mediaPanel, setMediaPanel] = useState(null);
|
|
23
32
|
const [mediaUrl, setMediaUrl] = useState('');
|
|
24
|
-
const [showAtPicker, setShowAtPicker] = useState(false);
|
|
25
33
|
const [atPopoverPosition, setAtPopoverPosition] = useState(null);
|
|
26
34
|
const [atSearchQuery, setAtSearchQuery] = useState('');
|
|
27
35
|
const [faceSearchQuery, setFaceSearchQuery] = useState('');
|
|
28
|
-
const [atUserName, setAtUserName] = useState('');
|
|
29
36
|
const [atSuggestions] = useState([
|
|
30
|
-
{ id: '
|
|
31
|
-
{ id: '
|
|
32
|
-
{ id: '10010', name: 'Test User' }
|
|
37
|
+
{ id: 'actor-owner', name: '当前用户' }, { id: 'actor-reviewer', name: '审阅者' },
|
|
38
|
+
{ id: 'actor-operator', name: '协作者' }, { id: 'actor-bot', name: 'Sandbox Agent' }
|
|
33
39
|
]);
|
|
34
40
|
const [previewSegments, setPreviewSegments] = useState([]);
|
|
41
|
+
const [composerMode, setComposerMode] = useState('write');
|
|
35
42
|
const [showChannelList, setShowChannelList] = useState(false);
|
|
36
|
-
const [
|
|
43
|
+
const [showInspector, setShowInspector] = useState(false);
|
|
44
|
+
const [showRunSettings, setShowRunSettings] = useState(false);
|
|
45
|
+
const [showNewSession, setShowNewSession] = useState(false);
|
|
46
|
+
const [newSessionName, setNewSessionName] = useState('');
|
|
47
|
+
const [newSessionScope, setNewSessionScope] = useState('private');
|
|
48
|
+
const [confirmClear, setConfirmClear] = useState(false);
|
|
49
|
+
const [trace, setTrace] = useState(null);
|
|
50
|
+
const [traceLoading, setTraceLoading] = useState(true);
|
|
51
|
+
const [traceNotice, setTraceNotice] = useState(null);
|
|
37
52
|
const messagesEndRef = useRef(null);
|
|
38
53
|
const wsRef = useRef(null);
|
|
39
54
|
const editorRef = useRef(null);
|
|
55
|
+
const traceRef = useRef(null);
|
|
56
|
+
const activeChannelRef = useRef(activeChannel);
|
|
57
|
+
const endpointIdRef = useRef(endpointId);
|
|
58
|
+
const sessionKey = useMemo(() => buildSandboxSessionKey(endpointId, activeChannel.type, activeChannel.id), [activeChannel.id, activeChannel.type, endpointId]);
|
|
59
|
+
const traceSummary = useMemo(() => summarizeTrace(trace), [trace]);
|
|
60
|
+
const taskRuns = useMemo(() => deriveTaskRuns(trace), [trace]);
|
|
61
|
+
const workbenchArtifacts = useMemo(() => deriveWorkbenchArtifacts(trace), [trace]);
|
|
62
|
+
const currentTask = taskRuns.find((run) => run.status === 'running') ?? taskRuns[0];
|
|
63
|
+
const currentRunSteps = useMemo(() => deriveAgentRunSteps(trace, currentTask), [currentTask, trace]);
|
|
64
|
+
const currentRunArtifacts = useMemo(() => workbenchArtifacts.filter((artifact) => (artifact.turnId === currentTask?.turnId && artifact.runtimeId === currentTask?.runtimeId)), [currentTask?.runtimeId, currentTask?.turnId, workbenchArtifacts]);
|
|
65
|
+
useEffect(() => { activeChannelRef.current = activeChannel; }, [activeChannel]);
|
|
66
|
+
useEffect(() => { endpointIdRef.current = endpointId; }, [endpointId]);
|
|
67
|
+
useEffect(() => { setInlineRunExpanded(currentTask?.status === 'running'); }, [currentTask?.id]);
|
|
68
|
+
useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [currentTask?.id]);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
setPersistenceStatus(savePlaygroundState({
|
|
71
|
+
activeSessionId: activeChannel.id,
|
|
72
|
+
activeSessionType: activeChannel.type,
|
|
73
|
+
sessions: channels,
|
|
74
|
+
messages,
|
|
75
|
+
}) ? 'saved' : 'error');
|
|
76
|
+
}, [activeChannel.id, channels, messages]);
|
|
40
77
|
const fetchFaceList = async () => {
|
|
41
78
|
try {
|
|
42
79
|
const res = await fetch('https://face.viki.moe/metadata.json');
|
|
@@ -48,6 +85,35 @@ export default function Sandbox() {
|
|
|
48
85
|
};
|
|
49
86
|
useEffect(() => { fetchFaceList(); }, []);
|
|
50
87
|
const handleInboundPayload = (data) => {
|
|
88
|
+
if (data.type === 'ready') {
|
|
89
|
+
setBotName(data.endpoint || data.bot || 'sandbox-bot');
|
|
90
|
+
setCanExecute(data.canExecute !== false);
|
|
91
|
+
setTransportNotice(data.canExecute === false ? '当前 Token 只有演示权限,任务运行已禁用。' : null);
|
|
92
|
+
if (data.shellIsolation) {
|
|
93
|
+
setShellIsolation({
|
|
94
|
+
available: data.shellIsolation.available === true,
|
|
95
|
+
provider: data.shellIsolation.provider || 'docker',
|
|
96
|
+
message: data.shellIsolation.message || '未检测到隔离执行环境',
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (data.workingDirectory?.trim()) {
|
|
100
|
+
const workingDirectory = data.workingDirectory.trim();
|
|
101
|
+
setChannels((current) => current.map((session) => session.runConfig.workingDirectory
|
|
102
|
+
? session
|
|
103
|
+
: { ...session, runConfig: { ...session.runConfig, workingDirectory } }));
|
|
104
|
+
setActiveChannel((current) => current.runConfig.workingDirectory
|
|
105
|
+
? current
|
|
106
|
+
: { ...current, runConfig: { ...current.runConfig, workingDirectory } });
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (data.type === 'error') {
|
|
111
|
+
const notice = Array.isArray(data.content)
|
|
112
|
+
? String(data.content[0]?.data?.text ?? 'Sandbox 拒绝了本次操作')
|
|
113
|
+
: String(data.content ?? 'Sandbox 拒绝了本次操作');
|
|
114
|
+
setTransportNotice(notice);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
51
117
|
if (data.type === 'edit' && data.messageId) {
|
|
52
118
|
const content = Array.isArray(data.content)
|
|
53
119
|
? data.content
|
|
@@ -58,29 +124,53 @@ export default function Sandbox() {
|
|
|
58
124
|
const content = typeof data.content === 'string'
|
|
59
125
|
? parseTextToSegments(data.content)
|
|
60
126
|
: Array.isArray(data.content) ? data.content : parseTextToSegments(String(data.content ?? ''));
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
127
|
+
const channelType = data.type === 'group' || data.type === 'channel' ? data.type : 'private';
|
|
128
|
+
const channelName = channelType === 'private'
|
|
129
|
+
? `会话 ${data.id}`
|
|
130
|
+
: channelType === 'group'
|
|
131
|
+
? `群组场景 ${data.id}`
|
|
132
|
+
: `频道场景 ${data.id}`;
|
|
67
133
|
setChannels((prev) => {
|
|
68
|
-
if (prev.some((c) => c.id === data.id))
|
|
134
|
+
if (prev.some((c) => c.id === data.id && c.type === channelType))
|
|
69
135
|
return prev;
|
|
70
|
-
const created = {
|
|
136
|
+
const created = {
|
|
137
|
+
id: data.id, name: channelName, type: channelType, unread: 0,
|
|
138
|
+
runConfig: { ...activeChannelRef.current.runConfig },
|
|
139
|
+
};
|
|
71
140
|
setActiveChannel(created);
|
|
72
141
|
return [...prev, created];
|
|
73
142
|
});
|
|
74
143
|
setMessages((prev) => [...prev, {
|
|
75
144
|
id: data.messageId ?? `bot_${data.timestamp}`, type: 'received', channelType,
|
|
76
145
|
channelId: data.id, channelName, senderId: 'endpoint',
|
|
77
|
-
senderName: data.bot ||
|
|
146
|
+
senderName: data.bot || endpointIdRef.current, content, timestamp: data.timestamp,
|
|
78
147
|
}]);
|
|
79
148
|
};
|
|
80
|
-
const sendInteractiveAction = (payload) => {
|
|
149
|
+
const sendInteractiveAction = (payload, messageId) => {
|
|
150
|
+
const ws = wsRef.current;
|
|
151
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
152
|
+
setTransportNotice('Sandbox 连接尚未就绪,审批选择未发送。');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
81
155
|
const segments = [{ type: 'action', data: { id: payload, payload } }];
|
|
82
|
-
const payloadJson = JSON.stringify({
|
|
83
|
-
|
|
156
|
+
const payloadJson = JSON.stringify({
|
|
157
|
+
type: activeChannel.type,
|
|
158
|
+
id: activeChannel.id,
|
|
159
|
+
content: segments,
|
|
160
|
+
agentRun: activeChannel.runConfig,
|
|
161
|
+
timestamp: Date.now(),
|
|
162
|
+
});
|
|
163
|
+
try {
|
|
164
|
+
ws.send(payloadJson);
|
|
165
|
+
if (messageId) {
|
|
166
|
+
setMessages((current) => current.map((message) => message.id === messageId
|
|
167
|
+
? { ...message, interactionResolved: true }
|
|
168
|
+
: message));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
setTransportNotice('审批选择发送失败,请等待连接恢复后重试。');
|
|
173
|
+
}
|
|
84
174
|
};
|
|
85
175
|
useEffect(() => {
|
|
86
176
|
let closed = false;
|
|
@@ -181,6 +271,41 @@ export default function Sandbox() {
|
|
|
181
271
|
}, []);
|
|
182
272
|
useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]);
|
|
183
273
|
useEffect(() => { setPreviewSegments(inputText.trim() ? parseTextToSegments(inputText) : []); }, [inputText]);
|
|
274
|
+
const loadTrace = useCallback(async (quiet = false) => {
|
|
275
|
+
if (!quiet)
|
|
276
|
+
setTraceLoading(true);
|
|
277
|
+
try {
|
|
278
|
+
const current = traceRef.current?.sessionKey === sessionKey ? traceRef.current : null;
|
|
279
|
+
let incoming = await fetchAgentTrace(sessionKey, quiet ? current?.latestSequence ?? 0 : 0);
|
|
280
|
+
if (quiet && current?.runtimeId && incoming.runtimeId && current.runtimeId !== incoming.runtimeId) {
|
|
281
|
+
incoming = await fetchAgentTrace(sessionKey, 0);
|
|
282
|
+
}
|
|
283
|
+
const merged = mergeTraceSnapshot(current, incoming);
|
|
284
|
+
traceRef.current = merged;
|
|
285
|
+
setTrace(merged);
|
|
286
|
+
saveCachedAgentTrace(merged);
|
|
287
|
+
setTraceNotice(null);
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
setTraceNotice(error instanceof Error ? error.message : 'Agent Trace 暂不可用');
|
|
291
|
+
}
|
|
292
|
+
finally {
|
|
293
|
+
if (!quiet)
|
|
294
|
+
setTraceLoading(false);
|
|
295
|
+
}
|
|
296
|
+
}, [sessionKey]);
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
const cached = loadCachedAgentTrace(sessionKey);
|
|
299
|
+
traceRef.current = cached;
|
|
300
|
+
setTrace(cached);
|
|
301
|
+
setTraceNotice(null);
|
|
302
|
+
void loadTrace();
|
|
303
|
+
const timer = window.setInterval(() => {
|
|
304
|
+
if (document.visibilityState === 'visible')
|
|
305
|
+
void loadTrace(true);
|
|
306
|
+
}, 2_000);
|
|
307
|
+
return () => window.clearInterval(timer);
|
|
308
|
+
}, [loadTrace]);
|
|
184
309
|
const parseTextToSegments = (text) => {
|
|
185
310
|
const segments = [];
|
|
186
311
|
const regex = /\[@([^\]]+)\]|\[face:(\d+)\]|\[image:([^\]]+)\]|\[video:([^\]]+)\]|\[audio:([^\]]+)\]/g;
|
|
@@ -222,16 +347,20 @@ export default function Sandbox() {
|
|
|
222
347
|
return true;
|
|
223
348
|
});
|
|
224
349
|
};
|
|
225
|
-
const renderMessageSegments = (segments, isSent) => {
|
|
350
|
+
const renderMessageSegments = (segments, isSent, messageId, interactionResolved = false) => {
|
|
226
351
|
const ring = isSent ? 'ring-1 ring-primary-foreground/25' : 'ring-1 ring-border/60';
|
|
227
352
|
return segments.map((segment, index) => {
|
|
228
353
|
if (typeof segment === 'string') {
|
|
229
|
-
return _jsx(
|
|
354
|
+
return _jsx(MarkdownContent, { text: segment, className: isSent ? 'zhin-markdown--inverse' : undefined }, index);
|
|
230
355
|
}
|
|
231
356
|
const d = segment.data;
|
|
232
357
|
switch (segment.type) {
|
|
233
358
|
case 'text':
|
|
234
|
-
|
|
359
|
+
case 'markdown':
|
|
360
|
+
case 'md':
|
|
361
|
+
return _jsx(MarkdownContent, { text: String(d.text ?? d.content ?? ''), className: isSent ? 'zhin-markdown--inverse' : undefined }, index);
|
|
362
|
+
case 'code':
|
|
363
|
+
return _jsx(CodeBlock, { code: String(d.code ?? d.text ?? d.content ?? ''), language: String(d.language ?? d.lang ?? '') }, index);
|
|
235
364
|
case 'mention':
|
|
236
365
|
case 'at':
|
|
237
366
|
return _jsxs("span", { className: "inline-flex items-center px-1.5 py-0.5 rounded bg-accent text-accent-foreground text-xs mx-0.5", children: ["@", String(d.name ?? d.target ?? d.qq ?? '')] }, index);
|
|
@@ -272,9 +401,8 @@ export default function Sandbox() {
|
|
|
272
401
|
}
|
|
273
402
|
case 'keyboard': {
|
|
274
403
|
const rows = d.rows ?? [];
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
: 'hover:bg-accent active:scale-95'), children: btn.label }, btn.payload))) }, ri))) }, index));
|
|
404
|
+
const resolved = Boolean(messageId && interactionResolved);
|
|
405
|
+
return (_jsxs("div", { className: "agent-playground-approval-actions", children: [rows.map((row, ri) => (_jsx("div", { children: row.map((btn) => (_jsx("button", { type: "button", disabled: btn.disabled || isSent || resolved, onClick: () => sendInteractiveAction(btn.payload, messageId), className: cn((btn.style === 'primary' || /^允许/u.test(btn.label)) && 'is-primary', (btn.style === 'danger' || /拒绝|取消/u.test(btn.label)) && 'is-danger'), children: btn.label }, btn.payload))) }, ri))), resolved && _jsxs("small", { children: [_jsx(Check, { size: 12 }), "\u5DF2\u63D0\u4EA4\u672C\u6B21\u9009\u62E9"] })] }, index));
|
|
278
406
|
}
|
|
279
407
|
default:
|
|
280
408
|
return _jsxs("span", { className: "text-xs opacity-70", children: ["[", segment.type, "]"] }, index);
|
|
@@ -282,30 +410,47 @@ export default function Sandbox() {
|
|
|
282
410
|
});
|
|
283
411
|
};
|
|
284
412
|
const handleSendMessage = (text, segments) => {
|
|
285
|
-
if (!hasRenderableSegments(segments))
|
|
413
|
+
if (!canExecute || !hasRenderableSegments(segments))
|
|
286
414
|
return;
|
|
287
415
|
const newMessage = { id: `msg_${Date.now()}`, type: 'sent', channelType: activeChannel.type, channelId: activeChannel.id, channelName: activeChannel.name, senderId: 'test_user', senderName: '测试用户', content: segments, timestamp: Date.now() };
|
|
288
416
|
setMessages((prev) => [...prev, newMessage]);
|
|
289
417
|
setInputText('');
|
|
290
418
|
setPreviewSegments([]);
|
|
419
|
+
setComposerMode('write');
|
|
291
420
|
editorRef.current?.clear();
|
|
292
421
|
// Stamp type+id so Host sandbox endpoint preserves channel context for outbound replies.
|
|
293
|
-
const payload = JSON.stringify({
|
|
422
|
+
const payload = JSON.stringify({
|
|
423
|
+
type: activeChannel.type,
|
|
424
|
+
id: activeChannel.id,
|
|
425
|
+
messageId: newMessage.id,
|
|
426
|
+
content: segments,
|
|
427
|
+
agentRun: activeChannel.runConfig,
|
|
428
|
+
timestamp: Date.now(),
|
|
429
|
+
});
|
|
294
430
|
wsRef.current?.send(payload);
|
|
295
431
|
};
|
|
296
|
-
const clearMessages = () => {
|
|
297
|
-
setMessages(
|
|
298
|
-
|
|
432
|
+
const clearMessages = () => {
|
|
433
|
+
setMessages((current) => current.filter((message) => !(message.channelId === activeChannel.id && message.channelType === activeChannel.type)));
|
|
434
|
+
setConfirmClear(false);
|
|
435
|
+
};
|
|
436
|
+
const switchChannel = (channel) => { setActiveChannel(channel); setChannels((prev) => prev.map((c) => c.id === channel.id && c.type === channel.type ? { ...c, unread: 0 } : c)); if (window.innerWidth < 900)
|
|
299
437
|
setShowChannelList(false); };
|
|
300
|
-
const
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
438
|
+
const updateRunConfig = (patch) => {
|
|
439
|
+
const next = { ...activeChannel, runConfig: { ...activeChannel.runConfig, ...patch } };
|
|
440
|
+
setActiveChannel(next);
|
|
441
|
+
setChannels((sessions) => sessions.map((session) => session.id === activeChannel.id && session.type === activeChannel.type ? next : session));
|
|
442
|
+
};
|
|
443
|
+
const addSession = () => {
|
|
444
|
+
const label = newSessionName.trim() || '未命名试验';
|
|
445
|
+
const id = `${newSessionScope}-${Date.now().toString(36)}`;
|
|
446
|
+
const session = {
|
|
447
|
+
id, name: label, type: newSessionScope, unread: 0,
|
|
448
|
+
runConfig: { ...activeChannel.runConfig },
|
|
449
|
+
};
|
|
450
|
+
setChannels((current) => [...current, session]);
|
|
451
|
+
setActiveChannel(session);
|
|
452
|
+
setNewSessionName('');
|
|
453
|
+
setShowNewSession(false);
|
|
309
454
|
};
|
|
310
455
|
const getChannelIcon = (type) => { switch (type) {
|
|
311
456
|
case 'private': return _jsx(User, { size: 16 });
|
|
@@ -327,8 +472,6 @@ export default function Sandbox() {
|
|
|
327
472
|
setMediaUrl('');
|
|
328
473
|
setMediaPanel(null);
|
|
329
474
|
};
|
|
330
|
-
const insertAtUser = () => { if (!atUserName.trim())
|
|
331
|
-
return; editorRef.current?.insertAt(atUserName.trim()); setAtUserName(''); setShowAtPicker(false); };
|
|
332
475
|
const selectAtUser = (user) => { editorRef.current?.replaceAtTrigger(user.name, user.id); setAtPopoverPosition(null); setAtSearchQuery(''); };
|
|
333
476
|
const handleAtTrigger = (show, searchQuery, position) => {
|
|
334
477
|
if (activeChannel.type === 'private') {
|
|
@@ -349,15 +492,124 @@ export default function Sandbox() {
|
|
|
349
492
|
return true; const q = atSearchQuery.toLowerCase(); return user.name.toLowerCase().includes(q) || user.id.toLowerCase().includes(q); });
|
|
350
493
|
const handleEditorChange = (text, segments) => { setInputText(text); setPreviewSegments(segments); };
|
|
351
494
|
const filteredFaces = faceList.filter(face => face.name.toLowerCase().includes(faceSearchQuery.toLowerCase()) || face.describe.toLowerCase().includes(faceSearchQuery.toLowerCase()));
|
|
352
|
-
const channelMessages = messages.filter((msg) => msg.channelId === activeChannel.id);
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
495
|
+
const channelMessages = messages.filter((msg) => msg.channelId === activeChannel.id && msg.channelType === activeChannel.type);
|
|
496
|
+
const lastUserMessage = [...channelMessages].reverse().find((message) => message.type === 'sent');
|
|
497
|
+
const currentTaskMessage = currentTask?.sourceMessageId
|
|
498
|
+
? channelMessages.find((message) => message.id === currentTask.sourceMessageId && message.type === 'sent')
|
|
499
|
+
: undefined;
|
|
500
|
+
const scopeLabel = activeChannel.type === 'private' ? '单用户' : activeChannel.type === 'group' ? '群组上下文' : '频道上下文';
|
|
501
|
+
const runPrompt = (prompt) => handleSendMessage(prompt, [{ type: 'text', data: { text: prompt } }]);
|
|
502
|
+
const recentTraceEvents = trace?.events.slice(-8).reverse() ?? [];
|
|
503
|
+
const stopActiveTask = async () => {
|
|
504
|
+
setStoppingTask(true);
|
|
505
|
+
try {
|
|
506
|
+
const cancelled = await cancelAgentTask(sessionKey);
|
|
507
|
+
setTraceNotice(cancelled ? '已发送停止请求,正在等待任务结束。' : '当前会话没有运行中的任务。');
|
|
508
|
+
window.setTimeout(() => void loadTrace(true), 240);
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
setTraceNotice(error instanceof Error ? error.message : '无法停止任务');
|
|
512
|
+
setShowInspector(true);
|
|
513
|
+
}
|
|
514
|
+
finally {
|
|
515
|
+
setStoppingTask(false);
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
const retryLastTask = () => {
|
|
519
|
+
if (!lastUserMessage)
|
|
520
|
+
return;
|
|
521
|
+
handleSendMessage(messageText(lastUserMessage.content), [...lastUserMessage.content]);
|
|
522
|
+
};
|
|
523
|
+
const retryCurrentTask = () => {
|
|
524
|
+
if (!currentTaskMessage)
|
|
525
|
+
return;
|
|
526
|
+
handleSendMessage(messageText(currentTaskMessage.content), [...currentTaskMessage.content]);
|
|
527
|
+
};
|
|
528
|
+
const exportCurrentRun = () => {
|
|
529
|
+
if (!trace || !currentTask)
|
|
530
|
+
return;
|
|
531
|
+
const report = buildAgentRunReport(trace, {
|
|
532
|
+
run: currentTask,
|
|
533
|
+
sessionName: activeChannel.name,
|
|
534
|
+
taskPrompt: currentTaskMessage ? messageText(currentTaskMessage.content) : undefined,
|
|
535
|
+
workingDirectory: activeChannel.runConfig.workingDirectory,
|
|
536
|
+
safetyMode: activeChannel.runConfig.safetyMode,
|
|
537
|
+
approvalMode: activeChannel.runConfig.approvalMode,
|
|
538
|
+
networkAccess: activeChannel.runConfig.safetyMode === 'danger-full-access' || activeChannel.runConfig.networkAccess,
|
|
539
|
+
});
|
|
540
|
+
const url = URL.createObjectURL(new Blob([report], { type: 'text/markdown;charset=utf-8' }));
|
|
541
|
+
const link = document.createElement('a');
|
|
542
|
+
link.href = url;
|
|
543
|
+
link.download = `zhin-agent-run-${safeFileName(activeChannel.name)}-${currentTask.turnId.slice(0, 8)}.md`;
|
|
544
|
+
document.body.appendChild(link);
|
|
545
|
+
link.click();
|
|
546
|
+
link.remove();
|
|
547
|
+
window.setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
548
|
+
};
|
|
549
|
+
const inlineRunCard = currentTask ? (_jsxs("article", { className: cn('agent-playground-inline-run', `is-${currentTask.status}`), "aria-live": currentTask.status === 'running' ? 'polite' : undefined, children: [_jsx("span", { className: "agent-playground-inline-run-rail", "aria-hidden": "true" }), _jsxs("div", { className: "agent-playground-inline-run-head", children: [_jsx("span", { className: "agent-playground-inline-run-icon", children: _jsx(ListChecks, { size: 17 }) }), _jsxs("div", { className: "agent-playground-inline-run-copy", children: [_jsx("span", { children: currentTask.status === 'running' ? 'Live agent run' : 'Agent run summary' }), _jsx("h3", { children: taskStatusLabel(currentTask.status) }), currentTaskMessage && _jsx("p", { children: messageText(currentTaskMessage.content) })] }), _jsxs("div", { className: "agent-playground-inline-run-actions", children: [currentTask.status === 'running' ? (_jsxs("button", { type: "button", className: "is-stop", onClick: () => void stopActiveTask(), disabled: stoppingTask, children: [_jsx(Square, { size: 12 }), stoppingTask ? '停止中' : '停止'] })) : currentTaskMessage ? (_jsxs("button", { type: "button", onClick: retryCurrentTask, children: [_jsx(RotateCcw, { size: 13 }), "\u91CD\u8BD5"] })) : null, _jsxs("button", { type: "button", onClick: exportCurrentRun, title: "\u5BFC\u51FA Markdown \u8FD0\u884C\u62A5\u544A", children: [_jsx(FileDown, { size: 13 }), "\u5BFC\u51FA"] })] })] }), _jsxs("dl", { className: "agent-playground-inline-run-metrics", children: [_jsxs("div", { children: [_jsx("dt", { children: "\u8017\u65F6" }), _jsx("dd", { children: currentTask.durationMs === undefined ? '进行中' : formatDuration(currentTask.durationMs) })] }), _jsxs("div", { children: [_jsx("dt", { children: "\u6B65\u9AA4" }), _jsx("dd", { children: currentRunSteps.length })] }), _jsxs("div", { children: [_jsx("dt", { children: "\u5DE5\u5177" }), _jsx("dd", { children: currentTask.toolCount })] }), _jsxs("div", { children: [_jsx("dt", { children: "Token" }), _jsx("dd", { children: currentTask.tokenCount.toLocaleString() })] }), _jsxs("div", { className: cn(currentTask.problemCount > 0 && 'has-problem'), children: [_jsx("dt", { children: "\u5F02\u5E38" }), _jsx("dd", { children: currentTask.problemCount })] })] }), inlineRunExpanded && (currentRunSteps.length > 0 ? (_jsx("ol", { className: "agent-playground-inline-run-steps", children: currentRunSteps.map((step) => (_jsxs("li", { className: cn(`is-${step.status}`), children: [_jsx("i", { "aria-hidden": "true" }), _jsxs("div", { children: [_jsx("strong", { children: step.title }), step.detail && _jsx("small", { children: step.detail })] }), _jsx("span", { children: step.durationMs === undefined ? runStepStatusLabel(step.status) : formatDuration(step.durationMs) })] }, step.id))) })) : _jsx("div", { className: "agent-playground-inline-run-empty", children: "\u6B63\u5728\u7B49\u5F85\u7B2C\u4E00\u6761\u8FD0\u884C\u4E8B\u4EF6\u2026" })), _jsxs("footer", { children: [_jsxs("button", { type: "button", "aria-expanded": inlineRunExpanded, onClick: () => setInlineRunExpanded((expanded) => !expanded), children: [_jsx(ChevronDown, { size: 13 }), inlineRunExpanded ? '收起步骤' : `查看 ${currentRunSteps.length} 个步骤`] }), _jsxs("button", { type: "button", onClick: () => { setInspectorView(currentRunArtifacts.length > 0 ? 'artifacts' : 'runs'); setShowInspector(true); }, children: [currentRunArtifacts.length > 0 ? `${currentRunArtifacts.length} 个变更与产物` : '打开运行检查器', _jsx(PanelRight, { size: 13 })] })] })] })) : null;
|
|
550
|
+
return (_jsxs("section", { className: "agent-playground-shell", children: [_jsxs("div", { className: "agent-playground-mobilebar", children: [_jsxs("button", { type: "button", "aria-expanded": showChannelList, onClick: () => setShowChannelList(!showChannelList), children: [_jsx(MessageSquare, { size: 17 }), "\u6D4B\u8BD5\u4F1A\u8BDD"] }), _jsx("strong", { children: "Agent \u8BD5\u9A8C\u53F0" }), _jsxs("button", { type: "button", "aria-expanded": showInspector, onClick: () => setShowInspector(!showInspector), children: [_jsx(PanelRight, { size: 17 }), "\u68C0\u67E5\u5668"] })] }), _jsxs("nav", { className: cn("channel-sidebar agent-playground-sessions", showChannelList && "show"), "aria-label": "\u6D4B\u8BD5\u4F1A\u8BDD", children: [_jsx("div", { className: "agent-playground-brand", children: _jsxs("div", { className: "flex justify-between items-center", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "agent-playground-mark", children: _jsx(Sparkles, { size: 16 }) }), _jsxs("div", { children: [_jsx("h2", { children: "Agent \u8BD5\u9A8C\u53F0" }), _jsx("small", { children: "Sandbox playground" })] })] }), _jsx("span", { className: cn("agent-playground-connection", connected && "is-online"), title: connected ? 'Sandbox WebSocket 已连接' : '正在重连 Sandbox WebSocket', children: connected ? _jsx(Wifi, { size: 12 }) : _jsx(WifiOff, { size: 12 }) })] }) }), _jsxs("div", { className: "agent-playground-section-label", children: [_jsx("span", { children: "\u6D4B\u8BD5\u4F1A\u8BDD" }), _jsx("span", { children: channels.length })] }), _jsx("div", { className: "agent-playground-session-list", children: channels.map((channel) => {
|
|
551
|
+
const isActive = activeChannel.id === channel.id && activeChannel.type === channel.type;
|
|
552
|
+
return (_jsxs("button", { type: "button", "aria-current": isActive ? 'page' : undefined, className: cn("agent-playground-session", isActive && "active"), onClick: () => switchChannel(channel), children: [_jsx("span", { className: "agent-playground-session-icon", children: getChannelIcon(channel.type) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "text-sm font-medium truncate", children: channel.name }), _jsx("div", { className: "text-xs text-muted-foreground", children: channel.type === 'private' ? '单用户作用域' : channel.type === 'group' ? '群组作用域' : '频道作用域' })] }), channel.unread > 0 && _jsx("span", { className: "inline-flex items-center justify-center h-5 min-w-5 rounded-full bg-destructive text-destructive-foreground text-[10px] font-medium px-1", children: channel.unread })] }, `${channel.type}:${channel.id}`));
|
|
553
|
+
}) }), _jsx("div", { className: "agent-playground-new-session", children: showNewSession ? (_jsxs("div", { className: "agent-playground-new-form", children: [_jsx("input", { value: newSessionName, onChange: (event) => setNewSessionName(event.target.value), onKeyDown: (event) => { if (event.key === 'Enter')
|
|
554
|
+
addSession(); }, placeholder: "\u4F1A\u8BDD\u540D\u79F0", autoFocus: true }), _jsx("div", { className: "agent-playground-scope-picker", role: "group", "aria-label": "\u6D88\u606F\u4F5C\u7528\u57DF", children: ['private', 'group', 'channel'].map((scope) => (_jsx("button", { type: "button", "aria-pressed": newSessionScope === scope, className: cn(newSessionScope === scope && 'active'), onClick: () => setNewSessionScope(scope), children: scope === 'private' ? '单用户' : scope === 'group' ? '群组' : '频道' }, scope))) }), _jsxs("div", { className: "agent-playground-new-actions", children: [_jsx("button", { type: "button", onClick: () => setShowNewSession(false), children: "\u53D6\u6D88" }), _jsx("button", { type: "button", className: "primary", onClick: addSession, children: "\u521B\u5EFA" })] })] })) : (_jsxs("button", { type: "button", className: "agent-playground-add", onClick: () => setShowNewSession(true), children: [_jsx(Plus, { size: 15 }), "\u65B0\u5EFA\u6D4B\u8BD5\u4F1A\u8BDD"] })) }), _jsxs("div", { className: "agent-playground-endpoint", children: [_jsx("span", { children: persistenceStatus === 'saved' ? '会话已持久化' : '会话保存失败' }), _jsx("strong", { children: endpointId })] })] }), showChannelList && _jsx("div", { className: "channel-overlay", onClick: () => setShowChannelList(false) }), _jsxs("main", { className: "chat-area agent-playground-main", children: [_jsxs("header", { className: "agent-playground-runbar", children: [_jsxs("div", { className: "agent-playground-run-identity", children: [_jsx("span", { className: "agent-playground-run-icon", children: getChannelIcon(activeChannel.type) }), _jsxs("div", { children: [_jsxs("div", { className: "agent-playground-run-title", children: [_jsx("h1", { children: activeChannel.name }), _jsx("span", { children: scopeLabel })] }), _jsx("code", { children: sessionKey })] })] }), _jsxs("div", { className: "agent-playground-run-actions", children: [_jsxs("span", { className: cn("agent-playground-run-state", currentTask?.status === 'running' && "is-running", currentTask?.status === 'failed' && 'has-problem'), children: [currentTask?.status === 'running' ? _jsx(Activity, { size: 14 }) : _jsx(Gauge, { size: 14 }), currentTask ? taskStatusLabel(currentTask.status) : `${channelMessages.length} 条消息`] }), currentTask?.status === 'running' ? (_jsxs("button", { type: "button", className: "agent-playground-stop", onClick: () => void stopActiveTask(), disabled: stoppingTask, "aria-label": "\u505C\u6B62\u5F53\u524D\u4EFB\u52A1", children: [_jsx(Square, { size: 13 }), stoppingTask ? '停止中' : '停止'] })) : lastUserMessage ? (_jsxs("button", { type: "button", onClick: retryLastTask, "aria-label": "\u91CD\u65B0\u8FD0\u884C\u4E0A\u4E00\u4E2A\u4EFB\u52A1", children: [_jsx(RotateCcw, { size: 14 }), "\u91CD\u8BD5"] })) : null, _jsxs("a", { href: agentStudioPath(sessionKey), children: [_jsx(ExternalLink, { size: 14 }), "Agent Studio"] }), _jsx("button", { type: "button", className: cn(showRunSettings && 'active'), "aria-expanded": showRunSettings, onClick: () => setShowRunSettings((visible) => !visible), "aria-label": "\u914D\u7F6E\u8FD0\u884C\u73AF\u5883", children: _jsx(SlidersHorizontal, { size: 16 }) }), _jsx("button", { type: "button", "aria-expanded": showInspector, onClick: () => setShowInspector(!showInspector), "aria-label": "\u5207\u6362\u8FD0\u884C\u68C0\u67E5\u5668", children: _jsx(PanelRight, { size: 16 }) }), _jsx("button", { type: "button", onClick: () => setConfirmClear(true), "aria-label": "\u6E05\u7A7A\u5F53\u524D\u4F1A\u8BDD", children: _jsx(Trash2, { size: 15 }) })] })] }), confirmClear && (_jsxs("div", { className: "agent-playground-confirm", role: "alert", children: [_jsxs("div", { children: [_jsx("strong", { children: "\u6E05\u7A7A\u5F53\u524D\u6D4B\u8BD5\u8BB0\u5F55\uFF1F" }), _jsxs("span", { children: ["\u53EA\u79FB\u9664\u201C", activeChannel.name, "\u201D\u5728\u6D4F\u89C8\u5668\u4E2D\u7684\u6D88\u606F\uFF0C\u4E0D\u5F71\u54CD Agent \u4F1A\u8BDD\u5B58\u50A8\u3002"] })] }), _jsx("button", { type: "button", onClick: () => setConfirmClear(false), children: "\u53D6\u6D88" }), _jsx("button", { type: "button", className: "danger", onClick: clearMessages, children: "\u786E\u8BA4\u6E05\u7A7A" })] })), transportNotice && (_jsxs("div", { className: "agent-playground-confirm", role: "status", children: [_jsxs("div", { children: [_jsx("strong", { children: "\u8FD0\u884C\u6682\u4E0D\u53EF\u7528" }), _jsx("span", { children: transportNotice })] }), _jsx("button", { type: "button", onClick: () => setTransportNotice(null), children: "\u77E5\u9053\u4E86" })] })), showRunSettings && (_jsxs("section", { className: "agent-playground-run-settings", "aria-label": "\u8FD0\u884C\u914D\u7F6E", children: [_jsxs("label", { className: "agent-playground-directory-field", children: [_jsxs("span", { children: [_jsx(FolderOpen, { size: 14 }), "\u5DE5\u4F5C\u76EE\u5F55"] }), _jsx("input", { value: activeChannel.runConfig.workingDirectory, onChange: (event) => updateRunConfig({ workingDirectory: event.target.value }), placeholder: "\u4F7F\u7528 Host \u9879\u76EE\u76EE\u5F55", spellCheck: false })] }), _jsxs("label", { children: [_jsxs("span", { children: [_jsx(ShieldCheck, { size: 14 }), "\u5B89\u5168\u7B56\u7565"] }), _jsxs("select", { value: activeChannel.runConfig.safetyMode, onChange: (event) => {
|
|
555
|
+
const safetyMode = event.target.value;
|
|
556
|
+
updateRunConfig({ safetyMode, ...(safetyMode === 'danger-full-access' ? { networkAccess: true } : {}) });
|
|
557
|
+
}, children: [_jsx("option", { value: "read-only", children: "\u53EA\u8BFB" }), _jsx("option", { value: "workspace-write", children: "\u5DE5\u4F5C\u533A\u5199\u5165" }), _jsx("option", { value: "danger-full-access", children: "\u5B8C\u5168\u8BBF\u95EE" })] })] }), _jsxs("label", { children: [_jsx("span", { children: "\u5BA1\u6279\u7B56\u7565" }), _jsxs("select", { value: activeChannel.runConfig.safetyMode === 'read-only' ? 'deny' : activeChannel.runConfig.safetyMode === 'danger-full-access' ? 'allow' : activeChannel.runConfig.approvalMode, disabled: activeChannel.runConfig.safetyMode !== 'workspace-write', onChange: (event) => updateRunConfig({ approvalMode: event.target.value }), children: [_jsx("option", { value: "ask", children: "\u6309\u9700\u786E\u8BA4" }), _jsx("option", { value: "deny", children: "\u81EA\u52A8\u62D2\u7EDD" }), _jsx("option", { value: "allow", children: "\u81EA\u52A8\u5141\u8BB8" })] })] }), _jsxs("label", { className: "agent-playground-network-toggle", children: [_jsxs("span", { children: [_jsx(Network, { size: 14 }), "\u7F51\u7EDC\u8BBF\u95EE"] }), _jsx("input", { type: "checkbox", checked: activeChannel.runConfig.safetyMode === 'danger-full-access' || activeChannel.runConfig.networkAccess, disabled: activeChannel.runConfig.safetyMode === 'danger-full-access', onChange: (event) => updateRunConfig({ networkAccess: event.target.checked }) }), _jsx("i", { "aria-hidden": "true" })] }), activeChannel.runConfig.safetyMode === 'danger-full-access' && (_jsxs("p", { children: [_jsx(CircleAlert, { size: 14 }), "\u5B8C\u5168\u8BBF\u95EE\u5141\u8BB8 Agent \u64CD\u4F5C\u5DE5\u4F5C\u76EE\u5F55\u4E4B\u5916\u7684\u6587\u4EF6\u5E76\u8BBF\u95EE\u7F51\u7EDC\uFF0C\u8BF7\u4EC5\u7528\u4E8E\u53EF\u4FE1\u4EFB\u52A1\u3002"] })), activeChannel.runConfig.safetyMode !== 'danger-full-access' && shellIsolation?.available === false && (_jsxs("p", { children: [_jsx(CircleAlert, { size: 14 }), "\u5B89\u5168 Shell \u9700\u8981\u53EF\u7528\u7684 Docker daemon\uFF1B\u5F53\u524D\u4EC5\u6587\u4EF6\u5DE5\u5177\u53EF\u8FD0\u884C\u3002", shellIsolation.message] }))] })), _jsx("section", { className: "agent-playground-conversation", "aria-label": "Agent \u5BF9\u8BDD", children: _jsx("div", { className: "agent-playground-message-scroll", children: channelMessages.length === 0 ? (_jsxs("div", { className: "agent-playground-empty", children: [_jsx("span", { className: "agent-playground-empty-mark", children: _jsx(Sparkles, { size: 24 }) }), _jsxs("div", { children: [_jsx("h2", { children: "\u5F00\u59CB\u4E00\u6B21\u53EF\u89C2\u5BDF\u7684 Agent \u8FD0\u884C" }), _jsx("p", { children: "\u53D1\u9001\u4EFB\u52A1\u540E\uFF0C\u56DE\u590D\u3001\u5DE5\u5177\u8C03\u7528\u3001Token \u4E0E\u5F02\u5E38\u4F1A\u5728\u540C\u4E00\u4F1A\u8BDD\u4E0A\u4E0B\u6587\u4E2D\u5173\u8054\u3002" })] }), _jsxs("div", { className: "agent-playground-prompts", children: [_jsx("button", { type: "button", onClick: () => runPrompt('介绍当前 Agent 可以使用的能力,并给出三个具体示例。'), children: "\u63A2\u7D22\u53EF\u7528\u80FD\u529B" }), _jsx("button", { type: "button", onClick: () => runPrompt('用 Markdown 表格总结当前运行环境,并附上一段 TypeScript 示例代码。'), children: "\u6D4B\u8BD5\u5BCC\u6587\u672C\u8F93\u51FA" }), _jsx("button", { type: "button", onClick: () => runPrompt('分析一个任务从推理、工具调用到最终回复的完整执行路径。'), children: "\u89C2\u5BDF\u6267\u884C\u8DEF\u5F84" })] })] })) : (_jsxs("div", { className: "agent-playground-message-list", children: [channelMessages.map((msg) => {
|
|
558
|
+
const isApproval = msg.type === 'received' && msg.content.some((segment) => segment.type === 'keyboard');
|
|
559
|
+
return (_jsxs(React.Fragment, { children: [_jsxs("article", { className: cn("agent-playground-message", msg.type === 'sent' ? "is-user" : "is-agent", isApproval && "is-approval"), children: [_jsx("span", { className: "agent-playground-message-avatar", children: msg.type === 'received' ? _jsx(Bot, { size: 16 }) : _jsx(User, { size: 16 }) }), _jsxs("div", { className: "agent-playground-message-main", children: [_jsxs("div", { className: "agent-playground-message-meta", children: [_jsx("strong", { children: isApproval ? '需要你的确认' : msg.type === 'received' ? endpointId : '你' }), isApproval && _jsx("span", { children: "approval required" }), _jsx("time", { children: new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) })] }), _jsx("div", { className: "agent-playground-message-content", children: renderMessageSegments(msg.content, msg.type === 'sent', msg.id, msg.interactionResolved === true) })] })] }), currentTaskMessage?.id === msg.id && inlineRunCard] }, msg.id));
|
|
560
|
+
}), !currentTaskMessage && inlineRunCard, _jsx("div", { ref: messagesEndRef })] })) }) }), _jsxs("section", { className: "agent-playground-composer", "aria-label": "\u4EFB\u52A1\u8F93\u5165", children: [_jsxs("div", { className: "flex gap-2 items-center flex-wrap", children: [_jsx("button", { type: "button", className: cn("h-8 w-8 rounded-md flex items-center justify-center border transition-colors", showFacePicker ? "bg-primary text-primary-foreground" : "hover:bg-accent"), onClick: () => { setShowFacePicker(!showFacePicker); setMediaPanel(null); }, title: "\u63D2\u5165\u8868\u60C5", children: _jsx(Smile, { size: 16 }) }), _jsx("button", { type: "button", className: cn("h-8 w-8 rounded-md flex items-center justify-center border transition-colors", mediaPanel === 'image' ? "bg-primary text-primary-foreground" : "hover:bg-accent"), onClick: () => { setMediaPanel((p) => (p === 'image' ? null : 'image')); setShowFacePicker(false); }, title: "\u63D2\u5165\u56FE\u7247 URL", children: _jsx(Image, { size: 16 }) }), _jsx("button", { type: "button", className: cn("h-8 w-8 rounded-md flex items-center justify-center border transition-colors", mediaPanel === 'video' ? "bg-primary text-primary-foreground" : "hover:bg-accent"), onClick: () => { setMediaPanel((p) => (p === 'video' ? null : 'video')); setShowFacePicker(false); }, title: "\u63D2\u5165\u89C6\u9891 URL", children: _jsx(Video, { size: 16 }) }), _jsx("button", { type: "button", className: cn("h-8 w-8 rounded-md flex items-center justify-center border transition-colors", mediaPanel === 'audio' ? "bg-primary text-primary-foreground" : "hover:bg-accent"), onClick: () => { setMediaPanel((p) => (p === 'audio' ? null : 'audio')); setShowFacePicker(false); }, title: "\u63D2\u5165\u97F3\u9891 URL", children: _jsx(Music, { size: 16 }) }), _jsx("div", { className: "flex-1 min-w-[1rem]" }), _jsxs("div", { className: "sandbox-composer-tabs", role: "tablist", "aria-label": "\u6D88\u606F\u7F16\u8F91\u6A21\u5F0F", children: [_jsx("button", { type: "button", role: "tab", "aria-selected": composerMode === 'write', className: cn(composerMode === 'write' && 'active'), onClick: () => setComposerMode('write'), children: "\u7F16\u5199" }), _jsx("button", { type: "button", role: "tab", "aria-selected": composerMode === 'preview', className: cn(composerMode === 'preview' && 'active'), onClick: () => setComposerMode('preview'), children: "\u9884\u89C8" })] }), inputText && (_jsx("button", { className: "h-8 w-8 rounded-md flex items-center justify-center hover:bg-accent transition-colors", onClick: () => { editorRef.current?.clear(); setInputText(''); setPreviewSegments([]); setComposerMode('write'); }, "aria-label": "\u6E05\u7A7A\u8F93\u5165", children: _jsx(X, { size: 16 }) }))] }), showFacePicker && (_jsxs("div", { className: "p-3 rounded-md border bg-muted/30 max-h-64 overflow-y-auto space-y-2", children: [_jsx("input", { value: faceSearchQuery, onChange: (e) => setFaceSearchQuery(e.target.value), placeholder: "\u641C\u7D22\u8868\u60C5...", className: "w-full h-8 rounded-md border bg-transparent px-2 text-sm" }), _jsx("div", { className: "grid grid-cols-8 gap-1", children: filteredFaces.slice(0, 80).map((face) => (_jsx("button", { onClick: () => insertFace(face.id), title: face.name, className: "w-10 h-10 rounded-md border flex items-center justify-center hover:bg-accent transition-colors", children: _jsx("img", { src: `https://face.viki.moe/apng/${face.id}.png`, alt: face.name, className: "w-8 h-8" }) }, face.id))) }), filteredFaces.length === 0 && (_jsxs("div", { className: "flex flex-col items-center gap-2 py-4", children: [_jsx(Search, { size: 32, className: "text-muted-foreground/30" }), _jsx("span", { className: "text-sm text-muted-foreground", children: "\u672A\u627E\u5230\u5339\u914D\u7684\u8868\u60C5" })] }))] })), mediaPanel && (_jsxs("div", { className: "p-3 rounded-md border bg-muted/30 space-y-2", children: [_jsxs("p", { className: "text-xs text-muted-foreground", children: [mediaPanel === 'image' && '支持 http(s) 图片链接或 data URL', mediaPanel === 'video' && '支持浏览器可解码的视频直链(如 .mp4、.webm)', mediaPanel === 'audio' && '支持 .mp3、.ogg、.wav 等音频直链'] }), _jsx("input", { value: mediaUrl, onChange: (e) => setMediaUrl(e.target.value), placeholder: mediaPanel === 'image' ? '图片 URL…' : mediaPanel === 'video' ? '视频 URL…' : '音频 URL…', className: "w-full h-8 rounded-md border border-input bg-background px-2 text-sm", onKeyDown: (e) => { if (e.key === 'Enter') {
|
|
561
|
+
e.preventDefault();
|
|
562
|
+
commitMediaUrl();
|
|
563
|
+
} } }), _jsxs("button", { type: "button", className: "inline-flex items-center gap-1 h-8 px-3 rounded-md bg-primary text-primary-foreground text-sm disabled:opacity-50", onClick: commitMediaUrl, disabled: !mediaUrl.trim(), children: [_jsx(Check, { size: 14 }), " \u63D2\u5165\u5230\u8F93\u5165\u6846"] })] })), _jsxs("div", { className: "flex gap-2 items-start", children: [_jsxs("div", { className: "flex-1 relative", children: [_jsx("div", { className: composerMode === 'write' ? 'block' : 'hidden', children: _jsx(RichTextEditor, { ref: editorRef, placeholder: `向 ${activeChannel.name} 发送消息,支持 Markdown...`, onSend: handleSendMessage, onChange: handleEditorChange, onAtTrigger: handleAtTrigger, minHeight: "44px", maxHeight: "200px" }) }), composerMode === 'preview' && (_jsx("div", { className: "sandbox-markdown-preview", role: "tabpanel", children: inputText.trim()
|
|
564
|
+
? _jsx(MarkdownContent, { text: inputText })
|
|
565
|
+
: _jsx("span", { className: "sandbox-markdown-preview-empty", children: "\u8F93\u5165 Markdown \u540E\u53EF\u5728\u8FD9\u91CC\u68C0\u67E5\u6700\u7EC8\u6548\u679C" }) })), atPopoverPosition && (_jsx("div", { className: "absolute z-50 rounded-lg border bg-popover shadow-md min-w-60 max-h-72 overflow-y-auto p-1", style: { top: `${atPopoverPosition.top}px`, left: `${atPopoverPosition.left}px` }, children: filteredAtSuggestions.length > 0 ? filteredAtSuggestions.map((user) => (_jsxs("div", { className: "flex items-center gap-2 p-2 rounded-md cursor-pointer hover:bg-accent transition-colors", onClick: () => selectAtUser(user), children: [_jsx(User, { size: 16, className: "text-muted-foreground" }), _jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "text-sm font-medium", children: user.name }), _jsxs("div", { className: "text-xs text-muted-foreground", children: ["ID: ", user.id] })] })] }, user.id))) : (_jsxs("div", { className: "flex flex-col items-center gap-2 p-4", children: [_jsx(Search, { size: 20, className: "text-muted-foreground/50" }), _jsx("span", { className: "text-xs text-muted-foreground", children: "\u672A\u627E\u5230\u5339\u914D\u7684\u7528\u6237" })] })) }))] }), _jsxs("button", { className: "inline-flex items-center gap-1.5 h-10 px-4 rounded-md bg-primary text-primary-foreground text-sm font-medium disabled:opacity-50 transition-colors hover:bg-primary/90", onClick: () => { const c = editorRef.current?.getContent(); if (c)
|
|
566
|
+
handleSendMessage(c.text, c.segments); }, disabled: !canExecute || !hasRenderableSegments(previewSegments), children: [_jsx(Send, { size: 16 }), " \u53D1\u9001"] })] }), _jsxs("div", { className: "flex items-center gap-2 flex-wrap text-xs text-muted-foreground", children: [_jsx(Info, { size: 12 }), " \u5FEB\u6377\u64CD\u4F5C:", _jsx("span", { className: "px-1 py-0.5 rounded border text-[10px]", children: "Enter" }), " \u53D1\u9001", _jsx("span", { className: "px-1 py-0.5 rounded border text-[10px]", children: "Shift+Enter" }), " \u6362\u884C", _jsx("span", { className: "px-1 py-0.5 rounded border text-[10px]", children: "```ts" }), " \u4EE3\u7801\u5757", _jsx("span", { className: "px-1 py-0.5 rounded border text-[10px]", children: "**\u6587\u672C**" }), " \u52A0\u7C97", _jsx("span", { className: "px-1 py-0.5 rounded border text-[10px]", children: "[@\u540D\u79F0]" }), " @\u67D0\u4EBA", _jsx("span", { className: "px-1 py-0.5 rounded border text-[10px]", children: "[video:URL]" }), _jsx("span", { className: "px-1 py-0.5 rounded border text-[10px]", children: "[audio:URL]" })] })] })] }), _jsxs("aside", { className: cn("agent-playground-inspector", showInspector && "show"), "aria-label": "\u8FD0\u884C\u68C0\u67E5\u5668", children: [_jsxs("div", { className: "agent-playground-inspector-head", children: [_jsxs("div", { children: [_jsx("span", { children: "Run inspector" }), _jsx("h2", { children: "\u8FD0\u884C\u68C0\u67E5\u5668" })] }), _jsxs("div", { children: [_jsx("button", { type: "button", onClick: () => void loadTrace(), "aria-label": "\u5237\u65B0 Agent Trace", disabled: traceLoading, children: _jsx(RefreshCw, { size: 15, className: cn(traceLoading && 'animate-spin') }) }), _jsx("button", { type: "button", className: "agent-playground-inspector-close", onClick: () => setShowInspector(false), "aria-label": "\u5173\u95ED\u8FD0\u884C\u68C0\u67E5\u5668", children: _jsx(X, { size: 15 }) })] })] }), _jsxs("div", { className: "agent-playground-inspector-tabs", role: "tablist", "aria-label": "\u68C0\u67E5\u5668\u89C6\u56FE", children: [_jsxs("button", { type: "button", role: "tab", "aria-selected": inspectorView === 'runs', className: cn(inspectorView === 'runs' && 'active'), onClick: () => setInspectorView('runs'), children: [_jsx(Activity, { size: 13 }), "\u4EFB\u52A1"] }), _jsxs("button", { type: "button", role: "tab", "aria-selected": inspectorView === 'artifacts', className: cn(inspectorView === 'artifacts' && 'active'), onClick: () => setInspectorView('artifacts'), children: [_jsx(FileDiff, { size: 13 }), "\u53D8\u66F4\u4E0E\u4EA7\u7269 ", _jsx("span", { children: workbenchArtifacts.length })] })] }), _jsxs("div", { className: cn("agent-playground-inspector-view", inspectorView !== 'runs' && 'hidden'), children: [_jsxs("section", { className: "agent-playground-inspector-section", children: [_jsxs("div", { className: "agent-playground-inspector-title", children: [_jsx("span", { children: "\u8FD0\u884C\u6982\u89C8" }), _jsx("small", { children: traceSummary.activeTurns > 0 ? 'live' : 'idle' })] }), _jsxs("div", { className: "agent-playground-metrics", children: [_jsxs("div", { children: [_jsx(Activity, {}), _jsx("strong", { children: traceSummary.eventCount.toLocaleString() }), _jsx("span", { children: "\u4E8B\u4EF6" })] }), _jsxs("div", { children: [_jsx(Wrench, {}), _jsx("strong", { children: traceSummary.toolCount.toLocaleString() }), _jsx("span", { children: "\u5DE5\u5177" })] }), _jsxs("div", { children: [_jsx(Coins, {}), _jsx("strong", { children: traceSummary.tokenCount.toLocaleString() }), _jsx("span", { children: "Token" })] }), _jsxs("div", { className: cn(traceSummary.problemCount > 0 && 'has-problem'), children: [_jsx(CircleAlert, {}), _jsx("strong", { children: traceSummary.problemCount }), _jsx("span", { children: "\u5F02\u5E38" })] })] })] }), _jsxs("section", { className: "agent-playground-inspector-section", children: [_jsx("div", { className: "agent-playground-inspector-title", children: _jsx("span", { children: "\u4E0A\u4E0B\u6587" }) }), _jsxs("dl", { className: "agent-playground-context-list", children: [_jsxs("div", { children: [_jsx("dt", { children: "Endpoint" }), _jsx("dd", { children: endpointId })] }), _jsxs("div", { children: [_jsx("dt", { children: "Scope" }), _jsx("dd", { children: activeChannel.type })] }), _jsxs("div", { children: [_jsx("dt", { children: "Scene" }), _jsx("dd", { children: activeChannel.id })] }), _jsxs("div", { children: [_jsx("dt", { children: "Workdir" }), _jsx("dd", { title: activeChannel.runConfig.workingDirectory, children: activeChannel.runConfig.workingDirectory || 'Host project root' })] }), _jsxs("div", { children: [_jsx("dt", { children: "Security" }), _jsx("dd", { children: activeChannel.runConfig.safetyMode })] }), _jsxs("div", { children: [_jsx("dt", { children: "Approval" }), _jsx("dd", { children: activeChannel.runConfig.safetyMode === 'read-only' ? 'deny' : activeChannel.runConfig.safetyMode === 'danger-full-access' ? 'allow' : activeChannel.runConfig.approvalMode })] }), _jsxs("div", { children: [_jsx("dt", { children: "Network" }), _jsx("dd", { children: activeChannel.runConfig.networkAccess ? 'enabled' : 'disabled' })] }), _jsxs("div", { children: [_jsx("dt", { children: "Isolation" }), _jsx("dd", { className: cn(shellIsolation?.available && 'is-online'), children: shellIsolation ? `${shellIsolation.provider}: ${shellIsolation.available ? 'ready' : 'unavailable'}` : 'checking' })] }), _jsxs("div", { children: [_jsx("dt", { children: "Transport" }), _jsx("dd", { className: cn(connected && 'is-online'), children: connected ? 'WebSocket online' : 'reconnecting' })] })] })] }), _jsxs("section", { className: "agent-playground-inspector-section agent-playground-task-section", children: [_jsxs("div", { className: "agent-playground-inspector-title", children: [_jsx("span", { children: "\u4EFB\u52A1\u5386\u53F2" }), _jsxs("small", { children: [taskRuns.length, " runs"] })] }), taskRuns.length > 0 ? (_jsx("div", { className: "agent-playground-task-list", children: taskRuns.slice(0, 8).map((run) => (_jsxs("article", { className: cn(`is-${run.status}`), children: [_jsx("i", {}), _jsxs("div", { children: [_jsx("strong", { children: taskStatusLabel(run.status) }), _jsx("code", { children: run.turnId.slice(0, 10) })] }), _jsxs("dl", { children: [_jsxs("div", { children: [_jsx("dt", { children: "\u8017\u65F6" }), _jsx("dd", { children: run.durationMs === undefined ? '进行中' : `${run.durationMs.toLocaleString()} ms` })] }), _jsxs("div", { children: [_jsx("dt", { children: "\u5DE5\u5177" }), _jsx("dd", { children: run.toolCount })] }), _jsxs("div", { children: [_jsx("dt", { children: "Token" }), _jsx("dd", { children: run.tokenCount.toLocaleString() })] })] })] }, run.id))) })) : _jsxs("div", { className: "agent-playground-artifact-empty", children: [_jsx(Activity, { size: 18 }), _jsx("span", { children: "\u8FD0\u884C\u4EFB\u52A1\u540E\u4F1A\u5728\u8FD9\u91CC\u5F62\u6210\u53EF\u56DE\u6EAF\u8BB0\u5F55" })] })] }), _jsxs("section", { className: "agent-playground-inspector-section agent-playground-trace-section", children: [_jsxs("div", { className: "agent-playground-inspector-title", children: [_jsx("span", { children: "\u6700\u8FD1\u6267\u884C" }), _jsx("small", { children: "2s sync" })] }), traceLoading && !trace ? (_jsxs("div", { className: "agent-playground-trace-loading", children: [_jsx("i", {}), _jsx("i", {}), _jsx("i", {})] })) : traceNotice && !trace?.events.length ? (_jsxs("div", { className: "agent-playground-trace-notice", children: [_jsx(CircleAlert, { size: 16 }), _jsx("span", { children: traceNotice })] })) : recentTraceEvents.length ? (_jsx("div", { className: "agent-playground-trace-list", children: recentTraceEvents.map((event) => {
|
|
567
|
+
const item = presentTraceEvent(event);
|
|
568
|
+
return (_jsxs("div", { className: cn("agent-playground-trace-item", `is-${item.tone}`), children: [_jsx("span", { className: "agent-playground-trace-dot" }), _jsxs("div", { children: [_jsx("strong", { children: item.title }), item.detail ? _jsx("small", { children: item.detail }) : null] }), _jsx("time", { children: new Date(event.recordedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) })] }, event.sequence));
|
|
569
|
+
}) })) : (_jsxs("div", { className: "agent-playground-trace-empty", children: [_jsx(Activity, { size: 18 }), _jsx("span", { children: "\u53D1\u9001\u4EFB\u52A1\u540E\u663E\u793A\u63A8\u7406\u4E0E\u5DE5\u5177\u8F68\u8FF9" })] }))] })] }), _jsx("div", { className: cn("agent-playground-inspector-view", inspectorView !== 'artifacts' && 'hidden'), children: _jsxs("section", { className: "agent-playground-inspector-section agent-playground-artifact-section", children: [_jsxs("div", { className: "agent-playground-inspector-title", children: [_jsx("span", { children: "\u6587\u4EF6\u4E0E\u547D\u4EE4\u4EA7\u7269" }), _jsxs("small", { children: [workbenchArtifacts.length, " items"] })] }), workbenchArtifacts.length > 0 ? (_jsx("div", { className: "agent-playground-artifact-list", children: workbenchArtifacts.map((artifact) => {
|
|
570
|
+
const ArtifactIcon = artifact.kind === 'file-change' ? FileDiff : artifact.kind === 'test' ? FlaskConical : Terminal;
|
|
571
|
+
const expanded = expandedArtifact === artifact.id;
|
|
572
|
+
return (_jsxs("article", { className: cn(`is-${artifact.status}`, expanded && 'is-expanded'), children: [_jsxs("button", { type: "button", onClick: () => setExpandedArtifact(expanded ? null : artifact.id), "aria-expanded": expanded, children: [_jsx("span", { className: "agent-playground-artifact-icon", children: _jsx(ArtifactIcon, { size: 14 }) }), _jsxs("span", { children: [_jsx("strong", { children: artifact.title }), _jsx("small", { children: artifact.path ?? artifact.detail ?? artifact.kind })] }), _jsx("em", { children: artifact.status }), _jsx(ChevronDown, { size: 14 })] }), expanded && (_jsxs("div", { className: "agent-playground-artifact-detail", children: [artifact.path && _jsx("code", { children: artifact.path }), artifact.diff ? _jsx("pre", { children: artifact.diff }) : _jsx("pre", { children: artifact.detail || '暂无输出' }), _jsxs("footer", { children: [_jsx("span", { children: artifact.durationMs === undefined ? '等待结果' : `${artifact.durationMs.toLocaleString()} ms` }), _jsx("code", { children: artifact.turnId.slice(0, 12) })] })] }))] }, artifact.id));
|
|
573
|
+
}) })) : _jsxs("div", { className: "agent-playground-artifact-empty", children: [_jsx(FileDiff, { size: 18 }), _jsx("span", { children: "Agent \u4FEE\u6539\u6587\u4EF6\u3001\u6267\u884C\u547D\u4EE4\u6216\u6D4B\u8BD5\u540E\uFF0C\u4EA7\u7269\u4F1A\u96C6\u4E2D\u51FA\u73B0\u5728\u8FD9\u91CC" })] })] }) }), _jsxs("a", { className: "agent-playground-studio-link", href: agentStudioPath(sessionKey), children: [_jsxs("span", { children: [_jsx(ExternalLink, { size: 15 }), "\u5728 Agent Studio \u4E2D\u5B8C\u6574\u8BCA\u65AD"] }), _jsx("code", { children: sessionKey })] })] }), showInspector && _jsx("div", { className: "agent-playground-inspector-overlay", onClick: () => setShowInspector(false) })] }));
|
|
574
|
+
}
|
|
575
|
+
function taskStatusLabel(status) {
|
|
576
|
+
if (status === 'running')
|
|
577
|
+
return 'Agent 运行中';
|
|
578
|
+
if (status === 'completed')
|
|
579
|
+
return '最近任务已完成';
|
|
580
|
+
if (status === 'failed')
|
|
581
|
+
return '最近任务失败';
|
|
582
|
+
return '最近任务已取消';
|
|
583
|
+
}
|
|
584
|
+
function messageText(segments) {
|
|
585
|
+
return segments.map((segment) => {
|
|
586
|
+
const data = segment.data;
|
|
587
|
+
if (segment.type === 'text' || segment.type === 'markdown' || segment.type === 'md') {
|
|
588
|
+
return String(data.text ?? data.content ?? '');
|
|
589
|
+
}
|
|
590
|
+
if (segment.type === 'mention' || segment.type === 'at')
|
|
591
|
+
return `@${String(data.name ?? data.target ?? '')}`;
|
|
592
|
+
return `[${segment.type}]`;
|
|
593
|
+
}).join(' ').trim();
|
|
594
|
+
}
|
|
595
|
+
function formatDuration(durationMs) {
|
|
596
|
+
if (durationMs < 1_000)
|
|
597
|
+
return `${durationMs.toLocaleString()} ms`;
|
|
598
|
+
if (durationMs < 60_000)
|
|
599
|
+
return `${(durationMs / 1_000).toFixed(durationMs < 10_000 ? 1 : 0)} s`;
|
|
600
|
+
return `${Math.floor(durationMs / 60_000)}m ${Math.round((durationMs % 60_000) / 1_000)}s`;
|
|
601
|
+
}
|
|
602
|
+
function runStepStatusLabel(status) {
|
|
603
|
+
if (status === 'running')
|
|
604
|
+
return '进行中';
|
|
605
|
+
if (status === 'completed')
|
|
606
|
+
return '完成';
|
|
607
|
+
if (status === 'denied')
|
|
608
|
+
return '已拒绝';
|
|
609
|
+
if (status === 'cancelled')
|
|
610
|
+
return '已取消';
|
|
611
|
+
return '失败';
|
|
612
|
+
}
|
|
613
|
+
function safeFileName(value) {
|
|
614
|
+
return value.trim().replace(/[^\p{L}\p{N}._-]+/gu, '-').replace(/^-+|-+$/gu, '').slice(0, 48) || 'session';
|
|
363
615
|
}
|