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.
- package/README.md +7 -3
- package/README.zh-CN.md +7 -3
- package/dist/commands/hermes-file-transfer-outcome.d.ts +18 -0
- package/dist/commands/hermes-file-transfer-outcome.js +40 -0
- package/dist/commands/hermes-file-transfer-outcome.js.map +1 -0
- package/dist/commands/send-file.js +39 -33
- package/dist/commands/send-file.js.map +1 -1
- package/dist/core/command-types.d.ts +5 -0
- package/dist/core/relay/chat-payload.js +32 -22
- package/dist/core/relay/chat-payload.js.map +1 -1
- package/dist/core/relay/file-upload.d.ts +3 -0
- package/dist/core/relay/file-upload.js +4 -0
- package/dist/core/relay/file-upload.js.map +1 -1
- package/dist/core/relay/mobile-chat-run-bridge.d.ts +53 -8
- package/dist/core/relay/mobile-chat-run-bridge.js +45 -0
- package/dist/core/relay/mobile-chat-run-bridge.js.map +1 -1
- package/dist/core/relay/office-payload.js +54 -22
- package/dist/core/relay/office-payload.js.map +1 -1
- package/dist/core/relay/source-commit.d.ts +41 -0
- package/dist/core/relay/source-commit.js +118 -0
- package/dist/core/relay/source-commit.js.map +1 -0
- package/dist/core/relay/timeline-event-builder.d.ts +6 -0
- package/dist/core/relay/timeline-event-builder.js +3 -0
- package/dist/core/relay/timeline-event-builder.js.map +1 -1
- package/dist/core/relay/timeline-event-log.d.ts +29 -0
- package/dist/core/relay/timeline-event-log.js +37 -0
- package/dist/core/relay/timeline-event-log.js.map +1 -1
- package/dist/core/relay/timeline-projection-v3.d.ts +63 -0
- package/dist/core/relay/timeline-projection-v3.js +87 -0
- package/dist/core/relay/timeline-projection-v3.js.map +1 -0
- package/dist/hermes/hermes-relay-manager.js +3 -0
- package/dist/hermes/hermes-relay-manager.js.map +1 -1
- package/dist/hermes/hermes-runtime-test-support.d.ts +3 -3
- package/dist/hermes/hermes-runtime-test-support.js +12 -6
- package/dist/hermes/hermes-runtime-test-support.js.map +1 -1
- package/dist/hermes/runtime/hermes-file-transfer-state.d.ts +33 -0
- package/dist/hermes/runtime/hermes-file-transfer-state.js +139 -0
- package/dist/hermes/runtime/hermes-file-transfer-state.js.map +1 -0
- package/dist/hermes/runtime/hermes-mobile-file-route-store.d.ts +17 -0
- package/dist/hermes/runtime/hermes-mobile-file-route-store.js +103 -0
- package/dist/hermes/runtime/hermes-mobile-file-route-store.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-api-client.d.ts +1 -0
- package/dist/hermes/runtime/hermes-runtime-api-client.js +16 -0
- package/dist/hermes/runtime/hermes-runtime-api-client.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-chat.d.ts +35 -4
- package/dist/hermes/runtime/hermes-runtime-chat.js +426 -51
- package/dist/hermes/runtime/hermes-runtime-chat.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-command-router.js +9 -1
- package/dist/hermes/runtime/hermes-runtime-command-router.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-cron.d.ts +1 -0
- package/dist/hermes/runtime/hermes-runtime-cron.js +34 -66
- package/dist/hermes/runtime/hermes-runtime-cron.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-history-completion.d.ts +8 -1
- package/dist/hermes/runtime/hermes-runtime-history-completion.js +112 -41
- package/dist/hermes/runtime/hermes-runtime-history-completion.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-history.js +92 -17
- package/dist/hermes/runtime/hermes-runtime-history.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-models.js +4 -1
- package/dist/hermes/runtime/hermes-runtime-models.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-process.js +4 -3
- package/dist/hermes/runtime/hermes-runtime-process.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-state-db.d.ts +6 -0
- package/dist/hermes/runtime/hermes-runtime-state-db.js +20 -1
- package/dist/hermes/runtime/hermes-runtime-state-db.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-tool-log-watcher.js +12 -20
- package/dist/hermes/runtime/hermes-runtime-tool-log-watcher.js.map +1 -1
- package/dist/index.js +16 -2
- package/dist/index.js.map +1 -1
- package/dist/openclaw/config/provider-config.js +4 -1
- package/dist/openclaw/config/provider-config.js.map +1 -1
- package/dist/openclaw/relay/agent-identity-helpers.d.ts +9 -0
- package/dist/openclaw/relay/agent-identity-helpers.js +118 -0
- package/dist/openclaw/relay/agent-identity-helpers.js.map +1 -0
- package/dist/openclaw/relay/assistant-media-sidecar.d.ts +28 -1
- package/dist/openclaw/relay/assistant-media-sidecar.js +270 -10
- package/dist/openclaw/relay/assistant-media-sidecar.js.map +1 -1
- package/dist/openclaw/relay/chat-history.d.ts +37 -2
- package/dist/openclaw/relay/chat-history.js +570 -49
- package/dist/openclaw/relay/chat-history.js.map +1 -1
- package/dist/openclaw/relay/chat-run-identity.js +3 -1
- package/dist/openclaw/relay/chat-run-identity.js.map +1 -1
- package/dist/openclaw/relay/gateway-history-projection.d.ts +4 -0
- package/dist/openclaw/relay/gateway-history-projection.js +67 -0
- package/dist/openclaw/relay/gateway-history-projection.js.map +1 -0
- package/dist/openclaw/relay/openclaw-active-run-state.d.ts +67 -0
- package/dist/openclaw/relay/openclaw-active-run-state.js +283 -0
- package/dist/openclaw/relay/openclaw-active-run-state.js.map +1 -0
- package/dist/openclaw/relay/openclaw-message-tool-real-shape.fixture.d.ts +12 -0
- package/dist/openclaw/relay/openclaw-message-tool-real-shape.fixture.js +48 -0
- package/dist/openclaw/relay/openclaw-message-tool-real-shape.fixture.js.map +1 -0
- package/dist/openclaw/relay/openclaw-source-commit-watcher.d.ts +22 -0
- package/dist/openclaw/relay/openclaw-source-commit-watcher.js +64 -0
- package/dist/openclaw/relay/openclaw-source-commit-watcher.js.map +1 -0
- package/dist/openclaw/relay/outgoing-media-relay.d.ts +35 -1
- package/dist/openclaw/relay/outgoing-media-relay.js +541 -123
- package/dist/openclaw/relay/outgoing-media-relay.js.map +1 -1
- package/dist/openclaw/relay/relay-manager-history-timing.d.ts +1 -4
- package/dist/openclaw/relay/relay-manager-history-timing.js +4 -4
- package/dist/openclaw/relay/relay-manager-history-timing.js.map +1 -1
- package/dist/openclaw/relay/relay-manager-payload-helpers.d.ts +28 -1
- package/dist/openclaw/relay/relay-manager-payload-helpers.js +219 -11
- package/dist/openclaw/relay/relay-manager-payload-helpers.js.map +1 -1
- package/dist/openclaw/relay/session-context.d.ts +15 -0
- package/dist/openclaw/relay/session-context.js +48 -4
- package/dist/openclaw/relay/session-context.js.map +1 -1
- package/dist/openclaw/relay/source-commit-projection.d.ts +25 -0
- package/dist/openclaw/relay/source-commit-projection.js +140 -0
- package/dist/openclaw/relay/source-commit-projection.js.map +1 -0
- package/dist/openclaw/relay/transcript-dedupe.js +11 -42
- package/dist/openclaw/relay/transcript-dedupe.js.map +1 -1
- package/dist/openclaw/relay-manager.js +329 -131
- package/dist/openclaw/relay-manager.js.map +1 -1
- package/dist/openclaw/runtime/local-runtime.js +4 -2
- package/dist/openclaw/runtime/local-runtime.js.map +1 -1
- package/package.json +1 -1
- package/dist/openclaw/session-store.d.ts +0 -6
- package/dist/openclaw/session-store.js +0 -231
- 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
|
|
23
|
-
const
|
|
24
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
49
|
-
const
|
|
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
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
...(
|
|
371
|
+
...(reconciledSnapshot
|
|
108
372
|
? {
|
|
109
373
|
timelineSnapshot: {
|
|
110
374
|
...snapshot,
|
|
111
|
-
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
|
|
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
|
|
516
|
+
const record = await readOutgoingMediaRecordWhenReady(attachmentId, opts);
|
|
182
517
|
const filePath = record.original?.path?.trim();
|
|
183
|
-
const
|
|
184
|
-
if (!filePath || !
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
if (
|
|
498
|
-
paths.add(
|
|
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
|
|
504
|
-
const
|
|
505
|
-
if (!
|
|
506
|
-
return
|
|
507
|
-
|
|
508
|
-
|
|
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
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
|
530
|
-
|
|
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
|