@invago/mixin 1.0.8 → 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/package.json CHANGED
@@ -1 +1,79 @@
1
- {"name":"@invago/mixin","version":"1.0.8","description":"Mixin Messenger channel plugin for OpenClaw","type":"module","main":"index.ts","scripts":{"dev":"nodemon --exec \"node --import jiti/register index.ts\" --ext ts","lint":"eslint src/**/*.ts index.ts","typecheck":"tsc --noEmit"},"peerDependencies":{"openclaw":">=2026.2.0"},"dependencies":{"@mixin.dev/mixin-node-sdk":"^7.4.1","@noble/curves":"^2.0.1","@noble/hashes":"^2.0.1","axios":"^1.6.0","express":"^5.2.1","proxy-agent":"^6.5.0","ws":"^8.18.3","zod":"^4.3.6"},"devDependencies":{"@eslint/js":"^10.0.1","@types/node":"^20.0.0","eslint":"^10.0.3","globals":"^17.4.0","jiti":"^1.21.0","nodemon":"^3.0.0","typescript":"^5.3.0","typescript-eslint":"^8.56.1"},"keywords":["openclaw","mixin","messenger","plugin","channel"],"author":"invagao","license":"MIT","repository":{"type":"git","url":"git+https://github.com/invago/mixinclaw.git"},"openclaw":{"extensions":["./index.ts"],"channel":{"id":"mixin","label":"Mixin Messenger","selectionLabel":"Mixin Messenger (Blaze WebSocket)","docsPath":"/channels/mixin","order":70,"aliases":["mixin-messenger","mixin"],"quickstartAllowFrom":true},"install":{"npmSpec":"@invago/mixin","localPath":"extensions/mixin"}}}
1
+ {
2
+ "name": "@invago/mixin",
3
+ "version": "1.0.10",
4
+ "description": "Mixin Messenger channel plugin for OpenClaw",
5
+ "type": "module",
6
+ "main": "index.ts",
7
+ "scripts": {
8
+ "dev": "nodemon --exec \"node --import jiti/register index.ts\" --ext ts",
9
+ "lint": "eslint src/**/*.ts index.ts",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "peerDependencies": {
13
+ "openclaw": ">=2026.2.0"
14
+ },
15
+ "dependencies": {
16
+ "@mixin.dev/mixin-node-sdk": "^7.4.1",
17
+ "@noble/curves": "^2.0.1",
18
+ "@noble/hashes": "^2.0.1",
19
+ "axios": "^1.13.6",
20
+ "express": "^5.2.1",
21
+ "jiti": "^1.21.0",
22
+ "proxy-agent": "^6.5.0",
23
+ "ws": "^8.18.3",
24
+ "zod": "^4.3.6"
25
+ },
26
+ "devDependencies": {
27
+ "@eslint/js": "^10.0.1",
28
+ "@types/node": "^20.0.0",
29
+ "eslint": "^10.0.3",
30
+ "globals": "^17.4.0",
31
+ "nodemon": "^3.0.0",
32
+ "typescript": "^5.3.0",
33
+ "typescript-eslint": "^8.56.1"
34
+ },
35
+ "keywords": [
36
+ "openclaw",
37
+ "mixin",
38
+ "messenger",
39
+ "plugin",
40
+ "channel"
41
+ ],
42
+ "author": "invagao",
43
+ "license": "MIT",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/invago/mixinclaw.git"
47
+ },
48
+ "openclaw": {
49
+ "extensions": [
50
+ "./index.ts"
51
+ ],
52
+ "channel": {
53
+ "id": "mixin",
54
+ "label": "Mixin Messenger",
55
+ "selectionLabel": "Mixin Messenger (Blaze WebSocket)",
56
+ "docsPath": "/channels/mixin",
57
+ "order": 70,
58
+ "aliases": [
59
+ "mixin-messenger",
60
+ "mixin"
61
+ ],
62
+ "quickstartAllowFrom": true
63
+ },
64
+ "install": {
65
+ "npmSpec": "@invago/mixin",
66
+ "localPath": "extensions/mixin"
67
+ }
68
+ },
69
+ "files": [
70
+ "README.md",
71
+ "README.zh-CN.md",
72
+ "index.ts",
73
+ "openclaw.plugin.json",
74
+ "package.json",
75
+ "src/",
76
+ "tsconfig.json",
77
+ "eslint.config.mjs"
78
+ ]
79
+ }
@@ -9,12 +9,8 @@ import WebSocket from "ws";
9
9
  import crypto from "crypto";
