@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
|
@@ -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
|
+

|
|
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 灰色!!` |
|
package/src/accounts.ts
ADDED
|
@@ -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
|
+
}
|