openclaw-plugin-yuanbao 2.7.1 → 2.8.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/index.js +8 -0
- package/dist/openclaw.plugin.json +1 -1
- package/dist/src/accounts.js +1 -1
- package/dist/src/channel.js +5 -5
- package/dist/src/dm/send-dm.js +0 -1
- package/dist/src/logger.d.ts +0 -1
- package/dist/src/logger.js +24 -21
- package/dist/src/message-handler/callbacks/recall.js +2 -2
- package/dist/src/message-handler/context.d.ts +0 -6
- package/dist/src/message-handler/handlers/custom.js +1 -1
- package/dist/src/message-handler/handlers/index.js +6 -4
- package/dist/src/message-handler/inbound.js +2 -2
- package/dist/src/message-handler/outbound.js +8 -19
- package/dist/src/message-tool/action-runtime.js +0 -1
- package/dist/src/outbound-queue.d.ts +2 -0
- package/dist/src/outbound-queue.js +34 -10
- package/dist/src/types.d.ts +7 -0
- package/dist/src/utils/markdown-table-sanitize.d.ts +1 -0
- package/dist/src/utils/markdown-table-sanitize.js +89 -0
- package/dist/src/yuanbao-server/http/main.d.ts +3 -3
- package/dist/src/yuanbao-server/http/main.js +4 -4
- package/dist/src/yuanbao-server/http/request.d.ts +5 -5
- package/dist/src/yuanbao-server/http/request.js +23 -23
- package/dist/src/yuanbao-server/ws/client.d.ts +0 -2
- package/dist/src/yuanbao-server/ws/client.js +1 -1
- package/dist/src/yuanbao-server/ws/gateway.d.ts +1 -8
- package/dist/src/yuanbao-server/ws/gateway.js +25 -39
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,12 +8,20 @@ import { logUploadCommandDefinition } from './src/commands/log-upload.js';
|
|
|
8
8
|
import { initEnv } from './src/utils/get-env.js';
|
|
9
9
|
import { initBuiltinStickers } from './src/sticker/init-builtin-stickers.js';
|
|
10
10
|
import pluginManifest from './openclaw.plugin.json' with { type: 'json' };
|
|
11
|
+
function patchCommandQueueState() {
|
|
12
|
+
const key = Symbol.for('openclaw.commandQueueState');
|
|
13
|
+
const state = globalThis[key];
|
|
14
|
+
if (state && !state.activeTaskWaiters) {
|
|
15
|
+
state.activeTaskWaiters = new Set();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
11
18
|
const plugin = {
|
|
12
19
|
id: pluginManifest.id,
|
|
13
20
|
name: pluginManifest.name,
|
|
14
21
|
description: pluginManifest.description,
|
|
15
22
|
configSchema: emptyPluginConfigSchema(),
|
|
16
23
|
register(api) {
|
|
24
|
+
patchCommandQueueState();
|
|
17
25
|
initEnv(api);
|
|
18
26
|
initLogger(api);
|
|
19
27
|
setYuanbaoRuntime(api.runtime);
|
package/dist/src/accounts.js
CHANGED
|
@@ -91,7 +91,7 @@ export function resolveYuanbaoAccount(params) {
|
|
|
91
91
|
: 100;
|
|
92
92
|
const disableBlockStreaming = merged.disableBlockStreaming !== undefined ? merged.disableBlockStreaming : false;
|
|
93
93
|
const requireMention = merged.requireMention !== undefined ? merged.requireMention : true;
|
|
94
|
-
const fallbackReply = merged.fallbackReply?.trim()
|
|
94
|
+
const fallbackReply = merged.fallbackReply?.trim();
|
|
95
95
|
const markdownHintEnabled = merged.markdownHintEnabled !== false;
|
|
96
96
|
const configured = Boolean(appKey && appSecret);
|
|
97
97
|
if (!configured && Boolean(yuanbaoConfig)) {
|
package/dist/src/channel.js
CHANGED
|
@@ -167,7 +167,8 @@ export const yuanbaoPlugin = {
|
|
|
167
167
|
blockStreamingChunkMaxChars: 3000,
|
|
168
168
|
blockStreamingCoalesceDefaults: {
|
|
169
169
|
minChars: 2800,
|
|
170
|
-
idleMs:
|
|
170
|
+
idleMs: 1000,
|
|
171
|
+
joiner: '',
|
|
171
172
|
},
|
|
172
173
|
},
|
|
173
174
|
outbound: {
|
|
@@ -176,10 +177,10 @@ export const yuanbaoPlugin = {
|
|
|
176
177
|
textChunkLimit: 3000,
|
|
177
178
|
chunker: (text, limit) => getYuanbaoRuntime()?.channel.text.chunkMarkdownText(text, limit) ?? [text],
|
|
178
179
|
sendText: async (params) => {
|
|
179
|
-
const slog = createLog('channel.utbound');
|
|
180
180
|
const { cfg, accountId, to: _to, text } = params;
|
|
181
181
|
const to = _to.replace(/^yuanbao:/, '');
|
|
182
182
|
const account = resolveYuanbaoAccount({ cfg, accountId: accountId ?? undefined });
|
|
183
|
+
const slog = createLog('channel.outbound');
|
|
183
184
|
slog.info('sendText', { accountId, to });
|
|
184
185
|
const wsClient = getActiveWsClient(account.accountId);
|
|
185
186
|
if (!wsClient) {
|
|
@@ -202,10 +203,10 @@ export const yuanbaoPlugin = {
|
|
|
202
203
|
return toChannelResult(await sendTextToTarget(account, to, text, wsClient));
|
|
203
204
|
},
|
|
204
205
|
sendMedia: async (params) => {
|
|
205
|
-
const slog = createLog('channel.outbound');
|
|
206
206
|
const { cfg, accountId, to: _to, mediaUrl, text, mediaLocalRoots } = params;
|
|
207
207
|
const to = _to.replace(/^yuanbao:/, '');
|
|
208
208
|
const account = resolveYuanbaoAccount({ cfg, accountId: accountId ?? undefined });
|
|
209
|
+
const slog = createLog('channel.outbound');
|
|
209
210
|
const wsClient = getActiveWsClient(account.accountId);
|
|
210
211
|
slog.info('sendMedia', { accountId, to, mediaUrl, text });
|
|
211
212
|
if (!wsClient) {
|
|
@@ -269,7 +270,7 @@ export const yuanbaoPlugin = {
|
|
|
269
270
|
gateway: {
|
|
270
271
|
startAccount: async (ctx) => {
|
|
271
272
|
const { account } = ctx;
|
|
272
|
-
const slog = createLog('gateway'
|
|
273
|
+
const slog = createLog('gateway');
|
|
273
274
|
slog.debug('启动账号', account);
|
|
274
275
|
if (!account.configured) {
|
|
275
276
|
slog.warn('yuanbao not configured; skipping');
|
|
@@ -295,7 +296,6 @@ export const yuanbaoPlugin = {
|
|
|
295
296
|
account,
|
|
296
297
|
config: ctx.cfg,
|
|
297
298
|
abortSignal: ctx.abortSignal,
|
|
298
|
-
log: ctx.log,
|
|
299
299
|
runtime: getYuanbaoRuntime(),
|
|
300
300
|
statusSink: patch => ctx.setStatus({ accountId: ctx.accountId, ...patch }),
|
|
301
301
|
});
|
package/dist/src/dm/send-dm.js
CHANGED
package/dist/src/logger.d.ts
CHANGED
|
@@ -8,7 +8,6 @@ export interface PluginLogger {
|
|
|
8
8
|
}
|
|
9
9
|
export declare function initLogger(api: OpenClawPluginApi): void;
|
|
10
10
|
export declare const logger: PluginLogger;
|
|
11
|
-
export declare function isVerbose(): boolean;
|
|
12
11
|
export interface LogSink {
|
|
13
12
|
info?: (msg: string) => void;
|
|
14
13
|
warn?: (msg: string) => void;
|
package/dist/src/logger.js
CHANGED
|
@@ -2,7 +2,22 @@ import { getPluginVersion } from './utils/get-env.js';
|
|
|
2
2
|
export const LOG_PREFIX = `[yuanbao:${getPluginVersion()}]`;
|
|
3
3
|
let childLogger = null;
|
|
4
4
|
let initialized = false;
|
|
5
|
-
let
|
|
5
|
+
let debugMode = false;
|
|
6
|
+
const SANITIZE_RULES = {
|
|
7
|
+
always: {
|
|
8
|
+
omit: [],
|
|
9
|
+
mask: [
|
|
10
|
+
'token', 'signature', 'app_key', 'appkey',
|
|
11
|
+
'appsecret', 'app_secret', 'secret', 'password', 'x-token',
|
|
12
|
+
],
|
|
13
|
+
},
|
|
14
|
+
debug: {
|
|
15
|
+
omit: ['msg_body'],
|
|
16
|
+
mask: ['user_input', 'cloud_custom_data', 'model_output'],
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
let OMIT_KEYS = new Set([...SANITIZE_RULES.always.omit, ...SANITIZE_RULES.debug.omit]);
|
|
20
|
+
let SENSITIVE_KEYS = new Set([...SANITIZE_RULES.always.mask, ...SANITIZE_RULES.debug.mask]);
|
|
6
21
|
const fallbackLogger = {
|
|
7
22
|
info(message, meta) {
|
|
8
23
|
console.log(`${LOG_PREFIX} ${message}`, meta ?? '');
|
|
@@ -19,8 +34,12 @@ const fallbackLogger = {
|
|
|
19
34
|
};
|
|
20
35
|
export function initLogger(api) {
|
|
21
36
|
try {
|
|
37
|
+
debugMode = api.config.channels?.yuanbao?.debug === true;
|
|
38
|
+
if (debugMode) {
|
|
39
|
+
OMIT_KEYS = new Set(SANITIZE_RULES.always.omit);
|
|
40
|
+
SENSITIVE_KEYS = new Set(SANITIZE_RULES.always.mask);
|
|
41
|
+
}
|
|
22
42
|
childLogger = api.runtime.logging.getChildLogger({ plugin: 'yuanbao' });
|
|
23
|
-
verboseEnabled = api.runtime.logging.shouldLogVerbose?.() ?? false;
|
|
24
43
|
initialized = true;
|
|
25
44
|
}
|
|
26
45
|
catch (err) {
|
|
@@ -52,12 +71,11 @@ export const logger = {
|
|
|
52
71
|
getActiveLogger().debug(message, meta);
|
|
53
72
|
},
|
|
54
73
|
};
|
|
55
|
-
export function isVerbose() {
|
|
56
|
-
return verboseEnabled;
|
|
57
|
-
}
|
|
58
74
|
export function formatLog(module, msg, data) {
|
|
59
75
|
const prefix = module ? `${LOG_PREFIX}[${module}]` : LOG_PREFIX;
|
|
60
|
-
|
|
76
|
+
if (data === undefined)
|
|
77
|
+
return `${prefix} ${msg}`;
|
|
78
|
+
return `${prefix} ${msg} ${sanitize(data)}`;
|
|
61
79
|
}
|
|
62
80
|
export function createLog(module, sink) {
|
|
63
81
|
const target = sink ?? logger;
|
|
@@ -71,21 +89,6 @@ export function createLog(module, sink) {
|
|
|
71
89
|
debug: (msg, data) => (target.debug ?? target.verbose)?.(fmt(msg, data)),
|
|
72
90
|
};
|
|
73
91
|
}
|
|
74
|
-
const OMIT_KEYS = new Set(['msg_body']);
|
|
75
|
-
const SENSITIVE_KEYS = new Set([
|
|
76
|
-
'token',
|
|
77
|
-
'signature',
|
|
78
|
-
'app_key',
|
|
79
|
-
'appkey',
|
|
80
|
-
'appsecret',
|
|
81
|
-
'app_secret',
|
|
82
|
-
'secret',
|
|
83
|
-
'password',
|
|
84
|
-
'x-token',
|
|
85
|
-
'user_input',
|
|
86
|
-
'cloud_custom_data',
|
|
87
|
-
'model_output',
|
|
88
|
-
]);
|
|
89
92
|
function maskValue(value) {
|
|
90
93
|
if (value.length < 8)
|
|
91
94
|
return '***';
|
|
@@ -13,7 +13,7 @@ function enqueueRecallSystemEvent(params) {
|
|
|
13
13
|
}
|
|
14
14
|
export function handleGroupRecall(ctx, msg) {
|
|
15
15
|
const { core, account } = ctx;
|
|
16
|
-
const log = createLog('recall'
|
|
16
|
+
const log = createLog('recall');
|
|
17
17
|
const groupCode = msg.group_code?.trim() || 'unknown';
|
|
18
18
|
const seqList = msg.recall_msg_seq_list;
|
|
19
19
|
if (!seqList || seqList.length === 0) {
|
|
@@ -57,7 +57,7 @@ export function handleGroupRecall(ctx, msg) {
|
|
|
57
57
|
}
|
|
58
58
|
export function handleC2CRecall(ctx, msg) {
|
|
59
59
|
const { core, account } = ctx;
|
|
60
|
-
const log = createLog('recall'
|
|
60
|
+
const log = createLog('recall');
|
|
61
61
|
const fromAccount = msg.from_account?.trim() || 'unknown';
|
|
62
62
|
const seqList = msg.msg_id
|
|
63
63
|
? [{ msg_id: msg.msg_id, msg_seq: msg.msg_seq }]
|
|
@@ -7,12 +7,6 @@ export type MessageHandlerContext = {
|
|
|
7
7
|
account: ResolvedYuanbaoAccount;
|
|
8
8
|
config: OpenClawConfig;
|
|
9
9
|
core: PluginRuntime;
|
|
10
|
-
log: {
|
|
11
|
-
info: (msg: string) => void;
|
|
12
|
-
warn: (msg: string) => void;
|
|
13
|
-
error: (msg: string) => void;
|
|
14
|
-
verbose: (msg: string) => void;
|
|
15
|
-
};
|
|
16
10
|
statusSink?: (patch: {
|
|
17
11
|
lastInboundAt?: number;
|
|
18
12
|
lastOutboundAt?: number;
|
|
@@ -22,7 +22,7 @@ export const customHandler = {
|
|
|
22
22
|
if (!resData.isAtBot) {
|
|
23
23
|
resData.isAtBot = isAtBotSelf;
|
|
24
24
|
}
|
|
25
|
-
createLog('custom'
|
|
25
|
+
createLog('custom').info('@消息', { text: customContent?.text, userId: customContent?.user_id, isAtBot: resData.isAtBot });
|
|
26
26
|
if (!isAtBotSelf && customContent?.user_id) {
|
|
27
27
|
resData.mentions.push({
|
|
28
28
|
userId: customContent.user_id,
|
|
@@ -5,6 +5,7 @@ import { soundHandler } from './sound.js';
|
|
|
5
5
|
import { fileHandler } from './file.js';
|
|
6
6
|
import { videoHandler } from './video.js';
|
|
7
7
|
import { faceHandler } from './face.js';
|
|
8
|
+
import { sanitizePipeTables } from '../../utils/markdown-table-sanitize.js';
|
|
8
9
|
const handlerList = [
|
|
9
10
|
textHandler,
|
|
10
11
|
customHandler,
|
|
@@ -75,15 +76,16 @@ function resolveAtMentions(text, groupCode, memberInst) {
|
|
|
75
76
|
export function prepareOutboundContent(text, groupCode, memberInst) {
|
|
76
77
|
if (!text)
|
|
77
78
|
return [];
|
|
79
|
+
const sanitizedText = sanitizePipeTables(text);
|
|
78
80
|
const items = [];
|
|
79
|
-
if (
|
|
80
|
-
const trailing =
|
|
81
|
+
if (sanitizedText.length) {
|
|
82
|
+
const trailing = sanitizedText.trim();
|
|
81
83
|
if (trailing) {
|
|
82
84
|
items.push(...resolveAtMentions(trailing, groupCode, memberInst));
|
|
83
85
|
}
|
|
84
86
|
}
|
|
85
|
-
if (items.length === 0 &&
|
|
86
|
-
items.push(...resolveAtMentions(
|
|
87
|
+
if (items.length === 0 && sanitizedText.trim()) {
|
|
88
|
+
items.push(...resolveAtMentions(sanitizedText.trim(), groupCode, memberInst));
|
|
87
89
|
}
|
|
88
90
|
return items;
|
|
89
91
|
}
|
|
@@ -122,7 +122,7 @@ async function handleC2CMessage(params) {
|
|
|
122
122
|
const fromAccount = msg.from_account?.trim() || 'unknown';
|
|
123
123
|
const senderNickname = msg.sender_nickname?.trim() || undefined;
|
|
124
124
|
const outboundSender = resolveOutboundSenderAccount(account);
|
|
125
|
-
const log = createLog('inbound'
|
|
125
|
+
const log = createLog('inbound');
|
|
126
126
|
if (outboundSender && fromAccount === outboundSender) {
|
|
127
127
|
log.info(`跳过机器人自身消息 <- ${fromAccount}`);
|
|
128
128
|
return;
|
|
@@ -336,7 +336,7 @@ async function handleGroupMessage(params) {
|
|
|
336
336
|
const fromAccount = msg.from_account?.trim() || 'unknown';
|
|
337
337
|
const senderNickname = msg.sender_nickname?.trim() || undefined;
|
|
338
338
|
const outboundSender = resolveOutboundSenderAccount(account);
|
|
339
|
-
const glog = createLog('inbound'
|
|
339
|
+
const glog = createLog('inbound');
|
|
340
340
|
setGroupCode(groupCode);
|
|
341
341
|
if (outboundSender && fromAccount === outboundSender) {
|
|
342
342
|
glog.info('跳过机器人自身消息', { groupCode, fromAccount });
|
|
@@ -34,7 +34,7 @@ async function shouldAttachReplyRef(params) {
|
|
|
34
34
|
}
|
|
35
35
|
export async function sendYuanbaoMessageBody(params) {
|
|
36
36
|
const { toAccount, msgBody, fromAccount, ctx } = params;
|
|
37
|
-
const log = createLog('outbound'
|
|
37
|
+
const log = createLog('outbound');
|
|
38
38
|
if (!ctx?.wsClient) {
|
|
39
39
|
log.error('发送失败: WebSocket 客户端不可用');
|
|
40
40
|
return { ok: false, error: 'wsClient not available' };
|
|
@@ -77,7 +77,7 @@ export async function sendYuanbaoMessage(params) {
|
|
|
77
77
|
}
|
|
78
78
|
export async function sendYuanbaoGroupMessageBody(params) {
|
|
79
79
|
const { account, groupCode, msgBody, fromAccount, refMsgId, refFromAccount, ctx } = params;
|
|
80
|
-
const log = createLog('outbound'
|
|
80
|
+
const log = createLog('outbound');
|
|
81
81
|
if (!ctx?.wsClient) {
|
|
82
82
|
log.error('发送群消息失败: WebSocket 客户端不可用');
|
|
83
83
|
return { ok: false, error: 'wsClient not available' };
|
|
@@ -153,7 +153,7 @@ export async function sendMsgBodyDirect(params) {
|
|
|
153
153
|
}
|
|
154
154
|
export async function executeReply(params) {
|
|
155
155
|
const { transport, ctx, account, core, replyRuntime, splitFinalText, overflowPolicy, ctxPayload, sessionKey, appendText, } = params;
|
|
156
|
-
const rlog = createLog('outbound'
|
|
156
|
+
const rlog = createLog('outbound');
|
|
157
157
|
if (ctx.abortSignal?.aborted) {
|
|
158
158
|
rlog.warn(`[${account.accountId}] 回复已中止,跳过执行`);
|
|
159
159
|
return;
|
|
@@ -165,7 +165,6 @@ export async function executeReply(params) {
|
|
|
165
165
|
: null;
|
|
166
166
|
const collectedTexts = [];
|
|
167
167
|
let hasFinalInfo = false;
|
|
168
|
-
let prevDeliverKind = null;
|
|
169
168
|
const dispatchReply = () => core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
170
169
|
ctx: ctxPayload,
|
|
171
170
|
cfg: replyRuntime.config,
|
|
@@ -174,17 +173,6 @@ export async function executeReply(params) {
|
|
|
174
173
|
onAgentRunStart: () => {
|
|
175
174
|
session?.emitReplyHeartbeat(WS_HEARTBEAT.RUNNING);
|
|
176
175
|
},
|
|
177
|
-
onAssistantMessageStart: async () => {
|
|
178
|
-
session?.emitReplyHeartbeat(WS_HEARTBEAT.RUNNING);
|
|
179
|
-
rlog.info('[OpenClaw] onAssistantMessageStart');
|
|
180
|
-
try {
|
|
181
|
-
if (session)
|
|
182
|
-
await session.drainNow();
|
|
183
|
-
}
|
|
184
|
-
catch (err) {
|
|
185
|
-
rlog.error('[OpenClaw] onAssistantMessageStart drainNow 失败,跳过', { error: String(err) });
|
|
186
|
-
}
|
|
187
|
-
},
|
|
188
176
|
onToolStart: async () => {
|
|
189
177
|
rlog.info('[OpenClaw] onToolStart');
|
|
190
178
|
try {
|
|
@@ -203,6 +191,10 @@ export async function executeReply(params) {
|
|
|
203
191
|
return;
|
|
204
192
|
}
|
|
205
193
|
rlog.info('[deliver] 收到回复数据', { kind: info.kind, model_output: payload.text });
|
|
194
|
+
if (payload.isReasoning) {
|
|
195
|
+
rlog.info('[deliver] Reasoning', { text: payload.text });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
206
198
|
if (payload.isCompactionNotice) {
|
|
207
199
|
rlog.info('[deliver] CompactionNotice', { text: payload.text });
|
|
208
200
|
return;
|
|
@@ -213,13 +205,10 @@ export async function executeReply(params) {
|
|
|
213
205
|
if (info.kind === 'final') {
|
|
214
206
|
hasFinalInfo = true;
|
|
215
207
|
}
|
|
216
|
-
const prevKind = prevDeliverKind;
|
|
217
|
-
prevDeliverKind = info.kind;
|
|
218
208
|
const text = payload.text ?? '';
|
|
219
209
|
if (session) {
|
|
220
210
|
if (text.trim()) {
|
|
221
|
-
|
|
222
|
-
await session.push({ type: 'text', text: isAfterToolCall ? `\n\n${text}` : text });
|
|
211
|
+
await session.push({ type: 'text', text });
|
|
223
212
|
}
|
|
224
213
|
const mediaUrls = payload.mediaUrls ?? [];
|
|
225
214
|
for (const mediaUrl of mediaUrls) {
|
|
@@ -55,7 +55,6 @@ export async function handleYuanbaoAction(action, params, context) {
|
|
|
55
55
|
account: context.account,
|
|
56
56
|
config: context.config,
|
|
57
57
|
core: getYuanbaoRuntime(),
|
|
58
|
-
log: { info: () => { }, warn: () => { }, error: () => { }, verbose: () => { } },
|
|
59
58
|
wsClient: context.wsClient,
|
|
60
59
|
groupCode: getGroupCode(),
|
|
61
60
|
};
|
|
@@ -74,6 +74,8 @@ export declare function getOutboundQueue(accountId: string): OutboundQueueManage
|
|
|
74
74
|
export declare function destroyOutboundQueue(accountId: string): void;
|
|
75
75
|
export declare function endsWithTableRow(text: string): boolean;
|
|
76
76
|
export declare function hasUnclosedFence(text: string): boolean;
|
|
77
|
+
export declare function startsWithBlockElement(text: string): boolean;
|
|
78
|
+
export declare function inferBlockSeparator(buffer: string, incoming: string): string;
|
|
77
79
|
export type AtomicBlock = {
|
|
78
80
|
start: number;
|
|
79
81
|
end: number;
|
|
@@ -303,7 +303,7 @@ function createMergeOnFlushSession(callbacks, msgId, onComplete, log, heartbeatM
|
|
|
303
303
|
if (aborted)
|
|
304
304
|
return hasSentContent;
|
|
305
305
|
if (collectedTexts.length > 0) {
|
|
306
|
-
const merged = stripOuterMarkdownFence(collectedTexts.join('
|
|
306
|
+
const merged = stripOuterMarkdownFence(collectedTexts.join(''));
|
|
307
307
|
collectedTexts.length = 0;
|
|
308
308
|
if (merged.trim()) {
|
|
309
309
|
const result = await sendText(merged);
|
|
@@ -377,6 +377,36 @@ export function hasUnclosedFence(text) {
|
|
|
377
377
|
}
|
|
378
378
|
return inFence;
|
|
379
379
|
}
|
|
380
|
+
export function startsWithBlockElement(text) {
|
|
381
|
+
const firstLine = (text.trimStart().split('\n')[0] ?? '').trimStart();
|
|
382
|
+
return /^#{1,6}\s/.test(firstLine)
|
|
383
|
+
|| firstLine.startsWith('---')
|
|
384
|
+
|| firstLine.startsWith('***')
|
|
385
|
+
|| firstLine.startsWith('___')
|
|
386
|
+
|| firstLine.startsWith('> ')
|
|
387
|
+
|| firstLine.startsWith('```')
|
|
388
|
+
|| /^[*\-+]\s/.test(firstLine)
|
|
389
|
+
|| /^\d+[.)]\s/.test(firstLine)
|
|
390
|
+
|| firstLine.startsWith('|');
|
|
391
|
+
}
|
|
392
|
+
export function inferBlockSeparator(buffer, incoming) {
|
|
393
|
+
if (hasUnclosedFence(buffer))
|
|
394
|
+
return '';
|
|
395
|
+
if (buffer.endsWith('\n\n'))
|
|
396
|
+
return '';
|
|
397
|
+
const lastLine = (buffer.trimEnd().split('\n')
|
|
398
|
+
.at(-1) ?? '').trim();
|
|
399
|
+
const firstLine = (incoming.trimStart().split('\n')[0] ?? '').trimStart();
|
|
400
|
+
if (lastLine.startsWith('|') && !firstLine.startsWith('|')
|
|
401
|
+
&& firstLine.endsWith('|')) {
|
|
402
|
+
return ' ';
|
|
403
|
+
}
|
|
404
|
+
if (lastLine.startsWith('|') && firstLine.startsWith('|'))
|
|
405
|
+
return '\n';
|
|
406
|
+
if (startsWithBlockElement(incoming))
|
|
407
|
+
return '\n\n';
|
|
408
|
+
return '';
|
|
409
|
+
}
|
|
380
410
|
const DIAGRAM_LANGUAGES = new Set([
|
|
381
411
|
'mermaid', 'plantuml', 'sequence', 'flowchart',
|
|
382
412
|
'gantt', 'classdiagram', 'statediagram', 'erdiagram',
|
|
@@ -396,7 +426,7 @@ export function extractAtomicBlocks(text) {
|
|
|
396
426
|
const isTableLine = (line) => line.trim().startsWith('|');
|
|
397
427
|
const isTableSeparator = (line) => /^\|[\s|:-]+\|$/.test(line.trim());
|
|
398
428
|
const flushTable = () => {
|
|
399
|
-
if (tableStart !== -1 &&
|
|
429
|
+
if (tableStart !== -1 && tableEnd !== -1 && (tableHasSep || tableLineCount >= 2)) {
|
|
400
430
|
blocks.push({ start: tableStart, end: tableEnd, kind: 'table' });
|
|
401
431
|
}
|
|
402
432
|
tableStart = -1;
|
|
@@ -582,14 +612,8 @@ function createMergeTextSession(callbacks, msgId, sessionKey, onComplete, log, o
|
|
|
582
612
|
if (!item.text.trim())
|
|
583
613
|
return;
|
|
584
614
|
if (textBuffer) {
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
const incomingStartsWithTableRow = item.text.trimStart().startsWith('|');
|
|
588
|
-
const needsSeparator = !hasUnclosedFence(textBuffer)
|
|
589
|
-
&& incomingStartsWithTableRow
|
|
590
|
-
&& !textBuffer.endsWith('\n')
|
|
591
|
-
&& !lastLineOfBuffer.startsWith('|');
|
|
592
|
-
textBuffer = mergeBlockStreamingFences(needsSeparator ? `${textBuffer}\n\n` : textBuffer, item.text);
|
|
615
|
+
const separator = inferBlockSeparator(textBuffer, item.text);
|
|
616
|
+
textBuffer = mergeBlockStreamingFences(separator ? `${textBuffer}${separator}` : textBuffer, item.text);
|
|
593
617
|
}
|
|
594
618
|
else {
|
|
595
619
|
textBuffer = item.text;
|
package/dist/src/types.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ export type YuanbaoConfig = YuanbaoAccountConfig & {
|
|
|
32
32
|
accounts?: Record<string, YuanbaoAccountConfig>;
|
|
33
33
|
defaultAccount?: string;
|
|
34
34
|
routeEnv?: string;
|
|
35
|
+
debug?: boolean;
|
|
35
36
|
};
|
|
36
37
|
export type ResolvedYuanbaoAccount = {
|
|
37
38
|
accountId: string;
|
|
@@ -134,6 +135,12 @@ export type CloudCustomData = {
|
|
|
134
135
|
source_group?: string;
|
|
135
136
|
[key: string]: unknown;
|
|
136
137
|
};
|
|
138
|
+
export type YuanbaoBufferedDeliverPayload = {
|
|
139
|
+
text?: string;
|
|
140
|
+
mediaUrls?: string[];
|
|
141
|
+
isCompactionNotice: boolean;
|
|
142
|
+
isReasoning: boolean;
|
|
143
|
+
};
|
|
137
144
|
export type YuanbaoSendMsgRequest = {
|
|
138
145
|
sync_other_machine?: number;
|
|
139
146
|
from_account?: string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function sanitizePipeTables(text: string): string;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
function findTableRegions(lines) {
|
|
2
|
+
const regions = [];
|
|
3
|
+
let groupStart = -1;
|
|
4
|
+
let lastPipeLine = -1;
|
|
5
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6
|
+
const line = lines[i];
|
|
7
|
+
const hasPipe = line.includes('|');
|
|
8
|
+
const isBlank = line.trim() === '';
|
|
9
|
+
if (hasPipe) {
|
|
10
|
+
if (groupStart < 0)
|
|
11
|
+
groupStart = i;
|
|
12
|
+
lastPipeLine = i;
|
|
13
|
+
}
|
|
14
|
+
else if (isBlank) {
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
if (groupStart >= 0) {
|
|
18
|
+
regions.push({ startLine: groupStart, endLine: lastPipeLine });
|
|
19
|
+
groupStart = -1;
|
|
20
|
+
lastPipeLine = -1;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (groupStart >= 0) {
|
|
25
|
+
regions.push({ startLine: groupStart, endLine: lastPipeLine });
|
|
26
|
+
}
|
|
27
|
+
return regions;
|
|
28
|
+
}
|
|
29
|
+
const SEPARATOR_RE = /\|[\s]*:?-{2,}:?[\s]*(?:\|[\s]*:?-{2,}:?[\s]*)+\|/;
|
|
30
|
+
function findSeparatorInFlat(flat) {
|
|
31
|
+
const m = SEPARATOR_RE.exec(flat);
|
|
32
|
+
if (!m)
|
|
33
|
+
return null;
|
|
34
|
+
const pipeCount = (m[0].match(/\|/g) || []).length;
|
|
35
|
+
if (pipeCount < 2)
|
|
36
|
+
return null;
|
|
37
|
+
return { numColumns: pipeCount - 1 };
|
|
38
|
+
}
|
|
39
|
+
function healRegion(regionLines) {
|
|
40
|
+
if (!regionLines.some(l => l.trim() === ''))
|
|
41
|
+
return null;
|
|
42
|
+
const flat = regionLines.join('').replace(/\n/g, '');
|
|
43
|
+
if (!findSeparatorInFlat(flat))
|
|
44
|
+
return null;
|
|
45
|
+
const nonBlank = regionLines.filter(l => l.trim() !== '');
|
|
46
|
+
const result = [];
|
|
47
|
+
let acc = '';
|
|
48
|
+
for (const line of nonBlank) {
|
|
49
|
+
if (!acc) {
|
|
50
|
+
acc = line;
|
|
51
|
+
}
|
|
52
|
+
else if (acc.trimEnd().endsWith('|') && line.trimStart().startsWith('|')) {
|
|
53
|
+
result.push(acc);
|
|
54
|
+
acc = line;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
acc += line;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (acc)
|
|
61
|
+
result.push(acc);
|
|
62
|
+
return result.join('\n');
|
|
63
|
+
}
|
|
64
|
+
export function sanitizePipeTables(text) {
|
|
65
|
+
if (!text)
|
|
66
|
+
return text;
|
|
67
|
+
if (!text.includes('|'))
|
|
68
|
+
return text;
|
|
69
|
+
if (!text.includes('\n'))
|
|
70
|
+
return text;
|
|
71
|
+
const pipeCount = (text.match(/\|/g) || []).length;
|
|
72
|
+
if (pipeCount < 3)
|
|
73
|
+
return text;
|
|
74
|
+
const lines = text.split('\n');
|
|
75
|
+
const regions = findTableRegions(lines);
|
|
76
|
+
if (regions.length === 0) {
|
|
77
|
+
return text;
|
|
78
|
+
}
|
|
79
|
+
for (let ri = regions.length - 1; ri >= 0; ri--) {
|
|
80
|
+
const region = regions[ri];
|
|
81
|
+
const regionLines = lines.slice(region.startLine, region.endLine + 1);
|
|
82
|
+
const healed = healRegion(regionLines);
|
|
83
|
+
if (healed !== null) {
|
|
84
|
+
const healedLines = healed.split('\n');
|
|
85
|
+
lines.splice(region.startLine, region.endLine - region.startLine + 1, ...healedLines);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return lines.join('\n');
|
|
89
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { ResolvedYuanbaoAccount } from '../../types.js';
|
|
2
|
-
import type {
|
|
3
|
-
export declare function apiGetUploadInfo(account: ResolvedYuanbaoAccount, fileName: string, fileId: string
|
|
4
|
-
export declare function apiGetDownloadUrl(account: ResolvedYuanbaoAccount, resourceId: string
|
|
2
|
+
import type { CosUploadConfig } from './request.js';
|
|
3
|
+
export declare function apiGetUploadInfo(account: ResolvedYuanbaoAccount, fileName: string, fileId: string): Promise<CosUploadConfig>;
|
|
4
|
+
export declare function apiGetDownloadUrl(account: ResolvedYuanbaoAccount, resourceId: string): Promise<string>;
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { yuanbaoPost, yuanbaoGet } from './request.js';
|
|
2
2
|
const UPLOAD_INFO_PATH = '/api/resource/genUploadInfo';
|
|
3
3
|
const DOWNLOAD_INFO_PATH = '/api/resource/v1/download';
|
|
4
|
-
export async function apiGetUploadInfo(account, fileName, fileId
|
|
5
|
-
const data = await yuanbaoPost(account, UPLOAD_INFO_PATH, { fileName, fileId, docFrom: 'localDoc', docOpenId: '' }
|
|
4
|
+
export async function apiGetUploadInfo(account, fileName, fileId) {
|
|
5
|
+
const data = await yuanbaoPost(account, UPLOAD_INFO_PATH, { fileName, fileId, docFrom: 'localDoc', docOpenId: '' });
|
|
6
6
|
if (!data.bucketName || !data.location) {
|
|
7
7
|
throw new Error(`[yuanbao-api] genUploadInfo 配置不完整: ${JSON.stringify(data)}`);
|
|
8
8
|
}
|
|
9
9
|
return data;
|
|
10
10
|
}
|
|
11
|
-
export async function apiGetDownloadUrl(account, resourceId
|
|
12
|
-
const data = await yuanbaoGet(account, DOWNLOAD_INFO_PATH, { resourceId }
|
|
11
|
+
export async function apiGetDownloadUrl(account, resourceId) {
|
|
12
|
+
const data = await yuanbaoGet(account, DOWNLOAD_INFO_PATH, { resourceId });
|
|
13
13
|
const downloadUrl = data.url ?? data.realUrl;
|
|
14
14
|
if (!downloadUrl) {
|
|
15
15
|
throw new Error(`[yuanbao-api] resource/v1/download 未返回有效 URL: ${JSON.stringify(data)}`);
|
|
@@ -44,8 +44,8 @@ export declare function getTokenStatus(accountId: string): {
|
|
|
44
44
|
};
|
|
45
45
|
export declare function getCachedBotId(accountId: string): string | undefined;
|
|
46
46
|
export declare function verifySignature(expected: string, actual: string): boolean;
|
|
47
|
-
export declare function getSignToken(account: ResolvedYuanbaoAccount
|
|
48
|
-
export declare function forceRefreshSignToken(account: ResolvedYuanbaoAccount
|
|
49
|
-
export declare function getAuthHeaders(account: ResolvedYuanbaoAccount
|
|
50
|
-
export declare function yuanbaoPost<T>(account: ResolvedYuanbaoAccount, path: string, body: unknown
|
|
51
|
-
export declare function yuanbaoGet<T>(account: ResolvedYuanbaoAccount, path: string, params?: Record<string, string
|
|
47
|
+
export declare function getSignToken(account: ResolvedYuanbaoAccount): Promise<SignTokenData>;
|
|
48
|
+
export declare function forceRefreshSignToken(account: ResolvedYuanbaoAccount): Promise<SignTokenData>;
|
|
49
|
+
export declare function getAuthHeaders(account: ResolvedYuanbaoAccount): Promise<AuthHeaders>;
|
|
50
|
+
export declare function yuanbaoPost<T>(account: ResolvedYuanbaoAccount, path: string, body: unknown): Promise<T>;
|
|
51
|
+
export declare function yuanbaoGet<T>(account: ResolvedYuanbaoAccount, path: string, params?: Record<string, string>): Promise<T>;
|
|
@@ -59,8 +59,8 @@ export function verifySignature(expected, actual) {
|
|
|
59
59
|
}
|
|
60
60
|
return timingSafeEqual(expectedBuf, actualBuf);
|
|
61
61
|
}
|
|
62
|
-
async function doFetchSignToken(account
|
|
63
|
-
const mlog = createLog('http'
|
|
62
|
+
async function doFetchSignToken(account) {
|
|
63
|
+
const mlog = createLog('http');
|
|
64
64
|
const { appKey, appSecret, apiDomain } = account;
|
|
65
65
|
if (!appKey || !appSecret)
|
|
66
66
|
throw new Error('签票失败: 缺少 appKey 或 appSecret');
|
|
@@ -106,8 +106,8 @@ async function doFetchSignToken(account, log) {
|
|
|
106
106
|
}
|
|
107
107
|
throw new Error('签票失败: 超过最大重试次数');
|
|
108
108
|
}
|
|
109
|
-
function scheduleTokenRefresh(account, durationSec
|
|
110
|
-
const mlog = createLog('http'
|
|
109
|
+
function scheduleTokenRefresh(account, durationSec) {
|
|
110
|
+
const mlog = createLog('http');
|
|
111
111
|
const existing = tokenRefreshTimers.get(account.accountId);
|
|
112
112
|
if (existing) {
|
|
113
113
|
clearTimeout(existing);
|
|
@@ -122,7 +122,7 @@ function scheduleTokenRefresh(account, durationSec, log) {
|
|
|
122
122
|
tokenRefreshTimers.delete(account.accountId);
|
|
123
123
|
try {
|
|
124
124
|
mlog.info(`[${account.accountId}][token-timer]定时刷新触发,开始重新签票`);
|
|
125
|
-
await forceRefreshSignToken(account
|
|
125
|
+
await forceRefreshSignToken(account);
|
|
126
126
|
mlog.info(`[${account.accountId}][token-timer]定时刷新完成`);
|
|
127
127
|
}
|
|
128
128
|
catch (err) {
|
|
@@ -130,7 +130,7 @@ function scheduleTokenRefresh(account, durationSec, log) {
|
|
|
130
130
|
const retryTimer = setTimeout(async () => {
|
|
131
131
|
tokenRefreshTimers.delete(account.accountId);
|
|
132
132
|
try {
|
|
133
|
-
await forceRefreshSignToken(account
|
|
133
|
+
await forceRefreshSignToken(account);
|
|
134
134
|
mlog.info(`[${account.accountId}][token-timer]定时刷新重试成功`);
|
|
135
135
|
}
|
|
136
136
|
catch (retryErr) {
|
|
@@ -142,7 +142,7 @@ function scheduleTokenRefresh(account, durationSec, log) {
|
|
|
142
142
|
}, refreshAfterMs);
|
|
143
143
|
tokenRefreshTimers.set(account.accountId, timer);
|
|
144
144
|
}
|
|
145
|
-
export async function getSignToken(account
|
|
145
|
+
export async function getSignToken(account) {
|
|
146
146
|
if (account.token) {
|
|
147
147
|
return {
|
|
148
148
|
bot_id: account.botId || '',
|
|
@@ -152,7 +152,7 @@ export async function getSignToken(account, log) {
|
|
|
152
152
|
token: account.token,
|
|
153
153
|
};
|
|
154
154
|
}
|
|
155
|
-
const tlog = createLog('http'
|
|
155
|
+
const tlog = createLog('http');
|
|
156
156
|
const cached = tokenCacheMap.get(account.accountId);
|
|
157
157
|
if (cached && cached.expiresAt > Date.now()) {
|
|
158
158
|
const remainSec = Math.round((cached.expiresAt - Date.now()) / 1000);
|
|
@@ -166,11 +166,11 @@ export async function getSignToken(account, log) {
|
|
|
166
166
|
}
|
|
167
167
|
fetchPromise = (async () => {
|
|
168
168
|
try {
|
|
169
|
-
const data = await doFetchSignToken(account
|
|
169
|
+
const data = await doFetchSignToken(account);
|
|
170
170
|
const ttlMs = data.duration > 0 ? data.duration * 1000 : 0;
|
|
171
171
|
if (ttlMs > 0) {
|
|
172
172
|
tokenCacheMap.set(account.accountId, { data, expiresAt: Date.now() + ttlMs });
|
|
173
|
-
scheduleTokenRefresh(account, data.duration
|
|
173
|
+
scheduleTokenRefresh(account, data.duration);
|
|
174
174
|
}
|
|
175
175
|
return data;
|
|
176
176
|
}
|
|
@@ -181,15 +181,15 @@ export async function getSignToken(account, log) {
|
|
|
181
181
|
tokenFetchPromises.set(account.accountId, fetchPromise);
|
|
182
182
|
return fetchPromise;
|
|
183
183
|
}
|
|
184
|
-
export async function forceRefreshSignToken(account
|
|
185
|
-
const flog = createLog('http'
|
|
184
|
+
export async function forceRefreshSignToken(account) {
|
|
185
|
+
const flog = createLog('http');
|
|
186
186
|
flog.warn(`[${account.accountId}][force-refresh]清除缓存并重新签票`);
|
|
187
187
|
clearSignTokenCache(account.accountId);
|
|
188
188
|
tokenFetchPromises.delete(account.accountId);
|
|
189
|
-
return getSignToken(account
|
|
189
|
+
return getSignToken(account);
|
|
190
190
|
}
|
|
191
|
-
export async function getAuthHeaders(account
|
|
192
|
-
const data = await getSignToken(account
|
|
191
|
+
export async function getAuthHeaders(account) {
|
|
192
|
+
const data = await getSignToken(account);
|
|
193
193
|
if (data.bot_id && !account.botId) {
|
|
194
194
|
account.botId = data.bot_id;
|
|
195
195
|
}
|
|
@@ -207,11 +207,11 @@ export async function getAuthHeaders(account, log) {
|
|
|
207
207
|
}
|
|
208
208
|
return authHeaders;
|
|
209
209
|
}
|
|
210
|
-
export async function yuanbaoPost(account, path, body
|
|
211
|
-
const plog = createLog('http'
|
|
210
|
+
export async function yuanbaoPost(account, path, body) {
|
|
211
|
+
const plog = createLog('http');
|
|
212
212
|
const url = `https://${account.apiDomain}${path}`;
|
|
213
213
|
for (let attempt = 0; attempt <= HTTP_AUTH_RETRY_MAX; attempt++) {
|
|
214
|
-
const authHeaders = await getAuthHeaders(account
|
|
214
|
+
const authHeaders = await getAuthHeaders(account);
|
|
215
215
|
const response = await fetch(url, {
|
|
216
216
|
method: 'POST',
|
|
217
217
|
headers: {
|
|
@@ -222,7 +222,7 @@ export async function yuanbaoPost(account, path, body, log) {
|
|
|
222
222
|
});
|
|
223
223
|
if (response.status === 401 && attempt < HTTP_AUTH_RETRY_MAX) {
|
|
224
224
|
plog.warn(`[post][${account.accountId}] ${path} 收到 401,刷新 token 后重试 (attempt=${attempt + 1})`);
|
|
225
|
-
await forceRefreshSignToken(account
|
|
225
|
+
await forceRefreshSignToken(account);
|
|
226
226
|
continue;
|
|
227
227
|
}
|
|
228
228
|
if (!response.ok) {
|
|
@@ -237,11 +237,11 @@ export async function yuanbaoPost(account, path, body, log) {
|
|
|
237
237
|
}
|
|
238
238
|
throw new Error(`[yuanbao-api][POST] ${path} 401 重试次数已耗尽`);
|
|
239
239
|
}
|
|
240
|
-
export async function yuanbaoGet(account, path, params
|
|
241
|
-
const glog = createLog('http'
|
|
240
|
+
export async function yuanbaoGet(account, path, params) {
|
|
241
|
+
const glog = createLog('http');
|
|
242
242
|
const url = `https://${account.apiDomain}${path}${params ? `?${new URLSearchParams(params).toString()}` : ''}`;
|
|
243
243
|
for (let attempt = 0; attempt <= HTTP_AUTH_RETRY_MAX; attempt++) {
|
|
244
|
-
const authHeaders = await getAuthHeaders(account
|
|
244
|
+
const authHeaders = await getAuthHeaders(account);
|
|
245
245
|
const response = await fetch(url, {
|
|
246
246
|
method: 'GET',
|
|
247
247
|
headers: {
|
|
@@ -251,7 +251,7 @@ export async function yuanbaoGet(account, path, params, log) {
|
|
|
251
251
|
});
|
|
252
252
|
if (response.status === 401 && attempt < HTTP_AUTH_RETRY_MAX) {
|
|
253
253
|
glog.warn(`[get][${account.accountId}] ${path} 收到 401,刷新 token 后重试 (attempt=${attempt + 1})`);
|
|
254
|
-
await forceRefreshSignToken(account
|
|
254
|
+
await forceRefreshSignToken(account);
|
|
255
255
|
continue;
|
|
256
256
|
}
|
|
257
257
|
if (!response.ok) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { WsClientCallbacks, WsClientConfig, WsClientState, WsConnectionConfig, WsSendMessageResponse, WsSendC2CMessageData, WsSendGroupMessageData, WsSendPrivateHeartbeatData, WsSendGroupHeartbeatData, WsHeartbeatResponse, WsQueryGroupInfoData, WsQueryGroupInfoResponse, WsGetGroupMemberListData, WsGetGroupMemberListResponse } from './types.js';
|
|
2
|
-
import type { LogSink } from '../../logger.js';
|
|
3
2
|
export declare const BIZ_CMD: {
|
|
4
3
|
readonly SendC2CMessage: "send_c2c_message";
|
|
5
4
|
readonly SendGroupMessage: "send_group_message";
|
|
@@ -30,7 +29,6 @@ export declare class YuanbaoWsClient {
|
|
|
30
29
|
connection: WsConnectionConfig;
|
|
31
30
|
config?: WsClientConfig;
|
|
32
31
|
callbacks?: WsClientCallbacks;
|
|
33
|
-
log?: LogSink;
|
|
34
32
|
});
|
|
35
33
|
updateAuth(auth: WsConnectionConfig['auth']): void;
|
|
36
34
|
connect(): void;
|
|
@@ -57,7 +57,7 @@ export class YuanbaoWsClient {
|
|
|
57
57
|
disposed = false;
|
|
58
58
|
pendingRequests = new Map();
|
|
59
59
|
constructor(params) {
|
|
60
|
-
this.log = createLog('ws'
|
|
60
|
+
this.log = createLog('ws');
|
|
61
61
|
this.log.info('初始化 WebSocket 客户端', { connection: params.connection, config: params.config });
|
|
62
62
|
this.connectionConfig = params.connection;
|
|
63
63
|
this.clientConfig = {
|
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
import type { OpenClawConfig, PluginRuntime } from 'openclaw/plugin-sdk';
|
|
2
2
|
import type { ResolvedYuanbaoAccount, YuanbaoInboundMessage } from '../../types.js';
|
|
3
3
|
import type { WsPushEvent } from './types.js';
|
|
4
|
-
type GatewayLog = {
|
|
5
|
-
info?: (msg: string) => void;
|
|
6
|
-
warn?: (msg: string) => void;
|
|
7
|
-
error?: (msg: string) => void;
|
|
8
|
-
debug?: (msg: string) => void;
|
|
9
|
-
};
|
|
10
4
|
type GatewayStatusPatch = Record<string, unknown>;
|
|
11
5
|
export type StartWsGatewayParams = {
|
|
12
6
|
account: ResolvedYuanbaoAccount;
|
|
13
7
|
config: OpenClawConfig;
|
|
14
8
|
abortSignal: AbortSignal;
|
|
15
|
-
log?: GatewayLog;
|
|
16
9
|
runtime?: PluginRuntime;
|
|
17
10
|
statusSink?: (patch: GatewayStatusPatch) => void;
|
|
18
11
|
};
|
|
@@ -21,5 +14,5 @@ type InboundResult = {
|
|
|
21
14
|
msg: YuanbaoInboundMessage;
|
|
22
15
|
chatType: 'c2c' | 'group';
|
|
23
16
|
};
|
|
24
|
-
export declare function wsPushToInboundMessage(pushEvent: WsPushEvent
|
|
17
|
+
export declare function wsPushToInboundMessage(pushEvent: WsPushEvent): InboundResult | null;
|
|
25
18
|
export {};
|
|
@@ -6,9 +6,9 @@ import { setActiveWsClient } from './runtime.js';
|
|
|
6
6
|
import { decodeInboundMessage } from './biz-codec.js';
|
|
7
7
|
import { getSignToken, forceRefreshSignToken } from '../api.js';
|
|
8
8
|
export async function startYuanbaoWsGateway(params) {
|
|
9
|
-
const { account, config, abortSignal,
|
|
10
|
-
const gwlog = createLog('ws'
|
|
11
|
-
const auth = await resolveWsAuth(account
|
|
9
|
+
const { account, config, abortSignal, runtime, statusSink } = params;
|
|
10
|
+
const gwlog = createLog('ws');
|
|
11
|
+
const auth = await resolveWsAuth(account);
|
|
12
12
|
const client = new YuanbaoWsClient({
|
|
13
13
|
connection: {
|
|
14
14
|
gatewayUrl: account.wsGatewayUrl,
|
|
@@ -29,7 +29,7 @@ export async function startYuanbaoWsGateway(params) {
|
|
|
29
29
|
},
|
|
30
30
|
onDispatch: (pushEvent) => {
|
|
31
31
|
gwlog.debug(`[${account.accountId}] WS 推送: cmd=${pushEvent.cmd}, type=${pushEvent.type}`);
|
|
32
|
-
handleWsDispatchEvent({ account, config, pushEvent,
|
|
32
|
+
handleWsDispatchEvent({ account, config, pushEvent, runtime, client, statusSink, abortSignal });
|
|
33
33
|
},
|
|
34
34
|
onStateChange: (state) => {
|
|
35
35
|
gwlog.info(`[${account.accountId}] WS 状态: ${state}`);
|
|
@@ -52,7 +52,7 @@ export async function startYuanbaoWsGateway(params) {
|
|
|
52
52
|
},
|
|
53
53
|
onAuthFailed: async (code) => {
|
|
54
54
|
gwlog.warn(`[${account.accountId}] 收到 onAuthFailed 回调(code=${code}),开始刷新 token`);
|
|
55
|
-
const tokenData = await forceRefreshSignToken(account
|
|
55
|
+
const tokenData = await forceRefreshSignToken(account);
|
|
56
56
|
const uid = tokenData.bot_id || account.botId || '';
|
|
57
57
|
if (tokenData.bot_id) {
|
|
58
58
|
account.botId = tokenData.bot_id;
|
|
@@ -66,12 +66,6 @@ export async function startYuanbaoWsGateway(params) {
|
|
|
66
66
|
};
|
|
67
67
|
},
|
|
68
68
|
},
|
|
69
|
-
log: {
|
|
70
|
-
info: msg => log?.info?.(msg),
|
|
71
|
-
warn: msg => log?.warn?.(msg),
|
|
72
|
-
error: msg => log?.error?.(msg),
|
|
73
|
-
debug: msg => log?.debug?.(msg),
|
|
74
|
-
},
|
|
75
69
|
});
|
|
76
70
|
client.connect();
|
|
77
71
|
setActiveWsClient(account.accountId, client);
|
|
@@ -94,8 +88,8 @@ export async function startYuanbaoWsGateway(params) {
|
|
|
94
88
|
abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
95
89
|
});
|
|
96
90
|
}
|
|
97
|
-
async function resolveWsAuth(account
|
|
98
|
-
const mlog = createLog('ws'
|
|
91
|
+
async function resolveWsAuth(account) {
|
|
92
|
+
const mlog = createLog('ws');
|
|
99
93
|
mlog.info(`[${account.accountId}] resolveWsAuth 入参:`, {
|
|
100
94
|
botId: account.botId,
|
|
101
95
|
token: account.token,
|
|
@@ -115,7 +109,7 @@ async function resolveWsAuth(account, log) {
|
|
|
115
109
|
routeEnv: account.config?.routeEnv,
|
|
116
110
|
};
|
|
117
111
|
}
|
|
118
|
-
const tokenData = await getSignToken(account
|
|
112
|
+
const tokenData = await getSignToken(account);
|
|
119
113
|
const uid = tokenData.bot_id || account.botId || '';
|
|
120
114
|
if (tokenData.bot_id) {
|
|
121
115
|
account.botId = tokenData.bot_id;
|
|
@@ -157,14 +151,14 @@ function inferChatType(msg) {
|
|
|
157
151
|
function hasValidMsgFields(msg) {
|
|
158
152
|
return Boolean(msg.callback_command || msg.from_account || msg.msg_body);
|
|
159
153
|
}
|
|
160
|
-
function decodeFromProtobuf(rawData
|
|
154
|
+
function decodeFromProtobuf(rawData) {
|
|
161
155
|
const decoded = decodeInboundMessage(rawData);
|
|
162
156
|
if (!decoded || !hasValidMsgFields(decoded))
|
|
163
157
|
return null;
|
|
164
|
-
createLog('ws').debug(
|
|
158
|
+
createLog('ws').debug('[Protobuf] WS 推送事件解析', { ...decoded });
|
|
165
159
|
return { msg: decoded, chatType: inferChatType(decoded) };
|
|
166
160
|
}
|
|
167
|
-
function decodeFromRawDataJson(rawData
|
|
161
|
+
function decodeFromRawDataJson(rawData) {
|
|
168
162
|
try {
|
|
169
163
|
const rawJson = JSON.parse(new TextDecoder().decode(rawData));
|
|
170
164
|
if (!rawJson || !hasValidMsgFields(rawJson))
|
|
@@ -173,7 +167,7 @@ function decodeFromRawDataJson(rawData, pushType) {
|
|
|
173
167
|
if (!msg.trace_id) {
|
|
174
168
|
msg.trace_id = rawJson.log_ext?.trace_id;
|
|
175
169
|
}
|
|
176
|
-
createLog('ws').info(
|
|
170
|
+
createLog('ws').info('[Json] WS 推送事件解析', { ...msg });
|
|
177
171
|
return { msg, chatType: inferChatType(msg) };
|
|
178
172
|
}
|
|
179
173
|
catch {
|
|
@@ -206,34 +200,32 @@ function decodeFromContent(pushEvent) {
|
|
|
206
200
|
chatType,
|
|
207
201
|
};
|
|
208
202
|
}
|
|
209
|
-
export function wsPushToInboundMessage(pushEvent
|
|
203
|
+
export function wsPushToInboundMessage(pushEvent) {
|
|
210
204
|
if (pushEvent.connData && pushEvent.connData.length > 0) {
|
|
211
|
-
createLog('ws'
|
|
212
|
-
const
|
|
213
|
-
const result = decodeFromProtobuf(pushEvent.connData, pushType);
|
|
205
|
+
createLog('ws').debug('[connData] WS 推送事件解析');
|
|
206
|
+
const result = decodeFromProtobuf(pushEvent.connData);
|
|
214
207
|
if (result)
|
|
215
208
|
return result;
|
|
216
209
|
}
|
|
217
210
|
if (pushEvent.rawData && pushEvent.rawData.length > 0) {
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
createLog('ws', log).warn(`[${pushType}] WS 推送事件解析失败`);
|
|
211
|
+
const jsonResult = decodeFromRawDataJson(pushEvent.rawData);
|
|
212
|
+
if (jsonResult) {
|
|
213
|
+
createLog('ws').debug('[rawData] WS 推送事件解析', jsonResult);
|
|
214
|
+
return jsonResult;
|
|
215
|
+
}
|
|
216
|
+
createLog('ws').warn('[rawData] WS 推送事件解析失败');
|
|
225
217
|
}
|
|
226
218
|
if (pushEvent.content) {
|
|
227
|
-
createLog('ws'
|
|
219
|
+
createLog('ws').debug('[content] WS 推送事件解析, type=content', { content: pushEvent.content });
|
|
228
220
|
return decodeFromContent(pushEvent);
|
|
229
221
|
}
|
|
230
222
|
return null;
|
|
231
223
|
}
|
|
232
224
|
function handleWsDispatchEvent(params) {
|
|
233
|
-
const { account, config, pushEvent,
|
|
234
|
-
const dlog = createLog('ws'
|
|
225
|
+
const { account, config, pushEvent, runtime, client, statusSink, abortSignal } = params;
|
|
226
|
+
const dlog = createLog('ws');
|
|
235
227
|
dlog.debug(`[${account.accountId}][dispatch] cmd=${pushEvent.cmd}, module=${pushEvent.module}, msgId=${pushEvent.msgId}`);
|
|
236
|
-
const converted = wsPushToInboundMessage(pushEvent
|
|
228
|
+
const converted = wsPushToInboundMessage(pushEvent);
|
|
237
229
|
if (!converted) {
|
|
238
230
|
dlog.debug(`[${account.accountId}][dispatch] cmd=${pushEvent.cmd} (非消息事件,跳过)`);
|
|
239
231
|
return;
|
|
@@ -258,12 +250,6 @@ function handleWsDispatchEvent(params) {
|
|
|
258
250
|
account,
|
|
259
251
|
config,
|
|
260
252
|
core: runtime,
|
|
261
|
-
log: {
|
|
262
|
-
info: (m) => gwLog?.info?.(m),
|
|
263
|
-
warn: (m) => gwLog?.warn?.(m),
|
|
264
|
-
error: (m) => gwLog?.error?.(m),
|
|
265
|
-
verbose: (m) => gwLog?.debug?.(m),
|
|
266
|
-
},
|
|
267
253
|
statusSink: statusSink,
|
|
268
254
|
wsClient: client,
|
|
269
255
|
traceContext,
|
package/openclaw.plugin.json
CHANGED