adp-openclaw 0.0.53 → 0.0.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adp-openclaw",
3
- "version": "0.0.53",
3
+ "version": "0.0.55",
4
4
  "description": "ADP-OpenClaw demo channel plugin (Go WebSocket backend)",
5
5
  "type": "module",
6
6
  "dependencies": {
package/src/monitor.ts CHANGED
@@ -59,6 +59,8 @@ const MsgType = {
59
59
  // OpenClaw sessions list
60
60
  FetchOpenClawSessions: "fetch_openclaw_sessions",
61
61
  OpenClawSessionsResponse: "openclaw_sessions_response",
62
+ // Cancel generation
63
+ Cancel: "cancel", // Server → Client: cancel ongoing generation
62
64
  } as const;
63
65
 
64
66
  type WSMessage = {
@@ -167,6 +169,9 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
167
169
  let authenticated = false;
168
170
  let pingInterval: NodeJS.Timeout | null = null;
169
171
 
172
+ // Track active generations for cancel support (keyed by conversationId)
173
+ const activeGenerations = new Map<string, AbortController>();
174
+
170
175
  // Handle abort signal
171
176
  const abortHandler = () => {
172
177
  ws.close();
@@ -330,6 +335,9 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
330
335
  Timestamp: inMsg.timestamp || Date.now(),
331
336
  OriginatingChannel: "adp-openclaw",
332
337
  OriginatingTo: "adp-openclaw:bot",
338
+ // Authorize slash commands (/new, /status, /reset, etc.)
339
+ // Without this, commands are silently dropped (deny-by-default)
340
+ CommandAuthorized: true,
333
341
  // Pass user metadata through context (like Feishu does)
334
342
  ...userMetadata,
335
343
  });
@@ -340,6 +348,15 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
340
348
  let lastPartialText = ""; // Track last sent text for delta calculation
341
349
  let finalSent = false; // Track if outbound_end has been sent
342
350
  const displayName = inMsg.user?.username || inMsg.from;
351
+
352
+ // Per-message AbortController for cancel support
353
+ const generationController = new AbortController();
354
+ const convId = inMsg.conversationId || streamId;
355
+ // Cancel any previous generation for the same conversation
356
+ if (activeGenerations.has(convId)) {
357
+ activeGenerations.get(convId)!.abort();
358
+ }
359
+ activeGenerations.set(convId, generationController);
343
360
 
344
361
  // 收集上传结果,在发送最终回复时追加完整的下载链接
345
362
  let pendingUploadResults: AdpUploadToolResult[] = [];
@@ -427,6 +444,7 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
427
444
  // Enable block streaming for SSE support
428
445
  replyOptions: {
429
446
  disableBlockStreaming: false, // Force enable block streaming
447
+ abortSignal: generationController.signal, // Per-message cancel support
430
448
  // Use onPartialReply for real-time streaming (character-level)
431
449
  // onPartialReply receives cumulative text, so we need to calculate delta
432
450
  onPartialReply: async (payload: { text?: string }) => {
@@ -556,6 +574,16 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
556
574
 
557
575
  log?.info(`[adp-openclaw] dispatchReplyWithBufferedBlockDispatcher returned (finalSent=${finalSent}, chunkIndex=${chunkIndex})`);
558
576
 
577
+ // Clean up active generation tracking
578
+ activeGenerations.delete(convId);
579
+
580
+ // If generation was cancelled (aborted), send outbound_end with partial text
581
+ if (generationController.signal.aborted && !finalSent) {
582
+ const cancelText = lastPartialText ? `${lastPartialText}\n\n[已停止生成]` : "[已停止生成]";
583
+ log?.info(`[adp-openclaw] Generation cancelled, sending outbound_end with partial text`);
584
+ sendOutboundEnd(cancelText);
585
+ }
586
+
559
587
  // IMPORTANT: After dispatchReplyWithBufferedBlockDispatcher completes,
560
588
  // ensure outbound_end is sent even if "final" deliver was not called.
561
589
  // This handles cases where the SDK only sends blocks without a final callback.
@@ -566,6 +594,8 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
566
594
  sendOutboundEnd(finalText);
567
595
  }
568
596
  } catch (err) {
597
+ // Clean up on error
598
+ activeGenerations.delete(convId);
569
599
  log?.error(`[adp-openclaw] Failed to process message: ${err}`);
570
600
  }
571
601
  break;
@@ -582,6 +612,30 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
582
612
  break;
583
613
  }
