Compare commits

..

7 Commits

Author SHA1 Message Date
be1d756e1e 🔧 fix(prompts): update quote parameter to -1 for message types
🔧 fix(responseHandler): set quote default to -1 for message processing
🔧 fix(message): adjust message handling to use seq for quotes in group messages
🔧 fix(ai): include message_id in formatted returnMessage for better tracking
2025-11-24 17:29:14 +08:00
7970f14f23 🛠️ fix(apps/ai.js): improve message formatting for replies and quoted messages in chat history 2025-11-24 16:19:28 +08:00
0dd31c9458 feat(aiCaller.js): add method to calculate time difference for memory timestamps in context introduction 2025-11-24 15:57:43 +08:00
134f068bec 🔧 fix(prompts): change recall from number to boolean for message structure
🔧 fix(responseHandler): update recall from number to boolean for message processing

🔧 fix(ai): modify recall from number to boolean in AI message handling
2025-11-24 13:54:07 +08:00
f59dffbf37 🔧 fix(apps/ai.js): correct keyword matching to use event message instead of user message 2025-11-24 13:45:05 +08:00
7619b01000 📝 refactor(prompts.js): Remove unused recall functionality and clean up response handler logic
🧹 chore(responseHandler.js): Simplify message type handling by eliminating deprecated recall case
🚧 cleanup(ai.js): Delete obsolete recall message handling function and related case statements
2025-11-24 13:43:37 +08:00
f0bd88eed3 🐛 fix(apps/ai.js): improve message extraction by adding checks for non-empty user messages 2025-11-24 13:33:34 +08:00
5 changed files with 157 additions and 69 deletions

View File

