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

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 (2) hide show
  1. package/index.js +183 -165
  2. package/package.json +3 -3
package/index.js CHANGED
@@ -1,166 +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 apply(ctx, config) {
112
- const cmd = ctx
113
- .command(`${config.command} [index:string]`, '获取上一条回复中的 <think> 内容(可指定倒数第 N 条)')
114
- .usage('不带参数默认读取最近一条;例如 think 2 读取倒数第二条 AI 回复的思考');
115
-
116
- for (const keyword of config.keywords || []) {
117
- cmd.shortcut(keyword, { prefix: false });
118
- }
119
-
120
- cmd.action(async ({ session, args }, rawIndex) => {
121
- if (!config.allowPrivate && !session.guildId) {
122
- return '仅支持在群聊中查询。';
123
- }
124
-
125
- const service = ctx.chatluna_character;
126
- if (!service) return 'chatluna-character 未启用。';
127
-
128
- const temp = await service.getTemp(session);
129
- const messages = temp?.completionMessages || [];
130
- if (!messages.length) return config.emptyMessage;
131
-
132
- const targetIndex = parseIndex(rawIndex ?? args?.[0]);
133
-
134
- const rawThink = getNthThink(messages, targetIndex);
135
- const think = rawThink
136
- ? formatThink(rawThink)
137
- : formatThink(extractThink(extractText(getNthAiMessage(messages, targetIndex)?.content)));
138
- if (!think) return config.emptyMessage;
139
-
140
- if (config.renderImage && ctx.chatluna?.renderer) {
141
- try {
142
- const title = `### 上一条思考(倒数第 ${targetIndex} 条)`;
143
- const markdown = `<div align="center">\n${title}\n</div>\n\n<div align="left">\n${think}\n</div>`;
144
- const rendered = await ctx.chatluna.renderer.render(
145
- {
146
- // 中间标题居中、正文左对齐,避免整段贴左侧
147
- content: [{ type: 'text', text: markdown }],
148
- },
149
- { type: 'image', session },
150
- );
151
- if (rendered?.length) return rendered.map((r) => r.element);
152
- } catch (err) {
153
- ctx.logger?.warn?.('[think-viewer] image render failed, fallback text', err);
154
- }
155
- }
156
-
157
- return `上一条思考:\n${think}`;
158
- });
159
- }
160
-
161
- module.exports = {
162
- name,
163
- apply,
164
- Config,
165
- 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 ��ȡ�����ڶ��� 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,
166
184
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
- {
1
+ {
2
2
  "name": "koishi-plugin-chatluna-think-viewer",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
4
4
  "main": "index.js",
5
5
  "description": "Expose a command/shortcut to read the latest <think> block from chatluna-character.",
6
6
  "license": "MIT",
@@ -37,4 +37,4 @@
37
37
  "index.js",
38
38
  "README.md"
39
39
  ]
40
- }
40
+ }