@meet-im/meet 2.0.5 → 3.0.0-beta.0

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.
Files changed (70) hide show
  1. package/dist/account-inspect-api.d.ts +2 -0
  2. package/dist/account-inspect-api.js +4 -0
  3. package/dist/channel-plugin-api.d.ts +1 -0
  4. package/dist/channel-plugin-api.js +3 -0
  5. package/dist/index.d.ts +14 -0
  6. package/dist/index.js +43 -0
  7. package/dist/monitor-api.d.ts +1 -0
  8. package/dist/monitor-api.js +1 -0
  9. package/dist/probe-api.d.ts +1 -0
  10. package/dist/probe-api.js +1 -0
  11. package/dist/runtime-setter-api.d.ts +1 -0
  12. package/dist/runtime-setter-api.js +2 -0
  13. package/dist/send-api.d.ts +1 -0
  14. package/dist/send-api.js +1 -0
  15. package/dist/src/account-inspect.d.ts +5 -0
  16. package/dist/src/account-inspect.js +9 -0
  17. package/dist/src/accounts.d.ts +12 -0
  18. package/dist/src/accounts.js +134 -0
  19. package/dist/src/bot.d.ts +15 -0
  20. package/dist/src/bot.js +355 -0
  21. package/dist/src/channel.d.ts +3 -0
  22. package/dist/src/channel.js +402 -0
  23. package/dist/src/client.d.ts +8 -0
  24. package/dist/src/client.js +49 -0
  25. package/dist/src/config-schema.d.ts +82 -0
  26. package/dist/src/config-schema.js +46 -0
  27. package/dist/src/media.d.ts +57 -0
  28. package/dist/src/media.js +140 -0
  29. package/dist/src/monitor.d.ts +9 -0
  30. package/dist/src/monitor.js +153 -0
  31. package/dist/src/outbound.d.ts +2 -0
  32. package/dist/src/outbound.js +34 -0
  33. package/dist/src/policy.d.ts +30 -0
  34. package/dist/src/policy.js +78 -0
  35. package/dist/src/probe.d.ts +10 -0
  36. package/dist/src/probe.js +56 -0
  37. package/dist/src/reply-dispatcher.d.ts +29 -0
  38. package/dist/src/reply-dispatcher.js +173 -0
  39. package/dist/src/runtime.d.ts +11 -0
  40. package/dist/src/runtime.js +6 -0
  41. package/dist/src/sdk-bridge.d.ts +21 -0
  42. package/dist/src/sdk-bridge.js +214 -0
  43. package/dist/src/send.d.ts +60 -0
  44. package/dist/src/send.js +317 -0
  45. package/dist/src/targets.d.ts +15 -0
  46. package/dist/src/targets.js +63 -0
  47. package/dist/src/types.d.ts +76 -0
  48. package/dist/src/types.js +1 -0
  49. package/dist/vitest.config.d.ts +8 -0
  50. package/dist/vitest.config.js +7 -0
  51. package/openclaw.plugin.json +116 -0
  52. package/package.json +18 -17
  53. package/skills/meet-emojis/SKILL.md +13 -15
  54. package/index.ts +0 -26
  55. package/src/accounts.ts +0 -182
  56. package/src/bot.ts +0 -418
  57. package/src/channel.ts +0 -396
  58. package/src/client.ts +0 -63
  59. package/src/config-schema.ts +0 -50
  60. package/src/media.ts +0 -198
  61. package/src/monitor.ts +0 -195
  62. package/src/outbound.ts +0 -43
  63. package/src/policy.ts +0 -131
  64. package/src/probe.ts +0 -75
  65. package/src/reply-dispatcher.ts +0 -207
  66. package/src/runtime.ts +0 -14
  67. package/src/sdk-bridge.ts +0 -268
  68. package/src/send.ts +0 -363
  69. package/src/targets.ts +0 -101
  70. package/src/types.ts +0 -96