584
614
 
615
+ // Handle cancel generation request from server (user clicked "stop generating")
616
+ case MsgType.Cancel: {
617
+ if (!authenticated) break;
618
+ const cancelPayload = msg.payload as { conversationId?: string; streamId?: string };
619
+ const cancelConvId = cancelPayload.conversationId || cancelPayload.streamId;
620
+ log?.info(`[adp-openclaw] Received cancel request for conv=${cancelConvId}`);
621
+
622
+ if (cancelConvId && activeGenerations.has(cancelConvId)) {
623
+ activeGenerations.get(cancelConvId)!.abort();
624
+ log?.info(`[adp-openclaw] Generation cancelled for conv=${cancelConvId}`);
625
+ } else {
626
+ // If no specific convId, cancel all active generations
627
+ if (!cancelConvId && activeGenerations.size > 0) {
628
+ for (const [id, controller] of activeGenerations) {
629
+ controller.abort();
630
+ log?.info(`[adp-openclaw] Generation cancelled for conv=${id} (cancel-all)`);
631
+ }
632
+ } else {
633
+ log?.warn(`[adp-openclaw] No active generation found for conv=${cancelConvId}`);
634
+ }
635
+ }
636
+ break;
637
+ }
638
+
585
639
  // Handle fetch OpenClaw chat history request from GoServer
586
640
  case MsgType.ConvHistory: {
587
641
  if (!authenticated) break;
@@ -518,69 +518,6 @@ function findOpenClawCli(config: SessionFileConfig): string | null {
518
518
  return null;
519
519
  }
520
520
 
521
- /**
522
- * Extract valid JSON from CLI output that may contain TUI decoration characters
523
- * (e.g. │, ◇, ◆, spinner frames) mixed into stdout.
524
- */
525
- function extractJsonFromOutput(raw: string): string {
526
- const trimmed = raw.trim();
527
-
528
- // Fast path: already valid JSON
529
- if (
530
- (trimmed.startsWith("{") && trimmed.endsWith("}")) ||
531
- (trimmed.startsWith("[") && trimmed.endsWith("]"))
532
- ) {
533
- return trimmed;
534
- }
535
-
536
- // Try to find the first top-level JSON object or array in the output
537
- const jsonStart = trimmed.search(/[\[{]/);
538
- if (jsonStart === -1) {
539
- return trimmed; // no JSON-like content, return as-is and let caller handle the error
540
- }
541
-
542
- const opener = trimmed[jsonStart];
543
- const closer = opener === "{" ? "}" : "]";
544
-
545
- // Walk forward tracking brace/bracket depth to find the matching close
546
- let depth = 0;
547
- let inString = false;
548
- let escaped = false;
549
-
550
- for (let i = jsonStart; i < trimmed.length; i++) {
551
- const ch = trimmed[i];
552
-
553
- if (escaped) {
554
- escaped = false;
555
- continue;
556
- }
557
- if (ch === "\\") {
558
- if (inString) escaped = true;
559
- continue;
560
- }
561
- if (ch === '"') {
562
- inString = !inString;
563
- continue;
564
- }
565
- if (inString) continue;
566
-
567
- if (ch === opener || ch === (opener === "{" ? "[" : "{")) {
568
- // count both kinds of nesting
569
- if (ch === "{" || ch === "[") depth++;
570
- }
571
- if (ch === closer || ch === (closer === "}" ? "]" : "}")) {
572
- if (ch === "}" || ch === "]") depth--;
573
- }
574
-
575
- if (depth === 0) {
576
- return trimmed.slice(jsonStart, i + 1);
577
- }
578
- }
579
-
580
- // Fallback: return from jsonStart onwards
581
- return trimmed.slice(jsonStart);
582
- }
583
-
584
521
  /**
585
522
  * Execute an openclaw CLI command and return the result.
586
523
  * @param subcommands - Array of subcommands, e.g. ["gateway", "call", "chat.history"] or ["sessions"]
@@ -656,7 +593,7 @@ async function executeClawCommand(
656
593
  log?.error?.(`[session-history] CLI exited with code ${code}: ${stderr}`);
657
594
  reject(new Error(`CLI exited with code ${code}: ${stderr}`));
658
595
  } else {
659
- resolve(extractJsonFromOutput(stdout));
596
+ resolve(stdout);
660
597
  }
661
598
  });
662
599
  });