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.
- package/index.js +183 -165
- package/package.json +3 -3
package/index.js
CHANGED
|
@@ -1,166 +1,184 @@
|
|
|
1
|
-
|
|
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(['
|
|
13
|
-
allowPrivate: Schema.boolean().default(false).description('
|
|
14
|
-
emptyMessage: Schema.string().default('
|
|
15
|
-
renderImage: Schema.boolean().default(false).description('
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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.
|
|
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
|
+
}
|