clawconnect-agent 0.2.6 → 0.2.7

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.
Files changed (118) hide show
  1. package/README.md +7 -3
  2. package/README.zh-CN.md +7 -3
  3. package/dist/commands/hermes-file-transfer-outcome.d.ts +18 -0
  4. package/dist/commands/hermes-file-transfer-outcome.js +40 -0
  5. package/dist/commands/hermes-file-transfer-outcome.js.map +1 -0
  6. package/dist/commands/send-file.js +39 -33
  7. package/dist/commands/send-file.js.map +1 -1
  8. package/dist/core/command-types.d.ts +5 -0
  9. package/dist/core/relay/chat-payload.js +32 -22
  10. package/dist/core/relay/chat-payload.js.map +1 -1
  11. package/dist/core/relay/file-upload.d.ts +1 -0
  12. package/dist/core/relay/file-upload.js +3 -0
  13. package/dist/core/relay/file-upload.js.map +1 -1
  14. package/dist/core/relay/mobile-chat-run-bridge.d.ts +53 -8
  15. package/dist/core/relay/mobile-chat-run-bridge.js +45 -0
  16. package/dist/core/relay/mobile-chat-run-bridge.js.map +1 -1
  17. package/dist/core/relay/office-payload.js +54 -22
  18. package/dist/core/relay/office-payload.js.map +1 -1
  19. package/dist/core/relay/source-commit.d.ts +41 -0
  20. package/dist/core/relay/source-commit.js +118 -0
  21. package/dist/core/relay/source-commit.js.map +1 -0
  22. package/dist/core/relay/timeline-event-builder.d.ts +6 -0
  23. package/dist/core/relay/timeline-event-builder.js +3 -0
  24. package/dist/core/relay/timeline-event-builder.js.map +1 -1
  25. package/dist/core/relay/timeline-event-log.d.ts +29 -0
  26. package/dist/core/relay/timeline-event-log.js +37 -0
  27. package/dist/core/relay/timeline-event-log.js.map +1 -1
  28. package/dist/core/relay/timeline-projection-v3.d.ts +63 -0
  29. package/dist/core/relay/timeline-projection-v3.js +87 -0
  30. package/dist/core/relay/timeline-projection-v3.js.map +1 -0
  31. package/dist/hermes/hermes-relay-manager.js +3 -0
  32. package/dist/hermes/hermes-relay-manager.js.map +1 -1
  33. package/dist/hermes/hermes-runtime-test-support.d.ts +3 -3
  34. package/dist/hermes/hermes-runtime-test-support.js +12 -6
  35. package/dist/hermes/hermes-runtime-test-support.js.map +1 -1
  36. package/dist/hermes/runtime/hermes-file-transfer-state.d.ts +33 -0
  37. package/dist/hermes/runtime/hermes-file-transfer-state.js +139 -0
  38. package/dist/hermes/runtime/hermes-file-transfer-state.js.map +1 -0
  39. package/dist/hermes/runtime/hermes-mobile-file-route-store.d.ts +17 -0
  40. package/dist/hermes/runtime/hermes-mobile-file-route-store.js +103 -0
  41. package/dist/hermes/runtime/hermes-mobile-file-route-store.js.map +1 -0
  42. package/dist/hermes/runtime/hermes-runtime-api-client.d.ts +1 -0
  43. package/dist/hermes/runtime/hermes-runtime-api-client.js +16 -0
  44. package/dist/hermes/runtime/hermes-runtime-api-client.js.map +1 -1
  45. package/dist/hermes/runtime/hermes-runtime-chat.d.ts +35 -4
  46. package/dist/hermes/runtime/hermes-runtime-chat.js +426 -51
  47. package/dist/hermes/runtime/hermes-runtime-chat.js.map +1 -1
  48. package/dist/hermes/runtime/hermes-runtime-command-router.js +9 -1
  49. package/dist/hermes/runtime/hermes-runtime-command-router.js.map +1 -1
  50. package/dist/hermes/runtime/hermes-runtime-cron.d.ts +1 -0
  51. package/dist/hermes/runtime/hermes-runtime-cron.js +34 -66
  52. package/dist/hermes/runtime/hermes-runtime-cron.js.map +1 -1
  53. package/dist/hermes/runtime/hermes-runtime-history-completion.d.ts +8 -1
  54. package/dist/hermes/runtime/hermes-runtime-history-completion.js +112 -41
  55. package/dist/hermes/runtime/hermes-runtime-history-completion.js.map +1 -1
  56. package/dist/hermes/runtime/hermes-runtime-history.js +92 -17
  57. package/dist/hermes/runtime/hermes-runtime-history.js.map +1 -1
  58. package/dist/hermes/runtime/hermes-runtime-models.js +4 -1
  59. package/dist/hermes/runtime/hermes-runtime-models.js.map +1 -1
  60. package/dist/hermes/runtime/hermes-runtime-process.js +4 -3
  61. package/dist/hermes/runtime/hermes-runtime-process.js.map +1 -1
  62. package/dist/hermes/runtime/hermes-runtime-state-db.d.ts +6 -0
  63. package/dist/hermes/runtime/hermes-runtime-state-db.js +20 -1
  64. package/dist/hermes/runtime/hermes-runtime-state-db.js.map +1 -1
  65. package/dist/hermes/runtime/hermes-runtime-tool-log-watcher.js +12 -20
  66. package/dist/hermes/runtime/hermes-runtime-tool-log-watcher.js.map +1 -1
  67. package/dist/index.js +16 -2
  68. package/dist/index.js.map +1 -1
  69. package/dist/openclaw/config/provider-config.js +4 -1
  70. package/dist/openclaw/config/provider-config.js.map +1 -1
  71. package/dist/openclaw/relay/agent-identity-helpers.d.ts +9 -0
  72. package/dist/openclaw/relay/agent-identity-helpers.js +118 -0
  73. package/dist/openclaw/relay/agent-identity-helpers.js.map +1 -0
  74. package/dist/openclaw/relay/assistant-media-sidecar.d.ts +19 -2
  75. package/dist/openclaw/relay/assistant-media-sidecar.js +131 -15
  76. package/dist/openclaw/relay/assistant-media-sidecar.js.map +1 -1
  77. package/dist/openclaw/relay/chat-history.d.ts +37 -2
  78. package/dist/openclaw/relay/chat-history.js +565 -51
  79. package/dist/openclaw/relay/chat-history.js.map +1 -1
  80. package/dist/openclaw/relay/chat-run-identity.js +3 -1
  81. package/dist/openclaw/relay/chat-run-identity.js.map +1 -1
  82. package/dist/openclaw/relay/gateway-history-projection.d.ts +4 -0
  83. package/dist/openclaw/relay/gateway-history-projection.js +67 -0
  84. package/dist/openclaw/relay/gateway-history-projection.js.map +1 -0
  85. package/dist/openclaw/relay/openclaw-active-run-state.d.ts +67 -0
  86. package/dist/openclaw/relay/openclaw-active-run-state.js +283 -0
  87. package/dist/openclaw/relay/openclaw-active-run-state.js.map +1 -0
  88. package/dist/openclaw/relay/openclaw-message-tool-real-shape.fixture.d.ts +12 -0
  89. package/dist/openclaw/relay/openclaw-message-tool-real-shape.fixture.js +48 -0
  90. package/dist/openclaw/relay/openclaw-message-tool-real-shape.fixture.js.map +1 -0
  91. package/dist/openclaw/relay/openclaw-source-commit-watcher.d.ts +22 -0
  92. package/dist/openclaw/relay/openclaw-source-commit-watcher.js +64 -0
  93. package/dist/openclaw/relay/openclaw-source-commit-watcher.js.map +1 -0
  94. package/dist/openclaw/relay/outgoing-media-relay.d.ts +30 -3
  95. package/dist/openclaw/relay/outgoing-media-relay.js +407 -118
  96. package/dist/openclaw/relay/outgoing-media-relay.js.map +1 -1
  97. package/dist/openclaw/relay/relay-manager-history-timing.d.ts +1 -4
  98. package/dist/openclaw/relay/relay-manager-history-timing.js +4 -4
  99. package/dist/openclaw/relay/relay-manager-history-timing.js.map +1 -1
  100. package/dist/openclaw/relay/relay-manager-payload-helpers.d.ts +28 -1
  101. package/dist/openclaw/relay/relay-manager-payload-helpers.js +219 -11
  102. package/dist/openclaw/relay/relay-manager-payload-helpers.js.map +1 -1
  103. package/dist/openclaw/relay/session-context.d.ts +15 -0
  104. package/dist/openclaw/relay/session-context.js +48 -4
  105. package/dist/openclaw/relay/session-context.js.map +1 -1
  106. package/dist/openclaw/relay/source-commit-projection.d.ts +25 -0
  107. package/dist/openclaw/relay/source-commit-projection.js +140 -0
  108. package/dist/openclaw/relay/source-commit-projection.js.map +1 -0
  109. package/dist/openclaw/relay/transcript-dedupe.js +11 -42
  110. package/dist/openclaw/relay/transcript-dedupe.js.map +1 -1
  111. package/dist/openclaw/relay-manager.js +323 -135
  112. package/dist/openclaw/relay-manager.js.map +1 -1
  113. package/dist/openclaw/runtime/local-runtime.js +4 -2
  114. package/dist/openclaw/runtime/local-runtime.js.map +1 -1
  115. package/package.json +1 -1
  116. package/dist/openclaw/session-store.d.ts +0 -6
  117. package/dist/openclaw/session-store.js +0 -231
  118. package/dist/openclaw/session-store.js.map +0 -1
