koishi-plugin-chatluna-think-viewer 1.0.19 → 2.0.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.
Files changed (3) hide show
  1. package/README.md +47 -43
  2. package/index.js +53 -33
  3. package/package.json +4 -5
package/README.md CHANGED
@@ -1,44 +1,48 @@
1
- # koishi-plugin-chatluna-think-viewer
2
-
3
- ????/????? `chatluna-character` ??????? `<think>` ???????????????????????
4
-
5
- ## ??
6
- - ?? `chatluna-character` ??????????????????????
7
- - ??????????????????
8
- - **????????**?? bot ???????????????? `<think>` ????? JSON ???????????????????
9
-
10
- ## ??
11
- ```bash
12
- # Koishi ??????? chatluna-think-viewer ??
13
- npm install koishi-plugin-chatluna-think-viewer
14
- ```
15
-
16
- ## ???? (koishi.yml)
17
- ```yaml
18
- plugins:
19
- chatluna-character: {}
20
- chatluna-think-viewer:
21
- command: think
22
- keywords:
23
- - ????
24
- - ????
25
- allowPrivate: false
26
- emptyMessage: ????????????
27
- # ??????
28
- guardEnabled: true
29
- guardMode: recall # recall | block
30
- guardForbiddenPatterns:
31
- - '<think>[\s\S]*?<\/think>'
32
- - '"role"\s*:\s*"assistant"'
33
- ```
34
-
35
- ## ??
36
- - ????? `think` ????????????? `<think>` ???`think 2` ????? 2 ??
37
- - ???????????? bot ???????????????????????????????
38
-
39
- ## ??
40
- - koishi >= 4.18.0
41
- - koishi-plugin-chatluna-character >= 0.0.180
42
-
43
- ## ??
1
+ # koishi-plugin-chatluna-think-viewer
2
+
3
+ 通过命令/关键词查看 `chatluna-character` 最近一次回复的 `<think>` 思考内容,并提供“异常格式自动撤回/拦截”守卫。
4
+
5
+ ## 功能
6
+ - 依赖 `chatluna-character` 存储的思考上下文,支持命令与前缀关键词调用。
7
+ - 支持群聊使用(可配置是否允许私聊)。
8
+ - **异常格式自动撤回/拦截**:默认检测 `<think>`、`<status>`、`<output>`、`<analysis>`、`<system>` 等块或调试 JSON、think/json/yaml 代码块;命中后可选择先发后撤回(recall)或直接阻止(block)。
9
+ - **严格输出模式(小白化)**:可选仅允许 `<output><message>…</message></output>` 结构;@ 仅允许数字 user_id,1~5 条 message,不符合即拦截/撤回。
10
+
11
+ ## 安装
12
+ ```bash
13
+ # Koishi 控制台市场搜索 chatluna-think-viewer 安装
14
+ npm install koishi-plugin-chatluna-think-viewer
15
+ ```
16
+
17
+ ## 配置示例 (koishi.yml)
18
+ ```yaml
19
+ plugins:
20
+ chatluna-character: {}
21
+ chatluna-think-viewer:
22
+ command: think
23
+ keywords:
24
+ - 查看思考
25
+ allowPrivate: false
26
+ emptyMessage: 暂时没有可用的思考记录。
27
+ # 异常撤回/拦截
28
+ guardEnabled: true
29
+ guardMode: recall # recall | block
30
+ guardDelay: 1 # 撤回延迟(秒),block 模式忽略
31
+ guardStrictOutputOnly: true # 只允许 <output><message>…</message></output>
32
+ guardStrictPattern: '^\s*<output>\s*(<message>(?:<at>\d+<\/at>\s*)?(?:<sticker>[^<]*<\/sticker>|[^<]*)<\/message>\s*){1,5}<\/output>\s*$'
33
+ guardForbiddenPatterns:
34
+ - '<think>[\\s\\S]*?<\\/think>'
35
+ - '<status>[\\s\\S]*?<\\/status>'
36
+ - '```\\s*think[\\s\\S]*?```'
37
+ ```
38
+
39
+ ## 使用
40
+ - 群聊里发送 `think` 或配置的关键词查看最近一次 `<think>` 内容;`think 2` 查看倒数第 2 条。
41
+ - 异常/严格模式:当 bot 发送的消息命中禁用规则或不符合严格输出结构时,记录日志并撤回(或直接阻止发送)。
42
+
43
+ ## 依赖
44
+ - koishi >= 4.18.0
45
+ - koishi-plugin-chatluna-character >= 0.0.180
46
+
47
+ ## 协议
44
48
  MIT
