@inline-openclaw/inline 0.0.18 → 0.0.20

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.
@@ -1,867 +0,0 @@
1
- import { buildChannelConfigSchema, DEFAULT_ACCOUNT_ID, formatPairingApproveHint, PAIRING_APPROVED_MESSAGE, } from "openclaw/plugin-sdk";
2
- import { InlineSdkClient, Method } from "@inline-chat/realtime-sdk";
3
- import { InlineConfigSchema } from "./config-schema.js";
4
- import { listInlineAccountIds, resolveDefaultInlineAccountId, resolveInlineAccount, resolveInlineToken, } from "./accounts.js";
5
- import { looksLikeInlineTargetId, normalizeInlineTarget } from "./normalize.js";
6
- import { monitorInlineProvider } from "./monitor.js";
7
- import { resolveInlineGroupRequireMention, resolveInlineGroupToolPolicy } from "./policy.js";
8
- import { inlineMessageActions } from "./actions.js";
9
- import { getInlineRuntime } from "../runtime.js";
10
- import { uploadInlineMediaFromUrl } from "./media.js";
11
- const activeMonitors = new Map();
12
- const meta = {
13
- id: "inline",
14
- label: "Inline",
15
- selectionLabel: "Inline (native)",
16
- docsPath: "/channels/inline",
17
- docsLabel: "inline",
18
- blurb: "Inline Chat via realtime RPC (bot token).",
19
- aliases: ["inline-chat"],
20
- order: 30,
21
- quickstartAllowFrom: true,
22
- };
23
- function normalizeInlineAllowEntry(raw) {
24
- return raw.trim().replace(/^inline:/i, "").replace(/^user:/i, "");
25
- }
26
- function parseInlineId(raw) {
27
- if (raw == null)
28
- return undefined;
29
- if (typeof raw === "bigint")
30
- return raw;
31
- if (typeof raw === "number") {
32
- if (!Number.isFinite(raw) || !Number.isInteger(raw) || raw < 0)
33
- return undefined;
34
- return BigInt(raw);
35
- }
36
- if (typeof raw === "string") {
37
- const trimmed = raw.trim();
38
- if (!trimmed)
39
- return undefined;
40
- try {
41
- return BigInt(trimmed);
42
- }
43
- catch {
44
- return undefined;
45
- }
46
- }
47
- return undefined;
48
- }
49
- function parseInlineOutboundTarget(params) {
50
- let normalizedTarget = params.raw.trim();
51
- const hadInlinePrefix = /^inline:/i.test(normalizedTarget);
52
- if (hadInlinePrefix) {
53
- normalizedTarget = normalizedTarget.replace(/^inline:/i, "").trim();
54
- }
55
- if (!normalizedTarget) {
56
- throw new Error(`inline ${params.context}: missing target`);
57
- }
58
- let kind = "chat";
59
- let explicitKind = false;
60
- if (/^chat:/i.test(normalizedTarget)) {
61
- kind = "chat";
62
- explicitKind = true;
63
- normalizedTarget = normalizedTarget.replace(/^chat:/i, "").trim();
64
- }
65
- else if (/^user:/i.test(normalizedTarget)) {
66
- kind = "user";
67
- explicitKind = true;
68
- normalizedTarget = normalizedTarget.replace(/^user:/i, "").trim();
69
- }
70
- // Session-derived targets are persisted as `inline:<chatId>`.
71
- // Treat that shape as an explicit chat target so current-chat sends stay stable.
72
- if (hadInlinePrefix && !explicitKind) {
73
- explicitKind = true;
74
- }
75
- // Keep backward compatibility for existing bare numeric ids that
76
- // historically mapped to chat ids.
77
- normalizedTarget = normalizeInlineTarget(normalizedTarget) ?? normalizedTarget;
78
- if (!/^[0-9]+$/.test(normalizedTarget)) {
79
- throw new Error(`inline ${params.context}: invalid target "${params.raw}" (expected chat id or user id)`);
80
- }
81
- return {
82
- targetId: BigInt(normalizedTarget),
83
- kind,
84
- normalizedNumeric: normalizedTarget,
85
- explicitKind,
86
- raw: params.raw,
87
- };
88
- }
89
- async function listInlineTargetIds(client) {
90
- const result = await client.invokeRaw(Method.GET_CHATS, {
91
- oneofKind: "getChats",
92
- getChats: {},
93
- });
94
- if (result.oneofKind !== "getChats") {
95
- throw new Error(`inline target resolve: expected getChats result, got ${String(result.oneofKind)}`);
96
- }
97
- return {
98
- chatIds: new Set((result.getChats.chats ?? []).map((chat) => String(chat.id))),
99
- userIds: new Set((result.getChats.users ?? []).map((user) => String(user.id))),
100
- };
101
- }
102
- async function resolveInlineOutboundTarget(params) {
103
- const { target } = params;
104
- if (target.explicitKind || target.kind === "user") {
105
- return target;
106
- }
107
- let ids = null;
108
- try {
109
- ids = await listInlineTargetIds(params.client);
110
- }
111
- catch {
112
- // Keep legacy chat-target behavior if live lookup is unavailable.
113
- return target;
114
- }
115
- const matchesChat = ids.chatIds.has(target.normalizedNumeric);
116
- const matchesUser = ids.userIds.has(target.normalizedNumeric);
117
- if (matchesChat && matchesUser) {
118
- throw new Error(`inline ${params.context}: ambiguous numeric target "${target.raw}" matches both chat and user ids. Use chat:${target.normalizedNumeric} or user:${target.normalizedNumeric}.`);
119
- }
120
- if (!matchesChat && matchesUser) {
121
- return {
122
- ...target,
123
- kind: "user",
124
- };
125
- }
126
- return target;
127
- }
128
- function isChatInvalidError(error) {
129
- if (error == null)
130
- return false;
131
- const text = error instanceof Error ? error.message : String(error);
132
- return text.toUpperCase().includes("CHAT_INVALID");
133
- }
134
- function wrapInlineTargetError(params) {
135
- if (isChatInvalidError(params.error) &&
136
- params.resolvedTarget.kind === "chat" &&
137
- !params.target.explicitKind) {
138
- return new Error(`inline ${params.context}: target "${params.target.raw}" was sent as chatId ${params.target.normalizedNumeric} and failed with CHAT_INVALID. If this is a user id, use user:${params.target.normalizedNumeric}.`, { cause: params.error instanceof Error ? params.error : undefined });
139
- }
140
- if (isChatInvalidError(params.error) &&
141
- params.resolvedTarget.kind === "user" &&
142
- params.target.explicitKind) {
143
- return new Error(`inline ${params.context}: user target "${params.target.raw}" failed with CHAT_INVALID. This usually means no direct chat exists yet for that user.`, { cause: params.error instanceof Error ? params.error : undefined });
144
- }
145
- return params.error instanceof Error ? params.error : new Error(String(params.error));
146
- }
147
- function buildInlineSendTarget(target) {
148
- if (target.kind === "user") {
149
- return { userId: target.targetId };
150
- }
151
- return { chatId: target.targetId };
152
- }
153
- function formatInlineResultChatId(target) {
154
- if (target.kind === "user") {
155
- return `user:${target.normalizedNumeric}`;
156
- }
157
- return target.normalizedNumeric;
158
- }
159
- function buildInlineDisplayName(params) {
160
- const explicit = [params.firstName?.trim(), params.lastName?.trim()].filter(Boolean).join(" ");
161
- if (explicit)
162
- return explicit;
163
- const username = params.username?.trim();
164
- if (username)
165
- return `@${username}`;
166
- return "Unknown";
167
- }
168
- function toInlineUserDirectoryEntry(user) {
169
- return {
170
- kind: "user",
171
- id: String(user.id),
172
- name: buildInlineDisplayName(user),
173
- ...(user.username?.trim() ? { handle: `@${user.username.trim()}` } : {}),
174
- ...(user.profilePhoto?.cdnUrl ? { avatarUrl: user.profilePhoto.cdnUrl } : {}),
175
- raw: {
176
- username: user.username ?? null,
177
- phoneNumber: user.phoneNumber ?? null,
178
- bot: user.bot ?? false,
179
- },
180
- };
181
- }
182
- function toInlineUserTargetId(userId) {
183
- return `user:${userId}`;
184
- }
185
- function toInlineUserTargetDirectoryEntry(user) {
186
- const base = toInlineUserDirectoryEntry(user);
187
- return {
188
- ...base,
189
- id: toInlineUserTargetId(base.id),
190
- };
191
- }
192
- function toInlineGroupDirectoryEntry(chat, dialogByChatId) {
193
- const dialog = dialogByChatId.get(String(chat.id));
194
- return {
195
- kind: "group",
196
- id: String(chat.id),
197
- name: chat.title,
198
- raw: {
199
- spaceId: chat.spaceId != null ? String(chat.spaceId) : null,
200
- isPublic: chat.isPublic ?? false,
201
- unreadCount: dialog?.unreadCount ?? 0,
202
- archived: Boolean(dialog?.archived),
203
- pinned: Boolean(dialog?.pinned),
204
- },
205
- };
206
- }
207
- function matchesInlineQuery(value, query) {
208
- if (!query)
209
- return true;
210
- return value.toLowerCase().includes(query);
211
- }
212
- function normalizeSearchQuery(query) {
213
- return query?.trim().toLowerCase() ?? "";
214
- }
215
- function buildDialogMap(dialogs) {
216
- const map = new Map();
217
- for (const dialog of dialogs) {
218
- if (dialog.chatId != null) {
219
- map.set(String(dialog.chatId), dialog);
220
- continue;
221
- }
222
- const peer = dialog.peer?.type;
223
- if (peer?.oneofKind === "chat") {
224
- map.set(String(peer.chat.chatId), dialog);
225
- }
226
- }
227
- return map;
228
- }
229
- async function withInlineClient(params) {
230
- const account = resolveInlineAccount({ cfg: params.cfg, accountId: params.accountId ?? null });
231
- if (!account.configured || !account.baseUrl) {
232
- throw new Error(`Inline not configured for account "${account.accountId}" (missing token or baseUrl)`);
233
- }
234
- const token = await resolveInlineToken(account);
235
- const client = new InlineSdkClient({
236
- baseUrl: account.baseUrl,
237
- token,
238
- });
239
- await client.connect();
240
- try {
241
- return await params.fn(client, account);
242
- }
243
- finally {
244
- await client.close().catch(() => { });
245
- }
246
- }
247
- async function notifyPairingApprovedInline(params) {
248
- const normalizedId = normalizeInlineAllowEntry(params.id);
249
- if (!normalizedId)
250
- return;
251
- let userId;
252
- try {
253
- userId = BigInt(normalizedId);
254
- }
255
- catch {
256
- throw new Error(`inline pairing notify: invalid user id "${params.id}"`);
257
- }
258
- const accountId = resolveDefaultInlineAccountId(params.cfg);
259
- const account = resolveInlineAccount({ cfg: params.cfg, accountId });
260
- if (!account.configured || !account.baseUrl) {
261
- throw new Error(`Inline not configured for account "${account.accountId}" (missing token or baseUrl)`);
262
- }
263
- const token = await resolveInlineToken(account);
264
- const client = new InlineSdkClient({
265
- baseUrl: account.baseUrl,
266
- token,
267
- });
268
- await client.connect();
269
- try {
270
- await client.sendMessage({
271
- userId,
272
- text: PAIRING_APPROVED_MESSAGE,
273
- parseMarkdown: account.config.parseMarkdown ?? true,
274
- });
275
- }
276
- finally {
277
- await client.close().catch(() => { });
278
- }
279
- }
280
- async function sendMessageInline(params) {
281
- const account = resolveInlineAccount({ cfg: params.cfg, accountId: params.accountId ?? null });
282
- if (!account.configured || !account.baseUrl) {
283
- throw new Error(`Inline not configured for account "${account.accountId}" (missing token or baseUrl)`);
284
- }
285
- const token = await resolveInlineToken(account);
286
- const target = parseInlineOutboundTarget({
287
- raw: params.to,
288
- context: "sendText",
289
- });
290
- const client = new InlineSdkClient({
291
- baseUrl: account.baseUrl,
292
- token,
293
- });
294
- await client.connect();
295
- try {
296
- // Inline "threads" are modeled as chats (chatId). OpenClaw's threadId is not a message id.
297
- // Only map OpenClaw replyToId -> Inline replyToMsgId.
298
- const replyToMsgId = parseInlineId(params.replyToId);
299
- const resolvedTarget = await resolveInlineOutboundTarget({
300
- client,
301
- context: "sendText",
302
- target,
303
- });
304
- const result = await client
305
- .sendMessage({
306
- ...buildInlineSendTarget(resolvedTarget),
307
- text: params.text,
308
- ...(replyToMsgId != null ? { replyToMsgId } : {}),
309
- parseMarkdown: account.config.parseMarkdown ?? true,
310
- })
311
- .catch((error) => {
312
- throw wrapInlineTargetError({
313
- error,
314
- context: "sendText",
315
- target,
316
- resolvedTarget,
317
- });
318
- });
319
- const bestEffort = result.messageId != null ? String(result.messageId) : BigInt(Date.now()).toString();
320
- return {
321
- messageId: bestEffort,
322
- chatId: formatInlineResultChatId(resolvedTarget),
323
- };
324
- }
325
- finally {
326
- await client.close().catch(() => { });
327
- }
328
- }
329
- async function sendMediaInline(params) {
330
- const account = resolveInlineAccount({ cfg: params.cfg, accountId: params.accountId ?? null });
331
- if (!account.configured || !account.baseUrl) {
332
- throw new Error(`Inline not configured for account "${account.accountId}" (missing token or baseUrl)`);
333
- }
334
- const token = await resolveInlineToken(account);
335
- const target = parseInlineOutboundTarget({
336
- raw: params.to,
337
- context: "sendMedia",
338
- });
339
- const replyToMsgId = parseInlineId(params.replyToId);
340
- const caption = params.text.trim();
341
- const client = new InlineSdkClient({
342
- baseUrl: account.baseUrl,
343
- token,
344
- });
345
- await client.connect();
346
- try {
347
- const resolvedTarget = await resolveInlineOutboundTarget({
348
- client,
349
- context: "sendMedia",
350
- target,
351
- });
352
- const media = await uploadInlineMediaFromUrl({
353
- client,
354
- cfg: params.cfg,
355
- accountId: account.accountId,
356
- mediaUrl: params.mediaUrl,
357
- });
358
- const result = await client
359
- .sendMessage({
360
- ...buildInlineSendTarget(resolvedTarget),
361
- ...(caption ? { text: caption } : {}),
362
- media,
363
- ...(replyToMsgId != null ? { replyToMsgId } : {}),
364
- ...(caption ? { parseMarkdown: account.config.parseMarkdown ?? true } : {}),
365
- })
366
- .catch((error) => {
367
- throw wrapInlineTargetError({
368
- error,
369
- context: "sendMedia",
370
- target,
371
- resolvedTarget,
372
- });
373
- });
374
- const bestEffort = result.messageId != null ? String(result.messageId) : BigInt(Date.now()).toString();
375
- return {
376
- messageId: bestEffort,
377
- chatId: formatInlineResultChatId(resolvedTarget),
378
- };
379
- }
380
- finally {
381
- await client.close().catch(() => { });
382
- }
383
- }
384
- function resolveDirectoryLimit(limit) {
385
- const parsed = typeof limit === "number" ? Math.trunc(limit) : undefined;
386
- return Math.max(1, Math.min(200, parsed ?? 50));
387
- }
388
- async function fetchInlineChatsSnapshot(params) {
389
- return await withInlineClient({
390
- cfg: params.cfg,
391
- accountId: params.accountId ?? null,
392
- fn: async (client) => {
393
- const result = await client.invokeRaw(Method.GET_CHATS, {
394
- oneofKind: "getChats",
395
- getChats: {},
396
- });
397
- if (result.oneofKind !== "getChats") {
398
- throw new Error(`inline directory: expected getChats result, got ${String(result.oneofKind)}`);
399
- }
400
- const chats = result.getChats.chats ?? [];
401
- const users = result.getChats.users ?? [];
402
- const dialogByChatId = buildDialogMap(result.getChats.dialogs ?? []);
403
- return { chats, users, dialogByChatId };
404
- },
405
- });
406
- }
407
- function normalizeResolverInput(input) {
408
- return input.trim();
409
- }
410
- function resolveInlineGroupCandidates(params) {
411
- const raw = normalizeResolverInput(params.input);
412
- if (!raw)
413
- return [];
414
- const normalized = normalizeInlineTarget(raw) ?? raw;
415
- const lowered = normalized.toLowerCase();
416
- if (/^[0-9]+$/.test(normalized)) {
417
- const exact = params.chats.find((chat) => chat.id === normalized);
418
- return exact ? [{ id: exact.id, name: exact.name }] : [];
419
- }
420
- const byExactName = params.chats.filter((chat) => (chat.name ?? "").trim().toLowerCase() === lowered);
421
- if (byExactName.length > 0) {
422
- return byExactName.map((chat) => ({ id: chat.id, name: chat.name }));
423
- }
424
- return params.chats
425
- .filter((chat) => (chat.name ?? "").trim().toLowerCase().includes(lowered))
426
- .map((chat) => ({ id: chat.id, name: chat.name }));
427
- }
428
- function resolveInlineUserCandidates(params) {
429
- const raw = normalizeResolverInput(params.input);
430
- if (!raw)
431
- return [];
432
- const withoutPrefix = raw.replace(/^inline:/i, "").replace(/^user:/i, "").trim();
433
- const normalized = withoutPrefix.startsWith("@") ? withoutPrefix.slice(1) : withoutPrefix;
434
- const lowered = normalized.toLowerCase();
435
- if (/^[0-9]+$/.test(normalized)) {
436
- const exact = params.users.find((user) => user.id === normalized);
437
- return exact ? [{ id: exact.id, name: exact.name }] : [];
438
- }
439
- const byHandle = params.users.filter((user) => (user.handle ?? "").replace(/^@/, "").toLowerCase() === lowered);
440
- if (byHandle.length > 0) {
441
- return byHandle.map((user) => ({ id: user.id, name: user.name }));
442
- }
443
- const byName = params.users.filter((user) => (user.name ?? "").trim().toLowerCase() === lowered);
444
- if (byName.length > 0) {
445
- return byName.map((user) => ({ id: user.id, name: user.name }));
446
- }
447
- return params.users
448
- .filter((user) => {
449
- const haystack = [user.name ?? "", user.handle ?? "", user.id].join("\n").toLowerCase();
450
- return haystack.includes(lowered);
451
- })
452
- .map((user) => ({ id: user.id, name: user.name }));
453
- }
454
- export const inlineChannelPlugin = {
455
- id: "inline",
456
- meta,
457
- capabilities: {
458
- chatTypes: ["direct", "group"],
459
- media: true,
460
- reactions: true,
461
- edit: true,
462
- reply: true,
463
- groupManagement: true,
464
- threads: false,
465
- nativeCommands: false,
466
- blockStreaming: true,
467
- },
468
- streaming: {
469
- blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
470
- },
471
- reload: { configPrefixes: ["channels.inline"] },
472
- configSchema: buildChannelConfigSchema(InlineConfigSchema),
473
- config: {
474
- listAccountIds: (cfg) => listInlineAccountIds(cfg),
475
- resolveAccount: (cfg, accountId) => resolveInlineAccount({ cfg, accountId: accountId ?? null }),
476
- defaultAccountId: (cfg) => resolveDefaultInlineAccountId(cfg),
477
- isConfigured: (account) => account.configured,
478
- describeAccount: (account) => ({
479
- accountId: account.accountId,
480
- name: account.name,
481
- enabled: account.enabled,
482
- configured: account.configured,
483
- baseUrl: account.baseUrl ? "[set]" : "[missing]",
484
- tokenSource: account.token ? "config" : account.tokenFile ? "file" : "missing",
485
- }),
486
- resolveAllowFrom: ({ cfg, accountId }) => (resolveInlineAccount({ cfg, accountId: accountId ?? null }).config.allowFrom ?? []).map((entry) => normalizeInlineAllowEntry(String(entry))),
487
- formatAllowFrom: ({ allowFrom }) => allowFrom
488
- .map((entry) => String(entry).trim())
489
- .filter(Boolean)
490
- .map((entry) => normalizeInlineAllowEntry(entry)),
491
- },
492
- pairing: {
493
- idLabel: "inlineUserId",
494
- normalizeAllowEntry: (entry) => normalizeInlineAllowEntry(entry),
495
- notifyApproval: async ({ cfg, id }) => {
496
- await notifyPairingApprovedInline({ cfg, id });
497
- },
498
- },
499
- security: {
500
- resolveDmPolicy: ({ cfg, accountId, account }) => {
501
- const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
502
- const useAccountPath = Boolean(cfg.channels?.inline?.accounts?.[resolvedAccountId]);
503
- const basePath = useAccountPath
504
- ? `channels.inline.accounts.${resolvedAccountId}.`
505
- : "channels.inline.";
506
- return {
507
- policy: account.config.dmPolicy ?? "pairing",
508
- allowFrom: account.config.allowFrom ?? [],
509
- policyPath: `${basePath}dmPolicy`,
510
- allowFromPath: `${basePath}allowFrom`,
511
- approveHint: formatPairingApproveHint("inline"),
512
- normalizeEntry: (raw) => normalizeInlineAllowEntry(raw),
513
- };
514
- },
515
- collectWarnings: ({ account, cfg }) => {
516
- const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
517
- const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
518
- if (groupPolicy !== "open") {
519
- return [];
520
- }
521
- const groupRulesConfigured = Boolean(account.config.groups) && Object.keys(account.config.groups ?? {}).length > 0;
522
- if (groupRulesConfigured) {
523
- return [
524
- "- Inline groups: groupPolicy=\"open\" allows any group message to reach the agent (subject to mention policy). Set channels.inline.groupPolicy=\"allowlist\" for stricter routing.",
525
- ];
526
- }
527
- return [
528
- "- Inline groups: groupPolicy=\"open\" with no group rules means every group can trigger replies. Consider channels.inline.groupPolicy=\"allowlist\".",
529
- ];
530
- },
531
- },
532
- groups: {
533
- resolveRequireMention: ({ cfg, accountId, groupId }) => {
534
- const resolved = resolveInlineAccount({ cfg, accountId: accountId ?? null });
535
- return resolveInlineGroupRequireMention({
536
- cfg,
537
- groupId,
538
- accountId,
539
- requireMentionDefault: resolved.config.requireMention ?? false,
540
- });
541
- },
542
- resolveToolPolicy: ({ cfg, accountId, groupId, senderId, senderName, senderUsername, senderE164 }) => resolveInlineGroupToolPolicy({
543
- cfg,
544
- groupId,
545
- accountId,
546
- senderId,
547
- senderName,
548
- senderUsername,
549
- senderE164,
550
- }),
551
- },
552
- agentPrompt: {
553
- messageToolHints: () => [
554
- "- Inline targeting: omit `target` to reply in the current chat.",
555
- "- Inline explicit targets: `chat:<chatId>` for chats and `user:<userId>` for direct users. Prefer `user:` for DM user targets.",
556
- "- Inline special tools: use `inline_nudge` to send a nudge, and `inline_forward` to forward message ids between chats or users.",
557
- ],
558
- },
559
- messaging: {
560
- normalizeTarget: normalizeInlineTarget,
561
- targetResolver: {
562
- looksLikeId: looksLikeInlineTargetId,
563
- hint: "<chatId | chat:<chatId> | user:<userId>>",
564
- },
565
- },
566
- directory: {
567
- self: async ({ cfg, accountId }) => await withInlineClient({
568
- cfg,
569
- accountId: accountId ?? null,
570
- fn: async (client) => {
571
- const result = await client.invokeRaw(Method.GET_ME, {
572
- oneofKind: "getMe",
573
- getMe: {},
574
- });
575
- if (result.oneofKind !== "getMe") {
576
- throw new Error(`inline directory: expected getMe result, got ${String(result.oneofKind)}`);
577
- }
578
- if (!result.getMe.user) {
579
- throw new Error("inline directory: missing current user from getMe");
580
- }
581
- return toInlineUserDirectoryEntry(result.getMe.user);
582
- },
583
- }),
584
- listPeers: async ({ cfg, accountId, query, limit }) => {
585
- const snapshot = await fetchInlineChatsSnapshot({
586
- cfg,
587
- accountId: accountId ?? null,
588
- });
589
- const normalizedQuery = normalizeSearchQuery(query);
590
- const maxItems = resolveDirectoryLimit(limit);
591
- return snapshot.users
592
- .map((user) => toInlineUserTargetDirectoryEntry(user))
593
- .filter((user) => {
594
- if (!normalizedQuery)
595
- return true;
596
- const haystack = [user.id, user.name ?? "", user.handle ?? ""].join("\n").toLowerCase();
597
- return matchesInlineQuery(haystack, normalizedQuery);
598
- })
599
- .slice(0, maxItems);
600
- },
601
- listGroups: async ({ cfg, accountId, query, limit }) => {
602
- const snapshot = await fetchInlineChatsSnapshot({
603
- cfg,
604
- accountId: accountId ?? null,
605
- });
606
- const normalizedQuery = normalizeSearchQuery(query);
607
- const maxItems = resolveDirectoryLimit(limit);
608
- return snapshot.chats
609
- .map((chat) => toInlineGroupDirectoryEntry(chat, snapshot.dialogByChatId))
610
- .filter((chat) => {
611
- if (!normalizedQuery)
612
- return true;
613
- const haystack = [chat.id, chat.name ?? ""].join("\n").toLowerCase();
614
- return matchesInlineQuery(haystack, normalizedQuery);
615
- })
616
- .slice(0, maxItems);
617
- },
618
- listGroupMembers: async ({ cfg, accountId, groupId, limit }) => await withInlineClient({
619
- cfg,
620
- accountId: accountId ?? null,
621
- fn: async (client) => {
622
- const normalizedGroupId = normalizeInlineTarget(groupId) ?? groupId.trim();
623
- if (!/^[0-9]+$/.test(normalizedGroupId)) {
624
- throw new Error(`inline directory: invalid groupId "${groupId}"`);
625
- }
626
- const chatId = BigInt(normalizedGroupId);
627
- const result = await client.invokeRaw(Method.GET_CHAT_PARTICIPANTS, {
628
- oneofKind: "getChatParticipants",
629
- getChatParticipants: { chatId },
630
- });
631
- if (result.oneofKind !== "getChatParticipants") {
632
- throw new Error(`inline directory: expected getChatParticipants result, got ${String(result.oneofKind)}`);
633
- }
634
- const usersById = new Map((result.getChatParticipants.users ?? []).map((user) => [String(user.id), user]));
635
- const maxItems = resolveDirectoryLimit(limit);
636
- return (result.getChatParticipants.participants ?? [])
637
- .map((participant) => usersById.get(String(participant.userId)))
638
- .filter((user) => Boolean(user))
639
- .map((user) => toInlineUserTargetDirectoryEntry(user))
640
- .slice(0, maxItems);
641
- },
642
- }),
643
- },
644
- resolver: {
645
- resolveTargets: async ({ cfg, accountId, inputs, kind }) => {
646
- const snapshot = await fetchInlineChatsSnapshot({
647
- cfg,
648
- accountId: accountId ?? null,
649
- });
650
- if (kind === "group") {
651
- const groups = snapshot.chats.map((chat) => toInlineGroupDirectoryEntry(chat, snapshot.dialogByChatId));
652
- return inputs.map((input) => {
653
- const candidates = resolveInlineGroupCandidates({ chats: groups, input });
654
- if (candidates.length === 1) {
655
- const candidate = candidates[0];
656
- if (!candidate) {
657
- return { input, resolved: false, note: "group not found" };
658
- }
659
- return {
660
- input,
661
- resolved: true,
662
- id: candidate.id,
663
- ...(candidate.name ? { name: candidate.name } : {}),
664
- };
665
- }
666
- if (candidates.length > 1) {
667
- return { input, resolved: false, note: "multiple matching groups" };
668
- }
669
- return { input, resolved: false, note: "group not found" };
670
- });
671
- }
672
- const users = snapshot.users.map((user) => toInlineUserDirectoryEntry(user));
673
- return inputs.map((input) => {
674
- const candidates = resolveInlineUserCandidates({ users, input });
675
- if (candidates.length === 1) {
676
- const candidate = candidates[0];
677
- if (!candidate) {
678
- return { input, resolved: false, note: "user not found" };
679
- }
680
- return {
681
- input,
682
- resolved: true,
683
- id: toInlineUserTargetId(candidate.id),
684
- ...(candidate.name ? { name: candidate.name } : {}),
685
- };
686
- }
687
- if (candidates.length > 1) {
688
- return { input, resolved: false, note: "multiple matching users" };
689
- }
690
- return { input, resolved: false, note: "user not found" };
691
- });
692
- },
693
- },
694
- actions: inlineMessageActions,
695
- outbound: {
696
- deliveryMode: "direct",
697
- chunker: (text, limit) => getInlineRuntime().channel.text.chunkMarkdownText(text, limit),
698
- chunkerMode: "markdown",
699
- textChunkLimit: 4000,
700
- sendPayload: async ({ cfg, to, payload, accountId, replyToId }) => {
701
- const text = payload.text ?? "";
702
- const payloadReplyToId = typeof payload.replyToId === "string" ? payload.replyToId.trim() : null;
703
- const effectiveReplyToId = payloadReplyToId || replyToId || null;
704
- const mediaUrls = payload.mediaUrls?.length
705
- ? payload.mediaUrls
706
- : payload.mediaUrl
707
- ? [payload.mediaUrl]
708
- : [];
709
- if (mediaUrls.length === 0) {
710
- const result = await sendMessageInline({
711
- cfg,
712
- to,
713
- text,
714
- accountId: accountId ?? null,
715
- replyToId: effectiveReplyToId,
716
- });
717
- return { channel: "inline", to, messageId: result.messageId, chatId: result.chatId };
718
- }
719
- let finalResult = null;
720
- for (let index = 0; index < mediaUrls.length; index += 1) {
721
- const mediaUrl = mediaUrls[index];
722
- if (!mediaUrl?.trim())
723
- continue;
724
- const isFirst = index === 0;
725
- finalResult = await sendMediaInline({
726
- cfg,
727
- to,
728
- text: isFirst ? text : "",
729
- mediaUrl,
730
- accountId: accountId ?? null,
731
- replyToId: isFirst ? effectiveReplyToId : null,
732
- });
733
- }
734
- if (!finalResult) {
735
- const result = await sendMessageInline({
736
- cfg,
737
- to,
738
- text,
739
- accountId: accountId ?? null,
740
- replyToId: effectiveReplyToId,
741
- });
742
- return { channel: "inline", to, messageId: result.messageId, chatId: result.chatId };
743
- }
744
- return { channel: "inline", to, messageId: finalResult.messageId, chatId: finalResult.chatId };
745
- },
746
- sendText: async ({ cfg, to, text, accountId, replyToId, threadId }) => {
747
- // Inline threads are modeled as chats. OpenClaw threadId isn't a message id for Inline.
748
- const result = await sendMessageInline({
749
- cfg,
750
- to,
751
- text,
752
- accountId: accountId ?? null,
753
- replyToId: replyToId ?? null,
754
- });
755
- return { channel: "inline", to, messageId: result.messageId, chatId: result.chatId };
756
- },
757
- sendMedia: async ({ cfg, to, text, mediaUrl, accountId, replyToId, threadId }) => {
758
- if (!mediaUrl) {
759
- const result = await sendMessageInline({
760
- cfg,
761
- to,
762
- text,
763
- accountId: accountId ?? null,
764
- replyToId: replyToId ?? null,
765
- });
766
- return { channel: "inline", to, messageId: result.messageId, chatId: result.chatId };
767
- }
768
- // Inline threads are modeled as chats. OpenClaw threadId isn't a message id for Inline.
769
- const result = await sendMediaInline({
770
- cfg,
771
- to,
772
- text,
773
- mediaUrl,
774
- accountId: accountId ?? null,
775
- replyToId: replyToId ?? null,
776
- });
777
- return { channel: "inline", to, messageId: result.messageId, chatId: result.chatId };
778
- },
779
- },
780
- status: {
781
- defaultRuntime: {
782
- accountId: DEFAULT_ACCOUNT_ID,
783
- running: false,
784
- lastStartAt: null,
785
- lastStopAt: null,
786
- lastError: null,
787
- },
788
- buildChannelSummary: ({ snapshot }) => ({
789
- configured: snapshot.configured ?? false,
790
- running: snapshot.running ?? false,
791
- lastStartAt: snapshot.lastStartAt ?? null,
792
- lastStopAt: snapshot.lastStopAt ?? null,
793
- lastError: snapshot.lastError ?? null,
794
- lastInboundAt: snapshot.lastInboundAt ?? null,
795
- lastOutboundAt: snapshot.lastOutboundAt ?? null,
796
- }),
797
- buildAccountSnapshot: ({ account, runtime }) => ({
798
- accountId: account.accountId,
799
- name: account.name,
800
- enabled: account.enabled,
801
- configured: account.configured,
802
- baseUrl: account.baseUrl ? "[set]" : "[missing]",
803
- tokenSource: account.token ? "config" : account.tokenFile ? "file" : "missing",
804
- running: runtime?.running ?? false,
805
- lastStartAt: runtime?.lastStartAt ?? null,
806
- lastStopAt: runtime?.lastStopAt ?? null,
807
- lastError: runtime?.lastError ?? null,
808
- lastInboundAt: runtime?.lastInboundAt ?? null,
809
- lastOutboundAt: runtime?.lastOutboundAt ?? null,
810
- }),
811
- },
812
- gateway: {
813
- startAccount: async (ctx) => {
814
- const account = ctx.account;
815
- if (!account.configured || !account.baseUrl) {
816
- throw new Error(`Inline not configured for account "${account.accountId}" (missing baseUrl or token)`);
817
- }
818
- ctx.log?.info(`[${account.accountId}] starting Inline realtime monitor`);
819
- // Best-effort stop if already running for this account.
820
- const existing = activeMonitors.get(account.accountId);
821
- if (existing) {
822
- await existing.stop().catch(() => { });
823
- activeMonitors.delete(account.accountId);
824
- }
825
- const now = Date.now();
826
- ctx.setStatus({
827
- ...ctx.getStatus(),
828
- accountId: account.accountId,
829
- configured: true,
830
- running: true,
831
- lastStartAt: now,
832
- lastError: null,
833
- });
834
- const handle = await monitorInlineProvider({
835
- cfg: ctx.cfg,
836
- account,
837
- runtime: ctx.runtime,
838
- abortSignal: ctx.abortSignal,
839
- ...(ctx.log ? { log: ctx.log } : {}),
840
- statusSink: (patch) => {
841
- ctx.setStatus({ ...ctx.getStatus(), ...patch });
842
- },
843
- });
844
- activeMonitors.set(account.accountId, handle);
845
- try {
846
- await handle.done;
847
- }
848
- finally {
849
- activeMonitors.delete(account.accountId);
850
- await handle.stop().catch(() => { });
851
- }
852
- },
853
- stopAccount: async (ctx) => {
854
- const existing = activeMonitors.get(ctx.account.accountId);
855
- if (existing) {
856
- await existing.stop().catch(() => { });
857
- activeMonitors.delete(ctx.account.accountId);
858
- }
859
- ctx.setStatus({
860
- ...ctx.getStatus(),
861
- running: false,
862
- lastStopAt: Date.now(),
863
- });
864
- },
865
- },
866
- };
867
- //# sourceMappingURL=channel.js.map