@walkhi/code-relax 0.1.0-beta.22 → 0.1.0-beta.24
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/dist/bin/cleanup-accounts.mjs +63 -0
- package/dist/bin/self-relay-server.mjs +20 -19
- package/dist/shared/app-server-events.cjs +107 -100
- package/dist/shared/service-endpoints.cjs +21 -0
- package/dist/shared/timeline-sync.cjs +94 -5
- package/dist/skills/code-relax/SKILL.md +21 -18
- package/dist/src/lan-socket-server.mjs +50 -10
- package/dist/src/self-relay/account-cleanup.mjs +30 -0
- package/dist/src/self-relay/admin-page.mjs +8 -14
- package/dist/src/self-relay/admin-server.mjs +17 -9
- package/dist/src/self-relay/client.mjs +45 -45
- package/dist/src/self-relay/connector.mjs +293 -289
- package/dist/src/self-relay/diagnostics.mjs +54 -54
- package/dist/src/self-relay/email-accounts.mjs +158 -141
- package/dist/src/self-relay/lifecycle.mjs +160 -160
- package/dist/src/self-relay/privacy-page.mjs +26 -0
- package/dist/src/self-relay/server.mjs +153 -67
- package/dist/src/self-relay/usage-meter.mjs +141 -136
- package/dist/src/server.mjs +102 -203
- package/dist/src/shared-app-server.mjs +132 -116
- package/dist/src/shared-thread-stream.mjs +56 -50
- package/dist/src/turn-model-history.mjs +47 -0
- package/package.json +1 -1
package/dist/src/server.mjs
CHANGED
|
@@ -18,13 +18,16 @@ import { runtimePaths } from './platform/windows/runtime-paths.mjs';
|
|
|
18
18
|
import { openControl } from './service-control.mjs';
|
|
19
19
|
import { writeRecord, readRecord } from './service-lifecycle.mjs';
|
|
20
20
|
import { ExecutionHealth } from './execution-health.mjs';
|
|
21
|
-
import { authorizeLanDevice } from './self-relay/lan-authorizations.mjs';
|
|
22
|
-
import { createRelayManager } from './self-relay/lifecycle.mjs';
|
|
21
|
+
import { authorizeLanDevice } from './self-relay/lan-authorizations.mjs';
|
|
22
|
+
import { createRelayManager } from './self-relay/lifecycle.mjs';
|
|
23
23
|
import { generateAndSetThreadTitle } from './thread-title-generation.mjs';
|
|
24
24
|
import { desktopHost } from './platform/windows/desktop-host.mjs';
|
|
25
25
|
import { SharedCatalog } from './shared-catalog.mjs';
|
|
26
26
|
import { requestRecovery, recoveryStatus } from './shared-recovery.mjs';
|
|
27
27
|
import { requestServiceRestart } from './service-restart.mjs';
|
|
28
|
+
import { TurnModelHistory } from './turn-model-history.mjs';
|
|
29
|
+
|
|
30
|
+
const turnModelHistory = new TurnModelHistory();
|
|
28
31
|
|
|
29
32
|
const MAX_ATTACHMENT_BYTES = 16 * 1024 * 1024;
|
|
30
33
|
const MAX_CACHED_ATTACHMENTS = 32;
|
|
@@ -85,8 +88,8 @@ const sessionSettingsCache = new Map();
|
|
|
85
88
|
const liveActivityCache = new Map();
|
|
86
89
|
let sharedServer = null;
|
|
87
90
|
const managedAppServer = new ManagedAppServer({ stateRoot, host: desktopHost });
|
|
88
|
-
const runtimeDiscovery = new RuntimeDiscovery({ stateRoot, host: desktopHost, url: managedAppServerUrl });
|
|
89
|
-
const relayManager = createRelayManager(stateRoot);
|
|
91
|
+
const runtimeDiscovery = new RuntimeDiscovery({ stateRoot, host: desktopHost, url: managedAppServerUrl });
|
|
92
|
+
const relayManager = createRelayManager(stateRoot);
|
|
90
93
|
let sharedCatalog = null, executionMode = 'ordinary';
|
|
91
94
|
function selectSharedExecution(enabled) {
|
|
92
95
|
const next = enabled ? managedAppServer.client : null;
|
|
@@ -195,9 +198,10 @@ async function handleRequest(request, response) {
|
|
|
195
198
|
if (request.method === 'GET' && url.pathname === '/api/health') {
|
|
196
199
|
// Status readers consume the workstation's cached sample. A phone query
|
|
197
200
|
// must not trigger PowerShell, process-identity, or app-server checks.
|
|
201
|
+
const executionReady = executionHealth.state.ready && sharedServer?.ready === true;
|
|
198
202
|
sendJson(response, 200, {
|
|
199
203
|
status: 'ok', resident: true, version: runtimeVersion,
|
|
200
|
-
execution: { ...executionHealth.state, ready:
|
|
204
|
+
execution: { ...executionHealth.state, ready: executionReady, canWrite: executionReady,
|
|
201
205
|
mode: executionMode },
|
|
202
206
|
managedAppServer: managedAppServer.snapshot(),
|
|
203
207
|
...runtimeDiscovery.state,
|
|
@@ -283,7 +287,7 @@ async function handleRequest(request, response) {
|
|
|
283
287
|
return;
|
|
284
288
|
}
|
|
285
289
|
|
|
286
|
-
if (request.method === 'GET' && url.pathname === '/api/usage-events') {
|
|
290
|
+
if (request.method === 'GET' && url.pathname === '/api/usage-events') {
|
|
287
291
|
const execution = sharedServer;
|
|
288
292
|
response.startEvents();
|
|
289
293
|
const emit = value => { if (!response.destroyed) response.event('usage', value); };
|
|
@@ -292,45 +296,46 @@ async function handleRequest(request, response) {
|
|
|
292
296
|
execution.on('usage', emit); execution.on('usage-error', failed); execution.on('disconnected', disconnected);
|
|
293
297
|
response.once('close', () => { execution.off('usage', emit); execution.off('usage-error', failed); execution.off('disconnected', disconnected); });
|
|
294
298
|
try { emit(await execution.usage()); } catch (error) { failed(error); }
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
298
302
|
if (request.method === 'GET' && url.pathname === '/api/turn-events') {
|
|
303
|
+
const execution = sharedServer;
|
|
299
304
|
response.startEvents();
|
|
300
|
-
const listener = ({ method, params }) => {
|
|
301
|
-
if (!['turn/started', 'turn/completed'].includes(method)) return;
|
|
302
|
-
const threadId = params?.threadId, turnId = params?.turn?.id;
|
|
303
|
-
if (typeof threadId === 'string' && typeof turnId === 'string' && !response.destroyed) {
|
|
304
|
-
response.event('turn', { method, threadId, turnId });
|
|
305
|
-
}
|
|
306
|
-
};
|
|
307
|
-
const disconnected = () => response.end();
|
|
308
|
-
|
|
309
|
-
|
|
305
|
+
const listener = ({ method, params }) => {
|
|
306
|
+
if (!['turn/started', 'turn/completed'].includes(method)) return;
|
|
307
|
+
const threadId = params?.threadId, turnId = params?.turn?.id;
|
|
308
|
+
if (typeof threadId === 'string' && typeof turnId === 'string' && !response.destroyed) {
|
|
309
|
+
response.event('turn', { method, threadId, turnId });
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
const disconnected = () => response.end();
|
|
313
|
+
execution.on('notification', listener);
|
|
314
|
+
execution.on('disconnected', disconnected);
|
|
310
315
|
response.once('close', () => {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
});
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const activeTurnMatch = /^\/api\/threads\/([^/]+)\/active-turn$/.exec(url.pathname);
|
|
318
|
-
if (request.method === 'GET' && activeTurnMatch) {
|
|
319
|
-
const threadId = decodeURIComponent(activeTurnMatch[1]);
|
|
320
|
-
await sharedServer.connect();
|
|
321
|
-
const { thread } = await sharedServer.request('thread/read', { threadId, includeTurns: false });
|
|
322
|
-
const status = typeof thread.status === 'string' ? thread.status : thread.status?.type;
|
|
323
|
-
let turnId = null;
|
|
324
|
-
if (status === 'active') {
|
|
325
|
-
const page = await sharedServer.request('thread/turns/list', {
|
|
326
|
-
threadId, limit: 1, sortDirection: 'desc', itemsView: 'summary',
|
|
327
|
-
});
|
|
328
|
-
const latest = page.data?.[0];
|
|
329
|
-
if (latest?.status === 'inProgress') turnId = latest.id;
|
|
330
|
-
}
|
|
331
|
-
sendJson(response, 200, { turnId });
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
316
|
+
execution.off('notification', listener);
|
|
317
|
+
execution.off('disconnected', disconnected);
|
|
318
|
+
});
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const activeTurnMatch = /^\/api\/threads\/([^/]+)\/active-turn$/.exec(url.pathname);
|
|
323
|
+
if (request.method === 'GET' && activeTurnMatch) {
|
|
324
|
+
const threadId = decodeURIComponent(activeTurnMatch[1]);
|
|
325
|
+
await sharedServer.connect();
|
|
326
|
+
const { thread } = await sharedServer.request('thread/read', { threadId, includeTurns: false });
|
|
327
|
+
const status = typeof thread.status === 'string' ? thread.status : thread.status?.type;
|
|
328
|
+
let turnId = null;
|
|
329
|
+
if (status === 'active') {
|
|
330
|
+
const page = await sharedServer.request('thread/turns/list', {
|
|
331
|
+
threadId, limit: 1, sortDirection: 'desc', itemsView: 'summary',
|
|
332
|
+
});
|
|
333
|
+
const latest = page.data?.[0];
|
|
334
|
+
if (latest?.status === 'inProgress') turnId = latest.id;
|
|
335
|
+
}
|
|
336
|
+
sendJson(response, 200, { turnId });
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
334
339
|
|
|
335
340
|
const createThreadMatch = /^\/api\/projects\/([^/]+)\/threads$/.exec(url.pathname);
|
|
336
341
|
if (request.method === 'POST' && createThreadMatch) {
|
|
@@ -371,25 +376,15 @@ async function handleRequest(request, response) {
|
|
|
371
376
|
}
|
|
372
377
|
|
|
373
378
|
const timelineMatch = /^\/api\/threads\/([^/]+)\/timeline$/.exec(url.pathname);
|
|
374
|
-
if (request.method === 'GET' && timelineMatch) {
|
|
375
|
-
const threadId = decodeURIComponent(timelineMatch[1]);
|
|
376
|
-
const
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const sameHistoryView = knownHistoryView === timeline.historyView;
|
|
384
|
-
const unchanged = sameHistoryView
|
|
385
|
-
&& knownIndex === timeline.turns.length - 1
|
|
386
|
-
&& timeline.thread.status === 'idle' && latest?.status !== 'inProgress'
|
|
387
|
-
&& knownTurnStatus && knownTurnStatus !== 'inProgress'
|
|
388
|
-
&& (!Number.isFinite(knownUpdatedAt) || knownUpdatedAt <= 0 || timeline.thread.updatedAt === knownUpdatedAt);
|
|
389
|
-
const partial = !unchanged && sameHistoryView && knownIndex >= 0
|
|
390
|
-
? { ...timeline, turns: timeline.turns.slice(knownIndex), partialFromTurnId: knownTurnId }
|
|
391
|
-
: timeline;
|
|
392
|
-
sendJson(response, 200, unchanged ? { ...timeline, turns: [], notModified: true } : partial);
|
|
379
|
+
if (request.method === 'GET' && timelineMatch) {
|
|
380
|
+
const threadId = decodeURIComponent(timelineMatch[1]);
|
|
381
|
+
const execution = sharedServer;
|
|
382
|
+
const timeline = normalizeSharedTimeline(await execution.timeline(threadId, sharedHistoryPageOptions()));
|
|
383
|
+
if (request.socketRequest) {
|
|
384
|
+
response.on('progress', () => execution.keepSnapshotAlive(threadId, timeline.sync));
|
|
385
|
+
response.once('close', () => execution.keepSnapshotAlive(threadId, timeline.sync));
|
|
386
|
+
}
|
|
387
|
+
sendJson(response, 200, timeline);
|
|
393
388
|
return;
|
|
394
389
|
}
|
|
395
390
|
|
|
@@ -407,17 +402,18 @@ async function handleRequest(request, response) {
|
|
|
407
402
|
const after = Number(url.searchParams.get('afterSeq'));
|
|
408
403
|
const requestedSync = url.searchParams.get('syncId') && url.searchParams.get('syncEpoch') && Number.isSafeInteger(after) && after >= 0
|
|
409
404
|
? { id: url.searchParams.get('syncId'), epoch: url.searchParams.get('syncEpoch'), after } : undefined;
|
|
410
|
-
const knownUpdatedAt = Number(url.searchParams.get('knownUpdatedAt'));
|
|
411
|
-
const known = url.searchParams.get('knownTurnId') ? {
|
|
412
|
-
updatedAt: Number.isFinite(knownUpdatedAt) ? knownUpdatedAt : 0,
|
|
413
|
-
turnId: url.searchParams.get('knownTurnId'),
|
|
414
|
-
turnStatus: url.searchParams.get('knownTurnStatus') || '',
|
|
415
|
-
} : undefined;
|
|
416
405
|
await streamSharedThread(response, threadId, sharedServer,
|
|
417
|
-
normalizeSharedTimeline, requestedSync
|
|
406
|
+
normalizeSharedTimeline, requestedSync);
|
|
418
407
|
return;
|
|
419
408
|
}
|
|
420
409
|
|
|
410
|
+
const modelChangesMatch = /^\/api\/threads\/([^/]+)\/model-changes$/.exec(url.pathname);
|
|
411
|
+
if (request.method === 'GET' && modelChangesMatch) {
|
|
412
|
+
const threadId = decodeURIComponent(modelChangesMatch[1]);
|
|
413
|
+
if (!/^[0-9a-f]{8}-[0-9a-f-]{20,55}$/i.test(threadId)) throw new RequestError(400, '对话 ID 无效。');
|
|
414
|
+
sendJson(response, 200, await turnModelHistory.read(findSessionPath(threadId), (url.searchParams.get('turnIds') || '').split(',').filter(Boolean)));
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
421
417
|
const contextMatch = /^\/api\/threads\/([^/]+)\/context$/.exec(url.pathname);
|
|
422
418
|
if (request.method === 'GET' && contextMatch) {
|
|
423
419
|
const threadId = decodeURIComponent(contextMatch[1]);
|
|
@@ -449,17 +445,6 @@ async function handleRequest(request, response) {
|
|
|
449
445
|
}
|
|
450
446
|
}
|
|
451
447
|
|
|
452
|
-
const sharedOperationGroup = /^\/api\/threads\/([^/]+)\/turns\/([^/]+)\/groups\/([^/]+)\/items$/.exec(url.pathname);
|
|
453
|
-
if (request.method === 'GET' && sharedOperationGroup) {
|
|
454
|
-
const threadId = decodeURIComponent(sharedOperationGroup[1]);
|
|
455
|
-
const turnId = decodeURIComponent(sharedOperationGroup[2]);
|
|
456
|
-
const groupId = decodeURIComponent(sharedOperationGroup[3]);
|
|
457
|
-
const page = await sharedServer.operationGroupItems(threadId, turnId, groupId, url.searchParams.get('offset'));
|
|
458
|
-
sendJson(response, 200, { turnId: page.turnId, groupId: page.groupId,
|
|
459
|
-
entries: page.items.map(normalizeItem).filter(Boolean), nextOffset: page.nextOffset, total: page.total });
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
448
|
const archiveMatch = /^\/api\/threads\/([^/]+)\/(archive|unarchive)$/.exec(url.pathname);
|
|
464
449
|
if (request.method === 'POST' && archiveMatch) {
|
|
465
450
|
const threadId = decodeURIComponent(archiveMatch[1]);
|
|
@@ -477,8 +462,9 @@ async function handleRequest(request, response) {
|
|
|
477
462
|
if (request.method === 'GET') { sendJson(response, 200, await appServerEvents.readQueue(rpc, threadId)); return; }
|
|
478
463
|
if (request.method === 'POST') {
|
|
479
464
|
await requireExecutionWritable(threadId);
|
|
480
|
-
const
|
|
481
|
-
|
|
465
|
+
const body = await readJsonBody(request);
|
|
466
|
+
const args = appServerTurnArguments(threadId, body);
|
|
467
|
+
sendJson(response, 202, await appServerEvents.addQueue(rpc, threadId, args.input, crypto.randomUUID(), body.containsVoiceInput)); return;
|
|
482
468
|
}
|
|
483
469
|
}
|
|
484
470
|
|
|
@@ -510,8 +496,8 @@ async function handleRequest(request, response) {
|
|
|
510
496
|
await requireExecutionWritable(threadId);
|
|
511
497
|
const body = await readJsonBody(request);
|
|
512
498
|
if (body.hostId && body.hostId !== 'local') throw new RequestError(410, '此对话的连接方式已停用。', { code: 'unsupported_control' });
|
|
513
|
-
const submitted = await sharedServer.send(appServerTurnArguments(threadId, body));
|
|
514
|
-
sendJson(response, 200, { success: true, data: submitted.result, usage: submitted.usage });
|
|
499
|
+
const submitted = await sharedServer.send(appServerTurnArguments(threadId, body));
|
|
500
|
+
sendJson(response, 200, { success: true, data: submitted.result, usage: submitted.usage });
|
|
515
501
|
return;
|
|
516
502
|
}
|
|
517
503
|
|
|
@@ -543,7 +529,7 @@ void managedAppServer.start();
|
|
|
543
529
|
executionHealth.start();
|
|
544
530
|
runtimeDiscovery.start();
|
|
545
531
|
server.on('error', error => {
|
|
546
|
-
console.error('桌面服务监听失败:', error.message);
|
|
532
|
+
console.error('桌面服务监听失败:', error.message);
|
|
547
533
|
process.exit(1);
|
|
548
534
|
});
|
|
549
535
|
server.listen(options.port, options.lan ? '0.0.0.0' : '127.0.0.1', async () => {
|
|
@@ -551,12 +537,12 @@ server.listen(options.port, options.lan ? '0.0.0.0' : '127.0.0.1', async () => {
|
|
|
551
537
|
if (process.env.CODEX_REMOTE_INSTANCE && process.env.CODEX_REMOTE_CONTROL_SECRET && process.env.CODEX_REMOTE_READY_PATH) {
|
|
552
538
|
control = await openControl({ instanceId: process.env.CODEX_REMOTE_INSTANCE,
|
|
553
539
|
secret: process.env.CODEX_REMOTE_CONTROL_SECRET, shutdown: shutDown, actions: {
|
|
554
|
-
'refresh-execution': refreshExecution,
|
|
555
|
-
'relay-start': () => relayManager.run('relay-start'),
|
|
556
|
-
'relay-status': () => relayManager.run('relay-status'),
|
|
557
|
-
'relay-stop': () => relayManager.run('relay-stop'),
|
|
558
|
-
'relay-pair': () => relayManager.run('relay-pair'),
|
|
559
|
-
'relay-revoke': () => relayManager.run('relay-revoke'),
|
|
540
|
+
'refresh-execution': refreshExecution,
|
|
541
|
+
'relay-start': () => relayManager.run('relay-start'),
|
|
542
|
+
'relay-status': () => relayManager.run('relay-status'),
|
|
543
|
+
'relay-stop': () => relayManager.run('relay-stop'),
|
|
544
|
+
'relay-pair': () => relayManager.run('relay-pair'),
|
|
545
|
+
'relay-revoke': () => relayManager.run('relay-revoke'),
|
|
560
546
|
'restart-managed-app-server': () => {
|
|
561
547
|
const state = managedAppServer.snapshot();
|
|
562
548
|
return state.launcherRestartRequired ? managedAppServer.restart(crypto.randomUUID()) : state;
|
|
@@ -566,10 +552,10 @@ server.listen(options.port, options.lan ? '0.0.0.0' : '127.0.0.1', async () => {
|
|
|
566
552
|
pid: process.pid, controlPort: control.port });
|
|
567
553
|
}
|
|
568
554
|
} catch (error) {
|
|
569
|
-
console.error('桌面服务管理通道启动失败:', error.message);
|
|
555
|
+
console.error('桌面服务管理通道启动失败:', error.message);
|
|
570
556
|
process.exit(1);
|
|
571
557
|
}
|
|
572
|
-
console.log(`Code Relax 桌面服务已启动:http://127.0.0.1:${options.port}/`);
|
|
558
|
+
console.log(`Code Relax 桌面服务已启动:http://127.0.0.1:${options.port}/`);
|
|
573
559
|
if (options.lan) {
|
|
574
560
|
console.log('LAN 模式使用鉴权 WebSocket,仅适合受信任局域网测试,不要暴露到公网。');
|
|
575
561
|
}
|
|
@@ -582,8 +568,8 @@ async function shutDown() {
|
|
|
582
568
|
lanSocket.close();
|
|
583
569
|
server.close();
|
|
584
570
|
executionHealth?.stop();
|
|
585
|
-
runtimeDiscovery.stop();
|
|
586
|
-
await relayManager.close();
|
|
571
|
+
runtimeDiscovery.stop();
|
|
572
|
+
await relayManager.close();
|
|
587
573
|
managedAppServer.close();
|
|
588
574
|
await control?.close();
|
|
589
575
|
process.exit(0);
|
|
@@ -749,12 +735,12 @@ function messagePayload(body) {
|
|
|
749
735
|
return payload;
|
|
750
736
|
}
|
|
751
737
|
|
|
752
|
-
function appServerTurnArguments(threadId, body) {
|
|
738
|
+
function appServerTurnArguments(threadId, body) {
|
|
753
739
|
const payload = messagePayload(body);
|
|
754
740
|
const input = [];
|
|
755
741
|
if (payload.prompt) input.push({ type: 'text', text: payload.prompt });
|
|
756
742
|
for (const image of payload.images) input.push(image.startsWith('data:') ? { type: 'image', url: image } : { type: 'localImage', path: image });
|
|
757
|
-
const value = { threadId, input, ...appServerEvents.mobileTurnSource(body.clientUserMessageId) };
|
|
743
|
+
const value = { threadId, input, ...appServerEvents.mobileTurnSource(body.clientUserMessageId, body.containsVoiceInput) };
|
|
758
744
|
if (typeof body.clientUserMessageId === 'string' && /^[a-f0-9]{32}$/.test(body.clientUserMessageId)) value.clientUserMessageId = body.clientUserMessageId;
|
|
759
745
|
if (payload.model) value.model = payload.model;
|
|
760
746
|
if (payload.thinking) value.effort = payload.thinking;
|
|
@@ -775,7 +761,7 @@ function normalizeTimeline(data) {
|
|
|
775
761
|
function normalizeSharedTimeline({ thread, nextCursor, sync }) {
|
|
776
762
|
const timeline = normalizeTimeline({ thread: { ...thread, title: thread.name || thread.title || '', hostId: 'local', transport: 'shared-app-server' } });
|
|
777
763
|
return { ...timeline,
|
|
778
|
-
historyView: '
|
|
764
|
+
historyView: 'item-pages-v1',
|
|
779
765
|
...(nextCursor !== undefined ? { nextCursor } : {}), ...(sync ? { sync } : {}) };
|
|
780
766
|
}
|
|
781
767
|
|
|
@@ -799,7 +785,7 @@ function normalizeThread(thread) {
|
|
|
799
785
|
function normalizeTurn(turn) {
|
|
800
786
|
const source = turn && typeof turn === 'object' ? turn : {};
|
|
801
787
|
const turnId = source.id || '';
|
|
802
|
-
const entries =
|
|
788
|
+
const entries = Array.isArray(source.items) ? source.items.map(normalizeItem).filter(Boolean) : [];
|
|
803
789
|
for (const entry of entries) {
|
|
804
790
|
const images = [...(entry.attachments || []), entry.image, ...Object.values(entry.imageAssets || {})].filter(Boolean);
|
|
805
791
|
for (const image of images) if (image.remotePath && !image.recoveryTurnId) image.recoveryTurnId = turnId;
|
|
@@ -811,6 +797,7 @@ function normalizeTurn(turn) {
|
|
|
811
797
|
completedAt: source.completedAt || null,
|
|
812
798
|
durationMs: source.durationMs ?? null,
|
|
813
799
|
error: source.error ?? null,
|
|
800
|
+
...(source.itemPage ? { itemPage: source.itemPage } : {}),
|
|
814
801
|
entries,
|
|
815
802
|
};
|
|
816
803
|
}
|
|
@@ -818,17 +805,6 @@ function normalizeTurn(turn) {
|
|
|
818
805
|
function normalizeItem(item) {
|
|
819
806
|
if (!item || typeof item !== 'object') return null;
|
|
820
807
|
|
|
821
|
-
if (item.type === 'lazyOperationGroup') {
|
|
822
|
-
return {
|
|
823
|
-
type: 'activity',
|
|
824
|
-
id: item.id || '',
|
|
825
|
-
kind: 'lazyOperationGroup',
|
|
826
|
-
turnId: item.turnId || '',
|
|
827
|
-
groupId: item.groupId || '',
|
|
828
|
-
operationTypes: Array.isArray(item.operationTypes) ? item.operationTypes : [],
|
|
829
|
-
};
|
|
830
|
-
}
|
|
831
|
-
|
|
832
808
|
if (item.type === 'contextCompaction') {
|
|
833
809
|
return {
|
|
834
810
|
type: 'activity',
|
|
@@ -965,28 +941,28 @@ function normalizeItem(item) {
|
|
|
965
941
|
};
|
|
966
942
|
}
|
|
967
943
|
|
|
968
|
-
if (item.type === 'imageView') {
|
|
969
|
-
const image = typeof item.path === 'string' && item.path
|
|
970
|
-
? normalizeAttachment({ type: 'localImage', path: item.path })
|
|
971
|
-
: null;
|
|
972
|
-
if (image?.available) {
|
|
973
|
-
return {
|
|
974
|
-
type: 'message',
|
|
975
|
-
id: item.id || '',
|
|
976
|
-
role: 'assistant',
|
|
977
|
-
phase: 'commentary',
|
|
978
|
-
text: '',
|
|
979
|
-
attachments: [image],
|
|
980
|
-
};
|
|
981
|
-
}
|
|
982
|
-
return {
|
|
944
|
+
if (item.type === 'imageView') {
|
|
945
|
+
const image = typeof item.path === 'string' && item.path
|
|
946
|
+
? normalizeAttachment({ type: 'localImage', path: item.path })
|
|
947
|
+
: null;
|
|
948
|
+
if (image?.available) {
|
|
949
|
+
return {
|
|
950
|
+
type: 'message',
|
|
951
|
+
id: item.id || '',
|
|
952
|
+
role: 'assistant',
|
|
953
|
+
phase: 'commentary',
|
|
954
|
+
text: '',
|
|
955
|
+
attachments: [image],
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
return {
|
|
983
959
|
type: 'activity',
|
|
984
960
|
id: item.id || '',
|
|
985
961
|
kind: 'imageView',
|
|
986
962
|
title: '查看图片',
|
|
987
963
|
titleOnly: true,
|
|
988
964
|
path: item.path || '',
|
|
989
|
-
image,
|
|
965
|
+
image,
|
|
990
966
|
};
|
|
991
967
|
}
|
|
992
968
|
|
|
@@ -1048,83 +1024,6 @@ function normalizeItem(item) {
|
|
|
1048
1024
|
};
|
|
1049
1025
|
}
|
|
1050
1026
|
|
|
1051
|
-
function collapseSubAgentEntries(entries) {
|
|
1052
|
-
const agents = new Map();
|
|
1053
|
-
let firstIndex = -1;
|
|
1054
|
-
const retained = [];
|
|
1055
|
-
const ensureAgent = threadId => {
|
|
1056
|
-
if (!threadId) return null;
|
|
1057
|
-
let agent = agents.get(threadId);
|
|
1058
|
-
if (!agent) {
|
|
1059
|
-
agent = { threadId, name: '', status: 'pendingInit', objective: '', model: '', reasoningEffort: '' };
|
|
1060
|
-
agents.set(threadId, agent);
|
|
1061
|
-
}
|
|
1062
|
-
return agent;
|
|
1063
|
-
};
|
|
1064
|
-
const setProtocolStatus = (agent, status) => {
|
|
1065
|
-
if (!agent || !status || status === 'unknown') return;
|
|
1066
|
-
const terminal = ['interrupted', 'completed', 'errored', 'shutdown', 'notFound'];
|
|
1067
|
-
if (terminal.includes(agent.status) && ['pendingInit', 'running'].includes(status)) return;
|
|
1068
|
-
if (status === 'shutdown' && agent.status === 'completed') return;
|
|
1069
|
-
agent.status = status;
|
|
1070
|
-
};
|
|
1071
|
-
for (const entry of entries) {
|
|
1072
|
-
if (entry.kind === 'subAgent') {
|
|
1073
|
-
if (firstIndex < 0) firstIndex = retained.length;
|
|
1074
|
-
const agent = ensureAgent(entry.agentThreadId);
|
|
1075
|
-
if (!agent) continue;
|
|
1076
|
-
agent.name = subAgentDisplayName(entry.agentPath) || agent.name;
|
|
1077
|
-
const activityStatus = {
|
|
1078
|
-
started: 'running',
|
|
1079
|
-
interacted: 'running',
|
|
1080
|
-
interrupted: 'interrupted',
|
|
1081
|
-
completed: 'completed',
|
|
1082
|
-
}[entry.status];
|
|
1083
|
-
agent.activity = entry.status;
|
|
1084
|
-
setProtocolStatus(agent, activityStatus);
|
|
1085
|
-
continue;
|
|
1086
|
-
}
|
|
1087
|
-
if (entry.kind === 'collaboration') {
|
|
1088
|
-
const receiverIds = entry.receiverThreadIds || [];
|
|
1089
|
-
const createsAgent = entry.tool === 'spawnAgent';
|
|
1090
|
-
const updatesKnownAgent = receiverIds.some(threadId => agents.has(threadId));
|
|
1091
|
-
if (!createsAgent && !updatesKnownAgent) {
|
|
1092
|
-
retained.push(entry);
|
|
1093
|
-
continue;
|
|
1094
|
-
}
|
|
1095
|
-
if (firstIndex < 0) firstIndex = retained.length;
|
|
1096
|
-
for (const threadId of receiverIds) {
|
|
1097
|
-
const agent = ensureAgent(threadId);
|
|
1098
|
-
if (!agent) continue;
|
|
1099
|
-
if (entry.prompt) agent.objective = entry.prompt;
|
|
1100
|
-
if (entry.model) agent.model = entry.model;
|
|
1101
|
-
if (entry.reasoningEffort) agent.reasoningEffort = entry.reasoningEffort;
|
|
1102
|
-
setProtocolStatus(agent, statusText(entry.agentsStates?.[threadId]?.status));
|
|
1103
|
-
if (entry.status === 'failed' && !entry.agentsStates?.[threadId]?.status) agent.status = 'errored';
|
|
1104
|
-
if (entry.status === 'interrupted' && !entry.agentsStates?.[threadId]?.status) agent.status = 'interrupted';
|
|
1105
|
-
}
|
|
1106
|
-
continue;
|
|
1107
|
-
}
|
|
1108
|
-
retained.push(entry);
|
|
1109
|
-
}
|
|
1110
|
-
if (!agents.size) return retained;
|
|
1111
|
-
retained.splice(firstIndex < 0 ? retained.length : firstIndex, 0, {
|
|
1112
|
-
type: 'activity',
|
|
1113
|
-
id: `subagents:${[...agents.keys()].join(':')}`,
|
|
1114
|
-
kind: 'subAgentGroup',
|
|
1115
|
-
title: '子 Agent',
|
|
1116
|
-
agents: [...agents.values()],
|
|
1117
|
-
});
|
|
1118
|
-
return retained;
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
function subAgentDisplayName(agentPath) {
|
|
1122
|
-
const segment = String(agentPath || '').split('/').map(value => value.trim()).filter(value => value && value !== 'root').at(-1);
|
|
1123
|
-
if (!segment) return '';
|
|
1124
|
-
const text = segment.replace(/[_-]+/g, ' ').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
1125
|
-
return text ? text[0].toUpperCase() + text.slice(1) : '';
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
1027
|
function sharedHistoryPageOptions() {
|
|
1129
1028
|
return {
|
|
1130
1029
|
maxTurns: SHARED_HISTORY_PAGE_TURNS,
|
|
@@ -1367,7 +1266,7 @@ function imageMimeType(filePath) {
|
|
|
1367
1266
|
function extractUserText(content) {
|
|
1368
1267
|
if (!Array.isArray(content)) return { text: '(包含图片或界面批注)', files: [] };
|
|
1369
1268
|
let text = content
|
|
1370
|
-
.filter(item => ['text', 'input_text'].includes(item?.type) && typeof item.text === 'string')
|
|
1269
|
+
.filter(item => ['text', 'input_text'].includes(item?.type) && typeof item.text === 'string' && !appServerEvents.isMobileInputNotice(item))
|
|
1371
1270
|
.map(item => item.text)
|
|
1372
1271
|
// Codex injects project instructions as user-role context, not chat input.
|
|
1373
1272
|
.filter(value => !/^\s*# AGENTS\.md instructions(?: for [^\r\n]+)?\r?\n\s*<INSTRUCTIONS>[\s\S]*<\/INSTRUCTIONS>\s*$/.test(value))
|