@zhin.js/adapter-sandbox 7.0.11 → 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 +47 -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
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { DEFAULT_SANDBOX_AGENT_RUN_CONFIG, normalizeSandboxAgentRunConfig, } from "../lib/run-config.js";
|
|
3
|
+
export const PLAYGROUND_STORAGE_KEY = 'zhin.sandbox.agent-playground.v1';
|
|
4
|
+
export function createDefaultPlaygroundState(workingDirectory = '') {
|
|
5
|
+
const runConfig = (safetyMode) => ({
|
|
6
|
+
...DEFAULT_SANDBOX_AGENT_RUN_CONFIG,
|
|
7
|
+
workingDirectory,
|
|
8
|
+
safetyMode,
|
|
9
|
+
});
|
|
10
|
+
const sessions = [
|
|
11
|
+
{ id: 'sandbox-user', name: '快速试验', type: 'private', unread: 0, runConfig: runConfig('workspace-write') },
|
|
12
|
+
{ id: 'sandbox-group', name: '群组作用域', type: 'group', unread: 0, runConfig: runConfig('workspace-write') },
|
|
13
|
+
{ id: 'sandbox-channel', name: '频道作用域', type: 'channel', unread: 0, runConfig: runConfig('read-only') },
|
|
14
|
+
];
|
|
15
|
+
return Object.freeze({
|
|
16
|
+
version: 1,
|
|
17
|
+
activeSessionId: sessions[0].id,
|
|
18
|
+
activeSessionType: sessions[0].type,
|
|
19
|
+
sessions,
|
|
20
|
+
messages: [],
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export function loadPlaygroundState(storage = browserStorage()) {
|
|
24
|
+
const fallback = createDefaultPlaygroundState();
|
|
25
|
+
if (!storage)
|
|
26
|
+
return fallback;
|
|
27
|
+
try {
|
|
28
|
+
const raw = storage.getItem(PLAYGROUND_STORAGE_KEY);
|
|
29
|
+
if (!raw)
|
|
30
|
+
return fallback;
|
|
31
|
+
const parsed = JSON.parse(raw);
|
|
32
|
+
if (parsed.version !== 1)
|
|
33
|
+
return fallback;
|
|
34
|
+
const sessions = Array.isArray(parsed.sessions)
|
|
35
|
+
? parsed.sessions.map(parseSession).filter((item) => Boolean(item))
|
|
36
|
+
: [];
|
|
37
|
+
if (sessions.length === 0)
|
|
38
|
+
return fallback;
|
|
39
|
+
const sessionKeys = new Set(sessions.map(sessionIdentity));
|
|
40
|
+
const messages = Array.isArray(parsed.messages)
|
|
41
|
+
? parsed.messages.map(parseMessage).filter((item) => (Boolean(item) && sessionKeys.has(sessionIdentity({ id: item.channelId, type: item.channelType }))))
|
|
42
|
+
: [];
|
|
43
|
+
const requestedActive = typeof parsed.activeSessionId === 'string'
|
|
44
|
+
? sessions.find((session) => session.id === parsed.activeSessionId && (parsed.activeSessionType === undefined || session.type === parsed.activeSessionType))
|
|
45
|
+
: undefined;
|
|
46
|
+
const activeSession = requestedActive ?? sessions[0];
|
|
47
|
+
return Object.freeze({
|
|
48
|
+
version: 1,
|
|
49
|
+
activeSessionId: activeSession.id,
|
|
50
|
+
activeSessionType: activeSession.type,
|
|
51
|
+
sessions,
|
|
52
|
+
messages,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return fallback;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function savePlaygroundState(state, storage = browserStorage()) {
|
|
60
|
+
if (!storage)
|
|
61
|
+
return false;
|
|
62
|
+
try {
|
|
63
|
+
const sessions = state.sessions;
|
|
64
|
+
const sessionKeys = new Set(sessions.map(sessionIdentity));
|
|
65
|
+
const activeSession = sessions.find((session) => session.id === state.activeSessionId && (state.activeSessionType === undefined || session.type === state.activeSessionType)) ?? sessions[0];
|
|
66
|
+
storage.setItem(PLAYGROUND_STORAGE_KEY, JSON.stringify({
|
|
67
|
+
version: 1,
|
|
68
|
+
activeSessionId: activeSession?.id ?? '',
|
|
69
|
+
activeSessionType: activeSession?.type,
|
|
70
|
+
sessions,
|
|
71
|
+
messages: state.messages.filter((message) => sessionKeys.has(sessionIdentity({
|
|
72
|
+
id: message.channelId,
|
|
73
|
+
type: message.channelType,
|
|
74
|
+
}))),
|
|
75
|
+
}));
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function parseSession(value) {
|
|
83
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
84
|
+
return undefined;
|
|
85
|
+
const item = value;
|
|
86
|
+
if (typeof item.id !== 'string' || !item.id.trim() || typeof item.name !== 'string')
|
|
87
|
+
return undefined;
|
|
88
|
+
if (item.type !== 'private' && item.type !== 'group' && item.type !== 'channel')
|
|
89
|
+
return undefined;
|
|
90
|
+
return {
|
|
91
|
+
id: item.id.slice(0, 256),
|
|
92
|
+
name: item.name.trim().slice(0, 120) || '未命名试验',
|
|
93
|
+
type: item.type,
|
|
94
|
+
unread: Number.isSafeInteger(item.unread) && Number(item.unread) > 0 ? Number(item.unread) : 0,
|
|
95
|
+
runConfig: normalizeSandboxAgentRunConfig(item.runConfig) ?? DEFAULT_SANDBOX_AGENT_RUN_CONFIG,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function parseMessage(value) {
|
|
99
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
100
|
+
return undefined;
|
|
101
|
+
const item = value;
|
|
102
|
+
if (typeof item.id !== 'string' || typeof item.channelId !== 'string' || !Array.isArray(item.content))
|
|
103
|
+
return undefined;
|
|
104
|
+
if (item.type !== 'sent' && item.type !== 'received')
|
|
105
|
+
return undefined;
|
|
106
|
+
if (item.channelType !== 'private' && item.channelType !== 'group' && item.channelType !== 'channel')
|
|
107
|
+
return undefined;
|
|
108
|
+
return {
|
|
109
|
+
id: item.id.slice(0, 256),
|
|
110
|
+
type: item.type,
|
|
111
|
+
channelType: item.channelType,
|
|
112
|
+
channelId: item.channelId.slice(0, 256),
|
|
113
|
+
channelName: typeof item.channelName === 'string' ? item.channelName.slice(0, 120) : '',
|
|
114
|
+
senderId: typeof item.senderId === 'string' ? item.senderId.slice(0, 256) : '',
|
|
115
|
+
senderName: typeof item.senderName === 'string' ? item.senderName.slice(0, 120) : '',
|
|
116
|
+
content: item.content,
|
|
117
|
+
timestamp: Number.isFinite(item.timestamp) ? Number(item.timestamp) : Date.now(),
|
|
118
|
+
...(item.interactionResolved === true ? { interactionResolved: true } : {}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function browserStorage() {
|
|
122
|
+
return typeof window === 'undefined' ? undefined : window.localStorage;
|
|
123
|
+
}
|
|
124
|
+
function sessionIdentity(value) {
|
|
125
|
+
return `${value.type}\0${value.id}`;
|
|
126
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { PLAYGROUND_STORAGE_KEY, createDefaultPlaygroundState, loadPlaygroundState, savePlaygroundState, } from './playgroundState.js';
|
|
3
|
+
class MemoryStorage {
|
|
4
|
+
values = new Map();
|
|
5
|
+
getItem(key) { return this.values.get(key) ?? null; }
|
|
6
|
+
setItem(key, value) { this.values.set(key, value); }
|
|
7
|
+
}
|
|
8
|
+
describe('sandbox playground persistence', () => {
|
|
9
|
+
it('round-trips sessions, messages and per-session run configuration', () => {
|
|
10
|
+
const storage = new MemoryStorage();
|
|
11
|
+
const initial = createDefaultPlaygroundState('/workspace/zhin');
|
|
12
|
+
const sessions = initial.sessions.map((session, index) => index === 0
|
|
13
|
+
? { ...session, runConfig: { ...session.runConfig, safetyMode: 'read-only', networkAccess: true } }
|
|
14
|
+
: session);
|
|
15
|
+
expect(savePlaygroundState({
|
|
16
|
+
activeSessionId: sessions[0].id,
|
|
17
|
+
sessions,
|
|
18
|
+
messages: [{
|
|
19
|
+
id: 'm1', type: 'sent', channelType: 'private', channelId: sessions[0].id,
|
|
20
|
+
channelName: sessions[0].name, senderId: 'owner', senderName: 'Owner',
|
|
21
|
+
content: [{ type: 'text', data: { text: 'hello' } }], timestamp: 42,
|
|
22
|
+
interactionResolved: true,
|
|
23
|
+
}],
|
|
24
|
+
}, storage)).toBe(true);
|
|
25
|
+
const restored = loadPlaygroundState(storage);
|
|
26
|
+
expect(restored.activeSessionId).toBe('sandbox-user');
|
|
27
|
+
expect(restored.sessions[0]?.runConfig).toMatchObject({
|
|
28
|
+
workingDirectory: '/workspace/zhin', safetyMode: 'read-only', networkAccess: true,
|
|
29
|
+
});
|
|
30
|
+
expect(restored.messages[0]).toMatchObject({ id: 'm1', channelId: 'sandbox-user', interactionResolved: true });
|
|
31
|
+
});
|
|
32
|
+
it('fails closed to defaults for malformed storage', () => {
|
|
33
|
+
const storage = new MemoryStorage();
|
|
34
|
+
storage.values.set(PLAYGROUND_STORAGE_KEY, '{broken');
|
|
35
|
+
expect(loadPlaygroundState(storage).sessions.map((session) => session.id)).toEqual([
|
|
36
|
+
'sandbox-user', 'sandbox-group', 'sandbox-channel',
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
it('does not silently accept an unknown persisted schema version', () => {
|
|
40
|
+
const storage = new MemoryStorage();
|
|
41
|
+
storage.values.set(PLAYGROUND_STORAGE_KEY, JSON.stringify({ version: 2, sessions: [] }));
|
|
42
|
+
expect(loadPlaygroundState(storage).activeSessionId).toBe('sandbox-user');
|
|
43
|
+
});
|
|
44
|
+
it('keeps complete message history instead of silently trimming old entries', () => {
|
|
45
|
+
const storage = new MemoryStorage();
|
|
46
|
+
const initial = createDefaultPlaygroundState('/workspace/zhin');
|
|
47
|
+
const messages = Array.from({ length: 240 }, (_, index) => ({
|
|
48
|
+
id: `m${index}`,
|
|
49
|
+
type: 'sent',
|
|
50
|
+
channelType: 'private',
|
|
51
|
+
channelId: initial.sessions[0].id,
|
|
52
|
+
channelName: initial.sessions[0].name,
|
|
53
|
+
senderId: 'owner',
|
|
54
|
+
senderName: 'Owner',
|
|
55
|
+
content: [{ type: 'text', data: { text: `message ${index}` } }],
|
|
56
|
+
timestamp: index,
|
|
57
|
+
}));
|
|
58
|
+
expect(savePlaygroundState({
|
|
59
|
+
activeSessionId: initial.activeSessionId,
|
|
60
|
+
sessions: initial.sessions,
|
|
61
|
+
messages,
|
|
62
|
+
}, storage)).toBe(true);
|
|
63
|
+
expect(loadPlaygroundState(storage).messages).toHaveLength(240);
|
|
64
|
+
});
|
|
65
|
+
it('keeps sessions with the same id isolated by scope', () => {
|
|
66
|
+
const storage = new MemoryStorage();
|
|
67
|
+
const initial = createDefaultPlaygroundState('/workspace/zhin');
|
|
68
|
+
const sessions = [
|
|
69
|
+
{ ...initial.sessions[0], id: 'shared', type: 'private' },
|
|
70
|
+
{ ...initial.sessions[1], id: 'shared', type: 'group' },
|
|
71
|
+
];
|
|
72
|
+
expect(savePlaygroundState({
|
|
73
|
+
activeSessionId: 'shared',
|
|
74
|
+
activeSessionType: 'group',
|
|
75
|
+
sessions,
|
|
76
|
+
messages: sessions.map((session, index) => ({
|
|
77
|
+
id: `m${index}`,
|
|
78
|
+
type: 'sent',
|
|
79
|
+
channelType: session.type,
|
|
80
|
+
channelId: session.id,
|
|
81
|
+
channelName: session.name,
|
|
82
|
+
senderId: 'owner',
|
|
83
|
+
senderName: 'Owner',
|
|
84
|
+
content: [{ type: 'text', data: { text: session.type } }],
|
|
85
|
+
timestamp: index,
|
|
86
|
+
})),
|
|
87
|
+
}, storage)).toBe(true);
|
|
88
|
+
const restored = loadPlaygroundState(storage);
|
|
89
|
+
expect(restored.activeSessionType).toBe('group');
|
|
90
|
+
expect(restored.messages.map((message) => message.channelType)).toEqual(['private', 'group']);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PLAYGROUND_STORAGE_KEY,
|
|
3
|
+
createDefaultPlaygroundState,
|
|
4
|
+
loadPlaygroundState,
|
|
5
|
+
savePlaygroundState,
|
|
6
|
+
type PlaygroundStorage,
|
|
7
|
+
} from './playgroundState.js';
|
|
8
|
+
|
|
9
|
+
class MemoryStorage implements PlaygroundStorage {
|
|
10
|
+
readonly values = new Map<string, string>();
|
|
11
|
+
getItem(key: string): string | null { return this.values.get(key) ?? null; }
|
|
12
|
+
setItem(key: string, value: string): void { this.values.set(key, value); }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('sandbox playground persistence', () => {
|
|
16
|
+
it('round-trips sessions, messages and per-session run configuration', () => {
|
|
17
|
+
const storage = new MemoryStorage();
|
|
18
|
+
const initial = createDefaultPlaygroundState('/workspace/zhin');
|
|
19
|
+
const sessions = initial.sessions.map((session, index) => index === 0
|
|
20
|
+
? { ...session, runConfig: { ...session.runConfig, safetyMode: 'read-only' as const, networkAccess: true } }
|
|
21
|
+
: session);
|
|
22
|
+
expect(savePlaygroundState({
|
|
23
|
+
activeSessionId: sessions[0]!.id,
|
|
24
|
+
sessions,
|
|
25
|
+
messages: [{
|
|
26
|
+
id: 'm1', type: 'sent', channelType: 'private', channelId: sessions[0]!.id,
|
|
27
|
+
channelName: sessions[0]!.name, senderId: 'owner', senderName: 'Owner',
|
|
28
|
+
content: [{ type: 'text', data: { text: 'hello' } }], timestamp: 42,
|
|
29
|
+
interactionResolved: true,
|
|
30
|
+
}],
|
|
31
|
+
}, storage)).toBe(true);
|
|
32
|
+
|
|
33
|
+
const restored = loadPlaygroundState(storage);
|
|
34
|
+
expect(restored.activeSessionId).toBe('sandbox-user');
|
|
35
|
+
expect(restored.sessions[0]?.runConfig).toMatchObject({
|
|
36
|
+
workingDirectory: '/workspace/zhin', safetyMode: 'read-only', networkAccess: true,
|
|
37
|
+
});
|
|
38
|
+
expect(restored.messages[0]).toMatchObject({ id: 'm1', channelId: 'sandbox-user', interactionResolved: true });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('fails closed to defaults for malformed storage', () => {
|
|
42
|
+
const storage = new MemoryStorage();
|
|
43
|
+
storage.values.set(PLAYGROUND_STORAGE_KEY, '{broken');
|
|
44
|
+
expect(loadPlaygroundState(storage).sessions.map((session) => session.id)).toEqual([
|
|
45
|
+
'sandbox-user', 'sandbox-group', 'sandbox-channel',
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('does not silently accept an unknown persisted schema version', () => {
|
|
50
|
+
const storage = new MemoryStorage();
|
|
51
|
+
storage.values.set(PLAYGROUND_STORAGE_KEY, JSON.stringify({ version: 2, sessions: [] }));
|
|
52
|
+
expect(loadPlaygroundState(storage).activeSessionId).toBe('sandbox-user');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('keeps complete message history instead of silently trimming old entries', () => {
|
|
56
|
+
const storage = new MemoryStorage();
|
|
57
|
+
const initial = createDefaultPlaygroundState('/workspace/zhin');
|
|
58
|
+
const messages = Array.from({ length: 240 }, (_, index) => ({
|
|
59
|
+
id: `m${index}`,
|
|
60
|
+
type: 'sent' as const,
|
|
61
|
+
channelType: 'private' as const,
|
|
62
|
+
channelId: initial.sessions[0]!.id,
|
|
63
|
+
channelName: initial.sessions[0]!.name,
|
|
64
|
+
senderId: 'owner',
|
|
65
|
+
senderName: 'Owner',
|
|
66
|
+
content: [{ type: 'text', data: { text: `message ${index}` } }],
|
|
67
|
+
timestamp: index,
|
|
68
|
+
}));
|
|
69
|
+
expect(savePlaygroundState({
|
|
70
|
+
activeSessionId: initial.activeSessionId,
|
|
71
|
+
sessions: initial.sessions,
|
|
72
|
+
messages,
|
|
73
|
+
}, storage)).toBe(true);
|
|
74
|
+
expect(loadPlaygroundState(storage).messages).toHaveLength(240);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('keeps sessions with the same id isolated by scope', () => {
|
|
78
|
+
const storage = new MemoryStorage();
|
|
79
|
+
const initial = createDefaultPlaygroundState('/workspace/zhin');
|
|
80
|
+
const sessions = [
|
|
81
|
+
{ ...initial.sessions[0]!, id: 'shared', type: 'private' as const },
|
|
82
|
+
{ ...initial.sessions[1]!, id: 'shared', type: 'group' as const },
|
|
83
|
+
];
|
|
84
|
+
expect(savePlaygroundState({
|
|
85
|
+
activeSessionId: 'shared',
|
|
86
|
+
activeSessionType: 'group',
|
|
87
|
+
sessions,
|
|
88
|
+
messages: sessions.map((session, index) => ({
|
|
89
|
+
id: `m${index}`,
|
|
90
|
+
type: 'sent' as const,
|
|
91
|
+
channelType: session.type,
|
|
92
|
+
channelId: session.id,
|
|
93
|
+
channelName: session.name,
|
|
94
|
+
senderId: 'owner',
|
|
95
|
+
senderName: 'Owner',
|
|
96
|
+
content: [{ type: 'text', data: { text: session.type } }],
|
|
97
|
+
timestamp: index,
|
|
98
|
+
})),
|
|
99
|
+
}, storage)).toBe(true);
|
|
100
|
+
|
|
101
|
+
const restored = loadPlaygroundState(storage);
|
|
102
|
+
expect(restored.activeSessionType).toBe('group');
|
|
103
|
+
expect(restored.messages.map((message) => message.channelType)).toEqual(['private', 'group']);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { MessageSegment } from '@zhin.js/client';
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_SANDBOX_AGENT_RUN_CONFIG,
|
|
4
|
+
normalizeSandboxAgentRunConfig,
|
|
5
|
+
type SandboxAgentRunConfig,
|
|
6
|
+
type SandboxSafetyMode,
|
|
7
|
+
} from '../src/run-config.js';
|
|
8
|
+
|
|
9
|
+
export const PLAYGROUND_STORAGE_KEY = 'zhin.sandbox.agent-playground.v1';
|
|
10
|
+
|
|
11
|
+
export type PlaygroundScope = 'private' | 'group' | 'channel';
|
|
12
|
+
|
|
13
|
+
export interface PlaygroundSession {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly type: PlaygroundScope;
|
|
17
|
+
readonly unread: number;
|
|
18
|
+
readonly runConfig: SandboxAgentRunConfig;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PlaygroundMessage {
|
|
22
|
+
readonly id: string;
|
|
23
|
+
readonly type: 'sent' | 'received';
|
|
24
|
+
readonly channelType: PlaygroundScope;
|
|
25
|
+
readonly channelId: string;
|
|
26
|
+
readonly channelName: string;
|
|
27
|
+
readonly senderId: string;
|
|
28
|
+
readonly senderName: string;
|
|
29
|
+
readonly content: MessageSegment[];
|
|
30
|
+
readonly timestamp: number;
|
|
31
|
+
readonly interactionResolved?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PlaygroundState {
|
|
35
|
+
readonly version: 1;
|
|
36
|
+
readonly activeSessionId: string;
|
|
37
|
+
readonly activeSessionType?: PlaygroundScope;
|
|
38
|
+
readonly sessions: readonly PlaygroundSession[];
|
|
39
|
+
readonly messages: readonly PlaygroundMessage[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface PlaygroundStorage {
|
|
43
|
+
getItem(key: string): string | null;
|
|
44
|
+
setItem(key: string, value: string): void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createDefaultPlaygroundState(workingDirectory = ''): PlaygroundState {
|
|
48
|
+
const runConfig = (safetyMode: SandboxSafetyMode): SandboxAgentRunConfig => ({
|
|
49
|
+
...DEFAULT_SANDBOX_AGENT_RUN_CONFIG,
|
|
50
|
+
workingDirectory,
|
|
51
|
+
safetyMode,
|
|
52
|
+
});
|
|
53
|
+
const sessions: PlaygroundSession[] = [
|
|
54
|
+
{ id: 'sandbox-user', name: '快速试验', type: 'private', unread: 0, runConfig: runConfig('workspace-write') },
|
|
55
|
+
{ id: 'sandbox-group', name: '群组作用域', type: 'group', unread: 0, runConfig: runConfig('workspace-write') },
|
|
56
|
+
{ id: 'sandbox-channel', name: '频道作用域', type: 'channel', unread: 0, runConfig: runConfig('read-only') },
|
|
57
|
+
];
|
|
58
|
+
return Object.freeze({
|
|
59
|
+
version: 1,
|
|
60
|
+
activeSessionId: sessions[0]!.id,
|
|
61
|
+
activeSessionType: sessions[0]!.type,
|
|
62
|
+
sessions,
|
|
63
|
+
messages: [],
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function loadPlaygroundState(
|
|
68
|
+
storage: PlaygroundStorage | undefined = browserStorage(),
|
|
69
|
+
): PlaygroundState {
|
|
70
|
+
const fallback = createDefaultPlaygroundState();
|
|
71
|
+
if (!storage) return fallback;
|
|
72
|
+
try {
|
|
73
|
+
const raw = storage.getItem(PLAYGROUND_STORAGE_KEY);
|
|
74
|
+
if (!raw) return fallback;
|
|
75
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
76
|
+
if (parsed.version !== 1) return fallback;
|
|
77
|
+
const sessions = Array.isArray(parsed.sessions)
|
|
78
|
+
? parsed.sessions.map(parseSession).filter((item): item is PlaygroundSession => Boolean(item))
|
|
79
|
+
: [];
|
|
80
|
+
if (sessions.length === 0) return fallback;
|
|
81
|
+
const sessionKeys = new Set(sessions.map(sessionIdentity));
|
|
82
|
+
const messages = Array.isArray(parsed.messages)
|
|
83
|
+
? parsed.messages.map(parseMessage).filter((item): item is PlaygroundMessage => (
|
|
84
|
+
Boolean(item) && sessionKeys.has(sessionIdentity({ id: item.channelId, type: item.channelType }))
|
|
85
|
+
))
|
|
86
|
+
: [];
|
|
87
|
+
const requestedActive = typeof parsed.activeSessionId === 'string'
|
|
88
|
+
? sessions.find((session) => session.id === parsed.activeSessionId && (
|
|
89
|
+
parsed.activeSessionType === undefined || session.type === parsed.activeSessionType
|
|
90
|
+
))
|
|
91
|
+
: undefined;
|
|
92
|
+
const activeSession = requestedActive ?? sessions[0]!;
|
|
93
|
+
return Object.freeze({
|
|
94
|
+
version: 1,
|
|
95
|
+
activeSessionId: activeSession.id,
|
|
96
|
+
activeSessionType: activeSession.type,
|
|
97
|
+
sessions,
|
|
98
|
+
messages,
|
|
99
|
+
});
|
|
100
|
+
} catch {
|
|
101
|
+
return fallback;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function savePlaygroundState(
|
|
106
|
+
state: Omit<PlaygroundState, 'version'>,
|
|
107
|
+
storage: PlaygroundStorage | undefined = browserStorage(),
|
|
108
|
+
): boolean {
|
|
109
|
+
if (!storage) return false;
|
|
110
|
+
try {
|
|
111
|
+
const sessions = state.sessions;
|
|
112
|
+
const sessionKeys = new Set(sessions.map(sessionIdentity));
|
|
113
|
+
const activeSession = sessions.find((session) => session.id === state.activeSessionId && (
|
|
114
|
+
state.activeSessionType === undefined || session.type === state.activeSessionType
|
|
115
|
+
)) ?? sessions[0];
|
|
116
|
+
storage.setItem(PLAYGROUND_STORAGE_KEY, JSON.stringify({
|
|
117
|
+
version: 1,
|
|
118
|
+
activeSessionId: activeSession?.id ?? '',
|
|
119
|
+
activeSessionType: activeSession?.type,
|
|
120
|
+
sessions,
|
|
121
|
+
messages: state.messages.filter((message) => sessionKeys.has(sessionIdentity({
|
|
122
|
+
id: message.channelId,
|
|
123
|
+
type: message.channelType,
|
|
124
|
+
}))),
|
|
125
|
+
} satisfies PlaygroundState));
|
|
126
|
+
return true;
|
|
127
|
+
} catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseSession(value: unknown): PlaygroundSession | undefined {
|
|
133
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
|
134
|
+
const item = value as Record<string, unknown>;
|
|
135
|
+
if (typeof item.id !== 'string' || !item.id.trim() || typeof item.name !== 'string') return undefined;
|
|
136
|
+
if (item.type !== 'private' && item.type !== 'group' && item.type !== 'channel') return undefined;
|
|
137
|
+
return {
|
|
138
|
+
id: item.id.slice(0, 256),
|
|
139
|
+
name: item.name.trim().slice(0, 120) || '未命名试验',
|
|
140
|
+
type: item.type,
|
|
141
|
+
unread: Number.isSafeInteger(item.unread) && Number(item.unread) > 0 ? Number(item.unread) : 0,
|
|
142
|
+
runConfig: normalizeSandboxAgentRunConfig(item.runConfig) ?? DEFAULT_SANDBOX_AGENT_RUN_CONFIG,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function parseMessage(value: unknown): PlaygroundMessage | undefined {
|
|
147
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
|
148
|
+
const item = value as Record<string, unknown>;
|
|
149
|
+
if (typeof item.id !== 'string' || typeof item.channelId !== 'string' || !Array.isArray(item.content)) return undefined;
|
|
150
|
+
if (item.type !== 'sent' && item.type !== 'received') return undefined;
|
|
151
|
+
if (item.channelType !== 'private' && item.channelType !== 'group' && item.channelType !== 'channel') return undefined;
|
|
152
|
+
return {
|
|
153
|
+
id: item.id.slice(0, 256),
|
|
154
|
+
type: item.type,
|
|
155
|
+
channelType: item.channelType,
|
|
156
|
+
channelId: item.channelId.slice(0, 256),
|
|
157
|
+
channelName: typeof item.channelName === 'string' ? item.channelName.slice(0, 120) : '',
|
|
158
|
+
senderId: typeof item.senderId === 'string' ? item.senderId.slice(0, 256) : '',
|
|
159
|
+
senderName: typeof item.senderName === 'string' ? item.senderName.slice(0, 120) : '',
|
|
160
|
+
content: item.content as MessageSegment[],
|
|
161
|
+
timestamp: Number.isFinite(item.timestamp) ? Number(item.timestamp) : Date.now(),
|
|
162
|
+
...(item.interactionResolved === true ? { interactionResolved: true } : {}),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function browserStorage(): PlaygroundStorage | undefined {
|
|
167
|
+
return typeof window === 'undefined' ? undefined : window.localStorage;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function sessionIdentity(value: Pick<PlaygroundSession, 'id' | 'type'>): string {
|
|
171
|
+
return `${value.type}\0${value.id}`;
|
|
172
|
+
}
|