@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.
@@ -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
- const result = typeof event?.result === 'string' ? event.result.trim() : '';
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
- if (!text && !payload?.tool_calls?.length)
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: this.stringifyProgressDetail(payload?.text || payload),
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
- sessionId = this.decodeWorkbenchTarget(ctx?.peer?.id);
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({ content: text, sessionId, source: "channel_outbound" });
386
- return { ok: true, messageId: Date.now().toString() };
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
- if (!content)
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
+ }
@@ -13,6 +13,7 @@ export declare class TelemetryComponent implements IComponent {
13
13
  sessionId: string;
14
14
  tool_calls?: any[];
15
15
  reasoning_content?: string;
16
+ attachments?: any[];
16
17
  source?: string;
17
18
  msgId?: string;
18
19
  }): Promise<void>;
@@ -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 key = `${sessionId}:${payload.kind}:${payload.title || ""}`;
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: sentMediaUrls,
308
- messagingToolSentTargets: [{ to: target, ...(sentMediaUrls.length > 0 ? { mediaUrls: sentMediaUrls } : {}) }],
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.36";
1
+ export declare const dragonChannelPluginVersion = "0.5.38";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const dragonChannelPluginVersion = "0.5.36";
1
+ export const dragonChannelPluginVersion = "0.5.38";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@efengx/openclaw-channel-dragon",
3
- "version": "0.5.36",
3
+ "version": "0.5.38",
4
4
  "description": "Dragon workbench channel for OpenClaw",
5
5
  "author": "feng xiang <ofengx@gmail.com>",
6
6
  "type": "module",