@meet-im/meet 1.0.3
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 +1 -0
- package/index.ts +26 -0
- package/openclaw.plugin.json +10 -0
- package/package.json +75 -0
- package/skills/meet-at/SKILL.md +66 -0
- package/skills/meet-markdown/SKILL.md +235 -0
- package/skills/meet-markdown/references/colors.md +63 -0
- package/skills/meet-markdown/references/faq.md +159 -0
- package/skills/meet-markdown/references/quick-reference.md +86 -0
- package/src/accounts.ts +182 -0
- package/src/bot.ts +414 -0
- package/src/channel.ts +403 -0
- package/src/client.ts +63 -0
- package/src/config-schema.ts +49 -0
- package/src/media.ts +198 -0
- package/src/monitor.ts +197 -0
- package/src/outbound.ts +35 -0
- package/src/policy.ts +131 -0
- package/src/probe.ts +76 -0
- package/src/reply-dispatcher.ts +130 -0
- package/src/runtime.ts +14 -0
- package/src/sdk-bridge.ts +268 -0
- package/src/send.ts +223 -0
- package/src/targets.ts +101 -0
- package/src/types.ts +96 -0
package/src/channel.ts
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChannelPlugin,
|
|
3
|
+
ClawdbotConfig,
|
|
4
|
+
GroupToolPolicyConfig,
|
|
5
|
+
} from "openclaw/plugin-sdk";
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_ACCOUNT_ID,
|
|
8
|
+
normalizeAccountId,
|
|
9
|
+
PAIRING_APPROVED_MESSAGE,
|
|
10
|
+
} from "openclaw/plugin-sdk";
|
|
11
|
+
import type { ResolvedMeetAccount, MeetConfig } from "./types.js";
|
|
12
|
+
import {
|
|
13
|
+
resolveMeetAccount,
|
|
14
|
+
listMeetAccountIds,
|
|
15
|
+
resolveDefaultMeetAccountId,
|
|
16
|
+
isFlatAccountConfig,
|
|
17
|
+
getFlatAccountKey,
|
|
18
|
+
} from "./accounts.js";
|
|
19
|
+
import { meetOutbound } from "./outbound.js";
|
|
20
|
+
import { probeMeet } from "./probe.js";
|
|
21
|
+
import { resolveMeetGroupPolicy } from "./policy.js";
|
|
22
|
+
import {
|
|
23
|
+
parseMeetTarget,
|
|
24
|
+
looksLikeMeetId,
|
|
25
|
+
formatMeetTarget,
|
|
26
|
+
} from "./targets.js";
|
|
27
|
+
import { sendMessageMeet } from "./send.js";
|
|
28
|
+
|
|
29
|
+
const meta = {
|
|
30
|
+
id: "meet",
|
|
31
|
+
label: "Meet",
|
|
32
|
+
selectionLabel: "Meet IM",
|
|
33
|
+
docsPath: "/channels/meet",
|
|
34
|
+
docsLabel: "meet",
|
|
35
|
+
blurb: "Meet IM platform integration.",
|
|
36
|
+
aliases: [],
|
|
37
|
+
order: 80,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const meetPlugin: ChannelPlugin<ResolvedMeetAccount> = {
|
|
41
|
+
id: "meet",
|
|
42
|
+
meta: {
|
|
43
|
+
...meta,
|
|
44
|
+
},
|
|
45
|
+
pairing: {
|
|
46
|
+
idLabel: "meetUserId",
|
|
47
|
+
normalizeAllowEntry: (entry) => entry.replace(/^(meet|user):/i, ""),
|
|
48
|
+
notifyApproval: async ({ cfg, id }) => {
|
|
49
|
+
await sendMessageMeet({
|
|
50
|
+
cfg,
|
|
51
|
+
to: id,
|
|
52
|
+
text: PAIRING_APPROVED_MESSAGE,
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
capabilities: {
|
|
57
|
+
chatTypes: ["direct", "channel"],
|
|
58
|
+
polls: false,
|
|
59
|
+
threads: false,
|
|
60
|
+
media: true,
|
|
61
|
+
reactions: false,
|
|
62
|
+
edit: false,
|
|
63
|
+
reply: false,
|
|
64
|
+
},
|
|
65
|
+
agentPrompt: {
|
|
66
|
+
messageToolHints: () => [
|
|
67
|
+
"- Meet targeting: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `user:<id>` or `channel:<id>`.",
|
|
68
|
+
"- Meet mentions: use `<@USER_ID>` to mention users, `<@-1>` to mention everyone. Examples: `<@553> hello` or `<@-1> important announcement`.",
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
groups: {
|
|
72
|
+
resolveToolPolicy: ({
|
|
73
|
+
cfg,
|
|
74
|
+
accountId,
|
|
75
|
+
groupId,
|
|
76
|
+
}): GroupToolPolicyConfig | undefined => {
|
|
77
|
+
const account = resolveMeetAccount({ cfg, accountId });
|
|
78
|
+
const groupPolicy = resolveMeetGroupPolicy({
|
|
79
|
+
groupPolicy: account.config.groupPolicy,
|
|
80
|
+
groupAllowFrom: account.config.groupAllowFrom ?? [],
|
|
81
|
+
chatId: groupId ?? "",
|
|
82
|
+
groups: account.config.groups,
|
|
83
|
+
});
|
|
84
|
+
if (!groupPolicy.allowed) {
|
|
85
|
+
return { deny: ["*"] };
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
reload: { configPrefixes: ["channels.meet"] },
|
|
91
|
+
configSchema: {
|
|
92
|
+
schema: {
|
|
93
|
+
type: "object",
|
|
94
|
+
additionalProperties: false,
|
|
95
|
+
properties: {
|
|
96
|
+
enabled: { type: "boolean" },
|
|
97
|
+
apiEndpoint: { type: "string" },
|
|
98
|
+
apiToken: { type: "string" },
|
|
99
|
+
pollTimeout: { type: "integer", minimum: 1000, maximum: 300000 },
|
|
100
|
+
pollLimit: { type: "integer", minimum: 1, maximum: 1000 },
|
|
101
|
+
dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
|
|
102
|
+
allowFrom: {
|
|
103
|
+
type: "array",
|
|
104
|
+
items: { oneOf: [{ type: "string" }, { type: "number" }] },
|
|
105
|
+
},
|
|
106
|
+
groupPolicy: {
|
|
107
|
+
type: "string",
|
|
108
|
+
enum: ["open", "allowlist", "disabled"],
|
|
109
|
+
},
|
|
110
|
+
groupAllowFrom: {
|
|
111
|
+
type: "array",
|
|
112
|
+
items: { oneOf: [{ type: "string" }, { type: "number" }] },
|
|
113
|
+
},
|
|
114
|
+
groups: {
|
|
115
|
+
type: "object",
|
|
116
|
+
additionalProperties: {
|
|
117
|
+
type: "object",
|
|
118
|
+
properties: {
|
|
119
|
+
enabled: { type: "boolean" },
|
|
120
|
+
name: { type: "string" },
|
|
121
|
+
requireMention: { type: "boolean" },
|
|
122
|
+
systemPrompt: { type: "string" },
|
|
123
|
+
users: {
|
|
124
|
+
type: "array",
|
|
125
|
+
items: { oneOf: [{ type: "string" }, { type: "number" }] },
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
requireMention: { type: "boolean" },
|
|
131
|
+
historyLimit: { type: "integer", minimum: 0 },
|
|
132
|
+
dmHistoryLimit: { type: "integer", minimum: 0 },
|
|
133
|
+
textChunkLimit: { type: "integer", minimum: 1 },
|
|
134
|
+
mediaMaxMb: { type: "number", minimum: 0 },
|
|
135
|
+
accounts: {
|
|
136
|
+
type: "object",
|
|
137
|
+
additionalProperties: {
|
|
138
|
+
type: "object",
|
|
139
|
+
properties: {
|
|
140
|
+
enabled: { type: "boolean" },
|
|
141
|
+
name: { type: "string" },
|
|
142
|
+
apiEndpoint: { type: "string" },
|
|
143
|
+
apiToken: { type: "string" },
|
|
144
|
+
pollTimeout: { type: "integer", minimum: 1000, maximum: 300000 },
|
|
145
|
+
pollLimit: { type: "integer", minimum: 1, maximum: 1000 },
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
config: {
|
|
153
|
+
listAccountIds: (cfg) => listMeetAccountIds(cfg),
|
|
154
|
+
resolveAccount: (cfg, accountId) => resolveMeetAccount({ cfg, accountId }),
|
|
155
|
+
defaultAccountId: (cfg) => resolveDefaultMeetAccountId(cfg),
|
|
156
|
+
setAccountEnabled: ({ cfg, accountId, enabled }) => {
|
|
157
|
+
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
|
158
|
+
|
|
159
|
+
if (isDefault) {
|
|
160
|
+
return {
|
|
161
|
+
...cfg,
|
|
162
|
+
channels: {
|
|
163
|
+
...cfg.channels,
|
|
164
|
+
meet: {
|
|
165
|
+
...cfg.channels?.meet,
|
|
166
|
+
enabled,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (isFlatAccountConfig(cfg, accountId)) {
|
|
173
|
+
const key = getFlatAccountKey(accountId);
|
|
174
|
+
return {
|
|
175
|
+
...cfg,
|
|
176
|
+
channels: {
|
|
177
|
+
...cfg.channels,
|
|
178
|
+
[key]: {
|
|
179
|
+
...(cfg.channels?.[key] as Record<string, unknown> | undefined),
|
|
180
|
+
enabled,
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const meetCfg = cfg.channels?.meet as MeetConfig | undefined;
|
|
187
|
+
return {
|
|
188
|
+
...cfg,
|
|
189
|
+
channels: {
|
|
190
|
+
...cfg.channels,
|
|
191
|
+
meet: {
|
|
192
|
+
...meetCfg,
|
|
193
|
+
accounts: {
|
|
194
|
+
...meetCfg?.accounts,
|
|
195
|
+
[accountId]: {
|
|
196
|
+
...meetCfg?.accounts?.[accountId],
|
|
197
|
+
enabled,
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
deleteAccount: ({ cfg, accountId }) => {
|
|
205
|
+
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
|
206
|
+
|
|
207
|
+
if (isDefault) {
|
|
208
|
+
const next = { ...cfg } as ClawdbotConfig;
|
|
209
|
+
const nextChannels = { ...cfg.channels };
|
|
210
|
+
delete (nextChannels as Record<string, unknown>).meet;
|
|
211
|
+
if (Object.keys(nextChannels).length > 0) {
|
|
212
|
+
next.channels = nextChannels;
|
|
213
|
+
} else {
|
|
214
|
+
delete next.channels;
|
|
215
|
+
}
|
|
216
|
+
return next;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (isFlatAccountConfig(cfg, accountId)) {
|
|
220
|
+
const key = getFlatAccountKey(accountId);
|
|
221
|
+
const next = { ...cfg } as ClawdbotConfig;
|
|
222
|
+
const nextChannels = { ...cfg.channels };
|
|
223
|
+
delete (nextChannels as Record<string, unknown>)[key];
|
|
224
|
+
if (Object.keys(nextChannels).length > 0) {
|
|
225
|
+
next.channels = nextChannels;
|
|
226
|
+
} else {
|
|
227
|
+
delete next.channels;
|
|
228
|
+
}
|
|
229
|
+
return next;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const meetCfg = cfg.channels?.meet as MeetConfig | undefined;
|
|
233
|
+
const accounts = { ...meetCfg?.accounts };
|
|
234
|
+
delete accounts[accountId];
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
...cfg,
|
|
238
|
+
channels: {
|
|
239
|
+
...cfg.channels,
|
|
240
|
+
meet: {
|
|
241
|
+
...meetCfg,
|
|
242
|
+
accounts: Object.keys(accounts).length > 0 ? accounts : undefined,
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
},
|
|
247
|
+
isConfigured: (account) => account.configured,
|
|
248
|
+
describeAccount: (account) => ({
|
|
249
|
+
accountId: account.accountId,
|
|
250
|
+
enabled: account.enabled,
|
|
251
|
+
configured: account.configured,
|
|
252
|
+
name: account.name,
|
|
253
|
+
apiEndpoint: account.apiEndpoint,
|
|
254
|
+
}),
|
|
255
|
+
resolveAllowFrom: ({ cfg, accountId }) => {
|
|
256
|
+
const account = resolveMeetAccount({ cfg, accountId });
|
|
257
|
+
return (account.config?.allowFrom ?? [])
|
|
258
|
+
.map((entry) => String(entry).trim())
|
|
259
|
+
.filter(Boolean);
|
|
260
|
+
},
|
|
261
|
+
formatAllowFrom: ({ allowFrom }) =>
|
|
262
|
+
allowFrom
|
|
263
|
+
.map((entry) => String(entry).trim())
|
|
264
|
+
.filter(Boolean)
|
|
265
|
+
.map((entry) => entry.toLowerCase()),
|
|
266
|
+
},
|
|
267
|
+
security: {
|
|
268
|
+
collectWarnings: ({ cfg, accountId }) => {
|
|
269
|
+
const account = resolveMeetAccount({ cfg, accountId });
|
|
270
|
+
const meetCfg = account.config;
|
|
271
|
+
const defaultGroupPolicy = (
|
|
272
|
+
cfg.channels as Record<string, { groupPolicy?: string }> | undefined
|
|
273
|
+
)?.defaults?.groupPolicy;
|
|
274
|
+
const groupPolicy =
|
|
275
|
+
meetCfg?.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
|
|
276
|
+
if (groupPolicy !== "open") return [];
|
|
277
|
+
return [
|
|
278
|
+
`- Meet[${account.accountId}] groups: groupPolicy="open" allows any member to trigger (mention-gated). Set channels.meet.groupPolicy="allowlist" + channels.meet.groupAllowFrom to restrict senders.`,
|
|
279
|
+
];
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
setup: {
|
|
283
|
+
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
|
284
|
+
applyAccountConfig: ({ cfg, accountId }) => {
|
|
285
|
+
const isDefault = !accountId || accountId === DEFAULT_ACCOUNT_ID;
|
|
286
|
+
|
|
287
|
+
if (isDefault) {
|
|
288
|
+
return {
|
|
289
|
+
...cfg,
|
|
290
|
+
channels: {
|
|
291
|
+
...cfg.channels,
|
|
292
|
+
meet: {
|
|
293
|
+
...cfg.channels?.meet,
|
|
294
|
+
enabled: true,
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (isFlatAccountConfig(cfg, accountId)) {
|
|
301
|
+
const key = getFlatAccountKey(accountId);
|
|
302
|
+
return {
|
|
303
|
+
...cfg,
|
|
304
|
+
channels: {
|
|
305
|
+
...cfg.channels,
|
|
306
|
+
[key]: {
|
|
307
|
+
...(cfg.channels?.[key] as Record<string, unknown> | undefined),
|
|
308
|
+
enabled: true,
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const meetCfg = cfg.channels?.meet as MeetConfig | undefined;
|
|
315
|
+
return {
|
|
316
|
+
...cfg,
|
|
317
|
+
channels: {
|
|
318
|
+
...cfg.channels,
|
|
319
|
+
meet: {
|
|
320
|
+
...meetCfg,
|
|
321
|
+
accounts: {
|
|
322
|
+
...meetCfg?.accounts,
|
|
323
|
+
[accountId]: {
|
|
324
|
+
...meetCfg?.accounts?.[accountId],
|
|
325
|
+
enabled: true,
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
messaging: {
|
|
334
|
+
normalizeTarget: (raw: string) => {
|
|
335
|
+
const target = parseMeetTarget(raw);
|
|
336
|
+
return target ? formatMeetTarget(target) : raw;
|
|
337
|
+
},
|
|
338
|
+
targetResolver: {
|
|
339
|
+
looksLikeId: looksLikeMeetId,
|
|
340
|
+
hint: "<userId|user:<id>|channel:<id>>",
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
directory: {
|
|
344
|
+
self: async () => null,
|
|
345
|
+
listPeers: async () => [],
|
|
346
|
+
listGroups: async () => [],
|
|
347
|
+
listPeersLive: async () => [],
|
|
348
|
+
listGroupsLive: async () => [],
|
|
349
|
+
},
|
|
350
|
+
outbound: meetOutbound,
|
|
351
|
+
status: {
|
|
352
|
+
defaultRuntime: {
|
|
353
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
354
|
+
running: false,
|
|
355
|
+
lastStartAt: null,
|
|
356
|
+
lastStopAt: null,
|
|
357
|
+
lastError: null,
|
|
358
|
+
port: null,
|
|
359
|
+
},
|
|
360
|
+
buildChannelSummary: ({ snapshot }) => ({
|
|
361
|
+
configured: snapshot.configured ?? false,
|
|
362
|
+
running: snapshot.running ?? false,
|
|
363
|
+
lastStartAt: snapshot.lastStartAt ?? null,
|
|
364
|
+
lastStopAt: snapshot.lastStopAt ?? null,
|
|
365
|
+
lastError: snapshot.lastError ?? null,
|
|
366
|
+
port: snapshot.port ?? null,
|
|
367
|
+
probe: snapshot.probe,
|
|
368
|
+
lastProbeAt: snapshot.lastProbeAt ?? null,
|
|
369
|
+
}),
|
|
370
|
+
probeAccount: async ({ account }) => {
|
|
371
|
+
return await probeMeet(account);
|
|
372
|
+
},
|
|
373
|
+
buildAccountSnapshot: ({ account, runtime, probe }) => ({
|
|
374
|
+
accountId: account.accountId,
|
|
375
|
+
enabled: account.enabled,
|
|
376
|
+
configured: account.configured,
|
|
377
|
+
name: account.name,
|
|
378
|
+
apiEndpoint: account.apiEndpoint,
|
|
379
|
+
running: runtime?.running ?? false,
|
|
380
|
+
lastStartAt: runtime?.lastStartAt ?? null,
|
|
381
|
+
lastStopAt: runtime?.lastStopAt ?? null,
|
|
382
|
+
lastError: runtime?.lastError ?? null,
|
|
383
|
+
port: runtime?.port ?? null,
|
|
384
|
+
probe,
|
|
385
|
+
}),
|
|
386
|
+
},
|
|
387
|
+
gateway: {
|
|
388
|
+
startAccount: async (ctx) => {
|
|
389
|
+
const { monitorMeetProvider } = await import("./monitor.js");
|
|
390
|
+
const account = resolveMeetAccount({
|
|
391
|
+
cfg: ctx.cfg,
|
|
392
|
+
accountId: ctx.accountId,
|
|
393
|
+
});
|
|
394
|
+
ctx.log?.info(`[${ctx.accountId}] starting`);
|
|
395
|
+
return monitorMeetProvider({
|
|
396
|
+
config: ctx.cfg,
|
|
397
|
+
runtime: ctx.runtime,
|
|
398
|
+
abortSignal: ctx.abortSignal,
|
|
399
|
+
accountId: ctx.accountId,
|
|
400
|
+
});
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
};
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { MeetBot, POLLING } from "@meet-im/meet-bot-jssdk"
|
|
2
|
+
import type { PollingOptions } from "@meet-im/meet-bot-jssdk"
|
|
3
|
+
import type { ResolvedMeetAccount } from "./types.js"
|
|
4
|
+
|
|
5
|
+
const botInstances = new Map<string, MeetBot>()
|
|
6
|
+
|
|
7
|
+
export function createMeetClient(account: ResolvedMeetAccount): MeetBot {
|
|
8
|
+
const existing = botInstances.get(account.accountId)
|
|
9
|
+
if (existing) {
|
|
10
|
+
return existing
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!account.apiToken) {
|
|
14
|
+
throw new Error(`Meet account "${account.accountId}" missing apiToken`)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const pollTimeoutMs = account.config.pollTimeout ?? 30000
|
|
18
|
+
const pollTimeoutSec = Math.floor(pollTimeoutMs / 1000)
|
|
19
|
+
|
|
20
|
+
const logLevel = account.config.logLevel ?? "silent"
|
|
21
|
+
|
|
22
|
+
const bot = new MeetBot({
|
|
23
|
+
token: account.apiToken,
|
|
24
|
+
baseUrl: account.apiEndpoint,
|
|
25
|
+
pollingLimit: account.config.pollLimit ?? POLLING.DEFAULT_LIMIT,
|
|
26
|
+
longPollingTimeout: pollTimeoutSec,
|
|
27
|
+
logLevel,
|
|
28
|
+
useV2: true,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
botInstances.set(account.accountId, bot)
|
|
32
|
+
return bot
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getPollingOptions(account: ResolvedMeetAccount): PollingOptions {
|
|
36
|
+
const pollTimeoutMs = account.config.pollTimeout ?? 30000
|
|
37
|
+
const pollTimeoutSec = Math.floor(pollTimeoutMs / 1000)
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
timeout: pollTimeoutSec,
|
|
41
|
+
limit: account.config.pollLimit ?? POLLING.DEFAULT_LIMIT,
|
|
42
|
+
retryDelay: POLLING.DEFAULT_RETRY_DELAY,
|
|
43
|
+
maxRetries: 0,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getMeetClient(accountId: string): MeetBot | undefined {
|
|
48
|
+
return botInstances.get(accountId)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function closeMeetClient(accountId: string): void {
|
|
52
|
+
const bot = botInstances.get(accountId)
|
|
53
|
+
if (bot) {
|
|
54
|
+
bot.stopPolling()
|
|
55
|
+
botInstances.delete(accountId)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function closeAllMeetClients(): void {
|
|
60
|
+
for (const [accountId] of botInstances) {
|
|
61
|
+
closeMeetClient(accountId)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
|
|
3
|
+
export const MeetChannelConfigSchema = z.object({
|
|
4
|
+
enabled: z.boolean().optional(),
|
|
5
|
+
systemPrompt: z.string().optional(),
|
|
6
|
+
requireMention: z.boolean().optional(),
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
export const MeetGroupConfigSchema = z.object({
|
|
10
|
+
enabled: z.boolean().optional(),
|
|
11
|
+
name: z.string().optional(),
|
|
12
|
+
requireMention: z.boolean().optional(),
|
|
13
|
+
systemPrompt: z.string().optional(),
|
|
14
|
+
users: z.array(z.union([z.string(), z.number()])).optional(),
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
export const MeetAccountConfigSchema = z.object({
|
|
18
|
+
enabled: z.boolean().optional(),
|
|
19
|
+
name: z.string().optional(),
|
|
20
|
+
apiEndpoint: z.string().optional(),
|
|
21
|
+
token: z.string().optional(),
|
|
22
|
+
apiToken: z.string().optional(),
|
|
23
|
+
pollTimeout: z.number().min(1000).max(300000).optional(),
|
|
24
|
+
pollLimit: z.number().min(1).max(1000).optional(),
|
|
25
|
+
logLevel: z.enum(["silent", "info"]).optional(),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export const MeetConfigSchema = z.object({
|
|
29
|
+
enabled: z.boolean().optional(),
|
|
30
|
+
apiEndpoint: z.string().optional(),
|
|
31
|
+
token: z.string().optional(),
|
|
32
|
+
apiToken: z.string().optional(),
|
|
33
|
+
pollTimeout: z.number().min(1000).max(300000).optional().default(30000),
|
|
34
|
+
pollLimit: z.number().min(1).max(1000).optional().default(100),
|
|
35
|
+
logLevel: z.enum(["silent", "info"]).optional(),
|
|
36
|
+
dmPolicy: z.enum(["open", "pairing", "allowlist"]).optional().default("pairing"),
|
|
37
|
+
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
38
|
+
groupPolicy: z.enum(["open", "allowlist", "disabled"]).optional().default("allowlist"),
|
|
39
|
+
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
40
|
+
groups: z.record(z.string(), MeetGroupConfigSchema).optional(),
|
|
41
|
+
requireMention: z.boolean().optional().default(true),
|
|
42
|
+
systemPrompt: z.string().optional(),
|
|
43
|
+
channels: z.record(z.string(), MeetChannelConfigSchema).optional(),
|
|
44
|
+
historyLimit: z.number().min(0).optional(),
|
|
45
|
+
dmHistoryLimit: z.number().min(0).optional(),
|
|
46
|
+
textChunkLimit: z.number().min(1).optional(),
|
|
47
|
+
mediaMaxMb: z.number().min(0).optional().default(30),
|
|
48
|
+
accounts: z.record(z.string(), MeetAccountConfigSchema).optional(),
|
|
49
|
+
})
|
package/src/media.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type { MeetMediaAttachment } from "./types.js"
|
|
2
|
+
import { getMeetClient } from "./client.js"
|
|
3
|
+
import { getMeetRuntime } from "./runtime.js"
|
|
4
|
+
|
|
5
|
+
let _debugLog: ((msg: string) => void) | null = null
|
|
6
|
+
let _debugError: ((msg: string) => void) | null = null
|
|
7
|
+
|
|
8
|
+
export function setMediaDebugLogger(log: (msg: string) => void, error: (msg: string) => void): void {
|
|
9
|
+
_debugLog = log
|
|
10
|
+
_debugError = error
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function log(msg: string): void {
|
|
14
|
+
if (_debugLog) _debugLog(msg)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function logError(msg: string): void {
|
|
18
|
+
if (_debugError) _debugError(msg)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 获取文件下载 URL
|
|
23
|
+
*/
|
|
24
|
+
export async function getFileAccessUrl(params: {
|
|
25
|
+
accountId: string
|
|
26
|
+
sessionInfo: {
|
|
27
|
+
firstId: number
|
|
28
|
+
secondId: number
|
|
29
|
+
sessionType: number
|
|
30
|
+
companyId?: number
|
|
31
|
+
}
|
|
32
|
+
seqId: number
|
|
33
|
+
fileId: string | number
|
|
34
|
+
ossProcess?: string
|
|
35
|
+
}): Promise<string | null> {
|
|
36
|
+
const { accountId, sessionInfo, seqId, fileId, ossProcess } = params
|
|
37
|
+
const bot = getMeetClient(accountId)
|
|
38
|
+
if (!bot) {
|
|
39
|
+
logError(`[meet] getFileAccessUrl: bot client not found for account=${accountId}`)
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
log(`[meet] getFileAccessUrl: calling getAccessURL fileId=${fileId} seqId=${seqId} companyId=${sessionInfo.companyId}`)
|
|
45
|
+
const result = await bot.getAccessURL({
|
|
46
|
+
firstId: sessionInfo.firstId,
|
|
47
|
+
secondId: sessionInfo.secondId,
|
|
48
|
+
sessionType: sessionInfo.sessionType,
|
|
49
|
+
seqId,
|
|
50
|
+
fileId: Number(fileId),
|
|
51
|
+
companyId: sessionInfo.companyId,
|
|
52
|
+
"x-oss-process": ossProcess,
|
|
53
|
+
})
|
|
54
|
+
log(`[meet] getFileAccessUrl: got URL ${result.fileUrl?.slice(0, 80)}...`)
|
|
55
|
+
return result.fileUrl
|
|
56
|
+
} catch (err) {
|
|
57
|
+
logError(`[meet] getFileAccessUrl: getAccessURL failed: ${String(err)}`)
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 媒体信息(下载并保存后的结果)
|
|
64
|
+
*/
|
|
65
|
+
export type ResolvedMedia = {
|
|
66
|
+
path: string
|
|
67
|
+
contentType?: string
|
|
68
|
+
placeholder: string
|
|
69
|
+
url?: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 下载媒体文件到本地 media store
|
|
74
|
+
* 与 Discord 保持一致的处理流程
|
|
75
|
+
*/
|
|
76
|
+
export async function downloadAndSaveMedia(params: {
|
|
77
|
+
accountId: string
|
|
78
|
+
attachment: MeetMediaAttachment
|
|
79
|
+
sessionInfo: {
|
|
80
|
+
firstId: number
|
|
81
|
+
secondId: number
|
|
82
|
+
sessionType: number
|
|
83
|
+
companyId?: number
|
|
84
|
+
}
|
|
85
|
+
seqId: number
|
|
86
|
+
maxBytes?: number
|
|
87
|
+
}): Promise<ResolvedMedia | null> {
|
|
88
|
+
const { accountId, attachment, sessionInfo, seqId, maxBytes } = params
|
|
89
|
+
const runtime = getMeetRuntime()
|
|
90
|
+
|
|
91
|
+
log(`[meet] downloadAndSaveMedia: fileId=${attachment.fileId} fileName=${attachment.fileName} fileUrl=${attachment.fileUrl?.slice(0, 60)}...`)
|
|
92
|
+
|
|
93
|
+
// 确定下载 URL
|
|
94
|
+
let url: string
|
|
95
|
+
// fileUrl 可能是完整 URL 或相对路径,只有完整 URL 才能直接使用
|
|
96
|
+
if (attachment.fileUrl && /^https?:\/\//i.test(attachment.fileUrl)) {
|
|
97
|
+
url = attachment.fileUrl
|
|
98
|
+
log(`[meet] downloadAndSaveMedia: using complete fileUrl`)
|
|
99
|
+
} else {
|
|
100
|
+
log(`[meet] downloadAndSaveMedia: fileUrl is not complete URL, calling getFileAccessUrl`)
|
|
101
|
+
const accessUrl = await getFileAccessUrl({
|
|
102
|
+
accountId,
|
|
103
|
+
sessionInfo,
|
|
104
|
+
seqId,
|
|
105
|
+
fileId: attachment.fileId,
|
|
106
|
+
})
|
|
107
|
+
if (!accessUrl) {
|
|
108
|
+
logError(`[meet] downloadAndSaveMedia: getFileAccessUrl returned null`)
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
url = accessUrl
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
// 下载媒体
|
|
116
|
+
log(`[meet] downloadAndSaveMedia: fetching remote media from ${url.slice(0, 80)}...`)
|
|
117
|
+
const fetched = await runtime.channel.media.fetchRemoteMedia({
|
|
118
|
+
url,
|
|
119
|
+
filePathHint: attachment.fileName,
|
|
120
|
+
maxBytes,
|
|
121
|
+
})
|
|
122
|
+
log(`[meet] downloadAndSaveMedia: fetched ${fetched.buffer.length} bytes, contentType=${fetched.contentType}`)
|
|
123
|
+
|
|
124
|
+
// 保存到本地 media store
|
|
125
|
+
const saved = await runtime.channel.media.saveMediaBuffer(
|
|
126
|
+
fetched.buffer,
|
|
127
|
+
fetched.contentType,
|
|
128
|
+
"inbound",
|
|
129
|
+
maxBytes,
|
|
130
|
+
attachment.fileName,
|
|
131
|
+
)
|
|
132
|
+
log(`[meet] downloadAndSaveMedia: saved to ${saved.path}`)
|
|
133
|
+
|
|
134
|
+
const typeLabel = inferMediaType(fetched.contentType)
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
path: saved.path,
|
|
138
|
+
contentType: saved.contentType,
|
|
139
|
+
placeholder: `[${typeLabel}: ${attachment.fileName || "文件"}]`,
|
|
140
|
+
url,
|
|
141
|
+
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
logError(`[meet] downloadAndSaveMedia: download/save failed: ${String(err)}`)
|
|
144
|
+
// 下载失败时,返回 URL 作为 fallback
|
|
145
|
+
const typeLabel = inferMediaType(attachment.mimeType)
|
|
146
|
+
return {
|
|
147
|
+
path: url,
|
|
148
|
+
contentType: attachment.mimeType,
|
|
149
|
+
placeholder: `[${typeLabel}: ${attachment.fileName || "文件"}]`,
|
|
150
|
+
url,
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 批量处理媒体附件
|
|
157
|
+
*/
|
|
158
|
+
export async function resolveMediaAttachments(params: {
|
|
159
|
+
accountId: string
|
|
160
|
+
attachments: MeetMediaAttachment[]
|
|
161
|
+
sessionInfo: {
|
|
162
|
+
firstId: number
|
|
163
|
+
secondId: number
|
|
164
|
+
sessionType: number
|
|
165
|
+
companyId?: number
|
|
166
|
+
}
|
|
167
|
+
seqId: number
|
|
168
|
+
maxBytes?: number
|
|
169
|
+
}): Promise<ResolvedMedia[]> {
|
|
170
|
+
const results: ResolvedMedia[] = []
|
|
171
|
+
|
|
172
|
+
for (const attachment of params.attachments) {
|
|
173
|
+
const resolved = await downloadAndSaveMedia({
|
|
174
|
+
accountId: params.accountId,
|
|
175
|
+
attachment,
|
|
176
|
+
sessionInfo: params.sessionInfo,
|
|
177
|
+
seqId: params.seqId,
|
|
178
|
+
maxBytes: params.maxBytes,
|
|
179
|
+
})
|
|
180
|
+
if (resolved) {
|
|
181
|
+
results.push(resolved)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return results
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 根据 MIME 类型推断媒体类型标签
|
|
190
|
+
* 与 Discord 保持一致的格式
|
|
191
|
+
*/
|
|
192
|
+
function inferMediaType(mimeType?: string): string {
|
|
193
|
+
if (!mimeType) return "<media:document>"
|
|
194
|
+
if (mimeType.startsWith("image/")) return "<media:image>"
|
|
195
|
+
if (mimeType.startsWith("video/")) return "<media:video>"
|
|
196
|
+
if (mimeType.startsWith("audio/")) return "<media:audio>"
|
|
197
|
+
return "<media:document>"
|
|
198
|
+
}
|