adp-openclaw 0.0.54 → 0.0.56

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.54",
3
+ "version": "0.0.56",
4
4
  "description": "ADP-OpenClaw demo channel plugin (Go WebSocket backend)",
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -68,7 +68,7 @@ interface FileMetadata {
68
68
  // ==================== 常量配置 ====================
69
69
 
70
70
  /** 获取临时密钥的接口地址 */
71
- const CREDENTIAL_API_URL = "https://wss.lke.cloud.tencent.com/v1/gateway/storage/get_credential";
71
+ const CREDENTIAL_API_URL = "https://wss.lke.cloud.tencent.com/test/openclaw/v1/gateway/storage/get_credential";
72
72
 
73
73
  /** 请求超时时间 (毫秒) */
74
74
  const REQUEST_TIMEOUT_MS = 30000;
package/src/monitor.ts CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  formatUploadResultAsMarkdown,
24
24
  } from "./tool-result-message-blocks.js";
25
25
  import crypto from "crypto";
26
+ import fs from "fs";
26
27
  // @ts-ignore - import JSON file
27
28
  import packageJson from "../package.json" with { type: "json" };
28
29
 
@@ -59,6 +60,8 @@ const MsgType = {
59
60
  // OpenClaw sessions list
60
61
  FetchOpenClawSessions: "fetch_openclaw_sessions",
61
62
  OpenClawSessionsResponse: "openclaw_sessions_response",
63
+ // Cancel generation
64
+ Cancel: "cancel", // Server → Client: cancel ongoing generation
62
65
  } as const;
63
66
 
