@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/README.md +328 -4
- package/README.zh-CN.md +386 -69
- package/package.json +79 -1
- package/src/blaze-service.ts +24 -7
- package/src/channel.ts +185 -42
- package/src/config-schema.ts +36 -1
- package/src/config.ts +103 -10
- package/src/crypto.ts +5 -0
- package/src/inbound-handler.ts +1205 -576
- 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 +216 -0
- package/src/reply-format.ts +89 -24
- package/src/runtime.ts +26 -0
- package/src/send-service.ts +35 -27
- package/src/shared.ts +25 -0
- package/src/status.ts +114 -0
- package/src/decrypt.ts +0 -126
package/package.json
CHANGED
|
@@ -1 +1,79 @@
|
|
|
1
|
-
{
|
|
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
|
+
}
|
package/src/blaze-service.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
10
|
-
import {
|
|
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
|
-
|
|
21
|
-
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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 ??
|
|
72
|
-
|
|
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
|
-
|
|
148
|
+
|
|
149
|
+
const sendMediaUrl = async (mediaUrl: string): Promise<string | undefined> => {
|
|
84
150
|
const loaded = await runtime.media.loadWebMedia(mediaUrl, {
|
|
85
|
-
maxBytes:
|
|
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
|
-
|
|
158
|
+
mediaMaxBytes,
|
|
93
159
|
loaded.fileName,
|
|
94
160
|
);
|
|
95
161
|
|
|
96
|
-
if (loaded.kind === "audio") {
|
|
97
|
-
const duration =
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: () =>
|
|
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:
|
|
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 ??
|
|
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 =
|
|
288
|
-
|
|
289
|
-
:
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
};
|
package/src/config-schema.ts
CHANGED
|
@@ -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
|
|