feat: use sqlite instead of lowdb

This commit is contained in:
ikechan8370 2025-04-09 16:27:21 +08:00
parent 9c41251164
commit fd197abb33
22 changed files with 3519 additions and 39 deletions

View file

@ -0,0 +1,104 @@
import { ChaiteStorage, Channel } from 'chaite'
export class LowDBChannelStorage extends ChaiteStorage {
/**
*
* @param { LowDBStorage } storage
*/
constructor (storage) {
super()
this.storage = storage
/**
* 集合
* @type {LowDBCollection}
*/
this.collection = this.storage.collection('channel')
}
/**
*
* @param {string} key
* @returns {Promise<import('chaite').Channel>}
*/
async getItem (key) {
const obj = await this.collection.findOne({ id: key })
if (!obj) {
return null
}
return new Channel(obj)
}
/**
*
* @param {string} id
* @param {import('chaite').Channel} channel
* @returns {Promise<string>}
*/
async setItem (id, channel) {
if (id && await this.getItem(id)) {
await this.collection.updateById(id, channel)
return id
}
const result = await this.collection.insert(channel)
return result.id
}
/**
*
* @param {string} key
* @returns {Promise<void>}
*/
async removeItem (key) {
await this.collection.deleteById(key)
}
/**
*
* @returns {Promise<import('chaite').Channel[]>}
*/
async listItems () {
const list = await this.collection.findAll()
return list.map(item => new Channel({}).fromString(JSON.stringify(item)))
}
/**
*
* @param {Record<string, unknown>} filter
* @returns {Promise<import('chaite').Channel[]>}
*/
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
})
}
/**
*
* @param {Array<{
* field: string;
* values: unknown[];
* }>} query
* @returns {Promise<import('chaite').Channel[]>}
*/
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
})
}
async clear () {
await this.collection.deleteAll()
}
}

View file

@ -0,0 +1,107 @@
import { ChaiteStorage, ChatPreset } from 'chaite'
/**
* @extends {ChaiteStorage<import('chaite').ChatPreset>}
*/
export class LowDBChatPresetsStorage extends ChaiteStorage {
/**
*
* @param { LowDBStorage } storage
*/
constructor (storage) {
super()
this.storage = storage
/**
* 集合
* @type {LowDBCollection}
*/
this.collection = this.storage.collection('chat_presets')
}
/**
*
* @param key
* @returns {Promise<import('chaite').ChatPreset>}
*/
async getItem (key) {
const obj = await this.collection.findOne({ id: key })
if (!obj) {
return null
}
return new ChatPreset(obj)
}
/**
*
* @param {string} id
* @param {import('chaite').ChatPreset} preset
* @returns {Promise<string>}
*/
async setItem (id, preset) {
if (id && await this.getItem(id)) {
await this.collection.updateById(id, preset)
return id
}
const result = await this.collection.insert(preset)
return result.id
}
/**
*
* @param {string} key
* @returns {Promise<void>}
*/
async removeItem (key) {
await this.collection.deleteById(key)
}
/**
*
* @returns {Promise<import('chaite').ChatPreset[]>}
*/
async listItems () {
const list = await this.collection.findAll()
return list.map(item => new ChatPreset({}).fromString(JSON.stringify(item)))
}
/**
*
* @param {Record<string, unknown>} filter
* @returns {Promise<import('chaite').ChatPreset[]>}
*/
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
})
}
/**
*
* @param {Array<{
* field: string;
* values: unknown[];
* }>} query
* @returns {Promise<import('chaite').ChatPreset[]>}
*/
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
})
}
async clear () {
await this.collection.deleteAll()
}
}

View file

@ -0,0 +1,56 @@
import { AbstractHistoryManager } from 'chaite'
export class LowDBHistoryManager extends AbstractHistoryManager {
/**
*
* @param { LowDBStorage } storage
*/
constructor (storage) {
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<import('chaite').HistoryMessage[]>}
*/
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.parentId
}
return messages
} else if (conversationId) {
return this.collection.find({ conversationId })
}
return []
}
async deleteConversation (conversationId) {
await this.collection.delete({ conversationId })
}
async getOneHistory (messageId, conversationId) {
return this.collection.findOne({ id: messageId, conversationId })
}
}

View file

