@invago/mixin 1.0.7 → 1.0.9
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 +148 -3
- package/README.zh-CN.md +148 -3
- package/package.json +1 -1
- package/src/channel.ts +233 -15
- package/src/config-schema.ts +21 -0
- package/src/config.ts +99 -10
- package/src/inbound-handler.ts +479 -178
- package/src/outbound-plan.ts +197 -0
- package/src/reply-format.ts +90 -23
- package/src/send-service.ts +233 -14
- package/src/status.ts +100 -0
- package/tools/mixin-plugin-onboard/README.md +98 -0
- package/tools/mixin-plugin-onboard/bin/mixin-plugin-onboard.mjs +3 -0
- package/tools/mixin-plugin-onboard/src/commands/doctor.ts +28 -0
- package/tools/mixin-plugin-onboard/src/commands/info.ts +23 -0
- package/tools/mixin-plugin-onboard/src/commands/install.ts +5 -0
- package/tools/mixin-plugin-onboard/src/commands/update.ts +5 -0
- package/tools/mixin-plugin-onboard/src/index.ts +49 -0
- package/tools/mixin-plugin-onboard/src/utils.ts +189 -0
package/src/channel.ts
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import {
|
|
4
|
+
buildChannelConfigSchema,
|
|
5
|
+
createDefaultChannelRuntimeState,
|
|
6
|
+
formatPairingApproveHint,
|
|
7
|
+
resolveChannelMediaMaxBytes,
|
|
8
|
+
} from "openclaw/plugin-sdk";
|
|
9
|
+
import type { ChannelGatewayContext, OpenClawConfig, ReplyPayload } from "openclaw/plugin-sdk";
|
|
3
10
|
import { runBlazeLoop } from "./blaze-service.js";
|
|
4
11
|
import { MixinConfigSchema } from "./config-schema.js";
|
|
5
|
-
import { describeAccount, isConfigured, listAccountIds, resolveAccount } from "./config.js";
|
|
12
|
+
import { describeAccount, isConfigured, listAccountIds, resolveAccount, resolveDefaultAccountId, resolveMediaMaxMb } from "./config.js";
|
|
6
13
|
import { handleMixinMessage, type MixinInboundMessage } from "./inbound-handler.js";
|
|
7
|
-
import {
|
|
14
|
+
import { buildMixinOutboundPlanFromReplyPayload, executeMixinOutboundPlan } from "./outbound-plan.js";
|
|
15
|
+
import { getMixinRuntime } from "./runtime.js";
|
|
16
|
+
import { getOutboxStatus, sendAudioMessage, sendFileMessage, sendTextMessage, startSendWorker } from "./send-service.js";
|
|
17
|
+
import { buildMixinAccountSnapshot, buildMixinChannelSummary, resolveMixinStatusSnapshot } from "./status.js";
|
|
8
18
|
|
|
9
19
|
type ResolvedMixinAccount = ReturnType<typeof resolveAccount>;
|
|
10
20
|
|
|
11
21
|
const BASE_DELAY = 1000;
|
|
12
22
|
const MAX_DELAY = 3000;
|
|
13
23
|
const MULTIPLIER = 1.5;
|
|
24
|
+
const MEDIA_MAX_BYTES = 30 * 1024 * 1024;
|
|
25
|
+
const execFileAsync = promisify(execFile);
|
|
14
26
|
|
|
15
27
|
async function sleep(ms: number): Promise<void> {
|
|
16
28
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -23,6 +35,128 @@ function maskKey(key: string): string {
|
|
|
23
35
|
return key.slice(0, 4) + "****" + key.slice(-4);
|
|
24
36
|
}
|
|
25
37
|
|
|
38
|
+
async function resolveAudioDurationSeconds(filePath: string): Promise<number | null> {
|
|
39
|
+
try {
|
|
40
|
+
const { stdout } = await execFileAsync(
|
|
41
|
+
process.platform === "win32" ? "ffprobe.exe" : "ffprobe",
|
|
42
|
+
[
|
|
43
|
+
"-v",
|
|
44
|
+
"error",
|
|
45
|
+
"-show_entries",
|
|
46
|
+
"format=duration",
|
|
47
|
+
"-of",
|
|
48
|
+
"default=noprint_wrappers=1:nokey=1",
|
|
49
|
+
filePath,
|
|
50
|
+
],
|
|
51
|
+
{ timeout: 15_000, windowsHide: true },
|
|
52
|
+
);
|
|
53
|
+
const seconds = Number.parseFloat(stdout.trim());
|
|
54
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return Math.max(1, Math.ceil(seconds));
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveMixinMediaMaxBytes(cfg: OpenClawConfig, accountId?: string | null): number {
|
|
64
|
+
return resolveChannelMediaMaxBytes({
|
|
65
|
+
cfg,
|
|
66
|
+
resolveChannelLimitMb: ({ cfg, accountId }) => resolveMediaMaxMb(cfg, accountId),
|
|
67
|
+
accountId,
|
|
68
|
+
}) ?? MEDIA_MAX_BYTES;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function deliverOutboundMixinPayload(params: {
|
|
72
|
+
cfg: OpenClawConfig;
|
|
73
|
+
to: string;
|
|
74
|
+
text?: string;
|
|
75
|
+
mediaUrls?: string[];
|
|
76
|
+
mediaLocalRoots?: readonly string[];
|
|
77
|
+
accountId?: string | null;
|
|
78
|
+
}): Promise<{ channel: "mixin"; messageId: string }> {
|
|
79
|
+
const accountId = params.accountId ?? resolveDefaultAccountId(params.cfg);
|
|
80
|
+
const account = resolveAccount(params.cfg, accountId);
|
|
81
|
+
const mediaMaxBytes = resolveMixinMediaMaxBytes(params.cfg, accountId);
|
|
82
|
+
const runtime = getMixinRuntime();
|
|
83
|
+
|
|
84
|
+
const sendMediaUrl = async (mediaUrl: string): Promise<string | undefined> => {
|
|
85
|
+
const loaded = await runtime.media.loadWebMedia(mediaUrl, {
|
|
86
|
+
maxBytes: mediaMaxBytes,
|
|
87
|
+
localRoots: params.mediaLocalRoots,
|
|
88
|
+
});
|
|
89
|
+
const saved = await runtime.channel.media.saveMediaBuffer(
|
|
90
|
+
loaded.buffer,
|
|
91
|
+
loaded.contentType,
|
|
92
|
+
"mixin",
|
|
93
|
+
mediaMaxBytes,
|
|
94
|
+
loaded.fileName,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (loaded.kind === "audio" && account.config.audioSendAsVoiceByDefault !== false) {
|
|
98
|
+
const duration = account.config.audioAutoDetectDuration === false
|
|
99
|
+
? null
|
|
100
|
+
: await resolveAudioDurationSeconds(saved.path);
|
|
101
|
+
if (duration !== null) {
|
|
102
|
+
const audioResult = await sendAudioMessage(
|
|
103
|
+
params.cfg,
|
|
104
|
+
accountId,
|
|
105
|
+
params.to,
|
|
106
|
+
undefined,
|
|
107
|
+
{
|
|
108
|
+
filePath: saved.path,
|
|
109
|
+
mimeType: saved.contentType ?? loaded.contentType,
|
|
110
|
+
duration,
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
if (!audioResult.ok) {
|
|
114
|
+
throw new Error(audioResult.error ?? "mixin outbound audio send failed");
|
|
115
|
+
}
|
|
116
|
+
return audioResult.messageId;
|
|
117
|
+
}
|
|
118
|
+
if (account.config.audioRequireFfprobe) {
|
|
119
|
+
throw new Error("ffprobe is required to send mediaUrl audio as Mixin voice");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const fileResult = await sendFileMessage(
|
|
124
|
+
params.cfg,
|
|
125
|
+
accountId,
|
|
126
|
+
params.to,
|
|
127
|
+
undefined,
|
|
128
|
+
{
|
|
129
|
+
filePath: saved.path,
|
|
130
|
+
fileName: loaded.fileName,
|
|
131
|
+
mimeType: saved.contentType ?? loaded.contentType,
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
if (!fileResult.ok) {
|
|
135
|
+
throw new Error(fileResult.error ?? "mixin outbound file send failed");
|
|
136
|
+
}
|
|
137
|
+
return fileResult.messageId;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const payloadPlan = buildMixinOutboundPlanFromReplyPayload({
|
|
141
|
+
text: params.text,
|
|
142
|
+
mediaUrl: params.mediaUrls?.[0],
|
|
143
|
+
mediaUrls: params.mediaUrls,
|
|
144
|
+
} as ReplyPayload);
|
|
145
|
+
for (const warning of payloadPlan.warnings) {
|
|
146
|
+
console.warn(`[mixin] outbound plan warning: ${warning}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const lastMessageId = await executeMixinOutboundPlan({
|
|
150
|
+
cfg: params.cfg,
|
|
151
|
+
accountId,
|
|
152
|
+
conversationId: params.to,
|
|
153
|
+
steps: payloadPlan.steps,
|
|
154
|
+
sendMediaUrl,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return { channel: "mixin", messageId: lastMessageId ?? params.to };
|
|
158
|
+
}
|
|
159
|
+
|
|
26
160
|
export const mixinPlugin = {
|
|
27
161
|
id: "mixin",
|
|
28
162
|
|
|
@@ -41,7 +175,7 @@ export const mixinPlugin = {
|
|
|
41
175
|
chatTypes: ["direct", "group"] as Array<"direct" | "group">,
|
|
42
176
|
reactions: false,
|
|
43
177
|
threads: false,
|
|
44
|
-
media:
|
|
178
|
+
media: true,
|
|
45
179
|
nativeCommands: false,
|
|
46
180
|
blockStreaming: false,
|
|
47
181
|
},
|
|
@@ -50,27 +184,56 @@ export const mixinPlugin = {
|
|
|
50
184
|
listAccountIds,
|
|
51
185
|
resolveAccount: (cfg: OpenClawConfig, accountId?: string | null) =>
|
|
52
186
|
resolveAccount(cfg, accountId ?? undefined),
|
|
53
|
-
defaultAccountId: () =>
|
|
187
|
+
defaultAccountId: (cfg: OpenClawConfig) => resolveDefaultAccountId(cfg),
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
pairing: {
|
|
191
|
+
idLabel: "Mixin UUID",
|
|
192
|
+
normalizeAllowEntry: (entry: string) => entry.trim().toLowerCase(),
|
|
54
193
|
},
|
|
55
194
|
|
|
56
195
|
security: {
|
|
57
196
|
resolveDmPolicy: ({ account, accountId }: { account: ResolvedMixinAccount; accountId?: string | null }) => {
|
|
58
197
|
const allowFrom = account.config.allowFrom ?? [];
|
|
59
198
|
const basePath = accountId && accountId !== "default" ? `.accounts.${accountId}` : "";
|
|
199
|
+
const policy = account.config.dmPolicy ?? "pairing";
|
|
60
200
|
|
|
61
201
|
return {
|
|
62
|
-
policy
|
|
202
|
+
policy,
|
|
63
203
|
allowFrom,
|
|
204
|
+
policyPath: `channels.mixin${basePath}.dmPolicy`,
|
|
64
205
|
allowFromPath: `channels.mixin${basePath}.allowFrom`,
|
|
65
|
-
approveHint:
|
|
66
|
-
?
|
|
67
|
-
:
|
|
206
|
+
approveHint: policy === "pairing"
|
|
207
|
+
? formatPairingApproveHint("mixin")
|
|
208
|
+
: allowFrom.length > 0
|
|
209
|
+
? `已配置白名单用户数 ${allowFrom.length},将用户的 Mixin UUID 添加到 allowFrom 列表即可授权`
|
|
210
|
+
: "将用户的 Mixin UUID 添加到 allowFrom 列表即可授权",
|
|
68
211
|
};
|
|
69
212
|
},
|
|
70
213
|
},
|
|
71
214
|
|
|
72
215
|
outbound: {
|
|
73
216
|
deliveryMode: "direct" as const,
|
|
217
|
+
textChunkLimit: 4000,
|
|
218
|
+
sendPayload: async (ctx: {
|
|
219
|
+
cfg: OpenClawConfig;
|
|
220
|
+
to: string;
|
|
221
|
+
payload: ReplyPayload;
|
|
222
|
+
mediaLocalRoots?: readonly string[];
|
|
223
|
+
accountId?: string | null;
|
|
224
|
+
}) =>
|
|
225
|
+
deliverOutboundMixinPayload({
|
|
226
|
+
cfg: ctx.cfg,
|
|
227
|
+
to: ctx.to,
|
|
228
|
+
text: ctx.payload.text,
|
|
229
|
+
mediaUrls: ctx.payload.mediaUrls && ctx.payload.mediaUrls.length > 0
|
|
230
|
+
? ctx.payload.mediaUrls
|
|
231
|
+
: ctx.payload.mediaUrl
|
|
232
|
+
? [ctx.payload.mediaUrl]
|
|
233
|
+
: [],
|
|
234
|
+
mediaLocalRoots: ctx.mediaLocalRoots,
|
|
235
|
+
accountId: ctx.accountId,
|
|
236
|
+
}),
|
|
74
237
|
|
|
75
238
|
sendText: async (ctx: {
|
|
76
239
|
cfg: OpenClawConfig;
|
|
@@ -78,13 +241,29 @@ export const mixinPlugin = {
|
|
|
78
241
|
text: string;
|
|
79
242
|
accountId?: string | null;
|
|
80
243
|
}) => {
|
|
81
|
-
const id = ctx.accountId ??
|
|
244
|
+
const id = ctx.accountId ?? resolveDefaultAccountId(ctx.cfg);
|
|
82
245
|
const result = await sendTextMessage(ctx.cfg, id, ctx.to, undefined, ctx.text);
|
|
83
246
|
if (result.ok) {
|
|
84
247
|
return { channel: "mixin", messageId: result.messageId ?? ctx.to };
|
|
85
248
|
}
|
|
86
249
|
throw new Error(result.error ?? "sendText failed");
|
|
87
250
|
},
|
|
251
|
+
sendMedia: async (ctx: {
|
|
252
|
+
cfg: OpenClawConfig;
|
|
253
|
+
to: string;
|
|
254
|
+
text: string;
|
|
255
|
+
mediaUrl?: string;
|
|
256
|
+
mediaLocalRoots?: readonly string[];
|
|
257
|
+
accountId?: string | null;
|
|
258
|
+
}) =>
|
|
259
|
+
deliverOutboundMixinPayload({
|
|
260
|
+
cfg: ctx.cfg,
|
|
261
|
+
to: ctx.to,
|
|
262
|
+
text: ctx.text,
|
|
263
|
+
mediaUrls: ctx.mediaUrl ? [ctx.mediaUrl] : [],
|
|
264
|
+
mediaLocalRoots: ctx.mediaLocalRoots,
|
|
265
|
+
accountId: ctx.accountId,
|
|
266
|
+
}),
|
|
88
267
|
},
|
|
89
268
|
|
|
90
269
|
gateway: {
|
|
@@ -99,6 +278,12 @@ export const mixinPlugin = {
|
|
|
99
278
|
const config = account.config;
|
|
100
279
|
|
|
101
280
|
await startSendWorker(cfg, log);
|
|
281
|
+
const outboxStatus = await getOutboxStatus().catch(() => null);
|
|
282
|
+
const statusSnapshot = resolveMixinStatusSnapshot(cfg, accountId, outboxStatus);
|
|
283
|
+
ctx.setStatus({
|
|
284
|
+
accountId,
|
|
285
|
+
...statusSnapshot,
|
|
286
|
+
});
|
|
102
287
|
|
|
103
288
|
let stopped = false;
|
|
104
289
|
const stop = () => {
|
|
@@ -188,10 +373,43 @@ export const mixinPlugin = {
|
|
|
188
373
|
},
|
|
189
374
|
|
|
190
375
|
status: {
|
|
191
|
-
defaultRuntime:
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
376
|
+
defaultRuntime: createDefaultChannelRuntimeState("default"),
|
|
377
|
+
buildChannelSummary: (params: {
|
|
378
|
+
snapshot: {
|
|
379
|
+
configured?: boolean | null;
|
|
380
|
+
running?: boolean | null;
|
|
381
|
+
lastStartAt?: number | null;
|
|
382
|
+
lastStopAt?: number | null;
|
|
383
|
+
lastError?: string | null;
|
|
384
|
+
defaultAccountId?: string | null;
|
|
385
|
+
outboxDir?: string | null;
|
|
386
|
+
outboxFile?: string | null;
|
|
387
|
+
outboxPending?: number | null;
|
|
388
|
+
mediaMaxMb?: number | null;
|
|
389
|
+
};
|
|
390
|
+
}) => buildMixinChannelSummary({ snapshot: params.snapshot }),
|
|
391
|
+
buildAccountSnapshot: (params: {
|
|
392
|
+
account: ResolvedMixinAccount;
|
|
393
|
+
runtime?: {
|
|
394
|
+
running?: boolean | null;
|
|
395
|
+
lastStartAt?: number | null;
|
|
396
|
+
lastStopAt?: number | null;
|
|
397
|
+
lastError?: string | null;
|
|
398
|
+
lastInboundAt?: number | null;
|
|
399
|
+
lastOutboundAt?: number | null;
|
|
400
|
+
} | null;
|
|
401
|
+
probe?: unknown;
|
|
402
|
+
cfg: OpenClawConfig;
|
|
403
|
+
}) => {
|
|
404
|
+
const { account, runtime, probe, cfg } = params;
|
|
405
|
+
const statusSnapshot = resolveMixinStatusSnapshot(cfg, account.accountId);
|
|
406
|
+
return buildMixinAccountSnapshot({
|
|
407
|
+
account,
|
|
408
|
+
runtime,
|
|
409
|
+
probe,
|
|
410
|
+
defaultAccountId: statusSnapshot.defaultAccountId,
|
|
411
|
+
outboxPending: statusSnapshot.outboxPending,
|
|
412
|
+
});
|
|
195
413
|
},
|
|
196
414
|
},
|
|
197
415
|
};
|
package/src/config-schema.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DmPolicySchema, GroupPolicySchema } from "openclaw/plugin-sdk";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
|
|
3
4
|
export const MixinProxyConfigSchema = z.object({
|
|
@@ -21,6 +22,16 @@ export const MixinProxyConfigSchema = z.object({
|
|
|
21
22
|
|
|
22
23
|
export type MixinProxyConfig = z.infer<typeof MixinProxyConfigSchema>;
|
|
23
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
|
+
|
|
24
35
|
export const MixinAccountConfigSchema = z.object({
|
|
25
36
|
name: z.string().optional(),
|
|
26
37
|
enabled: z.boolean().optional().default(true),
|
|
@@ -28,8 +39,17 @@ export const MixinAccountConfigSchema = z.object({
|
|
|
28
39
|
sessionId: z.string().optional(),
|
|
29
40
|
serverPublicKey: z.string().optional(),
|
|
30
41
|
sessionPrivateKey: z.string().optional(),
|
|
42
|
+
dmPolicy: DmPolicySchema.optional().default("pairing"),
|
|
31
43
|
allowFrom: z.array(z.string()).optional().default([]),
|
|
44
|
+
groupPolicy: GroupPolicySchema.optional(),
|
|
45
|
+
groupAllowFrom: z.array(z.string()).optional(),
|
|
32
46
|
requireMentionInGroup: z.boolean().optional().default(true),
|
|
47
|
+
mediaBypassMentionInGroup: z.boolean().optional().default(true),
|
|
48
|
+
mediaMaxMb: z.number().positive().optional(),
|
|
49
|
+
audioAutoDetectDuration: z.boolean().optional().default(true),
|
|
50
|
+
audioSendAsVoiceByDefault: z.boolean().optional().default(true),
|
|
51
|
+
audioRequireFfprobe: z.boolean().optional().default(false),
|
|
52
|
+
conversations: z.record(z.string(), MixinConversationConfigSchema.optional()).optional(),
|
|
33
53
|
debug: z.boolean().optional().default(false),
|
|
34
54
|
proxy: MixinProxyConfigSchema.optional(),
|
|
35
55
|
});
|
|
@@ -37,6 +57,7 @@ export const MixinAccountConfigSchema = z.object({
|
|
|
37
57
|
export type MixinAccountConfig = z.infer<typeof MixinAccountConfigSchema>;
|
|
38
58
|
|
|
39
59
|
export const MixinConfigSchema: z.ZodTypeAny = MixinAccountConfigSchema.extend({
|
|
60
|
+
defaultAccount: z.string().optional(),
|
|
40
61
|
accounts: z.record(z.string(), MixinAccountConfigSchema.optional()).optional(),
|
|
41
62
|
});
|
|
42
63
|
|
package/src/config.ts
CHANGED
|
@@ -1,26 +1,60 @@
|
|
|
1
1
|
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
MixinAccountConfigSchema,
|
|
4
|
+
MixinConversationConfigSchema,
|
|
5
|
+
type MixinAccountConfig,
|
|
6
|
+
type MixinConversationConfig,
|
|
7
|
+
} from "./config-schema.js";
|
|
3
8
|
|
|
4
|
-
|
|
5
|
-
|
|
9
|
+
type RawMixinConfig = Partial<MixinAccountConfig> & {
|
|
10
|
+
defaultAccount?: string;
|
|
11
|
+
accounts?: Record<string, Partial<MixinAccountConfig> | undefined>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function getRawConfig(cfg: OpenClawConfig): RawMixinConfig {
|
|
15
|
+
return ((cfg.channels as Record<string, unknown>)?.mixin ?? {}) as RawMixinConfig;
|
|
6
16
|
}
|
|
7
17
|
|
|
8
|
-
|
|
18
|
+
function hasTopLevelAccountConfig(raw: RawMixinConfig): boolean {
|
|
19
|
+
return Boolean(raw.appId || raw.sessionId || raw.serverPublicKey || raw.sessionPrivateKey || raw.name);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveDefaultAccountId(cfg: OpenClawConfig): string {
|
|
9
23
|
const raw = getRawConfig(cfg);
|
|
24
|
+
const configuredDefault = raw.defaultAccount?.trim();
|
|
25
|
+
if (configuredDefault && raw.accounts?.[configuredDefault]) {
|
|
26
|
+
return configuredDefault;
|
|
27
|
+
}
|
|
28
|
+
if (configuredDefault === "default") {
|
|
29
|
+
return "default";
|
|
30
|
+
}
|
|
10
31
|
if (raw.accounts && Object.keys(raw.accounts).length > 0) {
|
|
11
|
-
|
|
32
|
+
if (hasTopLevelAccountConfig(raw)) {
|
|
33
|
+
return "default";
|
|
34
|
+
}
|
|
35
|
+
return Object.keys(raw.accounts)[0] ?? "default";
|
|
12
36
|
}
|
|
13
|
-
return
|
|
37
|
+
return "default";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function listAccountIds(cfg: OpenClawConfig): string[] {
|
|
41
|
+
const raw = getRawConfig(cfg);
|
|
42
|
+
const accountIds = raw.accounts ? Object.keys(raw.accounts) : [];
|
|
43
|
+
if (hasTopLevelAccountConfig(raw) || accountIds.length === 0) {
|
|
44
|
+
return ["default", ...accountIds.filter((accountId) => accountId !== "default")];
|
|
45
|
+
}
|
|
46
|
+
return accountIds;
|
|
14
47
|
}
|
|
15
48
|
|
|
16
49
|
export function getAccountConfig(cfg: OpenClawConfig, accountId?: string): MixinAccountConfig {
|
|
17
50
|
const raw = getRawConfig(cfg);
|
|
51
|
+
const resolvedAccountId = accountId ?? resolveDefaultAccountId(cfg);
|
|
18
52
|
let accountRaw: Partial<MixinAccountConfig>;
|
|
19
53
|
|
|
20
|
-
if (
|
|
21
|
-
accountRaw = raw.accounts[
|
|
54
|
+
if (resolvedAccountId !== "default" && raw.accounts?.[resolvedAccountId]) {
|
|
55
|
+
accountRaw = raw.accounts[resolvedAccountId] as Partial<MixinAccountConfig>;
|
|
22
56
|
} else {
|
|
23
|
-
accountRaw = raw
|
|
57
|
+
accountRaw = raw;
|
|
24
58
|
}
|
|
25
59
|
|
|
26
60
|
const result = MixinAccountConfigSchema.safeParse(accountRaw);
|
|
@@ -29,7 +63,7 @@ export function getAccountConfig(cfg: OpenClawConfig, accountId?: string): Mixin
|
|
|
29
63
|
}
|
|
30
64
|
|
|
31
65
|
export function resolveAccount(cfg: OpenClawConfig, accountId?: string) {
|
|
32
|
-
const id = accountId ??
|
|
66
|
+
const id = accountId ?? resolveDefaultAccountId(cfg);
|
|
33
67
|
const config = getAccountConfig(cfg, id);
|
|
34
68
|
const configured = Boolean(config.appId && config.sessionId && config.serverPublicKey && config.sessionPrivateKey);
|
|
35
69
|
return {
|
|
@@ -41,6 +75,7 @@ export function resolveAccount(cfg: OpenClawConfig, accountId?: string) {
|
|
|
41
75
|
sessionId: config.sessionId,
|
|
42
76
|
serverPublicKey: config.serverPublicKey,
|
|
43
77
|
sessionPrivateKey: config.sessionPrivateKey,
|
|
78
|
+
dmPolicy: config.dmPolicy,
|
|
44
79
|
allowFrom: config.allowFrom,
|
|
45
80
|
requireMentionInGroup: config.requireMentionInGroup,
|
|
46
81
|
debug: config.debug,
|
|
@@ -48,6 +83,60 @@ export function resolveAccount(cfg: OpenClawConfig, accountId?: string) {
|
|
|
48
83
|
};
|
|
49
84
|
}
|
|
50
85
|
|
|
86
|
+
export function resolveMediaMaxMb(cfg: OpenClawConfig, accountId?: string): number | undefined {
|
|
87
|
+
return getAccountConfig(cfg, accountId).mediaMaxMb;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getRawAccountConfig(cfg: OpenClawConfig, accountId?: string): Partial<MixinAccountConfig> {
|
|
91
|
+
const raw = getRawConfig(cfg);
|
|
92
|
+
const resolvedAccountId = accountId ?? resolveDefaultAccountId(cfg);
|
|
93
|
+
if (resolvedAccountId !== "default" && raw.accounts?.[resolvedAccountId]) {
|
|
94
|
+
return raw.accounts[resolvedAccountId] as Partial<MixinAccountConfig>;
|
|
95
|
+
}
|
|
96
|
+
return raw;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function getConversationConfig(
|
|
100
|
+
cfg: OpenClawConfig,
|
|
101
|
+
accountId: string,
|
|
102
|
+
conversationId: string,
|
|
103
|
+
): {
|
|
104
|
+
exists: boolean;
|
|
105
|
+
config: MixinConversationConfig;
|
|
106
|
+
} {
|
|
107
|
+
const accountRaw = getRawAccountConfig(cfg, accountId);
|
|
108
|
+
const conversationRaw = accountRaw.conversations?.[conversationId] as Partial<MixinConversationConfig> | undefined;
|
|
109
|
+
const result = MixinConversationConfigSchema.safeParse(conversationRaw ?? {});
|
|
110
|
+
return {
|
|
111
|
+
exists: Boolean(conversationRaw),
|
|
112
|
+
config: result.success ? result.data : MixinConversationConfigSchema.parse({}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function resolveConversationPolicy(
|
|
117
|
+
cfg: OpenClawConfig,
|
|
118
|
+
accountId: string,
|
|
119
|
+
conversationId: string,
|
|
120
|
+
): {
|
|
121
|
+
enabled: boolean;
|
|
122
|
+
requireMention: boolean;
|
|
123
|
+
mediaBypassMention: boolean;
|
|
124
|
+
groupPolicy: MixinAccountConfig["groupPolicy"];
|
|
125
|
+
groupAllowFrom: string[];
|
|
126
|
+
hasConversationOverride: boolean;
|
|
127
|
+
} {
|
|
128
|
+
const accountConfig = getAccountConfig(cfg, accountId);
|
|
129
|
+
const conversation = getConversationConfig(cfg, accountId, conversationId);
|
|
130
|
+
return {
|
|
131
|
+
enabled: conversation.config.enabled !== false,
|
|
132
|
+
requireMention: conversation.config.requireMention ?? accountConfig.requireMentionInGroup,
|
|
133
|
+
mediaBypassMention: conversation.config.mediaBypassMention ?? accountConfig.mediaBypassMentionInGroup,
|
|
134
|
+
groupPolicy: conversation.config.groupPolicy ?? accountConfig.groupPolicy,
|
|
135
|
+
groupAllowFrom: conversation.config.allowFrom ?? accountConfig.groupAllowFrom ?? [],
|
|
136
|
+
hasConversationOverride: conversation.exists,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
51
140
|
export function isConfigured(account: ReturnType<typeof resolveAccount>): boolean {
|
|
52
141
|
return account.configured;
|
|
53
142
|
}
|