@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/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,5 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
+
import { uniqueConversationID } from "@mixin.dev/mixin-node-sdk";
|
|
3
4
|
import {
|
|
4
5
|
buildChannelConfigSchema,
|
|
5
6
|
createDefaultChannelRuntimeState,
|
|
@@ -8,11 +9,14 @@ import {
|
|
|
8
9
|
} from "openclaw/plugin-sdk";
|
|
9
10
|
import type { ChannelGatewayContext, OpenClawConfig, ReplyPayload } from "openclaw/plugin-sdk";
|
|
10
11
|
import { runBlazeLoop } from "./blaze-service.js";
|
|
12
|
+
import { buildClient, sleep } from "./shared.js";
|
|
11
13
|
import { MixinConfigSchema } from "./config-schema.js";
|
|
12
14
|
import { describeAccount, isConfigured, listAccountIds, resolveAccount, resolveDefaultAccountId, resolveMediaMaxMb } from "./config.js";
|
|
15
|
+
import type { MixinAccountConfig } from "./config-schema.js";
|
|
13
16
|
import { handleMixinMessage, type MixinInboundMessage } from "./inbound-handler.js";
|
|
17
|
+
import { getMixpayStatusSnapshot, startMixpayWorker } from "./mixpay-worker.js";
|
|
14
18
|
import { buildMixinOutboundPlanFromReplyPayload, executeMixinOutboundPlan } from "./outbound-plan.js";
|
|
15
|
-
import { getMixinRuntime } from "./runtime.js";
|
|
19
|
+
import { getMixinRuntime, setMixinBlazeSender } from "./runtime.js";
|
|
16
20
|
import { getOutboxStatus, sendAudioMessage, sendFileMessage, sendTextMessage, startSendWorker } from "./send-service.js";
|
|
17
21
|
import { buildMixinAccountSnapshot, buildMixinChannelSummary, resolveMixinStatusSnapshot } from "./status.js";
|
|
18
22
|
|
|
@@ -23,10 +27,12 @@ const MAX_DELAY = 3000;
|
|
|
23
27
|
const MULTIPLIER = 1.5;
|
|
24
28
|
const MEDIA_MAX_BYTES = 30 * 1024 * 1024;
|
|
25
29
|
const execFileAsync = promisify(execFile);
|
|
30
|
+
const CONVERSATION_CATEGORY_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
const conversationCategoryCache = new Map<string, {
|
|
33
|
+
category: "CONTACT" | "GROUP";
|
|
34
|
+
expiresAt: number;
|
|
35
|
+
}>();
|
|
30
36
|
|
|
31
37
|
function maskKey(key: string): string {
|
|
32
38
|
if (!key || key.length < 8) {
|
|
@@ -35,6 +41,65 @@ function maskKey(key: string): string {
|
|
|
35
41
|
return key.slice(0, 4) + "****" + key.slice(-4);
|
|
36
42
|
}
|
|
37
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
|
+
|
|
38
103
|
async function resolveAudioDurationSeconds(filePath: string): Promise<number | null> {
|
|
39
104
|
try {
|
|
40
105
|
const { stdout } = await execFileAsync(
|
|
@@ -279,7 +344,9 @@ export const mixinPlugin = {
|
|
|
279
344
|
|
|
280
345
|
await startSendWorker(cfg, log);
|
|
281
346
|
const outboxStatus = await getOutboxStatus().catch(() => null);
|
|
282
|
-
|
|
347
|
+
await startMixpayWorker(cfg, log);
|
|
348
|
+
const mixpayStatus = await getMixpayStatusSnapshot().catch(() => null);
|
|
349
|
+
const statusSnapshot = resolveMixinStatusSnapshot(cfg, accountId, outboxStatus, mixpayStatus);
|
|
283
350
|
ctx.setStatus({
|
|
284
351
|
accountId,
|
|
285
352
|
...statusSnapshot,
|
|
@@ -288,6 +355,7 @@ export const mixinPlugin = {
|
|
|
288
355
|
let stopped = false;
|
|
289
356
|
const stop = () => {
|
|
290
357
|
stopped = true;
|
|
358
|
+
setMixinBlazeSender(accountId, null);
|
|
291
359
|
};
|
|
292
360
|
abortSignal?.addEventListener("abort", stop);
|
|
293
361
|
|
|
@@ -305,6 +373,9 @@ export const mixinPlugin = {
|
|
|
305
373
|
options: { parse: false, syncAck: true },
|
|
306
374
|
log,
|
|
307
375
|
abortSignal,
|
|
376
|
+
onSenderReady: (sender) => {
|
|
377
|
+
setMixinBlazeSender(accountId, sender);
|
|
378
|
+
},
|
|
308
379
|
handler: {
|
|
309
380
|
onMessage: async (rawMsg: any) => {
|
|
310
381
|
if (stopped) {
|
|
@@ -317,9 +388,15 @@ export const mixinPlugin = {
|
|
|
317
388
|
return;
|
|
318
389
|
}
|
|
319
390
|
|
|
320
|
-
const isDirect =
|
|
321
|
-
|
|
322
|
-
:
|
|
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
|
+
);
|
|
323
400
|
|
|
324
401
|
const msg: MixinInboundMessage = {
|
|
325
402
|
conversationId: rawMsg.conversation_id ?? "",
|
package/src/config-schema.ts
CHANGED
|
@@ -32,6 +32,21 @@ export const MixinConversationConfigSchema = z.object({
|
|
|
32
32
|
|
|
33
33
|
export type MixinConversationConfig = z.infer<typeof MixinConversationConfigSchema>;
|
|
34
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
|
+
|
|
35
50
|
export const MixinAccountConfigSchema = z.object({
|
|
36
51
|
name: z.string().optional(),
|
|
37
52
|
enabled: z.boolean().optional().default(true),
|
|
@@ -49,6 +64,7 @@ export const MixinAccountConfigSchema = z.object({
|
|
|
49
64
|
audioAutoDetectDuration: z.boolean().optional().default(true),
|
|
50
65
|
audioSendAsVoiceByDefault: z.boolean().optional().default(true),
|
|
51
66
|
audioRequireFfprobe: z.boolean().optional().default(false),
|
|
67
|
+
mixpay: MixinMixpayConfigSchema.optional(),
|
|
52
68
|
conversations: z.record(z.string(), MixinConversationConfigSchema.optional()).optional(),
|
|
53
69
|
debug: z.boolean().optional().default(false),
|
|
54
70
|
proxy: MixinProxyConfigSchema.optional(),
|
package/src/config.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
MixinConversationConfigSchema,
|
|
5
5
|
type MixinAccountConfig,
|
|
6
6
|
type MixinConversationConfig,
|
|
7
|
+
type MixinMixpayConfig,
|
|
7
8
|
} from "./config-schema.js";
|
|
8
9
|
|
|
9
10
|
type RawMixinConfig = Partial<MixinAccountConfig> & {
|
|
@@ -87,6 +88,10 @@ export function resolveMediaMaxMb(cfg: OpenClawConfig, accountId?: string): numb
|
|
|
87
88
|
return getAccountConfig(cfg, accountId).mediaMaxMb;
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
export function getMixpayConfig(cfg: OpenClawConfig, accountId?: string): MixinMixpayConfig | undefined {
|
|
92
|
+
return getAccountConfig(cfg, accountId).mixpay;
|
|
93
|
+
}
|
|
94
|
+
|
|
90
95
|
function getRawAccountConfig(cfg: OpenClawConfig, accountId?: string): Partial<MixinAccountConfig> {
|
|
91
96
|
const raw = getRawConfig(cfg);
|
|
92
97
|
const resolvedAccountId = accountId ?? resolveDefaultAccountId(cfg);
|
package/src/crypto.ts
CHANGED
|
@@ -12,6 +12,11 @@ function aes256CbcDecrypt(key: Buffer, iv: Buffer, ciphertext: Buffer): Buffer {
|
|
|
12
12
|
const final = Buffer.concat([decrypted, decipher.final()]);
|
|
13
13
|
const padLen = final[final.length - 1];
|
|
14
14
|
if (padLen > 0 && padLen <= 16) {
|
|
15
|
+
for (let i = final.length - padLen; i < final.length; i++) {
|
|
16
|
+
if (final[i] !== padLen) {
|
|
17
|
+
return final;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
15
20
|
return final.slice(0, final.length - padLen);
|
|
16
21
|
}
|
|
17
22
|
return final;
|