@rethinkingstudio/clawpilot 2.1.6 → 2.1.7-beta.0

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.
Files changed (63) hide show
  1. package/dist/ccconnect/api-client.d.ts +78 -0
  2. package/dist/ccconnect/api-client.js +98 -0
  3. package/dist/ccconnect/api-client.js.map +1 -0
  4. package/dist/ccconnect/local-runtime.d.ts +15 -0
  5. package/dist/ccconnect/local-runtime.js +181 -0
  6. package/dist/ccconnect/local-runtime.js.map +1 -0
  7. package/dist/ccconnect/project-config.d.ts +25 -0
  8. package/dist/ccconnect/project-config.js +309 -0
  9. package/dist/ccconnect/project-config.js.map +1 -0
  10. package/dist/commands/install.js +1 -0
  11. package/dist/commands/install.js.map +1 -1
  12. package/dist/commands/local-handlers.js +46 -0
  13. package/dist/commands/local-handlers.js.map +1 -1
  14. package/dist/commands/pair.d.ts +4 -0
  15. package/dist/commands/pair.js +87 -12
  16. package/dist/commands/pair.js.map +1 -1
  17. package/dist/commands/run.js +36 -1
  18. package/dist/commands/run.js.map +1 -1
  19. package/dist/commands/session-key.js +5 -2
  20. package/dist/commands/session-key.js.map +1 -1
  21. package/dist/commands/status.js +41 -4
  22. package/dist/commands/status.js.map +1 -1
  23. package/dist/config/config.d.ts +9 -1
  24. package/dist/config/config.js +30 -4
  25. package/dist/config/config.js.map +1 -1
  26. package/dist/hermes/agent-bridge.js +46 -2
  27. package/dist/hermes/agent-bridge.js.map +1 -1
  28. package/dist/hermes/home.d.ts +23 -0
  29. package/dist/hermes/home.js +250 -0
  30. package/dist/hermes/home.js.map +1 -0
  31. package/dist/hermes/model-catalog.js +10 -9
  32. package/dist/hermes/model-catalog.js.map +1 -1
  33. package/dist/i18n/index.js +2 -2
  34. package/dist/i18n/index.js.map +1 -1
  35. package/dist/index.js +7 -3
  36. package/dist/index.js.map +1 -1
  37. package/dist/media/assistant-attachments.d.ts +2 -0
  38. package/dist/media/assistant-attachments.js +18 -18
  39. package/dist/media/assistant-attachments.js.map +1 -1
  40. package/dist/platform/service-manager.js +9 -0
  41. package/dist/platform/service-manager.js.map +1 -1
  42. package/dist/relay/agent-status.d.ts +55 -0
  43. package/dist/relay/agent-status.js +294 -0
  44. package/dist/relay/agent-status.js.map +1 -0
  45. package/dist/relay/ccconnect-relay-manager.d.ts +13 -0
  46. package/dist/relay/ccconnect-relay-manager.js +1105 -0
  47. package/dist/relay/ccconnect-relay-manager.js.map +1 -0
  48. package/dist/relay/hermes-relay-manager.js +225 -53
  49. package/dist/relay/hermes-relay-manager.js.map +1 -1
  50. package/dist/relay/relay-command-dispatcher.d.ts +14 -0
  51. package/dist/relay/relay-command-dispatcher.js +25 -0
  52. package/dist/relay/relay-command-dispatcher.js.map +1 -0
  53. package/dist/relay/relay-manager.js +60 -29
  54. package/dist/relay/relay-manager.js.map +1 -1
  55. package/dist/relay/relay-method-registry.d.ts +9 -0
  56. package/dist/relay/relay-method-registry.js +61 -0
  57. package/dist/relay/relay-method-registry.js.map +1 -0
  58. package/dist/relay/relay-watchdog.d.ts +2 -0
  59. package/dist/relay/relay-watchdog.js +31 -0
  60. package/dist/relay/relay-watchdog.js.map +1 -0
  61. package/package.json +1 -1
  62. package/scripts/hermes_agent_bridge.py +14 -2
  63. package/scripts/verify-build-config.mjs +1 -3
