alemonjs-aichat 1.0.13 → 1.0.14

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.
@@ -0,0 +1,110 @@
1
+ import { availableTools } from '../../api.js';
2
+ import { getConfigValue, useMessage, Text, ImageURL, Image } from 'alemonjs';
3
+ import { Regular } from 'alemonjs/utils';
4
+ import OpenAi from 'openai';
5
+ import { getAIConfig } from '../../config.js';
6
+
7
+ const selects = onSelects(["message.create", "private.message.create"]);
8
+ const regular$1 = /(\/|#)(ai)?(pt|p图|ps|修图)(.*)$/i;
9
+ const regular$2 = /(\/|#)(ai)?(画图)(.*)$/i;
10
+ const regular = Regular.or(regular$1, regular$2);
11
+ var res = onResponse(selects, async (e) => {
12
+ getConfigValue();
13
+ // 创建
14
+ const [message] = useMessage(e);
15
+ if (regular$1.test(e.msg)) {
16
+ const match = e.msg.match(regular$1);
17
+ const [, , , , content] = match;
18
+ if (!content.trim()) {
19
+ message.send(format(Text("请提供修图内容,例如:\n#修图 换个背景")));
20
+ return;
21
+ }
22
+ const imgs = [];
23
+ if (e.reply) {
24
+ const replyImages = e.reply.message
25
+ .filter((item) => item.type === "image")
26
+ .map((item) => item.data.url);
27
+ imgs.push(...replyImages);
28
+ }
29
+ // 处理图片
30
+ if (e.img && e.img.length > 0) {
31
+ console.log("有图片");
32
+ for (const imgUrl of e.img) {
33
+ imgs.push(imgUrl);
34
+ }
35
+ }
36
+ console.log("imgs", imgs);
37
+ message.send(format(Text("开始修图喵~")));
38
+ const res = await availableTools.AIPS({
39
+ image_url: imgs,
40
+ instruction: content,
41
+ });
42
+ if (res && typeof res === "object" && res.length > 0) {
43
+ message.send(format(Text("修图完成喵~"), ImageURL(res[0].url)));
44
+ }
45
+ else {
46
+ try {
47
+ const json = JSON.parse(res);
48
+ console.log("res", json);
49
+ message.send(format(Text(json.error)));
50
+ }
51
+ catch (error) {
52
+ message.send(format(Text("图片过于色情被和谐了喵~")));
53
+ }
54
+ }
55
+ }
56
+ if (regular$2.test(e.msg)) {
57
+ const match = e.msg.match(regular$2);
58
+ const [, , , , content] = match;
59
+ if (!content.trim()) {
60
+ message.send(format(Text("请提供画图内容,例如:\n#画图 画一只猫")));
61
+ return;
62
+ }
63
+ message.send(format(Text("开始画图喵~")));
64
+ const aiConfig = await getAIConfig(e.guid);
65
+ if (!aiConfig) {
66
+ const res = await availableTools.StableDiffusionGenerateImage({
67
+ prompt: content,
68
+ base64: true,
69
+ });
70
+ if (res && typeof res === "object" && res.length > 0) {
71
+ message.send(format(Text("画图完成喵~"), Image(res[0])));
72
+ }
73
+ return;
74
+ }
75
+ const openai = new OpenAi({
76
+ baseURL: aiConfig.host,
77
+ apiKey: aiConfig.key,
78
+ timeout: 300000,
79
+ });
80
+ try {
81
+ const response = await openai.chat.completions.create({
82
+ model: aiConfig.model,
83
+ messages: [
84
+ {
85
+ role: "system",
86
+ content: "你是一个sd画图助手,协助用户将文字描述转换为提示词。描述中会出现一些动漫或游戏角色的名字, 请使用他们的英文名作为提示词, 请直接输出翻译后的提示词",
87
+ },
88
+ { role: "user", content: content },
89
+ ],
90
+ });
91
+ const prompt = response.choices[0].message?.content?.trim();
92
+ console.log("prompt", prompt);
93
+ if (prompt) {
94
+ const res = await availableTools.StableDiffusionGenerateImage({
95
+ prompt,
96
+ base64: true,
97
+ });
98
+ if (res && typeof res === "object" && res.length > 0) {
99
+ message.send(format(Text("画图完成喵~"), Image(res[0])));
100
+ }
101
+ }
102
+ }
103
+ catch (error) {
104
+ console.error("Error generating image:", error);
105
+ message.send(format(Text("画图失败了喵~")));
106
+ }
107
+ }
108
+ });
109
+
110
+ export { res as default, regular, selects };
@@ -1,13 +1,14 @@
1
1
  import { API } from '@alemonjs/onebot';
2
2
  import { getTimeString, availableTools, TTSClient } from '../../api.js';
3
- import { getAIConfig, getAIChatHistory, getAffectionLevelAll, tools, addAIChatHistory, incrementAffectionLevel } from '../../config.js';
3
+ import { getAIConfig, getAIChatHistory, getAffectionLevelAll, getAffectionSwitch, addAIChatHistory, tools, incrementAffectionLevel, getTTSResponseSwitch } from '../../config.js';
4
4
  import { emojiMap } from '../../emoji.js';
5
5
  import { redis } from '../../redis.js';
6
6
  import { uploadImageToR2 } from '../../s3.js';
7
- import { useMessage, Text, ImageFile, useClient } from 'alemonjs';
7
+ import { getConfigValue, useMessage, Text, Mention, ImageURL, ImageFile, useClient } from 'alemonjs';
8
8
  import { log } from 'console';
9
9
  import OpenAi from 'openai';
10
10
 
11
+ const value = getConfigValue();
11
12
  const selects = onSelects(["message.create", "private.message.create"]);
12
13
  const regular = /.*/;
13
14
  let ttsClient;
@@ -21,6 +22,7 @@ var res = onResponse(selects, async (e, next) => {
21
22
  next();
22
23
  return;
23
24
  }
25
+ const complexOutputIsOpen = (await redis.get(`chat:complex:response:${e.guid}`)) || "1"; // 复杂输出开关
24
26
  // 创建消息
25
27
  const [message] = useMessage(e);
26
28
  const config = await getAIConfig(e.guid);
@@ -29,27 +31,57 @@ var res = onResponse(selects, async (e, next) => {
29
31
  const imgs = [];
30
32
  let rawMessage = e.value.raw_message || e.raw_message || e.msg;
31
33
  // 好感度开关
32
- const affectionIsOpen = (await redis.get(`ai:affection:switch:${e.guid}`)) || "1";
34
+ const affectionIsOpen = await getAffectionSwitch(e.guid);
33
35
  const botName = e.bot.nickname || "小咸鱼";
34
- const systemPrompt = `你正在一个群聊中跟用户聊天,你的聊天昵称是'${botName}'
35
- 关于回复格式:
36
- 用户的发言方式为: [{'用户昵称(用户id)(发送时间)':回复内容}]
37
- 当聊天场景为私信时, 用户昵称前方会有[私信]前缀, 例如: [{'[私信]用户昵称(用户id)(发送时间)':回复内容}]
38
- 你的回复方式为JSON: [{assistant:回复内容}],你可以在数组中加入多条消息来模拟更真实的聊天
39
- 当然你觉得当前聊的话题确定与你有关是才回复,否则你就回复空数组[]来表示不回复.请尽可能降低回复频率.
40
- 关于图片发送:
41
- 如果你在使用画图工具后需要回复图片, 可在消息中追加image_url, 例如:
42
- [{'assistant':'画好啦','image_url':'图片地址'}]
43
- 注意:图片地址必须是接口返回的完整地址, 通常是相对路径
44
- 注意:你的回复中除了json必要的双引号之外, 不要再添加多余的双引号, 否则会导致解析失败, 需要在回复内容中使用单引号来表示字符串, 例如: [{"assistant":"这是一个'引号'的例子"}]
45
- ` +
46
- (affectionIsOpen === "1"
47
- ? `当你认为对方在示好或骂你时,可添加{affections:数字}表示好感度变化,你可以根据好感度来适当改变回复语气.
48
- 示例: [{'assistant':'你人真好[好感+2]','affections':2}], 或者 [{'assistant':'我才不理你这个坏人呢[好感-1]','affections':-1}]
49
- 当对方希望和好时提出道歉条件,可以给对方一个恢复的机会.
50
- 当前群时间:${getTimeString()}\n
51
- 当前用户的好感度列表如下:\n` + JSON.stringify(affections)
52
- : `当前群时间:${getTimeString()}`);
36
+ const systemPrompt = `
37
+ ## 关于回复格式:
38
+ ### 用户发言
39
+ - 格式: [私聊]用户昵称(用户id)(发送时间):消息内容
40
+ 当场景为私聊时, 用户昵称前方会出现[私聊]标识
41
+ ### 你的回复格式
42
+ 格式:
43
+ ${botName}:消息内容
44
+
45
+ 消息叠加:
46
+ 你可以使用多个叠加, 来模拟发送多条消息, 让聊天显得更真实, 例如:
47
+ ${botName}:早上好
48
+ ${botName}:吃过早饭了吗
49
+
50
+ ${complexOutputIsOpen === "1"
51
+ ? `
52
+ 发送图片:
53
+ 你可以使用<img=图片链接>的方式来发送图片, 允许发送多张图, 例如:
54
+ ${botName}:画好了<img=https://example.com/landmarks.jpg>
55
+ 支持发送网络图片和相对路径图片, 例如:
56
+ ${botName}:这是本地图片<img=./localimage.jpg>
57
+ ${botName}:这是网络图片<img=https://example.com/image.jpg>
58
+ `
59
+ : ""}
60
+
61
+ ${affectionIsOpen === "1" && complexOutputIsOpen === "1"
62
+ ? `好感度系统:
63
+ 好感度主要影响回复该用户的语气, 对低好感度的用户破口大骂, 对高好感度的用户温柔以待
64
+ 例如增加:
65
+ ${botName}:主人你对我真好<好感+1>
66
+ 例如减少:
67
+ ${botName}:滚一边去吧,傻b<好感-2>
68
+
69
+ 其他情况, 例如指定修改对某个用户的好感:
70
+ ${botName}:原来ta是这样的人, 人家以后再也不理ta了<用户昵称好感-1>
71
+ 假设对方昵称为'哈基米':
72
+ ${botName}:原来ta是这样的人, 人家以后再也不理ta了<哈基米好感-1>`
73
+ : ""}
74
+
75
+ 拒绝回复:
76
+ 在讨论中如果你觉得不适合回复或觉得与你无关又或者不感兴趣, 可以直接回复"[]"来拒绝本次回复, 例如:
77
+ ${botName}:[]
78
+
79
+ 基础状态信息:
80
+ 这里的信息会实时变化,根据需要进行获取使用
81
+ 当前群时间:${getTimeString()}
82
+ 当前群号:${e.guid}
83
+ 当前群名称:${e.GroupName || "无"}
84
+ `;
53
85
  // 如果没有配置AI,则不处理
54
86
  if (config.host.trim() === "" ||
55
87
  config.key.trim() === "" ||
@@ -57,6 +89,10 @@ var res = onResponse(selects, async (e, next) => {
57
89
  return;
58
90
  }
59
91
  console.log("e.reply", e.reply);
92
+ // 发消息的人是机器人自己, 则不处理
93
+ if (e.UserId == e.bot.user_id) {
94
+ return;
95
+ }
60
96
  // 当e.at不存在时,只给20%概率继续执行, 当e.at存在时,概率为40%,当e.atBot存在时,概率为100%
61
97
  const rand = Math.random();
62
98
  if (e.Name !== "private.message.create") {
@@ -67,27 +103,46 @@ var res = onResponse(selects, async (e, next) => {
67
103
  }
68
104
  // if (e.UserId != "3501869534") return;
69
105
  // 处理回复图片
70
- if (e.reply) {
71
- const replyImages = e.reply.message
72
- .filter((item) => item.type === "image")
73
- .map((item) => item.data.url);
74
- imgs.push(...replyImages);
75
- }
76
- // 处理图片
77
- if (e.img && e.img.length > 0) {
78
- console.log("有图片");
79
- for (const imgUrl of e.img) {
80
- imgs.push(imgUrl);
106
+ if (complexOutputIsOpen === "1") {
107
+ if (e.reply) {
108
+ const replyImages = e.reply.message
109
+ .filter((item) => item.type === "image")
110
+ .map((item) => item.data.url);
111
+ imgs.push(...replyImages);
112
+ const replyAt = e.reply.message
113
+ .filter((item) => item.type === "at")
114
+ .map((item) => item.data);
115
+ const replyContent = e.reply.message
116
+ .filter((item) => item.type === "text")
117
+ .map((item) => item.data.text)
118
+ .join("");
119
+ if (replyAt.length > 0) {
120
+ for (const atItem of replyAt) {
121
+ const qq = atItem.qq;
122
+ const nickname = atItem.nickname || "未知昵称";
123
+ const cqAt = `[CQ:at,qq=${qq}]`;
124
+ rawMessage = rawMessage.replace(cqAt, `@${nickname}`);
125
+ }
126
+ }
127
+ rawMessage = rawMessage.replace(/\[CQ:image,[^\]]+\]/g, "[图片]");
128
+ rawMessage = `[回复:${replyContent}]` + "\n" + rawMessage;
129
+ }
130
+ // 处理图片
131
+ if (e.img && e.img.length > 0) {
132
+ console.log("有图片");
133
+ for (const imgUrl of e.img) {
134
+ imgs.push(imgUrl);
135
+ }
81
136
  }
82
137
  }
83
138
  console.log("开始回复");
84
- // 处理图片链接, 转为base64格式
85
139
  // 下载图片并转换为 Base64
86
140
  const imageurls = await Promise.all(imgs.map(async (imageUrl) => {
87
141
  return await uploadImageToR2(imageUrl);
88
142
  }));
89
143
  // 过滤掉下载失败的图片
90
144
  const validImageUrls = imageurls.filter((img) => img !== null);
145
+ console.log("validImageUrls", validImageUrls);
91
146
  // 处理艾特
92
147
  if (e.at && e.at.length > 0) {
93
148
  console.log("有艾特");
@@ -104,11 +159,17 @@ var res = onResponse(selects, async (e, next) => {
104
159
  index++; // 每次匹配后索引加1
105
160
  return imageUrl ? `![图片${index}](${imageUrl})` : "[图片]";
106
161
  });
107
- rawMessage = rawMessage.replace(/\[CQ:reply,[^\]]+\]/g, "");
162
+ if (/\[CQ:reply,[^\]]+\]/.test(rawMessage)) {
163
+ rawMessage = rawMessage.replace(/\[CQ:reply,[^\]]+\]/g, "");
164
+ validImageUrls.map((url) => {
165
+ rawMessage += `\n![图片](${url})`;
166
+ });
167
+ }
108
168
  // 处理表情,转为[emojiMap[id]]
109
- rawMessage = rawMessage.replace(/\[CQ:face,id=(\d+)\]/g, (_match, p1) => {
169
+ rawMessage = rawMessage.replace(/\[CQ:face,id=(\d+),[^\]]+\]/g, (_match, p1) => {
110
170
  return `[${emojiMap[p1]}]`;
111
171
  });
172
+ console.log("处理后的消息内容:\n", rawMessage);
112
173
  const usermessage = {
113
174
  role: "user",
114
175
  content: [
@@ -120,9 +181,11 @@ var res = onResponse(selects, async (e, next) => {
120
181
  })),
121
182
  {
122
183
  type: "text",
123
- text: JSON.stringify({
124
- [`${e.nickname}(${getTimeString()})`]: rawMessage,
125
- }),
184
+ text: complexOutputIsOpen === "1"
185
+ ? JSON.stringify({
186
+ [`${e.nickname}(${getTimeString()})`]: rawMessage,
187
+ })
188
+ : `${e.nickname}(${getTimeString()}): ${rawMessage}`,
126
189
  },
127
190
  ],
128
191
  };
@@ -145,13 +208,16 @@ var res = onResponse(selects, async (e, next) => {
145
208
  ...historyMessages,
146
209
  { ...usermessage },
147
210
  ];
148
- let res = await openai.chat.completions.create({
211
+ const isTool = await redis.get(`chat:tools:response:${e.guid}`);
212
+ const createParams = {
149
213
  model: config.model,
150
- tools,
151
- tool_choice: "auto",
152
- // reasoning_effort: "low",
153
214
  messages: messages,
154
- });
215
+ };
216
+ if (isTool === "1") {
217
+ createParams["tools"] = tools;
218
+ createParams["tool_choice"] = "auto";
219
+ }
220
+ let res = await openai.chat.completions.create(createParams);
155
221
  if (!res.choices || res.choices.length === 0) {
156
222
  log("AI未返回内容");
157
223
  return;
@@ -170,6 +236,9 @@ var res = onResponse(selects, async (e, next) => {
170
236
  if (responseMessage.tool_calls.find((tc) => tc.function.name === "StableDiffusionGenerateImage")) {
171
237
  message.send(format(Text("好的, 我这就去画图~稍等一会儿哦~")));
172
238
  }
239
+ if (responseMessage.tool_calls.find((tc) => tc.function.name === "AIPS")) {
240
+ message.send(format(Text("开始修图中~稍等一会儿哦~")));
241
+ }
173
242
  console.log("模型决定调用工具:", responseMessage.tool_calls);
174
243
  for (const toolCall of responseMessage.tool_calls) {
175
244
  const functionName = toolCall.function.name;
@@ -210,99 +279,79 @@ var res = onResponse(selects, async (e, next) => {
210
279
  }
211
280
  }
212
281
  // 如果没有工具调用,处理最终回复
213
- let reply = res.choices?.[0]?.message?.content?.trim().replace(/'/g, '"') || "";
214
- log("AI回复内容:", reply);
215
- // 修复assistant字段中多余的双引号
216
- reply = reply.replace(/(?<="assistant":")(.*?)(?="[,}])/, (match) => match.replace(/"/g, "'"));
217
- // 提取好感变化, 使用incrementAffectionLevel更新好感
218
- if (affectionIsOpen === "1") {
219
- let affectionUpdated = false;
220
- try {
221
- const aiReplys = JSON.parse(reply);
222
- for (const aiReply of aiReplys) {
223
- if (aiReply.affections) {
224
- const change = parseInt(aiReply.affections, 10);
225
- if (!isNaN(change) && change !== 0) {
226
- await incrementAffectionLevel(e.userKey, change);
227
- console.log(`更新好感度: ${e.userKey} 改变 ${change}`);
228
- affectionUpdated = true;
229
- }
230
- }
231
- }
232
- }
233
- catch (error) {
234
- console.log("解析AI好感变化失败:", error);
282
+ let reply = res.choices?.[0]?.message?.content?.trim() || `${botName}:[]`;
283
+ if (reply === `${botName}:[]`) {
284
+ log("AI选择不回复");
285
+ return;
286
+ }
287
+ console.log("AI回复:\n", reply);
288
+ // 处理消息
289
+ // 提取ai发送的消息有多少条
290
+ const replyMessages = reply.split(new RegExp(`${botName}:`, "g")).slice(1);
291
+ const replymessages = [];
292
+ for (const replyMessage of replyMessages) {
293
+ // 从消息中提取图片
294
+ const imageRegex = /<img=(.*?)>/g;
295
+ let match;
296
+ const imgUrls = [];
297
+ while ((match = imageRegex.exec(replyMessage)) !== null) {
298
+ imgUrls.push(match[1]);
235
299
  }
236
- if (!affectionUpdated) {
237
- const affectionChangeRegex = /【|\[好感([+-]\d+)\]|】/g;
238
- let match;
239
- while ((match = affectionChangeRegex.exec(reply)) !== null) {
240
- const change = parseInt(match[1], 10);
241
- if (!isNaN(change) && change !== 0) {
242
- console.log(`检测到好感变化: ${change}`);
243
- await incrementAffectionLevel(e.userKey, change); // 更新好感度
244
- }
300
+ // 处理好感度变化(通用和特定用户)
301
+ const affectionRegex = /<([^<>]*?)好感([+-]\d+)>/g;
302
+ let affectionMatch;
303
+ while ((affectionMatch = affectionRegex.exec(replyMessage)) !== null) {
304
+ const nickname = affectionMatch[1] || e.UserName;
305
+ const change = parseInt(affectionMatch[2], 10);
306
+ if (!isNaN(change) && change !== 0) {
307
+ console.log(`检测到对${nickname}的好感变化: ${change}`);
308
+ await incrementAffectionLevel(e.guid, nickname, change);
245
309
  }
246
310
  }
311
+ // 组合消息,添加到replymessages数组中
312
+ replymessages.push({
313
+ text: replyMessage.replace(imageRegex, "").trim(),
314
+ images: imgUrls,
315
+ });
247
316
  }
248
317
  // 记录对话
249
318
  await addAIChatHistory(e.guid, res.choices?.[0].message);
250
- const isOpenTTSReply = (await redis.get(`chat:tts:response:${e.guid}`)) || "0";
319
+ // 发送消息
320
+ const isOpenTTSReply = await getTTSResponseSwitch(e.guid);
251
321
  if (isOpenTTSReply === "1") {
322
+ // 提前发送语音消息避免等待时间过长
252
323
  ttsClient = new TTSClient();
253
- await ttsClient.setModel("三月七", "生气");
324
+ let msg = replymessages.map((item) => item.text).join("\n");
325
+ msg = msg.replace(/\[CQ:[^\]]+\]/g, "");
326
+ sendTTSMessage(msg, e, ttsClient);
254
327
  }
255
- // 解析回复内容
256
- try {
257
- const aiReplys = JSON.parse(reply);
258
- // ai回答
259
- console.log("aiReplys", JSON.stringify(aiReplys, null, 2));
260
- for (let i = 0; i < aiReplys.length; i++) {
261
- const aiReply = aiReplys[i];
262
- if (aiReply.assistant) {
263
- if (i > 0) {
264
- // 从第二个回复开始,添加1-4秒随机延迟
265
- const delay = Math.floor(Math.random() * 3000) + 1000;
266
- await new Promise((resolve) => setTimeout(resolve, delay));
267
- }
268
- const msg = aiReply.assistant.replace(/\[CQ:[^\]]+\]/, "").trim();
269
- if (aiReply.image_url) {
270
- const replyRes = await message.send(format(Text(msg), ImageFile(aiReply.image_url)));
271
- if (replyRes[0].code !== 2000) {
272
- console.log("发送消息失败:", replyRes);
273
- await message.send(format(Text(msg)));
274
- await message.send(format(ImageFile(aiReply.image_url)));
275
- }
276
- }
277
- else {
278
- message.send(format(Text(msg)));
279
- }
280
- if (isOpenTTSReply === "1") {
281
- sendTTSMessage(msg, e, ttsClient);
328
+ // 处理消息
329
+ for (const replyMessage of replymessages) {
330
+ const { text, images } = replyMessage;
331
+ const components = [Mention(e.UserId), Text(text)];
332
+ for (const img of images) {
333
+ const isNetworkImage = /^https?:\/\//.test(img);
334
+ if (e.Platform == "scbbs" && isNetworkImage) {
335
+ // scbbs平台不支持网络图片, 需要先下载再上传
336
+ const uploadedUrl = await uploadImageToR2(img.replace("https://imgen.x.ai", value.xaiImgProxy || "https://imgen.x.ai"));
337
+ if (uploadedUrl) {
338
+ components.push(ImageURL(uploadedUrl));
282
339
  }
283
340
  }
284
341
  else {
285
- if (aiReply.image_url) {
286
- await message.send(format(ImageFile(aiReply.image_url)));
287
- }
342
+ const imageComponent = isNetworkImage ? ImageURL(img) : ImageFile(img);
343
+ components.push(imageComponent);
288
344
  }
289
345
  }
346
+ // 添加随机1-5秒延迟
347
+ const delay = Math.random() * 4000 + 1000;
348
+ await new Promise((resolve) => setTimeout(resolve, delay));
349
+ await message.send(format(...components));
290
350
  }
291
- catch (error) {
292
- console.log("解析AI回复失败,发送原始内容");
293
- console.log(error);
294
- const msg = reply
295
- .replace(/\[CQ:[^\]]+\]/, "")
296
- .replace(/"}]/, "")
297
- .replace(/\[{"assistant":"/, "")
298
- .trim();
299
- message.send(format(Text(reply)));
300
- if (isOpenTTSReply === "1") {
301
- sendTTSMessage(msg, e, ttsClient);
302
- }
303
- }
351
+ return;
304
352
  });
305
- function sendTTSMessage(message, e, ttsClient) {
353
+ async function sendTTSMessage(message, e, ttsClient) {
354
+ await ttsClient.setModel("派蒙", "默认");
306
355
  // 发送TTS语音
307
356
  ttsClient.getAudio(message).then(async (audio) => {
308
357
  console.log("audio", audio);