@invago/mixin 1.0.7 → 1.0.8
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 +82 -3
- package/README.zh-CN.md +82 -3
- package/package.json +1 -1
- package/src/channel.ts +160 -8
- package/src/config-schema.ts +2 -0
- package/src/config.ts +1 -0
- package/src/inbound-handler.ts +403 -163
- package/src/reply-format.ts +55 -2
- package/src/send-service.ts +222 -14
package/src/inbound-handler.ts
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { MixinApi } from "@mixin.dev/mixin-node-sdk";
|
|
5
|
+
import { buildAgentMediaPayload } from "openclaw/plugin-sdk";
|
|
6
|
+
import type { AgentMediaPayload, OpenClawConfig } from "openclaw/plugin-sdk";
|
|
7
|
+
import { getAccountConfig } from "./config.js";
|
|
8
|
+
import type { MixinAccountConfig } from "./config-schema.js";
|
|
9
|
+
import { decryptMixinMessage } from "./crypto.js";
|
|
10
|
+
import { buildRequestConfig } from "./proxy.js";
|
|
11
|
+
import { buildMixinReplyPlan } from "./reply-format.js";
|
|
2
12
|
import { getMixinRuntime } from "./runtime.js";
|
|
3
13
|
import {
|
|
14
|
+
sendAudioMessage,
|
|
4
15
|
getOutboxStatus,
|
|
5
16
|
purgePermanentInvalidOutboxEntries,
|
|
17
|
+
sendFileMessage,
|
|
6
18
|
sendButtonGroupMessage,
|
|
7
19
|
sendCardMessage,
|
|
8
20
|
sendPostMessage,
|
|
9
21
|
sendTextMessage,
|
|
10
22
|
} from "./send-service.js";
|
|
11
|
-
import { getAccountConfig } from "./config.js";
|
|
12
|
-
import type { MixinAccountConfig } from "./config-schema.js";
|
|
13
|
-
|
|
14
|
-
import { decryptMixinMessage } from "./crypto.js";
|
|
15
|
-
import { buildMixinReplyPlan } from "./reply-format.js";
|
|
16
23
|
|
|
17
24
|
export interface MixinInboundMessage {
|
|
18
25
|
conversationId: string;
|
|
@@ -27,8 +34,18 @@ export interface MixinInboundMessage {
|
|
|
27
34
|
const processedMessages = new Set<string>();
|
|
28
35
|
const MAX_DEDUP_SIZE = 2000;
|
|
29
36
|
const unauthNotifiedUsers = new Map<string, number>();
|
|
37
|
+
const loggedAllowFromAccounts = new Set<string>();
|
|
30
38
|
const UNAUTH_NOTIFY_INTERVAL = 20 * 60 * 1000;
|
|
31
39
|
const MAX_UNAUTH_NOTIFY_USERS = 1000;
|
|
40
|
+
const INBOUND_MEDIA_MAX_BYTES = 30 * 1024 * 1024;
|
|
41
|
+
|
|
42
|
+
type MixinAttachmentRequest = {
|
|
43
|
+
attachmentId: string;
|
|
44
|
+
mimeType?: string;
|
|
45
|
+
size?: number;
|
|
46
|
+
fileName?: string;
|
|
47
|
+
duration?: number;
|
|
48
|
+
};
|
|
32
49
|
|
|
33
50
|
function isProcessed(messageId: string): boolean {
|
|
34
51
|
return processedMessages.has(messageId);
|
|
@@ -37,7 +54,9 @@ function isProcessed(messageId: string): boolean {
|
|
|
37
54
|
function markProcessed(messageId: string): void {
|
|
38
55
|
if (processedMessages.size >= MAX_DEDUP_SIZE) {
|
|
39
56
|
const first = processedMessages.values().next().value;
|
|
40
|
-
if (first)
|
|
57
|
+
if (first) {
|
|
58
|
+
processedMessages.delete(first);
|
|
59
|
+
}
|
|
41
60
|
}
|
|
42
61
|
processedMessages.add(messageId);
|
|
43
62
|
}
|
|
@@ -69,14 +88,122 @@ function decodeContent(category: string, data: string): string {
|
|
|
69
88
|
return `[${category}]`;
|
|
70
89
|
}
|
|
71
90
|
|
|
91
|
+
function buildClient(config: MixinAccountConfig) {
|
|
92
|
+
return MixinApi({
|
|
93
|
+
keystore: {
|
|
94
|
+
app_id: config.appId!,
|
|
95
|
+
session_id: config.sessionId!,
|
|
96
|
+
server_public_key: config.serverPublicKey!,
|
|
97
|
+
session_private_key: config.sessionPrivateKey!,
|
|
98
|
+
},
|
|
99
|
+
requestConfig: buildRequestConfig(config.proxy),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseInboundAttachmentRequest(category: string, data: string): MixinAttachmentRequest | null {
|
|
104
|
+
if (category !== "PLAIN_DATA" && category !== "PLAIN_AUDIO") {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const decoded = Buffer.from(data, "base64").toString("utf-8");
|
|
110
|
+
const parsed = JSON.parse(decoded) as {
|
|
111
|
+
attachment_id?: unknown;
|
|
112
|
+
mime_type?: unknown;
|
|
113
|
+
size?: unknown;
|
|
114
|
+
name?: unknown;
|
|
115
|
+
duration?: unknown;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (typeof parsed.attachment_id !== "string" || !parsed.attachment_id.trim()) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
attachmentId: parsed.attachment_id.trim(),
|
|
124
|
+
mimeType: typeof parsed.mime_type === "string" ? parsed.mime_type.trim() || undefined : undefined,
|
|
125
|
+
size: typeof parsed.size === "number" && Number.isFinite(parsed.size) ? parsed.size : undefined,
|
|
126
|
+
fileName: typeof parsed.name === "string" ? parsed.name.trim() || undefined : undefined,
|
|
127
|
+
duration: typeof parsed.duration === "number" && Number.isFinite(parsed.duration) ? parsed.duration : undefined,
|
|
128
|
+
};
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function formatInboundAttachmentText(category: string, payload: MixinAttachmentRequest): string {
|
|
135
|
+
if (category === "PLAIN_AUDIO") {
|
|
136
|
+
const details = [
|
|
137
|
+
payload.fileName,
|
|
138
|
+
payload.mimeType,
|
|
139
|
+
typeof payload.duration === "number" ? `${payload.duration}s` : undefined,
|
|
140
|
+
typeof payload.size === "number" ? `${payload.size} bytes` : undefined,
|
|
141
|
+
].filter(Boolean);
|
|
142
|
+
return details.length > 0 ? `[Mixin audio] ${details.join(" | ")}` : "[Mixin audio]";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const details = [
|
|
146
|
+
payload.fileName,
|
|
147
|
+
payload.mimeType,
|
|
148
|
+
typeof payload.size === "number" ? `${payload.size} bytes` : undefined,
|
|
149
|
+
].filter(Boolean);
|
|
150
|
+
return details.length > 0 ? `[Mixin file] ${details.join(" | ")}` : "[Mixin file]";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function resolveInboundAttachment(params: {
|
|
154
|
+
rt: ReturnType<typeof getMixinRuntime>;
|
|
155
|
+
config: MixinAccountConfig;
|
|
156
|
+
msg: MixinInboundMessage;
|
|
157
|
+
log: { info: (m: string) => void; warn: (m: string) => void; error: (m: string, e?: unknown) => void };
|
|
158
|
+
}): Promise<{ text: string; mediaPayload?: AgentMediaPayload }> {
|
|
159
|
+
const payload = parseInboundAttachmentRequest(params.msg.category, params.msg.data);
|
|
160
|
+
if (!payload) {
|
|
161
|
+
return {
|
|
162
|
+
text: `[${params.msg.category}]`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const client = buildClient(params.config);
|
|
168
|
+
const attachment = await client.attachment.fetch(payload.attachmentId);
|
|
169
|
+
const fetched = await params.rt.channel.media.fetchRemoteMedia({
|
|
170
|
+
url: attachment.view_url,
|
|
171
|
+
filePathHint: payload.fileName,
|
|
172
|
+
maxBytes: INBOUND_MEDIA_MAX_BYTES,
|
|
173
|
+
});
|
|
174
|
+
const saved = await params.rt.channel.media.saveMediaBuffer(
|
|
175
|
+
fetched.buffer,
|
|
176
|
+
payload.mimeType ?? fetched.contentType,
|
|
177
|
+
"mixin",
|
|
178
|
+
INBOUND_MEDIA_MAX_BYTES,
|
|
179
|
+
payload.fileName ?? fetched.fileName,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
text: formatInboundAttachmentText(params.msg.category, payload),
|
|
184
|
+
mediaPayload: buildAgentMediaPayload([
|
|
185
|
+
{
|
|
186
|
+
path: saved.path,
|
|
187
|
+
contentType: saved.contentType ?? payload.mimeType ?? fetched.contentType,
|
|
188
|
+
},
|
|
189
|
+
]),
|
|
190
|
+
};
|
|
191
|
+
} catch (err) {
|
|
192
|
+
params.log.warn(
|
|
193
|
+
`[mixin] failed to resolve inbound attachment: messageId=${params.msg.messageId}, category=${params.msg.category}, error=${err instanceof Error ? err.message : String(err)}`,
|
|
194
|
+
);
|
|
195
|
+
return {
|
|
196
|
+
text: formatInboundAttachmentText(params.msg.category, payload),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
72
201
|
function shouldPassGroupFilter(config: MixinAccountConfig, text: string): boolean {
|
|
73
|
-
if (!config.requireMentionInGroup)
|
|
202
|
+
if (!config.requireMentionInGroup) {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
74
205
|
const lower = text.toLowerCase();
|
|
75
|
-
return (
|
|
76
|
-
lower.includes("?") ||
|
|
77
|
-
lower.includes("?") ||
|
|
78
|
-
/帮|请|分析|总结|help/i.test(lower)
|
|
79
|
-
);
|
|
206
|
+
return lower.includes("?") || /帮我|请|分析|总结|help/i.test(lower);
|
|
80
207
|
}
|
|
81
208
|
|
|
82
209
|
function isOutboxCommand(text: string): boolean {
|
|
@@ -105,6 +232,55 @@ function formatOutboxStatus(status: Awaited<ReturnType<typeof getOutboxStatus>>)
|
|
|
105
232
|
return lines.join("\n");
|
|
106
233
|
}
|
|
107
234
|
|
|
235
|
+
function normalizeAllowEntry(entry: string): string {
|
|
236
|
+
return entry.trim().toLowerCase();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function resolveMixinAllowFromPaths(
|
|
240
|
+
rt: ReturnType<typeof getMixinRuntime>,
|
|
241
|
+
accountId: string,
|
|
242
|
+
): string[] {
|
|
243
|
+
const oauthOverride = process.env.OPENCLAW_OAUTH_DIR?.trim();
|
|
244
|
+
const oauthDir = oauthOverride
|
|
245
|
+
? path.resolve(oauthOverride)
|
|
246
|
+
: path.join(rt.state.resolveStateDir(process.env, os.homedir), "credentials");
|
|
247
|
+
const normalizedAccountId = accountId.trim().toLowerCase();
|
|
248
|
+
const paths = [path.join(oauthDir, "mixin-allowFrom.json")];
|
|
249
|
+
if (normalizedAccountId) {
|
|
250
|
+
paths.unshift(path.join(oauthDir, `mixin-${normalizedAccountId}-allowFrom.json`));
|
|
251
|
+
}
|
|
252
|
+
return Array.from(new Set(paths));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function readAllowFromFile(filePath: string): Promise<string[]> {
|
|
256
|
+
try {
|
|
257
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
258
|
+
const parsed = JSON.parse(raw) as { allowFrom?: unknown };
|
|
259
|
+
return Array.isArray(parsed.allowFrom)
|
|
260
|
+
? parsed.allowFrom.map((entry) => String(entry)).map(normalizeAllowEntry).filter(Boolean)
|
|
261
|
+
: [];
|
|
262
|
+
} catch {
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function readEffectiveAllowFrom(
|
|
268
|
+
rt: ReturnType<typeof getMixinRuntime>,
|
|
269
|
+
accountId: string,
|
|
270
|
+
configAllowFrom: string[],
|
|
271
|
+
log?: { info: (m: string) => void },
|
|
272
|
+
): Promise<Set<string>> {
|
|
273
|
+
const runtimeAllowFrom = await rt.channel.pairing.readAllowFromStore("mixin", undefined, accountId).catch(() => []);
|
|
274
|
+
const filePaths = resolveMixinAllowFromPaths(rt, accountId);
|
|
275
|
+
if (!loggedAllowFromAccounts.has(accountId)) {
|
|
276
|
+
log?.info(`[mixin] allow-from paths: accountId=${accountId}, paths=${filePaths.join(", ")}`);
|
|
277
|
+
loggedAllowFromAccounts.add(accountId);
|
|
278
|
+
}
|
|
279
|
+
const fileEntries = await Promise.all(filePaths.map((filePath) => readAllowFromFile(filePath)));
|
|
280
|
+
const fileAllowFrom = fileEntries.flat();
|
|
281
|
+
return new Set([...configAllowFrom, ...runtimeAllowFrom, ...fileAllowFrom].map(normalizeAllowEntry).filter(Boolean));
|
|
282
|
+
}
|
|
283
|
+
|
|
108
284
|
async function deliverMixinReply(params: {
|
|
109
285
|
cfg: OpenClawConfig;
|
|
110
286
|
accountId: string;
|
|
@@ -130,6 +306,16 @@ async function deliverMixinReply(params: {
|
|
|
130
306
|
return;
|
|
131
307
|
}
|
|
132
308
|
|
|
309
|
+
if (plan.kind === "file") {
|
|
310
|
+
await sendFileMessage(cfg, accountId, conversationId, recipientId, plan.file, log);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (plan.kind === "audio") {
|
|
315
|
+
await sendAudioMessage(cfg, accountId, conversationId, recipientId, plan.audio, log);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
133
319
|
if (plan.kind === "buttons") {
|
|
134
320
|
if (plan.intro) {
|
|
135
321
|
await sendTextMessage(cfg, accountId, conversationId, recipientId, plan.intro, log);
|
|
@@ -141,6 +327,65 @@ async function deliverMixinReply(params: {
|
|
|
141
327
|
await sendCardMessage(cfg, accountId, conversationId, recipientId, plan.card, log);
|
|
142
328
|
}
|
|
143
329
|
|
|
330
|
+
async function handleUnauthorizedDirectMessage(params: {
|
|
331
|
+
rt: ReturnType<typeof getMixinRuntime>;
|
|
332
|
+
cfg: OpenClawConfig;
|
|
333
|
+
accountId: string;
|
|
334
|
+
config: MixinAccountConfig;
|
|
335
|
+
msg: MixinInboundMessage;
|
|
336
|
+
log: { info: (m: string) => void; warn: (m: string) => void; error: (m: string, e?: unknown) => void };
|
|
337
|
+
}): Promise<void> {
|
|
338
|
+
const { rt, cfg, accountId, config, msg, log } = params;
|
|
339
|
+
const dmPolicy = config.dmPolicy ?? "pairing";
|
|
340
|
+
|
|
341
|
+
if (dmPolicy === "disabled") {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const now = Date.now();
|
|
346
|
+
const lastNotified = unauthNotifiedUsers.get(msg.userId) ?? 0;
|
|
347
|
+
const shouldNotify = lastNotified === 0 || now - lastNotified > UNAUTH_NOTIFY_INTERVAL;
|
|
348
|
+
|
|
349
|
+
if (!shouldNotify) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
pruneUnauthNotifiedUsers(now);
|
|
354
|
+
unauthNotifiedUsers.set(msg.userId, now);
|
|
355
|
+
|
|
356
|
+
if (dmPolicy === "pairing") {
|
|
357
|
+
try {
|
|
358
|
+
const { code, created } = await rt.channel.pairing.upsertPairingRequest({
|
|
359
|
+
channel: "mixin",
|
|
360
|
+
id: msg.userId,
|
|
361
|
+
accountId,
|
|
362
|
+
meta: {
|
|
363
|
+
conversationId: msg.conversationId,
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
if (created && code) {
|
|
368
|
+
const reply = rt.channel.pairing.buildPairingReply({
|
|
369
|
+
channel: "mixin",
|
|
370
|
+
idLine: `Your Mixin UUID: ${msg.userId}`,
|
|
371
|
+
code,
|
|
372
|
+
});
|
|
373
|
+
await sendTextMessage(cfg, accountId, msg.conversationId, msg.userId, reply, log);
|
|
374
|
+
}
|
|
375
|
+
} catch (err) {
|
|
376
|
+
log.error(`[mixin] pairing reply failed for ${msg.userId}`, err);
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (dmPolicy === "allowlist") {
|
|
382
|
+
const reply = config.allowFrom.length > 0
|
|
383
|
+
? `OpenClaw: access not configured.\n\nYour Mixin UUID: ${msg.userId}\n\nAsk the bot owner to add your Mixin UUID to channels.mixin.allowFrom.`
|
|
384
|
+
: `OpenClaw: access not configured.\n\nYour Mixin UUID: ${msg.userId}\n\nAsk the bot owner to add your Mixin UUID to channels.mixin.allowFrom.`;
|
|
385
|
+
await sendTextMessage(cfg, accountId, msg.conversationId, msg.userId, reply, log);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
144
389
|
export async function handleMixinMessage(params: {
|
|
145
390
|
cfg: OpenClawConfig;
|
|
146
391
|
accountId: string;
|
|
@@ -151,73 +396,75 @@ export async function handleMixinMessage(params: {
|
|
|
151
396
|
const { cfg, accountId, msg, isDirect, log } = params;
|
|
152
397
|
const rt = getMixinRuntime();
|
|
153
398
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
399
|
+
if (isProcessed(msg.messageId)) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const config = getAccountConfig(cfg, accountId);
|
|
404
|
+
|
|
405
|
+
if (msg.category === "ENCRYPTED_TEXT" || msg.category === "ENCRYPTED_POST") {
|
|
406
|
+
log.info(`[mixin] decrypting encrypted message ${msg.messageId}, category=${msg.category}`);
|
|
407
|
+
try {
|
|
408
|
+
const decrypted = decryptMixinMessage(
|
|
409
|
+
msg.data,
|
|
410
|
+
config.sessionPrivateKey!,
|
|
411
|
+
config.sessionId!,
|
|
412
|
+
);
|
|
413
|
+
if (!decrypted) {
|
|
414
|
+
log.error(`[mixin] decryption failed for ${msg.messageId}`);
|
|
415
|
+
markProcessed(msg.messageId);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
log.info(`[mixin] decryption successful: messageId=${msg.messageId}, length=${decrypted.length}`);
|
|
419
|
+
msg.data = Buffer.from(decrypted).toString("base64");
|
|
420
|
+
msg.category = "PLAIN_TEXT";
|
|
421
|
+
} catch (err) {
|
|
422
|
+
log.error(`[mixin] decryption exception for ${msg.messageId}`, err);
|
|
423
|
+
markProcessed(msg.messageId);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const isTextMessage = msg.category.startsWith("PLAIN_TEXT") || msg.category.startsWith("PLAIN_POST");
|
|
429
|
+
const isAttachmentMessage = msg.category === "PLAIN_DATA" || msg.category === "PLAIN_AUDIO";
|
|
430
|
+
|
|
431
|
+
if (!isTextMessage && !isAttachmentMessage) {
|
|
432
|
+
log.info(`[mixin] skip non-text message: ${msg.category}`);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
let text = decodeContent(msg.category, msg.data).trim();
|
|
437
|
+
let mediaPayload: AgentMediaPayload | undefined;
|
|
438
|
+
if (isAttachmentMessage) {
|
|
439
|
+
const resolved = await resolveInboundAttachment({ rt, config, msg, log });
|
|
440
|
+
text = resolved.text.trim();
|
|
441
|
+
mediaPayload = resolved.mediaPayload;
|
|
442
|
+
}
|
|
443
|
+
log.info(`[mixin] decoded text: messageId=${msg.messageId}, category=${msg.category}, length=${text.length}`);
|
|
444
|
+
|
|
445
|
+
if (!text) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (!isDirect && !isAttachmentMessage && !shouldPassGroupFilter(config, text)) {
|
|
197
450
|
log.info(`[mixin] group message filtered: ${msg.messageId}`);
|
|
198
451
|
return;
|
|
199
452
|
}
|
|
200
453
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// 标记为已处理
|
|
454
|
+
const effectiveAllowFrom = await readEffectiveAllowFrom(rt, accountId, config.allowFrom, log);
|
|
455
|
+
const normalizedUserId = normalizeAllowEntry(msg.userId);
|
|
456
|
+
const dmPolicy = config.dmPolicy ?? "pairing";
|
|
457
|
+
const isAuthorized = dmPolicy === "open" || effectiveAllowFrom.has(normalizedUserId);
|
|
458
|
+
|
|
459
|
+
if (!isAuthorized) {
|
|
460
|
+
log.warn(`[mixin] user ${msg.userId} not authorized (dmPolicy=${dmPolicy})`);
|
|
461
|
+
markProcessed(msg.messageId);
|
|
462
|
+
if (isDirect) {
|
|
463
|
+
await handleUnauthorizedDirectMessage({ rt, cfg, accountId, config, msg, log });
|
|
464
|
+
}
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
221
468
|
markProcessed(msg.messageId);
|
|
222
469
|
|
|
223
470
|
if (isOutboxCommand(text)) {
|
|
@@ -238,99 +485,92 @@ if (!config.allowFrom.includes(msg.userId)) {
|
|
|
238
485
|
return;
|
|
239
486
|
}
|
|
240
487
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
storePath,
|
|
307
|
-
sessionKey: route.sessionKey,
|
|
308
|
-
ctx,
|
|
309
|
-
onRecordError: (err: unknown) => {
|
|
310
|
-
log.error("[mixin] session record error", err);
|
|
311
|
-
},
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
// 分发消息
|
|
488
|
+
const peerId = isDirect ? msg.userId : msg.conversationId;
|
|
489
|
+
log.info(`[mixin] resolving route: channel=mixin, accountId=${accountId}, peer.kind=${isDirect ? "direct" : "group"}, peer.id=${peerId}`);
|
|
490
|
+
|
|
491
|
+
const route = rt.channel.routing.resolveAgentRoute({
|
|
492
|
+
cfg,
|
|
493
|
+
channel: "mixin",
|
|
494
|
+
accountId,
|
|
495
|
+
peer: {
|
|
496
|
+
kind: isDirect ? "direct" : "group",
|
|
497
|
+
id: peerId,
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
log.info(`[mixin] route result: ${route ? "FOUND" : "NULL"} - agentId=${route?.agentId ?? "N/A"}`);
|
|
502
|
+
|
|
503
|
+
if (!route) {
|
|
504
|
+
log.warn(`[mixin] no agent route for ${msg.userId} (peerId: ${peerId})`);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const shouldComputeCommandAuthorized = rt.channel.commands.shouldComputeCommandAuthorized(text, cfg);
|
|
509
|
+
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
|
510
|
+
const senderAllowedForCommands = useAccessGroups ? effectiveAllowFrom.has(normalizedUserId) : true;
|
|
511
|
+
|
|
512
|
+
const commandAuthorized = shouldComputeCommandAuthorized
|
|
513
|
+
? rt.channel.commands.resolveCommandAuthorizedFromAuthorizers({
|
|
514
|
+
useAccessGroups,
|
|
515
|
+
authorizers: [
|
|
516
|
+
{
|
|
517
|
+
configured: effectiveAllowFrom.size > 0,
|
|
518
|
+
allowed: senderAllowedForCommands,
|
|
519
|
+
},
|
|
520
|
+
],
|
|
521
|
+
})
|
|
522
|
+
: undefined;
|
|
523
|
+
|
|
524
|
+
const ctx = rt.channel.reply.finalizeInboundContext({
|
|
525
|
+
Body: text,
|
|
526
|
+
RawBody: text,
|
|
527
|
+
CommandBody: text,
|
|
528
|
+
From: isDirect ? msg.userId : msg.conversationId,
|
|
529
|
+
SessionKey: route.sessionKey,
|
|
530
|
+
AccountId: accountId,
|
|
531
|
+
ChatType: isDirect ? "direct" : "group",
|
|
532
|
+
Provider: "mixin",
|
|
533
|
+
Surface: "mixin",
|
|
534
|
+
MessageSid: msg.messageId,
|
|
535
|
+
CommandAuthorized: commandAuthorized,
|
|
536
|
+
OriginatingChannel: "mixin",
|
|
537
|
+
OriginatingTo: isDirect ? msg.userId : msg.conversationId,
|
|
538
|
+
...mediaPayload,
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
const storePath = rt.channel.session.resolveStorePath(cfg.session?.store, {
|
|
542
|
+
agentId: route.agentId,
|
|
543
|
+
});
|
|
544
|
+
await rt.channel.session.recordInboundSession({
|
|
545
|
+
storePath,
|
|
546
|
+
sessionKey: route.sessionKey,
|
|
547
|
+
ctx,
|
|
548
|
+
onRecordError: (err: unknown) => {
|
|
549
|
+
log.error("[mixin] session record error", err);
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
|
|
315
553
|
log.info(`[mixin] dispatching ${msg.messageId} from ${msg.userId}`);
|
|
316
554
|
|
|
317
555
|
await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
318
556
|
ctx,
|
|
319
557
|
cfg,
|
|
320
558
|
dispatcherOptions: {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
559
|
+
deliver: async (payload) => {
|
|
560
|
+
const replyText = payload.text ?? "";
|
|
561
|
+
if (!replyText) {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const recipientId = isDirect ? msg.userId : undefined;
|
|
565
|
+
await deliverMixinReply({
|
|
566
|
+
cfg,
|
|
567
|
+
accountId,
|
|
568
|
+
conversationId: msg.conversationId,
|
|
569
|
+
recipientId,
|
|
570
|
+
text: replyText,
|
|
571
|
+
log,
|
|
572
|
+
});
|
|
573
|
+
},
|
|
334
574
|
},
|
|
335
575
|
});
|
|
336
576
|
}
|