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 +1 -1
- package/src/adp-upload-tool.ts +1 -1
- package/src/monitor.ts +112 -0
- package/src/session-history.ts +1 -64
package/package.json
CHANGED
package/src/adp-upload-tool.ts
CHANGED
|
@@ -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;
|
package/src/session-history.ts
CHANGED
|
@@ -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(
|
|
596
|
+
resolve(stdout);
|
|
660
597
|
}
|
|
661
598
|
});
|
|
662
599
|
});
|