This commit is contained in:
Baobhan Sith
2025-04-26 15:20:37 +08:00
parent 93b8863fdd
commit e269f40754
80 changed files with 868 additions and 1528 deletions
@@ -1,17 +1,12 @@
// packages/backend/src/repositories/appearance.repository.ts
// Import new async helpers and the instance getter, ensuring getDb is included
import { getDbInstance, runDb, getDb, allDb } from '../database/connection';
import { AppearanceSettings, UpdateAppearanceDto } from '../types/appearance.types';
import { defaultUiTheme } from '../config/default-themes';
// Import findThemeById from terminal theme repository for validation
import { findThemeById as findTerminalThemeById } from './terminal-theme.repository';
import * as sqlite3 from 'sqlite3'; // Import sqlite3 for Database type hint
import * as sqlite3 from 'sqlite3';
const TABLE_NAME = 'appearance_settings';
// Remove SETTINGS_ID as the table is key-value based
// const SETTINGS_ID = 1;
// Define the expected row structure from the database (key-value)
interface DbAppearanceSettingsRow {
key: string;
value: string;
@@ -19,13 +14,13 @@ interface DbAppearanceSettingsRow {
updated_at: number;
}
// Helper function to map DB rows (key-value pairs) to AppearanceSettings object
const mapRowsToAppearanceSettings = (rows: DbAppearanceSettingsRow[]): AppearanceSettings => {
const settings: Partial<AppearanceSettings> = {};
let latestUpdatedAt = 0;
for (const row of rows) {
// Update latestUpdatedAt
// 更新 latestUpdatedAt
if (row.updated_at > latestUpdatedAt) {
latestUpdatedAt = row.updated_at;
}
@@ -35,7 +30,6 @@ const mapRowsToAppearanceSettings = (rows: DbAppearanceSettingsRow[]): Appearanc
settings.customUiTheme = row.value;
break;
case 'activeTerminalThemeId':
// Ensure value is parsed as number or null
const parsedId = parseInt(row.value, 10);
settings.activeTerminalThemeId = isNaN(parsedId) ? null : parsedId;
break;
@@ -49,19 +43,17 @@ const mapRowsToAppearanceSettings = (rows: DbAppearanceSettingsRow[]): Appearanc
settings.editorFontSize = parseInt(row.value, 10);
break;
case 'terminalBackgroundImage':
settings.terminalBackgroundImage = row.value || undefined; // Use undefined if empty string
settings.terminalBackgroundImage = row.value || undefined;
break;
case 'pageBackgroundImage':
settings.pageBackgroundImage = row.value || undefined; // Use undefined if empty string
settings.pageBackgroundImage = row.value || undefined;
break;
// Add cases for other potential keys if needed
}
}
// Merge with defaults for any missing keys and add _id and updatedAt
const defaults = getDefaultAppearanceSettings(); // Get defaults
const defaults = getDefaultAppearanceSettings();
return {
_id: 'global_appearance', // Use a fixed string ID for the conceptual global settings
_id: 'global_appearance', // 全局外观设置的固定 ID
customUiTheme: settings.customUiTheme ?? defaults.customUiTheme,
activeTerminalThemeId: settings.activeTerminalThemeId ?? defaults.activeTerminalThemeId,
terminalFontFamily: settings.terminalFontFamily ?? defaults.terminalFontFamily,
@@ -69,59 +61,60 @@ const mapRowsToAppearanceSettings = (rows: DbAppearanceSettingsRow[]): Appearanc
editorFontSize: settings.editorFontSize ?? defaults.editorFontSize,
terminalBackgroundImage: settings.terminalBackgroundImage ?? defaults.terminalBackgroundImage,
pageBackgroundImage: settings.pageBackgroundImage ?? defaults.pageBackgroundImage,
updatedAt: latestUpdatedAt || defaults.updatedAt, // Use latest DB timestamp or default
updatedAt: latestUpdatedAt || defaults.updatedAt, // 使用最新的更新时间,否则使用默认时间戳
};
};
// 获取默认外观设置 (Simplified, _id is no longer relevant here)
// 获取默认外观设置 (已简化, _id 在此不再相关)
const getDefaultAppearanceSettings = (): Omit<AppearanceSettings, '_id'> => {
return {
customUiTheme: JSON.stringify(defaultUiTheme),
activeTerminalThemeId: null, // Default should be null initially
activeTerminalThemeId: null, // 初始默认应为 null
terminalFontFamily: 'Consolas, "Courier New", monospace, "Microsoft YaHei", "微软雅黑"',
terminalFontSize: 14,
editorFontSize: 14,
terminalBackgroundImage: undefined,
pageBackgroundImage: undefined,
updatedAt: Date.now(), // Provide a default timestamp
updatedAt: Date.now(), // 提供默认时间戳
};
};
/**
* Ensures default settings exist in the key-value table.
* This function is called during database initialization.
* 确保默认设置存在于键值表中。
* 此函数在数据库初始化期间调用。
* @param db - 活动的数据库实例
*/
export const ensureDefaultSettingsExist = async (db: sqlite3.Database): Promise<void> => {
const defaults = getDefaultAppearanceSettings();
const nowSeconds = Math.floor(Date.now() / 1000);
const sqlInsertOrIgnore = `INSERT OR IGNORE INTO ${TABLE_NAME} (key, value, created_at, updated_at) VALUES (?, ?, ?, ?)`;
// Define default key-value pairs to ensure existence
// 定义默认键值对以确保存在
const defaultEntries: Array<{ key: keyof Omit<AppearanceSettings, '_id' | 'updatedAt'>, value: any }> = [
{ key: 'customUiTheme', value: defaults.customUiTheme },
{ key: 'activeTerminalThemeId', value: null }, // Start with null
{ key: 'activeTerminalThemeId', value: null }, // null 开始
{ key: 'terminalFontFamily', value: defaults.terminalFontFamily },
{ key: 'terminalFontSize', value: defaults.terminalFontSize },
{ key: 'editorFontSize', value: defaults.editorFontSize },
{ key: 'terminalBackgroundImage', value: defaults.terminalBackgroundImage ?? '' }, // Use empty string for DB
{ key: 'pageBackgroundImage', value: defaults.pageBackgroundImage ?? '' }, // Use empty string for DB
{ key: 'terminalBackgroundImage', value: defaults.terminalBackgroundImage ?? '' }, // 数据库中使用空字符串
{ key: 'pageBackgroundImage', value: defaults.pageBackgroundImage ?? '' }, // 数据库中使用空字符串
];
try {
for (const entry of defaultEntries) {
// Convert value to string for DB storage, handle null/undefined
// 将值转换为字符串以存储到数据库,处理 null/undefined
let dbValue: string;
if (entry.value === null || entry.value === undefined) {
dbValue = entry.key === 'activeTerminalThemeId' ? 'null' : ''; // Store null specifically for theme ID, empty otherwise
dbValue = entry.key === 'activeTerminalThemeId' ? 'null' : ''; // 主题 ID 特殊存储为 'null',其他情况为空字符串
} else if (typeof entry.value === 'object') {
dbValue = JSON.stringify(entry.value);
} else {
dbValue = String(entry.value);
}
// Special handling for activeTerminalThemeId: store null as 'null' string or the number as string
// activeTerminalThemeId 的特殊处理:将 null 存储为 'null' 字符串,或将数字存储为字符串
if (entry.key === 'activeTerminalThemeId') {
dbValue = entry.value === null ? 'null' : String(entry.value);
}
@@ -129,9 +122,9 @@ export const ensureDefaultSettingsExist = async (db: sqlite3.Database): Promise<
await runDb(db, sqlInsertOrIgnore, [entry.key, dbValue, nowSeconds, nowSeconds]);
}
console.log('[AppearanceRepo] 默认外观设置键值对检查完成。');
// console.log('[AppearanceRepo] 默认外观设置键值对检查完成。'); // 移除:信息不太关键
// After ensuring keys exist, try to set the default theme ID if it's currently null
// 确保键存在后,如果当前为 null,则尝试设置默认主题 ID
await findAndSetDefaultThemeIdIfNull(db);
} catch (err: any) {
@@ -141,64 +134,67 @@ export const ensureDefaultSettingsExist = async (db: sqlite3.Database): Promise<
};
/**
* Finds the default terminal theme ID and updates the 'activeTerminalThemeId' setting if it's currently null.
* @param db - The active database instance
* 查找默认终端主题 ID,并在 'activeTerminalThemeId' 设置当前为 null 时更新它。
* @param db - 活动的数据库实例
*/
const findAndSetDefaultThemeIdIfNull = async (db: sqlite3.Database): Promise<void> => {
try {
// Check the current value of activeTerminalThemeId
// 检查 activeTerminalThemeId 的当前值
const currentSetting = await getDb<{ value: string }>(db, `SELECT value FROM ${TABLE_NAME} WHERE key = ?`, ['activeTerminalThemeId']);
// Proceed only if the setting exists and its value represents null ('null' string)
// 仅当设置存在且其值为 'null' 字符串时继续
if (currentSetting && currentSetting.value === 'null') {
// Find the default theme from the terminal_themes table (assuming name 'default' marks the default)
// 从 terminal_themes 表中查找默认主题(假设名称 'default' 标记为默认)
const defaultThemeSql = `SELECT id FROM terminal_themes WHERE name = 'default' AND theme_type = 'preset' LIMIT 1`;
const defaultThemeRow = await getDb<{ id: number }>(db, defaultThemeSql);
if (defaultThemeRow) {
const defaultThemeIdNum = defaultThemeRow.id;
console.log(`[AppearanceRepo] activeTerminalThemeId 为 null,尝试设置为默认主题 ID: ${defaultThemeIdNum}`);
// Update the setting using INSERT OR REPLACE
// console.log(`[AppearanceRepo] activeTerminalThemeId 为 null,尝试设置为默认主题 ID: ${defaultThemeIdNum}`); // 移除:信息不太关键
// 使用 INSERT OR REPLACE 更新设置
const sqlReplace = `INSERT OR REPLACE INTO ${TABLE_NAME} (key, value, updated_at) VALUES (?, ?, ?)`;
await runDb(db, sqlReplace, ['activeTerminalThemeId', String(defaultThemeIdNum), Math.floor(Date.now() / 1000)]);
} else {
console.warn("[AppearanceRepo] 未找到名为 'default' 的预设终端主题,无法设置默认 activeTerminalThemeId。");
}
} else {
// console.log(`[AppearanceRepo] activeTerminalThemeId 已设置 (${currentSetting?.value}) 或键不存在,跳过设置默认 ID。`);
}
// 如果 activeTerminalThemeId 已设置或键不存在,则不执行任何操作
} catch (error: any) {
console.error("[AppearanceRepo] 设置默认终端主题 ID 时出错:", error.message);
// Don't throw here, just log
// 这里不抛出错误,只记录日志
}
};
/**
* 获取外观设置
* @returns Promise<AppearanceSettings>
* 获取外观设置
* 从数据库中检索所有外观相关的键值对,并将它们映射到一个 AppearanceSettings 对象。
* @returns {Promise<AppearanceSettings>} 返回包含当前外观设置的对象。
* @throws {Error} 如果从数据库获取设置失败。
*/
export const getAppearanceSettings = async (): Promise<AppearanceSettings> => {
try {
const db = await getDbInstance();
// Fetch all rows from the key-value table
// 从键值表中获取所有行
const rows = await allDb<DbAppearanceSettingsRow>(db, `SELECT key, value, updated_at FROM ${TABLE_NAME}`);
return mapRowsToAppearanceSettings(rows); // Map the key-value pairs to the settings object
return mapRowsToAppearanceSettings(rows); // 将键值对映射到设置对象
} catch (err: any) {
console.error('获取外观设置失败:', err.message);
console.error('[AppearanceRepo] 获取外观设置失败:', err.message);
throw new Error('获取外观设置失败');
}
};
/**
* 更新外观设置 (Public API)
* @param settingsDto 更新的数据
* @returns Promise<boolean> 是否成功更新
* 更新外观设置 (公共 API)
* 接收一个包含要更新设置的 DTO,执行必要的验证,然后调用内部更新函数。
* @param {UpdateAppearanceDto} settingsDto - 包含要更新设置的对象。
* @returns {Promise<boolean>} 如果至少有一个设置被成功更新或插入,则返回 true,否则返回 false。
* @throws {Error} 如果验证失败或内部更新过程中发生错误。
*/
export const updateAppearanceSettings = async (settingsDto: UpdateAppearanceDto): Promise<boolean> => {
const db = await getDbInstance();
// Perform validation or complex logic if needed before calling internal update
// Example validation (already present in service, but could be here too):
// 在调用内部更新之前,如果需要,执行验证或复杂逻辑
// 验证示例(已存在于服务中,但也可以在这里):
if (settingsDto.activeTerminalThemeId !== undefined && settingsDto.activeTerminalThemeId !== null) {
try {
const themeExists = await findTerminalThemeById(settingsDto.activeTerminalThemeId);
@@ -210,18 +206,20 @@ export const updateAppearanceSettings = async (settingsDto: UpdateAppearanceDto)
throw new Error(`验证主题 ID 失败: ${validationError.message}`);
}
}
// ... other validations ...
// ... 其他验证 ...
return updateAppearanceSettingsInternal(db, settingsDto);
};
/**
* 内部更新外观设置函数 (供内部调用,如初始化)
* @param db - Active database instance
* @param settingsDto - Data to update
* @returns Promise<boolean> - Success status
* 内部更新外观设置函数 (供内部调用,例如在初始化或公共 API 中)。
* 此函数直接与数据库交互,使用 INSERT OR REPLACE 来更新或插入键值对。
* @param {sqlite3.Database} db - 活动的数据库实例。
* @param {UpdateAppearanceDto} settingsDto - 包含要更新设置的对象。
* @returns {Promise<boolean>} 如果至少有一个设置被成功更新或插入,则返回 true,否则返回 false。
* @throws {Error} 如果在数据库操作期间发生错误。
*/
// Internal function to update settings in the key-value table
// 在键值表中更新设置的内部函数
const updateAppearanceSettingsInternal = async (db: sqlite3.Database, settingsDto: UpdateAppearanceDto): Promise<boolean> => {
const nowSeconds = Math.floor(Date.now() / 1000);
const sqlReplace = `INSERT OR REPLACE INTO ${TABLE_NAME} (key, value, updated_at) VALUES (?, ?, ?)`;
@@ -232,37 +230,36 @@ const updateAppearanceSettingsInternal = async (db: sqlite3.Database, settingsDt
const value = settingsDto[key];
let dbValue: string;
// Convert value to string for DB, handle null/undefined
// 将值转换为字符串以存储到数据库,处理 null/undefined
if (value === null || value === undefined) {
dbValue = key === 'activeTerminalThemeId' ? 'null' : ''; // Store null specifically for theme ID
dbValue = key === 'activeTerminalThemeId' ? 'null' : ''; // 主题 ID 特殊存储为 'null'
} else if (typeof value === 'object') {
dbValue = JSON.stringify(value);
} else {
dbValue = String(value);
}
// Special handling for activeTerminalThemeId to store 'null' string or number string
// activeTerminalThemeId 的特殊处理:存储 'null' 字符串或数字字符串
if (key === 'activeTerminalThemeId') {
dbValue = value === null ? 'null' : String(value);
}
// Validation for active_terminal_theme_id type before saving
// 保存前验证 active_terminal_theme_id 类型
if (key === 'activeTerminalThemeId' && value !== null && typeof value !== 'number') {
console.error(`[AppearanceRepo] 更新 activeTerminalThemeId 时收到无效类型值: ${value} (类型: ${typeof value}),应为数字或 null。跳过此字段。`);
continue; // Skip this key
continue; // 跳过此键
}
// Execute INSERT OR REPLACE for each key-value pair
// 对每个键值对执行 INSERT OR REPLACE
const result = await runDb(db, sqlReplace, [key, dbValue, nowSeconds]);
if (result.changes > 0) {
changesMade = true;
}
}
console.log(`[AppearanceRepo] 更新外观设置完成。是否有更改: ${changesMade}`);
return changesMade; // Return true if any row was inserted or replaced
return changesMade; // 如果有任何行被插入或替换,则返回 true
} catch (err: any) {
console.error('更新外观设置失败:', err.message);
console.error('[AppearanceRepo] 更新外观设置失败:', err.message);
throw new Error('更新外观设置失败');
}
};
@@ -1,30 +1,27 @@
// packages/backend/src/repositories/audit.repository.ts
import { Database } from 'sqlite3';
// Import new async helpers and the instance getter
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
import { AuditLogEntry, AuditLogActionType } from '../types/audit.types';
// Define the expected row structure from the database if it matches AuditLogEntry
type DbAuditLogRow = AuditLogEntry;
export class AuditLogRepository {
// Remove constructor or leave it empty
// constructor() { }
/**
* 添加一条审计日志记录
* @param actionType 操作类型
* @param details 可选的详细信息 (对象或字符串)
* 添加一条审计日志记录
* @param actionType 操作类型
* @param details 可选的详细信息对象或字符串)。
*/
async addLog(actionType: AuditLogActionType, details?: Record<string, any> | string | null): Promise<void> {
const timestamp = Math.floor(Date.now() / 1000); // Unix timestamp in seconds
const timestamp = Math.floor(Date.now() / 1000);
let detailsString: string | null = null;
if (details) {
try {
detailsString = typeof details === 'string' ? details : JSON.stringify(details);
} catch (error: any) {
console.error(`[Audit Log] Failed to stringify details for action ${actionType}:`, error.message);
console.error(`[审计日志] 序列化操作 ${actionType} 的详情失败:`, error.message);
detailsString = JSON.stringify({ error: 'Failed to stringify details', originalDetails: String(details) }); // Ensure originalDetails is stringifiable
}
}
@@ -35,22 +32,20 @@ export class AuditLogRepository {
try {
const db = await getDbInstance();
await runDb(db, sql, params);
// console.log(`[Audit Log] Logged action: ${actionType}`); // Optional: verbose logging
// --- 添加日志清理逻辑 ---
await this.cleanupOldLogs(db);
// --- 清理逻辑结束 ---
} catch (err: any) {
console.error(`[Audit Log] Error adding log entry for action ${actionType}: ${err.message}`);
// Decide if logging failure should throw an error or just be logged
// throw new Error(`Error adding log entry: ${err.message}`); // Uncomment to make it critical
console.error(`[审计日志] 添加操作 ${actionType} 的日志条目时出错: ${err.message}`);
// 决定日志记录失败是应该抛出错误还是仅记录日志
}
}
/**
* 清理旧的审计日志,保持最多 MAX_LOG_ENTRIES 条记录
* @param db - 数据库实例
* 清理旧的审计日志,保持最多 MAX_LOG_ENTRIES 条记录
* @param db - 数据库实例
*/
private async cleanupOldLogs(db: Database): Promise<void> {
const MAX_LOG_ENTRIES = 50000; // 设置最大日志条数
@@ -71,24 +66,23 @@ export class AuditLogRepository {
if (total > MAX_LOG_ENTRIES) {
const logsToDelete = total - MAX_LOG_ENTRIES;
console.log(`[Audit Log] Log count (${total}) exceeds limit (${MAX_LOG_ENTRIES}). Deleting ${logsToDelete} oldest entries.`);
console.log(`[审计日志] 日志数量 (${total}) 超过限制 (${MAX_LOG_ENTRIES})。正在删除 ${logsToDelete} 条最旧的记录。`);
await runDb(db, deleteSql, [logsToDelete]);
console.log(`[Audit Log] Successfully deleted ${logsToDelete} oldest log entries.`);
}
} catch (err: any) {
console.error(`[Audit Log] Error during log cleanup: ${err.message}`);
// 清理失败不应阻止主日志记录流程,仅记录错误
console.error(`[审计日志] 日志清理过程中出错: ${err.message}`);
// 清理失败不应阻止主日志记录流程,仅记录错误
}
}
/**
* 获取审计日志列表 (支持分页和基本过滤)
* @param limit 每页数量
* @param offset 偏移量
* @param actionType 可选的操作类型过滤
* @param startDate 可选的开始时间戳 (秒)
* @param endDate 可选的结束时间戳 (秒)
* @param searchTerm 可选的搜索关键词 (模糊匹配 details)
* 获取审计日志列表支持分页和基本过滤)。
* @param limit 每页数量
* @param offset 偏移量
* @param actionType 可选的操作类型过滤
* @param startDate 可选的开始时间戳(秒)。
* @param endDate 可选的结束时间戳(秒)。
* @param searchTerm 可选的搜索关键词模糊匹配 details)。
*/
async getLogs(
limit: number = 50,
@@ -98,8 +92,7 @@ export class AuditLogRepository {
endDate?: number,
searchTerm?: string // 添加 searchTerm 参数
): Promise<{ logs: AuditLogEntry[], total: number }> {
console.log(`[Audit Repo] getLogs called with: actionType=${actionType}, searchTerm=${searchTerm}`); // 添加日志
let baseSql = 'SELECT * FROM audit_logs';
let countSql = 'SELECT COUNT(*) as total FROM audit_logs';
const whereClauses: string[] = [];
@@ -107,14 +100,12 @@ export class AuditLogRepository {
const countParams: (string | number)[] = [];
if (actionType) {
console.log(`[Audit Repo] Filtering by actionType: ${actionType}`); // 添加日志
whereClauses.push('action_type = ?');
params.push(actionType);
countParams.push(actionType);
}
// 添加 searchTerm 的过滤逻辑
if (searchTerm) {
console.log(`[Audit Repo] Filtering by searchTerm: ${searchTerm}`); // 添加日志
// 搜索 details 字段,使用 LIKE 进行模糊匹配
whereClauses.push('details LIKE ?');
const searchTermLike = `%${searchTerm}%`;
@@ -132,25 +123,20 @@ export class AuditLogRepository {
baseSql += ' ORDER BY timestamp DESC LIMIT ? OFFSET ?';
params.push(limit, offset);
console.log(`[Audit Repo] Executing count SQL: ${countSql} with params:`, countParams); // 添加日志
console.log(`[Audit Repo] Executing base SQL: ${baseSql} with params:`, params); // 添加日志
try {
const db = await getDbInstance();
// First get the total count
const countRow = await getDbRow<{ total: number }>(db, countSql, countParams);
const total = countRow?.total ?? 0;
// Then get the paginated logs
const logs = await allDb<DbAuditLogRow>(db, baseSql, params);
return { logs, total };
} catch (err: any) {
console.error(`Error fetching audit logs:`, err.message);
throw new Error(`Error fetching audit logs: ${err.message}`);
console.error(`获取审计日志时出错:`, err.message);
throw new Error(`获取审计日志时出错: ${err.message}`);
}
}
}
// Export the class (Removed redundant export below as class is already exported)
// export { AuditLogRepository };
@@ -1,5 +1,4 @@
// packages/backend/src/repositories/command-history.repository.ts
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection'; // Import new async helpers
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
// 定义命令历史记录的接口
export interface CommandHistoryEntry {
@@ -8,7 +7,6 @@ export interface CommandHistoryEntry {
timestamp: number; // Unix 时间戳 (秒)
}
// Define the expected row structure from the database if it matches CommandHistoryEntry
type DbCommandHistoryRow = CommandHistoryEntry;
/**
@@ -94,7 +92,7 @@ export const clearAllCommands = async (): Promise<number> => {
try {
const db = await getDbInstance();
const result = await runDb(db, sql);
return result.changes; // Return the number of deleted rows
return result.changes;
} catch (err: any) {
console.error('清空命令历史记录时出错:', err.message);
throw new Error('无法清空命令历史记录');
@@ -1,16 +1,12 @@
// packages/backend/src/repositories/connection.repository.ts
import { Database, Statement } from 'sqlite3';
// Import new async helpers and the instance getter
import { Database } from 'sqlite3';
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
// Remove top-level db instance
// const db = getDb();
// Define Connection 类型 (可以从 controller 或 types 文件导入,暂时在此定义)
// 注意:这里不包含加密字段,因为 Repository 不应处理解密
interface ConnectionBase {
id: number;
name: string | null; // 允许 name 为 null
name: string | null;
host: string;
port: number;
username: string;
@@ -21,9 +17,8 @@ interface ConnectionBase {
last_connected_at: number | null;
}
// Type for the result of the JOIN query in findAllConnectionsWithTags and findConnectionByIdWithTags
interface ConnectionWithTagsRow extends ConnectionBase {
tag_ids_str: string | null; // Raw string from GROUP_CONCAT
tag_ids_str: string | null;
}
export interface ConnectionWithTags extends ConnectionBase {
@@ -35,12 +30,10 @@ export interface FullConnectionData extends ConnectionBase {
encrypted_password?: string | null;
encrypted_private_key?: string | null;
encrypted_passphrase?: string | null;
// Include tag_ids for creation/update convenience if needed, handled separately
tag_ids?: number[];
}
// Type for the result of the JOIN query in findFullConnectionById
// Define a more specific type for the complex row structure
interface FullConnectionDbRow extends FullConnectionData {
proxy_db_id: number | null;
proxy_name: string | null;
@@ -70,7 +63,6 @@ export const findAllConnectionsWithTags = async (): Promise<ConnectionWithTags[]
try {
const db = await getDbInstance();
const rows = await allDb<ConnectionWithTagsRow>(db, sql);
// Safely map rows, handling potential null tag_ids_str
return rows.map(row => ({
...row,
tag_ids: row.tag_ids_str ? row.tag_ids_str.split(',').map(Number).filter(id => !isNaN(id)) : []
@@ -97,7 +89,7 @@ export const findConnectionByIdWithTags = async (id: number): Promise<Connection
try {
const db = await getDbInstance();
const row = await getDbRow<ConnectionWithTagsRow>(db, sql, [id]);
if (row && typeof row.id !== 'undefined') { // Check if a valid row was found
if (row && typeof row.id !== 'undefined') {
return {
...row,
tag_ids: row.tag_ids_str ? row.tag_ids_str.split(',').map(Number).filter(id => !isNaN(id)) : []
@@ -155,7 +147,6 @@ export const createConnection = async (data: Omit<FullConnectionData, 'id' | 'cr
try {
const db = await getDbInstance();
const result = await runDb(db, sql, params);
// Ensure lastID is valid before returning
if (typeof result.lastID !== 'number' || result.lastID <= 0) {
throw new Error('创建连接后未能获取有效的 lastID');
}
@@ -176,7 +167,7 @@ export const updateConnection = async (id: number, data: Partial<Omit<FullConnec
delete fieldsToUpdate.id;
delete fieldsToUpdate.created_at;
delete fieldsToUpdate.last_connected_at;
delete fieldsToUpdate.tag_ids; // Tags handled separately
delete fieldsToUpdate.tag_ids;
fieldsToUpdate.updated_at = Math.floor(Date.now() / 1000);
@@ -209,7 +200,6 @@ export const deleteConnection = async (id: number): Promise<boolean> => {
const sql = `DELETE FROM connections WHERE id = ?`;
try {
const db = await getDbInstance();
// ON DELETE CASCADE in connection_tags and ON DELETE SET NULL for proxy_id handle related data
const result = await runDb(db, sql, [id]);
return result.changes > 0;
} catch (err: any) {
@@ -245,21 +235,18 @@ export const updateLastConnected = async (id: number, timestamp: number): Promis
*/
export const updateConnectionTags = async (connectionId: number, tagIds: number[]): Promise<void> => {
const db = await getDbInstance();
// Use a transaction to ensure atomicity
try {
await runDb(db, 'BEGIN TRANSACTION');
// 1. Delete old associations
await runDb(db, `DELETE FROM connection_tags WHERE connection_id = ?`, [connectionId]);
// 2. Insert new associations (if any)
if (tagIds.length > 0) {
const insertSql = `INSERT INTO connection_tags (connection_id, tag_id) VALUES (?, ?)`;
// Use Promise.all for potentially better performance, though sequential inserts are safer for constraints
const insertPromises = tagIds
.filter(tagId => typeof tagId === 'number' && tagId > 0) // Basic validation
.filter(tagId => typeof tagId === 'number' && tagId > 0)
.map(tagId => runDb(db, insertSql, [connectionId, tagId]).catch(err => {
// Log warning but don't fail the whole transaction for a single tag insert error (e.g., invalid tag ID)
console.warn(`Repository: 更新连接 ${connectionId} 标签时,插入 tag_id ${tagId} 失败: ${err.message}`);
}));
await Promise.all(insertPromises);
@@ -269,21 +256,20 @@ export const updateConnectionTags = async (connectionId: number, tagIds: number[
} catch (err: any) {
console.error(`Repository: 更新连接 ${connectionId} 的标签关联时出错:`, err.message);
try {
await runDb(db, 'ROLLBACK'); // Attempt to rollback on error
await runDb(db, 'ROLLBACK');
} catch (rollbackErr: any) {
console.error(`Repository: 回滚连接 ${connectionId} 的标签更新事务失败:`, rollbackErr.message);
}
throw new Error('处理标签关联失败'); // Re-throw original error
throw new Error('处理标签关联失败');
}
};
/**
* 批量插入连接(用于导入)
* 注意:此函数应在事务中调用 (由调用者负责事务)
* Returns an array mapping new connection IDs to their original import data (for tag association)
*/
export const bulkInsertConnections = async (
db: Database, // Pass the transaction-aware db instance
db: Database,
connections: Array<Omit<FullConnectionData, 'id' | 'created_at' | 'updated_at' | 'last_connected_at'> & { tag_ids?: number[] }>
): Promise<{ connectionId: number, originalData: any }[]> => {
@@ -291,8 +277,6 @@ export const bulkInsertConnections = async (
const results: { connectionId: number, originalData: any }[] = [];
const now = Math.floor(Date.now() / 1000);
// Prepare statement outside the loop for efficiency (though sqlite3 might cache implicitly)
// Using direct runDb might be simpler here unless performance is critical
for (const connData of connections) {
const params = [
@@ -304,19 +288,15 @@ export const bulkInsertConnections = async (
now, now
];
try {
// Use the passed db instance (which should be in a transaction)
const connResult = await runDb(db, insertConnSql, params);
if (typeof connResult.lastID !== 'number' || connResult.lastID <= 0) {
throw new Error(`插入连接 "${connData.name}" 后未能获取有效的 lastID`);
}
results.push({ connectionId: connResult.lastID, originalData: connData });
} catch (err: any) {
// Log error but continue with other connections? Or re-throw to fail the whole batch?
console.error(`Repository: 批量插入连接 "${connData.name}" 时出错: ${err.message}`);
// Decide on error handling strategy for batch operations
throw new Error(`批量插入连接 "${connData.name}" 失败`); // Fail fast for now
throw new Error(`批量插入连接 "${connData.name}" 失败`);
}
}
return results;
// Tag insertion should be handled separately after connections are inserted, using the returned IDs
};
@@ -1,10 +1,7 @@
// packages/backend/src/repositories/notification.repository.ts
import { Database } from 'sqlite3';
// Import new async helpers and the instance getter
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
import { NotificationSetting, RawNotificationSetting, NotificationChannelType, NotificationEvent, NotificationChannelConfig } from '../types/notification.types';
// Helper to parse raw data from DB
const parseRawSetting = (raw: RawNotificationSetting): NotificationSetting => {
try {
return {
@@ -13,20 +10,18 @@ const parseRawSetting = (raw: RawNotificationSetting): NotificationSetting => {
config: JSON.parse(raw.config || '{}'),
enabled_events: JSON.parse(raw.enabled_events || '[]'),
};
} catch (error: any) { // Add type annotation
console.error(`Error parsing notification setting ID ${raw.id}:`, error.message);
} catch (error: any) {
console.error(`解析通知设置 ID ${raw.id} 时出错:`, error.message);
return {
...raw,
enabled: Boolean(raw.enabled),
config: {} as NotificationChannelConfig, // Indicate parsing error
config: {} as NotificationChannelConfig,
enabled_events: [],
};
}
};
export class NotificationSettingsRepository {
// Remove constructor or leave it empty
// constructor() { }
async getAll(): Promise<NotificationSetting[]> {
try {
@@ -34,8 +29,8 @@ export class NotificationSettingsRepository {
const rows = await allDb<RawNotificationSetting>(db, 'SELECT * FROM notification_settings ORDER BY created_at ASC');
return rows.map(parseRawSetting);
} catch (err: any) {
console.error(`Error fetching notification settings:`, err.message);
throw new Error(`Error fetching notification settings: ${err.message}`);
console.error(`获取通知设置时出错:`, err.message);
throw new Error(`获取通知设置时出错: ${err.message}`);
}
}
@@ -45,13 +40,12 @@ export class NotificationSettingsRepository {
const row = await getDbRow<RawNotificationSetting>(db, 'SELECT * FROM notification_settings WHERE id = ?', [id]);
return row ? parseRawSetting(row) : null;
} catch (err: any) {
console.error(`Error fetching notification setting by ID ${id}:`, err.message);
throw new Error(`Error fetching notification setting by ID ${id}: ${err.message}`);
console.error(`通过 ID ${id} 获取通知设置时出错:`, err.message);
throw new Error(`通过 ID ${id} 获取通知设置时出错: ${err.message}`);
}
}
async getEnabledByEvent(event: NotificationEvent): Promise<NotificationSetting[]> {
// Note: Query remains inefficient, consider optimization later if needed.
try {
const db = await getDbInstance();
const rows = await allDb<RawNotificationSetting>(db, 'SELECT * FROM notification_settings WHERE enabled = 1');
@@ -59,8 +53,8 @@ export class NotificationSettingsRepository {
const filteredRows = parsedRows.filter(setting => setting.enabled_events.includes(event));
return filteredRows;
} catch (err: any) {
console.error(`Error fetching enabled notification settings:`, err.message);
throw new Error(`Error fetching enabled notification settings: ${err.message}`);
console.error(`获取启用的通知设置时出错:`, err.message);
throw new Error(`获取启用的通知设置时出错: ${err.message}`);
}
}
@@ -68,10 +62,10 @@ export class NotificationSettingsRepository {
const sql = `
INSERT INTO notification_settings (channel_type, name, enabled, config, enabled_events, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now'))
`; // Added created_at, updated_at
`;
const params = [
setting.channel_type,
setting.name ?? '', // Ensure name is not undefined
setting.name ?? '',
setting.enabled ? 1 : 0,
JSON.stringify(setting.config || {}),
JSON.stringify(setting.enabled_events || [])
@@ -85,17 +79,15 @@ export class NotificationSettingsRepository {
}
return result.lastID;
} catch (err: any) {
console.error(`Error creating notification setting:`, err.message);
throw new Error(`Error creating notification setting: ${err.message}`);
console.error(`创建通知设置时出错:`, err.message);
throw new Error(`创建通知设置时出错: ${err.message}`);
}
}
async update(id: number, setting: Partial<Omit<NotificationSetting, 'id' | 'created_at' | 'updated_at'>>): Promise<boolean> {
// Build the SET part of the query dynamically
const fields: string[] = [];
const params: (string | number | null)[] = [];
// Dynamically build SET clauses
if (setting.channel_type !== undefined) { fields.push('channel_type = ?'); params.push(setting.channel_type); }
if (setting.name !== undefined) { fields.push('name = ?'); params.push(setting.name); }
if (setting.enabled !== undefined) { fields.push('enabled = ?'); params.push(setting.enabled ? 1 : 0); }
@@ -103,11 +95,11 @@ export class NotificationSettingsRepository {
if (setting.enabled_events !== undefined) { fields.push('enabled_events = ?'); params.push(JSON.stringify(setting.enabled_events || [])); }
if (fields.length === 0) {
console.warn(`[NotificationRepo] update called for ID ${id} with no fields to update.`);
return true; // Or false, depending on desired behavior for no-op update
console.warn(`[通知仓库] 针对 ID ${id} 调用了更新,但没有要更新的字段。`);
return true;
}
fields.push('updated_at = strftime(\'%s\', \'now\')'); // Always update timestamp
fields.push('updated_at = strftime(\'%s\', \'now\')');
const sql = `UPDATE notification_settings SET ${fields.join(', ')} WHERE id = ?`;
params.push(id);
@@ -117,8 +109,8 @@ export class NotificationSettingsRepository {
const result = await runDb(db, sql, params);
return result.changes > 0;
} catch (err: any) {
console.error(`Error updating notification setting ID ${id}:`, err.message);
throw new Error(`Error updating notification setting ID ${id}: ${err.message}`);
console.error(`更新通知设置 ID ${id} 时出错:`, err.message);
throw new Error(`更新通知设置 ID ${id} 时出错: ${err.message}`);
}
}
@@ -129,11 +121,9 @@ export class NotificationSettingsRepository {
const result = await runDb(db, sql, [id]);
return result.changes > 0;
} catch (err: any) {
console.error(`Error deleting notification setting ID ${id}:`, err.message);
throw new Error(`Error deleting notification setting ID ${id}: ${err.message}`);
console.error(`删除通知设置 ID ${id} 时出错:`, err.message);
throw new Error(`删除通知设置 ID ${id} 时出错: ${err.message}`);
}
}
}
// Export the class (Removed redundant export below as class is already exported)
// export { NotificationSettingsRepository };
@@ -1,6 +1,3 @@
// packages/backend/src/repositories/passkey.repository.ts
import { Database } from 'sqlite3';
// Import new async helpers and the instance getter
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
// 定义 Passkey 数据库记录的接口
@@ -9,18 +6,16 @@ export interface PasskeyRecord {
credential_id: string; // Base64URL encoded
public_key: string; // Base64URL encoded
counter: number;
transports: string | null; // JSON string or null
transports: string | null;
name: string | null;
created_at: number;
updated_at: number;
}
// Define the expected row structure from the database if it matches PasskeyRecord
type DbPasskeyRow = PasskeyRecord;
export class PasskeyRepository {
// Remove constructor or leave it empty, db instance will be fetched in each method
// constructor() { }
/**
* 保存新的 Passkey 凭证
@@ -48,7 +43,6 @@ export class PasskeyRepository {
return result.lastID;
} catch (err: any) {
console.error('保存 Passkey 时出错:', err.message);
// Handle potential UNIQUE constraint errors on credential_id
if (err.message.includes('UNIQUE constraint failed')) {
throw new Error(`Credential ID "${credentialId}" 已存在。`);
}
@@ -76,12 +70,10 @@ export class PasskeyRepository {
* 获取所有已注册的 Passkey 记录 (仅选择必要字段)
* @returns Promise<Partial<PasskeyRecord>[]> 所有记录的部分信息的数组
*/
// Adjust return type based on selected columns
async getAllPasskeys(): Promise<Array<Pick<PasskeyRecord, 'id' | 'credential_id' | 'name' | 'transports' | 'created_at'>>> {
const sql = `SELECT id, credential_id, name, transports, created_at FROM passkeys ORDER BY created_at DESC`;
try {
const db = await getDbInstance();
// Adjust the generic type for allDb to match the selected columns
const rows = await allDb<Pick<PasskeyRecord, 'id' | 'credential_id' | 'name' | 'transports' | 'created_at'>>(db, sql);
return rows;
} catch (err: any) {
@@ -173,11 +165,3 @@ export class PasskeyRepository {
}
}
// Export an instance or the class itself depending on usage pattern
// If used as a singleton service, export an instance:
// export const passkeyRepository = new PasskeyRepository();
// If instantiated elsewhere (e.g., dependency injection), export the class:
// export { PasskeyRepository };
// For now, let's assume it's used like other repositories (exporting functions/class)
// Exporting the class seems more appropriate given its structure
// Removed redundant export below as the class is already exported with 'export class'
@@ -1,12 +1,5 @@
// packages/backend/src/repositories/proxy.repository.ts
import { Database, Statement } from 'sqlite3';
// Import new async helpers and the instance getter
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
// Remove top-level db instance
// const db = getDb();
// 定义 Proxy 类型 (可以共享到 types 文件)
export interface ProxyData {
id: number;
name: string;
@@ -22,7 +15,6 @@ export interface ProxyData {
updated_at: number;
}
// Define the expected row structure from the database if it matches ProxyData
type DbProxyRow = ProxyData;
/**
@@ -59,14 +51,12 @@ export const createProxy = async (data: Omit<ProxyData, 'id' | 'created_at' | 'u
try {
const db = await getDbInstance();
const result = await runDb(db, sql, params);
// Ensure lastID is valid before returning
if (typeof result.lastID !== 'number' || result.lastID <= 0) {
throw new Error('创建代理后未能获取有效的 lastID');
}
return result.lastID;
} catch (err: any) {
console.error('Repository: 创建代理时出错:', err.message);
// Handle potential UNIQUE constraint errors if needed (e.g., on name)
throw new Error(`创建代理时出错: ${err.message}`);
}
};
@@ -108,11 +98,8 @@ export const findProxyById = async (id: number): Promise<ProxyData | null> => {
export const updateProxy = async (id: number, data: Partial<Omit<ProxyData, 'id' | 'created_at'>>): Promise<boolean> => {
const fieldsToUpdate: { [key: string]: any } = { ...data };
const params: any[] = [];
// Remove fields that should not be updated directly
delete fieldsToUpdate.id;
delete fieldsToUpdate.created_at;
// updated_at will be set explicitly
fieldsToUpdate.updated_at = Math.floor(Date.now() / 1000);
@@ -121,10 +108,10 @@ export const updateProxy = async (id: number, data: Partial<Omit<ProxyData, 'id'
if (!setClauses) {
console.warn(`[Repository] updateProxy called for ID ${id} with no fields to update.`);
return false; // Nothing to update
return false;
}
params.push(id); // Add the ID for the WHERE clause
params.push(id);
const sql = `UPDATE proxies SET ${setClauses} WHERE id = ?`;
@@ -134,7 +121,6 @@ export const updateProxy = async (id: number, data: Partial<Omit<ProxyData, 'id'
return result.changes > 0;
} catch (err: any) {
console.error(`Repository: 更新代理 ${id} 时出错:`, err.message);
// Handle potential UNIQUE constraint errors if needed
throw new Error('更新代理记录失败');
}
};
@@ -143,7 +129,6 @@ export const updateProxy = async (id: number, data: Partial<Omit<ProxyData, 'id'
* 删除代理
*/
export const deleteProxy = async (id: number): Promise<boolean> => {
// Note: connections table proxy_id foreign key has ON DELETE SET NULL.
const sql = `DELETE FROM proxies WHERE id = ?`;
try {
const db = await getDbInstance();
@@ -1,5 +1,4 @@
// packages/backend/src/repositories/quick-commands.repository.ts
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection'; // Import new async helpers
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
// 定义快捷指令的接口
export interface QuickCommand {
@@ -11,7 +10,6 @@ export interface QuickCommand {
updated_at: number; // Unix 时间戳 (秒)
}
// Define the expected row structure from the database if it matches QuickCommand
type DbQuickCommandRow = QuickCommand;
/**
@@ -25,7 +23,6 @@ export const addQuickCommand = async (name: string | null, command: string): Pro
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [name, command]);
// Ensure lastID is valid before returning
if (typeof result.lastID !== 'number' || result.lastID <= 0) {
throw new Error('添加快捷指令后未能获取有效的 lastID');
}
@@ -34,7 +31,7 @@ export const addQuickCommand = async (name: string | null, command: string): Pro
console.error('添加快捷指令时出错:', err.message);
throw new Error('无法添加快捷指令');
}
}; // End of addQuickCommand
};
/**
* 更新指定的快捷指令
@@ -53,7 +50,7 @@ export const updateQuickCommand = async (id: number, name: string | null, comman
console.error('更新快捷指令时出错:', err.message);
throw new Error('无法更新快捷指令');
}
}; // End of updateQuickCommand
};
/**
* 根据 ID 删除指定的快捷指令
@@ -70,7 +67,7 @@ export const deleteQuickCommand = async (id: number): Promise<boolean> => {
console.error('删除快捷指令时出错:', err.message);
throw new Error('无法删除快捷指令');
}
}; // End of deleteQuickCommand
};
/**
* 获取所有快捷指令
@@ -91,7 +88,7 @@ export const getAllQuickCommands = async (sortBy: 'name' | 'usage_count' = 'name
console.error('获取快捷指令时出错:', err.message);
throw new Error('无法获取快捷指令');
}
}; // End of getAllQuickCommands
};
/**
* 增加指定快捷指令的使用次数
@@ -108,7 +105,7 @@ export const incrementUsageCount = async (id: number): Promise<boolean> => {
console.error('增加快捷指令使用次数时出错:', err.message);
throw new Error('无法增加快捷指令使用次数');
}
}; // End of incrementUsageCount
};
/**
* 根据 ID 查找快捷指令 (用于编辑前获取数据)
@@ -120,9 +117,9 @@ export const findQuickCommandById = async (id: number): Promise<QuickCommand | u
try {
const db = await getDbInstance();
const row = await getDbRow<DbQuickCommandRow>(db, sql, [id]);
return row; // Returns undefined if not found
return row;
} catch (err: any) {
console.error('查找快捷指令时出错:', err.message);
throw new Error('无法查找快捷指令');
}
}; // End of findQuickCommandById
};
@@ -1,20 +1,16 @@
// packages/backend/src/repositories/settings.repository.ts
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
import { SidebarConfig, LayoutNode, PaneName } from '../types/settings.types'; // <-- Import LayoutNode and PaneName
import { CaptchaSettings } from '../types/settings.types'; // <-- Import CaptchaSettings
import * as sqlite3 from 'sqlite3'; // Import sqlite3 for Database type hint
import { SidebarConfig, LayoutNode, PaneName } from '../types/settings.types';
import { CaptchaSettings } from '../types/settings.types';
import * as sqlite3 from 'sqlite3';
// Define keys for specific settings
const SIDEBAR_CONFIG_KEY = 'sidebarConfig';
const CAPTCHA_CONFIG_KEY = 'captchaConfig'; // <-- Add key for CAPTCHA settings
const CAPTCHA_CONFIG_KEY = 'captchaConfig';
export interface Setting {
key: string;
value: string;
}
// Define the expected row structure from the database if different from Setting
// In this case, it seems Setting matches the SELECT columns.
type DbSettingRow = Setting;
export const settingsRepository = {
@@ -30,13 +26,12 @@ export const settingsRepository = {
},
async getSetting(key: string): Promise<string | null> {
console.log(`[Repository] Attempting to get setting with key: ${key}`);
console.log(`[仓库] 尝试获取键为 ${key} 的设置`);
try {
const db = await getDbInstance();
// Use the correct type for the expected row structure
const row = await getDbRow<{ value: string }>(db, 'SELECT value FROM settings WHERE key = ?', [key]);
const value = row ? row.value : null;
console.log(`[Repository] Found value for key ${key}:`, value);
console.log(`[仓库] 找到键 ${key} 的值:`, value);
return value;
} catch (err: any) {
console.error(`[Repository] 获取设置项 ${key} 时出错:`, err.message);
@@ -45,7 +40,7 @@ export const settingsRepository = {
},
async setSetting(key: string, value: string): Promise<void> {
const now = Math.floor(Date.now() / 1000); // Use seconds
const now = Math.floor(Date.now() / 1000);
const sql = `INSERT INTO settings (key, value, created_at, updated_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(key) DO UPDATE SET
@@ -53,27 +48,27 @@ export const settingsRepository = {
updated_at = excluded.updated_at`;
const params = [key, value, now, now];
console.log(`[Repository] Attempting to set setting. Key: ${key}, Value: ${value}`);
console.log(`[Repository] Executing SQL: ${sql} with params: ${JSON.stringify(params)}`);
console.log(`[仓库] 尝试设置设置项。键: ${key}, : ${value}`);
console.log(`[仓库] 执行 SQL: ${sql},参数: ${JSON.stringify(params)}`);
try {
const db = await getDbInstance();
const result = await runDb(db, sql, params);
console.log(`[Repository] Successfully set setting for key: ${key}. Rows affected: ${result.changes}`);
console.log(`[仓库] 成功设置键为 ${key} 的设置项。影响行数: ${result.changes}`);
} catch (err: any) {
console.error(`[Repository] 设置设置项 ${key} 时出错:`, err.message);
throw new Error(`设置设置项 ${key} 失败`);
}
},
async deleteSetting(key: string): Promise<boolean> { // Return boolean indicating success
console.log(`[Repository] Attempting to delete setting with key: ${key}`);
async deleteSetting(key: string): Promise<boolean> {
console.log(`[仓库] 尝试删除键为 ${key} 的设置`);
const sql = 'DELETE FROM settings WHERE key = ?';
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [key]);
console.log(`[Repository] Successfully deleted setting for key: ${key}. Rows affected: ${result.changes}`);
return result.changes > 0; // Return true if a row was deleted
console.log(`[仓库] 成功删除键为 ${key} 的设置。影响行数: ${result.changes}`);
return result.changes > 0;
} catch (err: any) {
console.error(`[Repository] 删除设置项 ${key} 时出错:`, err.message);
throw new Error(`删除设置项 ${key} 失败`);
@@ -81,28 +76,23 @@ export const settingsRepository = {
},
async setMultipleSettings(settings: Record<string, string>): Promise<void> {
console.log('[Repository] setMultipleSettings called with:', JSON.stringify(settings));
// Use Promise.all with the async setSetting method
// Note: 'this' inside map refers to the settingsRepository object correctly here
console.log('[仓库] 调用 setMultipleSettings,参数:', JSON.stringify(settings));
const promises = Object.entries(settings).map(([key, value]) =>
this.setSetting(key, value)
);
try {
await Promise.all(promises);
console.log('[Repository] setMultipleSettings finished successfully.');
console.log('[仓库] setMultipleSettings 成功完成。');
} catch (error) {
console.error('[Repository] setMultipleSettings failed:', error);
// Re-throw the error or handle it as needed
console.error('[仓库] setMultipleSettings 失败:', error);
throw new Error('批量设置失败');
}
},
};
// --- Specific Setting Getters/Setters ---
/**
* 获取侧栏配置
* @returns Promise<SidebarConfig> - Returns the parsed config or default
*/
export const getSidebarConfig = async (): Promise<SidebarConfig> => {
const defaultValue: SidebarConfig = { left: [], right: [] };
@@ -111,44 +101,38 @@ export const getSidebarConfig = async (): Promise<SidebarConfig> => {
if (jsonString) {
try {
const config = JSON.parse(jsonString);
// Basic validation
if (config && Array.isArray(config.left) && Array.isArray(config.right)) {
// TODO: Add deeper validation if needed (e.g., check if items are valid PaneName)
// TODO: 如果需要,添加更深入的验证(例如,检查项目是否为有效的 PaneName
return config as SidebarConfig;
}
console.warn(`[SettingsRepo] Invalid sidebarConfig format found in DB: ${jsonString}. Returning default.`);
console.warn(`[设置仓库] 在数据库中发现无效的 sidebarConfig 格式: ${jsonString}。返回默认值。`);
} catch (parseError) {
console.error(`[SettingsRepo] Failed to parse sidebarConfig JSON from DB: ${jsonString}`, parseError);
console.error(`[设置仓库] 从数据库解析 sidebarConfig JSON 失败: ${jsonString}`, parseError);
}
}
} catch (error) {
console.error(`[SettingsRepo] Error fetching sidebar config setting (key: ${SIDEBAR_CONFIG_KEY}):`, error);
console.error(`[设置仓库] 获取侧边栏配置设置时出错 (键: ${SIDEBAR_CONFIG_KEY}):`, error);
}
// Return default if not found, invalid, or error occurred
return defaultValue;
};
/**
* 设置侧栏配置
* @param config - The sidebar configuration object
*/
export const setSidebarConfig = async (config: SidebarConfig): Promise<void> => {
try {
// Basic validation before stringifying
if (!config || typeof config !== 'object' || !Array.isArray(config.left) || !Array.isArray(config.right)) {
throw new Error('Invalid sidebar config object provided.');
throw new Error('提供了无效的侧边栏配置对象。');
}
// TODO: Add deeper validation if needed (e.g., check PaneName validity)
// TODO: 如果需要,添加更深入的验证(例如,检查 PaneName 的有效性)
const jsonString = JSON.stringify(config);
await settingsRepository.setSetting(SIDEBAR_CONFIG_KEY, jsonString);
} catch (error) {
console.error(`[SettingsRepo] Error setting sidebar config (key: ${SIDEBAR_CONFIG_KEY}):`, error);
throw new Error('Failed to save sidebar configuration.');
console.error(`[设置仓库] 设置侧边栏配置时出错 (键: ${SIDEBAR_CONFIG_KEY}):`, error);
throw new Error('保存侧边栏配置失败。');
}
};
// --- CAPTCHA Settings ---
/**
* 获取 CAPTCHA 配置
* @returns Promise<CaptchaSettings> - 返回解析后的配置或默认值
@@ -158,18 +142,16 @@ export const getCaptchaConfig = async (): Promise<CaptchaSettings> => {
enabled: false,
provider: 'none',
hcaptchaSiteKey: '',
hcaptchaSecretKey: '', // Secret keys should ideally not have defaults stored directly here if possible
hcaptchaSecretKey: '',
recaptchaSiteKey: '',
recaptchaSecretKey: '', // Secret keys should ideally not have defaults stored directly here if possible
recaptchaSecretKey: '',
};
try {
const jsonString = await settingsRepository.getSetting(CAPTCHA_CONFIG_KEY);
if (jsonString) {
try {
const config = JSON.parse(jsonString);
// Basic validation (add more specific checks if needed)
if (config && typeof config.enabled === 'boolean' && typeof config.provider === 'string') {
// Ensure all keys exist, even if undefined/null from older saves
return {
enabled: config.enabled ?? defaultValue.enabled,
provider: config.provider ?? defaultValue.provider,
@@ -179,29 +161,25 @@ export const getCaptchaConfig = async (): Promise<CaptchaSettings> => {
recaptchaSecretKey: config.recaptchaSecretKey ?? defaultValue.recaptchaSecretKey,
} as CaptchaSettings;
}
console.warn(`[SettingsRepo] Invalid captchaConfig format found in DB: ${jsonString}. Returning default.`);
console.warn(`[设置仓库] 在数据库中发现无效的 captchaConfig 格式: ${jsonString}。返回默认值。`);
} catch (parseError) {
console.error(`[SettingsRepo] Failed to parse captchaConfig JSON from DB: ${jsonString}`, parseError);
console.error(`[设置仓库] 从数据库解析 captchaConfig JSON 失败: ${jsonString}`, parseError);
}
}
} catch (error) {
console.error(`[SettingsRepo] Error fetching captcha config setting (key: ${CAPTCHA_CONFIG_KEY}):`, error);
console.error(`[设置仓库] 获取 CAPTCHA 配置设置时出错 (键: ${CAPTCHA_CONFIG_KEY}):`, error);
}
// Return default if not found, invalid, or error occurred
return defaultValue;
};
/**
* 设置 CAPTCHA 配置
* @param config - The CAPTCHA configuration object
*/
export const setCaptchaConfig = async (config: CaptchaSettings): Promise<void> => {
try {
// Basic validation before stringifying
if (!config || typeof config !== 'object' || typeof config.enabled !== 'boolean' || typeof config.provider !== 'string') {
throw new Error('Invalid CAPTCHA config object provided.');
throw new Error('提供了无效的 CAPTCHA 配置对象。');
}
// Ensure secret keys are strings, even if empty
config.hcaptchaSecretKey = config.hcaptchaSecretKey || '';
config.recaptchaSecretKey = config.recaptchaSecretKey || '';
config.hcaptchaSiteKey = config.hcaptchaSiteKey || '';
@@ -210,22 +188,16 @@ export const setCaptchaConfig = async (config: CaptchaSettings): Promise<void> =
const jsonString = JSON.stringify(config);
await settingsRepository.setSetting(CAPTCHA_CONFIG_KEY, jsonString);
} catch (error) {
console.error(`[SettingsRepo] Error setting CAPTCHA config (key: ${CAPTCHA_CONFIG_KEY}):`, error);
throw new Error('Failed to save CAPTCHA configuration.');
console.error(`[设置仓库] 设置 CAPTCHA 配置时出错 (键: ${CAPTCHA_CONFIG_KEY}):`, error);
throw new Error('保存 CAPTCHA 配置失败。');
}
};
// --- Initialization ---
/**
* Ensures default settings exist in the settings table.
* This function should be called during database initialization.
* @param db - The active database instance
* 确保设置表中存在默认设置。
* 此函数应在数据库初始化期间调用。
*/
export const ensureDefaultSettingsExist = async (db: sqlite3.Database): Promise<void> => {
// --- Define Default Structures Here ---
// Use OmitIdRecursive helper type if needed, or define structure without IDs
type OmitIdRecursive<T> = T extends object
? { [K in keyof Omit<T, 'id'>]: OmitIdRecursive<T[K]> }
: T;
@@ -279,36 +251,34 @@ export const ensureDefaultSettingsExist = async (db: sqlite3.Database): Promise<
recaptchaSecretKey: '',
};
// --- Define All Default Settings ---
const defaultSettings: Record<string, string> = {
ipWhitelistEnabled: 'false',
ipWhitelist: '',
maxLoginAttempts: '5',
loginBanDuration: '300', // 5 minutes in seconds
focusSwitcherSequence: JSON.stringify(["quickCommandsSearch", "commandHistorySearch", "fileManagerSearch", "commandInput", "terminalSearch"]), // Default focus sequence
navBarVisible: 'true', // Default nav bar visibility
layoutTree: JSON.stringify(defaultLayoutTreeStructure), // Use the defined structure
autoCopyOnSelect: 'false', // Default auto copy setting
showPopupFileEditor: 'false', // Default popup editor setting
shareFileEditorTabs: 'true', // Default editor tab sharing
dockerStatusIntervalSeconds: '5', // Default Docker refresh interval
dockerDefaultExpand: 'false', // Default Docker expand state
statusMonitorIntervalSeconds: '3', // Default Status Monitor interval
[SIDEBAR_CONFIG_KEY]: JSON.stringify(defaultSidebarPanesStructure), // Use the defined structure
[CAPTCHA_CONFIG_KEY]: JSON.stringify(defaultCaptchaSettings), // Add default CAPTCHA settings
// Add other default settings here
loginBanDuration: '300',
focusSwitcherSequence: JSON.stringify(["quickCommandsSearch", "commandHistorySearch", "fileManagerSearch", "commandInput", "terminalSearch"]),
navBarVisible: 'true',
layoutTree: JSON.stringify(defaultLayoutTreeStructure),
autoCopyOnSelect: 'false',
showPopupFileEditor: 'false',
shareFileEditorTabs: 'true',
dockerStatusIntervalSeconds: '5',
dockerDefaultExpand: 'false',
statusMonitorIntervalSeconds: '3',
[SIDEBAR_CONFIG_KEY]: JSON.stringify(defaultSidebarPanesStructure),
[CAPTCHA_CONFIG_KEY]: JSON.stringify(defaultCaptchaSettings),
};
const nowSeconds = Math.floor(Date.now() / 1000);
const sqlInsertOrIgnore = `INSERT OR IGNORE INTO settings (key, value, created_at, updated_at) VALUES (?, ?, ?, ?)`;
console.log('[SettingsRepo] Ensuring default settings exist...');
console.log('[设置仓库] 确保默认设置存在...');
try {
for (const [key, value] of Object.entries(defaultSettings)) {
await runDb(db, sqlInsertOrIgnore, [key, value, nowSeconds, nowSeconds]);
}
console.log('[SettingsRepo] Default settings check complete.');
console.log('[设置仓库] 默认设置检查完成。');
} catch (err: any) {
console.error(`[SettingsRepo] Error ensuring default settings:`, err.message);
throw new Error(`Failed to ensure default settings: ${err.message}`);
console.error(`[设置仓库] 确保默认设置时出错:`, err.message);
throw new Error(`确保默认设置失败: ${err.message}`);
}
};
@@ -1,13 +1,8 @@
// packages/backend/src/repositories/tag.repository.ts
import { Database, Statement } from 'sqlite3'; // Keep Statement if using prepare directly, otherwise remove
// Import new async helpers and the instance getter
import { Database, Statement } from 'sqlite3';
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
// Remove top-level db instance
// const db = getDb();
// 定义 Tag 类型 (可以共享到 types 文件)
// Let's assume TagData is the correct interface for a row from the 'tags' table
export interface TagData {
id: number;
name: string;
@@ -24,7 +19,7 @@ export const findAllTags = async (): Promise<TagData[]> => {
const rows = await allDb<TagData>(db, `SELECT * FROM tags ORDER BY name ASC`);
return rows;
} catch (err: any) {
console.error('Repository: 查询标签列表时出错:', err.message);
console.error('[仓库] 查询标签列表时出错:', err.message);
throw new Error('获取标签列表失败');
}
};
@@ -38,7 +33,7 @@ export const findTagById = async (id: number): Promise<TagData | null> => {
const row = await getDbRow<TagData>(db, `SELECT * FROM tags WHERE id = ?`, [id]);
return row || null;
} catch (err: any) {
console.error(`Repository: 查询标签 ${id} 时出错:`, err.message);
console.error(`[仓库] 查询标签 ${id} 时出错:`, err.message);
throw new Error('获取标签信息失败');
}
};
@@ -48,19 +43,17 @@ export const findTagById = async (id: number): Promise<TagData | null> => {
* 创建新标签
*/
export const createTag = async (name: string): Promise<number> => {
const now = Math.floor(Date.now() / 1000); // Use seconds for consistency? Check table definition
const now = Math.floor(Date.now() / 1000);
const sql = `INSERT INTO tags (name, created_at, updated_at) VALUES (?, ?, ?)`;
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [name, now, now]);
// Ensure lastID is valid before returning
if (typeof result.lastID !== 'number' || result.lastID <= 0) {
throw new Error('创建标签后未能获取有效的 lastID');
}
return result.lastID;
} catch (err: any) {
console.error('Repository: 创建标签时出错:', err.message);
// Handle unique constraint error specifically if needed
console.error('[仓库] 创建标签时出错:', err.message);
if (err.message.includes('UNIQUE constraint failed')) {
throw new Error(`标签名称 "${name}" 已存在。`);
}
@@ -79,8 +72,7 @@ export const updateTag = async (id: number, name: string): Promise<boolean> => {
const result = await runDb(db, sql, [name, now, id]);
return result.changes > 0;
} catch (err: any) {
console.error(`Repository: 更新标签 ${id} 时出错:`, err.message);
// Handle unique constraint error specifically if needed
console.error(`[仓库] 更新标签 ${id} 时出错:`, err.message);
if (err.message.includes('UNIQUE constraint failed')) {
throw new Error(`标签名称 "${name}" 已存在。`);
}
@@ -92,15 +84,13 @@ export const updateTag = async (id: number, name: string): Promise<boolean> => {
* 删除标签
*/
export const deleteTag = async (id: number): Promise<boolean> => {
// Note: connection_tags junction table has ON DELETE CASCADE for tag_id,
// so related entries there will be deleted automatically.
const sql = `DELETE FROM tags WHERE id = ?`;
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [id]);
return result.changes > 0;
} catch (err: any) {
console.error(`Repository: 删除标签 ${id} 时出错:`, err.message);
console.error(`[仓库] 删除标签 ${id} 时出错:`, err.message);
throw new Error('删除标签失败');
}
};
@@ -1,12 +1,7 @@
// packages/backend/src/repositories/terminal-theme.repository.ts
import { Database } from 'sqlite3'; // Import Database type if needed for type hints
import { getDbInstance, runDb, getDb, allDb } from '../database/connection'; // Import new async helpers, including getDb
// Remove the incorrect import of DbTerminalThemeRow
import { Database } from 'sqlite3';
import { getDbInstance, runDb, getDb, allDb } from '../database/connection';
import { TerminalTheme, CreateTerminalThemeDto, UpdateTerminalThemeDto } from '../types/terminal-theme.types';
import { defaultXtermTheme } from '../config/default-themes';
// Define the interface for the raw database row structure
// Interface matching the schema in schema.ts
interface DbTerminalThemeRow {
id: number;
name: string;
@@ -36,23 +31,18 @@ interface DbTerminalThemeRow {
updated_at: number;
}
// SQL_CREATE_TABLE and createTableIfNotExists removed as initialization is handled in database/connection.ts
// 辅助函数:将数据库行转换为 TerminalTheme 对象
// Add type annotation for the input row
const mapRowToTerminalTheme = (row: DbTerminalThemeRow): TerminalTheme => {
// Basic check if row exists and has id property
if (!row || typeof row.id === 'undefined') {
console.error("mapRowToTerminalTheme received invalid row:", row);
// Return a default or throw an error, depending on desired behavior
// For now, let's throw an error to make the issue visible
throw new Error("Invalid database row provided to mapRowToTerminalTheme");
}
try {
return {
_id: row.id.toString(),
name: row.name,
// Reconstruct themeData from individual columns
themeData: {
foreground: row.foreground ?? undefined,
background: row.background ?? undefined,
@@ -77,16 +67,12 @@ const mapRowToTerminalTheme = (row: DbTerminalThemeRow): TerminalTheme => {
brightWhite: row.bright_white ?? undefined,
},
isPreset: row.theme_type === 'preset',
// isSystemDefault needs to be handled differently, maybe based on name 'default'?
// For now, let's assume it's not directly mapped or needed here.
isSystemDefault: row.name === 'default', // Tentative mapping based on name
isSystemDefault: row.name === 'default',
createdAt: row.created_at,
updatedAt: row.updated_at,
};
} catch (e: any) {
// Log the entire row for debugging instead of the non-existent theme_data
console.error(`Error mapping theme data for theme ID ${row.id}:`, e.message, "Raw row:", row);
// Return a partially mapped object or throw error
throw new Error(`Failed to map theme data for theme ID ${row.id}`);
}
};
@@ -98,23 +84,20 @@ const mapRowToTerminalTheme = (row: DbTerminalThemeRow): TerminalTheme => {
export const findAllThemes = async (): Promise<TerminalTheme[]> => {
try {
const db = await getDbInstance();
// Specify the expected row type for allDb
// Correct the ORDER BY clause to use theme_type and sort presets first
const rows = await allDb<DbTerminalThemeRow>(db, 'SELECT * FROM terminal_themes ORDER BY CASE theme_type WHEN \'preset\' THEN 0 ELSE 1 END ASC, name ASC');
// Filter out potential errors during mapping
return rows.map(row => {
try {
return mapRowToTerminalTheme(row);
} catch (mapError: any) {
console.error(`Error mapping row ID ${row?.id}:`, mapError.message);
return null; // Or handle differently
return null;
}
}).filter((theme): theme is TerminalTheme => theme !== null);
} catch (err: any) { // Add type annotation for err
} catch (err: any) {
console.error('查询所有终端主题失败:', err.message);
// 添加详细错误日志
console.error('详细错误:', err);
throw new Error('查询终端主题失败'); // Re-throw or handle error appropriately
throw new Error('查询终端主题失败');
}
};
@@ -126,15 +109,13 @@ export const findAllThemes = async (): Promise<TerminalTheme[]> => {
export const findThemeById = async (id: number): Promise<TerminalTheme | null> => {
if (isNaN(id) || id <= 0) {
console.error("findThemeById called with invalid ID:", id);
return null; // Return null for invalid IDs
return null;
}
try {
const db = await getDbInstance();
// Specify the expected row type for getDbRow
// Use getDb instead of the non-existent getDbRow
const row = await getDb<DbTerminalThemeRow>(db, 'SELECT * FROM terminal_themes WHERE id = ?', [id]);
return row ? mapRowToTerminalTheme(row) : null;
} catch (err: any) { // Add type annotation for err
} catch (err: any) {
console.error(`查询 ID 为 ${id} 的终端主题失败:`, err.message);
throw new Error('查询终端主题失败');
}
@@ -146,10 +127,10 @@ export const findThemeById = async (id: number): Promise<TerminalTheme | null> =
* @returns Promise<TerminalTheme> 新创建的主题
*/
export const createTheme = async (themeDto: CreateTerminalThemeDto): Promise<TerminalTheme> => {
const nowSeconds = Math.floor(Date.now() / 1000); // Use seconds for DB consistency
const nowSeconds = Math.floor(Date.now() / 1000);
const theme = themeDto.themeData;
// Define columns based on the DbTerminalThemeRow interface (excluding id, created_at, updated_at)
const columns = [
'name', 'theme_type', 'foreground', 'background', 'cursor', 'cursor_accent',
'selection_background', 'black', 'red', 'green', 'yellow', 'blue',
@@ -157,14 +138,13 @@ export const createTheme = async (themeDto: CreateTerminalThemeDto): Promise<Ter
'bright_yellow', 'bright_blue', 'bright_magenta', 'bright_cyan', 'bright_white',
'created_at', 'updated_at'
];
// Map themeDto data to corresponding columns, using null for missing optional values
const values = [
themeDto.name, 'user', // theme_type is 'user' for created themes
themeDto.name, 'user',
theme?.foreground ?? null, theme?.background ?? null, theme?.cursor ?? null, theme?.cursorAccent ?? null,
theme?.selectionBackground ?? null, theme?.black ?? null, theme?.red ?? null, theme?.green ?? null, theme?.yellow ?? null, theme?.blue ?? null,
theme?.magenta ?? null, theme?.cyan ?? null, theme?.white ?? null, theme?.brightBlack ?? null, theme?.brightRed ?? null, theme?.brightGreen ?? null,
theme?.brightYellow ?? null, theme?.brightBlue ?? null, theme?.brightMagenta ?? null, theme?.brightCyan ?? null, theme?.brightWhite ?? null,
nowSeconds, nowSeconds // Use seconds for timestamps
nowSeconds, nowSeconds
];
const placeholders = columns.map(() => '?').join(', ');
@@ -175,8 +155,7 @@ export const createTheme = async (themeDto: CreateTerminalThemeDto): Promise<Ter
try {
const db = await getDbInstance();
const result = await runDb(db, sql, values); // Use the mapped values array
// Ensure lastID is valid before trying to find the theme
const result = await runDb(db, sql, values);
if (typeof result.lastID !== 'number' || result.lastID <= 0) {
throw new Error('创建主题后未能获取有效的 lastID');
}
@@ -184,10 +163,9 @@ export const createTheme = async (themeDto: CreateTerminalThemeDto): Promise<Ter
if (newTheme) {
return newTheme;
} else {
// This case might happen if findThemeById fails for some reason
throw new Error(`创建主题后未能检索到 ID 为 ${result.lastID} 的主题`);
}
} catch (err: any) { // Add type annotation for err
} catch (err: any) {
console.error('创建新终端主题失败:', err.message);
if (err.message.includes('UNIQUE constraint failed')) {
throw new Error(`主题名称 "${themeDto.name}" 已存在。`);
@@ -215,7 +193,7 @@ export const updateTheme = async (id: number, themeDto: UpdateTerminalThemeDto):
const db = await getDbInstance();
const result = await runDb(db, sql, [themeDto.name, themeDataJson, now, id]);
return result.changes > 0;
} catch (err: any) { // Add type annotation for err
} catch (err: any) {
console.error(`更新 ID 为 ${id} 的终端主题失败:`, err.message);
if (err.message.includes('UNIQUE constraint failed')) {
throw new Error(`主题名称 "${themeDto.name}" 已存在。`);
@@ -231,13 +209,12 @@ export const updateTheme = async (id: number, themeDto: UpdateTerminalThemeDto):
* @returns Promise<boolean> 是否成功删除
*/
export const deleteTheme = async (id: number): Promise<boolean> => {
// Correct the WHERE clause to use theme_type = 'user' instead of is_preset = 0
const sql = 'DELETE FROM terminal_themes WHERE id = ? AND theme_type = \'user\'';
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [id]);
return result.changes > 0;
} catch (err: any) { // Add type annotation for err
} catch (err: any) {
console.error(`删除 ID 为 ${id} 的终端主题失败:`, err.message);
throw new Error('删除终端主题失败');
}
@@ -252,20 +229,15 @@ export const initializePresetThemes = async (db: Database, presets: Array<Omit<T
console.log('[DB Init] 开始检查并初始化预设主题...');
// 在这里添加日志,显示总共要处理多少个预设主题
console.log(`[DB Init] 发现 ${presets.length} 个预设主题定义。`);
const nowSeconds = Math.floor(Date.now() / 1000); // Use seconds for DB consistency
// const db = await getDbInstance(); // Use the passed db instance
const nowSeconds = Math.floor(Date.now() / 1000);
for (const preset of presets) {
// 在循环开始时添加日志,显示正在处理哪个主题
console.log(`[DB Init] 正在处理预设主题: "${preset.name}"`);
try {
// Check using name and theme_type
const existing = await getDb<{ id: number }>(db, `SELECT id FROM terminal_themes WHERE name = ? AND theme_type = 'preset'`, [preset.name]);
if (!existing) {
// 添加日志,表明正在插入新主题
console.log(`[DB Init] 主题 "${preset.name}" 不存在,正在插入...`);
// Map preset.themeData to individual columns
const theme = preset.themeData;
const columns = [
'name', 'theme_type', 'foreground', 'background', 'cursor', 'cursor_accent',
@@ -290,14 +262,10 @@ export const initializePresetThemes = async (db: Database, presets: Array<Omit<T
await runDb(db, insertSql, values);
console.log(`[DB Init] 预设主题 "${preset.name}" 已初始化到数据库。`);
} else {
// 取消注释并添加日志,表明主题已存在
console.log(`[DB Init] 预设主题 "${preset.name}" 已存在,跳过初始化。`);
}
} catch (err: any) { // Add type annotation for err
// Remove reference to non-existent preset_key
} catch (err: any) {
console.error(`[DB Init] 处理预设主题 "${preset.name}" 时出错:`, err.message);
// Decide if one error should stop the whole process or just log and continue
// throw err; // Uncomment to stop on first error
}
}
console.log('[DB Init] 预设主题检查和初始化完成。');