@rethinkingstudio/clawpilot 2.1.7-internal.1 → 2.1.7-internal.11

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,9 @@ 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";
9
+ import { installRelayWatchdog } from "./relay-watchdog.js";
7
10
  const DEFAULT_CONTEXT_TOKENS = 200_000;
8
11
  export async function runCCConnectRelayManager(opts) {
9
12
  const wsUrl = buildRelayUrl(opts.relayServerUrl, opts.gatewayId, opts.relaySecret);
@@ -19,7 +22,10 @@ export async function runCCConnectRelayManager(opts) {
19
22
  let bridgePingTimer = null;
20
23
  let bridgeReconnectAttempts = 0;
21
24
  const rawToEncoded = new Map();
25
+ const rawToProject = new Map();
22
26
  const activeRunByEncoded = new Map();
27
+ const previewHandleToRun = new Map();
28
+ const pendingInteractiveReplyByEncoded = new Set();
23
29
  try {
24
30
  relayWs = new WebSocket(wsUrl);
25
31
  }
@@ -28,6 +34,7 @@ export async function runCCConnectRelayManager(opts) {
28
34
  resolve(true);
29
35
  return;
30
36
  }
37
+ const uninstallWatchdog = installRelayWatchdog(relayWs, `ccconnect:${opts.gatewayId}`);
31
38
  function send(msg) {
32
39
  if (relayWs.readyState === WebSocket.OPEN) {
33
40
  relayWs.send(JSON.stringify(msg));
@@ -106,8 +113,25 @@ export async function runCCConnectRelayManager(opts) {
106
113
  ws.send(JSON.stringify({
107
114
  type: "register",
108
115
  platform: "pocketclaw",
109
- capabilities: ["text", "typing", "preview", "update_message", "reconstruct_reply"],
110
- metadata: { version: "1.0.0", description: "PocketClaw relay adapter" },
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
+ },
111
135
  }));
112
136
  });
113
137
  ws.on("message", (raw) => {
@@ -142,6 +166,9 @@ export async function runCCConnectRelayManager(opts) {
142
166
  });
143
167
  }
144
168
  function handleBridgeMessage(msg) {
169
+ void handleBridgeMessageAsync(msg);
170
+ }
171
+ async function handleBridgeMessageAsync(msg) {
145
172
  const record = msg;
146
173
  if (msg.type === "register_ack") {
147
174
  bridgeReady = Boolean(msg.ok);
@@ -157,11 +184,15 @@ export async function runCCConnectRelayManager(opts) {
157
184
  }
158
185
  return;
159
186
  }
187
+ if (msg.type === "pong")
188
+ return;
160
189
  const rawSessionKey = stringValue(record.session_key) ?? "";
161
- const encodedSessionKey = rawSessionKey ? rawToEncoded.get(rawSessionKey) : undefined;
190
+ const encodedSessionKey = await resolveEncodedBridgeSession(record);
162
191
  if (!encodedSessionKey)
163
192
  return;
164
- const runId = ensureRunId(encodedSessionKey);
193
+ const previewHandle = stringValue(record.preview_handle);
194
+ const runInfo = resolveRunForBridgeMessage(encodedSessionKey, previewHandle);
195
+ let runId = runInfo.runId;
165
196
  if (msg.type === "typing_start") {
166
197
  emitChat(runId, encodedSessionKey, "delta", undefined, true);
167
198
  return;
@@ -170,48 +201,141 @@ export async function runCCConnectRelayManager(opts) {
170
201
  return;
171
202
  }
172
203
  if (msg.type === "preview_start") {
173
- emitChat(runId, encodedSessionKey, "delta", stringValue(record.content) ?? "", false);
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);
174
210
  const refId = stringValue(record.ref_id);
211
+ const handle = `pocketclaw-${runId}`;
212
+ previewHandleToRun.set(handle, { encodedSessionKey, runId, sideChannel });
175
213
  if (refId) {
176
214
  bridgeWs?.send(JSON.stringify({
177
215
  type: "preview_ack",
178
216
  ref_id: refId,
179
- preview_handle: `pocketclaw-${runId}`,
217
+ preview_handle: handle,
180
218
  }));
181
219
  }
182
220
  return;
183
221
  }
184
222
  if (msg.type === "update_message") {
185
- emitChat(runId, encodedSessionKey, "delta", stringValue(record.content) ?? "", false);
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
+ });
186
245
  return;
187
246
  }
188
247
  if (msg.type === "reply_stream") {
189
248
  const text = stringValue(record.full_text) ?? stringValue(record.delta) ?? "";
190
249
  const done = Boolean(record.done);
191
- emitChat(runId, encodedSessionKey, done ? "final" : "delta", text, false);
192
- if (done)
193
- activeRunByEncoded.delete(encodedSessionKey);
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);
194
255
  return;
195
256
  }
