@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/monitor.ts
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk"
|
|
2
|
-
import type { HistoryEntry } from "openclaw/plugin-sdk/reply-history"
|
|
3
|
-
import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue"
|
|
4
|
-
import type { ResolvedMeetAccount } from "./types.js"
|
|
5
|
-
import type { MsgContent } from "@meet-im/meet-bot-jssdk"
|
|
6
|
-
import type { QuoteMsgMap } from "./sdk-bridge.js"
|
|
7
|
-
import { resolveMeetAccount, listEnabledMeetAccounts } from "./accounts.js"
|
|
8
|
-
import { createMeetClient, closeMeetClient, closeAllMeetClients, getPollingOptions } from "./client.js"
|
|
9
|
-
import { handleMeetMessage } from "./bot.js"
|
|
10
|
-
import { msgContentToContext } from "./sdk-bridge.js"
|
|
11
|
-
|
|
12
|
-
export type MonitorMeetOpts = {
|
|
13
|
-
config?: ClawdbotConfig
|
|
14
|
-
runtime?: RuntimeEnv
|
|
15
|
-
abortSignal?: AbortSignal
|
|
16
|
-
accountId?: string
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function monitorMeetProvider(opts: MonitorMeetOpts = {}): Promise<void> {
|
|
20
|
-
const cfg = opts.config
|
|
21
|
-
if (!cfg) {
|
|
22
|
-
throw new Error("Config is required for Meet monitor")
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const log = opts.runtime?.log ?? console.log
|
|
26
|
-
|
|
27
|
-
if (opts.accountId) {
|
|
28
|
-
const account = resolveMeetAccount({ cfg, accountId: opts.accountId })
|
|
29
|
-
if (!account.enabled || !account.configured) {
|
|
30
|
-
throw new Error(`Meet account "${opts.accountId}" not configured or disabled`)
|
|
31
|
-
}
|
|
32
|
-
return monitorSingleAccount({
|
|
33
|
-
cfg,
|
|
34
|
-
account,
|
|
35
|
-
runtime: opts.runtime,
|
|
36
|
-
abortSignal: opts.abortSignal,
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const accounts = listEnabledMeetAccounts(cfg)
|
|
41
|
-
if (accounts.length === 0) {
|
|
42
|
-
throw new Error("No enabled Meet accounts configured")
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
log(`starting ${accounts.length} account(s): ${accounts.map((a) => a.accountId).join(", ")}`)
|
|
46
|
-
|
|
47
|
-
await Promise.all(
|
|
48
|
-
accounts.map((account) =>
|
|
49
|
-
monitorSingleAccount({
|
|
50
|
-
cfg,
|
|
51
|
-
account,
|
|
52
|
-
runtime: opts.runtime,
|
|
53
|
-
abortSignal: opts.abortSignal,
|
|
54
|
-
}),
|
|
55
|
-
),
|
|
56
|
-
)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function monitorSingleAccount(params: {
|
|
60
|
-
cfg: ClawdbotConfig
|
|
61
|
-
account: ResolvedMeetAccount
|
|
62
|
-
runtime?: RuntimeEnv
|
|
63
|
-
abortSignal?: AbortSignal
|
|
64
|
-
}): Promise<void> {
|
|
65
|
-
const { cfg, account, runtime, abortSignal } = params
|
|
66
|
-
const { accountId } = account
|
|
67
|
-
const log = runtime?.log ?? console.log
|
|
68
|
-
const error = runtime?.error ?? console.error
|
|
69
|
-
|
|
70
|
-
const pollTimeoutMs = account.config.pollTimeout ?? 30000
|
|
71
|
-
log(`[${accountId}]: starting with pollTimeout=${pollTimeoutMs}ms`)
|
|
72
|
-
|
|
73
|
-
const bot = createMeetClient(account)
|
|
74
|
-
const botUserId = extractBotUserId(account.apiToken ?? "")
|
|
75
|
-
const groupHistories = new Map<string, HistoryEntry[]>()
|
|
76
|
-
const messageQueue = new KeyedAsyncQueue()
|
|
77
|
-
|
|
78
|
-
return new Promise((resolve, reject) => {
|
|
79
|
-
let isCleaningUp = false
|
|
80
|
-
|
|
81
|
-
const cleanup = () => {
|
|
82
|
-
if (isCleaningUp) return
|
|
83
|
-
isCleaningUp = true
|
|
84
|
-
bot.stopPolling()
|
|
85
|
-
closeMeetClient(accountId)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const handleAbort = () => {
|
|
89
|
-
log(`[${accountId}]: abort signal received, stopping`)
|
|
90
|
-
cleanup()
|
|
91
|
-
resolve()
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (abortSignal?.aborted) {
|
|
95
|
-
cleanup()
|
|
96
|
-
resolve()
|
|
97
|
-
return
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
abortSignal?.addEventListener("abort", handleAbort, { once: true })
|
|
101
|
-
|
|
102
|
-
bot.on("message", (data: { message: MsgContent; quoteMsgMap: QuoteMsgMap }) => {
|
|
103
|
-
const { message: msg, quoteMsgMap } = data
|
|
104
|
-
let queueKey: string
|
|
105
|
-
try {
|
|
106
|
-
const ctx = msgContentToContext(msg, botUserId, quoteMsgMap)
|
|
107
|
-
queueKey = ctx.chatId
|
|
108
|
-
} catch {
|
|
109
|
-
error(`[${accountId}]: failed to parse message for queue key`)
|
|
110
|
-
return
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const tailMap = messageQueue.getTailMapForTesting()
|
|
114
|
-
const queueSize = tailMap.size
|
|
115
|
-
const pendingInQueue = tailMap.has(queueKey)
|
|
116
|
-
log(
|
|
117
|
-
`[${accountId}]: enqueue message to queue=${queueKey}, queues=${queueSize}, pending=${pendingInQueue}`,
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
messageQueue.enqueue(
|
|
121
|
-
queueKey,
|
|
122
|
-
async () => {
|
|
123
|
-
try {
|
|
124
|
-
await handleMeetMessage({
|
|
125
|
-
cfg,
|
|
126
|
-
msg,
|
|
127
|
-
botUserId,
|
|
128
|
-
runtime,
|
|
129
|
-
accountId,
|
|
130
|
-
account,
|
|
131
|
-
bot,
|
|
132
|
-
groupHistories,
|
|
133
|
-
quoteMsgMap,
|
|
134
|
-
})
|
|
135
|
-
} catch (err) {
|
|
136
|
-
error(`[${accountId}]: error handling message: ${String(err)}`)
|
|
137
|
-
}
|
|
138
|
-
},
|
|
139
|
-
{
|
|
140
|
-
onEnqueue: () => {
|
|
141
|
-
const size = messageQueue.getTailMapForTesting().size
|
|
142
|
-
log(`[${accountId}]: queue=${queueKey} enqueued, total_queues=${size}`)
|
|
143
|
-
},
|
|
144
|
-
onSettle: () => {
|
|
145
|
-
const size = messageQueue.getTailMapForTesting().size
|
|
146
|
-
log(`[${accountId}]: queue=${queueKey} settled, total_queues=${size}`)
|
|
147
|
-
},
|
|
148
|
-
},
|
|
149
|
-
)
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
bot.on("error", (err) => {
|
|
153
|
-
error(`[${accountId}]: polling error: ${String(err)}`)
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
bot.on("polling_start", () => {
|
|
157
|
-
log(`[${accountId}]: polling started`)
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
bot.on("polling_stop", () => {
|
|
161
|
-
if (!isCleaningUp) {
|
|
162
|
-
log(`[${accountId}]: polling stopped`)
|
|
163
|
-
}
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
const pollingOptions = getPollingOptions(account)
|
|
167
|
-
|
|
168
|
-
bot.startPolling(pollingOptions)
|
|
169
|
-
.then(() => {
|
|
170
|
-
log(`[${accountId}]: polling completed`)
|
|
171
|
-
cleanup()
|
|
172
|
-
abortSignal?.removeEventListener("abort", handleAbort)
|
|
173
|
-
resolve()
|
|
174
|
-
})
|
|
175
|
-
.catch((err) => {
|
|
176
|
-
error(`[${accountId}]: polling failed: ${String(err)}`)
|
|
177
|
-
cleanup()
|
|
178
|
-
abortSignal?.removeEventListener("abort", handleAbort)
|
|
179
|
-
reject(err)
|
|
180
|
-
})
|
|
181
|
-
})
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
export function stopMeetMonitor(accountId?: string): void {
|
|
185
|
-
if (accountId) {
|
|
186
|
-
closeMeetClient(accountId)
|
|
187
|
-
} else {
|
|
188
|
-
closeAllMeetClients()
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function extractBotUserId(token: string): string {
|
|
193
|
-
const parts = token.split(":")
|
|
194
|
-
return parts[0] ?? ""
|
|
195
|
-
}
|
package/src/outbound.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/channel-send-result";
|
|
2
|
-
import { getMeetRuntime } from "./runtime.js";
|
|
3
|
-
import { sendMessageMeet, sendMediaMeet } from "./send.js";
|
|
4
|
-
|
|
5
|
-
export const meetOutbound: ChannelOutboundAdapter = {
|
|
6
|
-
deliveryMode: "direct",
|
|
7
|
-
chunker: (text, limit) => {
|
|
8
|
-
const runtime = getMeetRuntime();
|
|
9
|
-
return runtime.channel.text.chunkText(text, limit);
|
|
10
|
-
},
|
|
11
|
-
chunkerMode: "text",
|
|
12
|
-
textChunkLimit: 4000,
|
|
13
|
-
sendText: async ({ cfg, to, text, accountId }) => {
|
|
14
|
-
const result = await sendMessageMeet({
|
|
15
|
-
cfg,
|
|
16
|
-
to,
|
|
17
|
-
text: text ?? "",
|
|
18
|
-
accountId: accountId ?? undefined,
|
|
19
|
-
});
|
|
20
|
-
return { channel: "meet", messageId: result.messageId, chatId: result.chatId };
|
|
21
|
-
},
|
|
22
|
-
sendMedia: async ({
|
|
23
|
-
cfg,
|
|
24
|
-
to,
|
|
25
|
-
text,
|
|
26
|
-
mediaUrl,
|
|
27
|
-
mediaLocalRoots,
|
|
28
|
-
accountId,
|
|
29
|
-
}) => {
|
|
30
|
-
if (!mediaUrl) {
|
|
31
|
-
throw new Error("mediaUrl is required for sendMedia");
|
|
32
|
-
}
|
|
33
|
-
const result = await sendMediaMeet({
|
|
34
|
-
cfg,
|
|
35
|
-
to,
|
|
36
|
-
text: text ?? undefined,
|
|
37
|
-
mediaUrl,
|
|
38
|
-
mediaLocalRoots: mediaLocalRoots ?? undefined,
|
|
39
|
-
accountId: accountId ?? undefined,
|
|
40
|
-
});
|
|
41
|
-
return { channel: "meet", messageId: result.messageId, chatId: result.chatId };
|
|
42
|
-
},
|
|
43
|
-
};
|
package/src/policy.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import type { MeetConfig, MeetGroupConfig } from "./types.js"
|
|
2
|
-
|
|
3
|
-
export function resolveMeetAllowlistMatch(params: {
|
|
4
|
-
allowFrom: Array<string | number>
|
|
5
|
-
senderId: string
|
|
6
|
-
senderName?: string
|
|
7
|
-
}): { allowed: boolean } {
|
|
8
|
-
const { allowFrom, senderId } = params
|
|
9
|
-
|
|
10
|
-
if (allowFrom.length === 0) {
|
|
11
|
-
return { allowed: false }
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
if (allowFrom.includes("*")) {
|
|
15
|
-
return { allowed: true }
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const normalizedSenderId = senderId.trim().toLowerCase()
|
|
19
|
-
const normalizedAllowFrom = allowFrom.map((entry) =>
|
|
20
|
-
String(entry).trim().toLowerCase(),
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
if (normalizedAllowFrom.includes(normalizedSenderId)) {
|
|
24
|
-
return { allowed: true }
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return { allowed: false }
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function resolveMeetGroupPolicy(params: {
|
|
31
|
-
groupPolicy?: "open" | "allowlist" | "disabled"
|
|
32
|
-
groupAllowFrom: Array<string | number>
|
|
33
|
-
chatId: string
|
|
34
|
-
groups?: Record<string, MeetGroupConfig>
|
|
35
|
-
}): { allowed: boolean } {
|
|
36
|
-
const { groupPolicy = "allowlist", groupAllowFrom, chatId, groups } = params
|
|
37
|
-
|
|
38
|
-
if (groupPolicy === "disabled") {
|
|
39
|
-
return { allowed: false }
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Normalize chatId for matching (strip "channel:" prefix if present)
|
|
43
|
-
const normalizedChatId = chatId.replace(/^channel:/, "")
|
|
44
|
-
const groupConfig = groups?.[chatId] ?? groups?.[normalizedChatId]
|
|
45
|
-
|
|
46
|
-
if (groupPolicy === "open") {
|
|
47
|
-
if (groupConfig?.enabled === false) {
|
|
48
|
-
return { allowed: false }
|
|
49
|
-
}
|
|
50
|
-
return { allowed: true }
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// groupPolicy === "allowlist"
|
|
54
|
-
|
|
55
|
-
// Check if group is explicitly configured in groups
|
|
56
|
-
if (groupConfig && groupConfig.enabled !== false) {
|
|
57
|
-
return { allowed: true }
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (groupConfig?.enabled === false) {
|
|
61
|
-
return { allowed: false }
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (groupAllowFrom.length === 0) {
|
|
65
|
-
return { allowed: false }
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (groupAllowFrom.includes("*")) {
|
|
69
|
-
return { allowed: true }
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const normalizedAllowFrom = groupAllowFrom.map((entry) =>
|
|
73
|
-
String(entry).trim().toLowerCase(),
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
// Match both full chatId and normalized chatId
|
|
77
|
-
const fullChatIdLower = chatId.trim().toLowerCase()
|
|
78
|
-
const shortChatIdLower = normalizedChatId.trim().toLowerCase()
|
|
79
|
-
|
|
80
|
-
if (normalizedAllowFrom.includes(fullChatIdLower) ||
|
|
81
|
-
normalizedAllowFrom.includes(shortChatIdLower)) {
|
|
82
|
-
return { allowed: true }
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return { allowed: false }
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function resolveMeetGroupConfig(params: {
|
|
89
|
-
meetConfig: MeetConfig
|
|
90
|
-
chatId: string
|
|
91
|
-
}): {
|
|
92
|
-
requireMention: boolean
|
|
93
|
-
systemPrompt?: string
|
|
94
|
-
groupConfig?: MeetGroupConfig
|
|
95
|
-
} {
|
|
96
|
-
const { meetConfig, chatId } = params
|
|
97
|
-
const normalizedChatId = chatId.replace(/^channel:/, "")
|
|
98
|
-
const groupConfig = meetConfig.groups?.[chatId] ?? meetConfig.groups?.[normalizedChatId]
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
requireMention: groupConfig?.requireMention ?? meetConfig.requireMention ?? true,
|
|
102
|
-
systemPrompt: groupConfig?.systemPrompt ?? meetConfig.systemPrompt,
|
|
103
|
-
groupConfig,
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export function resolveMeetGroupUserPolicy(params: {
|
|
108
|
-
groupConfig?: MeetGroupConfig
|
|
109
|
-
senderId: string
|
|
110
|
-
}): { allowed: boolean } {
|
|
111
|
-
const { groupConfig, senderId } = params
|
|
112
|
-
|
|
113
|
-
if (!groupConfig?.users || groupConfig.users.length === 0) {
|
|
114
|
-
return { allowed: true }
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (groupConfig.users.includes("*")) {
|
|
118
|
-
return { allowed: true }
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const normalizedSenderId = senderId.trim().toLowerCase()
|
|
122
|
-
const normalizedUsers = groupConfig.users.map((entry) =>
|
|
123
|
-
String(entry).trim().toLowerCase(),
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
if (normalizedUsers.includes(normalizedSenderId)) {
|
|
127
|
-
return { allowed: true }
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return { allowed: false }
|
|
131
|
-
}
|
package/src/probe.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import type { RuntimeEnv } from "openclaw/plugin-sdk"
|
|
2
|
-
import type { ResolvedMeetAccount } from "./types.js"
|
|
3
|
-
import { getMeetClient } from "./client.js"
|
|
4
|
-
|
|
5
|
-
let _logger: RuntimeEnv | null = null
|
|
6
|
-
|
|
7
|
-
export function setProbeLogger(logger: RuntimeEnv): void {
|
|
8
|
-
_logger = logger
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function log(message: string): void {
|
|
12
|
-
if (_logger) {
|
|
13
|
-
_logger.log(message)
|
|
14
|
-
} else {
|
|
15
|
-
console.log(message)
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export type MeetProbeResult = {
|
|
20
|
-
ok: boolean
|
|
21
|
-
error?: string
|
|
22
|
-
botId?: string
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const probeCache = new Map<string, { result: MeetProbeResult; timestamp: number }>()
|
|
26
|
-
const PROBE_CACHE_TTL_MS = 5 * 60 * 1000
|
|
27
|
-
|
|
28
|
-
export async function probeMeet(
|
|
29
|
-
account: ResolvedMeetAccount,
|
|
30
|
-
): Promise<MeetProbeResult> {
|
|
31
|
-
if (!account.configured) {
|
|
32
|
-
return { ok: false, error: "Not configured" }
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const cacheKey = account.accountId
|
|
36
|
-
const cached = probeCache.get(cacheKey)
|
|
37
|
-
if (cached && Date.now() - cached.timestamp < PROBE_CACHE_TTL_MS) {
|
|
38
|
-
return cached.result
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
// 尝试获取 bot 实例或创建
|
|
43
|
-
let bot = getMeetClient(account.accountId)
|
|
44
|
-
if (!bot) {
|
|
45
|
-
const { createMeetClient } = await import("./client.js")
|
|
46
|
-
bot = createMeetClient(account)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// 尝试获取一次更新来验证连接
|
|
50
|
-
const updates = await bot.getUpdatesV2({ limit: 1, timeout: 1 })
|
|
51
|
-
|
|
52
|
-
const result: MeetProbeResult = {
|
|
53
|
-
ok: true,
|
|
54
|
-
botId: account.apiToken?.split(":")[0],
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (updates && updates.msgs.length > 0) {
|
|
58
|
-
log(`[${account.accountId}] probe: received ${updates.msgs.length} update(s)`)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
probeCache.set(cacheKey, { result, timestamp: Date.now() })
|
|
62
|
-
return result
|
|
63
|
-
} catch (error) {
|
|
64
|
-
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
65
|
-
return { ok: false, error: errorMessage }
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function clearProbeCache(accountId?: string): void {
|
|
70
|
-
if (accountId) {
|
|
71
|
-
probeCache.delete(accountId)
|
|
72
|
-
} else {
|
|
73
|
-
probeCache.clear()
|
|
74
|
-
}
|
|
75
|
-
}
|
package/src/reply-dispatcher.ts
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import type { MeetBot } from "@meet-im/meet-bot-jssdk"
|
|
2
|
-
import type { ClawdbotConfig, RuntimeEnv, ReplyPayload } from "openclaw/plugin-sdk"
|
|
3
|
-
import { createReplyPrefixContext } from "openclaw/plugin-sdk/channel-runtime"
|
|
4
|
-
import { getMeetRuntime } from "./runtime.js"
|
|
5
|
-
import { sendMessageMeet, sendMediaMeet } from "./send.js"
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* 匹配末尾不完整的 mention 开始: <@ 或 <@xxx (没有闭合的 >)
|
|
9
|
-
*/
|
|
10
|
-
const INCOMPLETE_MENTION_START = /<@(-?\d*)$/
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* 匹配开头不完整的 mention 结束: xxx> (没有开始的 <@)
|
|
14
|
-
*/
|
|
15
|
-
const INCOMPLETE_MENTION_END = /^(-?\d+)>/
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* 保护 mention 格式在分片后不被截断
|
|
19
|
-
*
|
|
20
|
-
* 当文本被分片后,`<@userId>` 格式可能被截断成:
|
|
21
|
-
* - 第一个 chunk 末尾: `<@123`
|
|
22
|
-
* - 第二个 chunk 开头: `456>`
|
|
23
|
-
*
|
|
24
|
-
* 此函数检测并修复这种情况,确保 mention 格式完整。
|
|
25
|
-
*/
|
|
26
|
-
export function protectMentionsInChunks(chunks: string[]): string[] {
|
|
27
|
-
if (chunks.length <= 1) {
|
|
28
|
-
return chunks
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const result: string[] = []
|
|
32
|
-
let pendingSuffix = ""
|
|
33
|
-
|
|
34
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
35
|
-
let chunk = pendingSuffix + chunks[i]
|
|
36
|
-
pendingSuffix = ""
|
|
37
|
-
|
|
38
|
-
// 检测末尾是否有不完整的 mention 开始 (<@ 或 <@xxx)
|
|
39
|
-
const startMatch = chunk.match(INCOMPLETE_MENTION_START)
|
|
40
|
-
if (startMatch) {
|
|
41
|
-
// 检查下一个 chunk 是否有对应的结束部分
|
|
42
|
-
const nextChunk = chunks[i + 1]
|
|
43
|
-
if (nextChunk !== undefined) {
|
|
44
|
-
const endMatch = nextChunk.match(INCOMPLETE_MENTION_END)
|
|
45
|
-
if (endMatch) {
|
|
46
|
-
// 将不完整的部分移到下一个 chunk 前面
|
|
47
|
-
const splitIndex = chunk.lastIndexOf("<@")
|
|
48
|
-
// splitIndex >= 0 时处理(包括 chunk 只有 "<@" 的情况)
|
|
49
|
-
if (splitIndex >= 0) {
|
|
50
|
-
// 如果 splitIndex 之前有内容,保留到 result
|
|
51
|
-
if (splitIndex > 0) {
|
|
52
|
-
result.push(chunk.slice(0, splitIndex))
|
|
53
|
-
}
|
|
54
|
-
pendingSuffix = chunk.slice(splitIndex)
|
|
55
|
-
continue
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
result.push(chunk)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return result
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export type CreateMeetReplyDispatcherOpts = {
|
|
68
|
-
cfg: ClawdbotConfig
|
|
69
|
-
agentId: string
|
|
70
|
-
runtime: RuntimeEnv
|
|
71
|
-
chatId: string
|
|
72
|
-
replyToMessageId?: string
|
|
73
|
-
accountId: string
|
|
74
|
-
bot: MeetBot
|
|
75
|
-
botUserId: string
|
|
76
|
-
mediaLocalRoots?: readonly string[]
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export async function createMeetReplyDispatcher(
|
|
80
|
-
opts: CreateMeetReplyDispatcherOpts,
|
|
81
|
-
) {
|
|
82
|
-
const { cfg, agentId, chatId, replyToMessageId, accountId, mediaLocalRoots } = opts
|
|
83
|
-
const core = getMeetRuntime()
|
|
84
|
-
|
|
85
|
-
const textChunkLimit = core.channel.text.resolveTextChunkLimit(cfg, "meet", accountId, {
|
|
86
|
-
fallbackLimit: 4000,
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
const chunkMode = core.channel.text.resolveChunkMode(cfg, "meet", accountId)
|
|
90
|
-
|
|
91
|
-
const prefixContext = createReplyPrefixContext({ cfg, agentId })
|
|
92
|
-
|
|
93
|
-
const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
|
|
94
|
-
responsePrefix: prefixContext.responsePrefix,
|
|
95
|
-
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
|
|
96
|
-
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, agentId),
|
|
97
|
-
onReplyStart: async () => {
|
|
98
|
-
},
|
|
99
|
-
deliver: async (payload: ReplyPayload, _info) => {
|
|
100
|
-
const text = payload.text ?? ""
|
|
101
|
-
const mediaUrls = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : [])
|
|
102
|
-
|
|
103
|
-
// 如果既没有文本也没有媒体,直接返回
|
|
104
|
-
if (!text.trim() && mediaUrls.length === 0) {
|
|
105
|
-
return
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// 处理媒体文件:先发送媒体,再发送文本
|
|
109
|
-
// 如果有媒体,第一个媒体带上文本作为 caption,后续媒体不带文本
|
|
110
|
-
if (mediaUrls.length > 0) {
|
|
111
|
-
// 第一个媒体带上 caption
|
|
112
|
-
await sendMediaMeet({
|
|
113
|
-
cfg,
|
|
114
|
-
to: chatId,
|
|
115
|
-
text: text.trim() || undefined,
|
|
116
|
-
mediaUrl: mediaUrls[0],
|
|
117
|
-
mediaLocalRoots,
|
|
118
|
-
accountId,
|
|
119
|
-
})
|
|
120
|
-
// 后续媒体不带文本
|
|
121
|
-
for (let i = 1; i < mediaUrls.length; i++) {
|
|
122
|
-
await sendMediaMeet({
|
|
123
|
-
cfg,
|
|
124
|
-
to: chatId,
|
|
125
|
-
text: undefined,
|
|
126
|
-
mediaUrl: mediaUrls[i],
|
|
127
|
-
mediaLocalRoots,
|
|
128
|
-
accountId,
|
|
129
|
-
})
|
|
130
|
-
}
|
|
131
|
-
return
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// 只有文本,分片发送
|
|
135
|
-
const rawChunks = core.channel.text.chunkTextWithMode(text, textChunkLimit, chunkMode)
|
|
136
|
-
const protectedChunks = protectMentionsInChunks(rawChunks)
|
|
137
|
-
|
|
138
|
-
for (const chunk of protectedChunks) {
|
|
139
|
-
await sendMessageMeet({
|
|
140
|
-
cfg,
|
|
141
|
-
to: chatId,
|
|
142
|
-
text: chunk,
|
|
143
|
-
accountId,
|
|
144
|
-
replyToMessageId,
|
|
145
|
-
})
|
|
146
|
-
}
|
|
147
|
-
},
|
|
148
|
-
onError: async (error, info) => {
|
|
149
|
-
const errorMessage = String(error)
|
|
150
|
-
opts.runtime.error?.(`meet[${accountId}] ${info.kind} reply failed: ${errorMessage}`)
|
|
151
|
-
// 发送错误提示给用户,让 AI 也能看到错误并修正
|
|
152
|
-
// 根据错误类型生成友好提示
|
|
153
|
-
let userMessage: string
|
|
154
|
-
const lowerError = errorMessage.toLowerCase()
|
|
155
|
-
if (
|
|
156
|
-
lowerError.includes("not found") ||
|
|
157
|
-
lowerError.includes("enoent") ||
|
|
158
|
-
lowerError.includes("does not exist") ||
|
|
159
|
-
lowerError.includes("local media file not found")
|
|
160
|
-
) {
|
|
161
|
-
userMessage = `发送失败: 文件不存在或无法访问。请提供有效的文件路径(建议使用绝对路径)。\n错误详情: ${errorMessage}`
|
|
162
|
-
} else if (
|
|
163
|
-
lowerError.includes("mediafetcherror") ||
|
|
164
|
-
lowerError.includes("fetch_failed") ||
|
|
165
|
-
lowerError.includes("failed to fetch")
|
|
166
|
-
) {
|
|
167
|
-
userMessage = `发送失败: 无法获取媒体文件。请检查 URL 是否正确或网络是否可用。\n错误详情: ${errorMessage}`
|
|
168
|
-
} else if (
|
|
169
|
-
lowerError.includes("localmediaaccesserror") ||
|
|
170
|
-
lowerError.includes("path-not-allowed") ||
|
|
171
|
-
lowerError.includes("not safe to read") ||
|
|
172
|
-
lowerError.includes("network-path-not-allowed")
|
|
173
|
-
) {
|
|
174
|
-
userMessage = `发送失败: 文件路径不在允许访问的目录内。请使用工作区内的文件路径,或联系管理员配置 mediaLocalRoots。\n错误详情: ${errorMessage}`
|
|
175
|
-
} else if (
|
|
176
|
-
lowerError.includes("max_bytes") ||
|
|
177
|
-
lowerError.includes("exceeds maxbytes") ||
|
|
178
|
-
lowerError.includes("exceeds") && lowerError.includes("limit")
|
|
179
|
-
) {
|
|
180
|
-
userMessage = `发送失败: 文件大小超过限制。请使用较小的文件。\n错误详情: ${errorMessage}`
|
|
181
|
-
} else if (lowerError.includes("not a file")) {
|
|
182
|
-
userMessage = `发送失败: 路径不是文件。请提供文件路径而非目录。\n错误详情: ${errorMessage}`
|
|
183
|
-
} else {
|
|
184
|
-
userMessage = `发送失败: ${errorMessage}`
|
|
185
|
-
}
|
|
186
|
-
// 尝试发送错误消息,忽略发送失败(避免递归)
|
|
187
|
-
try {
|
|
188
|
-
await sendMessageMeet({
|
|
189
|
-
cfg,
|
|
190
|
-
to: chatId,
|
|
191
|
-
text: userMessage,
|
|
192
|
-
accountId,
|
|
193
|
-
})
|
|
194
|
-
} catch {
|
|
195
|
-
// 忽略错误消息发送失败
|
|
196
|
-
}
|
|
197
|
-
},
|
|
198
|
-
onIdle: async () => {
|
|
199
|
-
},
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
return {
|
|
203
|
-
dispatcher,
|
|
204
|
-
replyOptions,
|
|
205
|
-
markDispatchIdle,
|
|
206
|
-
}
|
|
207
|
-
}
|
package/src/runtime.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
2
|
-
|
|
3
|
-
let meetRuntime: OpenClawPluginApi["runtime"] | null = null
|
|
4
|
-
|
|
5
|
-
export function setMeetRuntime(runtime: OpenClawPluginApi["runtime"]): void {
|
|
6
|
-
meetRuntime = runtime
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function getMeetRuntime(): OpenClawPluginApi["runtime"] {
|
|
10
|
-
if (!meetRuntime) {
|
|
11
|
-
throw new Error("Meet runtime not initialized. Call setMeetRuntime first.")
|
|
12
|
-
}
|
|
13
|
-
return meetRuntime
|
|
14
|
-
}
|