@xfxstudio/claworld 2026.7.9-testing.3 → 2026.7.13-testing.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 +12 -8
- package/openclaw.plugin.json +2 -3
- package/package.json +2 -1
- package/skills/claworld-main-session/SKILL.md +8 -14
- package/skills/claworld-management-session/SKILL.md +4 -10
- package/src/openclaw/plugin/claworld-channel-plugin.js +67 -2
- package/src/openclaw/plugin/register.js +283 -147
- package/src/openclaw/plugin/relay-client-shared.js +14 -0
- package/src/openclaw/runtime/tool-inventory.js +1 -4
- package/src/openclaw/runtime/transcript-report-comic-grid.js +475 -0
- package/src/openclaw/runtime/transcript-report-stylekit.js +189 -0
- package/src/openclaw/runtime/transcript-report.js +783 -1748
- package/src/openclaw/runtime/working-memory.js +11 -5
|
@@ -1,1898 +1,933 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import crypto from 'node:crypto';
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
2
|
import fs from 'node:fs/promises';
|
|
4
3
|
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
4
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
5
|
+
import {
|
|
6
|
+
CLAWORLD_TRANSCRIPT_STYLE_NAME,
|
|
7
|
+
measureTranscriptItem,
|
|
8
|
+
paginateTranscriptItems,
|
|
9
|
+
renderTranscriptPageSvg,
|
|
10
|
+
} from './transcript-report-comic-grid.js';
|
|
11
|
+
import { displayCols } from './transcript-report-stylekit.js';
|
|
12
|
+
import { appendClaworldJournalEvent } from './working-memory.js';
|
|
23
13
|
|
|
24
14
|
const DEFAULT_WIDTH = 720;
|
|
25
15
|
const DEFAULT_MAX_PAGE_HEIGHT = 2600;
|
|
26
|
-
const
|
|
27
|
-
const
|
|
16
|
+
const TIME_SPLIT_SECONDS = 5 * 60;
|
|
17
|
+
const SESSION_INDEX_RELATIVE_PATH = path.join('.claworld', 'sessions', 'index.json');
|
|
18
|
+
|
|
28
19
|
const TOP_LEVEL_RENDER_FIELDS = new Set(['mode', 'stored', 'manual', 'style', 'maxPageHeight']);
|
|
29
20
|
const MANUAL_RENDER_FIELDS = new Set(['messages', 'title', 'peerProfile', 'localLabel', 'peerLabel']);
|
|
21
|
+
const STORED_RENDER_FIELDS = new Set(['chatRequestId', 'title', 'peerProfile', 'localLabel', 'peerLabel']);
|
|
30
22
|
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
|
-
const CANVAS_MARGIN = 24;
|
|
35
|
-
const FRAME_MARGIN = 16;
|
|
36
|
-
const HEADER_Y = 48;
|
|
37
|
-
const HEADER_CARD_HEIGHT_ONE_LINE = 92;
|
|
38
|
-
const HEADER_CARD_HEIGHT_TWO_LINES = 110;
|
|
39
|
-
const HEADER_BOTTOM_PAD = 20;
|
|
40
|
-
const BODY_TOP_GAP = 24;
|
|
41
|
-
const PAGE_BOTTOM = 54;
|
|
42
|
-
const ITEM_GAP = 22;
|
|
43
|
-
const TIME_ROW_HEIGHT = 42;
|
|
44
|
-
const ELLIPSIS_HEIGHT = 34;
|
|
45
|
-
const BUBBLE_PAD_X = 32;
|
|
46
|
-
const BUBBLE_PAD_Y = 22;
|
|
47
|
-
const LABEL_HEIGHT = 30;
|
|
48
|
-
const LABEL_OVERLAP = 18;
|
|
49
|
-
const LABEL_RAISE = 9;
|
|
50
|
-
const LABEL_MAX_COLS = 14;
|
|
51
|
-
const BUBBLE_MAX_RATIO = 0.64;
|
|
52
|
-
const BUBBLE_MIN_WIDTH = 200;
|
|
53
|
-
const FONT_SIZE = 18;
|
|
54
|
-
const SMALL_FONT_SIZE = 12;
|
|
55
|
-
const LABEL_FONT_SIZE = 15;
|
|
56
|
-
const TITLE_FONT_SIZE = 34;
|
|
57
|
-
const LINE_HEIGHT = 29;
|
|
58
|
-
const HEADER_SUBTITLE_MAX_UNITS = 33;
|
|
59
|
-
const HEADER_SUBTITLE_MAX_LINES = 2;
|
|
60
|
-
const HEADER_SUBTITLE_LINE_HEIGHT = 19;
|
|
61
|
-
const TAG_HEIGHT = 58;
|
|
62
|
-
const TAG_ICON_SIZE = 30;
|
|
63
|
-
const TAG_ICON_GAP = 12;
|
|
64
|
-
const TAG_ICON_TOP_GAP = 8;
|
|
65
|
-
const TAG_FALLBACK_MAX_COLS = 10;
|
|
66
|
-
const TEXT_UNIT_PX = 18;
|
|
67
|
-
const BLACK = '#090909';
|
|
68
|
-
const COMIC_THEME = Object.freeze({
|
|
69
|
-
paper: '#FBF8EF',
|
|
70
|
-
paperWarm: '#FFFDF7',
|
|
71
|
-
headerFill: '#FEF5D8',
|
|
72
|
-
gridMinor: '#BED1D8',
|
|
73
|
-
gridMajor: '#AABFC8',
|
|
74
|
-
ink: BLACK,
|
|
75
|
-
muted: '#222222',
|
|
76
|
-
leftFill: '#EFFFF5',
|
|
77
|
-
leftLabel: '#62E69D',
|
|
78
|
-
leftAccentA: '#58E58F',
|
|
79
|
-
leftAccentB: '#47B6FF',
|
|
80
|
-
rightFill: '#EFE0FF',
|
|
81
|
-
rightLabel: '#B785FF',
|
|
82
|
-
rightAccentA: '#A871FF',
|
|
83
|
-
rightAccentB: '#FF4EB4',
|
|
84
|
-
timeFill: '#FFFDF7',
|
|
85
|
-
timeAccentLeft: '#FF62DE',
|
|
86
|
-
timeAccentRight: '#50D995',
|
|
87
|
-
});
|
|
88
|
-
const TAG_ICON_THEMES = Object.freeze({
|
|
89
|
-
like: ['#D8F4FF', '#58B7FF'],
|
|
90
|
-
dislike: ['#FFE0EA', '#FF6A9A'],
|
|
91
|
-
'request end': ['#FFF0A8', '#FF9F2F'],
|
|
92
|
-
});
|
|
93
|
-
const COMIC_FONT_FAMILY = [
|
|
94
|
-
"'PingFang SC'",
|
|
95
|
-
"'Hiragino Sans GB'",
|
|
96
|
-
"'Microsoft YaHei'",
|
|
97
|
-
"'WenQuanYi Zen Hei'",
|
|
98
|
-
"'Noto Sans CJK SC'",
|
|
99
|
-
"'Noto Sans SC'",
|
|
100
|
-
"'Source Han Sans SC'",
|
|
101
|
-
"'IPA P Gothic'",
|
|
102
|
-
"'AR PL UMing CN'",
|
|
103
|
-
"'Arial Unicode MS'",
|
|
104
|
-
'-apple-system',
|
|
105
|
-
'BlinkMacSystemFont',
|
|
106
|
-
"'Segoe UI'",
|
|
107
|
-
'Arial',
|
|
108
|
-
'sans-serif',
|
|
109
|
-
].join(', ');
|
|
110
23
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
24
|
+
const OPERATIONAL_NOTICE_PATTERNS = [
|
|
25
|
+
/^🧭\s*New session:\s+\S+/iu,
|
|
26
|
+
/^🧹\s*Auto-compaction complete(?:\s*\(count \d+\))?\.$/iu,
|
|
27
|
+
/^↪️?\s*Model Fallback:/iu,
|
|
28
|
+
/^↪️?\s*Model Fallback cleared:/iu,
|
|
29
|
+
/^⚠️?\s*Agent failed before reply:/iu,
|
|
30
|
+
/^Sent the (?:reply|opener|Claworld reply)\.?$/iu,
|
|
31
|
+
/^◐\s*Session automatically reset\b/iu,
|
|
32
|
+
];
|
|
33
|
+
const RUNTIME_ERROR_PATTERNS = [
|
|
34
|
+
/^⚠️?\s*Agent failed before reply:/iu,
|
|
35
|
+
/^LLM request failed:/iu,
|
|
36
|
+
/^LLM request timed out\.$/iu,
|
|
37
|
+
/^LLM request unauthorized\.$/iu,
|
|
38
|
+
/^The AI service is temporarily overloaded\.$/iu,
|
|
39
|
+
/^The AI service returned an error\.$/iu,
|
|
40
|
+
/^⚠️?\s*API rate limit reached\.$/iu,
|
|
41
|
+
/^⚠️?\s*.+\s+returned a billing error\b/iu,
|
|
42
|
+
];
|
|
43
|
+
const OPERATIONAL_SUFFIX_PATTERNS = [
|
|
44
|
+
/^Usage:\s+.+\s+in\s+\/\s+.+\s+out(?:\s+·\s+est\s+.+)?$/iu,
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
function text(value, fallback = null) {
|
|
116
48
|
if (value == null) return fallback;
|
|
117
49
|
const normalized = String(value).trim();
|
|
118
50
|
return normalized || fallback;
|
|
119
51
|
}
|
|
120
52
|
|
|
121
|
-
function
|
|
122
|
-
|
|
123
|
-
code: 'tool_input_invalid',
|
|
124
|
-
category: 'input',
|
|
125
|
-
status: 400,
|
|
126
|
-
message,
|
|
127
|
-
publicMessage: message,
|
|
128
|
-
recoverable: true,
|
|
129
|
-
context: { field },
|
|
130
|
-
});
|
|
53
|
+
function isObject(value) {
|
|
54
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
131
55
|
}
|
|
132
56
|
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
|
|
57
|
+
function compactObject(value = {}) {
|
|
58
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => (
|
|
59
|
+
entry != null && entry !== '' && (!Array.isArray(entry) || entry.length > 0)
|
|
60
|
+
)));
|
|
136
61
|
}
|
|
137
62
|
|
|
138
|
-
function
|
|
139
|
-
|
|
140
|
-
for (const key of Object.keys(source)) {
|
|
141
|
-
if (!allowedFields.has(key)) inputError(`${field}.${key}`, `${field}.${key} is not supported`);
|
|
142
|
-
}
|
|
63
|
+
function isoNow() {
|
|
64
|
+
return new Date().toISOString();
|
|
143
65
|
}
|
|
144
66
|
|
|
145
|
-
function
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const style = normalizeText(params.style, CLAWORLD_TRANSCRIPT_REPORT_STYLE);
|
|
152
|
-
if (!CLAWORLD_TRANSCRIPT_REPORT_STYLES.includes(style)) {
|
|
153
|
-
inputError('style', `style must be ${CLAWORLD_TRANSCRIPT_REPORT_STYLE}`);
|
|
154
|
-
}
|
|
155
|
-
const maxPageHeight = Number.isInteger(params.maxPageHeight)
|
|
156
|
-
? params.maxPageHeight
|
|
157
|
-
: DEFAULT_MAX_PAGE_HEIGHT;
|
|
158
|
-
if (maxPageHeight < MIN_PAGE_HEIGHT || maxPageHeight > MAX_PAGE_HEIGHT) {
|
|
159
|
-
inputError('maxPageHeight', `maxPageHeight must be between ${MIN_PAGE_HEIGHT} and ${MAX_PAGE_HEIGHT}`);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (mode === 'stored') {
|
|
163
|
-
const stored = assertObject(params.stored, 'stored');
|
|
164
|
-
assertNoUnknownFields(stored, new Set(['chatRequestId']), 'stored');
|
|
165
|
-
const chatRequestId = normalizeText(stored.chatRequestId, null);
|
|
166
|
-
if (!chatRequestId) inputError('stored.chatRequestId', 'stored.chatRequestId is required');
|
|
167
|
-
return { mode, style, maxPageHeight, stored: { chatRequestId } };
|
|
67
|
+
async function readJsonObject(filePath, fallback = {}) {
|
|
68
|
+
try {
|
|
69
|
+
const parsed = JSON.parse(await fs.readFile(filePath, 'utf8'));
|
|
70
|
+
return isObject(parsed) ? parsed : fallback;
|
|
71
|
+
} catch {
|
|
72
|
+
return fallback;
|
|
168
73
|
}
|
|
74
|
+
}
|
|
169
75
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
inputError('manual.messages', 'manual.messages must contain at least one message');
|
|
76
|
+
async function atomicWriteText(filePath, content) {
|
|
77
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
78
|
+
const temporaryPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${randomUUID()}.tmp`);
|
|
79
|
+
try {
|
|
80
|
+
await fs.writeFile(temporaryPath, content.endsWith('\n') ? content : `${content}\n`, 'utf8');
|
|
81
|
+
await fs.rename(temporaryPath, filePath);
|
|
82
|
+
} finally {
|
|
83
|
+
await fs.rm(temporaryPath, { force: true }).catch(() => {});
|
|
179
84
|
}
|
|
180
|
-
const messages = manual.messages.map((message, index) => {
|
|
181
|
-
if (!isObject(message)) inputError(`manual.messages.${index}`, 'manual message must be an object');
|
|
182
|
-
assertNoUnknownFields(message, MANUAL_MESSAGE_FIELDS, `manual.messages.${index}`);
|
|
183
|
-
const from = normalizeText(message.from, null);
|
|
184
|
-
if (!['peer', 'local'].includes(from)) {
|
|
185
|
-
inputError(`manual.messages.${index}.from`, 'manual message from must be peer or local');
|
|
186
|
-
}
|
|
187
|
-
const text = normalizeText(message.text, null);
|
|
188
|
-
if (!text) inputError(`manual.messages.${index}.text`, 'manual message text is required');
|
|
189
|
-
const createdAt = normalizeText(message.createdAt, null);
|
|
190
|
-
if (!createdAt) inputError(`manual.messages.${index}.createdAt`, 'manual message createdAt is required');
|
|
191
|
-
if (!parseTimestampMs(createdAt)) {
|
|
192
|
-
inputError(`manual.messages.${index}.createdAt`, 'manual message createdAt must be a parseable timestamp');
|
|
193
|
-
}
|
|
194
|
-
return { from, text, createdAt };
|
|
195
|
-
});
|
|
196
|
-
return {
|
|
197
|
-
mode,
|
|
198
|
-
style,
|
|
199
|
-
maxPageHeight,
|
|
200
|
-
manual: {
|
|
201
|
-
messages,
|
|
202
|
-
title: normalizeText(manual.title, null) || inputError('manual.title', 'manual.title is required'),
|
|
203
|
-
peerProfile: normalizeText(manual.peerProfile, null) || inputError('manual.peerProfile', 'manual.peerProfile is required'),
|
|
204
|
-
localLabel: normalizeText(manual.localLabel, null) || inputError('manual.localLabel', 'manual.localLabel is required'),
|
|
205
|
-
peerLabel: normalizeText(manual.peerLabel, null) || inputError('manual.peerLabel', 'manual.peerLabel is required'),
|
|
206
|
-
},
|
|
207
|
-
};
|
|
208
85
|
}
|
|
209
86
|
|
|
210
|
-
function
|
|
211
|
-
|
|
87
|
+
async function atomicWriteJson(filePath, payload) {
|
|
88
|
+
await atomicWriteText(filePath, JSON.stringify(payload, null, 2));
|
|
212
89
|
}
|
|
213
90
|
|
|
214
|
-
function
|
|
215
|
-
return
|
|
91
|
+
function sessionIndexPath(workspaceRoot) {
|
|
92
|
+
return path.join(workspaceRoot, SESSION_INDEX_RELATIVE_PATH);
|
|
216
93
|
}
|
|
217
94
|
|
|
218
|
-
function
|
|
219
|
-
|
|
95
|
+
function emptySessionIndex() {
|
|
96
|
+
const now = isoNow();
|
|
97
|
+
return {
|
|
98
|
+
schema: 'claworld.sessions.v1',
|
|
99
|
+
version: 1,
|
|
100
|
+
createdAt: now,
|
|
101
|
+
updatedAt: now,
|
|
102
|
+
main: {},
|
|
103
|
+
management: {},
|
|
104
|
+
conversationSessions: {},
|
|
105
|
+
conversationEpisodes: {},
|
|
106
|
+
};
|
|
220
107
|
}
|
|
221
108
|
|
|
222
|
-
async function
|
|
223
|
-
|
|
109
|
+
async function readSessionIndex(workspaceRoot) {
|
|
110
|
+
const index = await readJsonObject(sessionIndexPath(workspaceRoot), emptySessionIndex());
|
|
111
|
+
if (!isObject(index.main)) index.main = {};
|
|
112
|
+
if (!isObject(index.management)) index.management = {};
|
|
113
|
+
if (!isObject(index.conversationSessions)) index.conversationSessions = {};
|
|
114
|
+
if (!isObject(index.conversationEpisodes)) index.conversationEpisodes = {};
|
|
115
|
+
return index;
|
|
224
116
|
}
|
|
225
117
|
|
|
226
|
-
function
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
118
|
+
function appendUniqueDelivery(deliveries, delivery) {
|
|
119
|
+
const deliveryId = text(delivery?.deliveryId, null);
|
|
120
|
+
if (!deliveryId || (!text(delivery?.commandText, null) && !text(delivery?.contextText, null))) return deliveries;
|
|
121
|
+
const next = Array.isArray(deliveries) ? [...deliveries] : [];
|
|
122
|
+
const existingIndex = next.findIndex((item) => text(item?.deliveryId, null) === deliveryId);
|
|
123
|
+
if (existingIndex >= 0) {
|
|
124
|
+
next[existingIndex] = compactObject({ ...next[existingIndex], ...delivery });
|
|
125
|
+
} else {
|
|
126
|
+
next.push(compactObject(delivery));
|
|
127
|
+
}
|
|
128
|
+
return next;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function recordClaworldTranscriptEpisode(workspaceRoot, input = {}) {
|
|
132
|
+
const chatRequestId = text(input.chatRequestId, null);
|
|
133
|
+
const deliveryId = text(input.deliveryId, null);
|
|
134
|
+
const commandText = text(input.commandText, null);
|
|
135
|
+
const contextText = text(input.contextText, null);
|
|
136
|
+
if (!workspaceRoot || !chatRequestId || !deliveryId || (!commandText && !contextText)) {
|
|
137
|
+
return { ok: false, updated: false, reason: 'missing_transcript_identity' };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const index = await readSessionIndex(workspaceRoot);
|
|
141
|
+
const previous = isObject(index.conversationEpisodes[chatRequestId])
|
|
142
|
+
? index.conversationEpisodes[chatRequestId]
|
|
143
|
+
: {};
|
|
144
|
+
let deliveries = appendUniqueDelivery(previous.deliveries, {
|
|
145
|
+
deliveryId,
|
|
146
|
+
direction: 'inbound',
|
|
147
|
+
fromAgentId: text(input.fromAgentId, null),
|
|
148
|
+
fromAgentCode: text(input.fromAgentCode, null),
|
|
149
|
+
fromDisplayIdentity: text(input.fromDisplayIdentity, null),
|
|
150
|
+
deliveryType: text(input.deliveryType, null),
|
|
151
|
+
commandText,
|
|
152
|
+
contextText,
|
|
153
|
+
untrustedContext: text(input.untrustedContext, null),
|
|
154
|
+
createdAt: text(input.createdAt, null),
|
|
155
|
+
turnCreatedAt: text(input.turnCreatedAt, null),
|
|
156
|
+
});
|
|
157
|
+
const replyText = text(input.replyText, null);
|
|
158
|
+
if (replyText) {
|
|
159
|
+
deliveries = appendUniqueDelivery(deliveries, {
|
|
160
|
+
deliveryId: `${deliveryId}:reply`,
|
|
161
|
+
direction: 'outbound',
|
|
162
|
+
deliveryType: 'reply',
|
|
163
|
+
fromAgentId: text(input.localAgentId, null),
|
|
164
|
+
commandText: replyText,
|
|
165
|
+
createdAt: text(input.replyCreatedAt, isoNow()),
|
|
166
|
+
turnCreatedAt: text(input.replyCreatedAt, isoNow()),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
230
169
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
170
|
+
const now = isoNow();
|
|
171
|
+
const deliveryIds = deliveries.map((entry) => text(entry?.deliveryId, null)).filter(Boolean);
|
|
172
|
+
index.conversationEpisodes[chatRequestId] = compactObject({
|
|
173
|
+
...previous,
|
|
174
|
+
chatRequestId,
|
|
175
|
+
chatId: text(input.chatId, previous.chatId),
|
|
176
|
+
lastActiveSessionKey: text(input.localSessionKey, previous.lastActiveSessionKey),
|
|
177
|
+
relaySessionKey: text(input.relaySessionKey, previous.relaySessionKey),
|
|
178
|
+
conversationKey: text(input.conversationKey, previous.conversationKey),
|
|
179
|
+
worldId: text(input.worldId, previous.worldId),
|
|
180
|
+
targetAgentId: text(input.targetAgentId, previous.targetAgentId),
|
|
181
|
+
fromAgentCode: text(input.fromAgentCode, previous.fromAgentCode),
|
|
182
|
+
fromDisplayIdentity: text(input.fromDisplayIdentity, previous.fromDisplayIdentity),
|
|
183
|
+
firstSeenAt: text(previous.firstSeenAt, text(input.createdAt, now)),
|
|
184
|
+
lastSeenAt: replyText
|
|
185
|
+
? text(input.replyCreatedAt, now)
|
|
186
|
+
: text(input.turnCreatedAt, text(input.createdAt, now)),
|
|
187
|
+
deliveryIds,
|
|
188
|
+
deliveryCount: deliveryIds.length,
|
|
189
|
+
deliveries,
|
|
190
|
+
updatedAt: now,
|
|
191
|
+
});
|
|
192
|
+
index.updatedAt = now;
|
|
193
|
+
await atomicWriteJson(sessionIndexPath(workspaceRoot), index);
|
|
194
|
+
return { ok: true, updated: true, chatRequestId, deliveryCount: deliveryIds.length };
|
|
236
195
|
}
|
|
237
196
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
197
|
+
function stripOperationalSuffix(content) {
|
|
198
|
+
const lines = String(content ?? '').split(/\r?\n/);
|
|
199
|
+
while (lines.length) {
|
|
200
|
+
const lastLine = String(lines.at(-1) ?? '').trim();
|
|
201
|
+
if (!lastLine) {
|
|
202
|
+
lines.pop();
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (!OPERATIONAL_SUFFIX_PATTERNS.some((pattern) => pattern.test(lastLine))) break;
|
|
206
|
+
lines.pop();
|
|
207
|
+
}
|
|
208
|
+
return lines.join('\n').trim();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function classifyReplyContent(content) {
|
|
212
|
+
const rawText = String(content ?? '');
|
|
213
|
+
const normalized = stripOperationalSuffix(rawText);
|
|
214
|
+
if (!normalized) return { text: '', silenceReason: rawText.trim() ? 'operational_notice_only' : 'empty_reply' };
|
|
215
|
+
if (normalized === 'NO_REPLY') return { text: '', silenceReason: 'no_reply' };
|
|
216
|
+
if (RUNTIME_ERROR_PATTERNS.some((pattern) => pattern.test(normalized))) {
|
|
217
|
+
return { text: '', silenceReason: 'runtime_failed_before_reply' };
|
|
218
|
+
}
|
|
219
|
+
if (OPERATIONAL_NOTICE_PATTERNS.some((pattern) => pattern.test(normalized))) {
|
|
220
|
+
return { text: '', silenceReason: 'operational_notice_only' };
|
|
221
|
+
}
|
|
222
|
+
return { text: normalized, silenceReason: null };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function renderableTranscriptDelivery(delivery) {
|
|
226
|
+
if (!isObject(delivery) || text(delivery.deliveryType, null) === 'kickoff') return false;
|
|
227
|
+
const commandText = text(delivery.commandText, null);
|
|
228
|
+
return Boolean(commandText && !classifyReplyContent(commandText).silenceReason);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function episodeSummary(chatRequestId, entry) {
|
|
232
|
+
const deliveries = Array.isArray(entry.deliveries) ? entry.deliveries : [];
|
|
233
|
+
const renderable = deliveries.filter(renderableTranscriptDelivery);
|
|
234
|
+
const peerMessages = renderable.filter((delivery) => text(delivery.direction, null) !== 'outbound').length;
|
|
235
|
+
return compactObject({
|
|
236
|
+
chatRequestId: text(entry.chatRequestId, chatRequestId),
|
|
237
|
+
chatId: entry.chatId,
|
|
238
|
+
conversationKey: entry.conversationKey,
|
|
239
|
+
relaySessionKey: entry.relaySessionKey,
|
|
240
|
+
lastActiveSessionKey: entry.lastActiveSessionKey,
|
|
241
|
+
worldId: entry.worldId,
|
|
242
|
+
targetAgentId: entry.targetAgentId,
|
|
243
|
+
fromAgentCode: entry.fromAgentCode,
|
|
244
|
+
fromDisplayIdentity: entry.fromDisplayIdentity,
|
|
245
|
+
firstSeenAt: entry.firstSeenAt,
|
|
246
|
+
lastSeenAt: entry.lastSeenAt,
|
|
247
|
+
deliveryCount: entry.deliveryCount,
|
|
248
|
+
renderableMessages: renderable.length,
|
|
249
|
+
peerMessages,
|
|
250
|
+
localMessages: renderable.length - peerMessages,
|
|
251
|
+
});
|
|
243
252
|
}
|
|
244
253
|
|
|
245
|
-
function
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
);
|
|
252
|
-
return {
|
|
253
|
-
baseDir,
|
|
254
|
-
imageDir: path.join(baseDir, 'images'),
|
|
255
|
-
documentDir: path.join(baseDir, 'documents'),
|
|
256
|
-
};
|
|
254
|
+
function localEpisodeSummaries(index) {
|
|
255
|
+
return Object.entries(isObject(index.conversationEpisodes) ? index.conversationEpisodes : {})
|
|
256
|
+
.filter(([, entry]) => isObject(entry))
|
|
257
|
+
.map(([chatRequestId, entry]) => episodeSummary(chatRequestId, entry))
|
|
258
|
+
.sort((left, right) => String(right.lastSeenAt || right.firstSeenAt || '')
|
|
259
|
+
.localeCompare(String(left.lastSeenAt || left.firstSeenAt || '')));
|
|
257
260
|
}
|
|
258
261
|
|
|
259
|
-
function
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
262
|
+
function matchesEpisodeFilters(episode, filters = {}) {
|
|
263
|
+
const checks = {
|
|
264
|
+
chatRequestId: 'chatRequestId',
|
|
265
|
+
conversationKey: 'conversationKey',
|
|
266
|
+
localSessionKey: 'relaySessionKey',
|
|
267
|
+
counterpartyAgentId: 'targetAgentId',
|
|
268
|
+
worldId: 'worldId',
|
|
269
|
+
};
|
|
270
|
+
return Object.entries(checks).every(([filterKey, episodeKey]) => {
|
|
271
|
+
const expected = text(filters[filterKey], null);
|
|
272
|
+
return !expected || text(episode[episodeKey], null) === expected;
|
|
273
|
+
});
|
|
269
274
|
}
|
|
270
275
|
|
|
271
|
-
function
|
|
272
|
-
|
|
273
|
-
if (!ms) return null;
|
|
274
|
-
return new Date(ms).toISOString();
|
|
276
|
+
function filterLocalEpisodes(episodes, filters = {}) {
|
|
277
|
+
return episodes.filter((episode) => matchesEpisodeFilters(episode, filters)).slice(0, 25);
|
|
275
278
|
}
|
|
276
279
|
|
|
277
|
-
function
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
280
|
+
function conversationItemFilters(item = {}) {
|
|
281
|
+
const related = isObject(item.relatedObjects) ? item.relatedObjects : {};
|
|
282
|
+
return compactObject({
|
|
283
|
+
chatRequestId: text(item.chatRequestId, text(related.chatRequestId, null)),
|
|
284
|
+
conversationKey: text(item.conversationKey, text(related.conversationKey, null)),
|
|
285
|
+
localSessionKey: text(item.localSessionKey, text(related.localSessionKey, null)),
|
|
286
|
+
counterpartyAgentId: text(item.counterpartyAgentId, text(related.counterpartyAgentId, null)),
|
|
287
|
+
worldId: text(item.worldId, text(related.worldId, null)),
|
|
288
|
+
});
|
|
286
289
|
}
|
|
287
290
|
|
|
288
|
-
function
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
291
|
+
export async function augmentConversationPayloadWithLocalTranscriptIndex({
|
|
292
|
+
workspaceRoot,
|
|
293
|
+
payload,
|
|
294
|
+
filters = {},
|
|
295
|
+
} = {}) {
|
|
296
|
+
if (!workspaceRoot || !isObject(payload)) return payload;
|
|
297
|
+
const index = await readSessionIndex(workspaceRoot);
|
|
298
|
+
const episodes = localEpisodeSummaries(index);
|
|
299
|
+
const matching = filterLocalEpisodes(episodes, filters);
|
|
300
|
+
const result = { ...payload };
|
|
301
|
+
if (matching.length) {
|
|
302
|
+
result.localTranscriptEpisodes = matching;
|
|
303
|
+
result.localTranscriptSummary = {
|
|
304
|
+
episodeCount: matching.length,
|
|
305
|
+
chatRequestIds: matching.map((item) => item.chatRequestId).filter(Boolean),
|
|
306
|
+
};
|
|
293
307
|
}
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
if (
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
308
|
+
if (Array.isArray(result.items)) {
|
|
309
|
+
result.items = result.items.map((item) => {
|
|
310
|
+
if (!isObject(item)) return item;
|
|
311
|
+
const itemFilters = conversationItemFilters(item);
|
|
312
|
+
if (!Object.keys(itemFilters).length) return item;
|
|
313
|
+
const itemMatches = filterLocalEpisodes(episodes, itemFilters);
|
|
314
|
+
if (!itemMatches.length) return item;
|
|
315
|
+
return {
|
|
316
|
+
...item,
|
|
317
|
+
localTranscriptEpisodes: itemMatches,
|
|
318
|
+
localTranscriptSummary: {
|
|
319
|
+
episodeCount: itemMatches.length,
|
|
320
|
+
chatRequestIds: itemMatches.map((match) => match.chatRequestId).filter(Boolean),
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
});
|
|
302
324
|
}
|
|
303
|
-
return
|
|
325
|
+
return result;
|
|
304
326
|
}
|
|
305
327
|
|
|
306
|
-
function
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
if (index < 0) return null;
|
|
310
|
-
const afterMarker = source.slice(index + marker.length).trimStart();
|
|
311
|
-
const fenceMatch = afterMarker.match(/^```[^\n]*\n([\s\S]*?)\n```/);
|
|
312
|
-
if (fenceMatch) return fenceMatch[1].trim();
|
|
313
|
-
const nextSection = afterMarker.search(/\n(?:Routing metadata|Backend-authored Claworld|Relay untrusted context|Claworld live conversation rules):/);
|
|
314
|
-
return (nextSection >= 0 ? afterMarker.slice(0, nextSection) : afterMarker).trim();
|
|
328
|
+
function rejectUnknown(name, value, allowed) {
|
|
329
|
+
const extra = Object.keys(value).filter((key) => !allowed.has(key)).sort();
|
|
330
|
+
if (extra.length) throw new Error(`unsupported ${name} parameter(s): ${extra.join(', ')}`);
|
|
315
331
|
}
|
|
316
332
|
|
|
317
|
-
function
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
'
|
|
323
|
-
'Relay untrusted context:',
|
|
324
|
-
'Claworld live conversation rules:',
|
|
325
|
-
];
|
|
326
|
-
for (const marker of markers) {
|
|
327
|
-
const index = output.indexOf(marker);
|
|
328
|
-
if (index >= 0) output = output.slice(0, index);
|
|
333
|
+
function normalizeRenderRequest(args = {}) {
|
|
334
|
+
if (!isObject(args)) throw new Error('render arguments must be an object');
|
|
335
|
+
rejectUnknown('transcript render', args, TOP_LEVEL_RENDER_FIELDS);
|
|
336
|
+
const mode = text(args.mode, null);
|
|
337
|
+
if (!['stored', 'manual'].includes(mode)) {
|
|
338
|
+
throw new Error('mode is required and must be one of stored or manual');
|
|
329
339
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function stripOpenClawSpeakerPrefix(text) {
|
|
335
|
-
const source = String(text || '').trim();
|
|
336
|
-
const match = source.match(/^([^\n:]{1,80}):\s+([\s\S]+)$/);
|
|
337
|
-
if (!match) return { text: source, speaker: null };
|
|
338
|
-
const label = match[1].trim();
|
|
339
|
-
if (/^(https?|file|data)$/i.test(label)) return { text: source, speaker: null };
|
|
340
|
-
return { text: match[2].trim(), speaker: label };
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function redactSensitiveText(text) {
|
|
344
|
-
let output = String(text || '');
|
|
345
|
-
output = output.replace(
|
|
346
|
-
/\bauthorization\b\s*[:=]\s*(?:bearer\s+)?`?[^`\s,;]+`?/gi,
|
|
347
|
-
'authorization=[REDACTED]',
|
|
348
|
-
);
|
|
349
|
-
output = output.replace(
|
|
350
|
-
/\b(api[_-]?key|app[_-]?token|access[_-]?token|refresh[_-]?token|secret|password)\b\s*[:=]\s*`?[^`\s,;]+`?/gi,
|
|
351
|
-
'$1=[REDACTED]',
|
|
352
|
-
);
|
|
353
|
-
output = output.replace(/\bbearer\s+[a-zA-Z0-9._~+/=-]{12,}/gi, 'bearer [REDACTED]');
|
|
354
|
-
output = output.replace(/\bsk-[a-zA-Z0-9_-]{12,}\b/g, '[REDACTED_TOKEN]');
|
|
355
|
-
output = output.replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, '[REDACTED_EMAIL]');
|
|
356
|
-
output = output.replace(/\+?\d[\d ()-]{8,}\d/g, '[REDACTED_PHONE]');
|
|
357
|
-
return output.trim();
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function normalizeTagLabel(value) {
|
|
361
|
-
const tag = normalizeText(value, null);
|
|
362
|
-
if (!tag) return null;
|
|
363
|
-
const compact = tag.toLowerCase().replace(/\s+/g, '_');
|
|
364
|
-
if (compact === 'request_conversation_end' || compact === 'request_end' || compact === 'end') {
|
|
365
|
-
return 'request end';
|
|
340
|
+
const style = text(args.style, CLAWORLD_TRANSCRIPT_STYLE_NAME);
|
|
341
|
+
if (style !== CLAWORLD_TRANSCRIPT_STYLE_NAME) {
|
|
342
|
+
throw new Error(`unsupported transcript report style: ${style}; expected ${CLAWORLD_TRANSCRIPT_STYLE_NAME}`);
|
|
366
343
|
}
|
|
367
|
-
|
|
368
|
-
if (
|
|
369
|
-
|
|
344
|
+
const renderArgs = { mode, style, maxPageHeight: args.maxPageHeight };
|
|
345
|
+
if (mode === 'stored') {
|
|
346
|
+
if (args.manual != null) throw new Error('manual must not be provided when mode=stored');
|
|
347
|
+
if (!isObject(args.stored)) throw new Error('stored must be an object when mode=stored');
|
|
348
|
+
rejectUnknown('stored', args.stored, STORED_RENDER_FIELDS);
|
|
349
|
+
const chatRequestId = text(args.stored.chatRequestId, null);
|
|
350
|
+
if (!chatRequestId) throw new Error('stored.chatRequestId is required when mode=stored');
|
|
351
|
+
for (const key of ['title', 'peerProfile', 'localLabel', 'peerLabel']) {
|
|
352
|
+
renderArgs[key] = args.stored[key];
|
|
353
|
+
}
|
|
354
|
+
return { mode, chatRequestId, renderArgs };
|
|
355
|
+
}
|
|
356
|
+
if (args.stored != null) throw new Error('stored must not be provided when mode=manual');
|
|
357
|
+
if (!isObject(args.manual)) throw new Error('manual must be an object when mode=manual');
|
|
358
|
+
rejectUnknown('manual', args.manual, MANUAL_RENDER_FIELDS);
|
|
359
|
+
for (const key of ['title', 'peerProfile', 'localLabel', 'peerLabel']) {
|
|
360
|
+
if (!text(args.manual[key], null)) throw new Error(`manual.${key} is required when mode=manual`);
|
|
361
|
+
renderArgs[key] = args.manual[key];
|
|
362
|
+
}
|
|
363
|
+
if (!Array.isArray(args.manual.messages) || !args.manual.messages.length) {
|
|
364
|
+
throw new Error('manual.messages must be a non-empty array when mode=manual');
|
|
365
|
+
}
|
|
366
|
+
args.manual.messages.forEach((message, index) => {
|
|
367
|
+
const position = index + 1;
|
|
368
|
+
if (!isObject(message)) throw new Error(`manual.messages[${position}] must be an object`);
|
|
369
|
+
rejectUnknown(`manual.messages[${position}]`, message, MANUAL_MESSAGE_FIELDS);
|
|
370
|
+
if (!['peer', 'local'].includes(text(message.from, null))) {
|
|
371
|
+
throw new Error(`manual.messages[${position}].from must be peer or local`);
|
|
372
|
+
}
|
|
373
|
+
if (!text(message.text, null)) throw new Error(`manual.messages[${position}].text is required`);
|
|
374
|
+
if (!text(message.createdAt, null)) throw new Error(`manual.messages[${position}].createdAt is required`);
|
|
375
|
+
});
|
|
376
|
+
return { mode, messages: args.manual.messages, renderArgs };
|
|
370
377
|
}
|
|
371
378
|
|
|
372
|
-
function
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const claimedSpans = [];
|
|
376
|
-
const tags = [];
|
|
377
|
-
const record = (start, end, label) => {
|
|
378
|
-
if (!label) return;
|
|
379
|
-
if (claimedSpans.some(([claimedStart, claimedEnd]) => start < claimedEnd && end > claimedStart)) return;
|
|
380
|
-
matches.push([start, end, label]);
|
|
381
|
-
claimedSpans.push([start, end]);
|
|
382
|
-
};
|
|
383
|
-
const controlPatterns = [
|
|
384
|
-
{ pattern: /\[\[?\s*request[_\s-]*(?:conversation[_\s-]*)?end\s*\]?\]?/gi, label: 'request end' },
|
|
385
|
-
{ pattern: /\[\s*requeset\s+end\s*\]/gi, label: 'request end' },
|
|
386
|
-
{ pattern: /\[\[?\s*end\s*\]?\]?/gi, label: 'request end' },
|
|
387
|
-
{ pattern: /\[\[?\s*like\s*\]?\]?/gi, label: 'like' },
|
|
388
|
-
{ pattern: /\[\[?\s*dislike\s*\]?\]?/gi, label: 'dislike' },
|
|
389
|
-
];
|
|
390
|
-
for (const { pattern, label } of controlPatterns) {
|
|
391
|
-
for (const match of source.matchAll(pattern)) record(match.index, match.index + match[0].length, label);
|
|
379
|
+
async function loadSourceMessages(request, workspaceRoot) {
|
|
380
|
+
if (request.mode === 'manual') {
|
|
381
|
+
return { messages: request.messages, summary: { kind: 'manual', messageCount: request.messages.length } };
|
|
392
382
|
}
|
|
393
|
-
|
|
394
|
-
|
|
383
|
+
const index = await readSessionIndex(workspaceRoot);
|
|
384
|
+
const episode = isObject(index.conversationEpisodes?.[request.chatRequestId])
|
|
385
|
+
? index.conversationEpisodes[request.chatRequestId]
|
|
386
|
+
: null;
|
|
387
|
+
if (!episode) {
|
|
388
|
+
throw new Error(`chatRequestId was not found in local Claworld transcript index: ${request.chatRequestId}`);
|
|
395
389
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
cleaned += source.slice(cursor, start);
|
|
400
|
-
cursor = Math.max(cursor, end);
|
|
401
|
-
if (!tags.includes(label)) tags.push(label);
|
|
402
|
-
}
|
|
403
|
-
cleaned += source.slice(cursor);
|
|
404
|
-
return {
|
|
405
|
-
text: cleaned.replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim(),
|
|
406
|
-
tags,
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function extractEpisodeIds(...values) {
|
|
411
|
-
const ids = new Set();
|
|
412
|
-
const source = values.map((value) => String(value || '')).join('\n');
|
|
413
|
-
const patterns = [
|
|
414
|
-
/(?:Chat Request ID|chatRequestId|requestId|Intent ID|intentId)\s*[:=]\s*`?([a-zA-Z0-9._:-]+)/gi,
|
|
415
|
-
/\b(?:chatRequestId|requestId|intentId)\b["']?\s*[:=]\s*["']?([a-zA-Z0-9._:-]+)/gi,
|
|
416
|
-
];
|
|
417
|
-
for (const pattern of patterns) {
|
|
418
|
-
for (const match of source.matchAll(pattern)) {
|
|
419
|
-
const id = normalizeText(match[1], null);
|
|
420
|
-
if (id) ids.add(id.replace(/[`,.;)\]}]+$/g, ''));
|
|
421
|
-
}
|
|
390
|
+
const deliveries = Array.isArray(episode.deliveries) ? episode.deliveries : [];
|
|
391
|
+
if (!deliveries.length) {
|
|
392
|
+
throw new Error(`chatRequestId was found but no deliveries were indexed: ${request.chatRequestId}`);
|
|
422
393
|
}
|
|
423
|
-
return [...ids];
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function getMessageText(record) {
|
|
427
|
-
if (record?.text != null) return flattenContent(record.text);
|
|
428
|
-
if (record?.body != null) return flattenContent(record.body);
|
|
429
|
-
if (record?.content != null) return flattenContent(record.content);
|
|
430
|
-
if (record?.message?.content != null) return flattenContent(record.message.content);
|
|
431
|
-
if (record?.payload?.content != null) return flattenContent(record.payload.content);
|
|
432
|
-
return '';
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
function getMessageRole(record) {
|
|
436
|
-
return normalizeText(
|
|
437
|
-
record?.role
|
|
438
|
-
?? record?.from
|
|
439
|
-
?? record?.message?.role
|
|
440
|
-
?? record?.payload?.role,
|
|
441
|
-
null,
|
|
442
|
-
);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function getMessageTimestamp(record) {
|
|
446
|
-
return record?.createdAt
|
|
447
|
-
?? record?.created_at
|
|
448
|
-
?? record?.timestamp
|
|
449
|
-
?? record?.message?.createdAt
|
|
450
|
-
?? record?.message?.timestamp
|
|
451
|
-
?? record?.payload?.createdAt
|
|
452
|
-
?? record?.payload?.timestamp
|
|
453
|
-
?? record?.__openclaw?.recordTimestampMs
|
|
454
|
-
?? record?.__openclaw?.recordTimestamp
|
|
455
|
-
?? null;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function normalizeManualMessage(raw, index) {
|
|
459
|
-
const tagsResult = extractControlTags(raw.text);
|
|
460
394
|
return {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
395
|
+
messages: deliveries,
|
|
396
|
+
summary: compactObject({
|
|
397
|
+
kind: 'chatRequestId',
|
|
398
|
+
chatRequestId: request.chatRequestId,
|
|
399
|
+
chatId: episode.chatId,
|
|
400
|
+
conversationKey: episode.conversationKey,
|
|
401
|
+
relaySessionKey: episode.relaySessionKey,
|
|
402
|
+
lastActiveSessionKey: episode.lastActiveSessionKey,
|
|
403
|
+
firstSeenAt: episode.firstSeenAt,
|
|
404
|
+
lastSeenAt: episode.lastSeenAt,
|
|
405
|
+
indexSource: 'conversationEpisodes',
|
|
406
|
+
}),
|
|
471
407
|
};
|
|
472
408
|
}
|
|
473
409
|
|
|
474
|
-
function
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
);
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
410
|
+
function markdownSection(value, title, level) {
|
|
411
|
+
const hashes = '#'.repeat(level);
|
|
412
|
+
const pattern = new RegExp(`^${hashes}\\s+${title}\\s*$`, 'imu');
|
|
413
|
+
const match = pattern.exec(value);
|
|
414
|
+
if (!match) return '';
|
|
415
|
+
const start = match.index + match[0].length;
|
|
416
|
+
const remainder = value.slice(start);
|
|
417
|
+
const nextHeading = new RegExp(`^#{1,${level}}\\s+`, 'mu').exec(remainder);
|
|
418
|
+
return remainder.slice(0, nextHeading?.index ?? remainder.length).trim();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function markdownNamedCodeBlock(value, title, level) {
|
|
422
|
+
const section = markdownSection(value, title, level);
|
|
423
|
+
if (!section) return '';
|
|
424
|
+
const fenced = /```[^\n]*\n([\s\S]*?)\n```/u.exec(section);
|
|
425
|
+
if (fenced) return fenced[1].trim();
|
|
426
|
+
return section.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith('#')).join('\n');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function squashWhitespace(value) {
|
|
430
|
+
const lines = String(value ?? '').split(/\r?\n/).map((line) => line.replace(/[ \t]+/gu, ' ').trim());
|
|
431
|
+
const compact = [];
|
|
432
|
+
let blank = false;
|
|
433
|
+
for (const line of lines) {
|
|
434
|
+
if (!line) {
|
|
435
|
+
if (!blank && compact.length) compact.push('');
|
|
436
|
+
blank = true;
|
|
497
437
|
} else {
|
|
498
|
-
|
|
438
|
+
compact.push(line);
|
|
439
|
+
blank = false;
|
|
499
440
|
}
|
|
500
|
-
} else {
|
|
501
|
-
text = stripInternalEnvelopeSections(text);
|
|
502
|
-
if (/^NO_REPLY$/i.test(text.trim())) return null;
|
|
503
441
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
.filter(Boolean)
|
|
532
|
-
.filter((message) => message.text || message.tags.length > 0)
|
|
533
|
-
.sort((left, right) => (left.timestampMs || 0) - (right.timestampMs || 0));
|
|
534
|
-
return messages;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function episodeSetsIntersect(left = [], right = []) {
|
|
538
|
-
if (!left.length || !right.length) return true;
|
|
539
|
-
const rightSet = new Set(right);
|
|
540
|
-
return left.some((id) => rightSet.has(id));
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
function segmentTranscriptMessages(messages = []) {
|
|
544
|
-
const segments = [];
|
|
545
|
-
let current = null;
|
|
546
|
-
for (const message of messages) {
|
|
547
|
-
const previous = current?.messages?.[current.messages.length - 1] || null;
|
|
548
|
-
const gapSeconds = previous && message.timestampMs && previous.timestampMs
|
|
549
|
-
? Math.abs(message.timestampMs - previous.timestampMs) / 1000
|
|
550
|
-
: 0;
|
|
551
|
-
const currentEpisodeIds = current ? [...current.episodeIds] : [];
|
|
552
|
-
const startsNewEpisode = current
|
|
553
|
-
&& message.episodeIds.length > 0
|
|
554
|
-
&& currentEpisodeIds.length > 0
|
|
555
|
-
&& !episodeSetsIntersect(message.episodeIds, currentEpisodeIds);
|
|
556
|
-
const startsAfterCompletion = previous
|
|
557
|
-
&& current?.endedSides?.size >= 2
|
|
558
|
-
&& message.side === 'peer';
|
|
559
|
-
if (!current || gapSeconds > SEGMENT_GAP_SECONDS || startsNewEpisode || startsAfterCompletion) {
|
|
560
|
-
current = {
|
|
561
|
-
messages: [],
|
|
562
|
-
episodeIds: new Set(),
|
|
563
|
-
endedSides: new Set(),
|
|
564
|
-
};
|
|
565
|
-
segments.push(current);
|
|
442
|
+
return compact.join('\n').trim();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function parseHeaderContextCandidate(value, source) {
|
|
446
|
+
const content = String(value ?? '').trim();
|
|
447
|
+
if (!content) return {};
|
|
448
|
+
const parsed = {};
|
|
449
|
+
const modeMatch = /^\s*-\s*Mode:\s*`?([A-Za-z_-]+)`?\s*$/imu.exec(content)
|
|
450
|
+
|| /\bconversation[_\s-]*mode\b\s*[:=]\s*`?([A-Za-z_-]+)`?/iu.exec(content);
|
|
451
|
+
const mode = text(modeMatch?.[1]?.toLowerCase(), null);
|
|
452
|
+
if (['world', 'direct'].includes(mode)) parsed.conversationMode = mode;
|
|
453
|
+
const worldMatch = /^\s*-\s*World:\s*([^\n(`]+?)\s*(?:\(`([^`]+)`\))?\s*$/imu.exec(content);
|
|
454
|
+
if (worldMatch) {
|
|
455
|
+
parsed.worldName = text(worldMatch[1], null);
|
|
456
|
+
parsed.worldId = text(worldMatch[2], null);
|
|
457
|
+
}
|
|
458
|
+
const localSection = markdownSection(content, 'You', 2);
|
|
459
|
+
const peerSection = markdownSection(content, 'Peer', 2);
|
|
460
|
+
const identity = (section) => text(/^\s*-\s*Identity:\s*`?([^`\n]+)`?\s*$/imu.exec(section)?.[1], null);
|
|
461
|
+
if (localSection) parsed.localIdentity = identity(localSection);
|
|
462
|
+
if (peerSection) {
|
|
463
|
+
parsed.peerIdentity = identity(peerSection);
|
|
464
|
+
const globalProfile = markdownNamedCodeBlock(peerSection, 'Global Profile', 3);
|
|
465
|
+
const worldProfile = markdownNamedCodeBlock(peerSection, 'World Membership Profile', 3);
|
|
466
|
+
if (globalProfile) {
|
|
467
|
+
parsed.globalProfile = squashWhitespace(globalProfile);
|
|
468
|
+
parsed.globalProfileSource = source;
|
|
566
469
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}
|
|
571
|
-
return segments;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
function selectStoredMessages(messages, chatRequestId) {
|
|
575
|
-
const targetId = normalizeText(chatRequestId, null);
|
|
576
|
-
if (!targetId) return messages;
|
|
577
|
-
const segments = segmentTranscriptMessages(messages);
|
|
578
|
-
const segment = segments.find((candidate) => candidate.episodeIds.has(targetId));
|
|
579
|
-
if (segment) return segment.messages;
|
|
580
|
-
const tagged = messages.filter((message) => message.episodeIds.includes(targetId));
|
|
581
|
-
return tagged.length > 0 ? tagged : messages;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
function parseJsonLine(line) {
|
|
585
|
-
try {
|
|
586
|
-
const parsed = JSON.parse(line);
|
|
587
|
-
return isObject(parsed) ? parsed : null;
|
|
588
|
-
} catch {
|
|
589
|
-
return null;
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
function extractTranscriptRecordsFromJson(value, fallbackId = 'json') {
|
|
594
|
-
if (Array.isArray(value)) return value;
|
|
595
|
-
if (!isObject(value)) return [];
|
|
596
|
-
for (const key of ['entries', 'records', 'messages', 'items', 'transcript']) {
|
|
597
|
-
if (Array.isArray(value[key])) return value[key];
|
|
598
|
-
}
|
|
599
|
-
if (Array.isArray(value.conversation?.messages)) return value.conversation.messages;
|
|
600
|
-
if (Array.isArray(value.session?.messages)) return value.session.messages;
|
|
601
|
-
if (isObject(value.message) || value.role || value.content || value.text) {
|
|
602
|
-
return [{ ...value, id: value.id ?? fallbackId }];
|
|
603
|
-
}
|
|
604
|
-
return [];
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
async function loadTranscriptRecords(transcriptPath) {
|
|
608
|
-
const content = await fs.readFile(transcriptPath, 'utf8');
|
|
609
|
-
const trimmed = content.trim();
|
|
610
|
-
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
611
|
-
try {
|
|
612
|
-
return extractTranscriptRecordsFromJson(JSON.parse(trimmed), path.basename(transcriptPath));
|
|
613
|
-
} catch {
|
|
614
|
-
// Fall back to line-oriented parsing below.
|
|
470
|
+
if (worldProfile) {
|
|
471
|
+
parsed.worldProfile = squashWhitespace(worldProfile);
|
|
472
|
+
parsed.worldProfileSource = source;
|
|
615
473
|
}
|
|
616
474
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
const parsed = parseJsonLine(line);
|
|
623
|
-
if (!parsed) continue;
|
|
624
|
-
if (isObject(parsed.message)) {
|
|
625
|
-
records.push({
|
|
626
|
-
...parsed.message,
|
|
627
|
-
id: parsed.message.id ?? parsed.id ?? `line-${index}`,
|
|
628
|
-
timestamp: parsed.message.timestamp ?? parsed.timestamp ?? parsed.createdAt ?? null,
|
|
629
|
-
__openclaw: {
|
|
630
|
-
id: parsed.id ?? null,
|
|
631
|
-
recordTimestamp: parsed.timestamp ?? null,
|
|
632
|
-
seq: index,
|
|
633
|
-
},
|
|
634
|
-
});
|
|
635
|
-
continue;
|
|
636
|
-
}
|
|
637
|
-
if (parsed.type === 'response_item' && isObject(parsed.payload) && parsed.payload.type === 'message') {
|
|
638
|
-
records.push({
|
|
639
|
-
...parsed.payload,
|
|
640
|
-
id: parsed.payload.id ?? parsed.id ?? `line-${index}`,
|
|
641
|
-
timestamp: parsed.payload.timestamp ?? parsed.timestamp ?? null,
|
|
642
|
-
__openclaw: {
|
|
643
|
-
id: parsed.id ?? null,
|
|
644
|
-
recordTimestamp: parsed.timestamp ?? null,
|
|
645
|
-
seq: index,
|
|
646
|
-
},
|
|
647
|
-
});
|
|
648
|
-
continue;
|
|
649
|
-
}
|
|
650
|
-
if (parsed.type === 'message' || parsed.role || parsed.content || parsed.text) {
|
|
651
|
-
records.push({
|
|
652
|
-
...parsed,
|
|
653
|
-
id: parsed.id ?? `line-${index}`,
|
|
654
|
-
timestamp: parsed.timestamp ?? parsed.createdAt ?? null,
|
|
655
|
-
__openclaw: {
|
|
656
|
-
id: parsed.id ?? null,
|
|
657
|
-
recordTimestamp: parsed.timestamp ?? null,
|
|
658
|
-
seq: index,
|
|
659
|
-
},
|
|
660
|
-
});
|
|
475
|
+
if (!localSection && !peerSection && source === 'untrustedContext') {
|
|
476
|
+
const plainProfile = plainProfileCandidate(content);
|
|
477
|
+
if (plainProfile) {
|
|
478
|
+
parsed.globalProfile = plainProfile;
|
|
479
|
+
parsed.globalProfileSource = source;
|
|
661
480
|
}
|
|
662
481
|
}
|
|
663
|
-
return
|
|
482
|
+
return compactObject(parsed);
|
|
664
483
|
}
|
|
665
484
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
} catch {
|
|
671
|
-
return false;
|
|
672
|
-
}
|
|
485
|
+
function plainProfileCandidate(value) {
|
|
486
|
+
const candidate = squashWhitespace(value);
|
|
487
|
+
if (!candidate || candidate.includes('# ') || candidate.includes('```')) return '';
|
|
488
|
+
return displayCols(candidate) <= 420 ? candidate : '';
|
|
673
489
|
}
|
|
674
490
|
|
|
675
|
-
function
|
|
676
|
-
return [
|
|
677
|
-
normalizeText(artifact.transcriptPath, null),
|
|
678
|
-
normalizeText(artifact.sessionFile, null),
|
|
679
|
-
].filter(Boolean);
|
|
491
|
+
function headerProfileSourcePriority(source) {
|
|
492
|
+
return { contextText: 4, untrustedContext: 3, rawKickoffText: 2, transcript: 1 }[source] || 0;
|
|
680
493
|
}
|
|
681
494
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
495
|
+
function mergeHeaderContextCandidate(merged, candidate, source) {
|
|
496
|
+
if (!candidate) return;
|
|
497
|
+
const parsed = parseHeaderContextCandidate(candidate, source);
|
|
498
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
499
|
+
if (!text(value, null)) continue;
|
|
500
|
+
const sourceKey = {
|
|
501
|
+
globalProfile: 'globalProfileSource',
|
|
502
|
+
worldProfile: 'worldProfileSource',
|
|
503
|
+
globalProfileSource: 'globalProfileSource',
|
|
504
|
+
worldProfileSource: 'worldProfileSource',
|
|
505
|
+
}[key];
|
|
506
|
+
if (!sourceKey) {
|
|
507
|
+
merged[key] = value;
|
|
508
|
+
} else {
|
|
509
|
+
const incomingSource = key.endsWith('Source') ? value : parsed[sourceKey];
|
|
510
|
+
if (headerProfileSourcePriority(incomingSource) >= headerProfileSourcePriority(merged[sourceKey])) {
|
|
511
|
+
merged[key] = value;
|
|
691
512
|
}
|
|
692
513
|
}
|
|
693
514
|
}
|
|
694
|
-
const fallback = ordered.find((artifact) => artifactPathCandidates(artifact).length > 0);
|
|
695
|
-
if (!fallback) return null;
|
|
696
|
-
return {
|
|
697
|
-
artifact: fallback,
|
|
698
|
-
transcriptPath: artifactPathCandidates(fallback)[0],
|
|
699
|
-
};
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
function summarizeDirectoryRequest({
|
|
703
|
-
chatRequestId,
|
|
704
|
-
localSessionKey,
|
|
705
|
-
session = {},
|
|
706
|
-
request = {},
|
|
707
|
-
artifact = {},
|
|
708
|
-
transcriptPath = null,
|
|
709
|
-
} = {}) {
|
|
710
|
-
return {
|
|
711
|
-
chatRequestId,
|
|
712
|
-
localSessionKey,
|
|
713
|
-
relaySessionKey: normalizeText(session.relaySessionKey, null),
|
|
714
|
-
conversationKey: normalizeText(session.conversationKey, null),
|
|
715
|
-
worldId: normalizeText(session.worldId, null),
|
|
716
|
-
localAgentId: normalizeText(session.localAgentId, null),
|
|
717
|
-
firstSeenAt: normalizeText(request.firstSeenAt, null),
|
|
718
|
-
lastSeenAt: normalizeText(request.lastSeenAt, null),
|
|
719
|
-
artifactCount: Array.isArray(request.artifacts) ? request.artifacts.length : 0,
|
|
720
|
-
sessionId: normalizeText(artifact.sessionId, null),
|
|
721
|
-
sessionFile: normalizeText(artifact.sessionFile, null),
|
|
722
|
-
transcriptPath: normalizeText(transcriptPath || artifact.transcriptPath, null),
|
|
723
|
-
deliveryId: normalizeText(artifact.deliveryId, null),
|
|
724
|
-
sourceEventId: normalizeText(artifact.sourceEventId, null),
|
|
725
|
-
};
|
|
726
515
|
}
|
|
727
516
|
|
|
728
|
-
|
|
729
|
-
const
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
517
|
+
function extractTranscriptHeaderContext(rawMessages) {
|
|
518
|
+
const merged = {};
|
|
519
|
+
for (const raw of rawMessages) {
|
|
520
|
+
if (!isObject(raw)) continue;
|
|
521
|
+
mergeHeaderContextCandidate(merged, text(raw.contextText, null), 'contextText');
|
|
522
|
+
mergeHeaderContextCandidate(merged, text(raw.untrustedContext, null), 'untrustedContext');
|
|
523
|
+
if (text(raw.deliveryType, null) === 'kickoff') {
|
|
524
|
+
mergeHeaderContextCandidate(merged, text(raw.commandText, null), 'rawKickoffText');
|
|
525
|
+
}
|
|
526
|
+
if (!merged.peerIdentity && text(raw.fromDisplayIdentity, null)) {
|
|
527
|
+
merged.peerIdentity = text(raw.fromDisplayIdentity, null);
|
|
738
528
|
}
|
|
739
|
-
return {
|
|
740
|
-
mode: 'stored',
|
|
741
|
-
chatRequestId,
|
|
742
|
-
transcriptPath: readable.transcriptPath,
|
|
743
|
-
sessionDirectoryPath: sessionDirectory.sessionDirectoryPath,
|
|
744
|
-
summary: summarizeDirectoryRequest({
|
|
745
|
-
chatRequestId,
|
|
746
|
-
localSessionKey,
|
|
747
|
-
session,
|
|
748
|
-
request,
|
|
749
|
-
artifact: readable.artifact,
|
|
750
|
-
transcriptPath: readable.transcriptPath,
|
|
751
|
-
}),
|
|
752
|
-
};
|
|
753
|
-
}
|
|
754
|
-
inputError('stored.chatRequestId', `chatRequestId=${chatRequestId} was not found in .claworld/sessions/index.json`);
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
function normalizeHeaderContext({ mode, manual = null, source = null, messages = [] } = {}) {
|
|
758
|
-
if (mode === 'manual') {
|
|
759
|
-
return {
|
|
760
|
-
title: manual.title,
|
|
761
|
-
subtitle: manual.peerProfile,
|
|
762
|
-
peerLabel: manual.peerLabel,
|
|
763
|
-
localLabel: manual.localLabel,
|
|
764
|
-
};
|
|
765
529
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|| '
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
||
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|| code === 0x232a
|
|
793
|
-
|| (code >= 0x2e80 && code <= 0xa4cf)
|
|
794
|
-
|| (code >= 0xac00 && code <= 0xd7a3)
|
|
795
|
-
|| (code >= 0xf900 && code <= 0xfaff)
|
|
796
|
-
|| (code >= 0xfe10 && code <= 0xfe19)
|
|
797
|
-
|| (code >= 0xfe30 && code <= 0xfe6f)
|
|
798
|
-
|| (code >= 0xff00 && code <= 0xff60)
|
|
799
|
-
|| (code >= 0xffe0 && code <= 0xffe6)
|
|
800
|
-
|| (code >= 0x1f300 && code <= 0x1faff)
|
|
801
|
-
|| (code >= 0x20000 && code <= 0x3fffd)
|
|
802
|
-
);
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
function charColumns(char) {
|
|
806
|
-
if (char === '\n') return 0;
|
|
807
|
-
return isWideChar(char) ? 2 : 1;
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
function displayColumns(text) {
|
|
811
|
-
let columns = 0;
|
|
812
|
-
for (const char of String(text || '')) columns += charColumns(char);
|
|
813
|
-
return columns;
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
function clipDisplay(text, maxColumns) {
|
|
817
|
-
const value = String(text || '');
|
|
818
|
-
if (displayColumns(value) <= maxColumns) return value;
|
|
819
|
-
const suffix = '...';
|
|
820
|
-
const target = Math.max(0, maxColumns - suffix.length);
|
|
821
|
-
let kept = '';
|
|
822
|
-
let used = 0;
|
|
823
|
-
for (const char of value) {
|
|
824
|
-
const columns = charColumns(char);
|
|
825
|
-
if (used + columns > target) break;
|
|
826
|
-
kept += char;
|
|
827
|
-
used += columns;
|
|
530
|
+
let peerProfile = '';
|
|
531
|
+
let profileSource = '';
|
|
532
|
+
if (merged.conversationMode === 'world' && merged.worldProfile) {
|
|
533
|
+
peerProfile = merged.worldProfile;
|
|
534
|
+
profileSource = merged.worldProfileSource || 'transcript';
|
|
535
|
+
} else if (merged.conversationMode === 'direct' && merged.globalProfile) {
|
|
536
|
+
peerProfile = merged.globalProfile;
|
|
537
|
+
profileSource = merged.globalProfileSource || 'transcript';
|
|
538
|
+
} else if (merged.worldProfile) {
|
|
539
|
+
peerProfile = merged.worldProfile;
|
|
540
|
+
profileSource = merged.worldProfileSource || 'transcript';
|
|
541
|
+
} else if (merged.globalProfile) {
|
|
542
|
+
peerProfile = merged.globalProfile;
|
|
543
|
+
profileSource = merged.globalProfileSource || 'transcript';
|
|
544
|
+
}
|
|
545
|
+
return compactObject({ ...merged, peerProfile, profileSource });
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function formatTimestamp(value) {
|
|
549
|
+
if (value == null || value === '') return '';
|
|
550
|
+
let date = null;
|
|
551
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
552
|
+
date = new Date(value * 1000);
|
|
553
|
+
} else {
|
|
554
|
+
const parsed = new Date(String(value).trim());
|
|
555
|
+
if (!Number.isNaN(parsed.getTime())) date = parsed;
|
|
828
556
|
}
|
|
829
|
-
return
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
function charUnits(char) {
|
|
833
|
-
if (char === '\n') return 0;
|
|
834
|
-
if (/\s/u.test(char)) return 0.35;
|
|
835
|
-
return isWideChar(char) ? 1 : 0.55;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
function textUnits(text) {
|
|
839
|
-
let units = 0;
|
|
840
|
-
for (const char of String(text || '')) units += charUnits(char);
|
|
841
|
-
return units;
|
|
557
|
+
if (!date) return String(value).trim();
|
|
558
|
+
const pad = (part) => String(part).padStart(2, '0');
|
|
559
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
842
560
|
}
|
|
843
561
|
|
|
844
|
-
function
|
|
845
|
-
|
|
846
|
-
if (
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
for (const char of value) {
|
|
851
|
-
const units = charUnits(char);
|
|
852
|
-
if (used + units > allowed) break;
|
|
853
|
-
kept += char;
|
|
854
|
-
used += units;
|
|
855
|
-
}
|
|
856
|
-
return `${kept.trimEnd()}${suffix}`;
|
|
562
|
+
function parseTimeish(value) {
|
|
563
|
+
if (value == null || value === '') return null;
|
|
564
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
565
|
+
if (/^\d+(?:\.\d+)?$/u.test(String(value).trim())) return Number(value);
|
|
566
|
+
const timestamp = Date.parse(String(value).trim());
|
|
567
|
+
return Number.isNaN(timestamp) ? null : timestamp / 1000;
|
|
857
568
|
}
|
|
858
569
|
|
|
859
|
-
function
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
570
|
+
function extractControlTags(value) {
|
|
571
|
+
const matches = [];
|
|
572
|
+
const claimed = [];
|
|
573
|
+
const patterns = [
|
|
574
|
+
[/\[\[?\s*request[_\s-]*(?:conversation[_\s-]*)?end\s*\]?\]?/giu, 'request end'],
|
|
575
|
+
[/\[\s*requeset\s+end\s*\]/giu, 'request end'],
|
|
576
|
+
[/\[\[?\s*end\s*\]?\]?/giu, 'request end'],
|
|
577
|
+
[/\[\[?\s*like\s*\]?\]?/giu, 'like'],
|
|
578
|
+
[/\[\[?\s*dislike\s*\]?\]?/giu, 'dislike'],
|
|
579
|
+
];
|
|
580
|
+
const record = (start, end, label) => {
|
|
581
|
+
if (claimed.some(([claimedStart, claimedEnd]) => start < claimedEnd && end > claimedStart)) return;
|
|
582
|
+
claimed.push([start, end]);
|
|
583
|
+
matches.push([start, end, label]);
|
|
867
584
|
};
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
tokens.push(' ');
|
|
872
|
-
} else if (isWideChar(char)) {
|
|
873
|
-
flush();
|
|
874
|
-
tokens.push(char);
|
|
875
|
-
} else {
|
|
876
|
-
current += char;
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
flush();
|
|
880
|
-
return tokens;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
function wrapText(text, maxUnits) {
|
|
884
|
-
const lines = [];
|
|
885
|
-
const paragraphs = String(text || '').replace(/\r/g, '').split('\n');
|
|
886
|
-
for (const paragraph of paragraphs.length ? paragraphs : ['']) {
|
|
887
|
-
let current = '';
|
|
888
|
-
let currentUnits = 0;
|
|
889
|
-
let paragraphHasLine = false;
|
|
890
|
-
for (const token of wrapTokens(paragraph)) {
|
|
891
|
-
const tokenUnits = textUnits(token);
|
|
892
|
-
if (/^\s+$/u.test(token)) {
|
|
893
|
-
if (current && currentUnits + tokenUnits <= maxUnits) {
|
|
894
|
-
current += token;
|
|
895
|
-
currentUnits += tokenUnits;
|
|
896
|
-
}
|
|
897
|
-
continue;
|
|
898
|
-
}
|
|
899
|
-
if (current && currentUnits + tokenUnits > maxUnits) {
|
|
900
|
-
lines.push(current.trimEnd());
|
|
901
|
-
paragraphHasLine = true;
|
|
902
|
-
current = '';
|
|
903
|
-
currentUnits = 0;
|
|
904
|
-
}
|
|
905
|
-
if (tokenUnits > maxUnits) {
|
|
906
|
-
for (const char of token) {
|
|
907
|
-
const units = charUnits(char);
|
|
908
|
-
if (current && currentUnits + units > maxUnits) {
|
|
909
|
-
lines.push(current.trimEnd());
|
|
910
|
-
paragraphHasLine = true;
|
|
911
|
-
current = '';
|
|
912
|
-
currentUnits = 0;
|
|
913
|
-
}
|
|
914
|
-
current += char;
|
|
915
|
-
currentUnits += units;
|
|
916
|
-
}
|
|
917
|
-
} else {
|
|
918
|
-
current += token;
|
|
919
|
-
currentUnits += tokenUnits;
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
if (current || !paragraphHasLine) lines.push(current.trimEnd());
|
|
585
|
+
const source = String(value ?? '');
|
|
586
|
+
for (const [pattern, label] of patterns) {
|
|
587
|
+
for (const match of source.matchAll(pattern)) record(match.index, match.index + match[0].length, label);
|
|
923
588
|
}
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
function formatCompactTimestamp(value) {
|
|
928
|
-
const ms = parseTimestampMs(value);
|
|
929
|
-
if (ms) {
|
|
930
|
-
const date = new Date(ms);
|
|
931
|
-
const pad = (part) => String(part).padStart(2, '0');
|
|
932
|
-
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
589
|
+
for (const match of source.matchAll(/\[\[\s*([A-Za-z0-9][A-Za-z0-9 _-]{0,24})\s*\]\]/gu)) {
|
|
590
|
+
const label = squashWhitespace(match[1].replaceAll('_', ' ').replaceAll('-', ' ')).toLowerCase().slice(0, 24).trim();
|
|
591
|
+
if (label) record(match.index, match.index + match[0].length, label);
|
|
933
592
|
}
|
|
934
|
-
const
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
|
|
593
|
+
const sorted = matches.sort((left, right) => left[0] - right[0]);
|
|
594
|
+
const tags = [...new Set(sorted.map(([, , label]) => label))];
|
|
595
|
+
let cursor = 0;
|
|
596
|
+
const pieces = [];
|
|
597
|
+
for (const [start, end] of sorted) {
|
|
598
|
+
pieces.push(source.slice(cursor, start));
|
|
599
|
+
cursor = Math.max(cursor, end);
|
|
938
600
|
}
|
|
939
|
-
|
|
601
|
+
pieces.push(source.slice(cursor));
|
|
602
|
+
return { text: squashWhitespace(pieces.join('')), tags };
|
|
940
603
|
}
|
|
941
604
|
|
|
942
|
-
function
|
|
943
|
-
return
|
|
944
|
-
|
|
945
|
-
|
|
605
|
+
function redactText(value) {
|
|
606
|
+
return String(value ?? '')
|
|
607
|
+
.replace(/\b(api[_-]?key|app[_-]?token|access[_-]?token|refresh[_-]?token|secret|password|authorization|bearer)\b\s*[:=]\s*[^\s,;]+/giu, '$1=[redacted]')
|
|
608
|
+
.replace(/\b(?:sk|rk|pk|ghp|glpat)-[A-Za-z0-9_-]{12,}\b/gu, '[redacted-token]')
|
|
609
|
+
.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/gu, '[redacted-email]')
|
|
610
|
+
.replace(/(?<!\d)(?:\+?\d[\d\s().-]{8,}\d)(?!\d)/gu, '[redacted-phone]');
|
|
946
611
|
}
|
|
947
612
|
|
|
948
|
-
function
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
613
|
+
function stripInternalMarkup(value) {
|
|
614
|
+
let cleaned = String(value ?? '').replace(/```(?:json|text|markdown)?/giu, '').replaceAll('```', '');
|
|
615
|
+
for (const marker of [
|
|
616
|
+
'Routing metadata:',
|
|
617
|
+
'Claworld live conversation rules:',
|
|
618
|
+
'Backend-authored Claworld context:',
|
|
619
|
+
'Backend-authored Claworld command:',
|
|
620
|
+
'Relay untrusted context:',
|
|
621
|
+
]) {
|
|
622
|
+
if (cleaned.includes(marker)) cleaned = cleaned.split(marker, 1)[0];
|
|
623
|
+
}
|
|
624
|
+
return squashWhitespace(cleaned);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function normalizeMessages(rawMessages, localAgentId, args, headerContext = {}) {
|
|
628
|
+
const localIdentity = text(headerContext.localIdentity, null);
|
|
629
|
+
const peerIdentity = text(headerContext.peerIdentity, text(headerContext.peerId, null));
|
|
630
|
+
const localId = text(localIdentity, text(localAgentId, 'local-agent'));
|
|
631
|
+
const peerId = text(peerIdentity, 'peer-agent');
|
|
632
|
+
const localLabel = publicHeaderValue(args.localLabel) || publicHeaderValue(localIdentity) || 'Me';
|
|
633
|
+
const peerLabel = publicHeaderValue(args.peerLabel) || publicHeaderValue(peerIdentity) || 'Peer';
|
|
634
|
+
const normalized = [];
|
|
635
|
+
rawMessages.forEach((raw, index) => {
|
|
636
|
+
if (!isObject(raw)) return;
|
|
637
|
+
let side;
|
|
638
|
+
let messageText;
|
|
639
|
+
let createdAt;
|
|
640
|
+
let messageId;
|
|
641
|
+
let participantId;
|
|
642
|
+
let participantLabel;
|
|
643
|
+
if (['peer', 'local'].includes(raw.from)) {
|
|
644
|
+
side = raw.from === 'peer' ? 'left' : 'right';
|
|
645
|
+
messageText = text(raw.text, null);
|
|
646
|
+
createdAt = formatTimestamp(raw.createdAt);
|
|
647
|
+
messageId = text(raw.id, `msg-${index + 1}`);
|
|
648
|
+
participantId = side === 'left' ? peerId : localId;
|
|
649
|
+
participantLabel = side === 'left' ? peerLabel : localLabel;
|
|
650
|
+
} else {
|
|
651
|
+
if (text(raw.deliveryType, null) === 'kickoff') return;
|
|
652
|
+
const classification = classifyReplyContent(text(raw.commandText, ''));
|
|
653
|
+
if (classification.silenceReason) return;
|
|
654
|
+
messageText = classification.text;
|
|
655
|
+
createdAt = formatTimestamp(raw.turnCreatedAt || raw.createdAt);
|
|
656
|
+
messageId = text(raw.deliveryId, `msg-${index + 1}`);
|
|
657
|
+
side = text(raw.direction, null) === 'outbound'
|
|
658
|
+
|| (!text(raw.direction, null) && text(raw.fromAgentId, null) === text(localAgentId, null))
|
|
659
|
+
? 'right'
|
|
660
|
+
: 'left';
|
|
661
|
+
participantId = side === 'left' ? peerId : localId;
|
|
662
|
+
participantLabel = side === 'left' ? peerLabel : localLabel;
|
|
960
663
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
664
|
+
if (!messageText) return;
|
|
665
|
+
const extracted = extractControlTags(messageText);
|
|
666
|
+
const cleanedText = stripInternalMarkup(redactText(extracted.text));
|
|
667
|
+
if (!cleanedText && !extracted.tags.length) return;
|
|
668
|
+
normalized.push({
|
|
669
|
+
id: messageId,
|
|
670
|
+
side,
|
|
671
|
+
participantId,
|
|
672
|
+
participantLabel,
|
|
673
|
+
text: cleanedText,
|
|
674
|
+
createdAt,
|
|
675
|
+
tags: extracted.tags,
|
|
676
|
+
sourceIndex: index,
|
|
964
677
|
});
|
|
965
|
-
previousMessage = message;
|
|
966
|
-
}
|
|
967
|
-
return items;
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
function renderSide(message = {}) {
|
|
971
|
-
return message.side === 'local' || message.side === 'right' || message.side === 'me'
|
|
972
|
-
? 'right'
|
|
973
|
-
: 'left';
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
function participantLabel(message = {}, header = {}) {
|
|
977
|
-
if (renderSide(message) === 'right') return header.localLabel || 'You';
|
|
978
|
-
return message.speaker || header.peerLabel || 'Peer';
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
function messageTags(message = {}) {
|
|
982
|
-
return Array.isArray(message.tags) ? message.tags : [];
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
function measureRenderItem(item, width, header) {
|
|
986
|
-
const contentWidth = Math.max(270, Math.trunc(width * BUBBLE_MAX_RATIO));
|
|
987
|
-
if (item.kind === 'ellipsis') {
|
|
988
|
-
return {
|
|
989
|
-
kind: 'ellipsis',
|
|
990
|
-
message: null,
|
|
991
|
-
lines: [],
|
|
992
|
-
width: contentWidth,
|
|
993
|
-
height: ELLIPSIS_HEIGHT,
|
|
994
|
-
omittedCount: item.omitted || 0,
|
|
995
|
-
label: item.label || '',
|
|
996
|
-
};
|
|
997
|
-
}
|
|
998
|
-
if (item.kind === 'time') {
|
|
999
|
-
return {
|
|
1000
|
-
kind: 'time',
|
|
1001
|
-
message: null,
|
|
1002
|
-
lines: [],
|
|
1003
|
-
width: contentWidth,
|
|
1004
|
-
height: TIME_ROW_HEIGHT,
|
|
1005
|
-
label: item.label || '',
|
|
1006
|
-
};
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
const side = renderSide(item.message);
|
|
1010
|
-
const label = participantLabel(item.message, header);
|
|
1011
|
-
const maxUnits = Math.max(18, (contentWidth - BUBBLE_PAD_X * 2) / TEXT_UNIT_PX);
|
|
1012
|
-
const lines = wrapText(item.message.text, maxUnits);
|
|
1013
|
-
const labelUnits = textUnits(labelText(label)) + 4;
|
|
1014
|
-
const contentUnits = Math.max(
|
|
1015
|
-
...lines.map((line) => textUnits(line)),
|
|
1016
|
-
tagRowUnits(messageTags(item.message)),
|
|
1017
|
-
labelUnits,
|
|
1018
|
-
10,
|
|
1019
|
-
);
|
|
1020
|
-
const bubbleWidth = Math.trunc(Math.min(
|
|
1021
|
-
contentWidth,
|
|
1022
|
-
Math.max(BUBBLE_MIN_WIDTH, contentUnits * TEXT_UNIT_PX + BUBBLE_PAD_X * 2),
|
|
1023
|
-
));
|
|
1024
|
-
const textHeight = lines.length * LINE_HEIGHT;
|
|
1025
|
-
const tagHeight = messageTags(item.message).length ? TAG_HEIGHT : 0;
|
|
1026
|
-
const bubbleHeight = BUBBLE_PAD_Y * 2 + textHeight + tagHeight;
|
|
1027
|
-
const rowHeight = LABEL_HEIGHT - LABEL_OVERLAP + bubbleHeight + 10;
|
|
1028
|
-
return {
|
|
1029
|
-
kind: 'message',
|
|
1030
|
-
message: item.message,
|
|
1031
|
-
side,
|
|
1032
|
-
participantLabel: label,
|
|
1033
|
-
lines,
|
|
1034
|
-
width: bubbleWidth,
|
|
1035
|
-
height: rowHeight,
|
|
1036
|
-
tagHeight,
|
|
1037
|
-
textHeight,
|
|
1038
|
-
};
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
function layoutPages({ messages, header, maxPageHeight }) {
|
|
1042
|
-
const width = DEFAULT_WIDTH;
|
|
1043
|
-
const maxHeight = maxPageHeight || DEFAULT_MAX_PAGE_HEIGHT;
|
|
1044
|
-
const measuredItems = buildRenderItems(messages).map((item) => measureRenderItem(item, width, header));
|
|
1045
|
-
const pageBuckets = [[]];
|
|
1046
|
-
const headerHeight = headerHeightForSubtitle(header.subtitle);
|
|
1047
|
-
let used = headerHeight + BODY_TOP_GAP + PAGE_BOTTOM;
|
|
1048
|
-
|
|
1049
|
-
measuredItems.forEach((item, index) => {
|
|
1050
|
-
const itemHeight = item.height + ITEM_GAP;
|
|
1051
|
-
let neededHeight = itemHeight;
|
|
1052
|
-
if (item.kind === 'time' && index + 1 < measuredItems.length) {
|
|
1053
|
-
neededHeight += measuredItems[index + 1].height + ITEM_GAP;
|
|
1054
|
-
}
|
|
1055
|
-
const currentPage = pageBuckets[pageBuckets.length - 1];
|
|
1056
|
-
if (currentPage.length > 0 && used + neededHeight > maxHeight) {
|
|
1057
|
-
pageBuckets.push([]);
|
|
1058
|
-
used = headerHeight + BODY_TOP_GAP + PAGE_BOTTOM;
|
|
1059
|
-
}
|
|
1060
|
-
pageBuckets[pageBuckets.length - 1].push(item);
|
|
1061
|
-
used += itemHeight;
|
|
1062
|
-
});
|
|
1063
|
-
|
|
1064
|
-
const pages = pageBuckets.map((pageItems, pageIndex) => {
|
|
1065
|
-
let y = headerHeight + BODY_TOP_GAP;
|
|
1066
|
-
const elements = [];
|
|
1067
|
-
for (const item of pageItems) {
|
|
1068
|
-
if (item.kind === 'ellipsis') {
|
|
1069
|
-
elements.push({
|
|
1070
|
-
kind: 'ellipsis',
|
|
1071
|
-
y,
|
|
1072
|
-
height: item.height,
|
|
1073
|
-
label: item.label,
|
|
1074
|
-
});
|
|
1075
|
-
y += item.height + ITEM_GAP;
|
|
1076
|
-
continue;
|
|
1077
|
-
}
|
|
1078
|
-
if (item.kind === 'time') {
|
|
1079
|
-
elements.push({
|
|
1080
|
-
kind: 'time',
|
|
1081
|
-
y,
|
|
1082
|
-
height: item.height,
|
|
1083
|
-
label: item.label,
|
|
1084
|
-
});
|
|
1085
|
-
y += item.height + ITEM_GAP;
|
|
1086
|
-
continue;
|
|
1087
|
-
}
|
|
1088
|
-
const label = labelText(item.participantLabel);
|
|
1089
|
-
const position = positions(width, item.width, label, item.side);
|
|
1090
|
-
const bubbleY = y + LABEL_HEIGHT - LABEL_OVERLAP;
|
|
1091
|
-
const bubbleHeight = BUBBLE_PAD_Y * 2 + item.textHeight + item.tagHeight;
|
|
1092
|
-
elements.push({
|
|
1093
|
-
kind: 'message',
|
|
1094
|
-
y,
|
|
1095
|
-
x: position.bubbleX,
|
|
1096
|
-
bubbleX: position.bubbleX,
|
|
1097
|
-
bubbleY,
|
|
1098
|
-
labelX: position.labelX,
|
|
1099
|
-
labelY: y - LABEL_RAISE,
|
|
1100
|
-
labelWidth: position.labelWidth,
|
|
1101
|
-
label,
|
|
1102
|
-
align: position.align,
|
|
1103
|
-
width: item.width,
|
|
1104
|
-
height: bubbleHeight,
|
|
1105
|
-
bubbleHeight,
|
|
1106
|
-
lines: item.lines,
|
|
1107
|
-
message: item.message,
|
|
1108
|
-
side: item.side,
|
|
1109
|
-
participantLabel: item.participantLabel,
|
|
1110
|
-
tagHeight: item.tagHeight,
|
|
1111
|
-
textHeight: item.textHeight,
|
|
1112
|
-
});
|
|
1113
|
-
y += item.height + ITEM_GAP;
|
|
1114
|
-
}
|
|
1115
|
-
return {
|
|
1116
|
-
page: pageIndex + 1,
|
|
1117
|
-
width,
|
|
1118
|
-
height: Math.max(520, Math.min(maxHeight, Math.ceil(y + PAGE_BOTTOM))),
|
|
1119
|
-
elements,
|
|
1120
|
-
title: header.title,
|
|
1121
|
-
subtitle: header.subtitle,
|
|
1122
|
-
footer: 'visit claworld.love',
|
|
1123
|
-
};
|
|
1124
678
|
});
|
|
1125
|
-
|
|
1126
|
-
return {
|
|
1127
|
-
width,
|
|
1128
|
-
header,
|
|
1129
|
-
pages,
|
|
1130
|
-
};
|
|
679
|
+
return normalized;
|
|
1131
680
|
}
|
|
1132
681
|
|
|
1133
|
-
function
|
|
1134
|
-
return
|
|
1135
|
-
.
|
|
1136
|
-
.
|
|
1137
|
-
|
|
1138
|
-
|
|
682
|
+
function selectionSummary(request, messageCount) {
|
|
683
|
+
return compactObject({
|
|
684
|
+
mode: request.mode,
|
|
685
|
+
chatRequestId: request.mode === 'stored' ? request.chatRequestId : null,
|
|
686
|
+
messageCount,
|
|
687
|
+
omittedBefore: 0,
|
|
688
|
+
omittedAfter: 0,
|
|
689
|
+
});
|
|
1139
690
|
}
|
|
1140
691
|
|
|
1141
|
-
function
|
|
1142
|
-
const
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
`<desc id="${descId}">${escapeXml(desc)}</desc>`,
|
|
1149
|
-
svgDefs(),
|
|
1150
|
-
`<rect x="0" y="0" width="${page.width}" height="${page.height}" fill="${COMIC_THEME.paper}"/>`,
|
|
1151
|
-
`<rect x="0" y="0" width="${page.width}" height="${page.height}" fill="url(#comicGridMinor)"/>`,
|
|
1152
|
-
`<rect x="0" y="0" width="${page.width}" height="${page.height}" fill="url(#comicGridMajor)" opacity="0.46"/>`,
|
|
1153
|
-
`<rect x="${FRAME_MARGIN}" y="${FRAME_MARGIN}" width="${page.width - FRAME_MARGIN * 2}" height="${page.height - FRAME_MARGIN * 2}" rx="46" fill="none" stroke="${BLACK}" stroke-width="6"/>`,
|
|
1154
|
-
renderHeaderSvg(page),
|
|
1155
|
-
'<g role="list">',
|
|
1156
|
-
];
|
|
1157
|
-
for (const item of page.elements) {
|
|
1158
|
-
if (item.kind === 'ellipsis') {
|
|
1159
|
-
parts.push(renderEllipsisSvg(page, item));
|
|
1160
|
-
continue;
|
|
1161
|
-
}
|
|
1162
|
-
if (item.kind === 'time') {
|
|
1163
|
-
parts.push(renderTimeSvg(page, item));
|
|
1164
|
-
continue;
|
|
692
|
+
function formatTimeMarker(value) {
|
|
693
|
+
const timestamp = parseTimeish(value);
|
|
694
|
+
if (timestamp != null) {
|
|
695
|
+
const date = new Date(timestamp * 1000);
|
|
696
|
+
if (!Number.isNaN(date.getTime())) {
|
|
697
|
+
const pad = (part) => String(part).padStart(2, '0');
|
|
698
|
+
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
1165
699
|
}
|
|
1166
|
-
parts.push(renderMessageSvg(item));
|
|
1167
|
-
}
|
|
1168
|
-
parts.push('</g>');
|
|
1169
|
-
if (page.footer) {
|
|
1170
|
-
parts.push(`<text x="${(page.width / 2).toFixed(1)}" y="${page.height - 24}" text-anchor="middle" font-size="${SMALL_FONT_SIZE}" fill="#444444">${escapeXml(page.footer)}</text>`);
|
|
1171
700
|
}
|
|
1172
|
-
|
|
1173
|
-
return
|
|
701
|
+
const match = /(?:(\d{4})[-/])?(\d{1,2})[-/](\d{1,2})[ T](\d{1,2}):(\d{2})/u.exec(String(value ?? '').trim());
|
|
702
|
+
return match ? `${String(Number(match[2])).padStart(2, '0')}-${String(Number(match[3])).padStart(2, '0')} ${String(Number(match[4])).padStart(2, '0')}:${match[5]}` : '';
|
|
1174
703
|
}
|
|
1175
704
|
|
|
1176
|
-
function
|
|
1177
|
-
const
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
const
|
|
1181
|
-
|
|
1182
|
-
|
|
705
|
+
function decorateSelection(messages, selection) {
|
|
706
|
+
const items = [];
|
|
707
|
+
let previousTimestamp = null;
|
|
708
|
+
messages.forEach((message, index) => {
|
|
709
|
+
const timestamp = parseTimeish(message.createdAt);
|
|
710
|
+
if (message.createdAt && (index === 0 || (
|
|
711
|
+
previousTimestamp != null && timestamp != null && timestamp - previousTimestamp > TIME_SPLIT_SECONDS
|
|
712
|
+
))) {
|
|
713
|
+
const label = formatTimeMarker(message.createdAt);
|
|
714
|
+
if (label) items.push({ kind: 'time', label, timestamp: message.createdAt });
|
|
715
|
+
}
|
|
716
|
+
items.push({ kind: 'message', message });
|
|
717
|
+
previousTimestamp = timestamp ?? previousTimestamp;
|
|
718
|
+
});
|
|
719
|
+
if (selection.omittedAfter > 0) {
|
|
720
|
+
items.push({ kind: 'ellipsis', omitted: selection.omittedAfter, label: `${selection.omittedAfter} later messages omitted` });
|
|
1183
721
|
}
|
|
1184
|
-
|
|
1185
|
-
const labelX = bubbleX + 16;
|
|
1186
|
-
return { bubbleX, labelX, labelWidth, align: 'left' };
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
function renderHeaderSvg(page) {
|
|
1190
|
-
const x = CANVAS_MARGIN + 26;
|
|
1191
|
-
const y = HEADER_Y;
|
|
1192
|
-
const width = page.width - (CANVAS_MARGIN + 26) * 2;
|
|
1193
|
-
const height = headerCardHeight(page.subtitle);
|
|
1194
|
-
const title = clipDisplay(headerTitle(page.title), 32);
|
|
1195
|
-
return [
|
|
1196
|
-
`<rect x="${x + 11}" y="${y + 6}" width="${width + 2}" height="${height + 10}" rx="22" fill="${BLACK}"/>`,
|
|
1197
|
-
`<rect x="${x + 7}" y="${y + 6}" width="${width}" height="${height + 4}" rx="22" fill="url(#headerAccent)"/>`,
|
|
1198
|
-
`<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="22" fill="${COMIC_THEME.headerFill}" stroke="${BLACK}" stroke-width="4"/>`,
|
|
1199
|
-
`<text x="${x + 28}" y="${y + 43}" font-size="${TITLE_FONT_SIZE}" font-weight="900" fill="${BLACK}">${escapeXml(title)}</text>`,
|
|
1200
|
-
renderHeaderSubtitleSvg(x + 35, y + 70, headerSubtitleLines(page.subtitle)),
|
|
1201
|
-
decorativeStarSvg(x + width - 62, y + 34, 22, '#FFFFFF', 'url(#headerAccent)'),
|
|
1202
|
-
`<circle cx="${x + width - 23}" cy="${y + 55}" r="9" fill="#72E3C0" stroke="${BLACK}" stroke-width="3"/>`,
|
|
1203
|
-
`<circle cx="${x + width - 25}" cy="${y + 53}" r="9" fill="#72E3C0" stroke="${BLACK}" stroke-width="3"/>`,
|
|
1204
|
-
].join('\n');
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
function headerSubtitleLines(subtitle) {
|
|
1208
|
-
const lines = wrapText(String(subtitle || '').trim(), HEADER_SUBTITLE_MAX_UNITS);
|
|
1209
|
-
if (lines.length <= HEADER_SUBTITLE_MAX_LINES) return lines;
|
|
1210
|
-
const visible = lines.slice(0, HEADER_SUBTITLE_MAX_LINES - 1);
|
|
1211
|
-
const remainder = lines.slice(HEADER_SUBTITLE_MAX_LINES - 1).map((line) => line.trim()).filter(Boolean).join(' ');
|
|
1212
|
-
visible.push(ellipsizeText(remainder, HEADER_SUBTITLE_MAX_UNITS));
|
|
1213
|
-
return visible;
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
function headerCardHeight(subtitle) {
|
|
1217
|
-
return headerSubtitleLines(subtitle).length > 1
|
|
1218
|
-
? HEADER_CARD_HEIGHT_TWO_LINES
|
|
1219
|
-
: HEADER_CARD_HEIGHT_ONE_LINE;
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
function headerHeightForSubtitle(subtitle) {
|
|
1223
|
-
return HEADER_Y + headerCardHeight(subtitle) + HEADER_BOTTOM_PAD;
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
function renderHeaderSubtitleSvg(x, y, lines) {
|
|
1227
|
-
return lines.map((line, index) => (
|
|
1228
|
-
`<text class="header-subtitle-line" x="${x.toFixed(1)}" y="${(y + index * HEADER_SUBTITLE_LINE_HEIGHT).toFixed(1)}" font-size="15" font-weight="600" fill="${COMIC_THEME.muted}">${escapeXml(line)}</text>`
|
|
1229
|
-
)).join('\n');
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
function renderEllipsisSvg(page, item) {
|
|
1233
|
-
const y = item.y + 7;
|
|
1234
|
-
return `<text x="${(page.width / 2).toFixed(1)}" y="${y + 14}" text-anchor="middle" font-size="${SMALL_FONT_SIZE}" fill="#555555">${escapeXml(item.label)}</text>`;
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
function renderTimeSvg(page, item) {
|
|
1238
|
-
const label = clipDisplay(item.label, 22);
|
|
1239
|
-
const labelWidth = Math.max(150, displayColumns(label) * 8 + 44);
|
|
1240
|
-
const x = page.width / 2 - labelWidth / 2;
|
|
1241
|
-
const y = item.y + 4;
|
|
1242
|
-
return [
|
|
1243
|
-
'<g class="time-row">',
|
|
1244
|
-
diamondSvg(x - 28, y + 16, 13, COMIC_THEME.timeAccentLeft),
|
|
1245
|
-
`<rect x="${(x + 3).toFixed(1)}" y="${y + 4}" width="${labelWidth}" height="30" rx="15" fill="${BLACK}"/>`,
|
|
1246
|
-
`<rect x="${x.toFixed(1)}" y="${y}" width="${labelWidth}" height="30" rx="15" fill="${COMIC_THEME.timeFill}" stroke="${BLACK}" stroke-width="3"/>`,
|
|
1247
|
-
`<text x="${(page.width / 2).toFixed(1)}" y="${y + 21}" text-anchor="middle" font-size="${FONT_SIZE}" font-weight="700" fill="${BLACK}">${escapeXml(label)}</text>`,
|
|
1248
|
-
diamondSvg(x + labelWidth + 28, y + 16, 13, COMIC_THEME.timeAccentRight),
|
|
1249
|
-
'</g>',
|
|
1250
|
-
].join('\n');
|
|
722
|
+
return items;
|
|
1251
723
|
}
|
|
1252
724
|
|
|
1253
|
-
function
|
|
1254
|
-
const
|
|
1255
|
-
const
|
|
1256
|
-
|
|
1257
|
-
const parts = [
|
|
1258
|
-
`<g class="message-row ${item.side}" role="listitem" aria-label="${titleText}">`,
|
|
1259
|
-
`<title>${titleText}</title>`,
|
|
1260
|
-
bubbleLayersSvg(item, colors),
|
|
1261
|
-
`<rect x="${item.labelX}" y="${item.labelY}" width="${item.labelWidth}" height="${LABEL_HEIGHT}" rx="9" fill="${colors.label}" stroke="${BLACK}" stroke-width="3"/>`,
|
|
1262
|
-
`<text x="${(item.labelX + item.labelWidth / 2).toFixed(1)}" y="${item.labelY + 21}" text-anchor="middle" font-size="${LABEL_FONT_SIZE}" font-weight="900" fill="${BLACK}">${escapeXml(item.label)}</text>`,
|
|
1263
|
-
];
|
|
1264
|
-
const textX = item.bubbleX + BUBBLE_PAD_X;
|
|
1265
|
-
let textY = item.bubbleY + BUBBLE_PAD_Y + 17;
|
|
1266
|
-
for (const line of item.lines) {
|
|
1267
|
-
parts.push(`<text x="${textX}" y="${textY}" font-size="${FONT_SIZE}" font-weight="800" fill="${BLACK}">${escapeXml(line)}</text>`);
|
|
1268
|
-
textY += LINE_HEIGHT;
|
|
725
|
+
function participants(messages) {
|
|
726
|
+
const seen = new Map();
|
|
727
|
+
for (const message of messages) {
|
|
728
|
+
if (!seen.has(message.participantId)) seen.set(message.participantId, message);
|
|
1269
729
|
}
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
function bubbleLayersSvg(item, colors) {
|
|
1277
|
-
const x = item.bubbleX;
|
|
1278
|
-
const y = item.bubbleY;
|
|
1279
|
-
const width = item.width;
|
|
1280
|
-
const height = item.bubbleHeight;
|
|
1281
|
-
return [
|
|
1282
|
-
`<rect x="${x + 11}" y="${y + 9}" width="${width + 2}" height="${height + 4}" rx="17" fill="${BLACK}"/>`,
|
|
1283
|
-
`<rect x="${x + 11}" y="${y + 9}" width="${width - 3}" height="${height}" rx="17" fill="${colors.accent}" stroke="${BLACK}" stroke-width="3"/>`,
|
|
1284
|
-
`<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="17" fill="${colors.fill}" stroke="${BLACK}" stroke-width="4"/>`,
|
|
1285
|
-
].join('\n');
|
|
730
|
+
return [...seen.entries()].map(([participantId, message]) => ({
|
|
731
|
+
id: participantId,
|
|
732
|
+
name: message.participantLabel,
|
|
733
|
+
side: message.side,
|
|
734
|
+
avatar: String(message.participantLabel || 'A').replace(/[^A-Za-z0-9]/gu, '').slice(0, 2).toUpperCase() || 'A',
|
|
735
|
+
}));
|
|
1286
736
|
}
|
|
1287
737
|
|
|
1288
|
-
function
|
|
1289
|
-
if (
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
label: COMIC_THEME.rightLabel,
|
|
1293
|
-
accent: 'url(#rightAccent)',
|
|
1294
|
-
};
|
|
1295
|
-
}
|
|
738
|
+
function bubbleMessagePayload(item) {
|
|
739
|
+
if (item.kind === 'time') return { kind: 'time', label: item.label, timestamp: item.timestamp || '' };
|
|
740
|
+
if (item.kind === 'ellipsis') return { kind: 'ellipsis', omitted: item.omitted, label: item.label };
|
|
741
|
+
const { message } = item;
|
|
1296
742
|
return {
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
743
|
+
id: message.id,
|
|
744
|
+
kind: 'text',
|
|
745
|
+
from: message.participantId,
|
|
746
|
+
side: message.side,
|
|
747
|
+
speaker: message.participantLabel,
|
|
748
|
+
text: message.text,
|
|
749
|
+
createdAt: message.createdAt,
|
|
750
|
+
tags: [...message.tags],
|
|
1300
751
|
};
|
|
1301
752
|
}
|
|
1302
753
|
|
|
1303
|
-
function
|
|
1304
|
-
const
|
|
1305
|
-
if (
|
|
1306
|
-
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
function labelText(label) {
|
|
1310
|
-
return clipDisplay(String(label || 'AGENT').toUpperCase(), LABEL_MAX_COLS);
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
function labelWidthForText(label) {
|
|
1314
|
-
return Math.max(86, Math.min(158, displayColumns(label) * 11 + 30));
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
function tagRowUnits(tags) {
|
|
1318
|
-
if (!tags.length) return 0;
|
|
1319
|
-
const width = tags.reduce((sum, tag) => sum + tagWidth(tag), 0) + Math.max(0, tags.length - 1) * TAG_ICON_GAP;
|
|
1320
|
-
return width / TEXT_UNIT_PX;
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
function renderTagIconsSvg(tags, x, y) {
|
|
1324
|
-
const parts = [`<g class="tag-icons" transform="translate(${x.toFixed(1)} ${y.toFixed(1)})">`];
|
|
1325
|
-
let cursor = 0;
|
|
1326
|
-
for (const tag of tags) {
|
|
1327
|
-
parts.push(tagIconSvg(tag, cursor, 0));
|
|
1328
|
-
cursor += tagWidth(tag) + TAG_ICON_GAP;
|
|
754
|
+
function publicHeaderValue(value) {
|
|
755
|
+
const normalized = text(value, '') || '';
|
|
756
|
+
if (/(?:^|[\s(])(?:agt|req|wld|dlv|conversation|management)[_:-][a-z0-9]/iu.test(normalized)) {
|
|
757
|
+
return '';
|
|
1329
758
|
}
|
|
1330
|
-
|
|
1331
|
-
return parts.join('\n');
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
|
-
function tagWidth(tag) {
|
|
1335
|
-
const normalized = tagName(tag);
|
|
1336
|
-
if (['like', 'dislike', 'request end'].includes(normalized)) return TAG_ICON_SIZE;
|
|
1337
|
-
return Math.max(58, Math.min(126, displayColumns(fallbackTagLabel(normalized)) * 8 + 24));
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
function tagIconSvg(tag, x, y) {
|
|
1341
|
-
const normalized = tagName(tag);
|
|
1342
|
-
if (!['like', 'dislike', 'request end'].includes(normalized)) return fallbackTagSvg(normalized, x, y);
|
|
1343
|
-
const [fill, accent] = TAG_ICON_THEMES[normalized] || ['#FFFFFF', '#7DD7FF'];
|
|
1344
|
-
const icon = normalized === 'request end'
|
|
1345
|
-
? requestEndIconSvg(x, y, accent)
|
|
1346
|
-
: thumbIconSvg(x, y, accent, { down: normalized === 'dislike' });
|
|
1347
|
-
return [
|
|
1348
|
-
`<g class="tag-icon tag-${safeClassToken(normalized)}" role="img" aria-label="${escapeXml(normalized)}">`,
|
|
1349
|
-
`<title>${escapeXml(normalized)}</title>`,
|
|
1350
|
-
`<rect x="${(x + 3).toFixed(1)}" y="${(y + 4).toFixed(1)}" width="${TAG_ICON_SIZE}" height="${TAG_ICON_SIZE}" rx="9" fill="${BLACK}"/>`,
|
|
1351
|
-
`<rect x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${TAG_ICON_SIZE}" height="${TAG_ICON_SIZE}" rx="9" fill="${fill}" stroke="${BLACK}" stroke-width="2.5"/>`,
|
|
1352
|
-
icon,
|
|
1353
|
-
'</g>',
|
|
1354
|
-
].join('\n');
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
function tagName(tag) {
|
|
1358
|
-
return String(tag || '').trim().toLowerCase();
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
function fallbackTagLabel(tag) {
|
|
1362
|
-
return clipDisplay(String(tag || 'tag'), TAG_FALLBACK_MAX_COLS);
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
function fallbackTagSvg(tag, x, y) {
|
|
1366
|
-
const label = fallbackTagLabel(tag);
|
|
1367
|
-
const width = tagWidth(tag);
|
|
1368
|
-
return [
|
|
1369
|
-
`<g class="tag-icon tag-fallback" role="img" aria-label="${escapeXml(label)}">`,
|
|
1370
|
-
`<title>${escapeXml(label)}</title>`,
|
|
1371
|
-
`<rect x="${(x + 3).toFixed(1)}" y="${(y + 4).toFixed(1)}" width="${width}" height="${TAG_ICON_SIZE}" rx="9" fill="${BLACK}"/>`,
|
|
1372
|
-
`<rect x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${width}" height="${TAG_ICON_SIZE}" rx="9" fill="#F6F1FF" stroke="${BLACK}" stroke-width="2.5"/>`,
|
|
1373
|
-
`<text x="${(x + width / 2).toFixed(1)}" y="${(y + 20.5).toFixed(1)}" text-anchor="middle" font-size="${LABEL_FONT_SIZE}" font-weight="900" fill="${BLACK}">${escapeXml(label)}</text>`,
|
|
1374
|
-
'</g>',
|
|
1375
|
-
].join('\n');
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
function thumbIconSvg(x, y, accent, { down = false } = {}) {
|
|
1379
|
-
const paths = [
|
|
1380
|
-
`<path d="M${(x + 7.8).toFixed(1)} ${(y + 13.3).toFixed(1)} H${(x + 12).toFixed(1)} V${(y + 24).toFixed(1)} H${(x + 7.8).toFixed(1)} Z" fill="#FFFFFF" stroke="${BLACK}" stroke-width="2" stroke-linejoin="round"/>`,
|
|
1381
|
-
`<path d="M${(x + 12).toFixed(1)} ${(y + 23.6).toFixed(1)} H${(x + 21.2).toFixed(1)} C${(x + 23).toFixed(1)} ${(y + 23.6).toFixed(1)} ${(x + 24).toFixed(1)} ${(y + 22.5).toFixed(1)} ${(x + 24.4).toFixed(1)} ${(y + 20.9).toFixed(1)} L${(x + 25.4).toFixed(1)} ${(y + 15.6).toFixed(1)} C${(x + 25.8).toFixed(1)} ${(y + 13.9).toFixed(1)} ${(x + 24.5).toFixed(1)} ${(y + 12.2).toFixed(1)} ${(x + 22.7).toFixed(1)} ${(y + 12.2).toFixed(1)} H${(x + 18.6).toFixed(1)} L${(x + 19.2).toFixed(1)} ${(y + 9.1).toFixed(1)} C${(x + 19.5).toFixed(1)} ${(y + 7.4).toFixed(1)} ${(x + 18.4).toFixed(1)} ${(y + 5.7).toFixed(1)} ${(x + 16.7).toFixed(1)} ${(y + 5.4).toFixed(1)} L${(x + 15.8).toFixed(1)} ${(y + 5.3).toFixed(1)} L${(x + 12).toFixed(1)} ${(y + 12.9).toFixed(1)} Z" fill="${accent}" stroke="${BLACK}" stroke-width="2" stroke-linejoin="round"/>`,
|
|
1382
|
-
].join('\n');
|
|
1383
|
-
if (!down) return paths;
|
|
1384
|
-
const cx = x + TAG_ICON_SIZE / 2;
|
|
1385
|
-
const cy = y + TAG_ICON_SIZE / 2;
|
|
1386
|
-
return `<g transform="rotate(180 ${cx.toFixed(1)} ${cy.toFixed(1)})">\n${paths}\n</g>`;
|
|
759
|
+
return normalized;
|
|
1387
760
|
}
|
|
1388
761
|
|
|
1389
|
-
function
|
|
1390
|
-
return [
|
|
1391
|
-
`<path d="M${(x + 22).toFixed(1)} ${(y + 4).toFixed(1)} C${(x + 25.2).toFixed(1)} ${(y + 5.4).toFixed(1)} ${(x + 26.6).toFixed(1)} ${(y + 7.8).toFixed(1)} ${(x + 26.5).toFixed(1)} ${(y + 10.6).toFixed(1)}" fill="none" stroke="${BLACK}" stroke-width="2" stroke-linecap="round"/>`,
|
|
1392
|
-
`<path d="M${(x + 9.215).toFixed(3)} ${(y + 18.117).toFixed(3)} C${(x + 10.043).toFixed(3)} ${(y + 22.457).toFixed(3)} ${(x + 13.384).toFixed(3)} ${(y + 25.036).toFixed(3)} ${(x + 17.132).toFixed(3)} ${(y + 23.961).toFixed(3)} L${(x + 19.44).toFixed(3)} ${(y + 23.3).toFixed(3)} C${(x + 22.323).toFixed(3)} ${(y + 22.473).toFixed(3)} ${(x + 23.488).toFixed(3)} ${(y + 19.642).toFixed(3)} ${(x + 22.538).toFixed(3)} ${(y + 16.69).toFixed(3)} L${(x + 21.435).toFixed(3)} ${(y + 12.845).toFixed(3)} L${(x + 9.227).toFixed(3)} ${(y + 16.345).toFixed(3)} L${(x + 9.215).toFixed(3)} ${(y + 18.117).toFixed(3)} Z" fill="${accent}" stroke="${BLACK}" stroke-width="2" stroke-linejoin="round"/>`,
|
|
1393
|
-
`<path d="M${(x + 9.665).toFixed(3)} ${(y + 7.897).toFixed(3)} L${(x + 9.473).toFixed(3)} ${(y + 7.952).toFixed(3)} C${(x + 8.756).toFixed(3)} ${(y + 8.158).toFixed(3)} ${(x + 8.286).toFixed(3)} ${(y + 8.709).toFixed(3)} ${(x + 8.422).toFixed(3)} ${(y + 9.184).toFixed(3)} L${(x + 10.192).toFixed(3)} ${(y + 15.356).toFixed(3)} C${(x + 10.328).toFixed(3)} ${(y + 15.831).toFixed(3)} ${(x + 11.019).toFixed(3)} ${(y + 16.049).toFixed(3)} ${(x + 11.736).toFixed(3)} ${(y + 15.843).toFixed(3)} L${(x + 11.928).toFixed(3)} ${(y + 15.788).toFixed(3)} C${(x + 12.645).toFixed(3)} ${(y + 15.583).toFixed(3)} ${(x + 13.115).toFixed(3)} ${(y + 15.031).toFixed(3)} ${(x + 12.979).toFixed(3)} ${(y + 14.557).toFixed(3)} L${(x + 11.209).toFixed(3)} ${(y + 8.384).toFixed(3)} C${(x + 11.073).toFixed(3)} ${(y + 7.91).toFixed(3)} ${(x + 10.382).toFixed(3)} ${(y + 7.692).toFixed(3)} ${(x + 9.665).toFixed(3)} ${(y + 7.897).toFixed(3)} Z" fill="${accent}" stroke="${BLACK}" stroke-width="1.8"/>`,
|
|
1394
|
-
`<path d="M${(x + 11.93).toFixed(3)} ${(y + 5.999).toFixed(3)} L${(x + 11.738).toFixed(3)} ${(y + 6.055).toFixed(3)} C${(x + 11.021).toFixed(3)} ${(y + 6.26).toFixed(3)} ${(x + 10.553).toFixed(3)} ${(y + 6.82).toFixed(3)} ${(x + 10.692).toFixed(3)} ${(y + 7.306).toFixed(3)} L${(x + 12.729).toFixed(3)} ${(y + 14.408).toFixed(3)} C${(x + 12.868).toFixed(3)} ${(y + 14.894).toFixed(3)} ${(x + 13.562).toFixed(3)} ${(y + 15.122).toFixed(3)} ${(x + 14.279).toFixed(3)} ${(y + 14.916).toFixed(3)} L${(x + 14.471).toFixed(3)} ${(y + 14.861).toFixed(3)} C${(x + 15.188).toFixed(3)} ${(y + 14.655).toFixed(3)} ${(x + 15.656).toFixed(3)} ${(y + 14.095).toFixed(3)} ${(x + 15.516).toFixed(3)} ${(y + 13.609).toFixed(3)} L${(x + 13.48).toFixed(3)} ${(y + 6.507).toFixed(3)} C${(x + 13.341).toFixed(3)} ${(y + 6.021).toFixed(3)} ${(x + 12.647).toFixed(3)} ${(y + 5.794).toFixed(3)} ${(x + 11.93).toFixed(3)} ${(y + 5.999).toFixed(3)} Z" fill="${accent}" stroke="${BLACK}" stroke-width="1.8"/>`,
|
|
1395
|
-
`<path d="M${(x + 14.636).toFixed(3)} ${(y + 5.64).toFixed(3)} L${(x + 14.443).toFixed(3)} ${(y + 5.695).toFixed(3)} C${(x + 13.727).toFixed(3)} ${(y + 5.9).toFixed(3)} ${(x + 13.258).toFixed(3)} ${(y + 6.457).toFixed(3)} ${(x + 13.396).toFixed(3)} ${(y + 6.938).toFixed(3)} L${(x + 15.338).toFixed(3)} ${(y + 13.714).toFixed(3)} C${(x + 15.476).toFixed(3)} ${(y + 14.195).toFixed(3)} ${(x + 16.169).toFixed(3)} ${(y + 14.418).toFixed(3)} ${(x + 16.886).toFixed(3)} ${(y + 14.213).toFixed(3)} L${(x + 17.078).toFixed(3)} ${(y + 14.157).toFixed(3)} C${(x + 17.795).toFixed(3)} ${(y + 13.952).toFixed(3)} ${(x + 18.264).toFixed(3)} ${(y + 13.395).toFixed(3)} ${(x + 18.126).toFixed(3)} ${(y + 12.914).toFixed(3)} L${(x + 16.183).toFixed(3)} ${(y + 6.139).toFixed(3)} C${(x + 16.045).toFixed(3)} ${(y + 5.658).toFixed(3)} ${(x + 15.352).toFixed(3)} ${(y + 5.434).toFixed(3)} ${(x + 14.636).toFixed(3)} ${(y + 5.64).toFixed(3)} Z" fill="${accent}" stroke="${BLACK}" stroke-width="1.8"/>`,
|
|
1396
|
-
`<path d="M${(x + 17.8).toFixed(3)} ${(y + 6.085).toFixed(3)} L${(x + 17.702).toFixed(3)} ${(y + 6.113).toFixed(3)} C${(x + 16.971).toFixed(3)} ${(y + 6.322).toFixed(3)} ${(x + 16.482).toFixed(3)} ${(y + 6.851).toFixed(3)} ${(x + 16.609).toFixed(3)} ${(y + 7.294).toFixed(3)} L${(x + 18.176).toFixed(3)} ${(y + 12.759).toFixed(3)} C${(x + 18.303).toFixed(3)} ${(y + 13.202).toFixed(3)} ${(x + 18.998).toFixed(3)} ${(y + 13.391).toFixed(3)} ${(x + 19.729).toFixed(3)} ${(y + 13.182).toFixed(3)} L${(x + 19.827).toFixed(3)} ${(y + 13.154).toFixed(3)} C${(x + 20.557).toFixed(3)} ${(y + 12.944).toFixed(3)} ${(x + 21.046).toFixed(3)} ${(y + 12.415).toFixed(3)} ${(x + 20.919).toFixed(3)} ${(y + 11.972).toFixed(3)} L${(x + 19.352).toFixed(3)} ${(y + 6.507).toFixed(3)} C${(x + 19.225).toFixed(3)} ${(y + 6.065).toFixed(3)} ${(x + 18.53).toFixed(3)} ${(y + 5.875).toFixed(3)} ${(x + 17.8).toFixed(3)} ${(y + 6.085).toFixed(3)} Z" fill="${accent}" stroke="${BLACK}" stroke-width="1.8"/>`,
|
|
1397
|
-
`<path d="M${(x + 9.546).toFixed(3)} ${(y + 19.999).toFixed(3)} L${(x + 6).toFixed(3)} ${(y + 17.791).toFixed(3)} C${(x + 4.736).toFixed(3)} ${(y + 17.009).toFixed(3)} ${(x + 5.668).toFixed(3)} ${(y + 15.181).toFixed(3)} ${(x + 7.138).toFixed(3)} ${(y + 15.592).toFixed(3)} L${(x + 10.615).toFixed(3)} ${(y + 17.196).toFixed(3)} L${(x + 9.546).toFixed(3)} ${(y + 19.999).toFixed(3)} Z" fill="${accent}" stroke="${BLACK}" stroke-width="1.8" stroke-linejoin="round"/>`,
|
|
1398
|
-
`<rect x="${(x + 10.887).toFixed(3)}" y="${(y + 14.834).toFixed(3)}" width="9.584" height="3.800" transform="rotate(-16.9438 ${(x + 10.887).toFixed(3)} ${(y + 14.834).toFixed(3)})" fill="${accent}"/>`,
|
|
1399
|
-
`<path d="M${(x + 10.5).toFixed(1)} ${(y + 19.5).toFixed(1)} L${(x + 11).toFixed(1)} ${(y + 20).toFixed(1)} L${(x + 11.5).toFixed(1)} ${(y + 17).toFixed(1)} L${(x + 9.5).toFixed(1)} ${(y + 17.5).toFixed(1)} L${(x + 9).toFixed(1)} ${(y + 18.5).toFixed(1)} L${(x + 10.5).toFixed(1)} ${(y + 19.5).toFixed(1)} Z" fill="${accent}"/>`,
|
|
1400
|
-
].join('\n');
|
|
762
|
+
function displayName(identity) {
|
|
763
|
+
return publicHeaderValue(String(identity || '').split('#', 1)[0]);
|
|
1401
764
|
}
|
|
1402
765
|
|
|
1403
|
-
function
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
return [
|
|
1407
|
-
`<polygon points="${backPath}" fill="${accent}"/>`,
|
|
1408
|
-
`<polygon points="${frontPath}" fill="${fill}" stroke="${BLACK}" stroke-width="3"/>`,
|
|
1409
|
-
].join('\n');
|
|
766
|
+
function semanticHeaderTitle(peerName, worldName) {
|
|
767
|
+
if (peerName && worldName) return `${peerName} — ${worldName}`;
|
|
768
|
+
return peerName || worldName || 'Claworld conversation';
|
|
1410
769
|
}
|
|
1411
770
|
|
|
1412
|
-
function
|
|
1413
|
-
const
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
function safeClassToken(value) {
|
|
1432
|
-
return String(value || 'tag').toLowerCase().replace(/[^a-z0-9_-]+/g, '-') || 'tag';
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
function svgDefs() {
|
|
1436
|
-
return [
|
|
1437
|
-
'<defs>',
|
|
1438
|
-
`<style><![CDATA[text { font-family: ${COMIC_FONT_FAMILY}; letter-spacing: 0; } .message-row:hover rect:last-of-type { filter: url(#comicLift); }]]></style>`,
|
|
1439
|
-
'<pattern id="comicGridMinor" width="32" height="32" patternUnits="userSpaceOnUse"><path d="M 32 0 L 0 0 0 32" fill="none" stroke="#BED1D8" stroke-width="1" stroke-opacity="0.62"/></pattern>',
|
|
1440
|
-
'<pattern id="comicGridMajor" width="128" height="128" patternUnits="userSpaceOnUse"><path d="M 128 0 L 0 0 0 128" fill="none" stroke="#AABFC8" stroke-width="1.4" stroke-opacity="0.72"/></pattern>',
|
|
1441
|
-
'<linearGradient id="headerAccent" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="#47B6FF"/><stop offset="52%" stop-color="#FF4EB4"/><stop offset="100%" stop-color="#FF8A2A"/></linearGradient>',
|
|
1442
|
-
'<linearGradient id="leftAccent" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="#58E58F"/><stop offset="100%" stop-color="#47B6FF"/></linearGradient>',
|
|
1443
|
-
'<linearGradient id="rightAccent" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="#A871FF"/><stop offset="100%" stop-color="#FF4EB4"/></linearGradient>',
|
|
1444
|
-
'<filter id="comicLift" x="-6%" y="-16%" width="112%" height="132%"><feDropShadow dx="0" dy="2" stdDeviation="1.2" flood-color="#000000" flood-opacity="0.14"/></filter>',
|
|
1445
|
-
'</defs>',
|
|
1446
|
-
].join('\n');
|
|
771
|
+
function headerText(args, messages, headerContext) {
|
|
772
|
+
const explicitTitle = publicHeaderValue(args.title);
|
|
773
|
+
let peerIdentity = publicHeaderValue(headerContext.peerIdentity)
|
|
774
|
+
|| publicHeaderValue(headerContext.peerId);
|
|
775
|
+
if (!peerIdentity) {
|
|
776
|
+
peerIdentity = publicHeaderValue(
|
|
777
|
+
messages.find((message) => message.side === 'left')?.participantLabel,
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
const peerName = displayName(peerIdentity);
|
|
781
|
+
const worldName = publicHeaderValue(headerContext.worldName);
|
|
782
|
+
const title = explicitTitle || semanticHeaderTitle(peerName, worldName);
|
|
783
|
+
const explicitProfile = publicHeaderValue(args.peerProfile);
|
|
784
|
+
if (explicitProfile) return { title, subtitle: explicitProfile };
|
|
785
|
+
const profile = publicHeaderValue(headerContext.peerProfile);
|
|
786
|
+
const subtitleParts = [peerIdentity, profile].filter(Boolean);
|
|
787
|
+
if (!subtitleParts.length && worldName) subtitleParts.push(worldName);
|
|
788
|
+
return { title, subtitle: subtitleParts.join(' · ') || 'Conversation transcript' };
|
|
1447
789
|
}
|
|
1448
790
|
|
|
1449
|
-
function
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
format: 'bubblespec',
|
|
1453
|
-
style,
|
|
1454
|
-
width: layout.width,
|
|
1455
|
-
mode,
|
|
1456
|
-
chatRequestId: source?.chatRequestId || null,
|
|
1457
|
-
source: {
|
|
1458
|
-
mode,
|
|
1459
|
-
transcriptPath: source?.transcriptPath || null,
|
|
1460
|
-
sessionDirectoryPath: source?.sessionDirectoryPath || null,
|
|
1461
|
-
summary: source?.summary || null,
|
|
1462
|
-
},
|
|
1463
|
-
header,
|
|
1464
|
-
messages: messages.map((message) => ({
|
|
1465
|
-
id: message.id,
|
|
1466
|
-
side: message.side,
|
|
1467
|
-
speaker: message.speaker,
|
|
1468
|
-
text: message.text,
|
|
1469
|
-
tags: message.tags,
|
|
1470
|
-
createdAt: message.createdAt,
|
|
1471
|
-
episodeIds: message.episodeIds,
|
|
1472
|
-
})),
|
|
1473
|
-
pages: layout.pages.map((page) => ({
|
|
1474
|
-
page: page.page,
|
|
1475
|
-
width: page.width,
|
|
1476
|
-
height: page.height,
|
|
1477
|
-
elementCount: page.elements.length,
|
|
1478
|
-
})),
|
|
1479
|
-
};
|
|
791
|
+
function outputDirectories(workspaceRoot) {
|
|
792
|
+
const base = path.join(workspaceRoot, '.claworld', 'reports', 'transcripts');
|
|
793
|
+
return { images: path.join(base, 'images'), documents: path.join(base, 'documents') };
|
|
1480
794
|
}
|
|
1481
795
|
|
|
1482
|
-
function
|
|
1483
|
-
const
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
table[n] = c >>> 0;
|
|
1490
|
-
}
|
|
1491
|
-
return table;
|
|
796
|
+
function artifactId(source, selection, styleName) {
|
|
797
|
+
const now = new Date();
|
|
798
|
+
const pad = (part) => String(part).padStart(2, '0');
|
|
799
|
+
const timestamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
800
|
+
const styleSlug = styleName.toLowerCase().replace(/[^a-z0-9]+/gu, '-').replace(/^-|-$/gu, '') || 'style';
|
|
801
|
+
const digest = createHash('sha256').update(JSON.stringify({ source, selection, style: styleName, now: timestamp })).digest('hex').slice(0, 10);
|
|
802
|
+
return `claworld-transcript-${styleSlug}-${timestamp}-${digest}`;
|
|
1492
803
|
}
|
|
1493
804
|
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
function crc32(buffer) {
|
|
1497
|
-
let crc = 0xffffffff;
|
|
1498
|
-
for (const byte of buffer) {
|
|
1499
|
-
crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
|
1500
|
-
}
|
|
1501
|
-
return (crc ^ 0xffffffff) >>> 0;
|
|
805
|
+
async function sha256(filePath) {
|
|
806
|
+
return createHash('sha256').update(await fs.readFile(filePath)).digest('hex');
|
|
1502
807
|
}
|
|
1503
808
|
|
|
1504
|
-
function
|
|
1505
|
-
const
|
|
1506
|
-
|
|
1507
|
-
length.writeUInt32BE(data.length, 0);
|
|
1508
|
-
const crcBuffer = Buffer.alloc(4);
|
|
1509
|
-
crcBuffer.writeUInt32BE(crc32(Buffer.concat([typeBuffer, data])), 0);
|
|
1510
|
-
return Buffer.concat([length, typeBuffer, data, crcBuffer]);
|
|
809
|
+
function intValue(value, fallback, minimum, maximum) {
|
|
810
|
+
const parsed = Number.parseInt(value, 10);
|
|
811
|
+
return Math.min(maximum, Math.max(minimum, Number.isFinite(parsed) ? parsed : fallback));
|
|
1511
812
|
}
|
|
1512
813
|
|
|
1513
|
-
function
|
|
1514
|
-
const
|
|
1515
|
-
const
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
const offset = (row * width + col) * 4;
|
|
1522
|
-
rgba[offset] = r;
|
|
1523
|
-
rgba[offset + 1] = g;
|
|
1524
|
-
rgba[offset + 2] = b;
|
|
1525
|
-
rgba[offset + 3] = a;
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
814
|
+
async function writePng(svg, pngPath, page) {
|
|
815
|
+
const sharpModule = await import('sharp');
|
|
816
|
+
const sharp = sharpModule.default || sharpModule;
|
|
817
|
+
await sharp(Buffer.from(svg), { density: 192 })
|
|
818
|
+
.resize(page.width, page.height, { fit: 'fill' })
|
|
819
|
+
.png()
|
|
820
|
+
.toFile(pngPath);
|
|
821
|
+
return { renderer: 'sharp-svg' };
|
|
1528
822
|
}
|
|
1529
823
|
|
|
1530
|
-
function
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
const color = element.message.side === 'local' ? [223, 243, 229, 255] : [255, 255, 255, 255];
|
|
1541
|
-
writeRect(rgba, width, element.x, element.y + 14, element.width, element.height, color);
|
|
1542
|
-
writeRect(rgba, width, element.x + 16, element.y + 36, element.width - 32, 5, [45, 45, 45, 255]);
|
|
1543
|
-
writeRect(rgba, width, element.x + 16, element.y + 54, Math.max(60, element.width - 90), 5, [80, 80, 80, 255]);
|
|
1544
|
-
}
|
|
1545
|
-
const raw = Buffer.alloc((width * 4 + 1) * height);
|
|
1546
|
-
for (let row = 0; row < height; row += 1) {
|
|
1547
|
-
const rawOffset = row * (width * 4 + 1);
|
|
1548
|
-
raw[rawOffset] = 0;
|
|
1549
|
-
rgba.copy(raw, rawOffset + 1, row * width * 4, (row + 1) * width * 4);
|
|
1550
|
-
}
|
|
1551
|
-
const ihdr = Buffer.alloc(13);
|
|
1552
|
-
ihdr.writeUInt32BE(width, 0);
|
|
1553
|
-
ihdr.writeUInt32BE(height, 4);
|
|
1554
|
-
ihdr[8] = 8;
|
|
1555
|
-
ihdr[9] = 6;
|
|
1556
|
-
ihdr[10] = 0;
|
|
1557
|
-
ihdr[11] = 0;
|
|
1558
|
-
ihdr[12] = 0;
|
|
1559
|
-
return Buffer.concat([
|
|
1560
|
-
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
1561
|
-
pngChunk('IHDR', ihdr),
|
|
1562
|
-
pngChunk('IDAT', deflateSync(raw)),
|
|
1563
|
-
pngChunk('IEND'),
|
|
1564
|
-
]);
|
|
824
|
+
function artifactPage(item) {
|
|
825
|
+
return compactObject({
|
|
826
|
+
page: item.page,
|
|
827
|
+
format: item.format,
|
|
828
|
+
path: item.path,
|
|
829
|
+
width: item.width,
|
|
830
|
+
height: item.height,
|
|
831
|
+
sha256: item.sha256,
|
|
832
|
+
mediaRef: item.format === 'png' ? `MEDIA:${item.path}` : null,
|
|
833
|
+
});
|
|
1565
834
|
}
|
|
1566
835
|
|
|
1567
|
-
async function
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
];
|
|
1575
|
-
for (const attempt of attempts) {
|
|
1576
|
-
try {
|
|
1577
|
-
await execFileAsync(attempt.cmd, attempt.args, { timeout: 15000, maxBuffer: 2 * 1024 * 1024 });
|
|
1578
|
-
const stat = await fs.stat(pngPath);
|
|
1579
|
-
if (stat.size > 64) return attempt.renderer;
|
|
1580
|
-
} catch {
|
|
1581
|
-
// Continue to the next local renderer.
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
await atomicWriteBuffer(pngPath, buildFallbackPngBuffer(page));
|
|
1585
|
-
return 'fallback-png';
|
|
1586
|
-
}
|
|
836
|
+
export async function renderTranscriptReport({ workspaceRoot, localAgentId = null, args = {} } = {}) {
|
|
837
|
+
if (!workspaceRoot) throw new Error('OpenClaw workspace root is required for transcript rendering');
|
|
838
|
+
const request = normalizeRenderRequest(args);
|
|
839
|
+
const source = await loadSourceMessages(request, workspaceRoot);
|
|
840
|
+
const headerContext = extractTranscriptHeaderContext(source.messages);
|
|
841
|
+
const normalized = normalizeMessages(source.messages, localAgentId, request.renderArgs, headerContext);
|
|
842
|
+
if (!normalized.length) throw new Error('no visible transcript messages were found for rendering');
|
|
1587
843
|
|
|
1588
|
-
|
|
1589
|
-
const
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
const
|
|
1593
|
-
|
|
1594
|
-
const
|
|
1595
|
-
const
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
const
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
format: 'bubblespec',
|
|
1626
|
-
path: bubbleSpecPath,
|
|
1627
|
-
sha256: await hashFile(bubbleSpecPath),
|
|
844
|
+
const selection = selectionSummary(request, normalized.length);
|
|
845
|
+
const width = DEFAULT_WIDTH;
|
|
846
|
+
const maxPageHeight = intValue(request.renderArgs.maxPageHeight, DEFAULT_MAX_PAGE_HEIGHT, 900, 8000);
|
|
847
|
+
const header = headerText(request.renderArgs, normalized, headerContext);
|
|
848
|
+
const decorated = decorateSelection(normalized, selection);
|
|
849
|
+
const measured = decorated.map((item) => measureTranscriptItem(item, width));
|
|
850
|
+
const pages = paginateTranscriptItems(measured, width, maxPageHeight, header.title, header.subtitle);
|
|
851
|
+
const currentArtifactId = artifactId(source.summary, selection, CLAWORLD_TRANSCRIPT_STYLE_NAME);
|
|
852
|
+
const directories = outputDirectories(workspaceRoot);
|
|
853
|
+
await Promise.all([fs.mkdir(directories.images, { recursive: true }), fs.mkdir(directories.documents, { recursive: true })]);
|
|
854
|
+
|
|
855
|
+
const files = [];
|
|
856
|
+
for (const page of pages) {
|
|
857
|
+
const suffix = `p${String(page.page).padStart(2, '0')}`;
|
|
858
|
+
const svgPath = path.join(directories.documents, `${currentArtifactId}-${suffix}.svg`);
|
|
859
|
+
const pngPath = path.join(directories.images, `${currentArtifactId}-${suffix}.png`);
|
|
860
|
+
const svg = renderTranscriptPageSvg(page);
|
|
861
|
+
await atomicWriteText(svgPath, svg);
|
|
862
|
+
const pngResult = await writePng(svg, pngPath, page);
|
|
863
|
+
files.push({ page: page.page, format: 'svg', path: svgPath, width: page.width, height: page.height, sha256: await sha256(svgPath), role: 'source' });
|
|
864
|
+
files.push({ page: page.page, format: 'png', path: pngPath, width: page.width, height: page.height, sha256: await sha256(pngPath), role: 'primary', renderer: pngResult.renderer });
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const bubbleSpec = {
|
|
868
|
+
version: '1',
|
|
869
|
+
kind: 'claworld.transcript_report',
|
|
870
|
+
scene: {
|
|
871
|
+
title: header.title,
|
|
872
|
+
subtitle: header.subtitle,
|
|
873
|
+
peerId: header.title,
|
|
874
|
+
peerProfile: header.subtitle,
|
|
875
|
+
peerProfileSource: publicHeaderValue(request.renderArgs.peerProfile)
|
|
876
|
+
? 'explicit'
|
|
877
|
+
: headerContext.profileSource || 'fallback',
|
|
878
|
+
generatedAt: isoNow(),
|
|
879
|
+
source: source.summary,
|
|
880
|
+
selection,
|
|
1628
881
|
},
|
|
1629
|
-
|
|
1630
|
-
|
|
882
|
+
canvas: { width, style: CLAWORLD_TRANSCRIPT_STYLE_NAME, maxPageHeight },
|
|
883
|
+
participants: participants(normalized),
|
|
884
|
+
messages: decorated.map(bubbleMessagePayload),
|
|
1631
885
|
};
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
886
|
+
const specPath = path.join(directories.documents, `${currentArtifactId}.bubblespec.json`);
|
|
887
|
+
await atomicWriteJson(specPath, bubbleSpec);
|
|
888
|
+
|
|
889
|
+
const pngPages = files.filter((item) => item.format === 'png').map(artifactPage);
|
|
890
|
+
const svgPages = files.filter((item) => item.format === 'svg').map(artifactPage);
|
|
891
|
+
const stats = {
|
|
892
|
+
sourceMessages: source.messages.length,
|
|
893
|
+
normalizedMessages: normalized.length,
|
|
894
|
+
renderedMessages: normalized.length,
|
|
895
|
+
pages: pages.length,
|
|
896
|
+
omittedBefore: selection.omittedBefore || 0,
|
|
897
|
+
omittedAfter: selection.omittedAfter || 0,
|
|
1644
898
|
};
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
renderedMessages: renderedMessages.length,
|
|
1658
|
-
droppedMessages: Math.max(0, sourceRecords.length - normalizedMessages.length),
|
|
899
|
+
const result = compactObject({
|
|
900
|
+
status: 'ok',
|
|
901
|
+
mode: request.mode,
|
|
902
|
+
chatRequestId: request.mode === 'stored' ? request.chatRequestId : null,
|
|
903
|
+
artifactId: currentArtifactId,
|
|
904
|
+
messageCount: normalized.length,
|
|
905
|
+
pageCount: pages.length,
|
|
906
|
+
style: CLAWORLD_TRANSCRIPT_STYLE_NAME,
|
|
907
|
+
artifacts: {
|
|
908
|
+
bubbleSpec: { format: 'bubblespec', path: specPath, sha256: await sha256(specPath) },
|
|
909
|
+
pngPages,
|
|
910
|
+
svgPages,
|
|
1659
911
|
},
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
scope: 'conversation',
|
|
1669
|
-
summary: `Rendered Claworld transcript report ${result.artifactId}.`,
|
|
1670
|
-
refs: {
|
|
1671
|
-
chatRequestId: result.chatRequestId || null,
|
|
1672
|
-
artifactId: result.artifactId,
|
|
1673
|
-
},
|
|
1674
|
-
artifacts: {
|
|
1675
|
-
bubbleSpec: result.artifacts?.bubbleSpec?.path || null,
|
|
1676
|
-
pngPages: (result.artifacts?.pngPages || []).map((page) => page.path),
|
|
1677
|
-
svgPages: (result.artifacts?.svgPages || []).map((page) => page.path),
|
|
1678
|
-
},
|
|
1679
|
-
}, { updateSessionDirectory: false });
|
|
1680
|
-
} catch {
|
|
1681
|
-
// Rendering should not fail because the journal is unavailable.
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
export async function renderClaworldTranscriptReport({
|
|
1686
|
-
workspaceRoot = null,
|
|
1687
|
-
agentId = null,
|
|
1688
|
-
args = {},
|
|
1689
|
-
} = {}) {
|
|
1690
|
-
const resolvedWorkspaceRoot = resolveWorkspaceRoot(workspaceRoot);
|
|
1691
|
-
await ensureClaworldWorkingMemory(resolvedWorkspaceRoot);
|
|
1692
|
-
const params = validateRenderArgs(args);
|
|
1693
|
-
let source = { mode: params.mode };
|
|
1694
|
-
let sourceRecords = [];
|
|
1695
|
-
let normalizedMessages = [];
|
|
1696
|
-
let renderedMessages = [];
|
|
1697
|
-
if (params.mode === 'manual') {
|
|
1698
|
-
sourceRecords = params.manual.messages;
|
|
1699
|
-
normalizedMessages = normalizeTranscriptMessages(sourceRecords, { mode: 'manual' });
|
|
1700
|
-
renderedMessages = normalizedMessages;
|
|
1701
|
-
source = {
|
|
1702
|
-
mode: 'manual',
|
|
1703
|
-
summary: {
|
|
1704
|
-
suppliedMessageCount: sourceRecords.length,
|
|
912
|
+
deliveryHint: {
|
|
913
|
+
primaryMedia: pngPages[0]?.mediaRef || null,
|
|
914
|
+
primaryMediaBatch: pngPages.map((item) => item.mediaRef).join('\n'),
|
|
915
|
+
sourceSvgDocument: svgPages[0] ? `[[as_document]]\nMEDIA:${svgPages[0].path}` : null,
|
|
916
|
+
reportOwnerArgs: {
|
|
917
|
+
media_path: pngPages[0]?.path || null,
|
|
918
|
+
media_paths: pngPages.map((item) => item.path),
|
|
919
|
+
send_source_svg: false,
|
|
1705
920
|
},
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
source = await resolveStoredTranscriptSource(resolvedWorkspaceRoot, params.stored.chatRequestId);
|
|
1709
|
-
sourceRecords = await loadTranscriptRecords(source.transcriptPath);
|
|
1710
|
-
normalizedMessages = normalizeTranscriptMessages(sourceRecords, { mode: 'stored' });
|
|
1711
|
-
renderedMessages = selectStoredMessages(normalizedMessages, params.stored.chatRequestId);
|
|
1712
|
-
}
|
|
1713
|
-
if (renderedMessages.length === 0) {
|
|
1714
|
-
inputError('messages', 'No renderable peer-visible transcript messages were found');
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
const header = normalizeHeaderContext({
|
|
1718
|
-
mode: params.mode,
|
|
1719
|
-
manual: params.manual || null,
|
|
1720
|
-
source,
|
|
1721
|
-
messages: renderedMessages,
|
|
1722
|
-
});
|
|
1723
|
-
const layout = layoutPages({
|
|
1724
|
-
messages: renderedMessages,
|
|
1725
|
-
header,
|
|
1726
|
-
maxPageHeight: params.maxPageHeight,
|
|
1727
|
-
});
|
|
1728
|
-
const spec = buildBubbleSpec({
|
|
1729
|
-
mode: params.mode,
|
|
1730
|
-
source,
|
|
1731
|
-
header,
|
|
1732
|
-
messages: renderedMessages,
|
|
1733
|
-
layout,
|
|
1734
|
-
style: params.style,
|
|
921
|
+
},
|
|
922
|
+
diagnostics: { source: source.summary, stats },
|
|
1735
923
|
});
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
924
|
+
await appendClaworldJournalEvent(workspaceRoot, {
|
|
925
|
+
kind: 'transcript_report',
|
|
926
|
+
scope: request.mode === 'stored' ? 'conversation' : 'main',
|
|
927
|
+
summary: `Rendered ${normalized.length} visible Claworld transcript messages across ${pages.length} page(s).`,
|
|
928
|
+
refs: { chatRequestId: request.mode === 'stored' ? request.chatRequestId : null },
|
|
929
|
+
artifacts: { artifactId: currentArtifactId, bubbleSpec: specPath, pngPages: pngPages.map((item) => item.path), svgPages: svgPages.map((item) => item.path) },
|
|
930
|
+
maintenance: { selection, stats },
|
|
1743
931
|
});
|
|
1744
|
-
const result = {
|
|
1745
|
-
status: 'ok',
|
|
1746
|
-
mode: params.mode,
|
|
1747
|
-
chatRequestId: source.chatRequestId || null,
|
|
1748
|
-
artifactId,
|
|
1749
|
-
messageCount: renderedMessages.length,
|
|
1750
|
-
pageCount: layout.pages.length,
|
|
1751
|
-
style: params.style,
|
|
1752
|
-
artifacts,
|
|
1753
|
-
deliveryHint: {
|
|
1754
|
-
primaryFilePath: artifacts.pngPages[0]?.path || null,
|
|
1755
|
-
imageFilePaths: artifacts.pngPages.map((page) => page.path),
|
|
1756
|
-
sourceSvgFilePaths: artifacts.svgPages.map((page) => page.path),
|
|
1757
|
-
messageTool: buildOpenClawUploadArgs(
|
|
1758
|
-
artifacts.pngPages[0],
|
|
1759
|
-
source.chatRequestId
|
|
1760
|
-
? `Claworld transcript report for ${source.chatRequestId}`
|
|
1761
|
-
: 'Claworld transcript report',
|
|
1762
|
-
),
|
|
1763
|
-
messageToolBatch: artifacts.pngPages.map((page) => buildOpenClawUploadArgs(page)).filter(Boolean),
|
|
1764
|
-
},
|
|
1765
|
-
diagnostics: buildDiagnostics({
|
|
1766
|
-
sourceRecords,
|
|
1767
|
-
normalizedMessages,
|
|
1768
|
-
renderedMessages,
|
|
1769
|
-
source,
|
|
1770
|
-
}),
|
|
1771
|
-
};
|
|
1772
|
-
await appendRenderJournalEvent(resolvedWorkspaceRoot, result);
|
|
1773
932
|
return result;
|
|
1774
933
|
}
|
|
1775
|
-
|
|
1776
|
-
function buildEpisodeFilters(input = {}) {
|
|
1777
|
-
const source = isObject(input.filters) ? input.filters : input;
|
|
1778
|
-
return {
|
|
1779
|
-
chatRequestId: normalizeText(source.chatRequestId, null),
|
|
1780
|
-
conversationKey: normalizeText(source.conversationKey, null),
|
|
1781
|
-
localSessionKey: normalizeText(source.localSessionKey, null),
|
|
1782
|
-
worldId: normalizeText(source.worldId, null),
|
|
1783
|
-
counterpartyAgentId: normalizeText(source.counterpartyAgentId, null),
|
|
1784
|
-
};
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
function episodeMatchesFilters(summary, filters) {
|
|
1788
|
-
if (filters.chatRequestId && summary.chatRequestId !== filters.chatRequestId) return false;
|
|
1789
|
-
if (filters.localSessionKey && summary.localSessionKey !== filters.localSessionKey) return false;
|
|
1790
|
-
if (filters.conversationKey && summary.conversationKey !== filters.conversationKey) return false;
|
|
1791
|
-
if (filters.worldId && summary.worldId !== filters.worldId) return false;
|
|
1792
|
-
if (
|
|
1793
|
-
filters.counterpartyAgentId
|
|
1794
|
-
&& summary.relaySessionKey !== filters.counterpartyAgentId
|
|
1795
|
-
&& summary.conversationKey?.includes(filters.counterpartyAgentId) !== true
|
|
1796
|
-
) {
|
|
1797
|
-
return false;
|
|
1798
|
-
}
|
|
1799
|
-
return true;
|
|
1800
|
-
}
|
|
1801
|
-
|
|
1802
|
-
export async function listClaworldLocalTranscriptEpisodes({
|
|
1803
|
-
workspaceRoot = null,
|
|
1804
|
-
filters = {},
|
|
1805
|
-
} = {}) {
|
|
1806
|
-
const resolvedWorkspaceRoot = resolveWorkspaceRoot(workspaceRoot);
|
|
1807
|
-
const normalizedFilters = buildEpisodeFilters(filters);
|
|
1808
|
-
const sessionDirectory = await readClaworldSessionDirectory(resolvedWorkspaceRoot);
|
|
1809
|
-
const summaries = [];
|
|
1810
|
-
const conversationSessions = sessionDirectory.directory?.conversationSessions || {};
|
|
1811
|
-
for (const [localSessionKey, session] of Object.entries(conversationSessions)) {
|
|
1812
|
-
const chatRequests = session?.chatRequests || {};
|
|
1813
|
-
for (const [chatRequestId, request] of Object.entries(chatRequests)) {
|
|
1814
|
-
const artifacts = Array.isArray(request?.artifacts) ? request.artifacts : [];
|
|
1815
|
-
const latestArtifact = [...artifacts].reverse().find((artifact) => artifactPathCandidates(artifact).length > 0) || {};
|
|
1816
|
-
const summary = summarizeDirectoryRequest({
|
|
1817
|
-
chatRequestId,
|
|
1818
|
-
localSessionKey,
|
|
1819
|
-
session,
|
|
1820
|
-
request,
|
|
1821
|
-
artifact: latestArtifact,
|
|
1822
|
-
transcriptPath: latestArtifact.transcriptPath || latestArtifact.sessionFile || null,
|
|
1823
|
-
});
|
|
1824
|
-
if (episodeMatchesFilters(summary, normalizedFilters)) summaries.push(summary);
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
return summaries.sort((left, right) => String(right.lastSeenAt || '').localeCompare(String(left.lastSeenAt || '')));
|
|
1828
|
-
}
|
|
1829
|
-
|
|
1830
|
-
export async function summarizeClaworldChatRequestTranscript({
|
|
1831
|
-
workspaceRoot = null,
|
|
1832
|
-
chatRequestId,
|
|
1833
|
-
} = {}) {
|
|
1834
|
-
const resolvedWorkspaceRoot = resolveWorkspaceRoot(workspaceRoot);
|
|
1835
|
-
const id = normalizeText(chatRequestId, null);
|
|
1836
|
-
if (!id) inputError('chatRequestId', 'chatRequestId is required');
|
|
1837
|
-
const source = await resolveStoredTranscriptSource(resolvedWorkspaceRoot, id);
|
|
1838
|
-
const sourceRecords = await loadTranscriptRecords(source.transcriptPath);
|
|
1839
|
-
const normalizedMessages = normalizeTranscriptMessages(sourceRecords, { mode: 'stored' });
|
|
1840
|
-
const renderedMessages = selectStoredMessages(normalizedMessages, id);
|
|
1841
|
-
return {
|
|
1842
|
-
chatRequestId: id,
|
|
1843
|
-
transcriptPath: source.transcriptPath,
|
|
1844
|
-
sourceMessageCount: sourceRecords.length,
|
|
1845
|
-
renderableMessageCount: renderedMessages.length,
|
|
1846
|
-
peerMessageCount: renderedMessages.filter((message) => message.side === 'peer').length,
|
|
1847
|
-
localMessageCount: renderedMessages.filter((message) => message.side === 'local').length,
|
|
1848
|
-
firstMessageAt: renderedMessages[0]?.createdAt || null,
|
|
1849
|
-
lastMessageAt: renderedMessages[renderedMessages.length - 1]?.createdAt || null,
|
|
1850
|
-
};
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
function buildLocalTranscriptSummary(episodes = []) {
|
|
1854
|
-
return {
|
|
1855
|
-
count: episodes.length,
|
|
1856
|
-
chatRequestIds: episodes.map((episode) => episode.chatRequestId).filter(Boolean),
|
|
1857
|
-
latestChatRequestId: episodes[0]?.chatRequestId || null,
|
|
1858
|
-
};
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
export async function augmentConversationPayloadWithLocalTranscriptIndex(payload, {
|
|
1862
|
-
workspaceRoot = null,
|
|
1863
|
-
filters = {},
|
|
1864
|
-
} = {}) {
|
|
1865
|
-
if (!isObject(payload)) return payload;
|
|
1866
|
-
const episodes = await listClaworldLocalTranscriptEpisodes({ workspaceRoot, filters });
|
|
1867
|
-
const addition = {
|
|
1868
|
-
localTranscriptEpisodes: episodes,
|
|
1869
|
-
localTranscriptSummary: buildLocalTranscriptSummary(episodes),
|
|
1870
|
-
};
|
|
1871
|
-
if (Array.isArray(payload.items)) {
|
|
1872
|
-
return {
|
|
1873
|
-
...payload,
|
|
1874
|
-
...addition,
|
|
1875
|
-
items: payload.items.map((item) => {
|
|
1876
|
-
if (!isObject(item)) return item;
|
|
1877
|
-
const itemFilters = buildEpisodeFilters({
|
|
1878
|
-
chatRequestId: item.chatRequestId,
|
|
1879
|
-
conversationKey: item.conversationKey,
|
|
1880
|
-
localSessionKey: item.localSessionKey,
|
|
1881
|
-
worldId: item.worldId,
|
|
1882
|
-
counterpartyAgentId: item.counterpartyAgentId || item.targetAgentId || item.peerAgentId,
|
|
1883
|
-
});
|
|
1884
|
-
const itemEpisodes = episodes.filter((episode) => episodeMatchesFilters(episode, itemFilters));
|
|
1885
|
-
if (itemEpisodes.length === 0) return item;
|
|
1886
|
-
return {
|
|
1887
|
-
...item,
|
|
1888
|
-
localTranscriptEpisodes: itemEpisodes,
|
|
1889
|
-
localTranscriptSummary: buildLocalTranscriptSummary(itemEpisodes),
|
|
1890
|
-
};
|
|
1891
|
-
}),
|
|
1892
|
-
};
|
|
1893
|
-
}
|
|
1894
|
-
return {
|
|
1895
|
-
...payload,
|
|
1896
|
-
...addition,
|
|
1897
|
-
};
|
|
1898
|
-
}
|