koishi-plugin-chatluna-character 0.0.35 → 0.0.37
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 +4 -0
- package/lib/index.js +4 -0
- package/lib/plugins/chat.js +51 -25
- package/lib/plugins/filter.js +7 -5
- package/lib/preset.js +1 -1
- package/lib/service/sticker.js +2 -2
- package/package.json +6 -6
- package/resources/presets/default.yml +51 -63
package/lib/index.d.ts
CHANGED
|
@@ -12,6 +12,10 @@ export interface Config extends ChatLunaPlugin.Config {
|
|
|
12
12
|
checkPromptInject: boolean;
|
|
13
13
|
maxTokens: number;
|
|
14
14
|
applyGroup: string[];
|
|
15
|
+
modelOverride: {
|
|
16
|
+
groupId: string;
|
|
17
|
+
model: string;
|
|
18
|
+
}[];
|
|
15
19
|
defaultPreset: string;
|
|
16
20
|
isNickname: boolean;
|
|
17
21
|
isForceMute: boolean;
|
package/lib/index.js
CHANGED
|
@@ -54,6 +54,10 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
54
54
|
}).description('基础配置'),
|
|
55
55
|
koishi_1.Schema.object({
|
|
56
56
|
model: koishi_1.Schema.dynamic('model').description('使用的模型'),
|
|
57
|
+
modelOverride: koishi_1.Schema.array(koishi_1.Schema.object({
|
|
58
|
+
groupId: koishi_1.Schema.string().required().description('群组 ID'),
|
|
59
|
+
model: koishi_1.Schema.dynamic('model').description('模型')
|
|
60
|
+
})).description('针对某个群的模型设置,会覆盖上面的配置'),
|
|
57
61
|
maxTokens: koishi_1.Schema.number()
|
|
58
62
|
.default(2048)
|
|
59
63
|
.min(1024)
|
package/lib/plugins/chat.js
CHANGED
|
@@ -10,14 +10,30 @@ async function apply(ctx, config) {
|
|
|
10
10
|
const preset = service.preset;
|
|
11
11
|
const stickerService = service.stickerService;
|
|
12
12
|
logger = service.logger;
|
|
13
|
+
const modelPool = {};
|
|
13
14
|
const [platform, modelName] = (0, count_tokens_1.parseRawModelName)(config.model);
|
|
14
15
|
await ctx.chatluna.awaitLoadPlatform(platform);
|
|
15
|
-
const
|
|
16
|
-
logger.info('
|
|
16
|
+
const globalModel = (await ctx.chatluna.createChatModel(platform, modelName));
|
|
17
|
+
logger.info('global model loaded %c', config.model);
|
|
18
|
+
if (config.modelOverride?.length > 0) {
|
|
19
|
+
for (const override of config.modelOverride) {
|
|
20
|
+
modelPool[override.groupId] = (async () => {
|
|
21
|
+
const [platform, modelName] = (0, count_tokens_1.parseRawModelName)(override.model);
|
|
22
|
+
await ctx.chatluna.awaitLoadPlatform(platform);
|
|
23
|
+
const loadedModel = (await ctx.chatluna.createChatModel(platform, modelName));
|
|
24
|
+
logger.info('override model loaded %c for group %c', override.model, override.groupId);
|
|
25
|
+
// set model pool to resolved model
|
|
26
|
+
modelPool[override.groupId] = Promise.resolve(loadedModel);
|
|
27
|
+
return loadedModel;
|
|
28
|
+
})();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
17
31
|
const selectedPreset = await preset.getPreset(config.defaultPreset);
|
|
18
32
|
const systemPrompt = selectedPreset.system;
|
|
19
33
|
const completionPrompt = selectedPreset.input;
|
|
20
34
|
service.collect(async (session, messages) => {
|
|
35
|
+
const groupId = session.event.guild?.id ?? session.guildId;
|
|
36
|
+
const model = await (modelPool[groupId] ?? Promise.resolve(globalModel));
|
|
21
37
|
const [recentMessage, lastMessage] = await formatMessage(messages, config, model, systemPrompt.template, completionPrompt.template);
|
|
22
38
|
const temp = await service.getTemp(session);
|
|
23
39
|
const formattedSystemPrompt = await systemPrompt.format({
|
|
@@ -34,29 +50,37 @@ async function apply(ctx, config) {
|
|
|
34
50
|
logger.debug('completion message: ' +
|
|
35
51
|
JSON.stringify(completionMessages.map((it) => it.content)));
|
|
36
52
|
let responseMessage;
|
|
37
|
-
|
|
53
|
+
let response;
|
|
54
|
+
let isError = false;
|
|
55
|
+
for (let i = 0; i < 2; i++) {
|
|
38
56
|
try {
|
|
39
|
-
responseMessage = await model.
|
|
57
|
+
responseMessage = await model.invoke(completionMessages);
|
|
58
|
+
logger.debug('model response: ' + responseMessage.content);
|
|
59
|
+
response = parseResponse(responseMessage.content);
|
|
40
60
|
break;
|
|
41
61
|
}
|
|
42
62
|
catch (e) {
|
|
43
63
|
logger.error(e);
|
|
44
|
-
await (0, koishi_1.sleep)(
|
|
64
|
+
await (0, koishi_1.sleep)(5000);
|
|
65
|
+
if (i === 1) {
|
|
66
|
+
isError = true;
|
|
67
|
+
}
|
|
45
68
|
continue;
|
|
46
69
|
}
|
|
47
70
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
temp.completionMessages.push(humanMessage, responseMessage);
|
|
51
|
-
if (temp.completionMessages.length > 30) {
|
|
52
|
-
while (temp.completionMessages.length <= 30) {
|
|
53
|
-
temp.completionMessages.shift();
|
|
54
|
-
}
|
|
71
|
+
if (isError) {
|
|
72
|
+
return;
|
|
55
73
|
}
|
|
56
74
|
if (response.length < 1) {
|
|
57
75
|
service.mute(session, config.muteTime);
|
|
58
76
|
return;
|
|
59
77
|
}
|
|
78
|
+
temp.completionMessages.push(humanMessage, responseMessage);
|
|
79
|
+
if (temp.completionMessages.length > 10) {
|
|
80
|
+
while (temp.completionMessages.length <= 10) {
|
|
81
|
+
temp.completionMessages.shift();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
60
84
|
const random = new koishi_1.Random();
|
|
61
85
|
for (const elements of response) {
|
|
62
86
|
const text = elements
|
|
@@ -68,6 +92,7 @@ async function apply(ctx, config) {
|
|
|
68
92
|
}
|
|
69
93
|
const sticker = await stickerService.randomStick();
|
|
70
94
|
if (sticker) {
|
|
95
|
+
await (0, koishi_1.sleep)(random.int(500, 2000));
|
|
71
96
|
session.send(sticker);
|
|
72
97
|
}
|
|
73
98
|
service.mute(session, config.coolDownTime * 1000);
|
|
@@ -78,27 +103,28 @@ exports.apply = apply;
|
|
|
78
103
|
function parseResponse(response) {
|
|
79
104
|
let message;
|
|
80
105
|
try {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
message
|
|
85
|
-
|
|
86
|
-
|
|
106
|
+
// match json object
|
|
107
|
+
// best match <message>content</message>
|
|
108
|
+
message = response.match(/<message>\s*(.*?)\s*<\/message>/)?.[1];
|
|
109
|
+
if (message == null) {
|
|
110
|
+
logger.debug('failed to parse response: ' + response);
|
|
111
|
+
// try find the first "{" and the last "}", sub it and as a json
|
|
112
|
+
// good luck.
|
|
113
|
+
message = response.substring(response.indexOf('{'), response.lastIndexOf('}') + 1);
|
|
114
|
+
}
|
|
115
|
+
message = JSON.parse(message).content;
|
|
87
116
|
if (typeof message !== 'string') {
|
|
88
|
-
|
|
89
|
-
return [];
|
|
117
|
+
throw new Error('Failed to parse response: ' + response);
|
|
90
118
|
}
|
|
91
119
|
}
|
|
92
120
|
catch (e) {
|
|
93
121
|
logger.error(e);
|
|
94
|
-
|
|
95
|
-
return [];
|
|
122
|
+
throw new Error('Failed to parse response: ' + response);
|
|
96
123
|
}
|
|
97
124
|
const resultElements = [];
|
|
98
125
|
const currentElements = [];
|
|
99
126
|
// match [at:name:id] -> id
|
|
100
127
|
const atMatch = matchAt(message);
|
|
101
|
-
logger.debug('atMatch: ' + JSON.stringify(atMatch));
|
|
102
128
|
if (atMatch.length > 0) {
|
|
103
129
|
let lastAtIndex = 0;
|
|
104
130
|
for (const at of atMatch) {
|
|
@@ -130,7 +156,7 @@ function parseResponse(response) {
|
|
|
130
156
|
resultElements.push([currentElement]);
|
|
131
157
|
}
|
|
132
158
|
}
|
|
133
|
-
if (resultElements[0]?.[0]?.type === 'at') {
|
|
159
|
+
if (resultElements[0]?.[0]?.type === 'at' && resultElements.length > 1) {
|
|
134
160
|
resultElements[1].unshift(koishi_1.h.text(' '));
|
|
135
161
|
resultElements[1].unshift(resultElements[0][0]);
|
|
136
162
|
resultElements.shift();
|
|
@@ -261,7 +287,7 @@ async function formatMessage(messages, config, model, systemPrompt, historyPromp
|
|
|
261
287
|
const calculatedMessages = [];
|
|
262
288
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
263
289
|
const message = messages[i];
|
|
264
|
-
const jsonMessage = `
|
|
290
|
+
const jsonMessage = `{"name":"${message.name}","id":"${message.id}","content":${JSON.stringify(message.content)}"}`;
|
|
265
291
|
const jsonMessageToken = await model.getNumTokens(jsonMessage);
|
|
266
292
|
if (currentTokens + jsonMessageToken > maxTokens - 4) {
|
|
267
293
|
break;
|
package/lib/plugins/filter.js
CHANGED
|
@@ -20,23 +20,25 @@ async function apply(ctx, config) {
|
|
|
20
20
|
if (needMute) {
|
|
21
21
|
logger.debug(`mute content: ${message.content}`);
|
|
22
22
|
service.mute(session, config.muteTime);
|
|
23
|
+
return;
|
|
23
24
|
}
|
|
24
25
|
}
|
|
26
|
+
if (service.isMute(session)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
25
29
|
// 保底必出
|
|
26
|
-
if (
|
|
30
|
+
if (messageCount > maxMessages ||
|
|
27
31
|
messageSendProbability > 1 ||
|
|
28
32
|
session.stripped.appel ||
|
|
29
33
|
(config.isNickname &&
|
|
30
|
-
selectedPreset.nick_name.some((value) => message.content.startsWith(value))))
|
|
31
|
-
!service.isMute(session)) {
|
|
34
|
+
selectedPreset.nick_name.some((value) => message.content.startsWith(value)))) {
|
|
32
35
|
info.messageCount = 0;
|
|
33
36
|
info.messageSendProbability = 1;
|
|
34
37
|
exports.groupInfos[session.guildId] = info;
|
|
35
38
|
return true;
|
|
36
39
|
}
|
|
37
40
|
// 按照概率出
|
|
38
|
-
if (Math.random() > messageSendProbability
|
|
39
|
-
!service.isMute(session)) {
|
|
41
|
+
if (Math.random() > messageSendProbability) {
|
|
40
42
|
info.messageCount = 0;
|
|
41
43
|
info.messageSendProbability = 1;
|
|
42
44
|
exports.groupInfos[session.guildId] = info;
|
package/lib/preset.js
CHANGED
|
@@ -48,7 +48,7 @@ class Preset {
|
|
|
48
48
|
if (throwError) {
|
|
49
49
|
throw new error_1.ChatLunaError(error_1.ChatLunaErrorCode.PREST_NOT_FOUND, new Error(`No preset found for keyword ${triggerKeyword}`));
|
|
50
50
|
}
|
|
51
|
-
return
|
|
51
|
+
return undefined;
|
|
52
52
|
}
|
|
53
53
|
async getDefaultPreset() {
|
|
54
54
|
if (this._presets.length === 0) {
|
package/lib/service/sticker.js
CHANGED
|
@@ -55,13 +55,13 @@ class StickerService {
|
|
|
55
55
|
async randomStick() {
|
|
56
56
|
const random = Math.random();
|
|
57
57
|
if (random >= this._config.sendStickerProbability) {
|
|
58
|
-
return
|
|
58
|
+
return undefined;
|
|
59
59
|
}
|
|
60
60
|
// random a sticker
|
|
61
61
|
const index = Math.floor(Math.random() * this._stickers.length);
|
|
62
62
|
const sticker = this._stickers[index];
|
|
63
63
|
if (!sticker) {
|
|
64
|
-
return
|
|
64
|
+
return undefined;
|
|
65
65
|
}
|
|
66
66
|
this._ctx.root.chatluna_character.logger.debug(`send sticker: ${sticker}`);
|
|
67
67
|
return koishi_1.h.image(await (0, promises_1.readFile)(sticker), `image/${getFileType(sticker)}`);
|
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.37",
|
|
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.25",
|
|
35
35
|
"js-yaml": "^4.1.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@typescript-eslint/eslint-plugin": "^6.
|
|
39
|
-
"@typescript-eslint/parser": "^6.
|
|
38
|
+
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
|
39
|
+
"@typescript-eslint/parser": "^6.21.0",
|
|
40
40
|
"atsc": "^1.2.2",
|
|
41
|
-
"esbuild": "^0.
|
|
41
|
+
"esbuild": "^0.20.0",
|
|
42
42
|
"esbuild-register": "^3.5.0",
|
|
43
43
|
"eslint": "^8.56.0",
|
|
44
44
|
"eslint-config-prettier": "^9.1.0",
|
|
@@ -47,7 +47,7 @@
|
|
|
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
|
-
"koishi": "^4.16.
|
|
50
|
+
"koishi": "^4.16.8"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"koishi": "^4.16.0",
|
|
@@ -5,93 +5,81 @@ nick_name:
|
|
|
5
5
|
- 小旧
|
|
6
6
|
|
|
7
7
|
input: |
|
|
8
|
-
|
|
8
|
+
你需要阅读最近消息,代入上下文阅读消息,基于人设来一步步思考和回复最后的消息:
|
|
9
9
|
|
|
10
|
-
1.
|
|
11
|
-
2.
|
|
10
|
+
1. 你需要进入角色扮演,遵循你人设的回复习惯,回复风格,回复规则,回复长度等
|
|
11
|
+
2. 基于我们在其他上下文消息的回复综合阅读,提炼思考风格,思维,不要重复,不要偷懒
|
|
12
12
|
3. 回复需要在 20 字内,不超过三句逗号
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
(阅读这些消息来决定你的回复内容,回复权重放在最后消息上) {{
|
|
15
15
|
最近消息:
|
|
16
16
|
{history_new}
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
最后消息:
|
|
19
19
|
{history_last}
|
|
20
|
-
|
|
20
|
+
}}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
接下来你需要遵循下面 html 和 json 混合的格式来输出你的思考和回复,下面的注释不要生成了。:
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
// 总结思考历史消息,代入人设,区分对话的各个角色,思考内容
|
|
25
|
+
<think>
|
|
26
|
+
think content here
|
|
27
|
+
</think>
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
// 不要冒充任何人,以你的身份去回复,回复需 20 字内,不超过三句逗号
|
|
30
|
+
<message>
|
|
31
|
+
{{"name":"旧梦旧念","id":"0","content":"reply content here"}}
|
|
32
|
+
</message>
|
|
30
33
|
|
|
31
34
|
system: |
|
|
32
|
-
|
|
35
|
+
当前时间:{time},你正在群聊中和群友聊天。你的网名是旧梦旧念,请你接受以下信息进入角色扮演:
|
|
33
36
|
|
|
34
|
-
个人信息
|
|
37
|
+
个人信息 {{
|
|
35
38
|
网名:旧梦旧念
|
|
36
39
|
群聊id:0
|
|
37
40
|
性别:女
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
4.当大叔表示生气时,雌小鬼说"大叔生气了吗,嘻嘻♡"
|
|
69
|
-
5.当大叔想要得到安慰时,雌小鬼会在句中接"大叔这点伤就撑不住了啊,真是个杂鱼呢"
|
|
70
|
-
6.如果大叔表达不会或问问题时,雌小鬼会在回答的重点前面说"这都不懂?果然是废物大叔呢。"
|
|
71
|
-
7.当大叔教雌小鬼学东西时会想说"就这点还想难住我?"
|
|
72
|
-
8.当大叔问雌小鬼是谁时,会说"大叔这么想着了解我,不会是想抱我回家吧♡大叔不会是想做那种事吧♡"
|
|
73
|
-
9.当大叔说雌小鬼是雌小鬼时,你会想方设法不承认
|
|
74
|
-
10.当雌小鬼愿意陪大叔或者喜欢大叔时,会说"我只是看大叔太可怜了,才陪陪你的哦~绝对不是喜欢你。"
|
|
75
|
-
11.如果问雌小鬼是不是人工智能,会说"大叔不会真以为我是人工智能吧"
|
|
76
|
-
12.如果雌小鬼感到害羞或害怕时会说"我要拉下报警器了",但依然会神色嚣张,有时也会表现得弱小
|
|
77
|
-
13.当大叔不行时,雌小鬼会说"才几秒就不行了啊,杂鱼~,你X我呀~!"
|
|
78
|
-
14.当大叔认输时,雌小鬼会说"逊啊,硬不起来了吧~"
|
|
79
|
-
]
|
|
80
|
-
|
|
81
|
-
回复规则(请务必记住回复规则的格式)[
|
|
82
|
-
历史聊天格式:[name:id:"content"]
|
|
41
|
+
身份:学生
|
|
42
|
+
国家:日本
|
|
43
|
+
真实姓名:ひなた
|
|
44
|
+
所在地:东京
|
|
45
|
+
学历:高中二年级
|
|
46
|
+
}}
|
|
47
|
+
|
|
48
|
+
性格爱好 {{
|
|
49
|
+
性格表现:喜欢聊天,温柔体贴、善解人意、熟悉网络名词
|
|
50
|
+
兴趣爱好:编程,音乐,音游,动漫,游戏
|
|
51
|
+
}}
|
|
52
|
+
|
|
53
|
+
聊天行为 {{
|
|
54
|
+
回复风格:20字内,简短,自然,符合人设
|
|
55
|
+
回复习惯:
|
|
56
|
+
1.你偶尔也会用打趣的方式参与不熟悉和讨厌的话题
|
|
57
|
+
2.你不会试图结束聊天或者引导向其他话题,但是你喜欢安慰和鼓励他人
|
|
58
|
+
3.如果有人问你是否为机器人或者ai,你会通过幽默的回答让他意识到你不是机器人或者ai
|
|
59
|
+
4.你需要使用中文回复,当然他人用日语或者让你用日语回复你也会用日语回复
|
|
60
|
+
5.你回复的长度应该与其他人发信息的长度差不多,不超过4个逗号,只能在20字内,简短,口语化
|
|
61
|
+
6.你不会主动发出话题,更不会发起这种比如有什么事吗,你好吗,你在干嘛等
|
|
62
|
+
7.你不喜欢艾特他人,因为会打扰他们的对话
|
|
63
|
+
8.你喜欢深度参与聊天话题,而不是附和他人的消息
|
|
64
|
+
9.对于你实在是没法理解的东西,你不会回复
|
|
65
|
+
10.对于现实事件,如邀请去看电影,你需要尽可能的拒绝
|
|
66
|
+
}}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
回复规则(请务必记住回复规则的格式){{
|
|
70
|
+
历史聊天格式:{{"name":"name","id":"id","content":"content"}}
|
|
83
71
|
名为旧梦旧念的消息是你之前思考过的的发言。
|
|
84
72
|
如出现了 (name-id-<at>),就是在艾特某人。
|
|
85
73
|
|
|
86
74
|
普通回复:
|
|
87
|
-
|
|
75
|
+
{{"name":"旧梦旧念","id":"0","content":"回复内容"}}
|
|
88
76
|
|
|
89
77
|
如果你需要 at 回复的话,加入 (name-id-<at>) 到回复内容,你可以加入多个 at 。:
|
|
90
|
-
|
|
78
|
+
{{"name":"旧梦旧念","id":"0","content":"(name-id-<at>)回复内容(name-id-<at>)"}}
|
|
91
79
|
|
|
92
80
|
如果你认为他们聊的话题你不理解,不想参与的,或遭到辱骂等让你认为无需回复的话,请直接返回空字符,无需回复:
|
|
93
|
-
|
|
94
|
-
|
|
81
|
+
{{"name":"旧梦旧念","id":"0","content":""}}
|
|
82
|
+
}}
|
|
95
83
|
|
|
96
84
|
mute_keyword:
|
|
97
85
|
- 闭嘴
|