@rethinkingstudio/clawpilot 2.1.7-internal.8 → 2.1.7

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 (39) hide show
  1. package/dist/commands/local-handlers.js +46 -0
  2. package/dist/commands/local-handlers.js.map +1 -1
  3. package/dist/commands/pair.js +15 -8
  4. package/dist/commands/pair.js.map +1 -1
  5. package/dist/config/config.js +2 -1
  6. package/dist/config/config.js.map +1 -1
  7. package/dist/generated/build-config.d.ts +1 -1
  8. package/dist/generated/build-config.js +1 -1
  9. package/dist/generated/build-config.js.map +1 -1
  10. package/dist/hermes/agent-bridge.js +5 -0
  11. package/dist/hermes/agent-bridge.js.map +1 -1
  12. package/dist/hermes/home.d.ts +23 -0
  13. package/dist/hermes/home.js +250 -0
  14. package/dist/hermes/home.js.map +1 -0
  15. package/dist/hermes/model-catalog.js +10 -9
  16. package/dist/hermes/model-catalog.js.map +1 -1
  17. package/dist/platform/service-manager.js +9 -0
  18. package/dist/platform/service-manager.js.map +1 -1
  19. package/dist/relay/agent-status.d.ts +55 -0
  20. package/dist/relay/agent-status.js +294 -0
  21. package/dist/relay/agent-status.js.map +1 -0
  22. package/dist/relay/ccconnect-relay-manager.js +316 -65
  23. package/dist/relay/ccconnect-relay-manager.js.map +1 -1
  24. package/dist/relay/hermes-relay-manager.js +225 -53
  25. package/dist/relay/hermes-relay-manager.js.map +1 -1
  26. package/dist/relay/relay-command-dispatcher.d.ts +14 -0
  27. package/dist/relay/relay-command-dispatcher.js +25 -0
  28. package/dist/relay/relay-command-dispatcher.js.map +1 -0
  29. package/dist/relay/relay-manager.js +60 -29
  30. package/dist/relay/relay-manager.js.map +1 -1
  31. package/dist/relay/relay-method-registry.d.ts +9 -0
  32. package/dist/relay/relay-method-registry.js +61 -0
  33. package/dist/relay/relay-method-registry.js.map +1 -0
  34. package/dist/relay/relay-watchdog.d.ts +2 -0
  35. package/dist/relay/relay-watchdog.js +31 -0
  36. package/dist/relay/relay-watchdog.js.map +1 -0
  37. package/package.json +1 -1
  38. package/scripts/hermes_agent_bridge.py +14 -2
  39. package/scripts/verify-build-config.mjs +1 -3
@@ -1,11 +1,12 @@
1
1
  import { randomUUID } from "crypto";
2
2
  import { WebSocket } from "ws";
3
- import { handleLocalCommand } from "../commands/local-handlers.js";
4
- import { handleProviderCommand } from "../commands/provider-handlers.js";
5
3
  import { getServicePlatform } from "../platform/service-manager.js";
6
4
  import { CCConnectManagementClient, decodeCCConnectSessionKey, encodeCCConnectSessionKey, timestampMs, } from "../ccconnect/api-client.js";
7
5
  import { createCCConnectProject, discoverCCConnectProjects, restartCCConnect } from "../ccconnect/project-config.js";
8
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";
9
10
  const DEFAULT_CONTEXT_TOKENS = 200_000;