package/index.js CHANGED
@@ -10,35 +10,53 @@ const inject = {
10
10
 
11
11
  const defaultForbidden = [
12
12
  '<think>[\\s\\S]*?<\\/think>',
13
+ '<status>[\\s\\S]*?<\\/status>',
14
+ '<output>[\\s\\S]*?<\\/output>',
15
+ '<analysis>[\\s\\S]*?<\\/analysis>',
16
+ '<system>[\\s\\S]*?<\\/system>',
13
17
  '```\\s*think[\\s\\S]*?```',
18
+ '```\\s*(json|yaml|yml)[\\s\\S]*?```',
14
19
  '"role"\\s*:\\s*"assistant"',
15
20
  '"analysis"\\s*:',
16
21
  '"thought"\\s*:',
22
+ '(?:human_relations|人际关系)\\s*[:=]',
23
+ '(?:memory|记忆|记忆点|总结)\\s*[:=]',
17
24
  ];
18
25
 
26
+ // 严格 <output><message>... 结构:允许文本 / <at>user_id</at> 文本 / <sticker>url</sticker>
27
+ // 1~5 条 message,@ 仅允许数字 user_id
28
+ const strictOutputPattern =
29
+ '^\\s*<output>\\s*(<message>(?:<at>\\d+<\\/at>\\s*)?(?:<sticker>[^<]*<\\/sticker>|[^<]*)<\\/message>\\s*){1,5}<\\/output>\\s*$';
30
+
19
31
  const Config = Schema.intersect([
20
32
  Schema.object({
21
- command: Schema.string().default('think').description('查看思考内容的指令名'),
22
- keywords: Schema.array(Schema.string()).default(['查看思考', '上次思考']).description('可无前缀触发的关键词'),
23
- allowPrivate: Schema.boolean().default(false).description('是否允许在私聊中使用'),
24
- emptyMessage: Schema.string().default('暂时没有可用的思考记录。').description('没有记录时的提示文本'),
25
- renderImage: Schema.boolean().default(false).description('是否通过 ChatLuna image renderer 将思考渲染为图片,失败时回退文本'),
26
- }).description('思考查看配置'),
33
+ command: Schema.string().default('think').description('\u67e5\u770b\u601d\u8003\u5185\u5bb9\u7684\u6307\u4ee4\u540d'),
34
+ keywords: Schema.array(Schema.string()).default(['\u67e5\u770b\u601d\u8003', '\u4e0a\u6b21\u601d\u8003']).description('\u53ef\u65e0\u524d\u7f00\u89e6\u53d1\u7684\u5173\u952e\u8bcd'),
35
+ allowPrivate: Schema.boolean().default(false).description('\u662f\u5426\u5141\u8bb8\u5728\u79c1\u804a\u4e2d\u4f7f\u7528'),
36
+ emptyMessage: Schema.string().default('\u6682\u65f6\u6ca1\u6709\u53ef\u7528\u7684\u601d\u8003\u8bb0\u5f55\u3002').description('\u6ca1\u6709\u8bb0\u5f55\u65f6\u7684\u63d0\u793a\u6587\u672c'),
37
+ renderImage: Schema.boolean().default(false).description('\u662f\u5426\u901a\u8fc7\u0020\u0043\u0068\u0061\u0074\u004c\u0075\u006e\u0061\u0020\u0069\u006d\u0061\u0067\u0065\u0020\u0072\u0065\u006e\u0064\u0065\u0072\u0065\u0072\u0020\u5c06\u601d\u8003\u6e32\u67d3\u4e3a\u56fe\u7247\uff0c\u5931\u8d25\u65f6\u56de\u9000\u6587\u672c'),
38
+ }).description('\u601d\u8003\u67e5\u770b\u914d\u7f6e'),
27
39
  Schema.object({
28
- guardEnabled: Schema.boolean().default(true).description('异常输出自动拦截开关'),
29
- guardMode: Schema.union(['recall', 'block']).default('recall').description('recall=先发送后撤回,block=直接阻止发送'),
30
- guardDelay: Schema.number().default(1).min(0).max(60).description('撤回延迟(秒)'),
31
- guardAllowPrivate: Schema.boolean().default(true).description('是否在私聊中也启用拦截'),
32
- guardGroups: Schema.array(Schema.string()).default([]).description('只在这些群生效,留空表示全部'),
40
+ guardEnabled: Schema.boolean().default(true).description('\u5f02\u5e38\u8f93\u51fa\u81ea\u52a8\u62e6\u622a\u5f00\u5173'),
41
+ guardMode: Schema.union(['recall', 'block']).default('recall').description('\u0072\u0065\u0063\u0061\u006c\u006c\u003d\u5148\u53d1\u9001\u540e\u64a4\u56de\uff0c\u0062\u006c\u006f\u0063\u006b\u003d\u76f4\u63a5\u963b\u6b62\u53d1\u9001'),
42
+ guardDelay: Schema.number().default(1).min(0).max(60).description('\u64a4\u56de\u5ef6\u8fdf\uff08\u79d2\uff09'),
43
+ guardAllowPrivate: Schema.boolean().default(true).description('\u662f\u5426\u5728\u79c1\u804a\u4e2d\u4e5f\u542f\u7528\u62e6\u622a'),
44
+ guardGroups: Schema.array(Schema.string()).default([]).description('\u53ea\u5728\u8fd9\u4e9b\u7fa4\u751f\u6548\uff0c\u7559\u7a7a\u8868\u793a\u5168\u90e8'),
33
45
  guardForbiddenPatterns: Schema.array(Schema.string())
34
46
  .default(defaultForbidden)
35
- .description('命中即视为异常的模式,用于避免思考泄露或 JSON 生出'),
47
+ .description('\u547d\u4e2d\u5373\u89c6\u4e3a\u5f02\u5e38\u7684\u6a21\u5f0f\uff0c\u7528\u4e8e\u907f\u514d\u601d\u8003\u6cc4\u9732\u6216\u0020\u004a\u0053\u004f\u004e\u0020\u751f\u51fa'),
36
48
  guardAllowedPatterns: Schema.array(Schema.string())
37
49
  .default(['[\\s\\S]+'])
38
- .description('可选白名单,至少匹配一个才算正常'),
39
- guardLog: Schema.boolean().default(true).description('是否在日志记录异常原因和内容'),
40
- guardContentPreview: Schema.number().default(80).min(10).max(500).description('日志内容预览长度'),
41
- }).description('异常输出自动处理'),
50
+ .description('\u53ef\u9009\u767d\u540d\u5355\uff0c\u81f3\u5c11\u5339\u914d\u4e00\u4e2a\u624d\u7b97\u6b63\u5e38'),
51
+ guardStrictOutputOnly: Schema.boolean()
52
+ .default(false)
53
+ .description('\u53ea\u5141\u8bb8\u7b26\u5408 <output><message>\u2026</message></output> \u7ed3\u6784\u7684\u6d88\u606f\uff0c\u4e0d\u7b26\u5408\u5373\u62e6\u622a/\u64a4\u56de'),
54
+ guardStrictPattern: Schema.string()
55
+ .default(strictOutputPattern)
56
+ .description('\u81ea\u5b9a\u4e49\u4e25\u683c\u8f93\u51fa\u6b63\u5219\uff1b\u4e3a\u7a7a\u65f6\u4f7f\u7528\u5185\u7f6e\u7684 <output><message>\u2026</message> \u89c4\u5219'),
57
+ guardLog: Schema.boolean().default(true).description('\u662f\u5426\u5728\u65e5\u5fd7\u8bb0\u5f55\u5f02\u5e38\u539f\u56e0\u548c\u5185\u5bb9'),
58
+ guardContentPreview: Schema.number().default(80).min(10).max(500).description('\u65e5\u5fd7\u5185\u5bb9\u9884\u89c8\u957f\u5ea6'),
59
+ }).description('\u5f02\u5e38\u8f93\u51fa\u81ea\u52a8\u5904\u7406'),
42
60
  ]);
