@meet-im/meet 2.0.6 → 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.
- package/dist/account-inspect-api.d.ts +2 -0
- package/dist/account-inspect-api.js +4 -0
- package/dist/channel-plugin-api.d.ts +1 -0
- package/dist/channel-plugin-api.js +3 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +43 -0
- package/dist/monitor-api.d.ts +1 -0
- package/dist/monitor-api.js +1 -0
- package/dist/probe-api.d.ts +1 -0
- package/dist/probe-api.js +1 -0
- package/dist/runtime-setter-api.d.ts +1 -0
- package/dist/runtime-setter-api.js +2 -0
- package/dist/send-api.d.ts +1 -0
- package/dist/send-api.js +1 -0
- package/dist/src/account-inspect.d.ts +5 -0
- package/dist/src/account-inspect.js +9 -0
- package/dist/src/accounts.d.ts +12 -0
- package/dist/src/accounts.js +134 -0
- package/dist/src/bot.d.ts +15 -0
- package/dist/src/bot.js +355 -0
- package/dist/src/channel.d.ts +3 -0
- package/dist/src/channel.js +402 -0
- package/dist/src/client.d.ts +8 -0
- package/dist/src/client.js +49 -0
- package/dist/src/config-schema.d.ts +82 -0
- package/dist/src/config-schema.js +46 -0
- package/dist/src/media.d.ts +57 -0
- package/dist/src/media.js +140 -0
- package/dist/src/monitor.d.ts +9 -0
- package/dist/src/monitor.js +153 -0
- package/dist/src/outbound.d.ts +2 -0
- package/dist/src/outbound.js +34 -0
- package/dist/src/policy.d.ts +30 -0
- package/dist/src/policy.js +78 -0
- package/dist/src/probe.d.ts +10 -0
- package/dist/src/probe.js +56 -0
- package/dist/src/reply-dispatcher.d.ts +29 -0
- package/dist/src/reply-dispatcher.js +173 -0
- package/dist/src/runtime.d.ts +11 -0
- package/dist/src/runtime.js +6 -0
- package/dist/src/sdk-bridge.d.ts +21 -0
- package/dist/src/sdk-bridge.js +214 -0
- package/dist/src/send.d.ts +60 -0
- package/dist/src/send.js +317 -0
- package/dist/src/targets.d.ts +15 -0
- package/dist/src/targets.js +63 -0
- package/dist/src/types.d.ts +76 -0
- package/dist/src/types.js +1 -0
- package/dist/vitest.config.d.ts +8 -0
- package/dist/vitest.config.js +7 -0
- package/openclaw.plugin.json +116 -0
- package/package.json +18 -17
- package/index.ts +0 -26
- package/src/accounts.ts +0 -182
- package/src/bot.ts +0 -418
- package/src/channel.ts +0 -396
- package/src/client.ts +0 -63
- package/src/config-schema.ts +0 -50
- package/src/media.ts +0 -198
- package/src/monitor.ts +0 -195
- package/src/outbound.ts +0 -43
- package/src/policy.ts +0 -131
- package/src/probe.ts +0 -75
- package/src/reply-dispatcher.ts +0 -207
- package/src/runtime.ts +0 -14
- package/src/sdk-bridge.ts +0 -268
- package/src/send.ts +0 -383
- package/src/targets.ts +0 -101
- package/src/types.ts +0 -96
package/src/sdk-bridge.ts
DELETED
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
import type { MsgContent, SessionInfo, SessionType } from "@meet-im/meet-bot-jssdk"
|
|
2
|
-
import type { MeetMessageContext, MeetReplyContext, MeetMediaAttachment } from "./types.js"
|
|
3
|
-
|
|
4
|
-
// TODO: 从 SDK 导入,等待 SDK 发布
|
|
5
|
-
export type QuoteMsgMap = Record<string, MsgContent>
|
|
6
|
-
|
|
7
|
-
export function mapSessionType(sessionType: SessionType): "direct" | "channel" {
|
|
8
|
-
return sessionType === 1 ? "direct" : "channel"
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* 生成与 Discord 一致的媒体占位符
|
|
13
|
-
* 格式:<media:image> (2 images) 或 <media:document> (3 files)
|
|
14
|
-
*/
|
|
15
|
-
function buildMediaPlaceholder(media: MeetMediaAttachment[]): string {
|
|
16
|
-
const images = media.filter((m) => m.mimeType?.startsWith("image/"))
|
|
17
|
-
const videos = media.filter((m) => m.mimeType?.startsWith("video/"))
|
|
18
|
-
const audios = media.filter((m) => m.mimeType?.startsWith("audio/"))
|
|
19
|
-
const documents = media.filter(
|
|
20
|
-
(m) => !m.mimeType?.startsWith("image/") &&
|
|
21
|
-
!m.mimeType?.startsWith("video/") &&
|
|
22
|
-
!m.mimeType?.startsWith("audio/")
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
const parts: string[] = []
|
|
26
|
-
|
|
27
|
-
if (images.length > 0) {
|
|
28
|
-
const label = images.length === 1 ? "image" : "images"
|
|
29
|
-
parts.push(`<media:image> (${images.length} ${label})`)
|
|
30
|
-
}
|
|
31
|
-
if (videos.length > 0) {
|
|
32
|
-
const label = videos.length === 1 ? "video" : "videos"
|
|
33
|
-
parts.push(`<media:video> (${videos.length} ${label})`)
|
|
34
|
-
}
|
|
35
|
-
if (audios.length > 0) {
|
|
36
|
-
const label = audios.length === 1 ? "audio" : "audios"
|
|
37
|
-
parts.push(`<media:audio> (${audios.length} ${label})`)
|
|
38
|
-
}
|
|
39
|
-
if (documents.length > 0) {
|
|
40
|
-
const label = documents.length === 1 ? "document" : "documents"
|
|
41
|
-
parts.push(`<media:document> (${documents.length} ${label})`)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return parts.join("\n")
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* 构建引用消息查找 Key
|
|
49
|
-
* 群聊: firstID+secondID:quoteSeqID
|
|
50
|
-
* 私聊: min(firstID,secondID):max(firstID,secondID):quoteSeqID
|
|
51
|
-
*/
|
|
52
|
-
export function buildQuoteMsgKey(sessionInfo: SessionInfo, quoteSeqID: number): string {
|
|
53
|
-
const { firstID, secondID, sessionType } = sessionInfo
|
|
54
|
-
if (sessionType === 3) {
|
|
55
|
-
return `${firstID}+${secondID}:${quoteSeqID}`
|
|
56
|
-
} else {
|
|
57
|
-
const [minId, maxId] = firstID < secondID ? [firstID, secondID] : [secondID, firstID]
|
|
58
|
-
return `${minId}:${maxId}:${quoteSeqID}`
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* 解析引用消息
|
|
64
|
-
*/
|
|
65
|
-
export function resolveQuoteMessage(
|
|
66
|
-
msg: MsgContent,
|
|
67
|
-
quoteMsgMap: QuoteMsgMap
|
|
68
|
-
): MeetReplyContext | undefined {
|
|
69
|
-
// 使用 quoteSeqID 字段(SDK V2)
|
|
70
|
-
const quoteSeqID = (msg as { quoteSeqID?: number }).quoteSeqID
|
|
71
|
-
if (!quoteSeqID || !msg.sessionInfo) {
|
|
72
|
-
return undefined
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const key = buildQuoteMsgKey(msg.sessionInfo, quoteSeqID)
|
|
76
|
-
const quoteMsg = quoteMsgMap[key]
|
|
77
|
-
|
|
78
|
-
if (!quoteMsg) {
|
|
79
|
-
return undefined
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// 对齐 Discord: 引用消息的 body 包含媒体占位符
|
|
83
|
-
const body = buildQuoteMessageBody(quoteMsg)
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
messageId: String(quoteMsg.seqId ?? 0),
|
|
87
|
-
senderId: quoteMsg.fromUid ? String(quoteMsg.fromUid) : undefined,
|
|
88
|
-
content: body,
|
|
89
|
-
timestamp: quoteMsg.timestamp,
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* 从引用消息中提取媒体附件信息(用于下载)
|
|
95
|
-
*/
|
|
96
|
-
export function extractQuoteMessageMedia(
|
|
97
|
-
msg: MsgContent,
|
|
98
|
-
quoteMsgMap: QuoteMsgMap
|
|
99
|
-
): MeetMediaAttachment[] | undefined {
|
|
100
|
-
const quoteSeqID = (msg as { quoteSeqID?: number }).quoteSeqID
|
|
101
|
-
if (!quoteSeqID || !msg.sessionInfo) {
|
|
102
|
-
return undefined
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const key = buildQuoteMsgKey(msg.sessionInfo, quoteSeqID)
|
|
106
|
-
const quoteMsg = quoteMsgMap[key]
|
|
107
|
-
|
|
108
|
-
if (!quoteMsg) {
|
|
109
|
-
return undefined
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return extractMediaAttachments(quoteMsg)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* 从消息中提取媒体附件信息
|
|
117
|
-
*/
|
|
118
|
-
function extractMediaAttachments(msg: MsgContent): MeetMediaAttachment[] | undefined {
|
|
119
|
-
const extraInfo = msg.extraInfo
|
|
120
|
-
if (!extraInfo) {
|
|
121
|
-
return undefined
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const attachments: MeetMediaAttachment[] = []
|
|
125
|
-
|
|
126
|
-
// 单附件
|
|
127
|
-
if (extraInfo.attechmentInfo) {
|
|
128
|
-
const att = extraInfo.attechmentInfo
|
|
129
|
-
attachments.push({
|
|
130
|
-
fileId: att.fileID,
|
|
131
|
-
fileName: att.fileName,
|
|
132
|
-
fileSize: att.fileSize,
|
|
133
|
-
mimeType: att.mimeType,
|
|
134
|
-
fileUrl: att.fileUrl,
|
|
135
|
-
})
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// 多附件
|
|
139
|
-
if (extraInfo.attechmentInfos && Array.isArray(extraInfo.attechmentInfos)) {
|
|
140
|
-
for (const att of extraInfo.attechmentInfos) {
|
|
141
|
-
attachments.push({
|
|
142
|
-
fileId: att.fileID,
|
|
143
|
-
fileName: att.fileName,
|
|
144
|
-
fileSize: att.fileSize,
|
|
145
|
-
mimeType: att.mimeType,
|
|
146
|
-
fileUrl: att.fileUrl,
|
|
147
|
-
})
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return attachments.length > 0 ? attachments : undefined
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* 构建引用消息的 body(对齐 Discord)
|
|
156
|
-
* 包含文本内容 + 媒体占位符
|
|
157
|
-
*/
|
|
158
|
-
function buildQuoteMessageBody(quoteMsg: MsgContent): string {
|
|
159
|
-
const parts: string[] = []
|
|
160
|
-
|
|
161
|
-
// 添加文本内容
|
|
162
|
-
const text = quoteMsg.content?.trim()
|
|
163
|
-
if (text) {
|
|
164
|
-
parts.push(text)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// 添加媒体占位符
|
|
168
|
-
const media = extractMediaAttachments(quoteMsg)
|
|
169
|
-
if (media && media.length > 0) {
|
|
170
|
-
const placeholder = buildMediaPlaceholder(media)
|
|
171
|
-
if (placeholder) {
|
|
172
|
-
parts.push(placeholder)
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return parts.join("\n")
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export function msgContentToContext(
|
|
180
|
-
msg: MsgContent,
|
|
181
|
-
botUserId: string,
|
|
182
|
-
quoteMsgMap: QuoteMsgMap = {},
|
|
183
|
-
): MeetMessageContext {
|
|
184
|
-
if (!msg.sessionInfo) {
|
|
185
|
-
throw new Error("MsgContent missing sessionInfo")
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const chatType = mapSessionType(msg.sessionInfo.sessionType)
|
|
189
|
-
const chatId =
|
|
190
|
-
chatType === "direct"
|
|
191
|
-
? `user:${msg.fromUid}`
|
|
192
|
-
: `channel:${msg.sessionInfo.secondID}`
|
|
193
|
-
|
|
194
|
-
const mentionedBot = msg.atIds?.includes(Number(botUserId)) ?? false
|
|
195
|
-
const replyContext = resolveQuoteMessage(msg, quoteMsgMap)
|
|
196
|
-
const media = extractMediaAttachments(msg)
|
|
197
|
-
|
|
198
|
-
// 检测消息类型,生成与 Discord 一致的占位符
|
|
199
|
-
let contentType = "text"
|
|
200
|
-
let placeholder: string | undefined
|
|
201
|
-
if (media && media.length > 0) {
|
|
202
|
-
contentType = "media"
|
|
203
|
-
placeholder = buildMediaPlaceholder(media)
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return {
|
|
207
|
-
chatId,
|
|
208
|
-
messageId: String(msg.seqId ?? 0),
|
|
209
|
-
senderId: String(msg.fromUid ?? 0),
|
|
210
|
-
senderOpenId: String(msg.fromUid ?? 0),
|
|
211
|
-
chatType,
|
|
212
|
-
mentionedBot,
|
|
213
|
-
content: msg.content ?? "",
|
|
214
|
-
contentType,
|
|
215
|
-
placeholder,
|
|
216
|
-
sessionInfo: msg.sessionInfo,
|
|
217
|
-
timestamp: msg.timestamp,
|
|
218
|
-
atIds: msg.atIds,
|
|
219
|
-
replyContext,
|
|
220
|
-
media,
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
export function parseTargetToSessionInfo(
|
|
225
|
-
target: string,
|
|
226
|
-
botUserId: number,
|
|
227
|
-
): SessionInfo {
|
|
228
|
-
const userMatch = target.match(/^user:(\d+)$/)
|
|
229
|
-
if (userMatch) {
|
|
230
|
-
return {
|
|
231
|
-
firstID: botUserId,
|
|
232
|
-
secondID: Number(userMatch[1]),
|
|
233
|
-
sessionType: 1,
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const channelMatch = target.match(/^channel:(\d+)$/)
|
|
238
|
-
if (channelMatch) {
|
|
239
|
-
return {
|
|
240
|
-
firstID: 1,
|
|
241
|
-
secondID: Number(channelMatch[1]),
|
|
242
|
-
sessionType: 3,
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const numericMatch = target.match(/^(\d+)$/)
|
|
247
|
-
if (numericMatch) {
|
|
248
|
-
return {
|
|
249
|
-
firstID: botUserId,
|
|
250
|
-
secondID: Number(numericMatch[1]),
|
|
251
|
-
sessionType: 1,
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
throw new Error(`Invalid Meet target: ${target}`)
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
export function buildMeetTarget(
|
|
259
|
-
sessionInfo: SessionInfo,
|
|
260
|
-
botUserId: number,
|
|
261
|
-
): string {
|
|
262
|
-
if (sessionInfo.sessionType === 1) {
|
|
263
|
-
return sessionInfo.secondID === botUserId
|
|
264
|
-
? `user:${sessionInfo.firstID}`
|
|
265
|
-
: `user:${sessionInfo.secondID}`
|
|
266
|
-
}
|
|
267
|
-
return `channel:${sessionInfo.secondID}`
|
|
268
|
-
}
|
package/src/send.ts
DELETED
|
@@ -1,383 +0,0 @@
|
|
|
1
|
-
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
2
|
-
import type { MeetMediaInfo } from "./types.js";
|
|
3
|
-
import type { UploadProgress as SdkUploadProgress } from "@meet-im/meet-bot-jssdk";
|
|
4
|
-
import { resolveMeetAccount } from "./accounts.js";
|
|
5
|
-
import { getMeetClient, createMeetClient } from "./client.js";
|
|
6
|
-
import { parseTargetToSessionInfo } from "./sdk-bridge.js";
|
|
7
|
-
import { getMeetRuntime } from "./runtime.js";
|
|
8
|
-
|
|
9
|
-
const MENTION_PATTERN = /<@(-?\d+)>|@(-?\d+)(?![\d])/g;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* 根据文件扩展名推断 MIME 类型
|
|
13
|
-
* 支持常见图片格式,包括现代格式如 avif, webp, heic
|
|
14
|
-
*/
|
|
15
|
-
export function inferContentTypeFromFileName(fileName: string): string | undefined {
|
|
16
|
-
const ext = fileName.toLowerCase().split(".").pop();
|
|
17
|
-
if (!ext) return undefined;
|
|
18
|
-
|
|
19
|
-
const mimeMap: Record<string, string> = {
|
|
20
|
-
// 常见图片格式
|
|
21
|
-
jpg: "image/jpeg",
|
|
22
|
-
jpeg: "image/jpeg",
|
|
23
|
-
png: "image/png",
|
|
24
|
-
gif: "image/gif",
|
|
25
|
-
webp: "image/webp",
|
|
26
|
-
avif: "image/avif",
|
|
27
|
-
heic: "image/heic",
|
|
28
|
-
heif: "image/heif",
|
|
29
|
-
bmp: "image/bmp",
|
|
30
|
-
ico: "image/x-icon",
|
|
31
|
-
svg: "image/svg+xml",
|
|
32
|
-
tiff: "image/tiff",
|
|
33
|
-
tif: "image/tiff",
|
|
34
|
-
// 视频格式
|
|
35
|
-
mp4: "video/mp4",
|
|
36
|
-
webm: "video/webm",
|
|
37
|
-
mov: "video/quicktime",
|
|
38
|
-
avi: "video/x-msvideo",
|
|
39
|
-
mkv: "video/x-matroska",
|
|
40
|
-
// 音频格式
|
|
41
|
-
mp3: "audio/mpeg",
|
|
42
|
-
wav: "audio/wav",
|
|
43
|
-
ogg: "audio/ogg",
|
|
44
|
-
flac: "audio/flac",
|
|
45
|
-
m4a: "audio/mp4",
|
|
46
|
-
// 文档格式
|
|
47
|
-
pdf: "application/pdf",
|
|
48
|
-
json: "application/json",
|
|
49
|
-
xml: "application/xml",
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
return mimeMap[ext];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* 根据 MIME 类型获取文件扩展名
|
|
57
|
-
*/
|
|
58
|
-
function getExtensionFromContentType(contentType: string): string | undefined {
|
|
59
|
-
const mimeToExt: Record<string, string> = {
|
|
60
|
-
"image/jpeg": ".jpg",
|
|
61
|
-
"image/png": ".png",
|
|
62
|
-
"image/gif": ".gif",
|
|
63
|
-
"image/webp": ".webp",
|
|
64
|
-
"image/avif": ".avif",
|
|
65
|
-
"image/heic": ".heic",
|
|
66
|
-
"image/heif": ".heif",
|
|
67
|
-
"image/bmp": ".bmp",
|
|
68
|
-
"image/x-icon": ".ico",
|
|
69
|
-
"image/svg+xml": ".svg",
|
|
70
|
-
"image/tiff": ".tiff",
|
|
71
|
-
"video/mp4": ".mp4",
|
|
72
|
-
"video/webm": ".webm",
|
|
73
|
-
"video/quicktime": ".mov",
|
|
74
|
-
"video/x-msvideo": ".avi",
|
|
75
|
-
"video/x-matroska": ".mkv",
|
|
76
|
-
"audio/mpeg": ".mp3",
|
|
77
|
-
"audio/wav": ".wav",
|
|
78
|
-
"audio/ogg": ".ogg",
|
|
79
|
-
"audio/flac": ".flac",
|
|
80
|
-
"audio/mp4": ".m4a",
|
|
81
|
-
"application/pdf": ".pdf",
|
|
82
|
-
"application/json": ".json",
|
|
83
|
-
"application/xml": ".xml",
|
|
84
|
-
};
|
|
85
|
-
return mimeToExt[contentType];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* 确保文件名有正确的扩展名
|
|
90
|
-
* 如果文件名没有扩展名,根据 contentType 添加
|
|
91
|
-
*/
|
|
92
|
-
function ensureFileNameExtension(fileName: string, contentType: string): string {
|
|
93
|
-
// 检查是否已有扩展名
|
|
94
|
-
const hasExtension = /\.[a-zA-Z0-9]+$/.test(fileName);
|
|
95
|
-
if (hasExtension) {
|
|
96
|
-
return fileName;
|
|
97
|
-
}
|
|
98
|
-
// 根据 contentType 添加扩展名
|
|
99
|
-
const ext = getExtensionFromContentType(contentType);
|
|
100
|
-
if (ext) {
|
|
101
|
-
return fileName + ext;
|
|
102
|
-
}
|
|
103
|
-
return fileName;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* 获取最终的 contentType
|
|
108
|
-
* 如果原始 contentType 缺失或为通用二进制流,则根据文件名推断
|
|
109
|
-
*/
|
|
110
|
-
export function resolveContentType(
|
|
111
|
-
fileName: string,
|
|
112
|
-
originalContentType?: string,
|
|
113
|
-
): string {
|
|
114
|
-
// 如果有明确的 MIME 类型(非通用二进制流),直接使用
|
|
115
|
-
if (originalContentType && originalContentType !== "application/octet-stream") {
|
|
116
|
-
return originalContentType;
|
|
117
|
-
}
|
|
118
|
-
// 根据文件名推断
|
|
119
|
-
const inferred = inferContentTypeFromFileName(fileName);
|
|
120
|
-
return inferred || originalContentType || "application/octet-stream";
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
let _logger: RuntimeEnv | null = null;
|
|
124
|
-
|
|
125
|
-
export function setSendMessageLogger(logger: RuntimeEnv): void {
|
|
126
|
-
_logger = logger;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function log(message: string): void {
|
|
130
|
-
if (_logger) {
|
|
131
|
-
_logger.log(message);
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
console.log(message);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function logError(message: string): void {
|
|
138
|
-
if (_logger) {
|
|
139
|
-
_logger.error(message);
|
|
140
|
-
} else {
|
|
141
|
-
console.error(message);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export function extractAtIds(text: string): { text: string; atIds: number[] } {
|
|
146
|
-
const atIds: number[] = [];
|
|
147
|
-
text.replace(MENTION_PATTERN, (_, id1, id2) => {
|
|
148
|
-
const id = id1 ?? id2;
|
|
149
|
-
if (id) {
|
|
150
|
-
atIds.push(Number(id));
|
|
151
|
-
}
|
|
152
|
-
return _;
|
|
153
|
-
});
|
|
154
|
-
return { text: text.trim(), atIds };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export type SendMessageMeetOpts = {
|
|
158
|
-
cfg: ClawdbotConfig;
|
|
159
|
-
to: string;
|
|
160
|
-
text: string;
|
|
161
|
-
accountId?: string;
|
|
162
|
-
replyToMessageId?: string;
|
|
163
|
-
atIds?: number[];
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
export async function sendMessageMeet(
|
|
167
|
-
opts: SendMessageMeetOpts,
|
|
168
|
-
): Promise<{ messageId: string; chatId: string }> {
|
|
169
|
-
const { cfg, to, text, accountId, atIds: explicitAtIds } = opts;
|
|
170
|
-
const account = resolveMeetAccount({ cfg, accountId });
|
|
171
|
-
if (!account.configured) {
|
|
172
|
-
throw new Error(`Meet account not configured: ${accountId ?? "default"}`);
|
|
173
|
-
}
|
|
174
|
-
// logLevel: info 时记录 AI 输出的原始内容,方便调试 mention 格式问题
|
|
175
|
-
// 默认为 silent,只有显式设置为 info 时才输出
|
|
176
|
-
if (account.config.logLevel === "info") {
|
|
177
|
-
console.log(`[${account.accountId}] AI output raw text: ${JSON.stringify(text)}`);
|
|
178
|
-
}
|
|
179
|
-
const token = account.apiToken;
|
|
180
|
-
if (!token) {
|
|
181
|
-
throw new Error("Meet API token not configured");
|
|
182
|
-
}
|
|
183
|
-
// 检查 token 是否未解析(仍是 secret ref 格式)
|
|
184
|
-
if (token.startsWith("${secret:")) {
|
|
185
|
-
const accountArg = accountId ? ` --account ${accountId}` : "";
|
|
186
|
-
throw new Error(
|
|
187
|
-
`Meet API token not resolved (still a secret reference).\n` +
|
|
188
|
-
`\n` +
|
|
189
|
-
`This usually means:\n` +
|
|
190
|
-
`1. Gateway is not running in this workspace, or\n` +
|
|
191
|
-
`2. Token is already in use by another process (Meet only allows single login)\n` +
|
|
192
|
-
`\n` +
|
|
193
|
-
`Solutions:\n` +
|
|
194
|
-
`- If Gateway is running elsewhere: use \`message\` tool (OpenClaw will route through Gateway)\n` +
|
|
195
|
-
`- If you need to send files: put them in a shared location and ask someone with Gateway access to send\n` +
|
|
196
|
-
`- To start Gateway: openclaw gateway start${accountArg}\n` +
|
|
197
|
-
`\n` +
|
|
198
|
-
`AccountId: ${account.accountId}`
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
const botUserId = token.split(":")[0];
|
|
202
|
-
if (!botUserId) {
|
|
203
|
-
throw new Error("Invalid Meet API token format");
|
|
204
|
-
}
|
|
205
|
-
// 如果 client 不存在则自动创建(支持 message tool 直接发送消息)
|
|
206
|
-
let bot = getMeetClient(account.accountId);
|
|
207
|
-
if (!bot) {
|
|
208
|
-
bot = createMeetClient(account);
|
|
209
|
-
}
|
|
210
|
-
const { text: cleanText, atIds: extractedAtIds } = extractAtIds(text);
|
|
211
|
-
const finalAtIds = explicitAtIds
|
|
212
|
-
? [...explicitAtIds, ...extractedAtIds]
|
|
213
|
-
: extractedAtIds;
|
|
214
|
-
log(`send message to=${to} atIds=${finalAtIds.join(",") || "none"}`);
|
|
215
|
-
const sessionInfo = parseTargetToSessionInfo(to, Number(botUserId));
|
|
216
|
-
try {
|
|
217
|
-
const result = await bot.sendMessage(sessionInfo, {
|
|
218
|
-
content: cleanText,
|
|
219
|
-
atIds: finalAtIds,
|
|
220
|
-
});
|
|
221
|
-
return {
|
|
222
|
-
messageId: String(result.msgContent?.seqId ?? 0),
|
|
223
|
-
chatId: to,
|
|
224
|
-
};
|
|
225
|
-
} catch (error) {
|
|
226
|
-
logError(`send message error: ${String(error)}`);
|
|
227
|
-
throw error;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
export type SendMediaMeetOpts = {
|
|
232
|
-
cfg: ClawdbotConfig;
|
|
233
|
-
to: string;
|
|
234
|
-
text?: string;
|
|
235
|
-
mediaUrl: string;
|
|
236
|
-
mediaLocalRoots?: readonly string[];
|
|
237
|
-
accountId?: string;
|
|
238
|
-
/** 上传进度回调 */
|
|
239
|
-
onProgress?: (progress: { percent: string; loaded: number; total: number; speedPerSecond: string }) => void;
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
export async function sendMediaMeet(
|
|
243
|
-
opts: SendMediaMeetOpts,
|
|
244
|
-
): Promise<{ messageId: string; chatId: string }> {
|
|
245
|
-
const { cfg, to, text, mediaUrl, mediaLocalRoots, accountId, onProgress } = opts;
|
|
246
|
-
const account = resolveMeetAccount({ cfg, accountId });
|
|
247
|
-
if (!account.configured) {
|
|
248
|
-
throw new Error(`Meet account not configured: ${accountId ?? "default"}`);
|
|
249
|
-
}
|
|
250
|
-
const token = account.apiToken;
|
|
251
|
-
if (!token) {
|
|
252
|
-
throw new Error("Meet API token not configured");
|
|
253
|
-
}
|
|
254
|
-
// 检查 token 是否未解析(仍是 secret ref 格式)
|
|
255
|
-
if (token.startsWith("${secret:")) {
|
|
256
|
-
const accountArg = accountId ? ` --account ${accountId}` : "";
|
|
257
|
-
throw new Error(
|
|
258
|
-
`Meet API token not resolved (still a secret reference).\n` +
|
|
259
|
-
`\n` +
|
|
260
|
-
`This usually means:\n` +
|
|
261
|
-
`1. Gateway is not running in this workspace, or\n` +
|
|
262
|
-
`2. Token is already in use by another process (Meet only allows single login)\n` +
|
|
263
|
-
`\n` +
|
|
264
|
-
`Solutions:\n` +
|
|
265
|
-
`- If Gateway is running elsewhere: use \`message\` tool (OpenClaw will route through Gateway)\n` +
|
|
266
|
-
`- If you need to send files: put them in a shared location and ask someone with Gateway access to send\n` +
|
|
267
|
-
`- To start Gateway: openclaw gateway start${accountArg}\n` +
|
|
268
|
-
`\n` +
|
|
269
|
-
`AccountId: ${account.accountId}`
|
|
270
|
-
);
|
|
271
|
-
}
|
|
272
|
-
const botUserId = token.split(":")[0];
|
|
273
|
-
if (!botUserId) {
|
|
274
|
-
throw new Error("Invalid Meet API token format");
|
|
275
|
-
}
|
|
276
|
-
// 如果 client 不存在则自动创建(支持 message tool 直接发送消息)
|
|
277
|
-
let bot = getMeetClient(account.accountId);
|
|
278
|
-
if (!bot) {
|
|
279
|
-
bot = createMeetClient(account);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const runtime = getMeetRuntime();
|
|
283
|
-
const maxBytes = account.config.mediaMaxMb
|
|
284
|
-
? account.config.mediaMaxMb * 1024 * 1024
|
|
285
|
-
: undefined;
|
|
286
|
-
|
|
287
|
-
log(`loading media: ${mediaUrl}`);
|
|
288
|
-
const media = await runtime.media.loadWebMedia(mediaUrl, {
|
|
289
|
-
maxBytes,
|
|
290
|
-
localRoots: mediaLocalRoots,
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
// 检查媒体大小限制
|
|
294
|
-
if (maxBytes && media.buffer.length > maxBytes) {
|
|
295
|
-
throw new Error(`Media file too large: ${media.buffer.length} bytes (max: ${maxBytes})`);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const sessionInfo = parseTargetToSessionInfo(to, Number(botUserId));
|
|
299
|
-
const rawFileName = media.fileName || "file";
|
|
300
|
-
const contentType = resolveContentType(rawFileName, media.contentType);
|
|
301
|
-
// 确保文件名有正确的扩展名
|
|
302
|
-
const fileName = ensureFileNameExtension(rawFileName, contentType);
|
|
303
|
-
|
|
304
|
-
log(
|
|
305
|
-
`sending media to=${to} fileName=${fileName} size=${media.buffer.length} contentType=${contentType}`,
|
|
306
|
-
);
|
|
307
|
-
|
|
308
|
-
// 包装进度回调
|
|
309
|
-
const progressCallback = onProgress
|
|
310
|
-
? (progress: SdkUploadProgress) => {
|
|
311
|
-
onProgress({
|
|
312
|
-
percent: progress.percent,
|
|
313
|
-
loaded: progress.loaded,
|
|
314
|
-
total: progress.total,
|
|
315
|
-
speedPerSecond: progress.speedPerSecond,
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
: undefined;
|
|
319
|
-
|
|
320
|
-
try {
|
|
321
|
-
const result = await bot.sendMedia(sessionInfo, {
|
|
322
|
-
buffer: media.buffer,
|
|
323
|
-
fileName,
|
|
324
|
-
contentType,
|
|
325
|
-
content: text || "",
|
|
326
|
-
onProgress: progressCallback,
|
|
327
|
-
});
|
|
328
|
-
return {
|
|
329
|
-
messageId: String(result.msgContent?.seqId ?? 0),
|
|
330
|
-
chatId: to,
|
|
331
|
-
};
|
|
332
|
-
} catch (error) {
|
|
333
|
-
logError(`send media error: ${String(error)}`);
|
|
334
|
-
throw error;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
export async function resolveMeetMedia(
|
|
339
|
-
mediaUrl: string,
|
|
340
|
-
opts?: {
|
|
341
|
-
cfg?: ClawdbotConfig;
|
|
342
|
-
accountId?: string;
|
|
343
|
-
mediaLocalRoots?: readonly string[];
|
|
344
|
-
},
|
|
345
|
-
): Promise<MeetMediaInfo> {
|
|
346
|
-
const runtime = getMeetRuntime();
|
|
347
|
-
let maxBytes: number | undefined;
|
|
348
|
-
if (opts?.cfg) {
|
|
349
|
-
const account = resolveMeetAccount({
|
|
350
|
-
cfg: opts.cfg,
|
|
351
|
-
accountId: opts.accountId,
|
|
352
|
-
});
|
|
353
|
-
if (account.config.mediaMaxMb) {
|
|
354
|
-
maxBytes = account.config.mediaMaxMb * 1024 * 1024;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
const media = await runtime.media.loadWebMedia(mediaUrl, {
|
|
358
|
-
maxBytes,
|
|
359
|
-
localRoots: opts?.mediaLocalRoots,
|
|
360
|
-
});
|
|
361
|
-
return {
|
|
362
|
-
path: media.fileName || "file",
|
|
363
|
-
contentType: media.contentType,
|
|
364
|
-
placeholder: `[Meet file: ${media.fileName || "file"}]`,
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
export async function getMessageMeet(opts: {
|
|
369
|
-
cfg: ClawdbotConfig;
|
|
370
|
-
messageId: string;
|
|
371
|
-
accountId?: string;
|
|
372
|
-
}): Promise<{ content: string } | null> {
|
|
373
|
-
const { cfg, accountId } = opts;
|
|
374
|
-
const account = resolveMeetAccount({ cfg, accountId });
|
|
375
|
-
if (!account.configured) {
|
|
376
|
-
return null;
|
|
377
|
-
}
|
|
378
|
-
const bot = getMeetClient(account.accountId);
|
|
379
|
-
if (!bot) {
|
|
380
|
-
return null;
|
|
381
|
-
}
|
|
382
|
-
return null;
|
|
383
|
-
}
|