@invago/mixin 1.0.15 → 1.0.17
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/index.ts +5 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/channel.ts +24 -18
- package/src/inbound-handler.ts +208 -117
package/index.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { OpenClawPluginApi, OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
|
-
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
3
2
|
import { mixinPlugin } from "./src/channel.js";
|
|
4
3
|
import {
|
|
5
4
|
buildMixinAccountsText,
|
|
@@ -33,7 +32,11 @@ const plugin = {
|
|
|
33
32
|
id: "mixin",
|
|
34
33
|
name: "Mixin Messenger Channel",
|
|
35
34
|
description: "Mixin Messenger channel via Blaze WebSocket",
|
|
36
|
-
configSchema:
|
|
35
|
+
configSchema: {
|
|
36
|
+
type: "object",
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
properties: {},
|
|
39
|
+
},
|
|
37
40
|
register(api: OpenClawPluginApi): void {
|
|
38
41
|
setMixinRuntime(api.runtime);
|
|
39
42
|
api.registerChannel({ plugin: mixinPlugin });
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -3,8 +3,6 @@ import { promisify } from "node:util";
|
|
|
3
3
|
import { uniqueConversationID } from "@mixin.dev/mixin-node-sdk";
|
|
4
4
|
import {
|
|
5
5
|
buildChannelConfigSchema,
|
|
6
|
-
formatPairingApproveHint,
|
|
7
|
-
resolveChannelMediaMaxBytes,
|
|
8
6
|
} from "openclaw/plugin-sdk";
|
|
9
7
|
import type { ChannelGatewayContext, OpenClawConfig, ReplyPayload } from "openclaw/plugin-sdk";
|
|
10
8
|
import { runBlazeLoop } from "./blaze-service.js";
|
|
@@ -23,9 +21,10 @@ import { buildMixinAccountSnapshot, buildMixinChannelSummary, resolveMixinStatus
|
|
|
23
21
|
type ResolvedMixinAccount = ReturnType<typeof resolveAccount>;
|
|
24
22
|
|
|
25
23
|
const BASE_DELAY = 1000;
|
|
26
|
-
const MAX_DELAY = 3000;
|
|
27
|
-
const MULTIPLIER = 1.5;
|
|
28
|
-
const MEDIA_MAX_BYTES = 30 * 1024 * 1024;
|
|
24
|
+
const MAX_DELAY = 3000;
|
|
25
|
+
const MULTIPLIER = 1.5;
|
|
26
|
+
const MEDIA_MAX_BYTES = 30 * 1024 * 1024;
|
|
27
|
+
const MB = 1024 * 1024;
|
|
29
28
|
const execFileAsync = promisify(execFile);
|
|
30
29
|
const CONVERSATION_CATEGORY_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
31
30
|
|
|
@@ -44,6 +43,10 @@ function createDefaultMixinRuntimeState(accountId: string): {
|
|
|
44
43
|
lastError: null,
|
|
45
44
|
};
|
|
46
45
|
}
|
|
46
|
+
|
|
47
|
+
function formatMixinPairingApproveHint(channelId: string): string {
|
|
48
|
+
return `Approve via: \`openclaw pairing list ${channelId}\` / \`openclaw pairing approve ${channelId} <code>\``;
|
|
49
|
+
}
|
|
47
50
|
|
|
48
51
|
const conversationCategoryCache = new Map<string, {
|
|
49
52
|
category: "CONTACT" | "GROUP";
|
|
@@ -172,13 +175,16 @@ async function resolveAudioDurationSeconds(filePath: string): Promise<number | n
|
|
|
172
175
|
}
|
|
173
176
|
}
|
|
174
177
|
|
|
175
|
-
function resolveMixinMediaMaxBytes(cfg: OpenClawConfig, accountId?: string | null): number {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
178
|
+
function resolveMixinMediaMaxBytes(cfg: OpenClawConfig, accountId?: string | null): number {
|
|
179
|
+
const channelLimitMb = resolveMediaMaxMb(cfg, accountId ?? undefined);
|
|
180
|
+
if (channelLimitMb) {
|
|
181
|
+
return channelLimitMb * MB;
|
|
182
|
+
}
|
|
183
|
+
if (cfg.agents?.defaults?.mediaMaxMb) {
|
|
184
|
+
return cfg.agents.defaults.mediaMaxMb * MB;
|
|
185
|
+
}
|
|
186
|
+
return MEDIA_MAX_BYTES;
|
|
187
|
+
}
|
|
182
188
|
|
|
183
189
|
async function deliverOutboundMixinPayload(params: {
|
|
184
190
|
cfg: OpenClawConfig;
|
|
@@ -344,12 +350,12 @@ export const mixinPlugin = {
|
|
|
344
350
|
policy,
|
|
345
351
|
allowFrom,
|
|
346
352
|
policyPath: `channels.mixin${basePath}.dmPolicy`,
|
|
347
|
-
allowFromPath: `channels.mixin${basePath}.allowFrom`,
|
|
348
|
-
approveHint: policy === "pairing"
|
|
349
|
-
?
|
|
350
|
-
: allowFrom.length > 0
|
|
351
|
-
? `宸查厤缃櫧鍚嶅崟鐢ㄦ埛鏁?${allowFrom.length}锛屽皢鐢ㄦ埛鐨?Mixin UUID 娣诲姞鍒?allowFrom 鍒楄〃鍗冲彲鎺堟潈`
|
|
352
|
-
: "灏嗙敤鎴风殑 Mixin UUID 娣诲姞鍒?allowFrom 鍒楄〃鍗冲彲鎺堟潈",
|
|
353
|
+
allowFromPath: `channels.mixin${basePath}.allowFrom`,
|
|
354
|
+
approveHint: policy === "pairing"
|
|
355
|
+
? formatMixinPairingApproveHint("mixin")
|
|
356
|
+
: allowFrom.length > 0
|
|
357
|
+
? `宸查厤缃櫧鍚嶅崟鐢ㄦ埛鏁?${allowFrom.length}锛屽皢鐢ㄦ埛鐨?Mixin UUID 娣诲姞鍒?allowFrom 鍒楄〃鍗冲彲鎺堟潈`
|
|
358
|
+
: "灏嗙敤鎴风殑 Mixin UUID 娣诲姞鍒?allowFrom 鍒楄〃鍗冲彲鎺堟潈",
|
|
353
359
|
};
|
|
354
360
|
},
|
|
355
361
|
},
|
package/src/inbound-handler.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { createRequire } from "node:module";
|
|
5
|
-
import { pathToFileURL } from "node:url";
|
|
6
|
-
import {
|
|
7
|
-
import type { AgentMediaPayload, OpenClawConfig } from "openclaw/plugin-sdk";
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import type { AgentMediaPayload, OpenClawConfig } from "openclaw/plugin-sdk";
|
|
8
7
|
import { getAccountConfig, resolveConversationPolicy } from "./config.js";
|
|
9
8
|
import type { MixinAccountConfig } from "./config-schema.js";
|
|
10
9
|
import { decryptMixinMessage } from "./crypto.js";
|
|
@@ -30,8 +29,9 @@ export interface MixinInboundMessage {
|
|
|
30
29
|
publicKey?: string;
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
const processedMessages = new Set<string>();
|
|
34
|
-
const
|
|
32
|
+
const processedMessages = new Set<string>();
|
|
33
|
+
const processingMessages = new Set<string>();
|
|
34
|
+
const MAX_DEDUP_SIZE = 2000;
|
|
35
35
|
const unauthNotifiedUsers = new Map<string, number>();
|
|
36
36
|
const unauthNotifiedGroups = new Map<string, number>();
|
|
37
37
|
const loggedAllowFromAccounts = new Set<string>();
|
|
@@ -69,6 +69,13 @@ type CachedBotIdentity = {
|
|
|
69
69
|
expiresAt: number;
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
+
type GroupAccessDecision = {
|
|
73
|
+
allowed: boolean;
|
|
74
|
+
groupPolicy: "open" | "allowlist" | "disabled";
|
|
75
|
+
providerMissingFallbackApplied: boolean;
|
|
76
|
+
reason: "allowed" | "disabled" | "empty_allowlist" | "sender_not_allowlisted";
|
|
77
|
+
};
|
|
78
|
+
|
|
72
79
|
type MixinAttachmentRequest = {
|
|
73
80
|
attachmentId: string;
|
|
74
81
|
mimeType?: string;
|
|
@@ -84,21 +91,99 @@ const cachedBotIdentities = new Map<string, CachedBotIdentity>();
|
|
|
84
91
|
let cachedUpdateSessionStore:
|
|
85
92
|
| ((storePath: string, mutator: (store: Record<string, Record<string, unknown>>) => void | Promise<void>) => Promise<unknown>)
|
|
86
93
|
| null
|
|
87
|
-
| undefined;
|
|
88
|
-
|
|
89
|
-
function
|
|
90
|
-
|
|
91
|
-
|
|
94
|
+
| undefined;
|
|
95
|
+
|
|
96
|
+
function buildMixinAgentMediaPayload(mediaList: Array<{ path: string; contentType?: string }>): AgentMediaPayload {
|
|
97
|
+
const first = mediaList[0];
|
|
98
|
+
const mediaPaths = mediaList.map((media) => media.path);
|
|
99
|
+
const mediaTypes = mediaList.map((media) => media.contentType).filter((value): value is string => Boolean(value));
|
|
100
|
+
return {
|
|
101
|
+
MediaPath: first?.path,
|
|
102
|
+
MediaType: first?.contentType ?? undefined,
|
|
103
|
+
MediaUrl: first?.path,
|
|
104
|
+
MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
|
|
105
|
+
MediaUrls: mediaPaths.length > 0 ? mediaPaths : undefined,
|
|
106
|
+
MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function resolveMixinDefaultGroupPolicy(cfg: OpenClawConfig): "open" | "allowlist" | "disabled" | undefined {
|
|
111
|
+
const groupPolicy = cfg.channels?.defaults?.groupPolicy;
|
|
112
|
+
return groupPolicy === "open" || groupPolicy === "allowlist" || groupPolicy === "disabled"
|
|
113
|
+
? groupPolicy
|
|
114
|
+
: undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function evaluateMixinSenderGroupAccess(params: {
|
|
118
|
+
providerConfigPresent: boolean;
|
|
119
|
+
configuredGroupPolicy?: "open" | "allowlist" | "disabled";
|
|
120
|
+
defaultGroupPolicy?: "open" | "allowlist" | "disabled";
|
|
121
|
+
groupAllowFrom: string[];
|
|
122
|
+
senderId: string;
|
|
123
|
+
isSenderAllowed: (senderId: string, allowFrom: string[]) => boolean;
|
|
124
|
+
}): GroupAccessDecision {
|
|
125
|
+
const configuredPolicy = params.configuredGroupPolicy ?? params.defaultGroupPolicy;
|
|
126
|
+
const providerMissingFallbackApplied = !params.providerConfigPresent && !configuredPolicy;
|
|
127
|
+
const groupPolicy = configuredPolicy ?? (params.providerConfigPresent ? "open" : "allowlist");
|
|
128
|
+
|
|
129
|
+
if (groupPolicy === "disabled") {
|
|
130
|
+
return {
|
|
131
|
+
allowed: false,
|
|
132
|
+
groupPolicy,
|
|
133
|
+
providerMissingFallbackApplied,
|
|
134
|
+
reason: "disabled",
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (groupPolicy === "allowlist") {
|
|
139
|
+
if (params.groupAllowFrom.length === 0) {
|
|
140
|
+
return {
|
|
141
|
+
allowed: false,
|
|
142
|
+
groupPolicy,
|
|
143
|
+
providerMissingFallbackApplied,
|
|
144
|
+
reason: "empty_allowlist",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (!params.isSenderAllowed(params.senderId, params.groupAllowFrom)) {
|
|
148
|
+
return {
|
|
149
|
+
allowed: false,
|
|
150
|
+
groupPolicy,
|
|
151
|
+
providerMissingFallbackApplied,
|
|
152
|
+
reason: "sender_not_allowlisted",
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
allowed: true,
|
|
159
|
+
groupPolicy,
|
|
160
|
+
providerMissingFallbackApplied,
|
|
161
|
+
reason: "allowed",
|
|
162
|
+
};
|
|
163
|
+
}
|
|
92
164
|
|
|
93
|
-
function
|
|
94
|
-
if (processedMessages.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
165
|
+
function markProcessing(messageId: string): boolean {
|
|
166
|
+
if (processedMessages.has(messageId) || processingMessages.has(messageId)) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
processingMessages.add(messageId);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function markProcessed(messageId: string): void {
|
|
174
|
+
processingMessages.delete(messageId);
|
|
175
|
+
if (processedMessages.size >= MAX_DEDUP_SIZE) {
|
|
176
|
+
const first = processedMessages.values().next().value;
|
|
177
|
+
if (first) {
|
|
178
|
+
processedMessages.delete(first);
|
|
98
179
|
}
|
|
99
180
|
}
|
|
100
|
-
processedMessages.add(messageId);
|
|
101
|
-
}
|
|
181
|
+
processedMessages.add(messageId);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function clearProcessing(messageId: string): void {
|
|
185
|
+
processingMessages.delete(messageId);
|
|
186
|
+
}
|
|
102
187
|
|
|
103
188
|
function pruneUnauthNotifiedUsers(now: number): void {
|
|
104
189
|
for (const [userId, lastNotified] of unauthNotifiedUsers) {
|
|
@@ -577,11 +662,11 @@ async function resolveInboundAttachment(params: {
|
|
|
577
662
|
);
|
|
578
663
|
|
|
579
664
|
return {
|
|
580
|
-
text: formatInboundAttachmentText(params.msg.category, payload),
|
|
581
|
-
mediaPayload:
|
|
582
|
-
{
|
|
583
|
-
path: saved.path,
|
|
584
|
-
contentType: saved.contentType ?? payload.mimeType ?? fetched.contentType,
|
|
665
|
+
text: formatInboundAttachmentText(params.msg.category, payload),
|
|
666
|
+
mediaPayload: buildMixinAgentMediaPayload([
|
|
667
|
+
{
|
|
668
|
+
path: saved.path,
|
|
669
|
+
contentType: saved.contentType ?? payload.mimeType ?? fetched.contentType,
|
|
585
670
|
},
|
|
586
671
|
]),
|
|
587
672
|
};
|
|
@@ -887,14 +972,14 @@ function evaluateMixinGroupAccess(params: {
|
|
|
887
972
|
};
|
|
888
973
|
}
|
|
889
974
|
|
|
890
|
-
const normalizedGroupAllowFrom = normalizeAllowEntries(conversationPolicy.groupAllowFrom);
|
|
891
|
-
const decision =
|
|
892
|
-
providerConfigPresent: true,
|
|
893
|
-
configuredGroupPolicy: conversationPolicy.groupPolicy,
|
|
894
|
-
defaultGroupPolicy:
|
|
895
|
-
groupAllowFrom: normalizedGroupAllowFrom,
|
|
896
|
-
senderId: normalizeAllowEntry(params.senderId),
|
|
897
|
-
isSenderAllowed: (senderId, allowFrom) => allowFrom.includes(normalizeAllowEntry(senderId)),
|
|
975
|
+
const normalizedGroupAllowFrom = normalizeAllowEntries(conversationPolicy.groupAllowFrom);
|
|
976
|
+
const decision = evaluateMixinSenderGroupAccess({
|
|
977
|
+
providerConfigPresent: true,
|
|
978
|
+
configuredGroupPolicy: conversationPolicy.groupPolicy,
|
|
979
|
+
defaultGroupPolicy: resolveMixinDefaultGroupPolicy(params.cfg),
|
|
980
|
+
groupAllowFrom: normalizedGroupAllowFrom,
|
|
981
|
+
senderId: normalizeAllowEntry(params.senderId),
|
|
982
|
+
isSenderAllowed: (senderId, allowFrom) => allowFrom.includes(normalizeAllowEntry(senderId)),
|
|
898
983
|
});
|
|
899
984
|
|
|
900
985
|
return {
|
|
@@ -905,76 +990,77 @@ function evaluateMixinGroupAccess(params: {
|
|
|
905
990
|
};
|
|
906
991
|
}
|
|
907
992
|
|
|
908
|
-
export async function handleMixinMessage(params: {
|
|
993
|
+
export async function handleMixinMessage(params: {
|
|
909
994
|
cfg: OpenClawConfig;
|
|
910
995
|
accountId: string;
|
|
911
996
|
msg: MixinInboundMessage;
|
|
912
997
|
isDirect: boolean;
|
|
913
998
|
log: { info: (m: string) => void; warn: (m: string) => void; error: (m: string, e?: unknown) => void };
|
|
914
999
|
}): Promise<void> {
|
|
915
|
-
const { cfg, accountId, msg, isDirect, log } = params;
|
|
916
|
-
const rt = getMixinRuntime();
|
|
917
|
-
|
|
918
|
-
if (isProcessed(msg.messageId)) {
|
|
919
|
-
return;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
const config = getAccountConfig(cfg, accountId);
|
|
923
|
-
|
|
924
|
-
if (msg.category === "ENCRYPTED_TEXT" || msg.category === "ENCRYPTED_POST") {
|
|
925
|
-
log.info(`[mixin] decrypting encrypted message ${msg.messageId}, category=${msg.category}`);
|
|
926
|
-
try {
|
|
927
|
-
const decrypted = decryptMixinMessage(
|
|
928
|
-
msg.data,
|
|
929
|
-
config.sessionPrivateKey!,
|
|
930
|
-
config.sessionId!,
|
|
931
|
-
);
|
|
932
|
-
if (!decrypted) {
|
|
933
|
-
log.error(`[mixin] decryption failed for ${msg.messageId}`);
|
|
934
|
-
markProcessed(msg.messageId);
|
|
935
|
-
return;
|
|
936
|
-
}
|
|
937
|
-
log.info(`[mixin] decryption successful: messageId=${msg.messageId}, length=${decrypted.length}`);
|
|
938
|
-
msg.data = Buffer.from(decrypted).toString("base64");
|
|
939
|
-
msg.category = "PLAIN_TEXT";
|
|
940
|
-
} catch (err) {
|
|
941
|
-
log.error(`[mixin] decryption exception for ${msg.messageId}`, err);
|
|
942
|
-
markProcessed(msg.messageId);
|
|
943
|
-
return;
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
let isTextMessage = msg.category.startsWith("PLAIN_TEXT") || msg.category.startsWith("PLAIN_POST");
|
|
948
|
-
const isAttachmentMessage = msg.category === "PLAIN_DATA" || msg.category === "PLAIN_AUDIO";
|
|
1000
|
+
const { cfg, accountId, msg, isDirect, log } = params;
|
|
1001
|
+
const rt = getMixinRuntime();
|
|
949
1002
|
|
|
950
|
-
if (!
|
|
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
|
-
);
|
|
1003
|
+
if (!markProcessing(msg.messageId)) {
|
|
966
1004
|
return;
|
|
967
1005
|
}
|
|
968
1006
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1007
|
+
try {
|
|
1008
|
+
const config = getAccountConfig(cfg, accountId);
|
|
1009
|
+
|
|
1010
|
+
if (msg.category === "ENCRYPTED_TEXT" || msg.category === "ENCRYPTED_POST") {
|
|
1011
|
+
log.info(`[mixin] decrypting encrypted message ${msg.messageId}, category=${msg.category}`);
|
|
1012
|
+
try {
|
|
1013
|
+
const decrypted = decryptMixinMessage(
|
|
1014
|
+
msg.data,
|
|
1015
|
+
config.sessionPrivateKey!,
|
|
1016
|
+
config.sessionId!,
|
|
1017
|
+
);
|
|
1018
|
+
if (!decrypted) {
|
|
1019
|
+
log.error(`[mixin] decryption failed for ${msg.messageId}`);
|
|
1020
|
+
markProcessed(msg.messageId);
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
log.info(`[mixin] decryption successful: messageId=${msg.messageId}, length=${decrypted.length}`);
|
|
1024
|
+
msg.data = Buffer.from(decrypted).toString("base64");
|
|
1025
|
+
msg.category = "PLAIN_TEXT";
|
|
1026
|
+
} catch (err) {
|
|
1027
|
+
log.error(`[mixin] decryption exception for ${msg.messageId}`, err);
|
|
1028
|
+
markProcessed(msg.messageId);
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
let isTextMessage = msg.category.startsWith("PLAIN_TEXT") || msg.category.startsWith("PLAIN_POST");
|
|
1034
|
+
const isAttachmentMessage = msg.category === "PLAIN_DATA" || msg.category === "PLAIN_AUDIO";
|
|
1035
|
+
|
|
1036
|
+
if (!isTextMessage && !isAttachmentMessage) {
|
|
1037
|
+
const fallbackText = tryDecodeFallbackText(msg.data);
|
|
1038
|
+
if (fallbackText) {
|
|
1039
|
+
log.warn(
|
|
1040
|
+
`[mixin] treating unexpected category as text: messageId=${msg.messageId}, category=${msg.category}, fallbackLength=${fallbackText.length}`,
|
|
1041
|
+
);
|
|
1042
|
+
msg.category = "PLAIN_TEXT";
|
|
1043
|
+
msg.data = Buffer.from(fallbackText).toString("base64");
|
|
1044
|
+
isTextMessage = true;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
if (!isTextMessage && !isAttachmentMessage) {
|
|
1049
|
+
log.info(
|
|
1050
|
+
`[mixin] skip non-text message: messageId=${msg.messageId}, category=${msg.category}, quoteMessageId=${msg.quoteMessageId ?? "none"}`,
|
|
1051
|
+
);
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
const decodedBody = decodeContent(msg.category, msg.data);
|
|
1056
|
+
let text = decodedBody.trim();
|
|
1057
|
+
let mediaPayload: AgentMediaPayload | undefined;
|
|
1058
|
+
if (isAttachmentMessage) {
|
|
1059
|
+
const resolved = await resolveInboundAttachment({ rt, config, msg, log });
|
|
1060
|
+
text = resolved.text.trim();
|
|
1061
|
+
mediaPayload = resolved.mediaPayload;
|
|
1062
|
+
}
|
|
1063
|
+
log.info(`[mixin] decoded text: messageId=${msg.messageId}, category=${msg.category}, length=${text.length}`);
|
|
978
1064
|
|
|
979
1065
|
const botIdentity = await resolveBotIdentity({
|
|
980
1066
|
accountId,
|
|
@@ -1322,26 +1408,31 @@ export async function handleMixinMessage(params: {
|
|
|
1322
1408
|
|
|
1323
1409
|
log.info(`[mixin] dispatching ${msg.messageId} from ${msg.userId}`);
|
|
1324
1410
|
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
}
|
|
1411
|
+
await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
1412
|
+
ctx,
|
|
1413
|
+
cfg,
|
|
1414
|
+
dispatcherOptions: {
|
|
1415
|
+
deliver: async (payload) => {
|
|
1416
|
+
const replyText = payload.text ?? "";
|
|
1417
|
+
if (!replyText) {
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
const recipientId = isDirect ? msg.userId : undefined;
|
|
1421
|
+
await deliverMixinReply({
|
|
1422
|
+
cfg,
|
|
1423
|
+
accountId,
|
|
1424
|
+
conversationId: msg.conversationId,
|
|
1425
|
+
recipientId,
|
|
1426
|
+
creatorId: msg.userId,
|
|
1427
|
+
text: replyText,
|
|
1428
|
+
log,
|
|
1429
|
+
});
|
|
1430
|
+
},
|
|
1431
|
+
},
|
|
1432
|
+
});
|
|
1433
|
+
} finally {
|
|
1434
|
+
if (!processedMessages.has(msg.messageId)) {
|
|
1435
|
+
clearProcessing(msg.messageId);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
}
|