@efengx/openclaw-channel-dragon 0.5.35 → 0.5.37
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 +5 -2
- package/dist/components/channel/ChannelComponent.js +26 -31
- package/dist/components/media/MediaRegistryComponent.d.ts +35 -0
- package/dist/components/media/MediaRegistryComponent.js +141 -0
- package/dist/components/sync/SseComponent.d.ts +1 -0
- package/dist/components/sync/SseComponent.js +19 -16
- package/dist/components/telemetry/TelemetryComponent.d.ts +1 -0
- package/dist/components/telemetry/TelemetryComponent.js +1 -1
- package/dist/index.js +17 -45
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { IComponent } from "../../core/IComponent.js";
|
|
2
2
|
import { TelemetryComponent } from "../telemetry/TelemetryComponent.js";
|
|
3
|
+
import { MediaRegistryComponent } from "../media/MediaRegistryComponent.js";
|
|
3
4
|
export interface ChannelOptions {
|
|
4
5
|
accountId: string;
|
|
5
6
|
agentId: string;
|
|
@@ -10,9 +11,10 @@ export interface ChannelOptions {
|
|
|
10
11
|
export declare class ChannelComponent implements IComponent {
|
|
11
12
|
private options;
|
|
12
13
|
private telemetry;
|
|
14
|
+
private mediaRegistry;
|
|
13
15
|
private processedMessageIds;
|
|
14
16
|
private sessionQueues;
|
|
15
|
-
constructor(options: ChannelOptions, telemetry: TelemetryComponent);
|
|
17
|
+
constructor(options: ChannelOptions, telemetry: TelemetryComponent, mediaRegistry: MediaRegistryComponent);
|
|
16
18
|
start(): Promise<void>;
|
|
17
19
|
stop(): Promise<void>;
|
|
18
20
|
private normalizeSessionSegment;
|
|
@@ -23,16 +25,17 @@ export declare class ChannelComponent implements IComponent {
|
|
|
23
25
|
private resolveWorkbenchSessionIdFromOpenClawSessionKey;
|
|
24
26
|
private stringifyProgressDetail;
|
|
25
27
|
private findTaskCompletionEvent;
|
|
26
|
-
private extractGeneratedMediaLines;
|
|
27
28
|
private formatTaskCompletionReply;
|
|
28
29
|
deliverToOpenClaw: (content: string, sessionId?: string, modelId?: string, attachments?: any[], messageId?: string | number) => Promise<void>;
|
|
29
30
|
handleOutboundText: (ctx: any) => Promise<{
|
|
30
31
|
ok: boolean;
|
|
31
32
|
messageId: string;
|
|
32
33
|
error: any;
|
|
34
|
+
attachments?: undefined;
|
|
33
35
|
} | {
|
|
34
36
|
ok: boolean;
|
|
35
37
|
messageId: string;
|
|
38
|
+
attachments: import("../media/MediaRegistryComponent.js").DragonMediaAttachment[];
|
|
36
39
|
error?: undefined;
|
|
37
40
|
}>;
|
|
38
41
|
reportTargetProtocolError: (message: string, rawTarget: unknown) => Promise<void>;
|
|
@@ -4,11 +4,13 @@ const workbenchTargetPrefix = "dragon-workbench:";
|
|
|
4
4
|
export class ChannelComponent {
|
|
5
5
|
options;
|
|
6
6
|
telemetry;
|
|
7
|
+
mediaRegistry;
|
|
7
8
|
processedMessageIds = new Set();
|
|
8
9
|
sessionQueues = new Map();
|
|
9
|
-
constructor(options, telemetry) {
|
|
10
|
+
constructor(options, telemetry, mediaRegistry) {
|
|
10
11
|
this.options = options;
|
|
11
12
|
this.telemetry = telemetry;
|
|
13
|
+
this.mediaRegistry = mediaRegistry;
|
|
12
14
|
}
|
|
13
15
|
async start() { }
|
|
14
16
|
async stop() { }
|
|
@@ -96,29 +98,8 @@ export class ChannelComponent {
|
|
|
96
98
|
}
|
|
97
99
|
return undefined;
|
|
98
100
|
}
|
|
99
|
-
extractGeneratedMediaLines(event) {
|
|
100
|
-
const lines = new Set();
|
|
101
|
-
for (const url of Array.isArray(event?.mediaUrls) ? event.mediaUrls : []) {
|
|
102
|
-
if (typeof url === 'string' && url.trim()) {
|
|
103
|
-
lines.add(`MEDIA:${url.trim()}`);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
for (const attachment of Array.isArray(event?.attachments) ? event.attachments : []) {
|
|
107
|
-
const value = attachment?.url || attachment?.path || attachment?.filePath || attachment?.mediaUrl;
|
|
108
|
-
if (typeof value === 'string' && value.trim()) {
|
|
109
|
-
lines.add(`MEDIA:${value.trim()}`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return Array.from(lines);
|
|
113
|
-
}
|
|
114
101
|
formatTaskCompletionReply(event) {
|
|
115
|
-
|
|
116
|
-
const mediaLines = this.extractGeneratedMediaLines(event);
|
|
117
|
-
const lines = [
|
|
118
|
-
result,
|
|
119
|
-
...mediaLines.filter((line) => !result.includes(line)),
|
|
120
|
-
].filter((line) => line.trim());
|
|
121
|
-
return lines.join('\n');
|
|
102
|
+
return typeof event?.result === 'string' ? event.result.trim() : '';
|
|
122
103
|
}
|
|
123
104
|
deliverToOpenClaw = async (content, sessionId = 'default', modelId, attachments, messageId) => {
|
|
124
105
|
const replyMsgId = messageId ? `dragon_msg_${messageId}` : `dragon_msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -166,13 +147,15 @@ export class ChannelComponent {
|
|
|
166
147
|
dispatcherOptions: {
|
|
167
148
|
deliver: async (payload) => {
|
|
168
149
|
const text = payload?.text || "";
|
|
169
|
-
|
|
150
|
+
const archivedAttachments = await this.mediaRegistry.archiveStructuredMedia(payload);
|
|
151
|
+
if (!text && !payload?.tool_calls?.length && archivedAttachments.length === 0)
|
|
170
152
|
return;
|
|
171
153
|
await this.telemetry.reportReply({
|
|
172
154
|
content: text,
|
|
173
155
|
sessionId,
|
|
174
156
|
tool_calls: payload?.tool_calls,
|
|
175
157
|
reasoning_content: payload?.reasoning_content,
|
|
158
|
+
attachments: archivedAttachments.length > 0 ? archivedAttachments : payload?.attachments,
|
|
176
159
|
source: "telemetry_deliver",
|
|
177
160
|
msgId: replyMsgId,
|
|
178
161
|
});
|
|
@@ -218,12 +201,16 @@ export class ChannelComponent {
|
|
|
218
201
|
});
|
|
219
202
|
},
|
|
220
203
|
onToolResult: async (payload) => {
|
|
204
|
+
const archivedAttachments = await this.mediaRegistry.archiveStructuredMedia(payload);
|
|
221
205
|
await progress({
|
|
222
|
-
kind: 'tool',
|
|
206
|
+
kind: archivedAttachments.length > 0 ? 'media' : 'tool',
|
|
223
207
|
phase: 'end',
|
|
224
208
|
status: 'completed',
|
|
225
|
-
title: '工具执行完成',
|
|
226
|
-
detail:
|
|
209
|
+
title: archivedAttachments.length > 0 ? '媒体文件已生成' : '工具执行完成',
|
|
210
|
+
detail: archivedAttachments.length > 0
|
|
211
|
+
? archivedAttachments.map((item) => item.name).join(', ')
|
|
212
|
+
: this.stringifyProgressDetail(payload?.text || payload),
|
|
213
|
+
data: archivedAttachments.length > 0 ? { ...payload, attachments: archivedAttachments } : payload,
|
|
227
214
|
});
|
|
228
215
|
},
|
|
229
216
|
onItemEvent: async (payload) => {
|
|
@@ -371,6 +358,7 @@ export class ChannelComponent {
|
|
|
371
358
|
handleOutboundText = async (ctx) => {
|
|
372
359
|
const text = ctx?.text || "";
|
|
373
360
|
const { logger } = this.options;
|
|
361
|
+
const archivedAttachments = await this.mediaRegistry.archiveStructuredMedia(ctx);
|
|
374
362
|
let sessionId;
|
|
375
363
|
try {
|
|
376
364
|
sessionId = this.decodeWorkbenchTarget(ctx?.peer?.id);
|
|
@@ -382,8 +370,13 @@ export class ChannelComponent {
|
|
|
382
370
|
return { ok: false, messageId: Date.now().toString(), error: message };
|
|
383
371
|
}
|
|
384
372
|
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({
|
|
386
|
-
|
|
373
|
+
await this.telemetry.reportReply({
|
|
374
|
+
content: text,
|
|
375
|
+
sessionId,
|
|
376
|
+
attachments: archivedAttachments.length > 0 ? archivedAttachments : ctx?.attachments,
|
|
377
|
+
source: "channel_outbound",
|
|
378
|
+
});
|
|
379
|
+
return { ok: true, messageId: Date.now().toString(), attachments: archivedAttachments };
|
|
387
380
|
};
|
|
388
381
|
reportTargetProtocolError = async (message, rawTarget) => {
|
|
389
382
|
const content = [
|
|
@@ -407,7 +400,8 @@ export class ChannelComponent {
|
|
|
407
400
|
if (!taskCompletion)
|
|
408
401
|
return;
|
|
409
402
|
const content = this.formatTaskCompletionReply(taskCompletion);
|
|
410
|
-
|
|
403
|
+
const archivedAttachments = await this.mediaRegistry.archiveStructuredMedia(taskCompletion);
|
|
404
|
+
if (!content && archivedAttachments.length === 0)
|
|
411
405
|
return;
|
|
412
406
|
await this.telemetry.reportProgress({
|
|
413
407
|
sessionId,
|
|
@@ -417,11 +411,12 @@ export class ChannelComponent {
|
|
|
417
411
|
status: taskCompletion.status === 'ok' ? 'completed' : taskCompletion.status,
|
|
418
412
|
title: taskCompletion.status === 'ok' ? '后台任务完成' : '后台任务更新',
|
|
419
413
|
detail: this.stringifyProgressDetail(taskCompletion.taskLabel || taskCompletion.statusLabel),
|
|
420
|
-
data: taskCompletion,
|
|
414
|
+
data: archivedAttachments.length > 0 ? { ...taskCompletion, attachments: archivedAttachments } : taskCompletion,
|
|
421
415
|
});
|
|
422
416
|
await this.telemetry.reportReply({
|
|
423
417
|
content,
|
|
424
418
|
sessionId,
|
|
419
|
+
attachments: archivedAttachments.length > 0 ? archivedAttachments : taskCompletion?.attachments,
|
|
425
420
|
source: 'agent_event_task_completion',
|
|
426
421
|
msgId: taskCompletion.childSessionId ? `dragon_task_reply_${taskCompletion.childSessionId}` : undefined,
|
|
427
422
|
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { IComponent } from "../../core/IComponent.js";
|
|
2
|
+
type LoggerLike = {
|
|
3
|
+
info?: (...args: any[]) => void;
|
|
4
|
+
warn?: (...args: any[]) => void;
|
|
5
|
+
error?: (...args: any[]) => void;
|
|
6
|
+
};
|
|
7
|
+
export type DragonMediaAttachment = {
|
|
8
|
+
type: string;
|
|
9
|
+
kind: string;
|
|
10
|
+
name: string;
|
|
11
|
+
mimeType?: string;
|
|
12
|
+
sourcePath?: string;
|
|
13
|
+
workspacePath?: string;
|
|
14
|
+
path?: string;
|
|
15
|
+
url?: string;
|
|
16
|
+
size?: number;
|
|
17
|
+
};
|
|
18
|
+
export declare class MediaRegistryComponent implements IComponent {
|
|
19
|
+
private options;
|
|
20
|
+
constructor(options: {
|
|
21
|
+
cfg: any;
|
|
22
|
+
logger?: LoggerLike;
|
|
23
|
+
});
|
|
24
|
+
start(): Promise<void>;
|
|
25
|
+
stop(): Promise<void>;
|
|
26
|
+
private resolveWorkspaceRoot;
|
|
27
|
+
private normalizeLocalPath;
|
|
28
|
+
private isPathInside;
|
|
29
|
+
private inferMime;
|
|
30
|
+
private inferKind;
|
|
31
|
+
private buildWorkspaceRelativePath;
|
|
32
|
+
private collectStructuredMedia;
|
|
33
|
+
archiveStructuredMedia(value: any): Promise<DragonMediaAttachment[]>;
|
|
34
|
+
}
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export class MediaRegistryComponent {
|
|
4
|
+
options;
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.options = options;
|
|
7
|
+
}
|
|
8
|
+
async start() { }
|
|
9
|
+
async stop() { }
|
|
10
|
+
resolveWorkspaceRoot() {
|
|
11
|
+
const candidates = [
|
|
12
|
+
this.options.cfg?.agents?.defaults?.workspace,
|
|
13
|
+
this.options.cfg?.agents?.default?.workspace,
|
|
14
|
+
...(Array.isArray(this.options.cfg?.agents?.list)
|
|
15
|
+
? this.options.cfg.agents.list.map((agent) => agent?.workspace)
|
|
16
|
+
: []),
|
|
17
|
+
process.env.OPENCLAW_WORKSPACE_ROOT,
|
|
18
|
+
process.env.DRAGON_OPENCLAW_WORKSPACE_ROOT,
|
|
19
|
+
];
|
|
20
|
+
const found = candidates.find((value) => typeof value === "string" && value.trim());
|
|
21
|
+
return found ? path.resolve(String(found).trim()) : undefined;
|
|
22
|
+
}
|
|
23
|
+
normalizeLocalPath(value) {
|
|
24
|
+
if (typeof value !== "string")
|
|
25
|
+
return undefined;
|
|
26
|
+
const raw = value.trim();
|
|
27
|
+
if (!raw)
|
|
28
|
+
return undefined;
|
|
29
|
+
if (raw.startsWith("file://")) {
|
|
30
|
+
try {
|
|
31
|
+
return path.resolve(decodeURIComponent(new URL(raw).pathname));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!path.isAbsolute(raw))
|
|
38
|
+
return undefined;
|
|
39
|
+
return path.resolve(raw);
|
|
40
|
+
}
|
|
41
|
+
isPathInside(parent, child) {
|
|
42
|
+
const rel = path.relative(parent, child);
|
|
43
|
+
return rel === "" || (!!rel && !rel.startsWith("..") && !path.isAbsolute(rel));
|
|
44
|
+
}
|
|
45
|
+
inferMime(filePath, fallback) {
|
|
46
|
+
if (fallback)
|
|
47
|
+
return fallback;
|
|
48
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
49
|
+
if (ext === ".png")
|
|
50
|
+
return "image/png";
|
|
51
|
+
if (ext === ".jpg" || ext === ".jpeg")
|
|
52
|
+
return "image/jpeg";
|
|
53
|
+
if (ext === ".webp")
|
|
54
|
+
return "image/webp";
|
|
55
|
+
if (ext === ".gif")
|
|
56
|
+
return "image/gif";
|
|
57
|
+
if (ext === ".svg")
|
|
58
|
+
return "image/svg+xml";
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
inferKind(mimeType) {
|
|
62
|
+
if (mimeType?.startsWith("image/"))
|
|
63
|
+
return "image";
|
|
64
|
+
if (mimeType?.startsWith("video/"))
|
|
65
|
+
return "video";
|
|
66
|
+
if (mimeType?.startsWith("audio/"))
|
|
67
|
+
return "audio";
|
|
68
|
+
return "file";
|
|
69
|
+
}
|
|
70
|
+
buildWorkspaceRelativePath(sourcePath, workspaceRoot) {
|
|
71
|
+
const normalized = path.resolve(sourcePath);
|
|
72
|
+
if (this.isPathInside(workspaceRoot, normalized)) {
|
|
73
|
+
return path.relative(workspaceRoot, normalized).split(path.sep).join("/");
|
|
74
|
+
}
|
|
75
|
+
const marker = `${path.sep}media${path.sep}`;
|
|
76
|
+
const mediaIndex = normalized.lastIndexOf(marker);
|
|
77
|
+
if (mediaIndex >= 0) {
|
|
78
|
+
return path
|
|
79
|
+
.join("media", normalized.slice(mediaIndex + marker.length))
|
|
80
|
+
.split(path.sep)
|
|
81
|
+
.join("/");
|
|
82
|
+
}
|
|
83
|
+
return path.join("media", path.basename(normalized)).split(path.sep).join("/");
|
|
84
|
+
}
|
|
85
|
+
collectStructuredMedia(value) {
|
|
86
|
+
const items = [];
|
|
87
|
+
if (!value || typeof value !== "object")
|
|
88
|
+
return items;
|
|
89
|
+
if (Array.isArray(value.attachments))
|
|
90
|
+
items.push(...value.attachments);
|
|
91
|
+
if (Array.isArray(value.media))
|
|
92
|
+
items.push(...value.media);
|
|
93
|
+
if (Array.isArray(value.mediaUrls)) {
|
|
94
|
+
items.push(...value.mediaUrls.map((url) => ({ url })));
|
|
95
|
+
}
|
|
96
|
+
return items.filter(Boolean);
|
|
97
|
+
}
|
|
98
|
+
async archiveStructuredMedia(value) {
|
|
99
|
+
const workspaceRoot = this.resolveWorkspaceRoot();
|
|
100
|
+
if (!workspaceRoot)
|
|
101
|
+
return [];
|
|
102
|
+
const media = this.collectStructuredMedia(value);
|
|
103
|
+
const seen = new Set();
|
|
104
|
+
const attachments = [];
|
|
105
|
+
for (const item of media) {
|
|
106
|
+
const sourcePath = this.normalizeLocalPath(item?.path ?? item?.filePath ?? item?.mediaPath ?? item?.url);
|
|
107
|
+
if (!sourcePath || seen.has(sourcePath))
|
|
108
|
+
continue;
|
|
109
|
+
seen.add(sourcePath);
|
|
110
|
+
try {
|
|
111
|
+
const stat = await fs.stat(sourcePath);
|
|
112
|
+
if (!stat.isFile())
|
|
113
|
+
continue;
|
|
114
|
+
const workspacePath = this.buildWorkspaceRelativePath(sourcePath, workspaceRoot);
|
|
115
|
+
const targetPath = path.resolve(workspaceRoot, workspacePath);
|
|
116
|
+
if (!this.isPathInside(workspaceRoot, targetPath))
|
|
117
|
+
continue;
|
|
118
|
+
if (path.resolve(sourcePath) !== targetPath) {
|
|
119
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
120
|
+
await fs.copyFile(sourcePath, targetPath);
|
|
121
|
+
}
|
|
122
|
+
const mimeType = this.inferMime(targetPath, item?.mimeType || item?.mime);
|
|
123
|
+
const kind = item?.kind || item?.type || this.inferKind(mimeType);
|
|
124
|
+
attachments.push({
|
|
125
|
+
type: kind,
|
|
126
|
+
kind,
|
|
127
|
+
name: item?.name || path.basename(targetPath),
|
|
128
|
+
mimeType,
|
|
129
|
+
sourcePath,
|
|
130
|
+
workspacePath,
|
|
131
|
+
path: workspacePath,
|
|
132
|
+
size: stat.size,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
this.options.logger?.warn?.(`[dragon channels][media] failed to archive media: source=${sourcePath}, error=${error?.message || error}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return attachments;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -15,15 +15,18 @@ export class SseComponent {
|
|
|
15
15
|
this.channel = channel;
|
|
16
16
|
this.options = options;
|
|
17
17
|
}
|
|
18
|
+
logContext() {
|
|
19
|
+
return `version=${this.options.version}, channelAgent=${this.options.agentId}, account=${this.options.accountId}`;
|
|
20
|
+
}
|
|
18
21
|
async start() {
|
|
19
|
-
this.options.logger?.info?.(`[dragon channels][SSE] component start:
|
|
22
|
+
this.options.logger?.info?.(`[dragon channels][SSE] component start: ${this.logContext()}, baseUrl=${this.http.options.baseURL}`);
|
|
20
23
|
this.options.setStatus?.({
|
|
21
24
|
accountId: this.options.accountId,
|
|
22
25
|
connected: false,
|
|
23
26
|
lastError: null,
|
|
24
27
|
});
|
|
25
28
|
this.options.abortSignal?.addEventListener('abort', () => {
|
|
26
|
-
this.options.logger?.info?.(`[dragon channels][SSE] upstream abort received:
|
|
29
|
+
this.options.logger?.info?.(`[dragon channels][SSE] upstream abort received: ${this.logContext()}`);
|
|
27
30
|
void this.stop();
|
|
28
31
|
}, { once: true });
|
|
29
32
|
this.connect();
|
|
@@ -50,7 +53,7 @@ export class SseComponent {
|
|
|
50
53
|
connected: false,
|
|
51
54
|
lastStopAt: Date.now(),
|
|
52
55
|
});
|
|
53
|
-
this.options.logger?.info?.(`[dragon channels][SSE] component stopped:
|
|
56
|
+
this.options.logger?.info?.(`[dragon channels][SSE] component stopped: ${this.logContext()}`);
|
|
54
57
|
}
|
|
55
58
|
connect() {
|
|
56
59
|
if (!this.shouldReconnect)
|
|
@@ -64,12 +67,12 @@ export class SseComponent {
|
|
|
64
67
|
});
|
|
65
68
|
const url = `${this.http.options.baseURL}/api/agents/events?${params.toString()}`;
|
|
66
69
|
const connectionId = `${Date.now()}-${++this.connectionSeq}`;
|
|
67
|
-
logger?.info?.(`[dragon channels][SSE] connect start: connection=${connectionId}, attempt=${this.reconnectAttempts + 1},
|
|
70
|
+
logger?.info?.(`[dragon channels][SSE] connect start: connection=${connectionId}, attempt=${this.reconnectAttempts + 1}, ${this.logContext()}, url=${url}`);
|
|
68
71
|
try {
|
|
69
72
|
void this.startSseReader(url, connectionId);
|
|
70
73
|
}
|
|
71
74
|
catch (err) {
|
|
72
|
-
logger?.error?.(`[dragon channels][SSE] connect init failed: connection=${connectionId}, error=${err.message}`);
|
|
75
|
+
logger?.error?.(`[dragon channels][SSE] connect init failed: connection=${connectionId}, ${this.logContext()}, error=${err.message}`);
|
|
73
76
|
this.markDisconnected(err?.message || String(err));
|
|
74
77
|
this.scheduleReconnect('init_failed');
|
|
75
78
|
}
|
|
@@ -84,7 +87,7 @@ export class SseComponent {
|
|
|
84
87
|
lastTransportActivityAt: now,
|
|
85
88
|
lastError: null,
|
|
86
89
|
});
|
|
87
|
-
this.options.logger?.debug?.(`[dragon channels][SSE] transport activity: connection=${connectionId}, reason=${reason}, at=${now}`);
|
|
90
|
+
this.options.logger?.debug?.(`[dragon channels][SSE] transport activity: connection=${connectionId}, ${this.logContext()}, reason=${reason}, at=${now}`);
|
|
88
91
|
this.armIdleWatchdog(connectionId);
|
|
89
92
|
}
|
|
90
93
|
armIdleWatchdog(connectionId) {
|
|
@@ -95,7 +98,7 @@ export class SseComponent {
|
|
|
95
98
|
if (!this.shouldReconnect)
|
|
96
99
|
return;
|
|
97
100
|
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`);
|
|
101
|
+
this.options.logger?.warn?.(`[dragon channels][SSE] idle timeout: connection=${connectionId}, ${this.logContext()}, idleMs=${idleMs}, timeoutMs=${timeoutMs}; aborting fetch for reconnect`);
|
|
99
102
|
this.markDisconnected(`SSE idle timeout after ${idleMs}ms`);
|
|
100
103
|
this.fetchAbort?.abort();
|
|
101
104
|
}, timeoutMs);
|
|
@@ -142,14 +145,14 @@ export class SseComponent {
|
|
|
142
145
|
lastError: null,
|
|
143
146
|
reconnectAttempts: 0,
|
|
144
147
|
});
|
|
145
|
-
this.options.logger?.info?.(`[dragon channels][SSE] stream established: connection=${connectionId},
|
|
148
|
+
this.options.logger?.info?.(`[dragon channels][SSE] stream established: connection=${connectionId}, ${this.logContext()}`);
|
|
146
149
|
this.armIdleWatchdog(connectionId);
|
|
147
150
|
const decoder = new TextDecoder();
|
|
148
151
|
let buffer = "";
|
|
149
152
|
while (this.shouldReconnect) {
|
|
150
153
|
const { value, done } = await reader.read();
|
|
151
154
|
if (done) {
|
|
152
|
-
this.options.logger?.warn?.(`[dragon channels][SSE] stream ended by remote: connection=${connectionId}`);
|
|
155
|
+
this.options.logger?.warn?.(`[dragon channels][SSE] stream ended by remote: connection=${connectionId}, ${this.logContext()}`);
|
|
153
156
|
break;
|
|
154
157
|
}
|
|
155
158
|
this.touchTransport(connectionId, 'chunk');
|
|
@@ -165,13 +168,13 @@ export class SseComponent {
|
|
|
165
168
|
if (cleanLine.startsWith("data: ")) {
|
|
166
169
|
try {
|
|
167
170
|
const data = JSON.parse(cleanLine.slice(6));
|
|
168
|
-
this.options.logger?.debug?.(`[dragon channels][SSE] event received: connection=${connectionId}, type=${data?.type || 'unknown'},
|
|
171
|
+
this.options.logger?.debug?.(`[dragon channels][SSE] event received: connection=${connectionId}, ${this.logContext()}, type=${data?.type || 'unknown'}, eventAgent=${data?.agentId || 'unknown'}`);
|
|
169
172
|
void this.handleEvent(data).catch((err) => {
|
|
170
|
-
this.options.logger?.error?.(`[dragon channels][SSE] event handling failed: connection=${connectionId}, error=${err?.message || err}`);
|
|
173
|
+
this.options.logger?.error?.(`[dragon channels][SSE] event handling failed: connection=${connectionId}, ${this.logContext()}, error=${err?.message || err}`);
|
|
171
174
|
});
|
|
172
175
|
}
|
|
173
176
|
catch (e) {
|
|
174
|
-
this.options.logger?.error?.(`[dragon channels][SSE] event JSON parse failed: connection=${connectionId}, line="${cleanLine}", error=${e?.message || e}`);
|
|
177
|
+
this.options.logger?.error?.(`[dragon channels][SSE] event JSON parse failed: connection=${connectionId}, ${this.logContext()}, line="${cleanLine}", error=${e?.message || e}`);
|
|
175
178
|
}
|
|
176
179
|
}
|
|
177
180
|
}
|
|
@@ -187,7 +190,7 @@ export class SseComponent {
|
|
|
187
190
|
const message = err?.name === 'AbortError'
|
|
188
191
|
? 'SSE fetch aborted'
|
|
189
192
|
: (err?.message || String(err));
|
|
190
|
-
this.options.logger?.warn?.(`[dragon channels][SSE] stream disconnected: connection=${connectionId}, error=${message}`);
|
|
193
|
+
this.options.logger?.warn?.(`[dragon channels][SSE] stream disconnected: connection=${connectionId}, ${this.logContext()}, error=${message}`);
|
|
191
194
|
this.markDisconnected(message);
|
|
192
195
|
this.scheduleReconnect(message);
|
|
193
196
|
}
|
|
@@ -220,12 +223,12 @@ export class SseComponent {
|
|
|
220
223
|
// 2. Handle specific types
|
|
221
224
|
if (data.type === 'WORKBENCH_MESSAGE') {
|
|
222
225
|
const { content, sessionId, attachments, id, msgId } = data.payload || {};
|
|
223
|
-
logger?.info?.(`[dragon channels][Dragon Plugin]
|
|
226
|
+
logger?.info?.(`[dragon channels][Dragon Plugin] [SSE] Received from Workbench: ${this.logContext()}, messageAgent=${data?.agentId || 'unknown'}, "${content?.substring(0, 50)}${content?.length > 50 ? '...' : ''}" [Session: ${sessionId}]`);
|
|
224
227
|
await this.channel.deliverToOpenClaw(content, sessionId, undefined, attachments, msgId || id);
|
|
225
228
|
}
|
|
226
229
|
else if (data.type === 'FETCH_HISTORY') {
|
|
227
230
|
const { sessionId } = data.payload || {};
|
|
228
|
-
logger?.info?.(`[dragon channels][Dragon Plugin] Triggering history sync
|
|
231
|
+
logger?.info?.(`[dragon channels][Dragon Plugin] [SSE] Triggering history sync: ${this.logContext()}, session=${sessionId}`);
|
|
229
232
|
// Handle history sync if needed, but deliverToOpenClaw usually handles normal chat
|
|
230
233
|
}
|
|
231
234
|
}
|
|
@@ -244,7 +247,7 @@ export class SseComponent {
|
|
|
244
247
|
reconnectAttempts: this.reconnectAttempts,
|
|
245
248
|
lastError: reason,
|
|
246
249
|
});
|
|
247
|
-
this.options.logger?.info?.(`[dragon channels][SSE] reconnect scheduled: attempt=${this.reconnectAttempts}, delayMs=${delayMs}, reason=${reason},
|
|
250
|
+
this.options.logger?.info?.(`[dragon channels][SSE] reconnect scheduled: attempt=${this.reconnectAttempts}, delayMs=${delayMs}, reason=${reason}, ${this.logContext()}`);
|
|
248
251
|
this.reconnectTimer = setTimeout(() => {
|
|
249
252
|
this.reconnectTimer = undefined;
|
|
250
253
|
this.options.setStatus?.({
|
|
@@ -15,7 +15,7 @@ export class TelemetryComponent {
|
|
|
15
15
|
const sessionId = payload.sessionId || "default";
|
|
16
16
|
const content = payload.content || "";
|
|
17
17
|
const normalized = content.replace(/\s+/g, "");
|
|
18
|
-
if (normalized) {
|
|
18
|
+
if (normalized && !payload.attachments?.length) {
|
|
19
19
|
const key = `${source}_${sessionId}`;
|
|
20
20
|
const last = this.lastReplies.get(key);
|
|
21
21
|
const now = Date.now();
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { TelemetryComponent } from "./components/telemetry/TelemetryComponent.js
|
|
|
6
6
|
import { HttpComponent } from "./components/http/HttpComponent.js";
|
|
7
7
|
import { ChannelComponent } from "./components/channel/ChannelComponent.js";
|
|
8
8
|
import { SseComponent } from "./components/sync/SseComponent.js";
|
|
9
|
+
import { MediaRegistryComponent } from "./components/media/MediaRegistryComponent.js";
|
|
9
10
|
import { dragonChannelPluginVersion } from "./version.js";
|
|
10
11
|
const channelId = "dragon";
|
|
11
12
|
const workbenchTargetPrefix = "dragon-workbench:";
|
|
@@ -62,6 +63,10 @@ async function getOrCreateContainer(account, ctx) {
|
|
|
62
63
|
logger
|
|
63
64
|
}));
|
|
64
65
|
const telemetry = container.register('telemetry', new TelemetryComponent(http, account.agentId));
|
|
66
|
+
const mediaRegistry = container.register('mediaRegistry', new MediaRegistryComponent({
|
|
67
|
+
cfg: ctx.cfg,
|
|
68
|
+
logger,
|
|
69
|
+
}));
|
|
65
70
|
// 2. Channel Logic (Centralized)
|
|
66
71
|
const channel = container.register('channel', new ChannelComponent({
|
|
67
72
|
accountId: account.accountId,
|
|
@@ -69,7 +74,7 @@ async function getOrCreateContainer(account, ctx) {
|
|
|
69
74
|
channelRuntime: ctx.channelRuntime,
|
|
70
75
|
cfg: ctx.cfg,
|
|
71
76
|
logger
|
|
72
|
-
}, telemetry));
|
|
77
|
+
}, telemetry, mediaRegistry));
|
|
73
78
|
// 3. Sync Infrastructure
|
|
74
79
|
container.register('sse', new SseComponent(http, channel, {
|
|
75
80
|
agentId: account.agentId,
|
|
@@ -248,54 +253,21 @@ const plugin = createChatChannelPlugin({
|
|
|
248
253
|
};
|
|
249
254
|
}
|
|
250
255
|
logger?.info?.(`[dragon channels][Dragon Plugin] handleAction target resolved: sessionId=${sessionId}, target=${target}, rawTarget=${rawTarget}`);
|
|
251
|
-
// Collect ALL media URLs from the params (mirrors what OpenClaw tracks internally
|
|
252
|
-
// via collectMessagingMediaUrlsFromRecord when the tool call starts, so that
|
|
253
|
-
// hasGatewayAgentDeliveredExpectedMedia can match them against expectedMediaUrls).
|
|
254
|
-
const sentMediaUrls = [];
|
|
255
|
-
const pushMedia = (v, source) => {
|
|
256
|
-
if (typeof v === 'string' && v.trim()) {
|
|
257
|
-
const url = v.trim();
|
|
258
|
-
sentMediaUrls.push(url);
|
|
259
|
-
logger?.info?.(`[dragon channels][Dragon Plugin] handleAction extracted media URL from ${source}: ${url}`);
|
|
260
|
-
}
|
|
261
|
-
};
|
|
262
|
-
pushMedia(params.media, "params.media");
|
|
263
|
-
pushMedia(params.mediaUrl, "params.mediaUrl");
|
|
264
|
-
pushMedia(params.path, "params.path");
|
|
265
|
-
pushMedia(params.filePath, "params.filePath");
|
|
266
|
-
pushMedia(params.fileUrl, "params.fileUrl");
|
|
267
|
-
if (Array.isArray(params.mediaUrls)) {
|
|
268
|
-
logger?.info?.(`[dragon channels][Dragon Plugin] handleAction found params.mediaUrls array, size=${params.mediaUrls.length}`);
|
|
269
|
-
for (const u of params.mediaUrls)
|
|
270
|
-
pushMedia(u, "params.mediaUrls");
|
|
271
|
-
}
|
|
272
|
-
if (Array.isArray(params.attachments)) {
|
|
273
|
-
logger?.info?.(`[dragon channels][Dragon Plugin] handleAction found params.attachments array, size=${params.attachments.length}`);
|
|
274
|
-
for (const a of params.attachments) {
|
|
275
|
-
if (a && typeof a === 'object') {
|
|
276
|
-
pushMedia(a.media, "attachment.media");
|
|
277
|
-
pushMedia(a.mediaUrl, "attachment.mediaUrl");
|
|
278
|
-
pushMedia(a.path, "attachment.path");
|
|
279
|
-
pushMedia(a.filePath, "attachment.filePath");
|
|
280
|
-
pushMedia(a.fileUrl, "attachment.fileUrl");
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
logger?.info?.(`[dragon channels][Dragon Plugin] handleAction total media URLs extracted: count=${sentMediaUrls.length}, urls=${JSON.stringify(sentMediaUrls)}`);
|
|
285
256
|
let text = params.message || params.text || "";
|
|
286
257
|
logger?.info?.(`[dragon channels][Dragon Plugin] handleAction base message text length: ${text.length}`);
|
|
287
|
-
// Append media path to text so the Workbench can render the image
|
|
288
|
-
for (const url of sentMediaUrls) {
|
|
289
|
-
const mediaLine = `MEDIA: ${url}`;
|
|
290
|
-
if (!text.includes(mediaLine)) {
|
|
291
|
-
text = text ? `${text}\n${mediaLine}` : mediaLine;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
258
|
logger?.info?.(`[dragon channels][Dragon Plugin] handleAction sending text over channel: text_len=${text.length}, targetSession=${sessionId}`);
|
|
295
259
|
const result = await channel.handleOutboundText({
|
|
296
260
|
text,
|
|
297
|
-
peer: { id: target }
|
|
261
|
+
peer: { id: target },
|
|
262
|
+
attachments: params.attachments,
|
|
263
|
+
media: params.media,
|
|
264
|
+
mediaUrls: params.mediaUrls,
|
|
298
265
|
});
|
|
266
|
+
const deliveredMediaUrls = Array.isArray(result.attachments)
|
|
267
|
+
? result.attachments
|
|
268
|
+
.map((attachment) => attachment?.sourcePath || attachment?.path || attachment?.url)
|
|
269
|
+
.filter((value) => typeof value === 'string' && value.trim())
|
|
270
|
+
: (Array.isArray(params.mediaUrls) ? params.mediaUrls : []);
|
|
299
271
|
logger?.info?.(`[dragon channels][Dragon Plugin] handleAction channel send result: ok=${result.ok}, messageId=${result.messageId || "none"}`);
|
|
300
272
|
const evidence = {
|
|
301
273
|
ok: result.ok,
|
|
@@ -304,8 +276,8 @@ const plugin = createChatChannelPlugin({
|
|
|
304
276
|
// and hasGatewayAgentDeliveredExpectedMedia
|
|
305
277
|
didSendViaMessagingTool: true,
|
|
306
278
|
messagingToolSentTexts: text ? [text] : [],
|
|
307
|
-
messagingToolSentMediaUrls:
|
|
308
|
-
messagingToolSentTargets: [{ to: target, ...(
|
|
279
|
+
messagingToolSentMediaUrls: deliveredMediaUrls,
|
|
280
|
+
messagingToolSentTargets: [{ to: target, ...(deliveredMediaUrls.length > 0 ? { mediaUrls: deliveredMediaUrls } : {}) }],
|
|
309
281
|
};
|
|
310
282
|
logger?.info?.(`[dragon channels][Dragon Plugin] handleAction returning evidence to OpenClaw: didSendViaMessagingTool=${evidence.didSendViaMessagingTool}, sentMediaUrlsCount=${evidence.messagingToolSentMediaUrls.length}`);
|
|
311
283
|
return evidence;
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const dragonChannelPluginVersion = "0.5.
|
|
1
|
+
export declare const dragonChannelPluginVersion = "0.5.37";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const dragonChannelPluginVersion = "0.5.
|
|
1
|
+
export const dragonChannelPluginVersion = "0.5.37";
|