From fbcf4e6c08695147be442fb606ed81fecb614bbb Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Mon, 10 Mar 2025 23:08:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20chaite=20=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config.js | 10 +- guoba.support.js | 1117 +----------------------- index.js | 17 +- models/chaite/channel_storage.js | 81 +- models/chaite/chat_preset_storage.js | 51 +- models/chaite/cloud.js | 141 ++- models/chaite/history_manager.js | 57 ++ models/chaite/processors_storage.js | 68 ++ models/chaite/tool_settings_storage.js | 53 -- models/chaite/tools_storage.js | 68 ++ models/chaite/user_mode_selector.js | 12 + models/chaite/user_state_storage.js | 66 ++ models/chaite/vector_database.js | 66 +- models/storage.js | 2 +- package.json | 5 +- utils/common.js | 4 + 16 files changed, 549 insertions(+), 1269 deletions(-) create mode 100644 models/chaite/history_manager.js create mode 100644 models/chaite/processors_storage.js delete mode 100644 models/chaite/tool_settings_storage.js create mode 100644 models/chaite/tools_storage.js create mode 100644 models/chaite/user_mode_selector.js create mode 100644 models/chaite/user_state_storage.js create mode 100644 utils/common.js diff --git a/config/config.js b/config/config.js index 82c65a9..bb84c53 100644 --- a/config/config.js +++ b/config/config.js @@ -1,9 +1,15 @@ class ChatGPTConfig { dataDir = 'data' - processorsDirPath = 'data/processors' - toolsDirPath = 'data/tools' + processorsDirPath = 'utils/processors' + toolsDirPath = 'utils/tools' cloudBaseUrl = '' cloudApiKey = '' + + embeddingModel = 'gemini-embedding-exp-03-07' + dimensions = 0 + + serverAuthKey = '' + version = '3.0.0' } export default new ChatGPTConfig() diff --git a/guoba.support.js b/guoba.support.js index f14782f..2b591b2 100644 --- a/guoba.support.js +++ b/guoba.support.js @@ -1,7 +1,4 @@ -import { Config } from './utils/config.js' -import { speakers } from './utils/tts.js' -import { supportConfigurations as azureRoleList } from './utils/tts/microsoft-azure.js' -import { supportConfigurations as voxRoleList } from './utils/tts/voicevox.js' +import Config from './config/config.js' // 支持锅巴 export function supportGuoba () { return { @@ -28,1102 +25,12 @@ export function supportGuoba () { // 配置项 schemas schemas: [ { - field: 'toggleMode', - label: '触发方式', - bottomHelpMessage: 'at模式下只有at机器人才会回复。#chat模式下不需要at,但需要添加前缀#chat', - component: 'Select', - componentProps: { - options: [ - { label: 'at', value: 'at' }, - { label: '#chat', value: 'prefix' } - ] + field: 'testRender', + label: '表格测试', + component: 'Render', + render: () => { + `你好` } - }, - { - field: 'allowOtherMode', - label: '允许其他模式', - bottomHelpMessage: '开启后,则允许用户使用#chat1/#chat3/#chatglm/#bing等命令无视全局模式进行聊天', - component: 'Switch' - }, - { - field: 'assistantLabel', - label: 'AI名字', - bottomHelpMessage: 'AI认为的自己的名字,当你问他你是谁是他会回答这里的名字', - component: 'Input' - }, - { - field: 'enableBYM', - label: '开启伪人模式', - bottomHelpMessage: '开启后,将在群内随机发言,伪装成人。取消机器人前缀体验最佳。目前仅支持gemini,会使用gemini的配置。发言包括AI名字会必定触发回复。', - component: 'Switch' - }, - { - field: 'proxy', - label: '代理服务器地址', - bottomHelpMessage: '数据通过代理服务器发送,http或socks5代理。配置后需重启', - component: 'Input' - }, - { - field: 'debug', - label: '调试信息', - bottomHelpMessage: '将输出更多调试信息,如果不希望控制台刷屏的话,可以关闭', - component: 'Switch' - }, - { - field: 'enableToolbox', - label: '开启工具箱', - bottomHelpMessage: '独立的后台管理面板(默认3321端口),与锅巴类似。工具箱会有额外占用,启动速度稍慢,酌情开启。修改后需重启生效!!!', - component: 'Switch' - }, - { - field: 'enableToolPrivateSend', - label: '允许智能模式私聊', - bottomHelpMessage: '是否允许智能模式下发起临时对话骚扰其他群友。默认开启,如果怕Bot乱骚扰其他人可以关闭。主人不受影响。', - component: 'Switch' - }, - { - field: 'translateSource', - label: '翻译来源', - bottomHelpMessage: '#gpt翻译使用的AI来源', - component: 'Select', - componentProps: { - options: [ - { label: 'OpenAI', value: 'openai' }, - { label: 'Gemini', value: 'gemini' }, - { label: '星火', value: 'xh' }, - { label: '通义千问', value: 'qwen' } - ] - } - }, - { - label: '以下为服务超时配置。', - component: 'Divider' - }, - { - field: 'defaultTimeoutMs', - label: '默认超时时间', - helpMessage: '单位:毫秒', - bottomHelpMessage: '各个地方的默认超时时间', - component: 'InputNumber', - componentProps: { - min: 0 - } - }, - { - field: 'chromeTimeoutMS', - label: '浏览器超时时间', - helpMessage: '单位:毫秒', - bottomHelpMessage: '浏览器默认超时,浏览器可能需要更高的超时时间', - component: 'InputNumber', - componentProps: { - min: 0 - } - }, - { - field: 'sydneyFirstMessageTimeout', - label: 'Sydney模式接受首条信息超时时间', - helpMessage: '单位:毫秒', - bottomHelpMessage: '超过该时间阈值未收到Bing的任何消息,则断开本次连接并重试(最多重试3次,失败后将返回timeout waiting for first message)', - component: 'InputNumber', - componentProps: { - min: 15000 - } - }, - { - label: '以下为API方式(默认)的配置', - component: 'Divider' - }, - { - field: 'apiKey', - label: 'OpenAI API Key', - bottomHelpMessage: 'OpenAI的ApiKey,用于访问OpenAI的API接口', - component: 'InputPassword' - }, - { - field: 'model', - label: 'OpenAI 模型', - bottomHelpMessage: '填写OpenAI模型或OpenAI API兼容的其他模型。', - component: 'Input' - }, - { - field: 'apiMaxToken', - label: 'max token', - bottomHelpMessage: '默认4096', - component: 'InputNumber' - }, - { - field: 'smartMode', - label: '智能模式', - bottomHelpMessage: '仅建议gpt-4-32k和gpt-3.5-turbo-16k-0613开启,gpt-4-0613也可。开启后机器人可以群管、收发图片、发视频发音乐、联网搜索等。注意较费token。配合开启读取群聊上下文效果更佳', - component: 'Switch' - }, - { - field: 'forwardReasoning', - label: '是否转发思考过程', - bottomHelpMessage: 'OpenAI的o系列、deepseek的r系列等思考模型的思考过程是否以转发形式发出。仅适配reasoning_content。默认开启。', - component: 'Switch' - }, - { - field: 'openAiBaseUrl', - label: 'OpenAI API服务器地址', - bottomHelpMessage: 'OpenAI兼容API服务器地址。注意要带上/v1。默认为https://api.openai.com/v1', - component: 'Input' - }, - { - field: 'openAiForceUseReverse', - label: '强制使用OpenAI反代', - bottomHelpMessage: '即使配置了proxy,依然使用OpenAI反代', - component: 'Switch' - }, - { - field: 'promptPrefixOverride', - label: 'AI风格', - bottomHelpMessage: '你可以在这里写入你希望AI回答的风格,比如希望优先回答中文,回答长一点等', - component: 'InputTextArea' - }, - { - field: 'temperature', - label: 'temperature', - bottomHelpMessage: '用于控制回复内容的多样性,数值越大回复越加随机、多元化,数值越小回复越加保守', - component: 'InputNumber', - componentProps: { - min: 0, - max: 2 - } - }, - { - label: '以下为必应方式的配置。', - component: 'Divider' - }, - { - field: 'bingReasoning', - label: 'Bing开启思考', - bottomHelpMessage: 'Copilot的思考功能。开启后无法搜索', - component: 'Switch' - }, - { - field: 'enableGroupContext', - label: '是否允许机器人读取近期的群聊聊天记录', - bottomHelpMessage: '开启后机器人可以知道群名、最近发言等信息', - component: 'Switch' - }, - { - field: 'groupContextTip', - label: '机器人读取聊天记录时的后台prompt', - component: 'InputTextArea' - }, - { - field: 'enforceMaster', - label: '加强主人认知', - bottomHelpMessage: '加强主人认知。希望机器人认清主人,避免NTR可开启。开启后可能会与自设定的内容有部分冲突。sydney模式可以放心开启', - component: 'Switch' - }, - { - field: 'groupContextLength', - label: '允许机器人读取近期的最多群聊聊天记录条数。', - bottomHelpMessage: '允许机器人读取近期的最多群聊聊天记录条数。太多可能会超。默认50。同时影响所有模式,不止必应', - component: 'InputNumber' - }, - { - field: 'enableRobotAt', - label: '是否允许机器人真at', - bottomHelpMessage: '开启后机器人的回复如果at群友会真的at', - component: 'Switch' - }, - { - field: 'sydney', - label: 'Custom的设定', - bottomHelpMessage: '你可以自己改写设定,让Copilot变成你希望的样子。可能存在不稳定的情况', - component: 'InputTextArea' - }, - { - field: 'sydneyReverseProxy', - label: '必应反代', - bottomHelpMessage: '用于创建对话(默认不用于正式对话)。目前国内ip和部分境外IDC IP由于微软限制创建对话,如果有bing.com的反代可以填在此处,或者使用proxy', - component: 'Input' - }, - { - field: 'bingAiToken', - label: '必应AccessToken', - bottomHelpMessage: 'Copilot的AccessToken,scope需为ChatAI.ReadWrite。可以发送`#Copilot配置方法`查看浏览器获取配置的方法。', - component: 'Input' - }, - { - field: 'bingAiClientId', - label: '必应ClientId', - bottomHelpMessage: '配合RefreshToken刷新AccessToken', - component: 'Input' - }, - { - field: 'bingAiScope', - label: '必应Auth Scope', - bottomHelpMessage: '配合RefreshToken刷新AccessToken', - component: 'Input' - }, - { - field: 'bingAiRefreshToken', - label: '必应RefreshToken', - bottomHelpMessage: '配合RefreshToken刷新AccessToken', - component: 'Input' - }, - { - field: 'bingAiOid', - label: '必应Oid', - bottomHelpMessage: '(homeAccountId)配合RefreshToken刷新AccessToken', - component: 'Input' - }, - { - field: '_2captchaKey', - label: '2captcha API密钥', - bottomHelpMessage: '用于解除Copilot的验证码', - component: 'Input' - }, - { - label: '以下为API3方式的配置', - component: 'Divider' - }, - { - field: 'api', - label: 'ChatGPT API反代服务器地址', - bottomHelpMessage: 'ChatGPT的API反代服务器,用于绕过Cloudflare访问ChatGPT API', - component: 'Input' - }, - { - field: 'apiBaseUrl', - label: 'apiBaseUrl地址', - bottomHelpMessage: 'apiBaseUrl地址', - component: 'Input' - }, - { - field: 'apiForceUseReverse', - label: '强制使用ChatGPT反代', - bottomHelpMessage: '即使配置了proxy,依然使用ChatGPT反代', - component: 'Switch' - }, - { - field: 'useGPT4', - label: '使用GPT-4', - bottomHelpMessage: '使用GPT-4,注意试用配额较低,如果用不了就关掉', - component: 'Switch' - }, - { - label: '以下为智谱清言(ChatGLM)方式的配置。', - component: 'Divider' - }, - { - field: 'chatglmRefreshToken', - label: 'refresh token', - bottomHelpMessage: 'chatglm_refresh_token 6个月有效期', - component: 'Input' - }, - { - label: '以下为Claude API方式的配置', - component: 'Divider' - }, - { - field: 'claudeApiKey', - label: 'claude API Key', - bottomHelpMessage: '前往 https://console.anthropic.com/settings/keys 注册和生成。可以填写多个,用英文逗号隔开', - component: 'InputPassword' - }, - { - field: 'claudeApiModel', - label: 'claude API 模型', - bottomHelpMessage: '如 claude-3-sonnet-20240229 或 claude-3-opus-20240229', - component: 'Input' - }, - { - field: 'claudeApiBaseUrl', - label: 'claude API 反代', - component: 'Input' - }, - { - field: 'claudeApiMaxToken', - label: 'claude 最大回复token数', - component: 'InputNumber' - }, - { - field: 'claudeApiTemperature', - label: 'claude 温度', - component: 'InputNumber', - componentProps: { - min: 0, - max: 1 - } - }, - { - field: 'claudeSystemPrompt', - label: 'claude 设定', - component: 'InputTextArea' - }, - { - label: '以下为Claude2方式的配置', - component: 'Divider' - }, - { - field: 'claudeAIOrganizationId', - label: 'claude2 OrganizationId', - bottomHelpMessage: 'claude.ai的OrganizationId', - component: 'Input' - }, - { - field: 'claudeAISessionKey', - label: 'claude2 SessionKey', - bottomHelpMessage: 'claude.ai Cookie中的SessionKey', - component: 'Input' - }, - { - field: 'claudeAIReverseProxy', - label: 'claude2 反代', - bottomHelpMessage: 'claude.ai 的反代。或许可以参考https://github.com/ikechan8370/sydney-ws-proxy/tree/claude.ai搭建', - component: 'Input' - }, - { - field: 'claudeAIJA3', - label: 'claude2浏览器指纹', - bottomHelpMessage: 'claude.ai使用的浏览器TLS指纹,去https://scrapfly.io/web-scraping-tools/ja3-fingerprint或https://ja3.zone/check查看。如果用了反代就不用管', - component: 'Input' - }, - { - field: 'claudeAIUA', - label: 'claude2浏览器UA', - bottomHelpMessage: 'claude.ai使用的浏览器UA,https://scrapfly.io/web-scraping-tools/http2-fingerprint或https://ja3.zone/check查看。如果用了反代就不用管', - component: 'Input' - }, - { - field: 'claudeAITimeout', - label: 'claude2超时时间', - bottomHelpMessage: '等待响应的超时时间,单位为秒,默认为120。如果不使用反代而是使用代理可以适当调低。', - component: 'InputNumber' - }, - { - label: '以下为星火方式的配置', - component: 'Divider' - }, - { - field: 'xhmode', - label: '星火模式', - bottomHelpMessage: '设置星火使用的对话模式', - component: 'Select', - componentProps: { - options: [ - { label: '体验版', value: 'web' }, - { label: '讯飞星火认知大模型V1.5', value: 'api' }, - { label: '讯飞星火认知大模型V2.0', value: 'apiv2' }, - { label: '讯飞星火认知大模型V3.0', value: 'apiv3' }, - { label: '讯飞星火认知大模型V3.5', value: 'apiv3.5' }, - { label: '讯飞星火认知大模型V4.0', value: 'apiv4.0' }, - { label: '讯飞星火助手', value: 'assistants' } - ] - } - }, - { - field: 'xinghuoToken', - label: '星火Cookie', - bottomHelpMessage: '获取对话页面的ssoSessionId cookie。不要带等号和分号', - component: 'InputPassword' - }, - { - field: 'xhAppId', - label: 'AppId', - bottomHelpMessage: '应用页面获取', - component: 'Input' - }, - { - field: 'xhAPISecret', - label: 'APISecret', - bottomHelpMessage: '应用页面获取', - component: 'InputPassword' - }, - { - field: 'xhAPIKey', - label: '星火APIKey', - bottomHelpMessage: '应用页面获取', - component: 'InputPassword' - }, - { - field: 'xhAssistants', - label: '助手接口', - bottomHelpMessage: '助手页面获取', - component: 'Input' - }, - { - field: 'xhTemperature', - label: '核采样阈值', - bottomHelpMessage: '核采样阈值。用于决定结果随机性,取值越高随机性越强即相同的问题得到的不同答案的可能性越高', - component: 'InputNumber' - }, - { - field: 'xhMaxTokens', - label: '最大Token', - bottomHelpMessage: '模型回答的tokens的最大长度', - component: 'InputNumber' - }, - { - field: 'xhPromptSerialize', - label: '序列化设定', - bottomHelpMessage: '是否将设定内容进行json序列化', - component: 'Switch' - }, - { - field: 'xhPrompt', - label: '设定', - bottomHelpMessage: '若开启序列化,请传入json数据,例如[{ \"role\": \"user\", \"content\": \"现在是10点\" },{ \"role\": \"assistant\", \"content\": \"了解,现在10点了\" }]', - component: 'InputTextArea' - }, - { - field: 'xhRetRegExp', - label: '回复替换正则', - bottomHelpMessage: '要替换文本的正则', - component: 'Input' - }, - { - field: 'xhRetReplace', - label: '回复内容替换', - bottomHelpMessage: '替换回复内容中的文本', - component: 'Input' - }, - { - label: '以下为通义千问API方式的配置', - component: 'Divider' - }, - { - field: 'qwenApiKey', - label: '通义千问API Key', - component: 'InputPassword' - }, - { - field: 'qwenModel', - label: '通义千问模型', - bottomHelpMessage: '指明需要调用的模型,目前可选 qwen-turbo 和 qwen-plus', - component: 'Input' - }, - { - field: 'qwenTopP', - label: '通义千问topP', - bottomHelpMessage: '生成时,核采样方法的概率阈值。例如,取值为0.8时,仅保留累计概率之和大于等于0.8的概率分布中的token,作为随机采样的候选集。取值范围为(0,1.0),取值越大,生成的随机性越高;取值越低,生成的随机性越低。默认值 0.5。注意,取值不要大于等于1', - component: 'InputNumber' - }, - { - field: 'qwenTopK', - label: '通义千问topK', - bottomHelpMessage: '生成时,采样候选集的大小。例如,取值为50时,仅将单次生成中得分最高的50个token组成随机采样的候选集。取值越大,生成的随机性越高;取值越小,生成的确定性越高。注意:如果top_k的值大于100,top_k将采用默认值0,表示不启用top_k策略,此时仅有top_p策略生效。', - component: 'InputNumber' - }, - { - field: 'qwenSeed', - label: '通义千问Seed', - bottomHelpMessage: '生成时,随机数的种子,用于控制模型生成的随机性。如果使用相同的种子,每次运行生成的结果都将相同;当需要复现模型的生成结果时,可以使用相同的种子。seed参数支持无符号64位整数类型。默认值 0, 表示每次随机生成', - component: 'InputNumber' - }, - { - field: 'qwenTemperature', - label: '通义千问温度', - bottomHelpMessage: '用于控制随机性和多样性的程度。具体来说,temperature值控制了生成文本时对每个候选词的概率分布进行平滑的程度。较高的temperature值会降低概率分布的峰值,使得更多的低概率词被选择,生成结果更加多样化;而较低的temperature值则会增强概率分布的峰值,使得高概率词更容易被选择,生成结果更加确定。\n' + - '\n' + - '取值范围: (0, 2),系统默认值1.0', - component: 'InputNumber' - }, - { - field: 'qwenEnableSearch', - label: '通义千问允许搜索', - bottomHelpMessage: '生成时,是否参考夸克搜索的结果。注意:打开搜索并不意味着一定会使用搜索结果;如果打开搜索,模型会将搜索结果作为prompt,进而“自行判断”是否生成结合搜索结果的文本,默认为false', - component: 'Switch' - }, - { - label: '以下为Gemini方式的配置', - component: 'Divider' - }, - { - field: 'geminiKey', - label: 'API密钥', - bottomHelpMessage: '前往https://makersuite.google.com/app/apikey获取,如果有多个Keys,用英文逗号隔开', - component: 'InputPassword' - }, - { - field: 'geminiModel', - label: '模型', - bottomHelpMessage: '目前仅支持gemini-pro', - component: 'Input' - }, - { - field: 'geminiPrompt', - label: '设定', - component: 'InputTextArea' - }, - { - field: 'geminiBaseUrl', - label: 'Gemini反代', - bottomHelpMessage: '对https://generativelanguage.googleapis.com的反代', - component: 'Input' - }, - { - field: 'geminiForceToolKeywords', - label: 'gemini强制工具关键词', - bottomHelpMessage: 'gemini强制工具关键词,包含这里关键词的问题一定会调用工具。', - component: 'GTags', - componentProps: { - placeholder: '请输入强制工具关键词', - allowAdd: true, - allowDel: true, - showPrompt: true, - promptProps: { - content: '添加新的强制工具关键词', - okText: '添加', - rules: [ - { required: true, message: '强制工具关键词不能为空' } - ] - }, - valueParser: (value) => value.split(',') || [] - } - }, - { - label: '以下为一些杂项配置。', - component: 'Divider' - }, - { - field: 'blockWords', - label: '输出黑名单', - bottomHelpMessage: '检查输出结果中是否有违禁词,如果存在黑名单中的违禁词则不输出。英文逗号隔开', - component: 'InputTextArea' - }, - { - field: 'promptBlockWords', - label: '输入黑名单', - bottomHelpMessage: '检查输入结果中是否有违禁词,如果存在黑名单中的违禁词则不输出。英文逗号隔开', - component: 'InputTextArea' - }, - { - field: 'whitelist', - label: '对话白名单', - bottomHelpMessage: '默认设置为添加群号。优先级高于黑名单。\n' + - '注意:需要添加QQ号时在前面添加^(例如:^123456),此全局添加白名单,即除白名单以外的所有人都不能使用插件对话。\n' + - '如果需要在某个群里独享moment,即群聊中只有白名单上的qq号能用,则使用(群号^qq)的格式(例如:123456^123456)。\n' + - '白名单优先级:混合制 > qq > 群号。\n' + - '黑名单优先级: 群号 > qq > 混合制。', - component: 'Input' - }, - { - field: 'blacklist', - label: '对话黑名单', - bottomHelpMessage: '参考白名单设置规则。', - component: 'Input' - }, - { - field: 'imgOcr', - label: '图片识别', - bottomHelpMessage: '是否识别消息中图片的文字内容,需要同时包含图片和消息才生效', - component: 'Switch' - }, - { - field: 'enablePrivateChat', - label: '是否允许私聊机器人', - component: 'Switch' - }, - { - field: 'defaultUsePicture', - label: '全局图片模式', - bottomHelpMessage: '全局默认以图片形式回复', - component: 'Switch' - }, - { - field: 'defaultUseTTS', - label: '全局语音模式', - bottomHelpMessage: '全局默认以语音形式回复,使用默认角色音色', - component: 'Switch' - }, - { - field: 'ttsMode', - label: '语音模式源', - bottomHelpMessage: '语音模式下使用何种语音源进行文本->音频转换', - component: 'Select', - componentProps: { - options: [ - { - label: 'vits-uma-genshin-honkai', - value: 'vits-uma-genshin-honkai' - }, - { - label: '微软Azure', - value: 'azure' - }, - { - label: 'VoiceVox', - value: 'voicevox' - } - ] - } - }, - { - field: 'defaultTTSRole', - label: 'vits默认角色', - bottomHelpMessage: 'vits-uma-genshin-honkai语音模式下,未指定角色时使用的角色。若留空,将使用随机角色回复。若用户通过指令指定了角色,将忽略本设定', - component: 'Select', - componentProps: { - options: [{ - label: '随机', - value: '随机' - }].concat(speakers.map(s => { return { label: s, value: s } })) - } - }, - { - field: 'azureTTSSpeaker', - label: 'Azure默认角色', - bottomHelpMessage: '微软Azure语音模式下,未指定角色时使用的角色。若用户通过指令指定了角色,将忽略本设定', - component: 'Select', - componentProps: { - options: [{ - label: '随机', - value: '随机' - }, - ...azureRoleList.flatMap(item => [ - item.roleInfo - ]).map(s => ({ - label: s, - value: s - }))] - } - }, - { - field: 'voicevoxTTSSpeaker', - label: 'VoiceVox默认角色', - bottomHelpMessage: 'VoiceVox语音模式下,未指定角色时使用的角色。若留空,将使用随机角色回复。若用户通过指令指定了角色,将忽略本设定', - component: 'Select', - componentProps: { - options: [{ - label: '随机', - value: '随机' - }, - ...voxRoleList.flatMap(item => [ - ...item.styles.map(style => `${item.name}-${style.name}`), - item.name - ]).map(s => ({ - label: s, - value: s - }))] - } - }, - { - field: 'ttsRegex', - label: '语音过滤正则表达式', - bottomHelpMessage: '语音模式下,配置此项以过滤不想被读出来的内容。表达式测试地址:https://www.runoob.com/regexp/regexp-syntax.html', - component: 'Input' - }, - { - field: 'ttsAutoFallbackThreshold', - label: '语音转文字阈值', - helpMessage: '语音模式下,字数超过这个阈值就降级为文字', - bottomHelpMessage: '语音转为文字的阈值', - component: 'InputNumber', - componentProps: { - min: 0, - max: 299 - } - }, - { - field: 'alsoSendText', - label: '语音同时发送文字', - bottomHelpMessage: '语音模式下,同时发送文字版,避免音质较低听不懂', - component: 'Switch' - }, - { - field: 'autoJapanese', - label: 'vits模式日语输出', - bottomHelpMessage: '使用vits语音时,将机器人的文字回复翻译成日文后获取语音。' + - '若想使用插件的翻译功能,发送"#chatgpt翻译帮助"查看使用方法,支持图片翻译,引用翻译...', - component: 'Switch' - }, - { - field: 'autoUsePicture', - label: '长文本自动转图片', - bottomHelpMessage: '字数大于阈值会自动用图片发送,即使是文本模式', - component: 'Switch' - }, - { - field: 'autoUsePictureThreshold', - label: '自动转图片阈值', - helpMessage: '长文本自动转图片开启后才生效', - bottomHelpMessage: '自动转图片的字数阈值', - component: 'InputNumber', - componentProps: { - min: 0 - } - }, - { - field: 'conversationPreserveTime', - label: '对话保留时长', - helpMessage: '单位:秒', - bottomHelpMessage: '每个人发起的对话保留时长。超过这个时长没有进行对话,再进行对话将开启新的对话。', - component: 'InputNumber', - componentProps: { - min: 0 - } - }, - { - field: 'groupMerge', - label: '群组消息合并', - bottomHelpMessage: '开启后,群聊消息将被视为同一对话', - component: 'Switch' - }, - { - field: 'quoteReply', - label: '图片引用消息', - bottomHelpMessage: '在回复图片时引用原始消息', - component: 'Switch' - }, - { - field: 'showQRCode', - label: '启用二维码', - bottomHelpMessage: '在图片模式中启用二维码。该对话内容将被发送至第三方服务器以进行渲染展示,如果不希望对话内容被上传到第三方服务器请关闭此功能', - component: 'Switch' - }, - { - field: 'drawCD', - label: '绘图CD', - helpMessage: '单位:秒', - bottomHelpMessage: '绘图指令的CD时间,主人不受限制', - component: 'InputNumber', - componentProps: { - min: 0 - } - }, - { - field: 'enableDraw', - label: '绘图功能开关', - component: 'Switch' - }, - { - label: '以下为Suno音乐合成的配置。', - component: 'Divider' - }, - { - field: 'sunoSessToken', - label: 'sunoSessToken', - bottomHelpMessage: 'suno的__sess token,需要与sunoClientToken一一对应数量相同,多个用逗号隔开', - component: 'InputTextArea' - }, - { - field: 'sunoClientToken', - label: 'sunoClientToken', - bottomHelpMessage: 'suno的__client token,需要与sunoSessToken一一对应数量相同,多个用逗号隔开', - component: 'InputTextArea' - }, - { - label: '以下为杂七杂八的配置', - component: 'Divider' - }, - // { - // field: '2captchaToken', - // label: '验证码平台Token', - // bottomHelpMessage: '可注册2captcha实现跳过验证码,收费服务但很便宜。否则可能会遇到验证码而卡住', - // component: 'InputPassword' - // }, - { - field: 'ttsSpace', - label: 'vits-uma-genshin-honkai语音转换API地址', - bottomHelpMessage: '前往duplicate空间https://huggingface.co/spaces/ikechan8370/vits-uma-genshin-honkai后查看api地址', - component: 'Input' - }, - { - field: 'voicevoxSpace', - label: 'voicevox语音转换API地址', - bottomHelpMessage: '可使用https://2ndelement-voicevox.hf.space, 也可github搜索voicevox-engine自建', - component: 'Input' - }, - { - field: 'azureTTSKey', - label: 'Azure语音服务密钥', - component: 'Input' - }, - { - field: 'azureTTSRegion', - label: 'Azure语音服务区域', - bottomHelpMessage: '例如japaneast', - component: 'Input' - }, - { - field: 'azureTTSEmotion', - label: 'Azure情绪多样化', - bottomHelpMessage: '切换角色后使用"#chatgpt使用设定xxx"重新开始对话以更新不同角色的情绪配置。支持使用不同的说话风格回复,各个角色支持说话风格详情:https://speech.microsoft.com/portal/voicegallery', - component: 'Switch' - }, - { - field: 'enhanceAzureTTSEmotion', - label: 'Azure情绪纠正', - bottomHelpMessage: '当机器人未使用或使用了不支持的说话风格时,将在对话中提醒机器人。注意:bing模式开启此项后有概率增大触发抱歉的机率,且不要单独开启此项。', - component: 'Switch' - }, - { - field: 'huggingFaceReverseProxy', - label: '语音转换huggingface反代', - bottomHelpMessage: '没有就空着', - component: 'Input' - }, - { - field: 'cloudTranscode', - label: '云转码API地址', - bottomHelpMessage: '目前只支持node-silk语音转码,可在本地node-silk无法使用时尝试使用云端资源转码', - component: 'Input' - }, - { - field: 'cloudMode', - label: '云转码API发送数据模式', - bottomHelpMessage: '默认发送数据链接,如果你部署的是本地vits服务或使用的是微软azure,请改为文件', - component: 'Select', - componentProps: { - options: [ - { label: '文件', value: 'file' }, - { label: '链接', value: 'url' } - // { label: '数据', value: 'buffer' } - ] - } - }, - { - field: 'noiseScale', - label: 'noiseScale', - bottomHelpMessage: '控制情感变化程度', - component: 'InputNumber', - componentProps: { - min: 0, - max: 1 - } - }, - { - field: 'noiseScaleW', - label: 'noiseScaleW', - bottomHelpMessage: '控制音素发音长度', - component: 'InputNumber', - componentProps: { - min: 0, - max: 1 - } - }, - { - field: 'lengthScale', - label: 'lengthScale', - bottomHelpMessage: '控制整体语速', - component: 'InputNumber', - componentProps: { - min: 0, - max: 2 - } - }, - { - field: 'initiativeChatGroups', - label: '主动发起聊天群聊的群号', - bottomHelpMessage: '在这些群聊里会不定时主动说一些随机的打招呼的话,用英文逗号隔开。必须配置了OpenAI Key', - component: 'Input' - }, - { - field: 'helloPrompt', - label: '打招呼prompt', - bottomHelpMessage: '将会用这段文字询问ChatGPT,由ChatGPT给出随机的打招呼文字', - component: 'Input' - }, - { - field: 'helloInterval', - label: '打招呼间隔(小时)', - component: 'InputNumber', - componentProps: { - min: 1, - max: 24 - } - }, - { - field: 'helloProbability', - label: '打招呼的触发概率(%)', - bottomHelpMessage: '设置为100则每次经过间隔时间必定触发主动打招呼事件。', - component: 'InputNumber', - componentProps: { - min: 0, - max: 100 - } - }, - { - field: 'emojiBaseURL', - label: '合成emoji的API地址,默认谷歌厨房', - component: 'Input' - }, - { - field: 'bymRate', - label: '伪人模式触发概率,单位为%', - component: 'InputNumber', - componentProps: { - min: 0, - max: 100 - } - }, - { - field: 'bymDisableGroup', - label: '伪人禁用群', - bottomHelpMessage: '设置在该群禁用伪人模式', - component: "GTags", - componentProps: { - placeholder: '请输入群号', - allowAdd: true, - allowDel: true, - valueParser: ((value) => value.split(',') || []), - }, - }, - { - field: 'bymMode', - label: '伪人模型', - component: 'Select', - componentProps: { - options: [ - { label: 'Gemini(推荐)', value: 'gemini' }, - { label: '通义千问', value: 'qwen' }, - { label: 'OpenAI API', value: 'api' }, - { label: '星火', value: 'xh' }, - { label: 'Claude', value: 'claude' } - ] - } - }, - { - field: 'bymPreset', - label: '伪人模式的额外预设', - component: 'Input' - }, - { - field: 'bymFuckPrompt', - label: '伪人模式骂人反击的设定词', - component: 'Input' - }, - { - field: 'bymFuckList', - label: '伪人模式反击的触发词', - bottomHelpMessage: '请输入用于伪人模式下骂人反击的触发词,每个词组将被单独处理', - component: 'GTags', - componentProps: { - placeholder: '请输入反击触发词', - allowAdd: true, - allowDel: true, - showPrompt: true, - promptProps: { - content: '添加新的反击触发词', - okText: '添加', - rules: [ - { required: true, message: '触发词不能为空' } - ] - }, - valueParser: (value) => value.split(',') || [] - } - }, - { - label: '以下为Azure chatGPT的配置', - component: 'Divider' - }, - { - field: 'azApiKey', - label: 'Azure API Key', - bottomHelpMessage: '管理密钥,用于访问Azure的API接口', - component: 'InputPassword' - }, - { - field: 'azureUrl', - label: '端点地址', - bottomHelpMessage: 'https://xxxx.openai.azure.com/', - component: 'Input' - }, - { - field: 'azureDeploymentName', - label: '部署名称', - bottomHelpMessage: '创建部署时输入的名称', - component: 'Input' - }, - { - label: '以下为后台与渲染相关配置', - component: 'Divider' - }, - { - field: 'serverPort', - label: '系统Api服务端口', - bottomHelpMessage: '系统Api服务开启的端口号,如需外网访问请将系统防火墙和服务器防火墙对应端口开放,修改后请重启', - component: 'InputNumber' - }, - { - field: 'serverHost', - label: '系统服务访问域名', - bottomHelpMessage: '使用域名代替公网ip,适用于有服务器和域名的朋友避免暴露ip使用', - component: 'Input' - }, - { - field: 'viewHost', - label: '渲染服务器地址', - bottomHelpMessage: '可选择第三方渲染服务器', - component: 'Input' - }, - { - field: 'chatViewWidth', - label: '图片渲染宽度', - bottomHelpMessage: '聊天页面渲染窗口的宽度', - component: 'InputNumber' - }, - { - field: 'cloudRender', - label: '云渲染', - bottomHelpMessage: '是否使用云资源进行图片渲染,需要开放服务器端口后才能使用,不支持旧版本渲染', - component: 'Switch' - }, - { - field: 'chatViewBotName', - label: 'Bot命名', - bottomHelpMessage: '新渲染模式强制修改Bot命名', - component: 'Input' - }, - { - field: 'groupAdminPage', - label: '允许群获取后台地址', - bottomHelpMessage: '是否允许群获取后台地址,关闭后将只能私聊获取', - component: 'Switch' - }, - { - field: 'live2d', - label: 'Live2D显示', - bottomHelpMessage: '开启Live2D显示', - component: 'Switch' - }, - { - field: 'live2dModel', - label: 'Live2D模型', - bottomHelpMessage: '选择Live2D使用的模型', - component: 'Input' - }, - { - field: 'amapKey', - label: '高德APIKey', - bottomHelpMessage: '用于查询天气', - component: 'Input' - }, - { - field: 'azSerpKey', - label: 'Azure search key', - bottomHelpMessage: 'https://www.microsoft.com/en-us/bing/apis/bing-web-search-api', - component: 'Input' - }, - { - field: 'serpSource', - label: '搜索来源,azure需填写key,ikechan8370为作者自备源', - component: 'Select', - componentProps: { - options: [ - { label: 'Azure', value: 'azure' }, - { label: 'ikechan8370', value: 'ikechan8370' } - // { label: '数据', value: 'buffer' } - ] - } - }, - { - field: 'extraUrl', - label: '额外工具url', - bottomHelpMessage: '(测试期间提供一个公益接口,一段时间后撤掉)参考搭建:https://github.com/ikechan8370/chatgpt-plugin-extras', - component: 'Input' - }, - { - field: 'githubAPIKey', - label: 'github Access Token', - bottomHelpMessage: '去https://github.com/settings/personal-access-tokens生成。用于提高AI调用github工具的Rate Limit', - component: 'Input' } ], // 获取配置数据方法(用于前端填充显示数据) @@ -1152,18 +59,6 @@ export function supportGuoba () { } if (Config[keyPath] !== value) { Config[keyPath] = value } } - // 正确储存azureRoleSelect结果 - const azureSpeaker = azureRoleList.find(config => { - let i = config.roleInfo || config.code - if (i === data.azureTTSSpeaker) { - return config - } else { - return false - } - }) - if (typeof azureSpeaker === 'object' && azureSpeaker !== null) { - Config.azureTTSSpeaker = azureSpeaker.code - } return Result.ok({}, '保存成功~') } } diff --git a/index.js b/index.js index 6993a38..246273f 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,6 @@ import fs from 'node:fs' -import { Config } from './utils/config.js' -import { createServer, runServer } from './server/index.js' - +import ChatGPTConfig from './config/config.js' +import { initChaite } from './models/chaite/cloud.js' logger.info('**************************************') logger.info('chatgpt-plugin加载中') @@ -36,17 +35,9 @@ for (let i in files) { global.chatgpt = { } -// 启动服务器 -if (Config.enableToolbox) { - logger.info('开启工具箱配置项,工具箱启动中') - await createServer() - await runServer() - logger.info('工具箱启动成功') -} else { - logger.info('提示:当前配置未开启chatgpt工具箱,可通过锅巴或`#chatgpt开启工具箱`指令开启') -} +initChaite() logger.info('chatgpt-plugin加载成功') -logger.info(`当前版本${Config.version}`) +logger.info(`当前版本${ChatGPTConfig.version}`) logger.info('仓库地址 https://github.com/ikechan8370/chatgpt-plugin') logger.info('文档地址 https://www.yunzai.chat') logger.info('插件群号 559567232') diff --git a/models/chaite/channel_storage.js b/models/chaite/channel_storage.js index 379d1ab..d0d1577 100644 --- a/models/chaite/channel_storage.js +++ b/models/chaite/channel_storage.js @@ -1,18 +1,13 @@ import ChatGPTStorage from '../storage.js' +import { ChaiteStorage } from 'chaite' -/** - * @returns {import('chaite').ChannelsStorage} - */ -export async function createChannelsStorage () { - return new LowDBChannelStorage(ChatGPTStorage) -} - -class LowDBChannelStorage { +class LowDBChannelStorage extends ChaiteStorage { /** * * @param { LowDBStorage } storage */ - constructor (storage) { + constructor (storage = ChatGPTStorage) { + super() this.storage = storage /** * 集合 @@ -21,54 +16,50 @@ class LowDBChannelStorage { this.collection = this.storage.collection('channel') } - async saveChannel (channel) { - await this.collection.insert(channel) - } - - async getChannel (id) { - return this.collection.collection() + /** + * + * @param {string} key + * @returns {Promise} + */ + async getItem (key) { + return this.collection.findOne({ id: key }) } /** * - * @param name - * @returns {Promise} + * @param {string} id + * @param {import('chaite').Channel} channel + * @returns {Promise} */ - async getChannelByName (name) { - return this.collection.find({ name }) - } - - async deleteChannel (name) { - await this.collection.delete({ name }) + async setItem (id, channel) { + if (id) { + await this.collection.updateById(id, channel) + return id + } + const result = await this.collection.insert(channel) + return result.id } /** - * 获取所有渠道 - * @param {string?} model + * + * @param {string} key + * @returns {Promise} + */ + async removeItem (key) { + await this.collection.deleteById(key) + } + + /** + * * @returns {Promise} */ - async getAllChannels (model) { - if (model) { - return this.collection.find({ 'options.model': model }) - } + async listItems () { return this.collection.findAll() } - /** - * - * @param {import('chaite').ClientType} type - * @returns {Promise} - */ - async getChannelByType (type) { - return this.collection.find({ type }) - } - - /** - * - * @param {'enabled' | 'disabled'} status - * @returns {Promise<*>} - */ - async getChannelByStatus (status) { - return this.collection.find({ status }) + async clear () { + await this.collection.deleteAll() } } + +export default new LowDBChannelStorage() diff --git a/models/chaite/chat_preset_storage.js b/models/chaite/chat_preset_storage.js index 7c4e615..9ef303c 100644 --- a/models/chaite/chat_preset_storage.js +++ b/models/chaite/chat_preset_storage.js @@ -1,18 +1,16 @@ import ChatGPTStorage from '../storage.js' +import { ChaiteStorage } from 'chaite' /** - * @returns {import('chaite').ChatPresetsStorage} + * @extends {ChaiteStorage} */ -export async function createChatPresetsStorage () { - return new LowDBChatPresetsStorage(ChatGPTStorage) -} - -class LowDBChatPresetsStorage { +class LowDBChatPresetsStorage extends ChaiteStorage { /** * * @param { LowDBStorage } storage */ - constructor (storage) { + constructor (storage = ChatGPTStorage) { + super() this.storage = storage /** * 集合 @@ -23,31 +21,48 @@ class LowDBChatPresetsStorage { /** * - * @param {import('chaite').ChatPreset} preset - * @returns {Promise} + * @param key + * @returns {Promise} */ - async savePreset (preset) { - await this.collection.insert(preset) + async getItem (key) { + } /** * - * @param { string } name - * @returns {Promise} + * @param {string} id + * @param {import('chaite').ChatPreset} preset + * @returns {Promise} */ - async getPreset (name) { - return this.collection.findOne({ name }) + async setItem (id, preset) { + if (id) { + await this.collection.updateById(id, preset) + return id + } + const result = await this.collection.insert(preset) + return result.id + } + + /** + * + * @param {string} key + * @returns {Promise} + */ + async removeItem (key) { + await this.collection.deleteById(key) } /** * * @returns {Promise} */ - async getAllPresets () { + async listItems () { return this.collection.findAll() } - async deletePreset (name) { - await this.collection.delete({ name }) + async clear () { + await this.collection.deleteAll() } } + +export default new LowDBChatPresetsStorage() diff --git a/models/chaite/cloud.js b/models/chaite/cloud.js index 53b824d..2713cd0 100644 --- a/models/chaite/cloud.js +++ b/models/chaite/cloud.js @@ -1,28 +1,133 @@ -import { DefaultToolCloudService, ToolManager } from 'chaite' +import { Chaite, ChannelsManager, ChatPresetManager, DefaultChannelLoadBalancer, GeminiClient, OpenAIClient, ProcessorsManager, RAGManager, ToolManager } from 'chaite' import ChatGPTConfig from '../../config/config.js' -import { createToolsSettingsStorage } from './tool_settings_storage.js' -const ChatGPTToolCloudService = new DefaultToolCloudService(ChatGPTConfig.cloudBaseUrl, '', {}) -/** - * @type {import('chaite').ToolManager} - */ -let ChatGPTToolManager -ToolManager.getInstance(ChatGPTConfig.toolsDirPath, createToolsSettingsStorage(), ChatGPTToolCloudService).then((manager) => { - ChatGPTToolManager = manager -}) +import ChatGPTChannelStorage from './channel_storage.js' +import ChatPresetStorage from './chat_preset_storage.js' +import ChatGPTToolStorage from './tools_storage.js' +import ChatGPTProcessorsStorage from './processors_storage.js' +import { ChatGPTUserModeSelector } from './user_mode_selector.js' +import { LowDBUserStateStorage } from './user_state_storage.js' +import { LowDBHistoryManager } from './history_manager.js' +import { ChatGPTVectorDatabase } from './vector_database.js' /** * 认证,以便共享上传 * @param apiKey - * @returns {Promise} + * @returns {Promise | null} */ -export async function authCloud (apiKey) { - const user = await ChatGPTToolCloudService.authenticate(apiKey) - ChatGPTToolManager.setCloudService(ChatGPTToolCloudService) - return user +export async function authCloud (apiKey = ChatGPTConfig.cloudApiKey) { + await Chaite.getInstance().auth(apiKey) + return Chaite.getInstance().getToolsManager().cloudService.getUser() } -export default { - ChatGPTToolCloudService, - ChatGPTToolManager +/** + * + * @param {import('chaite').Channel} channel + * @returns {Promise} + */ +async function getIClientByChannel (channel) { + await channel.ready() + switch (channel.adapterType) { + case 'openai': { + return new OpenAIClient(channel.options) + } + case 'gemini': { + return new GeminiClient(channel.options) + } + case 'claude': { + throw new Error('claude doesn\'t support embedding') + } + } } +/** + * 初始化RAG管理器 + * @param {string} model + * @param {number} dimensions + */ +export async function initRagManager (model, dimensions) { + const vectorizer = new class { + async textToVector (text) { + const channels = await Chaite.getInstance().getChannelsManager().getChannelByModel(model) + if (channels.length === 0) { + throw new Error('No channel found for model: ' + model) + } + const channel = channels[0] + const client = await getIClientByChannel(channel) + const result = await client.getEmbedding(text) + return result.embeddings[0] + } + + /** + * + * @param {string[]} texts + * @returns {Promise[]>} + */ + async batchTextToVector (texts) { + const availableChannels = (await Chaite.getInstance().getChannelsManager().getAllChannels()).filter(c => c.models.includes(model)) + if (availableChannels.length === 0) { + throw new Error('No channel found for model: ' + model) + } + const channels = await Chaite.getInstance().getChannelsManager().getChannelsByModel(model, texts.length) + /** + * @type {import('chaite').IClient[]} + */ + const clients = await Promise.all(channels.map(({ channel }) => getIClientByChannel(channel))) + const results = [] + let startIndex = 0 + for (let i = 0; i < channels.length; i++) { + const { quantity } = channels[i] + const textsSlice = texts.slice(startIndex, startIndex + quantity) + const embeddings = await clients[i].getEmbedding(textsSlice, { + model, + dimensions + }) + results.push(...embeddings.embeddings) + startIndex += quantity + } + return results + } + }() + const ragManager = new RAGManager(ChatGPTVectorDatabase, vectorizer) + return Chaite.getInstance().setRAGManager(ragManager) +} + +export async function initChaite () { + const channelsManager = await ChannelsManager.init(ChatGPTChannelStorage, new DefaultChannelLoadBalancer()) + const toolsManager = await ToolManager.init(ChatGPTConfig.toolsDirPath, ChatGPTToolStorage) + const processorsManager = await ProcessorsManager.init(ChatGPTConfig.processorsDirPath, ChatGPTProcessorsStorage) + const chatPresetManager = await ChatPresetManager.init(ChatPresetStorage) + const userModeSelector = new ChatGPTUserModeSelector() + const userStateStorage = new LowDBUserStateStorage() + const historyManager = new LowDBHistoryManager() + let chaite = Chaite.init(channelsManager, toolsManager, processorsManager, chatPresetManager, + userModeSelector, userStateStorage, historyManager, logger) + logger.info('Chaite 初始化完成') + chaite.setCloudService(ChatGPTConfig.cloudBaseUrl) + logger.info('Chaite.Cloud 初始化完成') + ChatGPTConfig.cloudApiKey && await chaite.auth(ChatGPTConfig.cloudApiKey) + await initRagManager(ChatGPTConfig.embeddingModel, ChatGPTConfig.dimensions) + // 监听Chaite配置变化,同步需要同步的配置 + chaite.on('config-change', obj => { + const { key, newVal, oldVal } = obj + if (key === 'authKey') { + ChatGPTConfig.serverAuthKey = newVal + } + logger.debug(`Chaite config changed: ${key} from ${oldVal} to ${newVal}`) + }) + // 监听通过chaite对插件配置修改 + chaite.setUpdateConfigCallback(config => { + logger.debug('chatgpt-plugin config updated') + Object.keys(config).forEach(key => { + ChatGPTConfig[key] = config[key] + // 回传部分需要同步的配置,以防不一致 + if (key === 'serverAuthKey') { + chaite.getGlobalConfig().setAuthKey(config[key]) + } + }) + }) + // 授予Chaite获取插件配置的能力以便通过api放出 + chaite.setGetConfig(async () => { + return ChatGPTConfig + }) + logger.info('Chaite.RAGManager 初始化完成') +} diff --git a/models/chaite/history_manager.js b/models/chaite/history_manager.js new file mode 100644 index 0000000..d5b8981 --- /dev/null +++ b/models/chaite/history_manager.js @@ -0,0 +1,57 @@ +import { AbstractHistoryManager } from 'chaite' +import ChatGPTStorage from '../storage.js' + +export class LowDBHistoryManager extends AbstractHistoryManager { + /** + * + * @param { LowDBStorage } storage + */ + constructor (storage = ChatGPTStorage) { + super() + this.storage = storage + /** + * 集合 + * @type {LowDBCollection} + */ + this.collection = this.storage.collection('history') + } + + async saveHistory (message, conversationId) { + const historyObj = { ...message, conversationId } + if (message.id) { + await this.collection.updateById(message.id, historyObj) + } + await this.collection.insert(historyObj) + } + + /** + * + * @param messageId + * @param conversationId + * @returns {Promise} + */ + async getHistory (messageId, conversationId) { + if (messageId) { + const messages = [] + let currentId = messageId + while (currentId) { + const message = await this.collection.findOne({ id: currentId }) + if (!message) break + messages.unshift(message) + currentId = message.parentMessageId + } + return messages + } else if (conversationId) { + return this.collection.find({ conversationId }) + } + return this.collection.findAll() + } + + async deleteConversation (conversationId) { + await this.collection.delete({ conversationId }) + } + + async getOneHistory (messageId, conversationId) { + return this.collection.findOne({ id: messageId, conversationId }) + } +} diff --git a/models/chaite/processors_storage.js b/models/chaite/processors_storage.js new file mode 100644 index 0000000..8398144 --- /dev/null +++ b/models/chaite/processors_storage.js @@ -0,0 +1,68 @@ +import ChatGPTStorage from '../storage.js' +import { ChaiteStorage } from 'chaite' + +/** + * @extends {ChaiteStorage} + */ +class LowDBProcessorsStorage extends ChaiteStorage { + /** + * + * @param { LowDBStorage } storage + */ + constructor (storage = ChatGPTStorage) { + super() + this.storage = storage + /** + * 集合 + * @type {LowDBCollection} + */ + this.collection = this.storage.collection('processors') + } + + /** + * + * @param {string} key + * @returns {Promise} + */ + async getItem (key) { + return this.collection.findOne({ id: key }) + } + + /** + * + * @param {string} id + * @param {import('chaite').Processor} processor + * @returns {Promise} + */ + async setItem (id, processor) { + if (id) { + await this.collection.updateById(id, processor) + return id + } + const result = await this.collection.insert(processor) + return result.id + } + + /** + * + * @param {string} key + * @returns {Promise} + */ + async removeItem (key) { + await this.collection.deleteById(key) + } + + /** + * + * @returns {Promise} + */ + async listItems () { + return this.collection.findAll() + } + + async clear () { + await this.collection.deleteAll() + } +} + +export default new LowDBProcessorsStorage() diff --git a/models/chaite/tool_settings_storage.js b/models/chaite/tool_settings_storage.js deleted file mode 100644 index 2c55269..0000000 --- a/models/chaite/tool_settings_storage.js +++ /dev/null @@ -1,53 +0,0 @@ -import ChatGPTStorage from '../storage.js' - -/** - * @returns {import('chaite').ToolSettingsStorage} - */ -export function createToolsSettingsStorage () { - return new LowDBToolsSettingsStorage(ChatGPTStorage) -} - -class LowDBToolsSettingsStorage { - /** - * - * @param { LowDBStorage } storage - */ - constructor (storage) { - this.storage = storage - /** - * 集合 - * @type {LowDBCollection} - */ - this.collection = this.storage.collection('tool_settings') - } - - /** - * - * @param {import('chaite').ToolSettings} settings - * @returns {Promise} - */ - async saveToolSettings (settings) { - await this.collection.insert(settings) - } - - /** - * - * @param { string } name - * @returns {Promise} - */ - async getToolSettings (name) { - return this.collection.findOne({ name }) - } - - /** - * - * @returns {Promise} - */ - async getAllToolSettings () { - return this.collection.findAll() - } - - async deleteToolSettings (name) { - await this.collection.delete({ name }) - } -} diff --git a/models/chaite/tools_storage.js b/models/chaite/tools_storage.js new file mode 100644 index 0000000..435eacc --- /dev/null +++ b/models/chaite/tools_storage.js @@ -0,0 +1,68 @@ +import ChatGPTStorage from '../storage.js' +import { ChaiteStorage } from 'chaite' + +/** + * @extends {ChaiteStorage} + */ +class LowDBToolSettingsStorage extends ChaiteStorage { + /** + * + * @param { LowDBStorage } storage + */ + constructor (storage = ChatGPTStorage) { + super() + this.storage = storage + /** + * 集合 + * @type {LowDBCollection} + */ + this.collection = this.storage.collection('tools') + } + + /** + * + * @param {string} key + * @returns {Promise} + */ + async getItem (key) { + return this.collection.findOne({ id: key }) + } + + /** + * + * @param {string} id + * @param {import('chaite').ToolDTO} tools + * @returns {Promise} + */ + async setItem (id, tools) { + if (id) { + await this.collection.updateById(id, tools) + return id + } + const result = await this.collection.insert(tools) + return result.id + } + + /** + * + * @param {string} key + * @returns {Promise} + */ + async removeItem (key) { + await this.collection.deleteById(key) + } + + /** + * + * @returns {Promise} + */ + async listItems () { + return this.collection.findAll() + } + + async clear () { + await this.collection.deleteAll() + } +} + +export default new LowDBToolSettingsStorage() diff --git a/models/chaite/user_mode_selector.js b/models/chaite/user_mode_selector.js new file mode 100644 index 0000000..a96c586 --- /dev/null +++ b/models/chaite/user_mode_selector.js @@ -0,0 +1,12 @@ +import { AbstractUserModeSelector } from '../../../../../../../WebstormProjects/node-chaite/src/types/external.js' + +export class ChatGPTUserModeSelector extends AbstractUserModeSelector { + /** + * 根据e判断当前要使用的预设,非常灵活。 + * @param e + * @returns {Promise} + */ + getChatPreset (e) { + // todo + } +} diff --git a/models/chaite/user_state_storage.js b/models/chaite/user_state_storage.js new file mode 100644 index 0000000..450b55a --- /dev/null +++ b/models/chaite/user_state_storage.js @@ -0,0 +1,66 @@ +import ChatGPTStorage from '../storage.js' +import { ChaiteStorage } from 'chaite' + +/** + * @extends {ChaiteStorage} + */ +export class LowDBUserStateStorage extends ChaiteStorage { + /** + * + * @param {LowDBStorage} storage + */ + constructor (storage = ChatGPTStorage) { + super() + this.storage = storage + /** + * 集合 + * @type {LowDBCollection} + */ + this.collection = this.storage.collection('user_states') + } + + /** + * + * @param {string} key + * @returns {Promise} + */ + async getItem (key) { + return this.collection.findOne({ id: key }) + } + + /** + * + * @param {string} id + * @param {import('chaite').UserState} state + * @returns {Promise} + */ + async setItem (id, state) { + if (id) { + await this.collection.updateById(id, state) + return id + } + const result = await this.collection.insert(state) + return result.id + } + + /** + * + * @param {string} key + * @returns {Promise} + */ + async removeItem (key) { + await this.collection.deleteById(key) + } + + /** + * + * @returns {Promise} + */ + async listItems () { + return this.collection.findAll() + } + + async clear () { + await this.collection.deleteAll() + } +} diff --git a/models/chaite/vector_database.js b/models/chaite/vector_database.js index 46f54c7..ebaa062 100644 --- a/models/chaite/vector_database.js +++ b/models/chaite/vector_database.js @@ -1,36 +1,90 @@ -// todo -class FaissVectorDatabase { - constructor (index) { - this.index = index +import { LocalIndex } from 'vectra' +import { md5 } from '../../utils/common.js' + +/** + * 基于Vectra实现的简单向量数据库,作为默认实现 + */ +class VectraVectorDatabase { + constructor (indexFile) { + this.index = new LocalIndex(indexFile) + this.init() + } + + async init () { + if (!(await this.index.isIndexCreated())) { + await this.index.createIndex() + } } async addVector (vector, text) { + const id = md5(text) + await this.index.insertItem({ + vector, + id, + metadata: { text } + }) + return id } + /** + * + * @param vectors + * @param texts + * @returns {Promise} + */ async addVectors (vectors, texts) { + return await Promise.all(vectors.map((v, i) => this.addVector(v, texts[i]))) } + /** + * + * @param queryVector + * @param k + * @returns {Promise>} + */ async search (queryVector, k) { + const results = await this.index.queryItems(queryVector, k) + return results.map(r => ({ id: r.item.id, score: r.score, text: r.item.metadata.text })) } + /** + * + * @param id + * @returns {Promise<{ vector: number[], text: string } | null>} + */ async getVector (id) { + const result = await this.index.getItem(id) + return { + vector: result.vector, + text: result.metadata.text + } } async deleteVector (id) { + await this.index.deleteItem(id) + return true } async updateVector (id, newVector, newText) { + await this.index.upsertItem({ + id, + vector: newVector, + metadata: { text: newText } + }) + return true } async count () { + return (await this.index.getIndexStats()).items } async clear () { + await this.index.deleteIndex() } } /** - * 默认向量库 + * 默认向量库 todo * @type {import('chaite').VectorDatabase} */ -export const ChatGPTVectorDatabase = new FaissVectorDatabase() +export const ChatGPTVectorDatabase = new VectraVectorDatabase() diff --git a/models/storage.js b/models/storage.js index a39ce9d..233d76f 100644 --- a/models/storage.js +++ b/models/storage.js @@ -137,7 +137,7 @@ export class LowDBCollection { /** * 创建新文档 * @param {Object} doc 要插入的文档 - * @returns {Promise} 插入的文档(带ID) + * @returns {Promise} 插入的文档(带ID) */ async insert (doc) { // 生成唯一ID,如果没有提供 diff --git a/package.json b/package.json index bd66701..8171c95 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,11 @@ "type": "module", "author": "ikechan8370", "dependencies": { - "chaite": "^1.0.3", + "chaite": "^1.1.1", "keyv": "^5.3.1", "keyv-file": "^5.1.2", - "lowdb": "^7.0.1" + "lowdb": "^7.0.1", + "vectra": "^0.9.0" }, "pnpm": {} } diff --git a/utils/common.js b/utils/common.js new file mode 100644 index 0000000..7f1ecf4 --- /dev/null +++ b/utils/common.js @@ -0,0 +1,4 @@ +import * as crypto from 'node:crypto' +export function md5 (str) { + return crypto.createHash('md5').update(str).digest('hex') +}