openclaw-plugin-yuanbao 2.8.0 → 2.9.1
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/openclaw.plugin.json +1 -1
- package/dist/src/channel.js +69 -29
- package/dist/src/commands/upgrade/upgrade.js +55 -9
- package/dist/src/commands/upgrade/utils.d.ts +3 -1
- package/dist/src/commands/upgrade/utils.js +11 -9
- package/dist/src/directory-adapter.d.ts +2 -0
- package/dist/src/directory-adapter.js +58 -0
- package/dist/src/media.d.ts +1 -0
- package/dist/src/media.js +134 -4
- package/dist/src/message-handler/handlers/custom.js +5 -3
- package/dist/src/message-handler/handlers/index.js +2 -1
- package/dist/src/message-handler/handlers/types.d.ts +1 -0
- package/dist/src/message-handler/inbound.js +13 -8
- package/dist/src/message-tool/hints.js +3 -3
- package/dist/src/messaging-adapter.d.ts +4 -0
- package/dist/src/messaging-adapter.js +31 -0
- package/dist/src/module/member.d.ts +5 -0
- package/dist/src/module/member.js +48 -0
- package/dist/src/outbound-queue.d.ts +0 -12
- package/dist/src/outbound-queue.js +1 -179
- package/dist/src/utils/markdown-stream.d.ts +14 -0
- package/dist/src/utils/markdown-stream.js +242 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/src/channel.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { DEFAULT_ACCOUNT_ID, deleteAccountFromConfigSection, setAccountEnabledInConfigSection, } from 'openclaw/plugin-sdk/core';
|
|
2
2
|
import { formatPairingApproveHint } from 'openclaw/plugin-sdk/mattermost';
|
|
3
|
-
import { listYuanbaoAccountIds, resolveDefaultYuanbaoAccountId, resolveYuanbaoAccount } from './accounts.js';
|
|
3
|
+
import { listYuanbaoAccountIds, resolveDefaultYuanbaoAccountId, resolveYuanbaoAccount, } from './accounts.js';
|
|
4
4
|
import { yuanbaoConfigSchema } from './config-schema.js';
|
|
5
5
|
import { yuanbaoOnboardingAdapter } from './onboarding.js';
|
|
6
6
|
import { createLog } from './logger.js';
|
|
7
7
|
import { yuanbaoSetupAdapter } from './setup.js';
|
|
8
|
-
import { startYuanbaoWsGateway, getActiveWsClient } from './yuanbao-server/ws/index.js';
|
|
8
|
+
import { startYuanbaoWsGateway, getActiveWsClient, } from './yuanbao-server/ws/index.js';
|
|
9
9
|
import { getYuanbaoRuntime } from './runtime.js';
|
|
10
|
-
import { sendYuanbaoMessage, sendYuanbaoGroupMessage } from './message-handler/index.js';
|
|
11
|
-
import { initOutboundQueue, destroyOutboundQueue, getOutboundQueue } from './outbound-queue.js';
|
|
10
|
+
import { sendYuanbaoMessage, sendYuanbaoGroupMessage, } from './message-handler/index.js';
|
|
11
|
+
import { initOutboundQueue, destroyOutboundQueue, getOutboundQueue, } from './outbound-queue.js';
|
|
12
12
|
import { ChatType, getGroupCode, parseTarget } from './targets.js';
|
|
13
|
-
import { buildMessageToolHints, yuanbaoMessageActions } from './message-tool/index.js';
|
|
13
|
+
import { buildMessageToolHints, yuanbaoMessageActions, } from './message-tool/index.js';
|
|
14
|
+
import { validateMediaBeforeQueue } from './media.js';
|
|
15
|
+
import { yuanbaoMessagingAdapter } from './messaging-adapter.js';
|
|
16
|
+
import { yuanbaoDirectoryAdapter } from './directory-adapter.js';
|
|
14
17
|
function toChannelResult(result) {
|
|
15
18
|
return {
|
|
16
19
|
channel: 'yuanbao',
|
|
@@ -35,15 +38,32 @@ async function sendTextToTarget(account, target, text, wsClient) {
|
|
|
35
38
|
account,
|
|
36
39
|
config: {},
|
|
37
40
|
core: {},
|
|
38
|
-
log: {
|
|
41
|
+
log: {
|
|
42
|
+
info: () => { },
|
|
43
|
+
warn: () => { },
|
|
44
|
+
error: () => { },
|
|
45
|
+
verbose: () => { },
|
|
46
|
+
},
|
|
39
47
|
wsClient,
|
|
40
48
|
}
|
|
41
49
|
: undefined;
|
|
42
50
|
const { chatType, target: targetId } = parseTarget(target, account.accountId);
|
|
43
51
|
if (chatType === ChatType.GROUP) {
|
|
44
|
-
return sendYuanbaoGroupMessage({
|
|
52
|
+
return sendYuanbaoGroupMessage({
|
|
53
|
+
account,
|
|
54
|
+
groupCode: targetId,
|
|
55
|
+
text,
|
|
56
|
+
fromAccount: account.botId,
|
|
57
|
+
ctx: minCtx,
|
|
58
|
+
});
|
|
45
59
|
}
|
|
46
|
-
return sendYuanbaoMessage({
|
|
60
|
+
return sendYuanbaoMessage({
|
|
61
|
+
account,
|
|
62
|
+
toAccount: targetId,
|
|
63
|
+
text,
|
|
64
|
+
fromAccount: account.botId,
|
|
65
|
+
ctx: minCtx,
|
|
66
|
+
});
|
|
47
67
|
}
|
|
48
68
|
const meta = {
|
|
49
69
|
id: 'yuanbao',
|
|
@@ -57,12 +77,6 @@ const meta = {
|
|
|
57
77
|
order: 85,
|
|
58
78
|
quickstartAllowFrom: true,
|
|
59
79
|
};
|
|
60
|
-
function normalizeYuanbaoMessagingTarget(raw) {
|
|
61
|
-
const trimmed = raw.trim();
|
|
62
|
-
if (!trimmed)
|
|
63
|
-
return undefined;
|
|
64
|
-
return trimmed.replace(/^(yuanbao):/i, '').trim() || undefined;
|
|
65
|
-
}
|
|
66
80
|
export const yuanbaoPlugin = {
|
|
67
81
|
id: 'yuanbao',
|
|
68
82
|
meta,
|
|
@@ -117,7 +131,10 @@ export const yuanbaoPlugin = {
|
|
|
117
131
|
tokenStatus: account.configured ? 'available' : 'missing',
|
|
118
132
|
}),
|
|
119
133
|
resolveAllowFrom: ({ cfg, accountId }) => {
|
|
120
|
-
const account = resolveYuanbaoAccount({
|
|
134
|
+
const account = resolveYuanbaoAccount({
|
|
135
|
+
cfg: cfg,
|
|
136
|
+
accountId,
|
|
137
|
+
});
|
|
121
138
|
return (account.config.dm?.allowFrom ?? []).map(entry => String(entry));
|
|
122
139
|
},
|
|
123
140
|
formatAllowFrom: ({ allowFrom }) => allowFrom
|
|
@@ -129,7 +146,9 @@ export const yuanbaoPlugin = {
|
|
|
129
146
|
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
|
130
147
|
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
131
148
|
const useAccountPath = Boolean(cfg.channels?.yuanbao?.accounts?.[resolvedAccountId]);
|
|
132
|
-
const basePath = useAccountPath
|
|
149
|
+
const basePath = useAccountPath
|
|
150
|
+
? `channels.yuanbao.accounts.${resolvedAccountId}.`
|
|
151
|
+
: 'channels.yuanbao.';
|
|
133
152
|
const policy = account.config.dm?.policy ?? 'open';
|
|
134
153
|
const rawAllowFrom = (account.config.dm?.allowFrom ?? []).map(entry => String(entry));
|
|
135
154
|
const allowFrom = policy === 'open' && !rawAllowFrom.includes('*')
|
|
@@ -151,13 +170,8 @@ export const yuanbaoPlugin = {
|
|
|
151
170
|
threading: {
|
|
152
171
|
resolveReplyToMode: () => 'all',
|
|
153
172
|
},
|
|
154
|
-
messaging:
|
|
155
|
-
|
|
156
|
-
targetResolver: {
|
|
157
|
-
looksLikeId: raw => Boolean(raw.trim()),
|
|
158
|
-
hint: '<userid> or group:<groupcode>',
|
|
159
|
-
},
|
|
160
|
-
},
|
|
173
|
+
messaging: yuanbaoMessagingAdapter,
|
|
174
|
+
directory: yuanbaoDirectoryAdapter,
|
|
161
175
|
agentPrompt: {
|
|
162
176
|
messageToolHints() {
|
|
163
177
|
return buildMessageToolHints();
|
|
@@ -175,7 +189,9 @@ export const yuanbaoPlugin = {
|
|
|
175
189
|
deliveryMode: 'direct',
|
|
176
190
|
chunkerMode: 'markdown',
|
|
177
191
|
textChunkLimit: 3000,
|
|
178
|
-
chunker: (text, limit) => getYuanbaoRuntime()?.channel.text.chunkMarkdownText(text, limit) ?? [
|
|
192
|
+
chunker: (text, limit) => getYuanbaoRuntime()?.channel.text.chunkMarkdownText(text, limit) ?? [
|
|
193
|
+
text,
|
|
194
|
+
],
|
|
179
195
|
sendText: async (params) => {
|
|
180
196
|
const { cfg, accountId, to: _to, text } = params;
|
|
181
197
|
const to = _to.replace(/^yuanbao:/, '');
|
|
@@ -184,7 +200,12 @@ export const yuanbaoPlugin = {
|
|
|
184
200
|
slog.info('sendText', { accountId, to });
|
|
185
201
|
const wsClient = getActiveWsClient(account.accountId);
|
|
186
202
|
if (!wsClient) {
|
|
187
|
-
return {
|
|
203
|
+
return {
|
|
204
|
+
channel: 'yuanbao',
|
|
205
|
+
ok: false,
|
|
206
|
+
messageId: '',
|
|
207
|
+
error: new Error(`WebSocket client not connected for account ${account.accountId}`),
|
|
208
|
+
};
|
|
188
209
|
}
|
|
189
210
|
const queueManager = getOutboundQueue(account.accountId);
|
|
190
211
|
if (queueManager) {
|
|
@@ -203,18 +224,28 @@ export const yuanbaoPlugin = {
|
|
|
203
224
|
return toChannelResult(await sendTextToTarget(account, to, text, wsClient));
|
|
204
225
|
},
|
|
205
226
|
sendMedia: async (params) => {
|
|
206
|
-
const { cfg, accountId, to: _to, mediaUrl, text, mediaLocalRoots } = params;
|
|
227
|
+
const { cfg, accountId, to: _to, mediaUrl, text, mediaLocalRoots, } = params;
|
|
207
228
|
const to = _to.replace(/^yuanbao:/, '');
|
|
208
229
|
const account = resolveYuanbaoAccount({ cfg, accountId: accountId ?? undefined });
|
|
209
230
|
const slog = createLog('channel.outbound');
|
|
210
231
|
const wsClient = getActiveWsClient(account.accountId);
|
|
211
232
|
slog.info('sendMedia', { accountId, to, mediaUrl, text });
|
|
212
233
|
if (!wsClient) {
|
|
213
|
-
return {
|
|
234
|
+
return {
|
|
235
|
+
channel: 'yuanbao',
|
|
236
|
+
ok: false,
|
|
237
|
+
messageId: '',
|
|
238
|
+
error: new Error(`WebSocket client not connected for account ${account.accountId}`),
|
|
239
|
+
};
|
|
214
240
|
}
|
|
215
241
|
if (!mediaUrl) {
|
|
216
242
|
return { channel: 'yuanbao', ok: true, messageId: '' };
|
|
217
243
|
}
|
|
244
|
+
const validationError = validateMediaBeforeQueue(mediaUrl);
|
|
245
|
+
if (validationError) {
|
|
246
|
+
slog.error(`sendMedia 前置校验失败: ${validationError}`, { accountId, to, mediaUrl });
|
|
247
|
+
return { channel: 'yuanbao', ok: false, messageId: '', error: new Error(validationError) };
|
|
248
|
+
}
|
|
218
249
|
const queueManager = getOutboundQueue(account.accountId);
|
|
219
250
|
if (queueManager) {
|
|
220
251
|
const { chatType, target, sessionKey } = parseTarget(to, account.accountId);
|
|
@@ -232,7 +263,12 @@ export const yuanbaoPlugin = {
|
|
|
232
263
|
await session.flush();
|
|
233
264
|
return { channel: 'yuanbao', ok: true, messageId: '' };
|
|
234
265
|
}
|
|
235
|
-
return {
|
|
266
|
+
return {
|
|
267
|
+
channel: 'yuanbao',
|
|
268
|
+
ok: false,
|
|
269
|
+
messageId: '',
|
|
270
|
+
error: new Error('No session found'),
|
|
271
|
+
};
|
|
236
272
|
},
|
|
237
273
|
},
|
|
238
274
|
status: {
|
|
@@ -274,7 +310,11 @@ export const yuanbaoPlugin = {
|
|
|
274
310
|
slog.debug('启动账号', account);
|
|
275
311
|
if (!account.configured) {
|
|
276
312
|
slog.warn('yuanbao not configured; skipping');
|
|
277
|
-
ctx.setStatus({
|
|
313
|
+
ctx.setStatus({
|
|
314
|
+
accountId: account.accountId,
|
|
315
|
+
running: false,
|
|
316
|
+
configured: false,
|
|
317
|
+
});
|
|
278
318
|
return;
|
|
279
319
|
}
|
|
280
320
|
slog.info('使用 WebSocket 模式连接');
|
|
@@ -5,8 +5,32 @@ const INSTALL_SCRIPT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
|
5
5
|
var MessageEnum;
|
|
6
6
|
(function (MessageEnum) {
|
|
7
7
|
MessageEnum["REPAIR_BOT_CONFIG_GUIDE"] = "\u274C \u5347\u7EA7\u5931\u8D25\uFF0C\u8BF7\u524D\u5F80 Bot \u7BA1\u7406\u9875\u9762\u4F7F\u7528\u300C\u4FEE\u590D Bot \u914D\u7F6E\u300D\u529F\u80FD\u4FEE\u590D\u3002";
|
|
8
|
-
MessageEnum["AUTO_UPGRADE_FAILED_FALLBACK"] = "\u274C \u5347\u7EA7\u547D\u4EE4\u6267\u884C\u5931\u8D25\uFF0C\u5143\u5B9D\u521B\u5EFA\u7684 Bot \u53EF\u524D\u5F80\u300CBot \u8BBE\u7F6E\u300D\u70B9\u51FB\u300C\u66F4\u65B0\u63D2\u4EF6\u300D\u8FDB\u884C\u5347\u7EA7\u3002";
|
|
8
|
+
MessageEnum["AUTO_UPGRADE_FAILED_FALLBACK"] = "\u274C \u5347\u7EA7\u547D\u4EE4\u6267\u884C\u5931\u8D25\uFF0C\u5143\u5B9D\u521B\u5EFA\u7684 Bot \u53EF\u524D\u5F80\u300CBot \u8BBE\u7F6E\u300D\u70B9\u51FB\u300C\u66F4\u65B0\u63D2\u4EF6\u300D\u8FDB\u884C\u5347\u7EA7\uFF0C\u975E\u5143\u5B9D\u521B\u5EFA\u7684Bot\uFF0C\u8BF7\u524D\u5F80\u5404\u81EA\u5E73\u53F0\u624B\u52A8\u66F4\u65B0\u3002";
|
|
9
|
+
MessageEnum["RATE_LIMITED"] = "\u274C \u5347\u7EA7\u5931\u8D25\uFF0C\u5F53\u524D\u670D\u52A1\u7E41\u5FD9\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002\u5143\u5B9D\u521B\u5EFA\u7684 Bot \u53EF\u524D\u5F80\u300CBot \u8BBE\u7F6E\u300D\u70B9\u51FB\u300C\u66F4\u65B0\u63D2\u4EF6\u300D\u8FDB\u884C\u5347\u7EA7\u3002";
|
|
9
10
|
})(MessageEnum || (MessageEnum = {}));
|
|
11
|
+
async function verifyVersionAfterFailedCommand(params) {
|
|
12
|
+
const { currentVersion, targetVersion, commandResult, commandName } = params;
|
|
13
|
+
const installedVersion = await readInstalledVersion(PLUGIN_ID);
|
|
14
|
+
const upgraded = !!installedVersion && ((targetVersion != null && installedVersion === targetVersion)
|
|
15
|
+
|| (targetVersion == null && currentVersion != null && installedVersion !== currentVersion));
|
|
16
|
+
if (upgraded) {
|
|
17
|
+
log.warn(`${commandName} 安装命令执行异常,但版本已更新成功`, {
|
|
18
|
+
currentVersion,
|
|
19
|
+
targetVersion,
|
|
20
|
+
installedVersion,
|
|
21
|
+
error: commandResult.error,
|
|
22
|
+
});
|
|
23
|
+
return { upgraded: true, installedVersion };
|
|
24
|
+
}
|
|
25
|
+
log.error(`${commandName} 执行失败`, {
|
|
26
|
+
currentVersion,
|
|
27
|
+
targetVersion,
|
|
28
|
+
installedVersion: installedVersion ?? '(读取失败)',
|
|
29
|
+
error: commandResult.error,
|
|
30
|
+
rateLimited: !!commandResult.rateLimited,
|
|
31
|
+
});
|
|
32
|
+
return { upgraded: false, installedVersion, rateLimited: !!commandResult.rateLimited };
|
|
33
|
+
}
|
|
10
34
|
async function runSpecifiedVersionFlow(params) {
|
|
11
35
|
const { targetVersion: _targetVersion, currentVersion, config, onProgress, } = params;
|
|
12
36
|
const hasTargetVersion = !!_targetVersion;
|
|
@@ -85,12 +109,19 @@ async function runSpecifiedVersionFlow(params) {
|
|
|
85
109
|
};
|
|
86
110
|
}
|
|
87
111
|
if (!installResult.ok) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
};
|
|
112
|
+
const verify = await verifyVersionAfterFailedCommand({
|
|
113
|
+
currentVersion,
|
|
114
|
+
targetVersion: targetVersion ?? null,
|
|
115
|
+
commandResult: installResult,
|
|
116
|
+
commandName: 'plugins install',
|
|
117
|
+
});
|
|
118
|
+
if (!verify.upgraded) {
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
error: installResult.error ?? '插件安装失败',
|
|
122
|
+
message: verify.rateLimited ? MessageEnum.RATE_LIMITED : MessageEnum.REPAIR_BOT_CONFIG_GUIDE,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
94
125
|
}
|
|
95
126
|
log.info('指定版本安装流程完成', { targetVersion, hasSnapshot: !!restoreSnapshotJson });
|
|
96
127
|
await onProgress?.(currentVersion
|
|
@@ -116,8 +147,23 @@ async function runRegularUpgradeFlow(params) {
|
|
|
116
147
|
commandName: 'plugins update',
|
|
117
148
|
});
|
|
118
149
|
if (!updateResult.ok) {
|
|
119
|
-
|
|
120
|
-
|
|
150
|
+
const verify = await verifyVersionAfterFailedCommand({
|
|
151
|
+
currentVersion,
|
|
152
|
+
targetVersion: latestStableVersion,
|
|
153
|
+
commandResult: updateResult,
|
|
154
|
+
commandName: 'plugins update',
|
|
155
|
+
});
|
|
156
|
+
if (verify.upgraded) {
|
|
157
|
+
await onProgress?.(latestStableVersion
|
|
158
|
+
? `✅ 更新成功!**元宝 Bot 插件**已从 v${currentVersion} 升级至 v${latestStableVersion}`
|
|
159
|
+
: `✅ 更新成功!**元宝 Bot 插件**已更新至 v${verify.installedVersion}`);
|
|
160
|
+
return { ok: true };
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
ok: false,
|
|
164
|
+
error: updateResult.error ?? '常规升级失败',
|
|
165
|
+
message: verify.rateLimited ? MessageEnum.RATE_LIMITED : undefined,
|
|
166
|
+
};
|
|
121
167
|
}
|
|
122
168
|
if (updateResult.stdout?.includes('No install record')) {
|
|
123
169
|
return { ok: false, error: updateResult.error ?? '常规升级失败,需要重新安装', needToInstall: true };
|
|
@@ -19,4 +19,6 @@ export declare function runOpenClawCommandWithRetry(params: {
|
|
|
19
19
|
nextAttempt: number;
|
|
20
20
|
maxAttempts: number;
|
|
21
21
|
}) => Promise<void>;
|
|
22
|
-
}): Promise<Awaited<ReturnType<typeof runOpenClawCommand
|
|
22
|
+
}): Promise<Awaited<ReturnType<typeof runOpenClawCommand>> & {
|
|
23
|
+
rateLimited?: boolean;
|
|
24
|
+
}>;
|
|
@@ -131,18 +131,16 @@ export async function isPublishedVersionOnNpm(version) {
|
|
|
131
131
|
}
|
|
132
132
|
export async function readInstalledVersion(pluginId) {
|
|
133
133
|
log.info('读取已安装版本', { pluginId });
|
|
134
|
-
const result = await runOpenClawCommand(['plugins', '
|
|
134
|
+
const result = await runOpenClawCommand(['plugins', 'info', pluginId]);
|
|
135
135
|
if (!result.ok) {
|
|
136
|
-
log.warn('openclaw plugins
|
|
136
|
+
log.warn('openclaw plugins info 执行失败', { summary: result.error, ...(result.stderr ? { stderr: result.stderr } : {}) });
|
|
137
137
|
return null;
|
|
138
138
|
}
|
|
139
139
|
for (const line of (result.stdout ?? '').split('\n')) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return match[1];
|
|
145
|
-
}
|
|
140
|
+
const match = line.match(/^Version:\s*(\d+\.\d+\.\d+(?:-[\w.]+)?)/i);
|
|
141
|
+
if (match) {
|
|
142
|
+
log.info('已安装版本', { pluginId, version: match[1] });
|
|
143
|
+
return match[1];
|
|
146
144
|
}
|
|
147
145
|
}
|
|
148
146
|
log.warn('未检测到已安装版本', { pluginId });
|
|
@@ -199,8 +197,12 @@ export async function runOpenClawCommandWithRetry(params) {
|
|
|
199
197
|
if (lastResult.ok)
|
|
200
198
|
return lastResult;
|
|
201
199
|
const retriable = isRateLimitPluginCommandError(lastResult);
|
|
202
|
-
if (!retriable || attempt >= PLUGIN_CMD_RETRY_MAX_ATTEMPTS)
|
|
200
|
+
if (!retriable || attempt >= PLUGIN_CMD_RETRY_MAX_ATTEMPTS) {
|
|
201
|
+
if (retriable) {
|
|
202
|
+
return { ...lastResult, rateLimited: true };
|
|
203
|
+
}
|
|
203
204
|
break;
|
|
205
|
+
}
|
|
204
206
|
const nextAttempt = attempt + 1;
|
|
205
207
|
const retryDelayMs = PLUGIN_CMD_RETRY_DELAY_MS * attempt;
|
|
206
208
|
log.warn('openclaw plugin 命令触发限频,准备重试', {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { getMember } from './module/member.js';
|
|
2
|
+
function toUserEntry(u) {
|
|
3
|
+
return { kind: 'user', id: u.userId, name: u.nickName };
|
|
4
|
+
}
|
|
5
|
+
function toGroupEntry(g) {
|
|
6
|
+
return { kind: 'group', id: g };
|
|
7
|
+
}
|
|
8
|
+
function applyLimit(arr, limit) {
|
|
9
|
+
return limit != null && limit > 0 ? arr.slice(0, limit) : arr;
|
|
10
|
+
}
|
|
11
|
+
export const yuanbaoDirectoryAdapter = {
|
|
12
|
+
async listPeers({ accountId, query, limit }) {
|
|
13
|
+
const member = getMember(accountId ?? 'default');
|
|
14
|
+
if (query?.trim()) {
|
|
15
|
+
const q = query.trim().toLowerCase();
|
|
16
|
+
const seen = new Set();
|
|
17
|
+
const entries = [];
|
|
18
|
+
const collect = (users) => {
|
|
19
|
+
for (const u of users) {
|
|
20
|
+
if (!seen.has(u.userId) && u.nickName.toLowerCase().includes(q)) {
|
|
21
|
+
seen.add(u.userId);
|
|
22
|
+
entries.push(toUserEntry(u));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
collect(member.listC2cUsers());
|
|
27
|
+
for (const code of member.listGroupCodes()) {
|
|
28
|
+
collect(member.lookupUsers(code));
|
|
29
|
+
}
|
|
30
|
+
return applyLimit(entries, limit);
|
|
31
|
+
}
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
const entries = [];
|
|
34
|
+
const collect = (users) => {
|
|
35
|
+
for (const u of users) {
|
|
36
|
+
if (!seen.has(u.userId)) {
|
|
37
|
+
seen.add(u.userId);
|
|
38
|
+
entries.push(toUserEntry(u));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
collect(member.listC2cUsers());
|
|
43
|
+
for (const code of member.listGroupCodes()) {
|
|
44
|
+
collect(member.lookupUsers(code));
|
|
45
|
+
}
|
|
46
|
+
return applyLimit(entries, limit);
|
|
47
|
+
},
|
|
48
|
+
async listGroups({ accountId, limit }) {
|
|
49
|
+
const member = getMember(accountId ?? 'default');
|
|
50
|
+
const codes = member.listGroupCodes();
|
|
51
|
+
return applyLimit(codes.map(toGroupEntry), limit);
|
|
52
|
+
},
|
|
53
|
+
async listGroupMembers({ accountId, groupId, limit }) {
|
|
54
|
+
const member = getMember(accountId ?? 'default');
|
|
55
|
+
const users = await member.queryMembers(groupId);
|
|
56
|
+
return applyLimit(users.map(toUserEntry), limit);
|
|
57
|
+
},
|
|
58
|
+
};
|
package/dist/src/media.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export declare function parseImageSize(buf: Buffer): {
|
|
|
23
23
|
width: number;
|
|
24
24
|
height: number;
|
|
25
25
|
} | undefined;
|
|
26
|
+
export declare function validateMediaBeforeQueue(url: string): string | null;
|
|
26
27
|
export declare function downloadMediaForYuanbao(url: string, maxMb?: number, account?: ResolvedYuanbaoAccount): Promise<MediaFile>;
|
|
27
28
|
export declare function uploadMediaToCos(mediaFile: MediaFile, account: ResolvedYuanbaoAccount, onProgress?: (percent: number) => void): Promise<MediaUploadResult>;
|
|
28
29
|
export declare function downloadAndUploadMedia(mediaUrl: string, core: PluginRuntime, account: ResolvedYuanbaoAccount, mediaLocalRoots?: string[], onProgress?: (percent: number) => void): Promise<MediaUploadResult>;
|
package/dist/src/media.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { createReadStream, statSync, existsSync } from 'node:fs';
|
|
1
|
+
import { createReadStream, statSync, existsSync, openSync, readSync, closeSync } from 'node:fs';
|
|
2
2
|
import { writeFile, mkdir } from 'node:fs/promises';
|
|
3
3
|
import { tmpdir, homedir } from 'node:os';
|
|
4
4
|
import path, { basename, extname, join } from 'node:path';
|
|
5
5
|
import { randomBytes, createHash } from 'node:crypto';
|
|
6
6
|
import { apiGetDownloadUrl, apiGetUploadInfo } from './yuanbao-server/api.js';
|
|
7
|
+
import { logger } from './logger.js';
|
|
7
8
|
const DEFAULT_MAX_MB = 20;
|
|
8
9
|
export const IMAGE_EXTS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.heic', '.tiff', '.ico']);
|
|
9
10
|
export function guessMimeType(filename) {
|
|
@@ -148,6 +149,134 @@ function normalizePath(s) {
|
|
|
148
149
|
}
|
|
149
150
|
return p;
|
|
150
151
|
}
|
|
152
|
+
function readMagicBytes(filePath, n = 16) {
|
|
153
|
+
const buf = Buffer.alloc(n);
|
|
154
|
+
const fd = openSync(filePath, 'r');
|
|
155
|
+
try {
|
|
156
|
+
const bytesRead = readSync(fd, buf, 0, n, 0);
|
|
157
|
+
return buf.subarray(0, bytesRead);
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
closeSync(fd);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function isMagicJpeg(h) {
|
|
164
|
+
return h.length >= 3 && h[0] === 0xff && h[1] === 0xd8 && h[2] === 0xff;
|
|
165
|
+
}
|
|
166
|
+
function isMagicPng(h) {
|
|
167
|
+
return h.length >= 8
|
|
168
|
+
&& h[0] === 0x89 && h[1] === 0x50 && h[2] === 0x4e && h[3] === 0x47
|
|
169
|
+
&& h[4] === 0x0d && h[5] === 0x0a && h[6] === 0x1a && h[7] === 0x0a;
|
|
170
|
+
}
|
|
171
|
+
function isMagicGif(h) {
|
|
172
|
+
if (h.length < 6)
|
|
173
|
+
return false;
|
|
174
|
+
const sig = h.toString('ascii', 0, 6);
|
|
175
|
+
return sig === 'GIF87a' || sig === 'GIF89a';
|
|
176
|
+
}
|
|
177
|
+
function isMagicWebp(h) {
|
|
178
|
+
return h.length >= 12
|
|
179
|
+
&& h.toString('ascii', 0, 4) === 'RIFF'
|
|
180
|
+
&& h.toString('ascii', 8, 12) === 'WEBP';
|
|
181
|
+
}
|
|
182
|
+
function isMagicBmp(h) {
|
|
183
|
+
return h.length >= 2 && h[0] === 0x42 && h[1] === 0x4d;
|
|
184
|
+
}
|
|
185
|
+
function isMagicPdf(h) {
|
|
186
|
+
return h.length >= 4 && h.toString('ascii', 0, 4) === '%PDF';
|
|
187
|
+
}
|
|
188
|
+
function isMagicZip(h) {
|
|
189
|
+
return h.length >= 3 && h[0] === 0x50 && h[1] === 0x4b && (h[2] === 0x03 || h[2] === 0x05);
|
|
190
|
+
}
|
|
191
|
+
function getTextSnippet(h) {
|
|
192
|
+
const start = (h[0] === 0xef && h[1] === 0xbb && h[2] === 0xbf) ? 3 : 0;
|
|
193
|
+
return h.toString('utf8', start).trimStart()
|
|
194
|
+
.toLowerCase();
|
|
195
|
+
}
|
|
196
|
+
function isMagicHtml(h) {
|
|
197
|
+
const s = getTextSnippet(h);
|
|
198
|
+
return s.startsWith('<!doctype html')
|
|
199
|
+
|| s.startsWith('<html')
|
|
200
|
+
|| s.startsWith('<head')
|
|
201
|
+
|| s.startsWith('<body');
|
|
202
|
+
}
|
|
203
|
+
function isMagicXml(h) {
|
|
204
|
+
const s = getTextSnippet(h);
|
|
205
|
+
return s.startsWith('<?xml') || s.startsWith('<svg');
|
|
206
|
+
}
|
|
207
|
+
function isMagicPrintableText(h) {
|
|
208
|
+
const start = (h[0] === 0xef && h[1] === 0xbb && h[2] === 0xbf) ? 3 : 0;
|
|
209
|
+
return h.length > start && h.subarray(start).every(b => b === 0x09 || b === 0x0a || b === 0x0d || (b >= 0x20 && b <= 0x7e));
|
|
210
|
+
}
|
|
211
|
+
function detectMagicFamily(header) {
|
|
212
|
+
if (header.length < 2)
|
|
213
|
+
return 'unknown';
|
|
214
|
+
if (isMagicJpeg(header))
|
|
215
|
+
return 'jpeg';
|
|
216
|
+
if (isMagicPng(header))
|
|
217
|
+
return 'png';
|
|
218
|
+
if (isMagicGif(header))
|
|
219
|
+
return 'gif';
|
|
220
|
+
if (isMagicWebp(header))
|
|
221
|
+
return 'webp';
|
|
222
|
+
if (isMagicBmp(header))
|
|
223
|
+
return 'bmp';
|
|
224
|
+
if (isMagicPdf(header))
|
|
225
|
+
return 'pdf';
|
|
226
|
+
if (isMagicZip(header))
|
|
227
|
+
return 'zip';
|
|
228
|
+
if (isMagicHtml(header))
|
|
229
|
+
return 'html';
|
|
230
|
+
if (isMagicXml(header))
|
|
231
|
+
return 'xml';
|
|
232
|
+
if (isMagicPrintableText(header))
|
|
233
|
+
return 'text';
|
|
234
|
+
return 'unknown';
|
|
235
|
+
}
|
|
236
|
+
const IMAGE_EXT_TO_EXPECTED_FAMILY = {
|
|
237
|
+
'.jpg': ['jpeg'],
|
|
238
|
+
'.jpeg': ['jpeg'],
|
|
239
|
+
'.png': ['png'],
|
|
240
|
+
'.gif': ['gif'],
|
|
241
|
+
'.webp': ['webp'],
|
|
242
|
+
'.bmp': ['bmp'],
|
|
243
|
+
};
|
|
244
|
+
export function validateMediaBeforeQueue(url) {
|
|
245
|
+
if (!url) {
|
|
246
|
+
return '媒体 URL 为空';
|
|
247
|
+
}
|
|
248
|
+
if (!isLocalPath(url)) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
const filePath = normalizePath(url);
|
|
252
|
+
if (!existsSync(filePath)) {
|
|
253
|
+
return `文件不存在: ${filePath}`;
|
|
254
|
+
}
|
|
255
|
+
let stat;
|
|
256
|
+
try {
|
|
257
|
+
stat = statSync(filePath);
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
return `无法读取文件信息: ${err instanceof Error ? err.message : String(err)}`;
|
|
261
|
+
}
|
|
262
|
+
if (stat.size === 0) {
|
|
263
|
+
return `文件内容为空(0 字节): ${filePath}`;
|
|
264
|
+
}
|
|
265
|
+
let header;
|
|
266
|
+
try {
|
|
267
|
+
header = readMagicBytes(filePath, 16);
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
const actual = detectMagicFamily(header);
|
|
273
|
+
const ext = extname(filePath).toLowerCase();
|
|
274
|
+
const expected = IMAGE_EXT_TO_EXPECTED_FAMILY[ext];
|
|
275
|
+
if (expected && !expected.includes(actual)) {
|
|
276
|
+
return `文件扩展名 ${ext} 与实际内容不符(期望 ${expected.join('/')}, 检测到 ${actual}): ${filePath}`;
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
151
280
|
const MIME_TO_EXT = {
|
|
152
281
|
'image/jpeg': '.jpg',
|
|
153
282
|
'image/png': '.png',
|
|
@@ -357,6 +486,7 @@ export async function downloadMediasToLocalFiles(medias, account, core, log) {
|
|
|
357
486
|
const maxBytes = account.mediaMaxMb * 1024 * 1024;
|
|
358
487
|
const cacheDir = join(tmpdir(), 'yuanbao-media');
|
|
359
488
|
const tasks = medias.slice(0, 20).map(async ({ url, mediaName }, i) => {
|
|
489
|
+
logger.info(`开始解析资源: ${url}`);
|
|
360
490
|
const mediaFile = await downloadMediaForYuanbao(url, account.mediaMaxMb, account);
|
|
361
491
|
const originalFilename = mediaName || mediaFile.filename;
|
|
362
492
|
const ext = extname(originalFilename).toLowerCase();
|
|
@@ -368,7 +498,7 @@ export async function downloadMediasToLocalFiles(medias, account, core, log) {
|
|
|
368
498
|
}
|
|
369
499
|
const cachedFilePath = join(cacheDir, md5Filename);
|
|
370
500
|
if (existsSync(cachedFilePath)) {
|
|
371
|
-
|
|
501
|
+
logger.info(`媒体 ${i + 1}/${medias.length} 命中本地缓存,跳过保存: ${cachedFilePath}`);
|
|
372
502
|
return { path: cachedFilePath, contentType };
|
|
373
503
|
}
|
|
374
504
|
if (typeof core.channel.media?.saveMediaBuffer === 'function') {
|
|
@@ -385,10 +515,10 @@ export async function downloadMediasToLocalFiles(medias, account, core, log) {
|
|
|
385
515
|
const r = settled[i];
|
|
386
516
|
if (r.status === 'fulfilled') {
|
|
387
517
|
results.push(r.value);
|
|
388
|
-
|
|
518
|
+
logger.info(`媒体 ${i + 1}/${medias.length} 下载完成: ${r.value.path} (${r.value.contentType})`);
|
|
389
519
|
}
|
|
390
520
|
else {
|
|
391
|
-
|
|
521
|
+
logger.warn(`媒体 ${i + 1}/${medias.length} 下载失败,跳过: ${String(r.reason)}`);
|
|
392
522
|
}
|
|
393
523
|
}
|
|
394
524
|
return {
|
|
@@ -23,14 +23,16 @@ export const customHandler = {
|
|
|
23
23
|
resData.isAtBot = isAtBotSelf;
|
|
24
24
|
}
|
|
25
25
|
createLog('custom').info('@消息', { text: customContent?.text, userId: customContent?.user_id, isAtBot: resData.isAtBot });
|
|
26
|
-
if (
|
|
26
|
+
if (customContent?.user_id) {
|
|
27
27
|
resData.mentions.push({
|
|
28
28
|
userId: customContent.user_id,
|
|
29
29
|
text: customContent.text || '',
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
if (isAtBotSelf && customContent.text) {
|
|
33
|
+
resData.botUsername = customContent.text.replace(/^@/, '');
|
|
34
|
+
}
|
|
35
|
+
return customContent.text || undefined;
|
|
34
36
|
}
|
|
35
37
|
catch { }
|
|
36
38
|
}
|
|
@@ -6,6 +6,7 @@ import { fileHandler } from './file.js';
|
|
|
6
6
|
import { videoHandler } from './video.js';
|
|
7
7
|
import { faceHandler } from './face.js';
|
|
8
8
|
import { sanitizePipeTables } from '../../utils/markdown-table-sanitize.js';
|
|
9
|
+
import { normalizeMathBlocks } from '../../utils/markdown-stream.js';
|
|
9
10
|
const handlerList = [
|
|
10
11
|
textHandler,
|
|
11
12
|
customHandler,
|
|
@@ -76,7 +77,7 @@ function resolveAtMentions(text, groupCode, memberInst) {
|
|
|
76
77
|
export function prepareOutboundContent(text, groupCode, memberInst) {
|
|
77
78
|
if (!text)
|
|
78
79
|
return [];
|
|
79
|
-
const sanitizedText = sanitizePipeTables(text);
|
|
80
|
+
const sanitizedText = sanitizePipeTables(normalizeMathBlocks(text));
|
|
80
81
|
const items = [];
|
|
81
82
|
if (sanitizedText.length) {
|
|
82
83
|
const trailing = sanitizedText.trim();
|