@invago/mixin 1.0.9 → 1.0.10
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 +262 -4
- package/README.zh-CN.md +328 -77
- package/package.json +79 -1
- package/src/blaze-service.ts +24 -7
- package/src/channel.ts +85 -8
- package/src/config-schema.ts +16 -0
- package/src/config.ts +5 -0
- package/src/crypto.ts +5 -0
- package/src/inbound-handler.ts +1205 -637
- package/src/mixpay-service.ts +211 -0
- package/src/mixpay-store.ts +205 -0
- package/src/mixpay-worker.ts +353 -0
- package/src/outbound-plan.ts +26 -7
- package/src/reply-format.ts +52 -1
- package/src/runtime.ts +26 -0
- package/src/send-service.ts +24 -27
- package/src/shared.ts +25 -0
- package/src/status.ts +14 -0
- package/src/decrypt.ts +0 -126
- package/tools/mixin-plugin-onboard/README.md +0 -98
- package/tools/mixin-plugin-onboard/bin/mixin-plugin-onboard.mjs +0 -3
- package/tools/mixin-plugin-onboard/src/commands/doctor.ts +0 -28
- package/tools/mixin-plugin-onboard/src/commands/info.ts +0 -23
- package/tools/mixin-plugin-onboard/src/commands/install.ts +0 -5
- package/tools/mixin-plugin-onboard/src/commands/update.ts +0 -5
- package/tools/mixin-plugin-onboard/src/index.ts +0 -49
- package/tools/mixin-plugin-onboard/src/utils.ts +0 -189
package/src/inbound-handler.ts
CHANGED
|
@@ -1,637 +1,1205 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if (!
|
|
207
|
-
return
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
return
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
async function
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
log
|
|
280
|
-
): Promise<
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
}
|
|
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 { buildAgentMediaPayload, evaluateSenderGroupAccess, resolveDefaultGroupPolicy } from "openclaw/plugin-sdk";
|
|
7
|
+
import type { AgentMediaPayload, OpenClawConfig } from "openclaw/plugin-sdk";
|
|
8
|
+
import { getAccountConfig, resolveConversationPolicy } from "./config.js";
|
|
9
|
+
import type { MixinAccountConfig } from "./config-schema.js";
|
|
10
|
+
import { decryptMixinMessage } from "./crypto.js";
|
|
11
|
+
import { getMixpayOrderStatusText, getRecentMixpayOrdersText, refreshMixpayOrderStatus } from "./mixpay-worker.js";
|
|
12
|
+
import { buildMixinOutboundPlanFromReplyText, executeMixinOutboundPlan } from "./outbound-plan.js";
|
|
13
|
+
import { getMixinRuntime } from "./runtime.js";
|
|
14
|
+
import {
|
|
15
|
+
getOutboxStatus,
|
|
16
|
+
purgePermanentInvalidOutboxEntries,
|
|
17
|
+
sendTextMessage,
|
|
18
|
+
} from "./send-service.js";
|
|
19
|
+
import { buildClient } from "./shared.js";
|
|
20
|
+
|
|
21
|
+
export interface MixinInboundMessage {
|
|
22
|
+
conversationId: string;
|
|
23
|
+
userId: string;
|
|
24
|
+
messageId: string;
|
|
25
|
+
category: string;
|
|
26
|
+
data: string;
|
|
27
|
+
createdAt: string;
|
|
28
|
+
publicKey?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const processedMessages = new Set<string>();
|
|
32
|
+
const MAX_DEDUP_SIZE = 2000;
|
|
33
|
+
const unauthNotifiedUsers = new Map<string, number>();
|
|
34
|
+
const unauthNotifiedGroups = new Map<string, number>();
|
|
35
|
+
const loggedAllowFromAccounts = new Set<string>();
|
|
36
|
+
const UNAUTH_NOTIFY_INTERVAL = 20 * 60 * 1000;
|
|
37
|
+
const MAX_UNAUTH_NOTIFY_USERS = 1000;
|
|
38
|
+
const MAX_UNAUTH_NOTIFY_GROUPS = 1000;
|
|
39
|
+
const INBOUND_MEDIA_MAX_BYTES = 30 * 1024 * 1024;
|
|
40
|
+
const USER_PROFILE_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
41
|
+
const MAX_USER_PROFILE_CACHE = 2000;
|
|
42
|
+
const GROUP_PROFILE_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
43
|
+
const MAX_GROUP_PROFILE_CACHE = 1000;
|
|
44
|
+
const BOT_PROFILE_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
45
|
+
const SESSION_LABEL_MAX_LENGTH = 64;
|
|
46
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
47
|
+
|
|
48
|
+
type CachedUserProfile = {
|
|
49
|
+
fullName: string;
|
|
50
|
+
expiresAt: number;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type CachedGroupProfile = {
|
|
54
|
+
name: string;
|
|
55
|
+
expiresAt: number;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type CachedBotProfile = {
|
|
59
|
+
name: string;
|
|
60
|
+
expiresAt: number;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type MixinAttachmentRequest = {
|
|
64
|
+
attachmentId: string;
|
|
65
|
+
mimeType?: string;
|
|
66
|
+
size?: number;
|
|
67
|
+
fileName?: string;
|
|
68
|
+
duration?: number;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const cachedUserProfiles = new Map<string, CachedUserProfile>();
|
|
72
|
+
const cachedGroupProfiles = new Map<string, CachedGroupProfile>();
|
|
73
|
+
const cachedBotProfiles = new Map<string, CachedBotProfile>();
|
|
74
|
+
let cachedUpdateSessionStore:
|
|
75
|
+
| ((storePath: string, mutator: (store: Record<string, Record<string, unknown>>) => void | Promise<void>) => Promise<unknown>)
|
|
76
|
+
| null
|
|
77
|
+
| undefined;
|
|
78
|
+
|
|
79
|
+
function isProcessed(messageId: string): boolean {
|
|
80
|
+
return processedMessages.has(messageId);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function markProcessed(messageId: string): void {
|
|
84
|
+
if (processedMessages.size >= MAX_DEDUP_SIZE) {
|
|
85
|
+
const first = processedMessages.values().next().value;
|
|
86
|
+
if (first) {
|
|
87
|
+
processedMessages.delete(first);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
processedMessages.add(messageId);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function pruneUnauthNotifiedUsers(now: number): void {
|
|
94
|
+
for (const [userId, lastNotified] of unauthNotifiedUsers) {
|
|
95
|
+
if (now - lastNotified > UNAUTH_NOTIFY_INTERVAL) {
|
|
96
|
+
unauthNotifiedUsers.delete(userId);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
while (unauthNotifiedUsers.size >= MAX_UNAUTH_NOTIFY_USERS) {
|
|
101
|
+
const first = unauthNotifiedUsers.keys().next().value;
|
|
102
|
+
if (!first) {
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
unauthNotifiedUsers.delete(first);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function pruneUnauthNotifiedGroups(now: number): void {
|
|
110
|
+
for (const [conversationId, lastNotified] of unauthNotifiedGroups) {
|
|
111
|
+
if (now - lastNotified > UNAUTH_NOTIFY_INTERVAL) {
|
|
112
|
+
unauthNotifiedGroups.delete(conversationId);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
while (unauthNotifiedGroups.size >= MAX_UNAUTH_NOTIFY_GROUPS) {
|
|
117
|
+
const first = unauthNotifiedGroups.keys().next().value;
|
|
118
|
+
if (!first) {
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
unauthNotifiedGroups.delete(first);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function decodeContent(category: string, data: string): string {
|
|
126
|
+
if (category.startsWith("PLAIN_TEXT") || category.startsWith("PLAIN_POST")) {
|
|
127
|
+
try {
|
|
128
|
+
return Buffer.from(data, "base64").toString("utf-8");
|
|
129
|
+
} catch {
|
|
130
|
+
return data;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return `[${category}]`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function buildUserProfileCacheKey(accountId: string, userId: string): string {
|
|
137
|
+
return `${accountId}:${userId.trim().toLowerCase()}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function buildGroupProfileCacheKey(accountId: string, conversationId: string): string {
|
|
141
|
+
return `${accountId}:${conversationId.trim().toLowerCase()}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function buildBotProfileCacheKey(accountId: string): string {
|
|
145
|
+
return accountId.trim().toLowerCase();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function pruneUserProfileCache(now: number): void {
|
|
149
|
+
for (const [key, cached] of cachedUserProfiles) {
|
|
150
|
+
if (cached.expiresAt <= now) {
|
|
151
|
+
cachedUserProfiles.delete(key);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
while (cachedUserProfiles.size >= MAX_USER_PROFILE_CACHE) {
|
|
156
|
+
const first = cachedUserProfiles.keys().next().value;
|
|
157
|
+
if (!first) {
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
cachedUserProfiles.delete(first);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function pruneGroupProfileCache(now: number): void {
|
|
165
|
+
for (const [key, cached] of cachedGroupProfiles) {
|
|
166
|
+
if (cached.expiresAt <= now) {
|
|
167
|
+
cachedGroupProfiles.delete(key);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
while (cachedGroupProfiles.size >= MAX_GROUP_PROFILE_CACHE) {
|
|
172
|
+
const first = cachedGroupProfiles.keys().next().value;
|
|
173
|
+
if (!first) {
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
cachedGroupProfiles.delete(first);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function pruneBotProfileCache(now: number): void {
|
|
181
|
+
for (const [key, cached] of cachedBotProfiles) {
|
|
182
|
+
if (cached.expiresAt <= now) {
|
|
183
|
+
cachedBotProfiles.delete(key);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function normalizePresentationName(value: string): string {
|
|
189
|
+
return value.replace(/\s+/g, " ").trim();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function sliceUtf16Safe(value: string, maxLength: number): string {
|
|
193
|
+
if (value.length <= maxLength) {
|
|
194
|
+
return value;
|
|
195
|
+
}
|
|
196
|
+
let sliced = value.slice(0, maxLength);
|
|
197
|
+
const lastCodeUnit = sliced.charCodeAt(sliced.length - 1);
|
|
198
|
+
if (lastCodeUnit >= 0xd800 && lastCodeUnit <= 0xdbff) {
|
|
199
|
+
sliced = sliced.slice(0, -1);
|
|
200
|
+
}
|
|
201
|
+
return sliced;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function clampSessionLabel(label: string): string {
|
|
205
|
+
const trimmed = normalizePresentationName(label);
|
|
206
|
+
if (!trimmed) {
|
|
207
|
+
return "";
|
|
208
|
+
}
|
|
209
|
+
if (trimmed.length <= SESSION_LABEL_MAX_LENGTH) {
|
|
210
|
+
return trimmed;
|
|
211
|
+
}
|
|
212
|
+
return sliceUtf16Safe(trimmed, SESSION_LABEL_MAX_LENGTH);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function loadUpdateSessionStore(log: {
|
|
216
|
+
info: (m: string) => void;
|
|
217
|
+
warn: (m: string) => void;
|
|
218
|
+
error: (m: string, e?: unknown) => void;
|
|
219
|
+
}): Promise<
|
|
220
|
+
((storePath: string, mutator: (store: Record<string, Record<string, unknown>>) => void | Promise<void>) => Promise<unknown>) | null
|
|
221
|
+
> {
|
|
222
|
+
if (cachedUpdateSessionStore !== undefined) {
|
|
223
|
+
return cachedUpdateSessionStore;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const openclawEntryPath = requireFromHere.resolve("openclaw");
|
|
228
|
+
const openclawEntryDir = path.dirname(openclawEntryPath);
|
|
229
|
+
const distDir = path.basename(openclawEntryDir).toLowerCase() === "dist" ? openclawEntryDir : path.join(openclawEntryDir, "dist");
|
|
230
|
+
|
|
231
|
+
const entries = await fs.readdir(distDir, { withFileTypes: true });
|
|
232
|
+
const sessionModules = entries
|
|
233
|
+
.filter((entry) => entry.isFile() && /^sessions-.*\.js$/i.test(entry.name))
|
|
234
|
+
.map((entry) => entry.name)
|
|
235
|
+
.sort();
|
|
236
|
+
if (!sessionModules.length) {
|
|
237
|
+
cachedUpdateSessionStore = null;
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
for (const sessionModule of sessionModules) {
|
|
242
|
+
try {
|
|
243
|
+
const moduleUrl = pathToFileURL(path.join(distDir, sessionModule)).href;
|
|
244
|
+
const imported = (await import(moduleUrl)) as Record<string, unknown>;
|
|
245
|
+
const candidate = Object.values(imported).find((val) => {
|
|
246
|
+
if (typeof val !== "function" || val.length !== 2) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
return true;
|
|
250
|
+
});
|
|
251
|
+
if (candidate) {
|
|
252
|
+
cachedUpdateSessionStore = candidate as (
|
|
253
|
+
storePath: string,
|
|
254
|
+
mutator: (store: Record<string, Record<string, unknown>>) => void | Promise<void>,
|
|
255
|
+
) => Promise<unknown>;
|
|
256
|
+
return cachedUpdateSessionStore;
|
|
257
|
+
}
|
|
258
|
+
} catch {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
log.warn("[mixin] no matching updateSessionStore export found in session modules");
|
|
264
|
+
cachedUpdateSessionStore = null;
|
|
265
|
+
return null;
|
|
266
|
+
} catch (err) {
|
|
267
|
+
log.warn(
|
|
268
|
+
`[mixin] failed to load OpenClaw session store updater: error=${err instanceof Error ? err.message : String(err)}`,
|
|
269
|
+
);
|
|
270
|
+
cachedUpdateSessionStore = null;
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function resolveSenderName(params: {
|
|
276
|
+
accountId: string;
|
|
277
|
+
config: MixinAccountConfig;
|
|
278
|
+
userId: string;
|
|
279
|
+
log: { info: (m: string) => void; warn: (m: string) => void; error: (m: string, e?: unknown) => void };
|
|
280
|
+
}): Promise<string> {
|
|
281
|
+
const userId = params.userId.trim();
|
|
282
|
+
if (!userId) {
|
|
283
|
+
return "";
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const now = Date.now();
|
|
287
|
+
const cacheKey = buildUserProfileCacheKey(params.accountId, userId);
|
|
288
|
+
const cached = cachedUserProfiles.get(cacheKey);
|
|
289
|
+
if (cached && cached.expiresAt > now) {
|
|
290
|
+
return cached.fullName;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
pruneUserProfileCache(now);
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const client = buildClient(params.config);
|
|
297
|
+
const user = await client.user.fetch(userId);
|
|
298
|
+
const fullName = typeof user.full_name === "string" && user.full_name.trim() ? user.full_name.trim() : userId;
|
|
299
|
+
cachedUserProfiles.set(cacheKey, {
|
|
300
|
+
fullName,
|
|
301
|
+
expiresAt: now + USER_PROFILE_CACHE_TTL_MS,
|
|
302
|
+
});
|
|
303
|
+
return fullName;
|
|
304
|
+
} catch (err) {
|
|
305
|
+
params.log.warn(
|
|
306
|
+
`[mixin] failed to resolve sender profile: accountId=${params.accountId}, userId=${userId}, error=${err instanceof Error ? err.message : String(err)}`,
|
|
307
|
+
);
|
|
308
|
+
return userId;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function resolveGroupName(params: {
|
|
313
|
+
accountId: string;
|
|
314
|
+
config: MixinAccountConfig;
|
|
315
|
+
conversationId: string;
|
|
316
|
+
log: { info: (m: string) => void; warn: (m: string) => void; error: (m: string, e?: unknown) => void };
|
|
317
|
+
}): Promise<string> {
|
|
318
|
+
const conversationId = params.conversationId.trim();
|
|
319
|
+
if (!conversationId) {
|
|
320
|
+
return "";
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const now = Date.now();
|
|
324
|
+
const cacheKey = buildGroupProfileCacheKey(params.accountId, conversationId);
|
|
325
|
+
const cached = cachedGroupProfiles.get(cacheKey);
|
|
326
|
+
if (cached && cached.expiresAt > now) {
|
|
327
|
+
return cached.name;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
pruneGroupProfileCache(now);
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
const client = buildClient(params.config);
|
|
334
|
+
const conversation = await client.conversation.fetch(conversationId);
|
|
335
|
+
const name = normalizePresentationName(String(conversation.name ?? "")) || conversationId;
|
|
336
|
+
cachedGroupProfiles.set(cacheKey, {
|
|
337
|
+
name,
|
|
338
|
+
expiresAt: now + GROUP_PROFILE_CACHE_TTL_MS,
|
|
339
|
+
});
|
|
340
|
+
return name;
|
|
341
|
+
} catch (err) {
|
|
342
|
+
params.log.warn(
|
|
343
|
+
`[mixin] failed to resolve group profile: accountId=${params.accountId}, conversationId=${conversationId}, error=${err instanceof Error ? err.message : String(err)}`,
|
|
344
|
+
);
|
|
345
|
+
return conversationId;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function resolveBotName(params: {
|
|
350
|
+
accountId: string;
|
|
351
|
+
config: MixinAccountConfig;
|
|
352
|
+
log: { info: (m: string) => void; warn: (m: string) => void; error: (m: string, e?: unknown) => void };
|
|
353
|
+
}): Promise<string> {
|
|
354
|
+
const configuredName = normalizePresentationName(params.config.name ?? "");
|
|
355
|
+
if (configuredName) {
|
|
356
|
+
return configuredName;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const now = Date.now();
|
|
360
|
+
const cacheKey = buildBotProfileCacheKey(params.accountId);
|
|
361
|
+
const cached = cachedBotProfiles.get(cacheKey);
|
|
362
|
+
if (cached && cached.expiresAt > now) {
|
|
363
|
+
return cached.name;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
pruneBotProfileCache(now);
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
const client = buildClient(params.config);
|
|
370
|
+
const profile = await client.user.profile();
|
|
371
|
+
const name = normalizePresentationName(String(profile.full_name ?? "")) || params.accountId;
|
|
372
|
+
cachedBotProfiles.set(cacheKey, {
|
|
373
|
+
name,
|
|
374
|
+
expiresAt: now + BOT_PROFILE_CACHE_TTL_MS,
|
|
375
|
+
});
|
|
376
|
+
return name;
|
|
377
|
+
} catch (err) {
|
|
378
|
+
params.log.warn(
|
|
379
|
+
`[mixin] failed to resolve bot profile: accountId=${params.accountId}, error=${err instanceof Error ? err.message : String(err)}`,
|
|
380
|
+
);
|
|
381
|
+
return params.accountId;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function updateSessionPresentation(params: {
|
|
386
|
+
storePath: string;
|
|
387
|
+
sessionKey: string;
|
|
388
|
+
label: string;
|
|
389
|
+
displayName?: string;
|
|
390
|
+
log: { info: (m: string) => void; warn: (m: string) => void; error: (m: string, e?: unknown) => void };
|
|
391
|
+
}): Promise<void> {
|
|
392
|
+
const nextLabel = clampSessionLabel(params.label);
|
|
393
|
+
const nextDisplayName = clampSessionLabel(params.displayName ?? "");
|
|
394
|
+
if (!nextLabel && !nextDisplayName) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
const updateSessionStore = await loadUpdateSessionStore(params.log);
|
|
400
|
+
if (!updateSessionStore) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
await updateSessionStore(params.storePath, (store: Record<string, Record<string, unknown>>) => {
|
|
404
|
+
const entry = store[params.sessionKey];
|
|
405
|
+
if (!entry || typeof entry !== "object") {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
let changed = false;
|
|
410
|
+
if (nextLabel && entry.label !== nextLabel) {
|
|
411
|
+
entry.label = nextLabel;
|
|
412
|
+
changed = true;
|
|
413
|
+
}
|
|
414
|
+
if (nextDisplayName && entry.displayName !== nextDisplayName) {
|
|
415
|
+
entry.displayName = nextDisplayName;
|
|
416
|
+
changed = true;
|
|
417
|
+
}
|
|
418
|
+
if (changed) {
|
|
419
|
+
entry.updatedAt = new Date().toISOString();
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
} catch (err) {
|
|
423
|
+
params.log.warn(
|
|
424
|
+
`[mixin] failed to update session presentation: sessionKey=${params.sessionKey}, error=${err instanceof Error ? err.message : String(err)}`,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function resolveInboundMediaMaxBytes(config: MixinAccountConfig): number {
|
|
430
|
+
const mediaMaxMb = config.mediaMaxMb;
|
|
431
|
+
if (typeof mediaMaxMb === "number" && Number.isFinite(mediaMaxMb) && mediaMaxMb > 0) {
|
|
432
|
+
return Math.max(1, Math.floor(mediaMaxMb * 1024 * 1024));
|
|
433
|
+
}
|
|
434
|
+
return INBOUND_MEDIA_MAX_BYTES;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function parseInboundAttachmentRequest(category: string, data: string): MixinAttachmentRequest | null {
|
|
438
|
+
if (category !== "PLAIN_DATA" && category !== "PLAIN_AUDIO") {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
const decoded = Buffer.from(data, "base64").toString("utf-8");
|
|
444
|
+
const parsed = JSON.parse(decoded) as {
|
|
445
|
+
attachment_id?: unknown;
|
|
446
|
+
mime_type?: unknown;
|
|
447
|
+
size?: unknown;
|
|
448
|
+
name?: unknown;
|
|
449
|
+
duration?: unknown;
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
if (typeof parsed.attachment_id !== "string" || !parsed.attachment_id.trim()) {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
attachmentId: parsed.attachment_id.trim(),
|
|
458
|
+
mimeType: typeof parsed.mime_type === "string" ? parsed.mime_type.trim() || undefined : undefined,
|
|
459
|
+
size: typeof parsed.size === "number" && Number.isFinite(parsed.size) ? parsed.size : undefined,
|
|
460
|
+
fileName: typeof parsed.name === "string" ? parsed.name.trim() || undefined : undefined,
|
|
461
|
+
duration: typeof parsed.duration === "number" && Number.isFinite(parsed.duration) ? parsed.duration : undefined,
|
|
462
|
+
};
|
|
463
|
+
} catch {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function formatInboundAttachmentText(category: string, payload: MixinAttachmentRequest): string {
|
|
469
|
+
if (category === "PLAIN_AUDIO") {
|
|
470
|
+
const details = [
|
|
471
|
+
payload.fileName,
|
|
472
|
+
payload.mimeType,
|
|
473
|
+
typeof payload.duration === "number" ? `${payload.duration}s` : undefined,
|
|
474
|
+
typeof payload.size === "number" ? `${payload.size} bytes` : undefined,
|
|
475
|
+
].filter(Boolean);
|
|
476
|
+
return details.length > 0 ? `[Mixin audio] ${details.join(" | ")}` : "[Mixin audio]";
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const details = [
|
|
480
|
+
payload.fileName,
|
|
481
|
+
payload.mimeType,
|
|
482
|
+
typeof payload.size === "number" ? `${payload.size} bytes` : undefined,
|
|
483
|
+
].filter(Boolean);
|
|
484
|
+
return details.length > 0 ? `[Mixin file] ${details.join(" | ")}` : "[Mixin file]";
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function resolveInboundAttachment(params: {
|
|
488
|
+
rt: ReturnType<typeof getMixinRuntime>;
|
|
489
|
+
config: MixinAccountConfig;
|
|
490
|
+
msg: MixinInboundMessage;
|
|
491
|
+
log: { info: (m: string) => void; warn: (m: string) => void; error: (m: string, e?: unknown) => void };
|
|
492
|
+
}): Promise<{ text: string; mediaPayload?: AgentMediaPayload }> {
|
|
493
|
+
const payload = parseInboundAttachmentRequest(params.msg.category, params.msg.data);
|
|
494
|
+
if (!payload) {
|
|
495
|
+
return {
|
|
496
|
+
text: `[${params.msg.category}]`,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
try {
|
|
501
|
+
const client = buildClient(params.config);
|
|
502
|
+
const maxBytes = resolveInboundMediaMaxBytes(params.config);
|
|
503
|
+
const attachment = await client.attachment.fetch(payload.attachmentId);
|
|
504
|
+
const fetched = await params.rt.channel.media.fetchRemoteMedia({
|
|
505
|
+
url: attachment.view_url,
|
|
506
|
+
filePathHint: payload.fileName,
|
|
507
|
+
maxBytes,
|
|
508
|
+
});
|
|
509
|
+
const saved = await params.rt.channel.media.saveMediaBuffer(
|
|
510
|
+
fetched.buffer,
|
|
511
|
+
payload.mimeType ?? fetched.contentType,
|
|
512
|
+
"mixin",
|
|
513
|
+
maxBytes,
|
|
514
|
+
payload.fileName ?? fetched.fileName,
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
return {
|
|
518
|
+
text: formatInboundAttachmentText(params.msg.category, payload),
|
|
519
|
+
mediaPayload: buildAgentMediaPayload([
|
|
520
|
+
{
|
|
521
|
+
path: saved.path,
|
|
522
|
+
contentType: saved.contentType ?? payload.mimeType ?? fetched.contentType,
|
|
523
|
+
},
|
|
524
|
+
]),
|
|
525
|
+
};
|
|
526
|
+
} catch (err) {
|
|
527
|
+
params.log.warn(
|
|
528
|
+
`[mixin] failed to resolve inbound attachment: messageId=${params.msg.messageId}, category=${params.msg.category}, error=${err instanceof Error ? err.message : String(err)}`,
|
|
529
|
+
);
|
|
530
|
+
return {
|
|
531
|
+
text: formatInboundAttachmentText(params.msg.category, payload),
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function shouldPassGroupFilter(config: MixinAccountConfig, text: string): boolean {
|
|
537
|
+
if (!config.requireMentionInGroup) {
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
540
|
+
if (text.trim().startsWith("/")) {
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
const lower = text.toLowerCase();
|
|
544
|
+
return lower.includes("?") || /帮我|请|分析|总结|help/i.test(lower);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function isOutboxCommand(text: string): boolean {
|
|
548
|
+
return /(^|\s)\/mixin-outbox(?:\s|$)/i.test(text.trim());
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function isOutboxPurgeInvalidCommand(text: string): boolean {
|
|
552
|
+
return /(^|\s)\/mixin-outbox\s+purge-invalid(?:\s|$)/i.test(normalizeCommandText(text));
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function isMixinGroupAuthCommand(text: string): boolean {
|
|
556
|
+
return /(^|\s)\/mixin-group-auth(?:\s|$)/i.test(normalizeCommandText(text));
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function isMixinGroupApproveCommand(text: string): boolean {
|
|
560
|
+
return /(^|\s)\/mixin-group-approve\s+\S+(?:\s|$)/i.test(normalizeCommandText(text));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function parseMixinGroupApproveCode(text: string): string | null {
|
|
564
|
+
const match = normalizeCommandText(text).match(/(?:^|\s)\/mixin-group-approve\s+(\S+)(?:\s|$)/i);
|
|
565
|
+
return match?.[1]?.trim() || null;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function isCollectStatusCommand(text: string): boolean {
|
|
569
|
+
return /(^|\s)\/collect\s+status\s+\S+(?:\s|$)/i.test(normalizeCommandText(text));
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function isCollectRecentCommand(text: string): boolean {
|
|
573
|
+
return /(^|\s)\/collect\s+recent(?:\s+\d+)?(?:\s|$)/i.test(normalizeCommandText(text));
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function parseCollectStatusCommand(text: string): string | null {
|
|
577
|
+
const match = normalizeCommandText(text).match(/(?:^|\s)\/collect\s+status\s+(\S+)(?:\s|$)/i);
|
|
578
|
+
return match?.[1]?.trim() || null;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function parseCollectRecentLimit(text: string): number {
|
|
582
|
+
const match = normalizeCommandText(text).match(/(?:^|\s)\/collect\s+recent(?:\s+(\d+))?(?:\s|$)/i);
|
|
583
|
+
const parsed = Number.parseInt(match?.[1] ?? "", 10);
|
|
584
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
585
|
+
return 5;
|
|
586
|
+
}
|
|
587
|
+
return Math.min(parsed, 20);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function normalizeCommandText(text: string): string {
|
|
591
|
+
return text
|
|
592
|
+
.replace(/[\u200B-\u200D\uFEFF]/g, "")
|
|
593
|
+
.replace(/\s+/g, " ")
|
|
594
|
+
.trim();
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function formatOutboxStatus(status: Awaited<ReturnType<typeof getOutboxStatus>>): string {
|
|
598
|
+
const lines = [
|
|
599
|
+
`Outbox pending: ${status.totalPending}`,
|
|
600
|
+
`Oldest pending: ${status.oldestPendingAt ?? "N/A"}`,
|
|
601
|
+
`Next attempt: ${status.nextAttemptAt ?? "N/A"}`,
|
|
602
|
+
`Latest error: ${status.latestError ?? "N/A"}`,
|
|
603
|
+
];
|
|
604
|
+
|
|
605
|
+
if (status.pendingByAccount.length > 0) {
|
|
606
|
+
lines.push("By account:");
|
|
607
|
+
for (const item of status.pendingByAccount) {
|
|
608
|
+
lines.push(`- ${item.accountId}: ${item.pending}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return lines.join("\n");
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function formatMixinGroupAuthReply(params: {
|
|
616
|
+
code: string;
|
|
617
|
+
created: boolean;
|
|
618
|
+
conversationId: string;
|
|
619
|
+
accountId: string;
|
|
620
|
+
}): string {
|
|
621
|
+
const lines = [
|
|
622
|
+
params.created ? "Group auth request created." : "Group auth request already exists.",
|
|
623
|
+
`Code: ${params.code}`,
|
|
624
|
+
`conversationId: ${params.conversationId}`,
|
|
625
|
+
"",
|
|
626
|
+
"Approve it in the OpenClaw terminal with:",
|
|
627
|
+
params.accountId === "default"
|
|
628
|
+
? `openclaw pairing approve mixin ${params.code}`
|
|
629
|
+
: `openclaw pairing approve --account ${params.accountId} mixin ${params.code}`,
|
|
630
|
+
];
|
|
631
|
+
return lines.join("\n");
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function buildGroupScopedPairingId(params: {
|
|
635
|
+
conversationId: string;
|
|
636
|
+
}): string {
|
|
637
|
+
return `group:${params.conversationId.trim().toLowerCase()}`;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function buildGroupScopedPairingMeta(params: {
|
|
641
|
+
conversationId: string;
|
|
642
|
+
userId: string;
|
|
643
|
+
}): Record<string, string> {
|
|
644
|
+
return {
|
|
645
|
+
kind: "group-auth",
|
|
646
|
+
conversationId: params.conversationId.trim(),
|
|
647
|
+
requestedBy: params.userId.trim(),
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function normalizeAllowEntry(entry: string): string {
|
|
652
|
+
return entry.trim().toLowerCase();
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function normalizeAllowEntries(entries: string[] | undefined): string[] {
|
|
656
|
+
return (entries ?? []).map(normalizeAllowEntry).filter(Boolean);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function resolveMixinAllowFromPaths(
|
|
660
|
+
rt: ReturnType<typeof getMixinRuntime>,
|
|
661
|
+
accountId: string,
|
|
662
|
+
): string[] {
|
|
663
|
+
const oauthOverride = process.env.OPENCLAW_OAUTH_DIR?.trim();
|
|
664
|
+
const oauthDir = oauthOverride
|
|
665
|
+
? path.resolve(oauthOverride)
|
|
666
|
+
: path.join(rt.state.resolveStateDir(process.env, os.homedir), "credentials");
|
|
667
|
+
const normalizedAccountId = accountId.trim().toLowerCase();
|
|
668
|
+
const paths = [path.join(oauthDir, "mixin-allowFrom.json")];
|
|
669
|
+
if (normalizedAccountId) {
|
|
670
|
+
paths.unshift(path.join(oauthDir, `mixin-${normalizedAccountId}-allowFrom.json`));
|
|
671
|
+
}
|
|
672
|
+
return Array.from(new Set(paths));
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
async function readAllowFromFile(filePath: string): Promise<string[]> {
|
|
676
|
+
try {
|
|
677
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
678
|
+
const parsed = JSON.parse(raw) as { allowFrom?: unknown };
|
|
679
|
+
return Array.isArray(parsed.allowFrom)
|
|
680
|
+
? parsed.allowFrom.map((entry) => String(entry)).map(normalizeAllowEntry).filter(Boolean)
|
|
681
|
+
: [];
|
|
682
|
+
} catch {
|
|
683
|
+
return [];
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
async function readEffectiveAllowFrom(
|
|
688
|
+
rt: ReturnType<typeof getMixinRuntime>,
|
|
689
|
+
accountId: string,
|
|
690
|
+
configAllowFrom: string[],
|
|
691
|
+
log?: { info: (m: string) => void },
|
|
692
|
+
): Promise<Set<string>> {
|
|
693
|
+
const runtimeAllowFrom = await rt.channel.pairing.readAllowFromStore("mixin", undefined, accountId).catch(() => []);
|
|
694
|
+
const filePaths = resolveMixinAllowFromPaths(rt, accountId);
|
|
695
|
+
if (!loggedAllowFromAccounts.has(accountId)) {
|
|
696
|
+
log?.info(`[mixin] allow-from paths: accountId=${accountId}, paths=${filePaths.join(", ")}`);
|
|
697
|
+
loggedAllowFromAccounts.add(accountId);
|
|
698
|
+
}
|
|
699
|
+
const fileEntries = await Promise.all(filePaths.map((filePath) => readAllowFromFile(filePath)));
|
|
700
|
+
const fileAllowFrom = fileEntries.flat();
|
|
701
|
+
return new Set([...configAllowFrom, ...runtimeAllowFrom, ...fileAllowFrom].map(normalizeAllowEntry).filter(Boolean));
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
async function deliverMixinReply(params: {
|
|
705
|
+
cfg: OpenClawConfig;
|
|
706
|
+
accountId: string;
|
|
707
|
+
conversationId: string;
|
|
708
|
+
recipientId?: string;
|
|
709
|
+
creatorId?: string;
|
|
710
|
+
text: string;
|
|
711
|
+
log: { info: (m: string) => void; warn: (m: string) => void; error: (m: string, e?: unknown) => void };
|
|
712
|
+
}): Promise<void> {
|
|
713
|
+
const { cfg, accountId, conversationId, recipientId, creatorId, text, log } = params;
|
|
714
|
+
const plan = buildMixinOutboundPlanFromReplyText(text);
|
|
715
|
+
if (plan.steps.length === 0) {
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
for (const warning of plan.warnings) {
|
|
719
|
+
log.warn(`[mixin] outbound plan warning: ${warning}`);
|
|
720
|
+
}
|
|
721
|
+
await executeMixinOutboundPlan({
|
|
722
|
+
cfg,
|
|
723
|
+
accountId,
|
|
724
|
+
conversationId,
|
|
725
|
+
recipientId,
|
|
726
|
+
creatorId,
|
|
727
|
+
steps: plan.steps,
|
|
728
|
+
log,
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async function handleUnauthorizedDirectMessage(params: {
|
|
733
|
+
rt: ReturnType<typeof getMixinRuntime>;
|
|
734
|
+
cfg: OpenClawConfig;
|
|
735
|
+
accountId: string;
|
|
736
|
+
config: MixinAccountConfig;
|
|
737
|
+
msg: MixinInboundMessage;
|
|
738
|
+
log: { info: (m: string) => void; warn: (m: string) => void; error: (m: string, e?: unknown) => void };
|
|
739
|
+
}): Promise<void> {
|
|
740
|
+
const { rt, cfg, accountId, config, msg, log } = params;
|
|
741
|
+
const dmPolicy = config.dmPolicy ?? "pairing";
|
|
742
|
+
|
|
743
|
+
if (dmPolicy === "disabled") {
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const now = Date.now();
|
|
748
|
+
const lastNotified = unauthNotifiedUsers.get(msg.userId) ?? 0;
|
|
749
|
+
const shouldNotify = lastNotified === 0 || now - lastNotified > UNAUTH_NOTIFY_INTERVAL;
|
|
750
|
+
|
|
751
|
+
if (!shouldNotify) {
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
pruneUnauthNotifiedUsers(now);
|
|
756
|
+
unauthNotifiedUsers.set(msg.userId, now);
|
|
757
|
+
|
|
758
|
+
if (dmPolicy === "pairing") {
|
|
759
|
+
try {
|
|
760
|
+
const { code, created } = await rt.channel.pairing.upsertPairingRequest({
|
|
761
|
+
channel: "mixin",
|
|
762
|
+
id: msg.userId,
|
|
763
|
+
accountId,
|
|
764
|
+
meta: {
|
|
765
|
+
conversationId: msg.conversationId,
|
|
766
|
+
},
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
if (created && code) {
|
|
770
|
+
const reply = rt.channel.pairing.buildPairingReply({
|
|
771
|
+
channel: "mixin",
|
|
772
|
+
idLine: `Your Mixin UUID: ${msg.userId}`,
|
|
773
|
+
code,
|
|
774
|
+
});
|
|
775
|
+
await sendTextMessage(cfg, accountId, msg.conversationId, msg.userId, reply, log);
|
|
776
|
+
}
|
|
777
|
+
} catch (err) {
|
|
778
|
+
log.error(`[mixin] pairing reply failed for ${msg.userId}`, err);
|
|
779
|
+
}
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (dmPolicy === "allowlist") {
|
|
784
|
+
const reply = `OpenClaw: access not configured.\n\nYour Mixin UUID: ${msg.userId}\n\nAsk the bot owner to add your Mixin UUID to channels.mixin.allowFrom.`;
|
|
785
|
+
await sendTextMessage(cfg, accountId, msg.conversationId, msg.userId, reply, log);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function evaluateMixinGroupAccess(params: {
|
|
790
|
+
cfg: OpenClawConfig;
|
|
791
|
+
config: MixinAccountConfig;
|
|
792
|
+
accountId: string;
|
|
793
|
+
conversationId: string;
|
|
794
|
+
senderId: string;
|
|
795
|
+
}): {
|
|
796
|
+
allowed: boolean;
|
|
797
|
+
reason: string;
|
|
798
|
+
groupPolicy: "open" | "disabled" | "allowlist";
|
|
799
|
+
groupAllowFrom: string[];
|
|
800
|
+
} {
|
|
801
|
+
const conversationPolicy = resolveConversationPolicy(params.cfg, params.accountId, params.conversationId);
|
|
802
|
+
if (!conversationPolicy.enabled) {
|
|
803
|
+
return {
|
|
804
|
+
allowed: false,
|
|
805
|
+
reason: "conversation disabled",
|
|
806
|
+
groupPolicy: "disabled",
|
|
807
|
+
groupAllowFrom: normalizeAllowEntries(conversationPolicy.groupAllowFrom),
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const normalizedGroupAllowFrom = normalizeAllowEntries(conversationPolicy.groupAllowFrom);
|
|
812
|
+
const decision = evaluateSenderGroupAccess({
|
|
813
|
+
providerConfigPresent: true,
|
|
814
|
+
configuredGroupPolicy: conversationPolicy.groupPolicy,
|
|
815
|
+
defaultGroupPolicy: resolveDefaultGroupPolicy(params.cfg),
|
|
816
|
+
groupAllowFrom: normalizedGroupAllowFrom,
|
|
817
|
+
senderId: normalizeAllowEntry(params.senderId),
|
|
818
|
+
isSenderAllowed: (senderId, allowFrom) => allowFrom.includes(normalizeAllowEntry(senderId)),
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
return {
|
|
822
|
+
allowed: decision.allowed,
|
|
823
|
+
reason: decision.reason,
|
|
824
|
+
groupPolicy: decision.groupPolicy,
|
|
825
|
+
groupAllowFrom: normalizedGroupAllowFrom,
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
export async function handleMixinMessage(params: {
|
|
830
|
+
cfg: OpenClawConfig;
|
|
831
|
+
accountId: string;
|
|
832
|
+
msg: MixinInboundMessage;
|
|
833
|
+
isDirect: boolean;
|
|
834
|
+
log: { info: (m: string) => void; warn: (m: string) => void; error: (m: string, e?: unknown) => void };
|
|
835
|
+
}): Promise<void> {
|
|
836
|
+
const { cfg, accountId, msg, isDirect, log } = params;
|
|
837
|
+
const rt = getMixinRuntime();
|
|
838
|
+
|
|
839
|
+
if (isProcessed(msg.messageId)) {
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const config = getAccountConfig(cfg, accountId);
|
|
844
|
+
|
|
845
|
+
if (msg.category === "ENCRYPTED_TEXT" || msg.category === "ENCRYPTED_POST") {
|
|
846
|
+
log.info(`[mixin] decrypting encrypted message ${msg.messageId}, category=${msg.category}`);
|
|
847
|
+
try {
|
|
848
|
+
const decrypted = decryptMixinMessage(
|
|
849
|
+
msg.data,
|
|
850
|
+
config.sessionPrivateKey!,
|
|
851
|
+
config.sessionId!,
|
|
852
|
+
);
|
|
853
|
+
if (!decrypted) {
|
|
854
|
+
log.error(`[mixin] decryption failed for ${msg.messageId}`);
|
|
855
|
+
markProcessed(msg.messageId);
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
log.info(`[mixin] decryption successful: messageId=${msg.messageId}, length=${decrypted.length}`);
|
|
859
|
+
msg.data = Buffer.from(decrypted).toString("base64");
|
|
860
|
+
msg.category = "PLAIN_TEXT";
|
|
861
|
+
} catch (err) {
|
|
862
|
+
log.error(`[mixin] decryption exception for ${msg.messageId}`, err);
|
|
863
|
+
markProcessed(msg.messageId);
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const isTextMessage = msg.category.startsWith("PLAIN_TEXT") || msg.category.startsWith("PLAIN_POST");
|
|
869
|
+
const isAttachmentMessage = msg.category === "PLAIN_DATA" || msg.category === "PLAIN_AUDIO";
|
|
870
|
+
|
|
871
|
+
if (!isTextMessage && !isAttachmentMessage) {
|
|
872
|
+
log.info(`[mixin] skip non-text message: ${msg.category}`);
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
let text = decodeContent(msg.category, msg.data).trim();
|
|
877
|
+
let mediaPayload: AgentMediaPayload | undefined;
|
|
878
|
+
if (isAttachmentMessage) {
|
|
879
|
+
const resolved = await resolveInboundAttachment({ rt, config, msg, log });
|
|
880
|
+
text = resolved.text.trim();
|
|
881
|
+
mediaPayload = resolved.mediaPayload;
|
|
882
|
+
}
|
|
883
|
+
log.info(`[mixin] decoded text: messageId=${msg.messageId}, category=${msg.category}, length=${text.length}`);
|
|
884
|
+
|
|
885
|
+
if (!text) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const conversationPolicy = isDirect
|
|
890
|
+
? null
|
|
891
|
+
: resolveConversationPolicy(cfg, accountId, msg.conversationId);
|
|
892
|
+
|
|
893
|
+
if (
|
|
894
|
+
!isDirect &&
|
|
895
|
+
conversationPolicy &&
|
|
896
|
+
!(isAttachmentMessage && conversationPolicy.mediaBypassMention) &&
|
|
897
|
+
!shouldPassGroupFilter({
|
|
898
|
+
...config,
|
|
899
|
+
requireMentionInGroup: conversationPolicy.requireMention,
|
|
900
|
+
}, text)
|
|
901
|
+
) {
|
|
902
|
+
log.info(`[mixin] group message filtered: ${msg.messageId}`);
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const effectiveAllowFrom = await readEffectiveAllowFrom(rt, accountId, config.allowFrom, log);
|
|
907
|
+
const normalizedUserId = normalizeAllowEntry(msg.userId);
|
|
908
|
+
const dmPolicy = config.dmPolicy ?? "pairing";
|
|
909
|
+
const groupAccess = isDirect
|
|
910
|
+
? null
|
|
911
|
+
: evaluateMixinGroupAccess({
|
|
912
|
+
cfg,
|
|
913
|
+
config,
|
|
914
|
+
accountId,
|
|
915
|
+
conversationId: msg.conversationId,
|
|
916
|
+
senderId: msg.userId,
|
|
917
|
+
});
|
|
918
|
+
const groupPairingAuthorized = isDirect
|
|
919
|
+
? false
|
|
920
|
+
: effectiveAllowFrom.has(normalizeAllowEntry(buildGroupScopedPairingId({
|
|
921
|
+
conversationId: msg.conversationId,
|
|
922
|
+
})));
|
|
923
|
+
const isAuthorized = isDirect
|
|
924
|
+
? dmPolicy === "open" || effectiveAllowFrom.has(normalizedUserId)
|
|
925
|
+
: groupAccess?.allowed === true || groupPairingAuthorized;
|
|
926
|
+
|
|
927
|
+
if (!isAuthorized) {
|
|
928
|
+
if (!isDirect && isMixinGroupAuthCommand(text)) {
|
|
929
|
+
const now = Date.now();
|
|
930
|
+
const lastNotified = unauthNotifiedGroups.get(msg.conversationId) ?? 0;
|
|
931
|
+
const shouldNotify = lastNotified === 0 || now - lastNotified > UNAUTH_NOTIFY_INTERVAL;
|
|
932
|
+
if (!shouldNotify) {
|
|
933
|
+
markProcessed(msg.messageId);
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
pruneUnauthNotifiedGroups(now);
|
|
937
|
+
unauthNotifiedGroups.set(msg.conversationId, now);
|
|
938
|
+
const { code, created } = await rt.channel.pairing.upsertPairingRequest({
|
|
939
|
+
channel: "mixin",
|
|
940
|
+
id: buildGroupScopedPairingId({
|
|
941
|
+
conversationId: msg.conversationId,
|
|
942
|
+
}),
|
|
943
|
+
accountId,
|
|
944
|
+
meta: buildGroupScopedPairingMeta({
|
|
945
|
+
conversationId: msg.conversationId,
|
|
946
|
+
userId: msg.userId,
|
|
947
|
+
}),
|
|
948
|
+
});
|
|
949
|
+
markProcessed(msg.messageId);
|
|
950
|
+
await sendTextMessage(
|
|
951
|
+
cfg,
|
|
952
|
+
accountId,
|
|
953
|
+
msg.conversationId,
|
|
954
|
+
undefined,
|
|
955
|
+
formatMixinGroupAuthReply({
|
|
956
|
+
code,
|
|
957
|
+
created,
|
|
958
|
+
accountId,
|
|
959
|
+
conversationId: msg.conversationId,
|
|
960
|
+
}),
|
|
961
|
+
log,
|
|
962
|
+
);
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
if (isDirect) {
|
|
966
|
+
log.warn(`[mixin] user ${msg.userId} not authorized (dmPolicy=${dmPolicy})`);
|
|
967
|
+
} else {
|
|
968
|
+
log.warn(
|
|
969
|
+
`[mixin] group sender ${msg.userId} blocked: conversationId=${msg.conversationId}, groupPolicy=${groupAccess?.groupPolicy ?? "unknown"}, reason=${groupAccess?.reason ?? "unknown"}`,
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
markProcessed(msg.messageId);
|
|
973
|
+
if (isDirect) {
|
|
974
|
+
await handleUnauthorizedDirectMessage({ rt, cfg, accountId, config, msg, log });
|
|
975
|
+
}
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
markProcessed(msg.messageId);
|
|
980
|
+
|
|
981
|
+
if (isOutboxCommand(text)) {
|
|
982
|
+
if (isOutboxPurgeInvalidCommand(text)) {
|
|
983
|
+
const result = await purgePermanentInvalidOutboxEntries();
|
|
984
|
+
const recipientId = isDirect ? msg.userId : undefined;
|
|
985
|
+
const replyText = result.removed > 0
|
|
986
|
+
? `Removed ${result.removed} invalid outbox entr${result.removed === 1 ? "y" : "ies"}.\n${result.removedJobIds.map((jobId) => `- ${jobId}`).join("\n")}`
|
|
987
|
+
: "No invalid outbox entries found.";
|
|
988
|
+
await sendTextMessage(cfg, accountId, msg.conversationId, recipientId, replyText, log);
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const status = await getOutboxStatus();
|
|
993
|
+
const replyText = formatOutboxStatus(status);
|
|
994
|
+
const recipientId = isDirect ? msg.userId : undefined;
|
|
995
|
+
await sendTextMessage(cfg, accountId, msg.conversationId, recipientId, replyText, log);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
if (isMixinGroupAuthCommand(text)) {
|
|
1000
|
+
const recipientId = isDirect ? msg.userId : undefined;
|
|
1001
|
+
if (isDirect) {
|
|
1002
|
+
await sendTextMessage(cfg, accountId, msg.conversationId, recipientId, "Use /mixin-group-auth in a group chat.", log);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
const { code, created } = await rt.channel.pairing.upsertPairingRequest({
|
|
1006
|
+
channel: "mixin",
|
|
1007
|
+
id: buildGroupScopedPairingId({
|
|
1008
|
+
conversationId: msg.conversationId,
|
|
1009
|
+
}),
|
|
1010
|
+
accountId,
|
|
1011
|
+
meta: buildGroupScopedPairingMeta({
|
|
1012
|
+
conversationId: msg.conversationId,
|
|
1013
|
+
userId: msg.userId,
|
|
1014
|
+
}),
|
|
1015
|
+
});
|
|
1016
|
+
await sendTextMessage(
|
|
1017
|
+
cfg,
|
|
1018
|
+
accountId,
|
|
1019
|
+
msg.conversationId,
|
|
1020
|
+
recipientId,
|
|
1021
|
+
formatMixinGroupAuthReply({
|
|
1022
|
+
code,
|
|
1023
|
+
created,
|
|
1024
|
+
accountId,
|
|
1025
|
+
conversationId: msg.conversationId,
|
|
1026
|
+
}),
|
|
1027
|
+
log,
|
|
1028
|
+
);
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
if (isMixinGroupApproveCommand(text)) {
|
|
1033
|
+
const code = parseMixinGroupApproveCode(text);
|
|
1034
|
+
const recipientId = isDirect ? msg.userId : undefined;
|
|
1035
|
+
if (!code) {
|
|
1036
|
+
await sendTextMessage(cfg, accountId, msg.conversationId, recipientId, "Usage: openclaw pairing approve mixin <code>", log);
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
await sendTextMessage(
|
|
1040
|
+
cfg,
|
|
1041
|
+
accountId,
|
|
1042
|
+
msg.conversationId,
|
|
1043
|
+
recipientId,
|
|
1044
|
+
[
|
|
1045
|
+
"Group auth approval must be done in the OpenClaw terminal.",
|
|
1046
|
+
"",
|
|
1047
|
+
accountId === "default"
|
|
1048
|
+
? `Run: openclaw pairing approve mixin ${code}`
|
|
1049
|
+
: `Run: openclaw pairing approve --account ${accountId} mixin ${code}`,
|
|
1050
|
+
].join("\n"),
|
|
1051
|
+
log,
|
|
1052
|
+
);
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
if (isCollectStatusCommand(text)) {
|
|
1057
|
+
const orderId = parseCollectStatusCommand(text);
|
|
1058
|
+
if (orderId) {
|
|
1059
|
+
await refreshMixpayOrderStatus({ cfg, accountId, orderId });
|
|
1060
|
+
}
|
|
1061
|
+
const replyText = orderId ? await getMixpayOrderStatusText(orderId) : "Usage: /collect status <orderId>";
|
|
1062
|
+
const recipientId = isDirect ? msg.userId : undefined;
|
|
1063
|
+
await sendTextMessage(cfg, accountId, msg.conversationId, recipientId, replyText, log);
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
if (isCollectRecentCommand(text)) {
|
|
1068
|
+
const recipientId = isDirect ? msg.userId : undefined;
|
|
1069
|
+
const replyText = await getRecentMixpayOrdersText({
|
|
1070
|
+
accountId,
|
|
1071
|
+
conversationId: msg.conversationId,
|
|
1072
|
+
limit: parseCollectRecentLimit(text),
|
|
1073
|
+
});
|
|
1074
|
+
await sendTextMessage(cfg, accountId, msg.conversationId, recipientId, replyText, log);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const peerId = isDirect ? msg.userId : msg.conversationId;
|
|
1079
|
+
log.info(`[mixin] resolving route: channel=mixin, accountId=${accountId}, peer.kind=${isDirect ? "direct" : "group"}, peer.id=${peerId}`);
|
|
1080
|
+
|
|
1081
|
+
const route = rt.channel.routing.resolveAgentRoute({
|
|
1082
|
+
cfg,
|
|
1083
|
+
channel: "mixin",
|
|
1084
|
+
accountId,
|
|
1085
|
+
peer: {
|
|
1086
|
+
kind: isDirect ? "direct" : "group",
|
|
1087
|
+
id: peerId,
|
|
1088
|
+
},
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
log.info(`[mixin] route result: ${route ? "FOUND" : "NULL"} - agentId=${route?.agentId ?? "N/A"}`);
|
|
1092
|
+
|
|
1093
|
+
if (!route) {
|
|
1094
|
+
log.warn(`[mixin] no agent route for ${msg.userId} (peerId: ${peerId})`);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
const shouldComputeCommandAuthorized = rt.channel.commands.shouldComputeCommandAuthorized(text, cfg);
|
|
1099
|
+
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
|
1100
|
+
const senderAllowedForCommands = useAccessGroups
|
|
1101
|
+
? isDirect
|
|
1102
|
+
? effectiveAllowFrom.has(normalizedUserId)
|
|
1103
|
+
: groupAccess?.allowed === true || groupPairingAuthorized
|
|
1104
|
+
: true;
|
|
1105
|
+
|
|
1106
|
+
const commandAuthorized = shouldComputeCommandAuthorized
|
|
1107
|
+
? rt.channel.commands.resolveCommandAuthorizedFromAuthorizers({
|
|
1108
|
+
useAccessGroups,
|
|
1109
|
+
authorizers: [
|
|
1110
|
+
{
|
|
1111
|
+
configured: isDirect ? effectiveAllowFrom.size > 0 : (groupAccess?.groupAllowFrom.length ?? 0) > 0,
|
|
1112
|
+
allowed: senderAllowedForCommands,
|
|
1113
|
+
},
|
|
1114
|
+
],
|
|
1115
|
+
})
|
|
1116
|
+
: undefined;
|
|
1117
|
+
|
|
1118
|
+
const senderName = await resolveSenderName({
|
|
1119
|
+
accountId,
|
|
1120
|
+
config,
|
|
1121
|
+
userId: msg.userId,
|
|
1122
|
+
log,
|
|
1123
|
+
});
|
|
1124
|
+
const botName = await resolveBotName({
|
|
1125
|
+
accountId,
|
|
1126
|
+
config,
|
|
1127
|
+
log,
|
|
1128
|
+
});
|
|
1129
|
+
const groupName = isDirect
|
|
1130
|
+
? ""
|
|
1131
|
+
: await resolveGroupName({
|
|
1132
|
+
accountId,
|
|
1133
|
+
config,
|
|
1134
|
+
conversationId: msg.conversationId,
|
|
1135
|
+
log,
|
|
1136
|
+
});
|
|
1137
|
+
const conversationLabel = isDirect
|
|
1138
|
+
? clampSessionLabel(`${botName}-${senderName || msg.userId}`)
|
|
1139
|
+
: clampSessionLabel(`${botName}-${groupName || msg.conversationId}`);
|
|
1140
|
+
|
|
1141
|
+
const ctx = rt.channel.reply.finalizeInboundContext({
|
|
1142
|
+
Body: text,
|
|
1143
|
+
RawBody: text,
|
|
1144
|
+
CommandBody: text,
|
|
1145
|
+
From: isDirect ? msg.userId : msg.conversationId,
|
|
1146
|
+
SenderId: msg.userId,
|
|
1147
|
+
SenderName: senderName,
|
|
1148
|
+
SessionKey: route.sessionKey,
|
|
1149
|
+
AccountId: accountId,
|
|
1150
|
+
ChatType: isDirect ? "direct" : "group",
|
|
1151
|
+
ConversationLabel: conversationLabel,
|
|
1152
|
+
GroupSubject: isDirect ? undefined : groupName || msg.conversationId,
|
|
1153
|
+
Provider: "mixin",
|
|
1154
|
+
Surface: "mixin",
|
|
1155
|
+
MessageSid: msg.messageId,
|
|
1156
|
+
CommandAuthorized: commandAuthorized,
|
|
1157
|
+
OriginatingChannel: "mixin",
|
|
1158
|
+
OriginatingTo: isDirect ? msg.userId : msg.conversationId,
|
|
1159
|
+
...mediaPayload,
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
const storePath = rt.channel.session.resolveStorePath(cfg.session?.store, {
|
|
1163
|
+
agentId: route.agentId,
|
|
1164
|
+
});
|
|
1165
|
+
await rt.channel.session.recordInboundSession({
|
|
1166
|
+
storePath,
|
|
1167
|
+
sessionKey: route.sessionKey,
|
|
1168
|
+
ctx,
|
|
1169
|
+
onRecordError: (err: unknown) => {
|
|
1170
|
+
log.error("[mixin] session record error", err);
|
|
1171
|
+
},
|
|
1172
|
+
});
|
|
1173
|
+
await updateSessionPresentation({
|
|
1174
|
+
storePath,
|
|
1175
|
+
sessionKey: route.sessionKey,
|
|
1176
|
+
label: conversationLabel,
|
|
1177
|
+
displayName: conversationLabel,
|
|
1178
|
+
log,
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
log.info(`[mixin] dispatching ${msg.messageId} from ${msg.userId}`);
|
|
1182
|
+
|
|
1183
|
+
await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
1184
|
+
ctx,
|
|
1185
|
+
cfg,
|
|
1186
|
+
dispatcherOptions: {
|
|
1187
|
+
deliver: async (payload) => {
|
|
1188
|
+
const replyText = payload.text ?? "";
|
|
1189
|
+
if (!replyText) {
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
const recipientId = isDirect ? msg.userId : undefined;
|
|
1193
|
+
await deliverMixinReply({
|
|
1194
|
+
cfg,
|
|
1195
|
+
accountId,
|
|
1196
|
+
conversationId: msg.conversationId,
|
|
1197
|
+
recipientId,
|
|
1198
|
+
creatorId: msg.userId,
|
|
1199
|
+
text: replyText,
|
|
1200
|
+
log,
|
|
1201
|
+
});
|
|
1202
|
+
},
|
|
1203
|
+
},
|
|
1204
|
+
});
|
|
1205
|
+
}
|