This commit is contained in:
ikechan8370 2025-04-28 16:18:27 +08:00
parent c3b7127333
commit a3c74f82db
7 changed files with 615 additions and 5 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@ node_modules/
data/ data/
utils/processors utils/processors
utils/tools utils/tools
utils/triggers

View file

@ -166,6 +166,8 @@ class ChatGPTConfig {
dataDir: 'data', dataDir: 'data',
// 处理器目录,相对于插件下 // 处理器目录,相对于插件下
processorsDirPath: 'utils/processors', processorsDirPath: 'utils/processors',
// 触发器目录,相对于插件目录下
triggersDir: 'utils/triggers',
// 工具目录,相对于插件目录下 // 工具目录,相对于插件目录下
toolsDirPath: 'utils/tools', toolsDirPath: 'utils/tools',
// 云端API url // 云端API url

View file

@ -8,7 +8,8 @@ import {
ProcessorsManager, ProcessorsManager,
RAGManager, RAGManager,
ToolManager, ToolManager,
ToolsGroupManager ToolsGroupManager,
TriggerManager
} from 'chaite' } from 'chaite'
import ChatGPTConfig from '../../config/config.js' import ChatGPTConfig from '../../config/config.js'
import { LowDBChannelStorage } from './storage/lowdb/channel_storage.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 { SQLiteToolsGroupStorage } from './storage/sqlite/tool_groups_storage.js'
import { checkMigrate } from './storage/sqlite/migrate.js' import { checkMigrate } from './storage/sqlite/migrate.js'
import { SQLiteHistoryManager } from './storage/sqlite/history_manager.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 () { export async function initChaite () {
const storage = ChatGPTConfig.chaite.storage const storage = ChatGPTConfig.chaite.storage
let channelsStorage, chatPresetsStorage, toolsStorage, processorsStorage, userStateStorage, historyStorage, toolsGroupStorage let channelsStorage, chatPresetsStorage, toolsStorage, processorsStorage, userStateStorage, historyStorage, toolsGroupStorage, triggerStorage
switch (storage) { switch (storage) {
case 'sqlite': { case 'sqlite': {
const dbPath = path.join(dataDir, 'data.db') const dbPath = path.join(dataDir, 'data.db')
@ -145,6 +148,8 @@ export async function initChaite () {
await userStateStorage.initialize() await userStateStorage.initialize()
toolsGroupStorage = new SQLiteToolsGroupStorage(dbPath) toolsGroupStorage = new SQLiteToolsGroupStorage(dbPath)
await toolsGroupStorage.initialize() await toolsGroupStorage.initialize()
triggerStorage = new SQLiteTriggerStorage(dbPath)
await triggerStorage.initialize()
historyStorage = new SQLiteHistoryManager(dbPath, path.join(dataDir, 'images')) historyStorage = new SQLiteHistoryManager(dbPath, path.join(dataDir, 'images'))
await checkMigrate() await checkMigrate()
break break
@ -157,6 +162,7 @@ export async function initChaite () {
toolsStorage = new LowDBToolsStorage(ChatGPTStorage) toolsStorage = new LowDBToolsStorage(ChatGPTStorage)
processorsStorage = new LowDBProcessorsStorage(ChatGPTStorage) processorsStorage = new LowDBProcessorsStorage(ChatGPTStorage)
userStateStorage = new LowDBUserStateStorage(ChatGPTStorage) userStateStorage = new LowDBUserStateStorage(ChatGPTStorage)
triggerStorage = new LowDBTriggerStorage(ChatGPTStorage)
const ChatGPTHistoryStorage = (await import('storage/lowdb/storage.js')).ChatGPTHistoryStorage const ChatGPTHistoryStorage = (await import('storage/lowdb/storage.js')).ChatGPTHistoryStorage
await ChatGPTHistoryStorage.init() await ChatGPTHistoryStorage.init()
historyStorage = new LowDBHistoryManager(ChatGPTHistoryStorage) historyStorage = new LowDBHistoryManager(ChatGPTHistoryStorage)
@ -176,8 +182,14 @@ export async function initChaite () {
const processorsManager = await ProcessorsManager.init(processorsDir, processorsStorage) const processorsManager = await ProcessorsManager.init(processorsDir, processorsStorage)
const chatPresetManager = await ChatPresetManager.init(chatPresetsStorage) const chatPresetManager = await ChatPresetManager.init(chatPresetsStorage)
const toolsGroupManager = await ToolsGroupManager.init(toolsGroupStorage) 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() 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) userModeSelector, userStateStorage, historyStorage, logger)
logger.info('Chaite 初始化完成') logger.info('Chaite 初始化完成')
chaite.setCloudService(ChatGPTConfig.chaite.cloudBaseUrl) chaite.setCloudService(ChatGPTConfig.chaite.cloudBaseUrl)

View file

@ -0,0 +1,122 @@
import { ChaiteStorage, TriggerDTO } from 'chaite'
/**
* @extends {ChaiteStorage<import('chaite').TriggerDTO>}
*/
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<import('chaite').TriggerDTO>}
*/
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<string>}
*/
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<void>}
*/
async removeItem (key) {
await this.collection.deleteById(key)
}
/**
* 获取所有触发器
* @returns {Promise<import('chaite').TriggerDTO[]>}
*/
async listItems () {
const list = await this.collection.findAll()
return list.map(item => new TriggerDTO({}).fromString(JSON.stringify(item)))
}
/**
* 根据条件筛选触发器
* @param {Record<string, unknown>} filter
* @returns {Promise<import('chaite').TriggerDTO[]>}
*/
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<import('chaite').TriggerDTO[]>}
*/
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<void>}
*/
async clear () {
await this.collection.deleteAll()
}
}
export default LowDBTriggerStorage

View file

@ -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<import('chaite').TriggerDTO>}
*/
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<void>}
*/
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<import('chaite').TriggerDTO>}
*/
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<string>}
*/
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<void>}
*/
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<import('chaite').TriggerDTO[]>}
*/
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<string, unknown>} filter 筛选条件
* @returns {Promise<import('chaite').TriggerDTO[]>}
*/
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<import('chaite').TriggerDTO[]>}
*/
async listItemsByInQuery (query) {
await this.ensureInitialized()
// 如果没有查询条<E8AFA2><E69DA1><EFBFBD>返回所有
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<void>}
*/
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<void>}
*/
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

View file

@ -4,7 +4,7 @@
"type": "module", "type": "module",
"author": "ikechan8370", "author": "ikechan8370",
"dependencies": { "dependencies": {
"chaite": "^1.3.11", "chaite": "^1.4.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"keyv": "^5.3.1", "keyv": "^5.3.1",
"keyv-file": "^5.1.2", "keyv-file": "^5.1.2",

View file

@ -1,7 +1,6 @@
import { Chaite } from 'chaite' import { Chaite } from 'chaite'
import common from '../../../lib/common/common.js' import common from '../../../lib/common/common.js'
import fetch from 'node-fetch' import fetch from 'node-fetch'
import res from 'express/lib/response.js'
/** /**
* 将e中的消息转换为chaite的UserMessage * 将e中的消息转换为chaite的UserMessage