@inline-openclaw/inline 0.0.3 → 0.0.5

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.
@@ -0,0 +1,117 @@
1
+ import path from "node:path";
2
+ import { detectMime, extensionForMime, loadWebMedia, resolveChannelMediaMaxBytes, } from "openclaw/plugin-sdk";
3
+ const DEFAULT_MEDIA_MAX_MB = 20;
4
+ const SUPPORTED_INLINE_PHOTO_MIME = new Set(["image/jpeg", "image/png", "image/gif"]);
5
+ const SUPPORTED_INLINE_VIDEO_MIME = new Set(["video/mp4"]);
6
+ const DEFAULT_VIDEO_WIDTH = 1280;
7
+ const DEFAULT_VIDEO_HEIGHT = 720;
8
+ const DEFAULT_VIDEO_DURATION = 1;
9
+ function normalizeMime(raw) {
10
+ const trimmed = raw?.trim().toLowerCase();
11
+ return trimmed || undefined;
12
+ }
13
+ function normalizeExt(rawFileName) {
14
+ const ext = path.extname(rawFileName ?? "").trim().toLowerCase();
15
+ if (!ext)
16
+ return undefined;
17
+ return ext.replace(/^\./, "");
18
+ }
19
+ function isSupportedPhoto(params) {
20
+ if (params.mime && SUPPORTED_INLINE_PHOTO_MIME.has(params.mime))
21
+ return true;
22
+ return params.ext === "jpg" || params.ext === "jpeg" || params.ext === "png" || params.ext === "gif";
23
+ }
24
+ function isSupportedVideo(params) {
25
+ if (params.mime && SUPPORTED_INLINE_VIDEO_MIME.has(params.mime))
26
+ return true;
27
+ return params.ext === "mp4";
28
+ }
29
+ function chooseUploadType(params) {
30
+ // Prefer explicit MIME/extension compatibility when available.
31
+ if (isSupportedPhoto(params))
32
+ return "photo";
33
+ if (isSupportedVideo(params))
34
+ return "video";
35
+ // Fall back to loader-detected media kind when MIME/extension is unavailable.
36
+ if (params.kind === "image")
37
+ return "photo";
38
+ if (params.kind === "video")
39
+ return "video";
40
+ // Fallback to document when Inline media validators would reject the file.
41
+ return "document";
42
+ }
43
+ function ensureUploadFileName(params) {
44
+ const trimmed = params.fileName?.trim();
45
+ if (trimmed) {
46
+ const ext = normalizeExt(trimmed);
47
+ if (ext)
48
+ return trimmed;
49
+ }
50
+ const inferredExt = params.ext ?? extensionForMime(params.mime) ?? undefined;
51
+ const fallbackExt = inferredExt ??
52
+ (params.uploadType === "photo" ? "jpg" : params.uploadType === "video" ? "mp4" : "bin");
53
+ return `attachment.${fallbackExt}`;
54
+ }
55
+ function resolveMediaMaxBytes(params) {
56
+ return (resolveChannelMediaMaxBytes({
57
+ cfg: params.cfg,
58
+ ...(params.accountId != null ? { accountId: params.accountId } : {}),
59
+ resolveChannelLimitMb: ({ cfg, accountId }) => cfg.channels?.inline?.accounts?.[accountId]?.mediaMaxMb ??
60
+ cfg.channels?.inline?.mediaMaxMb,
61
+ }) ??
62
+ DEFAULT_MEDIA_MAX_MB * 1024 * 1024);
63
+ }
64
+ function mediaFromUploadResult(params) {
65
+ if (params.uploadType === "photo" && params.result.photoId != null) {
66
+ return { kind: "photo", photoId: params.result.photoId };
67
+ }
68
+ if (params.uploadType === "video" && params.result.videoId != null) {
69
+ return { kind: "video", videoId: params.result.videoId };
70
+ }
71
+ if (params.result.documentId != null) {
72
+ return { kind: "document", documentId: params.result.documentId };
73
+ }
74
+ throw new Error(`inline media upload: missing ${params.uploadType} id in upload response`);
75
+ }
76
+ export async function uploadInlineMediaFromUrl(params) {
77
+ const maxBytes = resolveMediaMaxBytes({
78
+ cfg: params.cfg,
79
+ accountId: params.accountId ?? null,
80
+ });
81
+ const loaded = await loadWebMedia(params.mediaUrl, maxBytes);
82
+ const detectedMime = normalizeMime(loaded.contentType ??
83
+ (await detectMime({
84
+ buffer: loaded.buffer,
85
+ ...(loaded.fileName ? { filePath: loaded.fileName } : {}),
86
+ })));
87
+ const normalizedExt = normalizeExt(loaded.fileName);
88
+ const uploadType = chooseUploadType({
89
+ kind: loaded.kind,
90
+ ...(detectedMime ? { mime: detectedMime } : {}),
91
+ ...(normalizedExt ? { ext: normalizedExt } : {}),
92
+ });
93
+ const fileName = ensureUploadFileName({
94
+ ...(loaded.fileName ? { fileName: loaded.fileName } : {}),
95
+ uploadType,
96
+ ...(detectedMime ? { mime: detectedMime } : {}),
97
+ ...(normalizedExt ? { ext: normalizedExt } : {}),
98
+ });
99
+ const upload = await params.client.uploadFile({
100
+ type: uploadType,
101
+ file: loaded.buffer,
102
+ fileName,
103
+ ...(detectedMime ? { contentType: detectedMime } : {}),
104
+ ...(uploadType === "video"
105
+ ? {
106
+ width: DEFAULT_VIDEO_WIDTH,
107
+ height: DEFAULT_VIDEO_HEIGHT,
108
+ duration: DEFAULT_VIDEO_DURATION,
109
+ }
110
+ : {}),
111
+ });
112
+ return mediaFromUploadResult({
113
+ uploadType,
114
+ result: upload,
115
+ });
116
+ }
117
+ //# sourceMappingURL=media.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media.js","sourceRoot":"","sources":["../../src/inline/media.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"monitor.d.ts","sourceRoot":"","sources":["../../src/inline/monitor.ts"],"names":[],"mappings":"AAEA,OAAO,EAML,KAAK,cAAc,EACnB,KAAK,UAAU,EAChB,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EAAsB,KAAK,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAK9E,KAAK,mBAAmB,GAAG;IACzB,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC1B,CAAA;AAED,KAAK,UAAU,GAAG,CAAC,KAAK,EAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,KAAK,IAAI,CAAA;AAwE1G,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,GAAG,EAAE,cAAc,CAAA;IACnB,OAAO,EAAE,qBAAqB,CAAA;IAC9B,OAAO,EAAE,UAAU,CAAA;IACnB,WAAW,EAAE,WAAW,CAAA;IACxB,GAAG,CAAC,EAAE;QAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAA;IAC/H,UAAU,CAAC,EAAE,UAAU,CAAA;CACxB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAuX/B"}
1
+ {"version":3,"file":"monitor.d.ts","sourceRoot":"","sources":["../../src/inline/monitor.ts"],"names":[],"mappings":"AAEA,OAAO,EAML,KAAK,cAAc,EACnB,KAAK,UAAU,EAChB,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EAAsB,KAAK,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAO9E,KAAK,mBAAmB,GAAG;IACzB,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC1B,CAAA;AAED,KAAK,UAAU,GAAG,CAAC,KAAK,EAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,KAAK,IAAI,CAAA;AAgQ1G,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,GAAG,EAAE,cAAc,CAAA;IACnB,OAAO,EAAE,qBAAqB,CAAA;IAC9B,OAAO,EAAE,UAAU,CAAA;IACnB,WAAW,EAAE,WAAW,CAAA;IACxB,GAAG,CAAC,EAAE;QAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAA;IAC/H,UAAU,CAAC,EAAE,UAAU,CAAA;CACxB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAqiB/B"}
@@ -3,8 +3,15 @@ import path from "node:path";
3
3
  import { createReplyPrefixOptions, createTypingCallbacks, logInboundDrop, resolveControlCommandGate, resolveMentionGatingWithBypass, } from "openclaw/plugin-sdk";
