@zhin.js/adapter-sandbox 1.0.70 → 1.1.2

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.
@@ -0,0 +1,615 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
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
+ import { buildSandboxWebSocketUrl } from './sandboxTransport';
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
+ 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';
10
+ export default function Sandbox() {
11
+ const [initialState] = useState(() => loadPlaygroundState());
12
+ const [messages, setMessages] = useState(() => [...initialState.messages]);
13
+ const [channels, setChannels] = useState(() => [...initialState.sessions]);
14
+ const [faceList, setFaceList] = 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]));
18
+ const [inputText, setInputText] = useState('');
19
+ const [endpointId, setBotName] = useState('sandbox-bot');
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);
29
+ const [showFacePicker, setShowFacePicker] = useState(false);
30
+ /** 输入区:插入图片 / 视频 / 音频 URL */
31
+ const [mediaPanel, setMediaPanel] = useState(null);
32
+ const [mediaUrl, setMediaUrl] = useState('');
33
+ const [atPopoverPosition, setAtPopoverPosition] = useState(null);
34
+ const [atSearchQuery, setAtSearchQuery] = useState('');
35
+ const [faceSearchQuery, setFaceSearchQuery] = useState('');
36
+ const [atSuggestions] = useState([
37
+ { id: 'actor-owner', name: '当前用户' }, { id: 'actor-reviewer', name: '审阅者' },
38
+ { id: 'actor-operator', name: '协作者' }, { id: 'actor-bot', name: 'Sandbox Agent' }
39
+ ]);
40
+ const [previewSegments, setPreviewSegments] = useState([]);
41
+ const [composerMode, setComposerMode] = useState('write');
42
+ const [showChannelList, setShowChannelList] = useState(false);
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);
52
+ const messagesEndRef = useRef(null);
53
+ const wsRef = useRef(null);
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]);
77
+ const fetchFaceList = async () => {
78
+ try {
79
+ const res = await fetch('https://face.viki.moe/metadata.json');
80
+ setFaceList(await res.json());
81
+ }
82
+ catch (err) {
83
+ console.error('[Sandbox] Failed to fetch face list:', err);
84
+ }
85
+ };
86
+ useEffect(() => { fetchFaceList(); }, []);
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
+ }
117
+ if (data.type === 'edit' && data.messageId) {
118
+ const content = Array.isArray(data.content)
119
+ ? data.content
120
+ : parseTextToSegments(String(data.content ?? ''));
121
+ setMessages((prev) => prev.map((m) => (m.id === data.messageId ? { ...m, content } : m)));
122
+ return;
123
+ }
124
+ const content = typeof data.content === 'string'
125
+ ? parseTextToSegments(data.content)
126
+ : Array.isArray(data.content) ? data.content : parseTextToSegments(String(data.content ?? ''));
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}`;
133
+ setChannels((prev) => {
134
+ if (prev.some((c) => c.id === data.id && c.type === channelType))
135
+ return prev;
136
+ const created = {
137
+ id: data.id, name: channelName, type: channelType, unread: 0,
138
+ runConfig: { ...activeChannelRef.current.runConfig },
139
+ };
140
+ setActiveChannel(created);
141
+ return [...prev, created];
142
+ });
143
+ setMessages((prev) => [...prev, {
144
+ id: data.messageId ?? `bot_${data.timestamp}`, type: 'received', channelType,
145
+ channelId: data.id, channelName, senderId: 'endpoint',
146
+ senderName: data.bot || endpointIdRef.current, content, timestamp: data.timestamp,
147
+ }]);
148
+ };
149
+ const sendInteractiveAction = (payload, messageId) => {
150
+ const ws = wsRef.current;
151
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
152
+ setTransportNotice('Sandbox 连接尚未就绪,审批选择未发送。');
153
+ return;
154
+ }
155
+ const segments = [{ type: 'action', data: { id: payload, payload } }];
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
+ }
174
+ };
175
+ useEffect(() => {
176
+ let closed = false;
177
+ let retryTimer;
178
+ let attempt = 0;
179
+ /** Ignore close events from a socket we intentionally replaced (login/base change). */
180
+ let replaceInFlight = false;
181
+ const connect = () => {
182
+ if (closed)
183
+ return;
184
+ if (retryTimer) {
185
+ clearTimeout(retryTimer);
186
+ retryTimer = undefined;
187
+ }
188
+ const wsUrl = buildSandboxWebSocketUrl();
189
+ // Tear down previous socket before opening a new one so we don't
190
+ // leave two concurrent /sandbox sessions for fixed-name endpoints.
191
+ const previous = wsRef.current;
192
+ if (previous) {
193
+ replaceInFlight = true;
194
+ previous.onclose = null;
195
+ previous.onerror = null;
196
+ previous.onmessage = null;
197
+ previous.onopen = null;
198
+ try {
199
+ previous.close();
200
+ }
201
+ catch { /* already closed */ }
202
+ replaceInFlight = false;
203
+ }
204
+ const ws = new WebSocket(wsUrl);
205
+ wsRef.current = ws;
206
+ ws.onopen = () => {
207
+ attempt = 0;
208
+ setConnected(true);
209
+ };
210
+ ws.onmessage = (event) => {
211
+ try {
212
+ handleInboundPayload(JSON.parse(String(event.data)));
213
+ }
214
+ catch (err) {
215
+ console.error('[Sandbox] Failed to parse message:', err);
216
+ }
217
+ };
218
+ ws.onclose = () => {
219
+ if (wsRef.current !== ws)
220
+ return;
221
+ setConnected(false);
222
+ wsRef.current = null;
223
+ if (closed || replaceInFlight)
224
+ return;
225
+ const delay = Math.min(8_000, 500 * 2 ** attempt);
226
+ attempt += 1;
227
+ retryTimer = setTimeout(connect, delay);
228
+ };
229
+ ws.onerror = () => {
230
+ /* close handler reconnects */
231
+ };
232
+ };
233
+ const onAuthOrStorage = (event) => {
234
+ // storage fires for other tabs; same-tab login sets localStorage then
235
+ // dispatches zhin:auth-required / custom login events.
236
+ if (event && event.type === 'storage') {
237
+ const key = event.key;
238
+ if (key != null
239
+ && key !== 'zhin_api_token'
240
+ && key !== 'zhin_api_base'
241
+ && key !== 'HTTP_TOKEN'
242
+ && key !== 'zhin_http_token') {
243
+ return;
244
+ }
245
+ }
246
+ attempt = 0;
247
+ connect();
248
+ };
249
+ connect();
250
+ if (typeof window !== 'undefined') {
251
+ window.addEventListener('storage', onAuthOrStorage);
252
+ window.addEventListener('zhin:auth-required', onAuthOrStorage);
253
+ // Remote Console may fire this after successful login (token written).
254
+ window.addEventListener('zhin:auth-changed', onAuthOrStorage);
255
+ window.addEventListener('zhin:api-base-changed', onAuthOrStorage);
256
+ }
257
+ return () => {
258
+ closed = true;
259
+ if (retryTimer)
260
+ clearTimeout(retryTimer);
261
+ if (typeof window !== 'undefined') {
262
+ window.removeEventListener('storage', onAuthOrStorage);
263
+ window.removeEventListener('zhin:auth-required', onAuthOrStorage);
264
+ window.removeEventListener('zhin:auth-changed', onAuthOrStorage);
265
+ window.removeEventListener('zhin:api-base-changed', onAuthOrStorage);
266
+ }
267
+ wsRef.current?.close();
268
+ wsRef.current = null;
269
+ setConnected(false);
270
+ };
271
+ }, []);
272
+ useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]);
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]);
309
+ const parseTextToSegments = (text) => {
310
+ const segments = [];
311
+ const regex = /\[@([^\]]+)\]|\[face:(\d+)\]|\[image:([^\]]+)\]|\[video:([^\]]+)\]|\[audio:([^\]]+)\]/g;
312
+ let lastIndex = 0;
313
+ let match;
314
+ while ((match = regex.exec(text)) !== null) {
315
+ if (match.index > lastIndex) {
316
+ const t = text.substring(lastIndex, match.index);
317
+ if (t)
318
+ segments.push({ type: 'text', data: { text: t } });
319
+ }
320
+ if (match[1])
321
+ segments.push({ type: 'mention', data: { target: match[1], name: match[1] } });
322
+ else if (match[2])
323
+ segments.push({ type: 'face', data: { id: parseInt(match[2], 10) } });
324
+ else if (match[3])
325
+ segments.push({ type: 'image', data: { media: { kind: 'url', value: match[3] } } });
326
+ else if (match[4])
327
+ segments.push({ type: 'video', data: { media: { kind: 'url', value: match[4] } } });
328
+ else if (match[5])
329
+ segments.push({ type: 'audio', data: { media: { kind: 'url', value: match[5] } } });
330
+ lastIndex = regex.lastIndex;
331
+ }
332
+ if (lastIndex < text.length) {
333
+ const r = text.substring(lastIndex);
334
+ if (r)
335
+ segments.push({ type: 'text', data: { text: r } });
336
+ }
337
+ return segments.length > 0 ? segments : [{ type: 'text', data: { text } }];
338
+ };
339
+ const hasRenderableSegments = (segments) => {
340
+ if (segments.length === 0)
341
+ return false;
342
+ return segments.some((s) => {
343
+ if (s.type === 'text')
344
+ return Boolean(String(s.data?.text ?? '').trim());
345
+ if (s.type === 'keyboard')
346
+ return true;
347
+ return true;
348
+ });
349
+ };
350
+ const renderMessageSegments = (segments, isSent, messageId, interactionResolved = false) => {
351
+ const ring = isSent ? 'ring-1 ring-primary-foreground/25' : 'ring-1 ring-border/60';
352
+ return segments.map((segment, index) => {
353
+ if (typeof segment === 'string') {
354
+ return _jsx(MarkdownContent, { text: segment, className: isSent ? 'zhin-markdown--inverse' : undefined }, index);
355
+ }
356
+ const d = segment.data;
357
+ switch (segment.type) {
358
+ case 'text':
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);
364
+ case 'mention':
365
+ case 'at':
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);
367
+ case 'face':
368
+ return _jsx("img", { src: `https://face.viki.moe/apng/${d.id}.png`, alt: String(d.name ?? ''), className: "w-6 h-6 inline-block align-middle mx-0.5", title: String(d.name ?? d.id ?? '') }, index);
369
+ case 'dice':
370
+ return _jsxs("span", { className: "inline-flex items-center px-1.5 py-0.5 rounded bg-secondary text-xs mx-0.5", children: ["\uD83C\uDFB2 ", d.result != null ? `点数 ${String(d.result)}` : '骰子'] }, index);
371
+ case 'rps':
372
+ return _jsxs("span", { className: "inline-flex items-center px-1.5 py-0.5 rounded bg-secondary text-xs mx-0.5", children: ["\u270A ", d.result != null ? `结果 ${String(d.result)}` : '猜拳'] }, index);
373
+ case 'image': {
374
+ const raw = pickMediaRawUrl(d);
375
+ const src = resolveMediaSrc(raw, 'image');
376
+ if (!src)
377
+ return _jsx("span", { className: "text-xs opacity-70", children: "[\u56FE\u7247]" }, index);
378
+ return (_jsx("a", { href: src, target: "_blank", rel: "noreferrer", className: "block my-1", children: _jsx("img", { src: src, alt: "", className: cn('max-w-[min(320px,88vw)] rounded-lg block', ring, 'ring-offset-0'), onError: (e) => { e.target.style.display = 'none'; } }) }, index));
379
+ }
380
+ case 'video': {
381
+ const raw = pickMediaRawUrl(d);
382
+ const src = resolveMediaSrc(raw, 'video');
383
+ if (!src)
384
+ return _jsx("span", { className: "text-xs opacity-70", children: "[\u89C6\u9891\u65E0\u5730\u5740]" }, index);
385
+ return (_jsx("video", { src: src, controls: true, playsInline: true, preload: "metadata", className: cn('max-w-[min(360px,92vw)] max-h-72 rounded-lg my-1 bg-black/10', ring) }, index));
386
+ }
387
+ case 'audio':
388
+ case 'record': {
389
+ const raw = pickMediaRawUrl(d);
390
+ const src = resolveMediaSrc(raw, 'audio');
391
+ if (!src)
392
+ return _jsx("span", { className: "text-xs opacity-70", children: "[\u97F3\u9891\u65E0\u5730\u5740]" }, index);
393
+ return (_jsx("audio", { src: src, controls: true, preload: "metadata", className: cn('w-full max-w-sm my-2 h-10', isSent && 'opacity-95') }, index));
394
+ }
395
+ case 'reply':
396
+ return (_jsxs("div", { className: "mb-1 rounded-md border border-dashed px-2 py-1 text-xs opacity-90", children: ["\u21A9 \u5F15\u7528\u6D88\u606F #", String(d.message_id ?? d.id ?? '')] }, index));
397
+ case 'forward': {
398
+ const messages = d.messages;
399
+ const title = String(d.title ?? '聊天记录');
400
+ return (_jsxs("div", { className: "my-1 rounded-md border bg-background/40 px-2 py-2 text-xs space-y-1", children: [_jsxs("div", { className: "font-medium", children: ["\uD83D\uDCE8 ", title] }), Array.isArray(messages) && messages.length > 0 ? (_jsxs("div", { className: "space-y-1 pl-2 border-l-2 border-muted", children: [messages.slice(0, 3).map((batch, bi) => (_jsx("div", { className: "opacity-90", children: batch.map((s, si) => (_jsx("span", { children: s.type === 'text' ? String(s.data?.text ?? '') : `[${s.type ?? 'seg'}]` }, si))) }, bi))), messages.length > 3 && _jsxs("div", { className: "opacity-60", children: ["\u2026\u5171 ", messages.length, " \u6761"] })] })) : (_jsx("div", { className: "opacity-70", children: "[\u5408\u5E76\u8F6C\u53D1]" }))] }, index));
401
+ }
402
+ case 'keyboard': {
403
+ const rows = d.rows ?? [];
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));
406
+ }
407
+ default:
408
+ return _jsxs("span", { className: "text-xs opacity-70", children: ["[", segment.type, "]"] }, index);
409
+ }
410
+ });
411
+ };
412
+ const handleSendMessage = (text, segments) => {
413
+ if (!canExecute || !hasRenderableSegments(segments))
414
+ return;
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() };
416
+ setMessages((prev) => [...prev, newMessage]);
417
+ setInputText('');
418
+ setPreviewSegments([]);
419
+ setComposerMode('write');
420
+ editorRef.current?.clear();
421
+ // Stamp type+id so Host sandbox endpoint preserves channel context for outbound replies.
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
+ });
430
+ wsRef.current?.send(payload);
431
+ };
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)
437
+ setShowChannelList(false); };
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);
454
+ };
455
+ const getChannelIcon = (type) => { switch (type) {
456
+ case 'private': return _jsx(User, { size: 16 });
457
+ case 'group': return _jsx(Users, { size: 16 });
458
+ case 'channel': return _jsx(Hash, { size: 16 });
459
+ default: return _jsx(MessageSquare, { size: 16 });
460
+ } };
461
+ const insertFace = (faceId) => { editorRef.current?.insertFace(faceId); setShowFacePicker(false); };
462
+ const commitMediaUrl = () => {
463
+ const u = mediaUrl.trim();
464
+ if (!u || !mediaPanel)
465
+ return;
466
+ if (mediaPanel === 'image')
467
+ editorRef.current?.insertImage(u);
468
+ else if (mediaPanel === 'video')
469
+ editorRef.current?.insertVideo(u);
470
+ else
471
+ editorRef.current?.insertAudio(u);
472
+ setMediaUrl('');
473
+ setMediaPanel(null);
474
+ };
475
+ const selectAtUser = (user) => { editorRef.current?.replaceAtTrigger(user.name, user.id); setAtPopoverPosition(null); setAtSearchQuery(''); };
476
+ const handleAtTrigger = (show, searchQuery, position) => {
477
+ if (activeChannel.type === 'private') {
478
+ setAtPopoverPosition(null);
479
+ setAtSearchQuery('');
480
+ return;
481
+ }
482
+ if (show && position) {
483
+ setAtPopoverPosition(position);
484
+ setAtSearchQuery(searchQuery);
485
+ }
486
+ else {
487
+ setAtPopoverPosition(null);
488
+ setAtSearchQuery('');
489
+ }
490
+ };
491
+ const filteredAtSuggestions = atSuggestions.filter((user) => { if (!atSearchQuery.trim())
492
+ return true; const q = atSearchQuery.toLowerCase(); return user.name.toLowerCase().includes(q) || user.id.toLowerCase().includes(q); });
493
+ const handleEditorChange = (text, segments) => { setInputText(text); setPreviewSegments(segments); };
494
+ const filteredFaces = faceList.filter(face => face.name.toLowerCase().includes(faceSearchQuery.toLowerCase()) || face.describe.toLowerCase().includes(faceSearchQuery.toLowerCase()));
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' ? 'auto' : activeChannel.runConfig.safetyMode === 'danger-full-access' ? 'bypass' : 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: "auto", children: "\u5BA1\u6838 Agent \u81EA\u52A8\u5224\u65AD" }), _jsx("option", { value: "bypass", children: "\u7ED5\u8FC7\u5BA1\u6279" })] })] }), _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' ? 'auto' : activeChannel.runConfig.safetyMode === 'danger-full-access' ? 'bypass' : 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';
615
+ }