@@ -0,0 +1,1105 @@
1
+ import { randomUUID } from "crypto";
2
+ import { WebSocket } from "ws";
3
+ import { getServicePlatform } from "../platform/service-manager.js";
4
+ import { CCConnectManagementClient, decodeCCConnectSessionKey, encodeCCConnectSessionKey, timestampMs, } from "../ccconnect/api-client.js";
5
+ import { createCCConnectProject, discoverCCConnectProjects, restartCCConnect } from "../ccconnect/project-config.js";
6
+ import { uploadMediaBlocks } from "../media/assistant-attachments.js";
7
+ import { installRelayWatchdog } from "./relay-watchdog.js";
8
+ import { dispatchCommonRelayCommand } from "./relay-command-dispatcher.js";
9
+ import { relayMethodSupport } from "./relay-method-registry.js";
10
+ const DEFAULT_CONTEXT_TOKENS = 200_000;
11
+ export async function runCCConnectRelayManager(opts) {
12
+ const wsUrl = buildRelayUrl(opts.relayServerUrl, opts.gatewayId, opts.relaySecret);
13
+ const client = new CCConnectManagementClient({
14
+ managementUrl: opts.managementUrl,
15
+ managementToken: opts.managementToken,
16
+ });
17
+ return new Promise((resolve) => {
18
+ let relayWs;
19
+ let bridgeWs = null;
20
+ let bridgeReady = false;
21
+ let bridgeReconnectTimer = null;
22
+ let bridgePingTimer = null;
23
+ let bridgeReconnectAttempts = 0;
24
+ const rawToEncoded = new Map();
25
+ const rawToProject = new Map();
26
+ const activeRunByEncoded = new Map();
27
+ const previewHandleToRun = new Map();
28
+ const pendingInteractiveReplyByEncoded = new Set();
29
+ try {
30
+ relayWs = new WebSocket(wsUrl);
31
+ }
32
+ catch (err) {
33
+ console.error("Failed to create cc-connect relay WebSocket:", err);
34
+ resolve(true);
35
+ return;
36
+ }
37
+ const uninstallWatchdog = installRelayWatchdog(relayWs, `ccconnect:${opts.gatewayId}`);
38
+ function send(msg) {
39
+ if (relayWs.readyState === WebSocket.OPEN) {
40
+ relayWs.send(JSON.stringify(msg));
41
+ }
42
+ }
43
+ function sendResponse(id, payload) {
44
+ if (id)
45
+ send({ type: "res", id, ok: true, payload });
46
+ }
47
+ function sendError(id, error) {
48
+ if (id) {
49
+ send({
50
+ type: "res",
51
+ id,
52
+ ok: false,
53
+ error: { message: error instanceof Error ? error.message : String(error) },
54
+ });
55
+ }
56
+ }
57
+ function bridgeUrlWithToken() {
58
+ if (!opts.bridgeToken)
59
+ return opts.bridgeUrl;
60
+ const sep = opts.bridgeUrl.includes("?") ? "&" : "?";
61
+ return `${opts.bridgeUrl}${sep}token=${encodeURIComponent(opts.bridgeToken)}`;
62
+ }
63
+ function clearBridgeTimers() {
64
+ if (bridgeReconnectTimer) {
65
+ clearTimeout(bridgeReconnectTimer);
66
+ bridgeReconnectTimer = null;
67
+ }
68
+ if (bridgePingTimer) {
69
+ clearInterval(bridgePingTimer);
70
+ bridgePingTimer = null;
71
+ }
72
+ }
73
+ function startBridgePing() {
74
+ if (bridgePingTimer)
75
+ clearInterval(bridgePingTimer);
76
+ bridgePingTimer = setInterval(() => {
77
+ if (bridgeWs?.readyState === WebSocket.OPEN) {
78
+ bridgeWs.send(JSON.stringify({ type: "ping", ts: Date.now() }));
79
+ }
80
+ }, 30_000);
81
+ bridgePingTimer.unref();
82
+ }
83
+ function scheduleBridgeReconnect(reason) {
84
+ if (relayWs.readyState !== WebSocket.OPEN || bridgeReconnectTimer)
85
+ return;
86
+ const delay = Math.min(30_000, 1_000 * 2 ** Math.min(bridgeReconnectAttempts, 5));
87
+ bridgeReconnectAttempts += 1;
88
+ console.log(`[runtime:ccconnect] bridge reconnect in ${delay}ms: ${reason}`);
89
+ bridgeReconnectTimer = setTimeout(() => {
90
+ bridgeReconnectTimer = null;
91
+ connectBridge();
92
+ }, delay);
93
+ bridgeReconnectTimer.unref();
94
+ }
95
+ function connectBridge() {
96
+ clearBridgeTimers();
97
+ bridgeReady = false;
98
+ if (bridgeWs && bridgeWs.readyState !== WebSocket.CLOSED) {
99
+ bridgeWs.close();
100
+ }
101
+ try {
102
+ bridgeWs = new WebSocket(bridgeUrlWithToken());
103
+ }
104
+ catch (error) {
105
+ const detail = error instanceof Error ? error.message : String(error);
106
+ console.error(`[runtime:ccconnect] bridge create failed: ${detail}`);
107
+ send({ type: "gateway_disconnected", reason: detail });
108
+ scheduleBridgeReconnect(detail);
109
+ return;
110
+ }
111
+ const ws = bridgeWs;
112
+ ws.on("open", () => {
113
+ ws.send(JSON.stringify({
114
+ type: "register",
115
+ platform: "pocketclaw",
116
+ capabilities: [
117
+ "text",
118
+ "typing",
119
+ "preview",
120
+ "update_message",
121
+ "delete_message",
122
+ "reconstruct_reply",
123
+ "buttons",
124
+ "card",
125
+ "image",
126
+ "file",
127
+ "audio",
128
+ ],
129
+ metadata: {
130
+ version: "1.0.0",
131
+ description: "PocketClaw relay adapter",
132
+ progress_style: "card",
133
+ supports_progress_card_payload: true,
134
+ },
135
+ }));
136
+ });
137
+ ws.on("message", (raw) => {
138
+ if (bridgeWs !== ws)
139
+ return;
140
+ try {
141
+ handleBridgeMessage(JSON.parse(raw.toString()));
142
+ }
143
+ catch (error) {
144
+ console.warn(`[runtime:ccconnect] ignored invalid bridge message: ${String(error)}`);
145
+ }
146
+ });
147
+ ws.on("close", (code, reason) => {
148
+ if (bridgeWs !== ws)
149
+ return;
150
+ bridgeReady = false;
151
+ if (bridgePingTimer) {
152
+ clearInterval(bridgePingTimer);
153
+ bridgePingTimer = null;
154
+ }
155
+ console.log(`[runtime:ccconnect] bridge disconnected: ${code} ${reason.toString()}`);
156
+ send({ type: "gateway_disconnected", reason: "cc-connect bridge disconnected" });
157
+ scheduleBridgeReconnect(`close ${code}`);
158
+ });
159
+ ws.on("error", (error) => {
160
+ if (bridgeWs !== ws)
161
+ return;
162
+ bridgeReady = false;
163
+ console.error(`[runtime:ccconnect] bridge error: ${error.message}`);
164
+ send({ type: "gateway_disconnected", reason: error.message });
165
+ scheduleBridgeReconnect(error.message);
166
+ });
167
+ }
168
+ function handleBridgeMessage(msg) {
169
+ void handleBridgeMessageAsync(msg);
170
+ }
171
+ async function handleBridgeMessageAsync(msg) {
172
+ const record = msg;
173
+ if (msg.type === "register_ack") {
174
+ bridgeReady = Boolean(msg.ok);
175
+ if (bridgeReady) {
176
+ bridgeReconnectAttempts = 0;
177
+ console.log("[runtime:ccconnect] bridge connected");
178
+ startBridgePing();
179
+ send({ type: "gateway_connected" });
180
+ }
181
+ else {
182
+ send({ type: "gateway_disconnected", reason: stringValue(record.error) ?? "cc-connect bridge registration failed" });
183
+ scheduleBridgeReconnect(stringValue(record.error) ?? "registration failed");
184
+ }
185
+ return;
186
+ }
187
+ if (msg.type === "pong")
188
+ return;
189
+ const rawSessionKey = stringValue(record.session_key) ?? "";
190
+ const encodedSessionKey = await resolveEncodedBridgeSession(record);
191
+ if (!encodedSessionKey)
192
+ return;
193
+ const previewHandle = stringValue(record.preview_handle);
194
+ const runInfo = resolveRunForBridgeMessage(encodedSessionKey, previewHandle);
195
+ let runId = runInfo.runId;
196
+ if (msg.type === "typing_start") {
197
+ emitChat(runId, encodedSessionKey, "delta", undefined, true);
198
+ return;
199
+ }
200
+ if (msg.type === "typing_stop") {
201
+ return;
202
+ }
203
+ if (msg.type === "preview_start") {
204
+ const content = stringValue(record.content) ?? "";
205
+ const sideChannel = isProgressContent(content);
206
+ if (sideChannel) {
207
+ runId = randomUUID();
208
+ }
209
+ emitContentChat(runId, encodedSessionKey, "delta", normalizeBridgeContent(content), false, sideChannel);
210
+ const refId = stringValue(record.ref_id);
211
+ const handle = `pocketclaw-${runId}`;
212
+ previewHandleToRun.set(handle, { encodedSessionKey, runId, sideChannel });
213
+ if (refId) {
214
+ bridgeWs?.send(JSON.stringify({
215
+ type: "preview_ack",
216
+ ref_id: refId,
217
+ preview_handle: handle,
218
+ }));
219
+ }
220
+ return;
221
+ }
222
+ if (msg.type === "update_message") {
223
+ const content = stringValue(record.content) ?? "";
224
+ const normalized = normalizeBridgeContent(content);
225
+ const state = runInfo.sideChannel && progressContentIsCompleted(content) ? "final" : "delta";
226
+ emitContentChat(runId, encodedSessionKey, state, normalized, false, runInfo.sideChannel);
227
+ if (state === "final" && previewHandle)
228
+ previewHandleToRun.delete(previewHandle);
229
+ return;
230
+ }
231
+ if (msg.type === "delete_message") {
232
+ if (previewHandle)
233
+ previewHandleToRun.delete(previewHandle);
234
+ send({
235
+ type: "event",
236
+ event: "message.delete",
237
+ payload: {
238
+ runId,
239
+ sessionKey: encodedSessionKey,
240
+ previewHandle,
241
+ keepTurn: true,
242
+ sideChannel: runInfo.sideChannel,
243
+ },
244
+ });
245
+ return;
246
+ }
247
+ if (msg.type === "reply_stream") {
248
+ const text = stringValue(record.full_text) ?? stringValue(record.delta) ?? "";
249
+ const done = Boolean(record.done);
250
+ const hasProgressPreview = hasActiveSideChannelPreview(encodedSessionKey);
251
+ const state = done && !isIntermediateReply(text) && !hasProgressPreview ? "final" : "delta";
252
+ emitChat(runId, encodedSessionKey, state, text, false);
253
+ if (state === "final")
254
+ clearActiveBridgeRun(encodedSessionKey, runId);
255
+ return;
256
+ }
257
+ if (msg.type === "reply") {
258
+ const content = stringValue(record.content) ?? "";
259
+ const permissionFallback = permissionHintActions(content, rawSessionKey, stringValue(record.reply_ctx) ?? rawSessionKey);
260
+ const keepTurnOpen = consumeInteractiveReply(encodedSessionKey) ||
261
+ isIntermediateReply(content) ||
262
+ hasActiveSideChannelPreview(encodedSessionKey);
263
+ const state = keepTurnOpen ? "delta" : "final";
264
+ if (permissionFallback) {
265
+ emitStructuredChat(runId, encodedSessionKey, state, {
266
+ text: content,
267
+ blocks: [textBlock(content), permissionFallback].filter(Boolean),
268
+ });
269
+ }
270
+ else {
271
+ emitChat(runId, encodedSessionKey, state, content, false);
272
+ }
273
+ if (state === "final")
274
+ clearActiveBridgeRun(encodedSessionKey, runId);
275
+ return;
276
+ }
277
+ if (msg.type === "buttons") {
278
+ const actions = normalizeBridgeButtons(record.buttons);
279
+ console.log(`[runtime:ccconnect] bridge buttons session=${rawSessionKey} actions=${actions.length}`);
280
+ emitStructuredChat(runId, encodedSessionKey, "delta", {
281
+ text: stringValue(record.content) ?? "",
282
+ blocks: [
283
+ textBlock(stringValue(record.content) ?? ""),
284
+ actionsBlock(actions, rawSessionKey, stringValue(record.reply_ctx) ?? rawSessionKey),
285
+ ].filter(Boolean),
286
+ });
287
+ return;
288
+ }
289
+ if (msg.type === "card") {
290
+ console.log(`[runtime:ccconnect] bridge card session=${rawSessionKey}`);
291
+ emitStructuredChat(runId, encodedSessionKey, "delta", normalizeBridgeCard(record.card, rawSessionKey, stringValue(record.reply_ctx) ?? rawSessionKey));
292
+ return;
293
+ }
294
+ if (msg.type === "image" || msg.type === "file" || msg.type === "audio") {
295
+ const attachment = await uploadBridgeMedia(msg);
296
+ emitStructuredChat(runId, encodedSessionKey, "final", {
297
+ text: attachment.text,
298
+ attachments: attachment.attachments,
299
+ blocks: attachment.text ? [textBlock(attachment.text)] : [],
300
+ });
301
+ clearActiveBridgeRun(encodedSessionKey, runId);
302
+ return;
303
+ }
304
+ if (msg.type === "error") {
305
+ emitChat(runId, encodedSessionKey, "error", stringValue(record.message) ?? "cc-connect bridge error", false);
306
+ clearActiveBridgeRun(encodedSessionKey, runId);
307
+ }
308
+ }
309
+ async function resolveEncodedBridgeSession(record) {
310
+ const rawSessionKey = stringValue(record.session_key);
311
+ if (!rawSessionKey) {
312
+ if (activeRunByEncoded.size === 1) {
313
+ return activeRunByEncoded.keys().next().value;
314
+ }
315
+ console.warn(`[runtime:ccconnect] dropped bridge ${String(record.type)} without session_key`);
316
+ return undefined;
317
+ }
318
+ const existing = rawToEncoded.get(rawSessionKey);
319
+ if (existing)
320
+ return existing;
321
+ const explicitProject = stringValue(record.project) ?? rawToProject.get(rawSessionKey);
322
+ if (explicitProject) {
323
+ const encoded = encodeCCConnectSessionKey(explicitProject, rawSessionKey, rawSessionKey);
324
+ rawToEncoded.set(rawSessionKey, encoded);
325
+ rawToProject.set(rawSessionKey, explicitProject);
326
+ console.warn(`[runtime:ccconnect] recovered bridge session mapping project=${explicitProject} rawSessionKey=${rawSessionKey}`);
327
+ return encoded;
328
+ }
329
+ if (activeRunByEncoded.size === 1) {
330
+ const encoded = activeRunByEncoded.keys().next().value;
331
+ if (encoded) {
332
+ rawToEncoded.set(rawSessionKey, encoded);
333
+ console.warn(`[runtime:ccconnect] inferred bridge session mapping rawSessionKey=${rawSessionKey}`);
334
+ return encoded;
335
+ }
336
+ }
337
+ console.warn(`[runtime:ccconnect] dropped bridge ${String(record.type)} for unmapped session_key=${rawSessionKey}`);
338
+ return undefined;
339
+ }
340
+ function ensureRunId(encodedSessionKey, preferred) {
341
+ const existing = activeRunByEncoded.get(encodedSessionKey);
342
+ if (existing)
343
+ return existing;
344
+ const runId = preferred || randomUUID();
345
+ activeRunByEncoded.set(encodedSessionKey, runId);
346
+ return runId;
347
+ }
348
+ function resolveRunForBridgeMessage(encodedSessionKey, previewHandle) {
349
+ if (previewHandle) {
350
+ const preview = previewHandleToRun.get(previewHandle);
351
+ if (preview) {
352
+ return { runId: preview.runId, sideChannel: preview.sideChannel };
353
+ }
354
+ }
355
+ return { runId: ensureRunId(encodedSessionKey) };
356
+ }
357
+ function clearActiveBridgeRun(encodedSessionKey, runId) {
358
+ if (activeRunByEncoded.get(encodedSessionKey) === runId) {
359
+ activeRunByEncoded.delete(encodedSessionKey);
360
+ }
361
+ pendingInteractiveReplyByEncoded.delete(encodedSessionKey);
362
+ for (const [handle, preview] of previewHandleToRun) {
363
+ if (preview.runId === runId)
364
+ previewHandleToRun.delete(handle);
365
+ }
366
+ }
367
+ function hasActiveSideChannelPreview(encodedSessionKey) {
368
+ for (const preview of previewHandleToRun.values()) {
369
+ if (preview.encodedSessionKey === encodedSessionKey && preview.sideChannel) {
370
+ return true;
371
+ }
372
+ }
373
+ return false;
374
+ }
375
+ function consumeInteractiveReply(encodedSessionKey) {
376
+ if (!pendingInteractiveReplyByEncoded.has(encodedSessionKey))
377
+ return false;
378
+ pendingInteractiveReplyByEncoded.delete(encodedSessionKey);
379
+ return true;
380
+ }
381
+ function emitChat(runId, sessionKey, state, text, loading) {
382
+ const payload = { runId, sessionKey, state };
383
+ if (loading)
384
+ payload.loading = true;
385
+ if (text !== undefined) {
386
+ payload.message = { content: [{ type: "text", text }] };
387
+ }
388
+ if (state === "error")
389
+ payload.errorMessage = text ?? "cc-connect error";
390
+ send({ type: "event", event: "chat", payload });
391
+ }
392
+ function emitContentChat(runId, sessionKey, state, content, loading, sideChannel) {
393
+ const payload = { runId, sessionKey, state };
394
+ if (loading)
395
+ payload.loading = true;
396
+ if (sideChannel)
397
+ payload.sideChannel = true;
398
+ payload.message = { content: content.blocks?.length ? content.blocks : [{ type: "text", text: content.text }] };
399
+ if (state === "error")
400
+ payload.errorMessage = content.text || "cc-connect error";
401
+ send({ type: "event", event: "chat", payload });
402
+ }
403
+ function emitStructuredChat(runId, sessionKey, state, data) {
404
+ const blocks = data.blocks?.length ? data.blocks : data.text !== undefined ? [textBlock(data.text)] : undefined;
405
+ const payload = { runId, sessionKey, state };
406
+ if (blocks)
407
+ payload.message = { content: blocks };
408
+ if (data.attachments)
409
+ payload.attachments = data.attachments;
410
+ if (state === "error")
411
+ payload.errorMessage = data.text ?? "cc-connect error";
412
+ send({ type: "event", event: "chat", payload });
413
+ }
414
+ async function uploadBridgeMedia(msg) {
415
+ const data = stringValue(msg.data);
416
+ const fileName = stringValue(msg.file_name);
417
+ const mimeType = bridgeMediaMimeType(msg);
418
+ const label = fileName || (msg.type === "audio" ? "Audio message" : msg.type === "image" ? "Image" : "File");
419
+ if (!data) {
420
+ return { text: `[${label} missing data]`, attachments: [] };
421
+ }
422
+ const block = {
423
+ mimeType,
424
+ fileName,
425
+ data: Buffer.from(data.replace(/^data:[^;]+;base64,/, ""), "base64"),
426
+ };
427
+ const uploaded = await uploadMediaBlocks([block], opts.relayServerUrl, opts.gatewayId, opts.relaySecret);
428
+ const attachments = uploaded.map((att) => ({
429
+ ...att,
430
+ fileName: fileName ?? undefined,
431
+ }));
432
+ return { text: label, attachments };
433
+ }
434
+ async function respondSessionsList(requestId) {
435
+ const projects = await client.listProjects();
436
+ const sessions = [];
437
+ for (const project of projects) {
438
+ const projectSessions = await client.listSessions(project.name);
439
+ for (const session of projectSessions) {
440
+ const encoded = rememberSession(project.name, session);
441
+ sessions.push(toRuntimeSession(project.name, session, encoded));
442
+ }
443
+ }
444
+ sendResponse(requestId, {
445
+ defaults: { contextTokens: DEFAULT_CONTEXT_TOKENS, model: "cc-connect", modelProvider: "cc-connect" },
446
+ sessions,
447
+ count: sessions.length,
448
+ });
449
+ }
450
+ async function respondProjectsList(requestId) {
451
+ const projects = await client.listProjects();
452
+ const detailed = await Promise.all(projects.map(async (project) => {
453
+ try {
454
+ const detail = await client.getProject(project.name);
455
+ return {
456
+ name: detail.name,
457
+ agentType: detail.agent_type,
458
+ workDir: detail.work_dir,
459
+ mode: detail.agent_mode || detail.mode,
460
+ sessionsCount: detail.sessions_count ?? project.sessions_count ?? 0,
461
+ platforms: detail.platforms ?? project.platforms ?? [],
462
+ heartbeatEnabled: detail.heartbeat_enabled ?? project.heartbeat_enabled ?? false,
463
+ activeSessionKeys: detail.active_session_keys ?? [],
464
+ };
465
+ }
466
+ catch {
467
+ return {
468
+ name: project.name,
469
+ agentType: project.agent_type,
470
+ sessionsCount: project.sessions_count ?? 0,
471
+ platforms: project.platforms ?? [],
472
+ heartbeatEnabled: project.heartbeat_enabled ?? false,
473
+ activeSessionKeys: [],
474
+ };
475
+ }
476
+ }));
477
+ sendResponse(requestId, { projects: detailed, count: detailed.length });
478
+ }
479
+ async function respondProjectsDiscover(requestId) {
480
+ const projects = await client.listProjects();
481
+ const detailed = await Promise.all(projects.map(async (project) => {
482
+ try {
483
+ const detail = await client.getProject(project.name);
484
+ return { name: detail.name, workDir: detail.work_dir };
485
+ }
486
+ catch {
487
+ return { name: project.name, workDir: undefined };
488
+ }
489
+ }));
490
+ const directories = discoverCCConnectProjects(detailed);
491
+ sendResponse(requestId, { directories, count: directories.length });
492
+ }
493
+ async function respondProjectCreate(requestId, params) {
494
+ const p = params;
495
+ const created = createCCConnectProject(p);
496
+ restartCCConnect(created.configPath);
497
+ sendResponse(requestId, {
498
+ project: {
499
+ name: created.name,
500
+ agentType: created.agentType,
501
+ workDir: created.workDir,
502
+ mode: created.mode,
503
+ sessionsCount: 0,
504
+ platforms: ["line", "bridge"],
505
+ heartbeatEnabled: false,
506
+ activeSessionKeys: [],
507
+ },
508
+ restarted: true,
509
+ });
510
+ }
511
+ async function respondMessagesHistory(requestId, params) {
512
+ const p = params;
513
+ if (!p.sessionKey)
514
+ throw new Error("sessionKey required");
515
+ const decoded = decodeCCConnectSessionKey(p.sessionKey);
516
+ const sessionId = await resolveCCConnectSessionId(decoded.project, decoded.sessionKey, decoded.sessionId);
517
+ const detail = await client.getSession(decoded.project, sessionId, Math.min(p.limit ?? 50, 200));
518
+ const history = detail.history ?? [];
519
+ const messages = history.map((entry, index) => ({
520
+ id: index + 1,
521
+ role: entry.role === "user" ? "user" : "assistant",
522
+ content: entry.content ?? "",
523
+ runId: entry.role === "user" ? undefined : `${sessionId}-${index + 1}`,
524
+ state: "final",
525
+ createdAt: timestampMs(entry.timestamp),
526
+ updatedAt: timestampMs(entry.timestamp),
527
+ }));
528
+ sendResponse(requestId, { messages, hasMore: false });
529
+ }
530
+ async function respondSessionCreate(requestId, params) {
531
+ const p = params;
532
+ const projects = await client.listProjects();
533
+ const project = p.project?.trim() || projects[0]?.name;
534
+ if (!project)
535
+ throw new Error("cc-connect has no configured projects");
536
+ const title = p.title?.trim() || "New Session";
537
+ const rawSessionKey = `pocketclaw:${opts.gatewayId}:default`;
538
+ const created = await client.createSession(project, rawSessionKey, title);
539
+ const session = {
540
+ ...created,
541
+ id: created.id || rawSessionKey,
542
+ session_key: created.session_key || rawSessionKey,
543
+ name: created.name || title,
544
+ platform: created.platform || "pocketclaw",
545
+ live: created.live ?? false,
546
+ };
547
+ const encoded = rememberSession(project, session);
548
+ sendResponse(requestId, { session: toRuntimeSession(project, session, encoded) });
549
+ }
550
+ async function handleChatSend(params) {
551
+ const p = params;
552
+ if (!p.sessionKey)
553
+ throw new Error("sessionKey required");
554
+ if (!p.message)
555
+ throw new Error("message required");
556
+ const decoded = await resolveCCConnectSendTarget(p.sessionKey);
557
+ rawToEncoded.set(decoded.sessionKey, p.sessionKey);
558
+ rawToProject.set(decoded.sessionKey, decoded.project);
559
+ activeRunByEncoded.delete(p.sessionKey);
560
+ pendingInteractiveReplyByEncoded.delete(p.sessionKey);
561
+ const runId = ensureRunId(p.sessionKey, p.idempotencyKey);
562
+ emitChat(runId, p.sessionKey, "delta", undefined, true);
563
+ if (bridgeReady && bridgeWs?.readyState === WebSocket.OPEN) {
564
+ console.log(`[runtime:ccconnect] chat.send via bridge project=${decoded.project} rawSessionKey=${decoded.sessionKey}`);
565
+ bridgeWs.send(JSON.stringify({
566
+ type: "message",
567
+ msg_id: `pocketclaw-${Date.now()}`,
568
+ session_key: decoded.sessionKey,
569
+ user_id: "pocketclaw",
570
+ user_name: "PocketClaw",
571
+ content: p.message,
572
+ reply_ctx: decoded.sessionKey,
573
+ project: decoded.project,
574
+ ...bridgeAttachments(params),
575
+ }));
576
+ return;
577
+ }
578
+ scheduleBridgeReconnect("bridge not ready during chat.send");
579
+ console.log(`[runtime:ccconnect] chat.send via management fallback project=${decoded.project} rawSessionKey=${decoded.sessionKey}`);
580
+ await client.send(decoded.project, decoded.sessionKey, p.message);
581
+ }
582
+ async function handleChatAction(params) {
583
+ const p = params;
584
+ if (!p.sessionKey)
585
+ throw new Error("sessionKey required");
586
+ if (!p.action)
587
+ throw new Error("action required");
588
+ if (!bridgeReady || bridgeWs?.readyState !== WebSocket.OPEN) {
589
+ throw new Error("cc-connect bridge not ready");
590
+ }
591
+ const decoded = await resolveCCConnectSendTarget(p.sessionKey);
592
+ if (isInteractiveAction(p.action)) {
593
+ pendingInteractiveReplyByEncoded.add(p.sessionKey);
594
+ }
595
+ bridgeWs.send(JSON.stringify({
596
+ type: "card_action",
597
+ session_key: decoded.sessionKey,
598
+ action: p.action,
599
+ reply_ctx: p.replyCtx || decoded.sessionKey,
600
+ project: decoded.project,
601
+ }));
602
+ }
603
+ async function resolveCCConnectSendTarget(sessionKey) {
604
+ try {
605
+ return decodeCCConnectSessionKey(sessionKey);
606
+ }
607
+ catch {
608
+ const projects = await client.listProjects();
609
+ for (const project of projects) {
610
+ const sessions = await client.listSessions(project.name);
611
+ const preferred = sessions.find((session) => session.active || session.live) ?? sessions[0];
612
+ if (preferred?.session_key) {
613
+ console.warn(`[runtime:ccconnect] non-ccconnect sessionKey received (${sessionKey}); using ${project.name}/${preferred.session_key}`);
614
+ return {
615
+ project: project.name,
616
+ sessionKey: preferred.session_key,
617
+ sessionId: preferred.id || preferred.session_key,
618
+ };
619
+ }
620
+ }
621
+ throw new Error(`invalid cc-connect session key: ${sessionKey}`);
622
+ }
623
+ }
624
+ function rememberSession(project, session) {
625
+ const encoded = encodeCCConnectSessionKey(project, session.session_key, session.session_key);
626
+ if (!rawToEncoded.has(session.session_key)) {
627
+ rawToEncoded.set(session.session_key, encoded);
628
+ rawToProject.set(session.session_key, project);
629
+ }
630
+ return encoded;
631
+ }
632
+ async function resolveCCConnectSessionId(project, sessionKey, sessionId) {
633
+ if (sessionId && sessionId !== sessionKey)
634
+ return sessionId;
635
+ const sessions = await client.listSessions(project);
636
+ const matched = sessions.find((session) => session.session_key === sessionKey);
637
+ return matched?.id || sessionId || sessionKey;
638
+ }
639
+ function toRuntimeSession(project, session, encoded) {
640
+ const platform = session.platform || session.session_key.split(":")[0] || "cc-connect";
641
+ const title = session.name || session.chat_name || session.user_name || session.session_key || session.id;
642
+ return {
643
+ key: encoded,
644
+ label: `${project} / ${platform} / ${title}`,
645
+ displayName: title,
646
+ agentId: project,
647
+ kind: platform,
648
+ state: session.live ? "running" : "idle",
649
+ model: session.agent_type || "cc-connect",
650
+ contextTokens: DEFAULT_CONTEXT_TOKENS,
651
+ totalTokens: 0,
652
+ inputTokens: 0,
653
+ outputTokens: 0,
654
+ updatedAt: timestampMs(session.updated_at),
655
+ runtimeType: "ccconnect",
656
+ };
657
+ }
658
+ relayWs.on("open", async () => {
659
+ console.log(`Connected to relay server (gatewayId=${opts.gatewayId})`);
660
+ send({ type: "relay_hello", platform: getServicePlatform() });
661
+ opts.onConnected?.();
662
+ try {
663
+ await client.status();
664
+ connectBridge();
665
+ }
666
+ catch (error) {
667
+ const reason = error instanceof Error ? error.message : String(error);
668
+ console.error(`[runtime:ccconnect] management API unavailable: ${reason}`);
669
+ send({ type: "gateway_disconnected", reason });
670
+ }
671
+ });
672
+ relayWs.on("message", async (raw) => {
673
+ let msg;
674
+ try {
675
+ msg = JSON.parse(raw.toString());
676
+ }
677
+ catch {
678
+ return;
679
+ }
680
+ if (msg.type !== "cmd" || !msg.method)
681
+ return;
682
+ const requestId = msg.id;
683
+ const support = relayMethodSupport(msg.method, "ccconnect") ?? "unsupported";
684
+ console.log(`[relay:ccconnect] cmd received method=${msg.method} support=${support} id=${requestId ?? "(no-id)"}`);
685
+ if (await dispatchCommonRelayCommand(msg.method, msg.params, requestId, send)) {
686
+ return;
687
+ }
688
+ try {
689
+ switch (msg.method) {
690
+ case "projects.list":
691
+ if (requestId)
692
+ await respondProjectsList(requestId);
693
+ return;
694
+ case "projects.discover":
695
+ if (requestId)
696
+ await respondProjectsDiscover(requestId);
697
+ return;
698
+ case "projects.create":
699
+ if (requestId)
700
+ await respondProjectCreate(requestId, msg.params ?? {});
701
+ return;
702
+ case "sessions.list":
703
+ if (requestId)
704
+ await respondSessionsList(requestId);
705
+ return;
706
+ case "messages.history":
707
+ if (requestId)
708
+ await respondMessagesHistory(requestId, msg.params ?? {});
709
+ return;
710
+ case "sessions.create":
711
+ if (requestId)
712
+ await respondSessionCreate(requestId, msg.params ?? {});
713
+ return;
714
+ case "chat.send":
715
+ await handleChatSend(msg.params ?? {});
716
+ if (requestId)
717
+ sendResponse(requestId, { ok: true });
718
+ return;
719
+ case "chat.action":
720
+ await handleChatAction(msg.params ?? {});
721
+ if (requestId)
722
+ sendResponse(requestId, { ok: true });
723
+ return;
724
+ case "chat.abort":
725
+ if (requestId)
726
+ sendResponse(requestId, { ok: true });
727
+ return;
728
+ case "models.list":
729
+ if (requestId)
730
+ sendResponse(requestId, { models: [] });
731
+ return;
732
+ case "skills.status":
733
+ if (requestId)
734
+ sendResponse(requestId, { skills: [] });
735
+ return;
736
+ case "agents.list":
737
+ if (requestId)
738
+ sendResponse(requestId, { defaultId: "cc-connect", mainKey: "cc-connect", agents: [] });
739
+ return;
740
+ default:
741
+ if (requestId)
742
+ send({ type: "res", id: requestId, ok: false, error: { message: `unknown method ${msg.method}` } });
743
+ }
744
+ }
745
+ catch (error) {
746
+ sendError(requestId, error);
747
+ }
748
+ });
749
+ relayWs.on("close", (code, reason) => {
750
+ uninstallWatchdog();
751
+ console.log(`cc-connect relay connection closed: ${code} ${reason.toString()}`);
752
+ opts.onDisconnected?.();
753
+ clearBridgeTimers();
754
+ bridgeWs?.close();
755
+ bridgeWs = null;
756
+ resolve(code !== 4000);
757
+ });
758
+ relayWs.on("error", (err) => {
759
+ console.error("cc-connect relay WebSocket error:", err.message);
760
+ });
761
+ });
762
+ }
763
+ function buildRelayUrl(base, gatewayId, relaySecret) {
764
+ const normalized = base.replace(/^http/i, "ws").replace(/\/+$/, "");
765
+ return `${normalized}/relay/${gatewayId}?secret=${encodeURIComponent(relaySecret)}`;
766
+ }
767
+ function stringValue(value) {
768
+ return typeof value === "string" ? value : undefined;
769
+ }
770
+ function textBlock(text) {
771
+ return text ? { type: "text", text } : undefined;
772
+ }
773
+ const PROGRESS_CARD_PREFIX = "__cc_connect_progress_card_v1__:";
774
+ function isProgressContent(content) {
775
+ return content.startsWith(PROGRESS_CARD_PREFIX) || content.trimStart().startsWith("⏳ **Progress**");
776
+ }
777
+ function progressContentIsCompleted(content) {
778
+ const payload = parseProgressPayload(content);
779
+ return payload?.state === "completed" || payload?.state === "failed";
780
+ }
781
+ function normalizeBridgeContent(content) {
782
+ const progress = parseProgressPayload(content);
783
+ if (!progress)
784
+ return { text: content };
785
+ const text = renderProgressPayload(progress);
786
+ return {
787
+ text,
788
+ blocks: [
789
+ {
790
+ type: "markdown",
791
+ text,
792
+ },
793
+ ],
794
+ };
795
+ }
796
+ function parseProgressPayload(content) {
797
+ if (!content.startsWith(PROGRESS_CARD_PREFIX))
798
+ return undefined;
799
+ try {
800
+ const parsed = JSON.parse(content.slice(PROGRESS_CARD_PREFIX.length));
801
+ const items = Array.isArray(parsed.items)
802
+ ? parsed.items.map((item) => ({
803
+ kind: stringValue(item.kind),
804
+ text: stringValue(item.text)?.trim() ?? "",
805
+ tool: stringValue(item.tool),
806
+ status: stringValue(item.status),
807
+ })).filter((item) => item.text)
808
+ : [];
809
+ const entries = items.length > 0
810
+ ? items
811
+ : (Array.isArray(parsed.entries) ? parsed.entries.map((entry) => ({
812
+ kind: undefined,
813
+ text: typeof entry === "string" ? entry.trim() : "",
814
+ })).filter((entry) => entry.text) : []);
815
+ if (entries.length === 0)
816
+ return undefined;
817
+ return {
818
+ state: stringValue(parsed.state),
819
+ truncated: Boolean(parsed.truncated),
820
+ entries,
821
+ };
822
+ }
823
+ catch {
824
+ return undefined;
825
+ }
826
+ }
827
+ function renderProgressPayload(payload) {
828
+ const icon = payload.state === "completed" ? "✅" : payload.state === "failed" ? "❌" : "⏳";
829
+ const lines = [`${icon} **Progress**`];
830
+ if (payload.truncated)
831
+ lines.push("_Showing latest updates only._");
832
+ for (const [index, entry] of payload.entries.entries()) {
833
+ const prefix = progressEntryPrefix(entry.kind, entry.tool);
834
+ const text = entry.text.replace(/\n/g, "\n ");
835
+ lines.push(`\n${index + 1}. ${prefix}${text}`);
836
+ }
837
+ return lines.join("\n");
838
+ }
839
+ function progressEntryPrefix(kind, tool) {
840
+ if (kind === "thinking")
841
+ return "💭 ";
842
+ if (kind === "tool_use")
843
+ return tool ? `🔧 **工具: ${tool}**\n ` : "🔧 ";
844
+ if (kind === "tool_result")
845
+ return tool ? `🧾 **结果: ${tool}**\n ` : "🧾 ";
846
+ if (kind === "error")
847
+ return "❌ ";
848
+ return "";
849
+ }
850
+ function actionsBlock(buttons, sessionKey, replyCtx) {
851
+ if (buttons.length === 0)
852
+ return undefined;
853
+ return {
854
+ type: "actions",
855
+ actions: buttons,
856
+ sessionKey,
857
+ replyCtx,
858
+ };
859
+ }
860
+ function normalizeBridgeButtons(buttons) {
861
+ if (!Array.isArray(buttons))
862
+ return [];
863
+ const actions = [];
864
+ const rows = buttons.some(Array.isArray) ? buttons : [buttons];
865
+ for (const row of rows) {
866
+ const buttonRow = Array.isArray(row) ? row : [row];
867
+ for (const button of buttonRow) {
868
+ const action = normalizeBridgeAction(button);
869
+ if (action)
870
+ actions.push(action);
871
+ }
872
+ }
873
+ return actions;
874
+ }
875
+ function normalizeBridgeCard(card, sessionKey, replyCtx) {
876
+ if (!isRecord(card)) {
877
+ return { text: "[card message]", blocks: [textBlock("[card message]")].filter(Boolean) };
878
+ }
879
+ const blocks = [];
880
+ const textParts = [];
881
+ const header = isRecord(card.header) ? stringValue(card.header.title) : undefined;
882
+ if (header) {
883
+ blocks.push({ type: "text", text: `**${header}**` });
884
+ textParts.push(header);
885
+ }
886
+ const elements = Array.isArray(card.elements) ? card.elements : [];
887
+ for (const element of elements) {
888
+ if (!isRecord(element))
889
+ continue;
890
+ const type = stringValue(element.type);
891
+ if (type === "markdown") {
892
+ const text = stringValue(element.content) ?? "";
893
+ if (text) {
894
+ blocks.push({ type: "markdown", text });
895
+ textParts.push(text);
896
+ }
897
+ continue;
898
+ }
899
+ if (type === "note") {
900
+ const text = stringValue(element.text) ?? "";
901
+ if (text) {
902
+ blocks.push({ type: "text", text });
903
+ textParts.push(text);
904
+ }
905
+ continue;
906
+ }
907
+ if (type === "divider") {
908
+ blocks.push({ type: "divider" });
909
+ continue;
910
+ }
911
+ if (type === "actions") {
912
+ const actions = normalizeCardActionButtons(element.buttons);
913
+ const block = actionsBlock(actions, sessionKey, replyCtx);
914
+ if (block)
915
+ blocks.push(block);
916
+ continue;
917
+ }
918
+ if (type === "list_item") {
919
+ const text = stringValue(element.text) ?? "";
920
+ if (text)
921
+ textParts.push(text);
922
+ const btnText = stringValue(element.btn_text);
923
+ const btnValue = stringValue(element.btn_value);
924
+ blocks.push({
925
+ type: "list_item",
926
+ text,
927
+ action: btnText && btnValue ? {
928
+ id: btnValue,
929
+ label: btnText,
930
+ value: btnValue,
931
+ style: actionStyle(btnValue, stringValue(element.btn_type)),
932
+ } : undefined,
933
+ sessionKey,
934
+ replyCtx,
935
+ });
936
+ continue;
937
+ }
938
+ if (type === "select") {
939
+ const options = Array.isArray(element.options) ? element.options.filter(isRecord).map((option) => ({
940
+ id: stringValue(option.value) ?? "",
941
+ label: stringValue(option.text) ?? "",
942
+ value: stringValue(option.value) ?? "",
943
+ style: "default",
944
+ })).filter((option) => option.label && option.value) : [];
945
+ blocks.push({
946
+ type: "select",
947
+ placeholder: stringValue(element.placeholder) ?? "Select",
948
+ initValue: stringValue(element.init_value),
949
+ actions: options,
950
+ sessionKey,
951
+ replyCtx,
952
+ });
953
+ }
954
+ }
955
+ return { text: textParts.join("\n\n"), blocks };
956
+ }
957
+ function isInteractiveAction(action) {
958
+ const normalized = action.toLowerCase();
959
+ return (normalized.startsWith("perm:")
960
+ || normalized.startsWith("askq:")
961
+ || normalized === "allow"
962
+ || normalized === "deny"
963
+ || normalized === "allow all");
964
+ }
965
+ function normalizeCardActionButtons(buttons) {
966
+ if (!Array.isArray(buttons))
967
+ return [];
968
+ const actions = [];
969
+ for (const button of buttons) {
970
+ const action = normalizeBridgeAction(button);
971
+ if (action)
972
+ actions.push(action);
973
+ }
974
+ return actions;
975
+ }
976
+ function normalizeBridgeAction(button) {
977
+ if (!isRecord(button))
978
+ return undefined;
979
+ const label = stringValue(button.text)
980
+ ?? stringValue(button.label)
981
+ ?? stringValue(button.title);
982
+ const value = stringValue(button.data)
983
+ ?? stringValue(button.value)
984
+ ?? stringValue(button.id)
985
+ ?? inferActionValue(label);
986
+ if (!label || !value)
987
+ return undefined;
988
+ return {
989
+ id: value,
990
+ label,
991
+ value,
992
+ style: actionStyle(value, stringValue(button.btn_type) ?? stringValue(button.style)),
993
+ };
994
+ }
995
+ function permissionHintActions(content, sessionKey, replyCtx) {
996
+ if (!isPermissionHint(content))
997
+ return undefined;
998
+ return actionsBlock([
999
+ { id: "perm:allow", label: "允许", value: "perm:allow", style: "primary" },
1000
+ { id: "perm:deny", label: "拒绝", value: "perm:deny", style: "danger" },
1001
+ { id: "perm:allow_all", label: "允许所有", value: "perm:allow_all", style: "default" },
1002
+ ], sessionKey, replyCtx);
1003
+ }
1004
+ function isPermissionHint(content) {
1005
+ const normalized = content.toLowerCase();
1006
+ return (normalized.includes("等待权限响应") || normalized.includes("permission"))
1007
+ && (normalized.includes("允许所有") || normalized.includes("allow all"))
1008
+ && (normalized.includes("拒绝") || normalized.includes("deny"));
1009
+ }
1010
+ function isIntermediateReply(content) {
1011
+ const normalized = content.toLowerCase().trim();
1012
+ if (!normalized)
1013
+ return false;
1014
+ if (isPermissionHint(content))
1015
+ return true;
1016
+ return (normalized.includes("继续执行")
1017
+ || normalized.includes("继续处理中")
1018
+ || normalized.includes("continuing")
1019
+ || normalized.includes("continue execution")
1020
+ || normalized.includes("permission granted")
1021
+ || normalized.includes("已允许")
1022
+ || normalized.includes("已开启自动批准")
1023
+ || normalized.includes("自动批准")
1024
+ || normalized.includes("权限请求将自动允许"));
1025
+ }
1026
+ function inferActionValue(label) {
1027
+ if (!label)
1028
+ return undefined;
1029
+ const normalized = label.toLowerCase().replace(/\s+/g, "");
1030
+ if (normalized.includes("允许所有") || normalized.includes("全部允许") || normalized.includes("allowall")) {
1031
+ return "perm:allow_all";
1032
+ }
1033
+ if (normalized.includes("拒绝") || normalized.includes("deny") || normalized.includes("reject")) {
1034
+ return "perm:deny";
1035
+ }
1036
+ if (normalized.includes("允许") || normalized.includes("同意") || normalized.includes("allow") || normalized.includes("approve")) {
1037
+ return "perm:allow";
1038
+ }
1039
+ return undefined;
1040
+ }
1041
+ function actionStyle(value, explicit) {
1042
+ if (explicit === "primary" || explicit === "danger")
1043
+ return explicit;
1044
+ if (value.includes("deny") || value.includes("reject") || value.includes("cancel"))
1045
+ return "danger";
1046
+ if (value.includes("allow"))
1047
+ return "primary";
1048
+ return "default";
1049
+ }
1050
+ function bridgeMediaMimeType(msg) {
1051
+ const record = msg;
1052
+ const explicit = stringValue(record.mime_type);
1053
+ if (explicit)
1054
+ return explicit;
1055
+ if (msg.type === "audio") {
1056
+ const format = stringValue(record.format) ?? "mpeg";
1057
+ return format.includes("/") ? format : `audio/${format}`;
1058
+ }
1059
+ if (msg.type === "image")
1060
+ return "image/png";
1061
+ return "application/octet-stream";
1062
+ }
1063
+ function bridgeAttachments(params) {
1064
+ const attachments = isRecord(params) && Array.isArray(params.attachments) ? params.attachments : [];
1065
+ const images = [];
1066
+ const files = [];
1067
+ let audio;
1068
+ for (const attachment of attachments) {
1069
+ if (!isRecord(attachment))
1070
+ continue;
1071
+ const content = stringValue(attachment.content);
1072
+ const mimeType = stringValue(attachment.mimeType) ?? "application/octet-stream";
1073
+ const fileName = stringValue(attachment.fileName);
1074
+ if (!content)
1075
+ continue;
1076
+ const payload = {
1077
+ mime_type: mimeType,
1078
+ data: content.replace(/^data:[^;]+;base64,/, ""),
1079
+ file_name: fileName,
1080
+ };
1081
+ if (mimeType.startsWith("image/")) {
1082
+ images.push(payload);
1083
+ }
1084
+ else if (mimeType.startsWith("audio/") && !audio) {
1085
+ audio = {
1086
+ mime_type: mimeType,
1087
+ data: payload.data,
1088
+ format: mimeType.replace(/^audio\//, ""),
1089
+ file_name: fileName,
1090
+ };
1091
+ }
1092
+ else {
1093
+ files.push(payload);
1094
+ }
1095
+ }
1096
+ return {
1097
+ ...(images.length > 0 ? { images } : {}),
1098
+ ...(files.length > 0 ? { files } : {}),
1099
+ ...(audio ? { audio } : {}),
1100
+ };
1101
+ }
1102
+ function isRecord(value) {
1103
+ return typeof value === "object" && value !== null;
1104
+ }
1105
+ //# sourceMappingURL=ccconnect-relay-manager.js.map