4
4
  import { InlineSdkClient, JsonFileStateStore, Method } from "@inline-chat/realtime-sdk";
5
5
  import { resolveInlineToken } from "./accounts.js";
6
+ import { resolveInlineGroupRequireMention } from "./policy.js";
6
7
  import { getInlineRuntime } from "../runtime.js";
8
+ import { uploadInlineMediaFromUrl } from "./media.js";
7
9
  const CHANNEL_ID = "inline";
10
+ const DEFAULT_GROUP_HISTORY_LIMIT = 12;
11
+ const DEFAULT_DM_HISTORY_LIMIT = 6;
12
+ const HISTORY_LINE_MAX_CHARS = 280;
13
+ const BOT_MESSAGE_CACHE_LIMIT = 500;
14
+ const REACTION_TARGET_LOOKUP_LIMIT = 8;
8
15
  function normalizeAllowEntry(raw) {
9
16
  return raw.trim().replace(/^inline:/i, "").replace(/^user:/i, "");
10
17
  }
@@ -54,6 +61,146 @@ function rewriteNumericMentionsToUsernames(text, senderProfilesById) {
54
61
  return `${prefix}@${username}`;
55
62
  });
56
63
  }
64
+ function rememberBotMessageId(cache, chatId, messageId) {
65
+ const key = String(chatId);
66
+ const list = cache.get(key) ?? [];
67
+ const nextId = String(messageId);
68
+ if (!list.includes(nextId))
69
+ list.push(nextId);
70
+ if (list.length > BOT_MESSAGE_CACHE_LIMIT) {
71
+ list.splice(0, list.length - BOT_MESSAGE_CACHE_LIMIT);
72
+ }
73
+ cache.set(key, list);
74
+ }
75
+ function hasBotMessageId(cache, chatId, messageId) {
76
+ const key = String(chatId);
77
+ return (cache.get(key) ?? []).includes(String(messageId));
78
+ }
79
+ async function isReactionTargetBotMessage(params) {
80
+ if (hasBotMessageId(params.botMessageIdsByChat, params.chatId, params.messageId)) {
81
+ return true;
82
+ }
83
+ const result = await params.client.invokeRaw(Method.GET_CHAT_HISTORY, {
84
+ oneofKind: "getChatHistory",
85
+ getChatHistory: {
86
+ peerId: {
87
+ type: {
88
+ oneofKind: "chat",
89
+ chat: { chatId: params.chatId },
90
+ },
91
+ },
92
+ offsetId: params.messageId + 1n,
93
+ limit: REACTION_TARGET_LOOKUP_LIMIT,
94
+ },
95
+ });
96
+ if (result.oneofKind !== "getChatHistory") {
97
+ return false;
98
+ }
99
+ const messages = result.getChatHistory.messages ?? [];
100
+ for (const item of messages) {
101
+ if (item.fromId === params.meId) {
102
+ rememberBotMessageId(params.botMessageIdsByChat, params.chatId, item.id);
103
+ }
104
+ }
105
+ const target = messages.find((item) => item.id === params.messageId);
106
+ if (!target)
107
+ return false;
108
+ return target.fromId === params.meId;
109
+ }
110
+ function normalizeHistoryText(raw) {
111
+ const compact = (raw ?? "").replace(/\s+/g, " ").trim();
112
+ if (!compact)
113
+ return "";
114
+ if (compact.length <= HISTORY_LINE_MAX_CHARS)
115
+ return compact;
116
+ return `${compact.slice(0, HISTORY_LINE_MAX_CHARS - 1)}…`;
117
+ }
118
+ function resolveHistorySenderLabel(params) {
119
+ if (params.senderId === params.meId)
120
+ return "assistant";
121
+ const senderId = String(params.senderId);
122
+ const profile = params.senderProfilesById.get(senderId);
123
+ if (profile?.username)
124
+ return `@${profile.username}`;
125
+ if (profile?.name)
126
+ return profile.name;
127
+ return `user:${senderId}`;
128
+ }
129
+ function resolveHistoryLimit(params) {
130
+ if (params.isGroup) {
131
+ return params.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT;
132
+ }
133
+ return params.dmHistoryLimit ?? params.historyLimit ?? DEFAULT_DM_HISTORY_LIMIT;
134
+ }
135
+ async function buildHistoryContext(params) {
136
+ if (params.historyLimit <= 0) {
137
+ return {
138
+ historyText: null,
139
+ repliedToBot: params.replyToMsgId != null &&
140
+ hasBotMessageId(params.botMessageIdsByChat, params.chatId, params.replyToMsgId),
141
+ };
142
+ }
143
+ const result = await params.client.invokeRaw(Method.GET_CHAT_HISTORY, {
144
+ oneofKind: "getChatHistory",
145
+ getChatHistory: {
146
+ peerId: {
147
+ type: {
148
+ oneofKind: "chat",
149
+ chat: { chatId: params.chatId },
150
+ },
151
+ },
152
+ offsetId: params.currentMessageId,
153
+ limit: params.historyLimit,
154
+ },
155
+ });
156
+ if (result.oneofKind !== "getChatHistory") {
157
+ return {
158
+ historyText: null,
159
+ repliedToBot: params.replyToMsgId != null &&
160
+ hasBotMessageId(params.botMessageIdsByChat, params.chatId, params.replyToMsgId),
161
+ };
162
+ }
163
+ const messages = [...(result.getChatHistory.messages ?? [])]
164
+ .filter((item) => item.id !== params.currentMessageId)
165
+ .sort((a, b) => {
166
+ const byDate = Number(a.date - b.date);
167
+ if (byDate !== 0)
168
+ return byDate;
169
+ if (a.id === b.id)
170
+ return 0;
171
+ return a.id < b.id ? -1 : 1;
172
+ });
173
+ const lines = [];
174
+ let repliedToBot = params.replyToMsgId != null &&
175
+ hasBotMessageId(params.botMessageIdsByChat, params.chatId, params.replyToMsgId);
176
+ for (const item of messages) {
177
+ if (item.fromId === params.meId) {
178
+ rememberBotMessageId(params.botMessageIdsByChat, params.chatId, item.id);
179
+ }
180
+ if (params.replyToMsgId != null &&
181
+ item.id === params.replyToMsgId &&
182
+ item.fromId === params.meId) {
183
+ repliedToBot = true;
184
+ }
185
+ const text = normalizeHistoryText(item.message);
186
+ if (!text)
187
+ continue;
188
+ const label = resolveHistorySenderLabel({
189
+ senderId: item.fromId,
190
+ meId: params.meId,
191
+ senderProfilesById: params.senderProfilesById,
192
+ });
193
+ const replySuffix = item.replyToMsgId != null ? ` ->${String(item.replyToMsgId)}` : "";
194
+ lines.push(`#${String(item.id)}${replySuffix} ${label}: ${text}`);
195
+ }
196
+ if (!lines.length) {
197
+ return { historyText: null, repliedToBot };
198
+ }
199
+ return {
200
+ historyText: `Recent thread messages (oldest -> newest):\n${lines.join("\n")}`,
201
+ repliedToBot,
202
+ };
203
+ }
57
204
  export async function monitorInlineProvider(params) {
58
205
  const { cfg, account, runtime, abortSignal, log, statusSink } = params;
59
206
  const core = getInlineRuntime();
@@ -81,6 +228,7 @@ export async function monitorInlineProvider(params) {
81
228
  log?.info(`[${account.accountId}] inline connected (me=${String(me.userId)})`);
82
229
  const chatCache = new Map();
83
230
  const senderProfilesById = new Map();
231
+ const botMessageIdsByChat = new Map();
84
232
  const hydratedParticipantChats = new Set();
85
233
  const participantFetches = new Map();
86
234
  const hydrateChatParticipants = async (chatId) => {
@@ -127,15 +275,83 @@ export async function monitorInlineProvider(params) {
127
275
  for await (const event of client.events()) {
128
276
  if (abortSignal.aborted)
129
277
  break;
130
- if (event.kind !== "message.new")
131
- continue;
132
- const msg = event.message;
133
- const rawBody = messageText(msg);
134
- if (!rawBody)
135
- continue;
136
- // Ignore echoes / our own outbound messages.
137
- if (msg.out || msg.fromId === me.userId)
278
+ let msg;
279
+ let rawBody = "";
280
+ let reactionEvent = null;
281
+ if (event.kind === "message.new") {
282
+ msg = event.message;
283
+ rawBody = messageText(msg);
284
+ if (!rawBody)
285
+ continue;
286
+ // Ignore echoes / our own outbound messages.
287
+ if (msg.out || msg.fromId === me.userId)
288
+ continue;
289
+ }
290
+ else if (event.kind === "reaction.add") {
291
+ if (event.reaction.userId === me.userId)
292
+ continue;
293
+ const onBotMessage = await isReactionTargetBotMessage({
294
+ client,
295
+ chatId: event.chatId,
296
+ messageId: event.reaction.messageId,
297
+ meId: me.userId,
298
+ botMessageIdsByChat,
299
+ }).catch((err) => {
300
+ statusSink?.({ lastError: `getChatHistory (reaction target) failed: ${String(err)}` });
301
+ return false;
302
+ });
303
+ if (!onBotMessage)
304
+ continue;
305
+ reactionEvent = {
306
+ action: "added",
307
+ emoji: event.reaction.emoji,
308
+ targetMessageId: event.reaction.messageId,
309
+ };
310
+ msg = {
311
+ id: event.reaction.messageId,
312
+ chatId: event.chatId,
313
+ date: event.date,
314
+ fromId: event.reaction.userId,
315
+ message: "",
316
+ out: false,
317
+ mentioned: false,
318
+ replyToMsgId: event.reaction.messageId,
319
+ };
320
+ }
321
+ else if (event.kind === "reaction.delete") {
322
+ if (event.userId === me.userId)
323
+ continue;
324
+ const onBotMessage = await isReactionTargetBotMessage({
325
+ client,
326
+ chatId: event.chatId,
327
+ messageId: event.messageId,
328
+ meId: me.userId,
329
+ botMessageIdsByChat,
330
+ }).catch((err) => {
331
+ statusSink?.({ lastError: `getChatHistory (reaction target) failed: ${String(err)}` });
332
+ return false;
333
+ });
334
+ if (!onBotMessage)
335
+ continue;
336
+ reactionEvent = {
337
+ action: "removed",
338
+ emoji: event.emoji,
339
+ targetMessageId: event.messageId,
340
+ };
341
+ msg = {
342
+ id: event.messageId,
343
+ chatId: event.chatId,
344
+ date: event.date,
345
+ fromId: event.userId,
346
+ message: "",
347
+ out: false,
348
+ mentioned: false,
349
+ replyToMsgId: event.messageId,
350
+ };
351
+ }
352
+ else {
138
353
  continue;
354
+ }
139
355
  const chatId = event.chatId;
140
356
  statusSink?.({ lastInboundAt: Date.now() });
141
357
  let chatInfo;
@@ -153,6 +369,19 @@ export async function monitorInlineProvider(params) {
153
369
  const senderProfile = senderProfilesById.get(senderId);
154
370
  const senderUsername = senderProfile?.username;
155
371
  const senderName = senderProfile?.name ?? (!isGroup ? chatInfo.title ?? undefined : undefined);
372
+ if (reactionEvent) {
373
+ const actor = senderUsername != null && senderUsername.length > 0
374
+ ? `@${senderUsername}`
375
+ : senderName ?? `user:${senderId}`;
376
+ const emoji = reactionEvent.emoji.trim() || "a reaction";
377
+ const messageId = String(reactionEvent.targetMessageId);
378
+ if (reactionEvent.action === "added") {
379
+ rawBody = `${actor} reacted with ${emoji} to your message #${messageId}`;
380
+ }
381
+ else {
382
+ rawBody = `${actor} removed ${emoji} from your message #${messageId}`;
383
+ }
384
+ }
156
385
  const dmPolicy = account.config.dmPolicy ?? "pairing";
157
386
  const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
158
387
  const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
@@ -256,12 +485,43 @@ export async function monitorInlineProvider(params) {
256
485
  : mentionRegexes.length
257
486
  ? core.channel.mentions.matchesMentionPatterns(rawBody, mentionRegexes)
258
487
  : false;
259
- const requireMention = isGroup ? (account.config.requireMention ?? true) : false;
488
+ const historyLimit = resolveHistoryLimit({
489
+ isGroup,
490
+ historyLimit: account.config.historyLimit,
491
+ dmHistoryLimit: account.config.dmHistoryLimit,
492
+ });
493
+ const historyContext = await buildHistoryContext({
494
+ client,
495
+ chatId,
496
+ currentMessageId: msg.id,
497
+ replyToMsgId: msg.replyToMsgId,
498
+ senderProfilesById,
499
+ meId: me.userId,
500
+ historyLimit,
501
+ botMessageIdsByChat,
502
+ }).catch((err) => {
503
+ statusSink?.({ lastError: `getChatHistory failed: ${String(err)}` });
504
+ return { historyText: null, repliedToBot: false };
505
+ });
506
+ const implicitMention = (reactionEvent != null && isGroup) ||
507
+ (isGroup &&
508
+ (account.config.replyToBotWithoutMention ?? false) &&
509
+ msg.replyToMsgId != null &&
510
+ historyContext.repliedToBot);
511
+ const requireMention = isGroup
512
+ ? resolveInlineGroupRequireMention({
513
+ cfg,
514
+ groupId: String(chatId),
515
+ accountId: account.accountId,
516
+ requireMentionDefault: account.config.requireMention ?? false,
517
+ })
518
+ : false;
260
519
  const mentionGate = resolveMentionGatingWithBypass({
261
520
  isGroup,
262
521
  requireMention,
263
522
  canDetectMention: typeof msg.mentioned === "boolean" || mentionRegexes.length > 0,
264
523
  wasMentioned,
524
+ implicitMention,
265
525
  allowTextCommands,
266
526
  hasControlCommand,
267
527
  commandAuthorized,
@@ -281,7 +541,9 @@ export async function monitorInlineProvider(params) {
281
541
  timestamp,
282
542
  ...(previousTimestamp != null ? { previousTimestamp } : {}),
283
543
  envelope: envelopeOptions,
284
- body: rawBody,
544
+ body: historyContext.historyText
545
+ ? `${historyContext.historyText}\n\nCurrent message:\n${rawBody}`
546
+ : rawBody,
285
547
  });
286
548
  const ctxPayload = core.channel.reply.finalizeInboundContext({
287
549
  Body: body,
@@ -323,19 +585,35 @@ export async function monitorInlineProvider(params) {
323
585
  : {}),
324
586
  onRecordError: (err) => runtime.error?.(`inline: failed updating session meta: ${String(err)}`),
325
587
  });
326
- const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
327
- cfg,
328
- agentId: route.agentId,
329
- channel: CHANNEL_ID,
330
- accountId: account.accountId,
331
- });
332
- const typingCallbacks = createTypingCallbacks({
333
- start: () => client.sendTyping({ chatId, typing: true }),
334
- stop: () => client.sendTyping({ chatId, typing: false }),
335
- onStartError: (err) => runtime.error?.(`inline typing start failed: ${String(err)}`),
336
- onStopError: (err) => runtime.error?.(`inline typing stop failed: ${String(err)}`),
337
- });
588
+ const prefixConfig = (typeof createReplyPrefixOptions === "function"
589
+ ? createReplyPrefixOptions({
590
+ cfg,
591
+ agentId: route.agentId,
592
+ channel: CHANNEL_ID,
593
+ accountId: account.accountId,
594
+ })
595
+ : {});
596
+ const onModelSelected = typeof prefixConfig.onModelSelected === "function"
597
+ ? prefixConfig.onModelSelected
598
+ : undefined;
599
+ const { onModelSelected: _ignoredOnModelSelected, ...prefixOptions } = prefixConfig;
600
+ const typingCallbacks = typeof createTypingCallbacks === "function"
601
+ ? createTypingCallbacks({
602
+ start: () => client.sendTyping({ chatId, typing: true }),
603
+ stop: () => client.sendTyping({ chatId, typing: false }),
604
+ onStartError: (err) => runtime.error?.(`inline typing start failed: ${String(err)}`),
605
+ onStopError: (err) => runtime.error?.(`inline typing stop failed: ${String(err)}`),
606
+ })
607
+ : {};
338
608
  const parseMarkdown = account.config.parseMarkdown ?? true;
609
+ const disableBlockStreaming = typeof account.config.blockStreaming === "boolean"
610
+ ? !account.config.blockStreaming
611
+ : undefined;
612
+ const replyOptions = {
613
+ ...(onModelSelected ? { onModelSelected } : {}),
614
+ blockReplyTimeoutMs: 25_000,
615
+ ...(typeof disableBlockStreaming === "boolean" ? { disableBlockStreaming } : {}),
616
+ };
339
617
  await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
340
618
  ctx: ctxPayload,
341
619
  cfg,
@@ -343,21 +621,13 @@ export async function monitorInlineProvider(params) {
343
621
  ...prefixOptions,
344
622
  ...typingCallbacks,
345
623
  deliver: async (payload) => {
346
- const text = (payload.text ?? "").trim();
624
+ const rawText = (payload.text ?? "").trim();
347
625
  const mediaList = payload.mediaUrls?.length
348
626
  ? payload.mediaUrls
349
627
  : payload.mediaUrl
350
628
  ? [payload.mediaUrl]
351
629
  : [];
352
- const mediaBlock = mediaList.length ? mediaList.map((url) => `Attachment: ${url}`).join("\n") : "";
353
- const combined = text
354
- ? mediaBlock
355
- ? `${text}\n\n${mediaBlock}`
356
- : text
357
- : mediaBlock;
358
- if (!combined.trim())
359
- return;
360
- const outboundText = rewriteNumericMentionsToUsernames(combined, senderProfilesById);
630
+ const outboundText = rewriteNumericMentionsToUsernames(rawText, senderProfilesById);
361
631
  let replyToMsgId;
362
632
  if (payload.replyToId != null) {
363
633
  try {
@@ -367,20 +637,68 @@ export async function monitorInlineProvider(params) {
367
637
  // ignore
368
638
  }
369
639
  }
370
- await client.sendMessage({
371
- chatId,
372
- text: outboundText,
373
- ...(replyToMsgId != null ? { replyToMsgId } : {}),
374
- parseMarkdown,
375
- });
640
+ // Keep reply chains threaded when inbound is a reply in group chats.
641
+ if (replyToMsgId == null && isGroup && msg.replyToMsgId != null) {
642
+ replyToMsgId = msg.id;
643
+ }
644
+ const rememberSent = (messageId) => {
645
+ if (messageId != null) {
646
+ rememberBotMessageId(botMessageIdsByChat, chatId, messageId);
647
+ }
648
+ };
649
+ const sendTextFallback = async (text, includeReplyTo) => {
650
+ if (!text.trim())
651
+ return;
652
+ const sent = await client.sendMessage({
653
+ chatId,
654
+ text,
655
+ ...(includeReplyTo && replyToMsgId != null ? { replyToMsgId } : {}),
656
+ parseMarkdown,
657
+ });
658
+ rememberSent(sent.messageId);
659
+ };
660
+ if (mediaList.length === 0) {
661
+ if (!outboundText.trim())
662
+ return;
663
+ await sendTextFallback(outboundText, true);
664
+ statusSink?.({ lastOutboundAt: Date.now() });
665
+ return;
666
+ }
667
+ for (let index = 0; index < mediaList.length; index++) {
668
+ const mediaUrl = mediaList[index];
669
+ if (!mediaUrl?.trim())
670
+ continue;
671
+ const isFirst = index === 0;
672
+ const caption = isFirst ? outboundText : "";
673
+ try {
674
+ const media = await uploadInlineMediaFromUrl({
675
+ client,
676
+ cfg,
677
+ accountId: account.accountId,
678
+ mediaUrl,
679
+ });
680
+ const sent = await client.sendMessage({
681
+ chatId,
682
+ ...(caption ? { text: caption } : {}),
683
+ media,
684
+ ...(isFirst && replyToMsgId != null ? { replyToMsgId } : {}),
685
+ ...(caption ? { parseMarkdown } : {}),
686
+ });
687
+ rememberSent(sent.messageId);
688
+ }
689
+ catch (error) {
690
+ runtime.error?.(`inline media upload failed; falling back to url text (${String(error)})`);
691
+ const fallbackText = caption
692
+ ? `${caption}\n\nAttachment: ${mediaUrl}`
693
+ : `Attachment: ${mediaUrl}`;
694
+ await sendTextFallback(fallbackText, isFirst);
695
+ }
696
+ }
376
697
  statusSink?.({ lastOutboundAt: Date.now() });
377
698
  },
378
699
  onError: (err, info) => runtime.error?.(`inline ${info.kind} reply failed: ${String(err)}`),
379
700
  },
380
- replyOptions: {
381
- onModelSelected,
382
- blockReplyTimeoutMs: 25_000,
383
- },
701
+ replyOptions,
384
702
  });
385
703
  }
386
704
  }