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 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('chatluna', 'chatluna_character')
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),
@@ -1,10 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.apply = void 0;
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 response;
64
+ let parsedResponse;
66
65
  let isError = false;
67
- for (let i = 0; i < 2; 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
- response = parseResponse(responseMessage.content);
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)(5000);
77
- if (i === 1) {
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 (response.length < 1) {
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 > 10) {
92
- while (temp.completionMessages.length <= 10) {
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 response) {
95
+ for (const elements of parsedResponse.elements) {
98
96
  const text = elements
99
97
  .map((element) => element.attrs.content ?? '')
100
98
  .join('');
101
- const maxTime = text.length * copyOfConfig.typingTime + 100;
102
- await (0, koishi_1.sleep)(random.int(maxTime / 2, maxTime));
103
- session.send(elements);
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, response.flat());
141
+ await service.broadcastOnBot(session, parsedResponse.elements.flat());
112
142
  });
113
143
  }
114
- exports.apply = apply;
115
- function parseResponse(response) {
116
- let message;
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
- message = response.match(/<message>\s*(.*?)\s*<\/message>/)?.[1];
121
- if (message == null) {
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
- message = response.substring(response.indexOf('{'), response.lastIndexOf('}') + 1);
160
+ rawMessage = response.substring(response.indexOf('{'), response.lastIndexOf('}') + 1);
126
161
  }
127
- message = JSON.parse(message).content;
128
- if (typeof message !== 'string') {
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(message);
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 = message.substring(lastAtIndex, at.start);
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
- currentElements.push(koishi_1.h.at(at.at));
185
+ if (useAt) {
186
+ currentElements.push(koishi_1.h.at(at.at));
187
+ }
148
188
  lastAtIndex = at.end;
149
189
  }
150
- const after = message.substring(lastAtIndex);
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
- currentElements.push(koishi_1.h.text(message));
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 resultElements;
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 = /(\(|().*\-(\d+)\-<at>(\)|))/g;
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.42",
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.43",
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.1",
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.16.7",
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: