openclaw-plugin-yuanbao 2.7.1 → 2.7.2

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 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);
@@ -2,7 +2,7 @@
2
2
  "id": "openclaw-plugin-yuanbao",
3
3
  "name": "元宝 Bot",
4
4
  "description": "Tencent YuanBao intelligent bot channel plugin",
5
- "version": "2.7.1",
5
+ "version": "2.7.2",
6
6
  "channels": [
7
7
  "yuanbao"
8
8
  ],
@@ -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)) {
@@ -3,7 +3,7 @@ import { formatPairingApproveHint } from 'openclaw/plugin-sdk/mattermost';
3
3
  import { listYuanbaoAccountIds, resolveDefaultYuanbaoAccountId, resolveYuanbaoAccount } from './accounts.js';
4
4
  import { yuanbaoConfigSchema } from './config-schema.js';
5
5
  import { yuanbaoOnboardingAdapter } from './onboarding.js';
6
- import { createLog } from './logger.js';
6
+ import { createLog, setDebugBotIds } from './logger.js';
7
7
  import { yuanbaoSetupAdapter } from './setup.js';
8
8
  import { startYuanbaoWsGateway, getActiveWsClient } from './yuanbao-server/ws/index.js';
9
9
  import { getYuanbaoRuntime } from './runtime.js';
@@ -167,7 +167,8 @@ export const yuanbaoPlugin = {
167
167
  blockStreamingChunkMaxChars: 3000,
168
168
  blockStreamingCoalesceDefaults: {
169
169
  minChars: 2800,
170
- idleMs: 5000,
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', undefined, { botId: account.botId });
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', undefined, { botId: account.botId });
209
210
  const wsClient = getActiveWsClient(account.accountId);
210
211
  slog.info('sendMedia', { accountId, to, mediaUrl, text });
211
212
  if (!wsClient) {
@@ -269,7 +270,11 @@ export const yuanbaoPlugin = {
269
270
  gateway: {
270
271
  startAccount: async (ctx) => {
271
272
  const { account } = ctx;
272
- const slog = createLog('gateway', ctx.log);
273
+ const yuanbaoTopConfig = ctx.cfg.channels?.yuanbao;
274
+ if (yuanbaoTopConfig?.debugBotIds?.length) {
275
+ setDebugBotIds(yuanbaoTopConfig.debugBotIds);
276
+ }
277
+ const slog = createLog('gateway', ctx.log, { botId: account.botId });
273
278
  slog.debug('启动账号', account);
274
279
  if (!account.configured) {
275
280
  slog.warn('yuanbao not configured; skipping');
@@ -98,6 +98,13 @@ export const yuanbaoConfigSchema = {
98
98
  description: '开启后在系统提示词中自动注入指令,防止模型用代码块包裹整个 Markdown 回复',
99
99
  default: true,
100
100
  },
101
+ debugBotIds: {
102
+ type: 'array',
103
+ items: { type: 'string' },
104
+ title: '调试白名单 Bot ID',
105
+ description: '白名单内的 Bot ID 日志输出不做脱敏处理,方便开发调试。填写 bot 的 IM 用户 ID',
106
+ default: [],
107
+ },
101
108
  },
102
109
  additionalProperties: false,
103
110
  },
@@ -9,6 +9,8 @@ export interface PluginLogger {
9
9
  export declare function initLogger(api: OpenClawPluginApi): void;
10
10
  export declare const logger: PluginLogger;
11
11
  export declare function isVerbose(): boolean;
12
+ export declare function setDebugBotIds(ids: string[]): void;
13
+ export declare function isDebugBotId(botId?: string): boolean;
12
14
  export interface LogSink {
13
15
  info?: (msg: string) => void;
14
16
  warn?: (msg: string) => void;
@@ -22,8 +24,10 @@ export interface ModuleLog {
22
24
  error(msg: string, data?: Record<string, unknown>): void;
23
25
  debug(msg: string, data?: Record<string, unknown>): void;
24
26
  }
25
- export declare function formatLog(module: string, msg: string, data?: Record<string, unknown>): string;
26
- export declare function createLog(module: string, sink?: LogSink): ModuleLog;
27
+ export declare function formatLog(module: string, msg: string, data?: Record<string, unknown>, skipSanitize?: boolean): string;
28
+ export declare function createLog(module: string, sink?: LogSink, options?: {
29
+ botId?: string;
30
+ }): ModuleLog;
27
31
  export declare function sanitize(value: unknown): string;
28
32
  export declare function logSimple(level: 'info' | 'warn' | 'error', message: string): void;
29
33
  export declare function logDebug(message: string): void;
@@ -55,14 +55,41 @@ export const logger = {
55
55
  export function isVerbose() {
56
56
  return verboseEnabled;
57
57
  }
58
- export function formatLog(module, msg, data) {
58
+ function parseEnvDebugBotIds() {
59
+ const raw = process.env.YUANBAO_DEBUG_BOT_IDS;
60
+ if (!raw)
61
+ return [];
62
+ return raw.split(',').map(s => s.trim())
63
+ .filter(Boolean);
64
+ }
65
+ const debugBotIds = new Set(parseEnvDebugBotIds());
66
+ export function setDebugBotIds(ids) {
67
+ debugBotIds.clear();
68
+ for (const id of parseEnvDebugBotIds())
69
+ debugBotIds.add(id);
70
+ for (const id of ids) {
71
+ const trimmed = id.trim();
72
+ if (trimmed)
73
+ debugBotIds.add(trimmed);
74
+ }
75
+ }
76
+ export function isDebugBotId(botId) {
77
+ if (!botId)
78
+ return false;
79
+ return debugBotIds.has(botId);
80
+ }
81
+ export function formatLog(module, msg, data, skipSanitize) {
59
82
  const prefix = module ? `${LOG_PREFIX}[${module}]` : LOG_PREFIX;
60
- return data !== undefined ? `${prefix} ${msg} ${sanitize(data)}` : `${prefix} ${msg}`;
83
+ if (data === undefined)
84
+ return `${prefix} ${msg}`;
85
+ const serialized = skipSanitize ? JSON.stringify(data) : sanitize(data);
86
+ return `${prefix} ${msg} ${serialized}`;
61
87
  }
62
- export function createLog(module, sink) {
88
+ export function createLog(module, sink, options) {
63
89
  const target = sink ?? logger;
90
+ const skipSanitize = isDebugBotId(options?.botId);
64
91
  function fmt(msg, data) {
65
- return formatLog(module, msg, data);
92
+ return formatLog(module, msg, data, skipSanitize);
66
93
  }
67
94
  return {
68
95
  info: (msg, data) => target.info?.(fmt(msg, data)),
@@ -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', ctx.log);
125
+ const log = createLog('inbound', ctx.log, { botId: account.botId });
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', ctx.log);
339
+ const glog = createLog('inbound', ctx.log, { botId: account.botId });
340
340
  setGroupCode(groupCode);
341
341
  if (outboundSender && fromAccount === outboundSender) {
342
342
  glog.info('跳过机器人自身消息', { groupCode, fromAccount });
@@ -33,8 +33,8 @@ async function shouldAttachReplyRef(params) {
33
33
  return true;
34
34
  }
35
35
  export async function sendYuanbaoMessageBody(params) {
36
- const { toAccount, msgBody, fromAccount, ctx } = params;
37
- const log = createLog('outbound', ctx?.log);
36
+ const { account, toAccount, msgBody, fromAccount, ctx } = params;
37
+ const log = createLog('outbound', ctx?.log, { botId: account.botId });
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', ctx?.log);
80
+ const log = createLog('outbound', ctx?.log, { botId: account.botId });
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', ctx.log);
156
+ const rlog = createLog('outbound', ctx.log, { botId: account.botId });
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 {
@@ -213,13 +201,10 @@ export async function executeReply(params) {
213
201
  if (info.kind === 'final') {
214
202
  hasFinalInfo = true;
215
203
  }
216
- const prevKind = prevDeliverKind;
217
- prevDeliverKind = info.kind;
218
204
  const text = payload.text ?? '';
219
205
  if (session) {
220
206
  if (text.trim()) {
221
- const isAfterToolCall = info.kind === 'block' && prevKind !== null && prevKind !== 'block';
222
- await session.push({ type: 'text', text: isAfterToolCall ? `\n\n${text}` : text });
207
+ await session.push({ type: 'text', text });
223
208
  }
224
209
  const mediaUrls = payload.mediaUrls ?? [];
225
210
  for (const mediaUrl of mediaUrls) {
@@ -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('\n\n'));
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 && tableHasSep && tableEnd !== -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 lines = textBuffer.trimEnd().split('\n');
586
- const lastLineOfBuffer = (lines[lines.length - 1] ?? '').trim();
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;
@@ -32,6 +32,7 @@ export type YuanbaoConfig = YuanbaoAccountConfig & {
32
32
  accounts?: Record<string, YuanbaoAccountConfig>;
33
33
  defaultAccount?: string;
34
34
  routeEnv?: string;
35
+ debugBotIds?: string[];
35
36
  };
36
37
  export type ResolvedYuanbaoAccount = {
37
38
  accountId: string;
@@ -7,7 +7,7 @@ import { decodeInboundMessage } from './biz-codec.js';
7
7
  import { getSignToken, forceRefreshSignToken } from '../api.js';
8
8
  export async function startYuanbaoWsGateway(params) {
9
9
  const { account, config, abortSignal, log, runtime, statusSink } = params;
10
- const gwlog = createLog('ws', log);
10
+ const gwlog = createLog('ws', log, { botId: account.botId });
11
11
  const auth = await resolveWsAuth(account, log);
12
12
  const client = new YuanbaoWsClient({
13
13
  connection: {
@@ -95,7 +95,7 @@ export async function startYuanbaoWsGateway(params) {
95
95
  });
96
96
  }
97
97
  async function resolveWsAuth(account, log) {
98
- const mlog = createLog('ws', log);
98
+ const mlog = createLog('ws', log, { botId: account.botId });
99
99
  mlog.info(`[${account.accountId}] resolveWsAuth 入参:`, {
100
100
  botId: account.botId,
101
101
  token: account.token,
@@ -231,7 +231,7 @@ export function wsPushToInboundMessage(pushEvent, log) {
231
231
  }
232
232
  function handleWsDispatchEvent(params) {
233
233
  const { account, config, pushEvent, log: gwLog, runtime, client, statusSink, abortSignal } = params;
234
- const dlog = createLog('ws', gwLog);
234
+ const dlog = createLog('ws', gwLog, { botId: account.botId });
235
235
  dlog.debug(`[${account.accountId}][dispatch] cmd=${pushEvent.cmd}, module=${pushEvent.module}, msgId=${pushEvent.msgId}`);
236
236
  const converted = wsPushToInboundMessage(pushEvent, gwLog);
237
237
  if (!converted) {
@@ -2,7 +2,7 @@
2
2
  "id": "openclaw-plugin-yuanbao",
3
3
  "name": "元宝 Bot",
4
4
  "description": "Tencent YuanBao intelligent bot channel plugin",
5
- "version": "2.7.1",
5
+ "version": "2.7.2",
6
6
  "channels": [
7
7
  "yuanbao"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-plugin-yuanbao",
3
- "version": "2.7.1",
3
+ "version": "2.7.2",
4
4
  "type": "module",
5
5
  "description": "Tencent YuanBao intelligent bot channel plugin",
6
6
  "license": "MIT",