@rethinkingstudio/clawpilot 2.1.7-internal.0 → 2.1.7-internal.10

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.
@@ -4,6 +4,8 @@ import { handleLocalCommand } from "../commands/local-handlers.js";
4
4
  import { handleProviderCommand } from "../commands/provider-handlers.js";
5
5
  import { getServicePlatform } from "../platform/service-manager.js";
6
6
  import { CCConnectManagementClient, decodeCCConnectSessionKey, encodeCCConnectSessionKey, timestampMs, } from "../ccconnect/api-client.js";
7
+ import { createCCConnectProject, discoverCCConnectProjects, restartCCConnect } from "../ccconnect/project-config.js";
8
+ import { uploadMediaBlocks } from "../media/assistant-attachments.js";
7
9
  const DEFAULT_CONTEXT_TOKENS = 200_000;
8
10
  export async function runCCConnectRelayManager(opts) {
9
11
  const wsUrl = buildRelayUrl(opts.relayServerUrl, opts.gatewayId, opts.relaySecret);
@@ -15,7 +17,11 @@ export async function runCCConnectRelayManager(opts) {
15
17
  let relayWs;
16
18
  let bridgeWs = null;
17
19
  let bridgeReady = false;
20
+ let bridgeReconnectTimer = null;
21
+ let bridgePingTimer = null;
22
+ let bridgeReconnectAttempts = 0;
18
23
  const rawToEncoded = new Map();
24
+ const rawToProject = new Map();
19
25
  const activeRunByEncoded = new Map();
20
26
  try {
21
27
  relayWs = new WebSocket(wsUrl);
@@ -50,7 +56,44 @@ export async function runCCConnectRelayManager(opts) {
50
56
  const sep = opts.bridgeUrl.includes("?") ? "&" : "?";
51
57
  return `${opts.bridgeUrl}${sep}token=${encodeURIComponent(opts.bridgeToken)}`;
52
58
  }
59
+ function clearBridgeTimers() {
60
+ if (bridgeReconnectTimer) {
61
+ clearTimeout(bridgeReconnectTimer);
62
+ bridgeReconnectTimer = null;
63
+ }
64
+ if (bridgePingTimer) {
65
+ clearInterval(bridgePingTimer);
66
+ bridgePingTimer = null;
67
+ }
68
+ }
69
+ function startBridgePing() {
70
+ if (bridgePingTimer)
71
+ clearInterval(bridgePingTimer);
72
+ bridgePingTimer = setInterval(() => {
73
+ if (bridgeWs?.readyState === WebSocket.OPEN) {
74
+ bridgeWs.send(JSON.stringify({ type: "ping", ts: Date.now() }));
75
+ }
76
+ }, 30_000);
77
+ bridgePingTimer.unref();
78
+ }
79
+ function scheduleBridgeReconnect(reason) {
80
+ if (relayWs.readyState !== WebSocket.OPEN || bridgeReconnectTimer)
81
+ return;
82
+ const delay = Math.min(30_000, 1_000 * 2 ** Math.min(bridgeReconnectAttempts, 5));
83
+ bridgeReconnectAttempts += 1;
84
+ console.log(`[runtime:ccconnect] bridge reconnect in ${delay}ms: ${reason}`);
85
+ bridgeReconnectTimer = setTimeout(() => {
86
+ bridgeReconnectTimer = null;
87
+ connectBridge();
88
+ }, delay);
89
+ bridgeReconnectTimer.unref();
90
+ }
53
91
  function connectBridge() {
92
+ clearBridgeTimers();
93
+ bridgeReady = false;
94
+ if (bridgeWs && bridgeWs.readyState !== WebSocket.CLOSED) {
95
+ bridgeWs.close();
96
+ }
54
97
  try {
55
98
  bridgeWs = new WebSocket(bridgeUrlWithToken());
56
99
  }
@@ -58,17 +101,33 @@ export async function runCCConnectRelayManager(opts) {
58
101
  const detail = error instanceof Error ? error.message : String(error);
59
102
  console.error(`[runtime:ccconnect] bridge create failed: ${detail}`);
60
103
  send({ type: "gateway_disconnected", reason: detail });
104
+ scheduleBridgeReconnect(detail);
61
105
  return;
62
106
  }
63
- bridgeWs.on("open", () => {
64
- bridgeWs?.send(JSON.stringify({
107
+ const ws = bridgeWs;
108
+ ws.on("open", () => {
109
+ ws.send(JSON.stringify({
65
110
  type: "register",
66
111
  platform: "pocketclaw",
67
- capabilities: ["text", "typing", "preview", "update_message", "reconstruct_reply"],
112
+ capabilities: [
113
+ "text",
114
+ "typing",
115
+ "preview",
116
+ "update_message",
117
+ "delete_message",
118
+ "reconstruct_reply",
119
+ "buttons",
120
+ "card",
121
+ "image",
122
+ "file",
123
+ "audio",
124
+ ],
68
125
  metadata: { version: "1.0.0", description: "PocketClaw relay adapter" },
69
126
  }));
70
127
  });
71
- bridgeWs.on("message", (raw) => {
128
+ ws.on("message", (raw) => {
129
+ if (bridgeWs !== ws)
130
+ return;
72
131
  try {
73
132
  handleBridgeMessage(JSON.parse(raw.toString()));
74
133
  }
@@ -76,32 +135,50 @@ export async function runCCConnectRelayManager(opts) {
76
135
  console.warn(`[runtime:ccconnect] ignored invalid bridge message: ${String(error)}`);
77
136
  }
78
137
  });
79
- bridgeWs.on("close", (code, reason) => {
138
+ ws.on("close", (code, reason) => {
139
+ if (bridgeWs !== ws)
140
+ return;
80
141
  bridgeReady = false;
142
+ if (bridgePingTimer) {
143
+ clearInterval(bridgePingTimer);
144
+ bridgePingTimer = null;
145
+ }
81
146
  console.log(`[runtime:ccconnect] bridge disconnected: ${code} ${reason.toString()}`);
82
147
  send({ type: "gateway_disconnected", reason: "cc-connect bridge disconnected" });
148
+ scheduleBridgeReconnect(`close ${code}`);
83
149
  });
84
- bridgeWs.on("error", (error) => {
150
+ ws.on("error", (error) => {
151
+ if (bridgeWs !== ws)
152
+ return;
85
153
  bridgeReady = false;
86
154
  console.error(`[runtime:ccconnect] bridge error: ${error.message}`);
87
155
  send({ type: "gateway_disconnected", reason: error.message });
156
+ scheduleBridgeReconnect(error.message);
88
157
  });
89
158
  }
90
159
  function handleBridgeMessage(msg) {
160
+ void handleBridgeMessageAsync(msg);
161
+ }
162
+ async function handleBridgeMessageAsync(msg) {
91
163
  const record = msg;
92
164
  if (msg.type === "register_ack") {
93
165
  bridgeReady = Boolean(msg.ok);
94
166
  if (bridgeReady) {
167
+ bridgeReconnectAttempts = 0;
95
168
  console.log("[runtime:ccconnect] bridge connected");
169
+ startBridgePing();
96
170
  send({ type: "gateway_connected" });
97
171
  }
98
172
  else {
99
173
  send({ type: "gateway_disconnected", reason: stringValue(record.error) ?? "cc-connect bridge registration failed" });
174
+ scheduleBridgeReconnect(stringValue(record.error) ?? "registration failed");
100
175
  }
101
176
  return;
102
177
  }
178
+ if (msg.type === "pong")
179
+ return;
103
180
  const rawSessionKey = stringValue(record.session_key) ?? "";
104
- const encodedSessionKey = rawSessionKey ? rawToEncoded.get(rawSessionKey) : undefined;
181
+ const encodedSessionKey = await resolveEncodedBridgeSession(record);
105
182
  if (!encodedSessionKey)
106
183
  return;
107
184
  const runId = ensureRunId(encodedSessionKey);
@@ -128,27 +205,69 @@ export async function runCCConnectRelayManager(opts) {
128
205
  emitChat(runId, encodedSessionKey, "delta", stringValue(record.content) ?? "", false);
129
206
  return;
130
207
  }
208
+ if (msg.type === "delete_message") {
209
+ send({
210
+ type: "event",
211
+ event: "message.delete",
212
+ payload: {
213
+ runId,
214
+ sessionKey: encodedSessionKey,
215
+ previewHandle: stringValue(record.preview_handle),
216
+ },
217
+ });
218
+ activeRunByEncoded.delete(encodedSessionKey);
219
+ return;
220
+ }
131
221
  if (msg.type === "reply_stream") {
132
222
  const text = stringValue(record.full_text) ?? stringValue(record.delta) ?? "";
133
223
  const done = Boolean(record.done);
134
- emitChat(runId, encodedSessionKey, done ? "final" : "delta", text, false);
135
- if (done)
224
+ const state = done && !isIntermediateReply(text) ? "final" : "delta";
225
+ emitChat(runId, encodedSessionKey, state, text, false);
226
+ if (state === "final")
136
227
  activeRunByEncoded.delete(encodedSessionKey);
137
228
  return;
138
229
  }
139
230
  if (msg.type === "reply") {
140
- emitChat(runId, encodedSessionKey, "final", stringValue(record.content) ?? "", false);
141
- activeRunByEncoded.delete(encodedSessionKey);
231
+ const content = stringValue(record.content) ?? "";
232
+ const permissionFallback = permissionHintActions(content, rawSessionKey, stringValue(record.reply_ctx) ?? rawSessionKey);
233
+ const state = isIntermediateReply(content) ? "delta" : "final";
234
+ if (permissionFallback) {
235
+ emitStructuredChat(runId, encodedSessionKey, state, {
236
+ text: content,
237
+ blocks: [textBlock(content), permissionFallback].filter(Boolean),
238
+ });
239
+ }
240
+ else {
241
+ emitChat(runId, encodedSessionKey, state, content, false);
242
+ }
243
+ if (state === "final")
244
+ activeRunByEncoded.delete(encodedSessionKey);
142
245
  return;
143
246
  }
144
247
  if (msg.type === "buttons") {
145
- emitChat(runId, encodedSessionKey, "final", stringValue(record.content) ?? "[buttons]", false);
146
- activeRunByEncoded.delete(encodedSessionKey);
248
+ const actions = normalizeBridgeButtons(record.buttons);
249
+ console.log(`[runtime:ccconnect] bridge buttons session=${rawSessionKey} actions=${actions.length}`);
250
+ emitStructuredChat(runId, encodedSessionKey, "delta", {
251
+ text: stringValue(record.content) ?? "",
252
+ blocks: [
253
+ textBlock(stringValue(record.content) ?? ""),
254
+ actionsBlock(actions, rawSessionKey, stringValue(record.reply_ctx) ?? rawSessionKey),
255
+ ].filter(Boolean),
256
+ });
147
257
  return;
148
258
  }
149
259
  if (msg.type === "card") {
150
- emitChat(runId, encodedSessionKey, "final", "[card message]", false);
151
- activeRunByEncoded.delete(encodedSessionKey);
260
+ console.log(`[runtime:ccconnect] bridge card session=${rawSessionKey}`);
261
+ emitStructuredChat(runId, encodedSessionKey, "delta", normalizeBridgeCard(record.card, rawSessionKey, stringValue(record.reply_ctx) ?? rawSessionKey));
262
+ return;
263
+ }
264
+ if (msg.type === "image" || msg.type === "file" || msg.type === "audio") {
265
+ const attachment = await uploadBridgeMedia(msg);
266
+ emitStructuredChat(runId, encodedSessionKey, "final", {
267
+ text: attachment.text,
268
+ attachments: attachment.attachments,
269
+ blocks: attachment.text ? [textBlock(attachment.text)] : [],
270
+ });
152
271
  return;
153
272
  }
154
273
  if (msg.type === "error") {
@@ -156,6 +275,37 @@ export async function runCCConnectRelayManager(opts) {
156
275
  activeRunByEncoded.delete(encodedSessionKey);
157
276
  }
158
277
  }
278
+ async function resolveEncodedBridgeSession(record) {
279
+ const rawSessionKey = stringValue(record.session_key);
280
+ if (!rawSessionKey) {
281
+ if (activeRunByEncoded.size === 1) {
282
+ return activeRunByEncoded.keys().next().value;
283
+ }
284
+ console.warn(`[runtime:ccconnect] dropped bridge ${String(record.type)} without session_key`);
285
+ return undefined;
286
+ }
287
+ const existing = rawToEncoded.get(rawSessionKey);
288
+ if (existing)
289
+ return existing;
290
+ const explicitProject = stringValue(record.project) ?? rawToProject.get(rawSessionKey);
291
+ if (explicitProject) {
292
+ const encoded = encodeCCConnectSessionKey(explicitProject, rawSessionKey, rawSessionKey);
293
+ rawToEncoded.set(rawSessionKey, encoded);
294
+ rawToProject.set(rawSessionKey, explicitProject);
295
+ console.warn(`[runtime:ccconnect] recovered bridge session mapping project=${explicitProject} rawSessionKey=${rawSessionKey}`);
296
+ return encoded;
297
+ }
298
+ if (activeRunByEncoded.size === 1) {
299
+ const encoded = activeRunByEncoded.keys().next().value;
300
+ if (encoded) {
301
+ rawToEncoded.set(rawSessionKey, encoded);
302
+ console.warn(`[runtime:ccconnect] inferred bridge session mapping rawSessionKey=${rawSessionKey}`);
303
+ return encoded;
304
+ }
305
+ }
306
+ console.warn(`[runtime:ccconnect] dropped bridge ${String(record.type)} for unmapped session_key=${rawSessionKey}`);
307
+ return undefined;
308
+ }
159
309
  function ensureRunId(encodedSessionKey, preferred) {
160
310
  const existing = activeRunByEncoded.get(encodedSessionKey);
161
311
  if (existing)
@@ -175,6 +325,37 @@ export async function runCCConnectRelayManager(opts) {
175
325
  payload.errorMessage = text ?? "cc-connect error";
176
326
  send({ type: "event", event: "chat", payload });
177
327
  }
328
+ function emitStructuredChat(runId, sessionKey, state, data) {
329
+ const blocks = data.blocks?.length ? data.blocks : data.text !== undefined ? [textBlock(data.text)] : undefined;
330
+ const payload = { runId, sessionKey, state };
331
+ if (blocks)
332
+ payload.message = { content: blocks };
333
+ if (data.attachments)
334
+ payload.attachments = data.attachments;
335
+ if (state === "error")
336
+ payload.errorMessage = data.text ?? "cc-connect error";
337
+ send({ type: "event", event: "chat", payload });
338
+ }
339
+ async function uploadBridgeMedia(msg) {
340
+ const data = stringValue(msg.data);
341
+ const fileName = stringValue(msg.file_name);
342
+ const mimeType = bridgeMediaMimeType(msg);
343
+ const label = fileName || (msg.type === "audio" ? "Audio message" : msg.type === "image" ? "Image" : "File");
344
+ if (!data) {
345
+ return { text: `[${label} missing data]`, attachments: [] };
346
+ }
347
+ const block = {
348
+ mimeType,
349
+ fileName,
350
+ data: Buffer.from(data.replace(/^data:[^;]+;base64,/, ""), "base64"),
351
+ };
352
+ const uploaded = await uploadMediaBlocks([block], opts.relayServerUrl, opts.gatewayId, opts.relaySecret);
353
+ const attachments = uploaded.map((att) => ({
354
+ ...att,
355
+ fileName: fileName ?? undefined,
356
+ }));
357
+ return { text: label, attachments };
358
+ }
178
359
  async function respondSessionsList(requestId) {
179
360
  const projects = await client.listProjects();
180
361
  const sessions = [];
@@ -191,18 +372,80 @@ export async function runCCConnectRelayManager(opts) {
191
372
  count: sessions.length,
192
373
  });
193
374
  }
375
+ async function respondProjectsList(requestId) {
376
+ const projects = await client.listProjects();
377
+ const detailed = await Promise.all(projects.map(async (project) => {
378
+ try {
379
+ const detail = await client.getProject(project.name);
380
+ return {
381
+ name: detail.name,
382
+ agentType: detail.agent_type,
383
+ workDir: detail.work_dir,
384
+ mode: detail.agent_mode || detail.mode,
385
+ sessionsCount: detail.sessions_count ?? project.sessions_count ?? 0,
386
+ platforms: detail.platforms ?? project.platforms ?? [],
387
+ heartbeatEnabled: detail.heartbeat_enabled ?? project.heartbeat_enabled ?? false,
388
+ activeSessionKeys: detail.active_session_keys ?? [],
389
+ };
390
+ }
391
+ catch {
392
+ return {
393
+ name: project.name,
394
+ agentType: project.agent_type,
395
+ sessionsCount: project.sessions_count ?? 0,
396
+ platforms: project.platforms ?? [],
397
+ heartbeatEnabled: project.heartbeat_enabled ?? false,
398
+ activeSessionKeys: [],
399
+ };
400
+ }
401
+ }));
402
+ sendResponse(requestId, { projects: detailed, count: detailed.length });
403
+ }
404
+ async function respondProjectsDiscover(requestId) {
405
+ const projects = await client.listProjects();
406
+ const detailed = await Promise.all(projects.map(async (project) => {
407
+ try {
408
+ const detail = await client.getProject(project.name);
409
+ return { name: detail.name, workDir: detail.work_dir };
410
+ }
411
+ catch {
412
+ return { name: project.name, workDir: undefined };
413
+ }
414
+ }));
415
+ const directories = discoverCCConnectProjects(detailed);
416
+ sendResponse(requestId, { directories, count: directories.length });
417
+ }
418
+ async function respondProjectCreate(requestId, params) {
419
+ const p = params;
420
+ const created = createCCConnectProject(p);
421
+ restartCCConnect(created.configPath);
422
+ sendResponse(requestId, {
423
+ project: {
424
+ name: created.name,
425
+ agentType: created.agentType,
426
+ workDir: created.workDir,
427
+ mode: created.mode,
428
+ sessionsCount: 0,
429
+ platforms: ["line", "bridge"],
430
+ heartbeatEnabled: false,
431
+ activeSessionKeys: [],
432
+ },
433
+ restarted: true,
434
+ });
435
+ }
194
436
  async function respondMessagesHistory(requestId, params) {
195
437
  const p = params;
196
438
  if (!p.sessionKey)
197
439
  throw new Error("sessionKey required");
198
440
  const decoded = decodeCCConnectSessionKey(p.sessionKey);
199
- const detail = await client.getSession(decoded.project, decoded.sessionId, Math.min(p.limit ?? 50, 200));
441
+ const sessionId = await resolveCCConnectSessionId(decoded.project, decoded.sessionKey, decoded.sessionId);
442
+ const detail = await client.getSession(decoded.project, sessionId, Math.min(p.limit ?? 50, 200));
200
443
  const history = detail.history ?? [];
201
444
  const messages = history.map((entry, index) => ({
202
445
  id: index + 1,
203
446
  role: entry.role === "user" ? "user" : "assistant",
204
447
  content: entry.content ?? "",
205
- runId: entry.role === "user" ? undefined : `${decoded.sessionId}-${index + 1}`,
448
+ runId: entry.role === "user" ? undefined : `${sessionId}-${index + 1}`,
206
449
  state: "final",
207
450
  createdAt: timestampMs(entry.timestamp),
208
451
  updatedAt: timestampMs(entry.timestamp),
@@ -235,11 +478,14 @@ export async function runCCConnectRelayManager(opts) {
235
478
  throw new Error("sessionKey required");
236
479
  if (!p.message)
237
480
  throw new Error("message required");
238
- const decoded = decodeCCConnectSessionKey(p.sessionKey);
481
+ const decoded = await resolveCCConnectSendTarget(p.sessionKey);
239
482
  rawToEncoded.set(decoded.sessionKey, p.sessionKey);
483
+ rawToProject.set(decoded.sessionKey, decoded.project);
484
+ activeRunByEncoded.delete(p.sessionKey);
240
485
  const runId = ensureRunId(p.sessionKey, p.idempotencyKey);
241
486
  emitChat(runId, p.sessionKey, "delta", undefined, true);
242
487
  if (bridgeReady && bridgeWs?.readyState === WebSocket.OPEN) {
488
+ console.log(`[runtime:ccconnect] chat.send via bridge project=${decoded.project} rawSessionKey=${decoded.sessionKey}`);
243
489
  bridgeWs.send(JSON.stringify({
244
490
  type: "message",
245
491
  msg_id: `pocketclaw-${Date.now()}`,
@@ -249,16 +495,68 @@ export async function runCCConnectRelayManager(opts) {
249
495
  content: p.message,
250
496
  reply_ctx: decoded.sessionKey,
251
497
  project: decoded.project,
498
+ ...bridgeAttachments(params),
252
499
  }));
253
500
  return;
254
501
  }
502
+ scheduleBridgeReconnect("bridge not ready during chat.send");
503
+ console.log(`[runtime:ccconnect] chat.send via management fallback project=${decoded.project} rawSessionKey=${decoded.sessionKey}`);
255
504
  await client.send(decoded.project, decoded.sessionKey, p.message);
256
505
  }
506
+ async function handleChatAction(params) {
507
+ const p = params;
508
+ if (!p.sessionKey)
509
+ throw new Error("sessionKey required");
510
+ if (!p.action)
511
+ throw new Error("action required");
512
+ if (!bridgeReady || bridgeWs?.readyState !== WebSocket.OPEN) {
513
+ throw new Error("cc-connect bridge not ready");
514
+ }
515
+ const decoded = await resolveCCConnectSendTarget(p.sessionKey);
516
+ bridgeWs.send(JSON.stringify({
517
+ type: "card_action",
518
+ session_key: decoded.sessionKey,
519
+ action: p.action,
520
+ reply_ctx: p.replyCtx || decoded.sessionKey,
521
+ project: decoded.project,
522
+ }));
523
+ }
524
+ async function resolveCCConnectSendTarget(sessionKey) {
525
+ try {
526
+ return decodeCCConnectSessionKey(sessionKey);
527
+ }
528
+ catch {
529
+ const projects = await client.listProjects();
530
+ for (const project of projects) {
531
+ const sessions = await client.listSessions(project.name);
532
+ const preferred = sessions.find((session) => session.active || session.live) ?? sessions[0];
533
+ if (preferred?.session_key) {
534
+ console.warn(`[runtime:ccconnect] non-ccconnect sessionKey received (${sessionKey}); using ${project.name}/${preferred.session_key}`);
535
+ return {
536
+ project: project.name,
537
+ sessionKey: preferred.session_key,
538
+ sessionId: preferred.id || preferred.session_key,
539
+ };
540
+ }
541
+ }
542
+ throw new Error(`invalid cc-connect session key: ${sessionKey}`);
543
+ }
544
+ }
257
545
  function rememberSession(project, session) {
258
- const encoded = encodeCCConnectSessionKey(project, session.session_key, session.id);
259
- rawToEncoded.set(session.session_key, encoded);
546
+ const encoded = encodeCCConnectSessionKey(project, session.session_key, session.session_key);
547
+ if (!rawToEncoded.has(session.session_key)) {
548
+ rawToEncoded.set(session.session_key, encoded);
549
+ rawToProject.set(session.session_key, project);
550
+ }
260
551
  return encoded;
261
552
  }
553
+ async function resolveCCConnectSessionId(project, sessionKey, sessionId) {
554
+ if (sessionId && sessionId !== sessionKey)
555
+ return sessionId;
556
+ const sessions = await client.listSessions(project);
557
+ const matched = sessions.find((session) => session.session_key === sessionKey);
558
+ return matched?.id || sessionId || sessionKey;
559
+ }
262
560
  function toRuntimeSession(project, session, encoded) {
263
561
  const platform = session.platform || session.session_key.split(":")[0] || "cc-connect";
264
562
  const title = session.name || session.chat_name || session.user_name || session.session_key || session.id;
@@ -325,6 +623,18 @@ export async function runCCConnectRelayManager(opts) {
325
623
  }
326
624
  try {
327
625
  switch (msg.method) {
626
+ case "projects.list":
627
+ if (requestId)
628
+ await respondProjectsList(requestId);
629
+ return;
630
+ case "projects.discover":
631
+ if (requestId)
632
+ await respondProjectsDiscover(requestId);
633
+ return;
634
+ case "projects.create":
635
+ if (requestId)
636
+ await respondProjectCreate(requestId, msg.params ?? {});
637
+ return;
328
638
  case "sessions.list":
329
639
  if (requestId)
330
640
  await respondSessionsList(requestId);
@@ -342,6 +652,11 @@ export async function runCCConnectRelayManager(opts) {
342
652
  if (requestId)
343
653
  sendResponse(requestId, { ok: true });
344
654
  return;
655
+ case "chat.action":
656
+ await handleChatAction(msg.params ?? {});
657
+ if (requestId)
658
+ sendResponse(requestId, { ok: true });
659
+ return;
345
660
  case "chat.abort":
346
661
  if (requestId)
347
662
  sendResponse(requestId, { ok: true });
@@ -370,6 +685,7 @@ export async function runCCConnectRelayManager(opts) {
370
685
  relayWs.on("close", (code, reason) => {
371
686
  console.log(`cc-connect relay connection closed: ${code} ${reason.toString()}`);
372
687
  opts.onDisconnected?.();
688
+ clearBridgeTimers();
373
689
  bridgeWs?.close();
374
690
  bridgeWs = null;
375
691
  resolve(code !== 4000);
@@ -386,4 +702,254 @@ function buildRelayUrl(base, gatewayId, relaySecret) {
386
702
  function stringValue(value) {
387
703
  return typeof value === "string" ? value : undefined;
388
704
  }
705
+ function textBlock(text) {
706
+ return text ? { type: "text", text } : undefined;
707
+ }
708
+ function actionsBlock(buttons, sessionKey, replyCtx) {
709
+ if (buttons.length === 0)
710
+ return undefined;
711
+ return {
712
+ type: "actions",
713
+ actions: buttons,
714
+ sessionKey,
715
+ replyCtx,
716
+ };
717
+ }
718
+ function normalizeBridgeButtons(buttons) {
719
+ if (!Array.isArray(buttons))
720
+ return [];
721
+ const actions = [];
722
+ const rows = buttons.some(Array.isArray) ? buttons : [buttons];
723
+ for (const row of rows) {
724
+ const buttonRow = Array.isArray(row) ? row : [row];
725
+ for (const button of buttonRow) {
726
+ const action = normalizeBridgeAction(button);
727
+ if (action)
728
+ actions.push(action);
729
+ }
730
+ }
731
+ return actions;
732
+ }
733
+ function normalizeBridgeCard(card, sessionKey, replyCtx) {
734
+ if (!isRecord(card)) {
735
+ return { text: "[card message]", blocks: [textBlock("[card message]")].filter(Boolean) };
736
+ }
737
+ const blocks = [];
738
+ const textParts = [];
739
+ const header = isRecord(card.header) ? stringValue(card.header.title) : undefined;
740
+ if (header) {
741
+ blocks.push({ type: "text", text: `**${header}**` });
742
+ textParts.push(header);
743
+ }
744
+ const elements = Array.isArray(card.elements) ? card.elements : [];
745
+ for (const element of elements) {
746
+ if (!isRecord(element))
747
+ continue;
748
+ const type = stringValue(element.type);
749
+ if (type === "markdown") {
750
+ const text = stringValue(element.content) ?? "";
751
+ if (text) {
752
+ blocks.push({ type: "markdown", text });
753
+ textParts.push(text);
754
+ }
755
+ continue;
756
+ }
757
+ if (type === "note") {
758
+ const text = stringValue(element.text) ?? "";
759
+ if (text) {
760
+ blocks.push({ type: "text", text });
761
+ textParts.push(text);
762
+ }
763
+ continue;
764
+ }
765
+ if (type === "divider") {
766
+ blocks.push({ type: "divider" });
767
+ continue;
768
+ }
769
+ if (type === "actions") {
770
+ const actions = normalizeCardActionButtons(element.buttons);
771
+ const block = actionsBlock(actions, sessionKey, replyCtx);
772
+ if (block)
773
+ blocks.push(block);
774
+ continue;
775
+ }
776
+ if (type === "list_item") {
777
+ const text = stringValue(element.text) ?? "";
778
+ if (text)
779
+ textParts.push(text);
780
+ const btnText = stringValue(element.btn_text);
781
+ const btnValue = stringValue(element.btn_value);
782
+ blocks.push({
783
+ type: "list_item",
784
+ text,
785
+ action: btnText && btnValue ? {
786
+ id: btnValue,
787
+ label: btnText,
788
+ value: btnValue,
789
+ style: actionStyle(btnValue, stringValue(element.btn_type)),
790
+ } : undefined,
791
+ sessionKey,
792
+ replyCtx,
793
+ });
794
+ continue;
795
+ }
796
+ if (type === "select") {
797
+ const options = Array.isArray(element.options) ? element.options.filter(isRecord).map((option) => ({
798
+ id: stringValue(option.value) ?? "",
799
+ label: stringValue(option.text) ?? "",
800
+ value: stringValue(option.value) ?? "",
801
+ style: "default",
802
+ })).filter((option) => option.label && option.value) : [];
803
+ blocks.push({
804
+ type: "select",
805
+ placeholder: stringValue(element.placeholder) ?? "Select",
806
+ initValue: stringValue(element.init_value),
807
+ actions: options,
808
+ sessionKey,
809
+ replyCtx,
810
+ });
811
+ }
812
+ }
813
+ return { text: textParts.join("\n\n"), blocks };
814
+ }
815
+ function normalizeCardActionButtons(buttons) {
816
+ if (!Array.isArray(buttons))
817
+ return [];
818
+ const actions = [];
819
+ for (const button of buttons) {
820
+ const action = normalizeBridgeAction(button);
821
+ if (action)
822
+ actions.push(action);
823
+ }
824
+ return actions;
825
+ }
826
+ function normalizeBridgeAction(button) {
827
+ if (!isRecord(button))
828
+ return undefined;
829
+ const label = stringValue(button.text)
830
+ ?? stringValue(button.label)
831
+ ?? stringValue(button.title);
832
+ const value = stringValue(button.data)
833
+ ?? stringValue(button.value)
834
+ ?? stringValue(button.id)
835
+ ?? inferActionValue(label);
836
+ if (!label || !value)
837
+ return undefined;
838
+ return {
839
+ id: value,
840
+ label,
841
+ value,
842
+ style: actionStyle(value, stringValue(button.btn_type) ?? stringValue(button.style)),
843
+ };
844
+ }
845
+ function permissionHintActions(content, sessionKey, replyCtx) {
846
+ if (!isPermissionHint(content))
847
+ return undefined;
848
+ return actionsBlock([
849
+ { id: "perm:allow", label: "允许", value: "perm:allow", style: "primary" },
850
+ { id: "perm:deny", label: "拒绝", value: "perm:deny", style: "danger" },
851
+ { id: "perm:allow_all", label: "允许所有", value: "perm:allow_all", style: "default" },
852
+ ], sessionKey, replyCtx);
853
+ }
854
+ function isPermissionHint(content) {
855
+ const normalized = content.toLowerCase();
856
+ return (normalized.includes("等待权限响应") || normalized.includes("permission"))
857
+ && (normalized.includes("允许所有") || normalized.includes("allow all"))
858
+ && (normalized.includes("拒绝") || normalized.includes("deny"));
859
+ }
860
+ function isIntermediateReply(content) {
861
+ const normalized = content.toLowerCase().trim();
862
+ if (!normalized)
863
+ return false;
864
+ if (isPermissionHint(content))
865
+ return true;
866
+ return (normalized.includes("继续执行")
867
+ || normalized.includes("继续处理中")
868
+ || normalized.includes("continuing")
869
+ || normalized.includes("continue execution")
870
+ || normalized.includes("permission granted")
871
+ || normalized.includes("已允许")
872
+ || normalized.includes("已开启自动批准")
873
+ || normalized.includes("自动批准")
874
+ || normalized.includes("权限请求将自动允许"));
875
+ }
876
+ function inferActionValue(label) {
877
+ if (!label)
878
+ return undefined;
879
+ const normalized = label.toLowerCase().replace(/\s+/g, "");
880
+ if (normalized.includes("允许所有") || normalized.includes("全部允许") || normalized.includes("allowall")) {
881
+ return "perm:allow_all";
882
+ }
883
+ if (normalized.includes("拒绝") || normalized.includes("deny") || normalized.includes("reject")) {
884
+ return "perm:deny";
885
+ }
886
+ if (normalized.includes("允许") || normalized.includes("同意") || normalized.includes("allow") || normalized.includes("approve")) {
887
+ return "perm:allow";
888
+ }
889
+ return undefined;
890
+ }
891
+ function actionStyle(value, explicit) {
892
+ if (explicit === "primary" || explicit === "danger")
893
+ return explicit;
894
+ if (value.includes("deny") || value.includes("reject") || value.includes("cancel"))
895
+ return "danger";
896
+ if (value.includes("allow"))
897
+ return "primary";
898
+ return "default";
899
+ }
900
+ function bridgeMediaMimeType(msg) {
901
+ const record = msg;
902
+ const explicit = stringValue(record.mime_type);
903
+ if (explicit)
904
+ return explicit;
905
+ if (msg.type === "audio") {
906
+ const format = stringValue(record.format) ?? "mpeg";
907
+ return format.includes("/") ? format : `audio/${format}`;
908
+ }
909
+ if (msg.type === "image")
910
+ return "image/png";
911
+ return "application/octet-stream";
912
+ }
913
+ function bridgeAttachments(params) {
914
+ const attachments = isRecord(params) && Array.isArray(params.attachments) ? params.attachments : [];
915
+ const images = [];
916
+ const files = [];
917
+ let audio;
918
+ for (const attachment of attachments) {
919
+ if (!isRecord(attachment))
920
+ continue;
921
+ const content = stringValue(attachment.content);
922
+ const mimeType = stringValue(attachment.mimeType) ?? "application/octet-stream";
923
+ const fileName = stringValue(attachment.fileName);
924
+ if (!content)
925
+ continue;
926
+ const payload = {
927
+ mime_type: mimeType,
928
+ data: content.replace(/^data:[^;]+;base64,/, ""),
929
+ file_name: fileName,
930
+ };
931
+ if (mimeType.startsWith("image/")) {
932
+ images.push(payload);
933
+ }
934
+ else if (mimeType.startsWith("audio/") && !audio) {
935
+ audio = {
936
+ mime_type: mimeType,
937
+ data: payload.data,
938
+ format: mimeType.replace(/^audio\//, ""),
939
+ file_name: fileName,
940
+ };
941
+ }
942
+ else {
943
+ files.push(payload);
944
+ }
945
+ }
946
+ return {
947
+ ...(images.length > 0 ? { images } : {}),
948
+ ...(files.length > 0 ? { files } : {}),
949
+ ...(audio ? { audio } : {}),
950
+ };
951
+ }
952
+ function isRecord(value) {
953
+ return typeof value === "object" && value !== null;
954
+ }
389
955
  //# sourceMappingURL=ccconnect-relay-manager.js.map