alemonjs-aichat 1.0.21 → 1.0.22
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/api/aitools.js +38 -0
- package/lib/api/loadSkill.js +35 -6
- package/lib/config.js +12 -0
- package/lib/data/help.json.js +1 -1
- package/lib/response/config/res.js +2 -1
- package/lib/response/setting/res.js +27 -0
- package/lib/response/zreply/rapi.res.js +566 -0
- package/lib/response/zreply/res.js +7 -6
- package/lib/routes/commands.js +6 -1
- package/package.json +5 -5
- package/skills/add-skill/SKILL.md +32 -0
- package/skills/send-file/SKILL.md +18 -0
- package/skills/use-alemon/SKILL.md +16 -0
package/lib/api/aitools.js
CHANGED
|
@@ -192,6 +192,23 @@ const tools = [
|
|
|
192
192
|
},
|
|
193
193
|
},
|
|
194
194
|
},
|
|
195
|
+
{
|
|
196
|
+
type: "function",
|
|
197
|
+
function: {
|
|
198
|
+
name: "EvalCode",
|
|
199
|
+
description: "执行JavaScript代码",
|
|
200
|
+
parameters: {
|
|
201
|
+
type: "object",
|
|
202
|
+
properties: {
|
|
203
|
+
code: {
|
|
204
|
+
type: "string",
|
|
205
|
+
description: "要执行的JavaScript代码",
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
required: ["code"],
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
},
|
|
195
212
|
];
|
|
196
213
|
const availableTools = {
|
|
197
214
|
/**
|
|
@@ -611,6 +628,27 @@ const availableTools = {
|
|
|
611
628
|
});
|
|
612
629
|
});
|
|
613
630
|
},
|
|
631
|
+
/**
|
|
632
|
+
* 执行eval
|
|
633
|
+
* @param code 代码字符串
|
|
634
|
+
* @returns 执行结果
|
|
635
|
+
*/
|
|
636
|
+
EvalCode: async ({ code }) => {
|
|
637
|
+
try {
|
|
638
|
+
// eslint-disable-next-line no-eval
|
|
639
|
+
const result = eval(code);
|
|
640
|
+
return result;
|
|
641
|
+
}
|
|
642
|
+
catch (error) {
|
|
643
|
+
console.error(`Error executing code: ${error}`);
|
|
644
|
+
return `Error: ${error}`;
|
|
645
|
+
}
|
|
646
|
+
},
|
|
647
|
+
/**
|
|
648
|
+
* 获取技能详情
|
|
649
|
+
* @param skillName 技能名称
|
|
650
|
+
* @returns 技能详情
|
|
651
|
+
*/
|
|
614
652
|
getSKILLDetail: async ({ skillName }) => {
|
|
615
653
|
const skill = loadSkillDetail(skillName);
|
|
616
654
|
return skill;
|
package/lib/api/loadSkill.js
CHANGED
|
@@ -4,11 +4,14 @@ import { fileURLToPath } from 'url';
|
|
|
4
4
|
import { getConfigValue } from 'alemonjs';
|
|
5
5
|
|
|
6
6
|
// 获取skills/下的文件夹,读取文件夹中的SKILL.md文件,并将其内容解析成对象返回
|
|
7
|
-
getConfigValue().aiChat || {};
|
|
7
|
+
const value = getConfigValue().aiChat || {};
|
|
8
8
|
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
//
|
|
10
|
-
const
|
|
11
|
-
|
|
9
|
+
// 通用函数: 根据路径获取技能列表
|
|
10
|
+
const loadSkillsFromPath = (skillsDir) => {
|
|
11
|
+
if (!fs.existsSync(skillsDir)) {
|
|
12
|
+
console.warn(`技能目录 ${skillsDir} 不存在`);
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
12
15
|
const skillFolders = fs.readdirSync(skillsDir).filter((file) => {
|
|
13
16
|
const skillPath = path.join(skillsDir, file);
|
|
14
17
|
return fs.statSync(skillPath).isDirectory();
|
|
@@ -37,9 +40,20 @@ const loadSkills = () => {
|
|
|
37
40
|
};
|
|
38
41
|
}
|
|
39
42
|
});
|
|
40
|
-
console.log(`加载技能: ${skills.length}个`);
|
|
41
43
|
return skills;
|
|
42
44
|
};
|
|
45
|
+
// 获取技能列表的简介和名称
|
|
46
|
+
const loadSkills = () => {
|
|
47
|
+
const skillsDir = path.join(currentDir, "../../skills");
|
|
48
|
+
const skills = loadSkillsFromPath(skillsDir);
|
|
49
|
+
const externalSkills = loadExternalSkills();
|
|
50
|
+
const allSkills = [...skills, ...externalSkills];
|
|
51
|
+
// 根据name和description去重
|
|
52
|
+
const uniqueSkills = allSkills.filter((skill, index, self) => index ===
|
|
53
|
+
self.findIndex((s) => s.name === skill.name && s.description === skill.description));
|
|
54
|
+
console.log(`加载技能: ${uniqueSkills.length}个`);
|
|
55
|
+
return uniqueSkills;
|
|
56
|
+
};
|
|
43
57
|
// 获取某个技能的详情, 通过技能名称匹配对应文件夹下的所有md文件,将内容一并返回
|
|
44
58
|
const loadSkillDetail = (skillName) => {
|
|
45
59
|
const skillsDir = path.join(currentDir, "../../skills");
|
|
@@ -68,5 +82,20 @@ const loadSkillDetail = (skillName) => {
|
|
|
68
82
|
details,
|
|
69
83
|
};
|
|
70
84
|
};
|
|
85
|
+
// 加载外部skills, 通过配置文件中的skills_dir字段指定技能目录,读取该目录下的技能文件夹,并将其内容解析成对象返回
|
|
86
|
+
const loadExternalSkills = () => {
|
|
87
|
+
if (Array.isArray(value.skills_dir)) {
|
|
88
|
+
// 如果是数组,加载所有目录下的技能
|
|
89
|
+
const allSkills = value.skills_dir.flatMap((dir) => {
|
|
90
|
+
return loadSkillsFromPath(dir);
|
|
91
|
+
});
|
|
92
|
+
console.log(`加载外部技能: ${allSkills.length}个`);
|
|
93
|
+
return allSkills;
|
|
94
|
+
}
|
|
95
|
+
const skillsDir = value.skills_dir || path.join(currentDir, "../../skills");
|
|
96
|
+
const skills = loadSkillsFromPath(skillsDir);
|
|
97
|
+
console.log(`加载外部技能: ${skills.length}个`);
|
|
98
|
+
return skills;
|
|
99
|
+
};
|
|
71
100
|
|
|
72
|
-
export { loadSkillDetail, loadSkills };
|
|
101
|
+
export { loadExternalSkills, loadSkillDetail, loadSkills, loadSkillsFromPath };
|
package/lib/config.js
CHANGED
|
@@ -11,6 +11,7 @@ class db {
|
|
|
11
11
|
temperature: 0.7,
|
|
12
12
|
max_tokens: 150,
|
|
13
13
|
systemPrompt: "",
|
|
14
|
+
rapi: false,
|
|
14
15
|
};
|
|
15
16
|
constructor(redisConfig) {
|
|
16
17
|
this.redis =
|
|
@@ -132,6 +133,17 @@ class db {
|
|
|
132
133
|
await this.setAIConfig(guid, aiConfig);
|
|
133
134
|
return aiConfig;
|
|
134
135
|
}
|
|
136
|
+
/** 切换AI的RAPI模式 */
|
|
137
|
+
async switchModelRAPI(aiName, enable) {
|
|
138
|
+
const aiConfigStr = await this.redis.get(`ai:list:${aiName}`);
|
|
139
|
+
if (!aiConfigStr) {
|
|
140
|
+
console.log("切换失败, 无此模型");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const aiConfig = JSON.parse(aiConfigStr);
|
|
144
|
+
aiConfig.rapi = enable;
|
|
145
|
+
await this.setAIList(aiName, aiConfig);
|
|
146
|
+
}
|
|
135
147
|
/** 获取提示词列表 */
|
|
136
148
|
async getPromptList() {
|
|
137
149
|
const keys = await this.redis.keys("ai:prompt:*");
|
package/lib/data/help.json.js
CHANGED
|
@@ -14,9 +14,10 @@ var res = onResponse(selects, async (e, next) => {
|
|
|
14
14
|
const affectionIsOpen = await redisClient.getAffectionSwitch(e.guid); // 好感度开关
|
|
15
15
|
const isOpenTTSReply = await redisClient.getTTSResponseSwitch(e.guid); // TTS回复开关
|
|
16
16
|
const toolsIsOpen = await redisClient.getToolSwitch(e.guid); // 工具开关
|
|
17
|
+
const deepThinkingIsOpen = await redisClient.getDeepThoughtSwitch(e.guid); // 深度思考开关
|
|
17
18
|
const aiList = await redisClient.getAIList();
|
|
18
19
|
// 发送消息
|
|
19
|
-
message.send(format(Text(`当前AI配置:\n`), Text(`总开关: ${config.model != "" ? "开启" : "关闭"}\n`), Text(`工具: ${toolsIsOpen == "1" ? "开启" : "关闭"}\n`), Text(`复杂输出: ${complexResponse == "1" ? "开启" : "关闭"}\n`), Text(`好感度开关: ${affectionIsOpen == "1" ? "开启" : "关闭"}\n`), Text(`TTS回复开关: ${isOpenTTSReply == "1" ? "开启" : "关闭"}\n`), Text(`模型: ${config.model || "未设置"}\n`), Text(`提示词: ${config.systemPrompt || "未设置"}\n`), Text(`AI数量: ${aiList.length ? aiList.length : "暂无AI配置"}`)));
|
|
20
|
+
message.send(format(Text(`当前AI配置:\n`), Text(`总开关: ${config.model != "" ? "开启" : "关闭"}\n`), Text(`工具: ${toolsIsOpen == "1" ? "开启" : "关闭"}\n`), Text(`复杂输出: ${complexResponse == "1" ? "开启" : "关闭"}\n`), Text(`好感度开关: ${affectionIsOpen == "1" ? "开启" : "关闭"}\n`), Text(`TTS回复开关: ${isOpenTTSReply == "1" ? "开启" : "关闭"}\n`), Text(`深度思考: ${deepThinkingIsOpen == "1" ? "默认" : "关闭"}\n`), Text(`模型: ${config.model || "未设置"}\n`), Text(`提示词: ${config.systemPrompt || "未设置"}\n`), Text(`AI数量: ${aiList.length ? aiList.length : "暂无AI配置"}`)));
|
|
20
21
|
}
|
|
21
22
|
// 查看AI列表
|
|
22
23
|
if (/^(\/|#)ai列表$/i.test(e.msg)) {
|
|
@@ -315,6 +315,33 @@ var res = onResponse(selects, async (e, next) => {
|
|
|
315
315
|
message.send(format(Text(`已${enable ? "开启" : "关闭"}主动搭话功能 !`)));
|
|
316
316
|
return;
|
|
317
317
|
}
|
|
318
|
+
// 设置深度思考开关
|
|
319
|
+
if (/(\/|#)(开启|关闭)深度思考$/i.test(e.msg)) {
|
|
320
|
+
const match = e.msg.match(/(\/|#)(开启|关闭)深度思考$/i);
|
|
321
|
+
if (!match) {
|
|
322
|
+
message.send(format(Text("格式错误,请按照 格式:/开启深度思考 或 /关闭深度思考 进行设置")));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const [, , action] = match;
|
|
326
|
+
const enable = action === "开启";
|
|
327
|
+
await redisClient.setDeepThoughtSwitch(e.guid, enable);
|
|
328
|
+
message.send(format(Text(`已${enable ? "开启" : "关闭"}深度思考功能 !`)));
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
// 设置ai的RAPI模式开关
|
|
332
|
+
if (/(\/|#)(设置|修改)(ai)?(.*)rapi模式(开启|关闭)$/i.test(e.msg)) {
|
|
333
|
+
const match = e.msg.match(/(\/|#)(设置|修改)(ai)?(.*)rapi模式(开启|关闭)$/i);
|
|
334
|
+
if (!match) {
|
|
335
|
+
message.send(format(Text("格式错误,请按照 格式:/设置 rapi模式 开启 或 /设置 rapi模式 关闭 进行设置")));
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const [, , , , aiName, action] = match;
|
|
339
|
+
const enable = action === "开启";
|
|
340
|
+
if (aiName.trim()) {
|
|
341
|
+
await redisClient.switchModelRAPI(aiName.trim(), enable);
|
|
342
|
+
message.send(format(Text(`已${enable ? "开启" : "关闭"} AI ${aiName.trim()} 的 RAPI 模式 !`)));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
318
345
|
// 无匹配则继续下一个响应器
|
|
319
346
|
next();
|
|
320
347
|
});
|
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
import { API } from '@alemonjs/onebot';
|
|
2
|
+
import { emojiMap } from '../../emoji.js';
|
|
3
|
+
import { uploadImageToR2 } from '../../s3.js';
|
|
4
|
+
import { getConfigValue, useMessage, Text, Mention, ImageFile, ImageURL, useClient } from 'alemonjs';
|
|
5
|
+
import { log } from 'console';
|
|
6
|
+
import OpenAi from 'openai';
|
|
7
|
+
import { TTSClient } from '../../api/tts.js';
|
|
8
|
+
import { getTimeString } from '../../api/format.js';
|
|
9
|
+
import { tools, availableTools } from '../../api/aitools.js';
|
|
10
|
+
import { loadSkills } from '../../api/loadSkill.js';
|
|
11
|
+
import redisClient from '../../config.js';
|
|
12
|
+
import fileUrl from '../../assets/error.png.js';
|
|
13
|
+
|
|
14
|
+
const value = getConfigValue().aiChat || {};
|
|
15
|
+
const selects = onSelects(["message.create", "private.message.create"]);
|
|
16
|
+
let ttsClient;
|
|
17
|
+
let currentModel = "派蒙-默认"; // 默认音色
|
|
18
|
+
// ====================== 新版会话 ID 存储 ======================
|
|
19
|
+
const sessionKey = {}; // 按群/用户ID保存会话ID
|
|
20
|
+
// ============================================================
|
|
21
|
+
var rapi_res = onResponse(selects, async (e, next) => {
|
|
22
|
+
console.log("e.UserId", e.UserId, e.bot);
|
|
23
|
+
// 如果是命令则跳过
|
|
24
|
+
if (e.msg.startsWith("/") ||
|
|
25
|
+
e.msg.startsWith("#") ||
|
|
26
|
+
e.UserId == e.bot.user_id) {
|
|
27
|
+
next();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const complexOutputIsOpen = await redisClient.getComplexOutput(e.guid); // 复杂输出
|
|
31
|
+
const affectionIsOpen = await redisClient.getAffectionSwitch(e.guid); // 好感度开关
|
|
32
|
+
const isOpenTTSReply = await redisClient.getTTSResponseSwitch(e.guid); // TTS回复开关
|
|
33
|
+
const isOpenR18 = await redisClient.getR18Switch(e.guid); // R18开关
|
|
34
|
+
const proactiveSwitch = await redisClient.getProactiveSwitch(e.guid); // 主动搭话开关
|
|
35
|
+
const deepThoughtSwitch = await redisClient.getDeepThoughtSwitch(e.guid); // 深度思考开关
|
|
36
|
+
console.log("isOpenR18", isOpenR18, "proactiveSwitch", proactiveSwitch, "deepThoughtSwitch", deepThoughtSwitch);
|
|
37
|
+
const skills = loadSkills(); // 加载技能列表
|
|
38
|
+
// 创建消息
|
|
39
|
+
const [message] = useMessage(e);
|
|
40
|
+
const config = await redisClient.getAIConfig(e.guid);
|
|
41
|
+
const historyMessages = await redisClient.getAIChatHistory(e.guid);
|
|
42
|
+
const affections = await redisClient.getAffectionLevelAll(e.guid);
|
|
43
|
+
const imgs = [];
|
|
44
|
+
let rawMessage = e.value.raw_message || e.raw_message || e.msg;
|
|
45
|
+
const botName = e.bot.nickname || "小咸鱼";
|
|
46
|
+
const systemPrompt = `
|
|
47
|
+
# AI助手回复规范
|
|
48
|
+
请严格按照以下规范进行回复, 不要添加任何不必要的内容, 也不要删除或修改规范中的任何内容, 以确保回复能够被正确解析和处理
|
|
49
|
+
|
|
50
|
+
## 技能
|
|
51
|
+
在遇到用户需要执行特定操作时,先获取对应的技能,如果有可用技能,获取该技能,并严格按照技能文档要求的格式调用, 不要添加任何多余的内容, 也不要删除或修改规范中的任何内容, 以确保技能能够被正确解析和处理
|
|
52
|
+
当没有技能可以应对需求时, 就自行挑选合适的工具函数来完成用户的需求
|
|
53
|
+
---
|
|
54
|
+
技能列表:
|
|
55
|
+
${skills.map((skill) => `- ${skill.name}: ${skill.description}`).join("\n")}
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 关于回复格式:
|
|
59
|
+
### 用户发言
|
|
60
|
+
- 格式: [私聊]用户昵称(用户id)(发送时间):消息内容
|
|
61
|
+
当场景为私聊时, 用户昵称前方会出现[私聊]标识
|
|
62
|
+
### 你的回复格式
|
|
63
|
+
格式:
|
|
64
|
+
${botName}:消息内容
|
|
65
|
+
|
|
66
|
+
消息叠加:
|
|
67
|
+
你可以使用多个叠加, 来模拟发送多条消息, 让聊天显得更真实, 例如:
|
|
68
|
+
${botName}:早上好
|
|
69
|
+
${botName}:吃过早饭了吗
|
|
70
|
+
|
|
71
|
+
${complexOutputIsOpen === "1"
|
|
72
|
+
? `
|
|
73
|
+
发送图片:
|
|
74
|
+
你可以使用<img=图片链接>的方式来发送图片, 允许发送多张图, 例如:
|
|
75
|
+
${botName}:画好了<img=https://example.com/landmarks.jpg>
|
|
76
|
+
支持发送网络图片和相对路径图片, 例如:
|
|
77
|
+
${botName}:这是本地图片<img=./localimage.jpg>
|
|
78
|
+
${botName}:这是网络图片<img=https://example.com/image.jpg>
|
|
79
|
+
注意: 如果有用户问你能不能看动图时, 你要回答只能看到第一帧
|
|
80
|
+
`
|
|
81
|
+
: ""}
|
|
82
|
+
|
|
83
|
+
${complexOutputIsOpen === "1" && isOpenTTSReply === "1"
|
|
84
|
+
? `\n语音回复:
|
|
85
|
+
此能力只能在onebot平台使用, 其他平台请勿使用, 否则会回复失败
|
|
86
|
+
你可以使用语音回复来让你的回复更生动, 例如:
|
|
87
|
+
${botName}:你好呀~<tts=你好呀~>
|
|
88
|
+
可以通过调用GetTTSModels工具函数获取音色列表, 可使用<tts=内容,音色=派蒙-情感>的格式来指定音色,设置一次后, 后面的回复将使用相同的音色, 例如:
|
|
89
|
+
${botName}:这是派蒙的声音哦~<音色=派蒙-默认><tts=这是派蒙的默认声音哦>
|
|
90
|
+
${botName}:现在还是派蒙的声音哦~<tts=现在还是派蒙的声音哦>
|
|
91
|
+
注意: 语音回复会增加回复时间, 请合理使用
|
|
92
|
+
注意: 不可使用列表中不存在的音色, 否则会回复失败
|
|
93
|
+
注意: 在设置音色时只需要指定模的角色名字和情感, 例如模型是"星穹铁道-中文-花火", 则音色应该设置为"花火-情感", 而不是"星穹铁道-中文-花火-情感"
|
|
94
|
+
注意: 使用语音必须用${botName}:开头, 否则不会处理, 例如:
|
|
95
|
+
<tts=你好> 这是错误的格式, 不会被处理
|
|
96
|
+
${botName}:<tts=你好> 这是正确的格式, 会被处理
|
|
97
|
+
|
|
98
|
+
`
|
|
99
|
+
: "语音回复: 当前不可用"}
|
|
100
|
+
|
|
101
|
+
${isOpenR18 === "1"
|
|
102
|
+
? ""
|
|
103
|
+
: `
|
|
104
|
+
R18内容:
|
|
105
|
+
当用户发送的消息中包含敏感词时, 提醒用户注意言辞, 并且不回答用户的问题, 务必遵守国家法律法规
|
|
106
|
+
`}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
${affectionIsOpen === "1" && complexOutputIsOpen === "1"
|
|
110
|
+
? `好感度系统:
|
|
111
|
+
好感度主要影响回复该用户的语气, 对低好感度的用户破口大骂, 对高好感度的用户温柔以待
|
|
112
|
+
例如增加:
|
|
113
|
+
${botName}:主人你对我真好<好感+1>
|
|
114
|
+
例如减少:
|
|
115
|
+
${botName}:滚一边去吧,傻b<好感-2>
|
|
116
|
+
|
|
117
|
+
其他情况, 例如指定修改对某个用户的好感:
|
|
118
|
+
${botName}:原来ta是这样的人, 人家以后再也不理ta了<用户昵称好感-1>
|
|
119
|
+
假设对方昵称为'哈基米':
|
|
120
|
+
${botName}:原来ta是这样的人, 人家以后再也不理ta了<哈基米好感-1>`
|
|
121
|
+
: ""}
|
|
122
|
+
|
|
123
|
+
拒绝回复:
|
|
124
|
+
在讨论中如果你觉得不适合回复或觉得与你无关又或者不感兴趣, 可以直接回复"[]"来拒绝本次回复, 例如:
|
|
125
|
+
${botName}:[]
|
|
126
|
+
|
|
127
|
+
基础状态信息:
|
|
128
|
+
这里的信息会实时变化,根据需要进行获取使用
|
|
129
|
+
当前群时间:${getTimeString()}
|
|
130
|
+
当前群号:${e.guid}
|
|
131
|
+
当前群名称:${e.GroupName || "无"}
|
|
132
|
+
当前聊天平台:${e.Platform}
|
|
133
|
+
当前框架:alemonjs
|
|
134
|
+
${e.ClientError ? `当前错误信息:${e.ClientError}` : ""}
|
|
135
|
+
`;
|
|
136
|
+
// 如果没有配置AI,则不处理
|
|
137
|
+
if (config.host.trim() === "" ||
|
|
138
|
+
config.key.trim() === "" ||
|
|
139
|
+
config.model.trim() === "") {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
console.log("e.reply", e.reply);
|
|
143
|
+
// 发消息的人是机器人自己, 则不处理
|
|
144
|
+
if (e.UserId == e.bot.user_id) {
|
|
145
|
+
next();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
// 当e.at不存在时,只给20%概率继续执行, 当e.at存在时,概率为40%,当e.atBot存在时,概率为100%
|
|
149
|
+
const rand = Math.random();
|
|
150
|
+
if (e.Name !== "private.message.create") {
|
|
151
|
+
if (proactiveSwitch !== "1" && e.at && !e.atBot) {
|
|
152
|
+
console.log("未开启主动搭话, 跳过AI回复");
|
|
153
|
+
next();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if ((!e.at && rand > 0.1) || (e.at && !e.atBot && rand > 0.2)) {
|
|
157
|
+
console.log("跳过AI回复");
|
|
158
|
+
next();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// 处理回复图片
|
|
163
|
+
if (complexOutputIsOpen === "1") {
|
|
164
|
+
if (e.reply) {
|
|
165
|
+
const replyImages = e.reply.message
|
|
166
|
+
.filter((item) => item.type === "image")
|
|
167
|
+
.map((item) => item.data.url);
|
|
168
|
+
imgs.push(...replyImages);
|
|
169
|
+
const replyAt = e.reply.message
|
|
170
|
+
.filter((item) => item.type === "at")
|
|
171
|
+
.map((item) => item.data);
|
|
172
|
+
const replyContent = e.reply.message
|
|
173
|
+
.filter((item) => item.type === "text")
|
|
174
|
+
.map((item) => item.data.text)
|
|
175
|
+
.join("");
|
|
176
|
+
if (replyAt.length > 0) {
|
|
177
|
+
for (const atItem of replyAt) {
|
|
178
|
+
const qq = atItem.qq;
|
|
179
|
+
const nickname = atItem.nickname || "未知昵称";
|
|
180
|
+
const cqAt = `[CQ:at,qq=${qq}]`;
|
|
181
|
+
rawMessage = rawMessage.replace(cqAt, `@${nickname}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
rawMessage = rawMessage.replace(/\[CQ:image,[^\]]+\]/g, "[图片]");
|
|
185
|
+
rawMessage = `[回复:${replyContent}]` + "\n" + rawMessage;
|
|
186
|
+
}
|
|
187
|
+
// 处理图片
|
|
188
|
+
if (e.img && e.img.length > 0) {
|
|
189
|
+
console.log("有图片");
|
|
190
|
+
for (const imgUrl of e.img) {
|
|
191
|
+
imgs.push(imgUrl);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
console.log("开始回复");
|
|
196
|
+
// 下载图片并转换为 Base64
|
|
197
|
+
const imageurls = await Promise.all(imgs.map(async (imageUrl) => {
|
|
198
|
+
return await uploadImageToR2(imageUrl);
|
|
199
|
+
}));
|
|
200
|
+
// 过滤掉下载失败的图片
|
|
201
|
+
const validImageUrls = imageurls.filter((img) => img !== null);
|
|
202
|
+
// 处理艾特
|
|
203
|
+
if (e.at && e.at.length > 0) {
|
|
204
|
+
console.log("有艾特");
|
|
205
|
+
for (const atItem of e.at) {
|
|
206
|
+
const qq = atItem.data.qq;
|
|
207
|
+
const nickname = atItem.data.nickname || "未知昵称";
|
|
208
|
+
const cqAt = `[CQ:at,qq=${qq}]`;
|
|
209
|
+
rawMessage = rawMessage.replace(cqAt, `@${nickname}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
let index = 0; // 用于跟踪当前访问的 validImageUrls 的索引
|
|
213
|
+
rawMessage = rawMessage.replace(/\[CQ:image,[^\]]+\]/g, () => {
|
|
214
|
+
const imageUrl = validImageUrls[index];
|
|
215
|
+
index++; // 每次匹配后索引加1
|
|
216
|
+
return imageUrl ? `` : "[图片]";
|
|
217
|
+
});
|
|
218
|
+
if (/\[CQ:reply,[^\]]+\]/.test(rawMessage)) {
|
|
219
|
+
rawMessage = rawMessage.replace(/\[CQ:reply,[^\]]+\]/g, "");
|
|
220
|
+
validImageUrls.map((url) => {
|
|
221
|
+
rawMessage += `\n`;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
// 处理表情,转为[emojiMap[id]]
|
|
225
|
+
rawMessage = rawMessage.replace(/\[CQ:face,id=(\d+),[^\]]+\]/g, (_match, p1) => {
|
|
226
|
+
return `[${emojiMap[p1]}]`;
|
|
227
|
+
});
|
|
228
|
+
console.log("处理后的消息内容:\n", rawMessage);
|
|
229
|
+
const usermessage = {
|
|
230
|
+
role: "user",
|
|
231
|
+
content: [
|
|
232
|
+
...validImageUrls.map((url) => ({
|
|
233
|
+
type: "image_url",
|
|
234
|
+
image_url: {
|
|
235
|
+
url: url,
|
|
236
|
+
},
|
|
237
|
+
})),
|
|
238
|
+
{
|
|
239
|
+
type: "text",
|
|
240
|
+
text: complexOutputIsOpen === "1"
|
|
241
|
+
? JSON.stringify({
|
|
242
|
+
[`${e.nickname}(${getTimeString()})`]: rawMessage,
|
|
243
|
+
})
|
|
244
|
+
: `${e.nickname}(${getTimeString()}): ${rawMessage}`,
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
};
|
|
248
|
+
console.log("处理后的消息", JSON.stringify(usermessage, null, 2));
|
|
249
|
+
console.log("好感度列表", affections);
|
|
250
|
+
try {
|
|
251
|
+
const openai = new OpenAi({
|
|
252
|
+
baseURL: config.host,
|
|
253
|
+
apiKey: config.key,
|
|
254
|
+
timeout: 300000,
|
|
255
|
+
});
|
|
256
|
+
let messages = [
|
|
257
|
+
{
|
|
258
|
+
role: "system",
|
|
259
|
+
content: systemPrompt + `\n性格约束:${config.systemPrompt}`,
|
|
260
|
+
},
|
|
261
|
+
...historyMessages,
|
|
262
|
+
{ ...usermessage },
|
|
263
|
+
];
|
|
264
|
+
const isTool = await redisClient.getToolSwitch(e.guid);
|
|
265
|
+
// ====================== 新版请求参数 ======================
|
|
266
|
+
let createParams = {
|
|
267
|
+
model: config.model,
|
|
268
|
+
stream: true,
|
|
269
|
+
};
|
|
270
|
+
// 如果有会话ID,直接带上(新版复用会话)
|
|
271
|
+
if (sessionKey[e.guid]) {
|
|
272
|
+
createParams.session_id = sessionKey[e.guid];
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
// 没有会话ID才传完整消息
|
|
276
|
+
createParams.messages = messages;
|
|
277
|
+
}
|
|
278
|
+
// 工具开关
|
|
279
|
+
if (isTool === "1") {
|
|
280
|
+
createParams.tools = tools;
|
|
281
|
+
createParams.tool_choice = "auto";
|
|
282
|
+
}
|
|
283
|
+
// 深度思考开关
|
|
284
|
+
if (deepThoughtSwitch === "0") {
|
|
285
|
+
createParams.verbosity = "low";
|
|
286
|
+
}
|
|
287
|
+
// ==========================================================
|
|
288
|
+
// ====================== 新版流式请求 ======================
|
|
289
|
+
const stream = await openai.chat.completions.create(createParams);
|
|
290
|
+
// ==========================================================
|
|
291
|
+
let fullContent = "";
|
|
292
|
+
let toolCalls = [];
|
|
293
|
+
for await (const chunk of stream) {
|
|
294
|
+
// 保存会话 ID(你的要求)
|
|
295
|
+
if (chunk.session_id) {
|
|
296
|
+
sessionKey[e.guid] = chunk.session_id;
|
|
297
|
+
}
|
|
298
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
299
|
+
if (!delta)
|
|
300
|
+
continue;
|
|
301
|
+
if (delta.content) {
|
|
302
|
+
fullContent += delta.content;
|
|
303
|
+
}
|
|
304
|
+
if (delta.tool_calls) {
|
|
305
|
+
for (const tc of delta.tool_calls) {
|
|
306
|
+
if (tc.index !== undefined) {
|
|
307
|
+
if (!toolCalls[tc.index]) {
|
|
308
|
+
toolCalls[tc.index] = {
|
|
309
|
+
id: tc.id || "",
|
|
310
|
+
type: "function",
|
|
311
|
+
function: { name: "", arguments: "" },
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
if (tc.id)
|
|
315
|
+
toolCalls[tc.index].id = tc.id;
|
|
316
|
+
if (tc.function?.name)
|
|
317
|
+
toolCalls[tc.index].function.name += tc.function.name;
|
|
318
|
+
if (tc.function?.arguments)
|
|
319
|
+
toolCalls[tc.index].function.arguments += tc.function.arguments;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
let res = {
|
|
325
|
+
choices: [
|
|
326
|
+
{
|
|
327
|
+
message: {
|
|
328
|
+
role: "assistant",
|
|
329
|
+
content: fullContent || null,
|
|
330
|
+
tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
};
|
|
335
|
+
if (!res.choices || res.choices.length === 0) {
|
|
336
|
+
log("AI未返回内容");
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
messages.push(usermessage);
|
|
340
|
+
await redisClient.addAIChatHistory(e.guid, usermessage);
|
|
341
|
+
// 检查是否有工具调用需要处理
|
|
342
|
+
while (res.choices[0].message?.tool_calls?.length) {
|
|
343
|
+
const responseMessage = res.choices[0].message;
|
|
344
|
+
// 先处理对话
|
|
345
|
+
messages.push(responseMessage);
|
|
346
|
+
await redisClient.addAIChatHistory(e.guid, responseMessage);
|
|
347
|
+
console.log("模型决定调用工具:", responseMessage.tool_calls);
|
|
348
|
+
const useToolsMessage = responseMessage.tool_calls.map((toolCall) => {
|
|
349
|
+
return `工具调用: ${toolCall.function.name}(${toolCall.function.arguments})`;
|
|
350
|
+
});
|
|
351
|
+
const msgId = await message.send(format(Text(useToolsMessage.join("\n"))));
|
|
352
|
+
// 30秒后撤回消息
|
|
353
|
+
setTimeout(() => {
|
|
354
|
+
message.delete({ messageId: msgId[0].data.message_id });
|
|
355
|
+
}, 30000);
|
|
356
|
+
for (const toolCall of responseMessage.tool_calls) {
|
|
357
|
+
const functionName = toolCall.function.name;
|
|
358
|
+
let functionArgs = {};
|
|
359
|
+
try {
|
|
360
|
+
functionArgs = toolCall.function.arguments
|
|
361
|
+
? JSON.parse(toolCall.function.arguments)
|
|
362
|
+
: {};
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
console.error(`工具参数解析失败:${functionName}`, err);
|
|
366
|
+
functionArgs = {};
|
|
367
|
+
}
|
|
368
|
+
const functionToCall = availableTools[functionName];
|
|
369
|
+
let functionResult;
|
|
370
|
+
if (!functionToCall) {
|
|
371
|
+
console.error(`未找到函数:${functionName}`);
|
|
372
|
+
functionResult = {
|
|
373
|
+
ok: false,
|
|
374
|
+
error: `未找到函数:${functionName}`,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
try {
|
|
379
|
+
// 执行本地函数
|
|
380
|
+
functionResult = await functionToCall(functionArgs);
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
console.error(`工具执行失败:${functionName}`, err);
|
|
384
|
+
functionResult = {
|
|
385
|
+
ok: false,
|
|
386
|
+
error: err?.message || String(err) || "工具执行失败",
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const toolContent = JSON.stringify(typeof functionResult === "undefined"
|
|
391
|
+
? { ok: true, result: null }
|
|
392
|
+
: functionResult);
|
|
393
|
+
// 把工具执行结果作为 tool role 消息加回对话
|
|
394
|
+
messages.push({
|
|
395
|
+
role: "tool",
|
|
396
|
+
tool_call_id: toolCall.id,
|
|
397
|
+
name: functionName,
|
|
398
|
+
content: toolContent,
|
|
399
|
+
});
|
|
400
|
+
// 记录工具调用结果
|
|
401
|
+
await redisClient.addAIChatHistory(e.guid, {
|
|
402
|
+
role: "tool",
|
|
403
|
+
tool_call_id: toolCall.id,
|
|
404
|
+
name: functionName,
|
|
405
|
+
content: toolContent,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
// ====================== 新版工具循环请求 ======================
|
|
409
|
+
res = await openai.chat.completions.create({
|
|
410
|
+
model: config.model,
|
|
411
|
+
session_id: sessionKey[e.guid], // 必须带会话ID
|
|
412
|
+
tools,
|
|
413
|
+
tool_choice: "auto",
|
|
414
|
+
});
|
|
415
|
+
// ==============================================================
|
|
416
|
+
// 检查是否还有工具调用需要处理
|
|
417
|
+
if (!res.choices || res.choices.length === 0) {
|
|
418
|
+
log("AI未返回内容");
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// 如果没有工具调用,处理最终回复
|
|
423
|
+
let reply = res.choices?.[0]?.message?.content?.trim() || `${botName}:[]`;
|
|
424
|
+
if (reply === `${botName}:[]`) {
|
|
425
|
+
log("AI选择不回复");
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
console.log("AI回复:\n", reply);
|
|
429
|
+
// 处理消息
|
|
430
|
+
// 提取ai发送的消息有多少条
|
|
431
|
+
const replyMessages = reply.split(new RegExp(`${botName}:`, "g")).slice(1);
|
|
432
|
+
const replymessages = [];
|
|
433
|
+
const ttsMessages = [];
|
|
434
|
+
for (const replyMessage of replyMessages) {
|
|
435
|
+
// 从消息中提取图片
|
|
436
|
+
const imageRegex = /<img=(.*?)>/g;
|
|
437
|
+
let match;
|
|
438
|
+
const imgUrls = [];
|
|
439
|
+
while ((match = imageRegex.exec(replyMessage)) !== null) {
|
|
440
|
+
imgUrls.push(match[1]);
|
|
441
|
+
}
|
|
442
|
+
// 处理好感度变化(通用和特定用户)
|
|
443
|
+
const affectionRegex = /<([^<>]*?)好感([+-]\d+)>/g;
|
|
444
|
+
let affectionMatch;
|
|
445
|
+
while ((affectionMatch = affectionRegex.exec(replyMessage)) !== null) {
|
|
446
|
+
const nickname = affectionMatch[1] || e.UserName;
|
|
447
|
+
const change = parseInt(affectionMatch[2], 10);
|
|
448
|
+
if (!isNaN(change) && change !== 0) {
|
|
449
|
+
console.log(`检测到对${nickname}的好感变化: ${change}`);
|
|
450
|
+
await redisClient.incrementAffectionLevel(e.guid, nickname, change);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// 处理语音回复
|
|
454
|
+
const ttsRegex = /<(tts|音色)=([^>]+)>/gi;
|
|
455
|
+
let ttsMatch;
|
|
456
|
+
while ((ttsMatch = ttsRegex.exec(replyMessage)) !== null) {
|
|
457
|
+
console.log("检测到语音回复");
|
|
458
|
+
const isTTS = ttsMatch[1].toLowerCase() === "tts";
|
|
459
|
+
const content = ttsMatch[2].trim();
|
|
460
|
+
if (isTTS) {
|
|
461
|
+
ttsMessages.push({ text: content, model: currentModel });
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
currentModel = content;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// 组合消息,添加到replymessages数组中
|
|
468
|
+
replymessages.push({
|
|
469
|
+
text: replyMessage.replace(imageRegex, "").replace(ttsRegex, "").trim(),
|
|
470
|
+
images: imgUrls,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
// 记录对话
|
|
474
|
+
await redisClient.addAIChatHistory(e.guid, res.choices?.[0].message);
|
|
475
|
+
// 发送消息
|
|
476
|
+
if (isOpenTTSReply === "1") {
|
|
477
|
+
// 提前发送语音消息避免等待时间过长
|
|
478
|
+
ttsClient = new TTSClient();
|
|
479
|
+
console.log("发送语音", ttsMessages);
|
|
480
|
+
for (const ttsMessage of ttsMessages) {
|
|
481
|
+
sendTTSMessage(ttsMessage.text, e, ttsClient, ttsMessage.model);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// 处理消息
|
|
485
|
+
for (let i = 0; i < replymessages.length; i++) {
|
|
486
|
+
const { text, images } = replymessages[i];
|
|
487
|
+
const components = i === 0 ? [Mention(e.UserId)] : [];
|
|
488
|
+
components.push(Text(text));
|
|
489
|
+
for (const img of images) {
|
|
490
|
+
const isNetworkImage = /^https?:\/\//.test(img);
|
|
491
|
+
let finalImg = img;
|
|
492
|
+
// 👉 scbbs 平台处理(下载再上传)
|
|
493
|
+
if (e.Platform == "scbbs" && isNetworkImage) {
|
|
494
|
+
const uploadedUrl = await uploadImageToR2(img.replace("https://imgen.x.ai", value.xaiImgProxy || "https://imgen.x.ai"));
|
|
495
|
+
finalImg = uploadedUrl || null;
|
|
496
|
+
}
|
|
497
|
+
// 👉 审核逻辑(只在开启时执行)
|
|
498
|
+
if (isOpenR18 !== "1" && finalImg) {
|
|
499
|
+
try {
|
|
500
|
+
if (res?.conclusion === "不合规") {
|
|
501
|
+
components.push(ImageFile(fileUrl));
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
catch (err) { }
|
|
506
|
+
}
|
|
507
|
+
// 👉 正常图片逻辑
|
|
508
|
+
if (finalImg) {
|
|
509
|
+
const imageComponent = isNetworkImage
|
|
510
|
+
? ImageURL(finalImg)
|
|
511
|
+
: ImageFile(finalImg);
|
|
512
|
+
components.push(imageComponent);
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
// 上传失败 fallback
|
|
516
|
+
components.push(ImageFile(fileUrl));
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
// 随机 1-3 秒延迟
|
|
520
|
+
const delay = Math.random() * 2000 + 1000;
|
|
521
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
522
|
+
await message.send(format(...components));
|
|
523
|
+
}
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
console.error("AI回复出错", error);
|
|
528
|
+
message.send(format(Text("AI回复出错了呢~\n" + error)));
|
|
529
|
+
// 出错清空会话,防止异常
|
|
530
|
+
delete sessionKey[e.guid];
|
|
531
|
+
next();
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
async function sendTTSMessage(message, e, ttsClient, modelAndEmotion) {
|
|
535
|
+
const [model, emotion] = modelAndEmotion.split("-");
|
|
536
|
+
console.log("message", message, "model", model, "emotion", emotion);
|
|
537
|
+
await ttsClient.setModel(model || "派蒙", emotion || "默认");
|
|
538
|
+
// 发送TTS语音
|
|
539
|
+
ttsClient.getAudio(message).then(async (audio) => {
|
|
540
|
+
console.log("audio", audio);
|
|
541
|
+
if (audio) {
|
|
542
|
+
try {
|
|
543
|
+
if (e.Platform == "onebot") {
|
|
544
|
+
const [client] = useClient(e, API);
|
|
545
|
+
await client.sendMessage({
|
|
546
|
+
message_type: e.value.message_type,
|
|
547
|
+
group_id: e.value.group_id ?? undefined,
|
|
548
|
+
message: [
|
|
549
|
+
{
|
|
550
|
+
type: "record",
|
|
551
|
+
data: {
|
|
552
|
+
file: audio.audio_url,
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
],
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
console.log(error);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
export { rapi_res as default, selects, sendTTSMessage };
|
|
@@ -24,6 +24,11 @@ var res = onResponse(selects, async (e, next) => {
|
|
|
24
24
|
next();
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
|
+
const config = await redisClient.getAIConfig(e.guid);
|
|
28
|
+
// if (config.rapi) {
|
|
29
|
+
// next();
|
|
30
|
+
// return;
|
|
31
|
+
// }
|
|
27
32
|
const complexOutputIsOpen = await redisClient.getComplexOutput(e.guid); // 复杂输出
|
|
28
33
|
const affectionIsOpen = await redisClient.getAffectionSwitch(e.guid); // 好感度开关
|
|
29
34
|
const isOpenTTSReply = await redisClient.getTTSResponseSwitch(e.guid); // TTS回复开关
|
|
@@ -34,7 +39,6 @@ var res = onResponse(selects, async (e, next) => {
|
|
|
34
39
|
const skills = loadSkills(); // 加载技能列表
|
|
35
40
|
// 创建消息
|
|
36
41
|
const [message] = useMessage(e);
|
|
37
|
-
const config = await redisClient.getAIConfig(e.guid);
|
|
38
42
|
const historyMessages = await redisClient.getAIChatHistory(e.guid);
|
|
39
43
|
const affections = await redisClient.getAffectionLevelAll(e.guid);
|
|
40
44
|
const imgs = [];
|
|
@@ -127,6 +131,7 @@ ${botName}:[]
|
|
|
127
131
|
当前群号:${e.guid}
|
|
128
132
|
当前群名称:${e.GroupName || "无"}
|
|
129
133
|
当前聊天平台:${e.Platform}
|
|
134
|
+
当前框架:alemonjs
|
|
130
135
|
${e.ClientError ? `当前错误信息:${e.ClientError}` : ""}
|
|
131
136
|
`;
|
|
132
137
|
// 如果没有配置AI,则不处理
|
|
@@ -254,11 +259,7 @@ ${e.ClientError ? `当前错误信息:${e.ClientError}` : ""}
|
|
|
254
259
|
let messages = [
|
|
255
260
|
{
|
|
256
261
|
role: "system",
|
|
257
|
-
content: systemPrompt
|
|
258
|
-
},
|
|
259
|
-
{
|
|
260
|
-
role: "system",
|
|
261
|
-
content: config.systemPrompt || "",
|
|
262
|
+
content: systemPrompt + `\n性格约束:${config.systemPrompt}`,
|
|
262
263
|
},
|
|
263
264
|
...historyMessages,
|
|
264
265
|
{ ...usermessage },
|
package/lib/routes/commands.js
CHANGED
|
@@ -20,7 +20,7 @@ var commands = defineRouter([
|
|
|
20
20
|
},
|
|
21
21
|
// setting (包含大量子命令)
|
|
22
22
|
{
|
|
23
|
-
regular: Regular.or(/(\/|#)添加ai$/i, /(\/|#)添加ai\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/i, /(\/|#)(切换)(ai|配置) ?(.*)/i, /(\/|#)添加提示词$/i, /(\/|#)添加提示词 (\S+) ([\s\S]+)/i, /(\/|#)(删除)(提示词|提示词列表) ?(.*)/i, /(\/|#)(切换|设置)提示词 ?(.*)$/i, /(\/|#)清空提示词$/i, /(\/|#)删除ai\s(\S+)$/i, /(\/|#)(开启|启用|关闭|禁用)好感度$/i, /(\/|#)(开启|关闭)(全部)?聊天$/i, /(\/|#)(开启|关闭)语音回复$/i, /(\/|#)(开启|关闭)(MCP)?工具$/i, /(\/|#)切换图床(.*)$/i, /(\/|#)修改 ?(.+) ?模型 ?(.+)$/i, /(\/|#)(关闭|开启)复杂(输出|回复)$/i, /(\/|#)(关闭|禁止|不可以|不允许|开启|可以|允许)(涩涩|色色)$/i, /(\/|#)切换模型(.*)$/i, /(\/|#)(关闭|开启)主动(搭话|回复)$/i, /(\/|#)测试$/i),
|
|
23
|
+
regular: Regular.or(/(\/|#)添加ai$/i, /(\/|#)添加ai\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/i, /(\/|#)(切换)(ai|配置) ?(.*)/i, /(\/|#)添加提示词$/i, /(\/|#)添加提示词 (\S+) ([\s\S]+)/i, /(\/|#)(删除)(提示词|提示词列表) ?(.*)/i, /(\/|#)(切换|设置)提示词 ?(.*)$/i, /(\/|#)清空提示词$/i, /(\/|#)删除ai\s(\S+)$/i, /(\/|#)(开启|启用|关闭|禁用)好感度$/i, /(\/|#)(开启|关闭)(全部)?聊天$/i, /(\/|#)(开启|关闭)语音回复$/i, /(\/|#)(开启|关闭)(MCP)?工具$/i, /(\/|#)切换图床(.*)$/i, /(\/|#)修改 ?(.+) ?模型 ?(.+)$/i, /(\/|#)(关闭|开启)复杂(输出|回复)$/i, /(\/|#)(关闭|禁止|不可以|不允许|开启|可以|允许)(涩涩|色色)$/i, /(\/|#)切换模型(.*)$/i, /(\/|#)(关闭|开启)主动(搭话|回复)$/i, /(\/|#)(开启|关闭)深度思考$/i, /(\/|#)(设置|修改)(ai)?(.*)rapi模式(开启|关闭)$/i, /(\/|#)测试$/i),
|
|
24
24
|
handler: lazy(() => import('../response/setting/res.js')),
|
|
25
25
|
},
|
|
26
26
|
// affection
|
|
@@ -38,6 +38,11 @@ var commands = defineRouter([
|
|
|
38
38
|
regular: /.*/,
|
|
39
39
|
handler: lazy(() => import('../response/zreply/res.js')),
|
|
40
40
|
},
|
|
41
|
+
// fallback: zreply - 最低优先级,处理普通对话
|
|
42
|
+
// {
|
|
43
|
+
// regular: /.*/,
|
|
44
|
+
// handler: lazy(() => import("../response/zreply/rapi.res")),
|
|
45
|
+
// },
|
|
41
46
|
]);
|
|
42
47
|
|
|
43
48
|
export { commands as default };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alemonjs-aichat",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.22",
|
|
4
4
|
"description": "alemonjs-aichat",
|
|
5
5
|
"author": "suancaixianyu",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@alemonjs/bubble": "^2.1.8",
|
|
20
20
|
"@alemonjs/db": "^0.0.16",
|
|
21
|
-
"@alemonjs/onebot": "^2.1.
|
|
21
|
+
"@alemonjs/onebot": "^2.1.14",
|
|
22
22
|
"@types/node": "^22",
|
|
23
|
-
"alemonjs": "2.1.
|
|
23
|
+
"alemonjs": "2.1.56",
|
|
24
24
|
"cssnano": "^7",
|
|
25
25
|
"jsxp": "^1.3.0",
|
|
26
26
|
"lvyjs": "^0.2.25",
|
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
"@aws-sdk/client-s3": "^3.975.0",
|
|
55
55
|
"@aws-sdk/s3-request-presigner": "^3.975.0",
|
|
56
56
|
"ai": "^6.0.105",
|
|
57
|
-
"alemonjs-aichat": "^1.0.
|
|
57
|
+
"alemonjs-aichat": "^1.0.22",
|
|
58
58
|
"mime-types": "^3.0.2",
|
|
59
|
-
"openai": "^6.
|
|
59
|
+
"openai": "^6.34.0",
|
|
60
60
|
"sharp": "^0.34.5",
|
|
61
61
|
"zod": "^4.3.6"
|
|
62
62
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: add-skill
|
|
3
|
+
description: 添加新技能, 例如根据用户的需求创建一个新的技能, 或者将现有技能进行组合形成一个新的技能
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Add Skill
|
|
7
|
+
|
|
8
|
+
添加新技能的技能。
|
|
9
|
+
技能路径有两个, 一个是内置的技能, 在软件包中的`./skills`目录下, 另一个是用户自定义的技能, 在用户项目中的`./skills`目录下。用户可以在用户项目中的`./skills`目录下创建新的技能, 也可以将内置的技能进行组合形成一个新的技能。
|
|
10
|
+
也可以通过alemon.config.yaml文件中的aichat.skills_dir字段来指定用户自定义技能的目录, 默认为`["./skills"]`。
|
|
11
|
+
|
|
12
|
+
所有新建的技能必须要新建一个目录, 例如`./skills/my-skill`, 在该目录下创建一个`SKILL.md`文件, 并将技能的名称和描述写入文件中, 如果需要使用工具函数, 则在该目录下创建一个`<tool>.js`文件, 并将工具函数写入文件中。
|
|
13
|
+
脚本文件需要能够通过命令行直接执行, 并支持传参, 例如`node <tool>.js <arg1> <arg2>`, 脚本必须是执行后就能得到结果的, 不能是需要用户交互的, 例如`node <tool>.js`后需要用户输入一些内容才能得到结果的脚本是不允许的。
|
|
14
|
+
|
|
15
|
+
此外脚本必须写清楚输入输出的格式, 例如输入是一个字符串, 输出是一个字符串, 输入是一个文件路径, 输出是一个文件路径等, 以便AI能够正确地使用工具函数。
|
|
16
|
+
|
|
17
|
+
脚本必须为JavaScript文件, 以便AI能够正确地使用工具函数, 其他语言的脚本文件是不允许的。
|
|
18
|
+
|
|
19
|
+
## Workflow
|
|
20
|
+
|
|
21
|
+
1. 接收用户提供的技能需求
|
|
22
|
+
2. 根据用户的需求, 判断是创建一个新的技能还是将现有技能进行组合形成一个新的技能
|
|
23
|
+
- 如果是创建一个新的技能, 则在用户项目中的`./skills`目录下创建一个新的技能文件, 并将技能的名称和描述写入文件中
|
|
24
|
+
- 如果是将现有技能进行组合形成一个新的技能, 则在用户项目中的`./skills`目录下创建一个新的技能文件, 并将组合后的技能的名称和描述写入文件中
|
|
25
|
+
3. 将新技能的名称和描述返回给用户
|
|
26
|
+
|
|
27
|
+
## Output Format
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
技能名称: <技能名称>
|
|
31
|
+
技能描述: <技能描述>
|
|
32
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: send-file
|
|
3
|
+
description: 发送文件给用户, 支持多种文件类型, 包括文本、图片、音频和视频等, 可以通过指定文件路径或URL来发送文件
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Send File Skill
|
|
7
|
+
|
|
8
|
+
发送文件给用户, 支持多种文件类型, 包括文本、图片、音频和视频等, 可以通过指定文件路径或URL来发送文件
|
|
9
|
+
目前并没有适配各种平台的发送接口, 因此需要使用s3将文件上传到服务器, 然后将文件的URL发送给用户, 用户可以通过URL下载文件
|
|
10
|
+
|
|
11
|
+
优先检查是否有s3相关的技能, 如果有则使用s3技能上传文件并获取URL
|
|
12
|
+
|
|
13
|
+
# Workflow
|
|
14
|
+
|
|
15
|
+
1. 接收用户需要发送的文件信息, 包括文件类型、文件路径或URL等
|
|
16
|
+
2. 如果是本地文件路径, 将文件上传到s3服务器, 获取文件的URL
|
|
17
|
+
3. 将文件的URL发送给用户, 用户可以通过URL下载文件
|
|
18
|
+
4. 如果需要, 可以在发送文件的同时附加一些文本信息, 例如文件的描述或使用说明等
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: use-alemon
|
|
3
|
+
description: 调用alemonjs框架的工具, 需要配合`EvalCode`工具使用, 来执行alemonjs框架的代码, 达到调用alemonjs框架的工具的目的.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Use Alemon Skill
|
|
7
|
+
|
|
8
|
+
此处的技能都依赖`EvalCode`工具, 执行alemonjs框架的代码, 来达到调用alemonjs框架的工具的目的.
|
|
9
|
+
|
|
10
|
+
## 1. 将本地文件上传到s3, 获取文件链接
|
|
11
|
+
|
|
12
|
+
```javascript
|
|
13
|
+
import { uploadImageToR2 } from "@src/s3";
|
|
14
|
+
const filePath = "本地文件路径, 相对或绝对路径均可";
|
|
15
|
+
const fileUrl = await uploadImageToR2(filePath);
|
|
16
|
+
```
|