196
257
  if (msg.type === "reply") {
197
- emitChat(runId, encodedSessionKey, "final", stringValue(record.content) ?? "", false);
198
- activeRunByEncoded.delete(encodedSessionKey);
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);
199
275
  return;
200
276
  }
201
277
  if (msg.type === "buttons") {
202
- emitChat(runId, encodedSessionKey, "final", stringValue(record.content) ?? "[buttons]", false);
203
- activeRunByEncoded.delete(encodedSessionKey);
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
+ });
204
287
  return;
205
288
  }
206
289
  if (msg.type === "card") {
207
- emitChat(runId, encodedSessionKey, "final", "[card message]", false);
208
- activeRunByEncoded.delete(encodedSessionKey);
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);
209
302
  return;
210
303
  }
211
304
  if (msg.type === "error") {
212
305
  emitChat(runId, encodedSessionKey, "error", stringValue(record.message) ?? "cc-connect bridge error", false);
213
- activeRunByEncoded.delete(encodedSessionKey);
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;
214
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;
215
339
  }
216
340
  function ensureRunId(encodedSessionKey, preferred) {
217
341
  const existing = activeRunByEncoded.get(encodedSessionKey);
@@ -221,6 +345,39 @@ export async function runCCConnectRelayManager(opts) {
221
345
  activeRunByEncoded.set(encodedSessionKey, runId);
222
346
  return runId;
223
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
+ }
224
381
  function emitChat(runId, sessionKey, state, text, loading) {
225
382
  const payload = { runId, sessionKey, state };
226
383
  if (loading)
@@ -232,6 +389,48 @@ export async function runCCConnectRelayManager(opts) {
232
389
  payload.errorMessage = text ?? "cc-connect error";
233
390
  send({ type: "event", event: "chat", payload });
234
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
+ }
235
434
  async function respondSessionsList(requestId) {
236
435
  const projects = await client.listProjects();
237
436
  const sessions = [];
@@ -248,18 +447,80 @@ export async function runCCConnectRelayManager(opts) {
248
447
  count: sessions.length,
249
448
  });
250
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
+ }
251
511
  async function respondMessagesHistory(requestId, params) {
252
512
  const p = params;
253
513
  if (!p.sessionKey)
254
514
  throw new Error("sessionKey required");
255
515
  const decoded = decodeCCConnectSessionKey(p.sessionKey);
256
- const detail = await client.getSession(decoded.project, decoded.sessionId, Math.min(p.limit ?? 50, 200));
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));
257
518
  const history = detail.history ?? [];