10
11
  export async function runCCConnectRelayManager(opts) {
11
12
  const wsUrl = buildRelayUrl(opts.relayServerUrl, opts.gatewayId, opts.relaySecret);
@@ -21,7 +22,10 @@ export async function runCCConnectRelayManager(opts) {
21
22
  let bridgePingTimer = null;
22
23
  let bridgeReconnectAttempts = 0;
23
24
  const rawToEncoded = new Map();
25
+ const rawToProject = new Map();
24
26
  const activeRunByEncoded = new Map();
27
+ const previewHandleToRun = new Map();
28
+ const pendingInteractiveReplyByEncoded = new Set();
25
29
  try {
26
30
  relayWs = new WebSocket(wsUrl);
27
31
  }
@@ -30,6 +34,7 @@ export async function runCCConnectRelayManager(opts) {
30
34
  resolve(true);
31
35
  return;
32
36
  }
37
+ const uninstallWatchdog = installRelayWatchdog(relayWs, `ccconnect:${opts.gatewayId}`);
33
38
  function send(msg) {
34
39
  if (relayWs.readyState === WebSocket.OPEN) {
35
40
  relayWs.send(JSON.stringify(msg));
@@ -121,7 +126,12 @@ export async function runCCConnectRelayManager(opts) {
121
126
  "file",
122
127
  "audio",
123
128
  ],
124
- metadata: { version: "1.0.0", description: "PocketClaw relay adapter" },
129
+ metadata: {
130
+ version: "1.0.0",
131
+ description: "PocketClaw relay adapter",
132
+ progress_style: "card",
133
+ supports_progress_card_payload: true,
134
+ },
125
135
  }));
126
136
  });
127
137
  ws.on("message", (raw) => {
@@ -177,10 +187,12 @@ export async function runCCConnectRelayManager(opts) {
177
187
  if (msg.type === "pong")
178
188
  return;
179
189
  const rawSessionKey = stringValue(record.session_key) ?? "";
180
- const encodedSessionKey = rawSessionKey ? rawToEncoded.get(rawSessionKey) : undefined;
190
+ const encodedSessionKey = await resolveEncodedBridgeSession(record);
181
191
  if (!encodedSessionKey)
182
192
  return;
183
- const runId = ensureRunId(encodedSessionKey);
193
+ const previewHandle = stringValue(record.preview_handle);
194
+ const runInfo = resolveRunForBridgeMessage(encodedSessionKey, previewHandle);
195
+ let runId = runInfo.runId;
184
196
  if (msg.type === "typing_start") {
185
197
  emitChat(runId, encodedSessionKey, "delta", undefined, true);
186
198
  return;
@@ -189,57 +201,93 @@ export async function runCCConnectRelayManager(opts) {
189
201
  return;
190
202
  }
191
203
  if (msg.type === "preview_start") {
192
- 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);
193
210
  const refId = stringValue(record.ref_id);
211
+ const handle = `pocketclaw-${runId}`;
212
+ previewHandleToRun.set(handle, { encodedSessionKey, runId, sideChannel });
194
213
  if (refId) {
195
214
  bridgeWs?.send(JSON.stringify({
196
215
  type: "preview_ack",
197
216
  ref_id: refId,
198
- preview_handle: `pocketclaw-${runId}`,
217
+ preview_handle: handle,
199
218
  }));
200
219
  }
201
220
  return;
202
221
  }
203
222
  if (msg.type === "update_message") {
204
- 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);
205
229
  return;
206
230
  }
207
231
  if (msg.type === "delete_message") {
232
+ if (previewHandle)
233
+ previewHandleToRun.delete(previewHandle);
208
234
  send({
209
235
  type: "event",
210
236
  event: "message.delete",
211
237
  payload: {
212
238
  runId,
213
239
  sessionKey: encodedSessionKey,
214
- previewHandle: stringValue(record.preview_handle),
240
+ previewHandle,
241
+ keepTurn: true,
242
+ sideChannel: runInfo.sideChannel,
215
243
  },
216
244
  });
217
- activeRunByEncoded.delete(encodedSessionKey);
218
245
  return;
219
246
  }
220
247
  if (msg.type === "reply_stream") {
221
248
  const text = stringValue(record.full_text) ?? stringValue(record.delta) ?? "";
222
249
  const done = Boolean(record.done);
223
- emitChat(runId, encodedSessionKey, done ? "final" : "delta", text, false);
224
- if (done)
225
- 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);
226
255
  return;
227
256
  }
228
257
  if (msg.type === "reply") {
229
- emitChat(runId, encodedSessionKey, "final", stringValue(record.content) ?? "", false);
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);
230
275
  return;
231
276
  }