43
61
 
44
62
  function extractText(content) {
@@ -60,7 +78,7 @@ function extractText(content) {
60
78
  }
61
79
 
62
80
  function extractThink(text) {
63
- // 某些模型/中间件会在同一条消息里多次出现 <think>,取最后一次
81
+ // \u67d0\u4e9b\u6a21\u578b/\u4e2d\u95f4\u4ef6\u4f1a\u5728\u540c\u4e00\u6761\u6d88\u606f\u91cc\u591a\u6b21\u51fa\u73b0 <think>\uff0c\u53d6\u6700\u540e\u4e00\u6b21
64
82
  let last = '';
65
83
  const regex = /<think>([\s\S]*?)<\/think>/gi;
66
84
  let m;
@@ -72,7 +90,7 @@ function extractThink(text) {
72
90
 
73
91
  function formatThink(text) {
74
92
  if (!text) return text;
75
- // 尝试格式化 JSON,失败则做基础去空行/缩进美化
93
+ // \u5c1d\u8bd5\u683c\u5f0f\u5316 JSON\uff0c\u5931\u8d25\u5219\u505a\u57fa\u7840\u53bb\u7a7a\u884c/\u7f29\u8fdb\u7f8e\u5316
76
94
  try {
77
95
  const parsed = JSON.parse(text);
78
96
  return JSON.stringify(parsed, null, 2);
@@ -159,10 +177,10 @@ function compileRegex(list) {
159
177
  function detectAbnormal(text, forbidden, allowed) {
160
178
  if (!text) return null;
161
179
  for (const re of forbidden) {
162
- if (re.test(text)) return `命中禁止模式: /${re.source}/`;
180
+ if (re.test(text)) return `\u547d\u4e2d\u7981\u6b62\u6a21\u5f0f: /${re.source}/`;
163
181
  }
164
182
  if (allowed.length && !allowed.some((re) => re.test(text))) {
165
- return '未匹配任何允许模式';
183
+ return '\u672a\u5339\u914d\u4efb\u4f55\u5141\u8bb8\u6a21\u5f0f';
166
184
  }
167
185
  return null;
168
186
  }
@@ -187,7 +205,9 @@ function applyGuard(ctx, config) {
187
205
  if (!config.guardEnabled) return;
188
206
  const logger = ctx.logger(`${name}:guard`);
189
207
  const forbidden = compileRegex(config.guardForbiddenPatterns);
190
- const allowed = compileRegex(config.guardAllowedPatterns);
208
+ const allowed = config.guardStrictOutputOnly
209
+ ? compileRegex([config.guardStrictPattern || strictOutputPattern])
210
+ : compileRegex(config.guardAllowedPatterns);
191
211
  const original = Bot.prototype.sendMessage;
192
212
 
193
213
  Bot.prototype.sendMessage = async function patched(channelId, content, referrer, options = {}) {
@@ -229,10 +249,10 @@ function applyGuard(ctx, config) {
229
249
  }
230
250
 
231
251
  function apply(ctx, config) {
232
- // 思考查看指令
252
+ // \u601d\u8003\u67e5\u770b\u6307\u4ee4
233
253
  const cmd = ctx
234
- .command(`${config.command} [index:string]`, '读取上一条含 <think> 的内容,可指定倒数第 N ')
235
- .usage('不带参数默认最新;示例:think 2 查询倒数第 2 AI 回复的思考');
254
+ .command(`${config.command} [index:string]`, '\u8bfb\u53d6\u4e0a\u4e00\u6761\u542b <think> \u7684\u5185\u5bb9\uff0c\u53ef\u6307\u5b9a\u5012\u6570\u7b2c N \u6761')
255
+ .usage('\u4e0d\u5e26\u53c2\u6570\u9ed8\u8ba4\u6700\u65b0\uff1b\u793a\u4f8b\uff1athink 2 \u67e5\u8be2\u5012\u6570\u7b2c 2 \u6761 AI \u56de\u590d\u7684\u601d\u8003');
236
256
 
237
257
  for (const keyword of config.keywords || []) {
238
258
  cmd.shortcut(keyword, { prefix: false });
@@ -240,23 +260,23 @@ function apply(ctx, config) {
240
260
 
241
261
  cmd.action(async ({ session, args }, rawIndex) => {
242
262
  if (!config.allowPrivate && !session.guildId) {
243
- return '不支持在私聊中查询。';
263
+ return '\u4e0d\u652f\u6301\u5728\u79c1\u804a\u4e2d\u67e5\u8be2\u3002';
244
264
  }
245
265
 
246
266
  const service = ctx.chatluna_character;
247
- if (!service) return 'chatluna-character 未加载。';
267
+ if (!service) return 'chatluna-character \u672a\u52a0\u8f7d\u3002';
248
268
 
249
269
  const temp = await service.getTemp(session);
250
270
  const targetIndex = parseIndex(rawIndex ?? args?.[0]);
251
271
 
252
- // 1) 优先读取最新一次原始响应(通常仍含 <think>),仅对第 1 条有效
272
+ // 1) \u4f18\u5148\u8bfb\u53d6\u6700\u65b0\u4e00\u6b21\u539f\u59cb\u54cd\u5e94\uff08\u901a\u5e38\u4ecd\u542b <think>\uff09\uff0c\u4ec5\u5bf9\u7b2c 1 \u6761\u6709\u6548
253
273
  const thinkFromRaw = targetIndex === 1 ? getLatestRawThink(temp) : '';
254
274
 
255
- // 2) 历史 completionMessages 中真正带 <think> AI 消息
275
+ // 2) \u5386\u53f2 completionMessages \u4e2d\u771f\u6b63\u5e26 <think> \u7684 AI \u6d88\u606f
256
276
  const messages = temp?.completionMessages || [];
257
277
  const thinkFromHistory = thinkFromRaw ? '' : getNthThink(messages, targetIndex);
258
278
 
259
- // 3) 回退:第 N AI 消息再尝试抽取 <think>
279
+ // 3) \u56de\u9000\uff1a\u7b2c N \u6761 AI \u6d88\u606f\u518d\u5c1d\u8bd5\u62bd\u53d6 <think>
260
280
  const fallbackMsg = thinkFromRaw || thinkFromHistory ? null : getNthAiMessage(messages, targetIndex);
261
281
  const think = thinkFromRaw || thinkFromHistory || extractThink(extractText(fallbackMsg?.content));
262
282
  const formatted = formatThink(think);
@@ -264,7 +284,7 @@ function apply(ctx, config) {
264
284
 
265
285
  if (config.renderImage && ctx.chatluna?.renderer) {
266
286
  try {
267
- const title = `### 上一条思考(倒数第 ${targetIndex} 条)`;
287
+ const title = `### \u4e0a\u4e00\u6761\u601d\u8003\uff08\u5012\u6570\u7b2c ${targetIndex} \u6761\uff09`;
268
288
  const markdown = `<div align="center">\n${title}\n</div>\n\n<div align="left">\n${formatted}\n</div>`;
269
289
  const rendered = await ctx.chatluna.renderer.render(
270
290
  {
@@ -278,10 +298,10 @@ function apply(ctx, config) {
278
298
  }
279
299
  }
280
300
 
281
- return `上一条思考(倒数第 ${targetIndex} 条)\n${formatted}`;
301
+ return `\u4e0a\u4e00\u6761\u601d\u8003\uff08\u5012\u6570\u7b2c ${targetIndex} \u6761\uff09\n${formatted}`;
282
302
  });
283
303
 
284
- // 异常输出自动处理
304
+ // \u5f02\u5e38\u8f93\u51fa\u81ea\u52a8\u5904\u7406
285
305
  applyGuard(ctx, config);
286
306
  }
287
307
 
@@ -290,4 +310,4 @@ module.exports = {
290
310
  apply,
291
311
  Config,
292
312
  inject,
293
- };
313
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-chatluna-think-viewer",
3
- "version": "1.0.19",
3
+ "version": "2.0.0",
4
4
  "main": "index.js",
5
5
  "description": "View chatluna <think> blocks and auto recall abnormal formatted replies.",
6
6
  "license": "MIT",
@@ -27,8 +27,8 @@
27
27
  },
28
28
  "koishi": {
29
29
  "description": {
30
- "zh": "?? chatluna-character ? <think> ??????????????????/???",
31
- "en": "View <think> blocks from chatluna-character and auto recall malformed replies."
30
+ "zh": "通过命令/关键词查看 chatluna-character 最近一次回复的 <think> 思考内容,并在消息格式异常时自动拦截/撤回。",
31
+ "en": "View <think> blocks from chatluna-character and auto recall/guard malformed replies."
32
32
  },
33
33
  "service": {
34
34
  "required": [
@@ -40,5 +40,4 @@
40
40
  "index.js",
41
41
  "README.md"
42
42
  ]
43
- }
44
-
43
+ }