@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,268 @@
|
|
|
1
|
+
import type { MsgContent, SessionInfo, SessionType, AttachmentInfo } 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
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { ResolvedMeetAccount, 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 } 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
|
+
let _logger: RuntimeEnv | null = null;
|
|
12
|
+
|
|
13
|
+
export function setSendMessageLogger(logger: RuntimeEnv): void {
|
|
14
|
+
_logger = logger;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function log(message: string): void {
|
|
18
|
+
if (_logger) {
|
|
19
|
+
_logger.log(message);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function logError(message: string): void {
|
|
24
|
+
if (_logger) {
|
|
25
|
+
_logger.error(message);
|
|
26
|
+
} else {
|
|
27
|
+
console.error(message);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function extractAtIds(text: string): { text: string; atIds: number[] } {
|
|
32
|
+
const atIds: number[] = [];
|
|
33
|
+
text.replace(MENTION_PATTERN, (_, id1, id2) => {
|
|
34
|
+
const id = id1 ?? id2;
|
|
35
|
+
if (id) {
|
|
36
|
+
atIds.push(Number(id));
|
|
37
|
+
}
|
|
38
|
+
return _;
|
|
39
|
+
});
|
|
40
|
+
return { text: text.trim(), atIds };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type SendMessageMeetOpts = {
|
|
44
|
+
cfg: ClawdbotConfig;
|
|
45
|
+
to: string;
|
|
46
|
+
text: string;
|
|
47
|
+
accountId?: string;
|
|
48
|
+
replyToMessageId?: string;
|
|
49
|
+
atIds?: number[];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export async function sendMessageMeet(
|
|
53
|
+
opts: SendMessageMeetOpts,
|
|
54
|
+
): Promise<{ messageId: string; chatId: string }> {
|
|
55
|
+
const { cfg, to, text, accountId, atIds: explicitAtIds } = opts;
|
|
56
|
+
const account = resolveMeetAccount({ cfg, accountId });
|
|
57
|
+
if (!account.configured) {
|
|
58
|
+
throw new Error(`Meet account not configured: ${accountId ?? "default"}`);
|
|
59
|
+
}
|
|
60
|
+
const token = account.apiToken;
|
|
61
|
+
if (!token) {
|
|
62
|
+
throw new Error("Meet API token not configured");
|
|
63
|
+
}
|
|
64
|
+
const botUserId = token.split(":")[0];
|
|
65
|
+
if (!botUserId) {
|
|
66
|
+
throw new Error("Invalid Meet API token format");
|
|
67
|
+
}
|
|
68
|
+
const bot = getMeetClient(account.accountId);
|
|
69
|
+
if (!bot) {
|
|
70
|
+
throw new Error(`Meet client not found for account: ${account.accountId}`);
|
|
71
|
+
}
|
|
72
|
+
const { text: cleanText, atIds: extractedAtIds } = extractAtIds(text);
|
|
73
|
+
const finalAtIds = explicitAtIds
|
|
74
|
+
? [...explicitAtIds, ...extractedAtIds]
|
|
75
|
+
: extractedAtIds;
|
|
76
|
+
log(`send message to=${to} atIds=${finalAtIds.join(",") || "none"}`);
|
|
77
|
+
const sessionInfo = parseTargetToSessionInfo(to, Number(botUserId));
|
|
78
|
+
try {
|
|
79
|
+
const result = await bot.sendMessage(sessionInfo, {
|
|
80
|
+
content: cleanText,
|
|
81
|
+
atIds: finalAtIds,
|
|
82
|
+
});
|
|
83
|
+
return {
|
|
84
|
+
messageId: String(result.msgContent?.seqId ?? 0),
|
|
85
|
+
chatId: to,
|
|
86
|
+
};
|
|
87
|
+
} catch (error) {
|
|
88
|
+
logError(`send message error: ${String(error)}`);
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type SendMediaMeetOpts = {
|
|
94
|
+
cfg: ClawdbotConfig;
|
|
95
|
+
to: string;
|
|
96
|
+
text?: string;
|
|
97
|
+
mediaUrl: string;
|
|
98
|
+
mediaLocalRoots?: readonly string[];
|
|
99
|
+
accountId?: string;
|
|
100
|
+
/** 上传进度回调 */
|
|
101
|
+
onProgress?: (progress: { percent: string; loaded: number; total: number; speedPerSecond: string }) => void;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export async function sendMediaMeet(
|
|
105
|
+
opts: SendMediaMeetOpts,
|
|
106
|
+
): Promise<{ messageId: string; chatId: string }> {
|
|
107
|
+
const { cfg, to, text, mediaUrl, mediaLocalRoots, accountId, onProgress } = opts;
|
|
108
|
+
const account = resolveMeetAccount({ cfg, accountId });
|
|
109
|
+
if (!account.configured) {
|
|
110
|
+
throw new Error(`Meet account not configured: ${accountId ?? "default"}`);
|
|
111
|
+
}
|
|
112
|
+
const token = account.apiToken;
|
|
113
|
+
if (!token) {
|
|
114
|
+
throw new Error("Meet API token not configured");
|
|
115
|
+
}
|
|
116
|
+
const botUserId = token.split(":")[0];
|
|
117
|
+
if (!botUserId) {
|
|
118
|
+
throw new Error("Invalid Meet API token format");
|
|
119
|
+
}
|
|
120
|
+
const bot = getMeetClient(account.accountId);
|
|
121
|
+
if (!bot) {
|
|
122
|
+
throw new Error(`Meet client not found for account: ${account.accountId}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const runtime = getMeetRuntime();
|
|
126
|
+
const maxBytes = account.config.mediaMaxMb
|
|
127
|
+
? account.config.mediaMaxMb * 1024 * 1024
|
|
128
|
+
: undefined;
|
|
129
|
+
|
|
130
|
+
log(`loading media: ${mediaUrl}`);
|
|
131
|
+
const media = await runtime.media.loadWebMedia(mediaUrl, {
|
|
132
|
+
maxBytes,
|
|
133
|
+
localRoots: mediaLocalRoots,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// 检查媒体大小限制
|
|
137
|
+
if (maxBytes && media.buffer.length > maxBytes) {
|
|
138
|
+
throw new Error(`Media file too large: ${media.buffer.length} bytes (max: ${maxBytes})`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const sessionInfo = parseTargetToSessionInfo(to, Number(botUserId));
|
|
142
|
+
const fileName = media.fileName || "file";
|
|
143
|
+
|
|
144
|
+
log(
|
|
145
|
+
`sending media to=${to} fileName=${fileName} size=${media.buffer.length}`,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// 包装进度回调
|
|
149
|
+
const progressCallback = onProgress
|
|
150
|
+
? (progress: SdkUploadProgress) => {
|
|
151
|
+
onProgress({
|
|
152
|
+
percent: progress.percent,
|
|
153
|
+
loaded: progress.loaded,
|
|
154
|
+
total: progress.total,
|
|
155
|
+
speedPerSecond: progress.speedPerSecond,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
: undefined;
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const result = await bot.sendMedia(sessionInfo, {
|
|
162
|
+
buffer: media.buffer,
|
|
163
|
+
fileName,
|
|
164
|
+
contentType: media.contentType || "application/octet-stream",
|
|
165
|
+
content: text || "",
|
|
166
|
+
onProgress: progressCallback,
|
|
167
|
+
});
|
|
168
|
+
return {
|
|
169
|
+
messageId: String(result.msgContent?.seqId ?? 0),
|
|
170
|
+
chatId: to,
|
|
171
|
+
};
|
|
172
|
+
} catch (error) {
|
|
173
|
+
logError(`send media error: ${String(error)}`);
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function resolveMeetMedia(
|
|
179
|
+
mediaUrl: string,
|
|
180
|
+
opts?: {
|
|
181
|
+
cfg?: ClawdbotConfig;
|
|
182
|
+
accountId?: string;
|
|
183
|
+
mediaLocalRoots?: readonly string[];
|
|
184
|
+
},
|
|
185
|
+
): Promise<MeetMediaInfo> {
|
|
186
|
+
const runtime = getMeetRuntime();
|
|
187
|
+
let maxBytes: number | undefined;
|
|
188
|
+
if (opts?.cfg) {
|
|
189
|
+
const account = resolveMeetAccount({
|
|
190
|
+
cfg: opts.cfg,
|
|
191
|
+
accountId: opts.accountId,
|
|
192
|
+
});
|
|
193
|
+
if (account.config.mediaMaxMb) {
|
|
194
|
+
maxBytes = account.config.mediaMaxMb * 1024 * 1024;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const media = await runtime.media.loadWebMedia(mediaUrl, {
|
|
198
|
+
maxBytes,
|
|
199
|
+
localRoots: opts?.mediaLocalRoots,
|
|
200
|
+
});
|
|
201
|
+
return {
|
|
202
|
+
path: media.fileName || "file",
|
|
203
|
+
contentType: media.contentType,
|
|
204
|
+
placeholder: `[Meet file: ${media.fileName || "file"}]`,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export async function getMessageMeet(opts: {
|
|
209
|
+
cfg: ClawdbotConfig;
|
|
210
|
+
messageId: string;
|
|
211
|
+
accountId?: string;
|
|
212
|
+
}): Promise<{ content: string } | null> {
|
|
213
|
+
const { cfg, messageId, accountId } = opts;
|
|
214
|
+
const account = resolveMeetAccount({ cfg, accountId });
|
|
215
|
+
if (!account.configured) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
const bot = getMeetClient(account.accountId);
|
|
219
|
+
if (!bot) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
package/src/targets.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export type MeetTargetKind = "user" | "channel"
|
|
2
|
+
|
|
3
|
+
export type MeetTarget = {
|
|
4
|
+
kind: MeetTargetKind
|
|
5
|
+
id: string
|
|
6
|
+
raw: string
|
|
7
|
+
normalized: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type MeetTargetParseOptions = {
|
|
11
|
+
defaultKind?: MeetTargetKind
|
|
12
|
+
ambiguousMessage?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function buildMessagingTarget(kind: MeetTargetKind, id: string, raw: string): MeetTarget {
|
|
16
|
+
return { kind, id, raw, normalized: `${kind}:${id}` }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseMentionPrefixOrAtUserTarget(params: {
|
|
20
|
+
raw: string
|
|
21
|
+
mentionPattern: RegExp
|
|
22
|
+
prefixes: Array<{ prefix: string; kind: MeetTargetKind }>
|
|
23
|
+
atUserPattern: RegExp
|
|
24
|
+
atUserErrorMessage: string
|
|
25
|
+
}): MeetTarget | undefined {
|
|
26
|
+
const { raw, mentionPattern, prefixes, atUserPattern } = params
|
|
27
|
+
|
|
28
|
+
const mentionMatch = raw.match(mentionPattern)
|
|
29
|
+
if (mentionMatch) {
|
|
30
|
+
return buildMessagingTarget("user", mentionMatch[2], raw)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const { prefix, kind } of prefixes) {
|
|
34
|
+
if (raw.startsWith(prefix)) {
|
|
35
|
+
const id = raw.slice(prefix.length)
|
|
36
|
+
if (id) {
|
|
37
|
+
return buildMessagingTarget(kind, id, raw)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (atUserPattern.test(raw)) {
|
|
43
|
+
return buildMessagingTarget("user", raw, raw)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function parseMeetTarget(
|
|
50
|
+
raw: string,
|
|
51
|
+
options: MeetTargetParseOptions = {},
|
|
52
|
+
): MeetTarget | undefined {
|
|
53
|
+
const trimmed = raw.trim()
|
|
54
|
+
if (!trimmed) {
|
|
55
|
+
return undefined
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const userTarget = parseMentionPrefixOrAtUserTarget({
|
|
59
|
+
raw: trimmed,
|
|
60
|
+
mentionPattern: /^@\[([^\]]+)\]\((\d+)\)$/,
|
|
61
|
+
prefixes: [
|
|
62
|
+
{ prefix: "user:", kind: "user" },
|
|
63
|
+
{ prefix: "channel:", kind: "channel" },
|
|
64
|
+
{ prefix: "meet:", kind: "user" },
|
|
65
|
+
],
|
|
66
|
+
atUserPattern: /^\d+$/,
|
|
67
|
+
atUserErrorMessage: "Meet DMs require a user id (use user:<id>)",
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
if (userTarget) {
|
|
71
|
+
return userTarget
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (/^\d+$/.test(trimmed)) {
|
|
75
|
+
if (options.defaultKind) {
|
|
76
|
+
return buildMessagingTarget(options.defaultKind, trimmed, trimmed)
|
|
77
|
+
}
|
|
78
|
+
throw new Error(
|
|
79
|
+
options.ambiguousMessage ??
|
|
80
|
+
`Ambiguous Meet recipient "${trimmed}". Use "user:${trimmed}" for DMs or "channel:${trimmed}" for channel messages.`,
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return buildMessagingTarget("channel", trimmed, trimmed)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function resolveMeetChannelId(raw: string): string {
|
|
88
|
+
const target = parseMeetTarget(raw, { defaultKind: "channel" })
|
|
89
|
+
if (!target) {
|
|
90
|
+
throw new Error(`Invalid Meet channel: ${raw}`)
|
|
91
|
+
}
|
|
92
|
+
return target.id
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function formatMeetTarget(target: MeetTarget): string {
|
|
96
|
+
return `${target.kind}:${target.id}`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function looksLikeMeetId(input: string): boolean {
|
|
100
|
+
return /^\d+$/.test(input) || /^(user:|channel:|meet:)/.test(input)
|
|
101
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SessionInfo,
|
|
3
|
+
AttachmentInfo,
|
|
4
|
+
UploadProgress,
|
|
5
|
+
} from "@meet-im/meet-bot-jssdk"
|
|
6
|
+
import type { z } from "zod"
|
|
7
|
+
import {
|
|
8
|
+
MeetConfigSchema,
|
|
9
|
+
MeetAccountConfigSchema,
|
|
10
|
+
MeetChannelConfigSchema,
|
|
11
|
+
MeetGroupConfigSchema,
|
|
12
|
+
} from "./config-schema.js"
|
|
13
|
+
|
|
14
|
+
export type MeetConfig = z.infer<typeof MeetConfigSchema>
|
|
15
|
+
export type MeetAccountConfig = z.infer<typeof MeetAccountConfigSchema>
|
|
16
|
+
export type MeetChannelConfig = z.infer<typeof MeetChannelConfigSchema>
|
|
17
|
+
export type MeetGroupConfig = z.infer<typeof MeetGroupConfigSchema>
|
|
18
|
+
|
|
19
|
+
// 重新导出 SDK 类型
|
|
20
|
+
export type { AttachmentInfo, UploadProgress }
|
|
21
|
+
|
|
22
|
+
export type ResolvedMeetAccount = {
|
|
23
|
+
accountId: string
|
|
24
|
+
enabled: boolean
|
|
25
|
+
configured: boolean
|
|
26
|
+
name?: string
|
|
27
|
+
apiEndpoint?: string
|
|
28
|
+
apiToken?: string
|
|
29
|
+
config: MeetConfig
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type MeetReplyContext = {
|
|
33
|
+
messageId: string
|
|
34
|
+
senderId?: string
|
|
35
|
+
senderName?: string
|
|
36
|
+
content?: string
|
|
37
|
+
timestamp?: number
|
|
38
|
+
mediaPaths?: string[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type MeetMessageContext = {
|
|
42
|
+
chatId: string
|
|
43
|
+
messageId: string
|
|
44
|
+
senderId: string
|
|
45
|
+
senderOpenId: string
|
|
46
|
+
senderName?: string
|
|
47
|
+
chatType: "direct" | "channel"
|
|
48
|
+
mentionedBot: boolean
|
|
49
|
+
hasAnyMention?: boolean
|
|
50
|
+
content: string
|
|
51
|
+
contentType: string
|
|
52
|
+
placeholder?: string
|
|
53
|
+
rawPayload?: string
|
|
54
|
+
sessionInfo: SessionInfo
|
|
55
|
+
timestamp?: number
|
|
56
|
+
atIds?: number[]
|
|
57
|
+
replyContext?: MeetReplyContext
|
|
58
|
+
media?: MeetMediaAttachment[]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type MeetSendResult = {
|
|
62
|
+
messageId: string
|
|
63
|
+
chatId: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type MeetProbeResult = {
|
|
67
|
+
ok: boolean
|
|
68
|
+
error?: string
|
|
69
|
+
botId?: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type MeetMediaInfo = {
|
|
73
|
+
path: string
|
|
74
|
+
contentType?: string
|
|
75
|
+
placeholder: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 入站消息中的媒体附件信息
|
|
80
|
+
*/
|
|
81
|
+
export type MeetMediaAttachment = {
|
|
82
|
+
fileId: string | number
|
|
83
|
+
fileName?: string
|
|
84
|
+
fileSize?: number
|
|
85
|
+
mimeType?: string
|
|
86
|
+
fileUrl?: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 文件上传结果
|
|
91
|
+
*/
|
|
92
|
+
export type MeetUploadResult = {
|
|
93
|
+
fileID: number
|
|
94
|
+
path: string
|
|
95
|
+
size: number
|
|
96
|
+
}
|