@@ -1,43 +1,274 @@
1
1
  import { existsSync } from "fs";
2
- import { readFile, stat } from "fs/promises";
2
+ import { readFile, stat, realpath } from "fs/promises";
3
3
  import { homedir } from "os";
4
4
  import { isAbsolute, extname, join, relative, resolve } from "path";
5
5
  import { DatabaseSync } from "node:sqlite";
6
6
  import { createHash } from "crypto";
7
7
  import { uploadFileToRelay } from "../../core/relay/file-upload.js";
8
8
  import { resolveOpenClawStateDir } from "../runtime/openclaw-paths.js";
9
- import { normalizeOpenClawAssistantMediaSidecars, normalizeOpenClawAutomaticMediaReplies, } from "./assistant-media-sidecar.js";
9
+ import { extractOpenClawMessageToolRelation, normalizeOpenClawAssistantMediaSidecars, normalizeOpenClawAutomaticMediaReplies, } from "./assistant-media-sidecar.js";
10
+ import { canonicalizeOpenClawSessionScope, canonicalizeSessionKey, } from "./session-context.js";
10
11
  const OUTGOING_MEDIA_RE = /\/api\/chat\/media\/outgoing\/[^/]+\/([^/]+)\/full(?:$|[?#])/;
11
12
  const OPENCLAW_MEDIA_CONTROL_PREFIX_RE = /^MEDIA:\s*(?:file:\/\/|~[\\/]|\/|[A-Za-z]:[\\/]|\\\\)/i;
12
13
  const OPENCLAW_INPUT_MEDIA_MARKER_RE = /\[media attached:\s+(.+?)\s+\(([^)\r\n]+)\)\s+\|\s+(.+?)\]/g;
14
+ const OPENCLAW_DELIVERY_CONTRACT = "openclaw.message-tool-delivery.v1";
15
+ const OUTGOING_MEDIA_RECORD_WAIT_DELAYS_MS = [80, 160, 320, 640];
13
16
  const inFlightUploadsByCache = new WeakMap();
14
17
  export async function relayOutgoingMediaInPayload(payload, opts) {
15
18
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
16
19
  return payload;
17
20
  }
18
- const message = payload.message;
19
- if (!message || typeof message !== "object" || Array.isArray(message)) {
20
- return payload;
21
- }
22
21
  const payloadRecord = payload;
23
- const sourceRunId = payloadSourceRunId(payloadRecord);
24
- const messageContent = await relayOutgoingMediaContent(message.content, { ...opts, sourceRunId });
25
- const localArtifactBlocks = await relayLocalArtifactPathsInContent(messageContent.blocks, payloadRecord, opts);
22
+ const message = asRecord(payloadRecord.message);
23
+ const messageRelation = message ? extractOpenClawMessageToolRelation(message) : undefined;
24
+ const sourceRunId = messageRelation?.sourceRunId ?? payloadSourceRunId(payloadRecord);
25
+ let nextMessage;
26
+ let messageChanged = false;
27
+ let deliveryBlocks = [];
28
+ if (message) {
29
+ const messageContent = await relayOutgoingMediaContent(message.content, {
30
+ ...opts,
31
+ sourceRunId,
32
+ sessionKey: canonicalRelationSessionKey(firstString(message.sessionKey, message.sessionId, payloadRecord.sessionKey), opts),
33
+ ...(messageRelation?.toolCallId ? { toolCallId: messageRelation.toolCallId } : {}),
34
+ });
35
+ deliveryBlocks = await relayOpenClawDeliveryMedia(message, payloadRecord, opts, sourceRunId);
36
+ const appendedMessage = appendUniqueContentBlocks(messageContent.blocks, deliveryBlocks);
37
+ const nextMessageContent = appendedMessage.changed ? appendedMessage.blocks : messageContent.content;
38
+ messageChanged = messageContent.changed || appendedMessage.changed;
39
+ if (messageChanged) {
40
+ nextMessage = {
41
+ ...message,
42
+ content: nextMessageContent,
43
+ };
44
+ }
45
+ }
26
46
  const timelineEvents = await relayOutgoingMediaInTimelineEvents(payloadRecord.timelineEvents, opts, sourceRunId);
27
- if (!messageContent.changed && localArtifactBlocks.length === 0 && !timelineEvents.changed) {
47
+ const deliveryTimelineEvents = relayOpenClawDeliveryMediaInTimelineEvents(timelineEvents.events, deliveryBlocks, sourceRunId, opts.sessionDefaults);
48
+ if (!messageChanged
49
+ && !timelineEvents.changed
50
+ && !deliveryTimelineEvents.changed) {
28
51
  return payload;
29
52
  }
30
53
  return {
31
54
  ...payloadRecord,
32
- message: {
33
- ...message,
34
- content: localArtifactBlocks.length > 0
35
- ? [...messageContent.blocks, ...localArtifactBlocks]
36
- : messageContent.content,
37
- },
38
- ...(timelineEvents.changed ? { timelineEvents: timelineEvents.events } : {}),
55
+ ...(nextMessage ? { message: nextMessage } : {}),
56
+ ...(timelineEvents.changed || deliveryTimelineEvents.changed
57
+ ? { timelineEvents: deliveryTimelineEvents.events }
58
+ : {}),
39
59
  };
40
60
  }
61
+ async function relayOpenClawDeliveryMedia(message, payload, opts, sourceRunId) {
62
+ // `mediaUrls` is a typed OpenClaw delivery projection. Its presence is the
63
+ // producer's explicit publication receipt; assistant text and the original
64
+ // user prompt are intentionally not consulted here.
65
+ if (!sourceRunId) {
66
+ return [];
67
+ }
68
+ const receipt = typedOpenClawDeliveryReceipt(message.openclawDelivery, sourceRunId)
69
+ ?? adaptOpenClawMessageToolDelivery({ message, ...payload }, sourceRunId);
70
+ if (!receipt) {
71
+ return [];
72
+ }
73
+ const paths = trustedDeliverablePaths(receipt.mediaUrls);
74
+ if (paths.length === 0) {
75
+ return [];
76
+ }
77
+ return relayLocalArtifactPaths(paths, payload, opts, receipt.sourceRunId, receipt.toolCallId);
78
+ }
79
+ /**
80
+ * Adapt the shape emitted by the currently deployed OpenClaw message tool.
81
+ * The installed OpenClaw type declares `openclawDelivery.mediaUrls` only as a
82
+ * display projection. The authoritative toolResult carries the delivery
83
+ * status, tool call, idempotency key, run metadata, and trusted local media
84
+ * list in `details.sourceReply`; only that explicit relation becomes the
85
+ * internal typed v1 receipt. Generic mediaUrls stay inert.
86
+ */
87
+ export function adaptOpenClawMessageToolDelivery(value, expectedSourceRunId) {
88
+ const envelope = asRecord(value);
89
+ if (!envelope)
90
+ return undefined;
91
+ const candidates = [
92
+ asRecord(envelope.message),
93
+ asRecord(envelope.toolResult),
94
+ ...(Array.isArray(envelope.toolResults) ? envelope.toolResults.map(asRecord) : []),
95
+ envelope.role === "toolResult" || envelope.role === "tool_result" ? envelope : undefined,
96
+ ].filter((candidate) => Boolean(candidate));
97
+ for (const message of candidates) {
98
+ const details = asRecord(message.details);
99
+ const sourceReply = asRecord(details?.sourceReply);
100
+ const messageDelivery = asRecord(details?.messageDelivery);
101
+ const metadata = asRecord(message.__openclaw);
102
+ const toolName = firstString(message.toolName, message.tool_name);
103
+ const toolCallId = firstString(message.toolCallId, message.tool_call_id);
104
+ const idempotencyKey = firstString(details?.idempotencyKey, message.idempotencyKey, message.idempotency_key);
105
+ const sourceRunId = firstString(metadata?.runId, metadata?.sourceRunId, message.runId, message.sourceRunId, envelope.runId, envelope.sourceRunId);
106
+ const mediaUrls = sourceReply?.mediaUrls;
107
+ const attachments = sourceReply?.attachments;
108
+ const trustedAttachments = Array.isArray(attachments)
109
+ && attachments.length > 0
110
+ && attachments.every((attachment) => asRecord(attachment)?.trustedLocalMedia === true);
111
+ if ((message.role !== "toolResult" && message.role !== "tool_result")
112
+ || toolName !== "message"
113
+ || !toolCallId
114
+ || !idempotencyKey
115
+ || !sourceRunId
116
+ || (expectedSourceRunId && sourceRunId !== expectedSourceRunId)
117
+ || details?.status !== "ok"
118
+ || details.deliveryStatus !== "sent"
119
+ || details.sourceReplyDeliveryMode !== "message_tool_only"
120
+ || details.sourceReplyTranscriptOwner !== true
121
+ || details.dryRun === true
122
+ || sourceReply?.trustedLocalMedia !== true
123
+ || !trustedAttachments
124
+ || messageDelivery?.status !== "settled"
125
+ || messageDelivery.partialDelivery !== false
126
+ || !Array.isArray(mediaUrls)
127
+ || !mediaUrls.every((path) => typeof path === "string" && path.trim().length > 0)) {
128
+ continue;
129
+ }
130
+ return {
131
+ contract: OPENCLAW_DELIVERY_CONTRACT,
132
+ toolName: "message",
133
+ toolCallId,
134
+ idempotencyKey,
135
+ sourceRunId,
136
+ mediaUrls: mediaUrls.map((path) => path.trim()),
137
+ };
138
+ }
139
+ return undefined;
140
+ }
141
+ function typedOpenClawDeliveryReceipt(value, sourceRunId) {
142
+ const delivery = asRecord(value);
143
+ if (!delivery
144
+ || delivery.contract !== OPENCLAW_DELIVERY_CONTRACT
145
+ || delivery.toolName !== "message"
146
+ || typeof delivery.toolCallId !== "string"
147
+ || !delivery.toolCallId.trim()
148
+ || typeof delivery.idempotencyKey !== "string"
149
+ || !delivery.idempotencyKey.trim()
150
+ || delivery.sourceRunId !== sourceRunId
151
+ || !Array.isArray(delivery.mediaUrls)
152
+ || !delivery.mediaUrls.every((path) => typeof path === "string" && path.trim().length > 0)) {
153
+ return undefined;
154
+ }
155
+ return {
156
+ contract: OPENCLAW_DELIVERY_CONTRACT,
157
+ toolName: "message",
158
+ toolCallId: delivery.toolCallId.trim(),
159
+ idempotencyKey: delivery.idempotencyKey.trim(),
160
+ sourceRunId,
161
+ mediaUrls: delivery.mediaUrls.map((path) => path.trim()),
162
+ };
163
+ }
164
+ function relayOpenClawDeliveryMediaInTimelineEvents(events, blocks, sourceRunId, sessionDefaults) {
165
+ if (!Array.isArray(events) || blocks.length === 0 || !sourceRunId) {
166
+ return { events, changed: false };
167
+ }
168
+ let changed = false;
169
+ const nextEvents = events.map((event) => {
170
+ const record = asRecord(event);
171
+ if (!record || record.eventType !== "message.completed" || record.role !== "assistant") {
172
+ return event;
173
+ }
174
+ const eventRunId = firstString(record.runId);
175
+ const eventTurnId = firstString(record.turnId);
176
+ const matchesSourceRun = eventRunId === sourceRunId || (!eventRunId && eventTurnId === sourceRunId);
177
+ if (!matchesSourceRun) {
178
+ return event;
179
+ }
180
+ if (!Array.isArray(record.content)) {
181
+ return event;
182
+ }
183
+ const eventRelation = extractOpenClawMessageToolRelation(record);
184
+ const eventScope = canonicalizeOpenClawSessionScope(firstString(record.sessionKey, record.sessionId), sessionDefaults);
185
+ const matchingEvents = events.filter((candidate) => {
186
+ const candidateRecord = asRecord(candidate);
187
+ if (!candidateRecord || candidateRecord.eventType !== "message.completed" || candidateRecord.role !== "assistant") {
188
+ return false;
189
+ }
190
+ if (eventScope) {
191
+ const candidateScope = canonicalizeOpenClawSessionScope(firstString(candidateRecord.sessionKey, candidateRecord.sessionId), sessionDefaults);
192
+ if (candidateScope !== eventScope)
193
+ return false;
194
+ }
195
+ const eventRunId = firstString(candidateRecord.runId);
196
+ const eventTurnId = firstString(candidateRecord.turnId);
197
+ return eventRunId === sourceRunId || (!eventRunId && eventTurnId === sourceRunId);
198
+ });
199
+ const relationBlocks = blocks.filter((block) => {
200
+ const relation = extractOpenClawMessageToolRelation(block);
201
+ if (relation?.sourceRunId !== sourceRunId || !relation.toolCallId)
202
+ return false;
203
+ if (!eventScope)
204
+ return true;
205
+ const blockScope = canonicalizeOpenClawSessionScope(firstString(block.sessionKey, block.sessionId), sessionDefaults);
206
+ return !blockScope || blockScope === eventScope;
207
+ });
208
+ let eventBlocks = blocks;
209
+ if (relationBlocks.length > 0) {
210
+ if (!eventRelation?.toolCallId) {
211
+ // A run-level completion can own delivery blocks only when it is the
212
+ // sole completion for that run. Otherwise association is ambiguous;
213
+ // do not duplicate the media onto every assistant row.
214
+ if (matchingEvents.length !== 1)
215
+ return event;
216
+ }
217
+ else {
218
+ const matchingRelation = relationBlocks.filter((block) => (extractOpenClawMessageToolRelation(block)?.toolCallId === eventRelation.toolCallId));
219
+ if (matchingRelation.length === 0)
220
+ return event;
221
+ eventBlocks = matchingRelation;
222
+ }
223
+ }
224
+ const appended = appendUniqueContentBlocks(record.content, eventBlocks);
225
+ const attachmentIds = attachmentIdsFromContent(appended.blocks);
226
+ const currentAttachmentIds = Array.isArray(record.attachmentIds)
227
+ ? record.attachmentIds.filter((value) => typeof value === "string" && value.trim().length > 0)
228
+ : [];
229
+ const attachmentIdsChanged = JSON.stringify(currentAttachmentIds) !== JSON.stringify(attachmentIds);
230
+ if (!appended.changed && !attachmentIdsChanged) {
231
+ return event;
232
+ }
233
+ changed = true;
234
+ const { attachmentIds: _staleAttachmentIds, ...eventWithoutAttachmentIds } = record;
235
+ return {
236
+ ...eventWithoutAttachmentIds,
237
+ content: appended.blocks,
238
+ ...(attachmentIds.length > 0 ? { attachmentIds } : {}),
239
+ };
240
+ });
241
+ return { events: changed ? nextEvents : events, changed };
242
+ }
243
+ function appendUniqueContentBlocks(content, additions) {
244
+ if (additions.length === 0)
245
+ return { blocks: content, changed: false };
246
+ const existingIdentities = new Set(content.map(contentBlockIdentity).filter((identity) => Boolean(identity)));
247
+ const blocks = [...content];
248
+ let changed = false;
249
+ for (const addition of additions) {
250
+ const identity = contentBlockIdentity(addition);
251
+ if (identity && existingIdentities.has(identity))
252
+ continue;
253
+ blocks.push(addition);
254
+ if (identity)
255
+ existingIdentities.add(identity);
256
+ changed = true;
257
+ }
258
+ return { blocks, changed };
259
+ }
260
+ function contentBlockIdentity(block) {
261
+ const record = asRecord(block);
262
+ if (!record)
263
+ return undefined;
264
+ const sourceRunId = firstString(record.sourceRunId, record.source_run_id);
265
+ const toolCallId = firstString(record.toolCallId, record.tool_call_id, record.sourceToolCallId, record.source_tool_call_id);
266
+ const mediaId = firstString(record.attachmentId, record.attachment_id, record.fileId, record.file_id, record.artifactId, record.artifact_id, record.url, record.openUrl, record.downloadUrl, record.downloadPath, record.download_path);
267
+ if (sourceRunId && toolCallId) {
268
+ return `openclaw-tool-call\u0000${sourceRunId}\u0000${toolCallId}\u0000${mediaId ?? "media"}`;
269
+ }
270
+ return mediaId;
271
+ }
41
272
  async function relayOutgoingMediaInTimelineEvents(events, opts, fallbackSourceRunId) {
42
273
  if (!Array.isArray(events))
43
274
  return { events, changed: false };
@@ -46,8 +277,16 @@ async function relayOutgoingMediaInTimelineEvents(events, opts, fallbackSourceRu
46
277
  const record = asRecord(event);
47
278
  if (!record || !Array.isArray(record.content))
48
279
  return event;
49
- const sourceRunId = firstString(record.runId, record.turnId, fallbackSourceRunId);
50
- const content = await relayOutgoingMediaContent(record.content, { ...opts, sourceRunId });
280
+ const eventRelation = extractOpenClawMessageToolRelation(record);
281
+ const sourceRunId = eventRelation?.sourceRunId
282
+ ?? firstString(record.runId, record.turnId, fallbackSourceRunId);
283
+ const sessionKey = canonicalRelationSessionKey(firstString(record.sessionKey, record.sessionId), opts);
284
+ const content = await relayOutgoingMediaContent(record.content, {
285
+ ...opts,
286
+ sourceRunId,
287
+ ...(sessionKey ? { sessionKey } : {}),
288
+ ...(eventRelation?.toolCallId ? { toolCallId: eventRelation.toolCallId } : {}),
289
+ });
51
290
  if (!content.changed)
52
291
  return event;
53
292
  changed = true;
@@ -87,49 +326,101 @@ export async function relayOutgoingMediaInHistoryResponse(response, opts) {
87
326
  return response;
88
327
  }
89
328
  const responseRecord = response;
90
- const responseSessionKey = firstString(responseRecord.sessionKey, responseRecord.sessionId);
91
- const explicitSidecars = normalizeOpenClawAssistantMediaSidecars(messages, responseSessionKey);
92
- const normalizedMessages = normalizeOpenClawAutomaticMediaReplies(explicitSidecars.messages, responseSessionKey);
93
- const messageResult = await relayOutgoingMediaInMessageList(normalizedMessages.messages, responseSessionKey, opts);
329
+ const responseSessionKey = canonicalRelationSessionKey(firstString(responseRecord.sessionKey, responseRecord.sessionId), opts);
94
330
  const snapshot = responseRecord.timelineSnapshot;
95
331
  const snapshotMessages = snapshot && typeof snapshot === "object" && !Array.isArray(snapshot)
96
332
  ? snapshot.messages
97
333
  : undefined;
334
+ const projectionV3 = Array.isArray(snapshotMessages)
335
+ && snapshotMessages.some((message) => (Boolean(message) && typeof message === "object" && message.projectionVersion === 3));
336
+ const projectionOptions = projectionV3 ? { projectionVersion: 3 } : undefined;
337
+ const relationOptions = {
338
+ ...(projectionOptions ?? {}),
339
+ ...(opts.sessionDefaults ? { sessionDefaults: opts.sessionDefaults } : {}),
340
+ };
341
+ const explicitSidecars = normalizeOpenClawAssistantMediaSidecars(messages, responseSessionKey, relationOptions);
342
+ const normalizedMessages = normalizeOpenClawAutomaticMediaReplies(explicitSidecars.messages, responseSessionKey, relationOptions);
343
+ const messageResult = await relayOutgoingMediaInMessageList(normalizedMessages.messages, responseSessionKey, opts);
98
344
  const explicitSnapshotSidecars = Array.isArray(snapshotMessages)
99
- ? normalizeOpenClawAssistantMediaSidecars(snapshotMessages, responseSessionKey)
345
+ ? normalizeOpenClawAssistantMediaSidecars(snapshotMessages, responseSessionKey, relationOptions)
100
346
  : undefined;
101
347
  const normalizedSnapshotMessages = explicitSnapshotSidecars
102
- ? normalizeOpenClawAutomaticMediaReplies(explicitSnapshotSidecars.messages, responseSessionKey)
348
+ ? normalizeOpenClawAutomaticMediaReplies(explicitSnapshotSidecars.messages, responseSessionKey, relationOptions)
103
349
  : undefined;
104
350
  const snapshotResult = normalizedSnapshotMessages
105
351
  ? await relayOutgoingMediaInMessageList(normalizedSnapshotMessages.messages, responseSessionKey, opts)
106
352
  : undefined;
353
+ // The history projector builds the canonical snapshot before this relay
354
+ // enriches the provider rows. Reuse the already-relayed row by its explicit
355
+ // source identity so the canonical timeline receives the same attachment
356
+ // blocks without re-reading local paths or merging by run/text/time.
357
+ const reconciledSnapshot = snapshotResult
358
+ ? reconcileHistorySnapshotWithRelayedMessages(snapshotResult.messages, messageResult.messages)
359
+ : undefined;
107
360
  const changed = explicitSidecars.changed
108
361
  || normalizedMessages.changed
109
362
  || messageResult.changed
110
363
  || Boolean(explicitSnapshotSidecars?.changed)
111
364
  || Boolean(normalizedSnapshotMessages?.changed)
112
- || Boolean(snapshotResult?.changed);
365
+ || Boolean(snapshotResult?.changed)
366
+ || Boolean(reconciledSnapshot?.changed);
113
367
  return changed
114
368
  ? {
115
369
  ...responseRecord,
116
370
  messages: messageResult.messages,
117
- ...(snapshotResult
371
+ ...(reconciledSnapshot
118
372
  ? {
119
373
  timelineSnapshot: {
120
374
  ...snapshot,
121
- messages: snapshotResult.messages,
375
+ messages: reconciledSnapshot.messages,
122
376
  },
123
377
  }
124
378
  : {}),
125
379
  }
126
380
  : response;
127
381
  }
382
+ function reconcileHistorySnapshotWithRelayedMessages(snapshotMessages, relayedMessages) {
383
+ const bySourceIdentity = new Map();
384
+ for (const message of relayedMessages) {
385
+ const record = asRecord(message);
386
+ const identity = historySourceIdentity(record);
387
+ if (record && identity)
388
+ bySourceIdentity.set(identity, record);
389
+ }
390
+ let changed = false;
391
+ const messages = snapshotMessages.map((message) => {
392
+ const record = asRecord(message);
393
+ const source = record ? bySourceIdentity.get(historySourceIdentity(record) ?? "") : undefined;
394
+ if (!record || !source || !Array.isArray(source.content) || source.content.length === 0) {
395
+ return message;
396
+ }
397
+ const content = source.content;
398
+ const attachmentIds = attachmentIdsFromContent(content);
399
+ const currentContent = Array.isArray(record.content) ? record.content : undefined;
400
+ const currentAttachmentIds = Array.isArray(record.attachmentIds) ? record.attachmentIds : undefined;
401
+ if (JSON.stringify(currentContent) === JSON.stringify(content)
402
+ && JSON.stringify(currentAttachmentIds ?? []) === JSON.stringify(attachmentIds)) {
403
+ return message;
404
+ }
405
+ changed = true;
406
+ return {
407
+ ...record,
408
+ content,
409
+ ...(attachmentIds.length > 0 ? { attachmentIds } : {}),
410
+ };
411
+ });
412
+ return { messages: changed ? messages : snapshotMessages, changed };
413
+ }
414
+ function historySourceIdentity(record) {
415
+ return firstString(record?.sourceMessageId, record?.id, record?.messageId, record?.message_id);
416
+ }
128
417
  async function relayOutgoingMediaInMessageList(messages, sessionKey, opts) {
129
418
  let changed = false;
130
419
  const nextMessages = await Promise.all(messages.map(async (message) => {
131
420
  const restoredMessage = restoreOpenClawInputMediaInHistoryMessage(message);
132
- const wrapperInput = sessionKey ? { sessionKey, message: restoredMessage } : { message: restoredMessage };
421
+ const wrapperInput = sessionKey
422
+ ? { sessionKey: canonicalRelationSessionKey(sessionKey, opts), message: restoredMessage }
423
+ : { message: restoredMessage };
133
424
  const wrapper = await relayOutgoingMediaInPayload(wrapperInput, opts);
134
425
  const nextMessage = wrapper.message ?? message;
135
426
  changed ||= nextMessage !== message;
@@ -180,6 +471,40 @@ async function relayOutgoingMediaBlock(block, opts) {
180
471
  }
181
472
  const source = block;
182
473
  const url = firstString(source.url, source.openUrl, source.downloadUrl, source.download_path, source.downloadPath);
474
+ if (url?.startsWith("media://inbound/") && opts.sessionKey && opts.sourceRunId) {
475
+ const attachmentId = `openclaw-input-${createHash("sha256").update(JSON.stringify([opts.sessionKey, opts.sourceRunId, url])).digest("hex").slice(0, 32)}`;
476
+ try {
477
+ const root = await realpath(join(opts.stateDir ?? resolveOpenClawStateDir(), "media", "inbound"));
478
+ const filePath = await realpath(resolve(root, decodeURIComponent(url.slice("media://inbound/".length))));
479
+ const pathWithinRoot = relative(root, filePath);
480
+ if (!pathWithinRoot || pathWithinRoot.startsWith("..") || isAbsolute(pathWithinRoot))
481
+ throw new Error("invalid_managed_media_path");
482
+ const cacheKey = await outgoingFileCacheKey({ gatewayId: opts.gatewayId, sessionKey: opts.sessionKey, identity: attachmentId, filePath, sourceRunId: opts.sourceRunId });
483
+ const upload = await cachedUpload(opts, cacheKey, {
484
+ relayServerUrl: opts.relayServerUrl, relaySecret: opts.relaySecret,
485
+ gatewayId: opts.gatewayId, sessionKey: opts.sessionKey, filePath,
486
+ sourceRunId: opts.sourceRunId, timelineDelivery: "embedded",
487
+ sourceRole: "user",
488
+ });
489
+ return {
490
+ ...uploadToContentBlock(upload),
491
+ attachmentId,
492
+ fileName: source.fileName || upload.fileName,
493
+ ...(opts.toolCallId ? { toolCallId: opts.toolCallId } : {}),
494
+ };
495
+ }
496
+ catch {
497
+ return {
498
+ type: source.type,
499
+ attachmentId,
500
+ fileName: source.fileName || "图片",
501
+ ...(opts.toolCallId ? { toolCallId: opts.toolCallId } : {}),
502
+ transferState: "expired",
503
+ isRemoteExpired: true,
504
+ attachmentStatusText: "图片文件暂不可用",
505
+ };
506
+ }
507
+ }
183
508
  const attachmentId = outgoingAttachmentId(url);
184
509
  if (!attachmentId) {
185
510
  return block;
@@ -190,11 +515,12 @@ async function relayOutgoingMediaBlock(block, opts) {
190
515
  try {
191
516
  const record = await readOutgoingMediaRecordWhenReady(attachmentId, opts);
192
517
  const filePath = record.original?.path?.trim();
193
- const sessionKey = record.sessionKey?.trim();
194
- if (!filePath || !sessionKey) {
518
+ const rawSessionKey = record.sessionKey?.trim();
519
+ if (!filePath || !rawSessionKey) {
195
520
  console.warn(`[relay] outgoing media record is incomplete attachment=${attachmentId}`);
196
521
  return undefined;
197
522
  }
523
+ const sessionKey = canonicalRelationSessionKey(rawSessionKey, opts) ?? rawSessionKey;
198
524
  const cacheKey = await outgoingFileCacheKey({
199
525
  gatewayId: opts.gatewayId,
200
526
  sessionKey,
@@ -211,6 +537,7 @@ async function relayOutgoingMediaBlock(block, opts) {
211
537
  senderDisplayName: opts.senderDisplayName,
212
538
  sourceRunId: opts.sourceRunId,
213
539
  timelineDelivery: "embedded",
540
+ sourceRole: "assistant",
214
541
  });
215
542
  return {
216
543
  ...source,
@@ -229,6 +556,8 @@ async function relayOutgoingMediaBlock(block, opts) {
229
556
  downloadPath: upload.downloadPath,
230
557
  expiresAt: upload.expiresAt,
231
558
  sourceRunId: upload.sourceRunId,
559
+ ...(opts.toolCallId ? { toolCallId: opts.toolCallId } : {}),
560
+ sourceRole: upload.sourceRole ?? "assistant",
232
561
  gatewayId: upload.gatewayId,
233
562
  sessionKey: upload.sessionKey,
234
563
  transferState: "available",
@@ -245,6 +574,7 @@ async function relayOutgoingMediaBlock(block, opts) {
245
574
  type: typeof source.type === "string" && source.type.trim() ? source.type : "image",
246
575
  attachmentId,
247
576
  fileName: firstString(source.fileName, source.alt, "图片"),
577
+ ...(opts.toolCallId ? { toolCallId: opts.toolCallId } : {}),
248
578
  transferState: "expired",
249
579
  isRemoteExpired: true,
250
580
  attachmentStatusText: "图片文件在桌面端已不可用",
@@ -264,7 +594,9 @@ function payloadSourceRunId(payload) {
264
594
  const messageRecord = message && typeof message === "object" && !Array.isArray(message)
265
595
  ? message
266
596
  : undefined;
267
- return firstString(payload.sourceRunId, payload.source_run_id, payload.runId, payload.turnId, payload.messageId, payload.id, messageRecord?.sourceRunId, messageRecord?.source_run_id, messageRecord?.runId, messageRecord?.turnId, messageRecord?.messageId, messageRecord?.id);
597
+ const metadata = asRecord(messageRecord?.__openclaw);
598
+ const inputId = firstString(messageRecord?.idempotencyKey, metadata?.idempotencyKey);
599
+ return firstString(payload.sourceRunId, payload.source_run_id, payload.runId, payload.turnId, payload.messageId, payload.id, messageRecord?.sourceRunId, messageRecord?.source_run_id, messageRecord?.runId, messageRecord?.turnId, metadata?.runId, inputId?.replace(/:user$/, ""), messageRecord?.messageId, messageRecord?.id, metadata?.id);
268
600
  }
269
601
  async function readOutgoingMediaRecord(attachmentId, options) {
270
602
  const stateDir = options.stateDir ?? resolveOpenClawStateDir();
@@ -287,7 +619,9 @@ async function readOutgoingMediaRecordWhenReady(attachmentId, options) {
287
619
  // enrichment avoids making ordinary history reads slower for genuinely
288
620
  // expired media while giving the transaction up to roughly one second to
289
621
  // become observable.
290
- const delaysMs = options.waitForOutgoingMediaRecord ? [80, 160, 320, 640] : [];
622
+ const delaysMs = options.waitForOutgoingMediaRecord
623
+ ? OUTGOING_MEDIA_RECORD_WAIT_DELAYS_MS
624
+ : [];
291
625
  let lastError;
292
626
  for (const delayMs of [...delaysMs, 0]) {
293
627
  if (delayMs > 0) {
@@ -383,29 +717,11 @@ function asRecord(value) {
383
717
  ? value
384
718
  : undefined;
385
719
  }
386
- async function relayLocalArtifactPathsInContent(content, payload, opts) {
387
- if (opts.userMessage === undefined) {
388
- return [];
389
- }
390
- if (content.some(isUploadedMediaBlock)) {
391
- return [];
392
- }
393
- const text = content
394
- .map((block) => {
395
- if (!block || typeof block !== "object" || Array.isArray(block)) {
396
- return "";
397
- }
398
- const record = block;
399
- return record.type === "text" && typeof record.text === "string" ? record.text : "";
400
- })
401
- .filter(Boolean)
402
- .join("\n");
403
- const paths = extractDeliverablePaths(text, opts.userMessage);
404
- if (paths.length === 0) {
405
- return [];
406
- }
407
- const sessionKey = firstString(payload.sessionKey, payload.message?.sessionKey) ?? "main";
408
- const runId = payloadSourceRunId(payload);
720
+ async function relayLocalArtifactPaths(paths, payload, opts, sourceRunIdOverride, toolCallId) {
721
+ const messageRecord = asRecord(payload.message);
722
+ const rawSessionKey = firstString(messageRecord?.sessionKey, messageRecord?.sessionId, payload.sessionKey) ?? "main";
723
+ const sessionKey = canonicalRelationSessionKey(rawSessionKey, opts) ?? rawSessionKey;
724
+ const runId = sourceRunIdOverride ?? payloadSourceRunId(payload);
409
725
  const blocks = [];
410
726
  for (const filePath of paths) {
411
727
  try {
@@ -425,9 +741,10 @@ async function relayLocalArtifactPathsInContent(content, payload, opts) {
425
741
  senderDisplayName: opts.senderDisplayName,
426
742
  sourceRunId: runId,
427
743
  timelineDelivery: "embedded",
744
+ sourceRole: "assistant",
428
745
  };
429
746
  const upload = await cachedUpload(opts, cacheKey, request);
430
- blocks.push(uploadToContentBlock(upload));
747
+ blocks.push(uploadToContentBlock(upload, toolCallId));
431
748
  }
432
749
  catch (error) {
433
750
  console.warn(`[relay] failed to publish local artifact ${filePath}: ${String(error)}`);
@@ -459,7 +776,7 @@ function attachmentIdsFromContent(content) {
459
776
  }
460
777
  return [...ids];
461
778
  }
462
- function uploadToContentBlock(upload) {
779
+ function uploadToContentBlock(upload, toolCallId) {
463
780
  const type = upload.mimeType.startsWith("image/")
464
781
  ? "image"
465
782
  : upload.mimeType.startsWith("audio/")
@@ -483,6 +800,8 @@ function uploadToContentBlock(upload) {
483
800
  downloadPath: upload.downloadPath,
484
801
  expiresAt: upload.expiresAt,
485
802
  sourceRunId: upload.sourceRunId,
803
+ ...(toolCallId ? { toolCallId } : {}),
804
+ sourceRole: upload.sourceRole,
486
805
  sha256: upload.sha256,
487
806
  contentHash: upload.sha256,
488
807
  gatewayId: upload.gatewayId,
@@ -543,6 +862,16 @@ async function cachedUpload(opts, cacheKey, request) {
543
862
  inFlight.delete(cacheKey);
544
863
  }
545
864
  }
865
+ function canonicalRelationSessionKey(rawSessionKey, opts) {
866
+ if (!rawSessionKey)
867
+ return undefined;
868
+ const gatewayKey = opts.sessionDefaults
869
+ ? canonicalizeSessionKey(rawSessionKey, opts.sessionDefaults)
870
+ : rawSessionKey;
871
+ if (typeof gatewayKey !== "string" || !gatewayKey.trim())
872
+ return undefined;
873
+ return canonicalizeOpenClawSessionScope(gatewayKey, opts.sessionDefaults);
874
+ }
546
875
  function sanitizeOpenClawMediaControlBlocks(content) {
547
876
  let changed = false;
548
877
  const nextContent = [];
@@ -603,71 +932,31 @@ const DELIVERABLE_EXTENSIONS = [
603
932
  ".pptx", ".ppt", ".odp", ".zip", ".tar", ".gz", ".tgz", ".bz2", ".7z",
604
933
  ".html", ".htm",
605
934
  ];
606
- const DELIVERABLE_EXTENSION_PATTERN = DELIVERABLE_EXTENSIONS
607
- .map((extension) => extension.slice(1).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
608
- .sort((a, b) => b.length - a.length)
609
- .join("|");
610
- const DELIVERABLE_PATH_START = String.raw `(?:~[\\/]|\/|[A-Za-z]:[\\/]|\\\\[^\\/\s"'` + "`" + String.raw `<>|]+[\\/])`;
611
- const DELIVERABLE_PATH_REGEX_SOURCE = DELIVERABLE_PATH_START + String.raw `[^\n"'` + "`" + String.raw `<>|]*?\.(?:${DELIVERABLE_EXTENSION_PATTERN})(?=$|[\s).,"'` + "`" + String.raw `,。;;::!??])`;
612
- const DELIVERABLE_PATH_REGEX = new RegExp(String.raw `(?:^|[\s("'` + "`" + String.raw `::])(${DELIVERABLE_PATH_REGEX_SOURCE})`, "gi");
613
- export function extractDeliverablePathCandidates(text) {
614
- return [...text.matchAll(DELIVERABLE_PATH_REGEX)].map((match) => match[1]);
615
- }
616
- function extractDeliverablePaths(text, userMessage) {
617
- if (userMessage !== undefined && !hasDeliverableSendIntent(userMessage)) {
618
- return [];
619
- }
620
- const allowed = new Set(DELIVERABLE_EXTENSIONS);
935
+ function trustedDeliverablePaths(values) {
621
936
  const paths = new Set();
622
- for (const rawPath of extractDeliverablePathCandidates(text)) {
623
- const absolutePath = rawPath.startsWith("~/") || rawPath.startsWith("~\\")
624
- ? join(homedir(), rawPath.slice(2))
625
- : rawPath;
626
- if (allowed.has(extname(absolutePath).toLowerCase()) && existsSync(resolve(absolutePath))) {
627
- paths.add(resolve(absolutePath));
628
- }
937
+ for (const value of values) {
938
+ if (typeof value !== "string")
939
+ continue;
940
+ const filePath = trustedDeliverablePath(value);
941
+ if (filePath)
942
+ paths.add(filePath);
629
943
  }
630
944
  return [...paths];
631
945
  }
632
- function hasDeliverableSendIntent(message) {
633
- const text = message.trim().toLowerCase();
634
- if (!text) {
635
- return false;
636
- }
637
- if (/(不要|不用|别|无需|不需要).{0,20}(发|发送|传|上传|转发|分享|附件|attach|send|upload|share|deliver)/.test(text)
638
- || /(只要|仅要).{0,20}(路径|文件名|名字|文本|文字)/.test(text)
639
- || /\b(do not|don't|dont|no need to|without|not need to)\b.{0,30}\b(send|upload|attach|share|deliver)\b/.test(text)
640
- || /\bonly\b.{0,30}\b(path|filename|file name|text)\b/.test(text)) {
641
- return false;
946
+ function trustedDeliverablePath(rawPath) {
947
+ const candidate = rawPath.trim();
948
+ if (!candidate)
949
+ return undefined;
950
+ if (!candidate.startsWith("~/") && !candidate.startsWith("~\\") && !isAbsoluteHostPath(candidate)) {
951
+ return undefined;
642
952
  }
643
- const sendVerbs = [
644
- "发", "发送", "传", "上传", "转发", "分享", "发给", "传给", "send", "upload", "attach", "share", "deliver",
645
- ].join("|");
646
- const deliverableWords = [
647
- "文件", "图片", "照片", "图", "附件", "文档", "报告", "表格", "截图", "压缩包", "备份", "录音", "音频", "视频",
648
- "file", "image", "photo", "picture", "attachment", "document", "report", "spreadsheet", "screenshot", "archive",
649
- "zip", "pdf", "doc", "docx", "xlsx", "ppt", "pptx",
650
- ].join("|");
651
- const pathIntentPattern = new RegExp(`(?:(${sendVerbs}).{0,80}${DELIVERABLE_PATH_REGEX_SOURCE}|${DELIVERABLE_PATH_REGEX_SOURCE}.{0,80}(${sendVerbs}))`);
652
- if (pathIntentPattern.test(text)) {
653
- return true;
654
- }
655
- if (new RegExp(`(你.{0,6}(能|可以|会)|能不能|可不可以|能否|是否|会不会|支持).{0,30}(${sendVerbs}).{0,30}(${deliverableWords}).{0,8}(吗|么|嘛|\\?|?)?`).test(text)) {
656
- return false;
953
+ const expandedPath = candidate.startsWith("~/") || candidate.startsWith("~\\")
954
+ ? join(homedir(), candidate.slice(2))
955
+ : candidate;
956
+ if (!DELIVERABLE_EXTENSIONS.includes(extname(expandedPath).toLowerCase())) {
957
+ return undefined;
657
958
  }
658
- const directChinesePatterns = [
659
- new RegExp(`(把|将).{0,50}(${deliverableWords}).{0,30}(${sendVerbs})(给我|到手机|到移动端|过来|回来|一下|给这边)?`),
660
- new RegExp(`(${sendVerbs})(这张|这个|这份|该|那张|那份|一张|几张|一些|些|一下)?[^,。,.!??]{0,24}(${deliverableWords})(给我|到手机|到移动端|过来|回来|一下|给这边)?`),
661
- new RegExp(`(${sendVerbs})(给我|到手机|到移动端|过来|回来).{0,50}(${deliverableWords})?`),
662
- new RegExp(`(给我|帮我).{0,20}(${sendVerbs}).{0,50}(${deliverableWords})`),
663
- ];
664
- if (directChinesePatterns.some((pattern) => pattern.test(text))) {
665
- return true;
666
- }
667
- const englishPatterns = [
668
- new RegExp(`\\b(send|upload|attach|share|deliver)\\b.{0,50}\\b(${deliverableWords})\\b`),
669
- new RegExp(`\\b(${deliverableWords})\\b.{0,50}\\b(send|upload|attach|share|deliver)\\b`),
670
- ];
671
- return englishPatterns.some((pattern) => pattern.test(text));
959
+ const filePath = resolve(expandedPath);
960
+ return existsSync(filePath) ? filePath : undefined;
672
961
  }
673
962
  //# sourceMappingURL=outgoing-media-relay.js.map