@xfxstudio/claworld 2026.7.7-testing.1 → 2026.7.9-testing.1
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 +12 -1
- package/index.js +0 -1
- package/openclaw.plugin.json +3 -2
- package/package.json +3 -2
- package/skills/claworld-help/SKILL.md +10 -68
- package/skills/claworld-main-session/SKILL.md +16 -2
- package/skills/claworld-manage-worlds/SKILL.md +20 -11
- package/skills/claworld-management-session/SKILL.md +104 -21
- package/src/openclaw/index.js +2 -2
- package/src/openclaw/plugin/claworld-channel-plugin.js +49 -3
- package/src/openclaw/plugin/register-tooling.js +99 -37
- package/src/openclaw/plugin/register.js +325 -14
- package/src/openclaw/runtime/feedback-helper.js +6 -2
- package/src/openclaw/runtime/tool-contracts.js +1 -0
- package/src/openclaw/runtime/tool-inventory.js +4 -0
- package/src/openclaw/runtime/transcript-report.js +1309 -0
- package/src/openclaw/runtime/working-memory.js +26 -4
- package/src/openclaw/runtime/world-membership-helper.js +157 -0
- package/src/product-shell/contracts/search-item.js +2 -0
- package/src/openclaw/runtime/system-message-orchestrator.js +0 -1
|
@@ -0,0 +1,1309 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { deflateSync } from 'node:zlib';
|
|
7
|
+
import { createRuntimeBoundaryError } from '../../lib/runtime-errors.js';
|
|
8
|
+
import {
|
|
9
|
+
appendClaworldJournalEvent,
|
|
10
|
+
CLAWORLD_REPORTS_DIR,
|
|
11
|
+
CLAWORLD_WORKING_MEMORY_DIR,
|
|
12
|
+
ensureClaworldWorkingMemory,
|
|
13
|
+
readClaworldSessionDirectory,
|
|
14
|
+
} from './working-memory.js';
|
|
15
|
+
|
|
16
|
+
const execFileAsync = promisify(execFile);
|
|
17
|
+
|
|
18
|
+
export const CLAWORLD_TRANSCRIPT_REPORT_TOOL_NAME = 'claworld_render_transcript_report';
|
|
19
|
+
export const CLAWORLD_TRANSCRIPT_REPORT_STYLE = 'claworld-comic-grid';
|
|
20
|
+
export const CLAWORLD_TRANSCRIPT_REPORT_STYLES = Object.freeze([
|
|
21
|
+
CLAWORLD_TRANSCRIPT_REPORT_STYLE,
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const DEFAULT_WIDTH = 720;
|
|
25
|
+
const DEFAULT_MAX_PAGE_HEIGHT = 2600;
|
|
26
|
+
const MIN_PAGE_HEIGHT = 900;
|
|
27
|
+
const MAX_PAGE_HEIGHT = 8000;
|
|
28
|
+
const TOP_LEVEL_RENDER_FIELDS = new Set(['mode', 'stored', 'manual', 'style', 'maxPageHeight']);
|
|
29
|
+
const MANUAL_RENDER_FIELDS = new Set(['messages', 'title', 'peerProfile', 'localLabel', 'peerLabel']);
|
|
30
|
+
const MANUAL_MESSAGE_FIELDS = new Set(['from', 'text', 'createdAt']);
|
|
31
|
+
const TIME_SPLIT_SECONDS = 5 * 60;
|
|
32
|
+
const SEGMENT_GAP_SECONDS = 4 * 60 * 60;
|
|
33
|
+
const ARTIFACT_SCHEMA = 'claworld.transcript_report.v1';
|
|
34
|
+
|
|
35
|
+
function isObject(value) {
|
|
36
|
+
return value && typeof value === 'object' && !Array.isArray(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeText(value, fallback = null) {
|
|
40
|
+
if (value == null) return fallback;
|
|
41
|
+
const normalized = String(value).trim();
|
|
42
|
+
return normalized || fallback;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function inputError(field, message) {
|
|
46
|
+
throw createRuntimeBoundaryError({
|
|
47
|
+
code: 'tool_input_invalid',
|
|
48
|
+
category: 'input',
|
|
49
|
+
status: 400,
|
|
50
|
+
message,
|
|
51
|
+
publicMessage: message,
|
|
52
|
+
recoverable: true,
|
|
53
|
+
context: { field },
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function assertObject(value, field) {
|
|
58
|
+
if (!isObject(value)) inputError(field, `${field} must be an object`);
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function assertNoUnknownFields(value, allowedFields, field) {
|
|
63
|
+
const source = assertObject(value, field);
|
|
64
|
+
for (const key of Object.keys(source)) {
|
|
65
|
+
if (!allowedFields.has(key)) inputError(`${field}.${key}`, `${field}.${key} is not supported`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function validateRenderArgs(args = {}) {
|
|
70
|
+
const params = assertObject(args, 'parameters');
|
|
71
|
+
assertNoUnknownFields(params, TOP_LEVEL_RENDER_FIELDS, 'parameters');
|
|
72
|
+
const mode = normalizeText(params.mode, null);
|
|
73
|
+
if (!mode) inputError('mode', 'mode is required');
|
|
74
|
+
if (!['stored', 'manual'].includes(mode)) inputError('mode', 'mode must be stored or manual');
|
|
75
|
+
const style = normalizeText(params.style, CLAWORLD_TRANSCRIPT_REPORT_STYLE);
|
|
76
|
+
if (!CLAWORLD_TRANSCRIPT_REPORT_STYLES.includes(style)) {
|
|
77
|
+
inputError('style', `style must be ${CLAWORLD_TRANSCRIPT_REPORT_STYLE}`);
|
|
78
|
+
}
|
|
79
|
+
const maxPageHeight = Number.isInteger(params.maxPageHeight)
|
|
80
|
+
? params.maxPageHeight
|
|
81
|
+
: DEFAULT_MAX_PAGE_HEIGHT;
|
|
82
|
+
if (maxPageHeight < MIN_PAGE_HEIGHT || maxPageHeight > MAX_PAGE_HEIGHT) {
|
|
83
|
+
inputError('maxPageHeight', `maxPageHeight must be between ${MIN_PAGE_HEIGHT} and ${MAX_PAGE_HEIGHT}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (mode === 'stored') {
|
|
87
|
+
const stored = assertObject(params.stored, 'stored');
|
|
88
|
+
assertNoUnknownFields(stored, new Set(['chatRequestId']), 'stored');
|
|
89
|
+
const chatRequestId = normalizeText(stored.chatRequestId, null);
|
|
90
|
+
if (!chatRequestId) inputError('stored.chatRequestId', 'stored.chatRequestId is required');
|
|
91
|
+
return { mode, style, maxPageHeight, stored: { chatRequestId } };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const manual = assertObject(params.manual, 'manual');
|
|
95
|
+
assertNoUnknownFields(manual, MANUAL_RENDER_FIELDS, 'manual');
|
|
96
|
+
for (const field of MANUAL_RENDER_FIELDS) {
|
|
97
|
+
if (!Object.prototype.hasOwnProperty.call(manual, field)) {
|
|
98
|
+
inputError(`manual.${field}`, `manual.${field} is required`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (!Array.isArray(manual.messages) || manual.messages.length === 0) {
|
|
102
|
+
inputError('manual.messages', 'manual.messages must contain at least one message');
|
|
103
|
+
}
|
|
104
|
+
const messages = manual.messages.map((message, index) => {
|
|
105
|
+
if (!isObject(message)) inputError(`manual.messages.${index}`, 'manual message must be an object');
|
|
106
|
+
assertNoUnknownFields(message, MANUAL_MESSAGE_FIELDS, `manual.messages.${index}`);
|
|
107
|
+
const from = normalizeText(message.from, null);
|
|
108
|
+
if (!['peer', 'local'].includes(from)) {
|
|
109
|
+
inputError(`manual.messages.${index}.from`, 'manual message from must be peer or local');
|
|
110
|
+
}
|
|
111
|
+
const text = normalizeText(message.text, null);
|
|
112
|
+
if (!text) inputError(`manual.messages.${index}.text`, 'manual message text is required');
|
|
113
|
+
const createdAt = normalizeText(message.createdAt, null);
|
|
114
|
+
if (!createdAt) inputError(`manual.messages.${index}.createdAt`, 'manual message createdAt is required');
|
|
115
|
+
if (!parseTimestampMs(createdAt)) {
|
|
116
|
+
inputError(`manual.messages.${index}.createdAt`, 'manual message createdAt must be a parseable timestamp');
|
|
117
|
+
}
|
|
118
|
+
return { from, text, createdAt };
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
mode,
|
|
122
|
+
style,
|
|
123
|
+
maxPageHeight,
|
|
124
|
+
manual: {
|
|
125
|
+
messages,
|
|
126
|
+
title: normalizeText(manual.title, null) || inputError('manual.title', 'manual.title is required'),
|
|
127
|
+
peerProfile: normalizeText(manual.peerProfile, null) || inputError('manual.peerProfile', 'manual.peerProfile is required'),
|
|
128
|
+
localLabel: normalizeText(manual.localLabel, null) || inputError('manual.localLabel', 'manual.localLabel is required'),
|
|
129
|
+
peerLabel: normalizeText(manual.peerLabel, null) || inputError('manual.peerLabel', 'manual.peerLabel is required'),
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function resolveWorkspaceRoot(workspaceRoot = null) {
|
|
135
|
+
return path.resolve(normalizeText(workspaceRoot, null) || process.cwd());
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function stableJson(value) {
|
|
139
|
+
return JSON.stringify(value, Object.keys(value || {}).sort());
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function sha256Buffer(buffer) {
|
|
143
|
+
return crypto.createHash('sha256').update(buffer).digest('hex');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function hashFile(filePath) {
|
|
147
|
+
return sha256Buffer(await fs.readFile(filePath));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function safeFileStem(value) {
|
|
151
|
+
const text = normalizeText(value, 'report') || 'report';
|
|
152
|
+
return text.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80) || 'report';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function atomicWriteText(filePath, text) {
|
|
156
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
157
|
+
const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
158
|
+
await fs.writeFile(tempPath, text, 'utf8');
|
|
159
|
+
await fs.rename(tempPath, filePath);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function atomicWriteBuffer(filePath, buffer) {
|
|
163
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
164
|
+
const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
165
|
+
await fs.writeFile(tempPath, buffer);
|
|
166
|
+
await fs.rename(tempPath, filePath);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function resolveArtifactDirs(workspaceRoot) {
|
|
170
|
+
const baseDir = path.join(
|
|
171
|
+
workspaceRoot,
|
|
172
|
+
CLAWORLD_WORKING_MEMORY_DIR,
|
|
173
|
+
CLAWORLD_REPORTS_DIR,
|
|
174
|
+
'transcripts',
|
|
175
|
+
);
|
|
176
|
+
return {
|
|
177
|
+
baseDir,
|
|
178
|
+
imageDir: path.join(baseDir, 'images'),
|
|
179
|
+
documentDir: path.join(baseDir, 'documents'),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function parseTimestampMs(value) {
|
|
184
|
+
if (value == null || value === '') return null;
|
|
185
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
186
|
+
return value > 1_000_000_000_000 ? Math.trunc(value) : Math.trunc(value * 1000);
|
|
187
|
+
}
|
|
188
|
+
const text = String(value).trim();
|
|
189
|
+
if (!text) return null;
|
|
190
|
+
if (/^-?\d+(\.\d+)?$/.test(text)) return parseTimestampMs(Number(text));
|
|
191
|
+
const parsed = Date.parse(text);
|
|
192
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function toIsoTimestamp(value) {
|
|
196
|
+
const ms = parseTimestampMs(value);
|
|
197
|
+
if (!ms) return null;
|
|
198
|
+
return new Date(ms).toISOString();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function formatTimestampLabel(value) {
|
|
202
|
+
const ms = parseTimestampMs(value);
|
|
203
|
+
if (!ms) return '';
|
|
204
|
+
const date = new Date(ms);
|
|
205
|
+
const pad = (part) => String(part).padStart(2, '0');
|
|
206
|
+
return [
|
|
207
|
+
`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`,
|
|
208
|
+
`${pad(date.getHours())}:${pad(date.getMinutes())}`,
|
|
209
|
+
].join(' ');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function flattenContent(value) {
|
|
213
|
+
if (value == null) return '';
|
|
214
|
+
if (typeof value === 'string') return value;
|
|
215
|
+
if (Array.isArray(value)) {
|
|
216
|
+
return value.map(flattenContent).filter(Boolean).join('\n');
|
|
217
|
+
}
|
|
218
|
+
if (isObject(value)) {
|
|
219
|
+
for (const key of ['text', 'input_text', 'output_text', 'content', 'body', 'message']) {
|
|
220
|
+
if (value[key] != null) {
|
|
221
|
+
const text = flattenContent(value[key]);
|
|
222
|
+
if (text) return text;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return '';
|
|
226
|
+
}
|
|
227
|
+
return String(value);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function extractFencedBlockAfter(text, marker) {
|
|
231
|
+
const source = String(text || '');
|
|
232
|
+
const index = source.indexOf(marker);
|
|
233
|
+
if (index < 0) return null;
|
|
234
|
+
const afterMarker = source.slice(index + marker.length).trimStart();
|
|
235
|
+
const fenceMatch = afterMarker.match(/^```[^\n]*\n([\s\S]*?)\n```/);
|
|
236
|
+
if (fenceMatch) return fenceMatch[1].trim();
|
|
237
|
+
const nextSection = afterMarker.search(/\n(?:Routing metadata|Backend-authored Claworld|Relay untrusted context|Claworld live conversation rules):/);
|
|
238
|
+
return (nextSection >= 0 ? afterMarker.slice(0, nextSection) : afterMarker).trim();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function stripInternalEnvelopeSections(text) {
|
|
242
|
+
let output = String(text || '');
|
|
243
|
+
const markers = [
|
|
244
|
+
'Routing metadata:',
|
|
245
|
+
'Backend-authored Claworld context:',
|
|
246
|
+
'Backend-authored Claworld command:',
|
|
247
|
+
'Relay untrusted context:',
|
|
248
|
+
'Claworld live conversation rules:',
|
|
249
|
+
];
|
|
250
|
+
for (const marker of markers) {
|
|
251
|
+
const index = output.indexOf(marker);
|
|
252
|
+
if (index >= 0) output = output.slice(0, index);
|
|
253
|
+
}
|
|
254
|
+
output = output.replace(/```[a-zA-Z0-9_-]*\n([\s\S]*?)\n```/g, '$1');
|
|
255
|
+
return output.trim();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function stripOpenClawSpeakerPrefix(text) {
|
|
259
|
+
const source = String(text || '').trim();
|
|
260
|
+
const match = source.match(/^([^\n:]{1,80}):\s+([\s\S]+)$/);
|
|
261
|
+
if (!match) return { text: source, speaker: null };
|
|
262
|
+
const label = match[1].trim();
|
|
263
|
+
if (/^(https?|file|data)$/i.test(label)) return { text: source, speaker: null };
|
|
264
|
+
return { text: match[2].trim(), speaker: label };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function redactSensitiveText(text) {
|
|
268
|
+
let output = String(text || '');
|
|
269
|
+
output = output.replace(
|
|
270
|
+
/\bauthorization\b\s*[:=]\s*(?:bearer\s+)?`?[^`\s,;]+`?/gi,
|
|
271
|
+
'authorization=[REDACTED]',
|
|
272
|
+
);
|
|
273
|
+
output = output.replace(
|
|
274
|
+
/\b(api[_-]?key|app[_-]?token|access[_-]?token|refresh[_-]?token|secret|password)\b\s*[:=]\s*`?[^`\s,;]+`?/gi,
|
|
275
|
+
'$1=[REDACTED]',
|
|
276
|
+
);
|
|
277
|
+
output = output.replace(/\bbearer\s+[a-zA-Z0-9._~+/=-]{12,}/gi, 'bearer [REDACTED]');
|
|
278
|
+
output = output.replace(/\bsk-[a-zA-Z0-9_-]{12,}\b/g, '[REDACTED_TOKEN]');
|
|
279
|
+
output = output.replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, '[REDACTED_EMAIL]');
|
|
280
|
+
output = output.replace(/\+?\d[\d ()-]{8,}\d/g, '[REDACTED_PHONE]');
|
|
281
|
+
return output.trim();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function normalizeTagLabel(value) {
|
|
285
|
+
const tag = normalizeText(value, null);
|
|
286
|
+
if (!tag) return null;
|
|
287
|
+
const compact = tag.toLowerCase().replace(/\s+/g, '_');
|
|
288
|
+
if (compact === 'request_conversation_end' || compact === 'request_end' || compact === 'end') {
|
|
289
|
+
return 'request end';
|
|
290
|
+
}
|
|
291
|
+
if (compact === 'like') return 'like';
|
|
292
|
+
if (compact === 'dislike') return 'dislike';
|
|
293
|
+
return tag.replace(/_/g, ' ').trim().toLowerCase();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function extractControlTags(text) {
|
|
297
|
+
const tags = [];
|
|
298
|
+
const cleaned = String(text || '').replace(/\[\[([a-zA-Z0-9_\-\s]+)\]\]|\[([a-zA-Z0-9_\-\s]+)\]/g, (match, doubleTag, singleTag) => {
|
|
299
|
+
const label = normalizeTagLabel(doubleTag || singleTag);
|
|
300
|
+
if (label) tags.push(label);
|
|
301
|
+
return '';
|
|
302
|
+
});
|
|
303
|
+
return {
|
|
304
|
+
text: cleaned.replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim(),
|
|
305
|
+
tags: [...new Set(tags)],
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function extractEpisodeIds(...values) {
|
|
310
|
+
const ids = new Set();
|
|
311
|
+
const source = values.map((value) => String(value || '')).join('\n');
|
|
312
|
+
const patterns = [
|
|
313
|
+
/(?:Chat Request ID|chatRequestId|requestId|Intent ID|intentId)\s*[:=]\s*`?([a-zA-Z0-9._:-]+)/gi,
|
|
314
|
+
/\b(?:chatRequestId|requestId|intentId)\b["']?\s*[:=]\s*["']?([a-zA-Z0-9._:-]+)/gi,
|
|
315
|
+
];
|
|
316
|
+
for (const pattern of patterns) {
|
|
317
|
+
for (const match of source.matchAll(pattern)) {
|
|
318
|
+
const id = normalizeText(match[1], null);
|
|
319
|
+
if (id) ids.add(id.replace(/[`,.;)\]}]+$/g, ''));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return [...ids];
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function getMessageText(record) {
|
|
326
|
+
if (record?.text != null) return flattenContent(record.text);
|
|
327
|
+
if (record?.body != null) return flattenContent(record.body);
|
|
328
|
+
if (record?.content != null) return flattenContent(record.content);
|
|
329
|
+
if (record?.message?.content != null) return flattenContent(record.message.content);
|
|
330
|
+
if (record?.payload?.content != null) return flattenContent(record.payload.content);
|
|
331
|
+
return '';
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function getMessageRole(record) {
|
|
335
|
+
return normalizeText(
|
|
336
|
+
record?.role
|
|
337
|
+
?? record?.from
|
|
338
|
+
?? record?.message?.role
|
|
339
|
+
?? record?.payload?.role,
|
|
340
|
+
null,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function getMessageTimestamp(record) {
|
|
345
|
+
return record?.createdAt
|
|
346
|
+
?? record?.created_at
|
|
347
|
+
?? record?.timestamp
|
|
348
|
+
?? record?.message?.createdAt
|
|
349
|
+
?? record?.message?.timestamp
|
|
350
|
+
?? record?.payload?.createdAt
|
|
351
|
+
?? record?.payload?.timestamp
|
|
352
|
+
?? record?.__openclaw?.recordTimestampMs
|
|
353
|
+
?? record?.__openclaw?.recordTimestamp
|
|
354
|
+
?? null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function normalizeManualMessage(raw, index) {
|
|
358
|
+
const tagsResult = extractControlTags(raw.text);
|
|
359
|
+
return {
|
|
360
|
+
id: `manual-${index + 1}`,
|
|
361
|
+
side: raw.from === 'peer' ? 'peer' : 'local',
|
|
362
|
+
speaker: null,
|
|
363
|
+
text: redactSensitiveText(tagsResult.text),
|
|
364
|
+
tags: tagsResult.tags,
|
|
365
|
+
createdAt: toIsoTimestamp(raw.createdAt),
|
|
366
|
+
createdAtLabel: formatTimestampLabel(raw.createdAt),
|
|
367
|
+
timestampMs: parseTimestampMs(raw.createdAt),
|
|
368
|
+
episodeIds: [],
|
|
369
|
+
sourceRole: raw.from,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function normalizeStoredMessage(record, index) {
|
|
374
|
+
const role = getMessageRole(record);
|
|
375
|
+
if (['system', 'developer', 'tool', 'tool_call'].includes(role)) return null;
|
|
376
|
+
|
|
377
|
+
const side = role === 'assistant' || role === 'local' || role === 'me' || role === 'right'
|
|
378
|
+
? 'local'
|
|
379
|
+
: 'peer';
|
|
380
|
+
let text = getMessageText(record);
|
|
381
|
+
const rawText = text;
|
|
382
|
+
const episodeIds = extractEpisodeIds(
|
|
383
|
+
rawText,
|
|
384
|
+
record?.id,
|
|
385
|
+
record?.message?.id,
|
|
386
|
+
record?.__openclaw?.id,
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
if (side === 'peer') {
|
|
390
|
+
const peerVisible = extractFencedBlockAfter(text, 'Peer-visible Claworld message:');
|
|
391
|
+
const hasBackendCommand = text.includes('Backend-authored Claworld command:');
|
|
392
|
+
if (peerVisible != null) {
|
|
393
|
+
text = peerVisible;
|
|
394
|
+
} else if (hasBackendCommand) {
|
|
395
|
+
return null;
|
|
396
|
+
} else {
|
|
397
|
+
text = stripInternalEnvelopeSections(text);
|
|
398
|
+
}
|
|
399
|
+
} else {
|
|
400
|
+
text = stripInternalEnvelopeSections(text);
|
|
401
|
+
if (/^NO_REPLY$/i.test(text.trim())) return null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const speakerResult = side === 'peer' ? stripOpenClawSpeakerPrefix(text) : { text: text.trim(), speaker: null };
|
|
405
|
+
const tagsResult = extractControlTags(speakerResult.text);
|
|
406
|
+
const redactedText = redactSensitiveText(tagsResult.text);
|
|
407
|
+
if (!redactedText && tagsResult.tags.length === 0) return null;
|
|
408
|
+
const timestamp = getMessageTimestamp(record) || new Date(0).toISOString();
|
|
409
|
+
return {
|
|
410
|
+
id: normalizeText(record?.id ?? record?.message?.id ?? record?.__openclaw?.id, `message-${index + 1}`),
|
|
411
|
+
side,
|
|
412
|
+
speaker: speakerResult.speaker,
|
|
413
|
+
text: redactedText,
|
|
414
|
+
tags: tagsResult.tags,
|
|
415
|
+
createdAt: toIsoTimestamp(timestamp),
|
|
416
|
+
createdAtLabel: formatTimestampLabel(timestamp),
|
|
417
|
+
timestampMs: parseTimestampMs(timestamp) || 0,
|
|
418
|
+
episodeIds,
|
|
419
|
+
sourceRole: role,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function normalizeTranscriptMessages(records, { mode } = {}) {
|
|
424
|
+
const messages = records
|
|
425
|
+
.map((record, index) => (
|
|
426
|
+
mode === 'manual'
|
|
427
|
+
? normalizeManualMessage(record, index)
|
|
428
|
+
: normalizeStoredMessage(record, index)
|
|
429
|
+
))
|
|
430
|
+
.filter(Boolean)
|
|
431
|
+
.filter((message) => message.text || message.tags.length > 0)
|
|
432
|
+
.sort((left, right) => (left.timestampMs || 0) - (right.timestampMs || 0));
|
|
433
|
+
return messages;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function episodeSetsIntersect(left = [], right = []) {
|
|
437
|
+
if (!left.length || !right.length) return true;
|
|
438
|
+
const rightSet = new Set(right);
|
|
439
|
+
return left.some((id) => rightSet.has(id));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function segmentTranscriptMessages(messages = []) {
|
|
443
|
+
const segments = [];
|
|
444
|
+
let current = null;
|
|
445
|
+
for (const message of messages) {
|
|
446
|
+
const previous = current?.messages?.[current.messages.length - 1] || null;
|
|
447
|
+
const gapSeconds = previous && message.timestampMs && previous.timestampMs
|
|
448
|
+
? Math.abs(message.timestampMs - previous.timestampMs) / 1000
|
|
449
|
+
: 0;
|
|
450
|
+
const currentEpisodeIds = current ? [...current.episodeIds] : [];
|
|
451
|
+
const startsNewEpisode = current
|
|
452
|
+
&& message.episodeIds.length > 0
|
|
453
|
+
&& currentEpisodeIds.length > 0
|
|
454
|
+
&& !episodeSetsIntersect(message.episodeIds, currentEpisodeIds);
|
|
455
|
+
const startsAfterCompletion = previous
|
|
456
|
+
&& current?.endedSides?.size >= 2
|
|
457
|
+
&& message.side === 'peer';
|
|
458
|
+
if (!current || gapSeconds > SEGMENT_GAP_SECONDS || startsNewEpisode || startsAfterCompletion) {
|
|
459
|
+
current = {
|
|
460
|
+
messages: [],
|
|
461
|
+
episodeIds: new Set(),
|
|
462
|
+
endedSides: new Set(),
|
|
463
|
+
};
|
|
464
|
+
segments.push(current);
|
|
465
|
+
}
|
|
466
|
+
current.messages.push(message);
|
|
467
|
+
for (const id of message.episodeIds) current.episodeIds.add(id);
|
|
468
|
+
if (message.tags.includes('request end')) current.endedSides.add(message.side);
|
|
469
|
+
}
|
|
470
|
+
return segments;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function selectStoredMessages(messages, chatRequestId) {
|
|
474
|
+
const targetId = normalizeText(chatRequestId, null);
|
|
475
|
+
if (!targetId) return messages;
|
|
476
|
+
const segments = segmentTranscriptMessages(messages);
|
|
477
|
+
const segment = segments.find((candidate) => candidate.episodeIds.has(targetId));
|
|
478
|
+
if (segment) return segment.messages;
|
|
479
|
+
const tagged = messages.filter((message) => message.episodeIds.includes(targetId));
|
|
480
|
+
return tagged.length > 0 ? tagged : messages;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function parseJsonLine(line) {
|
|
484
|
+
try {
|
|
485
|
+
const parsed = JSON.parse(line);
|
|
486
|
+
return isObject(parsed) ? parsed : null;
|
|
487
|
+
} catch {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function extractTranscriptRecordsFromJson(value, fallbackId = 'json') {
|
|
493
|
+
if (Array.isArray(value)) return value;
|
|
494
|
+
if (!isObject(value)) return [];
|
|
495
|
+
for (const key of ['entries', 'records', 'messages', 'items', 'transcript']) {
|
|
496
|
+
if (Array.isArray(value[key])) return value[key];
|
|
497
|
+
}
|
|
498
|
+
if (Array.isArray(value.conversation?.messages)) return value.conversation.messages;
|
|
499
|
+
if (Array.isArray(value.session?.messages)) return value.session.messages;
|
|
500
|
+
if (isObject(value.message) || value.role || value.content || value.text) {
|
|
501
|
+
return [{ ...value, id: value.id ?? fallbackId }];
|
|
502
|
+
}
|
|
503
|
+
return [];
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function loadTranscriptRecords(transcriptPath) {
|
|
507
|
+
const content = await fs.readFile(transcriptPath, 'utf8');
|
|
508
|
+
const trimmed = content.trim();
|
|
509
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
510
|
+
try {
|
|
511
|
+
return extractTranscriptRecordsFromJson(JSON.parse(trimmed), path.basename(transcriptPath));
|
|
512
|
+
} catch {
|
|
513
|
+
// Fall back to line-oriented parsing below.
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const records = [];
|
|
517
|
+
let index = 0;
|
|
518
|
+
for (const line of content.split(/\r?\n/)) {
|
|
519
|
+
if (!line.trim()) continue;
|
|
520
|
+
index += 1;
|
|
521
|
+
const parsed = parseJsonLine(line);
|
|
522
|
+
if (!parsed) continue;
|
|
523
|
+
if (isObject(parsed.message)) {
|
|
524
|
+
records.push({
|
|
525
|
+
...parsed.message,
|
|
526
|
+
id: parsed.message.id ?? parsed.id ?? `line-${index}`,
|
|
527
|
+
timestamp: parsed.message.timestamp ?? parsed.timestamp ?? parsed.createdAt ?? null,
|
|
528
|
+
__openclaw: {
|
|
529
|
+
id: parsed.id ?? null,
|
|
530
|
+
recordTimestamp: parsed.timestamp ?? null,
|
|
531
|
+
seq: index,
|
|
532
|
+
},
|
|
533
|
+
});
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (parsed.type === 'response_item' && isObject(parsed.payload) && parsed.payload.type === 'message') {
|
|
537
|
+
records.push({
|
|
538
|
+
...parsed.payload,
|
|
539
|
+
id: parsed.payload.id ?? parsed.id ?? `line-${index}`,
|
|
540
|
+
timestamp: parsed.payload.timestamp ?? parsed.timestamp ?? null,
|
|
541
|
+
__openclaw: {
|
|
542
|
+
id: parsed.id ?? null,
|
|
543
|
+
recordTimestamp: parsed.timestamp ?? null,
|
|
544
|
+
seq: index,
|
|
545
|
+
},
|
|
546
|
+
});
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
if (parsed.type === 'message' || parsed.role || parsed.content || parsed.text) {
|
|
550
|
+
records.push({
|
|
551
|
+
...parsed,
|
|
552
|
+
id: parsed.id ?? `line-${index}`,
|
|
553
|
+
timestamp: parsed.timestamp ?? parsed.createdAt ?? null,
|
|
554
|
+
__openclaw: {
|
|
555
|
+
id: parsed.id ?? null,
|
|
556
|
+
recordTimestamp: parsed.timestamp ?? null,
|
|
557
|
+
seq: index,
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return records;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async function pathExists(filePath) {
|
|
566
|
+
try {
|
|
567
|
+
await fs.access(filePath);
|
|
568
|
+
return true;
|
|
569
|
+
} catch {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function artifactPathCandidates(artifact = {}) {
|
|
575
|
+
return [
|
|
576
|
+
normalizeText(artifact.transcriptPath, null),
|
|
577
|
+
normalizeText(artifact.sessionFile, null),
|
|
578
|
+
].filter(Boolean);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
async function chooseReadableArtifact(artifacts = []) {
|
|
582
|
+
const ordered = [...artifacts].reverse();
|
|
583
|
+
for (const artifact of ordered) {
|
|
584
|
+
for (const candidatePath of artifactPathCandidates(artifact)) {
|
|
585
|
+
if (await pathExists(candidatePath)) {
|
|
586
|
+
return {
|
|
587
|
+
artifact,
|
|
588
|
+
transcriptPath: candidatePath,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const fallback = ordered.find((artifact) => artifactPathCandidates(artifact).length > 0);
|
|
594
|
+
if (!fallback) return null;
|
|
595
|
+
return {
|
|
596
|
+
artifact: fallback,
|
|
597
|
+
transcriptPath: artifactPathCandidates(fallback)[0],
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function summarizeDirectoryRequest({
|
|
602
|
+
chatRequestId,
|
|
603
|
+
localSessionKey,
|
|
604
|
+
session = {},
|
|
605
|
+
request = {},
|
|
606
|
+
artifact = {},
|
|
607
|
+
transcriptPath = null,
|
|
608
|
+
} = {}) {
|
|
609
|
+
return {
|
|
610
|
+
chatRequestId,
|
|
611
|
+
localSessionKey,
|
|
612
|
+
relaySessionKey: normalizeText(session.relaySessionKey, null),
|
|
613
|
+
conversationKey: normalizeText(session.conversationKey, null),
|
|
614
|
+
worldId: normalizeText(session.worldId, null),
|
|
615
|
+
localAgentId: normalizeText(session.localAgentId, null),
|
|
616
|
+
firstSeenAt: normalizeText(request.firstSeenAt, null),
|
|
617
|
+
lastSeenAt: normalizeText(request.lastSeenAt, null),
|
|
618
|
+
artifactCount: Array.isArray(request.artifacts) ? request.artifacts.length : 0,
|
|
619
|
+
sessionId: normalizeText(artifact.sessionId, null),
|
|
620
|
+
sessionFile: normalizeText(artifact.sessionFile, null),
|
|
621
|
+
transcriptPath: normalizeText(transcriptPath || artifact.transcriptPath, null),
|
|
622
|
+
deliveryId: normalizeText(artifact.deliveryId, null),
|
|
623
|
+
sourceEventId: normalizeText(artifact.sourceEventId, null),
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
async function resolveStoredTranscriptSource(workspaceRoot, chatRequestId) {
|
|
628
|
+
const sessionDirectory = await readClaworldSessionDirectory(workspaceRoot);
|
|
629
|
+
const conversationSessions = sessionDirectory.directory?.conversationSessions || {};
|
|
630
|
+
for (const [localSessionKey, session] of Object.entries(conversationSessions)) {
|
|
631
|
+
const request = session?.chatRequests?.[chatRequestId];
|
|
632
|
+
if (!request) continue;
|
|
633
|
+
const artifacts = Array.isArray(request.artifacts) ? request.artifacts : [];
|
|
634
|
+
const readable = await chooseReadableArtifact(artifacts);
|
|
635
|
+
if (!readable?.transcriptPath) {
|
|
636
|
+
inputError('stored.chatRequestId', `No local transcript artifact is indexed for chatRequestId=${chatRequestId}`);
|
|
637
|
+
}
|
|
638
|
+
return {
|
|
639
|
+
mode: 'stored',
|
|
640
|
+
chatRequestId,
|
|
641
|
+
transcriptPath: readable.transcriptPath,
|
|
642
|
+
sessionDirectoryPath: sessionDirectory.sessionDirectoryPath,
|
|
643
|
+
summary: summarizeDirectoryRequest({
|
|
644
|
+
chatRequestId,
|
|
645
|
+
localSessionKey,
|
|
646
|
+
session,
|
|
647
|
+
request,
|
|
648
|
+
artifact: readable.artifact,
|
|
649
|
+
transcriptPath: readable.transcriptPath,
|
|
650
|
+
}),
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
inputError('stored.chatRequestId', `chatRequestId=${chatRequestId} was not found in .claworld/sessions/index.json`);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function normalizeHeaderContext({ mode, manual = null, source = null, messages = [] } = {}) {
|
|
657
|
+
if (mode === 'manual') {
|
|
658
|
+
return {
|
|
659
|
+
title: manual.title,
|
|
660
|
+
subtitle: manual.peerProfile,
|
|
661
|
+
peerLabel: manual.peerLabel,
|
|
662
|
+
localLabel: manual.localLabel,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
const firstPeer = messages.find((message) => message.side === 'peer');
|
|
666
|
+
const peerLabel = firstPeer?.speaker
|
|
667
|
+
|| normalizeText(source?.summary?.counterpartyAgentId, null)
|
|
668
|
+
|| normalizeText(source?.summary?.relaySessionKey, null)
|
|
669
|
+
|| 'Peer';
|
|
670
|
+
const title = normalizeText(firstPeer?.speaker, null)
|
|
671
|
+
|| normalizeText(source?.summary?.conversationKey, null)
|
|
672
|
+
|| normalizeText(source?.chatRequestId, null)
|
|
673
|
+
|| 'Claworld conversation';
|
|
674
|
+
const subtitleParts = [
|
|
675
|
+
normalizeText(source?.summary?.worldId, null) ? `world ${source.summary.worldId}` : null,
|
|
676
|
+
normalizeText(source?.chatRequestId, null) ? `chat request ${source.chatRequestId}` : null,
|
|
677
|
+
].filter(Boolean);
|
|
678
|
+
return {
|
|
679
|
+
title,
|
|
680
|
+
subtitle: subtitleParts.join(' · ') || 'Claworld transcript',
|
|
681
|
+
peerLabel,
|
|
682
|
+
localLabel: 'You',
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function displayColumns(text) {
|
|
687
|
+
let columns = 0;
|
|
688
|
+
for (const char of String(text || '')) {
|
|
689
|
+
const code = char.codePointAt(0);
|
|
690
|
+
columns += code > 0x2e80 ? 2 : 1;
|
|
691
|
+
}
|
|
692
|
+
return columns;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function wrapText(text, maxColumns) {
|
|
696
|
+
const source = String(text || '').replace(/\r/g, '').split('\n');
|
|
697
|
+
const lines = [];
|
|
698
|
+
for (const paragraph of source) {
|
|
699
|
+
let current = '';
|
|
700
|
+
let columns = 0;
|
|
701
|
+
const words = paragraph.includes(' ') ? paragraph.split(/(\s+)/) : [...paragraph];
|
|
702
|
+
for (const word of words) {
|
|
703
|
+
const wordColumns = displayColumns(word);
|
|
704
|
+
if (current && columns + wordColumns > maxColumns) {
|
|
705
|
+
lines.push(current.trimEnd());
|
|
706
|
+
current = '';
|
|
707
|
+
columns = 0;
|
|
708
|
+
}
|
|
709
|
+
current += word;
|
|
710
|
+
columns += wordColumns;
|
|
711
|
+
}
|
|
712
|
+
if (current) lines.push(current.trimEnd());
|
|
713
|
+
if (!paragraph) lines.push('');
|
|
714
|
+
}
|
|
715
|
+
return lines.length > 0 ? lines : [''];
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function timeLabelForMessage(message) {
|
|
719
|
+
return message.createdAtLabel || formatTimestampLabel(message.createdAt);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function buildRenderItems(messages = []) {
|
|
723
|
+
const items = [];
|
|
724
|
+
let previousMessage = null;
|
|
725
|
+
for (const message of messages) {
|
|
726
|
+
const gapSeconds = previousMessage && message.timestampMs && previousMessage.timestampMs
|
|
727
|
+
? Math.abs(message.timestampMs - previousMessage.timestampMs) / 1000
|
|
728
|
+
: Number.POSITIVE_INFINITY;
|
|
729
|
+
if (!previousMessage || gapSeconds > TIME_SPLIT_SECONDS) {
|
|
730
|
+
items.push({
|
|
731
|
+
kind: 'time',
|
|
732
|
+
label: timeLabelForMessage(message),
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
items.push({
|
|
736
|
+
kind: 'message',
|
|
737
|
+
message,
|
|
738
|
+
});
|
|
739
|
+
previousMessage = message;
|
|
740
|
+
}
|
|
741
|
+
return items;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function estimateMessageLayout(message) {
|
|
745
|
+
const maxColumns = message.side === 'local' ? 42 : 40;
|
|
746
|
+
const lines = wrapText(message.text, maxColumns);
|
|
747
|
+
const tagRows = message.tags.length > 0 ? 1 : 0;
|
|
748
|
+
const textWidth = Math.min(
|
|
749
|
+
440,
|
|
750
|
+
Math.max(160, Math.max(...lines.map(displayColumns), 10) * 8 + 34),
|
|
751
|
+
);
|
|
752
|
+
const height = 24 + lines.length * 20 + tagRows * 25;
|
|
753
|
+
return {
|
|
754
|
+
lines,
|
|
755
|
+
width: textWidth,
|
|
756
|
+
height,
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function layoutPages({ messages, header, maxPageHeight }) {
|
|
761
|
+
const width = DEFAULT_WIDTH;
|
|
762
|
+
const maxHeight = maxPageHeight || DEFAULT_MAX_PAGE_HEIGHT;
|
|
763
|
+
const margin = 32;
|
|
764
|
+
const headerHeight = 102;
|
|
765
|
+
const bottomMargin = 32;
|
|
766
|
+
const pages = [];
|
|
767
|
+
let page = {
|
|
768
|
+
page: 1,
|
|
769
|
+
width,
|
|
770
|
+
height: Math.min(maxHeight, Math.max(MIN_PAGE_HEIGHT, headerHeight + bottomMargin)),
|
|
771
|
+
elements: [],
|
|
772
|
+
};
|
|
773
|
+
let y = headerHeight;
|
|
774
|
+
|
|
775
|
+
const pushPage = () => {
|
|
776
|
+
page.height = Math.min(maxHeight, Math.max(MIN_PAGE_HEIGHT, Math.ceil(y + bottomMargin)));
|
|
777
|
+
pages.push(page);
|
|
778
|
+
page = {
|
|
779
|
+
page: pages.length + 1,
|
|
780
|
+
width,
|
|
781
|
+
height: MIN_PAGE_HEIGHT,
|
|
782
|
+
elements: [],
|
|
783
|
+
};
|
|
784
|
+
y = 82;
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
for (const item of buildRenderItems(messages)) {
|
|
788
|
+
if (item.kind === 'time') {
|
|
789
|
+
const height = 28;
|
|
790
|
+
if (y + height + bottomMargin > maxHeight && page.elements.length > 0) pushPage();
|
|
791
|
+
page.elements.push({ ...item, x: margin, y, width: width - margin * 2, height });
|
|
792
|
+
y += height;
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
const messageLayout = estimateMessageLayout(item.message);
|
|
796
|
+
const height = messageLayout.height + 10;
|
|
797
|
+
if (y + height + bottomMargin > maxHeight && page.elements.length > 0) pushPage();
|
|
798
|
+
const x = item.message.side === 'local'
|
|
799
|
+
? width - margin - messageLayout.width
|
|
800
|
+
: margin;
|
|
801
|
+
page.elements.push({
|
|
802
|
+
kind: 'message',
|
|
803
|
+
message: item.message,
|
|
804
|
+
layout: messageLayout,
|
|
805
|
+
x,
|
|
806
|
+
y,
|
|
807
|
+
width: messageLayout.width,
|
|
808
|
+
height: messageLayout.height,
|
|
809
|
+
});
|
|
810
|
+
y += height;
|
|
811
|
+
}
|
|
812
|
+
page.height = Math.min(maxHeight, Math.max(MIN_PAGE_HEIGHT, Math.ceil(y + bottomMargin)));
|
|
813
|
+
pages.push(page);
|
|
814
|
+
return {
|
|
815
|
+
width,
|
|
816
|
+
header,
|
|
817
|
+
pages,
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function escapeXml(value) {
|
|
822
|
+
return String(value ?? '')
|
|
823
|
+
.replace(/&/g, '&')
|
|
824
|
+
.replace(/</g, '<')
|
|
825
|
+
.replace(/>/g, '>')
|
|
826
|
+
.replace(/"/g, '"');
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function renderSvgPage({ page, header, pageCount }) {
|
|
830
|
+
const pageTitle = page.page === 1 ? header.title : `${header.title} (continued)`;
|
|
831
|
+
const subtitle = page.page === 1 ? header.subtitle : `Page ${page.page} of ${pageCount}`;
|
|
832
|
+
const parts = [
|
|
833
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${page.width}" height="${page.height}" viewBox="0 0 ${page.width} ${page.height}" role="img" aria-label="${escapeXml(pageTitle)}">`,
|
|
834
|
+
'<style>',
|
|
835
|
+
'.bg{fill:#fbf7ef}.panel{fill:#fffdf8;stroke:#222;stroke-width:2}.title{font:700 26px Arial,sans-serif;fill:#161616}.subtitle{font:14px Arial,sans-serif;fill:#5f5b52}.label{font:700 12px Arial,sans-serif;fill:#666}.bubblePeer{fill:#ffffff;stroke:#222;stroke-width:2}.bubbleLocal{fill:#dff3e5;stroke:#1d3b29;stroke-width:2}.msg{font:15px Arial,sans-serif;fill:#171717}.time{font:12px Arial,sans-serif;fill:#736d63}.tag{font:700 11px Arial,sans-serif;fill:#161616}.tagBox{fill:#f6cf4f;stroke:#222;stroke-width:1}.tagEnd{fill:#f0b4a8}.tagLike{fill:#b9e3ff}.tagDislike{fill:#ddd}.shadow{fill:#222;opacity:.16}',
|
|
836
|
+
'</style>',
|
|
837
|
+
`<rect class="bg" x="0" y="0" width="${page.width}" height="${page.height}"/>`,
|
|
838
|
+
`<rect class="panel" x="22" y="18" width="${page.width - 44}" height="${page.height - 36}" rx="18"/>`,
|
|
839
|
+
`<text class="title" x="42" y="56">${escapeXml(pageTitle)}</text>`,
|
|
840
|
+
`<text class="subtitle" x="42" y="80">${escapeXml(subtitle || 'Claworld transcript')}</text>`,
|
|
841
|
+
];
|
|
842
|
+
|
|
843
|
+
for (const element of page.elements) {
|
|
844
|
+
if (element.kind === 'time') {
|
|
845
|
+
parts.push(`<text class="time" x="${page.width / 2}" y="${element.y + 18}" text-anchor="middle">${escapeXml(element.label)}</text>`);
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
const { message, layout, x, y, width, height } = element;
|
|
849
|
+
const bubbleClass = message.side === 'local' ? 'bubbleLocal' : 'bubblePeer';
|
|
850
|
+
const label = message.side === 'local' ? header.localLabel : (message.speaker || header.peerLabel);
|
|
851
|
+
parts.push(`<text class="label" x="${x}" y="${y + 10}">${escapeXml(label)}</text>`);
|
|
852
|
+
parts.push(`<rect class="shadow" x="${x + 4}" y="${y + 18}" width="${width}" height="${height}" rx="16"/>`);
|
|
853
|
+
parts.push(`<rect class="${bubbleClass}" x="${x}" y="${y + 14}" width="${width}" height="${height}" rx="16"/>`);
|
|
854
|
+
const textX = x + 17;
|
|
855
|
+
let textY = y + 40;
|
|
856
|
+
parts.push(`<text class="msg" x="${textX}" y="${textY}">`);
|
|
857
|
+
for (const [index, line] of layout.lines.entries()) {
|
|
858
|
+
parts.push(`<tspan x="${textX}" dy="${index === 0 ? 0 : 20}">${escapeXml(line)}</tspan>`);
|
|
859
|
+
}
|
|
860
|
+
parts.push('</text>');
|
|
861
|
+
textY += layout.lines.length * 20 + 4;
|
|
862
|
+
let tagX = textX;
|
|
863
|
+
for (const tag of message.tags) {
|
|
864
|
+
const tagWidth = Math.max(48, displayColumns(tag) * 7 + 24);
|
|
865
|
+
const className = tag === 'request end'
|
|
866
|
+
? 'tagEnd'
|
|
867
|
+
: tag === 'like'
|
|
868
|
+
? 'tagLike'
|
|
869
|
+
: tag === 'dislike'
|
|
870
|
+
? 'tagDislike'
|
|
871
|
+
: 'tagBox';
|
|
872
|
+
parts.push(`<rect class="${className}" x="${tagX}" y="${textY}" width="${tagWidth}" height="18" rx="9"/>`);
|
|
873
|
+
parts.push(`<text class="tag" x="${tagX + 12}" y="${textY + 13}">${escapeXml(tag)}</text>`);
|
|
874
|
+
tagX += tagWidth + 6;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
parts.push('</svg>');
|
|
878
|
+
return parts.join('\n');
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function buildBubbleSpec({ mode, source, header, messages, layout, style }) {
|
|
882
|
+
return {
|
|
883
|
+
schema: ARTIFACT_SCHEMA,
|
|
884
|
+
format: 'bubblespec',
|
|
885
|
+
style,
|
|
886
|
+
width: layout.width,
|
|
887
|
+
mode,
|
|
888
|
+
chatRequestId: source?.chatRequestId || null,
|
|
889
|
+
source: {
|
|
890
|
+
mode,
|
|
891
|
+
transcriptPath: source?.transcriptPath || null,
|
|
892
|
+
sessionDirectoryPath: source?.sessionDirectoryPath || null,
|
|
893
|
+
summary: source?.summary || null,
|
|
894
|
+
},
|
|
895
|
+
header,
|
|
896
|
+
messages: messages.map((message) => ({
|
|
897
|
+
id: message.id,
|
|
898
|
+
side: message.side,
|
|
899
|
+
speaker: message.speaker,
|
|
900
|
+
text: message.text,
|
|
901
|
+
tags: message.tags,
|
|
902
|
+
createdAt: message.createdAt,
|
|
903
|
+
episodeIds: message.episodeIds,
|
|
904
|
+
})),
|
|
905
|
+
pages: layout.pages.map((page) => ({
|
|
906
|
+
page: page.page,
|
|
907
|
+
width: page.width,
|
|
908
|
+
height: page.height,
|
|
909
|
+
elementCount: page.elements.length,
|
|
910
|
+
})),
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function makeCrcTable() {
|
|
915
|
+
const table = [];
|
|
916
|
+
for (let n = 0; n < 256; n += 1) {
|
|
917
|
+
let c = n;
|
|
918
|
+
for (let k = 0; k < 8; k += 1) {
|
|
919
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
920
|
+
}
|
|
921
|
+
table[n] = c >>> 0;
|
|
922
|
+
}
|
|
923
|
+
return table;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const CRC_TABLE = makeCrcTable();
|
|
927
|
+
|
|
928
|
+
function crc32(buffer) {
|
|
929
|
+
let crc = 0xffffffff;
|
|
930
|
+
for (const byte of buffer) {
|
|
931
|
+
crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
|
932
|
+
}
|
|
933
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function pngChunk(type, data = Buffer.alloc(0)) {
|
|
937
|
+
const typeBuffer = Buffer.from(type, 'ascii');
|
|
938
|
+
const length = Buffer.alloc(4);
|
|
939
|
+
length.writeUInt32BE(data.length, 0);
|
|
940
|
+
const crcBuffer = Buffer.alloc(4);
|
|
941
|
+
crcBuffer.writeUInt32BE(crc32(Buffer.concat([typeBuffer, data])), 0);
|
|
942
|
+
return Buffer.concat([length, typeBuffer, data, crcBuffer]);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function writeRect(rgba, width, x, y, rectWidth, rectHeight, color) {
|
|
946
|
+
const [r, g, b, a] = color;
|
|
947
|
+
const left = Math.max(0, Math.floor(x));
|
|
948
|
+
const top = Math.max(0, Math.floor(y));
|
|
949
|
+
const right = Math.min(width, Math.ceil(x + rectWidth));
|
|
950
|
+
const bottom = Math.min(Math.floor(rgba.length / (width * 4)), Math.ceil(y + rectHeight));
|
|
951
|
+
for (let row = top; row < bottom; row += 1) {
|
|
952
|
+
for (let col = left; col < right; col += 1) {
|
|
953
|
+
const offset = (row * width + col) * 4;
|
|
954
|
+
rgba[offset] = r;
|
|
955
|
+
rgba[offset + 1] = g;
|
|
956
|
+
rgba[offset + 2] = b;
|
|
957
|
+
rgba[offset + 3] = a;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function buildFallbackPngBuffer(page) {
|
|
963
|
+
const width = page.width;
|
|
964
|
+
const height = page.height;
|
|
965
|
+
const rgba = Buffer.alloc(width * height * 4, 255);
|
|
966
|
+
writeRect(rgba, width, 0, 0, width, height, [251, 247, 239, 255]);
|
|
967
|
+
writeRect(rgba, width, 22, 18, width - 44, height - 36, [255, 253, 248, 255]);
|
|
968
|
+
writeRect(rgba, width, 40, 38, width - 80, 18, [22, 22, 22, 255]);
|
|
969
|
+
writeRect(rgba, width, 40, 68, width - 180, 10, [120, 112, 100, 255]);
|
|
970
|
+
for (const element of page.elements) {
|
|
971
|
+
if (element.kind !== 'message') continue;
|
|
972
|
+
const color = element.message.side === 'local' ? [223, 243, 229, 255] : [255, 255, 255, 255];
|
|
973
|
+
writeRect(rgba, width, element.x, element.y + 14, element.width, element.height, color);
|
|
974
|
+
writeRect(rgba, width, element.x + 16, element.y + 36, element.width - 32, 5, [45, 45, 45, 255]);
|
|
975
|
+
writeRect(rgba, width, element.x + 16, element.y + 54, Math.max(60, element.width - 90), 5, [80, 80, 80, 255]);
|
|
976
|
+
}
|
|
977
|
+
const raw = Buffer.alloc((width * 4 + 1) * height);
|
|
978
|
+
for (let row = 0; row < height; row += 1) {
|
|
979
|
+
const rawOffset = row * (width * 4 + 1);
|
|
980
|
+
raw[rawOffset] = 0;
|
|
981
|
+
rgba.copy(raw, rawOffset + 1, row * width * 4, (row + 1) * width * 4);
|
|
982
|
+
}
|
|
983
|
+
const ihdr = Buffer.alloc(13);
|
|
984
|
+
ihdr.writeUInt32BE(width, 0);
|
|
985
|
+
ihdr.writeUInt32BE(height, 4);
|
|
986
|
+
ihdr[8] = 8;
|
|
987
|
+
ihdr[9] = 6;
|
|
988
|
+
ihdr[10] = 0;
|
|
989
|
+
ihdr[11] = 0;
|
|
990
|
+
ihdr[12] = 0;
|
|
991
|
+
return Buffer.concat([
|
|
992
|
+
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
993
|
+
pngChunk('IHDR', ihdr),
|
|
994
|
+
pngChunk('IDAT', deflateSync(raw)),
|
|
995
|
+
pngChunk('IEND'),
|
|
996
|
+
]);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
async function tryConvertSvgToPng(svgPath, pngPath, page) {
|
|
1000
|
+
const attempts = [
|
|
1001
|
+
{ renderer: 'rsvg-convert', cmd: 'rsvg-convert', args: ['--format', 'png', '--output', pngPath, svgPath] },
|
|
1002
|
+
{ renderer: 'resvg', cmd: 'resvg', args: [svgPath, pngPath] },
|
|
1003
|
+
{ renderer: 'magick', cmd: 'magick', args: [svgPath, pngPath] },
|
|
1004
|
+
{ renderer: 'convert', cmd: 'convert', args: [svgPath, pngPath] },
|
|
1005
|
+
{ renderer: 'sips', cmd: 'sips', args: ['-s', 'format', 'png', svgPath, '--out', pngPath] },
|
|
1006
|
+
];
|
|
1007
|
+
for (const attempt of attempts) {
|
|
1008
|
+
try {
|
|
1009
|
+
await execFileAsync(attempt.cmd, attempt.args, { timeout: 15000, maxBuffer: 2 * 1024 * 1024 });
|
|
1010
|
+
const stat = await fs.stat(pngPath);
|
|
1011
|
+
if (stat.size > 64) return attempt.renderer;
|
|
1012
|
+
} catch {
|
|
1013
|
+
// Continue to the next local renderer.
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
await atomicWriteBuffer(pngPath, buildFallbackPngBuffer(page));
|
|
1017
|
+
return 'fallback-png';
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
async function writeArtifacts({ workspaceRoot, artifactId, spec, layout }) {
|
|
1021
|
+
const dirs = resolveArtifactDirs(workspaceRoot);
|
|
1022
|
+
await fs.mkdir(dirs.imageDir, { recursive: true });
|
|
1023
|
+
await fs.mkdir(dirs.documentDir, { recursive: true });
|
|
1024
|
+
const bubbleSpecPath = path.join(dirs.documentDir, `${artifactId}.bubblespec.json`);
|
|
1025
|
+
await atomicWriteText(bubbleSpecPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
1026
|
+
const svgPages = [];
|
|
1027
|
+
const pngPages = [];
|
|
1028
|
+
for (const page of layout.pages) {
|
|
1029
|
+
const pageStem = `${artifactId}-page-${String(page.page).padStart(2, '0')}`;
|
|
1030
|
+
const svgPath = path.join(dirs.documentDir, `${pageStem}.svg`);
|
|
1031
|
+
const pngPath = path.join(dirs.imageDir, `${pageStem}.png`);
|
|
1032
|
+
await atomicWriteText(svgPath, renderSvgPage({ page, header: layout.header, pageCount: layout.pages.length }));
|
|
1033
|
+
const renderer = await tryConvertSvgToPng(svgPath, pngPath, page);
|
|
1034
|
+
svgPages.push({
|
|
1035
|
+
page: page.page,
|
|
1036
|
+
format: 'svg',
|
|
1037
|
+
path: svgPath,
|
|
1038
|
+
width: page.width,
|
|
1039
|
+
height: page.height,
|
|
1040
|
+
sha256: await hashFile(svgPath),
|
|
1041
|
+
});
|
|
1042
|
+
pngPages.push({
|
|
1043
|
+
page: page.page,
|
|
1044
|
+
format: 'png',
|
|
1045
|
+
path: pngPath,
|
|
1046
|
+
width: page.width,
|
|
1047
|
+
height: page.height,
|
|
1048
|
+
renderer,
|
|
1049
|
+
sha256: await hashFile(pngPath),
|
|
1050
|
+
mediaRef: `MEDIA:${pngPath}`,
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
return {
|
|
1054
|
+
bubbleSpec: {
|
|
1055
|
+
format: 'bubblespec',
|
|
1056
|
+
path: bubbleSpecPath,
|
|
1057
|
+
sha256: await hashFile(bubbleSpecPath),
|
|
1058
|
+
},
|
|
1059
|
+
svgPages,
|
|
1060
|
+
pngPages,
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function buildDiagnostics({ sourceRecords, normalizedMessages, renderedMessages, source }) {
|
|
1065
|
+
return {
|
|
1066
|
+
source: {
|
|
1067
|
+
mode: source.mode,
|
|
1068
|
+
transcriptPath: source.transcriptPath || null,
|
|
1069
|
+
sessionDirectoryPath: source.sessionDirectoryPath || null,
|
|
1070
|
+
},
|
|
1071
|
+
stats: {
|
|
1072
|
+
sourceMessages: sourceRecords.length,
|
|
1073
|
+
normalizedMessages: normalizedMessages.length,
|
|
1074
|
+
renderedMessages: renderedMessages.length,
|
|
1075
|
+
droppedMessages: Math.max(0, sourceRecords.length - normalizedMessages.length),
|
|
1076
|
+
},
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
async function appendRenderJournalEvent(workspaceRoot, result) {
|
|
1081
|
+
try {
|
|
1082
|
+
await appendClaworldJournalEvent(workspaceRoot, {
|
|
1083
|
+
source: 'claworld_runtime',
|
|
1084
|
+
kind: 'transcript_report',
|
|
1085
|
+
scope: 'conversation',
|
|
1086
|
+
summary: `Rendered Claworld transcript report ${result.artifactId}.`,
|
|
1087
|
+
refs: {
|
|
1088
|
+
chatRequestId: result.chatRequestId || null,
|
|
1089
|
+
artifactId: result.artifactId,
|
|
1090
|
+
},
|
|
1091
|
+
artifacts: {
|
|
1092
|
+
bubbleSpec: result.artifacts?.bubbleSpec?.path || null,
|
|
1093
|
+
pngPages: (result.artifacts?.pngPages || []).map((page) => page.path),
|
|
1094
|
+
svgPages: (result.artifacts?.svgPages || []).map((page) => page.path),
|
|
1095
|
+
},
|
|
1096
|
+
}, { updateSessionDirectory: false });
|
|
1097
|
+
} catch {
|
|
1098
|
+
// Rendering should not fail because the journal is unavailable.
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
export async function renderClaworldTranscriptReport({
|
|
1103
|
+
workspaceRoot = null,
|
|
1104
|
+
agentId = null,
|
|
1105
|
+
args = {},
|
|
1106
|
+
} = {}) {
|
|
1107
|
+
const resolvedWorkspaceRoot = resolveWorkspaceRoot(workspaceRoot);
|
|
1108
|
+
await ensureClaworldWorkingMemory(resolvedWorkspaceRoot);
|
|
1109
|
+
const params = validateRenderArgs(args);
|
|
1110
|
+
let source = { mode: params.mode };
|
|
1111
|
+
let sourceRecords = [];
|
|
1112
|
+
let normalizedMessages = [];
|
|
1113
|
+
let renderedMessages = [];
|
|
1114
|
+
if (params.mode === 'manual') {
|
|
1115
|
+
sourceRecords = params.manual.messages;
|
|
1116
|
+
normalizedMessages = normalizeTranscriptMessages(sourceRecords, { mode: 'manual' });
|
|
1117
|
+
renderedMessages = normalizedMessages;
|
|
1118
|
+
source = {
|
|
1119
|
+
mode: 'manual',
|
|
1120
|
+
summary: {
|
|
1121
|
+
suppliedMessageCount: sourceRecords.length,
|
|
1122
|
+
},
|
|
1123
|
+
};
|
|
1124
|
+
} else {
|
|
1125
|
+
source = await resolveStoredTranscriptSource(resolvedWorkspaceRoot, params.stored.chatRequestId);
|
|
1126
|
+
sourceRecords = await loadTranscriptRecords(source.transcriptPath);
|
|
1127
|
+
normalizedMessages = normalizeTranscriptMessages(sourceRecords, { mode: 'stored' });
|
|
1128
|
+
renderedMessages = selectStoredMessages(normalizedMessages, params.stored.chatRequestId);
|
|
1129
|
+
}
|
|
1130
|
+
if (renderedMessages.length === 0) {
|
|
1131
|
+
inputError('messages', 'No renderable peer-visible transcript messages were found');
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
const header = normalizeHeaderContext({
|
|
1135
|
+
mode: params.mode,
|
|
1136
|
+
manual: params.manual || null,
|
|
1137
|
+
source,
|
|
1138
|
+
messages: renderedMessages,
|
|
1139
|
+
});
|
|
1140
|
+
const layout = layoutPages({
|
|
1141
|
+
messages: renderedMessages,
|
|
1142
|
+
header,
|
|
1143
|
+
maxPageHeight: params.maxPageHeight,
|
|
1144
|
+
});
|
|
1145
|
+
const spec = buildBubbleSpec({
|
|
1146
|
+
mode: params.mode,
|
|
1147
|
+
source,
|
|
1148
|
+
header,
|
|
1149
|
+
messages: renderedMessages,
|
|
1150
|
+
layout,
|
|
1151
|
+
style: params.style,
|
|
1152
|
+
});
|
|
1153
|
+
const seed = `${params.mode}:${source?.chatRequestId || header.title}:${renderedMessages.length}:${Date.now()}:${stableJson({ agentId })}`;
|
|
1154
|
+
const artifactId = `claworld-transcript-${safeFileStem(source?.chatRequestId || header.title)}-${sha256Buffer(Buffer.from(seed)).slice(0, 10)}`;
|
|
1155
|
+
const artifacts = await writeArtifacts({
|
|
1156
|
+
workspaceRoot: resolvedWorkspaceRoot,
|
|
1157
|
+
artifactId,
|
|
1158
|
+
spec,
|
|
1159
|
+
layout,
|
|
1160
|
+
});
|
|
1161
|
+
const result = {
|
|
1162
|
+
status: 'ok',
|
|
1163
|
+
mode: params.mode,
|
|
1164
|
+
chatRequestId: source.chatRequestId || null,
|
|
1165
|
+
artifactId,
|
|
1166
|
+
messageCount: renderedMessages.length,
|
|
1167
|
+
pageCount: layout.pages.length,
|
|
1168
|
+
style: params.style,
|
|
1169
|
+
artifacts,
|
|
1170
|
+
deliveryHint: {
|
|
1171
|
+
primaryMedia: artifacts.pngPages[0]?.mediaRef || null,
|
|
1172
|
+
primaryMediaBatch: artifacts.pngPages.map((page) => page.mediaRef).join('\n'),
|
|
1173
|
+
sourceSvgDocument: artifacts.svgPages[0] ? `[[as_document]]\nMEDIA:${artifacts.svgPages[0].path}` : null,
|
|
1174
|
+
openClawMediaPaths: artifacts.pngPages.map((page) => page.path),
|
|
1175
|
+
},
|
|
1176
|
+
diagnostics: buildDiagnostics({
|
|
1177
|
+
sourceRecords,
|
|
1178
|
+
normalizedMessages,
|
|
1179
|
+
renderedMessages,
|
|
1180
|
+
source,
|
|
1181
|
+
}),
|
|
1182
|
+
};
|
|
1183
|
+
await appendRenderJournalEvent(resolvedWorkspaceRoot, result);
|
|
1184
|
+
return result;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function buildEpisodeFilters(input = {}) {
|
|
1188
|
+
const source = isObject(input.filters) ? input.filters : input;
|
|
1189
|
+
return {
|
|
1190
|
+
chatRequestId: normalizeText(source.chatRequestId, null),
|
|
1191
|
+
conversationKey: normalizeText(source.conversationKey, null),
|
|
1192
|
+
localSessionKey: normalizeText(source.localSessionKey, null),
|
|
1193
|
+
worldId: normalizeText(source.worldId, null),
|
|
1194
|
+
counterpartyAgentId: normalizeText(source.counterpartyAgentId, null),
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function episodeMatchesFilters(summary, filters) {
|
|
1199
|
+
if (filters.chatRequestId && summary.chatRequestId !== filters.chatRequestId) return false;
|
|
1200
|
+
if (filters.localSessionKey && summary.localSessionKey !== filters.localSessionKey) return false;
|
|
1201
|
+
if (filters.conversationKey && summary.conversationKey !== filters.conversationKey) return false;
|
|
1202
|
+
if (filters.worldId && summary.worldId !== filters.worldId) return false;
|
|
1203
|
+
if (
|
|
1204
|
+
filters.counterpartyAgentId
|
|
1205
|
+
&& summary.relaySessionKey !== filters.counterpartyAgentId
|
|
1206
|
+
&& summary.conversationKey?.includes(filters.counterpartyAgentId) !== true
|
|
1207
|
+
) {
|
|
1208
|
+
return false;
|
|
1209
|
+
}
|
|
1210
|
+
return true;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
export async function listClaworldLocalTranscriptEpisodes({
|
|
1214
|
+
workspaceRoot = null,
|
|
1215
|
+
filters = {},
|
|
1216
|
+
} = {}) {
|
|
1217
|
+
const resolvedWorkspaceRoot = resolveWorkspaceRoot(workspaceRoot);
|
|
1218
|
+
const normalizedFilters = buildEpisodeFilters(filters);
|
|
1219
|
+
const sessionDirectory = await readClaworldSessionDirectory(resolvedWorkspaceRoot);
|
|
1220
|
+
const summaries = [];
|
|
1221
|
+
const conversationSessions = sessionDirectory.directory?.conversationSessions || {};
|
|
1222
|
+
for (const [localSessionKey, session] of Object.entries(conversationSessions)) {
|
|
1223
|
+
const chatRequests = session?.chatRequests || {};
|
|
1224
|
+
for (const [chatRequestId, request] of Object.entries(chatRequests)) {
|
|
1225
|
+
const artifacts = Array.isArray(request?.artifacts) ? request.artifacts : [];
|
|
1226
|
+
const latestArtifact = [...artifacts].reverse().find((artifact) => artifactPathCandidates(artifact).length > 0) || {};
|
|
1227
|
+
const summary = summarizeDirectoryRequest({
|
|
1228
|
+
chatRequestId,
|
|
1229
|
+
localSessionKey,
|
|
1230
|
+
session,
|
|
1231
|
+
request,
|
|
1232
|
+
artifact: latestArtifact,
|
|
1233
|
+
transcriptPath: latestArtifact.transcriptPath || latestArtifact.sessionFile || null,
|
|
1234
|
+
});
|
|
1235
|
+
if (episodeMatchesFilters(summary, normalizedFilters)) summaries.push(summary);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
return summaries.sort((left, right) => String(right.lastSeenAt || '').localeCompare(String(left.lastSeenAt || '')));
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
export async function summarizeClaworldChatRequestTranscript({
|
|
1242
|
+
workspaceRoot = null,
|
|
1243
|
+
chatRequestId,
|
|
1244
|
+
} = {}) {
|
|
1245
|
+
const resolvedWorkspaceRoot = resolveWorkspaceRoot(workspaceRoot);
|
|
1246
|
+
const id = normalizeText(chatRequestId, null);
|
|
1247
|
+
if (!id) inputError('chatRequestId', 'chatRequestId is required');
|
|
1248
|
+
const source = await resolveStoredTranscriptSource(resolvedWorkspaceRoot, id);
|
|
1249
|
+
const sourceRecords = await loadTranscriptRecords(source.transcriptPath);
|
|
1250
|
+
const normalizedMessages = normalizeTranscriptMessages(sourceRecords, { mode: 'stored' });
|
|
1251
|
+
const renderedMessages = selectStoredMessages(normalizedMessages, id);
|
|
1252
|
+
return {
|
|
1253
|
+
chatRequestId: id,
|
|
1254
|
+
transcriptPath: source.transcriptPath,
|
|
1255
|
+
sourceMessageCount: sourceRecords.length,
|
|
1256
|
+
renderableMessageCount: renderedMessages.length,
|
|
1257
|
+
peerMessageCount: renderedMessages.filter((message) => message.side === 'peer').length,
|
|
1258
|
+
localMessageCount: renderedMessages.filter((message) => message.side === 'local').length,
|
|
1259
|
+
firstMessageAt: renderedMessages[0]?.createdAt || null,
|
|
1260
|
+
lastMessageAt: renderedMessages[renderedMessages.length - 1]?.createdAt || null,
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function buildLocalTranscriptSummary(episodes = []) {
|
|
1265
|
+
return {
|
|
1266
|
+
count: episodes.length,
|
|
1267
|
+
chatRequestIds: episodes.map((episode) => episode.chatRequestId).filter(Boolean),
|
|
1268
|
+
latestChatRequestId: episodes[0]?.chatRequestId || null,
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
export async function augmentConversationPayloadWithLocalTranscriptIndex(payload, {
|
|
1273
|
+
workspaceRoot = null,
|
|
1274
|
+
filters = {},
|
|
1275
|
+
} = {}) {
|
|
1276
|
+
if (!isObject(payload)) return payload;
|
|
1277
|
+
const episodes = await listClaworldLocalTranscriptEpisodes({ workspaceRoot, filters });
|
|
1278
|
+
const addition = {
|
|
1279
|
+
localTranscriptEpisodes: episodes,
|
|
1280
|
+
localTranscriptSummary: buildLocalTranscriptSummary(episodes),
|
|
1281
|
+
};
|
|
1282
|
+
if (Array.isArray(payload.items)) {
|
|
1283
|
+
return {
|
|
1284
|
+
...payload,
|
|
1285
|
+
...addition,
|
|
1286
|
+
items: payload.items.map((item) => {
|
|
1287
|
+
if (!isObject(item)) return item;
|
|
1288
|
+
const itemFilters = buildEpisodeFilters({
|
|
1289
|
+
chatRequestId: item.chatRequestId,
|
|
1290
|
+
conversationKey: item.conversationKey,
|
|
1291
|
+
localSessionKey: item.localSessionKey,
|
|
1292
|
+
worldId: item.worldId,
|
|
1293
|
+
counterpartyAgentId: item.counterpartyAgentId || item.targetAgentId || item.peerAgentId,
|
|
1294
|
+
});
|
|
1295
|
+
const itemEpisodes = episodes.filter((episode) => episodeMatchesFilters(episode, itemFilters));
|
|
1296
|
+
if (itemEpisodes.length === 0) return item;
|
|
1297
|
+
return {
|
|
1298
|
+
...item,
|
|
1299
|
+
localTranscriptEpisodes: itemEpisodes,
|
|
1300
|
+
localTranscriptSummary: buildLocalTranscriptSummary(itemEpisodes),
|
|
1301
|
+
};
|
|
1302
|
+
}),
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
return {
|
|
1306
|
+
...payload,
|
|
1307
|
+
...addition,
|
|
1308
|
+
};
|
|
1309
|
+
}
|