10
10
  import type { MixinAccountConfig } from "./config-schema.js";
11
11
  import { createProxyAgent } from "./proxy.js";
12
-
13
- type SendLog = {
14
- info: (msg: string) => void;
15
- error: (msg: string, err?: unknown) => void;
16
- warn: (msg: string) => void;
17
- };
12
+ import type { MixinBlazeOutboundMessage } from "./runtime.js";
13
+ import type { SendLog } from "./shared.js";
18
14
 
19
15
  function buildKeystore(config: MixinAccountConfig) {
20
16
  return {
@@ -50,8 +46,9 @@ export async function runBlazeLoop(params: {
50
46
  handler: BlazeHandler;
51
47
  log: SendLog;
52
48
  abortSignal?: AbortSignal;
49
+ onSenderReady?: ((sender: ((message: MixinBlazeOutboundMessage) => Promise<void>) | null) => void) | undefined;
53
50
  }): Promise<void> {
54
- const { config, options, handler, log, abortSignal } = params;
51
+ const { config, options, handler, log, abortSignal, onSenderReady } = params;
55
52
  const keystore = buildKeystore(config);
56
53
  const jwtToken = signAccessToken("GET", "/", "", crypto.randomUUID(), keystore) || "";
57
54
  const agent = createProxyAgent(config.proxy);
@@ -67,6 +64,7 @@ export async function runBlazeLoop(params: {
67
64
  clearTimeout(pingTimeout);
68
65
  pingTimeout = null;
69
66
  }
67
+ onSenderReady?.(null);
70
68
  abortSignal?.removeEventListener("abort", onAbort);
71
69
  };
72
70
 
@@ -118,6 +116,25 @@ export async function runBlazeLoop(params: {
118
116
  ws.on("open", () => {
119
117
  opened = true;
120
118
  heartbeat();
119
+ onSenderReady?.(async (message) => {
120
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
121
+ throw new Error("blaze sender unavailable: socket not open");
122
+ }
123
+ const ok = await sendRaw(ws, {
124
+ id: crypto.randomUUID(),
125
+ action: "CREATE_MESSAGE",
126
+ params: {
127
+ conversation_id: message.conversationId,
128
+ status: "SENT",
129
+ message_id: message.messageId,
130
+ category: message.category,
131
+ data: message.dataBase64,
132
+ },
133
+ });
134
+ if (!ok) {
135
+ throw new Error("blaze sender timeout");
136
+ }
137
+ });
121
138
  void sendRaw(ws!, {
122
139
  id: crypto.randomUUID(),
123
140
  action: "LIST_PENDING_MESSAGES",
package/src/channel.ts CHANGED
@@ -1,13 +1,24 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
- import { buildChannelConfigSchema, formatPairingApproveHint } from "openclaw/plugin-sdk";
3
+ import { uniqueConversationID } from "@mixin.dev/mixin-node-sdk";
4
+ import {
5
+ buildChannelConfigSchema,
6
+ createDefaultChannelRuntimeState,
7
+ formatPairingApproveHint,
8
+ resolveChannelMediaMaxBytes,
9
+ } from "openclaw/plugin-sdk";
4
10
  import type { ChannelGatewayContext, OpenClawConfig, ReplyPayload } from "openclaw/plugin-sdk";
5
11
  import { runBlazeLoop } from "./blaze-service.js";
12
+ import { buildClient, sleep } from "./shared.js";
6
13
  import { MixinConfigSchema } from "./config-schema.js";
7
- import { describeAccount, isConfigured, listAccountIds, resolveAccount } from "./config.js";
14
+ import { describeAccount, isConfigured, listAccountIds, resolveAccount, resolveDefaultAccountId, resolveMediaMaxMb } from "./config.js";
15
+ import type { MixinAccountConfig } from "./config-schema.js";
8
16
  import { handleMixinMessage, type MixinInboundMessage } from "./inbound-handler.js";
9
- import { getMixinRuntime } from "./runtime.js";
10
- import { sendAudioMessage, sendFileMessage, sendTextMessage, startSendWorker } from "./send-service.js";
17
+ import { getMixpayStatusSnapshot, startMixpayWorker } from "./mixpay-worker.js";
18
+ import { buildMixinOutboundPlanFromReplyPayload, executeMixinOutboundPlan } from "./outbound-plan.js";
19
+ import { getMixinRuntime, setMixinBlazeSender } from "./runtime.js";
20
+ import { getOutboxStatus, sendAudioMessage, sendFileMessage, sendTextMessage, startSendWorker } from "./send-service.js";
21
+ import { buildMixinAccountSnapshot, buildMixinChannelSummary, resolveMixinStatusSnapshot } from "./status.js";
11
22
 
12
23
  type ResolvedMixinAccount = ReturnType<typeof resolveAccount>;
13
24
 
@@ -16,10 +27,12 @@ const MAX_DELAY = 3000;
16
27
  const MULTIPLIER = 1.5;
17
28
  const MEDIA_MAX_BYTES = 30 * 1024 * 1024;
18
29
  const execFileAsync = promisify(execFile);
30
+ const CONVERSATION_CATEGORY_CACHE_TTL_MS = 5 * 60 * 1000;
19
31
 
20
- async function sleep(ms: number): Promise<void> {
21
- return new Promise((resolve) => setTimeout(resolve, ms));
22
- }
32
+ const conversationCategoryCache = new Map<string, {
33
+ category: "CONTACT" | "GROUP";
34
+ expiresAt: number;
35
+ }>();
23
36
 
24
37
  function maskKey(key: string): string {
25
38
  if (!key || key.length < 8) {
@@ -28,6 +41,65 @@ function maskKey(key: string): string {
28
41
  return key.slice(0, 4) + "****" + key.slice(-4);
29
42
  }
30
43
 
44
+ async function resolveIsDirectMessage(params: {
45
+ config: MixinAccountConfig;
46
+ conversationId?: string;
47
+ userId?: string;
48
+ log: {
49
+ info: (m: string) => void;
50
+ warn: (m: string) => void;
51
+ };
52
+ }): Promise<boolean> {
53
+ const conversationId = params.conversationId?.trim();
54
+ if (!conversationId) {
55
+ return true;
56
+ }
57
+
58
+ const cached = conversationCategoryCache.get(conversationId);
59
+ if (cached && cached.expiresAt > Date.now()) {
60
+ params.log.info(`[mixin] conversation category resolved from cache: conversationId=${conversationId}, category=${cached.category}`);
61
+ return cached.category !== "GROUP";
62
+ }
63
+
64
+ const now = Date.now();
65
+ for (const [key, entry] of conversationCategoryCache) {
66
+ if (entry.expiresAt <= now) {
67
+ conversationCategoryCache.delete(key);
68
+ }
69
+ }
70
+
71
+ try {
72
+ const client = buildClient(params.config);
73
+ const conversation = await client.conversation.fetch(conversationId);
74
+ const category = conversation.category === "GROUP" ? "GROUP" : "CONTACT";
75
+ conversationCategoryCache.set(conversationId, {
76
+ category,
77
+ expiresAt: Date.now() + CONVERSATION_CATEGORY_CACHE_TTL_MS,
78
+ });
79
+ params.log.info(`[mixin] conversation category resolved: conversationId=${conversationId}, category=${category}`);
80
+ return category !== "GROUP";
81
+ } catch (err) {
82
+ const userId = params.userId?.trim();
83
+ if (userId && params.config.appId) {
84
+ const directConversationId = uniqueConversationID(params.config.appId, userId);
85
+ if (directConversationId === conversationId) {
86
+ params.log.info(
87
+ `[mixin] conversation category inferred locally: conversationId=${conversationId}, category=CONTACT`,
88
+ );
89
+ conversationCategoryCache.set(conversationId, {
90
+ category: "CONTACT",
91
+ expiresAt: Date.now() + CONVERSATION_CATEGORY_CACHE_TTL_MS,
92
+ });
93
+ return true;
94
+ }
95
+ }
96
+ params.log.warn(
97
+ `[mixin] failed to resolve conversation category: conversationId=${conversationId}, error=${err instanceof Error ? err.message : String(err)}`,
98
+ );
99
+ return false;
100
+ }
101
+ }
102
+
31
103
  async function resolveAudioDurationSeconds(filePath: string): Promise<number | null> {
32
104
  try {
33
105
  const { stdout } = await execFileAsync(
@@ -53,11 +125,12 @@ async function resolveAudioDurationSeconds(filePath: string): Promise<number | n
53
125
  }
54
126
  }
55
127
 
56
- function resolvePayloadMediaUrls(payload: ReplyPayload): string[] {
57
- if (payload.mediaUrls && payload.mediaUrls.length > 0) {
58
- return payload.mediaUrls;
59
- }
60
- return payload.mediaUrl ? [payload.mediaUrl] : [];
128
+ function resolveMixinMediaMaxBytes(cfg: OpenClawConfig, accountId?: string | null): number {
129
+ return resolveChannelMediaMaxBytes({
130
+ cfg,
131
+ resolveChannelLimitMb: ({ cfg, accountId }) => resolveMediaMaxMb(cfg, accountId),
132
+ accountId,
133
+ }) ?? MEDIA_MAX_BYTES;
61
134
  }
62
135
 
63
136
  async function deliverOutboundMixinPayload(params: {
@@ -68,33 +141,28 @@ async function deliverOutboundMixinPayload(params: {
68
141
  mediaLocalRoots?: readonly string[];
69
142
  accountId?: string | null;
70
143
  }): Promise<{ channel: "mixin"; messageId: string }> {
71
- const accountId = params.accountId ?? "default";
72
- let lastMessageId = params.to;
73
-
74
- if (params.text?.trim()) {
75
- const textResult = await sendTextMessage(params.cfg, accountId, params.to, undefined, params.text);
76
- if (!textResult.ok) {
77
- throw new Error(textResult.error ?? "mixin outbound text send failed");
78
- }
79
- lastMessageId = textResult.messageId ?? lastMessageId;
80
- }
81
-
144
+ const accountId = params.accountId ?? resolveDefaultAccountId(params.cfg);
145
+ const account = resolveAccount(params.cfg, accountId);
146
+ const mediaMaxBytes = resolveMixinMediaMaxBytes(params.cfg, accountId);
82
147
  const runtime = getMixinRuntime();
83
- for (const mediaUrl of params.mediaUrls ?? []) {
148
+
149
+ const sendMediaUrl = async (mediaUrl: string): Promise<string | undefined> => {
84
150
  const loaded = await runtime.media.loadWebMedia(mediaUrl, {
85
- maxBytes: MEDIA_MAX_BYTES,
151
+ maxBytes: mediaMaxBytes,
86
152
  localRoots: params.mediaLocalRoots,
87
153
  });
88
154
  const saved = await runtime.channel.media.saveMediaBuffer(
89
155
  loaded.buffer,
90
156
  loaded.contentType,
91
157
  "mixin",
92
- MEDIA_MAX_BYTES,
158
+ mediaMaxBytes,
93
159
  loaded.fileName,
94
160
  );
95
161
 
96
- if (loaded.kind === "audio") {
97
- const duration = await resolveAudioDurationSeconds(saved.path);
162
+ if (loaded.kind === "audio" && account.config.audioSendAsVoiceByDefault !== false) {
163
+ const duration = account.config.audioAutoDetectDuration === false
164
+ ? null
165
+ : await resolveAudioDurationSeconds(saved.path);
98
166
  if (duration !== null) {
99
167
  const audioResult = await sendAudioMessage(
100
168
  params.cfg,
@@ -110,8 +178,10 @@ async function deliverOutboundMixinPayload(params: {
110
178
  if (!audioResult.ok) {
111
179
  throw new Error(audioResult.error ?? "mixin outbound audio send failed");
112
180
  }
113
- lastMessageId = audioResult.messageId ?? lastMessageId;
114
- continue;
181
+ return audioResult.messageId;
182
+ }
183
+ if (account.config.audioRequireFfprobe) {
184
+ throw new Error("ffprobe is required to send mediaUrl audio as Mixin voice");
115
185
  }
116
186
  }
117
187
 
@@ -129,10 +199,27 @@ async function deliverOutboundMixinPayload(params: {
129
199
  if (!fileResult.ok) {
130
200
  throw new Error(fileResult.error ?? "mixin outbound file send failed");
131
201
  }
132
- lastMessageId = fileResult.messageId ?? lastMessageId;
202
+ return fileResult.messageId;
203
+ };
204
+
205
+ const payloadPlan = buildMixinOutboundPlanFromReplyPayload({
206
+ text: params.text,
207
+ mediaUrl: params.mediaUrls?.[0],
208
+ mediaUrls: params.mediaUrls,
209
+ } as ReplyPayload);
210
+ for (const warning of payloadPlan.warnings) {
211
+ console.warn(`[mixin] outbound plan warning: ${warning}`);
133
212
  }
134
213
 
135
- return { channel: "mixin", messageId: lastMessageId };
214
+ const lastMessageId = await executeMixinOutboundPlan({
215
+ cfg: params.cfg,
216
+ accountId,
217
+ conversationId: params.to,
218
+ steps: payloadPlan.steps,
219
+ sendMediaUrl,
220
+ });
221
+
222
+ return { channel: "mixin", messageId: lastMessageId ?? params.to };
136
223
  }
137
224
 
138
225
  export const mixinPlugin = {
@@ -162,7 +249,7 @@ export const mixinPlugin = {
162
249
  listAccountIds,
163
250
  resolveAccount: (cfg: OpenClawConfig, accountId?: string | null) =>
164
251
  resolveAccount(cfg, accountId ?? undefined),
165
- defaultAccountId: () => "default",
252
+ defaultAccountId: (cfg: OpenClawConfig) => resolveDefaultAccountId(cfg),
166
253
  },
167
254
 
168
255
  pairing: {
@@ -192,6 +279,7 @@ export const mixinPlugin = {
192
279
 
193
280
  outbound: {
194
281
  deliveryMode: "direct" as const,
282
+ textChunkLimit: 4000,
195
283
  sendPayload: async (ctx: {
196
284
  cfg: OpenClawConfig;
197
285
  to: string;
@@ -203,7 +291,11 @@ export const mixinPlugin = {
203
291
  cfg: ctx.cfg,
204
292
  to: ctx.to,
205
293
  text: ctx.payload.text,
206
- mediaUrls: resolvePayloadMediaUrls(ctx.payload),
294
+ mediaUrls: ctx.payload.mediaUrls && ctx.payload.mediaUrls.length > 0
295
+ ? ctx.payload.mediaUrls
296
+ : ctx.payload.mediaUrl
297
+ ? [ctx.payload.mediaUrl]
298
+ : [],
207
299
  mediaLocalRoots: ctx.mediaLocalRoots,
208
300
  accountId: ctx.accountId,
209
301
  }),
@@ -214,7 +306,7 @@ export const mixinPlugin = {
214
306
  text: string;
215
307
  accountId?: string | null;
216
308
  }) => {
217
- const id = ctx.accountId ?? "default";
309
+ const id = ctx.accountId ?? resolveDefaultAccountId(ctx.cfg);
218
310
  const result = await sendTextMessage(ctx.cfg, id, ctx.to, undefined, ctx.text);
219
311
  if (result.ok) {
220
312
  return { channel: "mixin", messageId: result.messageId ?? ctx.to };
@@ -251,10 +343,19 @@ export const mixinPlugin = {
251
343
  const config = account.config;
252
344
 
253
345
  await startSendWorker(cfg, log);
346
+ const outboxStatus = await getOutboxStatus().catch(() => null);
347
+ await startMixpayWorker(cfg, log);
348
+ const mixpayStatus = await getMixpayStatusSnapshot().catch(() => null);
349
+ const statusSnapshot = resolveMixinStatusSnapshot(cfg, accountId, outboxStatus, mixpayStatus);
350
+ ctx.setStatus({
351
+ accountId,
352
+ ...statusSnapshot,
353
+ });
254
354
 
255
355
  let stopped = false;
256
356
  const stop = () => {
257
357
  stopped = true;
358
+ setMixinBlazeSender(accountId, null);
258
359
  };
259
360
  abortSignal?.addEventListener("abort", stop);
260
361
 
@@ -272,6 +373,9 @@ export const mixinPlugin = {
272
373
  options: { parse: false, syncAck: true },
273
374
  log,
274
375
  abortSignal,
376
+ onSenderReady: (sender) => {
377
+ setMixinBlazeSender(accountId, sender);
378
+ },
275
379
  handler: {
276
380
  onMessage: async (rawMsg: any) => {
277
381
  if (stopped) {
@@ -284,9 +388,15 @@ export const mixinPlugin = {
284
388
  return;
285
389
  }
286
390
 
287
- const isDirect = rawMsg.conversation_id === undefined
288
- ? true
289
- : !rawMsg.representative_id;
391
+ const isDirect = await resolveIsDirectMessage({
392
+ config,
393
+ conversationId: rawMsg.conversation_id,
394
+ userId: rawMsg.user_id,
395
+ log,
396
+ });
397
+ log.info(
398
+ `[mixin] inbound route context: messageId=${rawMsg.message_id}, conversationId=${rawMsg.conversation_id ?? ""}, userId=${rawMsg.user_id}, isDirect=${isDirect}`,
399
+ );
290
400
 
291
401
  const msg: MixinInboundMessage = {
292
402
  conversationId: rawMsg.conversation_id ?? "",
@@ -340,10 +450,43 @@ export const mixinPlugin = {
340
450
  },
341
451
 
342
452
  status: {
343
- defaultRuntime: {
344
- accountId: "default",
345
- running: false,
346
- status: "stopped" as const,
453
+ defaultRuntime: createDefaultChannelRuntimeState("default"),
454
+ buildChannelSummary: (params: {
455
+ snapshot: {
456
+ configured?: boolean | null;
457
+ running?: boolean | null;
458
+ lastStartAt?: number | null;
459
+ lastStopAt?: number | null;
460
+ lastError?: string | null;
461
+ defaultAccountId?: string | null;
462
+ outboxDir?: string | null;
463
+ outboxFile?: string | null;
464
+ outboxPending?: number | null;
465
+ mediaMaxMb?: number | null;
466
+ };
467
+ }) => buildMixinChannelSummary({ snapshot: params.snapshot }),
468
+ buildAccountSnapshot: (params: {
469
+ account: ResolvedMixinAccount;
470
+ runtime?: {
471
+ running?: boolean | null;
472
+ lastStartAt?: number | null;
473
+ lastStopAt?: number | null;
474
+ lastError?: string | null;
475
+ lastInboundAt?: number | null;
476
+ lastOutboundAt?: number | null;
477
+ } | null;
478
+ probe?: unknown;
479
+ cfg: OpenClawConfig;
480
+ }) => {
481
+ const { account, runtime, probe, cfg } = params;
482
+ const statusSnapshot = resolveMixinStatusSnapshot(cfg, account.accountId);
483
+ return buildMixinAccountSnapshot({
484
+ account,
485
+ runtime,
486
+ probe,
487
+ defaultAccountId: statusSnapshot.defaultAccountId,
488
+ outboxPending: statusSnapshot.outboxPending,
489
+ });
347
490
  },
348
491
  },
349
492
  };
@@ -1,4 +1,4 @@
1
- import { DmPolicySchema } from "openclaw/plugin-sdk";
1
+ import { DmPolicySchema, GroupPolicySchema } from "openclaw/plugin-sdk";
2
2
  import { z } from "zod";
3
3
 
4
4
  export const MixinProxyConfigSchema = z.object({
@@ -22,6 +22,31 @@ export const MixinProxyConfigSchema = z.object({
22
22
 
23
23
  export type MixinProxyConfig = z.infer<typeof MixinProxyConfigSchema>;
24
24
 
25
+ export const MixinConversationConfigSchema = z.object({
26
+ enabled: z.boolean().optional(),
27
+ requireMention: z.boolean().optional(),
28
+ allowFrom: z.array(z.string()).optional(),
29
+ mediaBypassMention: z.boolean().optional(),
30
+ groupPolicy: GroupPolicySchema.optional(),
31
+ });
32
+
33
+ export type MixinConversationConfig = z.infer<typeof MixinConversationConfigSchema>;
34
+
35
+ export const MixinMixpayConfigSchema = z.object({
36
+ enabled: z.boolean().optional().default(false),
37
+ apiBaseUrl: z.string().optional(),
38
+ payeeId: z.string().optional(),
39
+ defaultQuoteAssetId: z.string().optional(),
40
+ defaultSettlementAssetId: z.string().optional(),
41
+ expireMinutes: z.number().positive().optional().default(15),
42
+ pollIntervalSec: z.number().positive().optional().default(30),
43
+ allowedCreators: z.array(z.string()).optional().default([]),
44
+ notifyOnPending: z.boolean().optional().default(false),
45
+ notifyOnPaidLess: z.boolean().optional().default(true),
46
+ });
47
+
48
+ export type MixinMixpayConfig = z.infer<typeof MixinMixpayConfigSchema>;
49
+
25
50
  export const MixinAccountConfigSchema = z.object({
26
51
  name: z.string().optional(),
27
52
  enabled: z.boolean().optional().default(true),
@@ -31,7 +56,16 @@ export const MixinAccountConfigSchema = z.object({
31
56
  sessionPrivateKey: z.string().optional(),
32
57
  dmPolicy: DmPolicySchema.optional().default("pairing"),
33
58
  allowFrom: z.array(z.string()).optional().default([]),
59
+ groupPolicy: GroupPolicySchema.optional(),
60
+ groupAllowFrom: z.array(z.string()).optional(),
34
61
  requireMentionInGroup: z.boolean().optional().default(true),
62
+ mediaBypassMentionInGroup: z.boolean().optional().default(true),
63
+ mediaMaxMb: z.number().positive().optional(),
64
+ audioAutoDetectDuration: z.boolean().optional().default(true),
65
+ audioSendAsVoiceByDefault: z.boolean().optional().default(true),
66
+ audioRequireFfprobe: z.boolean().optional().default(false),
67
+ mixpay: MixinMixpayConfigSchema.optional(),
68
+ conversations: z.record(z.string(), MixinConversationConfigSchema.optional()).optional(),
35
69
  debug: z.boolean().optional().default(false),
36
70
  proxy: MixinProxyConfigSchema.optional(),
37
71
  });
@@ -39,6 +73,7 @@ export const MixinAccountConfigSchema = z.object({
39
73
  export type MixinAccountConfig = z.infer<typeof MixinAccountConfigSchema>;
40
74
 
41
75
  export const MixinConfigSchema: z.ZodTypeAny = MixinAccountConfigSchema.extend({
76
+ defaultAccount: z.string().optional(),
42
77
  accounts: z.record(z.string(), MixinAccountConfigSchema.optional()).optional(),
43
78
  });
44
79