@kingsoftcloud/agentkit-personal-assistant 0.0.0-stage → 0.1.7

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.
Files changed (70) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +7 -0
  3. package/README.md +38 -2
  4. package/cordis.patch.yml +6 -0
  5. package/dist/client.js +6 -0
  6. package/package.json +83 -4
  7. package/personal-assistant.plugin.json +31 -0
  8. package/personal-assistant.plugin.schema.json +43 -0
  9. package/runtime/assistant-runtime.mjs +317 -0
  10. package/runtime/check-manifest.mjs +21 -0
  11. package/runtime/cloud-browser-cdp.mjs +313 -0
  12. package/runtime/cloud-browser.mjs +972 -0
  13. package/runtime/cloud-execution.mjs +320 -0
  14. package/runtime/cloud-session-channel.mjs +45 -0
  15. package/runtime/computer-agentd.mjs +488 -0
  16. package/runtime/desktop-clipboard-limit.mjs +2 -0
  17. package/runtime/desktop-launcher.mjs +106 -0
  18. package/runtime/desktop-permit.mjs +42 -0
  19. package/runtime/desktop-session.mjs +840 -0
  20. package/runtime/desktop-ws.mjs +371 -0
  21. package/runtime/prepare-dsh-profile.mjs +17 -0
  22. package/runtime/welcome-server.mjs +144 -0
  23. package/scripts/assistant-runtime-launcher.sh +3 -0
  24. package/scripts/build-artifacts.mjs +81 -0
  25. package/scripts/build-client.mjs +82 -0
  26. package/scripts/computer-agentd-launcher.sh +3 -0
  27. package/scripts/deterministic_tar.py +21 -0
  28. package/scripts/install-local.mjs +262 -0
  29. package/scripts/scan-secrets.mjs +41 -0
  30. package/scripts/sync-public.mjs +52 -0
  31. package/scripts/verification/gateway-desktop-client.mjs +142 -0
  32. package/scripts/verification/verify-desktop-applications-linux.mjs +46 -0
  33. package/scripts/verification/verify-desktop-input-linux.mjs +127 -0
  34. package/scripts/verification/verify-desktop-pod-gateway.mjs +204 -0
  35. package/scripts/verify-cloud-turn.mjs +18 -0
  36. package/scripts/verify-computer-broker.mjs +10 -0
  37. package/src/automation-tools.mjs +18 -0
  38. package/src/bot-private-tools.mjs +23 -0
  39. package/src/bot-registry.mjs +157 -0
  40. package/src/bot-tool-policy.mjs +48 -0
  41. package/src/bot-tool-scope.mjs +40 -0
  42. package/src/client/bot-directory.mjs +23 -0
  43. package/src/client/components/desktop-clipboard.mjs +194 -0
  44. package/src/client/conversation.mjs +558 -0
  45. package/src/client/desktop-control.mjs +70 -0
  46. package/src/client/desktop-view.mjs +68 -0
  47. package/src/client/entry.mjs +8 -0
  48. package/src/client/index.mjs +733 -0
  49. package/src/client/markdown.mjs +130 -0
  50. package/src/client/native-workbench.mjs +62 -0
  51. package/src/cloud-computer-tools.mjs +245 -0
  52. package/src/cloud-files.mjs +57 -0
  53. package/src/cloud-model.mjs +10 -0
  54. package/src/cloud-session-transport.mjs +76 -0
  55. package/src/computer-broker-protocol.mjs +116 -0
  56. package/src/computer-broker.mjs +220 -0
  57. package/src/desktop-tools.mjs +64 -0
  58. package/src/dsh-session-bridge.mjs +207 -0
  59. package/src/dsh-web-bridge.mjs +127 -0
  60. package/src/host-gateway.mjs +281 -0
  61. package/src/host-session-channel.mjs +410 -0
  62. package/src/implementation-capabilities.mjs +10 -0
  63. package/src/index.mjs +148 -0
  64. package/src/persistent-state.mjs +201 -0
  65. package/src/platform-manifest.mjs +24 -0
  66. package/src/plugin-manifest.mjs +82 -0
  67. package/src/schedule-manager.mjs +88 -0
  68. package/src/system-fetch.mjs +72 -0
  69. package/src/workspace-tools.mjs +22 -0
  70. package/src/workspace.mjs +22 -0
