@walkhi/code-relax 0.1.0-beta.1 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/bin/self-relay-server.mjs +5 -2
- package/dist/shared/app-server-events.cjs +45 -19
- package/dist/shared/p2p-data-channel.cjs +50 -0
- package/dist/src/app-server-tasks.mjs +28 -25
- package/dist/src/lan-socket-server.mjs +8 -2
- package/dist/src/platform/windows/IsolatedProcess.cs +7 -3
- package/dist/src/platform/windows/background-process.mjs +7 -2
- package/dist/src/platform/windows/launch-worker.ps1 +13 -6
- package/dist/src/platform/windows/start-hidden-console.ps1 +11 -6
- package/dist/src/self-relay/admin-state.mjs +61 -0
- package/dist/src/self-relay/client.mjs +2 -0
- package/dist/src/self-relay/connector.mjs +23 -8
- package/dist/src/self-relay/demo.mjs +2 -2
- package/dist/src/self-relay/lifecycle.mjs +1 -1
- package/dist/src/self-relay/p2p-probe.mjs +123 -83
- package/dist/src/self-relay/server.mjs +137 -11
- package/dist/src/server.mjs +337 -255
- package/dist/src/shared-app-server.mjs +180 -24
- package/dist/src/shared-catalog.mjs +4 -14
- package/dist/src/thread-catalog.mjs +12 -7
- package/dist/web/activity-view.js +283 -0
- package/dist/web/capabilities.js +2 -2
- package/dist/web/chat-transport.js +15 -9
- package/dist/web/chat.css +139 -93
- package/dist/web/chat.js +1451 -2770
- package/dist/web/community-view.js +20 -0
- package/dist/web/community.css +77 -0
- package/dist/web/community.html +37 -0
- package/dist/web/composer-controller.js +101 -0
- package/dist/web/conversation-controller.js +99 -0
- package/dist/web/disclosure-state-controller.js +95 -0
- package/dist/web/draft-controller.js +103 -0
- package/dist/web/harmony-platform.js +3 -2
- package/dist/web/history-cache.js +112 -18
- package/dist/web/history-controller.js +167 -0
- package/dist/web/index.html +141 -38
- package/dist/web/link-action-controller.js +212 -0
- package/dist/web/message-send-controller.js +177 -0
- package/dist/web/message-view.js +98 -0
- package/dist/web/p2p-data-channel.js +50 -0
- package/dist/web/p2p-probe.js +67 -27
- package/dist/web/page-resume.js +28 -0
- package/dist/web/pending-message-store.js +108 -0
- package/dist/web/queue-controller.js +82 -0
- package/dist/web/resources.json +1 -1
- package/dist/web/self-relay-session.js +28 -18
- package/dist/web/station-connection-controller.js +75 -0
- package/dist/web/task-list-view.js +296 -0
- package/dist/web/thread-attention-controller.js +124 -0
- package/dist/web/thread-context-controller.js +30 -0
- package/dist/web/thread-list-controller.js +61 -0
- package/dist/web/thread-list-sync.js +86 -0
- package/dist/web/thread-title-controller.js +57 -0
- package/dist/web/timeline-formatters.js +249 -0
- package/dist/web/timeline-reducer.js +81 -0
- package/dist/web/timeline-renderer.js +161 -0
- package/dist/web/timeline-scroll-controller.js +50 -0
- package/dist/web/usage-controller.js +258 -0
- package/dist/web/vendor/lucide.LICENSE.txt +17 -0
- package/package.json +1 -1
- package/tools/postinstall.mjs +66 -2
package/dist/src/server.mjs
CHANGED
|
@@ -5,7 +5,8 @@ import net from 'node:net';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import os from 'node:os';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import vm from 'node:vm';
|
|
8
|
+
import vm from 'node:vm';
|
|
9
|
+
import sharp from 'sharp';
|
|
9
10
|
import { attachLanSocket } from './lan-socket-server.mjs';
|
|
10
11
|
import { validateInlineImage, persistLegacyImage } from './message-images.mjs';
|
|
11
12
|
import { ThreadWriterConflictError } from './shared-app-server.mjs';
|
|
@@ -33,7 +34,9 @@ const MAX_ATTACHMENT_BYTES = 16 * 1024 * 1024;
|
|
|
33
34
|
const MAX_CACHED_ATTACHMENTS = 32;
|
|
34
35
|
const MAX_CACHED_FILES = 64;
|
|
35
36
|
const MAX_CONTEXT_TAIL_BYTES = 4 * 1024 * 1024;
|
|
36
|
-
const MAX_LIVE_ACTIVITY_TAIL_BYTES = 1024 * 1024;
|
|
37
|
+
const MAX_LIVE_ACTIVITY_TAIL_BYTES = 1024 * 1024;
|
|
38
|
+
const SHARED_HISTORY_PAGE_BYTES = 500 * 1024;
|
|
39
|
+
const SHARED_HISTORY_PAGE_TURNS = 8;
|
|
37
40
|
const MAX_SETTINGS_SCAN_BYTES = 64 * 1024 * 1024;
|
|
38
41
|
const ALLOWED_MODELS = new Set([
|
|
39
42
|
'gpt-6-astra',
|
|
@@ -49,7 +52,6 @@ const ALLOWED_TOOLS = new Set([
|
|
|
49
52
|
'get_usage_limits',
|
|
50
53
|
'list_projects',
|
|
51
54
|
'list_threads',
|
|
52
|
-
'navigate_to_codex_page',
|
|
53
55
|
'read_thread',
|
|
54
56
|
'send_message_to_thread',
|
|
55
57
|
'wait_threads',
|
|
@@ -68,9 +70,11 @@ const markdownSandbox = { atob };
|
|
|
68
70
|
vm.runInNewContext(markdownItScript.toString(), markdownSandbox);
|
|
69
71
|
const imageMarkdown = markdownSandbox.markdownit({ html: false });
|
|
70
72
|
let nextRequestId = 0;
|
|
71
|
-
const attachmentCache = new Map();
|
|
72
|
-
const fileCache = new Map();
|
|
73
|
-
const fileCapabilitySecret = crypto.randomBytes(32);
|
|
73
|
+
const attachmentCache = new Map();
|
|
74
|
+
const fileCache = new Map();
|
|
75
|
+
const fileCapabilitySecret = crypto.randomBytes(32);
|
|
76
|
+
const imageCapabilityKey = crypto.createHash('sha256')
|
|
77
|
+
.update('code-relax-image-capability\0').update(options.token || fileCapabilitySecret).digest();
|
|
74
78
|
const legacyUploadsRoot = path.resolve(webRoot, '..', 'uploads');
|
|
75
79
|
const allowedUploadsRoots = [uploadsRoot, legacyUploadsRoot,
|
|
76
80
|
...JSON.parse(process.env.CODEX_REMOTE_LEGACY_UPLOADS_ROOTS || '[]')];
|
|
@@ -88,7 +92,7 @@ function selectSharedExecution(enabled) {
|
|
|
88
92
|
const next = enabled ? managedAppServer.client : null;
|
|
89
93
|
if (sharedServer === next) return;
|
|
90
94
|
sharedServer = next;
|
|
91
|
-
sharedCatalog = sharedServer ? new SharedCatalog(sharedServer
|
|
95
|
+
sharedCatalog = sharedServer ? new SharedCatalog(sharedServer) : null;
|
|
92
96
|
}
|
|
93
97
|
const executionHealth = new ExecutionHealth(async () => {
|
|
94
98
|
await managedAppServer.ensure();
|
|
@@ -225,11 +229,23 @@ async function handleRequest(request, response) {
|
|
|
225
229
|
}
|
|
226
230
|
|
|
227
231
|
|
|
228
|
-
const imageMatch = /^\/api\/images\/([a-f0-9]{64})$/.exec(url.pathname);
|
|
229
|
-
if (request.method === 'GET' && imageMatch) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
232
|
+
const imageMatch = /^\/api\/images\/([a-f0-9]{64})$/.exec(url.pathname);
|
|
233
|
+
if (request.method === 'GET' && imageMatch) {
|
|
234
|
+
let image = attachmentCache.get(imageMatch[1])
|
|
235
|
+
|| recoverLocalImage(imageMatch[1], url.searchParams.get('recovery') || '');
|
|
236
|
+
const contextThreadId = url.searchParams.get('threadId') || '';
|
|
237
|
+
const contextTurnId = url.searchParams.get('turnId') || '';
|
|
238
|
+
if (!image && sharedServer && contextThreadId && contextTurnId
|
|
239
|
+
&& contextThreadId.length <= 128 && contextTurnId.length <= 128) {
|
|
240
|
+
try {
|
|
241
|
+
normalizeTurn(await sharedServer.turnContext(contextThreadId, contextTurnId));
|
|
242
|
+
image = attachmentCache.get(imageMatch[1]);
|
|
243
|
+
} catch {}
|
|
244
|
+
}
|
|
245
|
+
if (!image) throw new RequestError(404, '这张图片已失效,无法从工作站重新读取。');
|
|
246
|
+
const thumbnail = url.searchParams.get('variant') === 'thumbnail';
|
|
247
|
+
const asset = thumbnail ? await attachmentThumbnail(image) : { buffer: image.buffer, mimeType: image.mimeType };
|
|
248
|
+
sendJson(response, 200, { url: `data:${asset.mimeType};base64,${asset.buffer.toString('base64')}` });
|
|
233
249
|
return;
|
|
234
250
|
}
|
|
235
251
|
|
|
@@ -272,13 +288,14 @@ async function handleRequest(request, response) {
|
|
|
272
288
|
projects: catalogProjects };
|
|
273
289
|
}
|
|
274
290
|
let appServerThreads = archived ? [] : appServerTasks.store.list().map(record => ({
|
|
275
|
-
id: record.id,
|
|
276
|
-
kind: 'codex',
|
|
277
|
-
hostId: 'app-server',
|
|
278
|
-
title: record.title,
|
|
279
|
-
cwd: record.cwd,
|
|
280
|
-
projectId: record.projectId,
|
|
281
|
-
updatedAt: record.updatedAt,
|
|
291
|
+
id: record.id,
|
|
292
|
+
kind: 'codex',
|
|
293
|
+
hostId: 'app-server',
|
|
294
|
+
title: record.title,
|
|
295
|
+
cwd: record.cwd,
|
|
296
|
+
projectId: record.projectId,
|
|
297
|
+
updatedAt: record.updatedAt,
|
|
298
|
+
recencyAt: record.recencyAt ?? record.updatedAt,
|
|
282
299
|
status: 'unknown',
|
|
283
300
|
transport: 'app-server',
|
|
284
301
|
}));
|
|
@@ -398,17 +415,20 @@ async function handleRequest(request, response) {
|
|
|
398
415
|
if (request.method === 'GET' && timelineMatch) {
|
|
399
416
|
const threadId = decodeURIComponent(timelineMatch[1]);
|
|
400
417
|
if (!appServerTasks.owns(threadId) && usesSharedTask({ hostId: url.searchParams.get('hostId') })) {
|
|
401
|
-
const timeline = normalizeSharedTimeline(await sharedServer.timeline(threadId));
|
|
418
|
+
const timeline = normalizeSharedTimeline(await sharedServer.timeline(threadId, sharedHistoryPageOptions()));
|
|
402
419
|
const knownUpdatedAt = Number(url.searchParams.get('knownUpdatedAt'));
|
|
403
420
|
const knownTurnId = url.searchParams.get('knownTurnId') || '';
|
|
404
421
|
const knownTurnStatus = url.searchParams.get('knownTurnStatus') || '';
|
|
422
|
+
const knownHistoryView = url.searchParams.get('knownHistoryView') || '';
|
|
405
423
|
const latest = timeline.turns.at(-1);
|
|
406
424
|
const knownIndex = knownTurnId ? timeline.turns.findIndex(turn => turn.id === knownTurnId) : -1;
|
|
407
|
-
const
|
|
425
|
+
const sameHistoryView = knownHistoryView === timeline.historyView;
|
|
426
|
+
const unchanged = sameHistoryView
|
|
427
|
+
&& knownIndex === timeline.turns.length - 1
|
|
408
428
|
&& timeline.thread.status === 'idle' && latest?.status !== 'inProgress'
|
|
409
429
|
&& knownTurnStatus && knownTurnStatus !== 'inProgress'
|
|
410
430
|
&& (!Number.isFinite(knownUpdatedAt) || knownUpdatedAt <= 0 || timeline.thread.updatedAt === knownUpdatedAt);
|
|
411
|
-
const partial = !unchanged && knownIndex >= 0
|
|
431
|
+
const partial = !unchanged && sameHistoryView && knownIndex >= 0
|
|
412
432
|
? { ...timeline, turns: timeline.turns.slice(knownIndex), partialFromTurnId: knownTurnId }
|
|
413
433
|
: timeline;
|
|
414
434
|
sendJson(response, 200, unchanged ? { ...timeline, turns: [], notModified: true } : partial);
|
|
@@ -474,23 +494,6 @@ async function handleRequest(request, response) {
|
|
|
474
494
|
return;
|
|
475
495
|
}
|
|
476
496
|
|
|
477
|
-
const navigateMatch = /^\/api\/threads\/([^/]+)\/navigate$/.exec(url.pathname);
|
|
478
|
-
if (request.method === 'POST' && navigateMatch) {
|
|
479
|
-
const threadId = decodeURIComponent(navigateMatch[1]);
|
|
480
|
-
if (sharedServer && !desktopMonitor.pipe) {
|
|
481
|
-
sendJson(response, 200, { opened: false, reason: 'Desktop 工具通道尚未就绪' });
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
if (appServerTasks.owns(threadId)) {
|
|
485
|
-
sendJson(response, 200, { opened: false, transport: 'app-server' });
|
|
486
|
-
return;
|
|
487
|
-
}
|
|
488
|
-
sendToolResult(response, await callTool('navigate_to_codex_page', {
|
|
489
|
-
threadId,
|
|
490
|
-
}, 15_000));
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
497
|
const contextMatch = /^\/api\/threads\/([^/]+)\/context$/.exec(url.pathname);
|
|
495
498
|
if (request.method === 'GET' && contextMatch) {
|
|
496
499
|
const threadId = decodeURIComponent(contextMatch[1]);
|
|
@@ -502,11 +505,11 @@ async function handleRequest(request, response) {
|
|
|
502
505
|
}
|
|
503
506
|
|
|
504
507
|
const sharedAction = /^\/api\/threads\/([^/]+)\/(history|settings|permissions)$/.exec(url.pathname);
|
|
505
|
-
if (sharedAction) {
|
|
508
|
+
if (sharedAction) {
|
|
506
509
|
const id = decodeURIComponent(sharedAction[1]);
|
|
507
510
|
if (!usesSharedTask({ hostId: url.searchParams.get('hostId') }) || appServerTasks.owns(id)) throw new RequestError(400, '此连接不支持该操作。');
|
|
508
511
|
if (request.method === 'GET' && sharedAction[2] === 'history') {
|
|
509
|
-
sendJson(response, 200, normalizeSharedTimeline(await sharedServer.history(id, url.searchParams.get('cursor'))));
|
|
512
|
+
sendJson(response, 200, normalizeSharedTimeline(await sharedServer.history(id, url.searchParams.get('cursor'), sharedHistoryPageOptions())));
|
|
510
513
|
return;
|
|
511
514
|
}
|
|
512
515
|
if (request.method === 'POST' && sharedAction[2] === 'settings') {
|
|
@@ -520,10 +523,24 @@ async function handleRequest(request, response) {
|
|
|
520
523
|
const body = await readJsonBody(request);
|
|
521
524
|
sendJson(response, 200, await sharedServer.permissions(id, body.mode));
|
|
522
525
|
return;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
const
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const sharedOperationGroup = /^\/api\/threads\/([^/]+)\/turns\/([^/]+)\/groups\/([^/]+)\/items$/.exec(url.pathname);
|
|
530
|
+
if (request.method === 'GET' && sharedOperationGroup) {
|
|
531
|
+
const threadId = decodeURIComponent(sharedOperationGroup[1]);
|
|
532
|
+
const turnId = decodeURIComponent(sharedOperationGroup[2]);
|
|
533
|
+
const groupId = decodeURIComponent(sharedOperationGroup[3]);
|
|
534
|
+
if (!usesSharedTask({ hostId: url.searchParams.get('hostId') }) || appServerTasks.owns(threadId)) {
|
|
535
|
+
throw new RequestError(400, '此连接不支持操作组分页。');
|
|
536
|
+
}
|
|
537
|
+
const page = await sharedServer.operationGroupItems(threadId, turnId, groupId, url.searchParams.get('offset'));
|
|
538
|
+
sendJson(response, 200, { turnId: page.turnId, groupId: page.groupId,
|
|
539
|
+
entries: page.items.map(normalizeItem).filter(Boolean), nextOffset: page.nextOffset, total: page.total });
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const archiveMatch = /^\/api\/threads\/([^/]+)\/(archive|unarchive)$/.exec(url.pathname);
|
|
527
544
|
if (request.method === 'POST' && archiveMatch) {
|
|
528
545
|
const threadId = decodeURIComponent(archiveMatch[1]);
|
|
529
546
|
await requireExecutionWritable(threadId);
|
|
@@ -1232,17 +1249,17 @@ function appServerProjectTarget(projectId, projects) {
|
|
|
1232
1249
|
function mergeThreadCatalog(catalog, appThreads, limit, sharedSnapshot = Boolean(sharedServer)) {
|
|
1233
1250
|
const owned = new Map(appThreads.map(thread => [thread.id, thread]));
|
|
1234
1251
|
const pinnedThreads = (catalog.pinnedThreads || []).map(thread => owned.get(thread.id) || thread);
|
|
1235
|
-
const pinnedIds = new Set(pinnedThreads.map(thread => thread.id));
|
|
1236
|
-
if (sharedSnapshot) {
|
|
1237
|
-
const listed = new Set([...pinnedIds, ...(catalog.threads || []).map(thread => thread.id)]);
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
].slice(0, limit) };
|
|
1242
|
-
}
|
|
1252
|
+
const pinnedIds = new Set(pinnedThreads.map(thread => thread.id));
|
|
1253
|
+
if (sharedSnapshot) {
|
|
1254
|
+
const listed = new Set([...pinnedIds, ...(catalog.threads || []).map(thread => thread.id)]);
|
|
1255
|
+
const threads = (catalog.threads || []).map(thread => owned.get(thread.id) || thread);
|
|
1256
|
+
threads.push(...appThreads.filter(thread => !listed.has(thread.id)));
|
|
1257
|
+
threads.sort((left, right) => threadRecency(right) - threadRecency(left));
|
|
1258
|
+
return { ...catalog, pinnedThreads: [], threads: threads.slice(0, limit) };
|
|
1259
|
+
}
|
|
1243
1260
|
const desktopThreads = (catalog.threads || []).filter(thread => !owned.has(thread.id));
|
|
1244
1261
|
const merged = [...desktopThreads, ...appThreads.filter(thread => !pinnedIds.has(thread.id))]
|
|
1245
|
-
.sort((left, right) =>
|
|
1262
|
+
.sort((left, right) => threadRecency(right) - threadRecency(left))
|
|
1246
1263
|
.slice(0, limit);
|
|
1247
1264
|
return { ...catalog, pinnedThreads, threads: merged };
|
|
1248
1265
|
}
|
|
@@ -1489,10 +1506,11 @@ function normalizeAppServerTimeline(thread) {
|
|
|
1489
1506
|
}, false);
|
|
1490
1507
|
}
|
|
1491
1508
|
|
|
1492
|
-
function normalizeSharedTimeline({ thread, nextCursor, sync }) {
|
|
1493
|
-
return { ...normalizeTimeline({ thread: { ...thread, title: thread.name || thread.title || '', hostId: 'local', transport: 'shared-app-server' } }, false),
|
|
1494
|
-
|
|
1495
|
-
}
|
|
1509
|
+
function normalizeSharedTimeline({ thread, nextCursor, sync }) {
|
|
1510
|
+
return { ...normalizeTimeline({ thread: { ...thread, title: thread.name || thread.title || '', hostId: 'local', transport: 'shared-app-server' } }, false),
|
|
1511
|
+
historyView: 'grouped-lazy-skeleton-v11',
|
|
1512
|
+
...(nextCursor !== undefined ? { nextCursor } : {}), ...(sync ? { sync } : {}) };
|
|
1513
|
+
}
|
|
1496
1514
|
|
|
1497
1515
|
function restoreEmptyTurnMessages(threadId, turns) {
|
|
1498
1516
|
const missing = new Map(turns.filter(turn => turn.entries.length === 0).map(turn => [turn.id, turn]));
|
|
@@ -1539,6 +1557,7 @@ function normalizeThread(thread) {
|
|
|
1539
1557
|
title: source.name || source.title || source.preview || '',
|
|
1540
1558
|
status: statusText(source.status),
|
|
1541
1559
|
updatedAt: Number(source.updatedAt) || 0,
|
|
1560
|
+
recencyAt: Number(source.recencyAt ?? source.updatedAt) || 0,
|
|
1542
1561
|
projectId: source.projectId || '',
|
|
1543
1562
|
cwd: source.cwd || '',
|
|
1544
1563
|
hostId: source.hostId || '',
|
|
@@ -1548,21 +1567,39 @@ function normalizeThread(thread) {
|
|
|
1548
1567
|
};
|
|
1549
1568
|
}
|
|
1550
1569
|
|
|
1551
|
-
function normalizeTurn(turn) {
|
|
1552
|
-
const source = turn && typeof turn === 'object' ? turn : {};
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1570
|
+
function normalizeTurn(turn) {
|
|
1571
|
+
const source = turn && typeof turn === 'object' ? turn : {};
|
|
1572
|
+
const turnId = source.id || '';
|
|
1573
|
+
const entries = Array.isArray(source.items) ? source.items.map(normalizeItem).filter(Boolean) : [];
|
|
1574
|
+
for (const entry of entries) {
|
|
1575
|
+
const images = [...(entry.attachments || []), entry.image, ...Object.values(entry.imageAssets || {})].filter(Boolean);
|
|
1576
|
+
for (const image of images) if (image.remotePath && !image.recoveryTurnId) image.recoveryTurnId = turnId;
|
|
1577
|
+
}
|
|
1578
|
+
return {
|
|
1579
|
+
id: turnId,
|
|
1580
|
+
status: statusText(source.status),
|
|
1556
1581
|
startedAt: source.startedAt || null,
|
|
1557
1582
|
completedAt: source.completedAt || null,
|
|
1558
1583
|
durationMs: source.durationMs ?? null,
|
|
1559
|
-
error: source.error ?? null,
|
|
1560
|
-
entries
|
|
1561
|
-
};
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
function normalizeItem(item) {
|
|
1565
|
-
if (!item || typeof item !== 'object') return null;
|
|
1584
|
+
error: source.error ?? null,
|
|
1585
|
+
entries,
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
function normalizeItem(item) {
|
|
1590
|
+
if (!item || typeof item !== 'object') return null;
|
|
1591
|
+
|
|
1592
|
+
if (item.type === 'lazyOperationGroup') {
|
|
1593
|
+
return {
|
|
1594
|
+
type: 'activity',
|
|
1595
|
+
id: item.id || '',
|
|
1596
|
+
kind: 'lazyOperationGroup',
|
|
1597
|
+
turnId: item.turnId || '',
|
|
1598
|
+
groupId: item.groupId || '',
|
|
1599
|
+
title: item.title || '执行过程',
|
|
1600
|
+
count: Number(item.count) || 0,
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1566
1603
|
|
|
1567
1604
|
if (item.type === 'contextCompaction') {
|
|
1568
1605
|
return {
|
|
@@ -1616,132 +1653,147 @@ function normalizeItem(item) {
|
|
|
1616
1653
|
};
|
|
1617
1654
|
}
|
|
1618
1655
|
|
|
1619
|
-
if (item.type === 'reasoning') {
|
|
1620
|
-
const detail = Array.isArray(item.summary)
|
|
1621
|
-
? item.summary.map(summaryText).filter(Boolean).join('\n')
|
|
1622
|
-
: summaryText(item.summary);
|
|
1623
|
-
return {
|
|
1624
|
-
type: 'activity',
|
|
1625
|
-
id: item.id || '',
|
|
1626
|
-
kind: 'reasoning',
|
|
1627
|
-
title: '思考摘要',
|
|
1628
|
-
detail,
|
|
1629
|
-
};
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
if (item.type === 'commandExecution') {
|
|
1633
|
-
return {
|
|
1634
|
-
type: 'activity',
|
|
1635
|
-
id: item.id || '',
|
|
1636
|
-
kind: 'command',
|
|
1637
|
-
title: '命令',
|
|
1638
|
-
status: statusText(item.status),
|
|
1639
|
-
command: item.command || '',
|
|
1640
|
-
cwd: item.cwd || '',
|
|
1641
|
-
exitCode: item.exitCode ?? null,
|
|
1642
|
-
durationMs: item.durationMs ?? null,
|
|
1643
|
-
output: item.aggregatedOutput || item.output?.text || '',
|
|
1644
|
-
};
|
|
1645
|
-
}
|
|
1656
|
+
if (item.type === 'reasoning') {
|
|
1657
|
+
const detail = Array.isArray(item.summary)
|
|
1658
|
+
? item.summary.map(summaryText).filter(Boolean).join('\n')
|
|
1659
|
+
: summaryText(item.summary);
|
|
1660
|
+
return {
|
|
1661
|
+
type: 'activity',
|
|
1662
|
+
id: item.id || '',
|
|
1663
|
+
kind: 'reasoning',
|
|
1664
|
+
title: '思考摘要',
|
|
1665
|
+
detail,
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1646
1668
|
|
|
1669
|
+
if (item.type === 'commandExecution') {
|
|
1670
|
+
return {
|
|
1671
|
+
type: 'activity',
|
|
1672
|
+
id: item.id || '',
|
|
1673
|
+
kind: 'command',
|
|
1674
|
+
title: '命令',
|
|
1675
|
+
titleOnly: true,
|
|
1676
|
+
status: statusText(item.status),
|
|
1677
|
+
command: item.command,
|
|
1678
|
+
cwd: item.cwd,
|
|
1679
|
+
exitCode: item.exitCode,
|
|
1680
|
+
durationMs: item.durationMs,
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1647
1684
|
if (item.type === 'mcpToolCall') {
|
|
1648
|
-
const result = normalizeToolResult(item.result, item.error);
|
|
1649
1685
|
return {
|
|
1650
1686
|
type: 'activity',
|
|
1651
1687
|
id: item.id || '',
|
|
1652
|
-
kind: 'tool',
|
|
1653
|
-
title: '工具',
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1688
|
+
kind: 'tool',
|
|
1689
|
+
title: '工具',
|
|
1690
|
+
titleOnly: true,
|
|
1691
|
+
status: statusText(item.status),
|
|
1692
|
+
server: item.server,
|
|
1693
|
+
tool: item.tool,
|
|
1694
|
+
arguments: item.arguments,
|
|
1695
|
+
durationMs: item.durationMs,
|
|
1696
|
+
error: item.error,
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
if (item.type === 'dynamicToolCall') {
|
|
1701
|
+
return {
|
|
1702
|
+
type: 'activity',
|
|
1703
|
+
id: item.id || '',
|
|
1704
|
+
kind: 'tool',
|
|
1705
|
+
title: '工具',
|
|
1706
|
+
titleOnly: true,
|
|
1707
|
+
status: statusText(item.status),
|
|
1708
|
+
namespace: item.namespace,
|
|
1709
|
+
tool: item.tool,
|
|
1710
|
+
arguments: item.arguments,
|
|
1711
|
+
durationMs: item.durationMs,
|
|
1712
|
+
success: item.success,
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
if (item.type === 'fileChange') {
|
|
1717
|
+
return {
|
|
1718
|
+
type: 'activity',
|
|
1719
|
+
id: item.id || '',
|
|
1720
|
+
kind: 'fileChange',
|
|
1721
|
+
title: '文件修改',
|
|
1722
|
+
titleOnly: true,
|
|
1723
|
+
status: statusText(item.status),
|
|
1724
|
+
changes: Array.isArray(item.changes) ? item.changes : [],
|
|
1663
1725
|
};
|
|
1664
1726
|
}
|
|
1665
1727
|
|
|
1666
|
-
if (item.type === '
|
|
1667
|
-
return {
|
|
1668
|
-
type: 'activity',
|
|
1669
|
-
id: item.id || '',
|
|
1670
|
-
kind: '
|
|
1671
|
-
title: '
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
type: 'activity',
|
|
1736
|
-
id: item.id || '',
|
|
1737
|
-
kind: 'collaboration',
|
|
1738
|
-
title: '任务协作',
|
|
1739
|
-
status: statusText(item.status),
|
|
1740
|
-
tool: item.tool || '',
|
|
1741
|
-
prompt: item.prompt || '',
|
|
1742
|
-
receiverThreadIds: Array.isArray(item.receiverThreadIds) ? item.receiverThreadIds : [],
|
|
1743
|
-
};
|
|
1744
|
-
}
|
|
1728
|
+
if (item.type === 'webSearch') {
|
|
1729
|
+
return {
|
|
1730
|
+
type: 'activity',
|
|
1731
|
+
id: item.id || '',
|
|
1732
|
+
kind: 'webSearch',
|
|
1733
|
+
title: '网页搜索',
|
|
1734
|
+
titleOnly: true,
|
|
1735
|
+
query: item.query || item.action?.query || '',
|
|
1736
|
+
...(item.action && typeof item.action === 'object' ? { action: item.action } : {}),
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
if (item.type === 'imageView') {
|
|
1741
|
+
return {
|
|
1742
|
+
type: 'activity',
|
|
1743
|
+
id: item.id || '',
|
|
1744
|
+
kind: 'imageView',
|
|
1745
|
+
title: '查看图片',
|
|
1746
|
+
titleOnly: true,
|
|
1747
|
+
path: item.path || '',
|
|
1748
|
+
image: typeof item.path === 'string' && item.path
|
|
1749
|
+
? normalizeAttachment({ type: 'localImage', path: item.path })
|
|
1750
|
+
: null,
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
if (item.type === 'imageGeneration') {
|
|
1755
|
+
const image = typeof item.savedPath === 'string' && item.savedPath
|
|
1756
|
+
? normalizeAttachment({ type: 'localImage', path: item.savedPath, name: '生成的图片' })
|
|
1757
|
+
: null;
|
|
1758
|
+
return {
|
|
1759
|
+
type: 'activity',
|
|
1760
|
+
id: item.id || '',
|
|
1761
|
+
kind: 'imageGeneration',
|
|
1762
|
+
title: '生成图片',
|
|
1763
|
+
titleOnly: true,
|
|
1764
|
+
status: statusText(item.status),
|
|
1765
|
+
image,
|
|
1766
|
+
failure: item.failure || null,
|
|
1767
|
+
transparentBackground: item.transparentBackground === true,
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
if (item.type === 'subAgentActivity') {
|
|
1772
|
+
return {
|
|
1773
|
+
type: 'activity',
|
|
1774
|
+
id: item.id || '',
|
|
1775
|
+
kind: 'subAgent',
|
|
1776
|
+
title: '子任务',
|
|
1777
|
+
titleOnly: true,
|
|
1778
|
+
status: statusText(item.kind),
|
|
1779
|
+
agentPath: item.agentPath || '',
|
|
1780
|
+
agentThreadId: item.agentThreadId || '',
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
if (item.type === 'collabAgentToolCall') {
|
|
1785
|
+
return {
|
|
1786
|
+
type: 'activity',
|
|
1787
|
+
id: item.id || '',
|
|
1788
|
+
kind: 'collaboration',
|
|
1789
|
+
title: '任务协作',
|
|
1790
|
+
titleOnly: true,
|
|
1791
|
+
status: statusText(item.status),
|
|
1792
|
+
tool: item.tool || '',
|
|
1793
|
+
prompt: item.prompt || '',
|
|
1794
|
+
receiverThreadIds: Array.isArray(item.receiverThreadIds) ? item.receiverThreadIds : [],
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1745
1797
|
|
|
1746
1798
|
return {
|
|
1747
1799
|
type: 'activity',
|
|
@@ -1753,44 +1805,19 @@ function normalizeItem(item) {
|
|
|
1753
1805
|
};
|
|
1754
1806
|
}
|
|
1755
1807
|
|
|
1756
|
-
function
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
const addText = value => {
|
|
1762
|
-
const normalized = typeof value === 'string' ? value.trim() : '';
|
|
1763
|
-
if (normalized && !seen.has(normalized)) { seen.add(normalized); text.push(normalized); }
|
|
1764
|
-
};
|
|
1765
|
-
const visit = (value, depth = 0) => {
|
|
1766
|
-
if (value == null || depth > 4) return;
|
|
1767
|
-
if (typeof value === 'string') { addText(value); return; }
|
|
1768
|
-
if (Array.isArray(value)) { for (const item of value) visit(item, depth + 1); return; }
|
|
1769
|
-
if (typeof value !== 'object') return;
|
|
1770
|
-
if (value.type === 'image' || value.type === 'audio') { mediaCount += 1; return; }
|
|
1771
|
-
if (value.type === 'resource_link') {
|
|
1772
|
-
resources.push({ name: value.title || value.name || '资源', uri: value.uri || '', description: value.description || '' });
|
|
1773
|
-
return;
|
|
1774
|
-
}
|
|
1775
|
-
if (value.type === 'resource' && value.resource) {
|
|
1776
|
-
const resource = value.resource;
|
|
1777
|
-
if (typeof resource.text === 'string') addText(resource.text);
|
|
1778
|
-
else if (resource.uri) resources.push({ name: resource.title || resource.name || '资源', uri: resource.uri, description: resource.description || '' });
|
|
1779
|
-
return;
|
|
1780
|
-
}
|
|
1781
|
-
if (typeof value.text === 'string') addText(value.text);
|
|
1782
|
-
if (typeof value.message === 'string') addText(value.message);
|
|
1783
|
-
if (value.content) visit(value.content, depth + 1);
|
|
1784
|
-
if (value.raw) visit(value.raw, depth + 1);
|
|
1808
|
+
function sharedHistoryPageOptions() {
|
|
1809
|
+
return {
|
|
1810
|
+
maxTurns: SHARED_HISTORY_PAGE_TURNS,
|
|
1811
|
+
maxBytes: SHARED_HISTORY_PAGE_BYTES,
|
|
1812
|
+
measure: value => Buffer.byteLength(JSON.stringify(normalizeSharedTimeline(value))),
|
|
1785
1813
|
};
|
|
1786
|
-
visit(result);
|
|
1787
|
-
const resultError = result?.type === 'error' ? result.error : null;
|
|
1788
|
-
const errorValue = fallbackError || resultError;
|
|
1789
|
-
const error = typeof errorValue === 'string' ? errorValue : errorValue?.message || errorValue?.text || '';
|
|
1790
|
-
return { text: text.join('\n\n'), error, resources, mediaCount };
|
|
1791
1814
|
}
|
|
1792
|
-
|
|
1793
|
-
function
|
|
1815
|
+
|
|
1816
|
+
function threadRecency(thread) {
|
|
1817
|
+
return Number(thread?.recencyAt ?? thread?.updatedAt) || 0;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
function normalizeAttachments(content) {
|
|
1794
1821
|
if (!Array.isArray(content)) return [];
|
|
1795
1822
|
const items = content.filter(item => item && !['text', 'input_text'].includes(item.type)).map(item => ({ ...item }));
|
|
1796
1823
|
const text = content.filter(item => ['text', 'input_text'].includes(item?.type)).map(item => item.text || '').join('\n');
|
|
@@ -1833,18 +1860,19 @@ function markdownImageAssets(text) {
|
|
|
1833
1860
|
return assets;
|
|
1834
1861
|
}
|
|
1835
1862
|
|
|
1836
|
-
function markdownFileAssets(text) {
|
|
1863
|
+
function markdownFileAssets(text) {
|
|
1837
1864
|
const assets = Object.create(null);
|
|
1838
1865
|
function visit(tokens) {
|
|
1839
1866
|
for (const token of tokens) {
|
|
1840
1867
|
if (token.type === 'link_open') {
|
|
1841
1868
|
const value = token.attrGet('href');
|
|
1842
1869
|
if (value && !Object.hasOwn(assets, value)) {
|
|
1843
|
-
let localPath;
|
|
1844
|
-
try { localPath = decodeURIComponent(value); } catch { localPath = ''; }
|
|
1845
|
-
if (/^\/?[a-z]:[\\/]/i.test(localPath)) {
|
|
1846
|
-
localPath = localPath.replace(/^\/(?=[a-z]:)/i, '');
|
|
1847
|
-
|
|
1870
|
+
let localPath;
|
|
1871
|
+
try { localPath = decodeURIComponent(value); } catch { localPath = ''; }
|
|
1872
|
+
if (/^\/?[a-z]:[\\/]/i.test(localPath)) {
|
|
1873
|
+
localPath = localPath.replace(/^\/(?=[a-z]:)/i, '');
|
|
1874
|
+
localPath = localPath.replace(/:(\d+)(?::\d+)?$/, '');
|
|
1875
|
+
const asset = rememberLocalFile(localPath);
|
|
1848
1876
|
if (asset) assets[value] = { remotePath: asset.id, name: asset.name, size: asset.size };
|
|
1849
1877
|
}
|
|
1850
1878
|
}
|
|
@@ -1914,18 +1942,23 @@ function normalizeAttachment(item) {
|
|
|
1914
1942
|
name,
|
|
1915
1943
|
mimeType: cached?.mimeType || item.mimeType || '',
|
|
1916
1944
|
available: Boolean(cached),
|
|
1917
|
-
remotePath: cached ? cached.id : '',
|
|
1945
|
+
remotePath: cached ? cached.id : '',
|
|
1946
|
+
recoveryToken: cached?.recoveryToken || '',
|
|
1918
1947
|
url: '',
|
|
1919
1948
|
thumbnailUrl: '',
|
|
1920
1949
|
};
|
|
1921
1950
|
}
|
|
1922
1951
|
|
|
1923
|
-
function cacheLocalImage(filePath, name) {
|
|
1924
|
-
try {
|
|
1925
|
-
const
|
|
1926
|
-
const
|
|
1927
|
-
|
|
1928
|
-
|
|
1952
|
+
function cacheLocalImage(filePath, name) {
|
|
1953
|
+
try {
|
|
1954
|
+
const resolved = path.resolve(filePath);
|
|
1955
|
+
const stat = fs.statSync(resolved);
|
|
1956
|
+
const mimeType = imageMimeType(resolved);
|
|
1957
|
+
if (!stat.isFile() || !mimeType || stat.size > MAX_ATTACHMENT_BYTES) return null;
|
|
1958
|
+
const cached = rememberAttachment(fs.readFileSync(resolved), mimeType, name,
|
|
1959
|
+
localImageRecoveryToken(resolved, stat));
|
|
1960
|
+
void attachmentThumbnail(cached).catch(() => {});
|
|
1961
|
+
return cached;
|
|
1929
1962
|
} catch {
|
|
1930
1963
|
return null;
|
|
1931
1964
|
}
|
|
@@ -1943,16 +1976,65 @@ function cacheDataImage(dataUrl, name) {
|
|
|
1943
1976
|
}
|
|
1944
1977
|
}
|
|
1945
1978
|
|
|
1946
|
-
function rememberAttachment(buffer, mimeType, name) {
|
|
1947
|
-
const id = crypto.createHash('sha256').update(buffer).digest('hex');
|
|
1948
|
-
if (!attachmentCache.has(id)) {
|
|
1979
|
+
function rememberAttachment(buffer, mimeType, name, recoveryToken = '') {
|
|
1980
|
+
const id = crypto.createHash('sha256').update(buffer).digest('hex');
|
|
1981
|
+
if (!attachmentCache.has(id)) {
|
|
1949
1982
|
while (attachmentCache.size >= MAX_CACHED_ATTACHMENTS) {
|
|
1950
1983
|
attachmentCache.delete(attachmentCache.keys().next().value);
|
|
1951
|
-
}
|
|
1952
|
-
attachmentCache.set(id, { id, buffer, mimeType, name });
|
|
1953
|
-
}
|
|
1954
|
-
|
|
1955
|
-
}
|
|
1984
|
+
}
|
|
1985
|
+
attachmentCache.set(id, { id, buffer, mimeType, name, recoveryToken });
|
|
1986
|
+
} else if (recoveryToken && !attachmentCache.get(id).recoveryToken) {
|
|
1987
|
+
attachmentCache.get(id).recoveryToken = recoveryToken;
|
|
1988
|
+
}
|
|
1989
|
+
return attachmentCache.get(id);
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
function localImageRecoveryToken(filePath, stat) {
|
|
1993
|
+
const iv = crypto.randomBytes(12);
|
|
1994
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', imageCapabilityKey, iv);
|
|
1995
|
+
const payload = Buffer.from(JSON.stringify([filePath, stat.size, stat.mtimeMs]));
|
|
1996
|
+
const encrypted = Buffer.concat([cipher.update(payload), cipher.final()]);
|
|
1997
|
+
return Buffer.concat([iv, cipher.getAuthTag(), encrypted]).toString('base64url');
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
function recoverLocalImage(id, token) {
|
|
2001
|
+
if (!token || token.length > 2048) return null;
|
|
2002
|
+
try {
|
|
2003
|
+
const value = Buffer.from(token, 'base64url');
|
|
2004
|
+
if (value.length < 29) return null;
|
|
2005
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', imageCapabilityKey, value.subarray(0, 12));
|
|
2006
|
+
decipher.setAuthTag(value.subarray(12, 28));
|
|
2007
|
+
const [filePath, size, mtimeMs] = JSON.parse(Buffer.concat([
|
|
2008
|
+
decipher.update(value.subarray(28)), decipher.final(),
|
|
2009
|
+
]).toString('utf8'));
|
|
2010
|
+
if (typeof filePath !== 'string' || !path.isAbsolute(filePath)
|
|
2011
|
+
|| !Number.isSafeInteger(size) || size <= 0 || size > MAX_ATTACHMENT_BYTES
|
|
2012
|
+
|| typeof mtimeMs !== 'number' || !Number.isFinite(mtimeMs)) return null;
|
|
2013
|
+
const stat = fs.statSync(filePath);
|
|
2014
|
+
const mimeType = imageMimeType(filePath);
|
|
2015
|
+
if (!stat.isFile() || stat.size !== size || stat.mtimeMs !== mtimeMs || !mimeType) return null;
|
|
2016
|
+
const buffer = fs.readFileSync(filePath);
|
|
2017
|
+
if (crypto.createHash('sha256').update(buffer).digest('hex') !== id) return null;
|
|
2018
|
+
return rememberAttachment(buffer, mimeType, path.basename(filePath), token);
|
|
2019
|
+
} catch {
|
|
2020
|
+
return null;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
async function attachmentThumbnail(image) {
|
|
2025
|
+
if (!image.thumbnail) {
|
|
2026
|
+
image.thumbnail = sharp(image.buffer, { limitInputPixels: 40_000_000 }).rotate()
|
|
2027
|
+
.resize({ width: 640, height: 640, fit: 'inside', withoutEnlargement: true })
|
|
2028
|
+
.webp({ quality: 78, effort: 4 })
|
|
2029
|
+
.toBuffer()
|
|
2030
|
+
.then(buffer => ({ buffer, mimeType: 'image/webp' }))
|
|
2031
|
+
.catch(error => {
|
|
2032
|
+
image.thumbnail = null;
|
|
2033
|
+
throw error;
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
2036
|
+
return image.thumbnail;
|
|
2037
|
+
}
|
|
1956
2038
|
|
|
1957
2039
|
function imageMimeType(filePath) {
|
|
1958
2040
|
switch (path.extname(filePath).toLowerCase()) {
|
|
@@ -1999,18 +2081,18 @@ function extractDelegatedInput(value) {
|
|
|
1999
2081
|
.trim();
|
|
2000
2082
|
}
|
|
2001
2083
|
|
|
2002
|
-
function delegatedOutputText(output) {
|
|
2084
|
+
function delegatedOutputText(output) {
|
|
2003
2085
|
if (typeof output === 'string') return output;
|
|
2004
2086
|
return output && typeof output.text === 'string' ? output.text : '';
|
|
2005
|
-
}
|
|
2006
|
-
|
|
2007
|
-
function summaryText(value) {
|
|
2008
|
-
if (typeof value === 'string') return value;
|
|
2009
|
-
if (value && typeof value === 'object') return value.text || value.summary || JSON.stringify(value);
|
|
2010
|
-
return '';
|
|
2011
|
-
}
|
|
2012
|
-
|
|
2013
|
-
function statusText(value) {
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
function summaryText(value) {
|
|
2090
|
+
if (typeof value === 'string') return value;
|
|
2091
|
+
if (value && typeof value === 'object') return value.text || value.summary || JSON.stringify(value);
|
|
2092
|
+
return '';
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
function statusText(value) {
|
|
2014
2096
|
if (typeof value === 'string') return value;
|
|
2015
2097
|
if (value && typeof value === 'object') return value.type || value.status || 'unknown';
|
|
2016
2098
|
return value == null ? 'unknown' : String(value);
|