@invago/mixin 1.0.11 → 1.0.12
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 +30 -18
- package/README.zh-CN.md +22 -10
- package/package.json +4 -1
- package/src/channel.ts +50 -18
- package/src/inbound-handler.ts +282 -140
- package/src/message-context.ts +114 -0
- package/src/send-service.ts +35 -4
package/README.md
CHANGED
|
@@ -38,14 +38,22 @@ To install a specific version for the first time:
|
|
|
38
38
|
openclaw plugins install @invago/mixin@<version>
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
Then confirm the plugin is installed:
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
openclaw plugins list
|
|
45
|
-
openclaw plugins info mixin
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
##
|
|
41
|
+
Then confirm the plugin is installed:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
openclaw plugins list
|
|
45
|
+
openclaw plugins info mixin
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Cross-Platform Checklist
|
|
49
|
+
|
|
50
|
+
- The same install commands work on Windows, Linux, and macOS.
|
|
51
|
+
- Make sure `openclaw`, `node`, and your package manager (`npm` or `pnpm`) are available on `PATH`.
|
|
52
|
+
- Voice duration detection needs `ffprobe`. If it is missing, audio falls back to file sending unless `audioRequireFfprobe` is enabled.
|
|
53
|
+
- For local development, run `npm install` once and then `openclaw plugins install -l .` or `openclaw plugins install .`.
|
|
54
|
+
- Runtime data is stored under the OpenClaw state directory resolved from `OPENCLAW_STATE_DIR`, `CLAWDBOT_STATE_DIR`, or `OPENCLAW_HOME`; no OS-specific path is required in plugin config.
|
|
55
|
+
|
|
56
|
+
## Local Development Install
|
|
49
57
|
|
|
50
58
|
If you are developing locally, clone the repository and install dependencies:
|
|
51
59
|
|
|
@@ -315,12 +323,14 @@ Mixin now supports formal group access controls in addition to direct-message `d
|
|
|
315
323
|
- `groupPolicy: "disabled"` blocks the entire conversation.
|
|
316
324
|
- `conversations.<conversationId>` overrides account-level group settings for that single conversation.
|
|
317
325
|
|
|
318
|
-
Important delivery boundary:
|
|
319
|
-
|
|
320
|
-
- In practice, Mixin group bots reliably receive messages when the bot is explicitly mentioned.
|
|
321
|
-
-
|
|
322
|
-
-
|
|
323
|
-
-
|
|
326
|
+
Important delivery boundary:
|
|
327
|
+
|
|
328
|
+
- In practice, Mixin group bots reliably receive messages when the bot is explicitly mentioned.
|
|
329
|
+
- The most reliable format is `@<identity_number> your message`, for example `@7000103034 hello`.
|
|
330
|
+
- `requireMentionInGroup: false` only disables this plugin's own post-delivery filtering.
|
|
331
|
+
- It does not guarantee that Mixin will deliver every non-mention group message to the bot.
|
|
332
|
+
- If a non-mention group message produces no read receipt and no inbound log, the message most likely was not delivered to the plugin by Mixin in the first place.
|
|
333
|
+
- Group quote/reply interactions are currently not treated as a reliable bot trigger, because Mixin may not deliver those events to the bot over Blaze consistently.
|
|
324
334
|
|
|
325
335
|
Example:
|
|
326
336
|
|
|
@@ -373,10 +383,12 @@ Where to look in logs:
|
|
|
373
383
|
- Unauthorized or filtered group logs include `group sender <user_id>` and `conversationId=<conversationId>`
|
|
374
384
|
- If needed, temporarily enable a stricter group policy and let one member send a message once; the rejection log is often the fastest way to collect both values
|
|
375
385
|
|
|
376
|
-
## Usage
|
|
377
|
-
|
|
378
|
-
- Direct message: `/status` or `Hello`
|
|
379
|
-
- Group message:
|
|
386
|
+
## Usage
|
|
387
|
+
|
|
388
|
+
- Direct message: `/status` or `Hello`
|
|
389
|
+
- Group message: `@<identity_number> your question`
|
|
390
|
+
- Recommended example: `@7000103034 help me summarize this`
|
|
391
|
+
- Do not rely on quote-only or quote-plus-mention group replies as a stable trigger path.
|
|
380
392
|
|
|
381
393
|
## Operations
|
|
382
394
|
|
package/README.zh-CN.md
CHANGED
|
@@ -315,12 +315,14 @@ OpenClaw 支持通过 `bindings[].match.accountId` 把不同的频道账号直
|
|
|
315
315
|
- `groupPolicy: "disabled"` 表示整个群会话被禁用。
|
|
316
316
|
- `conversations.<conversationId>` 可以对某一个群会话覆盖账号级配置。
|
|
317
317
|
|
|
318
|
-
关于群消息投递边界:
|
|
319
|
-
|
|
320
|
-
- 目前从实际联调看,Mixin 群里稳定可用的触发方式仍然是显式 `@bot`。
|
|
321
|
-
-
|
|
322
|
-
-
|
|
323
|
-
-
|
|
318
|
+
关于群消息投递边界:
|
|
319
|
+
|
|
320
|
+
- 目前从实际联调看,Mixin 群里稳定可用的触发方式仍然是显式 `@bot`。
|
|
321
|
+
- 最稳的写法是 `@<identity_number> + 文本`,例如 `@7000103034 你好`。
|
|
322
|
+
- `requireMentionInGroup: false` 只表示关闭插件自身的群消息二次过滤。
|
|
323
|
+
- 它不能保证 Mixin 平台一定把所有未 `@` 的群消息投递给机器人。
|
|
324
|
+
- 如果群里未 `@` 的消息既没有已读,也没有任何入站日志,通常说明这条消息根本没有被 Mixin 投递到插件。
|
|
325
|
+
- 目前群内“引用回复”不应被当成稳定触发方式,因为 Mixin 不一定会把这类事件稳定投递给 bot。
|
|
324
326
|
|
|
325
327
|
示例:
|
|
326
328
|
|
|
@@ -376,10 +378,12 @@ OpenClaw 支持通过 `bindings[].match.accountId` 把不同的频道账号直
|
|
|
376
378
|
- 群消息被拦截或未授权时,日志里会带 `group sender <user_id>` 和 `conversationId=<conversationId>`
|
|
377
379
|
- 如果现场不好拿值,可以临时把群策略收紧一点,让目标成员先发一条消息;拒绝日志通常是最快同时拿到这两个值的方式
|
|
378
380
|
|
|
379
|
-
## 使用方式
|
|
380
|
-
|
|
381
|
-
- 私聊:`/status` 或 `Hello`
|
|
382
|
-
-
|
|
381
|
+
## 使用方式
|
|
382
|
+
|
|
383
|
+
- 私聊:`/status` 或 `Hello`
|
|
384
|
+
- 群聊:`@<identity_number> 你的问题`
|
|
385
|
+
- 推荐示例:`@7000103034 帮我总结一下`
|
|
386
|
+
- 目前不要把“仅引用回复”或“引用后再 @”当作稳定触发方式。
|
|
383
387
|
|
|
384
388
|
## 运维
|
|
385
389
|
|
|
@@ -696,3 +700,11 @@ Mixin 现在已经支持通过 MixPay one-time payment 做收款。
|
|
|
696
700
|
- 本地开发建议使用 `openclaw plugins install -l .`。
|
|
697
701
|
|
|
698
702
|
- 使用 `/setup` 进入配置引导流程。
|
|
703
|
+
|
|
704
|
+
## 跨平台检查清单
|
|
705
|
+
|
|
706
|
+
- Windows、Linux、macOS 都使用同一套安装命令。
|
|
707
|
+
- 请确保 `openclaw`、`node` 和包管理器(`npm` 或 `pnpm`)已经加入 `PATH`。
|
|
708
|
+
- 语音时长识别依赖 `ffprobe`。如果系统里没有它,音频会降级为按文件发送,除非你显式开启 `audioRequireFfprobe`。
|
|
709
|
+
- 本地开发时,先执行一次 `npm install`,然后用 `openclaw plugins install -l .` 或 `openclaw plugins install .` 安装。
|
|
710
|
+
- 运行时数据会存放在 OpenClaw 状态目录中,来源可能是 `OPENCLAW_STATE_DIR`、`CLAWDBOT_STATE_DIR` 或 `OPENCLAW_HOME`,插件配置里不需要写死操作系统路径。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@invago/mixin",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
4
4
|
"description": "Mixin Messenger channel plugin for OpenClaw",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -13,10 +13,13 @@
|
|
|
13
13
|
"openclaw": ">=2026.2.0"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
+
"@sinclair/typebox": "^0.34.48",
|
|
16
17
|
"@mixin.dev/mixin-node-sdk": "^7.4.1",
|
|
17
18
|
"@noble/curves": "^2.0.1",
|
|
18
19
|
"@noble/hashes": "^2.0.1",
|
|
19
20
|
"axios": "^1.13.6",
|
|
21
|
+
"ajv": "^8.18.0",
|
|
22
|
+
"ajv-formats": "^3.0.1",
|
|
20
23
|
"express": "^5.2.1",
|
|
21
24
|
"jiti": "^1.21.0",
|
|
22
25
|
"proxy-agent": "^6.5.0",
|
package/src/channel.ts
CHANGED
|
@@ -35,12 +35,43 @@ const conversationCategoryCache = new Map<string, {
|
|
|
35
35
|
expiresAt: number;
|
|
36
36
|
}>();
|
|
37
37
|
|
|
38
|
-
function maskKey(key: string): string {
|
|
39
|
-
if (!key || key.length < 8) {
|
|
40
|
-
return "****";
|
|
41
|
-
}
|
|
42
|
-
return key.slice(0, 4) + "****" + key.slice(-4);
|
|
43
|
-
}
|
|
38
|
+
function maskKey(key: string): string {
|
|
39
|
+
if (!key || key.length < 8) {
|
|
40
|
+
return "****";
|
|
41
|
+
}
|
|
42
|
+
return key.slice(0, 4) + "****" + key.slice(-4);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function extractQuoteMessageId(rawMsg: unknown): string | undefined {
|
|
46
|
+
const seen = new Set<unknown>();
|
|
47
|
+
const stack: unknown[] = [rawMsg];
|
|
48
|
+
|
|
49
|
+
while (stack.length > 0) {
|
|
50
|
+
const value = stack.pop();
|
|
51
|
+
if (!value || typeof value !== "object" || seen.has(value)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
seen.add(value);
|
|
55
|
+
|
|
56
|
+
const record = value as Record<string, unknown>;
|
|
57
|
+
const quoteMessageId = record.quote_message_id;
|
|
58
|
+
if (typeof quoteMessageId === "string" && quoteMessageId.trim()) {
|
|
59
|
+
return quoteMessageId.trim();
|
|
60
|
+
}
|
|
61
|
+
const camelQuoteMessageId = record.quoteMessageId;
|
|
62
|
+
if (typeof camelQuoteMessageId === "string" && camelQuoteMessageId.trim()) {
|
|
63
|
+
return camelQuoteMessageId.trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const nested of Object.values(record)) {
|
|
67
|
+
if (nested && typeof nested === "object") {
|
|
68
|
+
stack.push(nested);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
44
75
|
|
|
45
76
|
async function resolveIsDirectMessage(params: {
|
|
46
77
|
config: MixinAccountConfig;
|
|
@@ -429,18 +460,19 @@ export const mixinPlugin = {
|
|
|
429
460
|
`[mixin] inbound route context: messageId=${rawMsg.message_id}, conversationId=${rawMsg.conversation_id ?? ""}, userId=${rawMsg.user_id}, isDirect=${isDirect}`,
|
|
430
461
|
);
|
|
431
462
|
|
|
432
|
-
const msg: MixinInboundMessage = {
|
|
433
|
-
conversationId: rawMsg.conversation_id ?? "",
|
|
434
|
-
userId: rawMsg.user_id,
|
|
435
|
-
messageId: rawMsg.message_id,
|
|
436
|
-
category: rawMsg.category ?? "PLAIN_TEXT",
|
|
437
|
-
data: rawMsg.data_base64 ?? rawMsg.data ?? "",
|
|
438
|
-
createdAt: rawMsg.created_at ?? new Date().toISOString(),
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
463
|
+
const msg: MixinInboundMessage = {
|
|
464
|
+
conversationId: rawMsg.conversation_id ?? "",
|
|
465
|
+
userId: rawMsg.user_id,
|
|
466
|
+
messageId: rawMsg.message_id,
|
|
467
|
+
category: rawMsg.category ?? "PLAIN_TEXT",
|
|
468
|
+
data: rawMsg.data_base64 ?? rawMsg.data ?? "",
|
|
469
|
+
createdAt: rawMsg.created_at ?? new Date().toISOString(),
|
|
470
|
+
quoteMessageId: extractQuoteMessageId(rawMsg),
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
try {
|
|
474
|
+
await handleMixinMessage({ cfg, accountId, msg, isDirect, log });
|
|
475
|
+
} catch (err) {
|
|
444
476
|
log.error(`error handling message ${msg.messageId}`, err);
|
|
445
477
|
}
|
|
446
478
|
},
|
package/src/inbound-handler.ts
CHANGED
|
@@ -16,17 +16,19 @@ import {
|
|
|
16
16
|
purgePermanentInvalidOutboxEntries,
|
|
17
17
|
sendTextMessage,
|
|
18
18
|
} from "./send-service.js";
|
|
19
|
-
import { buildClient } from "./shared.js";
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
import { buildClient } from "./shared.js";
|
|
20
|
+
import { rememberMixinMessage, resolveMixinReplyContext } from "./message-context.js";
|
|
21
|
+
|
|
22
|
+
export interface MixinInboundMessage {
|
|
23
|
+
conversationId: string;
|
|
24
|
+
userId: string;
|
|
25
|
+
messageId: string;
|
|
26
|
+
category: string;
|
|
27
|
+
data: string;
|
|
28
|
+
createdAt: string;
|
|
29
|
+
quoteMessageId?: string;
|
|
30
|
+
publicKey?: string;
|
|
31
|
+
}
|
|
30
32
|
|
|
31
33
|
const processedMessages = new Set<string>();
|
|
32
34
|
const MAX_DEDUP_SIZE = 2000;
|
|
@@ -55,12 +57,19 @@ type CachedGroupProfile = {
|
|
|
55
57
|
expiresAt: number;
|
|
56
58
|
};
|
|
57
59
|
|
|
58
|
-
type CachedBotProfile = {
|
|
59
|
-
name: string;
|
|
60
|
-
expiresAt: number;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
type
|
|
60
|
+
type CachedBotProfile = {
|
|
61
|
+
name: string;
|
|
62
|
+
expiresAt: number;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
type CachedBotIdentity = {
|
|
66
|
+
name: string;
|
|
67
|
+
userId: string;
|
|
68
|
+
identityNumber: string;
|
|
69
|
+
expiresAt: number;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
type MixinAttachmentRequest = {
|
|
64
73
|
attachmentId: string;
|
|
65
74
|
mimeType?: string;
|
|
66
75
|
size?: number;
|
|
@@ -68,10 +77,11 @@ type MixinAttachmentRequest = {
|
|
|
68
77
|
duration?: number;
|
|
69
78
|
};
|
|
70
79
|
|
|
71
|
-
const cachedUserProfiles = new Map<string, CachedUserProfile>();
|
|
72
|
-
const cachedGroupProfiles = new Map<string, CachedGroupProfile>();
|
|
73
|
-
const cachedBotProfiles = new Map<string, CachedBotProfile>();
|
|
74
|
-
|
|
80
|
+
const cachedUserProfiles = new Map<string, CachedUserProfile>();
|
|
81
|
+
const cachedGroupProfiles = new Map<string, CachedGroupProfile>();
|
|
82
|
+
const cachedBotProfiles = new Map<string, CachedBotProfile>();
|
|
83
|
+
const cachedBotIdentities = new Map<string, CachedBotIdentity>();
|
|
84
|
+
let cachedUpdateSessionStore:
|
|
75
85
|
| ((storePath: string, mutator: (store: Record<string, Record<string, unknown>>) => void | Promise<void>) => Promise<unknown>)
|
|
76
86
|
| null
|
|
77
87
|
| undefined;
|
|
@@ -122,16 +132,20 @@ function pruneUnauthNotifiedGroups(now: number): void {
|
|
|
122
132
|
}
|
|
123
133
|
}
|
|
124
134
|
|
|
125
|
-
function decodeContent(category: string, data: string): string {
|
|
126
|
-
if (category.startsWith("PLAIN_TEXT") || category.startsWith("PLAIN_POST")) {
|
|
135
|
+
function decodeContent(category: string, data: string): string {
|
|
136
|
+
if (category.startsWith("PLAIN_TEXT") || category.startsWith("PLAIN_POST")) {
|
|
127
137
|
try {
|
|
128
138
|
return Buffer.from(data, "base64").toString("utf-8");
|
|
129
139
|
} catch {
|
|
130
140
|
return data;
|
|
131
141
|
}
|
|
132
|
-
}
|
|
133
|
-
return `[${category}]`;
|
|
134
|
-
}
|
|
142
|
+
}
|
|
143
|
+
return `[${category}]`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function escapeRegExp(value: string): string {
|
|
147
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
148
|
+
}
|
|
135
149
|
|
|
136
150
|
function buildUserProfileCacheKey(accountId: string, userId: string): string {
|
|
137
151
|
return `${accountId}:${userId.trim().toLowerCase()}`;
|
|
@@ -346,41 +360,51 @@ async function resolveGroupName(params: {
|
|
|
346
360
|
}
|
|
347
361
|
}
|
|
348
362
|
|
|
349
|
-
async function
|
|
350
|
-
accountId: string;
|
|
351
|
-
config: MixinAccountConfig;
|
|
352
|
-
log: { info: (m: string) => void; warn: (m: string) => void; error: (m: string, e?: unknown) => void };
|
|
353
|
-
}): Promise<
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
363
|
+
async function resolveBotIdentity(params: {
|
|
364
|
+
accountId: string;
|
|
365
|
+
config: MixinAccountConfig;
|
|
366
|
+
log: { info: (m: string) => void; warn: (m: string) => void; error: (m: string, e?: unknown) => void };
|
|
367
|
+
}): Promise<CachedBotIdentity> {
|
|
368
|
+
const cacheKey = buildBotProfileCacheKey(params.accountId);
|
|
369
|
+
const cached = cachedBotIdentities.get(cacheKey);
|
|
370
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
371
|
+
return cached;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const now = Date.now();
|
|
375
|
+
const configuredName = normalizePresentationName(params.config.name ?? "");
|
|
376
|
+
const configuredIdentity: CachedBotIdentity = {
|
|
377
|
+
name: configuredName || params.accountId,
|
|
378
|
+
userId: params.config.appId?.trim() || params.accountId,
|
|
379
|
+
identityNumber: "",
|
|
380
|
+
expiresAt: now + BOT_PROFILE_CACHE_TTL_MS,
|
|
381
|
+
};
|
|
382
|
+
if (configuredName) {
|
|
383
|
+
cachedBotIdentities.set(cacheKey, configuredIdentity);
|
|
384
|
+
return configuredIdentity;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
pruneBotProfileCache(now);
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
const client = buildClient(params.config);
|
|
391
|
+
const profile = await client.user.profile();
|
|
392
|
+
const identity: CachedBotIdentity = {
|
|
393
|
+
name: normalizePresentationName(String(profile.full_name ?? "")) || params.accountId,
|
|
394
|
+
userId: normalizePresentationName(String(profile.user_id ?? "")) || params.config.appId?.trim() || params.accountId,
|
|
395
|
+
identityNumber: normalizePresentationName(String(profile.identity_number ?? "")),
|
|
396
|
+
expiresAt: now + BOT_PROFILE_CACHE_TTL_MS,
|
|
397
|
+
};
|
|
398
|
+
cachedBotIdentities.set(cacheKey, identity);
|
|
399
|
+
return identity;
|
|
400
|
+
} catch (err) {
|
|
401
|
+
params.log.warn(
|
|
402
|
+
`[mixin] failed to resolve bot profile: accountId=${params.accountId}, error=${err instanceof Error ? err.message : String(err)}`,
|
|
403
|
+
);
|
|
404
|
+
cachedBotIdentities.set(cacheKey, configuredIdentity);
|
|
405
|
+
return configuredIdentity;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
384
408
|
|
|
385
409
|
async function updateSessionPresentation(params: {
|
|
386
410
|
storePath: string;
|
|
@@ -465,7 +489,7 @@ function parseInboundAttachmentRequest(category: string, data: string): MixinAtt
|
|
|
465
489
|
}
|
|
466
490
|
}
|
|
467
491
|
|
|
468
|
-
function formatInboundAttachmentText(category: string, payload: MixinAttachmentRequest): string {
|
|
492
|
+
function formatInboundAttachmentText(category: string, payload: MixinAttachmentRequest): string {
|
|
469
493
|
if (category === "PLAIN_AUDIO") {
|
|
470
494
|
const details = [
|
|
471
495
|
payload.fileName,
|
|
@@ -481,10 +505,48 @@ function formatInboundAttachmentText(category: string, payload: MixinAttachmentR
|
|
|
481
505
|
payload.mimeType,
|
|
482
506
|
typeof payload.size === "number" ? `${payload.size} bytes` : undefined,
|
|
483
507
|
].filter(Boolean);
|
|
484
|
-
return details.length > 0 ? `[Mixin file] ${details.join(" | ")}` : "[Mixin file]";
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
|
|
508
|
+
return details.length > 0 ? `[Mixin file] ${details.join(" | ")}` : "[Mixin file]";
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function tryDecodeFallbackText(data: string): string | null {
|
|
512
|
+
if (!data) {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
try {
|
|
517
|
+
const decoded = Buffer.from(data, "base64").toString("utf-8").replace(/^\uFEFF/, "").trim();
|
|
518
|
+
if (!decoded) {
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
const compact = decoded.replace(/\s+/g, "");
|
|
522
|
+
if (!compact) {
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
const printableCount = Array.from(compact).filter((char) => /[\p{L}\p{N}\p{P}\p{S}\u4e00-\u9fff]/u.test(char)).length;
|
|
526
|
+
if (printableCount / compact.length < 0.6) {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
return decoded;
|
|
530
|
+
} catch {
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function buildQuotedMessageContextNote(params: {
|
|
536
|
+
quoteMessageId: string;
|
|
537
|
+
found: boolean;
|
|
538
|
+
}): string[] {
|
|
539
|
+
if (params.found) {
|
|
540
|
+
return [];
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return [
|
|
544
|
+
`Quoted message id: ${params.quoteMessageId}`,
|
|
545
|
+
"Quoted message body was not available in cache.",
|
|
546
|
+
];
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function resolveInboundAttachment(params: {
|
|
488
550
|
rt: ReturnType<typeof getMixinRuntime>;
|
|
489
551
|
config: MixinAccountConfig;
|
|
490
552
|
msg: MixinInboundMessage;
|
|
@@ -533,13 +595,30 @@ async function resolveInboundAttachment(params: {
|
|
|
533
595
|
}
|
|
534
596
|
}
|
|
535
597
|
|
|
536
|
-
function
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
}
|
|
598
|
+
function hasBotMention(text: string, botName?: string): boolean {
|
|
599
|
+
const normalizedBotName = normalizePresentationName(botName ?? "").replace(/^@+/, "");
|
|
600
|
+
if (!normalizedBotName) {
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const mentionPattern = new RegExp(`@\\s*${escapeRegExp(normalizedBotName)}(?=$|[\\s::,,.!?。;;、])`, "i");
|
|
605
|
+
return mentionPattern.test(text);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function shouldPassGroupFilter(
|
|
609
|
+
config: MixinAccountConfig,
|
|
610
|
+
text: string,
|
|
611
|
+
botAliases: string[] = [],
|
|
612
|
+
): boolean {
|
|
613
|
+
if (!config.requireMentionInGroup) {
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
if (botAliases.some((alias) => hasBotMention(text, alias))) {
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
if (text.trim().startsWith("/")) {
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
543
622
|
const lower = text.toLowerCase();
|
|
544
623
|
return lower.includes("?") || /帮我|请|分析|总结|help/i.test(lower);
|
|
545
624
|
}
|
|
@@ -865,43 +944,103 @@ export async function handleMixinMessage(params: {
|
|
|
865
944
|
}
|
|
866
945
|
}
|
|
867
946
|
|
|
868
|
-
|
|
869
|
-
const isAttachmentMessage = msg.category === "PLAIN_DATA" || msg.category === "PLAIN_AUDIO";
|
|
870
|
-
|
|
871
|
-
if (!isTextMessage && !isAttachmentMessage) {
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
if (
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
}
|
|
947
|
+
let isTextMessage = msg.category.startsWith("PLAIN_TEXT") || msg.category.startsWith("PLAIN_POST");
|
|
948
|
+
const isAttachmentMessage = msg.category === "PLAIN_DATA" || msg.category === "PLAIN_AUDIO";
|
|
949
|
+
|
|
950
|
+
if (!isTextMessage && !isAttachmentMessage) {
|
|
951
|
+
const fallbackText = tryDecodeFallbackText(msg.data);
|
|
952
|
+
if (fallbackText) {
|
|
953
|
+
log.warn(
|
|
954
|
+
`[mixin] treating unexpected category as text: messageId=${msg.messageId}, category=${msg.category}, fallbackLength=${fallbackText.length}`,
|
|
955
|
+
);
|
|
956
|
+
msg.category = "PLAIN_TEXT";
|
|
957
|
+
msg.data = Buffer.from(fallbackText).toString("base64");
|
|
958
|
+
isTextMessage = true;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (!isTextMessage && !isAttachmentMessage) {
|
|
963
|
+
log.info(
|
|
964
|
+
`[mixin] skip non-text message: messageId=${msg.messageId}, category=${msg.category}, quoteMessageId=${msg.quoteMessageId ?? "none"}`,
|
|
965
|
+
);
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const decodedBody = decodeContent(msg.category, msg.data);
|
|
970
|
+
let text = decodedBody.trim();
|
|
971
|
+
let mediaPayload: AgentMediaPayload | undefined;
|
|
972
|
+
if (isAttachmentMessage) {
|
|
973
|
+
const resolved = await resolveInboundAttachment({ rt, config, msg, log });
|
|
974
|
+
text = resolved.text.trim();
|
|
975
|
+
mediaPayload = resolved.mediaPayload;
|
|
976
|
+
}
|
|
977
|
+
log.info(`[mixin] decoded text: messageId=${msg.messageId}, category=${msg.category}, length=${text.length}`);
|
|
978
|
+
|
|
979
|
+
const botIdentity = await resolveBotIdentity({
|
|
980
|
+
accountId,
|
|
981
|
+
config,
|
|
982
|
+
log,
|
|
983
|
+
});
|
|
984
|
+
const replyContext = resolveMixinReplyContext({
|
|
985
|
+
accountId,
|
|
986
|
+
conversationId: msg.conversationId,
|
|
987
|
+
quoteMessageId: msg.quoteMessageId,
|
|
988
|
+
});
|
|
989
|
+
rememberMixinMessage({
|
|
990
|
+
accountId,
|
|
991
|
+
conversationId: msg.conversationId,
|
|
992
|
+
messageId: msg.messageId,
|
|
993
|
+
senderId: msg.userId,
|
|
994
|
+
body: text,
|
|
995
|
+
timestamp: msg.createdAt,
|
|
996
|
+
direction: "inbound",
|
|
997
|
+
quoteMessageId: msg.quoteMessageId,
|
|
998
|
+
});
|
|
999
|
+
if (replyContext?.found) {
|
|
1000
|
+
log.info(
|
|
1001
|
+
`[mixin] reply context resolved: messageId=${msg.messageId}, quoteMessageId=${replyContext.id}, sender=${replyContext.sender ?? "unknown"}`,
|
|
1002
|
+
);
|
|
1003
|
+
} else if (replyContext?.id) {
|
|
1004
|
+
log.info(
|
|
1005
|
+
`[mixin] reply context missing from cache: messageId=${msg.messageId}, quoteMessageId=${replyContext.id}`,
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
if (!text) {
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const conversationPolicy = isDirect
|
|
1014
|
+
? null
|
|
1015
|
+
: resolveConversationPolicy(cfg, accountId, msg.conversationId);
|
|
1016
|
+
|
|
1017
|
+
const botAliases = [
|
|
1018
|
+
botIdentity.identityNumber,
|
|
1019
|
+
botIdentity.userId,
|
|
1020
|
+
].filter((value): value is string => Boolean(value && value.trim()));
|
|
1021
|
+
const groupMentioned = !isDirect && botAliases.some((alias) => hasBotMention(text, alias));
|
|
1022
|
+
if (!isDirect) {
|
|
1023
|
+
log.info(
|
|
1024
|
+
`[mixin] group trigger check: messageId=${msg.messageId}, botName=${botIdentity.name}, botUserId=${botIdentity.userId}, botIdentityNumber=${botIdentity.identityNumber || "none"}, replyContext=${replyContext?.id ?? "none"}, mentioned=${groupMentioned}`,
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
if (
|
|
1029
|
+
!isDirect &&
|
|
1030
|
+
conversationPolicy &&
|
|
1031
|
+
!(isAttachmentMessage && conversationPolicy.mediaBypassMention) &&
|
|
1032
|
+
!shouldPassGroupFilter(
|
|
1033
|
+
{
|
|
1034
|
+
...config,
|
|
1035
|
+
requireMentionInGroup: conversationPolicy.requireMention,
|
|
1036
|
+
},
|
|
1037
|
+
text,
|
|
1038
|
+
botAliases,
|
|
1039
|
+
)
|
|
1040
|
+
) {
|
|
1041
|
+
log.info(`[mixin] group message filtered: ${msg.messageId}`);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
905
1044
|
|
|
906
1045
|
const effectiveAllowFrom = await readEffectiveAllowFrom(rt, accountId, config.allowFrom, log);
|
|
907
1046
|
const normalizedUserId = normalizeAllowEntry(msg.userId);
|
|
@@ -1115,33 +1254,28 @@ export async function handleMixinMessage(params: {
|
|
|
1115
1254
|
})
|
|
1116
1255
|
: undefined;
|
|
1117
1256
|
|
|
1118
|
-
const senderName = await resolveSenderName({
|
|
1119
|
-
accountId,
|
|
1120
|
-
config,
|
|
1121
|
-
userId: msg.userId,
|
|
1122
|
-
log,
|
|
1123
|
-
});
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
log,
|
|
1128
|
-
});
|
|
1129
|
-
const groupName = isDirect
|
|
1130
|
-
? ""
|
|
1131
|
-
: await resolveGroupName({
|
|
1257
|
+
const senderName = await resolveSenderName({
|
|
1258
|
+
accountId,
|
|
1259
|
+
config,
|
|
1260
|
+
userId: msg.userId,
|
|
1261
|
+
log,
|
|
1262
|
+
});
|
|
1263
|
+
const groupName = isDirect
|
|
1264
|
+
? ""
|
|
1265
|
+
: await resolveGroupName({
|
|
1132
1266
|
accountId,
|
|
1133
1267
|
config,
|
|
1134
1268
|
conversationId: msg.conversationId,
|
|
1135
1269
|
log,
|
|
1136
1270
|
});
|
|
1137
|
-
const conversationLabel = isDirect
|
|
1138
|
-
? clampSessionLabel(`${
|
|
1139
|
-
: clampSessionLabel(`${
|
|
1140
|
-
|
|
1141
|
-
const ctx = rt.channel.reply.finalizeInboundContext({
|
|
1142
|
-
Body: text,
|
|
1143
|
-
RawBody: text,
|
|
1144
|
-
CommandBody: text,
|
|
1271
|
+
const conversationLabel = isDirect
|
|
1272
|
+
? clampSessionLabel(`${botIdentity.name}-${senderName || msg.userId}`)
|
|
1273
|
+
: clampSessionLabel(`${botIdentity.name}-${groupName || msg.conversationId}`);
|
|
1274
|
+
|
|
1275
|
+
const ctx = rt.channel.reply.finalizeInboundContext({
|
|
1276
|
+
Body: text,
|
|
1277
|
+
RawBody: text,
|
|
1278
|
+
CommandBody: text,
|
|
1145
1279
|
From: isDirect ? msg.userId : msg.conversationId,
|
|
1146
1280
|
SenderId: msg.userId,
|
|
1147
1281
|
SenderName: senderName,
|
|
@@ -1150,14 +1284,22 @@ export async function handleMixinMessage(params: {
|
|
|
1150
1284
|
ChatType: isDirect ? "direct" : "group",
|
|
1151
1285
|
ConversationLabel: conversationLabel,
|
|
1152
1286
|
GroupSubject: isDirect ? undefined : groupName || msg.conversationId,
|
|
1153
|
-
Provider: "mixin",
|
|
1154
|
-
Surface: "mixin",
|
|
1155
|
-
MessageSid: msg.messageId,
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1287
|
+
Provider: "mixin",
|
|
1288
|
+
Surface: "mixin",
|
|
1289
|
+
MessageSid: msg.messageId,
|
|
1290
|
+
ReplyToId: replyContext?.id,
|
|
1291
|
+
ReplyToBody: replyContext?.body,
|
|
1292
|
+
ReplyToSender: replyContext?.sender,
|
|
1293
|
+
ReplyToIsQuote: replyContext ? true : undefined,
|
|
1294
|
+
UntrustedContext: replyContext?.id ? buildQuotedMessageContextNote({
|
|
1295
|
+
quoteMessageId: replyContext.id,
|
|
1296
|
+
found: replyContext.found,
|
|
1297
|
+
}) : undefined,
|
|
1298
|
+
CommandAuthorized: commandAuthorized,
|
|
1299
|
+
OriginatingChannel: "mixin",
|
|
1300
|
+
OriginatingTo: isDirect ? msg.userId : msg.conversationId,
|
|
1301
|
+
...mediaPayload,
|
|
1302
|
+
});
|
|
1161
1303
|
|
|
1162
1304
|
const storePath = rt.channel.session.resolveStorePath(cfg.session?.store, {
|
|
1163
1305
|
agentId: route.agentId,
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
type MixinMessageDirection = "inbound" | "outbound";
|
|
2
|
+
|
|
3
|
+
export type MixinMessageContext = {
|
|
4
|
+
accountId: string;
|
|
5
|
+
conversationId: string;
|
|
6
|
+
messageId: string;
|
|
7
|
+
senderId?: string;
|
|
8
|
+
senderName?: string;
|
|
9
|
+
body: string;
|
|
10
|
+
timestamp: string;
|
|
11
|
+
direction: MixinMessageDirection;
|
|
12
|
+
quoteMessageId?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type ResolvedMixinReplyContext = {
|
|
16
|
+
id: string;
|
|
17
|
+
body?: string;
|
|
18
|
+
sender?: string;
|
|
19
|
+
senderId?: string;
|
|
20
|
+
timestamp?: string;
|
|
21
|
+
direction?: MixinMessageDirection;
|
|
22
|
+
found: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const MAX_MESSAGE_CONTEXTS = 4000;
|
|
26
|
+
const recentMessages = new Map<string, MixinMessageContext>();
|
|
27
|
+
|
|
28
|
+
function normalizeKeyPart(value: string | null | undefined): string {
|
|
29
|
+
return value?.trim().toLowerCase() ?? "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildMessageContextKey(params: {
|
|
33
|
+
accountId: string;
|
|
34
|
+
conversationId: string;
|
|
35
|
+
messageId: string;
|
|
36
|
+
}): string {
|
|
37
|
+
return [
|
|
38
|
+
normalizeKeyPart(params.accountId),
|
|
39
|
+
normalizeKeyPart(params.conversationId),
|
|
40
|
+
normalizeKeyPart(params.messageId),
|
|
41
|
+
].join(":");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function pruneRecentMessages(): void {
|
|
45
|
+
while (recentMessages.size > MAX_MESSAGE_CONTEXTS) {
|
|
46
|
+
const first = recentMessages.keys().next().value;
|
|
47
|
+
if (!first) {
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
recentMessages.delete(first);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function rememberMixinMessage(context: MixinMessageContext): void {
|
|
55
|
+
const accountId = context.accountId.trim();
|
|
56
|
+
const conversationId = context.conversationId.trim();
|
|
57
|
+
const messageId = context.messageId.trim();
|
|
58
|
+
if (!accountId || !conversationId || !messageId) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
recentMessages.set(
|
|
63
|
+
buildMessageContextKey({ accountId, conversationId, messageId }),
|
|
64
|
+
{
|
|
65
|
+
accountId,
|
|
66
|
+
conversationId,
|
|
67
|
+
messageId,
|
|
68
|
+
senderId: context.senderId?.trim() || undefined,
|
|
69
|
+
senderName: context.senderName?.trim() || undefined,
|
|
70
|
+
body: context.body ?? "",
|
|
71
|
+
timestamp: context.timestamp,
|
|
72
|
+
direction: context.direction,
|
|
73
|
+
quoteMessageId: context.quoteMessageId?.trim() || undefined,
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
pruneRecentMessages();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function resolveMixinReplyContext(params: {
|
|
81
|
+
accountId: string;
|
|
82
|
+
conversationId: string;
|
|
83
|
+
quoteMessageId?: string | null;
|
|
84
|
+
}): ResolvedMixinReplyContext | null {
|
|
85
|
+
const quoteMessageId = params.quoteMessageId?.trim();
|
|
86
|
+
if (!quoteMessageId) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const message = recentMessages.get(
|
|
91
|
+
buildMessageContextKey({
|
|
92
|
+
accountId: params.accountId,
|
|
93
|
+
conversationId: params.conversationId,
|
|
94
|
+
messageId: quoteMessageId,
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
if (!message) {
|
|
99
|
+
return {
|
|
100
|
+
id: quoteMessageId,
|
|
101
|
+
found: false,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
id: message.messageId,
|
|
107
|
+
body: message.body || undefined,
|
|
108
|
+
sender: message.senderName || message.senderId,
|
|
109
|
+
senderId: message.senderId,
|
|
110
|
+
timestamp: message.timestamp,
|
|
111
|
+
direction: message.direction,
|
|
112
|
+
found: true,
|
|
113
|
+
};
|
|
114
|
+
}
|
package/src/send-service.ts
CHANGED
|
@@ -4,6 +4,7 @@ import os from "os";
|
|
|
4
4
|
import path from "path";
|
|
5
5
|
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
6
6
|
import { getAccountConfig } from "./config.js";
|
|
7
|
+
import { rememberMixinMessage } from "./message-context.js";
|
|
7
8
|
import { getMixinBlazeSender, getMixinRuntime } from "./runtime.js";
|
|
8
9
|
import { buildClient, sleep, type SendLog } from "./shared.js";
|
|
9
10
|
|
|
@@ -623,7 +624,7 @@ export async function sendTextMessage(
|
|
|
623
624
|
text: string,
|
|
624
625
|
log?: SendLog,
|
|
625
626
|
): Promise<SendResult> {
|
|
626
|
-
return sendMixinMessage(cfg, accountId, conversationId, recipientId, "PLAIN_TEXT", text, log);
|
|
627
|
+
return sendMixinMessage(cfg, accountId, conversationId, recipientId, "PLAIN_TEXT", text, log, text);
|
|
627
628
|
}
|
|
628
629
|
|
|
629
630
|
export async function sendPostMessage(
|
|
@@ -634,7 +635,7 @@ export async function sendPostMessage(
|
|
|
634
635
|
text: string,
|
|
635
636
|
log?: SendLog,
|
|
636
637
|
): Promise<SendResult> {
|
|
637
|
-
return sendMixinMessage(cfg, accountId, conversationId, recipientId, "PLAIN_POST", text, log);
|
|
638
|
+
return sendMixinMessage(cfg, accountId, conversationId, recipientId, "PLAIN_POST", text, log, text);
|
|
638
639
|
}
|
|
639
640
|
|
|
640
641
|
export async function sendFileMessage(
|
|
@@ -654,7 +655,16 @@ export async function sendFileMessage(
|
|
|
654
655
|
mimeType,
|
|
655
656
|
} satisfies FileOutboxBody);
|
|
656
657
|
|
|
657
|
-
return sendMixinMessage(
|
|
658
|
+
return sendMixinMessage(
|
|
659
|
+
cfg,
|
|
660
|
+
accountId,
|
|
661
|
+
conversationId,
|
|
662
|
+
recipientId,
|
|
663
|
+
"PLAIN_DATA",
|
|
664
|
+
body,
|
|
665
|
+
log,
|
|
666
|
+
`${fileName} (${mimeType})`,
|
|
667
|
+
);
|
|
658
668
|
}
|
|
659
669
|
|
|
660
670
|
export async function sendAudioMessage(
|
|
@@ -674,7 +684,16 @@ export async function sendAudioMessage(
|
|
|
674
684
|
waveForm: audio.waveForm,
|
|
675
685
|
} satisfies AudioOutboxBody);
|
|
676
686
|
|
|
677
|
-
return sendMixinMessage(
|
|
687
|
+
return sendMixinMessage(
|
|
688
|
+
cfg,
|
|
689
|
+
accountId,
|
|
690
|
+
conversationId,
|
|
691
|
+
recipientId,
|
|
692
|
+
"PLAIN_AUDIO",
|
|
693
|
+
body,
|
|
694
|
+
log,
|
|
695
|
+
`${path.basename(audio.filePath)} (${mimeType}${audio.duration ? `, ${audio.duration}s` : ""})`,
|
|
696
|
+
);
|
|
678
697
|
}
|
|
679
698
|
|
|
680
699
|
export async function sendButtonGroupMessage(
|
|
@@ -718,6 +737,7 @@ async function sendMixinMessage(
|
|
|
718
737
|
category: MixinSupportedMessageCategory,
|
|
719
738
|
body: string,
|
|
720
739
|
log?: SendLog,
|
|
740
|
+
contextBody?: string,
|
|
721
741
|
): Promise<SendResult> {
|
|
722
742
|
updateRuntime(cfg, log);
|
|
723
743
|
await startSendWorker(cfg, log);
|
|
@@ -742,6 +762,17 @@ async function sendMixinMessage(
|
|
|
742
762
|
await persistEntries();
|
|
743
763
|
wakeWorker();
|
|
744
764
|
|
|
765
|
+
rememberMixinMessage({
|
|
766
|
+
accountId,
|
|
767
|
+
conversationId,
|
|
768
|
+
messageId: entry.messageId,
|
|
769
|
+
senderId: accountId,
|
|
770
|
+
senderName: "Mixin bot",
|
|
771
|
+
body: contextBody ?? body,
|
|
772
|
+
timestamp: now,
|
|
773
|
+
direction: "outbound",
|
|
774
|
+
});
|
|
775
|
+
|
|
745
776
|
state.log.info(
|
|
746
777
|
`[mixin] outbox enqueued: jobId=${entry.jobId}, messageId=${entry.messageId}, category=${category}, accountId=${accountId}, conversation=${conversationId}`,
|
|
747
778
|
);
|