diff --git a/package.json b/package.json index fa3b89e..838ade4 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "dependencies": { "@types/handlebars": "^4.0.40", "handlebars": "^4.7.8", + "js-toml": "^1.0.3", "lodash-es": "^4.17.21" }, "devDependencies": { diff --git a/src/AI/image.ts b/src/AI/image.ts deleted file mode 100644 index 9b72daa..0000000 --- a/src/AI/image.ts +++ /dev/null @@ -1,280 +0,0 @@ -import { ConfigManager } from "../config/configManager"; -import { sendITTRequest } from "../service"; -import { generateId } from "../utils/utils"; -import { logger } from "../logger"; -import { AI } from "./AI"; -import { MessageSegment, parseSpecialTokens } from "../utils/utils_string"; - -export class Image { - static validKeys: (keyof Image)[] = ['id', 'file', 'content']; - id: string; - file: string; // 图片url或本地路径 - content: string; - - constructor() { - this.id = generateId(); - this.file = ''; - this.content = ''; - } - - get type(): 'url' | 'local' | 'base64' { - if (this.file.startsWith('http')) return 'url'; - if (this.format) return 'base64'; - return 'local'; - } - - get base64(): string { - return ConfigManager.ext.storageGet(`base64_${this.id}`) || ''; - } - set base64(value: string) { - this.file = ''; - ConfigManager.ext.storageSet(`base64_${this.id}`, value); - } - - get format(): string { - return ConfigManager.ext.storageGet(`format_${this.id}`) || ''; - } - set format(value: string) { - ConfigManager.ext.storageSet(`format_${this.id}`, value); - } - - get CQCode(): string { - const file = this.type === 'base64' ? seal.base64ToImage(this.base64) : this.file; - return `[CQ:image,file=${file}]`; - } - - get base64Url(): string { - let format = this.format; - if (!format || format === "unknown") format = 'png'; - return `data:image/${format};base64,${this.base64}` - } - - /** - * 获取图片的URL,若为base64则返回base64Url - */ - get url(): string { - return this.type === 'base64' ? this.base64Url : this.file; - } - - async checkImageUrl(): Promise { - if (this.type !== 'url') return true; - let isValid = false; - try { - const response = await fetch(this.file, { method: 'GET' }); - - if (response.ok) { - const contentType = response.headers.get('Content-Type'); - if (contentType && contentType.startsWith('image')) { - logger.info('URL有效且未过期'); - isValid = true; - } else { - logger.warning(`URL有效但未返回图片 Content-Type: ${contentType}`); - } - } else { - if (response.status === 500) { - logger.warning(`URL不知道有没有效 状态码: ${response.status}`); - isValid = true; - } else { - logger.warning(`URL无效或过期 状态码: ${response.status}`); - } - } - } catch (error) { - logger.error('在checkImageUrl中请求出错:', error); - } - return isValid; - } - - async urlToBase64() { - if (this.type !== 'url') return; - const { imageTobase64Url } = ConfigManager.backend; - try { - const response = await fetch(`${imageTobase64Url}/image-to-base64`, { - method: 'POST', - headers: { - "Content-Type": "application/json", - "Accept": "application/json" - }, - body: JSON.stringify({ url: this.file }) - }); - - const text = await response.text(); - if (!response.ok) throw new Error(`请求失败! 状态码: ${response.status}\n响应体: ${text}`); - if (!text) throw new Error("响应体为空"); - - try { - const data = JSON.parse(text); - if (data.error) throw new Error(`请求失败! 错误信息: ${data.error.message}`); - if (!data.base64 || !data.format) throw new Error(`响应体中缺少base64或format字段`); - this.base64 = data.base64; - this.format = data.format; - } catch (e) { - throw new Error(`解析响应体时出错:${e}\n响应体:${text}`); - } - } catch (error) { - logger.error("在imageUrlToBase64中请求出错:", error); - } - } - - async imageToText(prompt = '') { - const { defaultPrompt, urlToBase64, maxChars } = ConfigManager.image; - - if (urlToBase64 == '总是' && this.type === 'url') await this.urlToBase64(); - - const messages = [{ - role: "user", - content: [{ - "type": "image_url", - "image_url": { "url": this.url } - }, { - "type": "text", - "text": prompt ? prompt : defaultPrompt - }] - }] - - this.content = (await sendITTRequest(messages)).slice(0, maxChars); - - if (!this.content && urlToBase64 === '自动' && this.type === 'url') { - logger.info(`图片${this.id}第一次识别失败,自动尝试使用转换为base64`); - await this.urlToBase64(); - messages[0].content[0].image_url.url = this.base64Url; - this.content = (await sendITTRequest(messages)).slice(0, maxChars); - } - - if (!this.content) logger.error(`图片${this.id}识别失败`); - } -} - -export class ImageManager { - static validKeys: (keyof ImageManager)[] = ['stolenImages', 'stealStatus']; - stolenImages: Image[]; - stealStatus: boolean; - - constructor() { - this.stolenImages = []; - this.stealStatus = false; - } - - static getUserAvatar(uid: string): Image { - const img = new Image(); - img.id = `user_avatar:${uid}`; - img.file = `https://q1.qlogo.cn/g?b=qq&nk=${uid.replace(/^.+:/, '')}&s=640`; - return img; - } - - static getGroupAvatar(gid: string): Image { - const img = new Image(); - img.id = `group_avatar:${gid}`; - img.file = `https://p.qlogo.cn/gh/${gid.replace(/^.+:/, '')}/${gid.replace(/^.+:/, '')}/640`; - return img; - } - - stealImages(images: Image[]) { - const { maxStolenImageNum } = ConfigManager.image; - this.stolenImages = this.stolenImages.concat(images).slice(-maxStolenImageNum); - } - - static getLocalImageListText(p: number = 1): string { - const { localImagePathMap } = ConfigManager.image; - const images = Object.keys(localImagePathMap).map(id => { - const image = new Image(); - image.id = id; - image.file = localImagePathMap[id]; - return image; - }); - if (images.length == 0) return ''; - if (p > Math.ceil(images.length / 5)) p = Math.ceil(images.length / 5); - return images.slice((p - 1) * 5, p * 5) - .map((img, i) => { - return `${i + 1 + (p - 1) * 5}. 名称:${img.id} -${img.CQCode}`; - }).join('\n') + `\n当前页码:${p}/${Math.ceil(images.length / 5)}`; - } - - async drawStolenImage(): Promise { - if (this.stolenImages.length === 0) return null; - const index = Math.floor(Math.random() * this.stolenImages.length); - const img = this.stolenImages.splice(index, 1)[0]; - if (!await img.checkImageUrl()) { - await new Promise(resolve => setTimeout(resolve, 500)); - return await this.drawStolenImage(); - } - return img; - } - - getStolenImageListText(p: number = 1): string { - if (this.stolenImages.length == 0) return ''; - if (p > Math.ceil(this.stolenImages.length / 5)) p = Math.ceil(this.stolenImages.length / 5); - return this.stolenImages.slice((p - 1) * 5, p * 5) - .map((img, i) => { - return `${i + 1 + (p - 1) * 5}. ID:${img.id} -${img.CQCode}`; - }).join('\n') + `\n当前页码:${p}/${Math.ceil(this.stolenImages.length / 5)}`; - } - - async drawImage(): Promise { - const { localImagePathMap } = ConfigManager.image; - const localImages = Object.keys(localImagePathMap).map(id => { - const image = new Image(); - image.id = id; - image.file = localImagePathMap[id]; - return image; - }); - if (this.stolenImages.length == 0 && localImages.length == 0) return null; - const index = Math.floor(Math.random() * (localImages.length + this.stolenImages.length)); - return index < localImages.length ? localImages[index] : await this.drawStolenImage(); - } - - /** - * 提取并替换CQ码中的图片 - * @param ctx - * @param message - * @returns - */ - async handleImageMessageSegment(ctx: seal.MsgContext, seg: MessageSegment): Promise<{ content: string, images: Image[] }> { - const { receiveImage } = ConfigManager.image; - if (!receiveImage || seg.type !== 'image') return { content: '', images: [] }; - - let content = ''; - const images: Image[] = []; - try { - const file = seg.data.url || seg.data.file || ''; - if (!file) return { content: '', images: [] }; - - const image = new Image(); - image.file = file; - const { condition } = ConfigManager.image; - const fmtCondition = parseInt(seal.format(ctx, `{${condition}}`)); - if (fmtCondition === 1) await image.imageToText(); - - content += image.content ? `<|img:${image.id}:${image.content}|>` : `<|img:${image.id}|>`; - images.push(image); - } catch (error) { - logger.error('在handleImageMessage中处理图片时出错:', error); - } - - if (this.stealStatus) this.stealImages(images); - return { content, images }; - } - - static async extractExistingImagesToSave(ctx: seal.MsgContext, ai: AI, s: string): Promise { - const segs = parseSpecialTokens(s); - const images: Image[] = []; - for (const seg of segs) { - switch (seg.type) { - case 'img': { - const id = seg.content; - const image = await ai.context.findImage(ctx, id); - - if (image) { - if (image.type === 'url') await image.urlToBase64(); - images.push(image); - } else { - logger.warning(`无法找到图片:${id}`); - } - break; - } - } - } - return images; - } -} \ No newline at end of file diff --git a/src/AI/memory.ts b/src/AI/memory.ts deleted file mode 100644 index 9d074b5..0000000 --- a/src/AI/memory.ts +++ /dev/null @@ -1,748 +0,0 @@ -import { ConfigManager } from "../config/configManager"; -import { AI, AIManager, GroupInfo, SessionInfo, UserInfo } from "./AI"; -import { Context } from "./context"; -import { cosineSimilarity, generateId, getCommonGroup, getCommonKeyword, getCommonUser, revive } from "../utils/utils"; -import { logger } from "../logger"; -import { fetchData, getEmbedding } from "../service"; -import { buildContent, getRoleSetting, parseBody } from "../utils/utils_message"; -import { ToolManager } from "../tool/tool"; -import { fmtDate } from "../utils/utils_string"; -import { Image, ImageManager } from "./image"; - -export interface searchOptions { - topK: number; - userList: UserInfo[]; - groupList: GroupInfo[]; - keywords: string[]; - includeImages: boolean; - method: 'weight' | 'similarity' | 'score' | 'early' | 'late' | 'recent'; -} - -export class Memory { - static validKeys: (keyof Memory)[] = ['id', 'vector', 'text', 'sessionInfo', 'userList', 'groupList', 'createTime', 'lastMentionTime', 'keywords', 'weight', 'images']; - id: string; // 记忆ID - vector: number[]; // 记忆向量 - text: string; // 记忆内容 - sessionInfo: SessionInfo; - userList: UserInfo[]; - groupList: GroupInfo[]; - createTime: number; // 秒级时间戳 - lastMentionTime: number; - keywords: string[]; - weight: number; // 记忆权重,0-10 - images: Image[]; - - constructor() { - this.id = ''; - this.vector = []; - this.text = ''; - this.sessionInfo = { - id: '', - isPrivate: false, - name: '', - }; - this.userList = []; - this.groupList = []; - this.createTime = 0; - this.lastMentionTime = 0; - this.keywords = []; - this.weight = 0; - this.images = []; - } - - get copy(): Memory { - const m = new Memory(); - m.id = this.id; - m.vector = [...this.vector]; - m.text = this.text; - m.sessionInfo = JSON.parse(JSON.stringify(this.sessionInfo)); - m.userList = JSON.parse(JSON.stringify(this.userList)); - m.groupList = JSON.parse(JSON.stringify(this.groupList)); - m.createTime = this.createTime; - m.lastMentionTime = this.lastMentionTime; - m.keywords = [...this.keywords]; - m.weight = this.weight; - m.images = [...this.images]; - return m; - } - - /** - * 计算记忆的新鲜度衰减因子,越大表示越新鲜 - * @returns 衰减因子(1→0) - */ - get decay() { - const now = Math.floor(Date.now() / 1000); - const ageInDays = (now - this.createTime) / (24 * 60 * 60); - const activityInHours = (now - this.lastMentionTime) / (60 * 60); - // 基础新鲜度: exp(-ageInDays / 7) - const ageDecay = Math.exp(-ageInDays / 7); - // 活跃度: exp(-activityInHours / 4) - const activityDecay = Math.exp(-activityInHours / 4); - // 衰减因子,取年龄衰减和活跃度衰减的较大值 - return Math.max(ageDecay, activityDecay); - } - - /** - * 计算记忆与查询的相似度分数 - * @param v 查询向量 - * @param ul 查询用户列表 - * @param gl 查询群组列表 - * @param kws 查询关键词列表 - * @returns 相似度分数(0-1) - */ - calculateSimilarity(v: number[], ul: UserInfo[], gl: GroupInfo[], kws: string[]): number { - // 总权重 0-1 - const totalWeight = (v.length ? 0.4 : 0) + (ul.length ? 0.2 : 0) + (gl.length ? 0.2 : 0) + (kws.length ? 0.2 : 0); - if (totalWeight === 0) return 0; - // 向量相似度分数(如果提供了向量v) 0-1 - const vectorSimilarity = (v && v.length > 0 && this.vector && this.vector.length > 0) ? (cosineSimilarity(v, this.vector) + 1) / 2 : 0; - // 用户相似度分数 0-1 - const commonUser = getCommonUser(this.userList, ul); - const userSimilarity = (ul && ul.length > 0) ? commonUser.length / (this.userList.length + ul.length - commonUser.length) : 0; - // 群组相似度分数 0-1 - const commonGroup = getCommonGroup(this.groupList, gl); - const groupSimilarity = (gl && gl.length > 0) ? commonGroup.length / (this.groupList.length + gl.length - commonGroup.length) : 0; - // 关键词匹配分数 0-1 - const commonKeyword = getCommonKeyword(this.keywords, kws); - const keywordSimilarity = (kws && kws.length > 0) ? commonKeyword.length / kws.length : 0; - // 综合相似度分数 0-1 - const avgSimilarity = vectorSimilarity * 0.4 + userSimilarity * 0.2 + groupSimilarity * 0.2 + keywordSimilarity * 0.2; - // 相似度增强因子 0-1 - return avgSimilarity / totalWeight; - } - - /** - * 计算记忆的最终分数 - * @param v 查询向量 - * @param ul 查询用户列表 - * @param gl 查询群组列表 - * @param kws 查询关键词列表 - * @returns 相似度分数(0-1) - */ - calculateScore(v: number[], ul: UserInfo[], gl: GroupInfo[], kws: string[]): number { - return this.weight * 0.03 + this.calculateSimilarity(v, ul, gl, kws) * 0.7; - } - - async updateVector() { - const { isMemoryVector, embeddingDimension } = ConfigManager.memory; - if (isMemoryVector) { - logger.info(`更新记忆向量: ${this.id}`); - const vector = await getEmbedding(this.text); - if (!vector.length) { - logger.error('返回向量为空'); - return; - } - if (vector.length !== embeddingDimension) { - logger.error(`向量维度不匹配。期望: ${embeddingDimension}, 实际: ${vector.length}`); - return; - } - this.vector = vector; - } - } -} - -export class MemoryManager { - static validKeys: (keyof MemoryManager)[] = ['persona', 'memoryMap', 'useShortMemory', 'shortMemoryList']; - persona: string; - memoryMap: { [id: string]: Memory }; - useShortMemory: boolean; - shortMemoryList: string[]; - - constructor() { - this.persona = '无'; - this.memoryMap = {}; - this.useShortMemory = false; - this.shortMemoryList = []; - } - - reviveMemoryMap() { - for (const id in this.memoryMap) { - this.memoryMap[id] = revive(Memory, this.memoryMap[id]); - if (!this.memoryMap[id].text) { - delete this.memoryMap[id]; - continue; - } - if (!this.memoryMap[id].hasOwnProperty('images')) this.memoryMap[id].images = []; - this.memoryMap[id].images = this.memoryMap[id].images.map(image => revive(Image, image)); - } - } - - get memoryIds() { - return Object.keys(this.memoryMap); - } - - get memoryList() { - return Object.values(this.memoryMap); - } - - get keywords() { - const keywords = new Set(); - this.memoryList.forEach(m => m.keywords.forEach(kw => keywords.add(kw))); - return Array.from(keywords); - } - - async addMemory(ctx: seal.MsgContext, ai: AI, ul: UserInfo[], gl: GroupInfo[], kws: string[], images: Image[], text: string) { - let id = generateId(), a = 0; - while (this.memoryMap.hasOwnProperty(id)) { - id = generateId(); - a++; - if (a > 1000) { - logger.error(`生成记忆id失败,已尝试1000次,放弃`); - return; - } - } - - for (const id of this.memoryIds) { - const m = this.memoryMap[id]; - if (text === m.text && m.sessionInfo.id === ai.id && getCommonUser(ul, m.userList).length > 0 && getCommonGroup(gl, m.groupList).length > 0) { - m.keywords = Array.from(new Set([...m.keywords, ...kws])); - logger.info(`记忆已存在,id:${id},合并关键词:${m.keywords.join(',')}`); - return; - } - } - - // 添加文本内插入的图片 - const imgIdSet = new Set(images.map(img => img.id)); - (await ImageManager.extractExistingImagesToSave(ctx, ai, text)).forEach(img => { - if (imgIdSet.has(img.id)) return; - imgIdSet.add(img.id); - images.push(img); - }); - - const now = Math.floor(Date.now() / 1000); - const m = new Memory(); - m.id = id; - m.text = text; - m.sessionInfo = { - id: ai.id, - isPrivate: ctx.isPrivate, - name: ctx.isPrivate ? ctx.player.name : ctx.group.groupName, - }; - m.userList = ul; - m.groupList = gl; - m.createTime = now; - m.lastMentionTime = now; - m.keywords = kws; - m.weight = 5; - m.images = images; - await m.updateVector(); - this.limitMemory(); - this.memoryMap[id] = m; - } - - deleteMemory(ids: string[] = [], kws: string[] = []) { - if (ids.length === 0 && kws.length === 0) return; - - ids.forEach(id => delete this.memoryMap?.[id]) - - if (kws.length > 0) { - for (const id in this.memoryMap) { - if (kws.some(kw => this.memoryMap[id].keywords.includes(kw))) { - delete this.memoryMap[id]; - } - } - } - } - - limitMemory() { - const { memoryLimit } = ConfigManager.memory; - const limit = memoryLimit > 0 ? memoryLimit - 1 : 0; // 预留1个位置用于存储最新记忆 - if (this.memoryList.length <= limit) return; - this.memoryList.map((m) => { - return { - id: m.id, - score: m.decay * m.weight - } - }) - .sort((a, b) => b.score - a.score) // 从大到小排序 - .slice(limit) - .forEach(item => delete this.memoryMap?.[item.id]); - } - - clearMemory() { - this.memoryMap = {}; - } - - limitShortMemory() { - const { shortMemoryLimit } = ConfigManager.memory; - if (this.shortMemoryList.length > shortMemoryLimit) { - this.shortMemoryList.splice(0, this.shortMemoryList.length - shortMemoryLimit); - } - } - - clearShortMemory() { - this.shortMemoryList = []; - } - - async updateShortMemory(ctx: seal.MsgContext, msg: seal.Message, ai: AI) { - if (!this.useShortMemory) { - return; - } - - const { url: chatUrl, apiKey: chatApiKey } = ConfigManager.request; - const { isPrefix, showNumber, showMsgId, showTime } = ConfigManager.message; - const { shortMemorySummaryRound, memoryUrl, memoryApiKey, memoryBodyTemplate, memoryPromptTemplate } = ConfigManager.memory; - - const { roleSetting } = getRoleSetting(ctx); - - const messages = ai.context.messages; - let sumMessages = messages.slice(); - let round = 0; - for (let i = 0; i < messages.length; i++) { - if (messages[i].role === 'user' && !messages[i].name.startsWith('_')) { - round++; - } - if (round > shortMemorySummaryRound) { - sumMessages = messages.slice(0, i); // 只保留最近的shortMemorySummaryRound轮对话 - break; - } - } - - if (sumMessages.length === 0) { - return; - } - - let url = chatUrl; - let apiKey = chatApiKey; - if (memoryUrl.trim()) { - url = memoryUrl; - apiKey = memoryApiKey; - } - - try { - const prompt = memoryPromptTemplate({ - "角色设定": roleSetting, - "平台": ctx.endPoint.platform, - "私聊": ctx.isPrivate, - "展示号码": showNumber, - "用户名称": ctx.player.name, - "用户号码": ctx.player.userId.replace(/^.+:/, ''), - "群聊名称": ctx.group.groupName, - "群聊号码": ctx.group.groupId.replace(/^.+:/, ''), - "添加前缀": isPrefix, - "展示消息ID": showMsgId, - "展示时间": showTime, - "对话内容": isPrefix ? sumMessages.map(message => { - if (message.role === 'assistant' && message?.tool_calls && message?.tool_calls.length > 0) { - return `\n[function_call]: ${message.tool_calls.map((tool_call, index) => `${index + 1}. ${JSON.stringify(tool_call.function, null, 2)}`).join('\n')}`; - } - - return `[${message.role}]: ${buildContent(message)}`; - }).join('\n') : JSON.stringify(sumMessages) - }) - - logger.info(`记忆总结prompt:\n`, prompt); - - const messages = [ - { - role: "system", - content: prompt - } - ] - const bodyObject = parseBody(memoryBodyTemplate, messages, [], "none"); - - const time = Date.now(); - const data = await fetchData(url, apiKey, bodyObject); - - if (data.choices && data.choices.length > 0) { - AIManager.updateUsage(data.model, data.usage); - - const message = data.choices[0].message; - const finish_reason = data.choices[0].finish_reason; - - if (message.hasOwnProperty('reasoning_content')) { - logger.info(`思维链内容:`, message.reasoning_content); - } - - const reply = message.content || ''; - logger.info(`响应内容:`, reply, '\nlatency:', Date.now() - time, 'ms', '\nfinish_reason:', finish_reason); - - const memoryData = JSON.parse(reply) as { - content: string, - memories: { - memory_type: 'private' | 'group', - name: string, - text: string, - keywords?: string[], - userList?: string[], - groupList?: string[], - }[] - }; - - - this.shortMemoryList.push(memoryData.content); - this.limitShortMemory(); - - memoryData.memories.forEach(m => { - ToolManager.toolMap["add_memory"].solve(ctx, msg, ai, m); - }); - } - } catch (e) { - logger.error(`更新短期记忆失败: ${e.message}`); - } - } - - async search(query: string, options: searchOptions = { - topK: 10, - userList: [], - groupList: [], - keywords: [], - includeImages: false, - method: 'score' - }) { - if (!this.memoryList.length) return []; - const { userList: ul, groupList: gl, keywords: kws, includeImages, method } = options; - - const { isMemoryVector, embeddingDimension } = ConfigManager.memory; - let qv: number[] = []; - if (isMemoryVector && query) { - qv = await getEmbedding(query); - if (!qv.length) { - logger.error('查询向量为空'); - return []; - } - await Promise.all(this.memoryList.map(async m => { - if (m.vector.length !== embeddingDimension) { - logger.info(`记忆向量维度不匹配,重新获取向量: ${m.id}`); - await m.updateVector(); - } - })) - } - - return this.memoryList - .map(m => { - if (includeImages && m.images.length === 0) return null; - const mc = m.copy; - if (mc.keywords.some(kw => query.includes(kw))) mc.weight += 10; //提权 - return mc; - }) - .filter(m => m) - .sort((a, b) => { - switch (method) { - case 'weight': return b.weight - a.weight; - case 'similarity': return b.calculateSimilarity(qv, ul, gl, kws) - a.calculateSimilarity(qv, ul, gl, kws); - case 'score': return b.calculateScore(qv, ul, gl, kws) - a.calculateScore(qv, ul, gl, kws); - case 'early': return a.createTime - b.createTime; - case 'late': return b.createTime - a.createTime; - case 'recent': return b.lastMentionTime - a.lastMentionTime; - } - }) - .slice(0, options.topK || 10); - } - - updateMemoryWeight(s: string, role: 'user' | 'assistant') { - const increase = role === 'user' ? 1 : 0.1; - const decrease = role === 'user' ? 0.1 : 0; - const now = Math.floor(Date.now() / 1000); - - for (const id in this.memoryMap) { - const m = this.memoryMap[id]; - if (m.keywords.some(kw => s.includes(kw))) { - m.weight = Math.max(10, m.weight + increase); - m.lastMentionTime = now; - } else { - m.weight = Math.min(0, m.weight - decrease); - } - } - } - - updateRelatedMemoryWeight(ctx: seal.MsgContext, context: Context, s: string, role: 'user' | 'assistant') { - // bot记忆权重更新 - AIManager.getAI(ctx.endPoint.userId).memory.updateMemoryWeight(s, role); - // 知识库记忆权重更新 - knowledgeMM.updateMemoryWeight(s, role); - // 会话自身记忆权重更新 - this.updateMemoryWeight(s, role); - // 群内用户的记忆权重更新 - if (!ctx.isPrivate) context.userInfoList.forEach(ui => AIManager.getAI(ui.id).memory.updateMemoryWeight(s, role)); - } - - async getTopScoreMemoryList(text: string = '', ui: UserInfo = null, gi: GroupInfo = null) { - const { memoryShowNumber } = ConfigManager.memory; - return await this.search(text, { - topK: memoryShowNumber, - userList: ui ? [ui] : [], - groupList: gi ? [gi] : [], - keywords: [], - includeImages: false, - method: 'score' - }); - } - - getLatestMemoryListText(si: SessionInfo, p: number = 1): string { - if (this.memoryList.length === 0) return ''; - if (p > Math.ceil(this.memoryList.length / 5)) p = Math.ceil(this.memoryList.length / 5); - const latestMemoryList = this.memoryList - .sort((a, b) => b.createTime - a.createTime) - .slice((p - 1) * 5, p * 5); - return this.buildMemory(si, latestMemoryList) + `\n当前页码: ${p}/${Math.ceil(this.memoryList.length / 5)}`; - } - - buildMemory(si: SessionInfo, ml: Memory[]): string { - if (this.persona === '无' && ml.length === 0) return ''; - const { showNumber } = ConfigManager.message; - const { memoryShowTemplate, memorySingleShowTemplate } = ConfigManager.memory; - - let memoryContent = ''; - if (ml.length === 0) { - memoryContent = '无'; - } else { - memoryContent = ml - .map((m, i) => { - return memorySingleShowTemplate({ - "序号": i + 1, - "记忆ID": m.id, - "记忆时间": fmtDate(m.createTime), - "个人记忆": si.isPrivate, - "私聊": m.sessionInfo.isPrivate, - "展示号码": showNumber, - "群聊名称": m.sessionInfo.name, - "群聊号码": m.sessionInfo.id, - "相关用户": m.userList.map(u => u.name + (showNumber ? `(${u.id.replace(/^.+:/, '')})` : '')).join(';'), - "相关群聊": m.groupList.map(g => g.name + (showNumber ? `(${g.id.replace(/^.+:/, '')})` : '')).join(';'), - "关键词": m.keywords.join(';'), - "记忆内容": m.text - }); - }).join('\n'); - } - - return memoryShowTemplate({ - "私聊": si.isPrivate, - "展示号码": showNumber, - "用户名称": si.name, - "用户号码": si.id.replace(/^.+:/, ''), - "群聊名称": si.name, - "群聊号码": si.id.replace(/^.+:/, ''), - "设定": this.persona, - "记忆列表": memoryContent - }) + '\n'; - } - - async buildMemoryPrompt(ctx: seal.MsgContext, context: Context, text: string, ui: UserInfo, gi: GroupInfo): Promise { - const ai = AIManager.getAI(ctx.endPoint.userId); - let s = ai.memory.buildMemory({ - isPrivate: true, - id: ctx.endPoint.userId, - name: seal.formatTmpl(ctx, "核心:骰子名字") - }, await ai.memory.getTopScoreMemoryList(text, ui, gi)); - - if (ctx.isPrivate) { - return this.buildMemory({ - isPrivate: true, - id: ctx.player.userId, - name: ctx.player.name - }, await ai.memory.getTopScoreMemoryList(text, ui, gi)); - } else { - // 群聊记忆 - s += this.buildMemory({ - isPrivate: false, - id: ctx.group.groupId, - name: ctx.group.groupName - }, await ai.memory.getTopScoreMemoryList(text, ui, gi)); - - // 群内用户的个人记忆 - const set = new Set(); - for (const ui of context.userInfoList) { - const name = ui.name; - const uid = ui.id; - if (set.has(uid)) continue; - set.add(uid); - - const ai = AIManager.getAI(uid); - s += ai.memory.buildMemory({ - isPrivate: true, - id: uid, - name: name - }, await ai.memory.getTopScoreMemoryList(text, ui, gi)); - } - - return s; - } - } - - findImage(id: string): Image | null { - for (const m of this.memoryList) { - const image = m.images.find(img => img.id === id); - if (image) { - m.weight += 0.2; - return image; - } - } - return null; - } - - findMemoryAndImageByImageIdPrefix(id: string): { memory: Memory, image: Image } | null { - for (const m of this.memoryList) { - const image = m.images.find(img => img.id.replace(/_\d+$/, "") === id); - if (image) { - m.weight += 0.2; - return { memory: m, image }; - } - } - return null; - } -} - -export class KnowledgeMemoryManager extends MemoryManager { - constructor() { - super(); - } - - init() { - this.memoryMap = JSON.parse(ConfigManager.ext.storageGet('knowledgeMemoryMap') || '{}'); - this.reviveMemoryMap(); - } - - save() { - ConfigManager.ext.storageSet('knowledgeMemoryMap', JSON.stringify(this.memoryMap)); - } - - async updateKnowledgeMemory(roleIndex: number) { - const { knowledgeMemoryStringList } = ConfigManager.memory; - if (roleIndex < 0 || roleIndex >= knowledgeMemoryStringList.length) return; - const s = knowledgeMemoryStringList[roleIndex]; - if (!s) return; - - const memoryMap: { [id: string]: Memory } = {} - const segs = s.split(/\n-{3,}\n/); - for (const seg of segs) { - if (!seg.trim()) continue; - - const lines = seg.split('\n'); - if (lines.length === 0) continue; - - const m = new Memory(); - for (let i = 0; i < lines.length; i++) { - const match = lines[i].match(/^\s*?(ID|用户|群聊|关键词|图片|内容)\s*?[::](.*)/); - if (!match) { - continue; - } - const type = match[1]; - const value = match[2].trim(); - switch (type) { - case 'ID': { - m.id = value; - break; - } - case '用户': { - m.userList = value.split(/[,,]/).map(s => { - const segs = s.split(/[::]/).map(s => s.trim()).filter(s => s); - if (segs.length < 2) return null; - const name = value.replace(/[::].*$/, '').trim(); - const id = segs[segs.length - 1]; - if (!name || !id) return null; - return { isPrivate: true, id, name }; - }).filter(ui => ui) as UserInfo[]; - break; - } - case '群聊': { - m.groupList = value.split(/[,,]/).map(s => { - const segs = s.split(/[::]/).map(s => s.trim()).filter(s => s); - if (segs.length < 2) return null; - const name = value.replace(/[::].*$/, '').trim(); - const id = segs[segs.length - 1]; - if (!name || !id) return null; - return { isPrivate: false, id, name }; - }).filter(ui => ui) as GroupInfo[]; - break; - } - case '关键词': { - m.keywords = value.split(/[,,]/).map(kw => kw.trim()).filter(kw => kw); - break; - } - case '图片': { - const { localImagePathMap } = ConfigManager.image; - - m.images = value.split(/[,,]/).map(id => id.trim()).map(id => { - if (localImagePathMap.hasOwnProperty(id)) { - const image = new Image(); - image.file = localImagePathMap[id]; - return image; - } - logger.error(`图片${id}不存在`); - return null; - }).filter(img => img); - break; - } - case '内容': { - m.text = lines.slice(i).join('\n').trim().replace(/^内容[::]/, ''); - break; - } - default: continue; - } - } - - if (!m.id && !m.text) continue; - - memoryMap[m.id] = m; - } - - const now = Math.floor(Date.now() / 1000); - await Promise.all(Object.values(memoryMap).map(async m => { - if (this.memoryMap.hasOwnProperty(m.id)) { - const m2 = this.memoryMap[m.id]; - m.vector = m2.vector; - if (m2.text !== m.text) await m.updateVector(); - m.createTime = m2.createTime; - m.lastMentionTime = m2.lastMentionTime; - m.weight = m2.weight; - } else { - await m.updateVector(); - m.createTime = now; - m.lastMentionTime = now; - m.weight = 5; - } - })) - - this.memoryMap = memoryMap; - this.save(); - } - - buildKnowledgeMemory(memoryList: Memory[]) { - const { showNumber } = ConfigManager.message; - const { knowledgeMemorySingleShowTemplate } = ConfigManager.memory; - if (memoryList.length === 0) return ''; - - let prompt = ''; - if (memoryList.length === 0) { - prompt = '无'; - } else { - prompt = memoryList - .map((m, i) => { - return knowledgeMemorySingleShowTemplate({ - "序号": i + 1, - "记忆ID": m.id, - "用户列表": m.userList.map(u => u.name + (showNumber ? `(${u.id.replace(/^.+:/, '')})` : '')).join(';'), - "群聊列表": m.groupList.map(g => g.name + (showNumber ? `(${g.id.replace(/^.+:/, '')})` : '')).join(';'), - "关键词": m.keywords.join(';'), - "记忆内容": m.text - }); - }).join('\n'); - } - - return prompt; - } - - async buildKnowledgeMemoryPrompt(roleIndex: number, text: string, ui: UserInfo, gi: GroupInfo): Promise { - await this.updateKnowledgeMemory(roleIndex); - if (this.memoryIds.length === 0) return ''; - - const { knowledgeMemoryShowNumber } = ConfigManager.memory; - const memoryList = await this.search(text, { - topK: knowledgeMemoryShowNumber, - userList: ui ? [ui] : [], - groupList: gi ? [gi] : [], - keywords: [], - includeImages: false, - method: 'score' - }); - - return this.buildKnowledgeMemory(memoryList); - } -} - -export const knowledgeMM = new KnowledgeMemoryManager(); - -// 可以通过维护一组索引来优化搜索性能。 -// 好麻烦,不想弄 -// 目前数量级应该没什么优化的需求 \ No newline at end of file diff --git a/src/AI/AI.ts b/src/agent/AI.ts similarity index 93% rename from src/AI/AI.ts rename to src/agent/AI.ts index b9e3ae1..2feeb53 100644 --- a/src/AI/AI.ts +++ b/src/agent/AI.ts @@ -1,13 +1,13 @@ import { Image, ImageManager } from "./image"; -import { ConfigManager } from "../config/configManager"; +import { Config } from "../config/config"; import { replyToSender, revive, transformMsgId } from "../utils/utils"; -import { endStream, pollStream, sendChatRequest, startStream } from "../service"; +import { endStream, pollStream, sendChatRequest, startStream } from "../agent/service"; import { Context } from "./context"; import { MemoryManager } from "./memory"; -import { handleMessages, parseBody } from "../utils/utils_message"; -import { ToolManager } from "../tool/tool"; +import { handleMessages, parseBody } from "../utils/message"; +import { ToolService } from "../tool/tool"; import { logger } from "../logger"; -import { checkRepeat, handleReply, MessageSegment, transformArrayToContent } from "../utils/utils_string"; +import { checkRepeat, handleReply, MessageSegment, transformArrayToContent } from "../utils/string"; import { TimerManager } from "../timer"; export interface GroupInfo { @@ -55,7 +55,7 @@ export class AI { static validKeys: (keyof AI)[] = ['context', 'tool', 'memory', 'imageManager', 'setting']; id: string; context: Context; - tool: ToolManager; + tool: ToolService; memory: MemoryManager; imageManager: ImageManager; setting: Setting; @@ -75,7 +75,7 @@ export class AI { constructor() { this.id = ''; this.context = new Context(); - this.tool = new ToolManager(); + this.tool = new ToolService(); this.memory = new MemoryManager(); this.imageManager = new ImageManager(); this.setting = new Setting(); @@ -112,7 +112,7 @@ export class AI { } //发送偷来的图片 - const { p } = ConfigManager.image; + const { p } = Config.image; if (Math.random() * 100 <= p) { const img = await this.imageManager.drawImage(); if (img) seal.replyToSender(ctx, msg, img.CQCode); @@ -123,7 +123,7 @@ export class AI { logger.info('触发回复:', reason || '未知原因'); if (reason !== '函数回调触发') { - const { bucketLimit, fillInterval } = ConfigManager.received; + const { bucketLimit, fillInterval } = Config.received; // 补充并检查触发次数 if (Date.now() - this.bucket.lastTime > fillInterval * 1000) { const fillCount = (Date.now() - this.bucket.lastTime) / (fillInterval * 1000); @@ -137,7 +137,7 @@ export class AI { } // 检查toolsNotAllow状态 - const { toolsNotAllow } = ConfigManager.tool; + const { toolsNotAllow } = Config.tool; toolsNotAllow.forEach(key => { if (this.tool.toolStatus.hasOwnProperty(key)) { this.tool.toolStatus[key] = false; @@ -150,7 +150,7 @@ export class AI { // 解析body,检查是否为流式 let stream = false; try { - const bodyTemplate = ConfigManager.request.bodyTemplate; + const bodyTemplate = Config.request.bodyTemplate; const bodyObject = parseBody(bodyTemplate, [], null, null); stream = bodyObject?.stream === true; } catch (err) { @@ -164,7 +164,7 @@ export class AI { } - const { isTool, usePromptEngineering } = ConfigManager.tool; + const { isTool, usePromptEngineering } = Config.tool; const toolInfos = this.tool.getToolsInfo(msg.messageType); let result = { contextArray: [], replyArray: [], images: [] }; @@ -190,7 +190,7 @@ export class AI { await this.context.addMessage(ctx, msg, this, match[0], [], "assistant", ''); try { - await ToolManager.handlePromptToolCall(ctx, msg, this, match[1]); + await ToolService.handlePromptToolCall(ctx, msg, this, match[1]); await this.chat(ctx, msg, '函数回调触发'); } catch (e) { logger.error(`在handlePromptToolCall中出错:`, e.message); @@ -206,7 +206,7 @@ export class AI { this.context.addToolCallsMessage(tool_calls); try { - tool_choice = await ToolManager.handleToolCalls(ctx, msg, this, tool_calls); + tool_choice = await ToolService.handleToolCalls(ctx, msg, this, tool_calls); await this.chat(ctx, msg, '函数回调触发', tool_choice); } catch (e) { logger.error(`在handleToolCalls中出错:`, e.message); @@ -238,7 +238,7 @@ export class AI { } async chatStream(ctx: seal.MsgContext, msg: seal.Message): Promise { - const { isTool, usePromptEngineering } = ConfigManager.tool; + const { isTool, usePromptEngineering } = Config.tool; await this.stopCurrentChatStream(); @@ -298,7 +298,7 @@ export class AI { await this.context.addMessage(ctx, msg, this, match[0], [], "assistant", ''); try { - await ToolManager.handlePromptToolCall(ctx, msg, this, match[1]); + await ToolService.handlePromptToolCall(ctx, msg, this, match[1]); } catch (e) { logger.error(`在handlePromptToolCall中出错:`, e.message); return; @@ -415,7 +415,7 @@ export class AIManager { static get usageMap(): { [model: string]: { [time: number]: UsageInfo } } { if (!this.usageMapCache) { try { - this.usageMapCache = JSON.parse(ConfigManager.ext.storageGet('usageMap') || '{}'); + this.usageMapCache = JSON.parse(Config.ext.storageGet('usageMap') || '{}'); } catch (error) { logger.error(`从数据库中获取usageMap失败:`, error); } @@ -432,7 +432,7 @@ export class AIManager { let ai = new AI(); try { - ai = JSON.parse(ConfigManager.ext.storageGet(`AI_${id}`) || '{}', (key, value) => { + ai = JSON.parse(Config.ext.storageGet(`AI_${id}`) || '{}', (key, value) => { if (key === "") { return revive(AI, value); } @@ -443,7 +443,7 @@ export class AIManager { return context; } if (key === "tool") { - const tm = revive(ToolManager, value); + const tm = revive(ToolService, value); tm.reviveToolStauts(); return tm; } @@ -474,7 +474,7 @@ export class AIManager { static saveAI(id: string) { if (this.cache.hasOwnProperty(id)) { - ConfigManager.ext.storageSet(`AI_${id}`, JSON.stringify(this.cache[id])); + Config.ext.storageSet(`AI_${id}`, JSON.stringify(this.cache[id])); } } @@ -526,7 +526,7 @@ export class AIManager { } static saveUsageMap() { - ConfigManager.ext.storageSet('usageMap', JSON.stringify(this.usageMapCache)); + Config.ext.storageSet('usageMap', JSON.stringify(this.usageMapCache)); } static updateUsage(model: string, usage: { diff --git a/src/agent/agent.ts b/src/agent/agent.ts new file mode 100644 index 0000000..eb72fad --- /dev/null +++ b/src/agent/agent.ts @@ -0,0 +1,68 @@ +import Config from "../config/config"; +import { logger } from "../logger"; +import { SessionService } from "../session/session"; +import { ToolName } from "../tool/tool"; +import { revive, TypeDescriptor } from "../utils/utils"; +import Model from "./model"; +import { ChatModelUse, ModelUse } from "../model/types"; + +export default class Agent { + static validKeysMap: { [key in keyof Agent]?: TypeDescriptor } = { + sessionService: SessionService, + tools: { array: 'string' }, + subAgents: { array: 'string' } + } + + name: string; + description: string; + instruction: string | ((sessionService: SessionService) => string); + use: ChatModelUse; + + sessionService: SessionService; + tools: ToolName[]; + subAgents: string[]; + + constructor() { + this.name = ""; + this.description = ""; + this.instruction = ""; + this.use = "chat"; + this.sessionService = new SessionService(); + this.sessionService.agentName = this.name; + this.tools = []; + this.subAgents = []; + } + + // wip + getRequestTools() { + } + + async chat(prompt: string): Promise { + const model = Model.getChatModel(this.use); + } + + static agentMap: { [key: string]: Agent } = {}; + + static get(name: string): Agent { + if (!this.agentMap.hasOwnProperty(name)) { + let agent = new Agent(); + try { + const data = JSON.parse(Config.ext.storageGet(`agent_${name}`) || '{}'); + agent = revive(Agent, data); + } catch (error) { + logger.error(`加载智能体${name}失败: ${error}`); + } + agent.name = name; + this.agentMap[name] = agent; + } + return this.agentMap[name]; + } + + static save(agent: Agent) { + Config.ext.storageSet(`agent_${agent.name}`, JSON.stringify(agent)); + } + + static init() { + + } +} \ No newline at end of file diff --git a/src/agent/agents/compress_agent.ts b/src/agent/agents/compress_agent.ts new file mode 100644 index 0000000..8366413 --- /dev/null +++ b/src/agent/agents/compress_agent.ts @@ -0,0 +1,9 @@ +import Agent from "../agent"; + +const compressAgent = Agent.get("compress_agent"); +compressAgent.name = "compress_agent"; +compressAgent.description = "压缩智能体"; +compressAgent.instruction = "你是一个压缩智能体,你可以压缩文本。"; +compressAgent.use = "compression"; +Agent.save(compressAgent); +export default compressAgent; diff --git a/src/agent/agents/samples.ts b/src/agent/agents/samples.ts new file mode 100644 index 0000000..c4b5acb --- /dev/null +++ b/src/agent/agents/samples.ts @@ -0,0 +1,9 @@ +import Agent from "../agent"; + +const sampleAgent = Agent.get("sample_agent"); +sampleAgent.name = "sample_agent"; +sampleAgent.description = "示例智能体"; +sampleAgent.instruction = "你是一个示例智能体。"; +sampleAgent.use = "chat"; +Agent.save(sampleAgent); +export default sampleAgent; diff --git a/src/agent/agents/summarize_agent.ts b/src/agent/agents/summarize_agent.ts new file mode 100644 index 0000000..b9eedb6 --- /dev/null +++ b/src/agent/agents/summarize_agent.ts @@ -0,0 +1,9 @@ +import Agent from "../agent"; + +const summarizeAgent = Agent.get("summarize_agent"); +summarizeAgent.name = "summarize_agent"; +summarizeAgent.description = "摘要智能体"; +summarizeAgent.instruction = "你是一个摘要智能体,你可以摘要文本。"; +summarizeAgent.use = "summarization"; +Agent.save(summarizeAgent); +export default summarizeAgent; diff --git a/src/agent/stream.ts b/src/agent/stream.ts new file mode 100644 index 0000000..68b4152 --- /dev/null +++ b/src/agent/stream.ts @@ -0,0 +1,151 @@ +import Config from "../config/config"; +import { logger } from "../logger"; +import { withTimeout } from "../utils/utils"; +import { Agent } from "./agent"; +import { ModelManager } from "./model"; +import { UsageManager } from "./usage"; + +export class streamService { + static async startStream(agent: Agent, sessionId: string): Promise { + const { timeout } = Config.request; + const { STREAM: streamUrl } = Config.backend; + const model = ModelManager.getChatModel('chat'); + try { + const body = model.buildChatBody(agent, sessionId); + + // 打印请求发送前的上下文 + const s = JSON.stringify(body.messages, (key, value) => { + if (key === "" && Array.isArray(value)) { + return value.filter(item => item.role !== "system"); + } + return value; + }); + logger.info(`请求发送前的上下文:\n`, s); + + const response = await withTimeout(() => fetch(`${streamUrl}/start`, { + method: 'POST', + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + }, + body: JSON.stringify({ + url: model.url, + api_key: model.apiKey, + body_obj: body + }) + }), timeout); + + // logger.info("响应体", JSON.stringify(response, null, 2)); + + const text = await response.text(); + if (!response.ok) { + throw new Error(`请求失败! 状态码: ${response.status}\n响应体:${text}`); + } + if (!text) { + throw new Error("响应体为空"); + } + + try { + const data = JSON.parse(text); + if (data.error) { + throw new Error(`请求失败! 错误信息: ${data.error.message}`); + } + if (!data.id) { + throw new Error("服务器响应中没有id字段"); + } + return data.id; + } catch (e) { + throw new Error(`解析响应体时出错:${e}\n响应体:${text}`); + } + } catch (e) { + logger.error("在startStream中出错:", e.message); + return ''; + } + } + + static async pollStream(streamId: string, after: number): Promise<{ status: string, reply: string, nextAfter: number }> { + const { STREAM: streamUrl } = Config.backend; + + try { + const response = await fetch(`${streamUrl}/poll?id=${streamId}&after=${after}`, { + method: 'GET', + headers: { + "Accept": "application/json" + } + }); + + // logger.info("响应体", JSON.stringify(response, null, 2)); + + const text = await response.text(); + if (!response.ok) { + throw new Error(`请求失败! 状态码: ${response.status}\n响应体:${text}`); + } + if (!text) { + throw new Error("响应体为空"); + } + + try { + const data = JSON.parse(text); + if (data.error) { + throw new Error(`请求失败! 错误信息: ${data.error.message}`); + } + if (!data.status) { + throw new Error("服务器响应中没有status字段"); + } + return { + status: data.status, + reply: data.results.join(''), + nextAfter: data.next_after + }; + } catch (e) { + throw new Error(`解析响应体时出错:${e}\n响应体:${text}`); + } + } catch (e) { + logger.error("在pollStream中出错:", e.message); + return { status: 'failed', reply: '', nextAfter: 0 }; + } + } + + static async endStream(streamId: string): Promise { + const { STREAM: streamUrl } = Config.backend; + + try { + const response = await fetch(`${streamUrl}/end?id=${streamId}`, { + method: 'GET', + headers: { + "Accept": "application/json" + } + }); + + // logger.info("响应体", JSON.stringify(response, null, 2)); + + const text = await response.text(); + if (!response.ok) { + throw new Error(`请求失败! 状态码: ${response.status}\n响应体:${text}`); + } + if (!text) { + throw new Error("响应体为空"); + } + + try { + const data = JSON.parse(text); + if (data.error) { + throw new Error(`请求失败! 错误信息: ${data.error.message}`); + } + if (!data.status) { + throw new Error("服务器响应中没有status字段"); + } + logger.info('对话结束', data.status === 'success' ? '成功' : '失败'); + if (data.status === 'success') { + UsageManager.updateUsage(data.model, data.usage); + } + return data.status; + } catch (e) { + throw new Error(`解析响应体时出错:${e}\n响应体:${text}`); + } + } catch (e) { + logger.error("在endStream中出错:", e.message); + return ''; + } + } +} \ No newline at end of file diff --git a/src/cmd/privilege.ts b/src/cmd/privilege.ts index 4ec7854..623c398 100644 --- a/src/cmd/privilege.ts +++ b/src/cmd/privilege.ts @@ -1,8 +1,8 @@ import { AI } from "../AI/AI"; import { logger } from "../logger"; -import { ConfigManager } from "../config/configManager"; +import { Config } from "../config/config"; import { aliasToCmd } from "../utils/utils"; -import { PRIVILEGELEVELMAP } from "../config/config"; +import { PRIVILEGE_LEVEL_MAP } from "../config/static_config"; export interface CmdPrivInfo { @@ -12,10 +12,10 @@ export interface CmdPrivInfo { export interface CmdPriv { [key: string]: CmdPrivInfo }; -export const U: [number, number, number] = [0, PRIVILEGELEVELMAP.user, PRIVILEGELEVELMAP.user]; // user -export const M: [number, number, number] = [0, PRIVILEGELEVELMAP.master, PRIVILEGELEVELMAP.master]; // master -export const I: [number, number, number] = [0, PRIVILEGELEVELMAP.inviter, PRIVILEGELEVELMAP.inviter]; // inviter -export const S: [number, number, number] = [1, PRIVILEGELEVELMAP.inviter, PRIVILEGELEVELMAP.master]; // spesial,会话所需权限为1,是才能被邀请者使用,否则需为骰主 +export const U: [number, number, number] = [0, PRIVILEGE_LEVEL_MAP.user, PRIVILEGE_LEVEL_MAP.user]; // user +export const M: [number, number, number] = [0, PRIVILEGE_LEVEL_MAP.master, PRIVILEGE_LEVEL_MAP.master]; // master +export const I: [number, number, number] = [0, PRIVILEGE_LEVEL_MAP.inviter, PRIVILEGE_LEVEL_MAP.inviter]; // inviter +export const S: [number, number, number] = [1, PRIVILEGE_LEVEL_MAP.inviter, PRIVILEGE_LEVEL_MAP.master]; // spesial,会话所需权限为1,是才能被邀请者使用,否则需为骰主 export const defaultCmdPriv: CmdPriv = { ai: { priv: U } }; @@ -24,7 +24,7 @@ export class PrivilegeManager { static reviveCmdPriv() { try { - const cmdPriv = JSON.parse(ConfigManager.ext.storageGet('cmdPriv') || '{}'); + const cmdPriv = JSON.parse(Config.ext.storageGet('cmdPriv') || '{}'); if (typeof cmdPriv === 'object' && !Array.isArray(cmdPriv)) { this.cmdPriv = this.updateCmdPriv(cmdPriv, JSON.parse(JSON.stringify(defaultCmdPriv))); this.saveCmdPriv(); @@ -37,7 +37,7 @@ export class PrivilegeManager { } static saveCmdPriv() { - ConfigManager.ext.storageSet('cmdPriv', JSON.stringify(this.cmdPriv)); + Config.ext.storageSet('cmdPriv', JSON.stringify(this.cmdPriv)); } static updateCmdPriv(cp: CmdPriv, defaultCp: CmdPriv): CmdPriv { diff --git a/src/cmd/root.ts b/src/cmd/root_cmd.ts similarity index 96% rename from src/cmd/root.ts rename to src/cmd/root_cmd.ts index b326d28..a4e6c12 100644 --- a/src/cmd/root.ts +++ b/src/cmd/root_cmd.ts @@ -1,5 +1,5 @@ import { AI, AIManager } from "../AI/AI"; -import { ConfigManager } from "../config/configManager"; +import { Config } from "../config/config"; import { logger } from "../logger"; import { CmdPrivInfo, defaultCmdPriv, PrivilegeManager, U } from "./privilege"; import { aliasToCmd } from "../utils/utils"; @@ -127,6 +127,6 @@ export function registerCmd() { } } - ConfigManager.ext.cmdMap['AI'] = cmd; - ConfigManager.ext.cmdMap['ai'] = cmd; + Config.ext.cmdMap['AI'] = cmd; + Config.ext.cmdMap['ai'] = cmd; } \ No newline at end of file diff --git a/src/cmd/sub_cmd/ctxn.ts b/src/cmd/sub_cmd/ctxn.ts index 352b18d..82705fa 100644 --- a/src/cmd/sub_cmd/ctxn.ts +++ b/src/cmd/sub_cmd/ctxn.ts @@ -1,6 +1,6 @@ import { aliasToCmd } from "../../utils/utils"; import { I, U } from "../privilege"; -import { SubCmd, SubCmdContext } from "../root"; +import { SubCmd, SubCmdContext } from "../root_cmd"; export function registerCmdCtxn() { const cmd = new SubCmd('ctxn'); diff --git a/src/cmd/sub_cmd/forget.ts b/src/cmd/sub_cmd/forget.ts index 59b7c52..99a39e1 100644 --- a/src/cmd/sub_cmd/forget.ts +++ b/src/cmd/sub_cmd/forget.ts @@ -1,7 +1,7 @@ import { AIManager } from "../../AI/AI"; import { aliasToCmd } from "../../utils/utils"; import { I, U } from "../privilege"; -import { SubCmd, SubCmdContext } from "../root"; +import { SubCmd, SubCmdContext } from "../root_cmd"; export function registerCmdForget() { const cmd = new SubCmd('forget'); diff --git a/src/cmd/sub_cmd/ignore.ts b/src/cmd/sub_cmd/ignore.ts index 728213b..8935818 100644 --- a/src/cmd/sub_cmd/ignore.ts +++ b/src/cmd/sub_cmd/ignore.ts @@ -1,7 +1,7 @@ import { AIManager } from "../../AI/AI"; import { aliasToCmd } from "../../utils/utils"; import { U } from "../privilege"; -import { SubCmd, SubCmdContext } from "../root"; +import { SubCmd, SubCmdContext } from "../root_cmd"; export function registerCmdIgnore() { const cmd = new SubCmd('ignore'); diff --git a/src/cmd/sub_cmd/image.ts b/src/cmd/sub_cmd/image.ts index f03ae33..d4d83f3 100644 --- a/src/cmd/sub_cmd/image.ts +++ b/src/cmd/sub_cmd/image.ts @@ -1,9 +1,9 @@ import { AIManager } from "../../AI/AI"; -import { ImageManager } from "../../AI/image"; +import { ImageService } from "../../image"; import { aliasToCmd } from "../../utils/utils"; -import { transformArrayToContent, transformTextToArray } from "../../utils/utils_string"; +import { transformArrayToContent, transformTextToArray } from "../../utils/string"; import { I, M, U } from "../privilege"; -import { SubCmd, SubCmdContext } from "../root"; +import { SubCmd, SubCmdContext } from "../root_cmd"; export function registerCmdImage() { const cmd = new SubCmd('image'); @@ -41,7 +41,7 @@ export function registerCmdImage() { return ret; } case 'local': { - seal.replyToSender(ctx, msg, ImageManager.getLocalImageListText(page) || '暂无本地图片'); + seal.replyToSender(ctx, msg, ImageService.getLocalImageListText(page) || '暂无本地图片'); return ret; } default: { @@ -88,7 +88,7 @@ export function registerCmdImage() { if (images.length === 0) seal.replyToSender(ctx, msg, '请附带图片'); const img = images[0]; await img.imageToText(cmdArgs.getRestArgsFrom(4)) - seal.replyToSender(ctx, msg, img.CQCode + `\n` + img.content); + seal.replyToSender(ctx, msg, img.CQCode + `\n` + img.description); return ret; } case 'find': { diff --git a/src/cmd/sub_cmd/memory.ts b/src/cmd/sub_cmd/memory.ts index 7b59807..e1fa414 100644 --- a/src/cmd/sub_cmd/memory.ts +++ b/src/cmd/sub_cmd/memory.ts @@ -1,8 +1,8 @@ import { AIManager } from "../../AI/AI"; -import { ConfigManager } from "../../config/configManager"; +import { Config } from "../../config/config"; import { aliasToCmd } from "../../utils/utils"; import { I, S, U } from "../privilege"; -import { SubCmd, SubCmdContext } from "../root"; +import { SubCmd, SubCmdContext } from "../root_cmd"; export function registerCmdMemory() { const cmd = new SubCmd('memory'); @@ -62,7 +62,7 @@ export function registerCmdMemory() { if (cmdArgs.at.length > 0 && (cmdArgs.at.length !== 1 || cmdArgs.at[0].userId !== epId)) { ai3 = ai2; } - const { isMemory, isShortMemory } = ConfigManager.memory; + const { isMemory, isShortMemory } = Config.memory; seal.replyToSender(ctx, msg, `${ai3.id} 长期记忆开启状态: ${isMemory ? '是' : '否'} 长期记忆条数: ${ai3.memory.memoryIds.length} diff --git a/src/cmd/sub_cmd/off.ts b/src/cmd/sub_cmd/off.ts index e52452d..51900ea 100644 --- a/src/cmd/sub_cmd/off.ts +++ b/src/cmd/sub_cmd/off.ts @@ -1,7 +1,7 @@ import { AIManager } from "../../AI/AI"; import { TimerManager } from "../../timer"; import { I } from "../privilege"; -import { SubCmd, SubCmdContext } from "../root"; +import { SubCmd, SubCmdContext } from "../root_cmd"; export function registerCmdOff() { const cmd = new SubCmd('off'); diff --git a/src/cmd/sub_cmd/on.ts b/src/cmd/sub_cmd/on.ts index 68e791c..b7db8dd 100644 --- a/src/cmd/sub_cmd/on.ts +++ b/src/cmd/sub_cmd/on.ts @@ -1,7 +1,7 @@ import { AIManager } from "../../AI/AI"; import { TimerManager } from "../../timer"; import { S } from "../privilege"; -import { SubCmd, SubCmdContext } from "../root"; +import { SubCmd, SubCmdContext } from "../root_cmd"; export function registerCmdOn() { const cmd = new SubCmd('on'); diff --git a/src/cmd/sub_cmd/privilege.ts b/src/cmd/sub_cmd/privilege.ts index 8646b58..0453159 100644 --- a/src/cmd/sub_cmd/privilege.ts +++ b/src/cmd/sub_cmd/privilege.ts @@ -1,8 +1,8 @@ import { AIManager } from "../../AI/AI"; -import { HELPMAP } from "../../config/config"; +import { HELP_MAP } from "../../config/static_config"; import { aliasToCmd } from "../../utils/utils"; import { M, PrivilegeManager, U } from "../privilege"; -import { SubCmd, SubCmdContext } from "../root"; +import { SubCmd, SubCmdContext } from "../root_cmd"; export function registerCmdPrivilege() { const cmd = new SubCmd('privilege'); @@ -13,10 +13,10 @@ export function registerCmdPrivilege() { 【.ai priv st <指令> <权限限制>】修改指令权限 【.ai priv show <指令>】检查指令权限 【.ai priv reset】重置指令权限 -${HELPMAP["ID"]} -${HELPMAP["会话权限"]} -${HELPMAP["指令"]} -${HELPMAP["权限限制"]}`; +${HELP_MAP["ID"]} +${HELP_MAP["会话权限"]} +${HELP_MAP["指令"]} +${HELP_MAP["权限限制"]}`; cmd.priv = { priv: M, args: { session: { @@ -43,8 +43,8 @@ ${HELPMAP["权限限制"]}`; if (!val4 || val4 == 'help') { seal.replyToSender(ctx, msg, `帮助: 【.ai priv ses st <会话权限>】修改会话权限 -${HELPMAP["ID"]} -${HELPMAP["会话权限"]}`); +${HELP_MAP["ID"]} +${HELP_MAP["会话权限"]}`); return ret; } @@ -69,7 +69,7 @@ ${HELPMAP["会话权限"]}`); if (!val4 || val4 == 'help') { seal.replyToSender(ctx, msg, `帮助: 【.ai priv ses ck 】检查会话权限 -${HELPMAP["ID"]}`); +${HELP_MAP["ID"]}`); return ret; } @@ -82,8 +82,8 @@ ${HELPMAP["ID"]}`); seal.replyToSender(ctx, msg, `帮助: 【.ai priv ses st <会话权限>】修改会话权限 【.ai priv ses ck 】检查会话权限 -${HELPMAP["ID"]} -${HELPMAP["会话权限"]}`); +${HELP_MAP["ID"]} +${HELP_MAP["会话权限"]}`); return ret; } } @@ -93,8 +93,8 @@ ${HELPMAP["会话权限"]}`); if (!val3 || val3 == 'help') { seal.replyToSender(ctx, msg, `帮助: 【.ai priv st <指令> <权限限制>】修改指令权限 -${HELPMAP["指令"]} -${HELPMAP["权限限制"]}`); +${HELP_MAP["指令"]} +${HELP_MAP["权限限制"]}`); return ret; } const cmdChain = val3.split('-').map(cmd => aliasToCmd(cmd)); @@ -129,7 +129,7 @@ ${HELPMAP["权限限制"]}`); if (!val3 || val3 == 'help') { seal.replyToSender(ctx, msg, `帮助: 【.ai priv show <指令>】检查指令权限 -${HELPMAP["指令"]}`); +${HELP_MAP["指令"]}`); return ret; } const cmdChain = val3.split('-'); diff --git a/src/cmd/sub_cmd/prompt.ts b/src/cmd/sub_cmd/prompt.ts index 1b10b29..f7b4aa0 100644 --- a/src/cmd/sub_cmd/prompt.ts +++ b/src/cmd/sub_cmd/prompt.ts @@ -1,7 +1,7 @@ import { logger } from "../../logger"; -import { buildSystemMessage } from "../../utils/utils_message"; +import { buildSystemMessage } from "../../utils/message"; import { M } from "../privilege"; -import { SubCmd, SubCmdContext } from "../root"; +import { SubCmd, SubCmdContext } from "../root_cmd"; export function registerCmdPrompt() { const cmd = new SubCmd('prompt'); @@ -11,8 +11,8 @@ export function registerCmdPrompt() { cmd.solve = async (scc: SubCmdContext) => { const { ctx, msg, ai, ret } = scc; const systemMessage = await buildSystemMessage(ctx, ai); - logger.info(`system prompt:\n`, systemMessage.msgArray[0].content); - seal.replyToSender(ctx, msg, systemMessage.msgArray[0].content); + logger.info(`system prompt:\n`, systemMessage.msgArray[0].text); + seal.replyToSender(ctx, msg, systemMessage.msgArray[0].text); return ret; } } diff --git a/src/cmd/sub_cmd/role.ts b/src/cmd/sub_cmd/role.ts index 6550f6a..7c2d239 100644 --- a/src/cmd/sub_cmd/role.ts +++ b/src/cmd/sub_cmd/role.ts @@ -1,7 +1,7 @@ -import { ConfigManager } from "../../config/configManager"; -import { getRoleSetting } from "../../utils/utils_message"; +import { Config } from "../../config/config"; +import { getRoleSetting } from "../../utils/message"; import { I } from "../privilege"; -import { SubCmd, SubCmdContext } from "../root"; +import { SubCmd, SubCmdContext } from "../root_cmd"; export function registerCmdRole() { const cmd = new SubCmd('role'); @@ -11,7 +11,7 @@ export function registerCmdRole() { cmd.solve = (scc: SubCmdContext) => { const { ctx, msg, cmdArgs, ret } = scc; - const { roleSettingNames, roleSettingTemplate } = ConfigManager.message; + const { roleSettingNames, roleSettingTemplate } = Config.message; const { roleName } = getRoleSetting(ctx); const val2 = cmdArgs.getArgN(2); if (!val2) { diff --git a/src/cmd/sub_cmd/sample.ts b/src/cmd/sub_cmd/sample.ts index 1bf6927..5a8a944 100644 --- a/src/cmd/sub_cmd/sample.ts +++ b/src/cmd/sub_cmd/sample.ts @@ -1,5 +1,5 @@ import { U } from "../privilege"; -import { SubCmd, SubCmdContext } from "../root"; +import { SubCmd, SubCmdContext } from "../root_cmd"; export function registerCmdSample() { const cmd = new SubCmd('sample'); diff --git a/src/cmd/sub_cmd/shut.ts b/src/cmd/sub_cmd/shut.ts index 6e6a1b0..1050c77 100644 --- a/src/cmd/sub_cmd/shut.ts +++ b/src/cmd/sub_cmd/shut.ts @@ -1,5 +1,5 @@ import { U } from "../privilege"; -import { SubCmd, SubCmdContext } from "../root"; +import { SubCmd, SubCmdContext } from "../root_cmd"; export function registerCmdShut() { const cmd = new SubCmd('shut'); diff --git a/src/cmd/sub_cmd/standby.ts b/src/cmd/sub_cmd/standby.ts index 9d16024..7e817c4 100644 --- a/src/cmd/sub_cmd/standby.ts +++ b/src/cmd/sub_cmd/standby.ts @@ -1,7 +1,7 @@ import { AIManager } from "../../AI/AI"; import { TimerManager } from "../../timer"; import { I } from "../privilege"; -import { SubCmd, SubCmdContext } from "../root"; +import { SubCmd, SubCmdContext } from "../root_cmd"; export function registerCmdStandby() { const cmd = new SubCmd('standby'); diff --git a/src/cmd/sub_cmd/status.ts b/src/cmd/sub_cmd/status.ts index 0ff4ec2..0bce740 100644 --- a/src/cmd/sub_cmd/status.ts +++ b/src/cmd/sub_cmd/status.ts @@ -1,5 +1,5 @@ import { U } from "../privilege"; -import { SubCmd, SubCmdContext } from "../root"; +import { SubCmd, SubCmdContext } from "../root_cmd"; export function registerCmdStatus() { const cmd = new SubCmd('status'); diff --git a/src/cmd/sub_cmd/timer.ts b/src/cmd/sub_cmd/timer.ts index 9ff7f77..185f482 100644 --- a/src/cmd/sub_cmd/timer.ts +++ b/src/cmd/sub_cmd/timer.ts @@ -1,7 +1,7 @@ import { TimerManager } from "../../timer"; import { aliasToCmd } from "../../utils/utils"; import { I, U } from "../privilege"; -import { SubCmd, SubCmdContext } from "../root"; +import { SubCmd, SubCmdContext } from "../root_cmd"; export function registerCmdTimer() { const cmd = new SubCmd('timer'); diff --git a/src/cmd/sub_cmd/token.ts b/src/cmd/sub_cmd/token.ts index 6587e98..38193c1 100644 --- a/src/cmd/sub_cmd/token.ts +++ b/src/cmd/sub_cmd/token.ts @@ -1,8 +1,8 @@ import { AIManager } from "../../AI/AI"; -import { get_chart_url } from "../../service"; +import { get_chart_url } from "../../agent/service"; import { aliasToCmd } from "../../utils/utils"; import { S, U } from "../privilege"; -import { SubCmd, SubCmdContext } from "../root"; +import { SubCmd, SubCmdContext } from "../root_cmd"; export function registerCmdToken() { const cmd = new SubCmd('token'); diff --git a/src/cmd/sub_cmd/tool.ts b/src/cmd/sub_cmd/tool.ts index fcf50e0..4459467 100644 --- a/src/cmd/sub_cmd/tool.ts +++ b/src/cmd/sub_cmd/tool.ts @@ -1,10 +1,10 @@ import { AIManager } from "../../AI/AI"; -import { ConfigManager } from "../../config/configManager"; +import { Config } from "../../config/config"; import { logger } from "../../logger"; -import { ToolManager } from "../../tool/tool"; +import { ToolService } from "../../tool/tool"; import { aliasToCmd } from "../../utils/utils"; import { I, M, U } from "../privilege"; -import { SubCmd, SubCmdContext } from "../root"; +import { SubCmd, SubCmdContext } from "../root_cmd"; export function registerCmdTool() { const cmd = new SubCmd('tool'); @@ -27,7 +27,7 @@ export function registerCmdTool() { case 'on': { const val3 = cmdArgs.getArgN(3); if (val3) { - const toolsNotAllow = ConfigManager.tool.toolsNotAllow; + const toolsNotAllow = Config.tool.toolsNotAllow; if (toolsNotAllow.includes(val3)) { seal.replyToSender(ctx, msg, `工具函数 ${val3} 不被允许开启`); return ret; @@ -38,7 +38,7 @@ export function registerCmdTool() { AIManager.saveAI(sid); return ret; } - const toolsNotAllow = ConfigManager.tool.toolsNotAllow; + const toolsNotAllow = Config.tool.toolsNotAllow; for (const key in ai.tool.toolStatus) { ai.tool.toolStatus[key] = toolsNotAllow.includes(key) ? false : true; } @@ -72,19 +72,19 @@ export function registerCmdTool() { return ret; } - if (!ToolManager.toolMap.hasOwnProperty(val3)) { + if (!ToolService.toolMap.hasOwnProperty(val3)) { seal.replyToSender(ctx, msg, '没有这个工具函数'); return ret; } - const tool = ToolManager.toolMap[val3]; - const s = `${tool.info.function.name} - 描述:${tool.info.function.description} + const tool = ToolService.toolMap[val3]; + const s = `${tool.toolInfo.function.name} + 描述:${tool.toolInfo.function.description} 参数信息: - ${JSON.stringify(tool.info.function.parameters.properties, null, 2)} + ${JSON.stringify(tool.toolInfo.function.parameters.properties, null, 2)} - 必需参数:${tool.info.function.parameters.required.join(',')}`; + 必需参数:${tool.toolInfo.function.parameters.required.join(',')}`; seal.replyToSender(ctx, msg, s); return ret; @@ -95,12 +95,12 @@ export function registerCmdTool() { seal.replyToSender(ctx, msg, `调用函数缺少工具函数名`); return ret; } - if (!ToolManager.toolMap.hasOwnProperty(val3)) { + if (!ToolService.toolMap.hasOwnProperty(val3)) { seal.replyToSender(ctx, msg, `调用函数失败:未注册的函数:${val3}`); return ret; } - const tool = ToolManager.toolMap[val3]; - if (tool.cmdInfo.ext !== '' && ToolManager.cmdArgs == null) { + const tool = ToolService.toolMap[val3]; + if (tool.ExtCmdInfo.extName !== '' && ToolService.cmdArgs == null) { seal.replyToSender(ctx, msg, `暂时无法调用函数,请先使用 .r 指令`); return ret; } @@ -116,7 +116,7 @@ export function registerCmdTool() { return acc; }, {}); - for (const key of tool.info.function.parameters.required) { + for (const key of tool.toolInfo.function.parameters.required) { if (!args.hasOwnProperty(key)) { logger.warning(`调用函数失败:缺少必需参数 ${key}`); seal.replyToSender(ctx, msg, `调用函数失败:缺少必需参数 ${key}`); diff --git a/src/config/config.ts b/src/config/config.ts index f3f6c1d..52bc6e8 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -1,369 +1,126 @@ -export const VERSION = "4.12.0"; -export const AUTHOR = "baiyu&错误"; -export const NAME = "aiplugin4"; +import Handlebars from "handlebars"; +import { logger } from "../logger"; +import { AUTHOR, CONFIG_CACHE_TTL, NAME, VERSION } from "./static_config"; +import BaseConfig from "./configs/base"; +import ModelConfig from "./configs/model"; +import BackendConfig from "./configs/backend"; +import ReceivedConfig from "./configs/received"; +import TriggerConfig from "./configs/trigger"; +import ImageConfig from "./configs/image"; +import ToolConfig from "./configs/tool"; +import MemoryConfig from "./configs/memory"; -export const CQTYPESALLOW = ["at", "image", "reply", "face", "poke"]; +const configMap = { + base: BaseConfig, + model: ModelConfig, + backend: BackendConfig, + received: ReceivedConfig, + trigger: TriggerConfig, + image: ImageConfig, + tool: ToolConfig, + memory: MemoryConfig, +} as const; -export const PRIVILEGELEVELMAP = { - "master": 100, - "whitelist": 70, - "owner": 60, - "admin": 50, - "inviter": 40, - "user": 0, - "blacklist": -30 +type ConfigMap = typeof configMap; +type ConfigKey = keyof ConfigMap; +type ConfigProps = { [K in ConfigKey]: ReturnType }; +type ConfigProp = ConfigProps[ConfigKey]; + +interface ConfigCache { + timestamp: number, + data: ConfigProp +} + +class _Config { + static ext: seal.ExtInfo; + static cache: { [K in ConfigKey]?: ConfigCache } = {} + + static registerConfig() { + this.ext = this.getExt(NAME); + for (const k of Object.keys(configMap) as ConfigKey[]) { + configMap[k].register(); + Object.defineProperty(this, k, { + get: () => this.getCache(k, configMap[k].get) + }) + } + } + + static getCache(key: ConfigKey, getFunc: () => ConfigProp): ConfigProp { + const timestamp = Date.now() + if (this.cache?.[key] && timestamp - this.cache[key].timestamp < CONFIG_CACHE_TTL) { + return this.cache[key].data; + } + const data = getFunc(); + this.cache[key] = { + timestamp: timestamp, + data: data + } + return data; + } + + static getExt(name: string): seal.ExtInfo { + if (name == NAME && this.ext) { + return this.ext; + } + + let ext = seal.ext.find(name); + if (!ext) { + ext = seal.ext.new(name, AUTHOR, VERSION); + seal.ext.register(ext); + } + + return ext; + } +} + +const Config = _Config as typeof _Config & ConfigProps; +export default Config; + +export function getRegexConfig(ext: seal.ExtInfo, key: string): RegExp { + const patterns = seal.ext.getTemplateConfig(ext, key).filter(x => x); + const pattern = patterns.join('|'); + if (pattern) { + try { + return new RegExp(pattern); + } catch (e) { + logger.error(`正则表达式错误,内容:${pattern},错误信息:${e.message}`); + return /(?!)/; + } + } + return /(?!)/; +} + +export function getRegexesConfig(ext: seal.ExtInfo, key: string): RegExp[] { + return seal.ext.getTemplateConfig(ext, key).map(x => { + try { + return new RegExp(x); + } catch (e) { + logger.error(`正则表达式错误,内容:${x},错误信息:${e.message}`); + return /(?!)/; + } + }); } -export const HELPMAP = { - "ID": `: -【QQ:1234567890】 私聊窗口 -【QQ-Group:1234】 群聊窗口 -【now】当前窗口`, - "会话权限": `<会话权限>:任意数字,越大权限越高`, - "指令": `<指令>:指令名称和参数,多个指令用-连接,如ai-sb`, - "权限限制": `<权限限制>:数字0-数字1-数字2,如0-0-0,含义如下: -0: 会话所需权限, 1: 会话检查通过后用户所需权限, 2: 强行触发指令用户所需权限, 进行检查时若通过0和1则无需检查2 -【-30】黑名单用户 -【0】普通用户 -【40】邀请者 -【50】群管理员 -【60】群主 -【70】白名单用户 -【100】骰主`, - "参数": `<参数>: -【c】计数器模式,接收消息数达到后触发 -单位/条,默认10条 -【t】计时器模式,最后一条消息后达到时限触发 -单位/秒,默认60秒 -【p】概率模式,每条消息按概率触发 -单位/%,默认10% -【a】活跃时间段和活跃次数 -格式为"开始时间-结束时间-活跃次数"(如"09:00-18:00-5")` +export function getHandlebarsTemplateConfig(ext: seal.ExtInfo, key: string): HandlebarsTemplateDelegate { + return Handlebars.compile(seal.ext.getTemplateConfig(ext, key)[0] || ''); } -export const aliasMap = { - "AI": "ai", - "priv": "privilege", - "ses": "session", - "st": "set", - "ck": "check", - "clr": "clear", - "sb": "standby", - "fgt": "forget", - "f": "forget", - "ass": "assistant", - "img": "image", - "memo": "memory", - "p": "private", - "g": "group", - "del": "delete", - "ign": "ignore", - "rm": "remove", - "lst": "list", - "tk": "token", - "y": "year", - "m": "month", - "lcl": "local", - "stl": "steal", - "ran": "random", - "nick": "nickname" +export function getHandlebarsTemplatesConfig(ext: seal.ExtInfo, key: string): HandlebarsTemplateDelegate[] { + return seal.ext.getTemplateConfig(ext, key).map(x => Handlebars.compile(x || '')); } -export const faceMap = { - "0": "惊讶", - "1": "撇嘴", - "2": "色", - "3": "发呆", - "4": "得意", - "5": "流泪", - "6": "害羞", - "7": "闭嘴", - "8": "睡", - "9": "大哭", - "10": "尴尬", - "11": "发怒", - "12": "调皮", - "13": "呲牙", - "14": "微笑", - "15": "难过", - "16": "酷", - "18": "抓狂", - "19": "吐", - "20": "偷笑", - "21": "可爱", - "22": "白眼", - "23": "傲慢", - "24": "饥饿", - "25": "困", - "26": "惊恐", - "27": "流汗", - "28": "憨笑", - "29": "悠闲", - "30": "奋斗", - "31": "咒骂", - "32": "疑问", - "33": "嘘", - "34": "晕", - "35": "折磨", - "36": "衰", - "37": "骷髅", - "38": "敲打", - "39": "再见", - "41": "发抖", - "42": "爱情", - "43": "跳跳", - "46": "猪头", - "49": "拥抱", - "53": "蛋糕", - "55": "炸弹", - "56": "刀", - "59": "便便", - "60": "咖啡", - "63": "玫瑰", - "64": "凋谢", - "66": "爱心", - "67": "心碎", - "74": "太阳", - "75": "月亮", - "76": "赞", - "77": "踩", - "78": "握手", - "79": "胜利", - "85": "飞吻", - "86": "怄火", - "89": "西瓜", - "96": "冷汗", - "97": "擦汗", - "98": "抠鼻", - "99": "鼓掌", - "100": "糗大了", - "101": "坏笑", - "102": "左哼哼", - "103": "右哼哼", - "104": "哈欠", - "105": "鄙视", - "106": "委屈", - "107": "快哭了", - "108": "阴险", - "109": "左亲亲", - "110": "吓", - "111": "可怜", - "112": "菜刀", - "114": "篮球", - "116": "示爱", - "118": "抱拳", - "119": "勾引", - "120": "拳头", - "121": "差劲", - "122": "爱你", - "123": "NO", - "124": "OK", - "125": "转圈", - "129": "挥手", - "137": "鞭炮", - "144": "喝彩", - "146": "爆筋", - "147": "棒棒糖", - "148": "喝奶", - "169": "手枪", - "171": "茶", - "172": "眨眼睛", - "173": "泪奔", - "174": "无奈", - "175": "卖萌", - "176": "小纠结", - "177": "喷血", - "178": "斜眼笑", - "179": "doge", - "180": "惊喜", - "181": "戳一戳", - "182": "笑哭", - "183": "我最美", - "185": "羊驼", - "187": "幽灵", - "193": "大笑", - "194": "不开心", - "198": "呃", - "200": "求求", - "201": "点赞", - "202": "无聊", - "203": "托脸", - "204": "吃", - "206": "害怕", - "210": "飙泪", - "211": "我不看", - "212": "托腮", - "214": "啵啵", - "215": "糊脸", - "216": "拍头", - "217": "扯一扯", - "218": "舔一舔", - "219": "蹭一蹭", - "221": "顶呱呱", - "222": "抱抱", - "223": "暴击", - "224": "开枪", - "225": "撩一撩", - "226": "拍桌", - "227": "拍手", - "229": "干杯", - "230": "嘲讽", - "231": "哼", - "232": "佛系", - "233": "掐一掐", - "235": "颤抖", - "237": "偷看", - "238": "扇脸", - "239": "原谅", - "240": "喷脸", - "241": "生日快乐", - "243": "甩头", - "244": "扔狗", - "262": "脑阔疼", - "263": "沧桑", - "264": "捂脸", - "265": "辣眼睛", - "266": "哦哟", - "267": "头秃", - "268": "问号脸", - "269": "暗中观察", - "270": "emm", - "271": "吃瓜", - "272": "呵呵哒", - "273": "我酸了", - "277": "汪汪", - "278": "汗", - "281": "无眼笑", - "282": "敬礼", - "283": "狂笑", - "284": "面无表情", - "285": "摸鱼", - "286": "魔鬼笑", - "287": "哦", - "288": "请", - "289": "睁眼", - "290": "敲开心", - "292": "让我康康", - "293": "摸锦鲤", - "294": "期待", - "295": "拿到红包", - "297": "拜谢", - "298": "元宝", - "299": "牛啊", - "300": "胖三斤", - "301": "好闪", - "302": "左拜年", - "303": "右拜年", - "305": "右亲亲", - "306": "牛气冲天", - "307": "喵喵", - "311": "打call", - "312": "变形", - "314": "仔细分析", - "317": "菜汪", - "318": "崇拜", - "319": "比心", - "320": "庆祝", - "322": "拒绝", - "323": "嫌弃", - "324": "吃糖", - "325": "惊吓", - "326": "生气", - "332": "举牌牌", - "333": "烟花", - "334": "虎虎生威", - "336": "豹富", - "337": "花朵脸", - "338": "我想开了", - "339": "舔屏", - "341": "打招呼", - "342": "酸Q", - "343": "我方了", - "344": "大怨种", - "345": "红包多多", - "346": "你真棒棒", - "347": "大展宏兔", - "348": "福萝卜", - "349": "坚强", - "350": "贴贴", - "351": "敲敲", - "352": "咦", - "353": "拜托", - "354": "尊嘟假嘟", - "355": "耶", - "356": "666", - "357": "裂开", - "358": "骰子", - "359": "包剪锤", - "360": "亲亲", - "361": "狗狗笑哭", - "362": "好兄弟", - "363": "狗狗可怜", - "364": "超级赞", - "365": "狗狗生气", - "366": "芒狗", - "367": "狗狗疑问", - "368": "奥特笑哭", - "369": "彩虹", - "370": "祝贺", - "371": "冒泡", - "372": "气呼呼", - "373": "忙", - "374": "波波流泪", - "375": "超级鼓掌", - "376": "跺脚", - "377": "嗨", - "378": "企鹅笑哭", - "379": "企鹅流泪", - "380": "真棒", - "381": "路过", - "382": "emo", - "383": "企鹅爱心", - "384": "晚安", - "385": "太气了", - "386": "呜呜呜", - "387": "太好笑", - "388": "太头疼", - "389": "太赞了", - "390": "太头秃", - "391": "太沧桑", - "392": "龙年快乐", - "393": "新年中龙", - "394": "新年大龙", - "395": "略略略", - "396": "狼狗", - "397": "抛媚眼", - "398": "超级ok", - "399": "tui", - "400": "快乐", - "401": "超级转圈", - "402": "别说话", - "403": "出去玩", - "404": "闪亮登场", - "405": "好运来", - "406": "姐是女王", - "407": "我听听", - "408": "臭美", - "409": "送你花花", - "410": "么么哒", - "411": "一起嗨", - "412": "开心", - "413": "摇起来", - "415": "划龙舟", - "416": "中龙舟", - "417": "大龙舟", - "419": "火车", - "420": "中火车", - "421": "大火车", - "422": "粽于等到你", - "423": "复兴号", - "424": "续标识", - "425": "求放过", - "426": "玩火", - "427": "偷感", - "428": "收到", - "429": "蛇年快乐", - "430": "蛇身", - "431": "蛇尾", - "432": "灵蛇献瑞" +export function getPathMapConfig(ext: seal.ExtInfo, key: string): { [id: string]: string } { + const paths = seal.ext.getTemplateConfig(ext, key).filter(x => x); + const pathMap: { [id: string]: string } = paths.reduce((acc: { [id: string]: string }, path: string) => { + if (path.trim() === '') return acc; + try { + const id = path.split('/').pop().replace(/\.[^/.]+$/, ''); + if (!id) throw new Error(`本地路径格式错误:${path}`); + acc[id] = path; + } catch (e) { + logger.error(`本地路径格式错误:${path},错误信息:${e.message}`); + } + return acc; + }, {}); + return pathMap; } \ No newline at end of file diff --git a/src/config/configManager.ts b/src/config/configManager.ts deleted file mode 100644 index 018cd92..0000000 --- a/src/config/configManager.ts +++ /dev/null @@ -1,123 +0,0 @@ -import Handlebars from "handlebars"; -import { logger } from "../logger"; -import { AUTHOR, NAME, VERSION } from "./config"; -import { BackendConfig } from "./config_backend"; -import { ImageConfig } from "./config_image"; -import { LogConfig } from "./config_log"; -import { MemoryConfig } from "./config_memory"; -import { MessageConfig } from "./config_message"; -import { ReceivedConfig } from "./config_received"; -import { ReplyConfig } from "./config_reply"; -import { RequestConfig } from "./config_request"; -import { ToolConfig } from "./config_tool"; - -export class ConfigManager { - static ext: seal.ExtInfo; - static cache: { - [key: string]: { - timestamp: number, - data: any - } - } = {} - - static registerConfig() { - this.ext = ConfigManager.getExt(NAME); - LogConfig.register(); - RequestConfig.register(); - MessageConfig.register(); - ToolConfig.register(); - ReceivedConfig.register(); - ReplyConfig.register(); - ImageConfig.register(); - BackendConfig.register(); - MemoryConfig.register(); - } - - static getCache(key: string, getFunc: () => T): T { - const timestamp = Date.now() - if (this.cache?.[key] && timestamp - this.cache[key].timestamp < 3000) { - return this.cache[key].data; - } - - const data = getFunc(); - this.cache[key] = { - timestamp: timestamp, - data: data - } - - return data; - } - - static get log() { return this.getCache('log', LogConfig.get) } - static get request() { return this.getCache('request', RequestConfig.get) } - static get message() { return this.getCache('message', MessageConfig.get) } - static get tool() { return this.getCache('tool', ToolConfig.get) } - static get received() { return this.getCache('received', ReceivedConfig.get) } - static get reply() { return this.getCache('reply', ReplyConfig.get) } - static get image() { return this.getCache('image', ImageConfig.get) } - static get backend() { return this.getCache('backend', BackendConfig.get) } - static get memory() { return this.getCache('memory', MemoryConfig.get) } - - static getExt(name: string): seal.ExtInfo { - if (name == NAME && ConfigManager.ext) { - return ConfigManager.ext; - } - - let ext = seal.ext.find(name); - if (!ext) { - ext = seal.ext.new(name, AUTHOR, VERSION); - seal.ext.register(ext); - } - - return ext; - } - - static getRegexConfig(ext: seal.ExtInfo, key: string): RegExp { - const patterns = seal.ext.getTemplateConfig(ext, key).filter(x => x); - const pattern = patterns.join('|'); - if (pattern) { - try { - return new RegExp(pattern); - } catch (e) { - logger.error(`正则表达式错误,内容:${pattern},错误信息:${e.message}`); - return /(?!)/; - } - } - return /(?!)/; - } - - static getRegexesConfig(ext: seal.ExtInfo, key: string): RegExp[] { - return seal.ext.getTemplateConfig(ext, key).map(x => { - try { - return new RegExp(x); - } catch (e) { - logger.error(`正则表达式错误,内容:${x},错误信息:${e.message}`); - return /(?!)/; - } - }); - } - - static getHandlebarsTemplateConfig(ext: seal.ExtInfo, key: string): HandlebarsTemplateDelegate { - return Handlebars.compile(seal.ext.getTemplateConfig(ext, key)[0] || ''); - } - - static getHandlebarsTemplatesConfig(ext: seal.ExtInfo, key: string): HandlebarsTemplateDelegate[] { - return seal.ext.getTemplateConfig(ext, key).map(x => Handlebars.compile(x || '')); - } - - static getPathMapConfig(ext: seal.ExtInfo, key: string): { [id: string]: string } { - const paths = seal.ext.getTemplateConfig(ext, key).filter(x => x); - const pathMap: { [id: string]: string } = paths.reduce((acc: { [id: string]: string }, path: string) => { - if (path.trim() === '') return acc; - try { - const id = path.split('/').pop().replace(/\.[^/.]+$/, ''); - if (!id) throw new Error(`本地路径格式错误:${path}`); - acc[id] = path; - } catch (e) { - logger.error(`本地路径格式错误:${path},错误信息:${e.message}`); - } - return acc; - }, {}); - return pathMap; - } -} \ No newline at end of file diff --git a/src/config/config_image.ts b/src/config/config_image.ts deleted file mode 100644 index 3ad1e4a..0000000 --- a/src/config/config_image.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ConfigManager } from "./configManager"; - -export class ImageConfig { - static ext: seal.ExtInfo; - - static register() { - ImageConfig.ext = ConfigManager.getExt('aiplugin4_5:图片'); - - seal.ext.registerTemplateConfig(ImageConfig.ext, "本地图片路径", ['data/images/sealdice.png'], "如不需要可以不填写,修改完需要重载js"); - seal.ext.registerBoolConfig(ImageConfig.ext, "是否接收图片", true, ""); - seal.ext.registerStringConfig(ImageConfig.ext, "图片识别需要满足的条件", '0', "使用豹语表达式,例如:$t群号_RAW=='2001'。若要开启所有图片自动识别转文字,请填写'1'"); - seal.ext.registerIntConfig(ImageConfig.ext, "发送图片的概率/%", 0, "在回复后发送本地图片或偷取图片的概率"); - seal.ext.registerStringConfig(ImageConfig.ext, "图片大模型URL", "https://open.bigmodel.cn/api/paas/v4/chat/completions"); - seal.ext.registerStringConfig(ImageConfig.ext, "图片API key", "yours"); - seal.ext.registerTemplateConfig(ImageConfig.ext, "图片body", [ - `"model":"glm-4v"`, - `"max_tokens":128`, - `"stop":null`, - `"stream":false`, - ], "messages不存在时,将会自动替换") - seal.ext.registerStringConfig(ImageConfig.ext, "图片识别默认prompt", "请帮我用简短的语言概括这张图片的特征,包括图片类型、场景、主题、主体等信息,如果有文字,请全部输出", ""); - seal.ext.registerOptionConfig(ImageConfig.ext, "识别图片时将url转换为base64", "永不", ["永不", "自动", "总是"], "解决大模型无法正常获取QQ图床图片的问题"); - seal.ext.registerIntConfig(ImageConfig.ext, "图片最大回复字符数", 500); - seal.ext.registerIntConfig(ImageConfig.ext, "偷取图片存储上限", 50, "每个群聊或私聊单独储存"); - } - - static get() { - return { - localImagePathMap: ConfigManager.getPathMapConfig(ImageConfig.ext, "本地图片路径"), - receiveImage: seal.ext.getBoolConfig(ImageConfig.ext, "是否接收图片"), - condition: seal.ext.getStringConfig(ImageConfig.ext, "图片识别需要满足的条件"), - p: seal.ext.getIntConfig(ImageConfig.ext, "发送图片的概率/%"), - url: seal.ext.getStringConfig(ImageConfig.ext, "图片大模型URL"), - apiKey: seal.ext.getStringConfig(ImageConfig.ext, "图片API key"), - bodyTemplate: seal.ext.getTemplateConfig(ImageConfig.ext, "图片body"), - defaultPrompt: seal.ext.getStringConfig(ImageConfig.ext, "图片识别默认prompt"), - urlToBase64: seal.ext.getOptionConfig(ImageConfig.ext, "识别图片时将url转换为base64"), - maxChars: seal.ext.getIntConfig(ImageConfig.ext, "图片最大回复字符数"), - maxStolenImageNum: seal.ext.getIntConfig(ImageConfig.ext, "偷取图片存储上限") - } - } -} \ No newline at end of file diff --git a/src/config/config_log.ts b/src/config/config_log.ts deleted file mode 100644 index bcc4e38..0000000 --- a/src/config/config_log.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ConfigManager } from "./configManager"; - -export class LogConfig { - static ext: seal.ExtInfo; - - static register() { - LogConfig.ext = ConfigManager.getExt('aiplugin4'); - - seal.ext.registerOptionConfig(LogConfig.ext, "日志打印方式", "简短", ["永不", "简短", "详细"]); - } - - static get() { - return { - logLevel: seal.ext.getOptionConfig(LogConfig.ext, "日志打印方式") - } - } -} \ No newline at end of file diff --git a/src/config/config_memory.ts b/src/config/config_memory.ts deleted file mode 100644 index a6d0d45..0000000 --- a/src/config/config_memory.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { ConfigManager } from "./configManager"; - -export class MemoryConfig { - static ext: seal.ExtInfo; - - static register() { - MemoryConfig.ext = ConfigManager.getExt('aiplugin4_7:记忆'); - - seal.ext.registerIntConfig(MemoryConfig.ext, "知识库记忆展示数量", 10, ""); - seal.ext.registerTemplateConfig(MemoryConfig.ext, "知识库记忆", [ - ``, - `ID:测试 -用户:用户1:114514,用户2:1919810 -群聊:群聊1:114514,群聊2:1919810 -关键词:关键词1,关键词2 -图片:本地图片1的名字,本地图片2的名字 -内容:这是内容 -内容放在最后,可以换行 ---- -ID:上面是分割符 -内容:用于多个知识词条的分割` - ], "与角色设定一一对应"); - seal.ext.registerTemplateConfig(MemoryConfig.ext, "单条知识库记忆展示模板", [ - ` {{{序号}}}. 记忆ID:{{{记忆ID}}} - 相关用户:{{{用户列表}}} - 相关群聊:{{{群聊列表}}} - 关键词:{{{关键词}}} - 内容:{{{记忆内容}}}` - ], ""); - seal.ext.registerBoolConfig(MemoryConfig.ext, "是否启用长期记忆", true, ""); - seal.ext.registerIntConfig(MemoryConfig.ext, "长期记忆上限", 50, ""); - seal.ext.registerIntConfig(MemoryConfig.ext, "长期记忆展示数量", 5, ""); - seal.ext.registerBoolConfig(MemoryConfig.ext, "长期记忆是否启用向量", false, ""); - seal.ext.registerIntConfig(MemoryConfig.ext, "向量维度", 1024, ""); - seal.ext.registerStringConfig(MemoryConfig.ext, "嵌入url地址", "https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings", ''); - seal.ext.registerStringConfig(MemoryConfig.ext, "嵌入API Key", "你的API Key", ''); - seal.ext.registerTemplateConfig(MemoryConfig.ext, "嵌入body", [ - `"model":"text-embedding-v4"`, - `"encoding_format":"float"` - ], "input, dimensions不存在时,将会自动替换。具体参数请参考你所使用模型的接口文档"); - seal.ext.registerTemplateConfig(MemoryConfig.ext, "长期记忆展示模板", [ - `{{#if 私聊}} -### 关于用户<{{{用户名称}}}>{{#if 展示号码}}({{{用户号码}}}){{/if}}: -{{else}} -### 关于群聊<{{{群聊名称}}}>{{#if 展示号码}}({{{群聊号码}}}){{/if}}: -{{/if}} - - 设定:{{{设定}}} - - 记忆: -{{{记忆列表}}}` - ], ""); - seal.ext.registerTemplateConfig(MemoryConfig.ext, "单条长期记忆展示模板", [ - ` {{{序号}}}. 记忆ID:{{{记忆ID}}} - 时间:{{{记忆时间}}} -{{#if 个人记忆}} - 来源:{{#if 私聊}}私聊{{else}}群聊<{{{群聊名称}}}>{{#if 展示号码}}({{{群聊号码}}}){{/if}}{{/if}} -{{/if}} - 相关用户:{{{用户列表}}} - 相关群聊:{{{群聊列表}}} - 关键词:{{{关键词}}} - 内容:{{{记忆内容}}}` - ], ""); - seal.ext.registerBoolConfig(MemoryConfig.ext, "是否启用短期记忆", true, ""); - seal.ext.registerIntConfig(MemoryConfig.ext, "短期记忆上限", 10, ""); - seal.ext.registerIntConfig(MemoryConfig.ext, "短期记忆总结轮数", 10, ""); - seal.ext.registerStringConfig(MemoryConfig.ext, "记忆总结 url地址", "", '为空时,默认使用对话接口'); - seal.ext.registerStringConfig(MemoryConfig.ext, "记忆总结 API Key", "你的API Key", '若使用对话接口无需填写'); - seal.ext.registerTemplateConfig(MemoryConfig.ext, "记忆总结 body", [ - `"model":"deepseek-chat"`, - `"max_tokens":1024`, - `"response_format": { "type": "json_object" }`, - `"stop":null`, - `"stream":false` - ], "messages不存在时,将会自动替换"); - seal.ext.registerTemplateConfig(MemoryConfig.ext, "记忆总结prompt模板", [ - `你现在扮演的角色如下: -## 扮演详情 -{{{角色设定}}} - -## 聊天相关 - - 当前平台:{{{平台}}} -{{#if 私聊}} - - 当前私聊:<{{{用户名称}}}>{{#if 展示号码}}({{{用户号码}}}){{/if}} -{{else}} - - 当前群聊:<{{{群聊名称}}}>{{#if 展示号码}}({{{群聊号码}}}){{/if}} - - <|at:xxx|>表示@某个群成员 - - <|poke:xxx|>表示戳一戳某个群成员 - - <|face:xxx|>表示使用某个表情,xxx为表情名称,注意与img表情包区分 -{{/if}} -{{#if 添加前缀}} - - <|from:xxx|>表示消息来源,不要在生成的回复中使用 -{{/if}} -{{#if 展示消息ID}} - - <|msg_id:xxx|>表示消息ID,仅用于调用函数时使用,不要在生成的回复中提及或使用 - - <|quote:xxx|>表示引用消息,xxx为对应的消息ID -{{/if}} -{{#if 展示时间}} - - <|time:xxxx-xx-xx xx:xx:xx|>表示消息发送时间,不要在生成的回复中提及或使用 -{{/if}} - - \\f用于分割多条消息 - -请根据你的设定,对以下对话内容进行总结: -{{{对话内容}}} - -返回格式为JSON,格式类型如下: -{ - "content": { - type: 'string', - description: '总结后的对话摘要,请根据人物、行为、场景,以所扮演角色的口吻进行简短描述,只保留核心内容' - }, - "memories": { - type: 'array', - description: '记忆数组。单条记忆应只有一个话题或事件。若对话内容对记忆有重要影响时返回,否则返回空数组', - items: { - type: 'object', - description: '记忆对象', - properties: { - "memory_type": { - type: "string", - description: "记忆类型,个人或群聊。", - enum: ["private", "group"] - }, - "name": { - type: 'string', - description: '用户名称或群聊名称{{#if 展示号码}}或纯数字QQ号、群号{{/if}},实际使用时与记忆类型对应' - }, - "text": { - type: 'string', - description: '记忆内容,尽量简短,无需附带时间与来源' - }, - "keywords": { - type: 'array', - description: '相关用户名称列表', - items: { - type: 'string' - } - }, - "userList": { - type: 'array', - description: '相关用户名称列表', - items: { - type: 'string' - } - }, - "groupList": { - type: 'array', - description: '相关群聊名称列表', - items: { - type: 'string' - } - } - }, - "required": ['memory_type', 'name', 'text'] - } - } -}` - ], ""); - } - - static get() { - return { - knowledgeMemoryShowNumber: seal.ext.getIntConfig(MemoryConfig.ext, "知识库记忆展示数量"), - knowledgeMemoryStringList: seal.ext.getTemplateConfig(MemoryConfig.ext, "知识库记忆"), - knowledgeMemorySingleShowTemplate: ConfigManager.getHandlebarsTemplateConfig(MemoryConfig.ext, "单条知识库记忆展示模板"), - isMemory: seal.ext.getBoolConfig(MemoryConfig.ext, "是否启用长期记忆"), - memoryLimit: seal.ext.getIntConfig(MemoryConfig.ext, "长期记忆上限"), - memoryShowNumber: seal.ext.getIntConfig(MemoryConfig.ext, "长期记忆展示数量"), - isMemoryVector: seal.ext.getBoolConfig(MemoryConfig.ext, "长期记忆是否启用向量"), - embeddingDimension: seal.ext.getIntConfig(MemoryConfig.ext, "向量维度"), - embeddingUrl: seal.ext.getStringConfig(MemoryConfig.ext, "嵌入url地址"), - embeddingApiKey: seal.ext.getStringConfig(MemoryConfig.ext, "嵌入API Key"), - embeddingBodyTemplate: seal.ext.getTemplateConfig(MemoryConfig.ext, "嵌入body"), - memoryShowTemplate: ConfigManager.getHandlebarsTemplateConfig(MemoryConfig.ext, "长期记忆展示模板"), - memorySingleShowTemplate: ConfigManager.getHandlebarsTemplateConfig(MemoryConfig.ext, "单条长期记忆展示模板"), - isShortMemory: seal.ext.getBoolConfig(MemoryConfig.ext, "是否启用短期记忆"), - shortMemoryLimit: seal.ext.getIntConfig(MemoryConfig.ext, "短期记忆上限"), - shortMemorySummaryRound: seal.ext.getIntConfig(MemoryConfig.ext, "短期记忆总结轮数"), - memoryUrl: seal.ext.getStringConfig(MemoryConfig.ext, "记忆总结 url地址"), - memoryApiKey: seal.ext.getStringConfig(MemoryConfig.ext, "记忆总结 API Key"), - memoryBodyTemplate: seal.ext.getTemplateConfig(MemoryConfig.ext, "记忆总结 body"), - memoryPromptTemplate: ConfigManager.getHandlebarsTemplateConfig(MemoryConfig.ext, "记忆总结prompt模板") - } - } -} \ No newline at end of file diff --git a/src/config/config_received.ts b/src/config/config_received.ts deleted file mode 100644 index 4fc53e5..0000000 --- a/src/config/config_received.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ConfigManager } from "./configManager"; - -export class ReceivedConfig { - static ext: seal.ExtInfo; - - static register() { - ReceivedConfig.ext = ConfigManager.getExt('aiplugin4_3:消息接收与触发'); - - seal.ext.registerBoolConfig(ReceivedConfig.ext, "是否录入指令消息", false, ""); - seal.ext.registerBoolConfig(ReceivedConfig.ext, "是否录入所有骰子发送的消息", false, ""); - seal.ext.registerBoolConfig(ReceivedConfig.ext, "私聊内不可用", false, ""); - seal.ext.registerBoolConfig(ReceivedConfig.ext, "是否开启全局待机", false, "开启后,全局的ai将进入待机状态,可能造成性能问题"); - seal.ext.registerStringConfig(ReceivedConfig.ext, "非指令触发需要满足的条件", '1', "使用豹语表达式,例如:$t群号_RAW=='2001'"); - seal.ext.registerTemplateConfig(ReceivedConfig.ext, "非指令消息触发正则表达式", [ - "\\[CQ:at,qq=748569109\\]", - "^正确.*[。?!?!]$" - ], ""); - seal.ext.registerTemplateConfig(ReceivedConfig.ext, "非指令消息忽略正则表达式", [ - "^忽略这句话$" - ], "匹配的消息不会接收录入上下文"); - seal.ext.registerIntConfig(ReceivedConfig.ext, "触发次数上限", 3, ""); - seal.ext.registerIntConfig(ReceivedConfig.ext, "触发次数补充间隔/s", 3, ""); - } - - static get() { - return { - allcmd: seal.ext.getBoolConfig(ReceivedConfig.ext, "是否录入指令消息"), - allmsg: seal.ext.getBoolConfig(ReceivedConfig.ext, "是否录入所有骰子发送的消息"), - disabledInPrivate: seal.ext.getBoolConfig(ReceivedConfig.ext, "私聊内不可用"), - globalStandby: seal.ext.getBoolConfig(ReceivedConfig.ext, "是否开启全局待机"), - triggerRegex: ConfigManager.getRegexConfig(ReceivedConfig.ext, "非指令消息触发正则表达式"), - ignoreRegex: ConfigManager.getRegexConfig(ReceivedConfig.ext, "非指令消息忽略正则表达式"), - triggerCondition: seal.ext.getStringConfig(ReceivedConfig.ext, "非指令触发需要满足的条件"), - bucketLimit: seal.ext.getIntConfig(ReceivedConfig.ext, "触发次数上限"), - fillInterval: seal.ext.getIntConfig(ReceivedConfig.ext, "触发次数补充间隔/s") - } - } - } \ No newline at end of file diff --git a/src/config/config_request.ts b/src/config/config_request.ts deleted file mode 100644 index 0a8b11b..0000000 --- a/src/config/config_request.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { ConfigManager } from "./configManager"; - -export class RequestConfig { - static ext: seal.ExtInfo; - - static register() { - RequestConfig.ext = ConfigManager.getExt('aiplugin4'); - - seal.ext.registerStringConfig(RequestConfig.ext, "url地址", "https://api.deepseek.com/v1/chat/completions", ''); - seal.ext.registerStringConfig(RequestConfig.ext, "API Key", "你的API Key", ''); - seal.ext.registerTemplateConfig(RequestConfig.ext, "body", [ - `"model":"deepseek-chat"`, - `"max_tokens":1024`, - `"stop":null`, - `"stream":false`, - `"frequency_penalty":0`, - `"presence_penalty":0`, - `"temperature":1`, - `"top_p":1` - ], "messages,tools,tool_choice不存在时,将会自动替换。具体参数请参考你所使用模型的接口文档"); - seal.ext.registerIntConfig(RequestConfig.ext, "请求超时时限/ms", 180000, ''); - } - - static get() { - return { - url: seal.ext.getStringConfig(RequestConfig.ext, "url地址"), - apiKey: seal.ext.getStringConfig(RequestConfig.ext, "API Key"), - bodyTemplate: seal.ext.getTemplateConfig(RequestConfig.ext, "body"), - timeout: seal.ext.getIntConfig(RequestConfig.ext, "请求超时时限/ms") - } - } -} \ No newline at end of file diff --git a/src/config/config_tool.ts b/src/config/config_tool.ts deleted file mode 100644 index aa7004b..0000000 --- a/src/config/config_tool.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { ConfigManager } from "./configManager"; - -export class ToolConfig { - static ext: seal.ExtInfo; - - static register() { - ToolConfig.ext = ConfigManager.getExt('aiplugin4_2:函数调用'); - - seal.ext.registerBoolConfig(ToolConfig.ext, "是否开启调用函数功能", true, ""); - seal.ext.registerBoolConfig(ToolConfig.ext, "是否切换为提示词工程", false, "API在不支持function calling功能的时候开启"); - seal.ext.registerTemplateConfig(ToolConfig.ext, "工具函数prompt模板", [ - `{{序号}}. 名称:{{{函数名称}}} - - 描述:{{{函数描述}}} - - 参数信息:{{{参数信息}}} - - 必需参数:{{{必需参数}}}` - ], "提示词工程中每个函数的prompt"); - seal.ext.registerIntConfig(ToolConfig.ext, "允许连续调用函数次数", 5, "单次对话中允许连续调用函数的次数"); - seal.ext.registerTemplateConfig(ToolConfig.ext, "不允许调用的函数", [ - 'ban', - 'whole_ban', - 'get_ban_list' - ], "修改后保存并重载js"); - seal.ext.registerTemplateConfig(ToolConfig.ext, "默认关闭的函数", [ - 'rename', - 'record', - 'text_to_image' - ], ""); - seal.ext.registerTemplateConfig(ToolConfig.ext, "提供给AI的牌堆名称", ["克苏鲁神话"], "没有的话建议把draw_deck这个函数加入不允许调用"); - seal.ext.registerOptionConfig(ToolConfig.ext, "ai语音使用的音色", '傲娇少女', [ - "小新", - "猴哥", - "四郎", - "东北老妹儿", - "广西大表哥", - "妲己", - "霸道总裁", - "酥心御姐", - "说书先生", - "憨憨小弟", - "憨厚老哥", - "吕布", - "元气少女", - "文艺少女", - "磁性大叔", - "邻家小妹", - "低沉男声", - "傲娇少女", - "爹系男友", - "暖心姐姐", - "温柔妹妹", - "书香少女", - "自定义" - ], "该功能在选择预设音色时,需要安装http依赖插件,且需要可以调用ai语音api版本的napcat/lagrange等。选择自定义音色时,则需要aitts依赖插件和ffmpeg"); - seal.ext.registerTemplateConfig(ToolConfig.ext, "本地语音路径", ['data/records/钢管落地.mp3'], "如不需要可以不填写,修改完需要重载js。发送语音需要配置ffmpeg到环境变量中"); - } - - static get() { - return { - isTool: seal.ext.getBoolConfig(ToolConfig.ext, "是否开启调用函数功能"), - usePromptEngineering: seal.ext.getBoolConfig(ToolConfig.ext, "是否切换为提示词工程"), - toolsPromptTemplate: ConfigManager.getHandlebarsTemplateConfig(ToolConfig.ext, "工具函数prompt模板"), - maxCallCount: seal.ext.getIntConfig(ToolConfig.ext, "允许连续调用函数次数"), - toolsNotAllow: seal.ext.getTemplateConfig(ToolConfig.ext, "不允许调用的函数"), - toolsDefaultClosed: seal.ext.getTemplateConfig(ToolConfig.ext, "默认关闭的函数"), - decks: seal.ext.getTemplateConfig(ToolConfig.ext, "提供给AI的牌堆名称"), - character: seal.ext.getOptionConfig(ToolConfig.ext, "ai语音使用的音色"), - recordPathMap: ConfigManager.getPathMapConfig(ToolConfig.ext, "本地语音路径"), - } - } -} \ No newline at end of file diff --git a/src/config/config_backend.ts b/src/config/configs/backend.ts similarity index 57% rename from src/config/config_backend.ts rename to src/config/configs/backend.ts index 30cb262..fe31a2d 100644 --- a/src/config/config_backend.ts +++ b/src/config/configs/backend.ts @@ -1,10 +1,10 @@ -import { ConfigManager } from "./configManager"; +import { Config } from "../config"; -export class BackendConfig { +export default class BackendConfig { static ext: seal.ExtInfo; static register() { - BackendConfig.ext = ConfigManager.getExt('aiplugin4_6:后端'); + BackendConfig.ext = Config.getExt('aiplugin4:后端'); seal.ext.registerStringConfig(BackendConfig.ext, "流式输出", "http://localhost:3010", '自行搭建或使用他人提供的后端'); seal.ext.registerStringConfig(BackendConfig.ext, "图片转base64", "https://urltobase64.fishwhite.top", '可自行搭建'); @@ -16,12 +16,12 @@ export class BackendConfig { static get() { return { - streamUrl: seal.ext.getStringConfig(BackendConfig.ext, "流式输出"), - imageTobase64Url: seal.ext.getStringConfig(BackendConfig.ext, "图片转base64"), - webSearchUrl: seal.ext.getStringConfig(BackendConfig.ext, "联网搜索"), - webReadUrl: seal.ext.getStringConfig(BackendConfig.ext, "网页读取"), - usageChartUrl: seal.ext.getStringConfig(BackendConfig.ext, "用量图表"), - renderUrl: seal.ext.getStringConfig(BackendConfig.ext, "md和html图片渲染") + STREAM: seal.ext.getStringConfig(BackendConfig.ext, "流式输出"), + IMAGE_TO_BASE64: seal.ext.getStringConfig(BackendConfig.ext, "图片转base64"), + WEB_SEARCH: seal.ext.getStringConfig(BackendConfig.ext, "联网搜索"), + WEB_READ: seal.ext.getStringConfig(BackendConfig.ext, "网页读取"), + USAGE_CHART: seal.ext.getStringConfig(BackendConfig.ext, "用量图表"), + RENDER: seal.ext.getStringConfig(BackendConfig.ext, "md和html图片渲染") } } } diff --git a/src/config/configs/base.ts b/src/config/configs/base.ts new file mode 100644 index 0000000..089ef44 --- /dev/null +++ b/src/config/configs/base.ts @@ -0,0 +1,23 @@ +import { Config } from "../config"; + +export default class BaseConfig { + static ext: seal.ExtInfo; + + static register() { + BaseConfig.ext = Config.getExt('aiplugin4'); + + seal.ext.registerOptionConfig(BaseConfig.ext, "日志打印方式", "简短", ["永不", "简短", "详细", "调试"]); + seal.ext.registerIntConfig(BaseConfig.ext, "请求超时时限/ms", 180000, ''); + seal.ext.registerStringConfig(BaseConfig.ext, "海豹核心全局路径", "/root/sealdice", ''); + seal.ext.registerBoolConfig(BaseConfig.ext, "是否开启全局待机", false, "开启后,全局的ai将进入待机状态,可能造成性能问题"); + } + + static get() { + return { + LOG_LEVEL: seal.ext.getOptionConfig(BaseConfig.ext, "日志打印方式") as "永不" | "简短" | "详细" | "调试", + TIMEOUT: seal.ext.getIntConfig(BaseConfig.ext, "请求超时时限/ms"), + SEALDICE_PATH: seal.ext.getStringConfig(BaseConfig.ext, "海豹核心全局路径"), + GLOBAL_STANDBY: seal.ext.getBoolConfig(BaseConfig.ext, "是否开启全局待机"), + } + } +} \ No newline at end of file diff --git a/src/config/config_message.ts b/src/config/configs/config_message.ts similarity index 99% rename from src/config/config_message.ts rename to src/config/configs/config_message.ts index 2f122e5..890b845 100644 --- a/src/config/config_message.ts +++ b/src/config/configs/config_message.ts @@ -1,4 +1,4 @@ -import { ConfigManager } from "./configManager"; +import { Config } from "../config"; export class MessageConfig { static ext: seal.ExtInfo; diff --git a/src/config/config_reply.ts b/src/config/configs/config_reply.ts similarity index 98% rename from src/config/config_reply.ts rename to src/config/configs/config_reply.ts index 4e37eb8..9341e14 100644 --- a/src/config/config_reply.ts +++ b/src/config/configs/config_reply.ts @@ -1,4 +1,4 @@ -import { ConfigManager } from "./configManager"; +import { Config } from "../config"; export class ReplyConfig { static ext: seal.ExtInfo; diff --git a/src/config/configs/image.ts b/src/config/configs/image.ts new file mode 100644 index 0000000..e850c66 --- /dev/null +++ b/src/config/configs/image.ts @@ -0,0 +1,25 @@ +import { Config, getPathMapConfig } from "../config"; + +export default class ImageConfig { + static ext: seal.ExtInfo; + + static register() { + ImageConfig.ext = Config.getExt('aiplugin4:图片'); + + seal.ext.registerTemplateConfig(ImageConfig.ext, "本地图片路径", ['data/images/sealdice.png'], "如不需要可以不填写,修改完需要重载js"); + seal.ext.registerStringConfig(ImageConfig.ext, "图片全局识别豹语条件", '0', "使用豹语表达式,例如:$t群号_RAW=='2001'。若要开启所有图片自动识别转文字,请填写'1'"); + seal.ext.registerStringConfig(ImageConfig.ext, "图片识别默认prompt", "请帮我用简短的语言概括这张图片的特征,包括图片类型、场景、主题、主体等信息,如果有文字,请全部输出", ""); + seal.ext.registerOptionConfig(ImageConfig.ext, "识别图片时将url转换为base64", "永不", ["永不", "自动", "总是"], "解决大模型无法正常获取QQ图床图片的问题"); + seal.ext.registerIntConfig(ImageConfig.ext, "图片转文字最大字符数", 500); + } + + static get() { + return { + LOCAL_IMAGE_PATH_MAP: getPathMapConfig(ImageConfig.ext, "本地图片路径"), + IMAGE_CONDITION: seal.ext.getStringConfig(ImageConfig.ext, "图片全局识别豹语条件"), + IMAGE_DEFAULT_PROMPT: seal.ext.getStringConfig(ImageConfig.ext, "图片识别默认prompt"), + URL_TO_BASE64: seal.ext.getOptionConfig(ImageConfig.ext, "识别图片时将url转换为base64"), + MAX_CHARS: seal.ext.getIntConfig(ImageConfig.ext, "图片转文字最大字符数") + } + } +} \ No newline at end of file diff --git a/src/config/configs/memory.ts b/src/config/configs/memory.ts new file mode 100644 index 0000000..8ae7b92 --- /dev/null +++ b/src/config/configs/memory.ts @@ -0,0 +1,259 @@ +import MemoryItem from "../../memory/memory_item"; +import { revive, TypeDescriptor } from "../../utils/utils"; +import Config, { getHandlebarsTemplateConfig } from "../config"; +import { load } from 'js-toml' + +export default class MemoryConfig { + static ext: seal.ExtInfo; + + static register() { + MemoryConfig.ext = Config.getExt('aiplugin4:记忆'); + + seal.ext.registerIntConfig(MemoryConfig.ext, "向量维度", 1024, ""); + seal.ext.registerBoolConfig(MemoryConfig.ext, "启用知识库记忆", false, ""); + seal.ext.registerIntConfig(MemoryConfig.ext, "知识库记忆展示数量", 10, ""); + seal.ext.registerBoolConfig(MemoryConfig.ext, "启用长期记忆", true, ""); + seal.ext.registerIntConfig(MemoryConfig.ext, "长期记忆上限", 50, ""); + seal.ext.registerIntConfig(MemoryConfig.ext, "长期记忆展示数量", 5, ""); + seal.ext.registerBoolConfig(MemoryConfig.ext, "启用总结记忆", true, ""); + seal.ext.registerIntConfig(MemoryConfig.ext, "总结记忆上限", 10, ""); + seal.ext.registerIntConfig(MemoryConfig.ext, "总结记忆间隔轮数", 10, ""); + seal.ext.registerIntConfig(MemoryConfig.ext, "总结记忆参与轮数", 10, ""); + seal.ext.registerTemplateConfig(MemoryConfig.ext, "知识库记忆", [ + `# 采用toml进行格式化 +roles = ["正确"] # 当数组为空或不存在时,默认对所有角色生效 + +[knowledges.测试] +content = """ +这是内容 +可以换行 +""" +importance = 0.9 # 记忆重要性,0-1之间的浮点数,默认0.5 +tags = ["标签1", "标签2"] # 标签列表 +relatedMemories = ["测试2"] # 相关记忆ID列表 +users = ["114514", "1919810"] # 相关用户ID列表 +groups = ["114514", "1919810"] # 相关群组ID列表 + +[knowledges.测试2] +content = "单行形式,只有content字段是必须的"` + ], ""); + seal.ext.registerTemplateConfig(MemoryConfig.ext, "知识库记忆展示模板", [ + `{{#if KNOWLEDGE}} + +## 知识库 + {{#each knowledges}} +{{index @index}}. ID:{{id}} + 重要性:{{importance}} + {{#each tags}}{{#if @first}}标签:{{/if}}{{{this}}}{{#unless @last}}, {{/unless}}{{/each}} + {{#each relatedMemories}}{{#if @first}}相关记忆:{{/if}}{{{this}}}{{#unless @last}}, {{/unless}}{{/each}} + {{#each users}}{{#if @first}}相关用户:{{/if}}{{{this}}}{{#unless @last}}, {{/unless}}{{/each}} + {{#each groups}}{{#if @first}}相关群组:{{/if}}{{{this}}}{{#unless @last}}, {{/unless}}{{/each}} + 内容:{{{content}}} + {{else}} +知识库为空 + {{/each}} +{{/if}}` + ], ""); + seal.ext.registerTemplateConfig(MemoryConfig.ext, "长期记忆展示模板", [ + `{{#if MEMORY}} + +## 长期记忆 + {{#each sources}} +来源:{{{source}}} + {{#each memories}} +{{index @index}}. ID:{{id}} + 重要性:{{importance}} + 创建时间:{{{time createAt}}} + {{#each tags}}{{#if @first}}标签:{{/if}}{{{this}}}{{#unless @last}}, {{/unless}}{{/each}} + {{#each relatedMemories}}{{#if @first}}相关记忆:{{/if}}{{{this}}}{{#unless @last}}, {{/unless}}{{/each}} + {{#each users}}{{#if @first}}相关用户:{{/if}}{{{this}}}{{#unless @last}}, {{/unless}}{{/each}} + {{#each groups}}{{#if @first}}相关群组:{{/if}}{{{this}}}{{#unless @last}}, {{/unless}}{{/each}} + 内容:{{{content}}} + {{else}} +暂无记忆 + {{/each}} +{{#unless @last}}---{{/unless}} + {{else}} +长期记忆为空 + {{/each}} +{{/if}}` + ], ""); + seal.ext.registerTemplateConfig(MemoryConfig.ext, "总结记忆展示模板", [ + `{{#if SUMMARY}} + +## 总结记忆 + {{#each summaries}} +{{index @index}}. {{{this}}} + {{else}} +总结记忆为空 + {{/each}} +{{/if}}` + ], ""); + seal.ext.registerTemplateConfig(MemoryConfig.ext, "记忆总结prompt模板", [ // wip + `你现在扮演的角色如下: +## 扮演详情 +{{{角色设定}}} + +## 聊天相关 + - 当前平台:{{{平台}}} +{{#if 私聊}} + - 当前私聊:<{{{用户名称}}}>{{#if 展示号码}}({{{用户号码}}}){{/if}} +{{else}} + - 当前群聊:<{{{群聊名称}}}>{{#if 展示号码}}({{{群聊号码}}}){{/if}} + - <|at:xxx|>表示@某个群成员 + - <|poke:xxx|>表示戳一戳某个群成员 + - <|face:xxx|>表示使用某个表情,xxx为表情名称,注意与img表情包区分 +{{/if}} +{{#if 添加前缀}} + - <|from:xxx|>表示消息来源,不要在生成的回复中使用 +{{/if}} +{{#if 展示消息ID}} + - <|msg_id:xxx|>表示消息ID,仅用于调用函数时使用,不要在生成的回复中提及或使用 + - <|quote:xxx|>表示引用消息,xxx为对应的消息ID +{{/if}} +{{#if 展示时间}} + - <|time:xxxx-xx-xx xx:xx:xx|>表示消息发送时间,不要在生成的回复中提及或使用 +{{/if}} + - \\f用于分割多条消息 + +请根据你的设定,对以下对话内容进行总结: +{{{对话内容}}} + +返回格式为JSON,格式类型如下: +{ + "content": { + type: 'string', + description: '总结后的对话摘要,请根据人物、行为、场景,以所扮演角色的口吻进行简短描述,只保留核心内容' + }, + "memories": { + type: 'array', + description: '记忆数组。单条记忆应只有一个话题或事件。若对话内容对记忆有重要影响时返回,否则返回空数组', + items: { + type: 'object', + description: '记忆对象', + properties: { + "memory_type": { + type: "string", + description: "记忆类型,个人或群聊。", + enum: ["private", "group"] + }, + "name": { + type: 'string', + description: '用户名称或群聊名称{{#if 展示号码}}或纯数字QQ号、群号{{/if}},实际使用时与记忆类型对应' + }, + "text": { + type: 'string', + description: '记忆内容,尽量简短,无需附带时间与来源' + }, + "keywords": { + type: 'array', + description: '相关用户名称列表', + items: { + type: 'string' + } + }, + "userList": { + type: 'array', + description: '相关用户名称列表', + items: { + type: 'string' + } + }, + "groupList": { + type: 'array', + description: '相关群聊名称列表', + items: { + type: 'string' + } + } + }, + "required": ['memory_type', 'name', 'text'] + } + } +}` + ], ""); + } + + static get() { + return { + DIMENSION: seal.ext.getIntConfig(MemoryConfig.ext, "向量维度"), + KNOWLEDGE: seal.ext.getBoolConfig(MemoryConfig.ext, "启用知识库记忆"), + KNOWLEDGE_SHOW_NUMBER: seal.ext.getIntConfig(MemoryConfig.ext, "知识库记忆展示数量"), + MEMORY: seal.ext.getBoolConfig(MemoryConfig.ext, "启用长期记忆"), + MEMORY_LIMIT: seal.ext.getIntConfig(MemoryConfig.ext, "长期记忆上限"), + MEMORY_SHOW_NUMBER: seal.ext.getIntConfig(MemoryConfig.ext, "长期记忆展示数量"), + SUMMARY: seal.ext.getBoolConfig(MemoryConfig.ext, "启用总结记忆"), + SUMMARY_LIMIT: seal.ext.getIntConfig(MemoryConfig.ext, "总结记忆上限"), + SUMMARY_INTERVAL: seal.ext.getIntConfig(MemoryConfig.ext, "总结记忆间隔轮数"), + SUMMARY_SIZE: seal.ext.getIntConfig(MemoryConfig.ext, "总结记忆参与轮数"), + KNOWLEDGE_MEMORIES_MAP: getKnowledgeMemoriesMapConfig(), + KNOWLEDGE_TEMPLATE: getHandlebarsTemplateConfig(MemoryConfig.ext, "知识库记忆展示模板"), + MEMORY_TEMPLATE: getHandlebarsTemplateConfig(MemoryConfig.ext, "长期记忆展示模板"), + SUMMARY_TEMPLATE: getHandlebarsTemplateConfig(MemoryConfig.ext, "总结记忆展示模板"), + SUMMARY_PROMPT_TEMPLATE: getHandlebarsTemplateConfig(MemoryConfig.ext, "记忆总结prompt模板") + } + } +} + +class KnowledgeConfigItem { + static validKeysMap: { [key in keyof KnowledgeConfigItem]?: TypeDescriptor } = { + roles: { array: 'string' }, + knowledges: { + objectValue: { + object: { + content: 'string', + importance: 'number', + tags: { array: 'string' }, + relatedMemories: { array: 'string' }, + users: { array: 'string' }, + groups: { array: 'string' } + } + } + } + } + roles: string[]; + knowledges: { + [id: string]: { + content: string, + importance: number, + tags: string[], + relatedMemories: string[] + users: string[], + groups: string[] + } + } + + constructor() { + this.roles = []; + this.knowledges = {}; + } +} + +function getKnowledgeMemoriesMapConfig(): { [role: string]: MemoryItem[] } { + const knowledgeMaps: { [role: string]: { [id: string]: MemoryItem } } = {}; + seal.ext.getTemplateConfig(MemoryConfig.ext, "知识库记忆").forEach(tomlString => { + const kc = revive(KnowledgeConfigItem, load(tomlString)); + const mmap: { [id: string]: MemoryItem } = {}; + for (const id in kc.knowledges) { + const k = kc.knowledges[id]; + const m = new MemoryItem(); + m.id = id; + m.importance = k.importance; + m.content = k.content; + m.tags = k.tags; + m.relatedMemories = k.relatedMemories; + m.users = k.users.map(u => String(u)); + m.groups = k.groups.map(g => String(g)); + mmap[id] = m; + } + if (kc.roles.length === 0) kc.roles.push('*'); + for (const role of kc.roles) { + if (!knowledgeMaps.hasOwnProperty(role)) knowledgeMaps[role] = {}; + knowledgeMaps[role] = { ...knowledgeMaps[role], ...mmap }; + } + }); + + const knowledgeMemoriesMap: { [role: string]: MemoryItem[] } = {}; + for (const role of Object.keys(knowledgeMaps)) knowledgeMemoriesMap[role] = Object.values(knowledgeMaps[role]); + return knowledgeMemoriesMap; +} \ No newline at end of file diff --git a/src/config/configs/model.ts b/src/config/configs/model.ts new file mode 100644 index 0000000..0e22dc7 --- /dev/null +++ b/src/config/configs/model.ts @@ -0,0 +1,67 @@ +import { ChatModel, EmbeddingModel, ImageModel } from "../../agent/model"; +import { ModelBody } from "../../model/types"; +import { logger } from "../../logger"; +import Config from "../config"; +import { CHAT_MODEL_TO_PROVIDER, EMBEDDING_MODEL_TO_PROVIDER, IMAGE_MODEL_TO_PROVIDER, PROVIDER_MAP } from "../static_config"; + +export default class ModelConfig { + static ext: seal.ExtInfo; + + static register() { + ModelConfig.ext = Config.getExt('aiplugin4:模型'); + + seal.ext.registerTemplateConfig(ModelConfig.ext, "对话模型", [`{ + "use": ["chat"], + "name": "deepseek-chat", + "api_key": "sk-xxxx", + "body": { + "temperature": 1, + "top_p": 1 + } +}`], ''); + seal.ext.registerTemplateConfig(ModelConfig.ext, "图片模型", [`{ + "use": ["image-understanding"], + "name": "glm-4v", + "api_key": "sk-xxxx" +}`], ''); + seal.ext.registerTemplateConfig(ModelConfig.ext, "嵌入模型", [`{ + "use": ["text-embedding"], + "name": "text-embedding-v4", + "api_key": "sk-xxxx" +}`], ''); + } + + static get() { + return { + CHAT_MODELS: getModelsConfig("对话模型", CHAT_MODEL_TO_PROVIDER, ChatModel), + IMAGE_MODELS: getModelsConfig("图片模型", IMAGE_MODEL_TO_PROVIDER, ImageModel), + EMBEDDING_MODELS: getModelsConfig("嵌入模型", EMBEDDING_MODEL_TO_PROVIDER, EmbeddingModel), + } + } +} + +function getModelsConfig( + key: string, + m2p: { [model: string]: string }, + modelConstructor: new (use: T['use'], name: string, provider: string, base_url: string, api_key: string, body: ModelBody) => T +): T[] { + return seal.ext.getTemplateConfig(ModelConfig.ext, key).map(x => { + try { + const data = JSON.parse(x); + if (!data.hasOwnProperty('name')) throw new Error('缺失模型名称'); + if (!data.hasOwnProperty('api_key')) throw new Error('缺失模型API密钥'); + if (!data.hasOwnProperty('use')) data.use = []; + if (!data.hasOwnProperty('body')) data.body = {}; + if (!data.hasOwnProperty('provider')) data.provider = m2p?.[data.name] || ""; + if (!data.hasOwnProperty('base_url')) { + if (!data.hasOwnProperty('provider')) throw new Error('缺失模型基础URL 且 缺失模型供应商'); + data.base_url = PROVIDER_MAP?.[data.provider] || ""; + if (!data.hasOwnProperty('base_url')) throw new Error('缺失模型基础URL'); + } + return new modelConstructor(data.use, data.name, data.provider, data.base_url, data.api_key, data.body); + } catch (e) { + logger.error(`${key}解析错误,内容:${x},错误信息:${e.message}`); + return null; + } + }).filter(x => x !== null); +} \ No newline at end of file diff --git a/src/config/configs/received.ts b/src/config/configs/received.ts new file mode 100644 index 0000000..34df114 --- /dev/null +++ b/src/config/configs/received.ts @@ -0,0 +1,29 @@ +import { Config, getRegexConfig } from "../config"; + +export default class ReceivedConfig { + static ext: seal.ExtInfo; + + static register() { + ReceivedConfig.ext = Config.getExt('aiplugin4:消息接收'); + + seal.ext.registerBoolConfig(ReceivedConfig.ext, "接收图片", true, ""); + seal.ext.registerBoolConfig(ReceivedConfig.ext, "接收指令消息", false, ""); + seal.ext.registerBoolConfig(ReceivedConfig.ext, "接收骰子发送的消息", false, ""); + seal.ext.registerBoolConfig(ReceivedConfig.ext, "忽略私聊消息", false, ""); + seal.ext.registerStringConfig(ReceivedConfig.ext, "忽略消息豹语条件", '1', "使用豹语表达式,例如:$t群号_RAW=='2001'"); + seal.ext.registerTemplateConfig(ReceivedConfig.ext, "忽略消息正则表达式", [ + "^忽略这句话$" + ], "匹配的消息将被忽略"); + } + + static get() { + return { + RECEIVE_IMAGE: seal.ext.getBoolConfig(ReceivedConfig.ext, "接收图片"), + RECEIVE_CMD: seal.ext.getBoolConfig(ReceivedConfig.ext, "接收指令消息"), + RECEIVE_MSG_BY_BOT: seal.ext.getBoolConfig(ReceivedConfig.ext, "接收骰子发送的消息"), + IGNORE_PRIVATE: seal.ext.getBoolConfig(ReceivedConfig.ext, "忽略私聊消息"), + IGNORE_CONDITION: seal.ext.getStringConfig(ReceivedConfig.ext, "忽略消息豹语条件"), + IGNORE_REGEX: getRegexConfig(ReceivedConfig.ext, "忽略消息正则表达式") + } + } +} \ No newline at end of file diff --git a/src/config/sample.ts b/src/config/configs/sample.ts similarity index 50% rename from src/config/sample.ts rename to src/config/configs/sample.ts index 272b9c5..ac9cbd3 100644 --- a/src/config/sample.ts +++ b/src/config/configs/sample.ts @@ -1,17 +1,17 @@ -import { ConfigManager } from "./configManager"; +import { Config } from "../config"; -export class SampleConfig { +export default class SampleConfig { static ext: seal.ExtInfo; static register() { - SampleConfig.ext = ConfigManager.getExt('aiplugin4_0:示例'); + SampleConfig.ext = Config.getExt('aiplugin4:示例'); seal.ext.registerBoolConfig(SampleConfig.ext, "是否启用", true, ''); } static get() { return { - enabled: seal.ext.getBoolConfig(SampleConfig.ext, "是否启用"), + ENABLED: seal.ext.getBoolConfig(SampleConfig.ext, "是否启用"), } } } \ No newline at end of file diff --git a/src/config/configs/tool.ts b/src/config/configs/tool.ts new file mode 100644 index 0000000..d953833 --- /dev/null +++ b/src/config/configs/tool.ts @@ -0,0 +1,85 @@ +import { Config, getHandlebarsTemplateConfig, getPathMapConfig } from "../config"; + +export default class ToolConfig { + static ext: seal.ExtInfo; + + static register() { + ToolConfig.ext = Config.getExt('aiplugin4:工具'); + + seal.ext.registerBoolConfig(ToolConfig.ext, "开启调用函数功能", true, ""); + seal.ext.registerBoolConfig(ToolConfig.ext, "切换为提示词工程", false, "API在不支持function calling功能的时候开启"); + seal.ext.registerTemplateConfig(ToolConfig.ext, "工具函数prompt模板", [ + `{{#if PROMPT_ENGINEERING}} + +## 调用函数 +当需要调用函数功能时,请严格使用以下JSON格式,示例: + + +[ +{ + "name": "函数名", + "arguments": "{\\"参数1\\": \\"值1\\",\\"参数2\\": \\"值2\\"}" +} +] + + +要使用成对的标签:\`\`在前面,\`\`在后面包裹调用工具的数组。 +可调用多个函数,每个调用需包含name字段和arguments字段,且arguments字段必须是JSON字符串。 + +可用函数列表: + {{#each tools}} +{{index @index}}. 名称:{{{name}}} + - 描述:{{{description}}} + - 参数信息:{{{json_stringify parameters.properties}}} + - 必需参数:{{#each parameters.required}}{{{this}}}{{#unless @last}}, {{/unless}}{{/each}} + {{else}} +暂无可用函数。 + {{/each}} +{{/if}}` + ], ""); + seal.ext.registerIntConfig(ToolConfig.ext, "允许连续调用函数次数", 5, "单次对话中允许连续调用函数的次数"); + seal.ext.registerTemplateConfig(ToolConfig.ext, "禁止调用的函数", [''], "修改后保存并重载js"); + seal.ext.registerTemplateConfig(ToolConfig.ext, "默认关闭的函数", [''], ""); + seal.ext.registerTemplateConfig(ToolConfig.ext, "提供给AI的牌堆名称", [''], "没有的话建议把draw_deck这个函数加入不允许调用"); + seal.ext.registerOptionConfig(ToolConfig.ext, "ai语音使用的音色", '傲娇少女', [ + "小新", + "猴哥", + "四郎", + "东北老妹儿", + "广西大表哥", + "妲己", + "霸道总裁", + "酥心御姐", + "说书先生", + "憨憨小弟", + "憨厚老哥", + "吕布", + "元气少女", + "文艺少女", + "磁性大叔", + "邻家小妹", + "低沉男声", + "傲娇少女", + "爹系男友", + "暖心姐姐", + "温柔妹妹", + "书香少女", + "自定义" + ], "该功能在选择预设音色时,需要安装http依赖插件,且需要可以调用ai语音api版本的napcat/lagrange等。选择自定义音色时,则需要aitts依赖插件和ffmpeg"); + seal.ext.registerTemplateConfig(ToolConfig.ext, "本地语音路径", ['data/records/钢管落地.mp3'], "如不需要可以不填写,修改完需要重载js。发送语音需要配置ffmpeg到环境变量中"); + } + + static get() { + return { + STATUS: seal.ext.getBoolConfig(ToolConfig.ext, "开启调用函数功能"), + PROMPT_ENGINEERING: seal.ext.getBoolConfig(ToolConfig.ext, "切换为提示词工程"), + TOOLS_PROMPT_TEMPLATE: getHandlebarsTemplateConfig(ToolConfig.ext, "工具函数prompt模板"), + MAX_CALL_COUNT: seal.ext.getIntConfig(ToolConfig.ext, "允许连续调用函数次数"), + BLOCKED: seal.ext.getTemplateConfig(ToolConfig.ext, "禁止调用的函数"), + DEFAULT_CLOSED: seal.ext.getTemplateConfig(ToolConfig.ext, "默认关闭的函数"), + DECKS: seal.ext.getTemplateConfig(ToolConfig.ext, "提供给AI的牌堆名称"), + CHARACTER: seal.ext.getOptionConfig(ToolConfig.ext, "ai语音使用的音色"), + RECORD_PATH_MAP: getPathMapConfig(ToolConfig.ext, "本地语音路径") + } + } +} \ No newline at end of file diff --git a/src/config/configs/trigger.ts b/src/config/configs/trigger.ts new file mode 100644 index 0000000..8ad1e64 --- /dev/null +++ b/src/config/configs/trigger.ts @@ -0,0 +1,34 @@ +import { Config, getRegexConfig } from "../config"; + +export default class TriggerConfig { + static ext: seal.ExtInfo; + + static register() { + TriggerConfig.ext = Config.getExt('aiplugin4:消息触发'); + + seal.ext.registerIntConfig(TriggerConfig.ext, "默认计数器", 10, ""); + seal.ext.registerFloatConfig(TriggerConfig.ext, "默认计时器/s", 60, ""); + seal.ext.registerFloatConfig(TriggerConfig.ext, "默认概率/%", 10, ""); + seal.ext.registerStringConfig(TriggerConfig.ext, "默认触发活跃时间", "10:00-20:00-5", ""); + seal.ext.registerFloatConfig(TriggerConfig.ext, "默认向量相似度", 0.8, ""); + seal.ext.registerTemplateConfig(TriggerConfig.ext, "触发正则表达式", [ + "\\[CQ:at,qq=748569109\\]", + "^正确.*[。?!?!]$" + ], ""); + seal.ext.registerIntConfig(TriggerConfig.ext, "触发次数上限", 3, ""); + seal.ext.registerIntConfig(TriggerConfig.ext, "触发次数补充间隔/s", 3, ""); + } + + static get() { + return { + COUNTER: seal.ext.getIntConfig(TriggerConfig.ext, "默认计数器"), + TIMER: seal.ext.getFloatConfig(TriggerConfig.ext, "默认计时器/s"), + PROBABILITY: seal.ext.getFloatConfig(TriggerConfig.ext, "默认概率/%"), + ACTIVE_TIME: seal.ext.getStringConfig(TriggerConfig.ext, "默认触发活跃时间"), + VECTOR_SIMILARITY: seal.ext.getFloatConfig(TriggerConfig.ext, "默认向量相似度"), + TRIGGER_REGEX: getRegexConfig(TriggerConfig.ext, "触发正则表达式"), + BUCKET_LIMIT: seal.ext.getIntConfig(TriggerConfig.ext, "触发次数上限"), + FILL_INTERVAL: seal.ext.getIntConfig(TriggerConfig.ext, "触发次数补充间隔/s") + } + } +} \ No newline at end of file diff --git a/src/config/static_config.ts b/src/config/static_config.ts new file mode 100644 index 0000000..aa739ec --- /dev/null +++ b/src/config/static_config.ts @@ -0,0 +1,414 @@ +export const VERSION = "4.12.0"; +export const AUTHOR = "baiyu&错误"; +export const NAME = "aiplugin4"; + +export const CONFIG_CACHE_TTL = 60000; + +export const CQ_TYPES_ALLOW = ["at", "image", "reply", "face", "poke"]; + +export const PRIVILEGE_LEVEL_MAP = { + "master": 100, + "whitelist": 70, + "owner": 60, + "admin": 50, + "inviter": 40, + "user": 0, + "blacklist": -30 +} + +export const HELP_MAP = { + "ID": `: +【QQ:1234567890】 私聊窗口 +【QQ-Group:1234】 群聊窗口 +【now】当前窗口`, + "会话权限": `<会话权限>:任意数字,越大权限越高`, + "指令": `<指令>:指令名称和参数,多个指令用-连接,如ai-sb`, + "权限限制": `<权限限制>:数字0-数字1-数字2,如0-0-0,含义如下: +0: 会话所需权限, 1: 会话检查通过后用户所需权限, 2: 强行触发指令用户所需权限, 进行检查时若通过0和1则无需检查2 +【-30】黑名单用户 +【0】普通用户 +【40】邀请者 +【50】群管理员 +【60】群主 +【70】白名单用户 +【100】骰主`, + "参数": `<参数>: +【c】计数器模式,接收消息数达到后触发 +单位/条,默认10条 +【t】计时器模式,最后一条消息后达到时限触发 +单位/秒,默认60秒 +【p】概率模式,每条消息按概率触发 +单位/%,默认10% +【a】活跃时间段和活跃次数 +格式为"开始时间-结束时间-活跃次数"(如"09:00-18:00-5")` +} + +export const ALIAS_MAP = { + "AI": "ai", + "priv": "privilege", + "ses": "session", + "st": "set", + "ck": "check", + "clr": "clear", + "sb": "standby", + "fgt": "forget", + "f": "forget", + "ass": "assistant", + "img": "image", + "memo": "memory", + "p": "private", + "g": "group", + "del": "delete", + "ign": "ignore", + "rm": "remove", + "lst": "list", + "tk": "token", + "y": "year", + "m": "month", + "lcl": "local", + "stl": "steal", + "ran": "random", + "nick": "nickname" +} + +export const FACE_MAP = { + "0": "惊讶", + "1": "撇嘴", + "2": "色", + "3": "发呆", + "4": "得意", + "5": "流泪", + "6": "害羞", + "7": "闭嘴", + "8": "睡", + "9": "大哭", + "10": "尴尬", + "11": "发怒", + "12": "调皮", + "13": "呲牙", + "14": "微笑", + "15": "难过", + "16": "酷", + "18": "抓狂", + "19": "吐", + "20": "偷笑", + "21": "可爱", + "22": "白眼", + "23": "傲慢", + "24": "饥饿", + "25": "困", + "26": "惊恐", + "27": "流汗", + "28": "憨笑", + "29": "悠闲", + "30": "奋斗", + "31": "咒骂", + "32": "疑问", + "33": "嘘", + "34": "晕", + "35": "折磨", + "36": "衰", + "37": "骷髅", + "38": "敲打", + "39": "再见", + "41": "发抖", + "42": "爱情", + "43": "跳跳", + "46": "猪头", + "49": "拥抱", + "53": "蛋糕", + "55": "炸弹", + "56": "刀", + "59": "便便", + "60": "咖啡", + "63": "玫瑰", + "64": "凋谢", + "66": "爱心", + "67": "心碎", + "74": "太阳", + "75": "月亮", + "76": "赞", + "77": "踩", + "78": "握手", + "79": "胜利", + "85": "飞吻", + "86": "怄火", + "89": "西瓜", + "96": "冷汗", + "97": "擦汗", + "98": "抠鼻", + "99": "鼓掌", + "100": "糗大了", + "101": "坏笑", + "102": "左哼哼", + "103": "右哼哼", + "104": "哈欠", + "105": "鄙视", + "106": "委屈", + "107": "快哭了", + "108": "阴险", + "109": "左亲亲", + "110": "吓", + "111": "可怜", + "112": "菜刀", + "114": "篮球", + "116": "示爱", + "118": "抱拳", + "119": "勾引", + "120": "拳头", + "121": "差劲", + "122": "爱你", + "123": "NO", + "124": "OK", + "125": "转圈", + "129": "挥手", + "137": "鞭炮", + "144": "喝彩", + "146": "爆筋", + "147": "棒棒糖", + "148": "喝奶", + "169": "手枪", + "171": "茶", + "172": "眨眼睛", + "173": "泪奔", + "174": "无奈", + "175": "卖萌", + "176": "小纠结", + "177": "喷血", + "178": "斜眼笑", + "179": "doge", + "180": "惊喜", + "181": "戳一戳", + "182": "笑哭", + "183": "我最美", + "185": "羊驼", + "187": "幽灵", + "193": "大笑", + "194": "不开心", + "198": "呃", + "200": "求求", + "201": "点赞", + "202": "无聊", + "203": "托脸", + "204": "吃", + "206": "害怕", + "210": "飙泪", + "211": "我不看", + "212": "托腮", + "214": "啵啵", + "215": "糊脸", + "216": "拍头", + "217": "扯一扯", + "218": "舔一舔", + "219": "蹭一蹭", + "221": "顶呱呱", + "222": "抱抱", + "223": "暴击", + "224": "开枪", + "225": "撩一撩", + "226": "拍桌", + "227": "拍手", + "229": "干杯", + "230": "嘲讽", + "231": "哼", + "232": "佛系", + "233": "掐一掐", + "235": "颤抖", + "237": "偷看", + "238": "扇脸", + "239": "原谅", + "240": "喷脸", + "241": "生日快乐", + "243": "甩头", + "244": "扔狗", + "262": "脑阔疼", + "263": "沧桑", + "264": "捂脸", + "265": "辣眼睛", + "266": "哦哟", + "267": "头秃", + "268": "问号脸", + "269": "暗中观察", + "270": "emm", + "271": "吃瓜", + "272": "呵呵哒", + "273": "我酸了", + "277": "汪汪", + "278": "汗", + "281": "无眼笑", + "282": "敬礼", + "283": "狂笑", + "284": "面无表情", + "285": "摸鱼", + "286": "魔鬼笑", + "287": "哦", + "288": "请", + "289": "睁眼", + "290": "敲开心", + "292": "让我康康", + "293": "摸锦鲤", + "294": "期待", + "295": "拿到红包", + "297": "拜谢", + "298": "元宝", + "299": "牛啊", + "300": "胖三斤", + "301": "好闪", + "302": "左拜年", + "303": "右拜年", + "305": "右亲亲", + "306": "牛气冲天", + "307": "喵喵", + "311": "打call", + "312": "变形", + "314": "仔细分析", + "317": "菜汪", + "318": "崇拜", + "319": "比心", + "320": "庆祝", + "322": "拒绝", + "323": "嫌弃", + "324": "吃糖", + "325": "惊吓", + "326": "生气", + "332": "举牌牌", + "333": "烟花", + "334": "虎虎生威", + "336": "豹富", + "337": "花朵脸", + "338": "我想开了", + "339": "舔屏", + "341": "打招呼", + "342": "酸Q", + "343": "我方了", + "344": "大怨种", + "345": "红包多多", + "346": "你真棒棒", + "347": "大展宏兔", + "348": "福萝卜", + "349": "坚强", + "350": "贴贴", + "351": "敲敲", + "352": "咦", + "353": "拜托", + "354": "尊嘟假嘟", + "355": "耶", + "356": "666", + "357": "裂开", + "358": "骰子", + "359": "包剪锤", + "360": "亲亲", + "361": "狗狗笑哭", + "362": "好兄弟", + "363": "狗狗可怜", + "364": "超级赞", + "365": "狗狗生气", + "366": "芒狗", + "367": "狗狗疑问", + "368": "奥特笑哭", + "369": "彩虹", + "370": "祝贺", + "371": "冒泡", + "372": "气呼呼", + "373": "忙", + "374": "波波流泪", + "375": "超级鼓掌", + "376": "跺脚", + "377": "嗨", + "378": "企鹅笑哭", + "379": "企鹅流泪", + "380": "真棒", + "381": "路过", + "382": "emo", + "383": "企鹅爱心", + "384": "晚安", + "385": "太气了", + "386": "呜呜呜", + "387": "太好笑", + "388": "太头疼", + "389": "太赞了", + "390": "太头秃", + "391": "太沧桑", + "392": "龙年快乐", + "393": "新年中龙", + "394": "新年大龙", + "395": "略略略", + "396": "狼狗", + "397": "抛媚眼", + "398": "超级ok", + "399": "tui", + "400": "快乐", + "401": "超级转圈", + "402": "别说话", + "403": "出去玩", + "404": "闪亮登场", + "405": "好运来", + "406": "姐是女王", + "407": "我听听", + "408": "臭美", + "409": "送你花花", + "410": "么么哒", + "411": "一起嗨", + "412": "开心", + "413": "摇起来", + "415": "划龙舟", + "416": "中龙舟", + "417": "大龙舟", + "419": "火车", + "420": "中火车", + "421": "大火车", + "422": "粽于等到你", + "423": "复兴号", + "424": "续标识", + "425": "求放过", + "426": "玩火", + "427": "偷感", + "428": "收到", + "429": "蛇年快乐", + "430": "蛇身", + "431": "蛇尾", + "432": "灵蛇献瑞" +} + +export const PROVIDER_MAP = { + "deepseek": "https://api.deepseek.com/v1", + "zhipu": "https://open.bigmodel.cn/api/paas/v4", + "alibaba": "https://dashscope.aliyuncs.com/compatible-mode/v1" + +} + +export const CHAT_MODEL_MAP = { + "deepseek": ["deepseek-chat", "deepseek-reasoner"] +}; +export const CHAT_MODEL_TO_PROVIDER = Object.entries(CHAT_MODEL_MAP).reduce((acc, [provider, models]) => { + models.forEach(model => acc[model] = provider); + return acc; +}, {} as { [model: string]: string }); +export const IMAGE_MODEL_MAP = { + "zhipu": ["glm-4v-plus-0111", "glm-4v"] +}; +export const IMAGE_MODEL_TO_PROVIDER = Object.entries(IMAGE_MODEL_MAP).reduce((acc, [provider, models]) => { + models.forEach(model => acc[model] = provider); + return acc; +}, {} as { [model: string]: string }); +export const EMBEDDING_MODEL_MAP = { + "alibaba": ["text-embedding-v4", "text-embedding-v3"] +}; +export const EMBEDDING_MODEL_TO_PROVIDER = Object.entries(EMBEDDING_MODEL_MAP).reduce((acc, [provider, models]) => { + models.forEach(model => acc[model] = provider); + return acc; +}, {} as { [model: string]: string }); + +export const DEFAULT_CHAT_MODEL_BODY = { + "max_tokens": 4096, + "stop": null, + "stream": false +} +export const DEFAULT_IMAGE_MODEL_BODY = { + "max_tokens": 4096, + "stop": null, + "stream": false +} +export const DEFAULT_EMBEDDING_MODEL_BODY = { + "encoding_format": "float" +} \ No newline at end of file diff --git a/src/image.ts b/src/image.ts new file mode 100644 index 0000000..9f070e2 --- /dev/null +++ b/src/image.ts @@ -0,0 +1,278 @@ +import { Config } from "./config/config"; +import { generateId, revive, TypeDescriptor } from "./utils/utils"; +import { logger } from "./logger"; +import { MessageSegment, parseSpecialTokens } from "./utils/string"; +import { getSessionId } from "./utils/seal"; +import { ModelManager } from "../agent/model"; + +export default class Image { + static validKeysMap: { [key in keyof Image]?: TypeDescriptor } = { + imageId: 'string', + sourceSessionId: 'string', + path: 'string', + url: 'string', + base64: 'string', + format: 'string', + description: 'string', + } + imageId: string; + sourceSessionId: string; + path: string; + url: string; + base64: string; + format: string; + description: string; + + constructor() { + this.imageId = ''; + this.sourceSessionId = ''; + this.path = ''; + this.url = ''; + this.base64 = ''; + this.format = ''; + this.description = ''; + } + + get type(): 'url' | 'local' | 'base64' { + if (this.base64) return 'base64'; + if (this.url.startsWith('http')) return 'url'; + return 'local'; + } + + get CQCode(): string { + const file = this.type === 'base64' ? seal.base64ToImage(this.base64) : this.url; + return `[CQ:image,file=${file}]`; + } + + get base64Url(): string { + let format = this.format; + if (!format || format === "unknown") format = 'png'; + return `data:image/${format};base64,${this.base64}` + } + + /** + * 获取图片的URL,若为base64则返回base64Url + */ + get src(): string { + return this.type === 'base64' ? this.base64Url : this.url; + } + + async checkImageUrl(): Promise { + if (this.type !== 'url') return true; + let isValid = false; + try { + const response = await fetch(this.url, { method: 'GET' }); + + if (response.ok) { + const contentType = response.headers.get('Content-Type'); + if (contentType && contentType.startsWith('image')) { + logger.info('URL有效且未过期'); + isValid = true; + } else { + logger.warning(`URL有效但未返回图片 Content-Type: ${contentType}`); + } + } else { + if (response.status === 500) { + logger.warning(`URL不知道有没有效 状态码: ${response.status}`); + isValid = true; + } else { + logger.warning(`URL无效或过期 状态码: ${response.status}`); + } + } + } catch (error) { + logger.error('在checkImageUrl中请求出错:', error); + } + return isValid; + } + + async urlToBase64() { + if (this.type !== 'url') return; + const { IMAGE_TO_BASE64: imageTobase64Url } = Config.backend; + try { + const response = await fetch(`${imageTobase64Url}/image-to-base64`, { + method: 'POST', + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + }, + body: JSON.stringify({ url: this.src }) + }); + + const text = await response.text(); + if (!response.ok) throw new Error(`请求失败! 状态码: ${response.status}\n响应体: ${text}`); + if (!text) throw new Error("响应体为空"); + + try { + const data = JSON.parse(text); + if (data.error) throw new Error(`请求失败! 错误信息: ${data.error.message}`); + if (!data.base64 || !data.format) throw new Error(`响应体中缺少base64或format字段`); + this.base64 = data.base64; + this.format = data.format; + } catch (e) { + throw new Error(`解析响应体时出错:${e}\n响应体:${text}`); + } + } catch (error) { + logger.error("在imageUrlToBase64中请求出错:", error); + } + + Image.save(this); + } + + async imageToText(prompt = '') { + const { IMAGE_DEFAULT_PROMPT, URL_TO_BASE64, MAX_CHARS } = Config.image; + + if (URL_TO_BASE64 == '总是' && this.type === 'url') await this.urlToBase64(); + + const model = ModelManager.getImageModel('image-understanding'); + if (!model) { + logger.error(`未找到支持image-understanding的模型`); + return; + } + + this.description = (await model.callITT(this.src, prompt ? prompt : IMAGE_DEFAULT_PROMPT)).slice(0, MAX_CHARS); + + if (!this.description && URL_TO_BASE64 === '自动' && this.type === 'url') { + logger.info(`图片${this.imageId}第一次识别失败,自动尝试使用转换为base64`); + await this.urlToBase64(); + this.description = (await model.callITT(this.src, prompt ? prompt : IMAGE_DEFAULT_PROMPT)).slice(0, MAX_CHARS); + } + + if (!this.description) logger.error(`图片${this.imageId}识别失败`); + } + + + static imageMap: { [key: string]: Image } = {}; + + static generateImageId(): string { + let id = generateId(), a = 0; + while (this.get(id)) { + id = generateId(); + a++; + if (a > 1000) { + logger.error(`生成图片id失败,已尝试1000次,放弃`); + throw new Error(`生成图片id失败,已尝试1000次,放弃`); + } + } + return id; + } + + static createUrlImage(sourceSessionId: string, url: string, imageId?: string): Image { + imageId = imageId || this.generateImageId(); + const img = new Image(); + img.imageId = imageId; + img.sourceSessionId = sourceSessionId; + img.url = url; + this.imageMap[imageId] = img; + return img; + } + + static createLocalImage(imageId: string, path: string): Image { + const img = new Image(); + img.imageId = imageId; + img.path = path; + this.imageMap[imageId] = img; + return img; + } + + static get(imageId: string): Image | null { + if (!this.imageMap.hasOwnProperty(imageId)) { + let img = new Image(); + try { + const text = Config.ext.storageGet(`image_${imageId}`); + if (!text) return null; + const data = JSON.parse(text || '{}'); + img = revive(Image, data); + } catch (error) { + logger.error(`加载图片${imageId}失败: ${error}`); + return null; + } + this.imageMap[imageId] = img; + } + return this.imageMap[imageId]; + } + static save(img: Image) { + Config.ext.storageSet(`image_${img.imageId}`, JSON.stringify(img)); + } + + static getUserAvatar(uid: string): Image { + const img = new Image(); + img.imageId = `user_avatar:${uid}`; + img.url = `https://q1.qlogo.cn/g?b=qq&nk=${uid.replace(/^.+:/, '')}&s=640`; + return img; + } + + static getGroupAvatar(gid: string): Image { + const img = new Image(); + img.imageId = `group_avatar:${gid}`; + img.url = `https://p.qlogo.cn/gh/${gid.replace(/^.+:/, '')}/${gid.replace(/^.+:/, '')}/640`; + return img; + } + + static get LocalImageList() { + const { LOCAL_IMAGE_PATH_MAP } = Config.image; + return Object.keys(LOCAL_IMAGE_PATH_MAP).map(id => this.createLocalImage(id, LOCAL_IMAGE_PATH_MAP[id])); + } + + static getLocalImageListText(p: number = 1): string { + const images = this.LocalImageList; + if (images.length == 0) return ''; + if (p > Math.ceil(images.length / 5)) p = Math.ceil(images.length / 5); + return images.slice((p - 1) * 5, p * 5) + .map((img, i) => { + return `${i + 1 + (p - 1) * 5}. 名称:${img.imageId} +${img.CQCode}`; + }).join('\n') + `\n当前页码:${p}/${Math.ceil(images.length / 5)}`; + } + + /** + * 提取并替换CQ码中的图片 wip + * @param ctx + * @param message + * @returns + */ + static async handleImageMessageSegment(ctx: seal.MsgContext, seg: MessageSegment): Promise<{ content: string, images: Image[] }> { + const { RECEIVE_IMAGE } = Config.received; + if (!RECEIVE_IMAGE || seg.type !== 'image') return { content: '', images: [] }; + + let content = ''; + const images: Image[] = []; + try { + const file = seg.data.url || seg.data.file || ''; + if (!file) return { content: '', images: [] }; + + const image = this.createUrlImage(getSessionId(ctx), file); + const { IMAGE_CONDITION } = Config.image; + const fmtCondition = parseInt(seal.format(ctx, `{${IMAGE_CONDITION}}`)); + if (fmtCondition === 1) await image.imageToText(); + + content += image.description ? `<|img:${image.imageId}:${image.description}|>` : `<|img:${image.imageId}|>`; + images.push(image); + } catch (error) { + logger.error('在handleImageMessage中处理图片时出错:', error); + } + + return { content, images }; + } + + static async extractExistingImagesToSave(s: string): Promise { + const segs = parseSpecialTokens(s); + const images: Image[] = []; + for (const seg of segs) { + switch (seg.type) { + case 'img': { + const id = seg.content; + const image = this.get(id); + + if (image) { + if (image.type === 'url') await image.urlToBase64(); + images.push(image); + } else { + logger.warning(`无法找到图片:${id}`); + } + break; + } + } + } + return images; + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index eb0a53f..3064b66 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,25 +1,29 @@ import { AIManager } from "./AI/AI"; -import { ToolManager } from "./tool/tool"; -import { ConfigManager } from "./config/configManager"; +import { ToolService } from "./tool/tool"; +import { Config } from "./config/config"; import { triggerConditionMap } from "./tool/tool_trigger"; import { logger } from "./logger"; -import { transformTextToArray } from "./utils/utils_string"; -import { checkUpdate } from "./utils/utils_update"; +import { fmtDate, transformTextToArray } from "./utils/string"; +import { checkUpdate } from "./utils/update"; import { TimerManager } from "./timer"; -import { createMsg } from "./utils/utils_seal"; +import { createMsg } from "./utils/seal"; import { PrivilegeManager } from "./cmd/privilege"; -import { knowledgeMM } from "./AI/memory"; -import { CQTYPESALLOW } from "./config/config"; -import { registerCmd } from "./cmd/root"; +import { knowledgeService } from "./memory/memory"; +import { CQ_TYPES_ALLOW } from "./config/static_config"; +import { registerCmd } from "./cmd/root_cmd"; function main() { - ConfigManager.registerConfig(); + Handlebars.registerHelper('index', (index: number) => index + 1); + Handlebars.registerHelper('json_stringify', (obj: any) => JSON.stringify(obj, null, 2)); + Handlebars.registerHelper('time', (t: number) => fmtDate(t)); + + Config.registerConfig(); checkUpdate(); - ToolManager.registerTool(); + ToolService.registerTool(); TimerManager.init(); - knowledgeMM.init(); + knowledgeService.init(); - const ext = ConfigManager.ext; + const ext = Config.ext; registerCmd(); PrivilegeManager.reviveCmdPriv(); @@ -34,7 +38,7 @@ function main() { //接受非指令消息 ext.onNotCommandReceived = (ctx, msg): void | Promise => { try { - const { disabledInPrivate, globalStandby, triggerRegex, ignoreRegex, triggerCondition } = ConfigManager.received; + const { disabledInPrivate, globalStandby, triggerRegex, ignoreRegex, triggerCondition } = Config.received; if (ctx.isPrivate && disabledInPrivate) { return; } @@ -58,7 +62,7 @@ function main() { // 检查CQ码 const CQTypes = messageArray.filter(item => item.type !== 'text').map(item => item.type); - if (CQTypes.length === 0 || CQTypes.every(item => CQTYPESALLOW.includes(item))) { + if (CQTypes.length === 0 || CQTypes.every(item => CQ_TYPES_ALLOW.includes(item))) { clearTimeout(ai.context.timer); ai.context.timer = null; @@ -126,11 +130,11 @@ function main() { //接受的指令 ext.onCommandReceived = (ctx, msg, cmdArgs) => { try { - if (ToolManager.cmdArgs === null) { - ToolManager.cmdArgs = cmdArgs; + if (ToolService.cmdArgs === null) { + ToolService.cmdArgs = cmdArgs; } - const { allcmd } = ConfigManager.received; + const { allcmd } = Config.received; if (allcmd) { const uid = ctx.player.userId; const gid = ctx.group.groupId; @@ -144,7 +148,7 @@ function main() { const messageArray = transformTextToArray(message); const CQTypes = messageArray.filter(item => item.type !== 'text').map(item => item.type); - if (CQTypes.length === 0 || CQTypes.every(item => CQTYPESALLOW.includes(item))) { + if (CQTypes.length === 0 || CQTypes.every(item => CQ_TYPES_ALLOW.includes(item))) { const setting = ai.setting; if (setting.standby) { ai.handleReceipt(ctx, msg, ai, messageArray); @@ -172,7 +176,7 @@ function main() { ai.tool.listen.resolve?.(message); // 将消息传递给监听工具 - const { allmsg } = ConfigManager.received; + const { allmsg } = Config.received; if (allmsg) { if (message === ai.context.lastReply) { ai.context.lastReply = ''; @@ -180,7 +184,7 @@ function main() { } const CQTypes = messageArray.filter(item => item.type !== 'text').map(item => item.type); - if (CQTypes.length === 0 || CQTypes.every(item => CQTYPESALLOW.includes(item))) { + if (CQTypes.length === 0 || CQTypes.every(item => CQ_TYPES_ALLOW.includes(item))) { const setting = ai.setting; if (setting.standby) { ai.handleReceipt(ctx, msg, ai, messageArray); diff --git a/src/logger.ts b/src/logger.ts index 7565f99..a1f21ce 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,14 +1,9 @@ -import { NAME } from "./config/config"; -import { ConfigManager } from "./config/configManager"; +import { NAME } from "./config/static_config"; +import Config from "./config/config"; -class Logger { - name: string; - constructor(name: string) { - this.name = name; - } - - handleLog(...data: any[]): string { - const { logLevel } = ConfigManager.log; +export default class Logger { + static handleLog(...data: any[]): string { + const { LOG_LEVEL: logLevel } = Config.base; if (logLevel === "永不") { return ''; } else if (logLevel === "简短") { @@ -18,36 +13,52 @@ class Logger { } else { return s; } - } else if (logLevel === "详细") { + } else if (logLevel === "详细" || logLevel === "调试") { return data.map(item => `${item}`).join(" "); } else { return ''; } } - info(...data: any[]) { + static info(...data: any[]) { + const s = this.handleLog(...data); + if (!s) { + return; + } + console.log(`【${NAME}】: ${s}`); + } + + static warning(...data: any[]) { const s = this.handleLog(...data); if (!s) { return; } - console.log(`【${this.name}】: ${s}`); + console.warn(`【${NAME}】: ${s}`); } - warning(...data: any[]) { + static error(...data: any[]) { const s = this.handleLog(...data); if (!s) { return; } - console.warn(`【${this.name}】: ${s}`); + console.error(`【${NAME}】: ${s}`); } - error(...data: any[]) { + static debug(...data: any[]) { + const { LOG_LEVEL: logLevel } = Config.base; + if (logLevel !== "调试") return; const s = this.handleLog(...data); if (!s) { return; } - console.error(`【${this.name}】: ${s}`); + console.info(`【${NAME}】: ${s}`); } -} -export const logger = new Logger(NAME); \ No newline at end of file + static logMessages(body: any) { + if (body.hasOwnProperty('messages')) { + const messages = body.messages.filter(item => item.role !== "system"); + if (messages.length === 0) return; + this.info(`请求发送前的上下文:\n`, JSON.stringify(messages)); + } + } +} \ No newline at end of file diff --git a/src/memory/knowledge.ts b/src/memory/knowledge.ts new file mode 100644 index 0000000..05c9046 --- /dev/null +++ b/src/memory/knowledge.ts @@ -0,0 +1,85 @@ +import Logger from "../logger"; +import Config from "../config/config"; +import { revive, TypeDescriptor } from "../utils/utils"; +import MemoryService from "./memory"; +import MemoryItem from "./memory_item"; +import Agent from "../agent/agent"; + +export default class KnowledgeService extends MemoryService { + static validKeysMap: { [key in keyof KnowledgeService]?: TypeDescriptor } = { + memoryMap: { array: MemoryItem }, + role: 'string' + }; + role: string; + + constructor() { + super(); + this.role = '*'; + } + + async initKnowledges() { + const { KNOWLEDGE_MEMORIES_MAP } = Config.memory; + const knowledges = KNOWLEDGE_MEMORIES_MAP[this.role] || []; + await Promise.all(knowledges.map(async m => m.updateVector())); + this.memoryMap = knowledges.reduce((map, m) => { + if (this.memoryMap.hasOwnProperty(m.id)) { + m.lastAccessedAt = Math.max(m.lastAccessedAt, this.memoryMap[m.id].lastAccessedAt); + m.accessCount = Math.max(m.accessCount, this.memoryMap[m.id].accessCount); + } + map[m.id] = m; + return map; + }, {} as { [id: string]: MemoryItem }); + KnowledgeService.save(this); + } + + async accessMemories(s: string) { + const now = Math.floor(Date.now() / 1000); + (await this.search(s, { + topK: 5, + tags: [], + relatedMemories: [], + users: [], + groups: [], + method: 'similarity' + })).forEach(m => { + m.lastAccessedAt = now; + m.accessCount++; + }) + KnowledgeService.save(this); + } + + buildKnowledgePrompt(sessionId: string, text: string): string { + if (this.memories.length === 0) return ''; + const agent = Agent.get(this.role); + const session = agent.sessionService.getSession(sessionId); + const users = session.sessionType === 'group' ? session.context.users : [session.sessionId]; + const groups = session.sessionType === 'group' ? [session.sessionId] : []; + const { KNOWLEDGE, KNOWLEDGE_TEMPLATE } = Config.memory; + return KNOWLEDGE_TEMPLATE({ + "KNOWLEDGE": KNOWLEDGE, + "memories": this.getTopScoreMemories(text, users, groups) + }); + } + + static knowledgeServiceMap: { [role: string]: KnowledgeService } = {}; + + static async get(role: string) { + if (!this.knowledgeServiceMap.hasOwnProperty(role)) { + let knowledgeService = new KnowledgeService(); + try { + const data = JSON.parse(Config.ext.storageGet(`knowledge_${role}`) || '{}'); + knowledgeService = revive(KnowledgeService, data); + } catch (error) { + Logger.error(`加载知识库${role}失败: ${error}`); + } + knowledgeService.role = role; + await knowledgeService.initKnowledges(); + this.knowledgeServiceMap[role] = knowledgeService; + } + return this.knowledgeServiceMap[role]; + } + + static save(knowledgeService: KnowledgeService) { + Config.ext.storageSet(`knowledge_${knowledgeService.role}`, JSON.stringify(knowledgeService)); + } +} \ No newline at end of file diff --git a/src/memory/memory.ts b/src/memory/memory.ts new file mode 100644 index 0000000..2491551 --- /dev/null +++ b/src/memory/memory.ts @@ -0,0 +1,245 @@ +import Config from "../config/config"; +import { generateId, TypeDescriptor } from "../utils/utils"; +import Logger from "../logger"; +import Model from "../model/model"; +import { MemorySource, searchOptions } from "./types"; +import Agent from "../agent/agent"; +import { Session } from "../session/session"; +import MemoryItem from "./memory_item"; + +export default class MemoryService { + static validKeysMap: { [key in keyof MemoryService]?: TypeDescriptor } = { + memoryMap: { array: MemoryItem } + }; + memoryMap: { [id: string]: MemoryItem }; + + constructor() { + this.memoryMap = {}; + } + + get memoryIds() { + return Object.keys(this.memoryMap); + } + get memories() { + return Object.values(this.memoryMap); + } + get tags() { + const tags = new Set(); + this.memories.forEach(m => m.tags.forEach(t => tags.add(t))); + return Array.from(tags); + } + + generateMemoryId(): string { + let id = generateId(), a = 0; + while (this.memoryMap.hasOwnProperty(id)) { + id = generateId(); + a++; + if (a > 1000) { + Logger.error(`生成记忆id失败,已尝试1000次,放弃`); + throw new Error(`生成记忆id失败,已尝试1000次,放弃`); + } + } + return id; + } + + async addMemories(memories: MemoryItem[]) { + const now = Math.floor(Date.now() / 1000); + const memoriesToAdd: MemoryItem[] = []; + for (const m of memories) { + for (const om of this.memories) { + if (om.compareWith(m)) { + Logger.info(`记忆已存在,id:${om.id},进行合并`); + om.merge(m); + om.accessCount++; + om.lastAccessedAt = now; + continue; + } + } + memoriesToAdd.push(m); + } + + if (memoriesToAdd.length === 0) return; + + await Promise.all(memoriesToAdd.map(async m => await m.updateVector())); + this.limitMemories(memoriesToAdd.length); + memoriesToAdd.forEach(m => this.memoryMap[m.id] = m); + } + + /** + * 删除记忆,删除完全符合条件的记忆 + * @param ids 记忆id列表 + * @param tags 标签列表 + * @param relatedMemories 相关记忆id列表 + * @param users 用户id列表 + * @param groups 群组id列表 + * @returns + */ + deleteMemories(ids: string[] = [], tags: string[] = [], relatedMemories: string[] = [], users: string[] = [], groups: string[] = []) { + if (ids.length === 0 && tags.length === 0 && relatedMemories.length === 0 && users.length === 0 && groups.length === 0) return; + + if (ids.length > 0) { + ids.forEach(id => { + if (this.memoryMap.hasOwnProperty(id)) { + const m = this.memoryMap[id]; + if ( + tags.every(t => m.tags.includes(t)) && + relatedMemories.every(r => m.relatedMemories.includes(r)) && + users.every(u => m.users.includes(u)) && + groups.every(g => m.groups.includes(g)) + ) delete this.memoryMap[id]; + } + }) + } else { + for (const id in this.memoryMap) { + const m = this.memoryMap[id]; + if ( + tags.every(t => m.tags.includes(t)) && + relatedMemories.every(r => m.relatedMemories.includes(r)) && + users.every(u => m.users.includes(u)) && + groups.every(g => m.groups.includes(g)) + ) delete this.memoryMap[id]; + } + } + } + + limitMemories(vacancy: number) { + const { MEMORY_LIMIT } = Config.memory; + const limit = MEMORY_LIMIT > vacancy ? MEMORY_LIMIT - vacancy : 0; // 预留空位用于存储最新记忆 + if (this.memories.length <= limit) return; + this.memories + .map((m) => { + return { + id: m.id, + score: m.calculateScore([], [], [], []) + } + }) + .sort((a, b) => b.score - a.score) // 从大到小排序 + .slice(limit) + .forEach(m => delete this.memoryMap?.[m.id]); + } + + clearMemories() { + this.memoryMap = {}; + } + + async search(query: string, options: searchOptions = { + topK: 10, + tags: [], + relatedMemories: [], + users: [], + groups: [], + method: 'score' + }) { + if (!this.memories.length) return []; + const { topK = 10, tags = [], relatedMemories = [], users = [], groups = [], method = 'score' } = options; + + const { DIMENSION } = Config.memory; + let v: number[] = []; + if (DIMENSION > 0 && query) { + const model = Model.getEmbeddingModel('text-embedding'); + v = await model.callEmbedding(query); + if (!v.length) { + Logger.error('查询向量为空'); + return []; + } + if (v.length !== DIMENSION) { + Logger.error(`查询向量维度不匹配。期望: ${DIMENSION}, 实际: ${v.length}`); + return []; + } + await Promise.all(this.memories.map(async m => { + if (m.vector.length !== DIMENSION) { + Logger.info(`记忆向量维度不匹配,重新获取向量: ${m.id}`); + await m.updateVector(); + } + })) + } + + return this.memories + .map(m => { + if (relatedMemories.length > 0 && relatedMemories.some(r => m.id === r || m.relatedMemories.includes(r))) return m; + return null; + }) + .filter(m => m !== null) + .sort((a, b) => { + switch (method) { + case 'importance': return b.importance - a.importance; + case 'similarity': return b.calculateSimilarity(v, tags, users, groups) - a.calculateSimilarity(v, tags, users, groups); + case 'score': return b.calculateScore(v, tags, users, groups) - a.calculateScore(v, tags, users, groups); + case 'early': return a.createAt - b.createAt; + case 'late': return b.createAt - a.createAt; + case 'recent': return b.lastAccessedAt - a.lastAccessedAt; + } + }) + .slice(0, topK); + } + + async accessMemories(s: string) { + const now = Math.floor(Date.now() / 1000); + (await this.search(s, { + topK: 5, + tags: [], + relatedMemories: [], + users: [], + groups: [], + method: 'similarity' + })).forEach(m => { + m.lastAccessedAt = now; + m.accessCount++; + }) + } + + static async accessRelatedMemories(session: Session, s: string) { + const agent = Agent.get(session.agentName); + const task: Promise[] = []; + // bot记忆权重更新 + task.push(agent.sessionService.memory.accessMemory(s)); + // 知识库记忆权重更新 + task.push(agent.sessionService.knowledge.accessMemories(s)); + // 会话自身记忆权重更新 + task.push(session.memory.accessMemories(s)); + // 群内用户的记忆权重更新 + if (session.sessionType === 'group') task.push(...session.context.users.map(u => agent.sessionService.getSession(u).memory.accessMemories(s))); + await Promise.all(task); + } + + async getTopScoreMemories(text: string = '', users: string[] = [], groups: string[] = []) { + const { MEMORY_SHOW_NUMBER } = Config.memory; + return await this.search(text, { + topK: MEMORY_SHOW_NUMBER, + tags: [], + relatedMemories: [], + users, + groups, + method: 'score' + }); + } + + getLatestMemories(p: number = 1): MemoryItem[] { + if (this.memories.length === 0) return []; + if (p > Math.ceil(this.memories.length / 5)) p = Math.ceil(this.memories.length / 5); + return this.memories + .sort((a, b) => b.createAt - a.createAt) + .slice((p - 1) * 5, p * 5); + } + + buildMemoriesPrompt(sources: MemorySource[]): string { + if (sources.length === 0) return ''; + const { MEMORY, MEMORY_TEMPLATE } = Config.memory; + return MEMORY_TEMPLATE({ + "MEMORY": MEMORY, + "sources": sources + }); + } + + buildLatestMemoriesText(p: number = 1): string { + const sources = [{ + source: '最新记忆', + memories: this.getLatestMemories(p) + }] + return this.buildMemoriesPrompt(sources) + `\n当前页码: ${p}/${Math.ceil(this.memories.length / 5)}`; + } +} + +// 可以通过维护一组索引来优化搜索性能。 +// 好麻烦,不想弄 +// 目前数量级应该没什么优化的需求 \ No newline at end of file diff --git a/src/memory/memory_item.ts b/src/memory/memory_item.ts new file mode 100644 index 0000000..85f3cc7 --- /dev/null +++ b/src/memory/memory_item.ts @@ -0,0 +1,146 @@ +import Model from "../model/model"; +import Config from "../config/config"; +import Logger from "../logger"; +import { cosineSimilarity, getCommonItem, revive, TypeDescriptor } from "../utils/utils"; + +export default class MemoryItem { + static validKeysMap: { [key in keyof MemoryItem]?: TypeDescriptor } = { + 'id': 'string', + 'sessionId': 'string', + 'visibility': 'string', + 'createAt': 'number', + 'lastAccessedAt': 'number', + 'accessCount': 'number', + 'importance': 'number', + 'content': 'string', + 'vector': { array: 'number' }, + 'tags': { array: 'string' }, + 'relatedMemories': { array: 'string' }, + 'users': { array: 'string' }, + 'groups': { array: 'string' } + }; + + // 核心字段 + id: string; // 记忆ID + sessionId: string; // 记忆来源会话ID + visibility: 'public' | 'private'; // 记忆可见性 + + // 淘汰策略相关 + createAt: number; // 创建时间 TTL + lastAccessedAt: number; // 最后访问时间 LRU + accessCount: number; // 访问次数 LFU + importance: number; // 重要性0-1 + + // 内容 + content: string; // 记忆内容 + vector: number[]; // 记忆向量 + tags: string[]; // 记忆标签列表 + relatedMemories: string[]; // 相关记忆ID列表 + users: string[]; // 记忆相关用户ID列表 + groups: string[]; // 记忆相关群组ID列表 + + constructor() { + this.id = ''; + this.sessionId = ''; + this.visibility = 'public'; + this.createAt = 0; + this.lastAccessedAt = 0; + this.accessCount = 0; + this.importance = 0; + this.content = ''; + this.vector = []; + this.tags = []; + this.relatedMemories = []; + this.users = []; + this.groups = []; + } + + get copy(): MemoryItem { + return revive(MemoryItem, JSON.parse(JSON.stringify(this))); + } + + /** + * 计算记忆的新鲜度衰减因子,越大表示越新鲜 + * @returns 衰减因子(1→0) + */ + get decay() { + const now = Math.floor(Date.now() / 1000); + // 年龄(天) + const age = (now - this.createAt) / (24 * 60 * 60); + // 活跃时间(小时) + const activity = (now - this.lastAccessedAt) / (60 * 60); + // 年龄衰减: 半衰期7天 + const ageDecay = this.createAt === 0 ? 1 : Math.exp(-age / 7 * Math.LN2); + // 活跃衰减: 半衰期4小时 + const activityDecay = this.lastAccessedAt === 0 ? 1 : Math.exp(-activity / 4 * Math.LN2); + // 衰减因子 + return ageDecay * 0.7 + activityDecay * 0.3; // 一拍脑门决定的加权 + } + + get accessScore() { + // 饱和函数,访问次数归一化 + const accessNorm = 1 - 1 / (this.accessCount + 1); + return accessNorm * this.decay; + } + + /** + * 计算记忆与查询的相似度分数 + * @param v 查询向量 + * @param t 查询标签列表 + * @param u 查询用户列表 + * @param g 查询群组列表 + * @returns 相似度分数(0-1) + */ + calculateSimilarity(v: number[], t: string[], u: string[], g: string[]): number { + // 总权重 0-1 + const tw = (v.length ? 0.4 : 0) + (u.length ? 0.2 : 0) + (g.length ? 0.2 : 0) + (t.length ? 0.2 : 0); + if (tw === 0) return 0; + // 向量相似度分数(如果提供了向量v) 0-1 + const vs = (v && v.length > 0 && this.vector && this.vector.length > 0) ? (cosineSimilarity(v, this.vector) + 1) / 2 : 0; + // 用户相似度分数 0-1 + const us = u.length ? getCommonItem(this.users, u).length / new Set([...this.users, ...u]).size : 0; + // 群组相似度分数 0-1 + const gs = g.length ? getCommonItem(this.groups, g).length / new Set([...this.groups, ...g]).size : 0; + // 标签匹配分数 0-1 + const ts = t.length ? getCommonItem(this.tags, t).length / new Set([...this.tags, ...t]).size : 0; + // 综合相似度分数 0-1 + const avs = vs * 0.4 + us * 0.2 + gs * 0.2 + ts * 0.2; + // 相似度增强因子 0-1 + return avs / tw; + } + + /** + * 计算记忆的最终分数 + * @param v 查询向量 + * @param t 查询标签列表 + * @param u 查询用户列表 + * @param g 查询群组列表 + * @returns 相似度分数(0-1) + */ + calculateScore(v: number[], t: string[], u: string[], g: string[]): number { + const similarity = this.calculateSimilarity(v, t, u, g); + return this.importance * 0.2 + this.accessScore * 0.2 + similarity * 0.6; + } + + compareWith(m: MemoryItem): boolean { + return this.content === m.content && this.sessionId === m.sessionId; + } + + merge(m: MemoryItem) { + this.importance = m.importance; + this.tags = Array.from(new Set([...this.tags, ...m.tags])); + this.relatedMemories = Array.from(new Set([...this.relatedMemories, ...m.relatedMemories])); + this.users = Array.from(new Set([...this.users, ...m.users])); + this.groups = Array.from(new Set([...this.groups, ...m.groups])); + } + + async updateVector() { + const { DIMENSION } = Config.memory; + Logger.info(`更新记忆向量: ${this.id}`); + const model = Model.getEmbeddingModel('text-embedding'); + const vector = await model.callEmbedding(this.content); + if (!vector.length) return Logger.error('返回向量为空'); + if (vector.length !== DIMENSION) return Logger.error(`向量维度不匹配。期望: ${DIMENSION}, 实际: ${vector.length}`); + this.vector = vector; + } +} \ No newline at end of file diff --git a/src/memory/session_memory.ts b/src/memory/session_memory.ts new file mode 100644 index 0000000..a63624b --- /dev/null +++ b/src/memory/session_memory.ts @@ -0,0 +1,180 @@ +import Agent from "../agent/agent"; +import Config from "../config/config"; +import Logger from "../logger"; +import { TypeDescriptor } from "../utils/utils"; +import MemoryService from "./memory"; +import MemoryItem from "./memory_item"; +import { MemorySource } from "./types"; + +export default class SessionMemoryService extends MemoryService { + static validKeysMap: { [key in keyof SessionMemoryService]?: TypeDescriptor } = { + memoryMap: { array: MemoryItem }, + sessionId: 'string', + summaryStatus: 'boolean', + summaries: { array: 'string' } + }; + sessionId: string; + summaryStatus: boolean; + summaries: string[]; + + constructor() { + super(); + this.sessionId = ''; + this.summaryStatus = false; + this.summaries = []; + } + + async buildMemoryPrompt(agent: Agent, text: string): Promise { + // 获取users、groups + const session = agent.sessionService.getSession(this.sessionId); + const users = session.sessionType === 'group' ? session.context.users : [session.sessionId]; + const groups = session.sessionType === 'group' ? [session.sessionId] : []; + const sources: MemorySource[] = []; + // bot记忆 + sources.push({ + source: '核心记忆', + memories: await agent.sessionService.memory.getTopScoreMemories(text, users, groups) + }) + // 会话记忆 + sources.push({ + source: '会话记忆', + memories: await session.memory.getTopScoreMemories(text, users, groups) + }) + // 群内用户的记忆 + if (session.sessionType === 'group') { + for (const u of session.context.users) { + sources.push({ + source: `用户${u}记忆`, + memories: await agent.sessionService.getSession(u).memory.getTopScoreMemories(text, users, groups) + }) + } + } + + return this.buildMemoriesPrompt(sources); + } + + // wip 使用总结智能体 + async summarize(session: Session) { + if (!this.summaryStatus) return; + + const { url: chatUrl, apiKey: chatApiKey } = Config.request; + const { isPrefix, showNumber, showMsgId, showTime } = Config.message; + const { shortMemorySummaryRound, memoryUrl, memoryApiKey, memoryBodyTemplate, memoryPromptTemplate } = Config.memory; + + const { roleSetting } = getRoleSetting(ctx); + + const messages = ai.context.messages; + let sumMessages = messages.slice(); + let round = 0; + for (let i = 0; i < messages.length; i++) { + if (messages[i].role === 'user' && !messages[i].name.startsWith('_')) { + round++; + } + if (round > shortMemorySummaryRound) { + sumMessages = messages.slice(0, i); // 只保留最近的shortMemorySummaryRound轮对话 + break; + } + } + + if (sumMessages.length === 0) { + return; + } + + let url = chatUrl; + let apiKey = chatApiKey; + if (memoryUrl.trim()) { + url = memoryUrl; + apiKey = memoryApiKey; + } + + try { + const prompt = memoryPromptTemplate({ + "角色设定": roleSetting, + "平台": ctx.endPoint.platform, + "私聊": ctx.isPrivate, + "展示号码": showNumber, + "用户名称": ctx.player.name, + "用户号码": ctx.player.userId.replace(/^.+:/, ''), + "群聊名称": ctx.group.groupName, + "群聊号码": ctx.group.groupId.replace(/^.+:/, ''), + "添加前缀": isPrefix, + "展示消息ID": showMsgId, + "展示时间": showTime, + "对话内容": isPrefix ? sumMessages.map(message => { + if (message.role === 'assistant' && message?.tool_calls && message?.tool_calls.length > 0) { + return `\n[function_call]: ${message.tool_calls.map((tool_call, index) => `${index + 1}. ${JSON.stringify(tool_call.function, null, 2)}`).join('\n')}`; + } + + return `[${message.role}]: ${buildContent(message)}`; + }).join('\n') : JSON.stringify(sumMessages) + }) + + Logger.info(`记忆总结prompt:\n`, prompt); + + const messages = [ + { + role: "system", + content: prompt + } + ] + const bodyObject = parseBody(memoryBodyTemplate, messages, [], "none"); + + const time = Date.now(); + const data = await fetchData(url, apiKey, bodyObject); + + if (data.choices && data.choices.length > 0) { + AIManager.updateUsage(data.model, data.usage); + + const message = data.choices[0].message; + const finish_reason = data.choices[0].finish_reason; + + if (message.hasOwnProperty('reasoning_content')) { + Logger.info(`思维链内容:`, message.reasoning_content); + } + + const reply = message.content || ''; + Logger.info(`响应内容:`, reply, '\nlatency:', Date.now() - time, 'ms', '\nfinish_reason:', finish_reason); + + const memoryData = JSON.parse(reply) as { + content: string, + memories: { + memory_type: 'private' | 'group', + name: string, + text: string, + keywords?: string[], + userList?: string[], + groupList?: string[], + }[] + }; + + + this.shortMemoryList.push(memoryData.content); + this.limitShortMemory(); + + memoryData.memories.forEach(m => { + Tool.toolMap["add_memory"].solve(ctx, msg, ai, m); + }); + } + } catch (e) { + Logger.error(`更新短期记忆失败: ${e.message}`); + } + } + + limitSummaries() { + const { SUMMARY_LIMIT } = Config.memory; + if (this.summaries.length > SUMMARY_LIMIT) this.summaries.splice(0, this.summaries.length - SUMMARY_LIMIT); + } + + clearSummaries() { + this.summaries = []; + } + + buildSummaryPrompt(): string { + if (this.summaries.length === 0) return ''; + const { SUMMARY, SUMMARY_TEMPLATE } = Config.memory; + return SUMMARY_TEMPLATE({ + "SUMMARY": SUMMARY, + "summaries": this.summaries + }); + } +} \ No newline at end of file diff --git a/src/memory/types.ts b/src/memory/types.ts new file mode 100644 index 0000000..f0b5265 --- /dev/null +++ b/src/memory/types.ts @@ -0,0 +1,14 @@ +import { MemoryItem } from "./memory_item"; + +export interface searchOptions { + topK: number; + tags: string[]; + relatedMemories: string[]; + users: string[]; + groups: string[]; + method: 'importance' | 'similarity' | 'score' | 'early' | 'late' | 'recent'; +} +export interface MemorySource { + source: string; + memories: MemoryItem[]; +} \ No newline at end of file diff --git a/src/model/chat.ts b/src/model/chat.ts new file mode 100644 index 0000000..fa76ea8 --- /dev/null +++ b/src/model/chat.ts @@ -0,0 +1,58 @@ +import Agent from "../agent/agent"; +import { UsageManager } from "../agent/usage"; +import { Config } from "../config/config"; +import { DEFAULT_CHAT_MODEL_BODY } from "../config/static_config"; +import { logger } from "../logger"; +import { ToolCall } from "../tool/types"; +import { withTimeout } from "../utils/utils"; +import { fetchData } from "../utils/web"; +import { BaseModel } from "./model"; +import { ChatModelUse, ModelBody } from "./types"; + +export default class ChatModel extends BaseModel { + use: ChatModelUse[]; + constructor(use: ChatModelUse[], name: string, provider: string, base_url: string, api_key: string, body: ModelBody) { + super(name, provider, base_url, api_key, body); + this.use = use; + } + + get url() { + return `${this.baseUrl}/chat/completions`; + } + + async callChat(agent: Agent, sessionId: string): Promise<{ content: string, tool_calls: ToolCall[] }> { + const { TIMEOUT } = Config.base; + try { + const body = this.buildBody({ + ...DEFAULT_CHAT_MODEL_BODY, + messages: agent.sessionService.getSession(sessionId).getMessages(), + tools: agent.getRequestTools() + }); + logger.logMessages(body) + + const time = Date.now(); + const data = await withTimeout(() => fetchData(this.url, this.apiKey, body), TIMEOUT); + if (data.choices && data.choices.length > 0) { + UsageManager.updateUsage(data.model, data.usage); + + const message = data.choices[0].message; + const finish_reason = data.choices[0].finish_reason; + + if (message.hasOwnProperty('reasoning_content')) { + logger.info(`思维链内容:`, message.reasoning_content); + } + + const content = message.content || ''; + + logger.info(`响应内容:`, content, '\nlatency:', Date.now() - time, 'ms', '\nfinish_reason:', finish_reason); + + return { content, tool_calls: message.tool_calls || [] }; + } else { + throw new Error(`服务器响应中没有choices或choices为空\n响应体:${JSON.stringify(data, null, 2)}`); + } + } catch (e) { + logger.error(`在调用模型${this.name}中出错:`, e.message); + return { content: '', tool_calls: [] }; + } + } +} \ No newline at end of file diff --git a/src/model/embedding.ts b/src/model/embedding.ts new file mode 100644 index 0000000..713d37c --- /dev/null +++ b/src/model/embedding.ts @@ -0,0 +1,63 @@ +import { UsageManager } from "../agent/usage"; +import { Config } from "../config/config"; +import { DEFAULT_EMBEDDING_MODEL_BODY } from "../config/static_config"; +import { logger } from "../logger"; +import { withTimeout } from "../utils/utils"; +import { fetchData } from "../utils/web"; +import { BaseModel } from "./model"; +import { EmbeddingModelUse, ModelBody } from "./types"; + +export default class EmbeddingModel extends BaseModel { + static vectorCache: { text: string, vector: number[] } = { text: '', vector: [] }; + + use: EmbeddingModelUse[]; + constructor(use: EmbeddingModelUse[], name: string, provider: string, base_url: string, api_key: string, body: ModelBody) { + super(name, provider, base_url, api_key, body); + this.use = use; + } + + get url() { + return `${this.baseUrl}/embeddings`; + } + + async callEmbedding(text: string): Promise { + if (!text) { + logger.warning(`getEmbedding: 文本为空`); + return []; + } + + const { TIMEOUT } = Config.base; + + if (EmbeddingModel.vectorCache.text === text && EmbeddingModel.vectorCache.vector.length === this.body.dimensions) { + const v = EmbeddingModel.vectorCache.vector; + return v; + } + + try { + const body = this.buildBody({ + ...DEFAULT_EMBEDDING_MODEL_BODY, + input: text + }); + + const time = Date.now(); + const data = await withTimeout(() => fetchData(this.url, this.apiKey, body), TIMEOUT); + if (data.data && data.data.length > 0) { + UsageManager.updateUsage(data.model, data.usage); + + const embedding = data.data[0].embedding; + + logger.info(`文本:`, text, `\n响应embedding长度:`, embedding.length, '\nlatency:', Date.now() - time, 'ms'); + EmbeddingModel.vectorCache.text = text; + EmbeddingModel.vectorCache.vector = embedding; + + return embedding; + } else { + throw new Error(`服务器响应中没有data或data为空\n响应体:${JSON.stringify(data, null, 2)}`); + } + } catch (e) { + logger.error(`在调用模型${this.name}中出错:`, e.message); + return []; + } + + } +} \ No newline at end of file diff --git a/src/model/image.ts b/src/model/image.ts new file mode 100644 index 0000000..72e8531 --- /dev/null +++ b/src/model/image.ts @@ -0,0 +1,96 @@ +import Agent from "../agent/agent"; +import { UsageManager } from "../agent/usage"; +import Config from "../config/config"; +import { DEFAULT_IMAGE_MODEL_BODY } from "../config/static_config"; +import Logger from "../logger"; +import { ToolCall } from "../tool/types"; +import { withTimeout } from "../utils/utils"; +import { fetchData } from "../utils/web"; +import { BaseModel } from "./model"; +import { ImageModelUse, ModelBody } from "./types"; + +export default class ImageModel extends BaseModel { + use: ImageModelUse[]; + constructor(use: ImageModelUse[], name: string, provider: string, base_url: string, api_key: string, body: ModelBody) { + super(name, provider, base_url, api_key, body); + this.use = use; + } + + get url() { + return `${this.baseUrl}/chat/completions`; + } + + async callITT(src: string, prompt = ''): Promise { + const { TIMEOUT } = Config.base; + try { + const body = this.buildBody({ + ...DEFAULT_IMAGE_MODEL_BODY, + messages: [{ + role: "user", + content: [{ + "type": "image_url", + "image_url": { "url": src } + }, { + "type": "text", + "text": prompt + }] + }] + }); + Logger.logMessages(body); + + const time = Date.now(); + const data = await withTimeout(() => fetchData(this.url, this.apiKey, body), TIMEOUT); + if (data.choices && data.choices.length > 0) { + UsageManager.updateUsage(data.model, data.usage); + + const message = data.choices[0].message; + const content = message.content || ''; + + Logger.info(`响应内容:`, content, '\nlatency', Date.now() - time, 'ms'); + + return content; + } else { + throw new Error(`服务器响应中没有choices或choices为空\n响应体:${JSON.stringify(data, null, 2)}`); + } + } catch (e) { + Logger.error(`在调用模型${this.name}中出错:`, e.message); + return ''; + } + } + + async callChat(agent: Agent, sessionId: string): Promise<{ content: string, tool_calls: ToolCall[] }> { + const { TIMEOUT } = Config.base; + try { + const body = this.buildBody({ + ...DEFAULT_IMAGE_MODEL_BODY, + messages: agent.sessionService.getSession(sessionId).getImageMessages(), + tools: agent.getRequestTools() + }); + Logger.logMessages(body); + + const time = Date.now(); + const data = await withTimeout(() => fetchData(this.url, this.apiKey, body), TIMEOUT); + if (data.choices && data.choices.length > 0) { + UsageManager.updateUsage(data.model, data.usage); + + const message = data.choices[0].message; + const finish_reason = data.choices[0].finish_reason; + + if (message.hasOwnProperty('reasoning_content')) { + Logger.info(`思维链内容:`, message.reasoning_content); + } + + const content = message.content || ''; + + Logger.info(`响应内容:`, content, '\nlatency:', Date.now() - time, 'ms', '\nfinish_reason:', finish_reason); + + return { content, tool_calls: message.tool_calls || [] }; + } else { + throw new Error(`服务器响应中没有choices或choices为空\n响应体:${JSON.stringify(data, null, 2)}`); + } + } catch (e) { + Logger.error(`在调用模型${this.name}中出错:`, e.message); + return { content: '', tool_calls: [] }; + } + } +} \ No newline at end of file diff --git a/src/model/model.ts b/src/model/model.ts new file mode 100644 index 0000000..5e47c47 --- /dev/null +++ b/src/model/model.ts @@ -0,0 +1,86 @@ +import ChatModel from "./chat"; +import EmbeddingModel from "./embedding"; +import ImageModel from "./image"; +import { ChatModelUse, EmbeddingModelUse, ImageModelUse, ModelBody } from "./types"; + +export class BaseModel { + name: string; + provider: string; + baseUrl: string; + apiKey: string; + body: ModelBody; + + constructor(name: string, provider: string, base_url: string, api_key: string, body: ModelBody) { + this.name = name; + this.provider = provider; + this.baseUrl = base_url; + this.apiKey = api_key; + this.body = body; + } + + buildBody(args: { [key: string]: any }) { + const body = JSON.parse(JSON.stringify(this.body)); + for (const key in args) { + if (!args.hasOwnProperty(key)) body[key] = args[key]; + } + return body; + } +} + +export default class Model { + static chatModels: ChatModel[] = []; + static imageModels: ImageModel[] = []; + static embeddingModels: EmbeddingModel[] = []; + + static getChatModel(use: ChatModelUse): ChatModel | ImageModel | null { + const chatModelList = Model.chatModels.filter(model => model.use.includes(use)); + if (chatModelList.length > 0) { + const randomIndex = Math.floor(Math.random() * chatModelList.length); + return chatModelList[randomIndex]; + } + const ImageModelList = Model.imageModels.filter(model => model.use.includes(use)); + if (ImageModelList.length > 0) { + const randomIndex = Math.floor(Math.random() * ImageModelList.length); + return ImageModelList[randomIndex]; + } + const chatModelAnyList = Model.chatModels.filter(model => model.use.length === 0); + if (chatModelAnyList.length > 0) { + const randomIndex = Math.floor(Math.random() * chatModelAnyList.length); + return chatModelAnyList[randomIndex]; + } + const ImageModelAnyList = Model.imageModels.filter(model => model.use.length === 0); + if (ImageModelAnyList.length > 0) { + const randomIndex = Math.floor(Math.random() * ImageModelAnyList.length); + return ImageModelAnyList[randomIndex]; + } + return null; + } + + static getImageModel(use: ImageModelUse): ImageModel | null { + const ImageModelList = Model.imageModels.filter(model => model.use.includes(use)); + if (ImageModelList.length > 0) { + const randomIndex = Math.floor(Math.random() * ImageModelList.length); + return ImageModelList[randomIndex]; + } + const ImageModelAnyList = Model.imageModels.filter(model => model.use.length === 0); + if (ImageModelAnyList.length > 0) { + const randomIndex = Math.floor(Math.random() * ImageModelAnyList.length); + return ImageModelAnyList[randomIndex]; + } + return null; + } + + static getEmbeddingModel(use: EmbeddingModelUse): EmbeddingModel | null { + const EmbeddingModelList = Model.embeddingModels.filter(model => model.use.includes(use)); + if (EmbeddingModelList.length > 0) { + const randomIndex = Math.floor(Math.random() * EmbeddingModelList.length); + return EmbeddingModelList[randomIndex]; + } + const EmbeddingModelAnyList = Model.embeddingModels.filter(model => model.use.length === 0); + if (EmbeddingModelAnyList.length > 0) { + const randomIndex = Math.floor(Math.random() * EmbeddingModelAnyList.length); + return EmbeddingModelAnyList[randomIndex]; + } + return null; + } +} \ No newline at end of file diff --git a/src/model/types.ts b/src/model/types.ts new file mode 100644 index 0000000..32505c3 --- /dev/null +++ b/src/model/types.ts @@ -0,0 +1,11 @@ +export type ChatModelUse = 'chat' | 'compression' | 'summarization'; +export type ImageModelUse = 'image-understanding' | ChatModelUse; +export type EmbeddingModelUse = 'text-embedding'; +export interface ModelBody { + max_tokens?: number, + stop?: string[] | null, + stream?: boolean, + temperature?: number, + top_p?: number, + [key: string]: any +} \ No newline at end of file diff --git a/src/note.txt b/src/note.txt new file mode 100644 index 0000000..ca1dc9e --- /dev/null +++ b/src/note.txt @@ -0,0 +1,89 @@ +tool state + +summary count + +last reply 放进 sendService 里,所有send换成一个,除了指令调用。 + +计数器、计时器逻辑移到trigger + +autoNameMod: number; // 自动修改上下文里的名字,0:不自动修改,1:修改为昵称,2:修改为群名片 + async setName(epId: string, gid: string, uid: string, mod: 'nickname' | 'card') { + let name = ''; + switch (mod) { + case 'nickname': { + const strangerInfo = await getStrangerInfo(epId, uid.replace(/^.+:/, '')); + if (!strangerInfo || !strangerInfo.nickname) { + logger.warning(`未找到用户<${uid}>的昵称`); + break; + } + name = strangerInfo.nickname; + break; + } + case 'card': { + if (!gid) break; + const memberInfo = await getGroupMemberInfo(epId, gid.replace(/^.+:/, ''), uid.replace(/^.+:/, '')); + if (!memberInfo) { + logger.warning(`获取用户<${uid}>的群成员信息失败,尝试使用昵称`); + this.setName(epId, gid, uid, 'nickname'); + break; + } + name = memberInfo.card || memberInfo.nickname; + if (!name) { + this.setName(epId, gid, uid, 'nickname'); + return; + } + break; + } + } + if (!name) { + logger.warning(`用户<${uid}>未设置昵称或群名片`); + return; + } + const { ctx } = getCtxAndMsg(epId, uid, gid); + ctx.player.name = name; + this.messages.forEach(message => message.name = message.uid === uid ? name : message.name); + } + + async updateName(epId: string, gid: string, uid: string) { + switch (this.autoNameMod) { + case 1: { + await this.setName(epId, gid, uid, 'nickname'); + break; + } + case 2: { + await this.setName(epId, gid, uid, 'card'); + break; + } + } + } +放到user manager + +tool分为触发callback和不触发callback两种,将tool choice删除 + +请求时多条消息合并到一条user消息中 + +把生成本地图片列表的逻辑也做进config + +把所有new Image()的地方整进imageMana + +model可以设置其提供给特定agent或all + +ai读取所有插件的指令并调用的功能 + +连续多条user时,对后面几条进行压缩 + +提供api给其他插件,实现kp agent + +kwarg,p,存在页码和概率撞车的情况 + +把manager合并入实例类里面,减少命名冗余 + +https://api-docs.deepseek.com/zh-cn/guides/thinking_mode#%E5%B7%A5%E5%85%B7%E8%B0%83%E7%94%A8 + +https://api-docs.deepseek.com/zh-cn/guides/tool_calls#%E6%80%9D%E8%80%83%E6%A8%A1%E5%BC%8F + +检查调用并修复的agent + +更新文本向量是不是要用一起吧标签,user,group之类的信息塞进去啊 + +知识库记忆加进去的时候别忘了更新向量 \ No newline at end of file diff --git a/src/service.ts b/src/service.ts deleted file mode 100644 index 1ed8e0e..0000000 --- a/src/service.ts +++ /dev/null @@ -1,352 +0,0 @@ -import { AIManager } from "./AI/AI"; -import { ToolCall, ToolInfo } from "./tool/tool"; -import { ConfigManager } from "./config/configManager"; -import { parseBody, parseEmbeddingBody } from "./utils/utils_message"; -import { logger } from "./logger"; -import { withTimeout } from "./utils/utils"; - -export async function sendChatRequest(messages: { - role: string, - content: string, - tool_calls?: ToolCall[], - tool_call_id?: string -}[], tools: ToolInfo[], tool_choice: string): Promise<{ content: string, tool_calls: ToolCall[] }> { - const { url, apiKey, bodyTemplate, timeout } = ConfigManager.request; - - try { - const bodyObject = parseBody(bodyTemplate, messages, tools, tool_choice); - const time = Date.now(); - - const data = await withTimeout(() => fetchData(url, apiKey, bodyObject), timeout); - - if (data.choices && data.choices.length > 0) { - AIManager.updateUsage(data.model, data.usage); - - const message = data.choices[0].message; - const finish_reason = data.choices[0].finish_reason; - - if (message.hasOwnProperty('reasoning_content')) { - logger.info(`思维链内容:`, message.reasoning_content); - } - - const content = message.content || ''; - - logger.info(`响应内容:`, content, '\nlatency:', Date.now() - time, 'ms', '\nfinish_reason:', finish_reason); - - return { content, tool_calls: message.tool_calls || [] }; - } else { - throw new Error(`服务器响应中没有choices或choices为空\n响应体:${JSON.stringify(data, null, 2)}`); - } - } catch (e) { - logger.error("在sendChatRequest中出错:", e.message); - return { content: '', tool_calls: [] }; - } -} - -export async function sendITTRequest(messages: { - role: string, - content: { - type: string, - image_url?: { url: string } - text?: string - }[] -}[]): Promise { - const { timeout } = ConfigManager.request; - const { url, apiKey, bodyTemplate } = ConfigManager.image; - - try { - const bodyObject = parseBody(bodyTemplate, messages, null, null); - const time = Date.now(); - - const data = await withTimeout(() => fetchData(url, apiKey, bodyObject), timeout); - - if (data.choices && data.choices.length > 0) { - AIManager.updateUsage(data.model, data.usage); - - const message = data.choices[0].message; - const content = message.content || ''; - - logger.info(`响应内容:`, content, '\nlatency', Date.now() - time, 'ms'); - - return content; - } else { - throw new Error(`服务器响应中没有choices或choices为空\n响应体:${JSON.stringify(data, null, 2)}`); - } - } catch (e) { - logger.error("在sendITTRequest中请求出错:", e.message); - return ''; - } -} - -const vectorCache: { text: string, vector: number[] } = { text: '', vector: [] }; - -export async function getEmbedding(text: string): Promise { - if (!text) { - logger.warning(`getEmbedding: 文本为空`); - return []; - } - - const { timeout } = ConfigManager.request; - const { embeddingDimension, embeddingUrl, embeddingApiKey, embeddingBodyTemplate } = ConfigManager.memory; - - if (vectorCache.text === text && vectorCache.vector.length === embeddingDimension) { - const v = vectorCache.vector; - return v; - } - - try { - const bodyObject = parseEmbeddingBody(embeddingBodyTemplate, text, embeddingDimension); - const time = Date.now(); - - const data = await withTimeout(() => fetchData(embeddingUrl, embeddingApiKey, bodyObject), timeout); - - if (data.data && data.data.length > 0) { - AIManager.updateUsage(data.model, data.usage); - - const embedding = data.data[0].embedding; - - logger.info(`文本:`, text, `\n响应embedding长度:`, embedding.length, '\nlatency:', Date.now() - time, 'ms'); - vectorCache.text = text; - vectorCache.vector = embedding; - - return embedding; - } else { - throw new Error(`服务器响应中没有data或data为空\n响应体:${JSON.stringify(data, null, 2)}`); - } - } catch (e) { - logger.error("在getEmbedding中出错:", e.message); - return []; - } -} - -export async function fetchData(url: string, apiKey: string, bodyObject: any): Promise { - // 打印请求发送前的上下文 - if (bodyObject.hasOwnProperty('messages')) { - const s = JSON.stringify(bodyObject.messages, (key, value) => { - if (key === "" && Array.isArray(value)) { - return value.filter(item => item.role !== "system"); - } - return value; - }); - logger.info(`请求发送前的上下文:\n`, s); - } - - const response = await fetch(url, { - method: 'POST', - headers: { - "Authorization": `Bearer ${apiKey}`, - "Content-Type": "application/json", - "Accept": "application/json" - }, - body: JSON.stringify(bodyObject) - }); - - // logger.info("响应体", JSON.stringify(response, null, 2)); - - const text = await response.text(); - if (!response.ok) { - throw new Error(`请求失败! 状态码: ${response.status}\n响应体:${text}`); - } - if (!text) { - throw new Error("响应体为空"); - } - - try { - const data = JSON.parse(text); - if (data.error) { - throw new Error(`请求失败! 错误信息: ${data.error.message}`); - } - return data; - } catch (e) { - throw new Error(`解析响应体时出错:${e.message}\n响应体:${text}`); - } -} - -export async function startStream(messages: { - role: string, - content: string -}[]): Promise { - const { url, apiKey, bodyTemplate, timeout } = ConfigManager.request; - const { streamUrl } = ConfigManager.backend; - - try { - const bodyObject = parseBody(bodyTemplate, messages, null, null); - - // 打印请求发送前的上下文 - const s = JSON.stringify(bodyObject.messages, (key, value) => { - if (key === "" && Array.isArray(value)) { - return value.filter(item => item.role !== "system"); - } - return value; - }); - logger.info(`请求发送前的上下文:\n`, s); - - const response = await withTimeout(() => fetch(`${streamUrl}/start`, { - method: 'POST', - headers: { - "Content-Type": "application/json", - "Accept": "application/json" - }, - body: JSON.stringify({ - url: url, - api_key: apiKey, - body_obj: bodyObject - }) - }), timeout); - - // logger.info("响应体", JSON.stringify(response, null, 2)); - - const text = await response.text(); - if (!response.ok) { - throw new Error(`请求失败! 状态码: ${response.status}\n响应体:${text}`); - } - if (!text) { - throw new Error("响应体为空"); - } - - try { - const data = JSON.parse(text); - if (data.error) { - throw new Error(`请求失败! 错误信息: ${data.error.message}`); - } - if (!data.id) { - throw new Error("服务器响应中没有id字段"); - } - return data.id; - } catch (e) { - throw new Error(`解析响应体时出错:${e}\n响应体:${text}`); - } - } catch (e) { - logger.error("在startStream中出错:", e.message); - return ''; - } -} - -export async function pollStream(id: string, after: number): Promise<{ status: string, reply: string, nextAfter: number }> { - const { streamUrl } = ConfigManager.backend; - - try { - const response = await fetch(`${streamUrl}/poll?id=${id}&after=${after}`, { - method: 'GET', - headers: { - "Accept": "application/json" - } - }); - - // logger.info("响应体", JSON.stringify(response, null, 2)); - - const text = await response.text(); - if (!response.ok) { - throw new Error(`请求失败! 状态码: ${response.status}\n响应体:${text}`); - } - if (!text) { - throw new Error("响应体为空"); - } - - try { - const data = JSON.parse(text); - if (data.error) { - throw new Error(`请求失败! 错误信息: ${data.error.message}`); - } - if (!data.status) { - throw new Error("服务器响应中没有status字段"); - } - return { - status: data.status, - reply: data.results.join(''), - nextAfter: data.next_after - }; - } catch (e) { - throw new Error(`解析响应体时出错:${e}\n响应体:${text}`); - } - } catch (e) { - logger.error("在pollStream中出错:", e.message); - return { status: 'failed', reply: '', nextAfter: 0 }; - } -} - -export async function endStream(id: string): Promise { - const { streamUrl } = ConfigManager.backend; - - try { - const response = await fetch(`${streamUrl}/end?id=${id}`, { - method: 'GET', - headers: { - "Accept": "application/json" - } - }); - - // logger.info("响应体", JSON.stringify(response, null, 2)); - - const text = await response.text(); - if (!response.ok) { - throw new Error(`请求失败! 状态码: ${response.status}\n响应体:${text}`); - } - if (!text) { - throw new Error("响应体为空"); - } - - try { - const data = JSON.parse(text); - if (data.error) { - throw new Error(`请求失败! 错误信息: ${data.error.message}`); - } - if (!data.status) { - throw new Error("服务器响应中没有status字段"); - } - logger.info('对话结束', data.status === 'success' ? '成功' : '失败'); - if (data.status === 'success') { - AIManager.updateUsage(data.model, data.usage); - } - return data.status; - } catch (e) { - throw new Error(`解析响应体时出错:${e}\n响应体:${text}`); - } - } catch (e) { - logger.error("在endStream中出错:", e.message); - return ''; - } -} - -export async function get_chart_url(chart_type: string, usage_data: { - [key: string]: { - prompt_tokens: number; - completion_tokens: number; - } -}) { - const { usageChartUrl } = ConfigManager.backend; - try { - const response = await fetch(`${usageChartUrl}/chart`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - }, - body: JSON.stringify({ - chart_type: chart_type, - data: usage_data - }) - }) - - const text = await response.text(); - if (!response.ok) { - throw new Error(`请求失败! 状态码: ${response.status}\n响应体: ${text}`); - } - if (!text) { - throw new Error("响应体为空"); - } - - try { - const data = JSON.parse(text); - if (data.error) { - throw new Error(`请求失败! 错误信息: ${data.error.message}`); - } - return data.image_url; - } catch (e) { - throw new Error(`解析响应体时出错:${e}\n响应体:${text}`); - } - } catch (e) { - logger.error("在get_chart_url中请求出错:", e.message); - return ''; - } -} \ No newline at end of file diff --git a/src/AI/context.ts b/src/session/context.ts similarity index 64% rename from src/AI/context.ts rename to src/session/context.ts index 7c3b81a..1cf1d6d 100644 --- a/src/AI/context.ts +++ b/src/session/context.ts @@ -1,89 +1,63 @@ -import { ToolCall } from "../tool/tool"; -import { ConfigManager } from "../config/configManager"; -import { Image, ImageManager } from "./image"; -import { getCtxAndMsg } from "../utils/utils_seal"; -import { levenshteinDistance } from "../utils/utils_string"; -import { AI, AIManager, GroupInfo, UserInfo } from "./AI"; +import { Config } from "../config/config"; +import { Image, ImageService } from "../image"; +import { getCtxAndMsg } from "../utils/seal"; +import { levenshteinDistance } from "../utils/string"; import { logger } from "../logger"; -import { netExists, getFriendList, getGroupList, getGroupMemberInfo, getGroupMemberList, getStrangerInfo } from "../utils/utils_ob11"; -import { revive } from "../utils/utils"; - -export interface MessageInfo { - msgId: string; - time: number; // 秒 - content: string; -} - -export interface Message { - role: string; - tool_calls?: ToolCall[]; - tool_call_id?: string; - - uid: string; - name: string; - images: Image[]; - msgArray: MessageInfo[]; -} +import { netExists, getFriendList, getGroupList, getGroupMemberInfo, getGroupMemberList, getStrangerInfo } from "../utils/ob11"; +import { TypeDescriptor } from "../utils/utils"; +import { MessageItem } from "./types"; export class Context { - static validKeys: (keyof Context)[] = ['messages', 'ignoreList', 'summaryCounter', 'autoNameMod']; - messages: Message[]; - ignoreList: string[]; - summaryCounter: number; // 用于短期记忆自动总结计数 - autoNameMod: number; // 自动修改上下文里的名字,0:不自动修改,1:修改为昵称,2:修改为群名片 - - lastReply: string; - counter: number; - timer: number; + static validKeysMap: { [key in keyof Context]?: TypeDescriptor } = { + messages: { array: 'any' } + } + messages: MessageItem[]; constructor() { this.messages = []; - this.ignoreList = []; - this.summaryCounter = 0; - this.lastReply = ''; - this.counter = 0; - this.timer = null; } - reviveMessages() { - this.messages = this.messages.map(message => { - if (!message.hasOwnProperty('role')) return null; - if (!message.hasOwnProperty('uid')) return null; - if (!message.hasOwnProperty('name')) return null; - if (!message.hasOwnProperty('images')) return null; - if (!message.hasOwnProperty('msgArray')) return null; + clearMessages(role?: 'user' | 'assistant') { + switch (role) { + case 'user': { + this.messages = this.messages.filter(m => !m.hasOwnProperty('userId')); + break; + } + case 'assistant': { + this.messages = this.messages.filter(m => m.hasOwnProperty('userId')); + break; + } + default: { + this.messages = []; + break; + } + } + } - message.msgArray = message.msgArray.map(msgInfo => { - if (!msgInfo.hasOwnProperty('msgId')) return null; - if (!msgInfo.hasOwnProperty('time')) return null; - if (!msgInfo.hasOwnProperty('content')) return null; + // 添加后检查压缩条件,并对过长user进行压缩 + addUserMessage() { - return msgInfo; - }).filter(msgInfo => msgInfo); + } - message.images = message.images.map(image => revive(Image, image)); + addAssistantMessage() { - return message; - }).filter(message => message); } - clearMessages(...roles: string[]) { - if (roles.length === 0) { - this.summaryCounter = 0; - this.messages = []; - } else { - this.messages = this.messages.filter(message => { - if (roles.includes(message.role)) { - this.summaryCounter--; - return false; - } - return true; - }); - } + addSystemUserMessage() { + + } + + addToolCallsMessage() { + + } + + // 同理,进行压缩 + addToolCallbackMessage() { + } async addMessage(ctx: seal.MsgContext, msg: seal.Message, ai: AI, content: string, images: Image[], role: 'user' | 'assistant', msgId: string = '') { - const { isShortMemory, shortMemorySummaryRound } = ConfigManager.memory; + const { isShortMemory, shortMemorySummaryRound } = Config.memory; const messages = this.messages; const now = Math.floor(Date.now() / 1000); @@ -123,9 +97,9 @@ export class Context { if (length !== 0 && messages[length - 1].uid === uid && !/<[\|│|]?function(?:_call)?>/.test(content)) { messages[length - 1].images.push(...images); messages[length - 1].msgArray.push({ - msgId: msgId, + messageId: msgId, time: now, - content: content + text: content }); } else { const message: Message = { @@ -134,9 +108,9 @@ export class Context { name: name, images: images, msgArray: [{ - msgId: msgId, + messageId: msgId, time: now, - content: content + text: content }] }; messages.push(message); @@ -181,9 +155,9 @@ export class Context { name: '', images: images, msgArray: [{ - msgId: '', + messageId: '', time: now, - content: s + text: s }] }; @@ -205,16 +179,16 @@ export class Context { name: `_${name}`, images: images, msgArray: [{ - msgId: '', + messageId: '', time: now, - content: s + text: s }] }; this.messages.push(message); } limitMessages() { - const { maxRounds } = ConfigManager.message; + const { maxRounds } = Config.message; const messages = this.messages; let round = 0; for (let i = messages.length - 1; i >= 0; i--) { @@ -365,56 +339,7 @@ export class Context { return null; } - async findImage(ctx: seal.MsgContext, id: string): Promise { - // 从用户头像中查找图片 - if (/^user_avatar[::]/.test(id)) { - const ui = await this.findUserInfo(ctx, id.replace(/^user_avatar[::]/, '')); - if (ui) return ImageManager.getUserAvatar(ui.id); - } - // 从群聊头像中查找图片 - if (/^group_avatar[::]/.test(id)) { - const gi = await this.findGroupInfo(ctx, id.replace(/^group_avatar[::]/, '')); - if (gi) return ImageManager.getGroupAvatar(gi.id); - } - - // 从上下文中查找图片 - const messages = this.messages; - const userSet = new Set(); - for (let i = messages.length - 1; i >= 0; i--) { - const image = messages[i].images.find(item => item.id === id); - if (image) return image; - - const uid = messages[i].uid; - if (userSet.has(uid) || messages[i].role !== 'user') continue; - const name = messages[i].name; - if (name.startsWith('_')) continue; - - const image2 = AIManager.getAI(uid).memory.findImage(id); - if (image2) return image2; - } - - if (!ctx.isPrivate) { - const image = AIManager.getAI(ctx.group.groupId).memory.findImage(id); - if (image) return image; - } - - // 从自己记忆中查找图片 - const image = AIManager.getAI(ctx.endPoint.userId).memory.findImage(id); - if (image) return image; - - // 从本地图片库中查找图片 - const { localImagePathMap } = ConfigManager.image; - if (localImagePathMap.hasOwnProperty(id)) { - const image = new Image(); - image.file = localImagePathMap[id]; - return image; - } - - logger.warning(`未找到图片<${id}>`); - return null; - } - - get userInfoList(): UserInfo[] { + get users(): string[] { const userMap: { [key: string]: UserInfo } = {}; this.messages.forEach(message => { if (message.role === 'user' && message.name && message.uid && !message.name.startsWith('_')) { @@ -427,54 +352,4 @@ export class Context { }); return Object.values(userMap); } - - async setName(epId: string, gid: string, uid: string, mod: 'nickname' | 'card') { - let name = ''; - switch (mod) { - case 'nickname': { - const strangerInfo = await getStrangerInfo(epId, uid.replace(/^.+:/, '')); - if (!strangerInfo || !strangerInfo.nickname) { - logger.warning(`未找到用户<${uid}>的昵称`); - break; - } - name = strangerInfo.nickname; - break; - } - case 'card': { - if (!gid) break; - const memberInfo = await getGroupMemberInfo(epId, gid.replace(/^.+:/, ''), uid.replace(/^.+:/, '')); - if (!memberInfo) { - logger.warning(`获取用户<${uid}>的群成员信息失败,尝试使用昵称`); - this.setName(epId, gid, uid, 'nickname'); - break; - } - name = memberInfo.card || memberInfo.nickname; - if (!name) { - this.setName(epId, gid, uid, 'nickname'); - return; - } - break; - } - } - if (!name) { - logger.warning(`用户<${uid}>未设置昵称或群名片`); - return; - } - const { ctx } = getCtxAndMsg(epId, uid, gid); - ctx.player.name = name; - this.messages.forEach(message => message.name = message.uid === uid ? name : message.name); - } - - async updateName(epId: string, gid: string, uid: string) { - switch (this.autoNameMod) { - case 1: { - await this.setName(epId, gid, uid, 'nickname'); - break; - } - case 2: { - await this.setName(epId, gid, uid, 'card'); - break; - } - } - } } diff --git a/src/session/group.ts b/src/session/group.ts new file mode 100644 index 0000000..65154fa --- /dev/null +++ b/src/session/group.ts @@ -0,0 +1,39 @@ +import { Config } from "../config/config"; +import { logger } from "../logger"; +import { revive, TypeDescriptor } from "../utils/utils"; +export default class Group { + static validKeysMap: { [key in keyof Group]?: TypeDescriptor } = { + groupId: 'string', + groupName: 'string', + role: 'string', + owner: 'string', + adminList: { array: 'string' }, + memberList: { array: 'string' } + } + groupId: string; + groupName: string; + role: 'owner' | 'admin' | 'member'; // 自己的角色 + owner: string; // 群主id + adminList: string[]; // 管理员id列表 + memberList: string[]; // 普通成员id列表 + + static groupMap: { [key: string]: Group }; + + static get(groupId: string): Group { + if (!this.groupMap.hasOwnProperty(groupId)) { + let group = new Group(); + try { + const data = JSON.parse(Config.ext.storageGet(`group_${groupId}`) || '{}'); + group = revive(Group, data); + } catch (error) { + logger.error(`加载群${groupId}失败: ${error}`); + } + group.groupId = groupId; + this.groupMap[groupId] = group; + } + return this.groupMap[groupId]; + } + static save(group: Group) { + Config.ext.storageSet(`group_${group.groupId}`, JSON.stringify(group)); + } +} diff --git a/src/session/session.ts b/src/session/session.ts new file mode 100644 index 0000000..f2ad847 --- /dev/null +++ b/src/session/session.ts @@ -0,0 +1,137 @@ +import Agent from "../agent/agent"; +import { Config } from "../config/config"; +import { logger } from "../logger"; +import { toolMap, ToolName, ToolState } from "../tool/tool"; +import { ToolListen } from "../tool/types"; +import { revive, TypeDescriptor } from "../utils/utils"; +import { Context } from "./context"; +import { MemoryService } from "../memory/memory"; +import { RequestMessage, SessionType, State } from "./types"; +import KnowledgeService from "../memory/knowlege"; +import SessionMemoryService from "../memory/session_memory"; + +export class Session { + static validKeysMap: { [key in keyof Session]?: TypeDescriptor } = { + agentName: 'string', + sessionId: 'string', + sessionType: 'string', + state: { + object: { + description: 'string', + impression: 'string', + }, + objectValue: 'any' + }, + context: Context, + memory: SessionMemoryService, + tool: { + object: { + state: { objectValue: 'boolean' } + }, + objectValue: 'default' + }, + ignoredUserIdList: { array: 'string' }, + } + agentName: string; + sessionId: string; + sessionType: SessionType; + state: State; + context: Context; + memory: SessionMemoryService; + tool: { + state: ToolState, + callCount: number, // 单次触发调用函数计数 + listen: ToolListen // 监听调用函数发送的内容 + } + ignoredUserIdList: string[]; + + constructor() { + this.agentName = ''; + this.sessionId = ''; + this.sessionType = 'group'; + this.state = { + description: '', + impression: '', + }; + this.context = new Context(); + this.tool = { + state: Object.keys(toolMap).reduce((acc, key) => { + acc[key as ToolName] = false; + return acc; + }, {} as ToolState), + callCount: 0, + listen: { + timeoutId: null, + resolve: null, + reject: null, + cleanup: () => { + if (this.tool.listen.timeoutId) clearTimeout(this.tool.listen.timeoutId); + this.tool.listen.timeoutId = null; + this.tool.listen.resolve = null; + this.tool.listen.reject = null; + } + } + } + this.ignoredUserIdList = []; + } + + // wip + getMessages(): RequestMessage[] { + return []; + } + + // wip + getImageMessages(): RequestMessage[] { + return []; + } + + get toolState(): ToolState { + const { BLOCKED, DEFAULT_CLOSED } = Config.tool; + const tools = Agent.get(this.agentName).tools; + const state: ToolState = {}; + tools.forEach(tool => { + if (BLOCKED.includes(tool)) return; + if (!this.state.hasOwnProperty(tool)) this.state[tool] = !DEFAULT_CLOSED.includes(tool); + state[tool] = this.state[tool]; + }) + return this.tool.state; + } +} + +export class SessionService { + static validKeysMap: { [key in keyof SessionService]?: TypeDescriptor } = { + agentName: 'string', + memory: MemoryService, + sessionMap: { objectValue: Session } + } + agentName: string; + memory: MemoryService; // 全局记忆服务 + sessionMap: { [key: string]: Session }; + + constructor() { + this.agentName = ''; + this.sessionMap = {}; + } + + getSession(sessionId: string): Session { + if (!this.sessionMap.hasOwnProperty(sessionId)) { + let session = new Session(); + try { + const data = JSON.parse(Config.ext.storageGet(`session_${sessionId}`) || '{}'); + session = revive(Session, data); + } catch (error) { + logger.error(`加载会话${sessionId}失败: ${error}`); + } + session.sessionId = sessionId; + if (sessionId.startsWith('QQ:')) session.sessionType = 'user'; + session.agentName = this.agentName; + this.sessionMap[sessionId] = session; + } + return this.sessionMap[sessionId]; + } + + get knowledge(): KnowledgeService { + if (!knowledgeServiceMap.hasOwnProperty(this.agentName)) return knowledgeServiceMap['*']; + return knowledgeServiceMap[this.agentName]; + } +} \ No newline at end of file diff --git a/src/session/types.ts b/src/session/types.ts new file mode 100644 index 0000000..94be47d --- /dev/null +++ b/src/session/types.ts @@ -0,0 +1,44 @@ +import { ToolCall } from "../tool/types"; + +export interface BaseMessageItem { + time: number; // 秒 + text: string; +} + +export interface UserMessageItem extends BaseMessageItem { + userId: string; + messageId: string; +} + +export interface AssistantMessageItem extends BaseMessageItem { + messageId: string; +} + +export interface SystemUserMessageItem extends BaseMessageItem { + tip: string; +} + +export interface ToolCallsMessageItem extends BaseMessageItem { + tool_calls: ToolCall[]; +} + +export interface ToolCallbackMessageItem extends BaseMessageItem { + tool_call_id: string; +} + +export type MessageItem = UserMessageItem | AssistantMessageItem | SystemUserMessageItem | ToolCallsMessageItem | ToolCallbackMessageItem; + +export interface State { + description: string; // 自定义描述 + impression: string; // ai可修改的印象 + [key: string]: any; +} + +export interface RequestMessage { + role: 'user' | 'assistant' | 'system' | 'tool'; + content?: string; + tool_calls?: ToolCall[]; + tool_call_id?: string; +} + +export type SessionType = 'user' | 'group'; \ No newline at end of file diff --git a/src/session/user.ts b/src/session/user.ts new file mode 100644 index 0000000..ee586b6 --- /dev/null +++ b/src/session/user.ts @@ -0,0 +1,32 @@ +import { Config } from "../config/config"; +import { logger } from "../logger"; +import { revive, TypeDescriptor } from "../utils/utils"; + +export default class User { + static validKeysMap: { [key in keyof User]?: TypeDescriptor } = { + userId: 'string', + userName: 'string' + } + userId: string; + userName: string; + + static userMap: { [key: string]: User }; + + static get(userId: string): User { + if (!this.userMap.hasOwnProperty(userId)) { + let user = new User(); + try { + const data = JSON.parse(Config.ext.storageGet(`user_${userId}`) || '{}'); + user = revive(User, data); + } catch (error) { + logger.error(`加载用户${userId}失败: ${error}`); + } + user.userId = userId; + this.userMap[userId] = user; + } + return this.userMap[userId]; + } + static save(user: User) { + Config.ext.storageSet(`user_${user.userId}`, JSON.stringify(user)); + } +} \ No newline at end of file diff --git a/src/timer.ts b/src/timer.ts index 791683d..e949cc2 100644 --- a/src/timer.ts +++ b/src/timer.ts @@ -1,8 +1,8 @@ -import { ConfigManager } from "./config/configManager"; -import { getSessionCtxAndMsg } from "./utils/utils_seal"; +import { Config } from "./config/config"; +import { getSessionCtxAndMsg } from "./utils/seal"; import { AI, AIManager } from "./AI/AI"; import { logger } from "./logger"; -import { fmtDate } from "./utils/utils_string"; +import { fmtDate } from "./utils/string"; import { revive } from "./utils/utils"; export class TimerInfo { @@ -37,7 +37,7 @@ export class TimerManager { static getTimerQueue() { try { - const data = JSON.parse(ConfigManager.ext.storageGet(`timerQueue`) || '[]') + const data = JSON.parse(Config.ext.storageGet(`timerQueue`) || '[]') if (!Array.isArray(data)) throw new Error('timerQueue不是数组'); data.forEach((item: any) => { if (!item.hasOwnProperty('sessionId')) return; @@ -50,7 +50,7 @@ export class TimerManager { } static saveTimerQueue() { - ConfigManager.ext.storageSet(`timerQueue`, JSON.stringify(this.timerQueue)); + Config.ext.storageSet(`timerQueue`, JSON.stringify(this.timerQueue)); } static addTargetTimer(ctx: seal.MsgContext, ai: AI, target: number, content: string) { diff --git a/src/tool/sample.ts b/src/tool/sample.ts deleted file mode 100644 index 1863d3c..0000000 --- a/src/tool/sample.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Tool } from "./tool"; - -export function registerSample() { - const tool = new Tool({ - type: "function", - function: { - name: "sample", - description: `示例工具`, - parameters: { - type: "object", - properties: { - arg: { - type: 'string', - description: '参数' - } - }, - required: ["arg"] - } - } - }); - tool.solve = async (ctx, msg, ai, args) => { - const { arg } = args; - arg; ctx; msg; ai; - return { content: "调用成功", images: [] }; - } -} \ No newline at end of file diff --git a/src/tool/tool.ts b/src/tool/tool.ts index 299f30b..d284235 100644 --- a/src/tool/tool.ts +++ b/src/tool/tool.ts @@ -1,221 +1,48 @@ -import { AI } from "../AI/AI" -import { ConfigManager } from "../config/configManager" -import { registerAttr } from "./tool_attr" -import { registerBan } from "./tool_ban" -import { registerDeck } from "./tool_deck" -import { registerImage } from "./tool_image" -import { registerJrrp } from "./tool_jrrp" -import { registerMemory } from "./tool_memory" -import { registerModu } from "./tool_modu" -import { registerRename } from "./tool_rename" -import { registerRollCheck } from "./tool_roll_check" -import { registerTime } from "./tool_time" -import { registerRecord } from "./tool_voice" -import { registerWeb } from "./tool_web" -import { registerGroupSign } from "./tool_group_sign" -import { registerGetPersonInfo } from "./tool_person_info" -import { registerMessage } from "./tool_message" -import { registerEssenceMsg } from "./tool_essence_msg" -import { registerContext } from "./tool_context" -import { registerQQList } from "./tool_qq_list" -import { registerSetTrigger } from "./tool_trigger" -import { registerMusicPlay } from "./tool_music" -import { registerMeme } from "./tool_meme" -import { registerRender } from "./tool_render" +import { Config } from "../config/config" import { logger } from "../logger" -import { Image } from "../AI/image"; -import { fixJsonString } from "../utils/utils_string"; - -export interface ToolInfoString { - type: "string"; - description?: string; - enum?: string[]; - minLength?: number; - maxLength?: number; - pattern?: string; - format?: "date-time" | "email" | "uri" | "uuid" | "hostname" | "ipv4" | "ipv6"; -} - -export interface ToolInfoNumber { - type: "number"; - description?: string; - minimum?: number; - maximum?: number; - exclusiveMinimum?: number; - exclusiveMaximum?: number; - multipleOf?: number; -} - -export interface ToolInfoInteger { - type: "integer"; - description?: string; - minimum?: number; - maximum?: number; - exclusiveMinimum?: number; - exclusiveMaximum?: number; - multipleOf?: number; -} - -export interface ToolInfoBoolean { - type: "boolean"; - description?: string; -} - -export interface ToolInfoNull { - type: "null"; - description?: string; -} - -export interface ToolInfoArray { - type: "array"; - description?: string; - items: ToolInfoItem; - minItems?: number; - maxItems?: number; - uniqueItems?: boolean; -} - -export interface ToolInfoObject { - type: "object"; - description?: string; - properties?: { - [key: string]: ToolInfoItem; - }; - required?: (keyof ToolInfoObject["properties"])[]; - additionalProperties?: boolean | ToolInfoItem; - minProperties?: number; - maxProperties?: number; +import { fixJsonString } from "../utils/string"; +import { ExtCmdInfo, ToolCall, ToolCallResult, ToolInfo, ToolListen } from "./types"; +import { Session } from "../session/session"; +import { SessionType } from "../session/types"; +import builtinCmdToolMap from "./tools/builtin_cmd.ts/init"; + +export const toolMap = { + ...builtinCmdToolMap, } -export type ToolInfoItem = - | ToolInfoString - | ToolInfoNumber - | ToolInfoInteger - | ToolInfoBoolean - | ToolInfoNull - | ToolInfoArray - | ToolInfoObject; - -export interface ToolInfo { - type: "function", - function: { - name: string, - description: string, - parameters: ToolInfoObject - } -} - -export interface ToolCall { - index: number, - id: string, - type: "function", - function: { - name: string, - arguments: string - } -} - -export interface CmdInfo { - ext: string, // 使用的扩展名称 - name: string, // 指令名称 - fixedArgs: string[] // 参数 -} +export type ToolName = keyof typeof toolMap; +export type ToolState = { [key in ToolName]?: boolean }; export class Tool { - info: ToolInfo; - cmdInfo: CmdInfo; // 海豹指令信息 - type: string; // 可使用函数的聊天场景类型:"private" | "group" | "all" - tool_choice: string; // 是否可以继续调用函数:"none" | "auto" | "required" - solve: (ctx: seal.MsgContext, msg: seal.Message, ai: AI, args: { [key: string]: any }) => Promise<{ content: string, images: Image[] }>; + toolInfo: ToolInfo; + ExtCmdInfo: ExtCmdInfo; // 海豹指令信息 + sessionType: 'any' | SessionType; // 可使用函数的会话类型 + callBack: boolean; // 是否回调智能体 + solve: (ctx: seal.MsgContext, msg: seal.Message, session: Session, args: { [key: string]: any }) => Promise; constructor(info: ToolInfo) { - this.info = info; - this.cmdInfo = { - ext: '', - name: '', - fixedArgs: [] + this.toolInfo = info; + this.ExtCmdInfo = { + extName: '', + cmd: '', + staticArgs: [] } - this.type = "all" - this.tool_choice = 'auto'; - this.solve = async (_, __, ___, ____) => ({ content: "函数未实现", images: [] }); + this.sessionType = "any"; + this.callBack = true; + this.solve = async (_, __, ___, ____) => "函数未实现"; - ToolManager.toolMap[info.function.name] = this; + toolMap[info.function.name] = this; } -} -export class ToolManager { - static validKeys: (keyof ToolManager)[] = ['toolStatus']; static cmdArgs: seal.CmdArgs = null; - static toolMap: { [key: string]: Tool } = {}; - toolStatus: { [key: string]: boolean }; - toolCallCount: number; // 一次性调用函数计数 - - // 监听调用函数发送的内容 - listen: { - timeoutId: number, - resolve: (content: string) => void, - reject: (err: Error) => void, - cleanup: () => void - } - - constructor() { - const { toolsNotAllow, toolsDefaultClosed } = ConfigManager.tool; - this.toolStatus = Object.keys(ToolManager.toolMap).reduce((acc, key) => { - acc[key] = !toolsNotAllow.includes(key) && !toolsDefaultClosed.includes(key); - return acc; - }, {}); - this.toolCallCount = 0; - - this.listen = { - timeoutId: null, - resolve: null, - reject: null, - cleanup: () => { - if (this.listen.timeoutId) { - clearTimeout(this.listen.timeoutId); - } - - this.listen.timeoutId = null; - this.listen.resolve = null; - this.listen.reject = null; - } - }; - } - - static registerTool() { - registerMemory(); - registerDeck(); - registerJrrp(); - registerModu(); - registerRollCheck(); - registerRename(); - registerAttr(); - registerBan(); - registerRecord(); - registerTime(); - registerWeb(); - registerImage(); - registerGroupSign(); - registerGetPersonInfo(); - registerMessage(); - registerEssenceMsg(); - registerContext(); - registerQQList(); - registerSetTrigger(); - registerMusicPlay(); - registerMeme(); - registerRender(); - } /** - * 利用预存的指令信息和额外输入的参数构建一个cmdArgs, 并调用solve函数 - * @param cmdArgs - * @param args + * 利用预存的指令信息和额外输入的参数构建一个cmdArgs并调用solve函数,监听消息并返回结果 */ - static async extensionSolve(ctx: seal.MsgContext, msg: seal.Message, ai: AI, cmdInfo: CmdInfo, args: string[], kwargs: seal.Kwarg[], at: seal.AtInfo[]): Promise<[string, boolean]> { + static async extensionSolve(ctx: seal.MsgContext, msg: seal.Message, listen: ToolListen, eci: ExtCmdInfo, args: string[], kwargs: seal.Kwarg[], at: seal.AtInfo[]): Promise<[string, boolean]> { const cmdArgs = this.cmdArgs; - cmdArgs.command = cmdInfo.name; - cmdArgs.args = cmdInfo.fixedArgs.concat(args); + cmdArgs.command = eci.cmd; + cmdArgs.args = eci.staticArgs.concat(args); cmdArgs.kwargs = kwargs; cmdArgs.at = at; cmdArgs.rawArgs = `${cmdArgs.args.join(' ')} ${kwargs.map(item => `--${item.name}${item.valueExists ? `=${item.value}` : ``}`).join(' ')}`; @@ -225,38 +52,35 @@ export class ToolManager { cmdArgs.specialExecuteTimes = 0; cmdArgs.rawText = `.${cmdArgs.command} ${cmdArgs.rawArgs} ${at.map(item => `[CQ:at,qq=${item.userId.replace(/^.+:/, '')}]`).join(' ')}`; - const ext = seal.ext.find(cmdInfo.ext); - if (!ext.cmdMap.hasOwnProperty(cmdInfo.name)) { - logger.warning(`扩展${cmdInfo.ext}中未找到指令:${cmdInfo.name}`); + const ext = seal.ext.find(eci.extName); + if (!ext.cmdMap.hasOwnProperty(eci.cmd)) { + logger.warning(`扩展${eci.extName}中未找到指令:${eci.cmd}`); return ['', false]; } - ai.tool.listen.reject?.(new Error('中断当前监听')); + listen.reject?.(new Error('中断当前监听')); return new Promise(( resolve: (result: [string, boolean]) => void, reject: (err: Error) => void ) => { - ai.tool.listen.timeoutId = setTimeout(() => { + listen.timeoutId = setTimeout(() => { reject(new Error('监听消息超时')); - ai.tool.listen.cleanup(); + listen.cleanup(); }, 10 * 1000); - - ai.tool.listen.resolve = (content: string) => { + listen.resolve = (content: string) => { resolve([content, true]); - ai.tool.listen.cleanup(); + listen.cleanup(); }; - - ai.tool.listen.reject = (err: Error) => { + listen.reject = (err: Error) => { reject(err); - ai.tool.listen.cleanup(); + listen.cleanup(); }; - try { - ext.cmdMap[cmdInfo.name].solve(ctx, msg, cmdArgs); + ext.cmdMap[eci.cmd].solve(ctx, msg, cmdArgs); } catch (err) { reject(new Error(`solve中发生错误:${err.message}`)); - ai.tool.listen.cleanup(); + listen.cleanup(); } }).catch((err) => { logger.error(`在extensionSolve中: 调用函数失败:${err.message}`); @@ -264,90 +88,27 @@ export class ToolManager { }); } - /** - * 调用函数并返回tool_choice - * @param ctx - * @param msg - * @param ai - * @param tool_calls - * @returns tool_choice - */ - static async handleToolCalls(ctx: seal.MsgContext, msg: seal.Message, ai: AI, tool_calls: ToolCall[]): Promise { - const { maxCallCount } = ConfigManager.tool; - - if (tool_calls.length !== 0) { - logger.info(`调用函数:`, tool_calls.map((item, i) => { - return `(${i}) ${item.function.name}:${item.function.arguments}`; - }).join('\n')); - } - - if (tool_calls.length + ai.tool.toolCallCount > maxCallCount) { - logger.warning('一次性调用超过上限,将进行截断操作……'); - tool_calls.splice(Math.max(0, maxCallCount - ai.tool.toolCallCount)); - } - - ai.tool.toolCallCount += tool_calls.length; - if (ai.tool.toolCallCount === maxCallCount) { - logger.warning('连续调用函数次数达到上限'); - } else if (ai.tool.toolCallCount === maxCallCount + tool_calls.length) { - logger.warning('连续调用函数次数超过上限'); - for (let i = 0; i < tool_calls.length; i++) { - const tool_call = tool_calls[i]; - await ai.context.addToolMessage(tool_call.id, `连续调用函数次数超过上限`, []); - ai.tool.toolCallCount++; - } - return "none"; - } else if (ai.tool.toolCallCount > maxCallCount + tool_calls.length) { - throw new Error('连续调用函数次数超过上限,已终止对话'); - } - - let tool_choice = 'none'; - for (let i = 0; i < tool_calls.length; i++) { - const tool_call = tool_calls[i]; - const tool_choice2 = await this.handleToolCall(ctx, msg, ai, tool_call); - - if (tool_choice2 === 'required') { - tool_choice = 'required'; - } else if (tool_choice === 'none' && tool_choice2 === 'auto') { - tool_choice = 'auto'; - } - } - - return tool_choice; - } - - static async handleToolCall(ctx: seal.MsgContext, msg: seal.Message, ai: AI, tool_call: { - index: number, - id: string, - type: "function", - function: { - name: string, - arguments: string - } - }): Promise { + static async handleToolCall(ctx: seal.MsgContext, msg: seal.Message, session: Session, tool_call: ToolCall): Promise<{ result: ToolCallResult, callBack: boolean }> { const name = tool_call.function.name; - if (ConfigManager.tool.toolsNotAllow.includes(name)) { - logger.warning(`调用函数失败:禁止调用的函数:${name}`); - await ai.context.addToolMessage(tool_call.id, `调用函数失败:禁止调用的函数:${name}`, []); - return "none"; - } - if (!this.toolMap.hasOwnProperty(name)) { + if (!toolMap.hasOwnProperty(name)) { logger.warning(`调用函数失败:未注册的函数:${name}`); - await ai.context.addToolMessage(tool_call.id, `调用函数失败:未注册的函数:${name}`, []); - return "none"; + return { result: { tool_call_id: tool_call.id, content: `调用函数失败:未注册的函数:${name}` }, callBack: true }; + } + if (session.toolState?.[name]) { + logger.warning(`调用函数失败:未经许可的函数:${name}`); + return { result: { tool_call_id: tool_call.id, content: `调用函数失败:未经许可的函数:${name}` }, callBack: true }; } - - const tool = this.toolMap[name]; - if (tool.cmdInfo.ext !== '' && this.cmdArgs == null) { + const tool = toolMap[name]; + if (tool.ExtCmdInfo.extName !== '' && this.cmdArgs === null) { logger.warning(`暂时无法调用函数,请先使用 .r 指令`); - await ai.context.addToolMessage(tool_call.id, `暂时无法调用函数,请先提示用户使用 .r 指令`, []); - return "none"; + return { result: { tool_call_id: tool_call.id, content: `暂时无法调用函数,请先提示用户使用 .r 指令` }, callBack: true }; } - if (tool.type !== "all" && tool.type !== msg.messageType) { - logger.warning(`调用函数失败:函数${name}可使用的场景类型为${tool.type},当前场景类型为${msg.messageType}`); - await ai.context.addToolMessage(tool_call.id, `调用函数失败:函数${name}可使用的场景类型为${tool.type},当前场景类型为${msg.messageType}`, []); - return "none"; + + const msgType = msg.messageType === 'private' ? 'user' : 'group'; + if (tool.sessionType !== "any" && tool.sessionType !== msgType) { + logger.warning(`调用函数失败:函数${name}可使用的场景类型为${tool.sessionType},当前场景类型为${msgType}`); + return { result: { tool_call_id: tool_call.id, content: `调用函数失败:函数${name}可使用的场景类型为${tool.sessionType},当前场景类型为${msgType}` }, callBack: true }; } let args = null; @@ -357,15 +118,13 @@ export class ToolManager { const fixedStr = fixJsonString(tool_call.function.arguments); if (fixedStr === '') { logger.error(`调用函数 (${name}:${tool_call.function.arguments}) 失败:${e.message}`); - await ai.context.addToolMessage(tool_call.id, `调用函数 (${name}:${tool_call.function.arguments}) 失败:${e.message}`, []); - return "none"; + return { result: { tool_call_id: tool_call.id, content: `调用函数 (${name}:${tool_call.function.arguments}) 失败:${e.message}` }, callBack: true }; } try { args = JSON.parse(fixedStr); } catch (e) { logger.error(`调用函数 (${name}:${tool_call.function.arguments}) 失败:${e.message}`); - await ai.context.addToolMessage(tool_call.id, `调用函数 (${name}:${tool_call.function.arguments}) 失败:${e.message}`, []); - return "none"; + return { result: { tool_call_id: tool_call.id, content: `调用函数 (${name}:${tool_call.function.arguments}) 失败:${e.message}` }, callBack: true }; } } @@ -373,179 +132,103 @@ export class ToolManager { try { if (args !== null && typeof args !== 'object') { logger.warning(`调用函数失败:arguement不是一个object`); - await ai.context.addToolMessage(tool_call.id, `调用函数失败:arguement不是一个object`, []); - return "auto"; + return { result: { tool_call_id: tool_call.id, content: `调用函数失败:arguement不是一个object` }, callBack: true }; } - for (const key of tool.info.function.parameters.required) { + for (const key of tool.toolInfo.function.parameters.required) { if (!args.hasOwnProperty(key)) { logger.warning(`调用函数失败:缺少必需参数 ${key}`); - await ai.context.addToolMessage(tool_call.id, `调用函数失败:缺少必需参数 ${key}`, []); - return "auto"; + return { result: { tool_call_id: tool_call.id, content: `调用函数失败:缺少必需参数 ${key}` }, callBack: true }; } } - const { content, images } = await tool.solve(ctx, msg, ai, args); - await ai.context.addToolMessage(tool_call.id, content, images); - return tool.tool_choice; + const content = await tool.solve(ctx, msg, session, args); + return { result: { tool_call_id: tool_call.id, content }, callBack: true }; } catch (e) { logger.error(`调用函数 (${name}:${tool_call.function.arguments}) 失败:${e.message}`); - await ai.context.addToolMessage(tool_call.id, `调用函数 (${name}:${tool_call.function.arguments}) 失败:${e.message}`, []); - return "none"; + return { result: { tool_call_id: tool_call.id, content: `调用函数 (${name}:${tool_call.function.arguments}) 失败:${e.message}` }, callBack: true }; } } + static async handleToolCalls(ctx: seal.MsgContext, msg: seal.Message, session: Session, tool_calls: ToolCall[]): Promise<{ result: ToolCallResult[], callBack: boolean }> { + const { MAX_CALL_COUNT } = Config.tool; - static async handlePromptToolCall(ctx: seal.MsgContext, msg: seal.Message, ai: AI, tool_call_str: string): Promise { - const { maxCallCount } = ConfigManager.tool; + const ret = { result: [], callBack: true }; - ai.tool.toolCallCount++; - if (ai.tool.toolCallCount === maxCallCount) { - logger.warning('连续调用函数次数达到上限'); - } else if (ai.tool.toolCallCount === maxCallCount + 1) { - logger.warning('连续调用函数次数超过上限'); - await ai.context.addSystemUserMessage('调用函数返回', `连续调用函数次数超过上限`, []); - return; - } else if (ai.tool.toolCallCount > maxCallCount + 1) { - throw new Error('连续调用函数次数超过上限,已终止对话'); - } - - let tool_call: { - name: string, - arguments: { - [key: string]: any - } - } = null; - - try { - tool_call = JSON.parse(tool_call_str); - } catch (e) { - const fixedStr = fixJsonString(tool_call_str); - if (fixedStr === '') { - logger.error('解析tool_call时出现错误:', e); - await ai.context.addSystemUserMessage('调用函数返回', `解析tool_call时出现错误:${e.message}`, []); - return; - } - try { - tool_call = JSON.parse(fixedStr); - } catch (e) { - logger.error('解析tool_call时出现错误:', e); - await ai.context.addSystemUserMessage('调用函数返回', `解析tool_call时出现错误:${e.message}`, []); - return; + for (let i = 0; i < tool_calls.length; i++) { + const tool_call = tool_calls[i]; + if (session.tool.callCount > MAX_CALL_COUNT) { + logger.warning('工具调用超过上限'); + ret.result.push({ + tool_call_id: tool_call.id, + content: '工具调用超过上限' + }); + ret.callBack = false; + continue; } + const { result, callBack } = await this.handleToolCall(ctx, msg, session, tool_call); + ret.result.push(result); + ret.callBack = ret.callBack && callBack; + session.tool.callCount++; } - if (!tool_call.hasOwnProperty('name') || !tool_call.hasOwnProperty('arguments')) { - logger.warning(`调用函数失败:缺少name或arguments`); - await ai.context.addSystemUserMessage('调用函数返回', `调用函数失败:缺少name或arguments`, []); - return; - } - - const name = tool_call.name; - if (ConfigManager.tool.toolsNotAllow.includes(name)) { - logger.warning(`调用函数失败:禁止调用的函数:${name}`); - await ai.context.addSystemUserMessage('调用函数返回', `调用函数失败:禁止调用的函数:${name}`, []); - return; - } - if (!this.toolMap.hasOwnProperty(name)) { - logger.warning(`调用函数失败:未注册的函数:${name}`); - await ai.context.addSystemUserMessage('调用函数返回', `调用函数失败:未注册的函数:${name}`, []); - return; - } - - - const tool = this.toolMap[name]; - if (tool.cmdInfo.ext !== '' && this.cmdArgs == null) { - logger.warning(`暂时无法调用函数,请先使用 .r 指令`); - await ai.context.addSystemUserMessage('调用函数返回', `暂时无法调用函数,请先提示用户使用 .r 指令`, []); - return; - } - if (tool.type !== "all" && tool.type !== msg.messageType) { - logger.warning(`调用函数失败:函数${name}可使用的场景类型为${tool.type},当前场景类型为${msg.messageType}`); - await ai.context.addSystemUserMessage('调用函数返回', `调用函数失败:函数${name}可使用的场景类型为${tool.type},当前场景类型为${msg.messageType}`, []); - return; - } - + return ret; + } + static async handlePromptToolCalls(ctx: seal.MsgContext, msg: seal.Message, session: Session, toolCallStr: string): Promise<{ result: ToolCallResult[], callBack: boolean }> { try { - const args = tool_call.arguments; - if (args !== null && typeof args !== 'object') { - logger.warning(`调用函数失败:arguement不是一个object`); - await ai.context.addSystemUserMessage('调用函数返回', `调用函数失败:arguement不是一个object`, []); - return; - } - for (const key of tool.info.function.parameters.required) { - if (!args.hasOwnProperty(key)) { - logger.warning(`调用函数失败:缺少必需参数 ${key}`); - await ai.context.addSystemUserMessage('调用函数返回', `调用函数失败:缺少必需参数 ${key}`, []); - return; - } + const data = JSON.parse(toolCallStr); + if (!Array.isArray(data)) { + logger.warning(`解析函数调用失败:tool_calls不是一个数组`); + return { result: [{ tool_call_id: '', content: `解析函数调用失败:tool_calls不是一个数组` }], callBack: true }; } - - const { content, images } = await tool.solve(ctx, msg, ai, args); - await ai.context.addSystemUserMessage('调用函数返回', content, images); + const tool_calls = data.map((item, index) => { + if (!item.hasOwnProperty('name') || !item.hasOwnProperty('arguments')) throw new Error(`缺少name或arguments属性`); + if (typeof item.name !== 'string' || typeof item.arguments !== 'string') throw new Error(`name或arguments不是字符串`); + return { + index: index, + id: index.toString(), + type: "function" as const, + function: { + name: item.name, + arguments: item.arguments + } + }; + }); + return await this.handleToolCalls(ctx, msg, session, tool_calls); } catch (e) { - logger.error(`调用函数 (${name}:${JSON.stringify(tool_call.arguments, null, 2)}) 失败:${e.message}`); - await ai.context.addSystemUserMessage('调用函数返回', `调用函数 (${name}:${JSON.stringify(tool_call.arguments, null, 2)}) 失败:${e.message}`, []); - } - } - - reviveToolStauts() { - const { toolsNotAllow, toolsDefaultClosed } = ConfigManager.tool; - const toolStatus: { [key: string]: boolean } = {}; - for (const k in ToolManager.toolMap) { - if (!this.toolStatus.hasOwnProperty(k)) { - toolStatus[k] = !toolsNotAllow.includes(k) && !toolsDefaultClosed.includes(k); - } else if (toolsNotAllow.includes(k)) { - toolStatus[k] = false; - } else { - toolStatus[k] = this.toolStatus[k]; - } + logger.error(`解析函数调用失败:${e.message}`); + return { result: [{ tool_call_id: '', content: `解析函数调用失败:${e.message}` }], callBack: true }; } - this.toolStatus = toolStatus; } - getToolsInfo(type: string): ToolInfo[] { - if (type !== "private" && type !== "group") { - type = "all"; - } - - const tools = Object.keys(this.toolStatus) + static getToolsInfo(session: Session): ToolInfo[] | null { + const toolState = session.toolState; + const sessionType = session.sessionType; + const tools = Object.keys(toolState) .map(key => { - if (this.toolStatus[key]) { - if (!ToolManager.toolMap.hasOwnProperty(key)) { - logger.error(`在getToolsInfo中找不到工具:${key}`); - return null; - } - const tool = ToolManager.toolMap[key]; - if (tool.type !== "all" && tool.type !== type) { + if (toolState[key]) { + if (!toolMap.hasOwnProperty(key)) { + logger.warning(`在getToolsInfo中找不到工具:${key}`); return null; } - return tool.info; + const tool: Tool = toolMap[key]; + if (tool.sessionType !== "any" && tool.sessionType !== sessionType) return null; + return tool.toolInfo; } else { return null; } }) .filter(item => item !== null); - if (tools.length === 0) { - return null; - } else { - return tools; - } + return tools.length > 0 ? tools : null; } + static getToolsInfoPrompt(session: Session): string { + const { PROMPT_ENGINEERING, TOOLS_PROMPT_TEMPLATE } = Config.tool; - getToolsPrompt(ctx: seal.MsgContext): string { - const { toolsPromptTemplate } = ConfigManager.tool; - - const tools = this.getToolsInfo(ctx.isPrivate ? 'private' : 'group'); + const tools = this.getToolsInfo(session); if (tools && tools.length > 0) { - return tools.map((item, index) => { - return toolsPromptTemplate({ - "序号": index + 1, - "函数名称": item.function.name, - "函数描述": item.function.description, - "参数信息": JSON.stringify(item.function.parameters.properties, null, 2), - "必需参数": item.function.parameters.required.join('\n') - }); - }).join('\n'); + return TOOLS_PROMPT_TEMPLATE({ + "PROMPT_ENGINEERING": PROMPT_ENGINEERING, + "tools": tools + }); } return ''; diff --git a/src/tool/tool_jrrp.ts b/src/tool/tool_jrrp.ts deleted file mode 100644 index cb614f2..0000000 --- a/src/tool/tool_jrrp.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { ConfigManager } from "../config/configManager"; -import { getCtxAndMsg } from "../utils/utils_seal"; -import { Tool, ToolManager } from "./tool"; - -export function registerJrrp() { - const tool = new Tool({ - type: "function", - function: { - name: "jrrp", - description: `查看指定用户的今日人品`, - parameters: { - type: "object", - properties: { - name: { - type: 'string', - description: '用户名称' + (ConfigManager.message.showNumber ? '或纯数字QQ号' : '') - } - }, - required: ["name"] - } - } - }); - tool.cmdInfo = { - ext: 'fun', - name: 'jrrp', - fixedArgs: [] - } - tool.solve = async (ctx, msg, ai, args) => { - const { name } = args; - - const ui = await ai.context.findUserInfo(ctx, name); - if (ui === null) return { content: `未找到<${name}>`, images: [] }; - - ({ ctx, msg } = getCtxAndMsg(ctx.endPoint.userId, ui.id, ctx.group.groupId)); - const [s, success] = await ToolManager.extensionSolve(ctx, msg, ai, tool.cmdInfo, [], [], []); - if (!success) return { content: '今日人品查询失败', images: [] }; - - return { content: s, images: [] }; - } -} \ No newline at end of file diff --git a/src/tool/tools/builtin_cmd.ts/init.ts b/src/tool/tools/builtin_cmd.ts/init.ts new file mode 100644 index 0000000..44e760d --- /dev/null +++ b/src/tool/tools/builtin_cmd.ts/init.ts @@ -0,0 +1,5 @@ +import { toolJrrp } from "./jrrp"; + +export default { + jrrp: toolJrrp, +} \ No newline at end of file diff --git a/src/tool/tools/builtin_cmd.ts/jrrp.ts b/src/tool/tools/builtin_cmd.ts/jrrp.ts new file mode 100644 index 0000000..b6dbe79 --- /dev/null +++ b/src/tool/tools/builtin_cmd.ts/jrrp.ts @@ -0,0 +1,39 @@ +import { getCtxAndMsg } from "../../../utils/seal"; +import { Tool } from "../../tool"; + +const tool = new Tool({ + type: "function", + function: { + name: "jrrp", + description: `查看指定用户的今日人品`, + parameters: { + type: "object", + properties: { + name: { + type: 'string', + description: '用户名称或纯数字QQ号' + } + }, + required: ["name"] + } + } +}); +tool.ExtCmdInfo = { + extName: 'fun', + cmd: 'jrrp', + staticArgs: [] +} +tool.solve = async (ctx, msg, session, args) => { + const { name } = args; + + const uid = await session.findUserId(ctx, name); + if (uid === '') return `未找到<${name}>`; + + ({ ctx, msg } = getCtxAndMsg(ctx.endPoint.userId, uid, ctx.group.groupId)); + const [s, success] = await Tool.extensionSolve(ctx, msg, session.tool.listen, tool.ExtCmdInfo, [], [], []); + if (!success) return '今日人品查询失败'; + + return s; +} + +export { tool as toolJrrp } \ No newline at end of file diff --git a/src/tool/tool_attr.ts b/src/tool/tools/builtin_cmd.ts/tool_attr.ts similarity index 96% rename from src/tool/tool_attr.ts rename to src/tool/tools/builtin_cmd.ts/tool_attr.ts index db50000..2e07f29 100644 --- a/src/tool/tool_attr.ts +++ b/src/tool/tools/builtin_cmd.ts/tool_attr.ts @@ -20,10 +20,10 @@ export function registerAttr() { } } }); - toolShow.cmdInfo = { - ext: 'coc7', - name: 'st', - fixedArgs: ['show'] + toolShow.ExtCmdInfo = { + extName: 'coc7', + cmd: 'st', + staticArgs: ['show'] } toolShow.solve = async (ctx, msg, ai, args) => { const { name } = args; @@ -33,7 +33,7 @@ export function registerAttr() { ({ ctx, msg } = getCtxAndMsg(ctx.endPoint.userId, ui.id, ctx.group.groupId)); - const [s, success] = await ToolManager.extensionSolve(ctx, msg, ai, toolShow.cmdInfo, [], [], []); + const [s, success] = await ToolManager.extensionSolve(ctx, msg, ai, toolShow.ExtCmdInfo, [], [], []); if (!success) return { content: '展示失败', images: [] }; return { content: s, images: [] }; diff --git a/src/tool/tool_modu.ts b/src/tool/tools/builtin_cmd.ts/tool_modu.ts similarity index 82% rename from src/tool/tool_modu.ts rename to src/tool/tools/builtin_cmd.ts/tool_modu.ts index 0ed9e39..47b2c6e 100644 --- a/src/tool/tool_modu.ts +++ b/src/tool/tools/builtin_cmd.ts/tool_modu.ts @@ -13,13 +13,13 @@ export function registerModu() { } } }); - toolRoll.cmdInfo = { - ext: 'story', - name: 'modu', - fixedArgs: ['roll'] + toolRoll.ExtCmdInfo = { + extName: 'story', + cmd: 'modu', + staticArgs: ['roll'] } toolRoll.solve = async (ctx, msg, ai, _) => { - const [s, success] = await ToolManager.extensionSolve(ctx, msg, ai, toolRoll.cmdInfo, [], [], []); + const [s, success] = await ToolManager.extensionSolve(ctx, msg, ai, toolRoll.ExtCmdInfo, [], [], []); if (!success) { return { content: '今日人品查询失败', images: [] }; } @@ -44,15 +44,15 @@ export function registerModu() { } } }); - toolSearch.cmdInfo = { - ext: 'story', - name: 'modu', - fixedArgs: ['search'] + toolSearch.ExtCmdInfo = { + extName: 'story', + cmd: 'modu', + staticArgs: ['search'] } toolSearch.solve = async (ctx, msg, ai, args) => { const { name } = args; - const [s, success] = await ToolManager.extensionSolve(ctx, msg, ai, toolSearch.cmdInfo, [name], [], []); + const [s, success] = await ToolManager.extensionSolve(ctx, msg, ai, toolSearch.ExtCmdInfo, [name], [], []); if (!success) { return { content: '今日人品查询失败', images: [] }; } diff --git a/src/tool/tool_roll_check.ts b/src/tool/tools/builtin_cmd.ts/tool_roll_check.ts similarity index 94% rename from src/tool/tool_roll_check.ts rename to src/tool/tools/builtin_cmd.ts/tool_roll_check.ts index d630cfa..79b96fd 100644 --- a/src/tool/tool_roll_check.ts +++ b/src/tool/tools/builtin_cmd.ts/tool_roll_check.ts @@ -41,10 +41,10 @@ export function registerRollCheck() { } } }); - toolRoll.cmdInfo = { - ext: 'coc7', - name: 'ra', - fixedArgs: [] + toolRoll.ExtCmdInfo = { + extName: 'coc7', + cmd: 'ra', + staticArgs: [] } toolRoll.solve = async (ctx, msg, ai, args) => { const { name, expression, rank = '', times = 1, additional_dice = '', reason = '' } = args; @@ -68,7 +68,7 @@ export function registerRollCheck() { if (parseInt(times) !== 1 && !isNaN(parseInt(times))) ToolManager.cmdArgs.specialExecuteTimes = parseInt(times); - const [s, success] = await ToolManager.extensionSolve(ctx, msg, ai, toolRoll.cmdInfo, args2, [], []); + const [s, success] = await ToolManager.extensionSolve(ctx, msg, ai, toolRoll.ExtCmdInfo, args2, [], []); ToolManager.cmdArgs.specialExecuteTimes = 1; if (!success) return { content: '检定执行失败', images: [] }; return { content: s, images: [] }; @@ -103,10 +103,10 @@ export function registerRollCheck() { } } }) - tool.cmdInfo = { - ext: 'coc7', - name: 'sc', - fixedArgs: [] + tool.ExtCmdInfo = { + extName: 'coc7', + cmd: 'sc', + staticArgs: [] } tool.solve = async (ctx, msg, ai, args) => { const { name, expression, additional_dice } = args; @@ -123,7 +123,7 @@ export function registerRollCheck() { if (additional_dice) args2.push(additional_dice); args2.push(expression); - const [s, success] = await ToolManager.extensionSolve(ctx, msg, ai, tool.cmdInfo, args2, [], []); + const [s, success] = await ToolManager.extensionSolve(ctx, msg, ai, tool.ExtCmdInfo, args2, [], []); if (!success) return { content: 'san check执行失败', images: [] }; return { content: s, images: [] }; } diff --git a/src/tool/tool_image.ts b/src/tool/tools/image.ts/tool_image.ts similarity index 93% rename from src/tool/tool_image.ts rename to src/tool/tools/image.ts/tool_image.ts index 4d4698e..708e37b 100644 --- a/src/tool/tool_image.ts +++ b/src/tool/tools/image.ts/tool_image.ts @@ -1,5 +1,5 @@ import { AIManager } from "../AI/AI"; -import { Image } from "../AI/image"; +import { Image } from "../image/image"; import { logger } from "../logger"; import { ConfigManager } from "../config/configManager"; import { Tool } from "./tool"; @@ -87,7 +87,7 @@ export function registerImage() { if (globalThis.aiDrawing && typeof globalThis.aiDrawing.sendImageRequest === 'function') { const result = await globalThis.aiDrawing.sendImageRequest(prompt, negative_prompt); const img = new Image(); - img.id = `${name}_${generateId()}`; + img.imageId = `${name}_${generateId()}`; if (result.startsWith("http://") || result.startsWith("https://")) { try { await img.urlToBase64(); @@ -100,11 +100,11 @@ export function registerImage() { } img.format = img.format || 'unknown'; - img.content = `AI绘图<|img:${img.id}|>\n${prompt ? `描述: ${prompt}` : ''}\n${negative_prompt ? `不希望出现: ${negative_prompt}` : ''}`; + img.description = `AI绘图<|img:${img.imageId}|>\n${prompt ? `描述: ${prompt}` : ''}\n${negative_prompt ? `不希望出现: ${negative_prompt}` : ''}`; - if (save) ai.memory.addMemory(ctx, ai, [], [], kws, [img], img.content); + if (save) ai.memory.addMemory(ctx, ai, [], [], kws, [img], img.description); - return { content: `生成成功,请使用<|img:${img.id}|>发送`, images: [img] }; + return { content: `生成成功,请使用<|img:${img.imageId}|>发送`, images: [img] }; } // 兼容旧版 AIDrawing diff --git a/src/tool/tool_meme.ts b/src/tool/tools/image.ts/tool_meme.ts similarity index 94% rename from src/tool/tool_meme.ts rename to src/tool/tools/image.ts/tool_meme.ts index 2cfef4a..0ffc849 100644 --- a/src/tool/tool_meme.ts +++ b/src/tool/tools/image.ts/tool_meme.ts @@ -1,235 +1,235 @@ -import { AIManager, GroupInfo, UserInfo } from "../AI/AI"; -import { Image, ImageManager } from "../AI/image"; -import { ConfigManager } from "../config/configManager"; -import { logger } from "../logger"; -import { generateId } from "../utils/utils"; -import { Tool } from "./tool"; - -const baseurl = "http://meme.lovesealdice.online/"; - -interface MemeInfo { - params_type: { - min_texts: number, - max_texts: number, - min_images: number, - max_images: number, - } -} - -async function getInfo(name: string): Promise<{ key: string, info: MemeInfo }> { - try { - const res1 = await fetch(baseurl + name + "/key"); - const json1 = await res1.json(); - const key = json1.result; - const res2 = await fetch(baseurl + key + "/info"); - const json2 = await res2.json(); - return { key, info: json2 }; - } catch (err) { - throw new Error("获取表情包信息失败"); - } -} - -export function registerMeme() { - const toolList = new Tool({ - type: "function", - function: { - name: "meme_list", - description: `访问可用表情包列表`, - parameters: { - type: "object", - properties: { - }, - required: [] - } - } - }); - toolList.solve = async (_, __, ___, ____) => { - try { - const res = await fetch(baseurl + "get_command"); - const json = await res.json(); - return { content: json.map((item: string[]) => item[0]).join("、"), images: [] }; - } catch (err) { - return { content: "获取表情包列表失败:" + err.message, images: [] }; - } - } - - const toolGet = new Tool({ - type: "function", - function: { - name: "get_meme_info", - description: `获取表情包制作信息`, - parameters: { - type: "object", - properties: { - name: { - type: "string", - description: "表情包名字,为 meme_list 返回的结果" - } - }, - required: ["name"] - } - } - }); - toolGet.solve = async (_, __, ___, args) => { - const { name } = args; - - const { info } = await getInfo(name); - const { max_images, max_texts, min_images, min_texts } = info.params_type; - const image_text = min_images === max_images ? `用户数量为 ${min_images} 名` : `用户数量范围为 ${min_images} - ${max_images} 名`; - const text_text = min_texts === max_texts ? `文字数量为 ${min_texts} 段` : `文字数量范围为 ${min_texts} - ${max_texts} 段`; - - return { content: `该表情包需要:${image_text},${text_text}`, images: [] }; - } - - const toolGenerator = new Tool({ - type: "function", - function: { - name: "meme_generator", - description: `制作表情包,使用之前需要调用meme_list获取可用表情包列表,调用get_meme_info获取制作信息`, - parameters: { - type: "object", - properties: { - name: { - type: "string", - description: "表情包名字,为 meme_list 返回的结果" - }, - text: { - type: "array", - items: { type: "string" }, - description: "文字信息,不能插入图片" - }, - image_ids: { - type: "array", - items: { type: "string" }, - description: `图片id,或user_avatar:用户名称` + (ConfigManager.message.showNumber ? '或纯数字QQ号' : '') + `,或group_avatar:群聊名称` + (ConfigManager.message.showNumber ? '或纯数字群号' : '') - }, - save: { - type: "boolean", - description: "是否保存图片" - } - }, - required: ["name", "text", "image_ids", "save"] - } - } - }); - toolGenerator.solve = async (ctx, _, ai, args) => { - const { name, text = [], image_ids = [], save } = args; - - // 切换到当前会话ai - if (!ctx.isPrivate) ai = AIManager.getAI(ctx.group.groupId); - - let s = ''; - - const { key, info } = await getInfo(name); - const { max_images, max_texts, min_images, min_texts } = info.params_type; - const image_text = min_images === max_images ? `用户数量为 ${min_images} 名` : `用户数量范围为 ${min_images} - ${max_images} 名`; - const text_text = min_texts === max_texts ? `文字数量为 ${min_texts} 段` : `文字数量范围为 ${min_texts} - ${max_texts} 段`; - if (text.length > max_texts || text.length < min_texts) { - if (max_texts === 0) { - text.length = 0; - s += `该表情包不需要文字信息,已舍弃。`; - } else { - return { content: `文字数量错误,${text_text},${image_text}`, images: [] }; - } - } - if (image_ids.length > max_images || image_ids.length < min_images) { - if (max_images === 0) { - image_ids.length = 0; - s += `该表情包不需要图片,已舍弃。`; - } else { - return { content: `图片数量错误,${image_text},${text_text}`, images: [] }; - } - } - - const images: Image[] = [] - const uiList: UserInfo[] = []; - const giList: GroupInfo[] = []; - for (const id of image_ids) { - if (/^user_avatar[::]/.test(id)) { - const ui = await this.findUserInfo(ctx, id.replace(/^user_avatar[::]/, '')); - if (ui) { - uiList.push(ui); - images.push(ImageManager.getUserAvatar(ui.id)); - } else { - return { content: `用户 ${id} 不存在`, images: [] }; - } - continue; - } - if (/^group_avatar[::]/.test(id)) { - const gi = await this.findGroupInfo(ctx, id.replace(/^group_avatar[::]/, '')); - if (gi) { - giList.push(gi); - images.push(ImageManager.getGroupAvatar(gi.id)); - } else { - return { content: `群聊 ${id} 不存在`, images: [] }; - } - continue; - } - const img = await ai.context.findImage(ctx, id); - if (img) { - if (img.type === 'url') images.push(img); - else return { content: `图片 ${id} 类型错误,仅支持url类型`, images: [] }; - } else { - return { content: `图片 ${id} 不存在`, images: [] }; - } - } - - const kws = ["meme", name, ...text, ...image_ids]; - - // 图片存在则直接返回 - const result = ai.memory.findMemoryAndImageByImageIdPrefix(name); - if (result) { - const { memory, image } = result; - if (memory.keywords.every((v, i) => v === kws[i]) && memory.images.slice(1).every((v, i) => v.id === images[i].id)) { - return { content: `${s}生成成功,请使用<|img:${image.id}|>发送`, images: [image] }; - } - } - - try { - const res = await fetch(baseurl + "meme_generate", { - method: "POST", - body: JSON.stringify({ - key, - text, - image: images.map(img => img.file), - args: {} - }), - }); - - const json = await res.json(); - if (json.status == "success") { - const base64 = json.message; - if (!base64) { - logger.error(`生成的base64为空`); - return { content: "生成的base64为空", images: [] }; - } - - const textText = text.join(';'); - const imageText = image_ids.join(';'); - - const img = new Image(); - img.id = `${name}_${generateId()}`; - img.base64 = base64; - img.format = 'unknown'; - img.content = `表情包<|img:${img.id}|> -${textText ? `文字:${textText}` : ''} -${imageText ? `图片:${imageText}` : ''}`; - - if (save) ai.memory.addMemory(ctx, ai, uiList, giList, kws, [img, ...images], img.content); - - return { content: `${s}生成成功,请使用<|img:${img.id}|>发送`, images: [img] }; - } else { - throw new Error(json.message); - } - } catch (err) { - return { content: "生成表情包失败:" + err.message, images: [] }; - } - } -} - -// 说实话感觉并不是最完美的状态 -// 感觉应该先把meme_list和meme_info本地化 -// 然后给出一个选择meme模板的模板配置项,毕竟有的人设并不适合所有的表情包 -// 再把选中的meme模板构建prompt,另外我注意到有的模板应该是有默认文本的,这其实也可以提示ai要输入什么文本,而不是牛头不对马嘴 -// 这样只需保留meme_generator的实现 +import { AIManager, GroupInfo, UserInfo } from "../AI/AI"; +import { Image, ImageManager } from "../image/image"; +import { ConfigManager } from "../config/configManager"; +import { logger } from "../logger"; +import { generateId } from "../utils/utils"; +import { Tool } from "./tool"; + +const baseurl = "http://meme.lovesealdice.online/"; + +interface MemeInfo { + params_type: { + min_texts: number, + max_texts: number, + min_images: number, + max_images: number, + } +} + +async function getInfo(name: string): Promise<{ key: string, info: MemeInfo }> { + try { + const res1 = await fetch(baseurl + name + "/key"); + const json1 = await res1.json(); + const key = json1.result; + const res2 = await fetch(baseurl + key + "/info"); + const json2 = await res2.json(); + return { key, info: json2 }; + } catch (err) { + throw new Error("获取表情包信息失败"); + } +} + +export function registerMeme() { + const toolList = new Tool({ + type: "function", + function: { + name: "meme_list", + description: `访问可用表情包列表`, + parameters: { + type: "object", + properties: { + }, + required: [] + } + } + }); + toolList.solve = async (_, __, ___, ____) => { + try { + const res = await fetch(baseurl + "get_command"); + const json = await res.json(); + return { content: json.map((item: string[]) => item[0]).join("、"), images: [] }; + } catch (err) { + return { content: "获取表情包列表失败:" + err.message, images: [] }; + } + } + + const toolGet = new Tool({ + type: "function", + function: { + name: "get_meme_info", + description: `获取表情包制作信息`, + parameters: { + type: "object", + properties: { + name: { + type: "string", + description: "表情包名字,为 meme_list 返回的结果" + } + }, + required: ["name"] + } + } + }); + toolGet.solve = async (_, __, ___, args) => { + const { name } = args; + + const { info } = await getInfo(name); + const { max_images, max_texts, min_images, min_texts } = info.params_type; + const image_text = min_images === max_images ? `用户数量为 ${min_images} 名` : `用户数量范围为 ${min_images} - ${max_images} 名`; + const text_text = min_texts === max_texts ? `文字数量为 ${min_texts} 段` : `文字数量范围为 ${min_texts} - ${max_texts} 段`; + + return { content: `该表情包需要:${image_text},${text_text}`, images: [] }; + } + + const toolGenerator = new Tool({ + type: "function", + function: { + name: "meme_generator", + description: `制作表情包,使用之前需要调用meme_list获取可用表情包列表,调用get_meme_info获取制作信息`, + parameters: { + type: "object", + properties: { + name: { + type: "string", + description: "表情包名字,为 meme_list 返回的结果" + }, + text: { + type: "array", + items: { type: "string" }, + description: "文字信息,不能插入图片" + }, + image_ids: { + type: "array", + items: { type: "string" }, + description: `图片id,或user_avatar:用户名称` + (ConfigManager.message.showNumber ? '或纯数字QQ号' : '') + `,或group_avatar:群聊名称` + (ConfigManager.message.showNumber ? '或纯数字群号' : '') + }, + save: { + type: "boolean", + description: "是否保存图片" + } + }, + required: ["name", "text", "image_ids", "save"] + } + } + }); + toolGenerator.solve = async (ctx, _, ai, args) => { + const { name, text = [], image_ids = [], save } = args; + + // 切换到当前会话ai + if (!ctx.isPrivate) ai = AIManager.getAI(ctx.group.groupId); + + let s = ''; + + const { key, info } = await getInfo(name); + const { max_images, max_texts, min_images, min_texts } = info.params_type; + const image_text = min_images === max_images ? `用户数量为 ${min_images} 名` : `用户数量范围为 ${min_images} - ${max_images} 名`; + const text_text = min_texts === max_texts ? `文字数量为 ${min_texts} 段` : `文字数量范围为 ${min_texts} - ${max_texts} 段`; + if (text.length > max_texts || text.length < min_texts) { + if (max_texts === 0) { + text.length = 0; + s += `该表情包不需要文字信息,已舍弃。`; + } else { + return { content: `文字数量错误,${text_text},${image_text}`, images: [] }; + } + } + if (image_ids.length > max_images || image_ids.length < min_images) { + if (max_images === 0) { + image_ids.length = 0; + s += `该表情包不需要图片,已舍弃。`; + } else { + return { content: `图片数量错误,${image_text},${text_text}`, images: [] }; + } + } + + const images: Image[] = [] + const uiList: UserInfo[] = []; + const giList: GroupInfo[] = []; + for (const id of image_ids) { + if (/^user_avatar[::]/.test(id)) { + const ui = await this.findUserInfo(ctx, id.replace(/^user_avatar[::]/, '')); + if (ui) { + uiList.push(ui); + images.push(ImageManager.getUserAvatar(ui.id)); + } else { + return { content: `用户 ${id} 不存在`, images: [] }; + } + continue; + } + if (/^group_avatar[::]/.test(id)) { + const gi = await this.findGroupInfo(ctx, id.replace(/^group_avatar[::]/, '')); + if (gi) { + giList.push(gi); + images.push(ImageManager.getGroupAvatar(gi.id)); + } else { + return { content: `群聊 ${id} 不存在`, images: [] }; + } + continue; + } + const img = await ai.context.findImage(ctx, id); + if (img) { + if (img.type === 'url') images.push(img); + else return { content: `图片 ${id} 类型错误,仅支持url类型`, images: [] }; + } else { + return { content: `图片 ${id} 不存在`, images: [] }; + } + } + + const kws = ["meme", name, ...text, ...image_ids]; + + // 图片存在则直接返回 + const result = ai.memory.findMemoryAndImageByImageIdPrefix(name); + if (result) { + const { memory, image } = result; + if (memory.keywords.every((v, i) => v === kws[i]) && memory.images.slice(1).every((v, i) => v.id === images[i].imageId)) { + return { content: `${s}生成成功,请使用<|img:${image.id}|>发送`, images: [image] }; + } + } + + try { + const res = await fetch(baseurl + "meme_generate", { + method: "POST", + body: JSON.stringify({ + key, + text, + image: images.map(img => img.file), + args: {} + }), + }); + + const json = await res.json(); + if (json.status == "success") { + const base64 = json.message; + if (!base64) { + logger.error(`生成的base64为空`); + return { content: "生成的base64为空", images: [] }; + } + + const textText = text.join(';'); + const imageText = image_ids.join(';'); + + const img = new Image(); + img.imageId = `${name}_${generateId()}`; + img.base64 = base64; + img.format = 'unknown'; + img.description = `表情包<|img:${img.imageId}|> +${textText ? `文字:${textText}` : ''} +${imageText ? `图片:${imageText}` : ''}`; + + if (save) ai.memory.addMemory(ctx, ai, uiList, giList, kws, [img, ...images], img.description); + + return { content: `${s}生成成功,请使用<|img:${img.imageId}|>发送`, images: [img] }; + } else { + throw new Error(json.message); + } + } catch (err) { + return { content: "生成表情包失败:" + err.message, images: [] }; + } + } +} + +// 说实话感觉并不是最完美的状态 +// 感觉应该先把meme_list和meme_info本地化 +// 然后给出一个选择meme模板的模板配置项,毕竟有的人设并不适合所有的表情包 +// 再把选中的meme模板构建prompt,另外我注意到有的模板应该是有默认文本的,这其实也可以提示ai要输入什么文本,而不是牛头不对马嘴 +// 这样只需保留meme_generator的实现 // 另外可以把url加进后端配置中,这个的后端是哪个项目啊———— \ No newline at end of file diff --git a/src/tool/tool_render.ts b/src/tool/tools/image.ts/tool_render.ts similarity index 95% rename from src/tool/tool_render.ts rename to src/tool/tools/image.ts/tool_render.ts index 334fa8a..074053e 100644 --- a/src/tool/tool_render.ts +++ b/src/tool/tools/image.ts/tool_render.ts @@ -2,7 +2,7 @@ import { logger } from "../logger"; import { Tool } from "./tool"; import { ConfigManager } from "../config/configManager"; import { AI, AIManager } from "../AI/AI"; -import { Image } from "../AI/image"; +import { Image } from "../image/image"; import { generateId } from "../utils/utils"; import { parseSpecialTokens } from "../utils/utils_string"; @@ -139,15 +139,15 @@ export function registerRender() { } const img = new Image(); - img.id = `${name}_${generateId()}`; + img.imageId = `${name}_${generateId()}`; img.base64 = base64; img.format = 'unknown'; - img.content = `Markdown 渲染图片<|img:${img.id}|> + img.description = `Markdown 渲染图片<|img:${img.imageId}|> 主题:${theme}`; - if (save) ai.memory.addMemory(ctx, ai, [], [], kws, [img], img.content); + if (save) ai.memory.addMemory(ctx, ai, [], [], kws, [img], img.description); - return { content: `渲染成功,请使用<|img:${img.id}|>发送`, images: [img] }; + return { content: `渲染成功,请使用<|img:${img.imageId}|>发送`, images: [img] }; } else { throw new Error(result.message || "渲染失败"); } @@ -206,14 +206,14 @@ export function registerRender() { } const img = new Image(); - img.id = `${name}_${generateId()}`; + img.imageId = `${name}_${generateId()}`; img.base64 = base64; img.format = 'unknown'; - img.content = `HTML 渲染图片<|img:${img.id}|>`; + img.description = `HTML 渲染图片<|img:${img.imageId}|>`; - if (save) ai.memory.addMemory(ctx, ai, [], [], kws, [img], img.content); + if (save) ai.memory.addMemory(ctx, ai, [], [], kws, [img], img.description); - return { content: `渲染成功,请使用<|img:${img.id}|>发送`, images: [img] }; + return { content: `渲染成功,请使用<|img:${img.imageId}|>发送`, images: [img] }; } else { throw new Error(result.message || "渲染失败"); } diff --git a/src/tool/tool_ban.ts b/src/tool/tools/ob11.ts/tool_ban.ts similarity index 97% rename from src/tool/tool_ban.ts rename to src/tool/tools/ob11.ts/tool_ban.ts index 10c846f..8b91651 100644 --- a/src/tool/tool_ban.ts +++ b/src/tool/tools/ob11.ts/tool_ban.ts @@ -25,7 +25,7 @@ export function registerBan() { } } }); - toolBan.type = 'group'; + toolBan.sessionType = 'group'; toolBan.solve = async (ctx, _, ai, args) => { const { name, duration } = args; @@ -65,7 +65,7 @@ export function registerBan() { } } }); - toolWhole.type = 'group'; + toolWhole.sessionType = 'group'; toolWhole.solve = async (ctx, _, __, args) => { const { enable } = args; @@ -91,7 +91,7 @@ export function registerBan() { } } }); - toolList.type = 'group'; + toolList.sessionType = 'group'; toolList.solve = async (ctx, _, __, ___) => { if (!netExists()) return { content: `未找到ob11网络连接依赖,请提示用户安装`, images: [] }; diff --git a/src/tool/tool_essence_msg.ts b/src/tool/tools/ob11.ts/tool_essence_msg.ts similarity index 99% rename from src/tool/tool_essence_msg.ts rename to src/tool/tools/ob11.ts/tool_essence_msg.ts index dde6c08..68f7db9 100644 --- a/src/tool/tool_essence_msg.ts +++ b/src/tool/tools/ob11.ts/tool_essence_msg.ts @@ -1,6 +1,6 @@ import { transformMsgIdBack, transformMsgId } from "../utils/utils"; import { Tool } from "./tool"; -import { Image } from "../AI/image"; +import { Image } from "../image/image"; import { transformArrayToContent } from "../utils/utils_string"; import { deleteEssenceMsg, getEssenceMsgList, getGroupMemberInfo, netExists, setEssenceMsg } from "../utils/utils_ob11"; diff --git a/src/tool/tool_group_sign.ts b/src/tool/tools/ob11.ts/tool_group_sign.ts similarity index 96% rename from src/tool/tool_group_sign.ts rename to src/tool/tools/ob11.ts/tool_group_sign.ts index 046b26a..2b1f679 100644 --- a/src/tool/tool_group_sign.ts +++ b/src/tool/tools/ob11.ts/tool_group_sign.ts @@ -15,7 +15,7 @@ export function registerGroupSign() { } } }); - tool.type = 'group'; + tool.sessionType = 'group'; tool.solve = async (ctx, _, __, ___) => { if (ctx.isPrivate) { return { content: `群打卡只能在群聊中使用`, images: [] }; diff --git a/src/tool/tool_person_info.ts b/src/tool/tools/ob11.ts/tool_person_info.ts similarity index 100% rename from src/tool/tool_person_info.ts rename to src/tool/tools/ob11.ts/tool_person_info.ts diff --git a/src/tool/tool_qq_list.ts b/src/tool/tools/ob11.ts/tool_qq_list.ts similarity index 100% rename from src/tool/tool_qq_list.ts rename to src/tool/tools/ob11.ts/tool_qq_list.ts diff --git a/src/tool/tool_rename.ts b/src/tool/tools/ob11.ts/tool_rename.ts similarity index 98% rename from src/tool/tool_rename.ts rename to src/tool/tools/ob11.ts/tool_rename.ts index 18a1fb7..7ed0d55 100644 --- a/src/tool/tool_rename.ts +++ b/src/tool/tools/ob11.ts/tool_rename.ts @@ -26,7 +26,7 @@ export function registerRename() { } } }); - tool.type = 'group'; + tool.sessionType = 'group'; tool.solve = async (ctx, msg, ai, args) => { const { name, new_name } = args; diff --git a/src/tool/tools/sample.ts b/src/tool/tools/sample.ts new file mode 100644 index 0000000..cfcd7bc --- /dev/null +++ b/src/tool/tools/sample.ts @@ -0,0 +1,26 @@ +import { Tool } from "../tool"; + +const tool = new Tool({ + type: "function", + function: { + name: "sample", + description: `示例工具`, + parameters: { + type: "object", + properties: { + arg: { + type: 'string', + description: '参数' + } + }, + required: ["arg"] + } + } +}); +tool.solve = async (ctx, msg, session, args) => { + const { arg } = args; + arg; ctx; msg; session; + return "调用示例函数成功"; +} + +export { tool as toolSample }; \ No newline at end of file diff --git a/src/tool/tool_deck.ts b/src/tool/tools/seal.ts/tool_deck.ts similarity index 100% rename from src/tool/tool_deck.ts rename to src/tool/tools/seal.ts/tool_deck.ts diff --git a/src/tool/tool_context.ts b/src/tool/tools/tool_context.ts similarity index 100% rename from src/tool/tool_context.ts rename to src/tool/tools/tool_context.ts diff --git a/src/tool/tool_memory.ts b/src/tool/tools/tool_memory.ts similarity index 95% rename from src/tool/tool_memory.ts rename to src/tool/tools/tool_memory.ts index 6ac197d..cb94e37 100644 --- a/src/tool/tool_memory.ts +++ b/src/tool/tools/tool_memory.ts @@ -2,7 +2,7 @@ import { AIManager, GroupInfo, SessionInfo, UserInfo } from "../AI/AI"; import { ConfigManager } from "../config/configManager"; import { getCtxAndMsg } from "../utils/utils_seal"; import { Tool } from "./tool"; -import { knowledgeMM, searchOptions as SearchOptions } from "../AI/memory"; +import { knowledgeService, searchOptions as SearchOptions } from "../session/memory"; import { getRoleSetting } from "../utils/utils_message"; export function registerMemory() { @@ -242,20 +242,20 @@ export function registerMemory() { const options: SearchOptions = { topK: topK, keywords: keywords, - userList: userList, - groupList: groupList, + userIdList: userList, + groupIdList: groupList, includeImages: includeImages, method: method } const { roleIndex } = getRoleSetting(ctx); - await knowledgeMM.updateKnowledgeMemory(roleIndex); - if (knowledgeMM.memoryIds.length === 0) return { content: `暂无记忆`, images: [] }; + await knowledgeService.updateKnowledgeMemory(roleIndex); + if (knowledgeService.memoryIdList.length === 0) return { content: `暂无记忆`, images: [] }; - const memoryList = await knowledgeMM.search(query, options); + const memoryList = await knowledgeService.search(query, options); const images = Array.from(new Set([].concat(...memoryList.map(m => m.images)))); - return { content: knowledgeMM.buildKnowledgeMemory(memoryList) || '暂无记忆', images: images }; + return { content: knowledgeService.buildKnowledgeMemory(memoryList) || '暂无记忆', images: images }; } else { return { content: `未知的记忆类型<${memory_type}>`, images: [] }; } @@ -276,8 +276,8 @@ export function registerMemory() { const options: SearchOptions = { topK: topK, keywords: keywords, - userList: userList, - groupList: groupList, + userIdList: userList, + groupIdList: groupList, includeImages: includeImages, method: method } diff --git a/src/tool/tool_message.ts b/src/tool/tools/tool_message.ts similarity index 99% rename from src/tool/tool_message.ts rename to src/tool/tools/tool_message.ts index ce6937b..f447c65 100644 --- a/src/tool/tool_message.ts +++ b/src/tool/tools/tool_message.ts @@ -4,10 +4,10 @@ import { replyToSender, transformMsgIdBack } from "../utils/utils"; import { getCtxAndMsg } from "../utils/utils_seal"; import { handleReply, MessageSegment, parseSpecialTokens, transformArrayToContent } from "../utils/utils_string"; import { Tool, ToolManager } from "./tool"; -import { CQTYPESALLOW, faceMap } from "../config/config"; +import { CQTYPESALLOW, faceMap } from "../config/static_config"; import { deleteMsg, getGroupMemberInfo, getMsg, sendGroupForwardMsg, sendPrivateForwardMsg, netExists } from "../utils/utils_ob11"; import { logger } from "../logger"; -import { Image } from "../AI/image"; +import { Image } from "../image/image"; export function registerMessage() { const toolSend = new Tool({ diff --git a/src/tool/tool_music.ts b/src/tool/tools/tool_music.ts similarity index 100% rename from src/tool/tool_music.ts rename to src/tool/tools/tool_music.ts diff --git a/src/tool/tool_time.ts b/src/tool/tools/tool_time.ts similarity index 100% rename from src/tool/tool_time.ts rename to src/tool/tools/tool_time.ts diff --git a/src/tool/tool_trigger.ts b/src/tool/tools/tool_trigger.ts similarity index 100% rename from src/tool/tool_trigger.ts rename to src/tool/tools/tool_trigger.ts diff --git a/src/tool/tool_voice.ts b/src/tool/tools/tool_voice.ts similarity index 100% rename from src/tool/tool_voice.ts rename to src/tool/tools/tool_voice.ts diff --git a/src/tool/tool_web.ts b/src/tool/tools/tool_web.ts similarity index 100% rename from src/tool/tool_web.ts rename to src/tool/tools/tool_web.ts diff --git a/src/tool/types.ts b/src/tool/types.ts new file mode 100644 index 0000000..aefe580 --- /dev/null +++ b/src/tool/types.ts @@ -0,0 +1,105 @@ +export interface ToolInfoObject { + type: "object"; + description?: string; + properties?: { + [key: string]: ToolInfoItem; + }; + required?: (keyof ToolInfoObject["properties"])[]; + additionalProperties?: boolean | ToolInfoItem; + minProperties?: number; + maxProperties?: number; +} + +export interface ToolInfoString { + type: "string"; + description?: string; + enum?: string[]; + minLength?: number; + maxLength?: number; + pattern?: string; // 正则表达式 + format?: "date-time" | "email" | "uri" | "uuid" | "hostname" | "ipv4" | "ipv6"; +} + +export interface ToolInfoNumber { + type: "number"; + description?: string; + minimum?: number; + maximum?: number; + exclusiveMinimum?: number; + exclusiveMaximum?: number; + multipleOf?: number; +} + +export interface ToolInfoInteger { + type: "integer"; + description?: string; + minimum?: number; + maximum?: number; + exclusiveMinimum?: number; + exclusiveMaximum?: number; + multipleOf?: number; +} + +export interface ToolInfoArray { + type: "array"; + description?: string; + items: ToolInfoItem; + minItems?: number; + maxItems?: number; + uniqueItems?: boolean; +} + +export interface ToolInfoBoolean { + type: "boolean"; + description?: string; +} + +export interface ToolInfoNull { + type: "null"; + description?: string; +} + +export type ToolInfoItem = + | ToolInfoString + | ToolInfoNumber + | ToolInfoInteger + | ToolInfoBoolean + | ToolInfoNull + | ToolInfoArray + | ToolInfoObject; + +export interface ToolInfo { + type: "function", + function: { + name: string, + description: string, + parameters: ToolInfoObject + } +} + +export interface ToolCall { + index: number, + id: string, + type: "function", + function: { + name: string, + arguments: string + } +} +export interface ToolCallResult { + tool_call_id: string, + content: string +} + +export interface ExtCmdInfo { + extName: string, // 使用的扩展名称 + cmd: string, // 指令名称 + staticArgs: string[] // 参数 +} + +export interface ToolListen { + timeoutId: number, + resolve: (content: string) => void, + reject: (err: Error) => void, + cleanup: () => void +} \ No newline at end of file diff --git a/src/trigger.ts b/src/trigger.ts new file mode 100644 index 0000000..0049927 --- /dev/null +++ b/src/trigger.ts @@ -0,0 +1 @@ +// 把AI触发逻辑解耦到这里 \ No newline at end of file diff --git a/src/usage.ts b/src/usage.ts new file mode 100644 index 0000000..3131597 --- /dev/null +++ b/src/usage.ts @@ -0,0 +1,53 @@ +import { Config } from "../config/config"; +import { logger } from "../logger"; + +export class UsageManager { + static usageMap: { [key: string]: Usage } = {}; + + static updateUsage(model: string, usage: Usage) { + + } +} + +export async function get_chart_url(chart_type: string, usage_data: { + [key: string]: { + prompt_tokens: number; + completion_tokens: number; + } +}) { + const { usageChartUrl } = Config.backend; + try { + const response = await fetch(`${usageChartUrl}/chart`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ + chart_type: chart_type, + data: usage_data + }) + }) + + const text = await response.text(); + if (!response.ok) { + throw new Error(`请求失败! 状态码: ${response.status}\n响应体: ${text}`); + } + if (!text) { + throw new Error("响应体为空"); + } + + try { + const data = JSON.parse(text); + if (data.error) { + throw new Error(`请求失败! 错误信息: ${data.error.message}`); + } + return data.image_url; + } catch (e) { + throw new Error(`解析响应体时出错:${e}\n响应体:${text}`); + } + } catch (e) { + logger.error("在get_chart_url中请求出错:", e.message); + return ''; + } +} \ No newline at end of file diff --git a/src/utils/utils_message.ts b/src/utils/message.ts similarity index 91% rename from src/utils/utils_message.ts rename to src/utils/message.ts index 1f812ba..c4aaa41 100644 --- a/src/utils/utils_message.ts +++ b/src/utils/message.ts @@ -1,15 +1,15 @@ import { AI, GroupInfo, UserInfo } from "../AI/AI"; -import { Message } from "../AI/context"; -import { ConfigManager } from "../config/configManager"; +import { Message } from "../session/context"; +import { Config } from "../config/config"; import { ToolInfo } from "../tool/tool"; -import { fmtDate } from "./utils_string"; -import { knowledgeMM } from "../AI/memory"; +import { fmtDate } from "./string"; +import { knowledgeService } from "../memory/memory"; export async function buildSystemMessage(ctx: seal.MsgContext, ai: AI): Promise { - const { systemMessageTemplate, isPrefix, showNumber, showMsgId, showTime } = ConfigManager.message; - const { isTool, usePromptEngineering } = ConfigManager.tool; - const { localImagePathMap, receiveImage, condition } = ConfigManager.image; - const { isMemory, isShortMemory } = ConfigManager.memory; + const { systemMessageTemplate, isPrefix, showNumber, showMsgId, showTime } = Config.message; + const { isTool, usePromptEngineering } = Config.tool; + const { localImagePathMap, receiveImage, condition } = Config.image; + const { isMemory, isShortMemory } = Config.memory; // 可发送的图片提示 const sandableImagesPrompt: string = Object.keys(localImagePathMap) @@ -38,7 +38,7 @@ export async function buildSystemMessage(ctx: seal.MsgContext, ai: AI): Promise< } // 知识库 - const knowledgePrompt = await knowledgeMM.buildKnowledgeMemoryPrompt(roleIndex, text, ui, gi); + const knowledgePrompt = await knowledgeService.buildKnowledgeMemoryPrompt(roleIndex, text, ui, gi); // 记忆 const memoryPrompt = isMemory ? await ai.memory.buildMemoryPrompt(ctx, ai.context, text, ui, gi) : ''; // 短期记忆 @@ -77,9 +77,9 @@ export async function buildSystemMessage(ctx: seal.MsgContext, ai: AI): Promise< name: '', images: [], msgArray: [{ - msgId: '', + messageId: '', time: Math.floor(Date.now() / 1000), - content: content + text: content }] }; @@ -87,7 +87,7 @@ export async function buildSystemMessage(ctx: seal.MsgContext, ai: AI): Promise< } function buildSamplesMessages(ctx: seal.MsgContext): Message[] { - const { samples } = ConfigManager.message; + const { samples } = Config.message; const samplesMessages: Message[] = samples .map((item, index) => { @@ -125,7 +125,7 @@ function buildSamplesMessages(ctx: seal.MsgContext): Message[] { } function buildContextMessages(systemMessage: Message, messages: Message[]): Message[] { - const { insertCount } = ConfigManager.message; + const { insertCount } = Config.message; const contextMessages = messages.slice(); @@ -156,7 +156,7 @@ function buildContextMessages(systemMessage: Message, messages: Message[]): Mess } export async function handleMessages(ctx: seal.MsgContext, ai: AI) { - const { isMerge } = ConfigManager.message; + const { isMerge } = Config.message; const systemMessage = await buildSystemMessage(ctx, ai); const samplesMessages = buildSamplesMessages(ctx); @@ -219,7 +219,7 @@ export async function handleMessages(ctx: seal.MsgContext, ai: AI) { } export function parseBody(template: string[], messages: any[], tools: ToolInfo[], tool_choice: string) { - const { isTool, usePromptEngineering } = ConfigManager.tool; + const { isTool, usePromptEngineering } = Config.tool; const bodyObject: any = {}; for (let i = 0; i < template.length; i++) { @@ -290,22 +290,22 @@ export function parseEmbeddingBody(template: string[], input: string, dimensions } export function buildContent(message: Message): string { - const { isPrefix, showNumber, showMsgId, showTime } = ConfigManager.message; + const { isPrefix, showNumber, showMsgId, showTime } = Config.message; const prefix = (isPrefix && message.name) ? ( message.name.startsWith('_') ? `<|${message.name}|>` : `<|from:${message.name}${showNumber ? `(${message.uid.replace(/^.+:/, '')})` : ``}|>` ) : ''; const content = message.msgArray.map(m => - ((showMsgId && m.msgId) ? `<|msg_id:${m.msgId}|>` : '') + + ((showMsgId && m.messageId) ? `<|msg_id:${m.messageId}|>` : '') + (showTime ? `<|time:${fmtDate(m.time)}|>` : '') + - m.content + m.text ).join('\f'); return prefix + content; } export function getRoleSetting(ctx: seal.MsgContext) { - const { roleSettingNames, roleSettingTemplate } = ConfigManager.message; + const { roleSettingNames, roleSettingTemplate } = Config.message; // 角色设定 const [roleName, exists] = seal.vars.strGet(ctx, "$gSYSPROMPT"); let roleIndex = 0; diff --git a/src/utils/utils_ob11.ts b/src/utils/ob11.ts similarity index 99% rename from src/utils/utils_ob11.ts rename to src/utils/ob11.ts index f2d0e5d..ee58703 100644 --- a/src/utils/utils_ob11.ts +++ b/src/utils/ob11.ts @@ -1,5 +1,5 @@ import { logger } from "../logger"; -import { MessageSegment } from "./utils_string"; +import { MessageSegment } from "./string"; export function getNet() { const net = globalThis.net || globalThis.http; diff --git a/src/utils/utils_seal.ts b/src/utils/seal.ts similarity index 91% rename from src/utils/utils_seal.ts rename to src/utils/seal.ts index afa0eca..d97b6e8 100644 --- a/src/utils/utils_seal.ts +++ b/src/utils/seal.ts @@ -33,4 +33,8 @@ export function getSessionCtxAndMsg(epId: string, sid: string, isPrivate: boolea const msg = createMsg(...args); const ctx = createCtx(epId, msg); return { ctx, msg }; -} \ No newline at end of file +} + +export function getSessionId(ctx: seal.MsgContext): string { + return ctx.isPrivate ? ctx.player.userId : ctx.group.groupId; +} diff --git a/src/utils/utils_string.ts b/src/utils/string.ts similarity index 97% rename from src/utils/utils_string.ts rename to src/utils/string.ts index f766910..868ce00 100644 --- a/src/utils/utils_string.ts +++ b/src/utils/string.ts @@ -1,11 +1,11 @@ -import { Context } from "../AI/context"; -import { Image } from "../AI/image"; +import { Context } from "../session/context"; +import { Image } from "../image"; import { logger } from "../logger"; -import { ConfigManager } from "../config/configManager"; +import { Config } from "../config/config"; import { transformMsgId, transformMsgIdBack } from "./utils"; import { AI } from "../AI/AI"; -import { getCtxAndMsg } from "./utils_seal"; -import { faceMap } from "../config/config"; +import { getCtxAndMsg } from "./seal"; +import { FACE_MAP } from "../config/static_config"; /* 先丢这一坨东西在这。之所以不用是因为被类型检查整烦了 @@ -174,7 +174,7 @@ export function transformArrayToText(messageArray: { type: string, data: { [key: } export async function transformArrayToContent(ctx: seal.MsgContext, ai: AI, messageArray: MessageSegment[]): Promise<{ content: string, images: Image[] }> { - const { showNumber, showMsgId } = ConfigManager.message; + const { showNumber, showMsgId } = Config.message; let content = ''; const images: Image[] = []; for (const seg of messageArray) { @@ -212,7 +212,7 @@ export async function transformArrayToContent(ctx: seal.MsgContext, ai: AI, mess break; } case 'face': { - const faceName = faceMap[seg.data.id] || ''; + const faceName = FACE_MAP[seg.data.id] || ''; content += faceName ? `<|face:${faceName}|>` : ''; break; } @@ -277,7 +277,7 @@ async function transformContentToText(ctx: seal.MsgContext, ai: AI, content: str break; } case 'face': { - const faceId = Object.keys(faceMap).find(key => faceMap[key] === seg.content) || ''; + const faceId = Object.keys(FACE_MAP).find(key => FACE_MAP[key] === seg.content) || ''; text += faceId ? `[CQ:face,id=${faceId}]` : ''; break; } @@ -287,7 +287,7 @@ async function transformContentToText(ctx: seal.MsgContext, ai: AI, content: str } export async function handleReply(ctx: seal.MsgContext, msg: seal.Message, ai: AI, s: string): Promise<{ contextArray: string[], replyArray: string[], images: Image[] }> { - const { replymsg, isTrim } = ConfigManager.reply; + const { replymsg, isTrim } = Config.reply; // 分离AI臆想出来的多轮对话 const segments = s @@ -337,7 +337,7 @@ export async function handleReply(ctx: seal.MsgContext, msg: seal.Message, ai: A } export function checkRepeat(context: Context, s: string) { - const { stopRepeat, similarityLimit } = ConfigManager.reply; + const { stopRepeat, similarityLimit } = Config.reply; if (!stopRepeat) { return false; @@ -348,7 +348,7 @@ export function checkRepeat(context: Context, s: string) { const message = messages[i]; // 寻找最后一条文本消息 if (message.role === 'assistant' && !message?.tool_calls) { - const content = message.msgArray[message.msgArray.length - 1].content || ''; + const content = message.msgArray[message.msgArray.length - 1].text || ''; const similarity = calculateSimilarity(content.trim(), s.trim()); logger.info(`复读相似度:${similarity}`); @@ -378,7 +378,7 @@ export function checkRepeat(context: Context, s: string) { } function filterString(s: string): { contextArray: string[], replyArray: string[] } { - const { maxChar, filterRegex, filterRegexes, contextTemplates, replyTemplates } = ConfigManager.reply; + const { maxChar, filterRegex, filterRegexes, contextTemplates, replyTemplates } = Config.reply; const contextArray: string[] = []; const replyArray: string[] = []; diff --git a/src/utils/utils_update.ts b/src/utils/update.ts similarity index 86% rename from src/utils/utils_update.ts rename to src/utils/update.ts index f46b4c1..a2755be 100644 --- a/src/utils/utils_update.ts +++ b/src/utils/update.ts @@ -1,7 +1,7 @@ import { logger } from "../logger"; import { updateInfo } from "../update"; -import { ConfigManager } from "../config/configManager"; -import { VERSION } from "../config/config"; +import { Config } from "../config/config"; +import { VERSION } from "../config/static_config"; /** * 比较两个版本号的大小。 @@ -31,11 +31,11 @@ export function compareVersions(version1: string, version2: string): number { } export function checkUpdate() { - const oldVersion = ConfigManager.ext.storageGet("version") || "0.0.0"; + const oldVersion = Config.ext.storageGet("version") || "0.0.0"; try { if (compareVersions(oldVersion, VERSION) < 0) { - ConfigManager.ext.storageSet("version", VERSION); + Config.ext.storageSet("version", VERSION); let info = []; for (const v in updateInfo) { if (compareVersions(oldVersion, v) >= 0) { diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 185a9ff..d63d013 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,9 +1,8 @@ -import { AI, GroupInfo, UserInfo } from "../AI/AI"; import { logger } from "../logger"; -import { ConfigManager } from "../config/configManager"; -import { transformTextToArray } from "./utils_string"; -import { aliasMap } from "../config/config"; -import { netExists, sendGroupMsg, sendPrivateMsg } from "./utils_ob11"; +import { Config } from "../config/config"; +import { transformTextToArray } from "./string"; +import { ALIAS_MAP } from "../config/static_config"; +import { netExists, sendGroupMsg, sendPrivateMsg } from "./ob11"; export function transformMsgId(msgId: string | number | null): string { if (msgId === null) { @@ -30,7 +29,7 @@ export async function replyToSender(ctx: seal.MsgContext, msg: seal.Message, ai: return ''; } - const { showMsgId } = ConfigManager.message; + const { showMsgId } = Config.message; if (showMsgId && netExists()) { const rawMessageArray = transformTextToArray(s); const messageArray = rawMessageArray.filter(item => item.type !== 'poke'); @@ -79,22 +78,70 @@ export function withTimeout(asyncFunc: () => Promise, timeoutMs: number): ]); } +export type TypeDescriptor = + 'default' + | 'string' + | 'number' + | 'boolean' + | 'any' + | TypeDescriptor[] // 元组元素类型 + | { array: TypeDescriptor } // 数组元素类型 + | { + object?: { [key: string]: TypeDescriptor }, + objectValue?: TypeDescriptor + } // 对象键值对类型,对象值类型 + | RevivableConstructor; // 嵌套类 + +interface RevivableConstructor { + new(): T; // 构造函数必须无参数 + validKeysMap: { [key in keyof T]?: TypeDescriptor }; +} + /** - * 恢复一个对象,只恢复构造函数中定义的属性,暂不支持嵌套属性 + * 恢复一个对象,只恢复构造函数中定义的属性,支持嵌套属性 希望没有bug * @param constructor 传入构造函数,必须有 validKeys 属性 * @param value 要恢复的对象 * @returns 恢复后的对象 */ -export function revive(constructor: { new(): T, validKeys: (keyof T)[] }, value: any): T { - const obj = new constructor(); - - if (!constructor.validKeys) { - logger.error(`revive: ${constructor.name} 没有 validKeys 属性`); - return obj; +export function revive(constructor: RevivableConstructor, value: any): T { + function reviveItem(descriptor: TypeDescriptor, defaultValue: any, value: any): any { + if (descriptor === 'string') { + if (typeof value === 'string') return value; + } else if (descriptor === 'number') { + if (typeof value === 'number') return value; + } else if (descriptor === 'boolean') { + if (typeof value === 'boolean') return value; + } else if (Array.isArray(descriptor)) { + if (Array.isArray(value)) return descriptor.map((d: any, i: number) => reviveItem(d, defaultValue?.[i], value?.[i])); + } else if (typeof descriptor === 'object' && 'array' in descriptor) { + if (Array.isArray(value)) return value.map((v: any, i: number) => reviveItem(descriptor.array, defaultValue?.[i], v)); + } else if (typeof descriptor === 'object' && ('object' in descriptor || 'objectValue' in descriptor)) { + const ov: any = {}, o: any = {}; + if (typeof value === 'object' && value !== null) { + if ('objectValue' in descriptor) Object.keys(value).forEach(k => ov[k] = reviveItem(descriptor.objectValue, defaultValue?.[k], value?.[k])); + if ('object' in descriptor) Object.keys(descriptor.object).forEach(k => o[k] = reviveItem(descriptor.object[k], defaultValue?.[k], value?.[k])); + return { ...o, ...ov }; + } + } else if (typeof descriptor === 'function') { + return revive(descriptor, value); + } else { + return value; + } + return defaultValue; } - for (const k of constructor.validKeys) { - if (value.hasOwnProperty(k)) { + const obj: any = new constructor(); + + if (constructor.validKeysMap) { + for (const k in constructor.validKeysMap) { + const descriptor = constructor.validKeysMap[k]; + if (value.hasOwnProperty(k)) { + const item = reviveItem(descriptor, obj[k], value[k]); + if (item !== undefined) obj[k] = item; + } + } + } else { // 没有定义 validKeysMap,直接赋值 + for (const k in value) { obj[k] = value[k]; } } @@ -103,7 +150,7 @@ export function revive(constructor: { new(): T, validKeys: (keyof T)[] }, val } export function aliasToCmd(val: string) { - return aliasMap[val] || val; + return ALIAS_MAP[val] || val; } // 计算余弦相似度 @@ -127,18 +174,8 @@ export function cosineSimilarity(a: number[], b: number[]): number { return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); } -export function getCommonUser(a: UserInfo[], b: UserInfo[]): UserInfo[] { - if (a.length === 0 || b.length === 0) return []; - const aid = new Set(a.map(u => u.id)); - return b.filter(u => aid.has(u.id)); -} -export function getCommonGroup(a: GroupInfo[], b: GroupInfo[]): GroupInfo[] { - if (a.length === 0 || b.length === 0) return []; - const aid = new Set(a.map(g => g.id)); - return b.filter(g => aid.has(g.id)); -} -export function getCommonKeyword(a: string[], b: string[]): string[] { +export function getCommonItem(a: string[], b: string[]): string[] { if (a.length === 0 || b.length === 0) return []; - const aid = new Set(a); - return b.filter(k => aid.has(k)); + const aset = new Set(a); + return b.filter(u => aset.has(u)); } \ No newline at end of file diff --git a/src/utils/web.ts b/src/utils/web.ts new file mode 100644 index 0000000..c9bd55d --- /dev/null +++ b/src/utils/web.ts @@ -0,0 +1,31 @@ +export async function fetchData(url: string, apiKey: string, body: any): Promise { + const response = await fetch(url, { + method: 'POST', + headers: { + "Authorization": `Bearer ${apiKey}`, + "Content-Type": "application/json", + "Accept": "application/json" + }, + body: JSON.stringify(body) + }); + + // logger.info("响应体", JSON.stringify(response, null, 2)); + + const text = await response.text(); + if (!response.ok) { + throw new Error(`请求失败! 状态码: ${response.status}\n响应体:${text}`); + } + if (!text) { + throw new Error("响应体为空"); + } + + try { + const data = JSON.parse(text); + if (data.error) { + throw new Error(`请求失败! 错误信息: ${data.error.message}`); + } + return data; + } catch (e) { + throw new Error(`解析响应体时出错:${e.message}\n响应体:${text}`); + } +} \ No newline at end of file