@syengup/friday-channel-next 0.0.37 → 0.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +10 -4
- package/index.ts +10 -4
- package/install.js +132 -257
- package/package.json +1 -1
- package/dist/attachments/0768c9b1-53b0-44df-83e8-be15c4ea188f.jpg +0 -0
- package/dist/attachments/0a379d01-116b-4da1-bf15-77cb2cbb0093.jpg +0 -0
- package/dist/attachments/181caab2-64a7-4004-a057-225a144f949e.mp3 +0 -0
- package/dist/attachments/19662331-e527-47d2-bc0e-0e19a7a91419.jpg +0 -0
- package/dist/attachments/26a23b2b-52df-4572-a5e1-15b34fb87e44.jpg +0 -0
- package/dist/attachments/2f9282c5-8db4-4c4a-a060-e65104f6f9ff.jpg +0 -0
- package/dist/attachments/3929ec3d-ea15-4de6-96bc-97e8b0b658a7.jpg +0 -0
- package/dist/attachments/403c0cbc-4e3c-4146-a3be-ff3746ee7cda.jpg +0 -0
- package/dist/attachments/441977f5-0f7b-4aa2-841a-1d63e787ea53.jpg +0 -0
- package/dist/attachments/453e8aa2-76e3-498d-8d6f-d7b96d6bf45b.jpg +0 -0
- package/dist/attachments/538cde71-d26e-4d3d-b901-e8dd905e668c.mp3 +0 -0
- package/dist/attachments/55c7f628-4ba2-4252-aa4b-4f3eb6045a8a.mp3 +0 -0
- package/dist/attachments/5f7683f5-8194-4698-b077-31d209525379.jpg +0 -0
- package/dist/attachments/60614a35-8f44-4197-b783-2f58f5a72ac8.jpeg +0 -0
- package/dist/attachments/62830489-8814-48b1-851c-3845e514f35e.mp3 +0 -0
- package/dist/attachments/66f4a62d-1531-4f38-a531-7456f9edf221.png +0 -0
- package/dist/attachments/6735d749-769e-483a-9b84-43b9338a720b.png +0 -0
- package/dist/attachments/6d1766b1-05e4-4b04-b3c8-1c25e9d182a1.png +0 -0
- package/dist/attachments/782b077b-06e3-484b-baf5-33e7160234ed.png +0 -0
- package/dist/attachments/7ad638b2-1f56-4d93-9ad8-b40346e0650f.jpg +0 -0
- package/dist/attachments/89f6fb15-e652-4111-a60c-baa414659052.png +0 -0
- package/dist/attachments/8a88b14f-442f-45fb-b01d-e51bab8f800d.mp3 +0 -0
- package/dist/attachments/92292034-9cf6-4f26-8d77-fddca3deb638.png +0 -0
- package/dist/attachments/92c2b414-d33d-4d93-bcb6-013da7bec9a4.jpg +0 -0
- package/dist/attachments/9664f69e-3c05-45ca-9a52-f2d0b9f9bf7e.jpg +0 -0
- package/dist/attachments/977d28c1-43c0-40e0-95e3-defe0f41afe8.jpg +0 -0
- package/dist/attachments/9df40f1a-c6e1-4177-8a03-06757a30b19e.png +0 -0
- package/dist/attachments/a68e6815-6163-4421-a70f-34493aa9a217.jpg +0 -0
- package/dist/attachments/aab32fea-6d99-47ec-ab1f-2340f31312eb.jpg +0 -0
- package/dist/attachments/ab403224-2fb1-49c1-8738-ea194ab65d44.png +0 -0
- package/dist/attachments/ac3da190-d6ee-4038-a673-8b893035a687.png +0 -0
- package/dist/attachments/af02be9c-87f7-4c5a-9969-7db32039bb58.png +0 -0
- package/dist/attachments/b011d42a-00e5-4f77-86bc-08da6112e6e1.mp3 +0 -0
- package/dist/attachments/b7d7df40-c627-4b1f-9b09-167b88545c25.mp3 +0 -0
- package/dist/attachments/c5e9bf09-a718-422c-bcb3-94c173e3755b.mp3 +0 -0
- package/dist/attachments/d5449e13-1995-44ba-9392-ecbfe5f9876f.jpg +0 -0
- package/dist/attachments/ea0069f5-01cf-4ea1-985e-3a1e426399c3.png +0 -0
- package/dist/attachments/f3989ff2-7b70-4a80-a896-74a6b197f7d8.png +0 -0
- package/dist/attachments/f64a4a14-e3aa-4eed-a8d9-1603f04baa5b.jpg +0 -0
- package/dist/src/http/handlers/device-token.d.ts +0 -2
- package/dist/src/http/handlers/device-token.js +0 -43
- package/dist/src/http/handlers/import.d.ts +0 -7
- package/dist/src/http/handlers/import.js +0 -69
- package/dist/src/http/handlers/info.d.ts +0 -2
- package/dist/src/http/handlers/info.js +0 -13
- package/dist/src/http/handlers/messages-list.d.ts +0 -7
- package/dist/src/http/handlers/messages-list.js +0 -44
- package/dist/src/http/handlers/pair.d.ts +0 -2
- package/dist/src/http/handlers/pair.js +0 -39
- package/dist/src/http/handlers/sessions-list.d.ts +0 -8
- package/dist/src/http/handlers/sessions-list.js +0 -24
- package/dist/src/http/handlers/sessions-messages-get.d.ts +0 -2
- package/dist/src/http/handlers/sessions-messages-get.js +0 -55
- package/dist/src/http/handlers/sessions-messages-post.d.ts +0 -2
- package/dist/src/http/handlers/sessions-messages-post.js +0 -92
- package/dist/src/http/handlers/sessions-messages.d.ts +0 -2
- package/dist/src/http/handlers/sessions-messages.js +0 -135
- package/dist/src/http/handlers/sync.d.ts +0 -7
- package/dist/src/http/handlers/sync.js +0 -56
- package/dist/src/push/apns.d.ts +0 -15
- package/dist/src/push/apns.js +0 -56
- package/dist/src/push/device-tokens.d.ts +0 -3
- package/dist/src/push/device-tokens.js +0 -39
- package/dist/src/sync/account-identity.d.ts +0 -14
- package/dist/src/sync/account-identity.js +0 -101
- package/dist/src/sync/archive.d.ts +0 -9
- package/dist/src/sync/archive.js +0 -25
- package/dist/src/sync/database.d.ts +0 -66
- package/dist/src/sync/database.js +0 -364
- package/dist/src/sync/init.d.ts +0 -3
- package/dist/src/sync/init.js +0 -14
- package/dist/src/sync/installation-id.d.ts +0 -1
- package/dist/src/sync/installation-id.js +0 -41
- package/dist/src/sync/message-accumulator.d.ts +0 -29
- package/dist/src/sync/message-accumulator.js +0 -188
- package/dist/src/sync/message-store.d.ts +0 -68
- package/dist/src/sync/message-store.js +0 -262
- package/dist/src/sync/push-store.d.ts +0 -5
- package/dist/src/sync/push-store.js +0 -54
- package/dist/src/sync/session-key.d.ts +0 -12
- package/dist/src/sync/session-key.js +0 -47
- package/dist/src/sync/sync-state.d.ts +0 -5
- package/dist/src/sync/sync-state.js +0 -54
- package/dist/src/sync/transcript-archive.d.ts +0 -13
- package/dist/src/sync/transcript-archive.js +0 -37
- package/dist/src/sync/transcript-store.d.ts +0 -35
- package/dist/src/sync/transcript-store.js +0 -221
- package/dist/src/sync/translate.d.ts +0 -42
- package/dist/src/sync/translate.js +0 -171
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { resolveFridayNextConfig } from "../config.js";
|
|
5
|
-
import { getHostOpenClawConfigSnapshot } from "../host-config.js";
|
|
6
|
-
import { getFridayNextRuntime } from "../runtime.js";
|
|
7
|
-
function currentHistoryDir() {
|
|
8
|
-
const cfg = resolveFridayNextConfig(getHostOpenClawConfigSnapshot(getFridayNextRuntime().config));
|
|
9
|
-
return cfg.historyDir;
|
|
10
|
-
}
|
|
11
|
-
function ensureDir(dir) {
|
|
12
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
13
|
-
}
|
|
14
|
-
function sha256(value) {
|
|
15
|
-
return crypto.createHash("sha256").update(value).digest("hex");
|
|
16
|
-
}
|
|
17
|
-
function safeSegment(value) {
|
|
18
|
-
const trimmed = value.trim().toLowerCase();
|
|
19
|
-
const slug = trimmed
|
|
20
|
-
.replace(/[^a-z0-9._-]+/gi, "-")
|
|
21
|
-
.replace(/-+/g, "-")
|
|
22
|
-
.replace(/^[-._]+|[-._]+$/g, "");
|
|
23
|
-
if (slug && slug.length <= 96)
|
|
24
|
-
return slug;
|
|
25
|
-
return `s-${sha256(value).slice(0, 32)}`;
|
|
26
|
-
}
|
|
27
|
-
export function sessionIdFromSessionKey(sessionKey, accountId) {
|
|
28
|
-
const parts = sessionKey.trim().split(":").filter(Boolean);
|
|
29
|
-
if (parts.length >= 4 && parts[0] === "friday" && parts[1] === "account") {
|
|
30
|
-
if (!accountId || parts[2] === accountId) {
|
|
31
|
-
return safeSegment(parts.slice(3).join("-"));
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
if (sessionKey.startsWith("agent:main:friday:account:")) {
|
|
35
|
-
const rest = sessionKey.slice("agent:main:friday:account:".length);
|
|
36
|
-
const parts2 = rest.split(":").filter(Boolean);
|
|
37
|
-
if (parts2.length >= 2 && (!accountId || parts2[0] === accountId)) {
|
|
38
|
-
return safeSegment(parts2.slice(1).join("-"));
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return `legacy-${sha256(sessionKey).slice(0, 32)}`;
|
|
42
|
-
}
|
|
43
|
-
function accountDir(accountId, historyDir = currentHistoryDir()) {
|
|
44
|
-
return path.join(historyDir, "accounts", safeSegment(accountId));
|
|
45
|
-
}
|
|
46
|
-
function sessionsDir(accountId, historyDir = currentHistoryDir()) {
|
|
47
|
-
return path.join(accountDir(accountId, historyDir), "sessions");
|
|
48
|
-
}
|
|
49
|
-
function sessionFile(accountId, sessionKey, historyDir = currentHistoryDir()) {
|
|
50
|
-
return path.join(sessionsDir(accountId, historyDir), `${sessionIdFromSessionKey(sessionKey, accountId)}.jsonl`);
|
|
51
|
-
}
|
|
52
|
-
function sessionsIndexPath(accountId, historyDir = currentHistoryDir()) {
|
|
53
|
-
return path.join(accountDir(accountId, historyDir), "sessions.json");
|
|
54
|
-
}
|
|
55
|
-
function readJsonFile(file, fallback) {
|
|
56
|
-
try {
|
|
57
|
-
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
return fallback;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
function writeJsonFile(file, value) {
|
|
64
|
-
ensureDir(path.dirname(file));
|
|
65
|
-
fs.writeFileSync(file, JSON.stringify(value, null, 2), "utf8");
|
|
66
|
-
}
|
|
67
|
-
function readEventsFromFile(file) {
|
|
68
|
-
try {
|
|
69
|
-
const raw = fs.readFileSync(file, "utf8");
|
|
70
|
-
const events = [];
|
|
71
|
-
for (const line of raw.split(/\n/)) {
|
|
72
|
-
const trimmed = line.trim();
|
|
73
|
-
if (!trimmed)
|
|
74
|
-
continue;
|
|
75
|
-
try {
|
|
76
|
-
const parsed = JSON.parse(trimmed);
|
|
77
|
-
if (parsed?.v === 1 && typeof parsed.seq === "number")
|
|
78
|
-
events.push(parsed);
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
// Ignore corrupt tail lines; append-only files may be truncated by crashes.
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
events.sort((a, b) => a.seq - b.seq);
|
|
85
|
-
return events;
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
return [];
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
function deriveTitle(event) {
|
|
92
|
-
if (event.kind !== "user_message")
|
|
93
|
-
return undefined;
|
|
94
|
-
const text = typeof event.payload.text === "string" ? event.payload.text.trim() : "";
|
|
95
|
-
if (!text)
|
|
96
|
-
return undefined;
|
|
97
|
-
return text.replace(/\s+/g, " ").slice(0, 80);
|
|
98
|
-
}
|
|
99
|
-
function upsertSessionSummary(accountId, sessionKey, events, historyDir) {
|
|
100
|
-
if (events.length === 0)
|
|
101
|
-
return;
|
|
102
|
-
const indexFile = sessionsIndexPath(accountId, historyDir);
|
|
103
|
-
const index = readJsonFile(indexFile, { sessions: [] });
|
|
104
|
-
const sessionId = sessionIdFromSessionKey(sessionKey, accountId);
|
|
105
|
-
const existingIdx = index.sessions.findIndex((s) => s.sessionId === sessionId || s.sessionKey === sessionKey);
|
|
106
|
-
const first = events[0];
|
|
107
|
-
const last = events[events.length - 1];
|
|
108
|
-
const messageCount = events.filter((e) => e.kind === "user_message" || e.kind === "assistant_message").length;
|
|
109
|
-
const title = events.map(deriveTitle).find((v) => typeof v === "string" && v.length > 0);
|
|
110
|
-
const next = {
|
|
111
|
-
sessionKey,
|
|
112
|
-
sessionId,
|
|
113
|
-
title: title ?? (existingIdx >= 0 ? index.sessions[existingIdx]?.title : undefined),
|
|
114
|
-
createdAt: existingIdx >= 0 ? index.sessions[existingIdx]?.createdAt ?? first.ts : first.ts,
|
|
115
|
-
lastActiveAt: last.ts,
|
|
116
|
-
lastSeq: last.seq,
|
|
117
|
-
messageCount,
|
|
118
|
-
};
|
|
119
|
-
if (existingIdx >= 0) {
|
|
120
|
-
index.sessions[existingIdx] = next;
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
index.sessions.push(next);
|
|
124
|
-
}
|
|
125
|
-
index.sessions.sort((a, b) => b.lastActiveAt - a.lastActiveAt);
|
|
126
|
-
writeJsonFile(indexFile, index);
|
|
127
|
-
}
|
|
128
|
-
function eventDedupeKey(event) {
|
|
129
|
-
if (typeof event.eventId === "string" && event.eventId.trim())
|
|
130
|
-
return `event:${event.eventId.trim()}`;
|
|
131
|
-
const payloadId = event.payload && typeof event.payload.id === "string" && event.payload.id.trim()
|
|
132
|
-
? event.payload.id.trim()
|
|
133
|
-
: "";
|
|
134
|
-
if (payloadId)
|
|
135
|
-
return `payload:${event.kind}:${payloadId}`;
|
|
136
|
-
return "";
|
|
137
|
-
}
|
|
138
|
-
export class TranscriptStore {
|
|
139
|
-
historyDir;
|
|
140
|
-
constructor(historyDir = currentHistoryDir()) {
|
|
141
|
-
this.historyDir = historyDir;
|
|
142
|
-
}
|
|
143
|
-
appendEvent(accountId, sessionKey, input) {
|
|
144
|
-
return this.appendEvents(accountId, sessionKey, [input])[0];
|
|
145
|
-
}
|
|
146
|
-
appendEvents(accountId, sessionKey, inputs) {
|
|
147
|
-
const cleanAccountId = accountId.trim();
|
|
148
|
-
const cleanSessionKey = sessionKey.trim();
|
|
149
|
-
if (!cleanAccountId)
|
|
150
|
-
throw new Error("Missing accountId");
|
|
151
|
-
if (!cleanSessionKey)
|
|
152
|
-
throw new Error("Missing sessionKey");
|
|
153
|
-
if (inputs.length === 0)
|
|
154
|
-
return [];
|
|
155
|
-
const file = sessionFile(cleanAccountId, cleanSessionKey, this.historyDir);
|
|
156
|
-
ensureDir(path.dirname(file));
|
|
157
|
-
const existing = readEventsFromFile(file);
|
|
158
|
-
const dedupe = new Set(existing.map(eventDedupeKey).filter(Boolean));
|
|
159
|
-
let nextSeq = existing.length > 0 ? Math.max(...existing.map((e) => e.seq)) + 1 : 1;
|
|
160
|
-
const appended = [];
|
|
161
|
-
for (const input of inputs) {
|
|
162
|
-
const key = eventDedupeKey(input);
|
|
163
|
-
if (key && dedupe.has(key))
|
|
164
|
-
continue;
|
|
165
|
-
const event = {
|
|
166
|
-
v: 1,
|
|
167
|
-
seq: nextSeq,
|
|
168
|
-
ts: typeof input.ts === "number" && Number.isFinite(input.ts) ? Math.floor(input.ts) : Date.now(),
|
|
169
|
-
sessionKey: cleanSessionKey,
|
|
170
|
-
clientId: input.clientId.trim() || "unknown",
|
|
171
|
-
kind: input.kind,
|
|
172
|
-
payload: input.payload,
|
|
173
|
-
eventId: input.eventId,
|
|
174
|
-
lamportTs: input.lamportTs,
|
|
175
|
-
};
|
|
176
|
-
fs.appendFileSync(file, `${JSON.stringify(event)}\n`, "utf8");
|
|
177
|
-
appended.push(event);
|
|
178
|
-
if (key)
|
|
179
|
-
dedupe.add(key);
|
|
180
|
-
nextSeq += 1;
|
|
181
|
-
}
|
|
182
|
-
if (appended.length > 0) {
|
|
183
|
-
upsertSessionSummary(cleanAccountId, cleanSessionKey, [...existing, ...appended], this.historyDir);
|
|
184
|
-
}
|
|
185
|
-
return appended;
|
|
186
|
-
}
|
|
187
|
-
readSince(accountId, sessionKey, sinceSeq = 0, limit = 200) {
|
|
188
|
-
const file = sessionFile(accountId, sessionKey, this.historyDir);
|
|
189
|
-
const max = Math.max(1, Math.min(1000, Math.floor(limit)));
|
|
190
|
-
return readEventsFromFile(file).filter((e) => e.seq > sinceSeq).slice(0, max);
|
|
191
|
-
}
|
|
192
|
-
listSessions(accountId) {
|
|
193
|
-
const index = readJsonFile(sessionsIndexPath(accountId, this.historyDir), { sessions: [] });
|
|
194
|
-
return [...index.sessions].sort((a, b) => b.lastActiveAt - a.lastActiveAt);
|
|
195
|
-
}
|
|
196
|
-
compact(accountId, sessionKey, keepLast) {
|
|
197
|
-
const file = sessionFile(accountId, sessionKey, this.historyDir);
|
|
198
|
-
const events = readEventsFromFile(file);
|
|
199
|
-
const keep = Math.max(1, Math.floor(keepLast));
|
|
200
|
-
if (events.length <= keep)
|
|
201
|
-
return;
|
|
202
|
-
const kept = events.slice(-keep);
|
|
203
|
-
const summary = {
|
|
204
|
-
v: 1,
|
|
205
|
-
seq: kept[0].seq - 1,
|
|
206
|
-
ts: Date.now(),
|
|
207
|
-
sessionKey,
|
|
208
|
-
clientId: "server",
|
|
209
|
-
kind: "summary",
|
|
210
|
-
payload: {
|
|
211
|
-
compactedBeforeSeq: kept[0].seq,
|
|
212
|
-
droppedEvents: events.length - kept.length,
|
|
213
|
-
},
|
|
214
|
-
};
|
|
215
|
-
fs.writeFileSync(file, `${JSON.stringify(summary)}\n${kept.map((e) => JSON.stringify(e)).join("\n")}\n`, "utf8");
|
|
216
|
-
upsertSessionSummary(accountId, sessionKey, [summary, ...kept], this.historyDir);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
export function getTranscriptStore() {
|
|
220
|
-
return new TranscriptStore();
|
|
221
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
export type StoredChatMessage = Record<string, unknown>;
|
|
2
|
-
/**
|
|
3
|
-
* Generate a serverId scoped to an OpenClaw transcript entry.
|
|
4
|
-
* Format: msg_<sessionId_short>_<seq>
|
|
5
|
-
* Seq is monotonically increasing per session, good for cursor-based sync.
|
|
6
|
-
*/
|
|
7
|
-
export declare function serverIdFromSeq(sessionId: string, seq: number): string;
|
|
8
|
-
/** Generate a standalone serverId (for messages not yet in transcript). */
|
|
9
|
-
export declare function generateStandaloneServerId(): string;
|
|
10
|
-
type ContentBlock = {
|
|
11
|
-
type?: string;
|
|
12
|
-
text?: string;
|
|
13
|
-
thinking?: string;
|
|
14
|
-
id?: string;
|
|
15
|
-
name?: string;
|
|
16
|
-
input?: unknown;
|
|
17
|
-
tool_use_id?: string;
|
|
18
|
-
content?: unknown;
|
|
19
|
-
is_error?: boolean;
|
|
20
|
-
};
|
|
21
|
-
type OpenClawMessage = {
|
|
22
|
-
role?: string;
|
|
23
|
-
content?: string | ContentBlock[];
|
|
24
|
-
model?: string;
|
|
25
|
-
provider?: string;
|
|
26
|
-
usage?: {
|
|
27
|
-
input?: number;
|
|
28
|
-
output?: number;
|
|
29
|
-
totalTokens?: number;
|
|
30
|
-
total_tokens?: number;
|
|
31
|
-
};
|
|
32
|
-
__openclaw?: {
|
|
33
|
-
seq?: number;
|
|
34
|
-
id?: string;
|
|
35
|
-
};
|
|
36
|
-
timestamp?: number;
|
|
37
|
-
};
|
|
38
|
-
/** Translate a single OpenClaw transcript message to app ChatMessage format. */
|
|
39
|
-
export declare function translateOpenClawMessage(msg: OpenClawMessage, sessionId: string): StoredChatMessage | null;
|
|
40
|
-
/** Translate an array of OpenClaw messages, filtering out non-user/assistant roles. */
|
|
41
|
-
export declare function translateOpenClawMessages(messages: OpenClawMessage[], sessionId: string): StoredChatMessage[];
|
|
42
|
-
export {};
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Translate OpenClaw native transcript messages to Friday app ChatMessage format.
|
|
3
|
-
*/
|
|
4
|
-
import crypto from "node:crypto";
|
|
5
|
-
/**
|
|
6
|
-
* Generate a serverId scoped to an OpenClaw transcript entry.
|
|
7
|
-
* Format: msg_<sessionId_short>_<seq>
|
|
8
|
-
* Seq is monotonically increasing per session, good for cursor-based sync.
|
|
9
|
-
*/
|
|
10
|
-
export function serverIdFromSeq(sessionId, seq) {
|
|
11
|
-
const sid = sessionId.slice(0, 8);
|
|
12
|
-
return `msg_${sid}_${String(seq).padStart(6, "0")}`;
|
|
13
|
-
}
|
|
14
|
-
/** Generate a standalone serverId (for messages not yet in transcript). */
|
|
15
|
-
export function generateStandaloneServerId() {
|
|
16
|
-
const ts = Date.now().toString(36);
|
|
17
|
-
const rand = crypto.randomBytes(6).toString("hex");
|
|
18
|
-
return `msg_${ts}_${rand}`;
|
|
19
|
-
}
|
|
20
|
-
function extractText(content) {
|
|
21
|
-
if (!content)
|
|
22
|
-
return "";
|
|
23
|
-
if (typeof content === "string")
|
|
24
|
-
return content;
|
|
25
|
-
const parts = [];
|
|
26
|
-
for (const block of content) {
|
|
27
|
-
if (!block || typeof block !== "object")
|
|
28
|
-
continue;
|
|
29
|
-
const t = block.type;
|
|
30
|
-
if (t === "text" || t === "output_text" || t === "input_text") {
|
|
31
|
-
if (typeof block.text === "string")
|
|
32
|
-
parts.push(block.text);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return parts.join("\n");
|
|
36
|
-
}
|
|
37
|
-
function extractThinking(content) {
|
|
38
|
-
if (!content || typeof content === "string")
|
|
39
|
-
return "";
|
|
40
|
-
if (!Array.isArray(content))
|
|
41
|
-
return "";
|
|
42
|
-
for (const block of content) {
|
|
43
|
-
if (!block || typeof block !== "object")
|
|
44
|
-
continue;
|
|
45
|
-
if (block.type === "thinking" && typeof block.thinking === "string") {
|
|
46
|
-
return block.thinking;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return "";
|
|
50
|
-
}
|
|
51
|
-
function extractToolCalls(content) {
|
|
52
|
-
if (!content || typeof content === "string")
|
|
53
|
-
return null;
|
|
54
|
-
if (!Array.isArray(content))
|
|
55
|
-
return null;
|
|
56
|
-
const calls = [];
|
|
57
|
-
for (const block of content) {
|
|
58
|
-
if (!block || typeof block !== "object")
|
|
59
|
-
continue;
|
|
60
|
-
const t = block.type;
|
|
61
|
-
if (t === "tool_use" || t === "toolcall" || t === "tooluse") {
|
|
62
|
-
calls.push({
|
|
63
|
-
id: block.id ?? "",
|
|
64
|
-
name: block.name ?? "unknown",
|
|
65
|
-
phase: "completed",
|
|
66
|
-
result: null,
|
|
67
|
-
error: null,
|
|
68
|
-
toolCallId: block.id ?? "",
|
|
69
|
-
durationMs: null,
|
|
70
|
-
startTs: null,
|
|
71
|
-
displayLabel: extractDisplayLabel(block.input),
|
|
72
|
-
displayEmoji: null,
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return calls.length > 0 ? calls : null;
|
|
77
|
-
}
|
|
78
|
-
function extractDisplayLabel(input) {
|
|
79
|
-
if (!input || typeof input !== "object")
|
|
80
|
-
return undefined;
|
|
81
|
-
const obj = input;
|
|
82
|
-
const str = obj.query ?? obj.path ?? obj.url ?? obj.message ?? obj.name ?? obj.text;
|
|
83
|
-
if (typeof str !== "string" || !str.trim())
|
|
84
|
-
return undefined;
|
|
85
|
-
if (str.length <= 80)
|
|
86
|
-
return str;
|
|
87
|
-
return str.slice(0, 77) + "...";
|
|
88
|
-
}
|
|
89
|
-
function extractThoughtEvents(content) {
|
|
90
|
-
if (!content || typeof content === "string")
|
|
91
|
-
return null;
|
|
92
|
-
if (!Array.isArray(content))
|
|
93
|
-
return null;
|
|
94
|
-
const events = [];
|
|
95
|
-
for (const block of content) {
|
|
96
|
-
if (!block || typeof block !== "object")
|
|
97
|
-
continue;
|
|
98
|
-
const t = block.type;
|
|
99
|
-
if (t === "thinking" && typeof block.thinking === "string") {
|
|
100
|
-
events.push({ kind: "thinking", thinkingText: block.thinking });
|
|
101
|
-
}
|
|
102
|
-
else if (t === "tool_use" || t === "toolcall" || t === "tooluse") {
|
|
103
|
-
events.push({
|
|
104
|
-
kind: "toolCall",
|
|
105
|
-
toolName: block.name ?? "unknown",
|
|
106
|
-
toolCallId: block.id ?? "",
|
|
107
|
-
phase: "completed",
|
|
108
|
-
startTs: null,
|
|
109
|
-
displayLabel: extractDisplayLabel(block.input),
|
|
110
|
-
displayEmoji: null,
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
else if (t === "tool_result" || t === "toolresult") {
|
|
114
|
-
events.push({
|
|
115
|
-
kind: "toolResult",
|
|
116
|
-
toolCallId: block.tool_use_id ?? "",
|
|
117
|
-
phase: block.is_error ? "failed" : "completed",
|
|
118
|
-
result: block.content ?? null,
|
|
119
|
-
error: block.is_error ? String(block.content ?? "") : null,
|
|
120
|
-
durationMs: null,
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return events.length > 0 ? events : null;
|
|
125
|
-
}
|
|
126
|
-
/** Translate a single OpenClaw transcript message to app ChatMessage format. */
|
|
127
|
-
export function translateOpenClawMessage(msg, sessionId) {
|
|
128
|
-
const role = (msg.role ?? "").toLowerCase();
|
|
129
|
-
if (role !== "user" && role !== "assistant")
|
|
130
|
-
return null;
|
|
131
|
-
const isFromCurrentUser = role === "user";
|
|
132
|
-
const content = extractText(msg.content);
|
|
133
|
-
const thinking = extractThinking(msg.content);
|
|
134
|
-
const toolCalls = extractToolCalls(msg.content);
|
|
135
|
-
const thoughtEvents = extractThoughtEvents(msg.content);
|
|
136
|
-
const entryId = msg.__openclaw?.id ?? "";
|
|
137
|
-
const seq = msg.__openclaw?.seq ?? 0;
|
|
138
|
-
const totalTokens = msg.usage?.totalTokens ?? msg.usage?.total_tokens ?? null;
|
|
139
|
-
const ts = msg.timestamp ? new Date(msg.timestamp).toISOString() : new Date().toISOString();
|
|
140
|
-
const sender = isFromCurrentUser
|
|
141
|
-
? { user: { name: "You" } }
|
|
142
|
-
: { assistant: { name: "Claude" } };
|
|
143
|
-
return {
|
|
144
|
-
id: "",
|
|
145
|
-
content,
|
|
146
|
-
sender,
|
|
147
|
-
timestamp: ts,
|
|
148
|
-
seq,
|
|
149
|
-
status: "sent",
|
|
150
|
-
isFromCurrentUser,
|
|
151
|
-
toolCalls,
|
|
152
|
-
attachment: null,
|
|
153
|
-
extraAttachments: null,
|
|
154
|
-
thoughtEvents,
|
|
155
|
-
ttsAttachment: null,
|
|
156
|
-
commandTerminals: null,
|
|
157
|
-
isInternalTrace: false,
|
|
158
|
-
modelName: msg.model ?? null,
|
|
159
|
-
totalTokens,
|
|
160
|
-
contextWindowMax: null,
|
|
161
|
-
contextTokensUsed: null,
|
|
162
|
-
runId: null,
|
|
163
|
-
serverId: seq > 0 ? serverIdFromSeq(sessionId, seq) : null,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
/** Translate an array of OpenClaw messages, filtering out non-user/assistant roles. */
|
|
167
|
-
export function translateOpenClawMessages(messages, sessionId) {
|
|
168
|
-
return messages
|
|
169
|
-
.map((m) => translateOpenClawMessage(m, sessionId))
|
|
170
|
-
.filter((m) => m !== null);
|
|
171
|
-
}
|