@ -0,0 +1,107 @@
import { ChaiteStorage, ProcessorDTO } from 'chaite'
/**
* @extends {ChaiteStorage<import('chaite').Processor>}
*/
export class LowDBProcessorsStorage extends ChaiteStorage {
/**
*
* @param { LowDBStorage } storage
*/
constructor (storage) {
super()
this.storage = storage
/**
* 集合
* @type {LowDBCollection}
*/
this.collection = this.storage.collection('processors')
}
/**
*
* @param {string} key
* @returns {Promise<import('chaite').Processor>}
*/
async getItem (key) {
const obj = await this.collection.findOne({ id: key })
if (!obj) {
return null
}
return new ProcessorDTO(obj)
}
/**
*
* @param {string} id
* @param {import('chaite').Processor} processor
* @returns {Promise<string>}
*/
async setItem (id, processor) {
if (id && await this.getItem(id)) {
await this.collection.updateById(id, processor)
return id
}
const result = await this.collection.insert(processor)
return result.id
}
/**
*
* @param {string} key
* @returns {Promise<void>}
*/
async removeItem (key) {
await this.collection.deleteById(key)
}
/**
*
* @returns {Promise<import('chaite').Processor[]>}
*/
async listItems () {
const list = await this.collection.findAll()
return list.map(item => new ProcessorDTO({}).fromString(JSON.stringify(item)))
}
/**
*
* @param {Record<string, unknown>} filter
* @returns {Promise<import('chaite').Processor[]>}
*/
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
})
}
/**
*
* @param {Array<{
* field: string;
* values: unknown[];
* }>} query
* @returns {Promise<import('chaite').Processor[]>}
*/
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
})
}
async clear () {
await this.collection.deleteAll()
}
}

View file