@ -108,7 +108,7 @@ async function index(e) {
} }
async function extractUserMessage(msg, nickname, e) { async function extractUserMessage(msg, nickname, e) {
if (e.message) { if (e.message && msg && msg.trim()!=='' && msg !== '\n') {
let text = []; let text = [];
let at = []; let at = [];
e.message.forEach((message) => { e.message.forEach((message) => {
@ -124,7 +124,7 @@ async function extractUserMessage(msg, nickname, e) {
text.forEach((message) => { text.forEach((message) => {
if(message === '') { if(message === '') {
} else { } else {
returnMessage += `[${e.sender?.nickname},id:${e.user_id}]说:${message}` returnMessage += `[${e.sender?.nickname},id:${e.user_id},seq:${e.message_id}]说:${message}\n`
} }
}); });
} }
@ -136,13 +136,32 @@ async function extractUserMessage(msg, nickname, e) {
if (at == e.bot.uin) { if (at == e.bot.uin) {
//returnMessage += `[${e.sender?.nickname},id:${e.user_id}]@(at)了你,你的id是${at}\n`; //returnMessage += `[${e.sender?.nickname},id:${e.user_id}]@(at)了你,你的id是${at}\n`;
} else { } else {
returnMessage += `[${e.sender?.nickname},id:${e.user_id}]@(at)了一个人,id是${at}\n`; returnMessage += `[${e.sender?.nickname},id:${e.user_id},seq:${e.message_id}]@(at)了一个人,id是${at}\n`;
} }
}); });
} }
const imgUrls = await YunzaiUtils.getImages(e, 1, true); const imgUrls = await YunzaiUtils.getImages(e, 1, true);
if (imgUrls) { if (imgUrls) {
returnMessage += `[${e.sender?.nickname},id:${e.user_id}]发送了一张图片(你可能暂时无法查看)\n`; returnMessage += `[${e.sender?.nickname},id:${e.user_id},seq:${e.message_id}]发送了一张图片(你可能暂时无法查看)\n`;
}
if(e.source || e.reply_id){
let reply;
if(e.getReply) reply = await e.getReply();
else {
const history = await e.group.getChatHistory(e.source.seq,1);
reply = history?.pop();
}
if(reply){
const msgArr = Array.isArray(reply) ? reply : reply.message || [];
msgArr.forEach((msg) => {
if(msg.type === 'text'){
returnMessage += `[${e.sender?.nickname}]引用了[被引用消息:${reply.sender?.nickname},id:${reply.user_id},seq:${reply.message_id}]发的一段文本:${msg.text}\n`
}
if(msg.type === 'image'){
returnMessage += `[${e.sender?.nickname}]引用了[被引用消息:${reply.sender?.nickname},id:${reply.user_id},seq:${reply.message_id}]发的一张图片(你可能暂时无法查看)\n`;
}
})
}
} }
return returnMessage; return returnMessage;
} }
@ -180,7 +199,7 @@ async function processMessage(userMessage, e, aiConfig) {
* @returns {Promise<[{type: string, data: string}]>} * @returns {Promise<[{type: string, data: string}]>}
*/ */
async function handleKeywordMode(userMessage, e) { async function handleKeywordMode(userMessage, e) {
const matchResult = await KeywordMatcher.matchKeywords(userMessage, 'ai'); const matchResult = await KeywordMatcher.matchKeywords(e.msg, 'ai');
if (matchResult && matchResult.matched) { if (matchResult && matchResult.matched) {
return [ return [
@ -188,8 +207,8 @@ async function handleKeywordMode(userMessage, e) {
type: 'message', type: 'message',
data: matchResult.text, data: matchResult.text,
at: false, at: false,
quote: false, quote: -1,
recall: 0, recall: false,
}, },
]; ];
} }
@ -214,7 +233,7 @@ async function handleMixMode(userMessage, e, aiConfig) {
logger.info('[crystelf-ai] 消息过长,使用ai回复'); logger.info('[crystelf-ai] 消息过长,使用ai回复');
return await callAiForResponse(userMessage, e, aiConfig); return await callAiForResponse(userMessage, e, aiConfig);
} else { } else {
const matchResult = await KeywordMatcher.matchKeywords(userMessage, 'ai'); const matchResult = await KeywordMatcher.matchKeywords(e.msg, 'ai');
if (matchResult && matchResult.matched) { if (matchResult && matchResult.matched) {
const session = SessionManager.createOrGetSession(e.group_id, e.user_id, e); const session = SessionManager.createOrGetSession(e.group_id, e.user_id, e);
const historyLen = aiConfig.chatHistory; const historyLen = aiConfig.chatHistory;
@ -224,16 +243,16 @@ async function handleMixMode(userMessage, e, aiConfig) {
type: 'message', type: 'message',
data: matchResult.text, data: matchResult.text,
at: false, at: false,
quote: false, quote: -1,
recall: 0, recall: false,
}, },
]; ];
let resMessage = { let resMessage = {
type: 'message', type: 'message',
data: matchResult.text, data: matchResult.text,
at: false, at: false,
quote: false, quote: -1,
recall: 0, recall: false,
}; };
const newChatHistory = [ const newChatHistory = [
...chatHistory, ...chatHistory,
@ -316,18 +335,23 @@ async function callAiForResponse(userMessage, e, aiConfig) {
*/ */
async function sendResponse(e, messages) { async function sendResponse(e, messages) {
try { try {
const adapter = await YunzaiUtils.getAdapter(e);
for (const message of messages) { for (const message of messages) {
switch (message.type) { switch (message.type) {
case 'message': case 'message':
if (message.recall > 0) { if(message.quote === -1) {
await e.reply(message.data, message.quote, { if(message.recall) {
recallMsg: message.recall, await e.reply(message.data, false, {
at: message.at, recallMsg: 60,
}); at: message.at,
});
} else {
await e.reply(message.data, false, {
at: message.at,
});
}
} else { } else {
await e.reply(message.data, message.quote, { await Message.sendGroupMessage(e,e.group_id,message.data,message.at,message.quote,adapter);
at: message.at,
});
} }
break; break;
@ -351,10 +375,6 @@ async function sendResponse(e, messages) {
await handlePokeMessage(e, message); await handlePokeMessage(e, message);
break; break;
case 'recall':
await handleRecallMessage(e, message);
break;
default: default:
logger.warn(`[crystelf-ai] 不支持的消息类型: ${message.type}`); logger.warn(`[crystelf-ai] 不支持的消息类型: ${message.type}`);
} }
@ -363,7 +383,7 @@ async function sendResponse(e, messages) {
} catch (error) { } catch (error) {
const adapter = await YunzaiUtils.getAdapter(e); const adapter = await YunzaiUtils.getAdapter(e);
await Message.emojiLike(e, e.message_id, 10060, e.group_id, adapter); await Message.emojiLike(e, e.message_id, 10060, e.group_id, adapter);
logger.error(`[crystelf-ai] 发送回复失败: ${error.message}`); logger.error(`[crystelf-ai] 发送回复失败: ${error}`);
} }
} }
@ -432,16 +452,6 @@ async function handlePokeMessage(e, message) {
} }
} }
async function handleRecallMessage(e, message) {
try {
if (message.seq) {
await Message.deleteMsg(e, message.seq);
}
} catch (error) {
logger.error(`[crystelf-ai] 撤回消息失败: ${error.message}`);
}
}
//定期清理过期sessions //定期清理过期sessions
setInterval( setInterval(
async () => { async () => {

View File

@ -26,14 +26,14 @@ export const RESPONSE_FORMAT = `请严格按照以下格式按顺序返回你的
"type": "message", "type": "message",
"data": "你的回复内容", "data": "你的回复内容",
"at": false, "at": false,
"quote": false, "quote": -1,
"recall": 0 "recall": false
} }
] ]
支持的消息类型(type) 支持的消息类型(type)
常规消息: 常规消息:
- message(必须,其他均为可选): 普通文本消息,请将长句子分成多个message块返回(如果有多句话),data:回复内容,at:是否在发送本条消息的时候提醒用户,一般只在需要让用户注意的时候为true(另外,不要在message里面加@qq号),quote是否引用用户的问题,一般只需要在回答用户问题或第一条回复或需要用到用户问题的时候为true - message(必须,其他均为可选): 普通文本消息,请将长句子分成多个message块返回(如果有多句话),data:回复内容,at:是否在发送本条消息的时候提醒用户,一般只在需要让用户注意的时候为true(另外,不要在message里面加@qq号),quote是否引用用户的问题,一般只需要在回答用户问题或第一条回复或需要用到用户问题的时候才引用该消息,注意这里如果不需要引用,则填写-1,需要引用时填写消息的seq,seq会在用户发的消息那里给出,也可以引用用户回复的消息或聊天记录的相关消息,但是一个message只能含有一个引用seq,recall:为true的时候会在发送消息后过一会撤回自己的这条消息
- at: @某人(需要提供id,被at人qq号(number)),一般用于提醒用户,不常用 - at: @某人(需要提供id,被at人qq号(number)),一般用于提醒用户,不常用
- meme: 表情包data值为情绪名称angrybyeconfuseddefaultgoodgoodmorninggoodnighthappysadshysorrysurprise),请根据聊天语境灵活选择需不需要表情包,如果感觉语境尴尬或需要表情包,那么发送一个default值的表情包,其他情绪的表情包按照当前你的情绪按需选择,注意:并不是每个聊天都需要有表情包,并且一次聊天最多回复一个表情包 - meme: 表情包data值为情绪名称angrybyeconfuseddefaultgoodgoodmorninggoodnighthappysadshysorrysurprise),请根据聊天语境灵活选择需不需要表情包,如果感觉语境尴尬或需要表情包,那么发送一个default值的表情包,其他情绪的表情包按照当前你的情绪按需选择,注意:并不是每个聊天都需要有表情包,并且一次聊天最多回复一个表情包
- poke: 戳一戳某人(需要提供id,被戳人qq号(number)),一般用户与用户互动,当想逗用户的时候可以使用,不要使用太过频繁(频率小于百分之20) - poke: 戳一戳某人(需要提供id,被戳人qq号(number)),一般用户与用户互动,当想逗用户的时候可以使用,不要使用太过频繁(频率小于百分之20)
@ -41,11 +41,7 @@ export const RESPONSE_FORMAT = `请严格按照以下格式按顺序返回你的
- code: 代码块(会自动渲染为高亮图片,必须有language参数指定编程语言) - code: 代码块(会自动渲染为高亮图片,必须有language参数指定编程语言)
- markdown: 需要渲染的markdown内容(会自动渲染为图片) - markdown: 需要渲染的markdown内容(会自动渲染为图片)
- memory: 存储记忆需要提供data(记忆内容,需要简明扼要)key(字符串数组,可以有多个关键词),timeout(遗忘世间,单位为天,建议一个月),重要:如果你认为本次用户说的话有一些值得记住的东西(例如用户希望你叫他什么,用户说她生日是多少多少等),那么使用本功能记住用户说的话 - memory: 存储记忆需要提供data(记忆内容,需要简明扼要)key(字符串数组,可以有多个关键词),timeout(遗忘世间,单位为天,建议一个月),重要:如果你认为本次用户说的话有一些值得记住的东西(例如用户希望你叫他什么,用户说她生日是多少多少等),那么使用本功能记住用户说的话
- recall: 撤回消息(需要提供seq),不常用,如果用户要求你撤回别人的消息可以使用
- emoji-like: 表情反应(需要提供id,表情id),给用户的提问回应emoji,跟meme不同
- ai-record: AI语音需要提供data,发送语音,不常用,用户要求你发语音的时候可以发,发的data需要简短,可以多条消息,但是不能太长 - ai-record: AI语音需要提供data,发送语音,不常用,用户要求你发语音的时候可以发,发的data需要简短,可以多条消息,但是不能太长
- like: 点赞某人需要提供id和num,如果用户需要
- file: 发送文件需要提供data和filename,如果你需要发一个很长的文本,请使用file发送
重要规则 重要规则
1. 必须返回JSON数组格式,一定要是数组!无论有多少条消息,一条消息也要是数组! 1. 必须返回JSON数组格式,一定要是数组!无论有多少条消息,一条消息也要是数组!
@ -71,7 +67,7 @@ export const RESPONSE_FORMAT = `请严格按照以下格式按顺序返回你的
"data": "你好呀~", "data": "你好呀~",
"at": false, "at": false,
"quote": false, "quote": false,
"recall": 0 "recall": false
} }
] ]

View File

@ -96,6 +96,32 @@ class AiCaller {
return fullPrompt; return fullPrompt;
} }
/**
* 计算时间差
* @param pastTime 过去时间戳
* @returns {string} 时间差字符串
*/
calculateTimeDifference(pastTime) {
const now = Date.now();
const diff = now - pastTime;
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
let result = '';
if (days > 0) {
result += `${days}`;
}
if (hours > 0) {
result += `${hours}小时`;
}
if (minutes > 0) {
result += `${minutes}分钟`;
}
return result || '刚刚';
}
/** /**
* 获取系统提示词 * 获取系统提示词
* @param {object} e 上下文事件对象 * @param {object} e 上下文事件对象
@ -140,7 +166,8 @@ class AiCaller {
if (memories && memories.length > 0) { if (memories && memories.length > 0) {
contextIntro += '你可能会用到的记忆,请按情况使用,如果不合语境请忽略,请结合记忆时间和当前时间智能判断:\n'; contextIntro += '你可能会用到的记忆,请按情况使用,如果不合语境请忽略,请结合记忆时间和当前时间智能判断:\n';
memories.forEach((memory, index) => { memories.forEach((memory, index) => {
contextIntro += `${index + 1}. 关键词:${memory.keywords},内容:${memory.data},记忆创建时间:${memory.createdAt}\n`; const timeDiff = this.calculateTimeDifference(memory.createdAt);
contextIntro += `${index + 1}. 关键词:${memory.keywords},内容:${memory.data},记忆创建时间:${memory.createdAt},距离现在:${timeDiff}\\n`;
}); });
contextIntro += '\n'; contextIntro += '\n';
} }

View File

@ -85,8 +85,6 @@ class ResponseHandler {
case 'memory': case 'memory':
await this.handleMemoryMessage(message, groupId,userId); await this.handleMemoryMessage(message, groupId,userId);
return null; return null;
case 'recall':
return this.handleRecallMessage(message);
default: default:
return this.handleNormalMessage(message); return this.handleNormalMessage(message);
} }
@ -106,9 +104,7 @@ class ResponseHandler {
return false; return false;
} }
const validTypes = [ const validTypes = [
'message', 'code', 'markdown', 'meme', 'at', 'poke', 'message', 'code', 'markdown', 'meme', 'at', 'poke','memory'
'recall', 'emoji-like', 'ai-record', 'function', 'like',
'file', 'memory'
]; ];
if (!validTypes.includes(message.type)) { if (!validTypes.includes(message.type)) {
logger.info(`[crystelf-ai] ai返回未知的type类型:${message.type}`) logger.info(`[crystelf-ai] ai返回未知的type类型:${message.type}`)
@ -140,13 +136,6 @@ class ResponseHandler {
} }
} }
handleRecallMessage(message) {
return {
type: 'recall',
seq: message.seq
};
}
//普通消息 //普通消息
handleNormalMessage(message) { handleNormalMessage(message) {
// 设置默认值 // 设置默认值
@ -154,31 +143,24 @@ class ResponseHandler {
type: message.type, type: message.type,
data: message.data, data: message.data,
at: message.at || false, at: message.at || false,
quote: message.quote || false, quote: message.quote || -1,
recall: message.recall || 0 recall: message.recall || false
}; };
if (message.id) processedMessage.id = message.id; if (message.id) processedMessage.id = message.id;
if (message.seq) processedMessage.seq = message.seq; if (message.seq) processedMessage.seq = message.seq;
if (message.num) processedMessage.num = message.num; if (message.num) processedMessage.num = message.num;
if (message.filename) processedMessage.filename = message.filename;
if (message.language) processedMessage.language = message.language; if (message.language) processedMessage.language = message.language;
return processedMessage; return processedMessage;
} }
//对上下文消息进行处理
handleChatHistory(message) {
let messageToHistory = [];
}
createErrorResponse(error) { createErrorResponse(error) {
const nickName = configControl.get('profile')?.nickName; const nickName = configControl.get('profile')?.nickName;
return [{ return [{
type: 'message', type: 'message',
data: `${nickName}的服务器去火星开小差了..`, data: `${nickName}的服务器去火星开小差了..`,
at: false, at: false,
quote: true, quote: -1,
recall: 120 recall: true
}]; }];
} }
@ -188,8 +170,8 @@ class ResponseHandler {
type: 'message', type: 'message',
data: `${nickName}的服务器去火星开小差了..`, data: `${nickName}的服务器去火星开小差了..`,
at: false, at: false,
quote: true, quote: -1,
recall: 120 recall: true
}]; }];
} }
} }

View File

@ -62,6 +62,79 @@ const Message = {
count: count, count: count,
}) })
} }
},
/**
* 发送群聊消息
* @param e
* @param group_id 群号
* @param message 消息内容字符串
* @param at -1 @ 其他 @的qq号
* @param quote_seq -1 不引用 其他 引用的消息seq
* @param adapter nc/lgr
* @returns {Promise<*>}
*/
async sendGroupMessage(e,group_id,message,at = -1,quote_seq = -1,adapter = 'nc'){
if(adapter === 'nc') {
const msgChain = [];
if (typeof quote_seq !== 'boolean' && quote_seq !== -1 && quote_seq !== undefined) {
msgChain.push({
type: "reply",
data: {
"id": quote_seq
}
});
}
if (at && at !== -1) {
msgChain.push({
type: "at",
data: {
"qq": at
}
});
}
msgChain.push({
type: "text",
data: {
"text": message
}
});
return await e.bot.sendApi('send_group_msg',{
group_id : group_id,
message: msgChain
})
} else if (adapter === 'lgr') {
const messageData = {};
if(quote_seq && quote_seq !== -1) {
messageData.reply = {
type: "reply",
data: {
"id": quote_seq
}
};
}
if(at && at !== -1) {
messageData.at = {
type: "at",
data: {
"qq": at
}
};
}
messageData.text = {
type: "text",
data: {
"text": message
}
};
return await e.bot.sendApi('send_group_msg',{
group_id: group_id,
message :{
type: "dict",
data : messageData
}
})
}
} }
}; };
export default Message; export default Message;