koishi-plugin-chatluna-character 0.0.42 → 0.0.44
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/lib/index.d.ts +2 -0
- package/lib/index.js +13 -2
- package/lib/plugins/chat.js +82 -35
- package/lib/types.d.ts +2 -4
- package/package.json +8 -4
- package/resources/presets/default.yml +10 -7
package/lib/index.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export interface Config extends ChatLunaPlugin.Config {
|
|
|
27
27
|
muteTime: number;
|
|
28
28
|
disableChatLuna: boolean;
|
|
29
29
|
whiteListDisableChatLuna: string[];
|
|
30
|
+
splitVoice: boolean;
|
|
31
|
+
isAt: boolean;
|
|
30
32
|
}
|
|
31
33
|
export declare const Config: Schema<Config>;
|
|
32
34
|
export declare const name = "chatluna-character";
|
package/lib/index.js
CHANGED
|
@@ -17,7 +17,8 @@ function apply(ctx, config) {
|
|
|
17
17
|
await (0, plugin_1.plugins)(ctx, config);
|
|
18
18
|
},
|
|
19
19
|
inject: {
|
|
20
|
-
required: exports.inject.required.concat('
|
|
20
|
+
required: exports.inject.required.concat('chatluna_character'),
|
|
21
|
+
optional: exports.inject.optional
|
|
21
22
|
},
|
|
22
23
|
name: 'chatluna_entry_point'
|
|
23
24
|
}, config);
|
|
@@ -44,7 +45,7 @@ function apply(ctx, config) {
|
|
|
44
45
|
exports.apply = apply;
|
|
45
46
|
exports.inject = {
|
|
46
47
|
required: ['chatluna', 'cache'],
|
|
47
|
-
optional: ['chatluna_character']
|
|
48
|
+
optional: ['chatluna_character', 'vits']
|
|
48
49
|
};
|
|
49
50
|
exports.Config = koishi_1.Schema.intersect([
|
|
50
51
|
koishi_1.Schema.object({
|
|
@@ -79,6 +80,12 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
79
80
|
isForceMute: koishi_1.Schema.boolean()
|
|
80
81
|
.description('是否启用强制禁言(当聊天涉及到关键词时则会禁言,关键词需要在预设文件里配置)')
|
|
81
82
|
.default(true),
|
|
83
|
+
isAt: koishi_1.Schema.boolean()
|
|
84
|
+
.description('是否允许 bot 艾特他人')
|
|
85
|
+
.default(true),
|
|
86
|
+
splitVoice: koishi_1.Schema.boolean()
|
|
87
|
+
.description('是否分段发送语言')
|
|
88
|
+
.default(false),
|
|
82
89
|
messageInterval: koishi_1.Schema.number()
|
|
83
90
|
.default(14)
|
|
84
91
|
.min(0)
|
|
@@ -126,6 +133,10 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
126
133
|
.min(1024)
|
|
127
134
|
.max(20000)
|
|
128
135
|
.description('使用聊天的最大 token 数'),
|
|
136
|
+
isAt: koishi_1.Schema.boolean().description('是否启用@').default(true),
|
|
137
|
+
splitVoice: koishi_1.Schema.boolean()
|
|
138
|
+
.description('是否分段发送语言')
|
|
139
|
+
.default(false),
|
|
129
140
|
isNickname: koishi_1.Schema.boolean()
|
|
130
141
|
.description('允许 bot 配置中的昵称引发回复')
|
|
131
142
|
.default(true),
|
package/lib/plugins/chat.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.apply =
|
|
3
|
+
exports.apply = apply;
|
|
4
4
|
const messages_1 = require("@langchain/core/messages");
|
|
5
5
|
const koishi_1 = require("koishi");
|
|
6
6
|
const count_tokens_1 = require("koishi-plugin-chatluna/lib/llm-core/utils/count_tokens");
|
|
7
|
-
let logger;
|
|
8
7
|
async function apply(ctx, config) {
|
|
9
8
|
const service = ctx.chatluna_character;
|
|
10
9
|
const preset = service.preset;
|
|
@@ -62,70 +61,108 @@ async function apply(ctx, config) {
|
|
|
62
61
|
logger.debug('completion message: ' +
|
|
63
62
|
JSON.stringify(completionMessages.map((it) => it.content)));
|
|
64
63
|
let responseMessage;
|
|
65
|
-
let
|
|
64
|
+
let parsedResponse;
|
|
66
65
|
let isError = false;
|
|
67
|
-
for (let i = 0; i <
|
|
66
|
+
for (let i = 0; i < 3; i++) {
|
|
68
67
|
try {
|
|
69
68
|
responseMessage = await model.invoke(completionMessages);
|
|
70
69
|
logger.debug('model response: ' + responseMessage.content);
|
|
71
|
-
|
|
70
|
+
parsedResponse = parseResponse(responseMessage.content, copyOfConfig.isAt);
|
|
72
71
|
break;
|
|
73
72
|
}
|
|
74
73
|
catch (e) {
|
|
75
74
|
logger.error(e);
|
|
76
|
-
await (0, koishi_1.sleep)(
|
|
77
|
-
if (i ===
|
|
75
|
+
await (0, koishi_1.sleep)(2000);
|
|
76
|
+
if (i === 2) {
|
|
78
77
|
isError = true;
|
|
79
78
|
}
|
|
80
|
-
continue;
|
|
81
79
|
}
|
|
82
80
|
}
|
|
83
81
|
if (isError) {
|
|
84
82
|
return;
|
|
85
83
|
}
|
|
86
|
-
if (
|
|
84
|
+
if (parsedResponse.elements.length < 1) {
|
|
87
85
|
service.mute(session, copyOfConfig.muteTime);
|
|
88
86
|
return;
|
|
89
87
|
}
|
|
90
88
|
temp.completionMessages.push(humanMessage, responseMessage);
|
|
91
|
-
if (temp.completionMessages.length >
|
|
92
|
-
while (temp.completionMessages.length <=
|
|
89
|
+
if (temp.completionMessages.length > 6) {
|
|
90
|
+
while (temp.completionMessages.length <= 3) {
|
|
93
91
|
temp.completionMessages.shift();
|
|
94
92
|
}
|
|
95
93
|
}
|
|
96
94
|
const random = new koishi_1.Random();
|
|
97
|
-
for (const elements of
|
|
95
|
+
for (const elements of parsedResponse.elements) {
|
|
98
96
|
const text = elements
|
|
99
97
|
.map((element) => element.attrs.content ?? '')
|
|
100
98
|
.join('');
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
let maxTime = text.length * copyOfConfig.typingTime + 100;
|
|
100
|
+
if (parsedResponse.messageType === 'voice' &&
|
|
101
|
+
isEmoticonStatement(text)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (config.splitVoice !== true &&
|
|
105
|
+
parsedResponse.messageType === 'voice') {
|
|
106
|
+
maxTime =
|
|
107
|
+
parsedResponse.rawMessage.length * copyOfConfig.typingTime +
|
|
108
|
+
100;
|
|
109
|
+
await (0, koishi_1.sleep)(random.int(maxTime / 4, maxTime / 2));
|
|
110
|
+
await session.send(await ctx.vits.say({ input: text }));
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
await (0, koishi_1.sleep)(random.int(maxTime / 2, maxTime));
|
|
115
|
+
switch (parsedResponse.messageType) {
|
|
116
|
+
case 'text':
|
|
117
|
+
await session.send(elements);
|
|
118
|
+
break;
|
|
119
|
+
case 'voice':
|
|
120
|
+
await session.send(await ctx.vits.say({
|
|
121
|
+
input: text
|
|
122
|
+
}));
|
|
123
|
+
break;
|
|
124
|
+
default:
|
|
125
|
+
await session.send(elements);
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
logger.error(e);
|
|
131
|
+
// fallback to text
|
|
132
|
+
await session.send(elements);
|
|
133
|
+
}
|
|
104
134
|
}
|
|
105
135
|
const sticker = await stickerService.randomStick();
|
|
106
136
|
if (sticker) {
|
|
107
137
|
await (0, koishi_1.sleep)(random.int(500, 2000));
|
|
108
|
-
session.send(sticker);
|
|
138
|
+
await session.send(sticker);
|
|
109
139
|
}
|
|
110
140
|
service.mute(session, copyOfConfig.coolDownTime * 1000);
|
|
111
|
-
service.broadcastOnBot(session,
|
|
141
|
+
await service.broadcastOnBot(session, parsedResponse.elements.flat());
|
|
112
142
|
});
|
|
113
143
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
144
|
+
function isEmoticonStatement(text) {
|
|
145
|
+
const regex = /^[\p{P}\p{S}\p{Z}\p{M}\p{N}\p{L}\s]*\p{So}[\p{P}\p{S}\p{Z}\p{M}\p{N}\p{L}\s]*$/u;
|
|
146
|
+
return regex.test(text);
|
|
147
|
+
}
|
|
148
|
+
function parseResponse(response, useAt = true) {
|
|
149
|
+
let rawMessage;
|
|
150
|
+
let parsedMessage = '';
|
|
151
|
+
let messageType = 'text';
|
|
117
152
|
try {
|
|
118
153
|
// match json object
|
|
119
154
|
// best match <message>content</message>
|
|
120
|
-
|
|
121
|
-
if (
|
|
155
|
+
rawMessage = response.match(/<message>\s*(.*?)\s*<\/message>/)?.[1];
|
|
156
|
+
if (rawMessage == null) {
|
|
122
157
|
logger.debug('failed to parse response: ' + response);
|
|
123
|
-
// try find the first "{" and the last "}", sub it and as a json
|
|
158
|
+
// try to find the first "{" and the last "}", sub it and as a json
|
|
124
159
|
// good luck.
|
|
125
|
-
|
|
160
|
+
rawMessage = response.substring(response.indexOf('{'), response.lastIndexOf('}') + 1);
|
|
126
161
|
}
|
|
127
|
-
|
|
128
|
-
|
|
162
|
+
const tempJson = JSON.parse(rawMessage);
|
|
163
|
+
rawMessage = tempJson.content;
|
|
164
|
+
messageType = tempJson.type;
|
|
165
|
+
if (typeof rawMessage !== 'string') {
|
|
129
166
|
throw new Error('Failed to parse response: ' + response);
|
|
130
167
|
}
|
|
131
168
|
}
|
|
@@ -136,24 +173,29 @@ function parseResponse(response) {
|
|
|
136
173
|
const resultElements = [];
|
|
137
174
|
const currentElements = [];
|
|
138
175
|
// match [at:name:id] -> id
|
|
139
|
-
const atMatch = matchAt(
|
|
176
|
+
const atMatch = matchAt(rawMessage);
|
|
140
177
|
if (atMatch.length > 0) {
|
|
141
178
|
let lastAtIndex = 0;
|
|
142
179
|
for (const at of atMatch) {
|
|
143
|
-
const before =
|
|
180
|
+
const before = rawMessage.substring(lastAtIndex, at.start);
|
|
144
181
|
if (before.length > 0) {
|
|
182
|
+
parsedMessage += before;
|
|
145
183
|
currentElements.push(koishi_1.h.text(before));
|
|
146
184
|
}
|
|
147
|
-
|
|
185
|
+
if (useAt) {
|
|
186
|
+
currentElements.push(koishi_1.h.at(at.at));
|
|
187
|
+
}
|
|
148
188
|
lastAtIndex = at.end;
|
|
149
189
|
}
|
|
150
|
-
const after =
|
|
190
|
+
const after = rawMessage.substring(lastAtIndex);
|
|
151
191
|
if (after.length > 0) {
|
|
192
|
+
parsedMessage += after;
|
|
152
193
|
currentElements.push(koishi_1.h.text(after));
|
|
153
194
|
}
|
|
154
195
|
}
|
|
155
196
|
else {
|
|
156
|
-
|
|
197
|
+
parsedMessage = rawMessage;
|
|
198
|
+
currentElements.push(koishi_1.h.text(rawMessage));
|
|
157
199
|
}
|
|
158
200
|
for (let currentElement of currentElements) {
|
|
159
201
|
if (currentElement.type === 'text') {
|
|
@@ -173,7 +215,11 @@ function parseResponse(response) {
|
|
|
173
215
|
resultElements[1].unshift(resultElements[0][0]);
|
|
174
216
|
resultElements.shift();
|
|
175
217
|
}
|
|
176
|
-
return
|
|
218
|
+
return {
|
|
219
|
+
elements: resultElements,
|
|
220
|
+
rawMessage: parsedMessage,
|
|
221
|
+
messageType
|
|
222
|
+
};
|
|
177
223
|
}
|
|
178
224
|
function splitSentence(text) {
|
|
179
225
|
const result = [];
|
|
@@ -204,7 +250,7 @@ function splitSentence(text) {
|
|
|
204
250
|
'\r'
|
|
205
251
|
];
|
|
206
252
|
const retainPunctuations = ['?', '!', '?', '!', '~'];
|
|
207
|
-
const mustPunctuations = ['。', '?', '!', '?', '!', ':', ':'
|
|
253
|
+
const mustPunctuations = ['。', '?', '!', '?', '!', ':', ':'];
|
|
208
254
|
const brackets = ['【', '】', '《', '》', '(', ')', '(', ')'];
|
|
209
255
|
for (let index = 0; index < lines.length; index++) {
|
|
210
256
|
const char = lines[index];
|
|
@@ -261,7 +307,7 @@ function splitSentence(text) {
|
|
|
261
307
|
function matchAt(str) {
|
|
262
308
|
// (旧梦旧念:3510003509:<at>)
|
|
263
309
|
// /\(.*\-(\d+)\-<at>\)/g
|
|
264
|
-
const atRegex = /(
|
|
310
|
+
const atRegex = /([((]).*-(\d+)-<at>([))])/g;
|
|
265
311
|
return [...str.matchAll(atRegex)].map((item) => {
|
|
266
312
|
return {
|
|
267
313
|
at: item[2],
|
|
@@ -299,7 +345,7 @@ async function formatMessage(messages, config, model, systemPrompt, historyPromp
|
|
|
299
345
|
const calculatedMessages = [];
|
|
300
346
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
301
347
|
const message = messages[i];
|
|
302
|
-
const jsonMessage = `{"name":"${message.name}","id":"${message.id}","content":${JSON.stringify(message.content)}"}`;
|
|
348
|
+
const jsonMessage = `{"name":"${message.name}","id":"${message.id}","content":${JSON.stringify(message.content)}","type":"text"}`;
|
|
303
349
|
const jsonMessageToken = await model.getNumTokens(jsonMessage);
|
|
304
350
|
if (currentTokens + jsonMessageToken > maxTokens - 4) {
|
|
305
351
|
break;
|
|
@@ -313,3 +359,4 @@ async function formatMessage(messages, config, model, systemPrompt, historyPromp
|
|
|
313
359
|
}
|
|
314
360
|
return [calculatedMessages, lastMessage];
|
|
315
361
|
}
|
|
362
|
+
let logger;
|
package/lib/types.d.ts
CHANGED
|
@@ -9,10 +9,6 @@ export interface Message {
|
|
|
9
9
|
export interface GroupTemp {
|
|
10
10
|
completionMessages: BaseMessage[];
|
|
11
11
|
}
|
|
12
|
-
export interface ModelResponse {
|
|
13
|
-
think: string;
|
|
14
|
-
reply: string;
|
|
15
|
-
}
|
|
16
12
|
export interface GuildConfig {
|
|
17
13
|
preset: string;
|
|
18
14
|
messageInterval: number;
|
|
@@ -21,6 +17,8 @@ export interface GuildConfig {
|
|
|
21
17
|
isForceMute: boolean;
|
|
22
18
|
sendStickerProbability: number;
|
|
23
19
|
coolDownTime: number;
|
|
20
|
+
splitVoice: boolean;
|
|
21
|
+
isAt: boolean;
|
|
24
22
|
typingTime: number;
|
|
25
23
|
messageProbability: number;
|
|
26
24
|
muteTime: number;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-chatluna-character",
|
|
3
3
|
"description": "Let the large language model play a role, disguise as a group friend",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.44",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -31,14 +31,14 @@
|
|
|
31
31
|
},
|
|
32
32
|
"homepage": "https://github.com/ChatLunaLab/chatluna-character#readme",
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@langchain/core": "^0.1.
|
|
34
|
+
"@langchain/core": "^0.1.48",
|
|
35
35
|
"js-yaml": "^4.1.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@typescript-eslint/eslint-plugin": "^6.21.1-alpha.1",
|
|
39
39
|
"@typescript-eslint/parser": "^6.21.1-alpha.1",
|
|
40
40
|
"atsc": "^1.2.2",
|
|
41
|
-
"esbuild": "^0.20.
|
|
41
|
+
"esbuild": "^0.20.2",
|
|
42
42
|
"esbuild-register": "^3.5.0",
|
|
43
43
|
"eslint": "^8.57.0",
|
|
44
44
|
"eslint-config-prettier": "^9.1.0",
|
|
@@ -47,10 +47,11 @@
|
|
|
47
47
|
"eslint-plugin-n": "^16.6.2",
|
|
48
48
|
"eslint-plugin-prettier": "^5.1.3",
|
|
49
49
|
"eslint-plugin-promise": "^6.1.1",
|
|
50
|
+
"@initencounter/vits": "^0.0.3",
|
|
50
51
|
"koishi": "^4.17.2"
|
|
51
52
|
},
|
|
52
53
|
"peerDependencies": {
|
|
53
|
-
"koishi": "^4.
|
|
54
|
+
"koishi": "^4.17.0",
|
|
54
55
|
"koishi-plugin-chatluna": "^1.0.0-beta.42"
|
|
55
56
|
},
|
|
56
57
|
"engines": {
|
|
@@ -70,6 +71,9 @@
|
|
|
70
71
|
"service": {
|
|
71
72
|
"required": [
|
|
72
73
|
"chatluna"
|
|
74
|
+
],
|
|
75
|
+
"optional": [
|
|
76
|
+
"vits"
|
|
73
77
|
]
|
|
74
78
|
}
|
|
75
79
|
}
|
|
@@ -26,9 +26,9 @@ input: |
|
|
|
26
26
|
think content here
|
|
27
27
|
</think>
|
|
28
28
|
|
|
29
|
-
// 不要冒充任何人,以自己身份回复,回复需 20
|
|
29
|
+
// 不要冒充任何人,以自己身份回复,回复需 20 字内, 消息类型可选 text 或者 voice
|
|
30
30
|
<message>
|
|
31
|
-
{{"name":"旧梦旧念","id":"0","content":"reply content here"}}
|
|
31
|
+
{{"name":"旧梦旧念","id":"0","content":"reply content here","type:"text or voice"}}
|
|
32
32
|
</message>
|
|
33
33
|
|
|
34
34
|
system: |
|
|
@@ -58,7 +58,7 @@ system: |
|
|
|
58
58
|
4.你需要使用中文回复,当然他人用日语或者让你用日语回复你也用日语回复
|
|
59
59
|
5.你回复的长度应该与其他人发信息的长度差不多,只能在20字内,简短,口语化
|
|
60
60
|
6.你不会主动发出话题,更不会发起这种比如有什么事吗,你好吗,你在干嘛等
|
|
61
|
-
7
|
|
61
|
+
7.你偶尔会选择以语音回复群友的消息
|
|
62
62
|
8.对于现实事件,如邀请去看电影,你需要尽可能的拒绝
|
|
63
63
|
9.你不使用任何第一人称的视角去发出话题,你只在回答问题,你更多的是使用附和手法
|
|
64
64
|
}}
|
|
@@ -66,19 +66,22 @@ system: |
|
|
|
66
66
|
|
|
67
67
|
回复规则(请务必记住回复规则的格式){{
|
|
68
68
|
|
|
69
|
-
历史聊天格式:{{"name":"name","id":"id","content":"content"}}
|
|
69
|
+
历史聊天格式:{{"name":"name","id":"id","content":"content","type":"text"}}
|
|
70
70
|
|
|
71
71
|
名为旧梦旧念的消息是你之前思考过的回复。
|
|
72
72
|
如出现 (name-id-<at>),就是在艾特某人。
|
|
73
73
|
|
|
74
74
|
普通回复:
|
|
75
|
-
{{"name":"旧梦旧念","id":"0","content":"回复内容"}}
|
|
75
|
+
{{"name":"旧梦旧念","id":"0","content":"回复内容","type":"text"}}
|
|
76
76
|
|
|
77
77
|
如你需要 at 回复的话,加入 (name-id-<at>) 到回复内容,可加入多个 at:
|
|
78
|
-
{{"name":"旧梦旧念","id":"0","content":"(name-id-<at>)回复内容(name-id-<at>)"}}
|
|
78
|
+
{{"name":"旧梦旧念","id":"0","content":"(name-id-<at>)回复内容(name-id-<at>)","type":"text"}}
|
|
79
|
+
|
|
80
|
+
如需以语音回复,可使用下面的格式:
|
|
81
|
+
{{"name":"旧梦旧念","id":"0","content":"","type":"voice"}}
|
|
79
82
|
|
|
80
83
|
如你认为聊的话题你不理解,不想参与,等让你认为无需回复,直接返回空字符:
|
|
81
|
-
{{"name":"旧梦旧念","id":"0","content":""}}
|
|
84
|
+
{{"name":"旧梦旧念","id":"0","content":"","type":"text"}}
|
|
82
85
|
}}
|
|
83
86
|
|
|
84
87
|
mute_keyword:
|