@ -0,0 +1,374 @@
// storage.js written by sonnet
import { Low } from 'lowdb'
import { JSONFile } from 'lowdb/node'
import path from 'path'
import fs from 'fs'
import { dataDir } from '../../../../utils/common.js'
/**
* 基于 LowDB 的简单存储类提供 CRUD 和条件查询功能
*/
export class LowDBStorage {
/**
* 创建一个新的存储实例
* @param {Object} options 配置选项
* @param {string} options.filename 数据文件名称
* @param {string} options.directory 数据目录默认为当前目录下的 data 文件夹
*/
constructor (options = {}) {
const { filename = 'db.json', directory = path.join(process.cwd(), 'data') } = options
// 确保目录存在
if (!fs.existsSync(directory)) {
fs.mkdirSync(directory, { recursive: true })
}
this.filePath = path.join(directory, filename)
this.adapter = new JSONFile(this.filePath)
this.db = new Low(this.adapter, { collections: {} })
this.initialized = false
}
/**
* 初始化存储
* @returns {Promise<LowDBStorage>} 当前存储实例
*/
async init () {
// 读取数据文件,如果不存在则创建默认结构
await this.db.read()
this.db.data ||= { collections: {} }
await this.db.write()
this.initialized = true
return this
}
/**
* 获取或创建一个集合
* @param {string} name 集合名称
* @returns {LowDBCollection} 集合实例
*/
collection (name) {
this._checkInit()
// 确保集合存在
if (!this.db.data.collections[name]) {
this.db.data.collections[name] = []
this.db.write()
}
return new LowDBCollection(this, name)
}
/**
* 列出所有集合名称
* @returns {string[]} 集合名称列表
*/
listCollections () {
this._checkInit()
return Object.keys(this.db.data.collections)
}
/**
* 删除一个集合
* @param {string} name 要删除的集合名称
* @returns {Promise<boolean>} 是否成功删除
*/
async dropCollection (name) {
this._checkInit()
if (this.db.data.collections[name]) {
delete this.db.data.collections[name]
await this.db.write()
return true
}
return false
}
/**
* 检查存储是否已初始化
* @private
*/
_checkInit () {
if (!this.initialized) {
throw new Error('存储尚未初始化,请先调用 init() 方法')
}
}
}
/**
* 集合类提供对特定数据集合的操作
*/
export class LowDBCollection {
/**
* 创建一个集合实例
* @param {LowDBStorage} storage 所属存储实例
* @param {string} name 集合名称
*/
constructor (storage, name) {
this.storage = storage
this.name = name
}
/**
* 获取集合数据引用
* @private
*/
get _collection () {
return this.storage.db.data.collections[this.name]
}
/**
* 保存数据到存储
* @private
*/
async _save () {
return this.storage.db.write()
}
/**
* 生成唯一ID
* @private
*/
_generateId () {
return Date.now().toString(36) + Math.random().toString(36).substring(2, 15)
}
/**
* 创建新文档
* @param {Object} doc 要插入的文档
* @returns {Promise<Object & {id: string}>} 插入的文档带ID
*/
async insert (doc) {
// 生成唯一ID如果没有提供
if (!doc.id) {
doc.id = this._generateId()
}
// 加上时间戳
if (!doc.createdAt) {
doc.createdAt = new Date().toISOString()
}
doc.updatedAt = new Date().toISOString()
// 添加到集合
this._collection.push(doc)
await this._save()
return doc
}
/**
* 批量插入多个文档
* @param {Object[]} docs 要插入的文档数组
* @returns {Promise<Object[]>} 插入的文档带ID
*/
async insertMany (docs) {
const inserted = []
for (const doc of docs) {
inserted.push(await this.insert(doc))
}
return inserted
}
/**
* 根据ID查找单个文档
* @param {string} id 文档ID
* @returns {Promise<Object|null>} 查找到的文档或null
*/
async findById (id) {
return this._collection.find(doc => doc.id === id) || null
}
/**
* 返回集合中的所有文档
* @returns {Promise<Object[]>} 文档数组
*/
async findAll () {
return [...this._collection]
}
/**
* 根据条件查找文档
* @param {Object} query 查询条件字段等值匹配
* @returns {Promise<Object[]>} 匹配的文档数组
*/
async find (query = {}) {
return this._collection.filter(doc => {
for (const key in query) {
const value = query[key]
// 处理嵌套属性 (例如 user.profile.name)
if (key.includes('.')) {
const parts = key.split('.')
let current = doc
for (let i = 0; i < parts.length; i++) {
if (current === undefined || current === null) return false
current = current[parts[i]]
}
if (current !== value) return false
} else if (doc[key] !== value) {
return false
}
}
return true
})
}
/**
* 根据条件查找单个文档
* @param {Object} query 查询条件
* @returns {Promise<Object|null>} 第一个匹配的文档或null
*/
async findOne (query = {}) {
const results = await this.find(query)
return results.length > 0 ? results[0] : null
}
/**
* 使用自定义函数进行高级查询
* @param {Function} filterFn 过滤函数
* @returns {Promise<Object[]>} 匹配的文档数组
*/
async findWhere (filterFn) {
return this._collection.filter(filterFn)
}
/**
* 根据ID更新文档
* @param {string} id 文档ID
* @param {Object} updates 要更新的字段
* @returns {Promise<Object|null>} 更新后的文档或null
*/
async updateById (id, updates) {
const index = this._collection.findIndex(doc => doc.id === id)
if (index === -1) return null
// 防止覆盖ID
const { id: _, ...safeUpdates } = updates
// 更新文档
const updatedDoc = {
...this._collection[index],
...safeUpdates,
updatedAt: new Date().toISOString()
}
this._collection[index] = updatedDoc
await this._save()
return updatedDoc
}
/**
* 根据条件更新文档
* @param {Object} query 查询条件
* @param {Object} updates 要更新的字段
* @returns {Promise<number>} 更新的文档数量
*/
async update (query, updates) {
const matches = await this.find(query)
let updated = 0
for (const doc of matches) {
await this.updateById(doc.id, updates)
updated++
}
return updated
}
/**
* 根据ID删除文档
* @param {string} id 文档ID
* @returns {Promise<boolean>} 是否成功删除
*/
async deleteById (id) {
const index = this._collection.findIndex(doc => doc.id === id)
if (index === -1) return false
this._collection.splice(index, 1)
await this._save()
return true
}
/**
* 根据条件删除文档
* @param {Object} query 查询条件
* @returns {Promise<number>} 删除的文档数量
*/
async delete (query) {
const before = this._collection.length
const remaining = this._collection.filter(doc => {
for (const key in query) {
if (doc[key] !== query[key]) {
return true // 保留不匹配的
}
}
return false // 删除匹配的
})
this.storage.db.data.collections[this.name] = remaining
await this._save()
return before - remaining.length
}
/**
* 清空集合中的所有文档
* @returns {Promise<number>} 删除的文档数量
*/
async deleteAll () {
const count = this._collection.length
this.storage.db.data.collections[this.name] = []
await this._save()
return count
}
/**
* 返回集合中文档的数量
* @returns {Promise<number>} 文档数量
*/
async count (query = {}) {
if (Object.keys(query).length === 0) {
return this._collection.length
}
const matches = await this.find(query)
return matches.length
}
}
const storageLocation = path.resolve(dataDir, 'storage.json')
if (!fs.existsSync(storageLocation)) {
fs.writeFileSync(storageLocation, JSON.stringify({ collections: {} }))
}
const ChatGPTStorage = new LowDBStorage({
filename: 'storage.json',
directory: dataDir
})
if (ChatGPTStorage.db.data.collections.history) {
ChatGPTStorage.dropCollection('history').then(() => {
logger.debug('drop older version history collection')
}).catch(err => {
logger.warn('failed to drop older version history collection', err)
})
}
export const ChatGPTHistoryStorage = new LowDBStorage({
filename: 'history.json',
directory: dataDir
})
export default ChatGPTStorage

View file

