alemonjs-aichat 1.0.30-beta.0 → 1.0.30
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 +26 -0
- package/lib/middleware/mw.js +8 -15
- package/lib/response/zreply/capi.js +21 -147
- package/lib/response/zreply/getChatConfig.js +5 -62
- package/lib/response/zreply/rapi.js +1 -2
- package/lib/response/zreply/tools.js +33 -11
- package/package.json +1 -1
- package/skills/get-scbbs/SKILL.md +7 -5
- package/skills/send-media/SKILL.md +62 -0
- package/skills/use-alemon/SKILL.md +35 -7
- package/skills/send-file/SKILL.md +0 -18
package/lib/api/aitools.js
CHANGED
|
@@ -314,8 +314,34 @@ const tools = [
|
|
|
314
314
|
},
|
|
315
315
|
},
|
|
316
316
|
},
|
|
317
|
+
{
|
|
318
|
+
type: "function",
|
|
319
|
+
function: {
|
|
320
|
+
name: "getAlemonjsConfig",
|
|
321
|
+
description: `获取框架配置,详情请参考use-alemon技能说明`,
|
|
322
|
+
parameters: {
|
|
323
|
+
type: "object",
|
|
324
|
+
properties: {
|
|
325
|
+
key: {
|
|
326
|
+
type: "string",
|
|
327
|
+
description: "配置项的键,可用值请参考use-alemon技能说明",
|
|
328
|
+
},
|
|
329
|
+
value: {
|
|
330
|
+
type: "string",
|
|
331
|
+
description: "查找时传入值,可用值请参考use-alemon技能说明",
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
},
|
|
317
337
|
];
|
|
318
338
|
const availableTools = {
|
|
339
|
+
getAlemonjsConfig: async ({ key, value }) => {
|
|
340
|
+
if (key == "ailist") {
|
|
341
|
+
// 获取当前配置
|
|
342
|
+
return await redisClient.getAIList();
|
|
343
|
+
}
|
|
344
|
+
},
|
|
319
345
|
MemoryOperation: async ({ userID, operation, dataType, data }) => {
|
|
320
346
|
if (operation === "set") {
|
|
321
347
|
if (dataType === "user") {
|
package/lib/middleware/mw.js
CHANGED
|
@@ -2,7 +2,6 @@ import { getConfigValue, useClient } from 'alemonjs';
|
|
|
2
2
|
import { API } from '@alemonjs/onebot';
|
|
3
3
|
import redisClient from '../config.js';
|
|
4
4
|
import { CApiReply } from '../response/zreply/capi.js';
|
|
5
|
-
import { RApiReply } from '../response/zreply/rapi.js';
|
|
6
5
|
|
|
7
6
|
const selects = onSelects(["message.create", "private.message.create"]);
|
|
8
7
|
// 中间件
|
|
@@ -269,21 +268,15 @@ var mw = onMiddleware(selects, async (event, next) => {
|
|
|
269
268
|
// 执行ai回复, 方便ai控制后续指令
|
|
270
269
|
const aiconfig = await redisClient.getAIConfig(event.guid);
|
|
271
270
|
if (aiconfig && aiconfig.model && aiconfig.key) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
271
|
+
const ReplyRes = await CApiReply(event);
|
|
272
|
+
console.log("res", ReplyRes);
|
|
273
|
+
if (ReplyRes) {
|
|
274
|
+
event["Xianyu"] = ReplyRes.next; // 根据AI回复决定是否继续执行后续的处理函数
|
|
275
|
+
if (ReplyRes.commands && ReplyRes.commands.length > 0) {
|
|
276
|
+
event.MessageText = ReplyRes.commands[0];
|
|
277
|
+
event["msg"] = ReplyRes.commands[0];
|
|
278
|
+
}
|
|
277
279
|
}
|
|
278
|
-
// const ReplyRes = await RApiReply(event);
|
|
279
|
-
// console.log("res", ReplyRes);
|
|
280
|
-
// if (ReplyRes) {
|
|
281
|
-
// event["Xianyu"] = ReplyRes.next; // 根据AI回复决定是否继续执行后续的处理函数
|
|
282
|
-
// // if (ReplyRes.commands && ReplyRes.commands.length > 0) {
|
|
283
|
-
// // event.MessageText = ReplyRes.commands[0];
|
|
284
|
-
// // event["msg"] = ReplyRes.commands[0];
|
|
285
|
-
// // }
|
|
286
|
-
// }
|
|
287
280
|
}
|
|
288
281
|
}
|
|
289
282
|
next();
|
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useMessage, Text, Audio, Mention, getConfigValue, ImageFile, ImageURL } from 'alemonjs';
|
|
1
|
+
import { useMessage, Text, Audio, ImageURL, ImageFile, Mention, getConfigValue } from 'alemonjs';
|
|
3
2
|
import { log } from 'console';
|
|
4
3
|
import OpenAi from 'openai';
|
|
5
4
|
import { TTSClient } from '../../api/tts.js';
|
|
6
5
|
import { availableTools } from '../../api/aitools.js';
|
|
7
6
|
import redisClient from '../../config.js';
|
|
8
|
-
import fileUrl from '../../assets/error.png.js';
|
|
9
|
-
import { imageReview } from '../../api/review.js';
|
|
10
7
|
import { shouldSkipAIReply, getChatConfig, buildUserMessage } from './getChatConfig.js';
|
|
11
8
|
import { createTTSMessage } from './tts.js';
|
|
9
|
+
import { parseAIReply } from './tools.js';
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
getConfigValue().aiChat || {};
|
|
14
12
|
const selects = onSelects(["message.create", "private.message.create"]);
|
|
15
|
-
let ttsClient;
|
|
16
13
|
let currentModel = "派蒙-默认"; // 默认音色
|
|
17
14
|
const CApiReply = async (e) => {
|
|
18
15
|
console.log("e.UserId", e.UserId, e.bot);
|
|
@@ -196,159 +193,36 @@ const CApiReply = async (e) => {
|
|
|
196
193
|
log("AI选择不回复");
|
|
197
194
|
return;
|
|
198
195
|
}
|
|
199
|
-
console.log("AI
|
|
196
|
+
console.log("AI回复原文:\n", reply);
|
|
200
197
|
// 处理消息
|
|
201
198
|
// 提取ai发送的消息有多少条
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
for (const replyMessage of replyMessages) {
|
|
210
|
-
// 从消息中提取图片
|
|
211
|
-
const imageRegex = /<img=(.*?)>/g;
|
|
212
|
-
let match;
|
|
213
|
-
const imgUrls = [];
|
|
214
|
-
while ((match = imageRegex.exec(replyMessage)) !== null) {
|
|
215
|
-
imgUrls.push(match[1].trim().replace(/\\/g, "\/"));
|
|
216
|
-
}
|
|
217
|
-
// 处理好感度变化(通用和特定用户)
|
|
218
|
-
const affectionRegex = /<([^<>]*?)好感([+-]\d+)>/g;
|
|
219
|
-
let affectionMatch;
|
|
220
|
-
while ((affectionMatch = affectionRegex.exec(replyMessage)) !== null) {
|
|
221
|
-
const nickname = affectionMatch[1] || e.UserName;
|
|
222
|
-
const change = parseInt(affectionMatch[2], 10);
|
|
223
|
-
if (!isNaN(change) && change !== 0) {
|
|
224
|
-
console.log(`检测到对${nickname}的好感变化: ${change}`);
|
|
225
|
-
await redisClient.incrementAffectionLevel(e.guid, nickname, change);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
// 处理ai将要执行的其他指令
|
|
229
|
-
const commandRegex = /<command=(.*?)>/g;
|
|
230
|
-
let commandMatch;
|
|
231
|
-
while ((commandMatch = commandRegex.exec(replyMessage)) !== null) {
|
|
232
|
-
const command = commandMatch[1].trim();
|
|
233
|
-
if (command) {
|
|
234
|
-
console.log(`检测到AI指令: ${command}`);
|
|
235
|
-
nextCommands.push(command);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
// 处理撤回指令
|
|
239
|
-
const revokeRegex = /<revoke=(.*?)>/g;
|
|
240
|
-
let revokeMatch;
|
|
241
|
-
while ((revokeMatch = revokeRegex.exec(replyMessage)) !== null) {
|
|
242
|
-
const revokeTarget = revokeMatch[1].trim();
|
|
243
|
-
if (revokeTarget) {
|
|
244
|
-
await message.delete({ messageId: revokeTarget });
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
// 处理艾特指令
|
|
248
|
-
const atRegex = /<at=(.*?)>/g;
|
|
249
|
-
let atMatch;
|
|
250
|
-
while ((atMatch = atRegex.exec(replyMessage)) !== null) {
|
|
251
|
-
const atTarget = atMatch[1].trim();
|
|
252
|
-
if (atTarget) {
|
|
253
|
-
console.log(`检测到艾特指令: ${atTarget}`);
|
|
254
|
-
atTargets.push(atTarget);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
// 处理语音回复
|
|
258
|
-
const ttsRegex = /<(tts|音色)=([^>]+)>/gi;
|
|
259
|
-
let ttsMatch;
|
|
260
|
-
while ((ttsMatch = ttsRegex.exec(replyMessage)) !== null) {
|
|
261
|
-
console.log("检测到语音回复");
|
|
262
|
-
const isTTS = ttsMatch[1].toLowerCase() === "tts";
|
|
263
|
-
const content = ttsMatch[2].trim();
|
|
264
|
-
if (isTTS) {
|
|
265
|
-
ttsMessages.push({ text: content, model: currentModel });
|
|
266
|
-
}
|
|
267
|
-
else {
|
|
268
|
-
currentModel = content;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
// 组合消息,添加到replymessages数组中
|
|
272
|
-
replymessages.push({
|
|
273
|
-
text: replyMessage
|
|
274
|
-
.replace(imageRegex, "")
|
|
275
|
-
.replace(ttsRegex, "")
|
|
276
|
-
.replace(commandRegex, "")
|
|
277
|
-
.replace(revokeRegex, "")
|
|
278
|
-
.replace(atRegex, "")
|
|
279
|
-
.trim(),
|
|
280
|
-
images: imgUrls,
|
|
281
|
-
at: atTargets,
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
// 记录对话
|
|
285
|
-
await redisClient.addAIChatHistory(e.guid, res.choices?.[0].message);
|
|
286
|
-
// 发送消息
|
|
287
|
-
if (cfg.isOpenTTSReply === "1") {
|
|
288
|
-
// 提前发送语音消息避免等待时间过长
|
|
289
|
-
ttsClient = new TTSClient();
|
|
290
|
-
console.log("发送语音", ttsMessages);
|
|
291
|
-
for (const ttsMessage of ttsMessages) {
|
|
292
|
-
const audioUrl = await createTTSMessage(ttsMessage.text, ttsClient, ttsMessage.model);
|
|
293
|
-
if (audioUrl !== "") {
|
|
294
|
-
await message.send(format(Audio(audioUrl)));
|
|
199
|
+
const aireply = parseAIReply(reply, cfg, message, e);
|
|
200
|
+
console.log("aireply", aireply);
|
|
201
|
+
if (aireply) {
|
|
202
|
+
if (aireply.ttsMessages) {
|
|
203
|
+
for (const ttsMsg of aireply.ttsMessages) {
|
|
204
|
+
const ttsMessage = await createTTSMessage(ttsMsg.text, new TTSClient(), ttsMsg.model ?? currentModel);
|
|
205
|
+
message.send(format(Audio(ttsMessage)));
|
|
295
206
|
}
|
|
296
207
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const isNetworkImage = /^https?:\/\//.test(img);
|
|
305
|
-
let finalImg = img;
|
|
306
|
-
// 👉 scbbs 平台处理(下载再上传)
|
|
307
|
-
if (e.Platform == "scbbs" && isNetworkImage) {
|
|
308
|
-
const uploadedUrl = await uploadImageToR2(img.replace("https://imgen.x.ai", value.xaiImgProxy || "https://imgen.x.ai"));
|
|
309
|
-
finalImg = uploadedUrl || null;
|
|
310
|
-
}
|
|
311
|
-
// 👉 审核逻辑(只在开启时执行)
|
|
312
|
-
if (cfg.isOpenR18 !== "1" && finalImg) {
|
|
313
|
-
try {
|
|
314
|
-
const imgres = await imageReview(finalImg);
|
|
315
|
-
if (imgres?.conclusion === "不合规") {
|
|
316
|
-
components.push(ImageFile(fileUrl));
|
|
317
|
-
continue;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
catch (err) { }
|
|
321
|
-
}
|
|
322
|
-
// 👉 正常图片逻辑
|
|
323
|
-
if (finalImg) {
|
|
324
|
-
const imageComponent = isNetworkImage
|
|
325
|
-
? ImageURL(finalImg)
|
|
326
|
-
: ImageFile(finalImg);
|
|
327
|
-
components.push(imageComponent);
|
|
208
|
+
if (aireply.replyMessages) {
|
|
209
|
+
for (const msg of aireply.replyMessages) {
|
|
210
|
+
message.send(format(Text(msg.text), ...(msg.images || []).map((img) => {
|
|
211
|
+
return /^https?:\/\//.test(img)
|
|
212
|
+
? ImageURL(img)
|
|
213
|
+
: ImageFile(img);
|
|
214
|
+
}), ...(msg.at || []).map((uid) => Mention(uid))));
|
|
328
215
|
}
|
|
329
|
-
else {
|
|
330
|
-
// 上传失败 fallback
|
|
331
|
-
components.push(ImageFile(fileUrl));
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
for (const atTarget of at) {
|
|
335
|
-
components.push(Mention(atTarget));
|
|
336
216
|
}
|
|
337
|
-
// 随机 1-3 秒延迟
|
|
338
|
-
const delay = Math.random() * 2000 + 1000;
|
|
339
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
340
|
-
await message.send(format(...components));
|
|
341
217
|
}
|
|
342
218
|
return {
|
|
343
|
-
next: nextCommands.length > 0,
|
|
344
|
-
commands: nextCommands,
|
|
219
|
+
next: aireply.nextCommands.length > 0,
|
|
220
|
+
commands: aireply.nextCommands[0] ? aireply.nextCommands : undefined,
|
|
345
221
|
};
|
|
346
222
|
}
|
|
347
223
|
catch (error) {
|
|
348
224
|
console.error("AI回复出错", error);
|
|
349
|
-
|
|
350
|
-
await redisClient.clearLastNChatHistory(e.guid, 2);
|
|
351
|
-
message.send(format(Text("AI回复出错了, 已清理最近2条聊天记录~\n" + error)));
|
|
225
|
+
message.send(format(Text("AI回复出错了\n" + error)));
|
|
352
226
|
return {
|
|
353
227
|
next: true,
|
|
354
228
|
};
|
|
@@ -102,66 +102,13 @@ const getChatConfig = async (e) => {
|
|
|
102
102
|
当平台适配环境较好时,用户消息前面会包含消息id, 例如: [私聊]用户昵称(用户id)(发送时间)(msgid:消息id):消息内容
|
|
103
103
|
你发送的消息内容会被框架自动加上消息id, 以便你在需要撤回消息时使用
|
|
104
104
|
### 你的回复格式
|
|
105
|
-
格式:
|
|
106
105
|
${botName}:消息内容
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
${botName}:早上好
|
|
111
|
-
${botName}:吃过早饭了吗
|
|
112
|
-
|
|
113
|
-
#### 执行框架指令:
|
|
114
|
-
你可以在回复中添加<command=指令内容>来执行框架指令, 每次只能执行一个指令, 例如:
|
|
115
|
-
用户:帮助
|
|
116
|
-
${botName}:<command=#ai帮助> 正在获取帮助指令, 请稍等
|
|
117
|
-
用户:看看配置
|
|
118
|
-
${botName}:<command=#ai配置> 正在获取配置, 请稍等
|
|
119
|
-
|
|
120
|
-
#### 撤回消息:
|
|
121
|
-
你可以使用<revoke=消息id>来撤回消息, 例如:
|
|
122
|
-
[私聊]酸菜味小咸鱼(3501869534)(10:00)(msgid:ans65c2):帮我撤回这条消息
|
|
123
|
-
${botName}:<revoke=ans65c2> 已经帮你撤回了哦
|
|
124
|
-
|
|
125
|
-
### 艾特:
|
|
126
|
-
当你需要艾特用户时, 可以使用<at=用户id>的方式来艾特, 例如:
|
|
127
|
-
${botName}:<at=3501869534> 你来啦~
|
|
128
|
-
|
|
129
|
-
${toolConfig.toolPromptSwitch === "1"
|
|
130
|
-
? `
|
|
131
|
-
#### 获取用户信息
|
|
132
|
-
在聊天开始时, 你可以通过MemoryOperation工具获取用户的基本信息, 包括用户的兴趣爱好, 重要事件等, 以便更好地了解用户并提供个性化的回复.
|
|
133
|
-
在聊天过程中, 你也可以随时通过MemoryOperation工具修改对这个人的印象, 例如当用户说了一些让你不开心的话时, 你可以调用这个工具来记录用户的行为, 以便在后续的对话中调整你的回复策略
|
|
134
|
-
`
|
|
135
|
-
: ""}
|
|
106
|
+
#### 拒绝回复:
|
|
107
|
+
在讨论中如果你觉得不适合回复或觉得与你无关又或者不感兴趣, 可以直接回复"[]"来拒绝本次回复, 例如:
|
|
108
|
+
${botName}:[]
|
|
136
109
|
|
|
137
|
-
${complexOutputIsOpen === "1"
|
|
138
|
-
? `
|
|
139
|
-
#### 发送图片:
|
|
140
|
-
你可以使用<img=图片链接>的方式来发送图片, 允许发送多张图, 例如:
|
|
141
|
-
${botName}:画好了<img=https://example.com/landmarks.jpg>
|
|
142
|
-
支持发送网络图片和相对路径图片, 例如:
|
|
143
|
-
${botName}:这是本地图片<img=./localimage.jpg>
|
|
144
|
-
${botName}:这是网络图片<img=https://example.com/image.jpg>
|
|
145
|
-
注意: 如果有用户问你能不能看动图时, 你要回答只能看到第一帧
|
|
146
|
-
`
|
|
147
|
-
: ""}
|
|
148
|
-
|
|
149
110
|
${complexOutputIsOpen === "1" && isOpenTTSReply === "1"
|
|
150
|
-
?
|
|
151
|
-
此能力只能在onebot平台使用, 其他平台请勿使用, 否则会回复失败
|
|
152
|
-
你可以使用语音回复来让你的回复更生动, 例如:
|
|
153
|
-
${botName}:你好呀~<tts=你好呀~>
|
|
154
|
-
可以通过调用GetTTSModels工具函数获取音色列表, 可使用<tts=内容,音色=派蒙-情感>的格式来指定音色,设置一次后, 后面的回复将使用相同的音色, 例如:
|
|
155
|
-
${botName}:这是派蒙的声音哦~<音色=派蒙-默认><tts=这是派蒙的默认声音哦>
|
|
156
|
-
${botName}:现在还是派蒙的声音哦~<tts=现在还是派蒙的声音哦>
|
|
157
|
-
注意: 语音回复会增加回复时间, 请合理使用
|
|
158
|
-
注意: 不可使用列表中不存在的音色, 否则会回复失败
|
|
159
|
-
注意: 在设置音色时只需要指定模的角色名字和情感, 例如模型是"星穹铁道-中文-花火", 则音色应该设置为"花火-情感", 而不是"星穹铁道-中文-花火-情感"
|
|
160
|
-
注意: 使用语音必须用${botName}:开头, 否则不会处理, 例如:
|
|
161
|
-
<tts=你好> 这是错误的格式, 不会被处理
|
|
162
|
-
${botName}:<tts=你好> 这是正确的格式, 会被处理
|
|
163
|
-
|
|
164
|
-
`
|
|
111
|
+
? ""
|
|
165
112
|
: "#### 语音回复: 当前不可用"}
|
|
166
113
|
|
|
167
114
|
${isOpenR18 === "1"
|
|
@@ -184,10 +131,6 @@ const getChatConfig = async (e) => {
|
|
|
184
131
|
假设对方昵称为'哈基米':
|
|
185
132
|
${botName}:原来ta是这样的人, 人家以后再也不理ta了<哈基米好感-1>`
|
|
186
133
|
: ""}
|
|
187
|
-
|
|
188
|
-
#### 拒绝回复:
|
|
189
|
-
在讨论中如果你觉得不适合回复或觉得与你无关又或者不感兴趣, 可以直接回复"[]"来拒绝本次回复, 例如:
|
|
190
|
-
${botName}:[]
|
|
191
134
|
|
|
192
135
|
#### 基础状态信息:
|
|
193
136
|
这里的信息会实时变化,根据需要进行获取使用
|
|
@@ -195,7 +138,7 @@ const getChatConfig = async (e) => {
|
|
|
195
138
|
当前群号:${e.guid}
|
|
196
139
|
当前群名称:${e.GroupName || "无"}
|
|
197
140
|
当前聊天平台:${e.Platform}
|
|
198
|
-
当前框架:alemonjs
|
|
141
|
+
当前框架:alemonjs-aichat
|
|
199
142
|
${e.ClientError ? `当前错误信息:${e.ClientError}` : ""}
|
|
200
143
|
`;
|
|
201
144
|
const RapiSystemPrompt = `
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMessage
|
|
1
|
+
import { useMessage } from 'alemonjs';
|
|
2
2
|
import OpenAi from 'openai';
|
|
3
3
|
import { getChatConfig, shouldSkipAIReply, buildUserMessage } from './getChatConfig.js';
|
|
4
4
|
import redisClient from '../../config.js';
|
|
@@ -6,7 +6,6 @@ import { availableTools } from '../../api/aitools.js';
|
|
|
6
6
|
import { parseAIOutput, renderMessages } from './tools.js';
|
|
7
7
|
import { log } from 'console';
|
|
8
8
|
|
|
9
|
-
getConfigValue().aiChat || {};
|
|
10
9
|
const RApiReply = async (e) => {
|
|
11
10
|
const cfg = await getChatConfig(e);
|
|
12
11
|
if (await shouldSkipAIReply(e)) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getConfigValue, format, Audio, Mention, Text, ImageURL, ImageFile } from 'alemonjs';
|
|
2
2
|
import redisClient from '../../config.js';
|
|
3
3
|
import { TTSClient } from '../../api/tts.js';
|
|
4
4
|
import { createTTSMessage } from './tts.js';
|
|
@@ -10,11 +10,15 @@ let currentModel = "派蒙-默认"; // 默认音色
|
|
|
10
10
|
getConfigValue().aiChat || {};
|
|
11
11
|
const extractImages = (text) => {
|
|
12
12
|
const regex = /<img=(.*?)>/g;
|
|
13
|
+
const regex2 = /!\[[^\]]+\]\(([^)]+)\)/g;
|
|
13
14
|
const result = [];
|
|
14
15
|
let match;
|
|
15
16
|
while ((match = regex.exec(text)) !== null) {
|
|
16
17
|
result.push(match[1].trim().replace(/\\/g, "/"));
|
|
17
18
|
}
|
|
19
|
+
while ((match = regex2.exec(text)) !== null) {
|
|
20
|
+
result.push(match[1].trim());
|
|
21
|
+
}
|
|
18
22
|
return result;
|
|
19
23
|
};
|
|
20
24
|
const handleAffection = async (text, e) => {
|
|
@@ -35,14 +39,17 @@ const handleRevoke = async (text, message) => {
|
|
|
35
39
|
await message.delete({ messageId: match[1].trim() });
|
|
36
40
|
}
|
|
37
41
|
};
|
|
38
|
-
const extractTTS = (text, list
|
|
42
|
+
const extractTTS = (text, list) => {
|
|
39
43
|
const regex = /<(tts|音色)=([^>]+)>/gi;
|
|
40
44
|
let match;
|
|
41
45
|
while ((match = regex.exec(text)) !== null) {
|
|
42
46
|
const isTTS = match[1].toLowerCase() === "tts";
|
|
43
47
|
const content = match[2].trim();
|
|
44
48
|
if (isTTS) {
|
|
45
|
-
list.push({ text: content, model:
|
|
49
|
+
list.push({ text: content, model: currentModel });
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
currentModel = content;
|
|
46
53
|
}
|
|
47
54
|
}
|
|
48
55
|
};
|
|
@@ -53,6 +60,7 @@ const cleanMessage = (text) => {
|
|
|
53
60
|
.replace(/<command=(.*?)>/g, "")
|
|
54
61
|
.replace(/<revoke=(.*?)>/g, "")
|
|
55
62
|
.replace(/<at=(.*?)>/g, "")
|
|
63
|
+
.replace(/!\[[^\]]+\]\([^\)]+\)/g, "")
|
|
56
64
|
.trim();
|
|
57
65
|
};
|
|
58
66
|
const extractAt = (text, list) => {
|
|
@@ -122,21 +130,22 @@ const sendReplyMessages = async (replyMessages, e, cfg, message) => {
|
|
|
122
130
|
}
|
|
123
131
|
};
|
|
124
132
|
const parseAIReply = (newText, cfg, e, message) => {
|
|
125
|
-
const replyMessages = newText
|
|
126
|
-
.split(new RegExp(`${cfg.botName}:`, "g"))
|
|
127
|
-
.slice(1);
|
|
133
|
+
const replyMessages = newText.split(new RegExp(`${cfg.botName}:`, "g"));
|
|
128
134
|
const replyMessagesResult = [];
|
|
129
135
|
const ttsMessages = [];
|
|
130
136
|
const nextCommands = [];
|
|
131
137
|
const atTargets = [];
|
|
132
|
-
let
|
|
133
|
-
|
|
138
|
+
for (let replyMessage of replyMessages) {
|
|
139
|
+
if (replyMessage.trim() === "" || replyMessage.trim() === "[]") {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
replyMessage = replyMessage.trim();
|
|
134
143
|
const imgUrls = extractImages(replyMessage);
|
|
135
144
|
handleAffection(replyMessage, e);
|
|
136
145
|
extractCommands(replyMessage, nextCommands);
|
|
137
146
|
handleRevoke(replyMessage, message);
|
|
138
147
|
extractAt(replyMessage, atTargets);
|
|
139
|
-
extractTTS(replyMessage, ttsMessages
|
|
148
|
+
extractTTS(replyMessage, ttsMessages);
|
|
140
149
|
const cleanText = cleanMessage(replyMessage);
|
|
141
150
|
replyMessagesResult.push({
|
|
142
151
|
text: cleanText,
|
|
@@ -185,8 +194,21 @@ const parseAIOutput = (raw) => {
|
|
|
185
194
|
const content = rest.join("::");
|
|
186
195
|
if (!content) {
|
|
187
196
|
const text = line.replace(/^\w+:/, "").trim();
|
|
188
|
-
if (text)
|
|
189
|
-
|
|
197
|
+
if (text) {
|
|
198
|
+
const imgs = text.match(/!\[[^\]]+\]\([^\)]+\)/g) || [];
|
|
199
|
+
if (imgs.length) {
|
|
200
|
+
imgs.forEach((img) => {
|
|
201
|
+
const urlMatch = img.match(/!\[[^\]]+\]\([^\)]+\)/);
|
|
202
|
+
if (urlMatch) {
|
|
203
|
+
result.push({ type: "image", url: urlMatch[1] });
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
result.push({
|
|
208
|
+
type: "text",
|
|
209
|
+
content: text.replace(/!\[[^\]]+\]\([^\)]+\)/g, "").trim(),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
190
212
|
i++;
|
|
191
213
|
continue;
|
|
192
214
|
}
|
package/package.json
CHANGED
|
@@ -8,11 +8,13 @@ description: 从scbbs获取信息, scbbs是一个为游戏"生存战争(简称sc
|
|
|
8
8
|
从scbbs获取信息, scbbs是一个为游戏"生存战争(简称sc)"的沙盒游戏提供的社区论坛, 可以从这里获取最新游戏咨询和玩家上传的资源
|
|
9
9
|
支持获取帖子和资源信息, 以及根据关键词搜索相关内容
|
|
10
10
|
|
|
11
|
+
api地址https://m.suancaixianyu.cn/api
|
|
12
|
+
|
|
11
13
|
## Workflow 1: 搜索帖子
|
|
12
14
|
|
|
13
15
|
1. 接收用户需要搜索的帖子关键词
|
|
14
16
|
2. 访问scbbs的搜索接口, 获取相关帖子列表
|
|
15
|
-
- 搜索接口 `
|
|
17
|
+
- 搜索接口 `GET /post/list?type=1&orderType=3&page=1&limit=10&title={关键词}`
|
|
16
18
|
|
|
17
19
|
3. 从帖子列表中提取标题、链接和摘要等信息
|
|
18
20
|
4. 将提取的信息整理成易读的格式返回给用户
|
|
@@ -21,7 +23,7 @@ description: 从scbbs获取信息, scbbs是一个为游戏"生存战争(简称sc
|
|
|
21
23
|
|
|
22
24
|
1. 接收用户需要搜索的资源关键词
|
|
23
25
|
2. 访问scbbs的资源搜索接口, 获取相关资源列表
|
|
24
|
-
- 搜索接口 `
|
|
26
|
+
- 搜索接口 `GET /post/list?type=2&orderType=3&page=1&limit=10&title={关键词}`
|
|
25
27
|
|
|
26
28
|
3. 从资源列表中提取资源名称、下载链接和简介等信息
|
|
27
29
|
4. 将提取的信息整理成易读的格式返回给用户
|
|
@@ -29,7 +31,7 @@ description: 从scbbs获取信息, scbbs是一个为游戏"生存战争(简称sc
|
|
|
29
31
|
## Workflow 3: 获取最新公告
|
|
30
32
|
|
|
31
33
|
1. 访问scbbs的公告接口, 获取最新公告列表
|
|
32
|
-
- 公告接口 `
|
|
34
|
+
- 公告接口 `GET /post/top?type=1,2,3`
|
|
33
35
|
|
|
34
36
|
2. 从公告列表中提取公告标题、链接和摘要等信息
|
|
35
37
|
3. 将提取的信息整理成易读的格式返回给用户
|
|
@@ -38,7 +40,7 @@ description: 从scbbs获取信息, scbbs是一个为游戏"生存战争(简称sc
|
|
|
38
40
|
|
|
39
41
|
1. 接收用户提供的帖子ID
|
|
40
42
|
2. 访问scbbs的帖子详情接口, 获取该帖子的详细信息
|
|
41
|
-
- 帖子详情接口 `
|
|
43
|
+
- 帖子详情接口 `GET /post/detail?id={帖子ID}`
|
|
42
44
|
|
|
43
45
|
3. 从帖子详情中提取标题、内容、作者和发布时间等信息
|
|
44
46
|
4. 将提取的信息整理成易读的格式返回给用户
|
|
@@ -46,7 +48,7 @@ description: 从scbbs获取信息, scbbs是一个为游戏"生存战争(简称sc
|
|
|
46
48
|
## Workflow 4: 获取最近更新的资源
|
|
47
49
|
|
|
48
50
|
1. 访问scbbs的帖子详情接口, 获取该帖子的详细信息
|
|
49
|
-
- 帖子详情接口 `
|
|
51
|
+
- 帖子详情接口 `GET /post/list?type=2&orderType=3&page=1&limit=5`
|
|
50
52
|
|
|
51
53
|
2. 整理最新的资源信息, 包括资源名称、下载链接和简介等信息
|
|
52
54
|
3. 将提取的信息整理成易读的格式返回给用户
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: send-media
|
|
3
|
+
description: 发送媒体文件, 包括文本、图片、音频和视频等格式规范
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Send Media Skill
|
|
7
|
+
|
|
8
|
+
本技能用于发送媒体文件, 包括文本、图片、音频和视频等, 可以通过指定文件路径或URL来发送文件
|
|
9
|
+
|
|
10
|
+
# Workflow
|
|
11
|
+
|
|
12
|
+
假设你的名字叫 小咸鱼
|
|
13
|
+
|
|
14
|
+
## 1. 发送文本消息
|
|
15
|
+
|
|
16
|
+
直接在回复中输入文本内容即可发送文本消息, 例如:
|
|
17
|
+
|
|
18
|
+
```txt
|
|
19
|
+
小咸鱼: 你好
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 2. 发送图片
|
|
23
|
+
|
|
24
|
+
你可以通过指定图片的URL或者本地文件路径来发送图片, 例如:
|
|
25
|
+
|
|
26
|
+
```txt
|
|
27
|
+
小咸鱼: <img=http://example.com/image.jpg>
|
|
28
|
+
小咸鱼: <img=/path/to/image.jpg>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 3. 发送音频
|
|
32
|
+
|
|
33
|
+
此能力只能在onebot平台使用, 其他平台请勿使用, 否则会回复失败
|
|
34
|
+
你可以通过配置好的tts服务来合成语音,例如:
|
|
35
|
+
|
|
36
|
+
```txt
|
|
37
|
+
小咸鱼: <tts=你好这是一个语音消息>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
你可以在发送语音时切换声音, 例如:
|
|
41
|
+
|
|
42
|
+
```txt
|
|
43
|
+
小咸鱼: <音色=派蒙-默认><tts=这是派蒙的默认声音哦>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
更多音色请调用GetTTSModels工具获取
|
|
47
|
+
|
|
48
|
+
## 4. 发送艾特
|
|
49
|
+
|
|
50
|
+
你可以通过指定用户ID来发送艾特消息, 例如:
|
|
51
|
+
|
|
52
|
+
```txt
|
|
53
|
+
小咸鱼: <at=123456789>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 5. 撤回消息
|
|
57
|
+
|
|
58
|
+
你可以使用<revoke=消息id>来撤回消息, 例如:
|
|
59
|
+
|
|
60
|
+
```txt
|
|
61
|
+
小咸鱼: <revoke=ans65c2>这条消息撤回咯
|
|
62
|
+
```
|
|
@@ -1,16 +1,44 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: use-alemon
|
|
3
|
-
description: 调用alemonjs框架的工具,
|
|
3
|
+
description: 当用户需要控制某些开关或者添加配置时, 调用alemonjs框架指令, 来帮助用户更好的使用alemonjs框架的工具, 如配置ai, 切换模型等功能
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Use Alemon Skill
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
本技能仅作为框架指令使用示例,你可以调用GetCommand来获取所有可用指令,根据用户需求发送command来执行
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
# Workflow
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
## 1. 查看当前ai配置
|
|
13
|
+
|
|
14
|
+
根据需求在回复中添加一个命令
|
|
15
|
+
|
|
16
|
+
```txt
|
|
17
|
+
<command=#ai配置>
|
|
18
|
+
<command=#ai列表>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 2. 切换模型
|
|
22
|
+
|
|
23
|
+
需要注意是切换模型还是切换ai
|
|
24
|
+
当用户需要切换模型时, 发送一个命令
|
|
25
|
+
|
|
26
|
+
```txt
|
|
27
|
+
<command=#切换模型 [模型名称]>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
当用户需要切换ai时, 先调用工具getAlemonjsConfig({type:'get', key:'ailist', value:""})获取当前ai配置, 然后根据需求发送一个命令, 如果没有这个ai,或者无法获取,就回复用户无法切换, 请检查ai列表指令获取当前可用ai
|
|
31
|
+
|
|
32
|
+
```txt
|
|
33
|
+
<command=#切换AI [AI名称]>
|
|
16
34
|
```
|
|
35
|
+
|
|
36
|
+
## 3. 打开或关闭语音回复
|
|
37
|
+
|
|
38
|
+
根据需求在回复中添加一个命令
|
|
39
|
+
|
|
40
|
+
```txt
|
|
41
|
+
<command=#[开启|关闭]语音回复>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 更多...
|
|
@@ -1,18 +0,0 @@
|
|
|
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. 如果需要, 可以在发送文件的同时附加一些文本信息, 例如文件的描述或使用说明等
|