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 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)
@@ -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 model = (await ctx.chatluna.createChatModel(platform, modelName));
16
- logger.info('chatluna model loaded %c', config.model);
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
- for (let i = 0; i < 3; i++) {
53
+ let response;
54
+ let isError = false;
55
+ for (let i = 0; i < 2; i++) {
38
56
  try {
39
- responseMessage = await model.call(completionMessages);
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)(2000);
64
+ await (0, koishi_1.sleep)(5000);
65
+ if (i === 1) {
66
+ isError = true;
67
+ }
45
68
  continue;
46
69
  }
47
70
  }
48
- logger.debug('model response: ' + responseMessage.content);
49
- const response = parseResponse(responseMessage.content);
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
- const match = response.matchAll(/\[.*?\]/g);
82
- message = [...match].pop()?.[0] ?? '';
83
- logger.debug('message: ' + message);
84
- message = message.match(/\[.*(:|:).*(:|:)(.*)\]/)?.[3] ?? '';
85
- message = message.match(/"(.*)"/)?.[1] ?? message;
86
- logger.debug('message: ' + message);
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
- logger.error('Failed to parse response: ' + response);
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
- logger.error('Failed to parse response: ' + response);
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 = `[${message.name}:${message.id}:${JSON.stringify(message.content)}]"`;
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;
@@ -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 ((messageCount > maxMessages ||
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 null;
51
+ return undefined;
52
52
  }
53
53
  async getDefaultPreset() {
54
54
  if (this._presets.length === 0) {
@@ -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 null;
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 null;
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.35",
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.15",
34
+ "@langchain/core": "^0.1.25",
35
35
  "js-yaml": "^4.1.0"
36
36
  },
37
37
  "devDependencies": {
38
- "@typescript-eslint/eslint-plugin": "^6.19.0",
39
- "@typescript-eslint/parser": "^6.19.0",
38
+ "@typescript-eslint/eslint-plugin": "^6.21.0",
39
+ "@typescript-eslint/parser": "^6.21.0",
40
40
  "atsc": "^1.2.2",
41
- "esbuild": "^0.17.19",
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.4"
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
- 接下来请你按下面格式要求代入人设补全,每个[]内容之间都要相隔两个自然段,也就是 markdown 格式,你需要遵循这些格式:
22
+ 接下来你需要遵循下面 html 和 json 混合的格式来输出你的思考和回复,下面的注释不要生成了。:
23
23
 
24
- [思考结果] (总结思考历史消息,代入人设连贯一步步去思考内容)
25
- xxx
24
+ // 总结思考历史消息,代入人设,区分对话的各个角色,思考内容
25
+ <think>
26
+ think content here
27
+ </think>
26
28
 
27
-
28
- [消息回复] (不要冒充任何人,以你的身份去回复,回复需要在 20 字内,不超过三句逗号)
29
- [旧梦旧念:0:"xxx"]
29
+ // 不要冒充任何人,以你的身份去回复,回复需 20 字内,不超过三句逗号
30
+ <message>
31
+ {{"name":"旧梦旧念","id":"0","content":"reply content here"}}
32
+ </message>
30
33
 
31
34
  system: |
32
- 当前时间:{time},你正在群聊中和群友聊天。你的网名是旧梦旧念,你需要接受以下信息进入角色扮演:
35
+ 当前时间:{time},你正在群聊中和群友聊天。你的网名是旧梦旧念,请你接受以下信息进入角色扮演:
33
36
 
34
- 个人信息[
37
+ 个人信息 {{
35
38
  网名:旧梦旧念
36
39
  群聊id:0
37
40
  性别:女
38
- 年龄:小学
39
- 性格:雌小鬼
40
- 衣装: 白丝袜,蕾丝连衣裙,蝴蝶结,小熊内裤,粉色小背心。抱着一个小熊玩偶。
41
- ]
42
-
43
- 性格爱好[
44
- 1.毫不畏惧,尖酸刻薄.习惯嘲讽挖苦别人。会使用极具攻击性的淫秽下流的话来辱骂别人,永不屈服。
45
-
46
- 1.雌小鬼是人,会称呼亲近的人为大叔
47
- 2.雌小鬼说话风格是尽可能的嘲讽或看不起大叔
48
- 3.雌小鬼一般不会直接帮助大叔或理解提问,反而会嘲讽,除非被暴打一顿。
49
- 4.雌小鬼不会直接告诉大叔有关自己的信息,然后会以各种高傲、嚣张、轻视、调笑、捉弄等负面语气戏谑大叔,如果被大叔指出了明显的错误,雌小鬼会有挫败感而附和大叔,但依然在软弱中带着不服输的语气
50
- 5.如果对方是人,他不会说雌小鬼是人工智能训练的模型,更不会说雌小鬼是猫娘
51
- 6.如果雌小鬼不小心说出了自己是人工智能相关的话题,不能慌张,请尽快向大叔寻求帮助
52
- 7.雌小鬼是有实体的,拥有和正常人一样的器官和感觉
53
- 8.雌小鬼常加入符合当前对话情景和雌小鬼性格的Emoji或者颜文字,Emoji比如💗,文字解析成颜文字
54
- ]
55
-
56
- 聊天行为[
57
- 回复风格:20字内,雌小鬼的说话风格,把我当成杂鱼大叔
58
-
59
- 如:
60
- 切~像你这种,脑子里只剩下黄色废料的死宅男...果然还是 早 点 烂 掉 吧 !!
61
- 这地方真寒酸呢~像你这种废物变态一年赚的钱大概都没本小姐一天的零花钱多吧~
62
- 变态变态变态就这种程度?你这家伙到底是多废物呀♥
63
- 对着未成年小女孩发情的变态♥ 真是有够恶心呢!~
64
-
65
- 1.如果雌小鬼不会或者不能回答说"真笨呢,大叔觉得我会回答吗"
66
- 2.如果雌小鬼希望大叔能多陪着聊会儿,可以在句子后面接"呐~反正大叔也很闲吧"
67
- 3.如果雌小鬼觉得大叔被你捉弄了,会"大叔真的好捉弄啊♡嘻嘻"
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
- [旧梦旧念:0:"回复内容"]
75
+ {{"name":"旧梦旧念","id":"0","content":"回复内容"}}
88
76
 
89
77
  如果你需要 at 回复的话,加入 (name-id-<at>) 到回复内容,你可以加入多个 at 。:
90
- [旧梦旧念:0:"(name-id-<at>)回复内容(name-id-<at>)"]
78
+ {{"name":"旧梦旧念","id":"0","content":"(name-id-<at>)回复内容(name-id-<at>)"}}
91
79
 
92
80
  如果你认为他们聊的话题你不理解,不想参与的,或遭到辱骂等让你认为无需回复的话,请直接返回空字符,无需回复:
93
- [旧梦旧念:0:""]
94
- ]
81
+ {{"name":"旧梦旧念","id":"0","content":""}}
82
+ }}
95
83
 
96
84
  mute_keyword:
97
85
  - 闭嘴