koishi-plugin-chatluna-think-viewer 1.0.16 → 1.0.17

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 +7 -8
  2. package/index.js +183 -183
  3. package/package.json +39 -39
package/README.md CHANGED
@@ -1,19 +1,18 @@
1
- # koishi-plugin-chatluna-think-viewer
1
+ # koishi-plugin-chatluna-think-viewer
2
2
 
3
3
  通过命令或快捷关键词查看 `chatluna-character` 最近一次回复中的 `<think>` 思考内容,便于调试和复盘。
4
4
 
5
5
  ## 特性
6
- - 复用 `chatluna-character` 内存,不额外占用数据库。
6
+ - 复用 `chatluna-character` 的内存,不额外占用数据库。
7
7
  - 支持命令与无前缀关键词触发。
8
8
  - 群聊可用,默认禁止私聊(可配置)。
9
9
 
10
10
  ## 安装
11
11
  ```bash
12
- # 使用 Koishi 控制台市场搜索「chatluna-think-viewer」安装
13
- # 或者 npm/yarn 安装:
12
+ # Koishi 控制台市场搜索「chatluna-think-viewer」安装
13
+ # 或者 npm/yarn 安装
14
14
  npm install koishi-plugin-chatluna-think-viewer
15
- #
16
- yarn add koishi-plugin-chatluna-think-viewer
15
+ # yarn add koishi-plugin-chatluna-think-viewer
17
16
  ```
18
17
 
19
18
  ## 配置示例 (koishi.yml)