@@ -0,0 +1,43 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "agentkit-personal-assistant.plugin/v1",
4
+ "title": "AgentKit Personal Assistant plugin metadata",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["format", "pluginId", "version", "compatibility", "metadata"],
8
+ "properties": {
9
+ "format": { "const": "agentkit-personal-assistant.plugin/v1" },
10
+ "pluginId": { "type": "string", "minLength": 1 },
11
+ "version": { "type": "string", "minLength": 1 },
12
+ "compatibility": {
13
+ "type": "object",
14
+ "additionalProperties": false,
15
+ "required": ["requiredDsh", "requiredHostApi", "requiredHostContract"],
16
+ "properties": {
17
+ "requiredDsh": { "type": "string", "minLength": 1 },
18
+ "requiredHostApi": { "type": "string", "minLength": 1 },
19
+ "requiredHostContract": { "type": "integer", "minimum": 1 }
20
+ }
21
+ },
22
+ "metadata": {
23
+ "type": "object",
24
+ "additionalProperties": false,
25
+ "required": ["defaultEnabled", "autoConnect", "capabilities", "contributes", "entrypoints"],
26
+ "properties": {
27
+ "defaultEnabled": { "type": "boolean" },
28
+ "autoConnect": { "type": "boolean" },
29
+ "capabilities": { "type": "array", "items": { "type": "string", "minLength": 1 }, "uniqueItems": true },
30
+ "contributes": { "type": "array", "items": { "type": "string", "minLength": 1 }, "uniqueItems": true },
31
+ "entrypoints": {
32
+ "type": "object",
33
+ "additionalProperties": false,
34
+ "required": ["host", "client"],
35
+ "properties": {
36
+ "host": { "const": "." },
37
+ "client": { "const": "./client" }
38
+ }
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,317 @@
1
+ import { createServer } from 'node:http';
2
+ import { readFile, mkdir } from 'node:fs/promises';
3
+ import { createComputerBrokerClient, createHttpComputerBrokerTransport } from '../src/computer-broker.mjs';
4
+ import { createComputerToolset } from '../src/cloud-computer-tools.mjs';
5
+ import { BotRegistry } from '../src/bot-registry.mjs';
6
+ import { createPersonalAssistantStateStore } from '../src/persistent-state.mjs';
7
+ import { createCloudSessionChannel } from './cloud-session-channel.mjs';
8
+ import { attachDesktopWebSocket } from './desktop-ws.mjs';
9
+ import { pathToFileURL } from 'node:url';
10
+ import { createHash, randomUUID } from 'node:crypto';
11
+ import { createScheduleManager } from '../src/schedule-manager.mjs';
12
+ import { createCloudFileStore } from '../src/cloud-files.mjs';
13
+
14
+ export async function startAssistantRuntime({ runtime, env = process.env, argv = [] } = {}) {
15
+ const values = new Map();
16
+ for (let index = 0; index < argv.length; index += 1) {
17
+ const key = argv[index];
18
+ if (!key.startsWith('--')) continue;
19
+ values.set(key.slice(2), argv[index + 1]?.startsWith('--') ? '' : (argv[++index] ?? ''));
20
+ }
21
+ const required = name => {
22
+ const value = values.get(name) || env[name.toUpperCase().replaceAll('-', '_')];
23
+ if (!value) throw new Error(`missing ${name}`);
24
+ return value;
25
+ };
26
+ const assistantId = required('assistant-id');
27
+ const computerId = required('computer-id');
28
+ const userId = required('user-id');
29
+ const brokerEndpoint = env.COMPUTER_BROKER_URL || required('computer-broker');
30
+ const host = env.AGENTKIT_RUNTIME_HOST || '0.0.0.0';
31
+ const port = Number(env.AGENTKIT_RUNTIME_PORT || 8080);
32
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('invalid runtime port');
33
+ const capabilities = String(values.get('capabilities') || env.AGENTKIT_PERSONAL_ASSISTANT_CAPABILITIES || '').split(',').map(item => item.trim()).filter(Boolean);
34
+ const workspace = values.get('workspace') || env.WORKSPACE_PATH || env.AGENTKIT_WORKSPACE_DIR || '/workspace';
35
+ const artifacts = values.get('artifacts') || env.AGENTKIT_ARTIFACTS_DIR || `${workspace}/files`;
36
+ const state = values.get('state') || env.ASSISTANT_STATE_PATH || env.AGENTKIT_STATE_DIR || `${workspace}/.agentkit/state`;
37
+ const runtimeToken = env.AGENTKIT_RUNTIME_TOKEN || '';
38
+ const registryPath = values.get('bot-registry') || env.BOT_REGISTRY_PATH;
39
+ await mkdir(workspace, { recursive: true, mode: 0o700 });
40
+ await mkdir(artifacts, { recursive: true, mode: 0o750 });
41
+ await mkdir(state, { recursive: true, mode: 0o700 });
42
+ const stateStore = await createPersonalAssistantStateStore(state).init();
43
+ const cloudFiles = createCloudFileStore(`${workspace}/workspace`);
44
+ let bots = [];
45
+ if (registryPath) {
46
+ const parsed = JSON.parse(await readFile(registryPath, 'utf8'));
47
+ bots = Array.isArray(parsed) ? parsed : (Array.isArray(parsed.bots) ? parsed.bots : []);
48
+ }
49
+ const registry = runtime?.registry ?? new BotRegistry({ profileId: assistantId, bots });
50
+ const turnBridge = runtime?.turnBridge;
51
+ turnBridge?.setMemoryProvider?.(botId => stateStore.memory(botId), (botId, value) => stateStore.putMemory({ ...value, botId }));
52
+ turnBridge?.setSessionOwnerResolver?.(async sessionId => (await stateStore.read('sessions')).find(row => row.sessionId === sessionId)?.botId);
53
+
54
+ const broker = createComputerBrokerClient({ transport: createHttpComputerBrokerTransport({ baseUrl: brokerEndpoint }) });
55
+
56
+ const send = (response, status, body) => {
57
+ response.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
58
+ response.end(JSON.stringify(body));
59
+ };
60
+ const readBody = request => new Promise((resolve, reject) => {
61
+ const chunks = [];
62
+ request.on('data', chunk => chunks.push(chunk));
63
+ request.on('end', () => {
64
+ try { resolve(JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}')); } catch (error) { reject(Object.assign(error, { code: 'INVALID_JSON' })); }
65
+ });
66
+ request.on('error', reject);
67
+ });
68
+ const requireRuntimeToken = request => {
69
+ const authorization = String(request.headers.authorization || '');
70
+ if (!runtimeToken || authorization !== `Bearer ${runtimeToken}`) throw Object.assign(new Error('runtime token required'), { code: 'RUNTIME_UNAUTHORIZED', status: 401 });
71
+ };
72
+ const brokerDesktopRequest = async (path, body) => {
73
+ const result = await fetch(`${brokerEndpoint}${path}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ ...body, userId, assistantId, computerId }) });
74
+ const payload = await result.json().catch(() => ({ error: 'COMPUTER_BROKER_INVALID_RESPONSE' }));
75
+ if (!result.ok) throw Object.assign(new Error(payload.error || 'computer broker request failed'), { code: payload.error || 'COMPUTER_BROKER_REQUEST_FAILED', status: result.status });
76
+ return payload;
77
+ };
78
+ const brokerDesktopFrameBinary = async body => {
79
+ const result = await fetch(`${brokerEndpoint}/broker/desktop/frame-binary`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ ...body, userId, assistantId, computerId }) });
80
+ if (!result.ok) { const payload = await result.json().catch(() => ({})); throw Object.assign(new Error(payload.error || 'computer frame failed'), { code: payload.error || 'COMPUTER_FRAME_FAILED', status: result.status }); }
81
+ return { unchanged: result.headers.get('x-desktop-unchanged') === '1', seq: Number(result.headers.get('x-desktop-seq') || 0), width: Number(result.headers.get('x-desktop-width') || 1280), height: Number(result.headers.get('x-desktop-height') || 800), bytes: Number(result.headers.get('x-desktop-bytes') || 0), nextFrameDelayMs: Number(result.headers.get('x-desktop-next-frame-delay-ms') || 80), data: Buffer.from(await result.arrayBuffer()) };
82
+ };
83
+ const botIdOf = value => {
84
+ const botId = String(value ?? '').trim();
85
+ if (!botId) throw Object.assign(new Error('botId is required'), { code: 'BOT_REQUIRED' });
86
+ registry.require(botId);
87
+ return botId;
88
+ };
89
+ const taskIdOf = value => {
90
+ const taskId = String(value ?? 'runtime-action').trim();
91
+ if (!taskId || taskId.length > 128) throw Object.assign(new Error('taskId is invalid'), { code: 'TASK_INVALID' });
92
+ return taskId;
93
+ };
94
+ const actionToolName = value => {
95
+ const tool = String(value ?? '').trim();
96
+ const allowed = new Set([
97
+ 'cloud_browser_open', 'cloud_browser_snapshot', 'cloud_browser_click',
98
+ 'cloud_browser_type', 'cloud_browser_press', 'cloud_browser_screenshot',
99
+ 'cloud_browser_download', 'cloud_browser_close', 'cloud_bash', 'cloud_run_code',
100
+ ]);
101
+ if (!allowed.has(tool)) throw Object.assign(new Error('unsupported personal assistant tool'), { code: 'TOOL_UNSUPPORTED' });
102
+ return tool;
103
+ };
104
+ const computerStatus = async body => broker.status({ userId, assistantId, botId: botIdOf(body.botId), computerId });
105
+ const computerAction = async body => {
106
+ const botId = botIdOf(body.botId);
107
+ const tool = actionToolName(body.tool || body.name);
108
+ const taskId = taskIdOf(body.taskId);
109
+ const toolset = createComputerToolset({ broker, userId, assistantId, botId, computerId, taskId });
110
+ try { return await toolset.executeRaw(tool, body.args || {}, { taskId }); }
111
+ finally { await toolset.dispose(); }
112
+ };
113
+ // A deterministic, side-effect-free broker smoke: status, lease acquisition,
114
+ // and the close action all cross the localhost broker. `cloud_browser_close`
115
+ // does not launch a browser and therefore works even on a fresh Computer Pod.
116
+ const computerSmoke = async body => {
117
+ const botId = botIdOf(body.botId);
118
+ const taskId = taskIdOf(body.taskId || 'broker-smoke');
119
+ const status = await broker.status({ userId, assistantId, botId, computerId });
120
+ const toolset = createComputerToolset({ broker, userId, assistantId, botId, computerId, taskId });
121
+ try {
122
+ const action = await toolset.executeRaw('cloud_browser_close', {}, { taskId });
123
+ return { status, action, smoke: 'passed' };
124
+ } finally {
125
+ await toolset.dispose();
126
+ }
127
+ };
128
+
129
+ const urlOf = request => new URL(request.url || '/', `http://${host}:${port}`);
130
+ const stateBotId = value => botIdOf(value);
131
+ const sessionCreate = async body => {
132
+ const botId = stateBotId(body.botId);
133
+ return stateStore.createSession({ botId, title: body.title, metadata: body.metadata });
134
+ };
135
+ const memoryWrite = async body => {
136
+ const botId = stateBotId(body.botId);
137
+ return stateStore.putMemory({ botId, key: body.key, content: body.content, metadata: body.metadata });
138
+ };
139
+ const scheduleWrite = async body => {
140
+ const botId = stateBotId(body.botId);
141
+ if (!turnBridge?.available) throw Object.assign(new Error('cloud DSH Session Controller unavailable'), { code: 'DSH_SESSION_CONTROLLER_UNAVAILABLE', status: 503 });
142
+ const input = String(body.input ?? body.metadata?.input ?? '').trim();
143
+ if (!input || Buffer.byteLength(input) > 64 * 1024) throw Object.assign(new Error('schedule input is required'), { code: 'SCHEDULE_INPUT_REQUIRED' });
144
+ if (!Number.isSafeInteger(body.runAt) || body.runAt <= Date.now()) throw Object.assign(new Error('schedule must be in the future'), { code: 'SCHEDULE_INVALID' });
145
+ const sessionId = body.sessionId || (await stateStore.createSession({ botId, title: `定时任务:${body.scheduleId}` })).sessionId;
146
+ await requireSession(botId, sessionId);
147
+ const row = await stateStore.putSchedule({ botId, scheduleId: body.scheduleId, runAt: body.runAt, metadata: { ...body.metadata, input, sessionId, generation: randomUUID() } });
148
+ await patchSchedule(row, { status: 'pending' });
149
+ armSchedule(row);
150
+ return { ...row, status: 'pending' };
151
+ };
152
+ const sessionResource = pathname => {
153
+ const match = pathname.match(/^\/v1\/sessions\/([^/]+)\/(messages|turns)$/u);
154
+ return match ? { sessionId: decodeURIComponent(match[1]), resource: match[2] } : null;
155
+ };
156
+ const messageWrite = async (body, sessionId) => {
157
+ const botId = stateBotId(body.botId);
158
+ await requireSession(botId, sessionId);
159
+ return stateStore.appendMessage({ botId, sessionId, role: body.role, content: body.content, metadata: body.metadata });
160
+ };
161
+ const requireSession = async (botId, sessionId) => {
162
+ if (!(await stateStore.sessions(botId)).some(item => item.sessionId === sessionId)) throw Object.assign(new Error('session not found for Bot'), { code: 'SESSION_NOT_FOUND', status: 404 });
163
+ };
164
+ const updateTurn = (turnId, patch) => stateStore.mutate('turns', rows => {
165
+ const row = rows.find(item => item.turnId === turnId);
166
+ if (!row) return null;
167
+ Object.assign(row, patch, { updatedAt: new Date().toISOString() });
168
+ return row;
169
+ });
170
+ const pendingTurns = new Set();
171
+ const submissionLocks = new Map();
172
+ const scheduler = runtime?.scheduleManager ?? createScheduleManager();
173
+ const scheduleKey = row => `task-${createHash('sha256').update(JSON.stringify([row.botId, row.scheduleId])).digest('hex')}`;
174
+ const patchSchedule = (record, patch) => stateStore.mutate('schedules', rows => {
175
+ const row = rows.find(item => item.botId === record.botId && item.scheduleId === record.scheduleId);
176
+ if (!row || row.metadata?.generation !== record.metadata?.generation) return null;
177
+ Object.assign(row, patch);
178
+ return row;
179
+ });
180
+ function armSchedule(row) {
181
+ scheduler.scheduleOnce({ scheduleId: scheduleKey(row), runAt: Math.min(Math.max(Date.now() + 1, row.nextRunAt), Date.now() + 2_147_483_647), task: async () => {
182
+ try {
183
+ if (row.nextRunAt > Date.now()) { armSchedule(row); return; }
184
+ const claimed = await patchSchedule(row, { status: 'running', startedAt: new Date().toISOString() });
185
+ if (!claimed) return;
186
+ const receipt = await turnWrite({ botId: row.botId, input: row.metadata.input, metadata: { scheduleId: row.scheduleId } }, row.metadata.sessionId);
187
+ await Promise.allSettled([...pendingTurns]);
188
+ const turn = (await stateStore.turns(row.botId, row.metadata.sessionId)).find(item => item.turnId === receipt.turnId);
189
+ await patchSchedule(row, { status: turn?.status === 'completed' ? 'completed' : 'failed', turnId: receipt.turnId, finishedAt: new Date().toISOString(), error: turn?.error || null });
190
+ console.info(JSON.stringify({ event: 'personal-assistant.schedule.finished', botId: row.botId, scheduleId: row.scheduleId, turnId: receipt.turnId, status: turn?.status || 'failed' }));
191
+ } catch (error) { await patchSchedule(row, { status: 'failed', error: error.code || 'SCHEDULE_EXECUTION_FAILED' }); }
192
+ } });
193
+ }
194
+ const turnWrite = async (body, sessionId) => {
195
+ const key = JSON.stringify([body.botId, sessionId]);
196
+ const previous = submissionLocks.get(key) || Promise.resolve();
197
+ const current = previous.catch(() => {}).then(() => submitCloudTurn(body, sessionId));
198
+ submissionLocks.set(key, current);
199
+ try { return await current; } finally { if (submissionLocks.get(key) === current) submissionLocks.delete(key); }
200
+ };
201
+ const submitCloudTurn = async (body, sessionId) => {
202
+ const botId = stateBotId(body.botId);
203
+ await requireSession(botId, sessionId);
204
+ if (!turnBridge?.available) throw Object.assign(new Error('cloud DSH Session Controller unavailable'), { code: 'DSH_SESSION_CONTROLLER_UNAVAILABLE', status: 503 });
205
+ const clientTurnId = body.metadata?.clientTurnId;
206
+ if (clientTurnId) {
207
+ const existing = (await stateStore.turns(botId, sessionId)).find(row => row.metadata?.clientTurnId === clientTurnId);
208
+ if (existing) {
209
+ if (existing.input !== body.input) throw Object.assign(new Error('同一发送标识不能改变内容'), { code: 'CLIENT_TURN_CONFLICT', status: 409 });
210
+ return existing;
211
+ }
212
+ }
213
+ const turn = await stateStore.createTurn({ botId, sessionId, input: body.input, metadata: body.metadata });
214
+ const message = await stateStore.appendMessage({ botId, sessionId, role: 'user', content: body.input, metadata: { turnId: turn.turnId, ...(clientTurnId ? { clientTurnId } : {}) } });
215
+ try {
216
+ const content = [{ type: 'text', text: body.input }];
217
+ for (const attachment of body.attachments || []) {
218
+ const data = await cloudFiles.attachment({ botId, sessionId, path: attachment.path });
219
+ // 只有明确的图像附件进入模型图片通道;其他文件由工作区工具读取处理。
220
+ if (['image/png', 'image/jpeg', 'image/webp', 'image/gif'].includes(attachment.mediaType)) {
221
+ const store = runtime?.ctx?.get?.('attachments');
222
+ if (!store?.saveImage) throw Object.assign(new Error('云端图片附件服务不可用'), { status: 503 });
223
+ const ref = await store.saveImage({ data, mediaType: attachment.mediaType, name: attachment.name });
224
+ content.push({ type: 'image', attachment: ref });
225
+ }
226
+ }
227
+ const receipt = await turnBridge.submitTurn({ botId, sessionId, input: body.input, parts: content, requestId: turn.turnId });
228
+ await updateTurn(turn.turnId, { status: 'running', execution: receipt.execution, requestId: receipt.requestId });
229
+ const pending = turnBridge.waitForTurn({ botId, sessionId, requestId: receipt.requestId, onProgress: progress => updateTurn(turn.turnId, { progress }) }).then(async result => {
230
+ for (const content of result.messages) await stateStore.appendMessage({ botId, sessionId, role: 'assistant', content, metadata: { turnId: turn.turnId, ...(clientTurnId ? { clientTurnId } : {}), execution: receipt.execution } });
231
+ await updateTurn(turn.turnId, { status: result.status, result: result.messages.join('\n'), error: result.error || null });
232
+ console.info(JSON.stringify({ event: 'personal-assistant.turn.finished', botId, sessionId, turnId: turn.turnId, status: result.status }));
233
+ }).catch(error => updateTurn(turn.turnId, { status: 'failed', error: error.code || error.message }));
234
+ pendingTurns.add(pending);
235
+ void pending.finally(() => pendingTurns.delete(pending));
236
+ return { ...turn, status: 'running', messageId: message.messageId, ...receipt };
237
+ } catch (error) {
238
+ await updateTurn(turn.turnId, { status: 'failed', error: error.code || error.message });
239
+ throw error;
240
+ }
241
+ };
242
+
243
+ // 尚未开始的任务可恢复;中断时不自动重放未知结果,避免重复执行操作。
244
+ await stateStore.mutate('turns', rows => {
245
+ for (const row of rows) if (row.status === 'running' || row.status === 'queued') Object.assign(row, { status: 'interrupted', error: 'RUNTIME_PROCESS_INTERRUPTED', updatedAt: new Date().toISOString() });
246
+ });
247
+ for (const row of await stateStore.read('schedules')) {
248
+ if (row.status === 'running') await patchSchedule(row, { status: 'interrupted', error: 'SCHEDULE_PROCESS_INTERRUPTED' });
249
+ else if ((!row.status || row.status === 'pending') && row.metadata?.input && row.metadata?.sessionId) armSchedule(row);
250
+ }
251
+ runtime?.setAutomationService?.(async args => {
252
+ const botId = stateBotId(args.botId);
253
+ if (args.action === 'list') return { schedules: await stateStore.schedules(botId) };
254
+ if (args.action === 'create') return scheduleWrite({ botId, scheduleId: args.scheduleId || randomUUID(), runAt: args.runAt, input: args.input });
255
+ if (typeof args.scheduleId !== 'string') throw new TypeError('scheduleId is required');
256
+ const row = (await stateStore.schedules(botId)).find(row => row.scheduleId === args.scheduleId);
257
+ if (row?.status === 'running') throw Object.assign(new Error('任务已开始执行,不能伪报取消'), { status: 409 });
258
+ if (row) { scheduler.cancel(scheduleKey(row)); await patchSchedule(row, { status: 'cancelled' }); }
259
+ return { cancelled: Boolean(row) };
260
+ });
261
+
262
+ const server = createServer(async (request, response) => {
263
+ try {
264
+ const url = urlOf(request);
265
+ if (request.method === 'GET' && ['/health', '/ready', '/readyz'].includes(url.pathname)) return send(response, turnBridge?.available === true ? 200 : 503, { status: turnBridge?.available === true ? 'ready' : 'dsh-unavailable', assistantId, computerId, bots: registry.list().length, dshSessionController: turnBridge?.available === true, capabilities });
266
+ requireRuntimeToken(request);
267
+ if (request.method === 'GET' && url.pathname === '/v1/bots') return send(response, 200, { bots: registry.list() });
268
+ if (request.method === 'GET' && url.pathname === '/v1/sessions') return send(response, 200, { sessions: await stateStore.sessions(stateBotId(url.searchParams.get('botId'))) });
269
+ if (request.method === 'GET' && sessionResource(url.pathname)?.resource === 'messages') {
270
+ const resource = sessionResource(url.pathname);
271
+ const botId = stateBotId(url.searchParams.get('botId')); await requireSession(botId, resource.sessionId);
272
+ return send(response, 200, { messages: await stateStore.messages({ botId, sessionId: resource.sessionId }) });
273
+ }
274
+ if (request.method === 'GET' && sessionResource(url.pathname)?.resource === 'turns') {
275
+ const resource = sessionResource(url.pathname);
276
+ const botId = stateBotId(url.searchParams.get('botId')); await requireSession(botId, resource.sessionId);
277
+ return send(response, 200, { turns: await stateStore.turns(botId, resource.sessionId) });
278
+ }
279
+ if (request.method === 'GET' && url.pathname === '/v1/memory') return send(response, 200, { memories: await stateStore.memory(stateBotId(url.searchParams.get('botId')), url.searchParams.get('query') || '') });
280
+ if (request.method === 'GET' && url.pathname === '/v1/schedules') return send(response, 200, { schedules: await stateStore.schedules(stateBotId(url.searchParams.get('botId'))) });
281
+ if (request.method === 'DELETE' && url.pathname.startsWith('/v1/schedules/')) {
282
+ const botId = stateBotId(url.searchParams.get('botId')), scheduleId = decodeURIComponent(url.pathname.slice('/v1/schedules/'.length));
283
+ scheduler.cancel(scheduleKey({ botId, scheduleId }));
284
+ const removed = await stateStore.removeSchedule(botId, scheduleId);
285
+ return send(response, 200, { removed });
286
+ }
287
+ if (request.method !== 'POST') return send(response, 404, { error: 'not_found' });
288
+ const body = await readBody(request);
289
+ if (url.pathname === '/v1/computer/status') return send(response, 200, await computerStatus(body));
290
+ if (url.pathname === '/v1/computer/action') return send(response, 200, await computerAction(body));
291
+ if (url.pathname === '/v1/computer/smoke') return send(response, 200, await computerSmoke(body));
292
+ if (url.pathname === '/v1/computer/desktop/session') { requireRuntimeToken(request); return send(response, 200, await brokerDesktopRequest('/broker/desktop/session', { botId: botIdOf(body.botId), taskId: taskIdOf(body.taskId), leaseId: body.leaseId || undefined })); }
293
+ if (url.pathname === '/v1/computer/desktop/frame') { requireRuntimeToken(request); return send(response, 200, await brokerDesktopRequest('/broker/desktop/frame', { sessionId: body.sessionId, afterSeq: body.afterSeq })); }
294
+ if (url.pathname === '/v1/computer/desktop/input') { requireRuntimeToken(request); return send(response, 202, await brokerDesktopRequest('/broker/desktop/input', { sessionId: body.sessionId, botId: botIdOf(body.botId), taskId: taskIdOf(body.taskId), leaseId: body.leaseId, event: body.event })); }
295
+ if (url.pathname === '/v1/computer/desktop/close') { requireRuntimeToken(request); return send(response, 200, await brokerDesktopRequest('/broker/desktop/close', { sessionId: body.sessionId })); }
296
+ if (url.pathname === '/v1/sessions') return send(response, 201, await sessionCreate(body));
297
+ if (sessionResource(url.pathname)?.resource === 'messages') return send(response, 201, await messageWrite(body, sessionResource(url.pathname).sessionId));
298
+ if (sessionResource(url.pathname)?.resource === 'turns') return send(response, 202, await turnWrite(body, sessionResource(url.pathname).sessionId));
299
+ if (url.pathname === '/v1/memory') return send(response, 201, await memoryWrite(body));
300
+ if (url.pathname === '/v1/schedules') return send(response, 201, await scheduleWrite(body));
301
+ return send(response, 404, { error: 'not_found' });
302
+ } catch (error) {
303
+ const status = Number.isInteger(error?.status) && error.status >= 400 && error.status < 600 ? error.status : 400;
304
+ return send(response, status, { error: error?.code || error?.message || 'PERSONAL_ASSISTANT_RUNTIME_FAILED' });
305
+ }
306
+ });
307
+ attachDesktopWebSocket({ server, runtimeToken, assistantId, brokerDesktopRequest, brokerDesktopFrameBinary, botIdOf, taskIdOf, listBots: () => registry.list(), sessionRequest: createCloudSessionChannel({ assistantId, computerId, stateStore, registry, sessionCreate, turnWrite, scheduleWrite, cloudFiles }) });
308
+ await new Promise((resolve, reject) => { server.once('error', reject); server.listen(port, host, resolve); });
309
+ return { server, stateStore, waitForIdle: () => Promise.allSettled([...pendingTurns]), close: () => { scheduler.dispose(); return new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve())); } };
310
+ }
311
+
312
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
313
+ // 没有真实 DSH Host 注入时 readiness 明确为 503,禁止伪造回合。
314
+ const handle = await startAssistantRuntime({ argv: process.argv.slice(2) });
315
+ const close = () => void handle.close().then(() => process.exit(0));
316
+ process.on('SIGTERM', close); process.on('SIGINT', close);
317
+ }
@@ -0,0 +1,21 @@
1
+ import { pathToFileURL } from 'node:url';
2
+ import { loadPersonalAssistantManifest } from '../src/platform-manifest.mjs';
3
+
4
+ export const PERSONAL_ASSISTANT_MANIFEST_PATH = '/app/code/agentengine.yaml';
5
+
6
+ /** 独立init校验只读声明及公开身份,不需要模型key、runtime token或可写目录。 */
7
+ export function checkPersonalAssistantManifest(env = process.env, { loadManifest = loadPersonalAssistantManifest } = {}) {
8
+ if (env.AGENTENGINE_MANIFEST_PATH !== PERSONAL_ASSISTANT_MANIFEST_PATH) throw Object.assign(new Error('MANAGED_MANIFEST_PATH_INVALID'), { code: 'MANAGED_MANIFEST_PATH_INVALID' });
9
+ const manifest = loadManifest(env);
10
+ return { verified: true, runtime: manifest.runtime.name, version: manifest.runtime.version, manifestSha256: env.AGENTENGINE_MANIFEST_SHA256 };
11
+ }
12
+
13
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
14
+ try {
15
+ if (process.argv.length !== 4 || process.argv[2] !== '--manifest' || process.argv[3] !== PERSONAL_ASSISTANT_MANIFEST_PATH) throw Object.assign(new Error('MANAGED_MANIFEST_ARGUMENTS_INVALID'), { code: 'MANAGED_MANIFEST_ARGUMENTS_INVALID' });
16
+ console.info(JSON.stringify(checkPersonalAssistantManifest()));
17
+ } catch (error) {
18
+ console.error(JSON.stringify({ error: error.code || 'MANAGED_MANIFEST_CHECK_FAILED' }));
19
+ process.exitCode = 1;
20
+ }
21
+ }