@efengx/openclaw-channel-dragon 0.5.30 → 0.5.33
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/dist/components/channel/ChannelComponent.d.ts +8 -0
- package/dist/components/channel/ChannelComponent.js +43 -3
- package/dist/components/sync/SseComponent.d.ts +14 -1
- package/dist/components/sync/SseComponent.js +172 -20
- package/dist/index.js +123 -53
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -16,6 +16,8 @@ export declare class ChannelComponent implements IComponent {
|
|
|
16
16
|
start(): Promise<void>;
|
|
17
17
|
stop(): Promise<void>;
|
|
18
18
|
private normalizeSessionSegment;
|
|
19
|
+
private encodeWorkbenchTarget;
|
|
20
|
+
private decodeWorkbenchTarget;
|
|
19
21
|
private resolveOpenClawAgentId;
|
|
20
22
|
private buildOpenClawSessionKey;
|
|
21
23
|
private resolveWorkbenchSessionIdFromOpenClawSessionKey;
|
|
@@ -27,6 +29,12 @@ export declare class ChannelComponent implements IComponent {
|
|
|
27
29
|
handleOutboundText: (ctx: any) => Promise<{
|
|
28
30
|
ok: boolean;
|
|
29
31
|
messageId: string;
|
|
32
|
+
error: any;
|
|
33
|
+
} | {
|
|
34
|
+
ok: boolean;
|
|
35
|
+
messageId: string;
|
|
36
|
+
error?: undefined;
|
|
30
37
|
}>;
|
|
38
|
+
reportTargetProtocolError: (message: string, rawTarget: unknown) => Promise<void>;
|
|
31
39
|
handleAgentEvent: (evt: any) => Promise<void>;
|
|
32
40
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { dragonChannelPluginVersion } from "../../version.js";
|
|
2
2
|
const channelId = "dragon";
|
|
3
|
+
const workbenchTargetPrefix = "dragon-workbench:";
|
|
3
4
|
export class ChannelComponent {
|
|
4
5
|
options;
|
|
5
6
|
telemetry;
|
|
@@ -20,6 +21,21 @@ export class ChannelComponent {
|
|
|
20
21
|
.slice(0, 64);
|
|
21
22
|
return normalized || fallback;
|
|
22
23
|
}
|
|
24
|
+
encodeWorkbenchTarget(sessionId) {
|
|
25
|
+
const value = String(sessionId || 'default').trim() || 'default';
|
|
26
|
+
return `${workbenchTargetPrefix}${value}`;
|
|
27
|
+
}
|
|
28
|
+
decodeWorkbenchTarget(value) {
|
|
29
|
+
const target = String(value || '').trim();
|
|
30
|
+
if (!target.startsWith(workbenchTargetPrefix)) {
|
|
31
|
+
throw new Error(`Invalid dragon workbench target "${target || '<empty>'}"; expected "${workbenchTargetPrefix}<sessionId>"`);
|
|
32
|
+
}
|
|
33
|
+
const sessionId = target.slice(workbenchTargetPrefix.length).trim();
|
|
34
|
+
if (!sessionId) {
|
|
35
|
+
throw new Error(`Invalid dragon workbench target "${target}"; sessionId is empty`);
|
|
36
|
+
}
|
|
37
|
+
return sessionId;
|
|
38
|
+
}
|
|
23
39
|
resolveOpenClawAgentId() {
|
|
24
40
|
const agents = this.options.cfg?.agents?.list;
|
|
25
41
|
if (Array.isArray(agents)) {
|
|
@@ -136,7 +152,7 @@ export class ChannelComponent {
|
|
|
136
152
|
ctx: {
|
|
137
153
|
Body: content,
|
|
138
154
|
From: sessionId === 'default' ? "workbench-user" : `workbench-user-${sessionId}`,
|
|
139
|
-
To: sessionId
|
|
155
|
+
To: this.encodeWorkbenchTarget(sessionId),
|
|
140
156
|
ChatType: "direct",
|
|
141
157
|
Provider: channelId,
|
|
142
158
|
ChannelId: channelId,
|
|
@@ -355,10 +371,34 @@ export class ChannelComponent {
|
|
|
355
371
|
handleOutboundText = async (ctx) => {
|
|
356
372
|
const text = ctx?.text || "";
|
|
357
373
|
const { logger } = this.options;
|
|
358
|
-
|
|
359
|
-
|
|
374
|
+
let sessionId;
|
|
375
|
+
try {
|
|
376
|
+
sessionId = this.decodeWorkbenchTarget(ctx?.peer?.id);
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
const message = error?.message || String(error);
|
|
380
|
+
logger?.error?.(`[dragon channels][Dragon Plugin] outbound target protocol error: ${message}`);
|
|
381
|
+
await this.reportTargetProtocolError(message, ctx?.peer?.id);
|
|
382
|
+
return { ok: false, messageId: Date.now().toString(), error: message };
|
|
383
|
+
}
|
|
384
|
+
logger?.info?.(`[dragon channels][Dragon Plugin] Outbound Text (Action): "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}" [Session: ${sessionId}] [RawPeer: ${ctx?.peer?.id || 'none'}]`);
|
|
385
|
+
await this.telemetry.reportReply({ content: text, sessionId, source: "channel_outbound" });
|
|
360
386
|
return { ok: true, messageId: Date.now().toString() };
|
|
361
387
|
};
|
|
388
|
+
reportTargetProtocolError = async (message, rawTarget) => {
|
|
389
|
+
const content = [
|
|
390
|
+
'⚠️ Dragon channel target 协议错误。',
|
|
391
|
+
`错误信息:${message}`,
|
|
392
|
+
`收到的 target:${String(rawTarget || '<empty>')}`,
|
|
393
|
+
`期望格式:${workbenchTargetPrefix}<sessionId>`,
|
|
394
|
+
].join('\n');
|
|
395
|
+
await this.telemetry.reportReply({
|
|
396
|
+
content,
|
|
397
|
+
sessionId: 'default',
|
|
398
|
+
source: 'target_protocol_error',
|
|
399
|
+
msgId: `dragon_target_error_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
400
|
+
});
|
|
401
|
+
};
|
|
362
402
|
handleAgentEvent = async (evt) => {
|
|
363
403
|
const sessionId = evt?.sessionId ||
|
|
364
404
|
this.resolveWorkbenchSessionIdFromOpenClawSessionKey(evt?.sessionKey);
|
|
@@ -5,17 +5,30 @@ export declare class SseComponent implements IComponent {
|
|
|
5
5
|
private http;
|
|
6
6
|
private channel;
|
|
7
7
|
private options;
|
|
8
|
-
private eventSource;
|
|
9
8
|
private shouldReconnect;
|
|
10
9
|
private reconnectTimer;
|
|
10
|
+
private idleTimer;
|
|
11
|
+
private fetchAbort;
|
|
12
|
+
private reader;
|
|
13
|
+
private reconnectAttempts;
|
|
14
|
+
private connectionSeq;
|
|
15
|
+
private lastTransportActivityAt;
|
|
11
16
|
constructor(http: HttpComponent, channel: ChannelComponent, options: {
|
|
12
17
|
agentId: string;
|
|
18
|
+
accountId: string;
|
|
13
19
|
version: string;
|
|
14
20
|
logger?: any;
|
|
21
|
+
abortSignal?: AbortSignal;
|
|
22
|
+
setStatus?: (next: Record<string, unknown>) => void;
|
|
23
|
+
reconnectMs?: number;
|
|
24
|
+
idleTimeoutMs?: number;
|
|
15
25
|
});
|
|
16
26
|
start(): Promise<void>;
|
|
17
27
|
stop(): Promise<void>;
|
|
18
28
|
private connect;
|
|
29
|
+
private touchTransport;
|
|
30
|
+
private armIdleWatchdog;
|
|
31
|
+
private markDisconnected;
|
|
19
32
|
private startSseReader;
|
|
20
33
|
private handleEvent;
|
|
21
34
|
private scheduleReconnect;
|
|
@@ -2,44 +2,123 @@ export class SseComponent {
|
|
|
2
2
|
http;
|
|
3
3
|
channel;
|
|
4
4
|
options;
|
|
5
|
-
eventSource;
|
|
6
5
|
shouldReconnect = true;
|
|
7
6
|
reconnectTimer;
|
|
7
|
+
idleTimer;
|
|
8
|
+
fetchAbort;
|
|
9
|
+
reader;
|
|
10
|
+
reconnectAttempts = 0;
|
|
11
|
+
connectionSeq = 0;
|
|
12
|
+
lastTransportActivityAt = 0;
|
|
8
13
|
constructor(http, channel, options) {
|
|
9
14
|
this.http = http;
|
|
10
15
|
this.channel = channel;
|
|
11
16
|
this.options = options;
|
|
12
17
|
}
|
|
13
18
|
async start() {
|
|
19
|
+
this.options.logger?.info?.(`[dragon channels][SSE] component start: version=${this.options.version}, agent=${this.options.agentId}, account=${this.options.accountId}, baseUrl=${this.http.options.baseURL}`);
|
|
20
|
+
this.options.setStatus?.({
|
|
21
|
+
accountId: this.options.accountId,
|
|
22
|
+
connected: false,
|
|
23
|
+
lastError: null,
|
|
24
|
+
});
|
|
25
|
+
this.options.abortSignal?.addEventListener('abort', () => {
|
|
26
|
+
this.options.logger?.info?.(`[dragon channels][SSE] upstream abort received: agent=${this.options.agentId}, account=${this.options.accountId}`);
|
|
27
|
+
void this.stop();
|
|
28
|
+
}, { once: true });
|
|
14
29
|
this.connect();
|
|
15
30
|
}
|
|
16
31
|
async stop() {
|
|
17
32
|
this.shouldReconnect = false;
|
|
18
33
|
if (this.reconnectTimer)
|
|
19
34
|
clearTimeout(this.reconnectTimer);
|
|
20
|
-
this.
|
|
35
|
+
this.reconnectTimer = undefined;
|
|
36
|
+
if (this.idleTimer)
|
|
37
|
+
clearTimeout(this.idleTimer);
|
|
38
|
+
this.idleTimer = undefined;
|
|
39
|
+
this.fetchAbort?.abort();
|
|
40
|
+
this.fetchAbort = undefined;
|
|
41
|
+
try {
|
|
42
|
+
await this.reader?.cancel?.();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Reader cancellation can race with fetch abort.
|
|
46
|
+
}
|
|
47
|
+
this.reader = undefined;
|
|
48
|
+
this.options.setStatus?.({
|
|
49
|
+
accountId: this.options.accountId,
|
|
50
|
+
connected: false,
|
|
51
|
+
lastStopAt: Date.now(),
|
|
52
|
+
});
|
|
53
|
+
this.options.logger?.info?.(`[dragon channels][SSE] component stopped: agent=${this.options.agentId}, account=${this.options.accountId}`);
|
|
21
54
|
}
|
|
22
55
|
connect() {
|
|
23
56
|
if (!this.shouldReconnect)
|
|
24
57
|
return;
|
|
25
58
|
const { agentId, logger } = this.options;
|
|
26
|
-
const
|
|
27
|
-
|
|
59
|
+
const params = new URLSearchParams({
|
|
60
|
+
client: 'dragon-channel',
|
|
61
|
+
agentId,
|
|
62
|
+
accountId: this.options.accountId,
|
|
63
|
+
version: this.options.version,
|
|
64
|
+
});
|
|
65
|
+
const url = `${this.http.options.baseURL}/api/agents/events?${params.toString()}`;
|
|
66
|
+
const connectionId = `${Date.now()}-${++this.connectionSeq}`;
|
|
67
|
+
logger?.info?.(`[dragon channels][SSE] connect start: connection=${connectionId}, attempt=${this.reconnectAttempts + 1}, version=${this.options.version}, url=${url}, agent=${agentId}, account=${this.options.accountId}`);
|
|
28
68
|
try {
|
|
29
|
-
|
|
30
|
-
// but for simplicity we'll use the same pattern as Bridge if possible.
|
|
31
|
-
// Actually, standard EventSource is not in Node globals yet.
|
|
32
|
-
// We'll use a simple fetch-based stream reader.
|
|
33
|
-
this.startSseReader(url);
|
|
69
|
+
void this.startSseReader(url, connectionId);
|
|
34
70
|
}
|
|
35
71
|
catch (err) {
|
|
36
|
-
logger?.error?.(`[dragon channels][
|
|
37
|
-
this.
|
|
72
|
+
logger?.error?.(`[dragon channels][SSE] connect init failed: connection=${connectionId}, error=${err.message}`);
|
|
73
|
+
this.markDisconnected(err?.message || String(err));
|
|
74
|
+
this.scheduleReconnect('init_failed');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
touchTransport(connectionId, reason) {
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
this.lastTransportActivityAt = now;
|
|
80
|
+
this.options.setStatus?.({
|
|
81
|
+
accountId: this.options.accountId,
|
|
82
|
+
connected: true,
|
|
83
|
+
lastEventAt: now,
|
|
84
|
+
lastTransportActivityAt: now,
|
|
85
|
+
lastError: null,
|
|
86
|
+
});
|
|
87
|
+
this.options.logger?.debug?.(`[dragon channels][SSE] transport activity: connection=${connectionId}, reason=${reason}, at=${now}`);
|
|
88
|
+
this.armIdleWatchdog(connectionId);
|
|
89
|
+
}
|
|
90
|
+
armIdleWatchdog(connectionId) {
|
|
91
|
+
if (this.idleTimer)
|
|
92
|
+
clearTimeout(this.idleTimer);
|
|
93
|
+
const timeoutMs = this.options.idleTimeoutMs ?? 90_000;
|
|
94
|
+
this.idleTimer = setTimeout(() => {
|
|
95
|
+
if (!this.shouldReconnect)
|
|
96
|
+
return;
|
|
97
|
+
const idleMs = Date.now() - this.lastTransportActivityAt;
|
|
98
|
+
this.options.logger?.warn?.(`[dragon channels][SSE] idle timeout: connection=${connectionId}, idleMs=${idleMs}, timeoutMs=${timeoutMs}; aborting fetch for reconnect`);
|
|
99
|
+
this.markDisconnected(`SSE idle timeout after ${idleMs}ms`);
|
|
100
|
+
this.fetchAbort?.abort();
|
|
101
|
+
}, timeoutMs);
|
|
102
|
+
if (typeof this.idleTimer === 'object' && 'unref' in this.idleTimer) {
|
|
103
|
+
this.idleTimer.unref();
|
|
38
104
|
}
|
|
39
105
|
}
|
|
40
|
-
|
|
106
|
+
markDisconnected(error) {
|
|
107
|
+
this.options.setStatus?.({
|
|
108
|
+
accountId: this.options.accountId,
|
|
109
|
+
connected: false,
|
|
110
|
+
lastError: error,
|
|
111
|
+
lastTransportActivityAt: this.lastTransportActivityAt || null,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
async startSseReader(url, connectionId) {
|
|
115
|
+
const abort = new AbortController();
|
|
116
|
+
this.fetchAbort = abort;
|
|
117
|
+
const upstreamAbort = () => abort.abort();
|
|
118
|
+
this.options.abortSignal?.addEventListener('abort', upstreamAbort, { once: true });
|
|
41
119
|
try {
|
|
42
120
|
const response = await fetch(url, {
|
|
121
|
+
signal: abort.signal,
|
|
43
122
|
headers: {
|
|
44
123
|
'Accept': 'text/event-stream',
|
|
45
124
|
'Authorization': `Bearer ${this.http.options.authToken || ''}`
|
|
@@ -51,38 +130,87 @@ export class SseComponent {
|
|
|
51
130
|
const reader = response.body?.getReader();
|
|
52
131
|
if (!reader)
|
|
53
132
|
throw new Error("Response body is null");
|
|
54
|
-
this.
|
|
133
|
+
this.reader = reader;
|
|
134
|
+
this.reconnectAttempts = 0;
|
|
135
|
+
const now = Date.now();
|
|
136
|
+
this.lastTransportActivityAt = now;
|
|
137
|
+
this.options.setStatus?.({
|
|
138
|
+
accountId: this.options.accountId,
|
|
139
|
+
connected: true,
|
|
140
|
+
lastConnectedAt: now,
|
|
141
|
+
lastTransportActivityAt: now,
|
|
142
|
+
lastError: null,
|
|
143
|
+
reconnectAttempts: 0,
|
|
144
|
+
});
|
|
145
|
+
this.options.logger?.info?.(`[dragon channels][SSE] stream established: connection=${connectionId}, agent=${this.options.agentId}, account=${this.options.accountId}`);
|
|
146
|
+
this.armIdleWatchdog(connectionId);
|
|
55
147
|
const decoder = new TextDecoder();
|
|
56
148
|
let buffer = "";
|
|
57
149
|
while (this.shouldReconnect) {
|
|
58
150
|
const { value, done } = await reader.read();
|
|
59
|
-
if (done)
|
|
151
|
+
if (done) {
|
|
152
|
+
this.options.logger?.warn?.(`[dragon channels][SSE] stream ended by remote: connection=${connectionId}`);
|
|
60
153
|
break;
|
|
154
|
+
}
|
|
155
|
+
this.touchTransport(connectionId, 'chunk');
|
|
61
156
|
buffer += decoder.decode(value, { stream: true });
|
|
62
157
|
const lines = buffer.split("\n");
|
|
63
158
|
buffer = lines.pop() || "";
|
|
64
159
|
for (const line of lines) {
|
|
65
160
|
const cleanLine = line.trim();
|
|
161
|
+
if (!cleanLine || cleanLine.startsWith(':')) {
|
|
162
|
+
this.touchTransport(connectionId, cleanLine.startsWith(':') ? 'heartbeat' : 'blank_line');
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
66
165
|
if (cleanLine.startsWith("data: ")) {
|
|
67
166
|
try {
|
|
68
167
|
const data = JSON.parse(cleanLine.slice(6));
|
|
168
|
+
this.options.logger?.debug?.(`[dragon channels][SSE] event received: connection=${connectionId}, type=${data?.type || 'unknown'}, agent=${data?.agentId || 'unknown'}`);
|
|
69
169
|
void this.handleEvent(data).catch((err) => {
|
|
70
|
-
this.options.logger?.error?.(`[dragon channels][
|
|
170
|
+
this.options.logger?.error?.(`[dragon channels][SSE] event handling failed: connection=${connectionId}, error=${err?.message || err}`);
|
|
71
171
|
});
|
|
72
172
|
}
|
|
73
173
|
catch (e) {
|
|
74
|
-
this.options.logger?.error?.(`[dragon channels][
|
|
174
|
+
this.options.logger?.error?.(`[dragon channels][SSE] event JSON parse failed: connection=${connectionId}, line="${cleanLine}", error=${e?.message || e}`);
|
|
75
175
|
}
|
|
76
176
|
}
|
|
77
177
|
}
|
|
78
178
|
}
|
|
179
|
+
if (this.shouldReconnect) {
|
|
180
|
+
const message = 'SSE stream ended';
|
|
181
|
+
this.markDisconnected(message);
|
|
182
|
+
this.scheduleReconnect(message);
|
|
183
|
+
}
|
|
79
184
|
}
|
|
80
185
|
catch (err) {
|
|
81
186
|
if (this.shouldReconnect) {
|
|
82
|
-
|
|
83
|
-
|
|
187
|
+
const message = err?.name === 'AbortError'
|
|
188
|
+
? 'SSE fetch aborted'
|
|
189
|
+
: (err?.message || String(err));
|
|
190
|
+
this.options.logger?.warn?.(`[dragon channels][SSE] stream disconnected: connection=${connectionId}, error=${message}`);
|
|
191
|
+
this.markDisconnected(message);
|
|
192
|
+
this.scheduleReconnect(message);
|
|
84
193
|
}
|
|
85
194
|
}
|
|
195
|
+
finally {
|
|
196
|
+
this.options.abortSignal?.removeEventListener('abort', upstreamAbort);
|
|
197
|
+
if (this.reader) {
|
|
198
|
+
try {
|
|
199
|
+
await this.reader.cancel?.();
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// Ignore cancellation races.
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (this.fetchAbort === abort) {
|
|
206
|
+
this.fetchAbort = undefined;
|
|
207
|
+
}
|
|
208
|
+
if (this.idleTimer) {
|
|
209
|
+
clearTimeout(this.idleTimer);
|
|
210
|
+
this.idleTimer = undefined;
|
|
211
|
+
}
|
|
212
|
+
this.reader = undefined;
|
|
213
|
+
}
|
|
86
214
|
}
|
|
87
215
|
async handleEvent(data) {
|
|
88
216
|
const { agentId, logger } = this.options;
|
|
@@ -101,9 +229,33 @@ export class SseComponent {
|
|
|
101
229
|
// Handle history sync if needed, but deliverToOpenClaw usually handles normal chat
|
|
102
230
|
}
|
|
103
231
|
}
|
|
104
|
-
scheduleReconnect() {
|
|
232
|
+
scheduleReconnect(reason) {
|
|
233
|
+
if (!this.shouldReconnect)
|
|
234
|
+
return;
|
|
105
235
|
if (this.reconnectTimer)
|
|
106
236
|
clearTimeout(this.reconnectTimer);
|
|
107
|
-
this.
|
|
237
|
+
this.reconnectAttempts += 1;
|
|
238
|
+
const baseMs = this.options.reconnectMs ?? 5_000;
|
|
239
|
+
const delayMs = Math.min(60_000, baseMs * Math.max(1, this.reconnectAttempts));
|
|
240
|
+
this.options.setStatus?.({
|
|
241
|
+
accountId: this.options.accountId,
|
|
242
|
+
connected: false,
|
|
243
|
+
restartPending: true,
|
|
244
|
+
reconnectAttempts: this.reconnectAttempts,
|
|
245
|
+
lastError: reason,
|
|
246
|
+
});
|
|
247
|
+
this.options.logger?.info?.(`[dragon channels][SSE] reconnect scheduled: attempt=${this.reconnectAttempts}, delayMs=${delayMs}, reason=${reason}, agent=${this.options.agentId}, account=${this.options.accountId}`);
|
|
248
|
+
this.reconnectTimer = setTimeout(() => {
|
|
249
|
+
this.reconnectTimer = undefined;
|
|
250
|
+
this.options.setStatus?.({
|
|
251
|
+
accountId: this.options.accountId,
|
|
252
|
+
restartPending: false,
|
|
253
|
+
reconnectAttempts: this.reconnectAttempts,
|
|
254
|
+
});
|
|
255
|
+
this.connect();
|
|
256
|
+
}, delayMs);
|
|
257
|
+
if (typeof this.reconnectTimer === 'object' && 'unref' in this.reconnectTimer) {
|
|
258
|
+
this.reconnectTimer.unref();
|
|
259
|
+
}
|
|
108
260
|
}
|
|
109
261
|
}
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { ChannelComponent } from "./components/channel/ChannelComponent.js";
|
|
|
8
8
|
import { SseComponent } from "./components/sync/SseComponent.js";
|
|
9
9
|
import { dragonChannelPluginVersion } from "./version.js";
|
|
10
10
|
const channelId = "dragon";
|
|
11
|
+
const workbenchTargetPrefix = "dragon-workbench:";
|
|
11
12
|
let cachedRuntime;
|
|
12
13
|
const containers = new Map();
|
|
13
14
|
function isDragonSessionKey(sessionKey) {
|
|
@@ -22,10 +23,31 @@ function resolveAccountIdFromSessionKey(sessionKey) {
|
|
|
22
23
|
}
|
|
23
24
|
return "default";
|
|
24
25
|
}
|
|
26
|
+
function encodeWorkbenchTarget(sessionId) {
|
|
27
|
+
const value = String(sessionId || "default").trim() || "default";
|
|
28
|
+
return `${workbenchTargetPrefix}${value}`;
|
|
29
|
+
}
|
|
30
|
+
function decodeWorkbenchTarget(raw) {
|
|
31
|
+
const target = String(raw || "").trim();
|
|
32
|
+
if (!target.startsWith(workbenchTargetPrefix)) {
|
|
33
|
+
throw new Error(`Invalid dragon workbench target "${target || "<empty>"}"; expected "${workbenchTargetPrefix}<sessionId>"`);
|
|
34
|
+
}
|
|
35
|
+
const sessionId = target.slice(workbenchTargetPrefix.length).trim();
|
|
36
|
+
if (!sessionId) {
|
|
37
|
+
throw new Error(`Invalid dragon workbench target "${target}"; sessionId is empty`);
|
|
38
|
+
}
|
|
39
|
+
return sessionId;
|
|
40
|
+
}
|
|
41
|
+
function containerKey(account) {
|
|
42
|
+
return `${account.accountId}:${account.agentId}:${account.orchestratorUrl}`;
|
|
43
|
+
}
|
|
25
44
|
async function getOrCreateContainer(account, ctx) {
|
|
26
|
-
const key =
|
|
27
|
-
if (containers.has(key))
|
|
45
|
+
const key = containerKey(account);
|
|
46
|
+
if (containers.has(key)) {
|
|
47
|
+
const logger = ctx?.logger ?? ctx?.log ?? cachedRuntime?.logger ?? cachedRuntime?.log;
|
|
48
|
+
logger?.debug?.(`[dragon channels][Dragon Plugin] Reusing channel container: key=${key}, agent=${account.agentId}, account=${account.accountId}`);
|
|
28
49
|
return containers.get(key);
|
|
50
|
+
}
|
|
29
51
|
const logger = ctx?.logger ?? ctx?.log ?? cachedRuntime?.logger ?? cachedRuntime?.log;
|
|
30
52
|
const container = new ServiceContainer();
|
|
31
53
|
logger?.info?.(`[dragon channels][Dragon Plugin] Starting channel plugin v${dragonChannelPluginVersion} for agent=${account.agentId}, account=${account.accountId}, orchestrator=${account.orchestratorUrl}`);
|
|
@@ -51,13 +73,43 @@ async function getOrCreateContainer(account, ctx) {
|
|
|
51
73
|
// 3. Sync Infrastructure
|
|
52
74
|
container.register('sse', new SseComponent(http, channel, {
|
|
53
75
|
agentId: account.agentId,
|
|
76
|
+
accountId: account.accountId,
|
|
54
77
|
version: dragonChannelPluginVersion,
|
|
55
|
-
logger
|
|
78
|
+
logger,
|
|
79
|
+
abortSignal: ctx.abortSignal,
|
|
80
|
+
setStatus: ctx.setStatus,
|
|
81
|
+
reconnectMs: 5_000,
|
|
82
|
+
idleTimeoutMs: 90_000,
|
|
56
83
|
}));
|
|
57
84
|
await container.startAll();
|
|
58
85
|
containers.set(key, container);
|
|
86
|
+
logger?.info?.(`[dragon channels][Dragon Plugin] Channel container started: key=${key}, agent=${account.agentId}, account=${account.accountId}`);
|
|
59
87
|
return container;
|
|
60
88
|
}
|
|
89
|
+
async function stopContainer(account, ctx) {
|
|
90
|
+
const key = containerKey(account);
|
|
91
|
+
const logger = ctx?.logger ?? ctx?.log ?? cachedRuntime?.logger ?? cachedRuntime?.log;
|
|
92
|
+
const container = containers.get(key);
|
|
93
|
+
if (!container) {
|
|
94
|
+
logger?.debug?.(`[dragon channels][Dragon Plugin] No channel container to stop: key=${key}, agent=${account.agentId}, account=${account.accountId}`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
logger?.info?.(`[dragon channels][Dragon Plugin] Stopping channel container: key=${key}, agent=${account.agentId}, account=${account.accountId}`);
|
|
98
|
+
try {
|
|
99
|
+
await container.stopAll();
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
containers.delete(key);
|
|
103
|
+
ctx?.setStatus?.({
|
|
104
|
+
accountId: account.accountId,
|
|
105
|
+
connected: false,
|
|
106
|
+
running: false,
|
|
107
|
+
restartPending: false,
|
|
108
|
+
lastStopAt: Date.now(),
|
|
109
|
+
});
|
|
110
|
+
logger?.info?.(`[dragon channels][Dragon Plugin] Channel container stopped: key=${key}, agent=${account.agentId}, account=${account.accountId}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
61
113
|
const base = createChannelPluginBase({
|
|
62
114
|
id: channelId,
|
|
63
115
|
meta: {
|
|
@@ -80,7 +132,7 @@ const base = createChannelPluginBase({
|
|
|
80
132
|
return {
|
|
81
133
|
accountId,
|
|
82
134
|
agentId: accountConfig.agentId || accountId,
|
|
83
|
-
orchestratorUrl: accountConfig.orchestratorUrl
|
|
135
|
+
orchestratorUrl: accountConfig.orchestratorUrl,
|
|
84
136
|
orchestratorAuthToken: accountConfig.orchestratorAuthToken || accountConfig.authToken,
|
|
85
137
|
};
|
|
86
138
|
},
|
|
@@ -93,35 +145,45 @@ const plugin = createChatChannelPlugin({
|
|
|
93
145
|
gateway: {
|
|
94
146
|
startAccount: async (ctx) => {
|
|
95
147
|
const account = base.config.resolveAccount(ctx.cfg, ctx.accountId);
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
148
|
+
const logger = ctx?.logger ?? ctx?.log ?? cachedRuntime?.logger ?? cachedRuntime?.log;
|
|
149
|
+
logger?.info?.(`[dragon channels][Dragon Plugin] startAccount begin: agent=${account.agentId}, account=${account.accountId}, orchestrator=${account.orchestratorUrl}`);
|
|
150
|
+
ctx.setStatus?.({
|
|
151
|
+
accountId: account.accountId,
|
|
152
|
+
enabled: true,
|
|
153
|
+
configured: true,
|
|
154
|
+
running: true,
|
|
155
|
+
connected: false,
|
|
156
|
+
lastStartAt: Date.now(),
|
|
157
|
+
lastError: null,
|
|
102
158
|
});
|
|
159
|
+
try {
|
|
160
|
+
await getOrCreateContainer(account, ctx);
|
|
161
|
+
if (ctx.abortSignal.aborted)
|
|
162
|
+
return;
|
|
163
|
+
await new Promise((resolve) => {
|
|
164
|
+
ctx.abortSignal.addEventListener('abort', resolve, { once: true });
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
logger?.info?.(`[dragon channels][Dragon Plugin] startAccount exiting: agent=${account.agentId}, account=${account.accountId}, aborted=${ctx.abortSignal.aborted}`);
|
|
169
|
+
await stopContainer(account, ctx);
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
stopAccount: async (ctx) => {
|
|
173
|
+
const account = base.config.resolveAccount(ctx.cfg, ctx.accountId);
|
|
174
|
+
const logger = ctx?.logger ?? ctx?.log ?? cachedRuntime?.logger ?? cachedRuntime?.log;
|
|
175
|
+
logger?.info?.(`[dragon channels][Dragon Plugin] stopAccount requested: agent=${account.agentId}, account=${account.accountId}`);
|
|
176
|
+
await stopContainer(account, ctx);
|
|
103
177
|
}
|
|
104
178
|
},
|
|
105
179
|
messaging: {
|
|
106
|
-
targetPrefixes: [
|
|
180
|
+
targetPrefixes: [workbenchTargetPrefix],
|
|
107
181
|
normalizeTarget: (raw) => {
|
|
108
|
-
|
|
109
|
-
for (const prefix of ["dragon-workbench-", "dragon-workbench:", "dragon-", "dragon:"]) {
|
|
110
|
-
if (target.startsWith(prefix)) {
|
|
111
|
-
target = target.substring(prefix.length);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return target ? `dragon-workbench-${target}` : "";
|
|
182
|
+
return encodeWorkbenchTarget(decodeWorkbenchTarget(raw));
|
|
115
183
|
},
|
|
116
184
|
resolveOutboundSessionRoute: (params) => {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (target.startsWith(prefix)) {
|
|
120
|
-
target = target.substring(prefix.length);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
if (!target)
|
|
124
|
-
return null;
|
|
185
|
+
const sessionId = decodeWorkbenchTarget(params.target);
|
|
186
|
+
const target = encodeWorkbenchTarget(sessionId);
|
|
125
187
|
return buildChannelOutboundSessionRoute({
|
|
126
188
|
cfg: params.cfg,
|
|
127
189
|
agentId: params.agentId,
|
|
@@ -129,27 +191,22 @@ const plugin = createChatChannelPlugin({
|
|
|
129
191
|
accountId: params.accountId,
|
|
130
192
|
peer: {
|
|
131
193
|
kind: "direct",
|
|
132
|
-
id:
|
|
194
|
+
id: sessionId
|
|
133
195
|
},
|
|
134
196
|
chatType: "direct",
|
|
135
|
-
from:
|
|
136
|
-
to:
|
|
197
|
+
from: target,
|
|
198
|
+
to: target
|
|
137
199
|
});
|
|
138
200
|
},
|
|
139
201
|
targetResolver: {
|
|
140
202
|
looksLikeId: (raw) => {
|
|
141
|
-
return raw.startsWith(
|
|
203
|
+
return raw.startsWith(workbenchTargetPrefix);
|
|
142
204
|
},
|
|
143
|
-
hint: "
|
|
205
|
+
hint: "dragon-workbench:<sessionId>",
|
|
144
206
|
resolveTarget: async ({ input }) => {
|
|
145
|
-
|
|
146
|
-
for (const prefix of ["dragon-workbench-", "dragon-workbench:", "dragon-", "dragon:"]) {
|
|
147
|
-
if (sessionId.startsWith(prefix)) {
|
|
148
|
-
sessionId = sessionId.substring(prefix.length);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
207
|
+
const sessionId = decodeWorkbenchTarget(input);
|
|
151
208
|
return {
|
|
152
|
-
to:
|
|
209
|
+
to: encodeWorkbenchTarget(sessionId),
|
|
153
210
|
kind: "direct",
|
|
154
211
|
display: `Dragon Workbench User (${sessionId})`,
|
|
155
212
|
source: "normalized"
|
|
@@ -169,15 +226,28 @@ const plugin = createChatChannelPlugin({
|
|
|
169
226
|
const account = base.config.resolveAccount(cfg, accountId);
|
|
170
227
|
const container = await getOrCreateContainer(account, ctx);
|
|
171
228
|
const channel = container.get('channel');
|
|
172
|
-
|
|
173
|
-
logger?.info?.(`[dragon channels][Dragon Plugin] handleAction target resolution input: ${
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
229
|
+
const rawTarget = params.to || params.target || ctx.toolContext?.currentChannelId || "default";
|
|
230
|
+
logger?.info?.(`[dragon channels][Dragon Plugin] handleAction target resolution input: ${rawTarget}`);
|
|
231
|
+
let sessionId;
|
|
232
|
+
let target;
|
|
233
|
+
try {
|
|
234
|
+
sessionId = decodeWorkbenchTarget(rawTarget);
|
|
235
|
+
target = encodeWorkbenchTarget(sessionId);
|
|
178
236
|
}
|
|
179
|
-
|
|
180
|
-
|
|
237
|
+
catch (error) {
|
|
238
|
+
const message = error?.message || String(error);
|
|
239
|
+
logger?.error?.(`[dragon channels][Dragon Plugin] handleAction target protocol error: ${message}`);
|
|
240
|
+
await channel.reportTargetProtocolError(message, rawTarget);
|
|
241
|
+
return {
|
|
242
|
+
ok: false,
|
|
243
|
+
error: message,
|
|
244
|
+
didSendViaMessagingTool: false,
|
|
245
|
+
messagingToolSentTexts: [],
|
|
246
|
+
messagingToolSentMediaUrls: [],
|
|
247
|
+
messagingToolSentTargets: [],
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
logger?.info?.(`[dragon channels][Dragon Plugin] handleAction target resolved: sessionId=${sessionId}, target=${target}, rawTarget=${rawTarget}`);
|
|
181
251
|
// Collect ALL media URLs from the params (mirrors what OpenClaw tracks internally
|
|
182
252
|
// via collectMessagingMediaUrlsFromRecord when the tool call starts, so that
|
|
183
253
|
// hasGatewayAgentDeliveredExpectedMedia can match them against expectedMediaUrls).
|
|
@@ -224,7 +294,7 @@ const plugin = createChatChannelPlugin({
|
|
|
224
294
|
logger?.info?.(`[dragon channels][Dragon Plugin] handleAction sending text over channel: text_len=${text.length}, targetSession=${sessionId}`);
|
|
225
295
|
const result = await channel.handleOutboundText({
|
|
226
296
|
text,
|
|
227
|
-
peer: { id:
|
|
297
|
+
peer: { id: target }
|
|
228
298
|
});
|
|
229
299
|
logger?.info?.(`[dragon channels][Dragon Plugin] handleAction channel send result: ok=${result.ok}, messageId=${result.messageId || "none"}`);
|
|
230
300
|
const evidence = {
|
|
@@ -272,12 +342,12 @@ const entry = defineChannelPluginEntry({
|
|
|
272
342
|
return;
|
|
273
343
|
const accountId = resolveAccountIdFromSessionKey(sessionKey);
|
|
274
344
|
const account = base.config.resolveAccount(api?.runtime?.cfg, accountId);
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
345
|
+
const logger = api?.runtime?.logger ?? api?.runtime?.log ?? cachedRuntime?.logger ?? cachedRuntime?.log;
|
|
346
|
+
const container = containers.get(containerKey(account));
|
|
347
|
+
if (!container) {
|
|
348
|
+
logger?.warn?.(`[dragon channels][Dragon Plugin] agent event skipped because managed container is not running: sessionKey=${sessionKey}, account=${account.accountId}, agent=${account.agentId}`);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
281
351
|
const channel = container.get('channel');
|
|
282
352
|
await channel.handleAgentEvent(evt);
|
|
283
353
|
});
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const dragonChannelPluginVersion = "0.5.
|
|
1
|
+
export declare const dragonChannelPluginVersion = "0.5.33";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const dragonChannelPluginVersion = "0.5.
|
|
1
|
+
export const dragonChannelPluginVersion = "0.5.33";
|