@izhimu/qq 0.2.0 → 0.2.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.
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { Logger as log, extractImageUrl, getEmojiForFaceId } from '../utils/index.js';
7
7
  import { CQCodeUtils } from '../utils/cqcode.js';
8
+ import { getMsg } from "../core/request.js";
8
9
  // =============================================================================
9
10
  // CQ Code Parsing
10
11
  // =============================================================================
@@ -64,7 +65,7 @@ function parseJsonSegment(segment) {
64
65
  // =============================================================================
65
66
  // NapCat -> OpenClaw Adapters (Inbound)
66
67
  // =============================================================================
67
- function napCatToOpenClaw(segment) {
68
+ async function napCatToOpenClaw(segment) {
68
69
  const data = segment.data;
69
70
  switch (segment.type) {
70
71
  case 'text':
@@ -80,7 +81,19 @@ function napCatToOpenClaw(segment) {
80
81
  return url ? { type: 'image', url, summary: data.summary } : null;
81
82
  }
82
83
  case 'reply':
83
- return { type: 'reply', messageId: String(data.id || '') };
84
+ const response = await getMsg({
85
+ message_id: Number(data.id),
86
+ });
87
+ if (response.data?.message == undefined) {
88
+ return null;
89
+ }
90
+ return {
91
+ type: 'reply',
92
+ messageId: String(data.id),
93
+ message: response.data.raw_message,
94
+ senderId: String(response.data.sender.user_id),
95
+ sender: response.data.sender.nickname
96
+ };
84
97
  case 'face':
85
98
  return { type: 'text', text: getEmojiForFaceId(String(data.id || '')) };
86
99
  case 'record':
@@ -143,7 +156,7 @@ export async function napCatToOpenClawMessage(segments) {
143
156
  const normalized = normalizeMessage(segments);
144
157
  const content = [];
145
158
  for (const segment of normalized) {
146
- const result = napCatToOpenClaw(segment);
159
+ const result = await napCatToOpenClaw(segment);
147
160
  if (result) {
148
161
  content.push(result);
149
162
  }
@@ -2,6 +2,6 @@
2
2
  * QQ NapCat Plugin for OpenClaw
3
3
  * Main plugin entry point
4
4
  */
5
- import type { ChannelPlugin } from "openclaw/plugin-sdk";
5
+ import { ChannelPlugin } from "openclaw/plugin-sdk";
6
6
  import type { QQConfig } from "./types/index.js";
7
7
  export declare const qqPlugin: ChannelPlugin<QQConfig>;
@@ -2,6 +2,7 @@
2
2
  * QQ NapCat Plugin for OpenClaw
3
3
  * Main plugin entry point
4
4
  */
5
+ import { setAccountEnabledInConfigSection, deleteAccountFromConfigSection } from "openclaw/plugin-sdk";
5
6
  import { buildChannelConfigSchema, DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk";
6
7
  import { messageIdToString, getFileType, getFileName, Logger as log } from "./utils/index.js";
7
8
  import { setContext, setContextStatus, clearContext, setConnection, getConnection, clearConnection } from "./core/runtime.js";
@@ -18,6 +19,7 @@ export const qqPlugin = {
18
19
  selectionLabel: "QQ",
19
20
  docsPath: "/channels/qq",
20
21
  blurb: "通过 NapCat WebSocket 连接 QQ 机器人",
22
+ quickstartAllowFrom: true,
21
23
  },
22
24
  capabilities: {
23
25
  chatTypes: ["direct", "group"],
@@ -33,6 +35,18 @@ export const qqPlugin = {
33
35
  resolveAccount: (cfg) => resolveQQAccount({ cfg }),
34
36
  isEnabled: (account) => Boolean(account?.enabled),
35
37
  isConfigured: (account) => Boolean(account?.wsUrl),
38
+ setAccountEnabled: ({ cfg, accountId, enabled }) => setAccountEnabledInConfigSection({
39
+ cfg,
40
+ sectionKey: "qq",
41
+ accountId,
42
+ enabled,
43
+ allowTopLevel: true,
44
+ }),
45
+ deleteAccount: ({ cfg, accountId }) => deleteAccountFromConfigSection({
46
+ cfg,
47
+ sectionKey: "qq",
48
+ accountId,
49
+ }),
36
50
  },
37
51
  configSchema: buildChannelConfigSchema(QQConfigSchema),
38
52
  messaging: {
@@ -3,7 +3,7 @@
3
3
  * Handles routing and dispatching incoming messages to the AI
4
4
  */
5
5
  import { getRuntime, getContext } from './runtime.js';
6
- import { getMsg, getFile, sendMsg, setInputStatus } from './request.js';
6
+ import { getFile, sendMsg, setInputStatus } from './request.js';
7
7
  import { napCatToOpenClawMessage } from '../adapters/message.js';
8
8
  import { Logger as log, markdownToText } from '../utils/index.js';
9
9
  import { CHANNEL_ID } from "./config.js";
@@ -14,19 +14,23 @@ import { CHANNEL_ID } from "./config.js";
14
14
  */
15
15
  async function contentToPlainText(content) {
16
16
  return content
17
- .filter(c => c.type !== 'reply' && c.type !== 'image' && c.type !== 'audio' && c.type !== 'file')
17
+ .filter(c => c.type !== 'image' && c.type !== 'audio' && c.type !== 'file')
18
18
  .map((c) => {
19
19
  switch (c.type) {
20
20
  case 'text':
21
- return `[消息]\n${c.text}`;
21
+ return `${c.text}`;
22
22
  case 'at':
23
23
  return c.isAll ? '@全体成员' : `@${c.userId}`;
24
24
  case 'json':
25
25
  return `[JSON]\n\`\`\`json\n${c.data}\n\`\`\``;
26
+ case 'reply':
27
+ let replyContent = `${c.sender}(${c.senderId}):\n${c.message}`;
28
+ replyContent = replyContent.split('\n').map(line => `> ${line}`).join('\n');
29
+ return `[回复]\n${replyContent}\n`;
26
30
  default:
27
31
  return '';
28
32
  }
29
- }).join('');
33
+ }).join('\n');
30
34
  }
31
35
  async function contextToMedia(content) {
32
36
  const hasMedia = content.some(c => c.type === 'image' || c.type === 'audio' || c.type === 'file');
@@ -63,30 +67,6 @@ async function contextToMedia(content) {
63
67
  }
64
68
  return;
65
69
  }
66
- // TODO弃用
67
- async function contextToReply(content) {
68
- const hasReply = content.some(c => c.type === 'reply');
69
- if (!hasReply) {
70
- return;
71
- }
72
- const reply = content.find(c => c.type === 'reply');
73
- if (!reply) {
74
- return;
75
- }
76
- const response = await getMsg({
77
- message_id: Number(reply.messageId),
78
- });
79
- if (response.data?.message == undefined) {
80
- return;
81
- }
82
- const replyMessage = await napCatToOpenClawMessage(response.data?.message);
83
- const text = await contentToPlainText(replyMessage);
84
- return {
85
- id: reply.messageId,
86
- content: text,
87
- sender: String(response.data?.sender.user_id)
88
- };
89
- }
90
70
  async function sendText(isGroup, chatId, text) {
91
71
  const cleanText = text.replace(/NO_REPLY\s*$/, '');
92
72
  const messageSegments = [{ type: 'text', data: { text: markdownToText(cleanText) } }];
@@ -107,7 +87,7 @@ async function sendText(isGroup, chatId, text) {
107
87
  * Dispatch an incoming message to the AI for processing
108
88
  */
109
89
  export async function dispatchMessage(params) {
110
- const { chatType, chatId, senderId, senderName, messageId, content, media, reply, timestamp } = params;
90
+ const { chatType, chatId, senderId, senderName, messageId, content, media, timestamp } = params;
111
91
  const runtime = getRuntime();
112
92
  if (!runtime) {
113
93
  log.warn('dispatch', `Plugin runtime not available`);
@@ -120,20 +100,20 @@ export async function dispatchMessage(params) {
120
100
  }
121
101
  const isGroup = chatType === 'group';
122
102
  const peerId = isGroup ? `group:${chatId}` : senderId;
123
- const fullContent = `${content}\n\nFrom QQ(${senderId}) - Nickname: ${senderName}`;
124
103
  const route = runtime.channel.routing.resolveAgentRoute({
125
104
  cfg: context.cfg,
126
105
  channel: CHANNEL_ID,
127
106
  peer: {
128
- kind: isGroup ? 'group' : 'dm',
107
+ kind: 'group',
129
108
  id: peerId,
130
109
  },
131
110
  });
111
+ log.debug('dispatch', `Resolved route: ${JSON.stringify(route)}`);
132
112
  const envelopeOptions = runtime.channel.reply.resolveEnvelopeFormatOptions(context.cfg);
133
113
  const body = runtime.channel.reply.formatInboundEnvelope({
134
114
  channel: CHANNEL_ID,
135
115
  from: senderName || senderId,
136
- body: fullContent,
116
+ body: content,
137
117
  timestamp,
138
118
  chatType: isGroup ? 'group' : 'direct',
139
119
  sender: {
@@ -142,12 +122,13 @@ export async function dispatchMessage(params) {
142
122
  },
143
123
  envelope: envelopeOptions,
144
124
  });
145
- const fromAddress = isGroup ? `qq:group:${chatId}` : `qq:private:${senderId}`;
146
- const toAddress = `qq:${route.accountId}`;
125
+ log.debug('dispatch', `Inbound envelope: ${body}`);
126
+ const fromAddress = isGroup ? `qq:group:${chatId}` : `qq:${senderId}`;
127
+ const toAddress = `qq:${chatId}`;
147
128
  const ctxPayload = runtime.channel.reply.finalizeInboundContext({
148
129
  Body: body,
149
- RawBody: fullContent,
150
- CommandBody: fullContent,
130
+ RawBody: content,
131
+ CommandBody: content,
151
132
  From: fromAddress,
152
133
  To: toAddress,
153
134
  SessionKey: route.sessionKey,
@@ -159,10 +140,6 @@ export async function dispatchMessage(params) {
159
140
  Surface: CHANNEL_ID,
160
141
  MessageSid: messageId,
161
142
  Timestamp: timestamp,
162
- ReplyToId: reply?.id,
163
- ReplyToBody: reply?.content,
164
- ReplyToSender: reply?.sender,
165
- ReplyToIsQuote: !!reply,
166
143
  MediaType: media?.type,
167
144
  MediaPath: media?.path,
168
145
  MediaUrl: media?.url,
@@ -224,8 +201,7 @@ export async function handleGroupMessage(event) {
224
201
  const content = await napCatToOpenClawMessage(event.message);
225
202
  const plainText = await contentToPlainText(content);
226
203
  const media = await contextToMedia(content);
227
- const reply = await contextToReply(content);
228
- log.info('dispatch', `Group message from ${event.sender?.nickname || event.sender?.card || event.user_id}: ${plainText}, media: ${media != undefined}, reply: ${reply != undefined}`);
204
+ log.info('dispatch', `Group message from ${event.sender?.nickname || event.sender?.card || event.user_id}: ${plainText}, media: ${media != undefined}`);
229
205
  await dispatchMessage({
230
206
  chatType: 'group',
231
207
  chatId: String(event.group_id),
@@ -234,7 +210,6 @@ export async function handleGroupMessage(event) {
234
210
  messageId: String(event.message_id),
235
211
  content: plainText,
236
212
  media,
237
- reply,
238
213
  timestamp: event.time * 1000,
239
214
  });
240
215
  }
@@ -245,8 +220,7 @@ export async function handlePrivateMessage(event) {
245
220
  const content = await napCatToOpenClawMessage(event.message);
246
221
  const plainText = await contentToPlainText(content);
247
222
  const media = await contextToMedia(content);
248
- const reply = await contextToReply(content);
249
- log.info('dispatch', `Private message from ${event.sender?.nickname || event.user_id}: ${plainText}, media: ${media != undefined}, reply: ${reply != undefined}`);
223
+ log.info('dispatch', `Private message from ${event.sender?.nickname || event.user_id}: ${plainText}, media: ${media != undefined}`);
250
224
  await dispatchMessage({
251
225
  chatType: 'direct',
252
226
  chatId: String(event.user_id),
@@ -255,7 +229,6 @@ export async function handlePrivateMessage(event) {
255
229
  messageId: String(event.message_id),
256
230
  content: plainText,
257
231
  media,
258
- reply,
259
232
  timestamp: event.time * 1000,
260
233
  });
261
234
  }
@@ -120,6 +120,9 @@ export interface OpenClawImageContent {
120
120
  export interface OpenClawReplyContent {
121
121
  type: 'reply';
122
122
  messageId: string;
123
+ message?: string;
124
+ senderId?: string;
125
+ sender?: string;
123
126
  }
124
127
  export interface OpenClawAudioContent {
125
128
  type: 'audio';
@@ -245,11 +248,6 @@ export interface DispatchMessageMedia {
245
248
  path?: string;
246
249
  url?: string;
247
250
  }
248
- export interface DispatchMessageReply {
249
- id?: string;
250
- content?: string;
251
- sender?: string;
252
- }
253
251
  export interface DispatchMessageParams {
254
252
  chatType: 'direct' | 'group';
255
253
  chatId: string;
@@ -258,6 +256,5 @@ export interface DispatchMessageParams {
258
256
  messageId: string;
259
257
  content: string;
260
258
  media?: DispatchMessageMedia;
261
- reply?: DispatchMessageReply;
262
259
  timestamp: number;
263
260
  }
@@ -4,21 +4,21 @@ export declare class MarkdownToText {
4
4
  private maskCounter;
5
5
  /**
6
6
  * 主入口:将 Markdown 转换为纯文本
7
- * @param markdown 原始 Markdown 字符串
8
7
  */
9
8
  convert(markdown: string): string;
10
9
  /**
11
- * 保护代码块
12
- * 支持 ```language 和 ~~~ 两种写法
10
+ * 保护代码块 (```)
11
+ * 生成 Key 格式:%%MD-MASK-BLOCK-0
13
12
  */
14
13
  private maskCodeBlocks;
15
14
  /**
16
- * 保护行内代码
17
- * `code` -> 使用单引号包裹,区别于普通文本
15
+ * 保护行内代码 (`)
16
+ * 生成 Key 格式:%%MD-MASK-INLINE-0
18
17
  */
19
18
  private maskInlineCode;
20
19
  /**
21
- * 还原被掩码的内容
20
+ * 还原掩码内容
21
+ * 必须匹配连字符,支持 %%MD-MASK-BLOCK-1 格式
22
22
  */
23
23
  private unmaskContent;
24
24
  /**
@@ -1,116 +1,122 @@
1
1
  export class MarkdownToText {
2
- // 用于暂存被保护的代码块,防止被正则误伤
2
+ // 1. 存储池
3
3
  codeBlockStore = new Map();
4
- maskPrefix = '%%MD_MASK_';
4
+ // 2. 关键修复:使用连字符 "-" 而非下划线,避免被斜体正则(Text)误伤
5
+ maskPrefix = '%%MD-MASK-';
5
6
  maskCounter = 0;
6
7
  /**
7
8
  * 主入口:将 Markdown 转换为纯文本
8
- * @param markdown 原始 Markdown 字符串
9
9
  */
10
10
  convert(markdown) {
11
11
  if (!markdown)
12
12
  return '';
13
- // 1. 初始化
13
+ // 初始化
14
14
  this.codeBlockStore.clear();
15
15
  this.maskCounter = 0;
16
16
  let text = markdown;
17
- // --- 阶段 1: 保护性预处理 (Protect) ---
18
- // 必须最先执行,防止代码里的注释 # 被当成标题,或 ** 被当成粗体
17
+ // ============================================================
18
+ // 阶段 1: 保护性预处理 (Protect)
19
+ // 必须最先执行,将代码块抽离,防止内部字符被后续逻辑误处理
20
+ // ============================================================
19
21
  text = this.maskCodeBlocks(text);
20
22
  text = this.maskInlineCode(text);
21
- // --- 阶段 2: 结构化转换 (Structure) ---
22
- // 2.1 清理 HTML 标签 (保留 <br> 的换行效果)
23
+ // ============================================================
24
+ // 阶段 2: 优先处理特殊标签 (Priority Tags)
25
+ // 必须在清理 HTML 之前处理,防止 <http://...> 被当成 HTML 标签误删
26
+ // ============================================================
27
+ // 2.1 图片 -> [图片: Alt]
28
+ text = text.replace(/!\[([^\]]*)]\(([^)]+)\)/g, (_match, alt) => {
29
+ return `[图片: ${alt || 'Image'}]`;
30
+ });
31
+ // 2.2 自动链接 <http://...> -> http://...
32
+ // 注意:这一步非常重要,Markdown 的自动链接语法和 HTML 标签很像
33
+ text = text.replace(/<((?:https?|ftp|email|mailto):[^>]+)>/g, '$1');
34
+ // 2.3 普通链接 [Text](url) -> Text (url)
35
+ text = text.replace(/\[([^\]]+)]\(([^)]+)\)/g, '$1 ($2)');
36
+ // ============================================================
37
+ // 阶段 3: 结构化转换 & 清理 (Structure & Clean)
38
+ // ============================================================
39
+ // 3.1 预处理换行和分割线标签
23
40
  text = text.replace(/<br\s*\/?>/gi, '\n');
24
- text = text.replace(/<hr\s*\/?>/gi, '\n────────────────────\n');
25
- text = text.replace(/<[^>]+>/g, ''); // 移除剩余所有标签
26
- // 2.2 标题 (Headers) -> 转换为视觉醒目的文本
27
- // H1/H2 使用双线/单线分隔,H3+ 使用括号包裹
28
- text = text.replace(/^#\s+(.*)$/gm, '\n$1\n════════════════════\n');
29
- text = text.replace(/^##\s+(.*)$/gm, '\n$1\n────────────────────\n');
41
+ text = text.replace(/<hr\s*\/?>/gi, '\n──────────\n');
42
+ // 3.2 安全清理 HTML 标签 (Smart Strip)
43
+ // A. 移除 <script> <style> 及其内容
44
+ text = text.replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi, '');
45
+ // B. 移除 HTML 注释 (修复了之前的语法错误)
46
+ text = text.replace(/<!--[\s\S]*?-->/g, '');
47
+ // C. 智能移除 HTML 标签
48
+ // 逻辑:匹配 < 后紧跟字母的模式,保留 "a < b" 或 "1 < 5" 这种数学公式
49
+ text = text.replace(/<\/?([a-z][a-z0-9]*)\b[^>]*>/gi, '');
50
+ // 3.3 标题 (Headers) -> 视觉醒目文本
51
+ text = text.replace(/^#\s+(.*)$/gm, '\n$1\n══════════\n');
52
+ text = text.replace(/^##\s+(.*)$/gm, '\n$1\n──────────\n');
30
53
  text = text.replace(/^(#{3,6})\s+(.*)$/gm, '\n【 $2 】\n');
31
- // 2.3 水平分割线 (---, ***, ___)
32
- text = text.replace(/^(-\s*?|\*\s*?|_\s*?){3,}\s*$/gm, '────────────────────');
33
- // 2.4 引用 (Blockquotes) -> 使用竖线前缀
34
- // 处理多级引用 >> Text
35
- text = text.replace(/^(>+)\s?(.*)$/gm, (_match, _arrows, content) => {
36
- return `▎ ${content}`;
37
- });
38
- // 2.5 任务列表 (Task Lists)
39
- text = text.replace(/^(\s*)-\s\[x]\s/gim, '$1✅ '); // 完成
40
- text = text.replace(/^(\s*)-\s\[\s]\s/gim, '$1⬜ '); // 未完成
41
- // 2.6 无序与有序列表 (Lists)
42
- // 保留 $1 (缩进空格),将 -/*/+ 替换为 •
43
- text = text.replace(/^(\s*)[-*+]\s+(.*)$/gm, '$1• $2');
44
- // 有序列表 1. 2. 通常不需要改动,保留原样即可
45
- // 2.7 表格 (Tables)
46
- // 移除对齐行 |---|---|
47
- text = text.replace(/^\s*\|?[\s\-:|]+\|?\s*$/gm, '');
48
- // 将 | Cell | Cell | 转换为空格分隔,尽量保持一行
54
+ // 3.4 Markdown 分割线 (---, ***)
55
+ text = text.replace(/^(-\s*?|\*\s*?|_\s*?){3,}\s*$/gm, '──────────');
56
+ // 3.5 引用 (Blockquotes)
57
+ text = text.replace(/^(>+)\s?(.*)$/gm, (_match, _arrows, content) => `▎ ${content}`);
58
+ // 3.6 任务列表 & 无序列表
59
+ text = text.replace(/^(\s*)-\s\[x]\s/gim, '$1✅ '); // 完成的任务
60
+ text = text.replace(/^(\s*)-\s\[\s]\s/gim, '$1⬜ '); // 未完成的任务
61
+ text = text.replace(/^(\s*)[-*+]\s+(.*)$/gm, '$1• $2'); // 列表项变圆点
62
+ // 3.7 表格 (Tables) -> 空格分隔
63
+ text = text.replace(/^\s*\|?[\s\-:|]+\|?\s*$/gm, ''); // 移除 |---|---| 分隔行
49
64
  text = text.replace(/^\|(.*)\|$/gm, (_match, content) => {
50
- // 移除首尾管道符,中间管道符变为空格
51
65
  return content.split('|').map((s) => s.trim()).join(' ');
52
66
  });
53
- // --- 阶段 3: 行内格式清理 (Inline Formatting) ---
54
- // 3.1 粗体 (Bold) -> 使用中文引号或双星号强调
55
- // 使用 [\s\S] 确保匹配跨行粗体
67
+ // ============================================================
68
+ // 阶段 4: 行内格式 (Inline Formatting)
69
+ // ============================================================
70
+ // 4.1 粗体 (**Text**) -> “Text”
56
71
  text = text.replace(/(\*\*|__)([\s\S]*?)\1/g, '“$2”');
57
- // 3.2 斜体 (Italic) -> 直接移除符号,纯文本很难表现斜体
58
- // 注意:必须在处理完粗体后处理斜体
72
+ // 4.2 斜体 (*Text*) -> Text
73
+ // 此时 Mask Key 是 "MD-MASK",不包含下划线或星号,所以不会被这里误伤
59
74
  text = text.replace(/([*_])([\s\S]*?)\1/g, '$2');
60
- // 3.3 删除线 (Strikethrough) -> 移除内容或仅移除符号?通常仅移除符号
75
+ // 4.3 删除线 (~~Text~~) -> Text
61
76
  text = text.replace(/~~([\s\S]*?)~~/g, '$1');
62
- // 3.4 图片 (Images) -> 转换为占位符
63
- text = text.replace(/!\[([^\]]*)]\(([^)]+)\)/g, (_match, alt) => {
64
- return `[图片: ${alt || 'Image'}]`;
65
- });
66
- // 3.5 链接 (Links) -> Text (URL)
67
- // 排除锚点链接或空链接
68
- text = text.replace(/\[([^\]]+)]\(([^)]+)\)/g, '$1 ($2)');
69
- // 处理自动链接 <http://example.com>
70
- text = text.replace(/<((?:https?|ftp|email):[^>]+)>/g, '$1');
71
- // --- 阶段 4: 还原与收尾 (Restore & Finalize) ---
72
- // 4.1 还原代码块
77
+ // ============================================================
78
+ // 阶段 5: 还原与收尾 (Restore & Finalize)
79
+ // ============================================================
80
+ // 5.1 还原代码块
73
81
  text = this.unmaskContent(text);
74
- // 4.2 解码 HTML 实体 (&amp; -> &)
82
+ // 5.2 解码 HTML 实体 (&amp; -> &)
75
83
  text = this.decodeHtmlEntities(text);
76
- // 4.3 最终排版优化
77
- // 移除段首段尾多余空白,将连续3个以上换行压缩为2个(段落间距)
84
+ // 5.3 最终排版优化:合并多余换行
78
85
  text = text.replace(/\n{3,}/g, '\n\n').trim();
79
86
  return text;
80
87
  }
81
88
  /**
82
- * 保护代码块
83
- * 支持 ```language 和 ~~~ 两种写法
89
+ * 保护代码块 (```)
90
+ * 生成 Key 格式:%%MD-MASK-BLOCK-0
84
91
  */
85
92
  maskCodeBlocks(text) {
86
- // 匹配 3个或更多反引号/波浪线
87
93
  const codeBlockRegex = /(`{3,}|~{3,})(\w*)\n([\s\S]*?)\1/g;
88
94
  return text.replace(codeBlockRegex, (_match, _fence, lang, code) => {
89
- const key = `${this.maskPrefix}BLOCK_${this.maskCounter++}`;
95
+ const key = `${this.maskPrefix}BLOCK-${this.maskCounter++}`;
90
96
  const langTag = lang ? ` [${lang}]` : '';
91
- // 构造美观的代码块样式
92
- const formatted = `\n────────────────────${langTag}\n${code.replace(/^\n+|\n+$/g, '')}\n────────────────────\n`;
97
+ const formatted = `\n───code───${langTag}\n${code.replace(/^\n+|\n+$/g, '')}\n──────────\n`;
93
98
  this.codeBlockStore.set(key, formatted);
94
99
  return key;
95
100
  });
96
101
  }
97
102
  /**
98
- * 保护行内代码
99
- * `code` -> 使用单引号包裹,区别于普通文本
103
+ * 保护行内代码 (`)
104
+ * 生成 Key 格式:%%MD-MASK-INLINE-0
100
105
  */
101
106
  maskInlineCode(text) {
102
107
  return text.replace(/`([^`]+)`/g, (_match, code) => {
103
- const key = `${this.maskPrefix}INLINE_${this.maskCounter++}`;
104
- this.codeBlockStore.set(key, ` ‘${code}’ `); // 加空格防止粘连
108
+ const key = `${this.maskPrefix}INLINE-${this.maskCounter++}`;
109
+ this.codeBlockStore.set(key, ` ‘${code}’ `);
105
110
  return key;
106
111
  });
107
112
  }
108
113
  /**
109
- * 还原被掩码的内容
114
+ * 还原掩码内容
115
+ * 必须匹配连字符,支持 %%MD-MASK-BLOCK-1 格式
110
116
  */
111
117
  unmaskContent(text) {
112
- // 使用正则全局匹配所有 mask key
113
- const maskRegex = new RegExp(`${this.maskPrefix}\\w+_\\d+`, 'g');
118
+ // 这里的正则 [\w-]+ 允许匹配字母、数字、下划线和连字符
119
+ const maskRegex = new RegExp(`${this.maskPrefix}[\\w-]+`, 'g');
114
120
  return text.replace(maskRegex, (key) => {
115
121
  return this.codeBlockStore.get(key) || '';
116
122
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@izhimu/qq",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "A QQ channel plugin for OpenClaw using NapCat WebSocket",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",