clawconnect-agent 0.2.5 → 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 +3 -0
  12. package/dist/core/relay/file-upload.js +4 -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 +28 -1
  75. package/dist/openclaw/relay/assistant-media-sidecar.js +270 -10
  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 +570 -49
  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 +35 -1
  95. package/dist/openclaw/relay/outgoing-media-relay.js +541 -123
  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 +329 -131
  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,42 +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
- import { extname, join, resolve } from "path";
4
+ import { isAbsolute, extname, join, relative, resolve } from "path";
5
+ import { DatabaseSync } from "node:sqlite";
5
6
  import { createHash } from "crypto";
6
7
  import { uploadFileToRelay } from "../../core/relay/file-upload.js";
7
8
  import { resolveOpenClawStateDir } from "../runtime/openclaw-paths.js";
8
- import { normalizeOpenClawAssistantMediaSidecars } from "./assistant-media-sidecar.js";
9
+ import { extractOpenClawMessageToolRelation, normalizeOpenClawAssistantMediaSidecars, normalizeOpenClawAutomaticMediaReplies, } from "./assistant-media-sidecar.js";
10
+ import { canonicalizeOpenClawSessionScope, canonicalizeSessionKey, } from "./session-context.js";
9
11
  const OUTGOING_MEDIA_RE = /\/api\/chat\/media\/outgoing\/[^/]+\/([^/]+)\/full(?:$|[?#])/;
10
12
  const OPENCLAW_MEDIA_CONTROL_PREFIX_RE = /^MEDIA:\s*(?:file:\/\/|~[\\/]|\/|[A-Za-z]:[\\/]|\\\\)/i;
11
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];
12
16
  const inFlightUploadsByCache = new WeakMap();
13
17
  export async function relayOutgoingMediaInPayload(payload, opts) {
14
18
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
15
19
  return payload;
16
20
  }
17
- const message = payload.message;
18
- if (!message || typeof message !== "object" || Array.isArray(message)) {
19
- return payload;
20
- }
21
21
  const payloadRecord = payload;
22
- const sourceRunId = payloadSourceRunId(payloadRecord);
23
- const messageContent = await relayOutgoingMediaContent(message.content, { ...opts, sourceRunId });
24
- 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
+ }
25
46
  const timelineEvents = await relayOutgoingMediaInTimelineEvents(payloadRecord.timelineEvents, opts, sourceRunId);
26
- 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) {
27
51
  return payload;
28
52
  }
29
53
  return {
30
54
  ...payloadRecord,
31
- message: {
32
- ...message,
33
- content: localArtifactBlocks.length > 0
34
- ? [...messageContent.blocks, ...localArtifactBlocks]
35
- : messageContent.content,
36
- },
37
- ...(timelineEvents.changed ? { timelineEvents: timelineEvents.events } : {}),
55
+ ...(nextMessage ? { message: nextMessage } : {}),
56
+ ...(timelineEvents.changed || deliveryTimelineEvents.changed
57
+ ? { timelineEvents: deliveryTimelineEvents.events }
58
+ : {}),
38
59
  };
39
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
+ }
40
272
  async function relayOutgoingMediaInTimelineEvents(events, opts, fallbackSourceRunId) {
41
273
  if (!Array.isArray(events))
42
274
  return { events, changed: false };
@@ -45,8 +277,16 @@ async function relayOutgoingMediaInTimelineEvents(events, opts, fallbackSourceRu
45
277
  const record = asRecord(event);
46
278
  if (!record || !Array.isArray(record.content))
47
279
  return event;
48
- const sourceRunId = firstString(record.runId, record.turnId, fallbackSourceRunId);
49
- 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
+ });
50
290
  if (!content.changed)
51
291
  return event;
52
292
  changed = true;
@@ -86,40 +326,101 @@ export async function relayOutgoingMediaInHistoryResponse(response, opts) {
86
326
  return response;
87
327
  }
88
328
  const responseRecord = response;
89
- const responseSessionKey = firstString(responseRecord.sessionKey, responseRecord.sessionId);
90
- const normalizedMessages = normalizeOpenClawAssistantMediaSidecars(messages, responseSessionKey);
91
- const messageResult = await relayOutgoingMediaInMessageList(normalizedMessages.messages, responseSessionKey, opts);
329
+ const responseSessionKey = canonicalRelationSessionKey(firstString(responseRecord.sessionKey, responseRecord.sessionId), opts);
92
330
  const snapshot = responseRecord.timelineSnapshot;
