koishi-plugin-chatluna-think-viewer 1.0.17 → 1.0.19
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/README.md +43 -38
- package/index.js +142 -33
- package/package.json +10 -6
package/README.md
CHANGED
|
@@ -1,39 +1,44 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
##
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
##
|
|
11
|
-
```bash
|
|
12
|
-
# Koishi
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
+
## ??
|
|
39
44
|
MIT
|
package/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
const { Schema, h } = require('koishi');
|
|
2
|
+
const { Bot } = require('@satorijs/core');
|
|
2
3
|
|
|
3
4
|
const name = 'chatluna-think-viewer';
|
|
4
5
|
|
|
@@ -7,37 +8,59 @@ const inject = {
|
|
|
7
8
|
chatluna: { required: false },
|
|
8
9
|
};
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
const defaultForbidden = [
|
|
12
|
+
'<think>[\\s\\S]*?<\\/think>',
|
|
13
|
+
'```\\s*think[\\s\\S]*?```',
|
|
14
|
+
'"role"\\s*:\\s*"assistant"',
|
|
15
|
+
'"analysis"\\s*:',
|
|
16
|
+
'"thought"\\s*:',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const Config = Schema.intersect([
|
|
20
|
+
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('思考查看配置'),
|
|
27
|
+
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('只在这些群生效,留空表示全部'),
|
|
33
|
+
guardForbiddenPatterns: Schema.array(Schema.string())
|
|
34
|
+
.default(defaultForbidden)
|
|
35
|
+
.description('命中即视为异常的模式,用于避免思考泄露或 JSON 生出'),
|
|
36
|
+
guardAllowedPatterns: Schema.array(Schema.string())
|
|
37
|
+
.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('异常输出自动处理'),
|
|
42
|
+
]);
|
|
17
43
|
|
|
18
44
|
function extractText(content) {
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (typeof content.text === 'string') return content.text;
|
|
33
|
-
if (typeof content.content === 'string') return content.content;
|
|
34
|
-
if (typeof content.value === 'string') return content.value;
|
|
45
|
+
if (content == null) return '';
|
|
46
|
+
const normalized = h.normalize(content);
|
|
47
|
+
const parts = [];
|
|
48
|
+
for (const el of normalized) {
|
|
49
|
+
if (typeof el === 'string') {
|
|
50
|
+
parts.push(el);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (Array.isArray(el.children) && el.children.length) {
|
|
54
|
+
parts.push(extractText(el.children));
|
|
55
|
+
}
|
|
56
|
+
const textLike = el.attrs?.content ?? el.attrs?.text ?? el.children?.join?.('') ?? '';
|
|
57
|
+
if (textLike) parts.push(textLike);
|
|
35
58
|
}
|
|
36
|
-
return '';
|
|
59
|
+
return parts.join('');
|
|
37
60
|
}
|
|
38
61
|
|
|
39
62
|
function extractThink(text) {
|
|
40
|
-
// 某些模型/中间件会在同一条消息里多次出现 <think
|
|
63
|
+
// 某些模型/中间件会在同一条消息里多次出现 <think>,取最后一次
|
|
41
64
|
let last = '';
|
|
42
65
|
const regex = /<think>([\s\S]*?)<\/think>/gi;
|
|
43
66
|
let m;
|
|
@@ -54,7 +77,6 @@ function formatThink(text) {
|
|
|
54
77
|
const parsed = JSON.parse(text);
|
|
55
78
|
return JSON.stringify(parsed, null, 2);
|
|
56
79
|
} catch {
|
|
57
|
-
// 保留原文,去掉多余空行并统一缩进
|
|
58
80
|
const lines = text.split('\n').map((l) => l.trimEnd());
|
|
59
81
|
const filtered = lines.filter((l, idx, arr) => !(l === '' && arr[idx - 1] === ''));
|
|
60
82
|
const nonEmpty = filtered.filter((l) => l.trim().length > 0);
|
|
@@ -122,10 +144,95 @@ function getLatestRawThink(temp) {
|
|
|
122
144
|
return '';
|
|
123
145
|
}
|
|
124
146
|
|
|
147
|
+
function compileRegex(list) {
|
|
148
|
+
return (list || [])
|
|
149
|
+
.map((p) => {
|
|
150
|
+
try {
|
|
151
|
+
return new RegExp(p, 'i');
|
|
152
|
+
} catch (err) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
.filter(Boolean);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function detectAbnormal(text, forbidden, allowed) {
|
|
160
|
+
if (!text) return null;
|
|
161
|
+
for (const re of forbidden) {
|
|
162
|
+
if (re.test(text)) return `命中禁止模式: /${re.source}/`;
|
|
163
|
+
}
|
|
164
|
+
if (allowed.length && !allowed.some((re) => re.test(text))) {
|
|
165
|
+
return '未匹配任何允许模式';
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function shorten(text, limit = 80) {
|
|
171
|
+
if (!text) return '';
|
|
172
|
+
if (text.length <= limit) return text;
|
|
173
|
+
return `${text.slice(0, limit)}...(${text.length} chars)`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function shouldGuard(config, options) {
|
|
177
|
+
const session = options?.session;
|
|
178
|
+
if (!session) return false;
|
|
179
|
+
const guildId = session.guildId || session.event?.guild?.id;
|
|
180
|
+
const isGroup = !!guildId;
|
|
181
|
+
if (!config.guardAllowPrivate && !isGroup) return false;
|
|
182
|
+
if (config.guardGroups?.length && isGroup && !config.guardGroups.includes(guildId)) return false;
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function applyGuard(ctx, config) {
|
|
187
|
+
if (!config.guardEnabled) return;
|
|
188
|
+
const logger = ctx.logger(`${name}:guard`);
|
|
189
|
+
const forbidden = compileRegex(config.guardForbiddenPatterns);
|
|
190
|
+
const allowed = compileRegex(config.guardAllowedPatterns);
|
|
191
|
+
const original = Bot.prototype.sendMessage;
|
|
192
|
+
|
|
193
|
+
Bot.prototype.sendMessage = async function patched(channelId, content, referrer, options = {}) {
|
|
194
|
+
if (!shouldGuard(config, options)) {
|
|
195
|
+
return original.call(this, channelId, content, referrer, options);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const text = extractText(content);
|
|
199
|
+
const reason = detectAbnormal(text, forbidden, allowed);
|
|
200
|
+
|
|
201
|
+
if (!reason) {
|
|
202
|
+
return original.call(this, channelId, content, referrer, options);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const preview = shorten(text, config.guardContentPreview);
|
|
206
|
+
if (config.guardMode === 'block') {
|
|
207
|
+
if (config.guardLog) logger.warn(`[block] ${reason} | content: ${preview}`);
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const ids = await original.call(this, channelId, content, referrer, options);
|
|
212
|
+
if (config.guardLog) logger.warn(`[recall] ${reason} | content: ${preview}`);
|
|
213
|
+
const delay = Math.max(0, config.guardDelay) * 1000;
|
|
214
|
+
if (Array.isArray(ids) && ids.length && typeof this.deleteMessage === 'function') {
|
|
215
|
+
setTimeout(() => {
|
|
216
|
+
for (const id of ids) {
|
|
217
|
+
this.deleteMessage(channelId, id).catch((err) => {
|
|
218
|
+
logger.warn(`[recall-failed] id=${id} reason=${err?.message || err}`);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}, delay);
|
|
222
|
+
}
|
|
223
|
+
return ids;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
ctx.on('dispose', () => {
|
|
227
|
+
Bot.prototype.sendMessage = original;
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
125
231
|
function apply(ctx, config) {
|
|
232
|
+
// 思考查看指令
|
|
126
233
|
const cmd = ctx
|
|
127
|
-
.command(`${config.command} [index:string]`, '
|
|
128
|
-
.usage('
|
|
234
|
+
.command(`${config.command} [index:string]`, '读取上一条含 <think> 的内容,可指定倒数第 N 条')
|
|
235
|
+
.usage('不带参数默认最新;示例:think 2 查询倒数第 2 条 AI 回复的思考');
|
|
129
236
|
|
|
130
237
|
for (const keyword of config.keywords || []) {
|
|
131
238
|
cmd.shortcut(keyword, { prefix: false });
|
|
@@ -142,14 +249,14 @@ function apply(ctx, config) {
|
|
|
142
249
|
const temp = await service.getTemp(session);
|
|
143
250
|
const targetIndex = parseIndex(rawIndex ?? args?.[0]);
|
|
144
251
|
|
|
145
|
-
// 1) 优先读取最新一次原始响应(通常仍含 <think
|
|
252
|
+
// 1) 优先读取最新一次原始响应(通常仍含 <think>),仅对第 1 条有效
|
|
146
253
|
const thinkFromRaw = targetIndex === 1 ? getLatestRawThink(temp) : '';
|
|
147
254
|
|
|
148
255
|
// 2) 历史 completionMessages 中真正带 <think> 的 AI 消息
|
|
149
256
|
const messages = temp?.completionMessages || [];
|
|
150
257
|
const thinkFromHistory = thinkFromRaw ? '' : getNthThink(messages, targetIndex);
|
|
151
258
|
|
|
152
|
-
// 3) 回退:第 N 条 AI 消息再尝试抽取
|
|
259
|
+
// 3) 回退:第 N 条 AI 消息再尝试抽取 <think>
|
|
153
260
|
const fallbackMsg = thinkFromRaw || thinkFromHistory ? null : getNthAiMessage(messages, targetIndex);
|
|
154
261
|
const think = thinkFromRaw || thinkFromHistory || extractThink(extractText(fallbackMsg?.content));
|
|
155
262
|
const formatted = formatThink(think);
|
|
@@ -161,7 +268,6 @@ function apply(ctx, config) {
|
|
|
161
268
|
const markdown = `<div align="center">\n${title}\n</div>\n\n<div align="left">\n${formatted}\n</div>`;
|
|
162
269
|
const rendered = await ctx.chatluna.renderer.render(
|
|
163
270
|
{
|
|
164
|
-
// 居中标题、左对齐正文,保持 renderer 兼容
|
|
165
271
|
content: [{ type: 'text', text: markdown }],
|
|
166
272
|
},
|
|
167
273
|
{ type: 'image', session },
|
|
@@ -174,6 +280,9 @@ function apply(ctx, config) {
|
|
|
174
280
|
|
|
175
281
|
return `上一条思考(倒数第 ${targetIndex} 条)\n${formatted}`;
|
|
176
282
|
});
|
|
283
|
+
|
|
284
|
+
// 异常输出自动处理
|
|
285
|
+
applyGuard(ctx, config);
|
|
177
286
|
}
|
|
178
287
|
|
|
179
288
|
module.exports = {
|
|
@@ -181,4 +290,4 @@ module.exports = {
|
|
|
181
290
|
apply,
|
|
182
291
|
Config,
|
|
183
292
|
inject,
|
|
184
|
-
};
|
|
293
|
+
};
|
package/package.json
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
{
|
|
2
2
|
"name": "koishi-plugin-chatluna-think-viewer",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.19",
|
|
4
4
|
"main": "index.js",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "View chatluna <think> blocks and auto recall abnormal formatted replies.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"koishi",
|
|
9
9
|
"chatluna",
|
|
10
10
|
"character",
|
|
11
|
-
"think"
|
|
11
|
+
"think",
|
|
12
|
+
"recall",
|
|
13
|
+
"guard",
|
|
14
|
+
"moderation"
|
|
12
15
|
],
|
|
13
16
|
"homepage": "https://github.com/sCR0WN-s/koishi-plugin-chatluna-think-viewer",
|
|
14
17
|
"repository": {
|
|
@@ -24,8 +27,8 @@
|
|
|
24
27
|
},
|
|
25
28
|
"koishi": {
|
|
26
29
|
"description": {
|
|
27
|
-
"zh": "
|
|
28
|
-
"en": "
|
|
30
|
+
"zh": "?? chatluna-character ? <think> ??????????????????/???",
|
|
31
|
+
"en": "View <think> blocks from chatluna-character and auto recall malformed replies."
|
|
29
32
|
},
|
|
30
33
|
"service": {
|
|
31
34
|
"required": [
|
|
@@ -38,3 +41,4 @@
|
|
|
38
41
|
"README.md"
|
|
39
42
|
]
|
|
40
43
|
}
|
|
44
|
+
|