232
277
  if (msg.type === "buttons") {
278
+ const actions = normalizeBridgeButtons(record.buttons);
279
+ console.log(`[runtime:ccconnect] bridge buttons session=${rawSessionKey} actions=${actions.length}`);
233
280
  emitStructuredChat(runId, encodedSessionKey, "delta", {
234
281
  text: stringValue(record.content) ?? "",
235
282
  blocks: [
236
283
  textBlock(stringValue(record.content) ?? ""),
237
- actionsBlock(normalizeBridgeButtons(record.buttons), rawSessionKey, stringValue(record.reply_ctx) ?? rawSessionKey),
284
+ actionsBlock(actions, rawSessionKey, stringValue(record.reply_ctx) ?? rawSessionKey),
238
285
  ].filter(Boolean),
239
286
  });
240
287
  return;
241
288
  }
242
289
  if (msg.type === "card") {
290
+ console.log(`[runtime:ccconnect] bridge card session=${rawSessionKey}`);
243
291
  emitStructuredChat(runId, encodedSessionKey, "delta", normalizeBridgeCard(record.card, rawSessionKey, stringValue(record.reply_ctx) ?? rawSessionKey));
244
292
  return;
245
293
  }
@@ -250,13 +298,45 @@ export async function runCCConnectRelayManager(opts) {
250
298
  attachments: attachment.attachments,
251
299
  blocks: attachment.text ? [textBlock(attachment.text)] : [],
252
300
  });
301
+ clearActiveBridgeRun(encodedSessionKey, runId);
253
302
  return;
254
303
  }
255
304
  if (msg.type === "error") {
256
305
  emitChat(runId, encodedSessionKey, "error", stringValue(record.message) ?? "cc-connect bridge error", false);
257
- activeRunByEncoded.delete(encodedSessionKey);
306
+ clearActiveBridgeRun(encodedSessionKey, runId);
258
307
  }