64
67
  type WSMessage = {
@@ -114,6 +117,53 @@ function generateRequestId(): string {
114
117
  return `req-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
115
118
  }
116
119
 
120
+ /**
121
+ * Mark a session's abortedLastRun flag in the sessions store.
122
+ * This tells the SDK to inject an "abort hint" on the next message,
123
+ * preventing the AI from resuming the cancelled task.
124
+ */
125
+ async function markSessionAborted(params: {
126
+ sessionKey: string;
127
+ runtime: ReturnType<typeof getAdpOpenclawRuntime>;
128
+ cfg?: ClawdbotConfig;
129
+ log?: PluginLogger;
130
+ }): Promise<void> {
131
+ const { sessionKey, runtime, cfg, log } = params;
132
+ try {
133
+ // Use SDK's resolveStorePath to find the sessions.json location
134
+ const storePath = runtime.channel.session.resolveStorePath(cfg?.session?.store);
135
+ if (!storePath || !fs.existsSync(storePath)) {
136
+ log?.warn?.(`[adp-openclaw] Cannot mark session aborted: store not found at ${storePath}`);
137
+ return;
138
+ }
139
+
140
+ const raw = fs.readFileSync(storePath, "utf-8");
141
+ const store = JSON.parse(raw) as Record<string, { abortedLastRun?: boolean; updatedAt?: number; [key: string]: unknown }>;
142
+
143
+ // Try both the raw sessionKey and the "agent:main:{sessionKey}" variant
144
+ const candidates = [sessionKey, `agent:main:${sessionKey}`];
145
+ let matchedKey: string | undefined;
146
+ for (const key of candidates) {
147
+ if (store[key]) {
148
+ matchedKey = key;
149
+ break;
150
+ }
151
+ }
152
+
153
+ if (!matchedKey) {
154
+ log?.info?.(`[adp-openclaw] Session key not found in store for abort marking: ${sessionKey}`);
155
+ return;
156
+ }
157
+
158
+ store[matchedKey].abortedLastRun = true;
159
+ store[matchedKey].updatedAt = Date.now();
160
+ fs.writeFileSync(storePath, JSON.stringify(store, null, 2), "utf-8");
161
+ log?.info?.(`[adp-openclaw] Marked session ${matchedKey} as abortedLastRun=true`);
162
+ } catch (err) {
163
+ log?.error?.(`[adp-openclaw] Failed to mark session aborted: ${err}`);
164
+ }
165
+ }
166
+
117
167
  export async function monitorAdpOpenclaw(params: MonitorParams): Promise<void> {
118
168
  const { wsUrl, clientToken, signKey, abortSignal, log, cfg } = params;
119
169
  const runtime = getAdpOpenclawRuntime();
@@ -167,6 +217,9 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
167
217
  let authenticated = false;
168
218
  let pingInterval: NodeJS.Timeout | null = null;
169
219
 
220
+ // Track active generations for cancel support (keyed by conversationId)
221
+ const activeGenerations = new Map<string, AbortController>();
222
+
170
223
  // Handle abort signal
171
224
  const abortHandler = () => {
172
225
  ws.close();
@@ -250,6 +303,7 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
250
303
  log?.info(`[adp-openclaw] Received: ${inMsg.from}: ${inMsg.text} (conv=${inMsg.conversationId}, rec=${inMsg.recordId || 'none'}, user=${JSON.stringify(inMsg.user || {})})`);
251
304
 
252
305
  // Process the message with full user identity
306
+ const convIdForCleanup = inMsg.conversationId || `fallback-${Date.now()}`;
253
307
  try {
254
308
  // Build user identity string for From field (like Feishu: "feishu:user_id")
255
309
  const userIdentifier = inMsg.user?.userId || inMsg.from;
@@ -330,6 +384,9 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
330
384
  Timestamp: inMsg.timestamp || Date.now(),
331
385
  OriginatingChannel: "adp-openclaw",
332
386
  OriginatingTo: "adp-openclaw:bot",
387
+ // Authorize slash commands (/new, /status, /reset, etc.)
388
+ // Without this, commands are silently dropped (deny-by-default)
389
+ CommandAuthorized: true,
333
390
  // Pass user metadata through context (like Feishu does)
334
391
  ...userMetadata,
335
392
  });
@@ -340,6 +397,15 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
340
397
  let lastPartialText = ""; // Track last sent text for delta calculation
341
398
  let finalSent = false; // Track if outbound_end has been sent
342
399
  const displayName = inMsg.user?.username || inMsg.from;
400
+
401
+ // Per-message AbortController for cancel support
402
+ const generationController = new AbortController();
403
+ const convId = inMsg.conversationId || streamId;
404
+ // Cancel any previous generation for the same conversation
405
+ if (activeGenerations.has(convId)) {
406
+ activeGenerations.get(convId)!.abort();
407
+ }
408
+ activeGenerations.set(convId, generationController);
343
409
 
344
410
  // 收集上传结果,在发送最终回复时追加完整的下载链接
345
411
  let pendingUploadResults: AdpUploadToolResult[] = [];
@@ -427,6 +493,7 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
427
493
  // Enable block streaming for SSE support
428
494
  replyOptions: {
429
495
  disableBlockStreaming: false, // Force enable block streaming
496
+ abortSignal: generationController.signal, // Per-message cancel support
430
497
  // Use onPartialReply for real-time streaming (character-level)
431
498
  // onPartialReply receives cumulative text, so we need to calculate delta
432
499
  onPartialReply: async (payload: { text?: string }) => {
@@ -556,6 +623,25 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
556
623
 
557
624
  log?.info(`[adp-openclaw] dispatchReplyWithBufferedBlockDispatcher returned (finalSent=${finalSent}, chunkIndex=${chunkIndex})`);
558
625
 
626
+ // Clean up active generation tracking
627
+ activeGenerations.delete(convId);
628
+
629
+ // If generation was cancelled (aborted), send outbound_end with partial text
630
+ if (generationController.signal.aborted && !finalSent) {
631
+ const cancelText = lastPartialText ? `${lastPartialText}\n\n[已停止生成]` : "[已停止生成]";
632
+ log?.info(`[adp-openclaw] Generation cancelled, sending outbound_end with partial text`);
633
+ sendOutboundEnd(cancelText);
634
+
635
+ // Mark the session as aborted so the SDK injects an "abort hint"
636
+ // on the next message, preventing the AI from resuming the cancelled task
637
+ await markSessionAborted({
638
+ sessionKey: route.sessionKey,
639
+ runtime,
640
+ cfg,
641
+ log,
642
+ });
643
+ }
644
+
559
645
  // IMPORTANT: After dispatchReplyWithBufferedBlockDispatcher completes,
560
646
  // ensure outbound_end is sent even if "final" deliver was not called.
561
647
  // This handles cases where the SDK only sends blocks without a final callback.
@@ -566,6 +652,8 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
566
652
  sendOutboundEnd(finalText);
567
653
  }
568
654
  } catch (err) {
655
+ // Clean up on error
656
+ activeGenerations.delete(convIdForCleanup);
569
657
  log?.error(`[adp-openclaw] Failed to process message: ${err}`);
570
658
  }
571
659
  break;
@@ -582,6 +670,30 @@ async function connectAndHandle(params: ConnectParams): Promise<void> {
582
670
  break;
583
671
  }
584
672
 
673
+ // Handle cancel generation request from server (user clicked "stop generating")
674
+ case MsgType.Cancel: {
675
+ if (!authenticated) break;
676
+ const cancelPayload = msg.payload as { conversationId?: string; streamId?: string };
677
+ const cancelConvId = cancelPayload.conversationId || cancelPayload.streamId;
678
+ log?.info(`[adp-openclaw] Received cancel request for conv=${cancelConvId}`);
679
+
680
+ if (cancelConvId && activeGenerations.has(cancelConvId)) {
681
+ activeGenerations.get(cancelConvId)!.abort();
682
+ log?.info(`[adp-openclaw] Generation cancelled for conv=${cancelConvId}`);
683
+ } else {
684
+ // If no specific convId, cancel all active generations
685
+ if (!cancelConvId && activeGenerations.size > 0) {
686
+ for (const [id, controller] of activeGenerations) {
687
+ controller.abort();
688
+ log?.info(`[adp-openclaw] Generation cancelled for conv=${id} (cancel-all)`);
689
+ }
690
+ } else {
691
+ log?.warn(`[adp-openclaw] No active generation found for conv=${cancelConvId}`);
692
+ }
693
+ }
694
+ break;
695
+ }
696
+
585
697
  // Handle fetch OpenClaw chat history request from GoServer
586
698
  case MsgType.ConvHistory: {
587
699
  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
  });