package/src/channel.ts DELETED
@@ -1,396 +0,0 @@
1
- import type {
2
- ChannelPlugin,
3
- ClawdbotConfig,
4
- } from "openclaw/plugin-sdk";
5
- import type { GroupToolPolicyConfig } from "openclaw/plugin-sdk/channel-policy";
6
- import {
7
- DEFAULT_ACCOUNT_ID,
8
- normalizeAccountId,
9
- } from "openclaw/plugin-sdk/account-id";
10
- import { PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk/channel-status";
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 {
22
- parseMeetTarget,
23
- looksLikeMeetId,
24
- formatMeetTarget,
25
- } from "./targets.js";
26
- import { sendMessageMeet } from "./send.js";
27
-
28
- const meta = {
29
- id: "meet",
30
- label: "Meet",
31
- selectionLabel: "Meet IM",
32
- docsPath: "/channels/meet",
33
- docsLabel: "meet",
34
- blurb: "Meet IM platform integration.",
35
- aliases: [],
36
- order: 80,
37
- };
38
-
39
- export const meetPlugin: ChannelPlugin<ResolvedMeetAccount> = {
40
- id: "meet",
41
- meta: {
42
- ...meta,
43
- },
44
- pairing: {
45
- idLabel: "meetUserId",
46
- normalizeAllowEntry: (entry) => entry.replace(/^(meet|user):/i, ""),
47
- notifyApproval: async ({ cfg, id }) => {
48
- await sendMessageMeet({
49
- cfg,
50
- to: id,
51
- text: PAIRING_APPROVED_MESSAGE,
52
- });
53
- },
54
- },
55
- capabilities: {
56
- chatTypes: ["direct", "channel"],
57
- polls: false,
58
- threads: false,
59
- media: true,
60
- reactions: false,
61
- edit: false,
62
- reply: false,
63
- },
64
- agentPrompt: {
65
- messageToolHints: () => [
66
- "- Meet targeting: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `user:<id>` or `channel:<id>`.",
67
- "- Meet mentions: use `<@USER_ID>` to mention users, `<@-1>` to mention everyone. Examples: `<@553> hello` or `<@-1> important announcement`.",
68
- "- Meet media: use `media` parameter to send images/files as attachments. DO NOT use Markdown image syntax like `![](path)` - Meet does not render it.",
69
- ],
70
- },
71
- groups: {
72
- resolveToolPolicy: ({
73
- cfg: _cfg,
74
- accountId: _accountId,
75
- groupId: _groupId,
76
- }): GroupToolPolicyConfig | undefined => {
77
- // 群组访问控制在 bot.ts 中处理,这里只返回工具策略配置
78
- // 不使用 { deny: ["*"] } 拒绝所有工具,否则会导致 tools: []
79
- return undefined
80
- },
81
- },
82
- reload: { configPrefixes: ["channels.meet"] },
83
- configSchema: {
84
- schema: {
85
- type: "object",
86
- additionalProperties: false,
87
- properties: {
88
- enabled: { type: "boolean" },
89
- defaultAccount: { type: "string" },
90
- apiEndpoint: { type: "string" },
91
- apiToken: { type: "string" },
92
- pollTimeout: { type: "integer", minimum: 1000, maximum: 300000 },
93
- pollLimit: { type: "integer", minimum: 1, maximum: 1000 },
94
- dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
95
- allowFrom: {
96
- type: "array",
97
- items: { oneOf: [{ type: "string" }, { type: "number" }] },
98
- },
99
- groupPolicy: {
100
- type: "string",
101
- enum: ["open", "allowlist", "disabled"],
102
- },
103
- groupAllowFrom: {
104
- type: "array",
105
- items: { oneOf: [{ type: "string" }, { type: "number" }] },
106
- },
107
- groups: {
108
- type: "object",
109
- additionalProperties: {
110
- type: "object",
111
- properties: {
112
- enabled: { type: "boolean" },
113
- name: { type: "string" },
114
- requireMention: { type: "boolean" },
115
- systemPrompt: { type: "string" },
116
- users: {
117
- type: "array",
118
- items: { oneOf: [{ type: "string" }, { type: "number" }] },
119
- },
120
- },
121
- },
122
- },
123
- requireMention: { type: "boolean" },
124
- historyLimit: { type: "integer", minimum: 0 },
125
- dmHistoryLimit: { type: "integer", minimum: 0 },
126
- textChunkLimit: { type: "integer", minimum: 1 },
127
- mediaMaxMb: { type: "number", minimum: 0 },
128
- accounts: {
129
- type: "object",
130
- additionalProperties: {
131
- type: "object",
132
- properties: {
133
- enabled: { type: "boolean" },
134
- name: { type: "string" },
135
- apiEndpoint: { type: "string" },
136
- apiToken: { type: "string" },
137
- pollTimeout: { type: "integer", minimum: 1000, maximum: 300000 },
138
- pollLimit: { type: "integer", minimum: 1, maximum: 1000 },
139
- },
140
- },
141
- },
142
- },
143
- },
144
- },
145
- config: {
146
- listAccountIds: (cfg) => listMeetAccountIds(cfg),
147
- resolveAccount: (cfg, accountId) => resolveMeetAccount({ cfg, accountId }),
148
- defaultAccountId: (cfg) => resolveDefaultMeetAccountId(cfg),
149
- setAccountEnabled: ({ cfg, accountId, enabled }) => {
150
- const isDefault = accountId === DEFAULT_ACCOUNT_ID;
151
-
152
- if (isDefault) {
153
- return {
154
- ...cfg,
155
- channels: {
156
- ...cfg.channels,
157
- meet: {
158
- ...cfg.channels?.meet,
159
- enabled,
160
- },
161
- },
162
- };
163
- }
164
-
165
- if (isFlatAccountConfig(cfg, accountId)) {
166
- const key = getFlatAccountKey(accountId);
167
- return {
168
- ...cfg,
169
- channels: {
170
- ...cfg.channels,
171
- [key]: {
172
- ...(cfg.channels?.[key] as Record<string, unknown> | undefined),
173
- enabled,
174
- },
175
- },
176
- };
177
- }
178
-
179
- const meetCfg = cfg.channels?.meet as MeetConfig | undefined;
180
- return {
181
- ...cfg,
182
- channels: {
183
- ...cfg.channels,
184
- meet: {
185
- ...meetCfg,
186
- accounts: {
187
- ...meetCfg?.accounts,
188
- [accountId]: {
189
- ...meetCfg?.accounts?.[accountId],
190
- enabled,
191
- },
192
- },
193
- },
194
- },
195
- };
196
- },
197
- deleteAccount: ({ cfg, accountId }) => {
198
- const isDefault = accountId === DEFAULT_ACCOUNT_ID;
199
-
200
- if (isDefault) {
201
- const next = { ...cfg } as ClawdbotConfig;
202
- const nextChannels = { ...cfg.channels };
203
- delete (nextChannels as Record<string, unknown>).meet;
204
- if (Object.keys(nextChannels).length > 0) {
205
- next.channels = nextChannels;
206
- } else {
207
- delete next.channels;
208
- }
209
- return next;
210
- }
211
-
212
- if (isFlatAccountConfig(cfg, accountId)) {
213
- const key = getFlatAccountKey(accountId);
214
- const next = { ...cfg } as ClawdbotConfig;
215
- const nextChannels = { ...cfg.channels };
216
- delete (nextChannels as Record<string, unknown>)[key];
217
- if (Object.keys(nextChannels).length > 0) {
218
- next.channels = nextChannels;
219
- } else {
220
- delete next.channels;
221
- }
222
- return next;
223
- }
224
-
225
- const meetCfg = cfg.channels?.meet as MeetConfig | undefined;
226
- const accounts = { ...meetCfg?.accounts };
227
- delete accounts[accountId];
228
-
229
- return {
230
- ...cfg,
231
- channels: {
232
- ...cfg.channels,
233
- meet: {
234
- ...meetCfg,
235
- accounts: Object.keys(accounts).length > 0 ? accounts : undefined,
236
- },
237
- },
238
- };
239
- },
240
- isConfigured: (account) => account.configured,
241
- describeAccount: (account) => ({
242
- accountId: account.accountId,
243
- enabled: account.enabled,
244
- configured: account.configured,
245
- name: account.name,
246
- apiEndpoint: account.apiEndpoint,
247
- }),
248
- resolveAllowFrom: ({ cfg, accountId }) => {
249
- const account = resolveMeetAccount({ cfg, accountId });
250
- return (account.config?.allowFrom ?? [])
251
- .map((entry) => String(entry).trim())
252
- .filter(Boolean);
253
- },
254
- formatAllowFrom: ({ allowFrom }) =>
255
- allowFrom
256
- .map((entry) => String(entry).trim())
257
- .filter(Boolean)
258
- .map((entry) => entry.toLowerCase()),
259
- },
260
- security: {
261
- collectWarnings: ({ cfg, accountId }) => {
262
- const account = resolveMeetAccount({ cfg, accountId });
263
- const meetCfg = account.config;
264
- const defaultGroupPolicy = (
265
- cfg.channels as Record<string, { groupPolicy?: string }> | undefined
266
- )?.defaults?.groupPolicy;
267
- const groupPolicy =
268
- meetCfg?.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
269
- if (groupPolicy !== "open") return [];
270
- return [
271
- `- Meet[${account.accountId}] groups: groupPolicy="open" allows any member to trigger (mention-gated). Set channels.meet.groupPolicy="allowlist" + channels.meet.groupAllowFrom to restrict senders.`,
272
- ];
273
- },
274
- },
275
- setup: {
276
- resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
277
- applyAccountConfig: ({ cfg, accountId }) => {
278
- const isDefault = !accountId || accountId === DEFAULT_ACCOUNT_ID;
279
-
280
- if (isDefault) {
281
- return {
282
- ...cfg,
283
- channels: {
284
- ...cfg.channels,
285
- meet: {
286
- ...cfg.channels?.meet,
287
- enabled: true,
288
- },
289
- },
290
- };
291
- }
292
-
293
- if (isFlatAccountConfig(cfg, accountId)) {
294
- const key = getFlatAccountKey(accountId);
295
- return {
296
- ...cfg,
297
- channels: {
298
- ...cfg.channels,
299
- [key]: {
300
- ...(cfg.channels?.[key] as Record<string, unknown> | undefined),
301
- enabled: true,
302
- },
303
- },
304
- };
305
- }
306
-
307
- const meetCfg = cfg.channels?.meet as MeetConfig | undefined;
308
- return {
309
- ...cfg,
310
- channels: {
311
- ...cfg.channels,
312
- meet: {
313
- ...meetCfg,
314
- accounts: {
315
- ...meetCfg?.accounts,
316
- [accountId]: {
317
- ...meetCfg?.accounts?.[accountId],
318
- enabled: true,
319
- },
320
- },
321
- },
322
- },
323
- };
324
- },
325
- },
326
- messaging: {
327
- normalizeTarget: (raw: string) => {
328
- const target = parseMeetTarget(raw);
329
- return target ? formatMeetTarget(target) : raw;
330
- },
331
- targetResolver: {
332
- looksLikeId: looksLikeMeetId,
333
- hint: "<userId|user:<id>|channel:<id>>",
334
- },
335
- },
336
- directory: {
337
- self: async () => null,
338
- listPeers: async () => [],
339
- listGroups: async () => [],
340
- listPeersLive: async () => [],
341
- listGroupsLive: async () => [],
342
- },
343
- outbound: meetOutbound,
344
- status: {
345
- defaultRuntime: {
346
- accountId: DEFAULT_ACCOUNT_ID,
347
- running: false,
348
- lastStartAt: null,
349
- lastStopAt: null,
350
- lastError: null,
351
- port: null,
352
- },
353
- buildChannelSummary: ({ snapshot }) => ({
354
- configured: snapshot.configured ?? false,
355
- running: snapshot.running ?? false,
356
- lastStartAt: snapshot.lastStartAt ?? null,
357
- lastStopAt: snapshot.lastStopAt ?? null,
358
- lastError: snapshot.lastError ?? null,
359
- port: snapshot.port ?? null,
360
- probe: snapshot.probe,
361
- lastProbeAt: snapshot.lastProbeAt ?? null,
362
- }),
363
- probeAccount: async ({ account }) => {
364
- return await probeMeet(account);
365
- },
366
- buildAccountSnapshot: ({ account, runtime, probe }) => ({
367
- accountId: account.accountId,
368
- enabled: account.enabled,
369
- configured: account.configured,
370
- name: account.name,
371
- apiEndpoint: account.apiEndpoint,
372
- running: runtime?.running ?? false,
373
- lastStartAt: runtime?.lastStartAt ?? null,
374
- lastStopAt: runtime?.lastStopAt ?? null,
375
- lastError: runtime?.lastError ?? null,
376
- port: runtime?.port ?? null,
377
- probe,
378
- }),
379
- },
380
- gateway: {
381
- startAccount: async (ctx) => {
382
- const { monitorMeetProvider } = await import("./monitor.js");
383
- resolveMeetAccount({
384
- cfg: ctx.cfg,
385
- accountId: ctx.accountId,
386
- });
387
- ctx.log?.info(`[${ctx.accountId}] starting`);
388
- return monitorMeetProvider({
389
- config: ctx.cfg,
390
- runtime: ctx.runtime,
391
- abortSignal: ctx.abortSignal,
392
- accountId: ctx.accountId,
393
- });
394
- },
395
- },
396
- };
package/src/client.ts DELETED
@@ -1,63 +0,0 @@
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
- }
@@ -1,50 +0,0 @@
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
- defaultAccount: z.string().optional(),
31
- apiEndpoint: z.string().optional(),
32
- token: z.string().optional(),
33
- apiToken: z.string().optional(),
34
- pollTimeout: z.number().min(1000).max(300000).optional().default(30000),
35
- pollLimit: z.number().min(1).max(1000).optional().default(100),
36
- logLevel: z.enum(["silent", "info"]).optional(),
37
- dmPolicy: z.enum(["open", "pairing", "allowlist"]).optional().default("pairing"),
38
- allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
39
- groupPolicy: z.enum(["open", "allowlist", "disabled"]).optional().default("allowlist"),
40
- groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
41
- groups: z.record(z.string(), MeetGroupConfigSchema).optional(),
42
- requireMention: z.boolean().optional().default(true),
43
- systemPrompt: z.string().optional(),
44
- channels: z.record(z.string(), MeetChannelConfigSchema).optional(),
45
- historyLimit: z.number().min(0).optional(),
46
- dmHistoryLimit: z.number().min(0).optional(),
47
- textChunkLimit: z.number().min(1).optional(),
48
- mediaMaxMb: z.number().min(0).optional().default(30),
49
- accounts: z.record(z.string(), MeetAccountConfigSchema).optional(),
50
- })
package/src/media.ts DELETED
@@ -1,198 +0,0 @@
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
- }