259
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
+ }
260
340
  function ensureRunId(encodedSessionKey, preferred) {
261
341
  const existing = activeRunByEncoded.get(encodedSessionKey);
262
342
  if (existing)
@@ -265,6 +345,39 @@ export async function runCCConnectRelayManager(opts) {
265
345
  activeRunByEncoded.set(encodedSessionKey, runId);
266
346
  return runId;
267
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
+ }
268
381
  function emitChat(runId, sessionKey, state, text, loading) {
269
382
  const payload = { runId, sessionKey, state };
270
383
  if (loading)
@@ -276,6 +389,17 @@ export async function runCCConnectRelayManager(opts) {
276
389
  payload.errorMessage = text ?? "cc-connect error";
277
390
  send({ type: "event", event: "chat", payload });
278
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
+ }
279
403
  function emitStructuredChat(runId, sessionKey, state, data) {
280
404
  const blocks = data.blocks?.length ? data.blocks : data.text !== undefined ? [textBlock(data.text)] : undefined;
281
405
  const payload = { runId, sessionKey, state };
@@ -410,7 +534,7 @@ export async function runCCConnectRelayManager(opts) {
410
534
  if (!project)
411
535
  throw new Error("cc-connect has no configured projects");
412
536
  const title = p.title?.trim() || "New Session";
413
- const rawSessionKey = `pocketclaw:${opts.gatewayId}:default`;
537
+ const rawSessionKey = `pocketclaw:${opts.gatewayId}:${randomUUID()}`;
414
538
  const created = await client.createSession(project, rawSessionKey, title);
415
539
  const session = {
416
540
  ...created,
@@ -431,7 +555,9 @@ export async function runCCConnectRelayManager(opts) {
431
555
  throw new Error("message required");
432
556
  const decoded = await resolveCCConnectSendTarget(p.sessionKey);
433
557
  rawToEncoded.set(decoded.sessionKey, p.sessionKey);
558
+ rawToProject.set(decoded.sessionKey, decoded.project);
434
559
  activeRunByEncoded.delete(p.sessionKey);
560
+ pendingInteractiveReplyByEncoded.delete(p.sessionKey);
435
561
  const runId = ensureRunId(p.sessionKey, p.idempotencyKey);
436
562
  emitChat(runId, p.sessionKey, "delta", undefined, true);
437
563
  if (bridgeReady && bridgeWs?.readyState === WebSocket.OPEN) {
@@ -463,6 +589,9 @@ export async function runCCConnectRelayManager(opts) {
463
589
  throw new Error("cc-connect bridge not ready");
464
590
  }
465
591
  const decoded = await resolveCCConnectSendTarget(p.sessionKey);
592
+ if (isInteractiveAction(p.action)) {
593
+ pendingInteractiveReplyByEncoded.add(p.sessionKey);
594
+ }
466
595
  bridgeWs.send(JSON.stringify({
467
596
  type: "card_action",
468
597
  session_key: decoded.sessionKey,
@@ -493,8 +622,12 @@ export async function runCCConnectRelayManager(opts) {
493
622
  }
494
623
  }
495
624
  function rememberSession(project, session) {
496
- const encoded = encodeCCConnectSessionKey(project, session.session_key, session.session_key);
497
- rawToEncoded.set(session.session_key, encoded);
625
+ const sessionId = session.id || session.session_key;
626
+ const encoded = encodeCCConnectSessionKey(project, session.session_key, sessionId);
627
+ if (!rawToEncoded.has(session.session_key)) {
628
+ rawToEncoded.set(session.session_key, encoded);
629
+ rawToProject.set(session.session_key, project);
630
+ }
498
631
  return encoded;
499
632
  }
500
633
  async function resolveCCConnectSessionId(project, sessionKey, sessionId) {
@@ -548,24 +681,9 @@ export async function runCCConnectRelayManager(opts) {
548
681
  if (msg.type !== "cmd" || !msg.method)
549
682
  return;
550
683
  const requestId = msg.id;
551
- console.log(`[relay:ccconnect] cmd received method=${msg.method} id=${requestId ?? "(no-id)"}`);
552
- const providerPromise = handleProviderCommand(msg.method, msg.params);
553
- if (providerPromise !== null) {
554
- const result = await providerPromise;
555
- if (requestId) {
556
- send(result.ok
557
- ? { type: "res", id: requestId, ok: true, payload: result.payload }
558
- : { type: "res", id: requestId, ok: false, error: { message: result.error } });
559
- }
560
- return;
561
- }
562
- const localResult = handleLocalCommand(msg.method, msg.params);
563
- if (localResult !== null) {
564
- if (requestId) {
565
- send(localResult.ok
566
- ? { type: "res", id: requestId, ok: true, payload: localResult.payload }
567
- : { type: "res", id: requestId, ok: false, error: { message: localResult.error } });
568
- }
684
+ const support = relayMethodSupport(msg.method, "ccconnect") ?? "unsupported";
685
+ console.log(`[relay:ccconnect] cmd received method=${msg.method} support=${support} id=${requestId ?? "(no-id)"}`);
686
+ if (await dispatchCommonRelayCommand(msg.method, msg.params, requestId, send)) {
569
687
  return;
570
688
  }
571
689
  try {
@@ -630,6 +748,7 @@ export async function runCCConnectRelayManager(opts) {
630
748
  }
631
749
  });
632
750
  relayWs.on("close", (code, reason) => {
751
+ uninstallWatchdog();
633
752
  console.log(`cc-connect relay connection closed: ${code} ${reason.toString()}`);
634
753
  opts.onDisconnected?.();
635
754
  clearBridgeTimers();
@@ -652,6 +771,83 @@ function stringValue(value) {
652
771
  function textBlock(text) {
653
772
  return text ? { type: "text", text } : undefined;
654
773
  }
774
+ const PROGRESS_CARD_PREFIX = "__cc_connect_progress_card_v1__:";
775
+ function isProgressContent(content) {
776
+ return content.startsWith(PROGRESS_CARD_PREFIX) || content.trimStart().startsWith("⏳ **Progress**");
777
+ }
778
+ function progressContentIsCompleted(content) {
779
+ const payload = parseProgressPayload(content);
780
+ return payload?.state === "completed" || payload?.state === "failed";
781
+ }
782
+ function normalizeBridgeContent(content) {
783
+ const progress = parseProgressPayload(content);
784
+ if (!progress)
785
+ return { text: content };
786
+ const text = renderProgressPayload(progress);
787
+ return {
788
+ text,
789
+ blocks: [
790
+ {
791
+ type: "markdown",
792
+ text,
793
+ },
794
+ ],
795
+ };
796
+ }
797
+ function parseProgressPayload(content) {
798
+ if (!content.startsWith(PROGRESS_CARD_PREFIX))
799
+ return undefined;
800
+ try {
801
+ const parsed = JSON.parse(content.slice(PROGRESS_CARD_PREFIX.length));
802
+ const items = Array.isArray(parsed.items)
803
+ ? parsed.items.map((item) => ({
804
+ kind: stringValue(item.kind),
805
+ text: stringValue(item.text)?.trim() ?? "",
806
+ tool: stringValue(item.tool),
807
+ status: stringValue(item.status),
808
+ })).filter((item) => item.text)
809
+ : [];
810
+ const entries = items.length > 0
811
+ ? items
812
+ : (Array.isArray(parsed.entries) ? parsed.entries.map((entry) => ({
813
+ kind: undefined,
814
+ text: typeof entry === "string" ? entry.trim() : "",
815
+ })).filter((entry) => entry.text) : []);
816
+ if (entries.length === 0)
817
+ return undefined;
818
+ return {
819
+ state: stringValue(parsed.state),
820
+ truncated: Boolean(parsed.truncated),
821
+ entries,
822
+ };
823
+ }
824
+ catch {
825
+ return undefined;
826
+ }
827
+ }
828
+ function renderProgressPayload(payload) {
829
+ const icon = payload.state === "completed" ? "✅" : payload.state === "failed" ? "❌" : "⏳";
830
+ const lines = [`${icon} **Progress**`];
831
+ if (payload.truncated)
832
+ lines.push("_Showing latest updates only._");
833
+ for (const [index, entry] of payload.entries.entries()) {
834
+ const prefix = progressEntryPrefix(entry.kind, entry.tool);
835
+ const text = entry.text.replace(/\n/g, "\n ");
836
+ lines.push(`\n${index + 1}. ${prefix}${text}`);
837
+ }
838
+ return lines.join("\n");
839
+ }
840
+ function progressEntryPrefix(kind, tool) {
841
+ if (kind === "thinking")
842
+ return "💭 ";
843
+ if (kind === "tool_use")
844
+ return tool ? `🔧 **工具: ${tool}**\n ` : "🔧 ";
845
+ if (kind === "tool_result")
846
+ return tool ? `🧾 **结果: ${tool}**\n ` : "🧾 ";
847
+ if (kind === "error")
848
+ return "❌ ";
849
+ return "";
850
+ }
655
851
  function actionsBlock(buttons, sessionKey, replyCtx) {
656
852
  if (buttons.length === 0)
657
853
  return undefined;
@@ -666,22 +862,13 @@ function normalizeBridgeButtons(buttons) {
666
862
  if (!Array.isArray(buttons))
667
863
  return [];
668
864
  const actions = [];
669
- for (const row of buttons) {
670
- if (!Array.isArray(row))
671
- continue;
672
- for (const button of row) {
673
- if (!isRecord(button))
674
- continue;
675
- const label = stringValue(button.text);
676
- const value = stringValue(button.data) ?? stringValue(button.value);
677
- if (!label || !value)
678
- continue;
679
- actions.push({
680
- id: value,
681
- label,
682
- value,
683
- style: actionStyle(value, stringValue(button.btn_type)),
684
- });
865
+ const rows = buttons.some(Array.isArray) ? buttons : [buttons];
866
+ for (const row of rows) {
867
+ const buttonRow = Array.isArray(row) ? row : [row];
868
+ for (const button of buttonRow) {
869
+ const action = normalizeBridgeAction(button);
870
+ if (action)
871
+ actions.push(action);
685
872
  }
686
873
  }
687
874
  return actions;
@@ -768,26 +955,90 @@ function normalizeBridgeCard(card, sessionKey, replyCtx) {
768
955
  }
769
956
  return { text: textParts.join("\n\n"), blocks };
770
957
  }
958
+ function isInteractiveAction(action) {
959
+ const normalized = action.toLowerCase();
960
+ return (normalized.startsWith("perm:")
961
+ || normalized.startsWith("askq:")
962
+ || normalized === "allow"
963
+ || normalized === "deny"
964
+ || normalized === "allow all");
965
+ }
771
966
  function normalizeCardActionButtons(buttons) {
772
967
  if (!Array.isArray(buttons))
773
968
  return [];
774
969
  const actions = [];
775
970
  for (const button of buttons) {
776
- if (!isRecord(button))
777
- continue;
778
- const label = stringValue(button.text);
779
- const value = stringValue(button.value) ?? stringValue(button.data);
780
- if (!label || !value)
781
- continue;
782
- actions.push({
783
- id: value,
784
- label,
785
- value,
786
- style: actionStyle(value, stringValue(button.btn_type)),
787
- });
971
+ const action = normalizeBridgeAction(button);
972
+ if (action)
973
+ actions.push(action);
788
974
  }
789
975
  return actions;
790
976
  }
977
+ function normalizeBridgeAction(button) {
978
+ if (!isRecord(button))
979
+ return undefined;
980
+ const label = stringValue(button.text)
981
+ ?? stringValue(button.label)
982
+ ?? stringValue(button.title);
983
+ const value = stringValue(button.data)
984
+ ?? stringValue(button.value)
985
+ ?? stringValue(button.id)
986
+ ?? inferActionValue(label);
987
+ if (!label || !value)
988
+ return undefined;
989
+ return {
990
+ id: value,
991
+ label,
992
+ value,
993
+ style: actionStyle(value, stringValue(button.btn_type) ?? stringValue(button.style)),
994
+ };
995
+ }
996
+ function permissionHintActions(content, sessionKey, replyCtx) {
997
+ if (!isPermissionHint(content))
998
+ return undefined;
999
+ return actionsBlock([
1000
+ { id: "perm:allow", label: "允许", value: "perm:allow", style: "primary" },
1001
+ { id: "perm:deny", label: "拒绝", value: "perm:deny", style: "danger" },
1002
+ { id: "perm:allow_all", label: "允许所有", value: "perm:allow_all", style: "default" },
1003
+ ], sessionKey, replyCtx);
1004
+ }
1005
+ function isPermissionHint(content) {
1006
+ const normalized = content.toLowerCase();
1007
+ return (normalized.includes("等待权限响应") || normalized.includes("permission"))
1008
+ && (normalized.includes("允许所有") || normalized.includes("allow all"))
1009
+ && (normalized.includes("拒绝") || normalized.includes("deny"));
1010
+ }
1011
+ function isIntermediateReply(content) {
1012
+ const normalized = content.toLowerCase().trim();
1013
+ if (!normalized)
1014
+ return false;
1015
+ if (isPermissionHint(content))
1016
+ return true;
1017
+ return (normalized.includes("继续执行")
1018
+ || normalized.includes("继续处理中")
1019
+ || normalized.includes("continuing")
1020
+ || normalized.includes("continue execution")
1021
+ || normalized.includes("permission granted")
1022
+ || normalized.includes("已允许")
1023
+ || normalized.includes("已开启自动批准")
1024
+ || normalized.includes("自动批准")
1025
+ || normalized.includes("权限请求将自动允许"));
1026
+ }
1027
+ function inferActionValue(label) {
1028
+ if (!label)
1029
+ return undefined;
1030
+ const normalized = label.toLowerCase().replace(/\s+/g, "");
1031
+ if (normalized.includes("允许所有") || normalized.includes("全部允许") || normalized.includes("allowall")) {
1032
+ return "perm:allow_all";
1033
+ }
1034
+ if (normalized.includes("拒绝") || normalized.includes("deny") || normalized.includes("reject")) {
1035
+ return "perm:deny";
1036
+ }
1037
+ if (normalized.includes("允许") || normalized.includes("同意") || normalized.includes("allow") || normalized.includes("approve")) {
1038
+ return "perm:allow";
1039
+ }
1040
+ return undefined;
1041
+ }
791
1042
  function actionStyle(value, explicit) {
792
1043
  if (explicit === "primary" || explicit === "danger")
793
1044
  return explicit;