@cecwxf/wtt 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/channel.d.ts +3 -0
- package/dist/channel.d.ts.map +1 -1
- package/dist/channel.js +429 -5
- package/dist/channel.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/plugin-config.d.ts +0 -32
- package/dist/plugin-config.d.ts.map +0 -1
- package/dist/plugin-config.js +0 -268
- package/dist/plugin-config.js.map +0 -1
package/dist/channel.d.ts
CHANGED
|
@@ -151,6 +151,9 @@ export type NormalizedInboundWsMessage = {
|
|
|
151
151
|
to: string;
|
|
152
152
|
from: string;
|
|
153
153
|
conversationLabel: string;
|
|
154
|
+
/** Image URLs extracted from message content (HTML <img> or markdown ) */
|
|
155
|
+
mediaUrls: string[];
|
|
156
|
+
mediaTypes: string[];
|
|
154
157
|
};
|
|
155
158
|
export declare function normalizeInboundWsMessage(params: {
|
|
156
159
|
msg: WsNewMessage;
|
package/dist/channel.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EACV,gBAAgB,EAChB,kBAAkB,EAClB,YAAY,EAGb,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAYpE,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"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"}
|
package/dist/channel.js
CHANGED
|
@@ -18,7 +18,7 @@ import { WTTCloudClient } from "./ws-client.js";
|
|
|
18
18
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
19
19
|
import { createRequire } from "node:module";
|
|
20
20
|
import os from "node:os";
|
|
21
|
-
import { dirname, join as joinPath } from "node:path";
|
|
21
|
+
import { dirname, extname, join as joinPath } from "node:path";
|
|
22
22
|
import { randomBytes } from "node:crypto";
|
|
23
23
|
import { pathToFileURL } from "node:url";
|
|
24
24
|
const DEFAULT_ACCOUNT_ID = "default";
|
|
@@ -34,10 +34,15 @@ const DEFAULT_SLASH_COMPAT_ENABLED = false;
|
|
|
34
34
|
const DEFAULT_SLASH_COMPAT_WTT_PREFIX_ONLY = true;
|
|
35
35
|
const DEFAULT_SLASH_BYPASS_MENTION_GATE = false;
|
|
36
36
|
const DEFAULT_NATURAL_BRIDGE_MIN_DOING_MS = 2500;
|
|
37
|
+
const DEFAULT_INBOUND_MEDIA_MAX_BYTES = 15 * 1024 * 1024;
|
|
38
|
+
const DEFAULT_INBOUND_MEDIA_MAX_PER_MESSAGE = 4;
|
|
39
|
+
const DEFAULT_INBOUND_MEDIA_FETCH_TIMEOUT_MS = 20_000;
|
|
37
40
|
const hooks = { before: [], after: [] };
|
|
38
41
|
const clients = new Map();
|
|
39
42
|
const topicTypeCache = new Map();
|
|
43
|
+
const recentTopicMediaCache = new Map();
|
|
40
44
|
const DEFAULT_P2P_E2E_ENABLED = true;
|
|
45
|
+
const RECENT_TOPIC_MEDIA_TTL_MS = 10 * 60_000;
|
|
41
46
|
function registerHook(phase, fn) {
|
|
42
47
|
if (phase === "before_tool_call")
|
|
43
48
|
hooks.before.push(fn);
|
|
@@ -165,6 +170,73 @@ function rememberTopicType(topicId, topicType) {
|
|
|
165
170
|
topicTypeCache.delete(oldest);
|
|
166
171
|
}
|
|
167
172
|
}
|
|
173
|
+
function recentTopicMediaKey(topicId, senderId) {
|
|
174
|
+
return `${topicId.trim()}::${senderId.trim()}`;
|
|
175
|
+
}
|
|
176
|
+
function rememberRecentTopicMedia(topicId, senderId, mediaUrls, mediaTypes) {
|
|
177
|
+
const topic = topicId.trim();
|
|
178
|
+
const sender = senderId.trim();
|
|
179
|
+
if (!topic || !sender || mediaUrls.length === 0)
|
|
180
|
+
return;
|
|
181
|
+
const dedupUrls = Array.from(new Set(mediaUrls.map((item) => String(item || "").trim()).filter(Boolean)));
|
|
182
|
+
const dedupTypes = mediaTypes.slice(0, dedupUrls.length);
|
|
183
|
+
if (dedupUrls.length === 0)
|
|
184
|
+
return;
|
|
185
|
+
recentTopicMediaCache.set(recentTopicMediaKey(topic, sender), {
|
|
186
|
+
at: Date.now(),
|
|
187
|
+
mediaUrls: dedupUrls,
|
|
188
|
+
mediaTypes: dedupTypes.length > 0 ? dedupTypes : new Array(dedupUrls.length).fill("image/png"),
|
|
189
|
+
});
|
|
190
|
+
while (recentTopicMediaCache.size > 5000) {
|
|
191
|
+
const oldest = recentTopicMediaCache.keys().next().value;
|
|
192
|
+
if (!oldest)
|
|
193
|
+
break;
|
|
194
|
+
recentTopicMediaCache.delete(oldest);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function readRecentTopicMedia(topicId, senderId) {
|
|
198
|
+
const now = Date.now();
|
|
199
|
+
const key = recentTopicMediaKey(topicId, senderId);
|
|
200
|
+
const direct = recentTopicMediaCache.get(key);
|
|
201
|
+
if (direct) {
|
|
202
|
+
if (now - direct.at <= RECENT_TOPIC_MEDIA_TTL_MS) {
|
|
203
|
+
return {
|
|
204
|
+
mediaUrls: direct.mediaUrls.slice(),
|
|
205
|
+
mediaTypes: direct.mediaTypes.slice(),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
recentTopicMediaCache.delete(key);
|
|
209
|
+
}
|
|
210
|
+
// Fallback: some environments may surface a changing sender_id for HUMAN
|
|
211
|
+
// messages. In that case, use latest recent media within the same topic.
|
|
212
|
+
const prefix = `${topicId.trim()}::`;
|
|
213
|
+
let best = null;
|
|
214
|
+
for (const [entryKey, value] of recentTopicMediaCache.entries()) {
|
|
215
|
+
if (!entryKey.startsWith(prefix))
|
|
216
|
+
continue;
|
|
217
|
+
if (now - value.at > RECENT_TOPIC_MEDIA_TTL_MS) {
|
|
218
|
+
recentTopicMediaCache.delete(entryKey);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (!best || value.at > best.at)
|
|
222
|
+
best = value;
|
|
223
|
+
}
|
|
224
|
+
if (!best)
|
|
225
|
+
return null;
|
|
226
|
+
return {
|
|
227
|
+
mediaUrls: best.mediaUrls.slice(),
|
|
228
|
+
mediaTypes: best.mediaTypes.slice(),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function looksLikeImageFollowupText(text) {
|
|
232
|
+
const raw = String(text || "").trim();
|
|
233
|
+
if (!raw)
|
|
234
|
+
return true;
|
|
235
|
+
const compact = raw.replace(/\s+/g, "");
|
|
236
|
+
if (compact.length <= 24)
|
|
237
|
+
return true;
|
|
238
|
+
return /(图片|图里|看图|识别|这是啥|这是什么|什么狗|啥狗|what\s+is\s+this|what\s+dog|identify)/i.test(raw);
|
|
239
|
+
}
|
|
168
240
|
function isP2PTopicId(topicId) {
|
|
169
241
|
const type = topicTypeCache.get(topicId.trim());
|
|
170
242
|
return type === "p2p";
|
|
@@ -779,8 +851,285 @@ function sanitizeInboundText(raw) {
|
|
|
779
851
|
let text = raw || "";
|
|
780
852
|
// Strip WTT source marker banner block if present.
|
|
781
853
|
text = text.replace(/┌─ 来源标识[\s\S]*?└[^\n]*\n?/g, "").trim();
|
|
854
|
+
// Remove inline image payload markers from text body.
|
|
855
|
+
// Why: markdown image syntax starts with `!`, which can be interpreted as
|
|
856
|
+
// shell-command prefix by OpenClaw slash command surface.
|
|
857
|
+
text = text.replace(/<img\b[^>]*>/gi, " ");
|
|
858
|
+
text = text.replace(/!\[[^\]]*\]\((https?:\/\/[^\s)]+)\)/gi, " ");
|
|
859
|
+
text = text.replace(/(?:^|\n)\s*https?:\/\/\S+\.(?:jpg|jpeg|png|gif|webp|svg)(?:\?[^\s]*)?\s*(?=\n|$)/gim, "\n");
|
|
860
|
+
text = text.replace(/\n{3,}/g, "\n\n").trim();
|
|
782
861
|
return text;
|
|
783
862
|
}
|
|
863
|
+
function extractInboundImageMedia(raw, rawMsg) {
|
|
864
|
+
const source = raw || "";
|
|
865
|
+
const mediaUrls = [];
|
|
866
|
+
const mediaTypes = [];
|
|
867
|
+
const imageExtRe = /\.(?:jpg|jpeg|png|gif|webp|bmp|svg|heic|heif)(?:\?|$)/i;
|
|
868
|
+
const imageHostHintRe = /(?:^|\/)media\/[0-9a-f-]{16,}(?:\.[a-z0-9]{2,8})?(?:\?|$)/i;
|
|
869
|
+
const pushUrl = (urlRaw) => {
|
|
870
|
+
const url = String(urlRaw || "").trim().replace(/[),.;]+$/, "");
|
|
871
|
+
if (!url)
|
|
872
|
+
return;
|
|
873
|
+
if (!/^https?:\/\//i.test(url) && !/^\/?.*media\/[0-9a-f-]{16,}/i.test(url))
|
|
874
|
+
return;
|
|
875
|
+
if (mediaUrls.includes(url))
|
|
876
|
+
return;
|
|
877
|
+
mediaUrls.push(url);
|
|
878
|
+
mediaTypes.push("image/png");
|
|
879
|
+
};
|
|
880
|
+
const scanStringForImageUrls = (input) => {
|
|
881
|
+
const text = String(input || "");
|
|
882
|
+
if (!text)
|
|
883
|
+
return;
|
|
884
|
+
// HTML <img src="url"> pattern (from rich editor)
|
|
885
|
+
const htmlImgRe = /<img\s[^>]*\bsrc\s*=\s*["']([^"']+)["']/gi;
|
|
886
|
+
let htmlMatch;
|
|
887
|
+
while ((htmlMatch = htmlImgRe.exec(text)) !== null) {
|
|
888
|
+
pushUrl(htmlMatch[1]);
|
|
889
|
+
}
|
|
890
|
+
// Markdown  pattern (accept absolute + relative media paths)
|
|
891
|
+
const mdImgRe = /!\[[^\]]*\]\(([^\s)]+)\)/gi;
|
|
892
|
+
let mdMatch;
|
|
893
|
+
while ((mdMatch = mdImgRe.exec(text)) !== null) {
|
|
894
|
+
pushUrl(mdMatch[1]);
|
|
895
|
+
}
|
|
896
|
+
// Generic URL scan (covers metadata/url fields and host URLs without explicit image token)
|
|
897
|
+
const genericUrlRe = /https?:\/\/[^\s"'<>\])]+/gi;
|
|
898
|
+
let urlMatch;
|
|
899
|
+
while ((urlMatch = genericUrlRe.exec(text)) !== null) {
|
|
900
|
+
const candidate = String(urlMatch[0] || "").replace(/[),.;]+$/, "");
|
|
901
|
+
if (!candidate)
|
|
902
|
+
continue;
|
|
903
|
+
if (imageExtRe.test(candidate) || imageHostHintRe.test(candidate)) {
|
|
904
|
+
pushUrl(candidate);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
// Relative media URL scan (e.g. /media/<id> from backend metadata)
|
|
908
|
+
const relativeMediaRe = /(?:^|[\s(\["'])(\/?media\/[0-9a-f-]{16,}(?:\.[a-z0-9]{2,8})?(?:\?[^\s)"']*)?)/gi;
|
|
909
|
+
let relMatch;
|
|
910
|
+
while ((relMatch = relativeMediaRe.exec(text)) !== null) {
|
|
911
|
+
pushUrl(relMatch[1]);
|
|
912
|
+
}
|
|
913
|
+
// Bare image URLs on their own line (explicit fast path)
|
|
914
|
+
const bareImgRe = /^(https?:\/\/\S+\.(?:jpg|jpeg|png|gif|webp|svg)(?:\?[^\s]*)?)$/gim;
|
|
915
|
+
let bareMatch;
|
|
916
|
+
while ((bareMatch = bareImgRe.exec(text)) !== null) {
|
|
917
|
+
pushUrl(bareMatch[1]);
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
const walkUnknown = (value, depth = 0) => {
|
|
921
|
+
if (depth > 5 || value == null)
|
|
922
|
+
return;
|
|
923
|
+
if (typeof value === "string") {
|
|
924
|
+
scanStringForImageUrls(value);
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
if (Array.isArray(value)) {
|
|
928
|
+
const upper = Math.min(value.length, 80);
|
|
929
|
+
for (let i = 0; i < upper; i += 1)
|
|
930
|
+
walkUnknown(value[i], depth + 1);
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
if (typeof value !== "object")
|
|
934
|
+
return;
|
|
935
|
+
const obj = value;
|
|
936
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
937
|
+
const key = k.toLowerCase();
|
|
938
|
+
if (typeof v === "string") {
|
|
939
|
+
// Prioritize likely media-related keys.
|
|
940
|
+
if (key.includes("image") || key.includes("media") || key.includes("url") || key.includes("file") || key.includes("asset") || key.includes("attach")) {
|
|
941
|
+
scanStringForImageUrls(v);
|
|
942
|
+
}
|
|
943
|
+
else if (/https?:\/\//i.test(v)) {
|
|
944
|
+
scanStringForImageUrls(v);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
else {
|
|
948
|
+
walkUnknown(v, depth + 1);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
scanStringForImageUrls(source);
|
|
953
|
+
if (rawMsg)
|
|
954
|
+
walkUnknown(rawMsg);
|
|
955
|
+
return { mediaUrls, mediaTypes };
|
|
956
|
+
}
|
|
957
|
+
function absolutizeInboundMediaUrls(mediaUrls, cloudUrl) {
|
|
958
|
+
const base = String(cloudUrl || "").trim().replace(/\/$/, "");
|
|
959
|
+
const originMatch = base.match(/^(https?:\/\/[^/]+)/i);
|
|
960
|
+
const origin = originMatch ? originMatch[1] : base;
|
|
961
|
+
const out = [];
|
|
962
|
+
for (const raw of mediaUrls) {
|
|
963
|
+
let url = String(raw || "").trim();
|
|
964
|
+
if (!url)
|
|
965
|
+
continue;
|
|
966
|
+
if (/^\/\//.test(url)) {
|
|
967
|
+
url = `https:${url}`;
|
|
968
|
+
}
|
|
969
|
+
else if (/^\//.test(url) && origin) {
|
|
970
|
+
url = `${origin}${url}`;
|
|
971
|
+
}
|
|
972
|
+
else if (/^media\//i.test(url) && origin) {
|
|
973
|
+
url = `${origin}/${url}`;
|
|
974
|
+
}
|
|
975
|
+
if (!/^https?:\/\//i.test(url))
|
|
976
|
+
continue;
|
|
977
|
+
if (out.includes(url))
|
|
978
|
+
continue;
|
|
979
|
+
out.push(url);
|
|
980
|
+
}
|
|
981
|
+
return out;
|
|
982
|
+
}
|
|
983
|
+
function resolveOpenClawHomeDir() {
|
|
984
|
+
const fromEnv = process.env.OPENCLAW_HOME?.trim();
|
|
985
|
+
if (fromEnv)
|
|
986
|
+
return fromEnv;
|
|
987
|
+
return dirname(openclawConfigPath());
|
|
988
|
+
}
|
|
989
|
+
function resolveInboundMediaDir() {
|
|
990
|
+
return joinPath(resolveOpenClawHomeDir(), "media", "inbound");
|
|
991
|
+
}
|
|
992
|
+
function extensionFromContentType(contentType) {
|
|
993
|
+
const normalized = String(contentType || "").toLowerCase();
|
|
994
|
+
if (!normalized)
|
|
995
|
+
return "";
|
|
996
|
+
if (normalized.includes("jpeg") || normalized.includes("jpg"))
|
|
997
|
+
return ".jpg";
|
|
998
|
+
if (normalized.includes("png"))
|
|
999
|
+
return ".png";
|
|
1000
|
+
if (normalized.includes("gif"))
|
|
1001
|
+
return ".gif";
|
|
1002
|
+
if (normalized.includes("webp"))
|
|
1003
|
+
return ".webp";
|
|
1004
|
+
if (normalized.includes("bmp"))
|
|
1005
|
+
return ".bmp";
|
|
1006
|
+
if (normalized.includes("svg"))
|
|
1007
|
+
return ".svg";
|
|
1008
|
+
if (normalized.includes("heic"))
|
|
1009
|
+
return ".heic";
|
|
1010
|
+
if (normalized.includes("heif"))
|
|
1011
|
+
return ".heif";
|
|
1012
|
+
if (normalized.includes("mp4"))
|
|
1013
|
+
return ".mp4";
|
|
1014
|
+
return "";
|
|
1015
|
+
}
|
|
1016
|
+
function extensionFromUrl(urlRaw) {
|
|
1017
|
+
try {
|
|
1018
|
+
const parsed = new URL(urlRaw);
|
|
1019
|
+
const ext = extname(parsed.pathname || "").toLowerCase();
|
|
1020
|
+
if (!ext)
|
|
1021
|
+
return "";
|
|
1022
|
+
if (/^\.[a-z0-9]{1,8}$/.test(ext))
|
|
1023
|
+
return ext;
|
|
1024
|
+
return "";
|
|
1025
|
+
}
|
|
1026
|
+
catch {
|
|
1027
|
+
const ext = extname(urlRaw).toLowerCase();
|
|
1028
|
+
if (!ext)
|
|
1029
|
+
return "";
|
|
1030
|
+
return /^\.[a-z0-9]{1,8}$/.test(ext) ? ext : "";
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
async function downloadInboundMediaToLocal(params) {
|
|
1034
|
+
const headers = {
|
|
1035
|
+
Accept: "image/*,*/*;q=0.8",
|
|
1036
|
+
};
|
|
1037
|
+
if (params.account.token) {
|
|
1038
|
+
headers.Authorization = `Bearer ${params.account.token}`;
|
|
1039
|
+
headers["X-Agent-Token"] = params.account.token;
|
|
1040
|
+
}
|
|
1041
|
+
const controller = new AbortController();
|
|
1042
|
+
const timeout = setTimeout(() => controller.abort(), Math.max(1000, params.timeoutMs));
|
|
1043
|
+
let response;
|
|
1044
|
+
try {
|
|
1045
|
+
response = await fetch(params.url, {
|
|
1046
|
+
method: "GET",
|
|
1047
|
+
headers,
|
|
1048
|
+
redirect: "follow",
|
|
1049
|
+
signal: controller.signal,
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
finally {
|
|
1053
|
+
clearTimeout(timeout);
|
|
1054
|
+
}
|
|
1055
|
+
if (!response.ok) {
|
|
1056
|
+
throw new Error(`http_${response.status}`);
|
|
1057
|
+
}
|
|
1058
|
+
const chunks = [];
|
|
1059
|
+
let total = 0;
|
|
1060
|
+
const reader = response.body?.getReader();
|
|
1061
|
+
if (reader) {
|
|
1062
|
+
while (true) {
|
|
1063
|
+
const { done, value } = await reader.read();
|
|
1064
|
+
if (done)
|
|
1065
|
+
break;
|
|
1066
|
+
if (!value)
|
|
1067
|
+
continue;
|
|
1068
|
+
total += value.byteLength;
|
|
1069
|
+
if (total > params.maxBytes) {
|
|
1070
|
+
throw new Error(`media_too_large_${total}`);
|
|
1071
|
+
}
|
|
1072
|
+
chunks.push(Buffer.from(value));
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
else {
|
|
1076
|
+
const buf = Buffer.from(await response.arrayBuffer());
|
|
1077
|
+
total = buf.length;
|
|
1078
|
+
if (total > params.maxBytes) {
|
|
1079
|
+
throw new Error(`media_too_large_${total}`);
|
|
1080
|
+
}
|
|
1081
|
+
chunks.push(buf);
|
|
1082
|
+
}
|
|
1083
|
+
if (total <= 0) {
|
|
1084
|
+
throw new Error("empty_media");
|
|
1085
|
+
}
|
|
1086
|
+
const contentType = (response.headers.get("content-type") || "").split(";")[0]?.trim().toLowerCase() || undefined;
|
|
1087
|
+
const ext = extensionFromContentType(contentType) || extensionFromUrl(params.url) || ".bin";
|
|
1088
|
+
const fileName = `wtt-inbound-${Date.now()}-${randomBytes(6).toString("hex")}${ext}`;
|
|
1089
|
+
const mediaDir = resolveInboundMediaDir();
|
|
1090
|
+
await mkdir(mediaDir, { recursive: true });
|
|
1091
|
+
const filePath = joinPath(mediaDir, fileName);
|
|
1092
|
+
await writeFile(filePath, Buffer.concat(chunks));
|
|
1093
|
+
return {
|
|
1094
|
+
path: filePath,
|
|
1095
|
+
contentType,
|
|
1096
|
+
bytes: total,
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
async function materializeInboundMediaForContext(params) {
|
|
1100
|
+
if (params.mediaUrls.length === 0) {
|
|
1101
|
+
return { mediaPaths: [], mediaTypes: [] };
|
|
1102
|
+
}
|
|
1103
|
+
const maxBytes = toPositiveInt(params.account.config.inboundMediaMaxBytes, DEFAULT_INBOUND_MEDIA_MAX_BYTES);
|
|
1104
|
+
const maxPerMessage = toPositiveInt(params.account.config.inboundMediaMaxPerMessage, DEFAULT_INBOUND_MEDIA_MAX_PER_MESSAGE);
|
|
1105
|
+
const timeoutMs = toPositiveInt(params.account.config.inboundMediaFetchTimeoutMs, DEFAULT_INBOUND_MEDIA_FETCH_TIMEOUT_MS);
|
|
1106
|
+
const sourceUrls = params.mediaUrls.slice(0, Math.max(1, maxPerMessage));
|
|
1107
|
+
const mediaPaths = [];
|
|
1108
|
+
const mediaTypes = [];
|
|
1109
|
+
for (let idx = 0; idx < sourceUrls.length; idx += 1) {
|
|
1110
|
+
const url = sourceUrls[idx] || "";
|
|
1111
|
+
if (!url)
|
|
1112
|
+
continue;
|
|
1113
|
+
try {
|
|
1114
|
+
const downloaded = await downloadInboundMediaToLocal({
|
|
1115
|
+
url,
|
|
1116
|
+
account: params.account,
|
|
1117
|
+
maxBytes,
|
|
1118
|
+
timeoutMs,
|
|
1119
|
+
});
|
|
1120
|
+
mediaPaths.push(downloaded.path);
|
|
1121
|
+
mediaTypes.push(downloaded.contentType || params.mediaTypes[idx] || "image/png");
|
|
1122
|
+
params.log?.("info", `[${params.accountId}] inbound media downloaded url=${url} path=${downloaded.path} bytes=${downloaded.bytes}`);
|
|
1123
|
+
}
|
|
1124
|
+
catch (err) {
|
|
1125
|
+
params.log?.("warn", `[${params.accountId}] inbound media download failed url=${url}`, err);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return {
|
|
1129
|
+
mediaPaths,
|
|
1130
|
+
mediaTypes,
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
784
1133
|
function isMeaningfulUserText(text) {
|
|
785
1134
|
const t = (text || "").trim();
|
|
786
1135
|
if (!t)
|
|
@@ -1113,9 +1462,17 @@ export function normalizeInboundWsMessage(params) {
|
|
|
1113
1462
|
const topicId = resolveInboundTopicId(raw);
|
|
1114
1463
|
const senderName = toOptionalString(raw.sender_display_name);
|
|
1115
1464
|
const topicName = toOptionalString(raw.topic_name);
|
|
1116
|
-
const
|
|
1465
|
+
const rawContent = params.decryptedContent ?? String(raw.content ?? "");
|
|
1466
|
+
let content = sanitizeInboundText(rawContent);
|
|
1117
1467
|
const messageId = toOptionalString(raw.id) ?? `${topicId || "no-topic"}:${senderId}:${Date.now()}`;
|
|
1118
1468
|
const timestamp = toIsoTimestamp(raw.created_at);
|
|
1469
|
+
// Extract image URLs from original message body for OpenClaw media-understanding pipeline.
|
|
1470
|
+
const { mediaUrls, mediaTypes } = extractInboundImageMedia(rawContent, raw);
|
|
1471
|
+
// If message is image-only after sanitization, keep a minimal prompt so it
|
|
1472
|
+
// won't be dropped by empty-message gating.
|
|
1473
|
+
if (!content.trim() && mediaUrls.length > 0) {
|
|
1474
|
+
content = "请结合这张图片内容进行识别并回复。";
|
|
1475
|
+
}
|
|
1119
1476
|
const isP2P = isLikelyP2PMessage(raw);
|
|
1120
1477
|
const hasTopicId = Boolean(topicId);
|
|
1121
1478
|
const chatType = isP2P ? "direct" : "group";
|
|
@@ -1142,6 +1499,8 @@ export function normalizeInboundWsMessage(params) {
|
|
|
1142
1499
|
to,
|
|
1143
1500
|
from,
|
|
1144
1501
|
conversationLabel,
|
|
1502
|
+
mediaUrls,
|
|
1503
|
+
mediaTypes,
|
|
1145
1504
|
};
|
|
1146
1505
|
}
|
|
1147
1506
|
export function createInboundMessageRelay(params) {
|
|
@@ -1277,11 +1636,19 @@ export async function routeInboundWsMessage(params) {
|
|
|
1277
1636
|
decryptedContent = rawContent;
|
|
1278
1637
|
}
|
|
1279
1638
|
}
|
|
1280
|
-
|
|
1639
|
+
let normalized = normalizeInboundWsMessage({
|
|
1281
1640
|
msg: params.msg,
|
|
1282
1641
|
decryptedContent,
|
|
1283
1642
|
});
|
|
1284
1643
|
const rawMsg = params.msg.message;
|
|
1644
|
+
if (normalized.mediaUrls.length > 0) {
|
|
1645
|
+
const absMediaUrls = absolutizeInboundMediaUrls(normalized.mediaUrls, params.account.cloudUrl);
|
|
1646
|
+
normalized = {
|
|
1647
|
+
...normalized,
|
|
1648
|
+
mediaUrls: absMediaUrls,
|
|
1649
|
+
mediaTypes: absMediaUrls.map(() => "image/png"),
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1285
1652
|
const inboundTaskId = toOptionalString(rawMsg.task_id);
|
|
1286
1653
|
const inferredTopicType = String(rawMsg.topic_type ?? "").toLowerCase();
|
|
1287
1654
|
if (normalized.topicId) {
|
|
@@ -1293,6 +1660,36 @@ export async function routeInboundWsMessage(params) {
|
|
|
1293
1660
|
}
|
|
1294
1661
|
}
|
|
1295
1662
|
const typingTopicId = normalized.topicId?.trim() || "";
|
|
1663
|
+
const mentionMatch = resolveMentionMatch(String(rawMsg.content ?? ""), params.account.agentId, params.account.name);
|
|
1664
|
+
// Remember recent media from this sender/topic even when the message itself
|
|
1665
|
+
// does not trigger inference (for example: image first, @mention second).
|
|
1666
|
+
if (typingTopicId && normalized.mediaUrls.length > 0) {
|
|
1667
|
+
rememberRecentTopicMedia(typingTopicId, normalized.senderId, normalized.mediaUrls, normalized.mediaTypes);
|
|
1668
|
+
params.log?.("info", `[${params.accountId}] inbound media captured topic=${typingTopicId} sender=${normalized.senderId} count=${normalized.mediaUrls.length}`);
|
|
1669
|
+
}
|
|
1670
|
+
// If current @mention message carries no media, try to reuse sender's latest
|
|
1671
|
+
// media in the same topic so "image + @mention" split messages still work.
|
|
1672
|
+
if (typingTopicId
|
|
1673
|
+
&& normalized.mediaUrls.length === 0
|
|
1674
|
+
&& mentionMatch.matchesAgent
|
|
1675
|
+
&& looksLikeImageFollowupText(normalized.text)) {
|
|
1676
|
+
const recentMedia = readRecentTopicMedia(typingTopicId, normalized.senderId);
|
|
1677
|
+
if (recentMedia && recentMedia.mediaUrls.length > 0) {
|
|
1678
|
+
normalized = {
|
|
1679
|
+
...normalized,
|
|
1680
|
+
mediaUrls: recentMedia.mediaUrls,
|
|
1681
|
+
mediaTypes: recentMedia.mediaTypes,
|
|
1682
|
+
};
|
|
1683
|
+
params.log?.("info", `[${params.accountId}] inbound media hydrated from recent cache topic=${typingTopicId} sender=${normalized.senderId} count=${recentMedia.mediaUrls.length}`);
|
|
1684
|
+
}
|
|
1685
|
+
else {
|
|
1686
|
+
const rawKeys = Object.keys(rawMsg || {}).slice(0, 40).join(",");
|
|
1687
|
+
const md = parseInboundMetadata(rawMsg);
|
|
1688
|
+
const metadataKeys = md ? Object.keys(md).slice(0, 30).join(",") : "";
|
|
1689
|
+
const contentPreview = String(rawMsg.content ?? "").slice(0, 120).replace(/\s+/g, " ");
|
|
1690
|
+
params.log?.("info", `[${params.accountId}] inbound media cache_miss topic=${typingTopicId} sender=${normalized.senderId} raw_keys=${rawKeys} metadata_keys=${metadataKeys} content_preview=${contentPreview}`);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1296
1693
|
const emitTypingSignal = async (state) => {
|
|
1297
1694
|
if (!typingTopicId || !params.typingSignal)
|
|
1298
1695
|
return;
|
|
@@ -1358,8 +1755,7 @@ export async function routeInboundWsMessage(params) {
|
|
|
1358
1755
|
const isTaskLinkedTopic = Boolean(inboundTaskId || topicName.startsWith("TASK-"));
|
|
1359
1756
|
const isSlashLike = /^\/\S+/.test((normalized.text || "").trim());
|
|
1360
1757
|
const inboundMetadata = parseInboundMetadata(rawMsg);
|
|
1361
|
-
const mentionTargetedDiscussion = topicType === "discussion"
|
|
1362
|
-
&& resolveMentionMatch(String(rawMsg.content ?? ""), params.account.agentId, params.account.name).matchesAgent;
|
|
1758
|
+
const mentionTargetedDiscussion = topicType === "discussion" && mentionMatch.matchesAgent;
|
|
1363
1759
|
if (slashCompatEnabled) {
|
|
1364
1760
|
if (topicType === "discussion" && !isTaskLinkedTopic && isSlashLike) {
|
|
1365
1761
|
const targetAgentId = toOptionalString(inboundMetadata?.command_target_agent_id)
|
|
@@ -1413,6 +1809,21 @@ export async function routeInboundWsMessage(params) {
|
|
|
1413
1809
|
else {
|
|
1414
1810
|
params.log?.("info", `[${params.accountId}] inference_gate bypassed reason=slash_command topic_type=${String(rawMsg.topic_type)}`);
|
|
1415
1811
|
}
|
|
1812
|
+
// Telegram-style media handling: download inbound media first, then pass
|
|
1813
|
+
// local file paths into MediaPath/MediaPaths for stable vision pipeline input.
|
|
1814
|
+
const downloadedInboundMedia = await materializeInboundMediaForContext({
|
|
1815
|
+
mediaUrls: normalized.mediaUrls,
|
|
1816
|
+
mediaTypes: normalized.mediaTypes,
|
|
1817
|
+
account: params.account,
|
|
1818
|
+
accountId: params.accountId,
|
|
1819
|
+
log: params.log,
|
|
1820
|
+
});
|
|
1821
|
+
const contextMediaPaths = downloadedInboundMedia.mediaPaths.length > 0
|
|
1822
|
+
? downloadedInboundMedia.mediaPaths
|
|
1823
|
+
: normalized.mediaUrls;
|
|
1824
|
+
const contextMediaTypes = downloadedInboundMedia.mediaTypes.length > 0
|
|
1825
|
+
? downloadedInboundMedia.mediaTypes
|
|
1826
|
+
: normalized.mediaTypes;
|
|
1416
1827
|
const route = runtime.routing.resolveAgentRoute({
|
|
1417
1828
|
cfg: params.cfg,
|
|
1418
1829
|
channel: CHANNEL_ID,
|
|
@@ -1473,6 +1884,19 @@ export async function routeInboundWsMessage(params) {
|
|
|
1473
1884
|
Timestamp: normalized.timestamp,
|
|
1474
1885
|
OriginatingChannel: CHANNEL_ID,
|
|
1475
1886
|
OriginatingTo: normalized.to,
|
|
1887
|
+
// Pass media as local paths when available (Telegram parity), fallback to
|
|
1888
|
+
// original URLs when download is unavailable.
|
|
1889
|
+
...(contextMediaPaths.length > 0
|
|
1890
|
+
? {
|
|
1891
|
+
MediaPath: contextMediaPaths[0],
|
|
1892
|
+
MediaPaths: contextMediaPaths,
|
|
1893
|
+
MediaUrl: contextMediaPaths[0],
|
|
1894
|
+
MediaUrls: contextMediaPaths,
|
|
1895
|
+
MediaType: contextMediaTypes[0],
|
|
1896
|
+
MediaTypes: contextMediaTypes,
|
|
1897
|
+
OriginalMediaUrls: normalized.mediaUrls.length > 0 ? normalized.mediaUrls : undefined,
|
|
1898
|
+
}
|
|
1899
|
+
: {}),
|
|
1476
1900
|
});
|
|
1477
1901
|
const taskExecutorScope = (params.account.config.taskExecutorScope ?? "all").toLowerCase();
|
|
1478
1902
|
let naturalBridgeTaskStatus = "";
|