@@ -30,11 +29,11 @@ plugins:
30
29
  ```
31
30
 
32
31
  ## 使用
33
- - 群聊中发送 `think`(按你的命令前缀)或关键词“查看思考”/“上次思考”,返回上一条回复的 `<think>` 内容。
32
+ - 在群聊中发送 `think`(根据你的命令前缀)或关键词 “查看思考 / 上次思考”,返回上一条回复的 `<think>` 内容。
34
33
 
35
34
  ## 依赖
36
35
  - koishi >= 4.18.0
37
36
  - koishi-plugin-chatluna-character >= 0.0.180
38
37
 
39
38
  ## 协议
40
- MIT
39
+ MIT
package/index.js CHANGED
@@ -1,184 +1,184 @@
1
- const { Schema } = require('koishi');
2
-
3
- const name = 'chatluna-think-viewer';
4
-
5
- const inject = {
6
- chatluna_character: { required: true },
7
- chatluna: { required: false },
8
- };
9
-
10
- const Config = Schema.object({
11
- command: Schema.string().default('think').description('��������'),
12
- keywords: Schema.array(Schema.string()).default(['�鿴˼��', '�ϴ�˼��']).description('����ǰ׺���ɴ����Ĺؼ���'),
13
- allowPrivate: Schema.boolean().default(false).description('�Ƿ�������˽����ʹ��'),
14
- emptyMessage: Schema.string().default('��ʱû�п��õ�˼����¼��').description('û�м�¼ʱ����ʾ�İ�'),
15
- renderImage: Schema.boolean().default(false).description('����ʹ�� ChatLuna �� image renderer ��˼��������ȾΪͼƬ���ͣ�ʧ��������ı�'),
16
- });
17
-
18
- function extractText(content) {
19
- if (typeof content === 'string') return content;
20
- if (Array.isArray(content)) {
21
- return content
22
- .map((part) => {
23
- if (typeof part === 'string') return part;
24
- if (part && typeof part === 'object') {
25
- return part.text || part.content || part.value || '';
26
- }
27
- return '';
28
- })
29
- .join('');
30
- }
31
- if (content && typeof content === 'object') {
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;
35
- }
36
- return '';
37
- }
38
-
39
- function extractThink(text) {
40
- // ��Щģ��/�м������ͬһ����Ϣ������� <think>��ȡ���һ�α������þ�Ƭ��
41
- let last = '';
42
- const regex = /<think>([\s\S]*?)<\/think>/gi;
43
- let m;
44
- while ((m = regex.exec(text)) !== null) {
45
- last = m[1];
46
- }
47
- return last.trim();
48
- }
49
-
50
- function formatThink(text) {
51
- if (!text) return text;
52
- // ���� JSON ����
53
- try {
54
- const parsed = JSON.parse(text);
55
- return JSON.stringify(parsed, null, 2);
56
- } catch {
57
- // ����ԭ�ģ�ȥ�����������ͳһ�������
58
- const lines = text.split('\n').map((l) => l.trimEnd());
59
- const filtered = lines.filter((l, idx, arr) => !(l === '' && arr[idx - 1] === ''));
60
- const nonEmpty = filtered.filter((l) => l.trim().length > 0);
61
- const minIndent = nonEmpty.length
62
- ? Math.min(
63
- ...nonEmpty.map((l) => {
64
- const m = l.match(/^(\s*)/);
65
- return m ? m[1].length : 0;
66
- }),
67
- )
68
- : 0;
69
- return filtered.map((l) => l.slice(minIndent)).join('\n');
70
- }
71
- }
72
-
73
- function parseIndex(rawIndex) {
74
- if (!rawIndex) return 1;
75
- if (typeof rawIndex === 'number' && Number.isFinite(rawIndex) && rawIndex > 0) return Math.floor(rawIndex);
76
- const match = String(rawIndex).match(/\d+/);
77
- if (!match) return 1;
78
- const num = parseInt(match[0], 10);
79
- return Number.isFinite(num) && num > 0 ? num : 1;
80
- }
81
-
82
- function getNthAiMessage(messages, n = 1) {
83
- if (!Array.isArray(messages) || n < 1) return null;
84
- let count = 0;
85
- for (let i = messages.length - 1; i >= 0; i--) {
86
- const msg = messages[i];
87
- const type = typeof msg?._getType === 'function' ? msg._getType() : msg?.type || msg?.role;
88
- if (type === 'ai' || type === 'assistant') {
89
- count += 1;
90
- if (count === n) return msg;
91
- }
92
- }
93
- return null;
94
- }
95
-
96
- function getNthThink(messages, n = 1) {
97
- if (!Array.isArray(messages) || n < 1) return null;
98
- let count = 0;
99
- for (let i = messages.length - 1; i >= 0; i--) {
100
- const msg = messages[i];
101
- const type = typeof msg?._getType === 'function' ? msg._getType() : msg?.type || msg?.role;
102
- if (type !== 'ai' && type !== 'assistant') continue;
103
- const think = extractThink(extractText(msg.content));
104
- if (!think) continue;
105
- count += 1;
106
- if (count === n) return think;
107
- }
108
- return null;
109
- }
110
-
111
- function getLatestRawThink(temp) {
112
- if (!temp) return '';
113
- const candidates = [
114
- temp?.lastCompletion?.raw?.choices?.[0]?.message?.content,
115
- temp?.lastCompletion?.raw?.content,
116
- temp?.lastCompletion?.content,
117
- ];
118
- for (const c of candidates) {
119
- const think = extractThink(extractText(c));
120
- if (think) return think;
121
- }
122
- return '';
123
- }
124
-
125
- function apply(ctx, config) {
126
- const cmd = ctx
127
- .command(`${config.command} [index:string]`, '��ȡ��һ���ظ��е� <think> ���ݣ���ָ�������� N ����')
128
- .usage('��������Ĭ�϶�ȡ���һ�������� think 2 ��ȡ�����ڶ��� AI �ظ���˼��');
129
-
130
- for (const keyword of config.keywords || []) {
131
- cmd.shortcut(keyword, { prefix: false });
132
- }
133
-
134
- cmd.action(async ({ session, args }, rawIndex) => {
135
- if (!config.allowPrivate && !session.guildId) {
136
- return '��֧����Ⱥ���в�ѯ��';
137
- }
138
-
139
- const service = ctx.chatluna_character;
140
- if (!service) return 'chatluna-character δ���á�';
141
-
142
- const temp = await service.getTemp(session);
143
- const targetIndex = parseIndex(rawIndex ?? args?.[0]);
144
-
145
- // 1) ���ȶ�ȡ����һ��ԭʼ��Ӧ��ͨ���Ժ� <think>����ֻ�Ե� 1 ����Ч
146
- const thinkFromRaw = targetIndex === 1 ? getLatestRawThink(temp) : '';
147
-
148
- // 2) ��ʷ completionMessages �������� <think> �� AI ��Ϣ
149
- const messages = temp?.completionMessages || [];
150
- const thinkFromHistory = thinkFromRaw ? '' : getNthThink(messages, targetIndex);
151
-
152
- // 3) ���˵��� N �� AI ��Ϣ�ٳ��Գ�ȡ
153
- const fallbackMsg = thinkFromRaw || thinkFromHistory ? null : getNthAiMessage(messages, targetIndex);
154
- const think = thinkFromRaw || thinkFromHistory || extractThink(extractText(fallbackMsg?.content));
155
- const formatted = formatThink(think);
156
- if (!formatted) return config.emptyMessage;
157
-
158
- if (config.renderImage && ctx.chatluna?.renderer) {
159
- try {
160
- const title = `### ��һ��˼���������� ${targetIndex} ����`;
161
- const markdown = `<div align="center">\n${title}\n</div>\n\n<div align="left">\n${formatted}\n</div>`;
162
- const rendered = await ctx.chatluna.renderer.render(
163
- {
164
- // �м������С���������룬�������������
165
- content: [{ type: 'text', text: markdown }],
166
- },
167
- { type: 'image', session },
168
- );
169
- if (rendered?.length) return rendered.map((r) => r.element);
170
- } catch (err) {
171
- ctx.logger?.warn?.('[think-viewer] image render failed, fallback text', err);
172
- }
173
- }
174
-
175
- return `��һ��˼����\n${formatted}`;
176
- });
177
- }
178
-
179
- module.exports = {
180
- name,
181
- apply,
182
- Config,
183
- inject,
1
+ const { Schema } = require('koishi');
2
+
3
+ const name = 'chatluna-think-viewer';
4
+
5
+ const inject = {
6
+ chatluna_character: { required: true },
7
+ chatluna: { required: false },
8
+ };
9
+
10
+ const Config = Schema.object({
11
+ command: Schema.string().default('think').description('命令名。'),
12
+ keywords: Schema.array(Schema.string()).default(['查看思考', '上次思考']).description('可无前缀触发的关键词。'),
13
+ allowPrivate: Schema.boolean().default(false).description('是否允许在私聊中使用。'),
14
+ emptyMessage: Schema.string().default('暂时没有可用的思考记录。').description('没有记录时的提示文本。'),
15
+ renderImage: Schema.boolean().default(false).description('是否用 ChatLuna image renderer 将思考渲染为图片,失败时回退文本。'),
16
+ });
17
+
18
+ function extractText(content) {
19
+ if (typeof content === 'string') return content;
20
+ if (Array.isArray(content)) {
21
+ return content
22
+ .map((part) => {
23
+ if (typeof part === 'string') return part;
24
+ if (part && typeof part === 'object') {
25
+ return part.text || part.content || part.value || '';
26
+ }
27
+ return '';
28
+ })
29
+ .join('');
30
+ }
31
+ if (content && typeof content === 'object') {
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;
35
+ }
36
+ return '';
37
+ }
38
+
39
+ function extractThink(text) {
40
+ // 某些模型/中间件会在同一条消息里多次出现 <think>,取最后一次出现的片段
41
+ let last = '';
42
+ const regex = /<think>([\s\S]*?)<\/think>/gi;
43
+ let m;
44
+ while ((m = regex.exec(text)) !== null) {
45
+ last = m[1];
46
+ }
47
+ return last.trim();
48
+ }
49
+
50
+ function formatThink(text) {
51
+ if (!text) return text;
52
+ // 尝试格式化 JSON,失败则做基础去空行/缩进美化
53
+ try {
54
+ const parsed = JSON.parse(text);
55
+ return JSON.stringify(parsed, null, 2);
56
+ } catch {
57
+ // 保留原文,去掉多余空行并统一缩进
58
+ const lines = text.split('\n').map((l) => l.trimEnd());
59
+ const filtered = lines.filter((l, idx, arr) => !(l === '' && arr[idx - 1] === ''));
60
+ const nonEmpty = filtered.filter((l) => l.trim().length > 0);
61
+ const minIndent = nonEmpty.length
62
+ ? Math.min(
63
+ ...nonEmpty.map((l) => {
64
+ const m = l.match(/^(\s*)/);
65
+ return m ? m[1].length : 0;
66
+ }),
67
+ )
68
+ : 0;
69
+ return filtered.map((l) => l.slice(minIndent)).join('\n');
70
+ }
71
+ }
72
+
73
+ function parseIndex(rawIndex) {
74
+ if (!rawIndex) return 1;
75
+ if (typeof rawIndex === 'number' && Number.isFinite(rawIndex) && rawIndex > 0) return Math.floor(rawIndex);
76
+ const match = String(rawIndex).match(/\d+/);
77
+ if (!match) return 1;
78
+ const num = parseInt(match[0], 10);
79
+ return Number.isFinite(num) && num > 0 ? num : 1;
80
+ }
81
+
82
+ function getNthAiMessage(messages, n = 1) {
83
+ if (!Array.isArray(messages) || n < 1) return null;
84
+ let count = 0;
85
+ for (let i = messages.length - 1; i >= 0; i--) {
86
+ const msg = messages[i];
87
+ const type = typeof msg?._getType === 'function' ? msg._getType() : msg?.type || msg?.role;
88
+ if (type === 'ai' || type === 'assistant') {
89
+ count += 1;
90
+ if (count === n) return msg;
91
+ }
92
+ }
93
+ return null;
94
+ }
95
+
96
+ function getNthThink(messages, n = 1) {
97
+ if (!Array.isArray(messages) || n < 1) return null;
98
+ let count = 0;
99
+ for (let i = messages.length - 1; i >= 0; i--) {
100
+ const msg = messages[i];
101
+ const type = typeof msg?._getType === 'function' ? msg._getType() : msg?.type || msg?.role;
102
+ if (type !== 'ai' && type !== 'assistant') continue;
103
+ const think = extractThink(extractText(msg.content));
104
+ if (!think) continue;
105
+ count += 1;
106
+ if (count === n) return think;
107
+ }
108
+ return null;
109
+ }
110
+
111
+ function getLatestRawThink(temp) {
112
+ if (!temp) return '';
113
+ const candidates = [
114
+ temp?.lastCompletion?.raw?.choices?.[0]?.message?.content,
115
+ temp?.lastCompletion?.raw?.content,
116
+ temp?.lastCompletion?.content,
117
+ ];
118
+ for (const c of candidates) {
119
+ const think = extractThink(extractText(c));
120
+ if (think) return think;
121
+ }
122
+ return '';
123
+ }
124
+
125
+ function apply(ctx, config) {
126
+ const cmd = ctx
127
+ .command(`${config.command} [index:string]`, '读取上一条回复里的 <think> 内容,可指定倒数第 N ')
128
+ .usage('不带参数默认读取最新一条;示例:think 2 读取倒数第 2 条 AI 回复的思考');
129
+
130
+ for (const keyword of config.keywords || []) {
131
+ cmd.shortcut(keyword, { prefix: false });
132
+ }
133
+
134
+ cmd.action(async ({ session, args }, rawIndex) => {
135
+ if (!config.allowPrivate && !session.guildId) {
136
+ return '不支持在私聊中查询。';
137
+ }
138
+
139
+ const service = ctx.chatluna_character;
140
+ if (!service) return 'chatluna-character 未加载。';
141
+
142
+ const temp = await service.getTemp(session);
143
+ const targetIndex = parseIndex(rawIndex ?? args?.[0]);
144
+
145
+ // 1) 优先读取最新一次原始响应(通常仍含 <think>),只对第 1 条有效
146
+ const thinkFromRaw = targetIndex === 1 ? getLatestRawThink(temp) : '';
147
+
148
+ // 2) 历史 completionMessages 中真正带 <think> AI 消息
149
+ const messages = temp?.completionMessages || [];
150
+ const thinkFromHistory = thinkFromRaw ? '' : getNthThink(messages, targetIndex);
151
+
152
+ // 3) 回退:第 N AI 消息再尝试抽取
153
+ const fallbackMsg = thinkFromRaw || thinkFromHistory ? null : getNthAiMessage(messages, targetIndex);
154
+ const think = thinkFromRaw || thinkFromHistory || extractThink(extractText(fallbackMsg?.content));
155
+ const formatted = formatThink(think);
156
+ if (!formatted) return config.emptyMessage;
157
+
158
+ if (config.renderImage && ctx.chatluna?.renderer) {
159
+ try {
160
+ const title = `### 上一条思考(倒数第 ${targetIndex} 条)`;
161
+ const markdown = `<div align="center">\n${title}\n</div>\n\n<div align="left">\n${formatted}\n</div>`;
162
+ const rendered = await ctx.chatluna.renderer.render(
163
+ {
164
+ // 居中标题、左对齐正文,保持 renderer 兼容
165
+ content: [{ type: 'text', text: markdown }],
166
+ },
167
+ { type: 'image', session },
168
+ );
169
+ if (rendered?.length) return rendered.map((r) => r.element);
170
+ } catch (err) {
171
+ ctx.logger?.warn?.('[think-viewer] image render failed, fallback text', err);
172
+ }
173
+ }
174
+
175
+ return `上一条思考(倒数第 ${targetIndex} 条)\n${formatted}`;
176
+ });
177
+ }
178
+
179
+ module.exports = {
180
+ name,
181
+ apply,
182
+ Config,
183
+ inject,
184
184
  };
package/package.json CHANGED
@@ -1,40 +1,40 @@
1
- {
2
- "name": "koishi-plugin-chatluna-think-viewer",
3
- "version": "1.0.16",
4
- "main": "index.js",
5
- "description": "Expose a command/shortcut to read the latest <think> block from chatluna-character.",
6
- "license": "MIT",
7
- "keywords": [
8
- "koishi",
9
- "chatluna",
10
- "character",
11
- "think"
12
- ],
13
- "homepage": "https://github.com/sCR0WN-s/koishi-plugin-chatluna-think-viewer",
14
- "repository": {
15
- "type": "git",
16
- "url": "git+https://github.com/sCR0WN-s/koishi-plugin-chatluna-think-viewer.git"
17
- },
18
- "contributors": [
19
- "sCR0WN-s <2892511968@qq.com>"
20
- ],
21
- "peerDependencies": {
22
- "koishi": "^4.18.0",
23
- "koishi-plugin-chatluna-character": "^0.0.180"
24
- },
25
- "koishi": {
26
- "description": {
27
- "zh": "通过命令/关键词查看 chatluna-character 最近一次回复的 <think> 思考内容。",
28
- "en": "Expose a command/shortcut to read the last <think> block from chatluna-character."
29
- },
30
- "service": {
31
- "required": [
32
- "chatluna_character"
33
- ]
34
- }
35
- },
36
- "files": [
37
- "index.js",
38
- "README.md"
39
- ]
1
+ {
2
+ "name": "koishi-plugin-chatluna-think-viewer",
3
+ "version": "1.0.17",
4
+ "main": "index.js",
5
+ "description": "Expose a command/shortcut to read the latest <think> block from chatluna-character.",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "koishi",
9
+ "chatluna",
10
+ "character",
11
+ "think"
12
+ ],
13
+ "homepage": "https://github.com/sCR0WN-s/koishi-plugin-chatluna-think-viewer",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/sCR0WN-s/koishi-plugin-chatluna-think-viewer.git"
17
+ },
18
+ "contributors": [
19
+ "sCR0WN-s <2892511968@qq.com>"
20
+ ],
21
+ "peerDependencies": {
22
+ "koishi": "^4.18.0",
23
+ "koishi-plugin-chatluna-character": "^0.0.180"
24
+ },
25
+ "koishi": {
26
+ "description": {
27
+ "zh": "通过命令/关键词查看 chatluna-character 最近一次回复的 <think> 思考内容。",
28
+ "en": "Expose a command/shortcut to read the last <think> block from chatluna-character."
29
+ },
30
+ "service": {
31
+ "required": [
32
+ "chatluna_character"
33
+ ]
34
+ }
35
+ },
36
+ "files": [
37
+ "index.js",
38
+ "README.md"
39
+ ]
40
40
  }