93
331
  const snapshotMessages = snapshot && typeof snapshot === "object" && !Array.isArray(snapshot)
94
332
  ? snapshot.messages
95
333
  : undefined;
96
- const normalizedSnapshotMessages = Array.isArray(snapshotMessages)
97
- ? normalizeOpenClawAssistantMediaSidecars(snapshotMessages, responseSessionKey)
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);
344
+ const explicitSnapshotSidecars = Array.isArray(snapshotMessages)
345
+ ? normalizeOpenClawAssistantMediaSidecars(snapshotMessages, responseSessionKey, relationOptions)
346
+ : undefined;
347
+ const normalizedSnapshotMessages = explicitSnapshotSidecars
348
+ ? normalizeOpenClawAutomaticMediaReplies(explicitSnapshotSidecars.messages, responseSessionKey, relationOptions)
98
349
  : undefined;
99
350
  const snapshotResult = normalizedSnapshotMessages
100
351
  ? await relayOutgoingMediaInMessageList(normalizedSnapshotMessages.messages, responseSessionKey, opts)
101
352
  : undefined;
102
- const changed = normalizedMessages.changed || messageResult.changed || Boolean(normalizedSnapshotMessages?.changed) || Boolean(snapshotResult?.changed);
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;
360
+ const changed = explicitSidecars.changed
361
+ || normalizedMessages.changed
362
+ || messageResult.changed
363
+ || Boolean(explicitSnapshotSidecars?.changed)
364
+ || Boolean(normalizedSnapshotMessages?.changed)
365
+ || Boolean(snapshotResult?.changed)
366
+ || Boolean(reconciledSnapshot?.changed);
103
367
  return changed