258
519
  const messages = history.map((entry, index) => ({
259
520
  id: index + 1,
260
521
  role: entry.role === "user" ? "user" : "assistant",
261
522
  content: entry.content ?? "",
262
- runId: entry.role === "user" ? undefined : `${decoded.sessionId}-${index + 1}`,
523
+ runId: entry.role === "user" ? undefined : `${sessionId}-${index + 1}`,
263
524
  state: "final",
264
525
  createdAt: timestampMs(entry.timestamp),
265
526
  updatedAt: timestampMs(entry.timestamp),
@@ -292,11 +553,15 @@ export async function runCCConnectRelayManager(opts) {
292
553
  throw new Error("sessionKey required");
293
554
  if (!p.message)
294
555
  throw new Error("message required");
295
- const decoded = decodeCCConnectSessionKey(p.sessionKey);
556
+ const decoded = await resolveCCConnectSendTarget(p.sessionKey);
296
557
  rawToEncoded.set(decoded.sessionKey, p.sessionKey);
558
+ rawToProject.set(decoded.sessionKey, decoded.project);
559
+ activeRunByEncoded.delete(p.sessionKey);
560
+ pendingInteractiveReplyByEncoded.delete(p.sessionKey);
297
561
  const runId = ensureRunId(p.sessionKey, p.idempotencyKey);
298
562
  emitChat(runId, p.sessionKey, "delta", undefined, true);
299
563
  if (bridgeReady && bridgeWs?.readyState === WebSocket.OPEN) {
564
+ console.log(`[runtime:ccconnect] chat.send via bridge project=${decoded.project} rawSessionKey=${decoded.sessionKey}`);
300
565
  bridgeWs.send(JSON.stringify({
301
566
  type: "message",
302
567
  msg_id: `pocketclaw-${Date.now()}`,
@@ -306,17 +571,71 @@ export async function runCCConnectRelayManager(opts) {
306
571
  content: p.message,
307
572
  reply_ctx: decoded.sessionKey,
308
573
  project: decoded.project,
574
+ ...bridgeAttachments(params),
309
575
  }));
310
576
  return;
311
577
  }
312
578
  scheduleBridgeReconnect("bridge not ready during chat.send");
579
+ console.log(`[runtime:ccconnect] chat.send via management fallback project=${decoded.project} rawSessionKey=${decoded.sessionKey}`);
313
580
  await client.send(decoded.project, decoded.sessionKey, p.message);
314
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
+ }
315
624
  function rememberSession(project, session) {
316
- const encoded = encodeCCConnectSessionKey(project, session.session_key, session.id);
317
- rawToEncoded.set(session.session_key, encoded);
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
+ }
318
630
  return encoded;
319
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
+ }
320
639
  function toRuntimeSession(project, session, encoded) {
321
640
  const platform = session.platform || session.session_key.split(":")[0] || "cc-connect";
322
641
  const title = session.name || session.chat_name || session.user_name || session.session_key || session.id;
@@ -383,6 +702,18 @@ export async function runCCConnectRelayManager(opts) {
383
702
  }
384
703
  try {
385
704
  switch (msg.method) {
705
+ case "projects.list":
706
+ if (requestId)
707
+ await respondProjectsList(requestId);
708
+ return;
709
+ case "projects.discover":
710
+ if (requestId)
711
+ await respondProjectsDiscover(requestId);
712
+ return;
713
+ case "projects.create":
714
+ if (requestId)
715
+ await respondProjectCreate(requestId, msg.params ?? {});
716
+ return;
386
717
  case "sessions.list":
387
718
  if (requestId)
388
719
  await respondSessionsList(requestId);
@@ -400,6 +731,11 @@ export async function runCCConnectRelayManager(opts) {
400
731
  if (requestId)
401
732
  sendResponse(requestId, { ok: true });
402
733
  return;
734
+ case "chat.action":
735
+ await handleChatAction(msg.params ?? {});
736
+ if (requestId)
737
+ sendResponse(requestId, { ok: true });
738
+ return;
403
739
  case "chat.abort":
404
740
  if (requestId)
405
741
  sendResponse(requestId, { ok: true });
@@ -426,6 +762,7 @@ export async function runCCConnectRelayManager(opts) {
426
762
  }
427
763
  });
428
764
  relayWs.on("close", (code, reason) => {
765
+ uninstallWatchdog();
429
766
  console.log(`cc-connect relay connection closed: ${code} ${reason.toString()}`);
430
767
  opts.onDisconnected?.();
431
768
  clearBridgeTimers();
@@ -445,4 +782,339 @@ function buildRelayUrl(base, gatewayId, relaySecret) {
445
782
  function stringValue(value) {
446
783
  return typeof value === "string" ? value : undefined;
447
784
  }
785
+ function textBlock(text) {
786
+ return text ? { type: "text", text } : undefined;
787
+ }
788
+ const PROGRESS_CARD_PREFIX = "__cc_connect_progress_card_v1__:";
789
+ function isProgressContent(content) {
790
+ return content.startsWith(PROGRESS_CARD_PREFIX) || content.trimStart().startsWith("⏳ **Progress**");
791
+ }
792
+ function progressContentIsCompleted(content) {
793
+ const payload = parseProgressPayload(content);
794
+ return payload?.state === "completed" || payload?.state === "failed";
795
+ }
796
+ function normalizeBridgeContent(content) {
797
+ const progress = parseProgressPayload(content);
798
+ if (!progress)
799
+ return { text: content };
800
+ const text = renderProgressPayload(progress);
801
+ return {
802
+ text,
803
+ blocks: [
804
+ {
805
+ type: "markdown",
806
+ text,
807
+ },
808
+ ],
809
+ };
810
+ }
811
+ function parseProgressPayload(content) {
812
+ if (!content.startsWith(PROGRESS_CARD_PREFIX))
813
+ return undefined;
814
+ try {
815
+ const parsed = JSON.parse(content.slice(PROGRESS_CARD_PREFIX.length));
816
+ const items = Array.isArray(parsed.items)
817
+ ? parsed.items.map((item) => ({
818
+ kind: stringValue(item.kind),
819
+ text: stringValue(item.text)?.trim() ?? "",
820
+ tool: stringValue(item.tool),
821
+ status: stringValue(item.status),
822
+ })).filter((item) => item.text)
823
+ : [];
824
+ const entries = items.length > 0
825
+ ? items
826
+ : (Array.isArray(parsed.entries) ? parsed.entries.map((entry) => ({
827
+ kind: undefined,
828
+ text: typeof entry === "string" ? entry.trim() : "",
829
+ })).filter((entry) => entry.text) : []);
830
+ if (entries.length === 0)
831
+ return undefined;
832
+ return {
833
+ state: stringValue(parsed.state),
834
+ truncated: Boolean(parsed.truncated),
835
+ entries,
836
+ };
837
+ }
838
+ catch {
839
+ return undefined;
840
+ }
841
+ }
842
+ function renderProgressPayload(payload) {
843
+ const icon = payload.state === "completed" ? "✅" : payload.state === "failed" ? "❌" : "⏳";
844
+ const lines = [`${icon} **Progress**`];
845
+ if (payload.truncated)
846
+ lines.push("_Showing latest updates only._");
847
+ for (const [index, entry] of payload.entries.entries()) {
848
+ const prefix = progressEntryPrefix(entry.kind, entry.tool);
849
+ const text = entry.text.replace(/\n/g, "\n ");
850
+ lines.push(`\n${index + 1}. ${prefix}${text}`);
851
+ }
852
+ return lines.join("\n");
853
+ }
854
+ function progressEntryPrefix(kind, tool) {
855
+ if (kind === "thinking")
856
+ return "💭 ";
857
+ if (kind === "tool_use")
858
+ return tool ? `🔧 **工具: ${tool}**\n ` : "🔧 ";
859
+ if (kind === "tool_result")
860
+ return tool ? `🧾 **结果: ${tool}**\n ` : "🧾 ";
861
+ if (kind === "error")
862
+ return "❌ ";
863
+ return "";
864
+ }
865
+ function actionsBlock(buttons, sessionKey, replyCtx) {
866
+ if (buttons.length === 0)
867
+ return undefined;
868
+ return {
869
+ type: "actions",
870
+ actions: buttons,
871
+ sessionKey,
872
+ replyCtx,
873
+ };
874
+ }
875
+ function normalizeBridgeButtons(buttons) {
876
+ if (!Array.isArray(buttons))
877
+ return [];
878
+ const actions = [];
879
+ const rows = buttons.some(Array.isArray) ? buttons : [buttons];
880
+ for (const row of rows) {
881
+ const buttonRow = Array.isArray(row) ? row : [row];
882
+ for (const button of buttonRow) {
883
+ const action = normalizeBridgeAction(button);
884
+ if (action)
885
+ actions.push(action);
886
+ }
887
+ }
888
+ return actions;
889
+ }
890
+ function normalizeBridgeCard(card, sessionKey, replyCtx) {
891
+ if (!isRecord(card)) {
892
+ return { text: "[card message]", blocks: [textBlock("[card message]")].filter(Boolean) };
893
+ }
894
+ const blocks = [];
895
+ const textParts = [];
896
+ const header = isRecord(card.header) ? stringValue(card.header.title) : undefined;
897
+ if (header) {
898
+ blocks.push({ type: "text", text: `**${header}**` });
899
+ textParts.push(header);
900
+ }
901
+ const elements = Array.isArray(card.elements) ? card.elements : [];
902
+ for (const element of elements) {
903
+ if (!isRecord(element))
904
+ continue;
905
+ const type = stringValue(element.type);
906
+ if (type === "markdown") {
907
+ const text = stringValue(element.content) ?? "";
908
+ if (text) {
909
+ blocks.push({ type: "markdown", text });
910
+ textParts.push(text);
911
+ }
912
+ continue;
913
+ }
914
+ if (type === "note") {
915
+ const text = stringValue(element.text) ?? "";
916
+ if (text) {
917
+ blocks.push({ type: "text", text });
918
+ textParts.push(text);
919
+ }
920
+ continue;
921
+ }
922
+ if (type === "divider") {
923
+ blocks.push({ type: "divider" });
924
+ continue;
925
+ }
926
+ if (type === "actions") {
927
+ const actions = normalizeCardActionButtons(element.buttons);
928
+ const block = actionsBlock(actions, sessionKey, replyCtx);
929
+ if (block)
930
+ blocks.push(block);
931
+ continue;
932
+ }
933
+ if (type === "list_item") {
934
+ const text = stringValue(element.text) ?? "";
935
+ if (text)
936
+ textParts.push(text);
937
+ const btnText = stringValue(element.btn_text);
938
+ const btnValue = stringValue(element.btn_value);
939
+ blocks.push({
940
+ type: "list_item",
941
+ text,
942
+ action: btnText && btnValue ? {
943
+ id: btnValue,
944
+ label: btnText,
945
+ value: btnValue,
946
+ style: actionStyle(btnValue, stringValue(element.btn_type)),
947
+ } : undefined,
948
+ sessionKey,
949
+ replyCtx,
950
+ });
951
+ continue;
952
+ }
953
+ if (type === "select") {
954
+ const options = Array.isArray(element.options) ? element.options.filter(isRecord).map((option) => ({
955
+ id: stringValue(option.value) ?? "",
956
+ label: stringValue(option.text) ?? "",
957
+ value: stringValue(option.value) ?? "",
958
+ style: "default",
959
+ })).filter((option) => option.label && option.value) : [];
960
+ blocks.push({
961
+ type: "select",
962
+ placeholder: stringValue(element.placeholder) ?? "Select",
963
+ initValue: stringValue(element.init_value),
964
+ actions: options,
965
+ sessionKey,
966
+ replyCtx,
967
+ });
968
+ }
969
+ }
970
+ return { text: textParts.join("\n\n"), blocks };
971
+ }
972
+ function isInteractiveAction(action) {
973
+ const normalized = action.toLowerCase();
974
+ return (normalized.startsWith("perm:")
975
+ || normalized.startsWith("askq:")
976
+ || normalized === "allow"
977
+ || normalized === "deny"
978
+ || normalized === "allow all");
979
+ }
980
+ function normalizeCardActionButtons(buttons) {
981
+ if (!Array.isArray(buttons))
982
+ return [];
983
+ const actions = [];
984
+ for (const button of buttons) {
985
+ const action = normalizeBridgeAction(button);
986
+ if (action)
987
+ actions.push(action);
988
+ }
989
+ return actions;
990
+ }
991
+ function normalizeBridgeAction(button) {
992
+ if (!isRecord(button))
993
+ return undefined;
994
+ const label = stringValue(button.text)
995
+ ?? stringValue(button.label)
996
+ ?? stringValue(button.title);
997
+ const value = stringValue(button.data)
998
+ ?? stringValue(button.value)
999
+ ?? stringValue(button.id)
1000
+ ?? inferActionValue(label);
1001
+ if (!label || !value)
1002
+ return undefined;
1003
+ return {
1004
+ id: value,
1005
+ label,
1006
+ value,
1007
+ style: actionStyle(value, stringValue(button.btn_type) ?? stringValue(button.style)),
1008
+ };
1009
+ }
1010
+ function permissionHintActions(content, sessionKey, replyCtx) {
1011
+ if (!isPermissionHint(content))
1012
+ return undefined;
1013
+ return actionsBlock([
1014
+ { id: "perm:allow", label: "允许", value: "perm:allow", style: "primary" },
1015
+ { id: "perm:deny", label: "拒绝", value: "perm:deny", style: "danger" },
1016
+ { id: "perm:allow_all", label: "允许所有", value: "perm:allow_all", style: "default" },
1017
+ ], sessionKey, replyCtx);
1018
+ }
1019
+ function isPermissionHint(content) {
1020
+ const normalized = content.toLowerCase();
1021
+ return (normalized.includes("等待权限响应") || normalized.includes("permission"))
1022
+ && (normalized.includes("允许所有") || normalized.includes("allow all"))
1023
+ && (normalized.includes("拒绝") || normalized.includes("deny"));
1024
+ }
1025
+ function isIntermediateReply(content) {
1026
+ const normalized = content.toLowerCase().trim();
1027
+ if (!normalized)
1028
+ return false;
1029
+ if (isPermissionHint(content))
1030
+ return true;
1031
+ return (normalized.includes("继续执行")
1032
+ || normalized.includes("继续处理中")
1033
+ || normalized.includes("continuing")
1034
+ || normalized.includes("continue execution")
1035
+ || normalized.includes("permission granted")
1036
+ || normalized.includes("已允许")
1037
+ || normalized.includes("已开启自动批准")
1038
+ || normalized.includes("自动批准")
1039
+ || normalized.includes("权限请求将自动允许"));
1040
+ }
1041
+ function inferActionValue(label) {
1042
+ if (!label)
1043
+ return undefined;
1044
+ const normalized = label.toLowerCase().replace(/\s+/g, "");
1045
+ if (normalized.includes("允许所有") || normalized.includes("全部允许") || normalized.includes("allowall")) {
1046
+ return "perm:allow_all";
1047
+ }
1048
+ if (normalized.includes("拒绝") || normalized.includes("deny") || normalized.includes("reject")) {
1049
+ return "perm:deny";
1050
+ }
1051
+ if (normalized.includes("允许") || normalized.includes("同意") || normalized.includes("allow") || normalized.includes("approve")) {
1052
+ return "perm:allow";
1053
+ }
1054
+ return undefined;
1055
+ }
1056
+ function actionStyle(value, explicit) {
1057
+ if (explicit === "primary" || explicit === "danger")
1058
+ return explicit;
1059
+ if (value.includes("deny") || value.includes("reject") || value.includes("cancel"))
1060
+ return "danger";
1061
+ if (value.includes("allow"))
1062
+ return "primary";
1063
+ return "default";
1064
+ }
1065
+ function bridgeMediaMimeType(msg) {
1066
+ const record = msg;
1067
+ const explicit = stringValue(record.mime_type);
1068
+ if (explicit)
1069
+ return explicit;
1070
+ if (msg.type === "audio") {
1071
+ const format = stringValue(record.format) ?? "mpeg";
1072
+ return format.includes("/") ? format : `audio/${format}`;
1073
+ }
1074
+ if (msg.type === "image")
1075
+ return "image/png";
1076
+ return "application/octet-stream";
1077
+ }
1078
+ function bridgeAttachments(params) {
1079
+ const attachments = isRecord(params) && Array.isArray(params.attachments) ? params.attachments : [];
1080
+ const images = [];
1081
+ const files = [];
1082
+ let audio;
1083
+ for (const attachment of attachments) {
1084
+ if (!isRecord(attachment))
1085
+ continue;
1086
+ const content = stringValue(attachment.content);
1087
+ const mimeType = stringValue(attachment.mimeType) ?? "application/octet-stream";
1088
+ const fileName = stringValue(attachment.fileName);
1089
+ if (!content)
1090
+ continue;
1091
+ const payload = {
1092
+ mime_type: mimeType,
1093
+ data: content.replace(/^data:[^;]+;base64,/, ""),
1094
+ file_name: fileName,
1095
+ };
1096
+ if (mimeType.startsWith("image/")) {
1097
+ images.push(payload);
1098
+ }
1099
+ else if (mimeType.startsWith("audio/") && !audio) {
1100
+ audio = {
1101
+ mime_type: mimeType,
1102
+ data: payload.data,
1103
+ format: mimeType.replace(/^audio\//, ""),
1104
+ file_name: fileName,
1105
+ };
1106
+ }
1107
+ else {
1108
+ files.push(payload);
1109
+ }
1110
+ }
1111
+ return {
1112
+ ...(images.length > 0 ? { images } : {}),
1113
+ ...(files.length > 0 ? { files } : {}),
1114
+ ...(audio ? { audio } : {}),
1115
+ };
1116
+ }
1117
+ function isRecord(value) {
1118
+ return typeof value === "object" && value !== null;
1119
+ }
448
1120
  //# sourceMappingURL=ccconnect-relay-manager.js.map