@ -0,0 +1,70 @@
import { ChaiteStorage, ToolsGroupDTO } from 'chaite'
/**
* @extends {ChaiteStorage<import('chaite').ToolsGroupDTO>}
*/
export class LowDBToolsGroupDTOsStorage extends ChaiteStorage {
/**
*
* @param { LowDBStorage } storage
*/
constructor (storage) {
super()
this.storage = storage
/**
* 集合
* @type {LowDBCollection}
*/
this.collection = this.storage.collection('tool_groups')
}
/**
*
* @param key
* @returns {Promise<import('chaite').ToolsGroupDTO>}
*/
async getItem (key) {
const obj = await this.collection.findOne({ id: key })
if (!obj) {
return null
}
return new ToolsGroupDTO(obj)
}
/**
*
* @param {string} id
* @param {import('chaite').ToolsGroupDTO} preset
* @returns {Promise<string>}
*/
async setItem (id, preset) {
if (id && await this.getItem(id)) {
await this.collection.updateById(id, preset)
return id
}
const result = await this.collection.insert(preset)
return result.id
}
/**
*
* @param {string} key
* @returns {Promise<void>}
*/
async removeItem (key) {
await this.collection.deleteById(key)
}
/**
*
* @returns {Promise<import('chaite').ToolsGroupDTO[]>}
*/
async listItems () {
const list = await this.collection.findAll()
return list.map(item => new ToolsGroupDTO({}).fromString(JSON.stringify(item)))
}
async clear () {
await this.collection.deleteAll()
}
}

View file

@ -0,0 +1,111 @@
import { ChaiteStorage, ToolDTO } from 'chaite'
/**
* @extends {ChaiteStorage<import('chaite').ToolDTO>}
*/
export class LowDBToolsStorage extends ChaiteStorage {
getName () {
return 'LowDBToolsStorage'
}
/**
*
* @param { LowDBStorage } storage
*/
constructor (storage) {
super()
this.storage = storage
/**
* 集合
* @type {LowDBCollection}
*/
this.collection = this.storage.collection('tools')
}
/**
*
* @param {string} key
* @returns {Promise<import('chaite').ToolDTO>}
*/
async getItem (key) {
const obj = await this.collection.findOne({ id: key })
if (!obj) {
return null
}
return new ToolDTO(obj)
}
/**
*
* @param {string} id
* @param {import('chaite').ToolDTO} tools
* @returns {Promise<string>}
*/
async setItem (id, tools) {
if (id && await this.getItem(id)) {
await this.collection.updateById(id, tools)
return id
}
const result = await this.collection.insert(tools)
return result.id
}
/**
*
* @param {string} key
* @returns {Promise<void>}
*/
async removeItem (key) {
await this.collection.deleteById(key)
}
/**
*
* @returns {Promise<import('chaite').ToolDTO[]>}
*/
async listItems () {
const list = await this.collection.findAll()
return list.map(item => new ToolDTO({}).fromString(JSON.stringify(item)))
}
/**
*
* @param {Record<string, unknown>} filter
* @returns {Promise<import('chaite').ToolDTO[]>}
*/
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
})
}
/**
*
* @param {Array<{
* field: string;
* values: unknown[];
* }>} query
* @returns {Promise<import('chaite').ToolDTO[]>}
*/
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
})
}
async clear () {
await this.collection.deleteAll()
}
}

View file

@ -0,0 +1,84 @@
import { ChaiteStorage } from 'chaite'
import * as crypto from 'node:crypto'
/**
* 继承UserState
*/
export class YunzaiUserState {
constructor (userId, nickname, card, conversationId = crypto.randomUUID()) {
this.userId = userId
this.nickname = nickname
this.card = card
this.conversations = []
this.settings = {}
this.current = {
conversationId,
messageId: ''
}
}
}
/**
* @extends {ChaiteStorage<import('chaite').UserState>}
*/
export class LowDBUserStateStorage extends ChaiteStorage {
/**
*
* @param {LowDBStorage} storage
*/
constructor (storage) {
super()
this.storage = storage
/**
* 集合
* @type {LowDBCollection}
*/
this.collection = this.storage.collection('user_states')
}
/**
*
* @param {string} key
* @returns {Promise<import('chaite').UserState>}
*/
async getItem (key) {
return this.collection.findOne({ id: key })
}
/**
*
* @param {string} id
* @param {import('chaite').UserState} state
* @returns {Promise<string>}
*/
async setItem (id, state) {
if (id && await this.getItem(id)) {
await this.collection.updateById(id, state)
return id
}
state.id = id
const result = await this.collection.insert(state)
return result.id
}
/**
*
* @param {string} key
* @returns {Promise<void>}
*/
async removeItem (key) {
await this.collection.deleteById(key)
}
/**
*
* @returns {Promise<import('chaite').UserState[]>}
*/
async listItems () {
return this.collection.findAll()
}
async clear () {
await this.collection.deleteAll()
}
}