104
368
  ? {
105
369
  ...responseRecord,
106
370
  messages: messageResult.messages,
107
- ...(snapshotResult
371
+ ...(reconciledSnapshot
108
372
  ? {
109
373
  timelineSnapshot: {
110
374
  ...snapshot,
111
- messages: snapshotResult.messages,
375
+ messages: reconciledSnapshot.messages,
112
376
  },
113
377
  }
114
378
  : {}),
115
379
  }
116
380
  : response;
117
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
+ }
118
417
  async function relayOutgoingMediaInMessageList(messages, sessionKey, opts) {
119
418
  let changed = false;
120
419
  const nextMessages = await Promise.all(messages.map(async (message) => {
121
420
  const restoredMessage = restoreOpenClawInputMediaInHistoryMessage(message);
122
- const wrapperInput = sessionKey ? { sessionKey, message: restoredMessage } : { message: restoredMessage };
421
+ const wrapperInput = sessionKey
422
+ ? { sessionKey: canonicalRelationSessionKey(sessionKey, opts), message: restoredMessage }
423
+ : { message: restoredMessage };
123
424
  const wrapper = await relayOutgoingMediaInPayload(wrapperInput, opts);
124
425
  const nextMessage = wrapper.message ?? message;
125
426
  changed ||= nextMessage !== message;
@@ -170,6 +471,40 @@ async function relayOutgoingMediaBlock(block, opts) {
170
471
  }
171
472
  const source = block;
172
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
+ }
173
508
  const attachmentId = outgoingAttachmentId(url);
174
509
  if (!attachmentId) {
175
510
  return block;
@@ -178,13 +513,14 @@ async function relayOutgoingMediaBlock(block, opts) {
178
513
  return block;
179
514
  }
180
515
  try {
181
- const record = await readOutgoingMediaRecord(attachmentId, opts.recordsDir);
516
+ const record = await readOutgoingMediaRecordWhenReady(attachmentId, opts);
182
517
  const filePath = record.original?.path?.trim();
183
- const sessionKey = record.sessionKey?.trim();
184
- if (!filePath || !sessionKey) {
518
+ const rawSessionKey = record.sessionKey?.trim();
519
+ if (!filePath || !rawSessionKey) {
185
520
  console.warn(`[relay] outgoing media record is incomplete attachment=${attachmentId}`);
186
521
  return undefined;
187
522
  }
523
+ const sessionKey = canonicalRelationSessionKey(rawSessionKey, opts) ?? rawSessionKey;
188
524
  const cacheKey = await outgoingFileCacheKey({
189
525
  gatewayId: opts.gatewayId,
190
526
  sessionKey,
@@ -200,6 +536,8 @@ async function relayOutgoingMediaBlock(block, opts) {
200
536
  filePath,
201
537
  senderDisplayName: opts.senderDisplayName,
202
538
  sourceRunId: opts.sourceRunId,
539
+ timelineDelivery: "embedded",
540
+ sourceRole: "assistant",
203
541
  });
204
542
  return {
205
543
  ...source,
@@ -218,6 +556,8 @@ async function relayOutgoingMediaBlock(block, opts) {
218
556
  downloadPath: upload.downloadPath,
219
557
  expiresAt: upload.expiresAt,
220
558
  sourceRunId: upload.sourceRunId,
559
+ ...(opts.toolCallId ? { toolCallId: opts.toolCallId } : {}),
560
+ sourceRole: upload.sourceRole ?? "assistant",
221
561
  gatewayId: upload.gatewayId,
222
562
  sessionKey: upload.sessionKey,
223
563
  transferState: "available",
@@ -225,7 +565,21 @@ async function relayOutgoingMediaBlock(block, opts) {
225
565
  }
226
566
  catch (error) {
227
567
  console.warn(`[relay] failed to publish outgoing media attachment ${attachmentId}: ${String(error)}`);
228
- return undefined;
568
+ // The desktop may clean up an outgoing-media record before the Relay has
569
+ // uploaded it. Keep a stable, explicit unavailable attachment instead of
570
+ // deleting the only content of an otherwise valid assistant reply.
571
+ const { url: _url, openUrl: _openUrl, downloadUrl: _downloadUrl, download_path: _downloadPath, downloadPath: _downloadPathCamel, ...unavailableSource } = source;
572
+ return {
573
+ ...unavailableSource,
574
+ type: typeof source.type === "string" && source.type.trim() ? source.type : "image",
575
+ attachmentId,
576
+ fileName: firstString(source.fileName, source.alt, "图片"),
577
+ ...(opts.toolCallId ? { toolCallId: opts.toolCallId } : {}),
578
+ transferState: "expired",
579
+ isRemoteExpired: true,
580
+ attachmentStatusText: "图片文件在桌面端已不可用",
581
+ uploadStatusText: "图片文件在桌面端已不可用",
582
+ };
229
583
  }
230
584
  }
231
585
  function outgoingAttachmentId(url) {
@@ -240,12 +594,120 @@ function payloadSourceRunId(payload) {
240
594
  const messageRecord = message && typeof message === "object" && !Array.isArray(message)
241
595
  ? message
242
596
  : undefined;
243
- 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);
600
+ }
601
+ async function readOutgoingMediaRecord(attachmentId, options) {
602
+ const stateDir = options.stateDir ?? resolveOpenClawStateDir();
603
+ const recordsDir = options.recordsDir ?? join(stateDir, "media", "outgoing", "records");
604
+ try {
605
+ const raw = await readFile(join(recordsDir, `${attachmentId}.json`), "utf8");
606
+ return JSON.parse(raw);
607
+ }
608
+ catch (error) {
609
+ // A caller that supplies recordsDir is explicitly testing or using the
610
+ // legacy JSON store; it must not read the host's normal state database.
611
+ if (options.recordsDir || !isFileNotFoundError(error))
612
+ throw error;
613
+ }
614
+ return readSqliteOutgoingMediaRecord(attachmentId, stateDir);
615
+ }
616
+ async function readOutgoingMediaRecordWhenReady(attachmentId, options) {
617
+ // OpenClaw emits the assistant-media event before its SQLite transaction is
618
+ // occasionally visible to another process. Retrying only live event
619
+ // enrichment avoids making ordinary history reads slower for genuinely
620
+ // expired media while giving the transaction up to roughly one second to
621
+ // become observable.
622
+ const delaysMs = options.waitForOutgoingMediaRecord
623
+ ? OUTGOING_MEDIA_RECORD_WAIT_DELAYS_MS
624
+ : [];
625
+ let lastError;
626
+ for (const delayMs of [...delaysMs, 0]) {
627
+ if (delayMs > 0) {
628
+ await wait(delayMs);
629
+ }
630
+ try {
631
+ return await readOutgoingMediaRecord(attachmentId, options);
632
+ }
633
+ catch (error) {
634
+ lastError = error;
635
+ if (!isFileNotFoundError(error))
636
+ throw error;
637
+ }
638
+ }
639
+ throw lastError;
640
+ }
641
+ function wait(delayMs) {
642
+ return new Promise((resolve) => setTimeout(resolve, delayMs));
643
+ }
644
+ function readSqliteOutgoingMediaRecord(attachmentId, stateDir) {
645
+ const database = new DatabaseSync(join(stateDir, "state", "openclaw.sqlite"), { readOnly: true });
646
+ try {
647
+ const row = database.prepare(`
648
+ SELECT
649
+ session_key,
650
+ alt,
651
+ original_media_root,
652
+ original_media_id,
653
+ original_media_subdir,
654
+ original_content_type,
655
+ original_width,
656
+ original_height,
657
+ original_size_bytes,
658
+ original_filename
659
+ FROM managed_outgoing_image_records
660
+ WHERE attachment_id = ? AND cleanup_pending = 0
661
+ `).get(attachmentId);
662
+ if (!row) {
663
+ const error = new Error(`OpenClaw outgoing media record was not found: ${attachmentId}`);
664
+ error.code = "ENOENT";
665
+ throw error;
666
+ }
667
+ const mediaRoot = requiredRecordString(row, "original_media_root");
668
+ const mediaId = requiredRecordString(row, "original_media_id");
669
+ const mediaSubdir = requiredRecordString(row, "original_media_subdir");
670
+ if (!isAbsolute(mediaRoot) || mediaSubdir !== "outgoing/originals" || mediaId !== mediaId.split(/[\\/]/).pop()) {
671
+ throw new Error(`OpenClaw outgoing media record is unsafe: ${attachmentId}`);
672
+ }
673
+ const originalsDir = resolve(mediaRoot, "outgoing", "originals");
674
+ const filePath = resolve(mediaRoot, mediaSubdir, mediaId);
675
+ const relativePath = relative(originalsDir, filePath);
676
+ if (!relativePath || relativePath.startsWith("..") || isAbsolute(relativePath)) {
677
+ throw new Error(`OpenClaw outgoing media path is outside the managed originals directory: ${attachmentId}`);
678
+ }
679
+ return {
680
+ attachmentId,
681
+ sessionKey: requiredRecordString(row, "session_key"),
682
+ alt: optionalRecordString(row.alt),
683
+ original: {
684
+ path: filePath,
685
+ contentType: optionalRecordString(row.original_content_type),
686
+ width: optionalRecordNumber(row.original_width),
687
+ height: optionalRecordNumber(row.original_height),
688
+ sizeBytes: optionalRecordNumber(row.original_size_bytes),
689
+ filename: optionalRecordString(row.original_filename),
690
+ },
691
+ };
692
+ }
693
+ finally {
694
+ database.close();
695
+ }
696
+ }
697
+ function isFileNotFoundError(error) {
698
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
699
+ }
700
+ function requiredRecordString(row, field) {
701
+ const value = optionalRecordString(row[field]);
702
+ if (!value)
703
+ throw new Error(`OpenClaw outgoing media record is missing ${field}`);
704
+ return value;
244
705
  }
245
- async function readOutgoingMediaRecord(attachmentId, recordsDir) {
246
- const root = recordsDir ?? join(resolveOpenClawStateDir(), "media", "outgoing", "records");
247
- const raw = await readFile(join(root, `${attachmentId}.json`), "utf8");
248
- return JSON.parse(raw);
706
+ function optionalRecordString(value) {
707
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
708
+ }
709
+ function optionalRecordNumber(value) {
710
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
249
711
  }
250
712
  function firstString(...values) {
251
713
  return values.find((value) => typeof value === "string" && value.trim().length > 0)?.trim();
@@ -255,29 +717,11 @@ function asRecord(value) {
255
717
  ? value
256
718
  : undefined;
257
719
  }
258
- async function relayLocalArtifactPathsInContent(content, payload, opts) {
259
- if (opts.userMessage === undefined) {
260
- return [];
261
- }
262
- if (content.some(isUploadedMediaBlock)) {
263
- return [];
264
- }
265
- const text = content
266
- .map((block) => {
267
- if (!block || typeof block !== "object" || Array.isArray(block)) {
268
- return "";
269
- }
270
- const record = block;
271
- return record.type === "text" && typeof record.text === "string" ? record.text : "";
272
- })
273
- .filter(Boolean)
274
- .join("\n");
275
- const paths = extractDeliverablePaths(text, opts.userMessage);
276
- if (paths.length === 0) {
277
- return [];
278
- }
279
- const sessionKey = firstString(payload.sessionKey, payload.message?.sessionKey) ?? "main";
280
- 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);
281
725
  const blocks = [];
282
726
  for (const filePath of paths) {
283
727
  try {
@@ -296,9 +740,11 @@ async function relayLocalArtifactPathsInContent(content, payload, opts) {
296
740
  filePath,
297
741
  senderDisplayName: opts.senderDisplayName,
298
742
  sourceRunId: runId,
743
+ timelineDelivery: "embedded",
744
+ sourceRole: "assistant",
299
745
  };
300
746
  const upload = await cachedUpload(opts, cacheKey, request);
301
- blocks.push(uploadToContentBlock(upload));
747
+ blocks.push(uploadToContentBlock(upload, toolCallId));
302
748
  }
303
749
  catch (error) {
304
750
  console.warn(`[relay] failed to publish local artifact ${filePath}: ${String(error)}`);
@@ -330,7 +776,7 @@ function attachmentIdsFromContent(content) {
330
776
  }
331
777
  return [...ids];
332
778
  }
333
- function uploadToContentBlock(upload) {
779
+ function uploadToContentBlock(upload, toolCallId) {
334
780
  const type = upload.mimeType.startsWith("image/")
335
781
  ? "image"
336
782
  : upload.mimeType.startsWith("audio/")
@@ -354,6 +800,8 @@ function uploadToContentBlock(upload) {
354
800
  downloadPath: upload.downloadPath,
355
801
  expiresAt: upload.expiresAt,
356
802
  sourceRunId: upload.sourceRunId,
803
+ ...(toolCallId ? { toolCallId } : {}),
804
+ sourceRole: upload.sourceRole,
357
805
  sha256: upload.sha256,
358
806
  contentHash: upload.sha256,
359
807
  gatewayId: upload.gatewayId,
@@ -414,6 +862,16 @@ async function cachedUpload(opts, cacheKey, request) {
414
862
  inFlight.delete(cacheKey);
415
863
  }
416
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
+ }
417
875
  function sanitizeOpenClawMediaControlBlocks(content) {
418
876
  let changed = false;
419
877
  const nextContent = [];
@@ -474,71 +932,31 @@ const DELIVERABLE_EXTENSIONS = [
474
932
  ".pptx", ".ppt", ".odp", ".zip", ".tar", ".gz", ".tgz", ".bz2", ".7z",
475
933
  ".html", ".htm",
476
934
  ];
477
- const DELIVERABLE_EXTENSION_PATTERN = DELIVERABLE_EXTENSIONS
478
- .map((extension) => extension.slice(1).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
479
- .sort((a, b) => b.length - a.length)
480
- .join("|");
481
- const DELIVERABLE_PATH_START = String.raw `(?:~[\\/]|\/|[A-Za-z]:[\\/]|\\\\[^\\/\s"'` + "`" + String.raw `<>|]+[\\/])`;
482
- const DELIVERABLE_PATH_REGEX_SOURCE = DELIVERABLE_PATH_START + String.raw `[^\n"'` + "`" + String.raw `<>|]*?\.(?:${DELIVERABLE_EXTENSION_PATTERN})(?=$|[\s).,"'` + "`" + String.raw `,。;;::!??])`;
483
- const DELIVERABLE_PATH_REGEX = new RegExp(String.raw `(?:^|[\s("'` + "`" + String.raw `::])(${DELIVERABLE_PATH_REGEX_SOURCE})`, "gi");
484
- export function extractDeliverablePathCandidates(text) {
485
- return [...text.matchAll(DELIVERABLE_PATH_REGEX)].map((match) => match[1]);
486
- }
487
- function extractDeliverablePaths(text, userMessage) {
488
- if (userMessage !== undefined && !hasDeliverableSendIntent(userMessage)) {
489
- return [];
490
- }
491
- const allowed = new Set(DELIVERABLE_EXTENSIONS);
935
+ function trustedDeliverablePaths(values) {
492
936
  const paths = new Set();
493
- for (const rawPath of extractDeliverablePathCandidates(text)) {
494
- const absolutePath = rawPath.startsWith("~/") || rawPath.startsWith("~\\")
495
- ? join(homedir(), rawPath.slice(2))
496
- : rawPath;
497
- if (allowed.has(extname(absolutePath).toLowerCase()) && existsSync(resolve(absolutePath))) {
498
- paths.add(resolve(absolutePath));
499
- }
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);
500
943
  }
501
944
  return [...paths];
502
945
  }
503
- function hasDeliverableSendIntent(message) {
504
- const text = message.trim().toLowerCase();
505
- if (!text) {
506
- return false;
507
- }
508
- if (/(不要|不用|别|无需|不需要).{0,20}(发|发送|传|上传|转发|分享|附件|attach|send|upload|share|deliver)/.test(text)
509
- || /(只要|仅要).{0,20}(路径|文件名|名字|文本|文字)/.test(text)
510
- || /\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)
511
- || /\bonly\b.{0,30}\b(path|filename|file name|text)\b/.test(text)) {
512
- 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;
513
952
  }
514
- const sendVerbs = [
515
- "发", "发送", "传", "上传", "转发", "分享", "发给", "传给", "send", "upload", "attach", "share", "deliver",
516
- ].join("|");
517
- const deliverableWords = [
518
- "文件", "图片", "照片", "图", "附件", "文档", "报告", "表格", "截图", "压缩包", "备份", "录音", "音频", "视频",
519
- "file", "image", "photo", "picture", "attachment", "document", "report", "spreadsheet", "screenshot", "archive",
520
- "zip", "pdf", "doc", "docx", "xlsx", "ppt", "pptx",
521
- ].join("|");
522
- const pathIntentPattern = new RegExp(`(?:(${sendVerbs}).{0,80}${DELIVERABLE_PATH_REGEX_SOURCE}|${DELIVERABLE_PATH_REGEX_SOURCE}.{0,80}(${sendVerbs}))`);
523
- if (pathIntentPattern.test(text)) {
524
- return true;
525
- }
526
- if (new RegExp(`(你.{0,6}(能|可以|会)|能不能|可不可以|能否|是否|会不会|支持).{0,30}(${sendVerbs}).{0,30}(${deliverableWords}).{0,8}(吗|么|嘛|\\?|?)?`).test(text)) {
527
- 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;
528
958
  }
529
- const directChinesePatterns = [
530
- new RegExp(`(把|将).{0,50}(${deliverableWords}).{0,30}(${sendVerbs})(给我|到手机|到移动端|过来|回来|一下|给这边)?`),
531
- new RegExp(`(${sendVerbs})(这张|这个|这份|该|那张|那份|一张|几张|一些|些|一下)?[^,。,.!??]{0,24}(${deliverableWords})(给我|到手机|到移动端|过来|回来|一下|给这边)?`),
532
- new RegExp(`(${sendVerbs})(给我|到手机|到移动端|过来|回来).{0,50}(${deliverableWords})?`),
533
- new RegExp(`(给我|帮我).{0,20}(${sendVerbs}).{0,50}(${deliverableWords})`),
534
- ];
535
- if (directChinesePatterns.some((pattern) => pattern.test(text))) {
536
- return true;
537
- }
538
- const englishPatterns = [
539
- new RegExp(`\\b(send|upload|attach|share|deliver)\\b.{0,50}\\b(${deliverableWords})\\b`),
540
- new RegExp(`\\b(${deliverableWords})\\b.{0,50}\\b(send|upload|attach|share|deliver)\\b`),
541
- ];
542
- return englishPatterns.some((pattern) => pattern.test(text));
959
+ const filePath = resolve(expandedPath);
960
+ return existsSync(filePath) ? filePath : undefined;
543
961
  }
544
962
  //# sourceMappingURL=outgoing-media-relay.js.map