@walkhi/code-relax 0.1.0-beta.22 → 0.1.0-beta.23
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/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/admin-page.mjs +9 -9
- package/dist/src/self-relay/connector.mjs +1 -1
- package/dist/src/self-relay/lifecycle.mjs +160 -160
- package/dist/src/server.mjs +100 -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,45 @@ 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
|
-
|
|
298
|
-
if (request.method === 'GET' && url.pathname === '/api/turn-events') {
|
|
299
|
-
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
|
-
sharedServer.on('notification', listener);
|
|
309
|
-
sharedServer.on('disconnected', disconnected);
|
|
310
|
-
response.once('close', () => {
|
|
311
|
-
sharedServer.off('notification', listener);
|
|
312
|
-
sharedServer.off('disconnected', disconnected);
|
|
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
|
-
}
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (request.method === 'GET' && url.pathname === '/api/turn-events') {
|
|
303
|
+
response.startEvents();
|
|
304
|
+
const listener = ({ method, params }) => {
|
|
305
|
+
if (!['turn/started', 'turn/completed'].includes(method)) return;
|
|
306
|
+
const threadId = params?.threadId, turnId = params?.turn?.id;
|
|
307
|
+
if (typeof threadId === 'string' && typeof turnId === 'string' && !response.destroyed) {
|
|
308
|
+
response.event('turn', { method, threadId, turnId });
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
const disconnected = () => response.end();
|
|
312
|
+
sharedServer.on('notification', listener);
|
|
313
|
+
sharedServer.on('disconnected', disconnected);
|
|
314
|
+
response.once('close', () => {
|
|
315
|
+
sharedServer.off('notification', listener);
|
|
316
|
+
sharedServer.off('disconnected', disconnected);
|
|
317
|
+
});
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const activeTurnMatch = /^\/api\/threads\/([^/]+)\/active-turn$/.exec(url.pathname);
|
|
322
|
+
if (request.method === 'GET' && activeTurnMatch) {
|
|
323
|
+
const threadId = decodeURIComponent(activeTurnMatch[1]);
|
|
324
|
+
await sharedServer.connect();
|
|
325
|
+
const { thread } = await sharedServer.request('thread/read', { threadId, includeTurns: false });
|
|
326
|
+
const status = typeof thread.status === 'string' ? thread.status : thread.status?.type;
|
|
327
|
+
let turnId = null;
|
|
328
|
+
if (status === 'active') {
|
|
329
|
+
const page = await sharedServer.request('thread/turns/list', {
|
|
330
|
+
threadId, limit: 1, sortDirection: 'desc', itemsView: 'summary',
|
|
331
|
+
});
|
|
332
|
+
const latest = page.data?.[0];
|
|
333
|
+
if (latest?.status === 'inProgress') turnId = latest.id;
|
|
334
|
+
}
|
|
335
|
+
sendJson(response, 200, { turnId });
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
334
338
|
|
|
335
339
|
const createThreadMatch = /^\/api\/projects\/([^/]+)\/threads$/.exec(url.pathname);
|
|
336
340
|
if (request.method === 'POST' && createThreadMatch) {
|
|
@@ -374,22 +378,11 @@ async function handleRequest(request, response) {
|
|
|
374
378
|
if (request.method === 'GET' && timelineMatch) {
|
|
375
379
|
const threadId = decodeURIComponent(timelineMatch[1]);
|
|
376
380
|
const timeline = normalizeSharedTimeline(await sharedServer.timeline(threadId, sharedHistoryPageOptions()));
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const knownIndex = knownTurnId ? timeline.turns.findIndex(turn => turn.id === knownTurnId) : -1;
|
|
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);
|
|
381
|
+
if (request.socketRequest) {
|
|
382
|
+
response.on('progress', () => sharedServer.keepSnapshotAlive(threadId, timeline.sync));
|
|
383
|
+
response.once('close', () => sharedServer.keepSnapshotAlive(threadId, timeline.sync));
|
|
384
|
+
}
|
|
385
|
+
sendJson(response, 200, timeline);
|
|
393
386
|
return;
|
|
394
387
|
}
|
|
395
388
|
|
|
@@ -407,17 +400,18 @@ async function handleRequest(request, response) {
|
|
|
407
400
|
const after = Number(url.searchParams.get('afterSeq'));
|
|
408
401
|
const requestedSync = url.searchParams.get('syncId') && url.searchParams.get('syncEpoch') && Number.isSafeInteger(after) && after >= 0
|
|
409
402
|
? { 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
403
|
await streamSharedThread(response, threadId, sharedServer,
|
|
417
|
-
normalizeSharedTimeline, requestedSync
|
|
404
|
+
normalizeSharedTimeline, requestedSync);
|
|
418
405
|
return;
|
|
419
406
|
}
|
|
420
407
|
|
|
408
|
+
const modelChangesMatch = /^\/api\/threads\/([^/]+)\/model-changes$/.exec(url.pathname);
|
|
409
|
+
if (request.method === 'GET' && modelChangesMatch) {
|
|
410
|
+
const threadId = decodeURIComponent(modelChangesMatch[1]);
|
|
411
|
+
if (!/^[0-9a-f]{8}-[0-9a-f-]{20,55}$/i.test(threadId)) throw new RequestError(400, '对话 ID 无效。');
|
|
412
|
+
sendJson(response, 200, await turnModelHistory.read(findSessionPath(threadId), (url.searchParams.get('turnIds') || '').split(',').filter(Boolean)));
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
421
415
|
const contextMatch = /^\/api\/threads\/([^/]+)\/context$/.exec(url.pathname);
|
|
422
416
|
if (request.method === 'GET' && contextMatch) {
|
|
423
417
|
const threadId = decodeURIComponent(contextMatch[1]);
|
|
@@ -449,17 +443,6 @@ async function handleRequest(request, response) {
|
|
|
449
443
|
}
|
|
450
444
|
}
|
|
451
445
|
|
|
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
446
|
const archiveMatch = /^\/api\/threads\/([^/]+)\/(archive|unarchive)$/.exec(url.pathname);
|
|
464
447
|
if (request.method === 'POST' && archiveMatch) {
|
|
465
448
|
const threadId = decodeURIComponent(archiveMatch[1]);
|
|
@@ -477,8 +460,9 @@ async function handleRequest(request, response) {
|
|
|
477
460
|
if (request.method === 'GET') { sendJson(response, 200, await appServerEvents.readQueue(rpc, threadId)); return; }
|
|
478
461
|
if (request.method === 'POST') {
|
|
479
462
|
await requireExecutionWritable(threadId);
|
|
480
|
-
const
|
|
481
|
-
|
|
463
|
+
const body = await readJsonBody(request);
|
|
464
|
+
const args = appServerTurnArguments(threadId, body);
|
|
465
|
+
sendJson(response, 202, await appServerEvents.addQueue(rpc, threadId, args.input, crypto.randomUUID(), body.containsVoiceInput)); return;
|
|
482
466
|
}
|
|
483
467
|
}
|
|
484
468
|
|
|
@@ -510,8 +494,8 @@ async function handleRequest(request, response) {
|
|
|
510
494
|
await requireExecutionWritable(threadId);
|
|
511
495
|
const body = await readJsonBody(request);
|
|
512
496
|
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 });
|
|
497
|
+
const submitted = await sharedServer.send(appServerTurnArguments(threadId, body));
|
|
498
|
+
sendJson(response, 200, { success: true, data: submitted.result, usage: submitted.usage });
|
|
515
499
|
return;
|
|
516
500
|
}
|
|
517
501
|
|
|
@@ -543,7 +527,7 @@ void managedAppServer.start();
|
|
|
543
527
|
executionHealth.start();
|
|
544
528
|
runtimeDiscovery.start();
|
|
545
529
|
server.on('error', error => {
|
|
546
|
-
console.error('桌面服务监听失败:', error.message);
|
|
530
|
+
console.error('桌面服务监听失败:', error.message);
|
|
547
531
|
process.exit(1);
|
|
548
532
|
});
|
|
549
533
|
server.listen(options.port, options.lan ? '0.0.0.0' : '127.0.0.1', async () => {
|
|
@@ -551,12 +535,12 @@ server.listen(options.port, options.lan ? '0.0.0.0' : '127.0.0.1', async () => {
|
|
|
551
535
|
if (process.env.CODEX_REMOTE_INSTANCE && process.env.CODEX_REMOTE_CONTROL_SECRET && process.env.CODEX_REMOTE_READY_PATH) {
|
|
552
536
|
control = await openControl({ instanceId: process.env.CODEX_REMOTE_INSTANCE,
|
|
553
537
|
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'),
|
|
538
|
+
'refresh-execution': refreshExecution,
|
|
539
|
+
'relay-start': () => relayManager.run('relay-start'),
|
|
540
|
+
'relay-status': () => relayManager.run('relay-status'),
|
|
541
|
+
'relay-stop': () => relayManager.run('relay-stop'),
|
|
542
|
+
'relay-pair': () => relayManager.run('relay-pair'),
|
|
543
|
+
'relay-revoke': () => relayManager.run('relay-revoke'),
|
|
560
544
|
'restart-managed-app-server': () => {
|
|
561
545
|
const state = managedAppServer.snapshot();
|
|
562
546
|
return state.launcherRestartRequired ? managedAppServer.restart(crypto.randomUUID()) : state;
|
|
@@ -566,10 +550,10 @@ server.listen(options.port, options.lan ? '0.0.0.0' : '127.0.0.1', async () => {
|
|
|
566
550
|
pid: process.pid, controlPort: control.port });
|
|
567
551
|
}
|
|
568
552
|
} catch (error) {
|
|
569
|
-
console.error('桌面服务管理通道启动失败:', error.message);
|
|
553
|
+
console.error('桌面服务管理通道启动失败:', error.message);
|
|
570
554
|
process.exit(1);
|
|
571
555
|
}
|
|
572
|
-
console.log(`Code Relax 桌面服务已启动:http://127.0.0.1:${options.port}/`);
|
|
556
|
+
console.log(`Code Relax 桌面服务已启动:http://127.0.0.1:${options.port}/`);
|
|
573
557
|
if (options.lan) {
|
|
574
558
|
console.log('LAN 模式使用鉴权 WebSocket,仅适合受信任局域网测试,不要暴露到公网。');
|
|
575
559
|
}
|
|
@@ -582,8 +566,8 @@ async function shutDown() {
|
|
|
582
566
|
lanSocket.close();
|
|
583
567
|
server.close();
|
|
584
568
|
executionHealth?.stop();
|
|
585
|
-
runtimeDiscovery.stop();
|
|
586
|
-
await relayManager.close();
|
|
569
|
+
runtimeDiscovery.stop();
|
|
570
|
+
await relayManager.close();
|
|
587
571
|
managedAppServer.close();
|
|
588
572
|
await control?.close();
|
|
589
573
|
process.exit(0);
|
|
@@ -749,12 +733,12 @@ function messagePayload(body) {
|
|
|
749
733
|
return payload;
|
|
750
734
|
}
|
|
751
735
|
|
|
752
|
-
function appServerTurnArguments(threadId, body) {
|
|
736
|
+
function appServerTurnArguments(threadId, body) {
|
|
753
737
|
const payload = messagePayload(body);
|
|
754
738
|
const input = [];
|
|
755
739
|
if (payload.prompt) input.push({ type: 'text', text: payload.prompt });
|
|
756
740
|
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) };
|
|
741
|
+
const value = { threadId, input, ...appServerEvents.mobileTurnSource(body.clientUserMessageId, body.containsVoiceInput) };
|
|
758
742
|
if (typeof body.clientUserMessageId === 'string' && /^[a-f0-9]{32}$/.test(body.clientUserMessageId)) value.clientUserMessageId = body.clientUserMessageId;
|
|
759
743
|
if (payload.model) value.model = payload.model;
|
|
760
744
|
if (payload.thinking) value.effort = payload.thinking;
|
|
@@ -775,7 +759,7 @@ function normalizeTimeline(data) {
|
|
|
775
759
|
function normalizeSharedTimeline({ thread, nextCursor, sync }) {
|
|
776
760
|
const timeline = normalizeTimeline({ thread: { ...thread, title: thread.name || thread.title || '', hostId: 'local', transport: 'shared-app-server' } });
|
|
777
761
|
return { ...timeline,
|
|
778
|
-
historyView: '
|
|
762
|
+
historyView: 'item-pages-v1',
|
|
779
763
|
...(nextCursor !== undefined ? { nextCursor } : {}), ...(sync ? { sync } : {}) };
|
|
780
764
|
}
|
|
781
765
|
|
|
@@ -799,7 +783,7 @@ function normalizeThread(thread) {
|
|
|
799
783
|
function normalizeTurn(turn) {
|
|
800
784
|
const source = turn && typeof turn === 'object' ? turn : {};
|
|
801
785
|
const turnId = source.id || '';
|
|
802
|
-
const entries =
|
|
786
|
+
const entries = Array.isArray(source.items) ? source.items.map(normalizeItem).filter(Boolean) : [];
|
|
803
787
|
for (const entry of entries) {
|
|
804
788
|
const images = [...(entry.attachments || []), entry.image, ...Object.values(entry.imageAssets || {})].filter(Boolean);
|
|
805
789
|
for (const image of images) if (image.remotePath && !image.recoveryTurnId) image.recoveryTurnId = turnId;
|
|
@@ -811,6 +795,7 @@ function normalizeTurn(turn) {
|
|
|
811
795
|
completedAt: source.completedAt || null,
|
|
812
796
|
durationMs: source.durationMs ?? null,
|
|
813
797
|
error: source.error ?? null,
|
|
798
|
+
...(source.itemPage ? { itemPage: source.itemPage } : {}),
|
|
814
799
|
entries,
|
|
815
800
|
};
|
|
816
801
|
}
|
|
@@ -818,17 +803,6 @@ function normalizeTurn(turn) {
|
|
|
818
803
|
function normalizeItem(item) {
|
|
819
804
|
if (!item || typeof item !== 'object') return null;
|
|
820
805
|
|
|
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
806
|
if (item.type === 'contextCompaction') {
|
|
833
807
|
return {
|
|
834
808
|
type: 'activity',
|
|
@@ -965,28 +939,28 @@ function normalizeItem(item) {
|
|
|
965
939
|
};
|
|
966
940
|
}
|
|
967
941
|
|
|
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 {
|
|
942
|
+
if (item.type === 'imageView') {
|
|
943
|
+
const image = typeof item.path === 'string' && item.path
|
|
944
|
+
? normalizeAttachment({ type: 'localImage', path: item.path })
|
|
945
|
+
: null;
|
|
946
|
+
if (image?.available) {
|
|
947
|
+
return {
|
|
948
|
+
type: 'message',
|
|
949
|
+
id: item.id || '',
|
|
950
|
+
role: 'assistant',
|
|
951
|
+
phase: 'commentary',
|
|
952
|
+
text: '',
|
|
953
|
+
attachments: [image],
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
return {
|
|
983
957
|
type: 'activity',
|
|
984
958
|
id: item.id || '',
|
|
985
959
|
kind: 'imageView',
|
|
986
960
|
title: '查看图片',
|
|
987
961
|
titleOnly: true,
|
|
988
962
|
path: item.path || '',
|
|
989
|
-
image,
|
|
963
|
+
image,
|
|
990
964
|
};
|
|
991
965
|
}
|
|
992
966
|
|
|
@@ -1048,83 +1022,6 @@ function normalizeItem(item) {
|
|
|
1048
1022
|
};
|
|
1049
1023
|
}
|
|
1050
1024
|
|
|
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
1025
|
function sharedHistoryPageOptions() {
|
|
1129
1026
|
return {
|
|
1130
1027
|
maxTurns: SHARED_HISTORY_PAGE_TURNS,
|
|
@@ -1367,7 +1264,7 @@ function imageMimeType(filePath) {
|
|
|
1367
1264
|
function extractUserText(content) {
|
|
1368
1265
|
if (!Array.isArray(content)) return { text: '(包含图片或界面批注)', files: [] };
|
|
1369
1266
|
let text = content
|
|
1370
|
-
.filter(item => ['text', 'input_text'].includes(item?.type) && typeof item.text === 'string')
|
|
1267
|
+
.filter(item => ['text', 'input_text'].includes(item?.type) && typeof item.text === 'string' && !appServerEvents.isMobileInputNotice(item))
|
|
1371
1268
|
.map(item => item.text)
|
|
1372
1269
|
// Codex injects project instructions as user-role context, not chat input.
|
|
1373
1270
|
.filter(value => !/^\s*# AGENTS\.md instructions(?: for [^\r\n]+)?\r?\n\s*<INSTRUCTIONS>[\s\S]*<\/INSTRUCTIONS>\s*$/.test(value))
|