@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.
@@ -0,0 +1,86 @@
1
+ # Meet Markdown 语法快速参考
2
+
3
+ ## 内联样式
4
+
5
+ | 语法 | 效果 | 示例 |
6
+ |------|------|------|
7
+ | `**text**` | 粗体 | **粗体** |
8
+ | `__text__` | 粗体 | __粗体__ |
9
+ | `_text_` | 斜体 | _斜体_ |
10
+ | `~~text~~` | 删除线 | ~~删除线~~ |
11
+ | `` `code` `` | 行内代码 | `code` |
12
+ | `!!#RRGGBB text!!` | 颜色文本 | 彩色文本 |
13
+
14
+ ## 块级元素
15
+
16
+ ### 标题
17
+
18
+ ```markdown
19
+ # H1
20
+ ## H2
21
+ ### H3
22
+ #### H4
23
+ ##### H5
24
+ ###### H6
25
+ ```
26
+
27
+ ### 代码块
28
+
29
+ ````markdown
30
+ ```language
31
+ 代码内容
32
+ ```
33
+ ````
34
+
35
+ 常用语言:javascript, typescript, python, json, bash, markdown
36
+
37
+ ### 引用
38
+
39
+ ```markdown
40
+ > 引用内容
41
+ > 可以多行
42
+ ```
43
+
44
+ ### 列表
45
+
46
+ ```markdown
47
+ - 无序列表
48
+ - 项目
49
+
50
+ 1. 有序列表
51
+ 2. 项目
52
+ ```
53
+
54
+ ### 表格
55
+
56
+ ```markdown
57
+ | 列1 | 列2 |
58
+ |-----|-----|
59
+ | 值1 | 值2 |
60
+ ```
61
+
62
+ ### 分割线
63
+
64
+ ```markdown
65
+ ---
66
+ ```
67
+
68
+ ## 链接与图片
69
+
70
+ ```markdown
71
+ [链接文本](URL)
72
+ ![图片替代文本](图片URL)
73
+ ```
74
+
75
+ ## 颜色代码参考
76
+
77
+ 常用颜色:
78
+
79
+ | 颜色 | 代码 | 示例 |
80
+ |------|------|------|
81
+ | 红色 | `#FF0000` | `!!#FF0000 红色!!` |
82
+ | 绿色 | `#00FF00` | `!!#00FF00 绿色!!` |
83
+ | 蓝色 | `#0000FF` | `!!#0000FF 蓝色!!` |
84
+ | 橙色 | `#FF6600` | `!!#FF6600 橙色!!` |
85
+ | 紫色 | `#9900FF` | `!!#9900FF 紫色!!` |
86
+ | 灰色 | `#666666` | `!!#666666 灰色!!` |
@@ -0,0 +1,182 @@
1
+ import {
2
+ DEFAULT_ACCOUNT_ID,
3
+ normalizeAccountId,
4
+ createAccountListHelpers,
5
+ type ClawdbotConfig,
6
+ } from "openclaw/plugin-sdk"
7
+ import type { MeetConfig, MeetAccountConfig, ResolvedMeetAccount } from "./types.js"
8
+
9
+ const { listAccountIds: listNestedAccountIds, resolveDefaultAccountId: resolveNestedDefaultAccountId } = createAccountListHelpers("meet")
10
+
11
+ const MEET_PREFIX = "meet."
12
+
13
+ export function getFlatAccountKey(accountId: string): string {
14
+ return `${MEET_PREFIX}${accountId}`
15
+ }
16
+
17
+ function listFlatAccountIds(cfg: ClawdbotConfig): string[] {
18
+ const channels = cfg.channels
19
+ if (!channels || typeof channels !== "object") {
20
+ return []
21
+ }
22
+ const ids: string[] = []
23
+ for (const key of Object.keys(channels)) {
24
+ if (key.startsWith(MEET_PREFIX)) {
25
+ const accountId = key.slice(MEET_PREFIX.length)
26
+ if (accountId) {
27
+ ids.push(accountId)
28
+ }
29
+ }
30
+ }
31
+ return ids
32
+ }
33
+
34
+ function resolveFlatAccountConfig(
35
+ cfg: ClawdbotConfig,
36
+ accountId: string,
37
+ ): MeetAccountConfig | undefined {
38
+ const key = `${MEET_PREFIX}${accountId}`
39
+ const config = cfg.channels?.[key]
40
+ if (!config || typeof config !== "object") {
41
+ return undefined
42
+ }
43
+ return config as MeetAccountConfig
44
+ }
45
+
46
+ function listMeetAccountIds(cfg: ClawdbotConfig): string[] {
47
+ const nestedIds = listNestedAccountIds(cfg)
48
+ const flatIds = listFlatAccountIds(cfg)
49
+
50
+ if (flatIds.length > 0) {
51
+ const filteredNestedIds = nestedIds.filter(id => id !== DEFAULT_ACCOUNT_ID)
52
+ const allIds = new Set([...filteredNestedIds, ...flatIds])
53
+ return [...allIds].sort((a, b) => a.localeCompare(b))
54
+ }
55
+
56
+ if (nestedIds.length === 0) {
57
+ return [DEFAULT_ACCOUNT_ID]
58
+ }
59
+ return nestedIds
60
+ }
61
+
62
+ function resolveDefaultMeetAccountId(cfg: ClawdbotConfig): string {
63
+ const flatIds = listFlatAccountIds(cfg)
64
+ if (flatIds.length > 0) {
65
+ const nestedDefault = resolveNestedDefaultAccountId(cfg)
66
+ if (nestedDefault !== DEFAULT_ACCOUNT_ID && flatIds.includes(nestedDefault)) {
67
+ return nestedDefault
68
+ }
69
+ return flatIds[0] ?? DEFAULT_ACCOUNT_ID
70
+ }
71
+ return resolveNestedDefaultAccountId(cfg)
72
+ }
73
+
74
+ export { listMeetAccountIds, resolveDefaultMeetAccountId }
75
+
76
+ export function isFlatAccountConfig(cfg: ClawdbotConfig, accountId: string): boolean {
77
+ if (accountId === DEFAULT_ACCOUNT_ID) {
78
+ return false
79
+ }
80
+ return resolveFlatAccountConfig(cfg, accountId) !== undefined
81
+ }
82
+
83
+ function resolveAccountConfig(
84
+ cfg: ClawdbotConfig,
85
+ accountId: string,
86
+ ): MeetAccountConfig | undefined {
87
+ const nestedConfig = (() => {
88
+ const accounts = (cfg.channels?.meet as MeetConfig | undefined)?.accounts
89
+ if (!accounts || typeof accounts !== "object") {
90
+ return undefined
91
+ }
92
+ return accounts[accountId] as MeetAccountConfig | undefined
93
+ })()
94
+ if (nestedConfig) {
95
+ return nestedConfig
96
+ }
97
+ return resolveFlatAccountConfig(cfg, accountId)
98
+ }
99
+
100
+ function mergeMeetAccountConfig(
101
+ cfg: ClawdbotConfig,
102
+ accountId: string,
103
+ ): MeetConfig {
104
+ const meetCfg = cfg.channels?.meet as MeetConfig | undefined
105
+ const { accounts: _ignored, ...base } = meetCfg ?? {}
106
+ const account = resolveAccountConfig(cfg, accountId) ?? {}
107
+
108
+ const baseGroups = base.groups ?? {}
109
+ const accountGroups = (account as MeetConfig).groups ?? {}
110
+ const mergedGroups = { ...baseGroups, ...accountGroups }
111
+
112
+ return {
113
+ ...base,
114
+ ...account,
115
+ groups: Object.keys(mergedGroups).length > 0 ? mergedGroups : undefined,
116
+ } as MeetConfig
117
+ }
118
+
119
+ function resolveMeetToken(
120
+ cfg: ClawdbotConfig,
121
+ accountId: string,
122
+ ): {
123
+ token: string
124
+ endpoint: string
125
+ source: "env" | "config" | "none"
126
+ } {
127
+ const merged = mergeMeetAccountConfig(cfg, accountId)
128
+
129
+ const configToken = merged.token || merged.apiToken
130
+ if (configToken) {
131
+ return {
132
+ token: configToken,
133
+ endpoint: merged.apiEndpoint ?? process.env.MEET_API_ENDPOINT ?? "https://staging-meet-api.miyachat.com",
134
+ source: "config",
135
+ }
136
+ }
137
+
138
+ const envToken = process.env.MEET_API_TOKEN
139
+ if (envToken) {
140
+ return {
141
+ token: envToken,
142
+ endpoint: merged.apiEndpoint ?? process.env.MEET_API_ENDPOINT ?? "https://staging-meet-api.miyachat.com",
143
+ source: "env",
144
+ }
145
+ }
146
+
147
+ return {
148
+ token: "",
149
+ endpoint: "",
150
+ source: "none",
151
+ }
152
+ }
153
+
154
+ export function resolveMeetAccount(params: {
155
+ cfg: ClawdbotConfig
156
+ accountId?: string | null
157
+ }): ResolvedMeetAccount {
158
+ const accountId = normalizeAccountId(params.accountId)
159
+ const baseEnabled = params.cfg.channels?.meet?.enabled !== false
160
+ const merged = mergeMeetAccountConfig(params.cfg, accountId)
161
+ const accountEnabled = merged.enabled !== false
162
+ const enabled = baseEnabled && accountEnabled
163
+ const tokenResolution = resolveMeetToken(params.cfg, accountId)
164
+
165
+ return {
166
+ accountId,
167
+ enabled,
168
+ configured: tokenResolution.source !== "none",
169
+ name: (merged as MeetAccountConfig).name?.trim() || undefined,
170
+ apiEndpoint: tokenResolution.endpoint,
171
+ apiToken: tokenResolution.token,
172
+ config: merged,
173
+ }
174
+ }
175
+
176
+ export function listEnabledMeetAccounts(
177
+ cfg: ClawdbotConfig,
178
+ ): ResolvedMeetAccount[] {
179
+ return listMeetAccountIds(cfg)
180
+ .map((accountId) => resolveMeetAccount({ cfg, accountId }))
181
+ .filter((account) => account.enabled && account.configured)
182
+ }
package/src/bot.ts ADDED
@@ -0,0 +1,414 @@
1
+ import type { ClawdbotConfig, RuntimeEnv, ReplyPayload, HistoryEntry } from "openclaw/plugin-sdk"
2
+ import {
3
+ buildPendingHistoryContextFromMap,
4
+ clearHistoryEntriesIfEnabled,
5
+ recordPendingHistoryEntryIfEnabled,
6
+ DEFAULT_ACCOUNT_ID,
7
+ } from "openclaw/plugin-sdk"
8
+ import type { MeetBot, MsgContent } from "@meet-im/meet-bot-jssdk"
9
+ import type { ResolvedMeetAccount, MeetMessageContext } from "./types.js"
10
+ import { msgContentToContext, extractQuoteMessageMedia } from "./sdk-bridge.js"
11
+ import { getMeetRuntime } from "./runtime.js"
12
+ import { resolveMeetAllowlistMatch, resolveMeetGroupPolicy, resolveMeetGroupConfig, resolveMeetGroupUserPolicy } from "./policy.js"
13
+ import { sendMessageMeet } from "./send.js"
14
+ import { resolveMediaAttachments, setMediaDebugLogger } from "./media.js"
15
+
16
+ const DEFAULT_GROUP_SYSTEM_PROMPT =
17
+ "你正在 Meet 群组中对话。请保持回复简洁明了,适合群聊场景。如需回复特定用户,请使用 <@USER_ID> 格式提及对方,例如 <@553>。注意:Meet 不支持用反引号包住 Markdown 语法标记,描述语法时直接写符号,不要加反引号。"
18
+
19
+ const DEFAULT_DM_SYSTEM_PROMPT =
20
+ "你正在 Meet 私聊中对话。注意:Meet 不支持用反引号包住 Markdown 语法标记,描述语法时直接写符号,不要加反引号。"
21
+
22
+ function formatHistoryEntry(entry: HistoryEntry): string {
23
+ return `${entry.sender}: ${entry.body}`
24
+ }
25
+
26
+ export async function handleMeetMessage(params: {
27
+ cfg: ClawdbotConfig
28
+ msg: MsgContent
29
+ botUserId: string
30
+ runtime?: RuntimeEnv
31
+ accountId: string
32
+ account: ResolvedMeetAccount
33
+ bot: MeetBot
34
+ groupHistories: Map<string, HistoryEntry[]>
35
+ quoteMsgMap?: Record<string, MsgContent>
36
+ }): Promise<void> {
37
+ const { cfg, msg, botUserId, runtime, accountId, account, bot, groupHistories, quoteMsgMap } = params
38
+ const log = runtime?.log ?? console.log
39
+ const error = runtime?.error ?? console.error
40
+
41
+ let ctx: MeetMessageContext
42
+ try {
43
+ ctx = msgContentToContext(msg, botUserId, quoteMsgMap)
44
+ } catch (err) {
45
+ error(`[${accountId}]: failed to parse message: ${String(err)}`)
46
+ return
47
+ }
48
+
49
+ const isGroup = ctx.chatType === "channel"
50
+
51
+ if (ctx.senderId === botUserId) {
52
+ log(`[${accountId}]: skipping own message ${ctx.messageId}`)
53
+ return
54
+ }
55
+
56
+ log(
57
+ `[${accountId}]: received message from ${ctx.senderId} in ${ctx.chatId} (${ctx.chatType})`,
58
+ )
59
+
60
+ const meetCfg = account.config
61
+ const dmPolicy = meetCfg.dmPolicy ?? "pairing"
62
+ const allowFrom = meetCfg.allowFrom ?? []
63
+ const historyLimit = isGroup
64
+ ? Math.max(0, meetCfg.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? 20)
65
+ : Math.max(0, meetCfg.dmHistoryLimit ?? 0)
66
+ const speaker = ctx.senderName ?? ctx.senderId
67
+ // Discord 做法:文字优先,无文字时用媒体占位符
68
+ const messageBody = ctx.content.trim()
69
+ ? `${speaker}: ${ctx.content.trim()}`
70
+ : `${speaker}: ${ctx.placeholder || ""}`
71
+
72
+ const pendingEntry: HistoryEntry = {
73
+ sender: speaker,
74
+ body: ctx.content.trim() || ctx.placeholder || "",
75
+ timestamp: ctx.timestamp,
76
+ messageId: ctx.messageId,
77
+ }
78
+
79
+ try {
80
+ const core = getMeetRuntime()
81
+
82
+ if (!isGroup) {
83
+ // pairing 模式:需要从 pairing store 读取已授权用户列表
84
+ // allowlist 模式:只使用配置中的 allowFrom
85
+ let effectiveAllowFrom = allowFrom
86
+ if (dmPolicy === "pairing") {
87
+ try {
88
+ const storeAllowFrom = await core.channel.pairing.readAllowFromStore({
89
+ channel: "meet",
90
+ accountId,
91
+ })
92
+ // 合并配置中的 allowFrom 和 store 中的授权列表
93
+ effectiveAllowFrom = [...allowFrom, ...storeAllowFrom]
94
+ } catch (err) {
95
+ error(`[${accountId}]: failed to read pairing store: ${String(err)}`)
96
+ }
97
+ }
98
+
99
+ const dmAllowed = resolveMeetAllowlistMatch({
100
+ allowFrom: effectiveAllowFrom,
101
+ senderId: ctx.senderId,
102
+ }).allowed
103
+
104
+ // pairing 模式:创建配对请求并发送授权码给用户
105
+ // allowlist 模式:直接拒绝未授权用户
106
+ if (dmPolicy !== "open" && !dmAllowed) {
107
+ if (dmPolicy === "pairing") {
108
+ log(`[${accountId}]: pairing request from ${ctx.senderId}`)
109
+
110
+ // 创建或更新配对请求
111
+ const { code, created } = await core.channel.pairing.upsertPairingRequest({
112
+ channel: "meet",
113
+ id: ctx.senderId,
114
+ accountId,
115
+ meta: {
116
+ name: ctx.senderName,
117
+ },
118
+ })
119
+
120
+ // 发送配对码给用户(新创建或已存在都发送)
121
+ if (code) {
122
+ const accountArg = accountId === DEFAULT_ACCOUNT_ID ? "" : ` --account ${accountId}`
123
+ const lines = [
124
+ "OpenClaw: 尚未授权访问。",
125
+ "",
126
+ `您的 Meet 用户 ID: ${ctx.senderId}`,
127
+ "",
128
+ `配对码: ${code}`,
129
+ "",
130
+ "请联系机器人管理员审批:",
131
+ `openclaw pairing approve meet ${code}${accountArg}`,
132
+ ]
133
+ // 如果是已存在的请求,添加提示
134
+ if (!created) {
135
+ lines.splice(2, 0, "(您的配对请求已在等待审批中)")
136
+ }
137
+ const replyText = lines.join("\n")
138
+ try {
139
+ await sendMessageMeet({
140
+ cfg,
141
+ to: `user:${ctx.senderId}`,
142
+ text: replyText,
143
+ accountId,
144
+ })
145
+ } catch (err) {
146
+ error(`[${accountId}]: failed to send pairing reply to ${ctx.senderId}: ${String(err)}`)
147
+ }
148
+ }
149
+ } else {
150
+ log(
151
+ `[${accountId}]: blocked unauthorized sender ${ctx.senderId} (dmPolicy=${dmPolicy})`,
152
+ )
153
+ }
154
+ return
155
+ }
156
+ }
157
+
158
+ if (isGroup) {
159
+ const groupPolicy = resolveMeetGroupPolicy({
160
+ groupPolicy: meetCfg.groupPolicy,
161
+ groupAllowFrom: meetCfg.groupAllowFrom ?? [],
162
+ chatId: ctx.chatId,
163
+ groups: meetCfg.groups,
164
+ })
165
+
166
+ if (!groupPolicy.allowed) {
167
+ log(
168
+ `[${accountId}]: group ${ctx.chatId} not allowed (groupPolicy=${meetCfg.groupPolicy})`,
169
+ )
170
+ return
171
+ }
172
+
173
+ const groupConfig = resolveMeetGroupConfig({
174
+ meetConfig: meetCfg,
175
+ chatId: ctx.chatId,
176
+ })
177
+
178
+ const groupUserPolicy = resolveMeetGroupUserPolicy({
179
+ groupConfig: groupConfig.groupConfig,
180
+ senderId: ctx.senderId,
181
+ })
182
+
183
+ if (!groupUserPolicy.allowed) {
184
+ log(`[${accountId}]: user ${ctx.senderId} not allowed in group ${ctx.chatId}`)
185
+ return
186
+ }
187
+
188
+ if (groupConfig.requireMention && !ctx.mentionedBot) {
189
+ log(`[${accountId}]: message in group ${ctx.chatId} skipped (mention required)`)
190
+ recordPendingHistoryEntryIfEnabled({
191
+ historyMap: groupHistories,
192
+ historyKey: ctx.chatId,
193
+ entry: pendingEntry,
194
+ limit: historyLimit,
195
+ })
196
+ return
197
+ }
198
+ }
199
+
200
+ const meetFrom = `meet:${ctx.senderId}`
201
+ const meetTo = isGroup ? `channel:${ctx.chatId}` : `user:${ctx.senderId}`
202
+
203
+ const peerId = isGroup ? ctx.chatId : ctx.senderId
204
+
205
+ const route = core.channel.routing.resolveAgentRoute({
206
+ cfg,
207
+ channel: "meet",
208
+ accountId,
209
+ peer: {
210
+ kind: isGroup ? "group" : "direct",
211
+ id: peerId,
212
+ },
213
+ })
214
+
215
+ // 处理媒体附件
216
+ let mediaContext = ""
217
+ let mediaPaths: string[] = []
218
+ if (ctx.media && ctx.media.length > 0) {
219
+ log(`[${accountId}]: processing ${ctx.media.length} media attachment(s)`)
220
+ // 初始化媒体调试日志
221
+ setMediaDebugLogger(log, error)
222
+ try {
223
+ const maxBytes = meetCfg.mediaMaxMb ? meetCfg.mediaMaxMb * 1024 * 1024 : undefined
224
+ const mediaInfos = await resolveMediaAttachments({
225
+ accountId,
226
+ attachments: ctx.media,
227
+ sessionInfo: {
228
+ firstId: ctx.sessionInfo.firstID,
229
+ secondId: ctx.sessionInfo.secondID,
230
+ sessionType: ctx.sessionInfo.sessionType,
231
+ companyId: ctx.sessionInfo.companyID,
232
+ },
233
+ seqId: Number(ctx.messageId),
234
+ maxBytes,
235
+ })
236
+ log(`[${accountId}]: resolved ${mediaInfos.length} media, paths=${mediaInfos.map(m => m.path).join(",")}`)
237
+ mediaPaths = mediaInfos.map((m) => m.path)
238
+ if (mediaInfos.length > 0) {
239
+ // 为 BodyForAgent 生成详细媒体描述(包含路径)
240
+ mediaContext = "\n\n" + mediaInfos.map((m) => {
241
+ const typeLabel = m.contentType?.startsWith("image/") ? "<media:image>"
242
+ : m.contentType?.startsWith("video/") ? "<media:video>"
243
+ : m.contentType?.startsWith("audio/") ? "<media:audio>"
244
+ : "<media:document>"
245
+ return `${typeLabel}: ${m.path}`
246
+ }).join("\n")
247
+ }
248
+ } catch (err) {
249
+ error(`[${accountId}]: failed to resolve media: ${String(err)}`)
250
+ }
251
+ }
252
+
253
+ // 处理引用消息的媒体附件
254
+ if (quoteMsgMap && ctx.replyContext) {
255
+ const quoteMedia = extractQuoteMessageMedia(msg, quoteMsgMap)
256
+ if (quoteMedia && quoteMedia.length > 0) {
257
+ log(`[${accountId}]: processing ${quoteMedia.length} quote message media attachment(s)`)
258
+ try {
259
+ const maxBytes = meetCfg.mediaMaxMb ? meetCfg.mediaMaxMb * 1024 * 1024 : undefined
260
+ const quoteMediaInfos = await resolveMediaAttachments({
261
+ accountId,
262
+ attachments: quoteMedia,
263
+ sessionInfo: {
264
+ firstId: ctx.sessionInfo.firstID,
265
+ secondId: ctx.sessionInfo.secondID,
266
+ sessionType: ctx.sessionInfo.sessionType,
267
+ companyId: ctx.sessionInfo.companyID,
268
+ },
269
+ seqId: Number(ctx.replyContext.messageId),
270
+ maxBytes,
271
+ })
272
+ log(`[${accountId}]: resolved ${quoteMediaInfos.length} quote media, paths=${quoteMediaInfos.map(m => m.path).join(",")}`)
273
+ // 将引用消息的媒体路径合并到 mediaPaths
274
+ mediaPaths = [...mediaPaths, ...quoteMediaInfos.map((m) => m.path)]
275
+ } catch (err) {
276
+ error(`[${accountId}]: failed to resolve quote media: ${String(err)}`)
277
+ }
278
+ }
279
+ }
280
+
281
+ // 构建最终的消息内容
282
+ // Discord 做法:文字优先,无文字时用媒体占位符
283
+ // 媒体路径通过 MediaPaths 传递,mediaContext 仅作为 BodyForAgent 的补充描述
284
+ const finalContent = ctx.content.trim()
285
+ ? `${ctx.content.trim()}${mediaContext}`
286
+ : (ctx.placeholder || "") + mediaContext
287
+
288
+ // Discord 做法:跳过空内容消息
289
+ if (!finalContent.trim() && mediaPaths.length === 0) {
290
+ log(`[${accountId}]: skip message ${ctx.messageId} (empty content)`)
291
+ return
292
+ }
293
+
294
+ const preview = finalContent.replace(/\s+/g, " ").slice(0, 160)
295
+ const inboundLabel = isGroup
296
+ ? `Meet[${accountId}] message in group ${ctx.chatId}`
297
+ : `Meet[${accountId}] DM from ${ctx.senderId}`
298
+
299
+ core.system.enqueueSystemEvent(`${inboundLabel}: ${preview}`, {
300
+ sessionKey: route.sessionKey,
301
+ contextKey: `meet:message:${ctx.chatId}:${ctx.messageId}`,
302
+ })
303
+
304
+ const envelopeFrom = isGroup ? `${ctx.chatId}:${ctx.senderId}` : ctx.senderId
305
+
306
+ const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg)
307
+
308
+ // 将当前消息添加到历史(在确认要处理消息之后)
309
+ const historyEntries = isGroup && historyLimit > 0
310
+ ? (() => {
311
+ const entries = groupHistories.get(ctx.chatId) ?? []
312
+ entries.push(pendingEntry)
313
+ while (entries.length > historyLimit) {
314
+ entries.shift()
315
+ }
316
+ groupHistories.set(ctx.chatId, entries)
317
+ return entries
318
+ })()
319
+ : []
320
+
321
+ const bodyWithContext = buildPendingHistoryContextFromMap({
322
+ historyMap: groupHistories,
323
+ historyKey: ctx.chatId,
324
+ limit: historyLimit,
325
+ currentMessage: messageBody,
326
+ formatEntry: formatHistoryEntry,
327
+ })
328
+
329
+ const body = core.channel.reply.formatAgentEnvelope({
330
+ channel: "Meet",
331
+ from: envelopeFrom,
332
+ timestamp: new Date(),
333
+ envelope: envelopeOptions,
334
+ body: bodyWithContext,
335
+ })
336
+
337
+ const inboundHistory =
338
+ isGroup && historyLimit > 0
339
+ ? historyEntries.map((entry) => ({
340
+ sender: entry.sender,
341
+ body: entry.body,
342
+ timestamp: entry.timestamp,
343
+ }))
344
+ : undefined
345
+
346
+ const channelConfig = isGroup ? meetCfg.channels?.[ctx.chatId] : undefined
347
+ const groupConfig = isGroup ? resolveMeetGroupConfig({ meetConfig: meetCfg, chatId: ctx.chatId }) : undefined
348
+ const systemPrompt = isGroup
349
+ ? (groupConfig?.systemPrompt ?? channelConfig?.systemPrompt ?? meetCfg.systemPrompt ?? DEFAULT_GROUP_SYSTEM_PROMPT)
350
+ : (meetCfg.systemPrompt ?? DEFAULT_DM_SYSTEM_PROMPT)
351
+
352
+ const inboundCtx = core.channel.reply.finalizeInboundContext({
353
+ Body: body,
354
+ BodyForAgent: finalContent,
355
+ RawBody: ctx.content,
356
+ CommandBody: ctx.content,
357
+ From: meetFrom,
358
+ To: meetTo,
359
+ SessionKey: route.sessionKey,
360
+ AccountId: route.accountId,
361
+ ChatType: isGroup ? "group" : "direct",
362
+ GroupSubject: isGroup ? ctx.chatId : undefined,
363
+ GroupSystemPrompt: systemPrompt,
364
+ SenderName: ctx.senderName,
365
+ SenderId: ctx.senderId,
366
+ Provider: "meet" as const,
367
+ Surface: "meet" as const,
368
+ MessageSid: ctx.messageId,
369
+ Timestamp: ctx.timestamp ?? Date.now(),
370
+ WasMentioned: ctx.mentionedBot,
371
+ ReplyToId: ctx.replyContext?.messageId,
372
+ ReplyToBody: ctx.replyContext?.content,
373
+ ReplyToSender: ctx.replyContext?.senderId,
374
+ InboundHistory: inboundHistory,
375
+ CommandAuthorized: true,
376
+ OriginatingChannel: "meet" as const,
377
+ OriginatingTo: meetTo,
378
+ MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
379
+ })
380
+
381
+ const { createMeetReplyDispatcher } = await import("./reply-dispatcher.js")
382
+ const { dispatcher, replyOptions, markDispatchIdle } = await createMeetReplyDispatcher({
383
+ cfg,
384
+ agentId: route.agentId,
385
+ runtime: runtime as RuntimeEnv,
386
+ chatId: ctx.chatId,
387
+ replyToMessageId: ctx.messageId,
388
+ accountId,
389
+ bot,
390
+ botUserId,
391
+ })
392
+
393
+ log(`[${accountId}]: dispatching to AI agent=${route.agentId} session=${route.sessionKey} history=${inboundHistory?.length ?? 0}`)
394
+
395
+ await core.channel.reply.dispatchReplyFromConfig({
396
+ ctx: inboundCtx,
397
+ cfg,
398
+ dispatcher,
399
+ replyOptions,
400
+ })
401
+
402
+ log(`[${accountId}]: AI response completed for message ${ctx.messageId}`)
403
+
404
+ markDispatchIdle()
405
+
406
+ clearHistoryEntriesIfEnabled({
407
+ historyMap: groupHistories,
408
+ historyKey: ctx.chatId,
409
+ limit: historyLimit,
410
+ })
411
+ } catch (err) {
412
+ error(`[${accountId}]: error processing message: ${String(err)}`)
413
+ }
414
+ }