@efengx/openclaw-channel-dragon 0.5.36 → 0.5.38
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 -2
- package/dist/components/channel/ChannelComponent.js +96 -33
- package/dist/components/media/MediaRegistryComponent.d.ts +35 -0
- package/dist/components/media/MediaRegistryComponent.js +141 -0
- package/dist/components/telemetry/TelemetryComponent.d.ts +1 -0
- package/dist/components/telemetry/TelemetryComponent.js +3 -2
- package/dist/index.js +18 -46
- 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,29 +11,34 @@ 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;
|
|
19
21
|
private encodeWorkbenchTarget;
|
|
20
22
|
private decodeWorkbenchTarget;
|
|
23
|
+
private maybeDecodeWorkbenchTarget;
|
|
24
|
+
private resolveOutboundSessionId;
|
|
21
25
|
private resolveOpenClawAgentId;
|
|
22
26
|
private buildOpenClawSessionKey;
|
|
23
27
|
private resolveWorkbenchSessionIdFromOpenClawSessionKey;
|
|
24
28
|
private stringifyProgressDetail;
|
|
29
|
+
private summarizeOutboundContext;
|
|
25
30
|
private findTaskCompletionEvent;
|
|
26
|
-
private extractGeneratedMediaLines;
|
|
27
31
|
private formatTaskCompletionReply;
|
|
28
32
|
deliverToOpenClaw: (content: string, sessionId?: string, modelId?: string, attachments?: any[], messageId?: string | number) => Promise<void>;
|
|
29
33
|
handleOutboundText: (ctx: any) => Promise<{
|
|
30
34
|
ok: boolean;
|
|
31
35
|
messageId: string;
|
|
32
36
|
error: any;
|
|
37
|
+
attachments?: undefined;
|
|
33
38
|
} | {
|
|
34
39
|
ok: boolean;
|
|
35
40
|
messageId: string;
|
|
41
|
+
attachments: import("../media/MediaRegistryComponent.js").DragonMediaAttachment[];
|
|
36
42
|
error?: undefined;
|
|
37
43
|
}>;
|
|
38
44
|
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() { }
|
|
@@ -36,6 +38,41 @@ export class ChannelComponent {
|
|
|
36
38
|
}
|
|
37
39
|
return sessionId;
|
|
38
40
|
}
|
|
41
|
+
maybeDecodeWorkbenchTarget(value) {
|
|
42
|
+
const target = String(value || '').trim();
|
|
43
|
+
if (!target || !target.startsWith(workbenchTargetPrefix))
|
|
44
|
+
return undefined;
|
|
45
|
+
return this.decodeWorkbenchTarget(target);
|
|
46
|
+
}
|
|
47
|
+
resolveOutboundSessionId(ctx) {
|
|
48
|
+
const rawTarget = ctx?.peer?.id ?? ctx?.target ?? ctx?.to ?? ctx?.route?.to ?? ctx?.route?.from;
|
|
49
|
+
const decoded = this.maybeDecodeWorkbenchTarget(rawTarget);
|
|
50
|
+
if (decoded)
|
|
51
|
+
return { sessionId: decoded, rawTarget, inferred: false };
|
|
52
|
+
const explicitTarget = String(rawTarget || '').trim();
|
|
53
|
+
if (explicitTarget) {
|
|
54
|
+
return { sessionId: this.decodeWorkbenchTarget(explicitTarget), rawTarget, inferred: false };
|
|
55
|
+
}
|
|
56
|
+
const currentChannelId = this.maybeDecodeWorkbenchTarget(ctx?.toolContext?.currentChannelId);
|
|
57
|
+
if (currentChannelId) {
|
|
58
|
+
return { sessionId: currentChannelId, rawTarget: ctx?.toolContext?.currentChannelId, inferred: true };
|
|
59
|
+
}
|
|
60
|
+
const sessionKey = ctx?.sessionKey ||
|
|
61
|
+
ctx?.SessionKey ||
|
|
62
|
+
ctx?.ctx?.SessionKey ||
|
|
63
|
+
ctx?.message?.sessionKey ||
|
|
64
|
+
ctx?.route?.sessionKey ||
|
|
65
|
+
ctx?.session?.key;
|
|
66
|
+
const sessionFromKey = this.resolveWorkbenchSessionIdFromOpenClawSessionKey(sessionKey);
|
|
67
|
+
if (sessionKey || sessionFromKey !== 'default') {
|
|
68
|
+
return { sessionId: sessionFromKey, rawTarget: sessionKey, inferred: true };
|
|
69
|
+
}
|
|
70
|
+
const sessionId = String(ctx?.sessionId || ctx?.session?.id || '').trim();
|
|
71
|
+
if (sessionId) {
|
|
72
|
+
return { sessionId, rawTarget: sessionId, inferred: true };
|
|
73
|
+
}
|
|
74
|
+
return { sessionId: 'default', rawTarget: rawTarget || '<empty>', inferred: true };
|
|
75
|
+
}
|
|
39
76
|
resolveOpenClawAgentId() {
|
|
40
77
|
const agents = this.options.cfg?.agents?.list;
|
|
41
78
|
if (Array.isArray(agents)) {
|
|
@@ -78,6 +115,32 @@ export class ChannelComponent {
|
|
|
78
115
|
return String(value).slice(0, maxLength);
|
|
79
116
|
}
|
|
80
117
|
}
|
|
118
|
+
summarizeOutboundContext(ctx) {
|
|
119
|
+
const summarizeValue = (value) => {
|
|
120
|
+
if (Array.isArray(value))
|
|
121
|
+
return { type: 'array', length: value.length };
|
|
122
|
+
if (value && typeof value === 'object')
|
|
123
|
+
return { type: 'object', keys: Object.keys(value).slice(0, 20) };
|
|
124
|
+
if (typeof value === 'string')
|
|
125
|
+
return value.length > 180 ? `${value.slice(0, 180)}...` : value;
|
|
126
|
+
return value;
|
|
127
|
+
};
|
|
128
|
+
return {
|
|
129
|
+
topLevelKeys: ctx && typeof ctx === 'object' ? Object.keys(ctx).slice(0, 40) : [],
|
|
130
|
+
textLength: String(ctx?.text || '').length,
|
|
131
|
+
peerId: ctx?.peer?.id,
|
|
132
|
+
target: ctx?.target,
|
|
133
|
+
to: ctx?.to,
|
|
134
|
+
routeTo: ctx?.route?.to,
|
|
135
|
+
routeFrom: ctx?.route?.from,
|
|
136
|
+
sessionKey: ctx?.sessionKey || ctx?.SessionKey || ctx?.ctx?.SessionKey || ctx?.message?.sessionKey || ctx?.route?.sessionKey || ctx?.session?.key,
|
|
137
|
+
sessionId: ctx?.sessionId || ctx?.session?.id,
|
|
138
|
+
currentChannelId: ctx?.toolContext?.currentChannelId,
|
|
139
|
+
attachments: summarizeValue(ctx?.attachments),
|
|
140
|
+
media: summarizeValue(ctx?.media),
|
|
141
|
+
mediaUrls: summarizeValue(ctx?.mediaUrls),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
81
144
|
findTaskCompletionEvent(value) {
|
|
82
145
|
if (!value || typeof value !== 'object')
|
|
83
146
|
return undefined;
|
|
@@ -96,29 +159,8 @@ export class ChannelComponent {
|
|
|
96
159
|
}
|
|
97
160
|
return undefined;
|
|
98
161
|
}
|
|
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
162
|
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');
|
|
163
|
+
return typeof event?.result === 'string' ? event.result.trim() : '';
|
|
122
164
|
}
|
|
123
165
|
deliverToOpenClaw = async (content, sessionId = 'default', modelId, attachments, messageId) => {
|
|
124
166
|
const replyMsgId = messageId ? `dragon_msg_${messageId}` : `dragon_msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -166,13 +208,15 @@ export class ChannelComponent {
|
|
|
166
208
|
dispatcherOptions: {
|
|
167
209
|
deliver: async (payload) => {
|
|
168
210
|
const text = payload?.text || "";
|
|
169
|
-
|
|
211
|
+
const archivedAttachments = await this.mediaRegistry.archiveStructuredMedia(payload);
|
|
212
|
+
if (!text && !payload?.tool_calls?.length && archivedAttachments.length === 0)
|
|
170
213
|
return;
|
|
171
214
|
await this.telemetry.reportReply({
|
|
172
215
|
content: text,
|
|
173
216
|
sessionId,
|
|
174
217
|
tool_calls: payload?.tool_calls,
|
|
175
218
|
reasoning_content: payload?.reasoning_content,
|
|
219
|
+
attachments: archivedAttachments.length > 0 ? archivedAttachments : payload?.attachments,
|
|
176
220
|
source: "telemetry_deliver",
|
|
177
221
|
msgId: replyMsgId,
|
|
178
222
|
});
|
|
@@ -218,12 +262,16 @@ export class ChannelComponent {
|
|
|
218
262
|
});
|
|
219
263
|
},
|
|
220
264
|
onToolResult: async (payload) => {
|
|
265
|
+
const archivedAttachments = await this.mediaRegistry.archiveStructuredMedia(payload);
|
|
221
266
|
await progress({
|
|
222
|
-
kind: 'tool',
|
|
267
|
+
kind: archivedAttachments.length > 0 ? 'media' : 'tool',
|
|
223
268
|
phase: 'end',
|
|
224
269
|
status: 'completed',
|
|
225
|
-
title: '工具执行完成',
|
|
226
|
-
detail:
|
|
270
|
+
title: archivedAttachments.length > 0 ? '媒体文件已生成' : '工具执行完成',
|
|
271
|
+
detail: archivedAttachments.length > 0
|
|
272
|
+
? archivedAttachments.map((item) => item.name).join(', ')
|
|
273
|
+
: this.stringifyProgressDetail(payload?.text || payload),
|
|
274
|
+
data: archivedAttachments.length > 0 ? { ...payload, attachments: archivedAttachments } : payload,
|
|
227
275
|
});
|
|
228
276
|
},
|
|
229
277
|
onItemEvent: async (payload) => {
|
|
@@ -371,9 +419,17 @@ export class ChannelComponent {
|
|
|
371
419
|
handleOutboundText = async (ctx) => {
|
|
372
420
|
const text = ctx?.text || "";
|
|
373
421
|
const { logger } = this.options;
|
|
422
|
+
const archivedAttachments = await this.mediaRegistry.archiveStructuredMedia(ctx);
|
|
374
423
|
let sessionId;
|
|
424
|
+
let rawTarget;
|
|
375
425
|
try {
|
|
376
|
-
|
|
426
|
+
const resolved = this.resolveOutboundSessionId(ctx);
|
|
427
|
+
sessionId = resolved.sessionId;
|
|
428
|
+
rawTarget = resolved.rawTarget;
|
|
429
|
+
if (resolved.inferred) {
|
|
430
|
+
logger?.warn?.(`[dragon channels][Dragon Plugin] outbound target missing; inferred workbench session "${sessionId}" from runtime context instead of reporting protocol error.`);
|
|
431
|
+
logger?.warn?.(`[dragon channels][Dragon Plugin] non-standard outbound context: ${JSON.stringify(this.summarizeOutboundContext(ctx))}`);
|
|
432
|
+
}
|
|
377
433
|
}
|
|
378
434
|
catch (error) {
|
|
379
435
|
const message = error?.message || String(error);
|
|
@@ -381,9 +437,14 @@ export class ChannelComponent {
|
|
|
381
437
|
await this.reportTargetProtocolError(message, ctx?.peer?.id);
|
|
382
438
|
return { ok: false, messageId: Date.now().toString(), error: message };
|
|
383
439
|
}
|
|
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({
|
|
386
|
-
|
|
440
|
+
logger?.info?.(`[dragon channels][Dragon Plugin] Outbound Text (Action): "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}" [Session: ${sessionId}] [RawPeer: ${String(rawTarget || ctx?.peer?.id || 'none')}]`);
|
|
441
|
+
await this.telemetry.reportReply({
|
|
442
|
+
content: text,
|
|
443
|
+
sessionId,
|
|
444
|
+
attachments: archivedAttachments.length > 0 ? archivedAttachments : ctx?.attachments,
|
|
445
|
+
source: "channel_outbound",
|
|
446
|
+
});
|
|
447
|
+
return { ok: true, messageId: Date.now().toString(), attachments: archivedAttachments };
|
|
387
448
|
};
|
|
388
449
|
reportTargetProtocolError = async (message, rawTarget) => {
|
|
389
450
|
const content = [
|
|
@@ -407,7 +468,8 @@ export class ChannelComponent {
|
|
|
407
468
|
if (!taskCompletion)
|
|
408
469
|
return;
|
|
409
470
|
const content = this.formatTaskCompletionReply(taskCompletion);
|
|
410
|
-
|
|
471
|
+
const archivedAttachments = await this.mediaRegistry.archiveStructuredMedia(taskCompletion);
|
|
472
|
+
if (!content && archivedAttachments.length === 0)
|
|
411
473
|
return;
|
|
412
474
|
await this.telemetry.reportProgress({
|
|
413
475
|
sessionId,
|
|
@@ -417,11 +479,12 @@ export class ChannelComponent {
|
|
|
417
479
|
status: taskCompletion.status === 'ok' ? 'completed' : taskCompletion.status,
|
|
418
480
|
title: taskCompletion.status === 'ok' ? '后台任务完成' : '后台任务更新',
|
|
419
481
|
detail: this.stringifyProgressDetail(taskCompletion.taskLabel || taskCompletion.statusLabel),
|
|
420
|
-
data: taskCompletion,
|
|
482
|
+
data: archivedAttachments.length > 0 ? { ...taskCompletion, attachments: archivedAttachments } : taskCompletion,
|
|
421
483
|
});
|
|
422
484
|
await this.telemetry.reportReply({
|
|
423
485
|
content,
|
|
424
486
|
sessionId,
|
|
487
|
+
attachments: archivedAttachments.length > 0 ? archivedAttachments : taskCompletion?.attachments,
|
|
425
488
|
source: 'agent_event_task_completion',
|
|
426
489
|
msgId: taskCompletion.childSessionId ? `dragon_task_reply_${taskCompletion.childSessionId}` : undefined,
|
|
427
490
|
});
|
|
@@ -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,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();
|
|
@@ -73,7 +73,8 @@ export class TelemetryComponent {
|
|
|
73
73
|
async reportProgress(payload) {
|
|
74
74
|
try {
|
|
75
75
|
const sessionId = payload.sessionId || "default";
|
|
76
|
-
const
|
|
76
|
+
const msgId = payload.msgId || "no-msg";
|
|
77
|
+
const key = `${sessionId}:${msgId}:${payload.kind}:${payload.title || ""}`;
|
|
77
78
|
const signature = JSON.stringify({
|
|
78
79
|
phase: payload.phase,
|
|
79
80
|
status: payload.status,
|
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,
|
|
@@ -226,7 +231,7 @@ const plugin = createChatChannelPlugin({
|
|
|
226
231
|
const account = base.config.resolveAccount(cfg, accountId);
|
|
227
232
|
const container = await getOrCreateContainer(account, ctx);
|
|
228
233
|
const channel = container.get('channel');
|
|
229
|
-
const rawTarget = params.to || params.target || ctx.toolContext?.currentChannelId || "default";
|
|
234
|
+
const rawTarget = params.to || params.target || ctx.toolContext?.currentChannelId || encodeWorkbenchTarget("default");
|
|
230
235
|
logger?.info?.(`[dragon channels][Dragon Plugin] handleAction target resolution input: ${rawTarget}`);
|
|
231
236
|
let sessionId;
|
|
232
237
|
let target;
|
|
@@ -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.38";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const dragonChannelPluginVersion = "0.5.
|
|
1
|
+
export const dragonChannelPluginVersion = "0.5.38";
|