diff --git a/.gitignore b/.gitignore index 6db5cad..80a3da6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules/ data/ utils/processors utils/tools +utils/triggers diff --git a/config/config.js b/config/config.js index 26834cf..acf8f6d 100644 --- a/config/config.js +++ b/config/config.js @@ -166,6 +166,8 @@ class ChatGPTConfig { dataDir: 'data', // 处理器目录,相对于插件下 processorsDirPath: 'utils/processors', + // 触发器目录,相对于插件目录下 + triggersDir: 'utils/triggers', // 工具目录,相对于插件目录下 toolsDirPath: 'utils/tools', // 云端API url diff --git a/models/chaite/cloud.js b/models/chaite/cloud.js index 0a9545d..5aaf1e3 100644 --- a/models/chaite/cloud.js +++ b/models/chaite/cloud.js @@ -8,7 +8,8 @@ import { ProcessorsManager, RAGManager, ToolManager, - ToolsGroupManager + ToolsGroupManager, + TriggerManager } from 'chaite' import ChatGPTConfig from '../../config/config.js' import { LowDBChannelStorage } from './storage/lowdb/channel_storage.js' @@ -31,6 +32,8 @@ import { SQLiteUserStateStorage } from './storage/sqlite/user_state_storage.js' import { SQLiteToolsGroupStorage } from './storage/sqlite/tool_groups_storage.js' import { checkMigrate } from './storage/sqlite/migrate.js' import { SQLiteHistoryManager } from './storage/sqlite/history_manager.js' +import SQLiteTriggerStorage from './storage/sqlite/trigger_storage.js' +import LowDBTriggerStorage from './storage/lowdb/trigger_storage,.js' /** * 认证,以便共享上传 @@ -129,7 +132,7 @@ export async function initRagManager (model, dimensions) { export async function initChaite () { const storage = ChatGPTConfig.chaite.storage - let channelsStorage, chatPresetsStorage, toolsStorage, processorsStorage, userStateStorage, historyStorage, toolsGroupStorage + let channelsStorage, chatPresetsStorage, toolsStorage, processorsStorage, userStateStorage, historyStorage, toolsGroupStorage, triggerStorage switch (storage) { case 'sqlite': { const dbPath = path.join(dataDir, 'data.db') @@ -145,6 +148,8 @@ export async function initChaite () { await userStateStorage.initialize() toolsGroupStorage = new SQLiteToolsGroupStorage(dbPath) await toolsGroupStorage.initialize() + triggerStorage = new SQLiteTriggerStorage(dbPath) + await triggerStorage.initialize() historyStorage = new SQLiteHistoryManager(dbPath, path.join(dataDir, 'images')) await checkMigrate() break @@ -157,6 +162,7 @@ export async function initChaite () { toolsStorage = new LowDBToolsStorage(ChatGPTStorage) processorsStorage = new LowDBProcessorsStorage(ChatGPTStorage) userStateStorage = new LowDBUserStateStorage(ChatGPTStorage) + triggerStorage = new LowDBTriggerStorage(ChatGPTStorage) const ChatGPTHistoryStorage = (await import('storage/lowdb/storage.js')).ChatGPTHistoryStorage await ChatGPTHistoryStorage.init() historyStorage = new LowDBHistoryManager(ChatGPTHistoryStorage) @@ -176,8 +182,14 @@ export async function initChaite () { const processorsManager = await ProcessorsManager.init(processorsDir, processorsStorage) const chatPresetManager = await ChatPresetManager.init(chatPresetsStorage) const toolsGroupManager = await ToolsGroupManager.init(toolsGroupStorage) + const triggersDir = path.resolve('./plugins/chatgpt-plugin', ChatGPTConfig.chaite.triggersDir) + if (!fs.existsSync(triggersDir)) { + fs.mkdirSync(triggersDir, { recursive: true }) + } + const triggerManager = new TriggerManager(triggersDir, triggerStorage) + await triggerManager.initialize() const userModeSelector = new ChatGPTUserModeSelector() - let chaite = Chaite.init(channelsManager, toolsManager, processorsManager, chatPresetManager, toolsGroupManager, + let chaite = Chaite.init(channelsManager, toolsManager, processorsManager, chatPresetManager, toolsGroupManager, triggerManager, userModeSelector, userStateStorage, historyStorage, logger) logger.info('Chaite 初始化完成') chaite.setCloudService(ChatGPTConfig.chaite.cloudBaseUrl) diff --git a/models/chaite/storage/lowdb/trigger_storage,.js b/models/chaite/storage/lowdb/trigger_storage,.js new file mode 100644 index 0000000..b444d0c --- /dev/null +++ b/models/chaite/storage/lowdb/trigger_storage,.js @@ -0,0 +1,122 @@ +import { ChaiteStorage, TriggerDTO } from 'chaite' + +/** + * @extends {ChaiteStorage} + */ +export class LowDBTriggerStorage extends ChaiteStorage { + getName () { + return 'LowDBTriggerStorage' + } + + /** + * @param {LowDBStorage} storage + */ + constructor (storage) { + super() + this.storage = storage + /** + * 集合 + * @type {LowDBCollection} + */ + this.collection = this.storage.collection('triggers') + } + + /** + * 获取单个触发器 + * @param {string} key + * @returns {Promise} + */ + async getItem (key) { + const obj = await this.collection.findOne({ id: key }) + if (!obj) { + return null + } + return new TriggerDTO(obj) + } + + /** + * 保存触发器 + * @param {string} id + * @param {import('chaite').TriggerDTO} trigger + * @returns {Promise} + */ + async setItem (id, trigger) { + // 设置或更新时间戳 + if (!trigger.createdAt) { + trigger.createdAt = new Date().toISOString() + } + trigger.updatedAt = new Date().toISOString() + + if (id && await this.getItem(id)) { + await this.collection.updateById(id, trigger) + return id + } + const result = await this.collection.insert(trigger) + return result.id + } + + /** + * 删除触发器 + * @param {string} key + * @returns {Promise} + */ + async removeItem (key) { + await this.collection.deleteById(key) + } + + /** + * 获取所有触发器 + * @returns {Promise} + */ + async listItems () { + const list = await this.collection.findAll() + return list.map(item => new TriggerDTO({}).fromString(JSON.stringify(item))) + } + + /** + * 根据条件筛选触发器 + * @param {Record} filter + * @returns {Promise} + */ + async listItemsByEqFilter (filter) { + const allList = await this.listItems() + return allList.filter(item => { + for (const key in filter) { + if (item[key] !== filter[key]) { + return false + } + } + return true + }) + } + + /** + * 根据IN条件筛选触发器 + * @param {Array<{ + * field: string; + * values: unknown[]; + * }>} query + * @returns {Promise} + */ + async listItemsByInQuery (query) { + const allList = await this.listItems() + return allList.filter(item => { + for (const { field, values } of query) { + if (!values.includes(item[field])) { + return false + } + } + return true + }) + } + + /** + * 清空所有触发器 + * @returns {Promise} + */ + async clear () { + await this.collection.deleteAll() + } +} + +export default LowDBTriggerStorage diff --git a/models/chaite/storage/sqlite/trigger_storage.js b/models/chaite/storage/sqlite/trigger_storage.js new file mode 100644 index 0000000..634d7a9 --- /dev/null +++ b/models/chaite/storage/sqlite/trigger_storage.js @@ -0,0 +1,474 @@ +import { ChaiteStorage, TriggerDTO } from 'chaite' +import sqlite3 from 'sqlite3' +import path from 'path' +import fs from 'fs' +import { generateId } from '../../../../utils/common.js' + +/** + * @extends {ChaiteStorage} + */ +export class SQLiteTriggerStorage extends ChaiteStorage { + getName () { + return 'SQLiteTriggerStorage' + } + + /** + * + * @param {string} dbPath 数据库文件路径 + */ + constructor (dbPath) { + super() + this.dbPath = dbPath + this.db = null + this.initialized = false + this.tableName = 'triggers' + } + + /** + * 初始化数据库连接和表结构 + * @returns {Promise} + */ + async initialize () { + if (this.initialized) return + + return new Promise((resolve, reject) => { + // 确保目录存在 + const dir = path.dirname(this.dbPath) + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }) + } + + this.db = new sqlite3.Database(this.dbPath, async (err) => { + if (err) { + return reject(err) + } + + // 创建触发器表,将主要属性分列存储 + this.db.run(`CREATE TABLE IF NOT EXISTS ${this.tableName} ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + modelType TEXT, + code TEXT, + cloudId INTEGER, + embedded INTEGER, + uploader TEXT, + createdAt TEXT, + updatedAt TEXT, + md5 TEXT, + status TEXT, + permission TEXT, + isOneTime INTEGER, + extraData TEXT -- 存储其他额外数据的JSON + )`, (err) => { + if (err) { + reject(err) + } else { + // 创建索引以提高查询性能 + this.db.run(`CREATE INDEX IF NOT EXISTS idx_triggers_name ON ${this.tableName} (name)`, (err) => { + if (err) { + reject(err) + } else { + this.db.run(`CREATE INDEX IF NOT EXISTS idx_triggers_status ON ${this.tableName} (status)`, (err) => { + if (err) { + reject(err) + } else { + this.initialized = true + resolve() + } + }) + } + }) + } + }) + }) + }) + } + + /** + * 确保数据库已初始化 + */ + async ensureInitialized () { + if (!this.initialized) { + await this.initialize() + } + } + + /** + * 将 TriggerDTO 对象转换为数据库记录 + * @param {import('chaite').TriggerDTO} trigger + * @returns {Object} 数据库记录 + */ + _triggerToRecord (trigger) { + // 提取主要字段,剩余的放入extraData + const { + id, name, description, modelType, code, cloudId, + embedded, uploader, createdAt, updatedAt, md5, + status, permission, isOneTime, ...rest + } = trigger + + // 序列化上传者对象 + const uploaderStr = uploader ? JSON.stringify(uploader) : null + + return { + id: id || '', + name: name || '', + description: description || '', + modelType: modelType || 'executable', + code: code || null, + cloudId: cloudId || null, + embedded: embedded ? 1 : 0, + uploader: uploaderStr, + createdAt: createdAt || '', + updatedAt: updatedAt || '', + md5: md5 || '', + status: status || 'enabled', + permission: permission || 'public', + isOneTime: isOneTime ? 1 : 0, + extraData: Object.keys(rest).length > 0 ? JSON.stringify(rest) : null + } + } + + /** + * 将数据库记录转换为 TriggerDTO 对象 + * @param {Object} record 数据库记录 + * @returns {import('chaite').TriggerDTO} TriggerDTO对象 + */ + _recordToTrigger (record) { + // 若记录不存在则返回null + if (!record) return null + + // 解析上传者 + let uploader = null + try { + if (record.uploader) { + uploader = JSON.parse(record.uploader) + } + } catch (e) { + // 解析错误,使用null + } + + // 解析额外数据 + let extraData = {} + try { + if (record.extraData) { + extraData = JSON.parse(record.extraData) + } + } catch (e) { + // 解析错误,使用空对象 + } + + // 构造基本对象 + const triggerData = { + id: record.id, + name: record.name, + description: record.description, + modelType: record.modelType, + code: record.code, + cloudId: record.cloudId, + embedded: Boolean(record.embedded), + uploader, + createdAt: record.createdAt, + updatedAt: record.updatedAt, + md5: record.md5, + status: record.status, + permission: record.permission, + isOneTime: Boolean(record.isOneTime), + ...extraData + } + + return new TriggerDTO(triggerData) + } + + /** + * 获取单个触发器 + * @param {string} key 触发器ID + * @returns {Promise} + */ + async getItem (key) { + await this.ensureInitialized() + + return new Promise((resolve, reject) => { + this.db.get(`SELECT * FROM ${this.tableName} WHERE id = ?`, [key], (err, row) => { + if (err) { + return reject(err) + } + + const trigger = this._recordToTrigger(row) + resolve(trigger) + }) + }) + } + + /** + * 保存触发器 + * @param {string} id 触发器ID + * @param {import('chaite').TriggerDTO} trigger 触发器对象 + * @returns {Promise} + */ + async setItem (id, trigger) { + await this.ensureInitialized() + + if (!id) { + id = generateId() + } + + // 加上时间戳 + if (!trigger.createdAt) { + trigger.createdAt = new Date().toISOString() + } + + trigger.updatedAt = new Date().toISOString() + + // 转换为数据库记录 + const record = this._triggerToRecord(trigger) + record.id = id // 确保ID是指定的ID + + // 构建插入或更新SQL + const fields = Object.keys(record) + const placeholders = fields.map(() => '?').join(', ') + const updates = fields.map(field => `${field} = ?`).join(', ') + const values = fields.map(field => record[field]) + const duplicateValues = [...values] // 用于ON CONFLICT时的更新 + + return new Promise((resolve, reject) => { + this.db.run( + `INSERT INTO ${this.tableName} (${fields.join(', ')}) + VALUES (${placeholders}) + ON CONFLICT(id) DO UPDATE SET ${updates}`, + [...values, ...duplicateValues], + function (err) { + if (err) { + return reject(err) + } + resolve(id) + } + ) + }) + } + + /** + * 删除触发器 + * @param {string} key 触发器ID + * @returns {Promise} + */ + async removeItem (key) { + await this.ensureInitialized() + + return new Promise((resolve, reject) => { + this.db.run(`DELETE FROM ${this.tableName} WHERE id = ?`, [key], (err) => { + if (err) { + return reject(err) + } + resolve() + }) + }) + } + + /** + * 查询所有触发器 + * @returns {Promise} + */ + async listItems () { + await this.ensureInitialized() + + return new Promise((resolve, reject) => { + this.db.all(`SELECT * FROM ${this.tableName}`, (err, rows) => { + if (err) { + return reject(err) + } + + const triggers = rows.map(row => this._recordToTrigger(row)).filter(Boolean) + resolve(triggers) + }) + }) + } + + /** + * 根据条件筛选触发器 + * @param {Record} filter 筛选条件 + * @returns {Promise} + */ + async listItemsByEqFilter (filter) { + await this.ensureInitialized() + + // 如果没有筛选条件,返回所有 + if (!filter || Object.keys(filter).length === 0) { + return this.listItems() + } + + // 尝试使用SQL字段直接过滤 + const directFields = ['id', 'name', 'description', 'modelType', 'cloudId', 'status', 'permission'] + const sqlFilters = [] + const sqlParams = [] + const extraFilters = {} + let hasExtraFilters = false + + // 区分数据库字段和额外字段 + for (const key in filter) { + const value = filter[key] + + // 如果是直接支持的字段,构建SQL条件 + if (directFields.includes(key)) { + sqlFilters.push(`${key} = ?`) + sqlParams.push(value) + } else if (key === 'embedded') { + // embedded 字段需要特殊处理为 0/1 + sqlFilters.push('embedded = ?') + sqlParams.push(value ? 1 : 0) + } else if (key === 'isOneTime') { + // isOneTime 字段需要特殊处理为 0/1 + sqlFilters.push('isOneTime = ?') + sqlParams.push(value ? 1 : 0) + } else { + // 其他字段需要在结果中进一步过滤 + extraFilters[key] = value + hasExtraFilters = true + } + } + + // 构建SQL查询 + let sql = `SELECT * FROM ${this.tableName}` + if (sqlFilters.length > 0) { + sql += ` WHERE ${sqlFilters.join(' AND ')}` + } + + return new Promise((resolve, reject) => { + this.db.all(sql, sqlParams, (err, rows) => { + if (err) { + return reject(err) + } + + let triggers = rows.map(row => this._recordToTrigger(row)).filter(Boolean) + + // 如果有需要在内存中过滤的额外字段 + if (hasExtraFilters) { + triggers = triggers.filter(trigger => { + for (const key in extraFilters) { + if (trigger[key] !== extraFilters[key]) { + return false + } + } + return true + }) + } + + resolve(triggers) + }) + }) + } + + /** + * 根据IN条件筛选触发器 + * @param {Array<{ field: string; values: unknown[]; }>} query + * @returns {Promise} + */ + async listItemsByInQuery (query) { + await this.ensureInitialized() + + // 如果没有查询条���,返回所有 + if (!query || query.length === 0) { + return this.listItems() + } + + // 尝试使用SQL IN子句来优化查询 + const directFields = ['id', 'name', 'description', 'modelType', 'cloudId', 'status', 'permission'] + const sqlFilters = [] + const sqlParams = [] + const extraQueries = [] + + // 处理每个查询条件 + for (const { field, values } of query) { + if (values.length === 0) continue + + // 如果是直接支持的字段,使用SQL IN子句 + if (directFields.includes(field)) { + const placeholders = values.map(() => '?').join(', ') + sqlFilters.push(`${field} IN (${placeholders})`) + sqlParams.push(...values) + } else if (field === 'embedded') { + const boolValues = values.map(v => v ? 1 : 0) + const placeholders = boolValues.map(() => '?').join(', ') + sqlFilters.push(`embedded IN (${placeholders})`) + sqlParams.push(...boolValues) + } else if (field === 'isOneTime') { + const boolValues = values.map(v => v ? 1 : 0) + const placeholders = boolValues.map(() => '?').join(', ') + sqlFilters.push(`isOneTime IN (${placeholders})`) + sqlParams.push(...boolValues) + } else { + // 其他字段在内存中过滤 + extraQueries.push({ field, values }) + } + } + + // 构建SQL查询 + let sql = `SELECT * FROM ${this.tableName}` + if (sqlFilters.length > 0) { + sql += ` WHERE ${sqlFilters.join(' AND ')}` + } + + return new Promise((resolve, reject) => { + this.db.all(sql, sqlParams, (err, rows) => { + if (err) { + return reject(err) + } + + let triggers = rows.map(row => this._recordToTrigger(row)).filter(Boolean) + + // 如果有需要在内存中过滤的条件 + if (extraQueries.length > 0) { + triggers = triggers.filter(trigger => { + for (const { field, values } of extraQueries) { + if (!values.includes(trigger[field])) { + return false + } + } + return true + }) + } + + resolve(triggers) + }) + }) + } + + /** + * 清空表中所有数据 + * @returns {Promise} + */ + async clear () { + await this.ensureInitialized() + + return new Promise((resolve, reject) => { + this.db.run(`DELETE FROM ${this.tableName}`, (err) => { + if (err) { + return reject(err) + } + resolve() + }) + }) + } + + /** + * 关闭数据库连接 + * @returns {Promise} + */ + async close () { + if (!this.db) return Promise.resolve() + + return new Promise((resolve, reject) => { + this.db.close(err => { + if (err) { + reject(err) + } else { + this.initialized = false + this.db = null + resolve() + } + }) + }) + } +} + +export default SQLiteTriggerStorage diff --git a/package.json b/package.json index fd3bcfd..62933ed 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "type": "module", "author": "ikechan8370", "dependencies": { - "chaite": "^1.3.11", + "chaite": "^1.4.0", "js-yaml": "^4.1.0", "keyv": "^5.3.1", "keyv-file": "^5.1.2", diff --git a/utils/message.js b/utils/message.js index 6969530..bc2cc77 100644 --- a/utils/message.js +++ b/utils/message.js @@ -1,7 +1,6 @@ import { Chaite } from 'chaite' import common from '../../../lib/common/common.js' import fetch from 'node-fetch' -import res from 'express/lib/response.js' /** * 将e中的消息转换为chaite的UserMessage