Initial clean project import
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import DB from './main';
|
||||
import { Log } from '../log/main';
|
||||
import { getCommonResourcePath } from '../../lib/resource-path';
|
||||
import { AppEnv } from '../env';
|
||||
|
||||
interface BuiltinAvatar {
|
||||
name: string;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
const BUILTIN_AVATARS: BuiltinAvatar[] = [
|
||||
{ name: '内置形象-07', fileName: '07.mp4' },
|
||||
{ name: '内置形象-25', fileName: '25.mp4' },
|
||||
{ name: '内置形象-4月5日', fileName: '4月5日.mp4' },
|
||||
];
|
||||
|
||||
const getAvatarSourcePath = (fileName: string): string => {
|
||||
return getCommonResourcePath(path.join('shuiziren', fileName));
|
||||
};
|
||||
|
||||
const getHubDir = (): string => {
|
||||
return path.join(AppEnv.dataRoot, 'hub', 'file');
|
||||
};
|
||||
|
||||
const copyToHub = (srcPath: string, destDir: string, fileName: string): string => {
|
||||
if (!fs.existsSync(destDir)) {
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
}
|
||||
|
||||
const baseName = path.basename(fileName, path.extname(fileName));
|
||||
const ext = path.extname(fileName);
|
||||
let destPath = path.join(destDir, fileName);
|
||||
let counter = 1;
|
||||
|
||||
while (fs.existsSync(destPath)) {
|
||||
destPath = path.join(destDir, `${baseName}_${counter}${ext}`);
|
||||
counter++;
|
||||
}
|
||||
|
||||
fs.copyFileSync(srcPath, destPath);
|
||||
return destPath;
|
||||
};
|
||||
|
||||
export const initBuiltinAvatars = async (): Promise<void> => {
|
||||
try {
|
||||
Log.info('initBuiltinAvatars.start', '开始检查内置形象');
|
||||
|
||||
const hubDir = getHubDir();
|
||||
let imported = 0;
|
||||
|
||||
for (const avatar of BUILTIN_AVATARS) {
|
||||
try {
|
||||
const existing = await DB.first(
|
||||
`SELECT id FROM data_video_template WHERE name = ?`,
|
||||
[avatar.name]
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
Log.info('initBuiltinAvatars.exists', '形象已存在', { name: avatar.name });
|
||||
continue;
|
||||
}
|
||||
|
||||
const srcPath = getAvatarSourcePath(avatar.fileName);
|
||||
|
||||
if (!fs.existsSync(srcPath)) {
|
||||
Log.warn('initBuiltinAvatars.fileNotFound', '内置形象文件不存在', { path: srcPath });
|
||||
continue;
|
||||
}
|
||||
|
||||
const savedPath = copyToHub(srcPath, hubDir, avatar.fileName);
|
||||
|
||||
await DB.insert(
|
||||
`INSERT INTO data_video_template (name, video, info) VALUES (?, ?, ?)`,
|
||||
[avatar.name, savedPath, JSON.stringify({})]
|
||||
);
|
||||
|
||||
imported++;
|
||||
Log.info('initBuiltinAvatars.imported', '已导入内置形象', {
|
||||
name: avatar.name,
|
||||
path: savedPath,
|
||||
});
|
||||
} catch (avatarError) {
|
||||
Log.error('initBuiltinAvatars.avatarError', '导入单个形象失败', {
|
||||
name: avatar.name,
|
||||
error: avatarError,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Log.info('initBuiltinAvatars.complete', '内置形象初始化完成', { imported });
|
||||
} catch (error) {
|
||||
Log.error('initBuiltinAvatars.error', '内置形象初始化失败', { error });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,852 @@
|
||||
/**
|
||||
* 系统默认模板初始化模块
|
||||
* 在应用启动时自动检查并初始化系统默认模板
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import DB from './main';
|
||||
import { Log } from '../log/main';
|
||||
import { AppEnv } from '../env';
|
||||
import { getCommonResourcePath } from '../../lib/resource-path';
|
||||
import { isPackaged } from '../../lib/env';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
interface CoverTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
is_system: number;
|
||||
config: any;
|
||||
thumbnailPath?: string;
|
||||
}
|
||||
|
||||
interface SubtitleStyle {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
preview: string;
|
||||
is_system: number;
|
||||
fontName: string;
|
||||
fontSize: number;
|
||||
fontColor: string;
|
||||
outlineColor: string;
|
||||
outlineWidth: number;
|
||||
shadowOffset?: number;
|
||||
shadowColor?: string;
|
||||
shadowBlur?: number;
|
||||
backgroundColor: string;
|
||||
backgroundOpacity: number;
|
||||
position: string;
|
||||
alignment: string;
|
||||
}
|
||||
|
||||
interface SubtitleTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
isSystem: boolean;
|
||||
readonly?: boolean;
|
||||
createdAt: number;
|
||||
config: any;
|
||||
}
|
||||
|
||||
interface SystemTemplatesConfig {
|
||||
version: string;
|
||||
description: string;
|
||||
timestamp: string;
|
||||
subtitleTemplates: SubtitleTemplate[];
|
||||
coverTemplates: CoverTemplate[];
|
||||
}
|
||||
|
||||
interface DefaultTemplatesConfig {
|
||||
version: string;
|
||||
description: string;
|
||||
coverTemplates: CoverTemplate[];
|
||||
subtitleStyles: SubtitleStyle[];
|
||||
}
|
||||
|
||||
interface PendingInlineRestoreResult {
|
||||
templates: SubtitleTemplate[];
|
||||
forceRestoreIds: Set<string>;
|
||||
pendingFilePath: string | null;
|
||||
}
|
||||
|
||||
const DEV_INLINE_RESTORE_PENDING_FILE = 'restore-inline-templates.pending.json';
|
||||
const DEV_SYSTEM_TEMPLATES_MARKER_FILE = 'dev-system-templates.seeded.json';
|
||||
const FORMAL_INLINE_SYSTEM_TEMPLATE_IDS = new Set([
|
||||
'template_system_11',
|
||||
'template_system_22',
|
||||
'template_system_33',
|
||||
'template_system_44',
|
||||
'template_system_55',
|
||||
'template_system_66',
|
||||
'template_system_77',
|
||||
'template_system_88',
|
||||
]);
|
||||
|
||||
const syncFormalInlineTemplateSoundEffects = (
|
||||
templateId: string,
|
||||
bundledKeywordGroupsStyles: any,
|
||||
existingKeywordGroupsStyles: any,
|
||||
): { keywordGroupsStyles: any; updated: boolean } => {
|
||||
if (!FORMAL_INLINE_SYSTEM_TEMPLATE_IDS.has(templateId)) {
|
||||
return { keywordGroupsStyles: existingKeywordGroupsStyles, updated: false };
|
||||
}
|
||||
|
||||
if (!Array.isArray(bundledKeywordGroupsStyles)) {
|
||||
return { keywordGroupsStyles: existingKeywordGroupsStyles, updated: false };
|
||||
}
|
||||
|
||||
if (!Array.isArray(existingKeywordGroupsStyles)) {
|
||||
return { keywordGroupsStyles: bundledKeywordGroupsStyles, updated: true };
|
||||
}
|
||||
|
||||
let updated = false;
|
||||
const mergedKeywordGroupsStyles = bundledKeywordGroupsStyles.map((bundledGroup, index) => {
|
||||
const existingGroup = existingKeywordGroupsStyles[index];
|
||||
if (!existingGroup) {
|
||||
updated = true;
|
||||
return bundledGroup;
|
||||
}
|
||||
|
||||
const mergedGroup = {
|
||||
...existingGroup,
|
||||
};
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(bundledGroup, 'soundEffectId')) {
|
||||
const nextSoundEffectId = bundledGroup.soundEffectId ?? null;
|
||||
if ((existingGroup.soundEffectId ?? null) !== nextSoundEffectId) {
|
||||
updated = true;
|
||||
}
|
||||
mergedGroup.soundEffectId = nextSoundEffectId;
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(bundledGroup, 'soundEffectConfig')) {
|
||||
const nextSoundEffectConfig = bundledGroup.soundEffectConfig ?? null;
|
||||
if (JSON.stringify(existingGroup.soundEffectConfig ?? null) !== JSON.stringify(nextSoundEffectConfig)) {
|
||||
updated = true;
|
||||
}
|
||||
mergedGroup.soundEffectConfig = nextSoundEffectConfig;
|
||||
}
|
||||
|
||||
return mergedGroup;
|
||||
});
|
||||
|
||||
if (existingKeywordGroupsStyles.length > bundledKeywordGroupsStyles.length) {
|
||||
mergedKeywordGroupsStyles.push(...existingKeywordGroupsStyles.slice(bundledKeywordGroupsStyles.length));
|
||||
}
|
||||
|
||||
return { keywordGroupsStyles: mergedKeywordGroupsStyles, updated };
|
||||
};
|
||||
|
||||
const getDevSystemTemplatesMarkerPath = (): string => {
|
||||
const baseDir = AppEnv.dataRoot || process.cwd();
|
||||
return path.join(baseDir, DEV_SYSTEM_TEMPLATES_MARKER_FILE);
|
||||
};
|
||||
|
||||
const writeDevSystemTemplatesMarker = (reason: string): void => {
|
||||
if (isProductionMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const markerPath = getDevSystemTemplatesMarkerPath();
|
||||
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(markerPath), { recursive: true });
|
||||
fs.writeFileSync(markerPath, JSON.stringify({
|
||||
reason,
|
||||
timestamp: Date.now(),
|
||||
}, null, 2), 'utf8');
|
||||
Log.info('initSystemTemplates.writeDevMarker', '已写入开发环境系统模板初始化标记', {
|
||||
markerPath,
|
||||
reason,
|
||||
});
|
||||
} catch (error) {
|
||||
Log.warn('initSystemTemplates.writeDevMarker', '写入开发环境系统模板初始化标记失败', {
|
||||
markerPath,
|
||||
reason,
|
||||
error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const shouldSkipDevSystemTemplateInit = async (): Promise<boolean> => {
|
||||
if (isProductionMode()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const markerPath = getDevSystemTemplatesMarkerPath();
|
||||
if (fs.existsSync(markerPath)) {
|
||||
Log.info('initSystemTemplates.skipDevInit', '检测到开发环境初始化标记,跳过系统模板自动同步', {
|
||||
markerPath,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 确保表中存在 is_system 列
|
||||
*/
|
||||
const ensureIsSystemColumn = async (): Promise<void> => {
|
||||
try {
|
||||
// 检查并添加 cover_templates 表的 is_system 列
|
||||
try {
|
||||
await DB.execute(`
|
||||
ALTER TABLE cover_templates
|
||||
ADD COLUMN is_system INTEGER DEFAULT 0
|
||||
`);
|
||||
Log.info('initSystemTemplates.ensureIsSystemColumn', '已添加 is_system 列到 cover_templates');
|
||||
} catch (error: any) {
|
||||
// 列已存在时会抛出错误,忽略
|
||||
if (error.message && error.message.includes('duplicate column name')) {
|
||||
Log.info('initSystemTemplates.ensureIsSystemColumn', 'cover_templates 的 is_system 列已存在');
|
||||
} else {
|
||||
Log.warn('initSystemTemplates.ensureIsSystemColumn', 'cover_templates 列检查警告', { error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// 检查并添加 subtitle_styles 表的 is_system 列
|
||||
try {
|
||||
await DB.execute(`
|
||||
ALTER TABLE subtitle_styles
|
||||
ADD COLUMN is_system INTEGER DEFAULT 0
|
||||
`);
|
||||
Log.info('initSystemTemplates.ensureIsSystemColumn', '已添加 is_system 列到 subtitle_styles');
|
||||
} catch (error: any) {
|
||||
// 列已存在时会抛出错误,忽略
|
||||
if (error.message && error.message.includes('duplicate column name')) {
|
||||
Log.info('initSystemTemplates.ensureIsSystemColumn', 'subtitle_styles 的 is_system 列已存在');
|
||||
} else {
|
||||
Log.warn('initSystemTemplates.ensureIsSystemColumn', 'subtitle_styles 列检查警告', { error: error.message });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
Log.error('initSystemTemplates.ensureIsSystemColumn', '确保列存在时出错', { error });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 读取系统模板配置文件 (system-templates.json)
|
||||
*/
|
||||
const loadSystemTemplatesConfig = (): SystemTemplatesConfig | null => {
|
||||
try {
|
||||
// 尝试多个可能的路径
|
||||
const possiblePaths = [
|
||||
// 优先尝试使用统一资源路径工具(支持开发和生产环境)
|
||||
getCommonResourcePath('config/system-templates.json'),
|
||||
// 关键修复:直接使用 process.resourcesPath(生产环境)
|
||||
process.resourcesPath ? path.join(process.resourcesPath, 'extra/common/config/system-templates.json') : '',
|
||||
path.join(__dirname, '../config/system-templates.json'),
|
||||
path.join(__dirname, '../../config/system-templates.json'),
|
||||
path.join(process.cwd(), 'electron/config/system-templates.json'),
|
||||
path.join(process.cwd(), 'dist-electron/config/system-templates.json'),
|
||||
// 添加更多备用路径
|
||||
path.join(process.cwd(), 'resources/extra/common/config/system-templates.json'),
|
||||
].filter(p => p); // 过滤空路径
|
||||
|
||||
let configPath: string | null = null;
|
||||
let configContent: string | null = null;
|
||||
|
||||
console.log('[initSystemTemplates] 开始搜索 system-templates.json');
|
||||
console.log('[initSystemTemplates] process.resourcesPath:', process.resourcesPath);
|
||||
console.log('[initSystemTemplates] process.cwd():', process.cwd());
|
||||
|
||||
for (const tryPath of possiblePaths) {
|
||||
console.log('[initSystemTemplates] 尝试加载系统模板:', tryPath, '存在:', fs.existsSync(tryPath));
|
||||
if (fs.existsSync(tryPath)) {
|
||||
configPath = tryPath;
|
||||
configContent = fs.readFileSync(tryPath, 'utf-8');
|
||||
console.log('[initSystemTemplates] ✅ 找到系统模板配置文件:', tryPath);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!configPath || !configContent) {
|
||||
console.warn('[initSystemTemplates] ⚠️ system-templates.json 不存在');
|
||||
Log.warn('initSystemTemplates.loadSystemTemplatesConfig', '系统模板配置文件不存在', { possiblePaths });
|
||||
return null;
|
||||
}
|
||||
|
||||
const config = JSON.parse(configContent) as SystemTemplatesConfig;
|
||||
|
||||
console.log('[initSystemTemplates] 成功加载系统模板配置:', {
|
||||
version: config.version,
|
||||
subtitleCount: config.subtitleTemplates.length,
|
||||
coverCount: config.coverTemplates.length
|
||||
});
|
||||
|
||||
Log.info('initSystemTemplates.loadSystemTemplatesConfig', '成功加载系统模板配置', {
|
||||
version: config.version,
|
||||
subtitleCount: config.subtitleTemplates.length,
|
||||
coverCount: config.coverTemplates.length
|
||||
});
|
||||
|
||||
return config;
|
||||
} catch (error) {
|
||||
console.error('[initSystemTemplates] 加载系统模板配置失败:', error);
|
||||
Log.error('initSystemTemplates.loadSystemTemplatesConfig', '加载系统模板配置失败', { error });
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const applyPendingInlineTemplateRestore = (
|
||||
templates: SubtitleTemplate[],
|
||||
): PendingInlineRestoreResult => {
|
||||
const pendingFilePath = path.join(process.cwd(), DEV_INLINE_RESTORE_PENDING_FILE);
|
||||
const enablePendingRestore = process.env.ENABLE_DEV_INLINE_RESTORE === '1';
|
||||
|
||||
if (!enablePendingRestore || isProductionMode() || !fs.existsSync(pendingFilePath)) {
|
||||
if (!isProductionMode() && !enablePendingRestore && fs.existsSync(pendingFilePath)) {
|
||||
Log.warn('initSystemTemplates.applyPendingInlineTemplateRestore', '检测到待恢复 Inline 模板文件,但已默认忽略。设置 ENABLE_DEV_INLINE_RESTORE=1 可重新启用。', {
|
||||
pendingFilePath,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
templates,
|
||||
forceRestoreIds: new Set<string>(),
|
||||
pendingFilePath: null,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(pendingFilePath, 'utf8');
|
||||
const pending = JSON.parse(raw);
|
||||
const restoreTemplates = Array.isArray(pending?.subtitleTemplates) ? pending.subtitleTemplates : [];
|
||||
const restoreMap = new Map<string, SubtitleTemplate>(
|
||||
restoreTemplates
|
||||
.filter((item: SubtitleTemplate | null | undefined) => Boolean(item?.id))
|
||||
.map((item: SubtitleTemplate) => [item.id, item])
|
||||
);
|
||||
|
||||
if (restoreMap.size === 0) {
|
||||
return {
|
||||
templates,
|
||||
forceRestoreIds: new Set<string>(),
|
||||
pendingFilePath: null,
|
||||
};
|
||||
}
|
||||
|
||||
const forceRestoreIds = new Set<string>();
|
||||
const mergedTemplates = templates.map((template) => {
|
||||
const restoreTemplate = restoreMap.get(template.id);
|
||||
if (!restoreTemplate) {
|
||||
return template;
|
||||
}
|
||||
|
||||
forceRestoreIds.add(template.id);
|
||||
|
||||
return {
|
||||
...template,
|
||||
config: {
|
||||
...template.config,
|
||||
...restoreTemplate.config,
|
||||
previewConfig: template.config?.previewConfig,
|
||||
keywordRenderMode: template.config?.keywordRenderMode,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Log.info('initSystemTemplates.applyPendingInlineTemplateRestore', '检测到待恢复的 Inline 模板配置', {
|
||||
pendingFilePath,
|
||||
count: forceRestoreIds.size,
|
||||
});
|
||||
|
||||
return {
|
||||
templates: mergedTemplates,
|
||||
forceRestoreIds,
|
||||
pendingFilePath,
|
||||
};
|
||||
} catch (error) {
|
||||
Log.error('initSystemTemplates.applyPendingInlineTemplateRestore', '读取待恢复 Inline 模板配置失败', {
|
||||
pendingFilePath,
|
||||
error,
|
||||
});
|
||||
return {
|
||||
templates,
|
||||
forceRestoreIds: new Set<string>(),
|
||||
pendingFilePath: null,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 读取默认模板配置文件
|
||||
*/
|
||||
const loadDefaultTemplatesConfig = (): DefaultTemplatesConfig | null => {
|
||||
try {
|
||||
// 尝试多个可能的路径
|
||||
const possiblePaths = [
|
||||
// 优先尝试使用统一资源路径工具(支持开发和生产环境)
|
||||
getCommonResourcePath('config/default-templates.json'),
|
||||
path.join(__dirname, '../config/default-templates.json'),
|
||||
path.join(__dirname, '../../config/default-templates.json'),
|
||||
path.join(process.cwd(), 'electron/config/default-templates.json'),
|
||||
path.join(process.cwd(), 'dist-electron/config/default-templates.json'),
|
||||
];
|
||||
|
||||
let configPath: string | null = null;
|
||||
let configContent: string | null = null;
|
||||
|
||||
for (const tryPath of possiblePaths) {
|
||||
console.log('[initSystemTemplates] 尝试路径:', tryPath);
|
||||
if (fs.existsSync(tryPath)) {
|
||||
configPath = tryPath;
|
||||
configContent = fs.readFileSync(tryPath, 'utf-8');
|
||||
console.log('[initSystemTemplates] ✅ 找到配置文件:', tryPath);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!configPath || !configContent) {
|
||||
console.error('[initSystemTemplates] ❌ 配置文件不存在,尝试的路径:', possiblePaths);
|
||||
Log.error('initSystemTemplates.loadConfig', '配置文件不存在', { possiblePaths });
|
||||
return null;
|
||||
}
|
||||
|
||||
const config = JSON.parse(configContent) as DefaultTemplatesConfig;
|
||||
|
||||
console.log('[initSystemTemplates] 成功加载配置文件:', {
|
||||
version: config.version,
|
||||
coverCount: config.coverTemplates.length,
|
||||
subtitleCount: config.subtitleStyles.length
|
||||
});
|
||||
|
||||
Log.info('initSystemTemplates.loadConfig', '成功加载配置文件', {
|
||||
version: config.version,
|
||||
coverCount: config.coverTemplates.length,
|
||||
subtitleCount: config.subtitleStyles.length
|
||||
});
|
||||
|
||||
return config;
|
||||
} catch (error) {
|
||||
console.error('[initSystemTemplates] 加载配置文件失败:', error);
|
||||
Log.error('initSystemTemplates.loadConfig', '加载配置文件失败', { error });
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查是否在生产模式
|
||||
*/
|
||||
const isProductionMode = (): boolean => {
|
||||
return isPackaged ||
|
||||
process.env.IS_PACKAGED === 'true' ||
|
||||
process.env.ELECTRON_ENV_PROD === '1' ||
|
||||
process.env.ELECTRON_ENV_PROD === 'true' ||
|
||||
process.env.NODE_ENV === 'production';
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化字幕模板
|
||||
* 从 system-templates.json 导入字幕模板到数据库
|
||||
*/
|
||||
const initSubtitleTemplates = async (
|
||||
templates: SubtitleTemplate[],
|
||||
options?: {
|
||||
forceRestoreIds?: Set<string>;
|
||||
}
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const now = Date.now();
|
||||
const isProd = isProductionMode();
|
||||
const forceRestoreIds = options?.forceRestoreIds || new Set<string>();
|
||||
const systemTemplateIds = templates.filter(template => template.isSystem).map(template => template.id);
|
||||
|
||||
if (systemTemplateIds.length > 0) {
|
||||
const placeholders = systemTemplateIds.map(() => '?').join(', ');
|
||||
await DB.execute(
|
||||
`DELETE FROM subtitle_templates
|
||||
WHERE is_system = 1
|
||||
AND id NOT IN (${placeholders})`,
|
||||
systemTemplateIds
|
||||
);
|
||||
}
|
||||
|
||||
for (const template of templates) {
|
||||
const { id, name, description, isSystem, createdAt, config } = template;
|
||||
const configJson = JSON.stringify(config);
|
||||
|
||||
// 在生产模式下,字幕模板设为只读
|
||||
const readonly = isProd ? 1 : 0;
|
||||
|
||||
// 检查模板是否已存在
|
||||
const existing = await DB.first(
|
||||
`SELECT id, is_system, config FROM subtitle_templates WHERE id = ?`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
// 开发环境下保留数据库中的系统模板修改,避免每次重启都被配置文件覆盖。
|
||||
// 生产环境仍然与配置文件保持同步,确保用户侧系统模板一致。
|
||||
if (isSystem && isProd) {
|
||||
await DB.execute(
|
||||
`UPDATE subtitle_templates
|
||||
SET name = ?, description = ?, config = ?, is_system = ?, readonly = ?, updated_at = ?
|
||||
WHERE id = ?`,
|
||||
[name, description, configJson, 1, readonly, now, id]
|
||||
);
|
||||
Log.info('initSystemTemplates.updateSubtitleTemplate', '同步字幕模板配置', { id, name });
|
||||
console.log(`[initSystemTemplates] ✅ 字幕模板已同步: ${name}`);
|
||||
} else if (isSystem) {
|
||||
if (forceRestoreIds.has(id)) {
|
||||
await DB.execute(
|
||||
`UPDATE subtitle_templates
|
||||
SET name = ?, description = ?, config = ?, is_system = ?, readonly = ?, updated_at = ?
|
||||
WHERE id = ?`,
|
||||
[name, description, configJson, 1, readonly, now, id]
|
||||
);
|
||||
Log.info('initSystemTemplates.restoreDevSubtitleTemplate', '开发环境恢复系统字幕模板配置', { id, name });
|
||||
console.log(`[initSystemTemplates] ℹ️ 开发环境恢复系统字幕模板: ${name}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
let existingConfig: Record<string, any> = {};
|
||||
let shouldBackfillPreview = false;
|
||||
|
||||
try {
|
||||
existingConfig = existing.config ? JSON.parse(existing.config) : {};
|
||||
const existingPreview = existingConfig.previewConfig || {};
|
||||
shouldBackfillPreview = !(
|
||||
existingPreview.backgroundImage ||
|
||||
existingPreview.backgroundKey ||
|
||||
(Array.isArray(existingPreview.titleTexts) && existingPreview.titleTexts.length > 0) ||
|
||||
(Array.isArray(existingPreview.subtitleTexts) && existingPreview.subtitleTexts.length > 0)
|
||||
);
|
||||
} catch (error) {
|
||||
shouldBackfillPreview = true;
|
||||
Log.warn('initSystemTemplates.parseDevSubtitleTemplate', '开发环境解析系统字幕模板失败,尝试回填预览配置', { id, name, error });
|
||||
}
|
||||
|
||||
const soundSyncResult = syncFormalInlineTemplateSoundEffects(
|
||||
id,
|
||||
config.keywordGroupsStyles,
|
||||
existingConfig.keywordGroupsStyles,
|
||||
);
|
||||
|
||||
if (shouldBackfillPreview || soundSyncResult.updated) {
|
||||
const mergedConfig = {
|
||||
...existingConfig,
|
||||
...(soundSyncResult.updated ? {
|
||||
keywordGroupsStyles: soundSyncResult.keywordGroupsStyles,
|
||||
} : {}),
|
||||
previewConfig: {
|
||||
...(config.previewConfig || {}),
|
||||
...(existingConfig.previewConfig || {}),
|
||||
},
|
||||
};
|
||||
|
||||
await DB.execute(
|
||||
`UPDATE subtitle_templates
|
||||
SET config = ?, is_system = ?, readonly = ?, updated_at = ?
|
||||
WHERE id = ?`,
|
||||
[JSON.stringify(mergedConfig), 1, readonly, now, id]
|
||||
);
|
||||
Log.info('initSystemTemplates.backfillDevSubtitleTemplateConfig', '开发环境补齐系统字幕模板配置', {
|
||||
id,
|
||||
name,
|
||||
previewBackfilled: shouldBackfillPreview,
|
||||
inlineSoundSynced: soundSyncResult.updated,
|
||||
});
|
||||
console.log(`[initSystemTemplates] ℹ️ 开发环境补齐系统字幕模板配置: ${name}`);
|
||||
} else {
|
||||
Log.info('initSystemTemplates.keepDevSubtitleTemplate', '开发环境保留数据库中的系统字幕模板', { id, name });
|
||||
console.log(`[initSystemTemplates] ℹ️ 开发环境保留系统字幕模板: ${name}`);
|
||||
}
|
||||
} else {
|
||||
// 用户创建的模板,不覆盖
|
||||
Log.info('initSystemTemplates.checkSubtitleTemplate', '用户自定义模板已存在,保留用户数据', { id, name });
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
// 插入新的字幕模板
|
||||
await DB.execute(
|
||||
`INSERT INTO subtitle_templates (id, name, description, config, is_system, readonly, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[id, name, description, configJson, isSystem ? 1 : 0, readonly, createdAt, now]
|
||||
);
|
||||
|
||||
Log.info('initSystemTemplates.insertSubtitleTemplate', '创建字幕模板', { id, name });
|
||||
console.log(`[initSystemTemplates] ✅ 创建字幕模板: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
Log.info('initSystemTemplates.initSubtitleTemplates', '字幕模板初始化完成', {
|
||||
count: templates.length,
|
||||
production: isProd
|
||||
});
|
||||
} catch (error) {
|
||||
Log.error('initSystemTemplates.initSubtitleTemplates', '字幕模板初始化失败', { error });
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化封面模板
|
||||
*/
|
||||
const initCoverTemplates = async (templates: CoverTemplate[]): Promise<void> => {
|
||||
try {
|
||||
const now = Date.now();
|
||||
const isProd = isProductionMode();
|
||||
|
||||
for (const template of templates) {
|
||||
const { id, name, description, is_system, config, thumbnailPath } = template;
|
||||
const configJson = JSON.stringify(config);
|
||||
|
||||
// 在生产模式下,封面模板设为只读
|
||||
const readonly = isProd ? 1 : 0;
|
||||
|
||||
// 检查模板是否已存在
|
||||
const existing = await DB.first(
|
||||
`SELECT id, is_system FROM cover_templates WHERE id = ?`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
// 开发环境下保留数据库中的系统模板修改,避免每次重启都被配置文件覆盖。
|
||||
// 生产环境仍然与配置文件保持同步,确保用户侧系统模板一致。
|
||||
if (is_system === 1 && isProd) {
|
||||
await DB.execute(
|
||||
`UPDATE cover_templates
|
||||
SET name = ?, config = ?, thumbnail_path = ?, is_system = ?, readonly = ?, updated_at = ?
|
||||
WHERE id = ?`,
|
||||
[name, configJson, thumbnailPath || null, is_system, readonly, now, id]
|
||||
);
|
||||
Log.info('initSystemTemplates.updateCoverTemplate', '同步封面模板配置', { id, name });
|
||||
console.log(`[initSystemTemplates] ✅ 封面模板已同步: ${name}`);
|
||||
} else if (is_system === 1) {
|
||||
Log.info('initSystemTemplates.keepDevCoverTemplate', '开发环境保留数据库中的系统封面模板', { id, name });
|
||||
console.log(`[initSystemTemplates] ℹ️ 开发环境保留系统封面模板: ${name}`);
|
||||
} else if (existing.is_system !== 1) {
|
||||
// 这是用户创建的模板,绝对不覆盖
|
||||
Log.info('initSystemTemplates.checkCoverTemplate', '用户自定义模板已存在,保留用户数据', { id, name });
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
// 插入新的系统模板
|
||||
await DB.execute(
|
||||
`INSERT INTO cover_templates (id, name, config, is_system, readonly, created_at, updated_at, thumbnail_path)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[id, name, configJson, is_system, readonly, now, now, thumbnailPath || null]
|
||||
);
|
||||
|
||||
Log.info('initSystemTemplates.insertCoverTemplate', '创建系统封面模板', { id, name });
|
||||
console.log(`[initSystemTemplates] ✅ 创建系统模板: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
Log.info('initSystemTemplates.initCoverTemplates', '封面模板初始化完成', {
|
||||
count: templates.length,
|
||||
production: isProd
|
||||
});
|
||||
} catch (error) {
|
||||
Log.error('initSystemTemplates.initCoverTemplates', '封面模板初始化失败', { error });
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化字幕样式
|
||||
*/
|
||||
const initSubtitleStyles = async (styles: SubtitleStyle[]): Promise<void> => {
|
||||
try {
|
||||
const now = Date.now();
|
||||
|
||||
for (const style of styles) {
|
||||
const {
|
||||
id, name, description, preview, is_system,
|
||||
fontName, fontSize, fontColor, outlineColor, outlineWidth,
|
||||
shadowOffset, shadowColor, shadowBlur,
|
||||
backgroundColor, backgroundOpacity, position, alignment
|
||||
} = style;
|
||||
|
||||
// 检查样式是否已存在
|
||||
const existing = await DB.first(
|
||||
`SELECT id, is_system FROM subtitle_styles WHERE id = ?`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
// 🔧 关键修复:不覆盖已存在的样式(无论是系统样式还是用户样式)
|
||||
// 原因:如果覆盖已存在的样式,会导致用户保存的自定义设置被抹掉
|
||||
// 只更新系统样式标记,不覆盖样式内容
|
||||
if (existing.is_system === 1) {
|
||||
// 这是一个系统样式,保持其原样(不更新内容)
|
||||
Log.info('initSystemTemplates.checkSubtitleStyle', '系统字幕样式已存在,跳过覆盖', { id, name });
|
||||
} else {
|
||||
// 这是用户创建的样式,绝对不覆盖
|
||||
Log.info('initSystemTemplates.checkSubtitleStyle', '用户自定义样式已存在,保留用户数据', { id, name });
|
||||
}
|
||||
} else {
|
||||
// 插入新的系统样式
|
||||
await DB.execute(
|
||||
`INSERT INTO subtitle_styles (
|
||||
id, name, description, preview, is_system,
|
||||
fontName, fontSize, fontColor, outlineColor, outlineWidth,
|
||||
shadowOffset, shadowColor, shadowBlur,
|
||||
backgroundColor, backgroundOpacity, position, alignment,
|
||||
createdAt, updatedAt
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
id, name, description, preview, is_system,
|
||||
fontName, fontSize, fontColor, outlineColor, outlineWidth,
|
||||
shadowOffset || 2, shadowColor || '#000000', shadowBlur || 4,
|
||||
backgroundColor, backgroundOpacity, position, alignment,
|
||||
now, now
|
||||
]
|
||||
);
|
||||
|
||||
Log.info('initSystemTemplates.insertSubtitleStyle', '创建系统字幕样式', { id, name });
|
||||
}
|
||||
}
|
||||
|
||||
Log.info('initSystemTemplates.initSubtitleStyles', '字幕样式初始化完成', {
|
||||
count: styles.length
|
||||
});
|
||||
} catch (error) {
|
||||
Log.error('initSystemTemplates.initSubtitleStyles', '字幕样式初始化失败', { error });
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化所有系统默认模板
|
||||
* 此函数应在应用启动时调用
|
||||
*/
|
||||
export const initSystemTemplates = async (): Promise<void> => {
|
||||
try {
|
||||
Log.info('initSystemTemplates.start', '开始初始化系统默认模板');
|
||||
|
||||
// 首先确保 is_system 列存在
|
||||
await ensureIsSystemColumn();
|
||||
|
||||
if (await shouldSkipDevSystemTemplateInit()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 优先读取系统模板配置(包含字幕和封面)
|
||||
const systemConfig = loadSystemTemplatesConfig();
|
||||
if (systemConfig) {
|
||||
const pendingInlineRestore = applyPendingInlineTemplateRestore(systemConfig.subtitleTemplates || []);
|
||||
|
||||
// 初始化字幕模板
|
||||
if (pendingInlineRestore.templates.length > 0) {
|
||||
await initSubtitleTemplates(pendingInlineRestore.templates, {
|
||||
forceRestoreIds: pendingInlineRestore.forceRestoreIds,
|
||||
});
|
||||
}
|
||||
|
||||
if (pendingInlineRestore.pendingFilePath) {
|
||||
fs.unlinkSync(pendingInlineRestore.pendingFilePath);
|
||||
Log.info('initSystemTemplates.applyPendingInlineTemplateRestore', '已清理待恢复 Inline 模板标记文件', {
|
||||
pendingFilePath: pendingInlineRestore.pendingFilePath,
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化封面模板(从 system-templates.json)
|
||||
if (systemConfig.coverTemplates && systemConfig.coverTemplates.length > 0) {
|
||||
await initCoverTemplates(systemConfig.coverTemplates);
|
||||
}
|
||||
|
||||
Log.info('initSystemTemplates.complete', '系统模板初始化完成', {
|
||||
version: systemConfig.version,
|
||||
subtitleCount: systemConfig.subtitleTemplates.length,
|
||||
coverCount: systemConfig.coverTemplates.length
|
||||
});
|
||||
|
||||
if (!isProductionMode()) {
|
||||
writeDevSystemTemplatesMarker('seeded-from-system-templates');
|
||||
}
|
||||
} else {
|
||||
// 如果 system-templates.json 不存在,回退到加载 default-templates.json
|
||||
console.warn('[initSystemTemplates] 无法加载 system-templates.json,尝试加载 default-templates.json');
|
||||
const config = loadDefaultTemplatesConfig();
|
||||
if (!config) {
|
||||
Log.error('initSystemTemplates.start', '无法加载任何配置文件,跳过初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化封面模板
|
||||
if (config.coverTemplates && config.coverTemplates.length > 0) {
|
||||
await initCoverTemplates(config.coverTemplates);
|
||||
}
|
||||
|
||||
// 初始化字幕样式
|
||||
if (config.subtitleStyles && config.subtitleStyles.length > 0) {
|
||||
await initSubtitleStyles(config.subtitleStyles);
|
||||
}
|
||||
|
||||
Log.info('initSystemTemplates.complete', '系统默认模板初始化完成(从 default-templates.json)', {
|
||||
version: config.version,
|
||||
coverCount: config.coverTemplates.length,
|
||||
subtitleCount: config.subtitleStyles.length
|
||||
});
|
||||
|
||||
if (!isProductionMode()) {
|
||||
writeDevSystemTemplatesMarker('seeded-from-default-templates');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
Log.error('initSystemTemplates.error', '系统模板初始化过程出错', { error });
|
||||
// 不抛出错误,避免影响应用启动
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查并修复缺失的系统模板
|
||||
* 可用于运行时检查
|
||||
*/
|
||||
export const checkAndRepairSystemTemplates = async (): Promise<{
|
||||
success: boolean;
|
||||
repairedCover: number;
|
||||
repairedSubtitle: number;
|
||||
}> => {
|
||||
try {
|
||||
const config = loadDefaultTemplatesConfig();
|
||||
if (!config) {
|
||||
return { success: false, repairedCover: 0, repairedSubtitle: 0 };
|
||||
}
|
||||
|
||||
let repairedCover = 0;
|
||||
let repairedSubtitle = 0;
|
||||
|
||||
// 检查封面模板
|
||||
for (const template of config.coverTemplates) {
|
||||
const existing = await DB.first(
|
||||
`SELECT id FROM cover_templates WHERE id = ?`,
|
||||
[template.id]
|
||||
);
|
||||
|
||||
if (!existing) {
|
||||
repairedCover++;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查字幕样式
|
||||
for (const style of config.subtitleStyles) {
|
||||
const existing = await DB.first(
|
||||
`SELECT id FROM subtitle_styles WHERE id = ?`,
|
||||
[style.id]
|
||||
);
|
||||
|
||||
if (!existing) {
|
||||
repairedSubtitle++;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有缺失,执行修复
|
||||
if (repairedCover > 0 || repairedSubtitle > 0) {
|
||||
await initSystemTemplates();
|
||||
}
|
||||
|
||||
return { success: true, repairedCover, repairedSubtitle };
|
||||
} catch (error) {
|
||||
Log.error('checkAndRepairSystemTemplates.error', '检查修复失败', { error });
|
||||
return { success: false, repairedCover: 0, repairedSubtitle: 0 };
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,289 @@
|
||||
import sqlite3, {Database} from "better-sqlite3";
|
||||
import path from "node:path";
|
||||
import migration from "./migration";
|
||||
import {AppEnv} from "../env";
|
||||
import {Log} from "../log/main";
|
||||
import {ipcMain} from "electron";
|
||||
import fs from "node:fs";
|
||||
import {Files} from "../file/main";
|
||||
|
||||
let dbPath: string | null = null;
|
||||
let dbConn: Database | null = null;
|
||||
let dbSuccess = false;
|
||||
|
||||
const db = {
|
||||
/**
|
||||
* 检查数据库连接是否已初始化
|
||||
* @throws {string} 如果数据库未初始化则抛出异常
|
||||
*/
|
||||
_check() {
|
||||
if (!dbSuccess) {
|
||||
const errorMsg = `DBNotInitialized: dbPath=${dbPath}, dbConn=${dbConn !== null}, dbSuccess=${dbSuccess}`;
|
||||
console.error("[DB:_check]", errorMsg);
|
||||
throw errorMsg;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 执行SQL语句(无返回值)
|
||||
* @param {string} sql - SQL语句
|
||||
* @param {any[]} params - 参数数组
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async execute(sql: string, params: any = []): Promise<void> {
|
||||
db._check();
|
||||
try {
|
||||
dbConn.prepare(sql).run(...params);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 插入数据并返回插入的行ID
|
||||
* @param {string} sql - SQL语句
|
||||
* @param {any[]} params - 参数数组
|
||||
* @returns {Promise<string | number>} 插入的行ID
|
||||
*/
|
||||
async insert(sql: string, params: any = []): Promise<string | number> {
|
||||
db._check();
|
||||
try {
|
||||
const result = dbConn.prepare(sql).run(...params);
|
||||
return result.lastInsertRowid;
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 查询单行数据
|
||||
* @param {string} sql - SQL语句
|
||||
* @param {any[]} params - 参数数组
|
||||
* @returns {Promise<any>} 查询结果
|
||||
*/
|
||||
async first(sql: string, params: any = []): Promise<any> {
|
||||
db._check();
|
||||
try {
|
||||
return dbConn.prepare(sql).get(...params);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 查询多行数据
|
||||
* @param {string} sql - SQL语句
|
||||
* @param {any[]} params - 参数数组
|
||||
* @returns {Promise<any[]>} 查询结果数组
|
||||
*/
|
||||
async select(sql: string, params: any = []): Promise<any[]> {
|
||||
db._check();
|
||||
try {
|
||||
return dbConn.prepare(sql).all(...params);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 更新数据并返回影响的行数
|
||||
* @param {string} sql - SQL语句
|
||||
* @param {any[]} params - 参数数组
|
||||
* @returns {Promise<number>} 影响的行数
|
||||
*/
|
||||
async update(sql: string, params: any = []): Promise<number> {
|
||||
db._check();
|
||||
try {
|
||||
const result = dbConn.prepare(sql).run(...params);
|
||||
return result.changes;
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 删除数据并返回影响的行数
|
||||
* @param {string} sql - SQL语句
|
||||
* @param {any[]} params - 参数数组
|
||||
* @returns {Promise<number>} 影响的行数
|
||||
*/
|
||||
async delete(sql: string, params: any = []): Promise<number> {
|
||||
db._check();
|
||||
try {
|
||||
const result = dbConn.prepare(sql).run(...params);
|
||||
return result.changes;
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const migrate = async () => {
|
||||
await db.execute(`CREATE TABLE IF NOT EXISTS migrate
|
||||
(
|
||||
id
|
||||
INTEGER
|
||||
PRIMARY
|
||||
KEY,
|
||||
version
|
||||
INTEGER
|
||||
)`);
|
||||
for (const version of migration.versions) {
|
||||
const result = await db.first(
|
||||
`SELECT *
|
||||
FROM migrate
|
||||
WHERE version = ?`,
|
||||
[version.version]
|
||||
);
|
||||
if (!result) {
|
||||
Log.info(`DB.Migrate`, {version: version.version});
|
||||
await version.up(db);
|
||||
await db.execute(
|
||||
`INSERT INTO migrate (version)
|
||||
VALUES (?)`,
|
||||
[version.version]
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化数据库连接
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const init = async () => {
|
||||
console.log("[DB:init] Starting database initialization");
|
||||
console.log("[DB:init] AppEnv.dataRoot =", AppEnv.dataRoot);
|
||||
console.log("[DB:init] AppEnv.userData =", AppEnv.userData);
|
||||
|
||||
dbPath = path.join(AppEnv.dataRoot, "database.db");
|
||||
const userDbPath = path.join(AppEnv.userData, "database.db");
|
||||
|
||||
console.log("[DB:init] Default dbPath =", dbPath);
|
||||
console.log("[DB:init] User dbPath =", userDbPath);
|
||||
console.log("[DB:init] User dbPath exists =", fs.existsSync(userDbPath));
|
||||
|
||||
if (fs.existsSync(userDbPath)) {
|
||||
dbPath = userDbPath;
|
||||
console.log("[DB:init] Using user dbPath instead");
|
||||
}
|
||||
|
||||
console.log("[DB:init] Final dbPath =", dbPath);
|
||||
|
||||
try {
|
||||
console.log("[DB:init] Creating SQLite3 connection...");
|
||||
dbConn = new sqlite3(dbPath);
|
||||
dbSuccess = true;
|
||||
console.log("[DB:init] SQLite3 connection created successfully, dbSuccess =", dbSuccess);
|
||||
|
||||
console.log("[DB:init] Running migrations...");
|
||||
await migrate();
|
||||
console.log("[DB:init] Migrations completed successfully");
|
||||
|
||||
Log.info("Database connected successfully");
|
||||
} catch (err) {
|
||||
console.error("[DB:init] ERROR:", err);
|
||||
Log.error("DBConnect SQLite database failed:", err.message);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
ipcMain.handle("db:execute", (event, sql: string, params: any) => {
|
||||
try {
|
||||
return db.execute(sql, params);
|
||||
} catch (error) {
|
||||
console.error("[db:execute] Error:", {
|
||||
sql,
|
||||
params,
|
||||
paramCount: Array.isArray(params) ? params.length : 0,
|
||||
placeholderCount: (sql.match(/\?/g) || []).length,
|
||||
error: error.message
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
ipcMain.handle("db:insert", (event, sql: string, params: any) => {
|
||||
try {
|
||||
return db.insert(sql, params);
|
||||
} catch (error) {
|
||||
console.error("[db:insert] Error:", {
|
||||
sql,
|
||||
params,
|
||||
paramCount: Array.isArray(params) ? params.length : 0,
|
||||
placeholderCount: (sql.match(/\?/g) || []).length,
|
||||
error: error.message
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
ipcMain.handle("db:first", (event, sql: string, params: any) => {
|
||||
try {
|
||||
return db.first(sql, params);
|
||||
} catch (error) {
|
||||
console.error("[db:first] Error:", {
|
||||
sql,
|
||||
params,
|
||||
paramCount: Array.isArray(params) ? params.length : 0,
|
||||
placeholderCount: (sql.match(/\?/g) || []).length,
|
||||
error: error.message
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
ipcMain.handle("db:select", (event, sql: string, params: any) => {
|
||||
try {
|
||||
return db.select(sql, params);
|
||||
} catch (error) {
|
||||
console.error("[db:select] Error:", {
|
||||
sql,
|
||||
params,
|
||||
paramCount: Array.isArray(params) ? params.length : 0,
|
||||
placeholderCount: (sql.match(/\?/g) || []).length,
|
||||
error: error.message
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
ipcMain.handle("db:update", (event, sql: string, params: any) => {
|
||||
try {
|
||||
return db.update(sql, params);
|
||||
} catch (error) {
|
||||
console.error("[db:update] Error:", {
|
||||
sql,
|
||||
params,
|
||||
paramCount: Array.isArray(params) ? params.length : 0,
|
||||
placeholderCount: (sql.match(/\?/g) || []).length,
|
||||
error: error.message
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
ipcMain.handle("db:delete", (event, sql: string, params: any) => {
|
||||
try {
|
||||
return db.delete(sql, params);
|
||||
} catch (error) {
|
||||
console.error("[db:delete] Error:", {
|
||||
sql,
|
||||
params,
|
||||
paramCount: Array.isArray(params) ? params.length : 0,
|
||||
placeholderCount: (sql.match(/\?/g) || []).length,
|
||||
error: error.message
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
export const DBMain = {
|
||||
init,
|
||||
execute: db.execute,
|
||||
insert: db.insert,
|
||||
first: db.first,
|
||||
select: db.select,
|
||||
update: db.update,
|
||||
delete: db.delete,
|
||||
// 诊断函数
|
||||
getStatus() {
|
||||
return {
|
||||
dbSuccess,
|
||||
dbPath,
|
||||
dbConnected: dbConn !== null,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default DBMain;
|
||||
@@ -0,0 +1,838 @@
|
||||
import StorageMain from "../storage/main";
|
||||
import Files from "../file/main";
|
||||
|
||||
const versions = [
|
||||
{
|
||||
version: 0,
|
||||
up: async (db: DB) => {
|
||||
// await db.execute(`CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)`);
|
||||
// console.log('db.insert', await db.insert(`INSERT INTO users (name, email) VALUES (?, ?)`,['Alice', 'alice@example.com']));
|
||||
// console.log('db.select', await db.select(`SELECT * FROM users`));
|
||||
// console.log('db.first', await db.first(`SELECT * FROM users`));
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 1,
|
||||
up: async (db: DB) => {
|
||||
await db.execute(`CREATE TABLE IF NOT EXISTS data_sound_tts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
serverName TEXT,
|
||||
serverTitle TEXT,
|
||||
serverVersion TEXT,
|
||||
text TEXT,
|
||||
param TEXT,
|
||||
|
||||
status TEXT,
|
||||
statusMsg TEXT,
|
||||
jobResult TEXT,
|
||||
startTime INTEGER,
|
||||
endTime INTEGER,
|
||||
resultWav TEXT
|
||||
)`);
|
||||
await db.execute(`CREATE TABLE IF NOT EXISTS data_sound_clone (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
serverName TEXT,
|
||||
serverTitle TEXT,
|
||||
serverVersion TEXT,
|
||||
promptName TEXT,
|
||||
promptWav TEXT,
|
||||
promptText TEXT,
|
||||
text TEXT,
|
||||
param TEXT,
|
||||
|
||||
status TEXT,
|
||||
statusMsg TEXT,
|
||||
jobResult TEXT,
|
||||
startTime INTEGER,
|
||||
endTime INTEGER,
|
||||
resultWav TEXT
|
||||
)`);
|
||||
await db.execute(`CREATE TABLE IF NOT EXISTS data_video_template (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
name TEXT,
|
||||
video TEXT
|
||||
)`);
|
||||
await db.execute(`CREATE TABLE IF NOT EXISTS data_video_gen (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
serverName TEXT,
|
||||
serverTitle TEXT,
|
||||
serverVersion TEXT,
|
||||
videoTemplateId INTEGER,
|
||||
videoTemplateName TEXT,
|
||||
soundType TEXT,
|
||||
soundTtsId INTEGER,
|
||||
soundTtsText TEXT,
|
||||
soundCloneId INTEGER,
|
||||
soundCloneText TEXT,
|
||||
|
||||
param TEXT,
|
||||
|
||||
status TEXT,
|
||||
statusMsg TEXT,
|
||||
jobResult TEXT,
|
||||
startTime INTEGER,
|
||||
endTime INTEGER,
|
||||
resultMp4 TEXT
|
||||
)`);
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 2,
|
||||
up: async (db: DB) => {
|
||||
await db.execute(`ALTER TABLE data_sound_tts ADD COLUMN result TEXT`);
|
||||
await db.execute(`ALTER TABLE data_sound_clone ADD COLUMN result TEXT`);
|
||||
await db.execute(`ALTER TABLE data_video_gen ADD COLUMN result TEXT`);
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 3,
|
||||
up: async (db: DB) => {
|
||||
await db.execute(`ALTER TABLE data_video_gen ADD COLUMN soundCustomFile TEXT`);
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 4,
|
||||
up: async (db: DB) => {
|
||||
await db.execute(`CREATE TABLE IF NOT EXISTS data_task (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
createdAt INTEGER DEFAULT (strftime('%s', 'now')),
|
||||
updatedAt INTEGER DEFAULT (strftime('%s', 'now')),
|
||||
|
||||
biz TEXT,
|
||||
|
||||
status TEXT,
|
||||
statusMsg TEXT,
|
||||
startTime INTEGER,
|
||||
endTime INTEGER,
|
||||
|
||||
serverName TEXT,
|
||||
serverTitle TEXT,
|
||||
serverVersion TEXT,
|
||||
|
||||
param TEXT,
|
||||
jobResult TEXT,
|
||||
modelConfig TEXT,
|
||||
result TEXT
|
||||
|
||||
)`);
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 5,
|
||||
up: async (db: DB) => {
|
||||
// await db.execute(`DELETE FROM data_task where 1=1`);
|
||||
// SoundClone
|
||||
let records = await db.select(`SELECT * FROM data_sound_clone`);
|
||||
for (const r of records) {
|
||||
const values = [
|
||||
"SoundClone",
|
||||
r.status,
|
||||
r.statusMsg,
|
||||
r.startTime,
|
||||
r.endTime,
|
||||
r.serverName,
|
||||
r.serverTitle,
|
||||
r.serverVersion,
|
||||
r.param,
|
||||
r.jobResult,
|
||||
JSON.stringify({
|
||||
promptName: r.promptName,
|
||||
promptWav: r.promptWav,
|
||||
promptText: r.promptText,
|
||||
text: r.text,
|
||||
}),
|
||||
JSON.stringify({
|
||||
url: r.resultWav,
|
||||
}),
|
||||
];
|
||||
await db.insert(
|
||||
`INSERT INTO data_task
|
||||
(biz, status, statusMsg, startTime, endTime, serverName, serverTitle, serverVersion, param, jobResult, modelConfig, result)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
values
|
||||
);
|
||||
}
|
||||
// SoundTts
|
||||
records = await db.select(`SELECT * FROM data_sound_tts`);
|
||||
for (const r of records) {
|
||||
const values = [
|
||||
"SoundTts",
|
||||
r.status,
|
||||
r.statusMsg,
|
||||
r.startTime,
|
||||
r.endTime,
|
||||
r.serverName,
|
||||
r.serverTitle,
|
||||
r.serverVersion,
|
||||
r.param,
|
||||
r.jobResult,
|
||||
JSON.stringify({
|
||||
text: r.text,
|
||||
}),
|
||||
JSON.stringify({
|
||||
url: r.resultWav,
|
||||
}),
|
||||
];
|
||||
await db.insert(
|
||||
`INSERT INTO data_task
|
||||
(biz, status, statusMsg, startTime, endTime, serverName, serverTitle, serverVersion, param, jobResult, modelConfig, result)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
values
|
||||
);
|
||||
}
|
||||
// VideoGen
|
||||
records = await db.select(`SELECT * FROM data_video_gen`);
|
||||
for (const r of records) {
|
||||
const values = [
|
||||
"VideoGen",
|
||||
r.status,
|
||||
r.statusMsg,
|
||||
r.startTime,
|
||||
r.endTime,
|
||||
r.serverName,
|
||||
r.serverTitle,
|
||||
r.serverVersion,
|
||||
r.param,
|
||||
r.jobResult,
|
||||
JSON.stringify({
|
||||
videoTemplateId: r.videoTemplateId,
|
||||
videoTemplateName: r.videoTemplateName,
|
||||
soundType: r.soundType,
|
||||
soundTtsId: r.soundTtsId,
|
||||
soundTtsText: r.soundTtsText,
|
||||
soundCloneId: r.soundCloneId,
|
||||
soundCloneText: r.soundCloneText,
|
||||
soundCustomFile: r.soundCustomFile,
|
||||
}),
|
||||
JSON.stringify({
|
||||
url: r.resultMp4,
|
||||
}),
|
||||
];
|
||||
await db.insert(
|
||||
`INSERT INTO data_task
|
||||
(biz, status, statusMsg, startTime, endTime, serverName, serverTitle, serverVersion, param, jobResult, modelConfig, result)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
values
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 6,
|
||||
up: async (db: DB) => {
|
||||
await db.execute(`CREATE TABLE IF NOT EXISTS data_storage (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
createdAt INTEGER DEFAULT (strftime('%s', 'now')),
|
||||
updatedAt INTEGER DEFAULT (strftime('%s', 'now')),
|
||||
|
||||
biz TEXT,
|
||||
|
||||
title TEXT,
|
||||
sort INTEGER,
|
||||
content TEXT
|
||||
|
||||
)`);
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 7,
|
||||
up: async (db: DB) => {
|
||||
const records = await StorageMain.get("soundClonePrompt", "records", []);
|
||||
for (const r of records) {
|
||||
const values = [
|
||||
"SoundPrompt",
|
||||
r.name,
|
||||
JSON.stringify({
|
||||
url: r.promptWav,
|
||||
promptText: r.promptText,
|
||||
}),
|
||||
];
|
||||
await db.insert(
|
||||
`INSERT INTO data_storage (biz, title, content)
|
||||
VALUES (?, ?, ?)`,
|
||||
values
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 8,
|
||||
up: async (db: DB) => {
|
||||
await db.execute(`ALTER TABLE data_task ADD COLUMN title TEXT`);
|
||||
const records = await db.select(`SELECT * FROM data_task`);
|
||||
for (const r of records) {
|
||||
let modelConfig: any = {};
|
||||
try {
|
||||
modelConfig = JSON.parse(r.modelConfig);
|
||||
} catch (e) {
|
||||
modelConfig = {};
|
||||
}
|
||||
let title = "";
|
||||
if (r.biz === "SoundTts" || r.biz === "SoundClone") {
|
||||
title = Files.textToName(modelConfig.text);
|
||||
} else if (r.biz === "VideoGen") {
|
||||
title = Files.textToName([modelConfig.videoTemplateName, modelConfig.soundTtsText].join("_"));
|
||||
} else if (r.biz === "VideoGenFlow") {
|
||||
title = Files.textToName([modelConfig.videoTemplateName, modelConfig.text].join("_"));
|
||||
}
|
||||
await db.execute(`UPDATE data_task SET title = ? WHERE id = ?`, [title, r.id]);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 9,
|
||||
up: async (db: DB) => {
|
||||
const records = await db.select(`SELECT * FROM data_task where biz in ('SoundTts', 'SoundClone')`);
|
||||
for (const r of records) {
|
||||
const modelConfigOld = JSON.parse(r.modelConfig);
|
||||
const paramOld = JSON.parse(r.param);
|
||||
const modelConfig: any = {
|
||||
type: r.biz,
|
||||
ttsParam: r.biz === "SoundTts" ? paramOld : undefined,
|
||||
cloneParam: r.biz === "SoundClone" ? paramOld : undefined,
|
||||
...modelConfigOld,
|
||||
};
|
||||
const values = [
|
||||
"SoundGenerate",
|
||||
r.title,
|
||||
r.status,
|
||||
r.statusMsg,
|
||||
r.startTime,
|
||||
r.endTime,
|
||||
r.serverName,
|
||||
r.serverTitle,
|
||||
r.serverVersion,
|
||||
JSON.stringify({}),
|
||||
r.jobResult,
|
||||
JSON.stringify(modelConfig),
|
||||
r.result,
|
||||
];
|
||||
await db.insert(
|
||||
`INSERT INTO data_task
|
||||
(biz, title, status, statusMsg, startTime, endTime, serverName, serverTitle, serverVersion, param, jobResult, modelConfig, result)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
values
|
||||
);
|
||||
await db.execute(`DELETE FROM data_task WHERE id = ?`, [r.id]);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 10,
|
||||
up: async (db: DB) => {
|
||||
await db.execute(`ALTER TABLE data_task
|
||||
ADD COLUMN type INTEGER DEFAULT 1`);
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 11,
|
||||
up: async (db: DB) => {
|
||||
await db.execute(`ALTER TABLE data_video_template
|
||||
ADD COLUMN info TEXT`);
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 12,
|
||||
up: async (db: DB) => {
|
||||
// 平台账号表
|
||||
await db.execute(`CREATE TABLE IF NOT EXISTS platform_accounts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
platform TEXT NOT NULL,
|
||||
nickname TEXT,
|
||||
uid TEXT,
|
||||
avatar TEXT,
|
||||
cookies TEXT,
|
||||
tokens TEXT,
|
||||
loginStatus TEXT DEFAULT 'pending',
|
||||
expiresAt INTEGER,
|
||||
createdAt INTEGER,
|
||||
updatedAt INTEGER
|
||||
)`);
|
||||
|
||||
// 发布任务表
|
||||
await db.execute(`CREATE TABLE IF NOT EXISTS publish_tasks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT,
|
||||
videoPath TEXT,
|
||||
coverPath TEXT,
|
||||
platforms TEXT,
|
||||
publishConfig TEXT,
|
||||
scheduledTime INTEGER,
|
||||
status TEXT DEFAULT 'pending',
|
||||
createdAt INTEGER,
|
||||
updatedAt INTEGER
|
||||
)`);
|
||||
|
||||
// 发布历史表
|
||||
await db.execute(`CREATE TABLE IF NOT EXISTS publish_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
taskId INTEGER,
|
||||
platform TEXT,
|
||||
accountId INTEGER,
|
||||
videoUrl TEXT,
|
||||
videoId TEXT,
|
||||
status TEXT,
|
||||
errorMessage TEXT,
|
||||
publishedAt INTEGER,
|
||||
viewCount INTEGER DEFAULT 0,
|
||||
likeCount INTEGER DEFAULT 0
|
||||
)`);
|
||||
|
||||
// 创建索引
|
||||
await db.execute(`CREATE INDEX IF NOT EXISTS idx_platform_accounts_platform ON platform_accounts(platform)`);
|
||||
await db.execute(`CREATE INDEX IF NOT EXISTS idx_publish_tasks_status ON publish_tasks(status)`);
|
||||
await db.execute(`CREATE INDEX IF NOT EXISTS idx_publish_history_taskId ON publish_history(taskId)`);
|
||||
await db.execute(`CREATE INDEX IF NOT EXISTS idx_publish_history_platform ON publish_history(platform)`);
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 13,
|
||||
up: async (db: DB) => {
|
||||
// 封面模板表
|
||||
await db.execute(`CREATE TABLE IF NOT EXISTS cover_templates (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
thumbnail_path TEXT,
|
||||
config TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
)`);
|
||||
|
||||
// 创建索引以提升查询性能
|
||||
await db.execute(`CREATE INDEX IF NOT EXISTS idx_cover_templates_created_at ON cover_templates(created_at DESC)`);
|
||||
await db.execute(`CREATE INDEX IF NOT EXISTS idx_cover_templates_name ON cover_templates(name)`);
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 14,
|
||||
up: async (db: DB) => {
|
||||
// 关键词分组表 - 用于字幕特效管理
|
||||
await db.execute(`CREATE TABLE IF NOT EXISTS keyword_groups (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
keywords TEXT NOT NULL,
|
||||
color TEXT,
|
||||
effectId TEXT,
|
||||
styleOverride TEXT,
|
||||
effectConfig TEXT,
|
||||
styleConfig TEXT,
|
||||
createdAt INTEGER,
|
||||
updatedAt INTEGER
|
||||
)`);
|
||||
|
||||
// 创建索引以提升查询性能
|
||||
await db.execute(`CREATE INDEX IF NOT EXISTS idx_keyword_groups_name ON keyword_groups(name)`);
|
||||
await db.execute(`CREATE INDEX IF NOT EXISTS idx_keyword_groups_createdAt ON keyword_groups(createdAt DESC)`);
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 15,
|
||||
up: async (db: DB) => {
|
||||
// 字幕样式表 - 用于保存字幕样式配置
|
||||
await db.execute(`CREATE TABLE IF NOT EXISTS subtitle_styles (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
preview TEXT,
|
||||
fontName TEXT NOT NULL,
|
||||
fontSize INTEGER,
|
||||
fontColor TEXT,
|
||||
outlineColor TEXT,
|
||||
outlineWidth INTEGER,
|
||||
shadowOffset INTEGER,
|
||||
backgroundColor TEXT,
|
||||
backgroundOpacity REAL,
|
||||
position TEXT,
|
||||
alignment TEXT,
|
||||
createdAt INTEGER,
|
||||
updatedAt INTEGER
|
||||
)`);
|
||||
|
||||
// 创建索引以提升查询性能
|
||||
await db.execute(`CREATE INDEX IF NOT EXISTS idx_subtitle_styles_name ON subtitle_styles(name)`);
|
||||
await db.execute(`CREATE INDEX IF NOT EXISTS idx_subtitle_styles_createdAt ON subtitle_styles(createdAt DESC)`);
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 16,
|
||||
up: async (db: DB) => {
|
||||
// 为 cover_templates 表添加 is_system 字段,标识系统默认模板
|
||||
await db.execute(`
|
||||
ALTER TABLE cover_templates
|
||||
ADD COLUMN is_system INTEGER DEFAULT 0
|
||||
`);
|
||||
|
||||
// 为 subtitle_styles 表添加 is_system 字段,标识系统默认样式
|
||||
await db.execute(`
|
||||
ALTER TABLE subtitle_styles
|
||||
ADD COLUMN is_system INTEGER DEFAULT 0
|
||||
`);
|
||||
|
||||
console.log('[Migration v16] 已添加 is_system 字段到 cover_templates 和 subtitle_styles 表');
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 17,
|
||||
up: async (db: DB) => {
|
||||
// 为 subtitle_styles 表添加 shadowColor 和 shadowBlur 字段
|
||||
try {
|
||||
await db.execute(`
|
||||
ALTER TABLE subtitle_styles
|
||||
ADD COLUMN shadowColor TEXT DEFAULT '#000000'
|
||||
`);
|
||||
console.log('[Migration v17] 已添加 shadowColor 字段到 subtitle_styles 表');
|
||||
} catch (error: any) {
|
||||
if (!error.message?.includes('duplicate column')) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await db.execute(`
|
||||
ALTER TABLE subtitle_styles
|
||||
ADD COLUMN shadowBlur INTEGER DEFAULT 4
|
||||
`);
|
||||
console.log('[Migration v17] 已添加 shadowBlur 字段到 subtitle_styles 表');
|
||||
} catch (error: any) {
|
||||
if (!error.message?.includes('duplicate column')) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 18,
|
||||
up: async (db: DB) => {
|
||||
console.log('[Migration v18] 开始处理封面模板系统迁移');
|
||||
|
||||
try {
|
||||
// 第一步:删除旧的系统模板(system-cover-01/02/03/04)
|
||||
const oldSystemTemplates = [
|
||||
'system-cover-01',
|
||||
'system-cover-02',
|
||||
'system-cover-03',
|
||||
'system-cover-04'
|
||||
];
|
||||
|
||||
for (const templateId of oldSystemTemplates) {
|
||||
try {
|
||||
await db.execute(
|
||||
`DELETE FROM cover_templates WHERE id = ?`,
|
||||
[templateId]
|
||||
);
|
||||
console.log(`[Migration v18] ✅ 已删除旧系统模板: ${templateId}`);
|
||||
} catch (error) {
|
||||
// 如果模板不存在,忽略错误(新用户的情况)
|
||||
console.log(`[Migration v18] ℹ️ 旧系统模板不存在(预期行为): ${templateId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 第二步:将4个自定义模板标记为新的系统模板(仅对现有模板操作)
|
||||
const newSystemTemplates = [
|
||||
'485183be-6eed-4eae-b74a-8591f08b69fa', // 黄白实描
|
||||
'8f17c467-c69c-45b2-8011-94a6c3071d16', // 斜黄白
|
||||
'6404718a-1b1c-4148-9807-ad3c57a53e0c', // 蓝底白字
|
||||
'705fa010-5bf3-4041-b624-0fd364bea5ad' // 大四字报
|
||||
];
|
||||
|
||||
for (const templateId of newSystemTemplates) {
|
||||
try {
|
||||
const result = await db.execute(
|
||||
`UPDATE cover_templates SET is_system = 1 WHERE id = ?`,
|
||||
[templateId]
|
||||
);
|
||||
console.log(`[Migration v18] ✅ 已标记为系统模板: ${templateId}`);
|
||||
} catch (error) {
|
||||
// 如果模板不存在,忽略(新用户的情况)
|
||||
console.log(`[Migration v18] ℹ️ 模板不存在,跳过标记: ${templateId}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[Migration v18] ✅ 封面模板系统迁移完成(仅对现有模板生效)');
|
||||
console.log('[Migration v18] 💡 提示:新用户请运行 \"ipAgent:exportCoverTemplatesToConfig\" 来导出这4个模板到配置文件');
|
||||
} catch (error: any) {
|
||||
console.error('[Migration v18] ❌ 迁移失败:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 19,
|
||||
up: async (db: DB) => {
|
||||
console.log('[Migration v19] 开始音效系统初始化');
|
||||
|
||||
try {
|
||||
// 第一步:创建音效库表
|
||||
await db.execute(`CREATE TABLE IF NOT EXISTS sound_effects (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
category TEXT,
|
||||
type TEXT,
|
||||
audio_source TEXT,
|
||||
volume REAL DEFAULT 0.7,
|
||||
duration REAL,
|
||||
metadata TEXT,
|
||||
created_at INTEGER,
|
||||
updated_at INTEGER,
|
||||
is_system INTEGER DEFAULT 0
|
||||
)`);
|
||||
console.log('[Migration v19] ✅ 创建 sound_effects 表');
|
||||
|
||||
// 第二步:创建用户自定义音效表
|
||||
await db.execute(`CREATE TABLE IF NOT EXISTS user_sound_effects (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
file_path TEXT NOT NULL,
|
||||
duration REAL,
|
||||
file_size INTEGER,
|
||||
created_at INTEGER,
|
||||
updated_at INTEGER
|
||||
)`);
|
||||
console.log('[Migration v19] ✅ 创建 user_sound_effects 表');
|
||||
|
||||
// 第三步:检查并添加 keyword_groups 新字段
|
||||
try {
|
||||
await db.execute(`ALTER TABLE keyword_groups ADD COLUMN sound_effect_id TEXT`);
|
||||
console.log('[Migration v19] ✅ 为 keyword_groups 添加 sound_effect_id 列');
|
||||
} catch (error: any) {
|
||||
if (error.message?.includes('duplicate')) {
|
||||
console.log('[Migration v19] ℹ️ sound_effect_id 列已存在,跳过');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await db.execute(`ALTER TABLE keyword_groups ADD COLUMN sound_effect_config TEXT`);
|
||||
console.log('[Migration v19] ✅ 为 keyword_groups 添加 sound_effect_config 列');
|
||||
} catch (error: any) {
|
||||
if (error.message?.includes('duplicate')) {
|
||||
console.log('[Migration v19] ℹ️ sound_effect_config 列已存在,跳过');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[Migration v19] ✅ 音效系统初始化完成');
|
||||
} catch (error: any) {
|
||||
console.error('[Migration v19] ❌ 迁移失败:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 20,
|
||||
up: async (db: DB) => {
|
||||
console.log('[Migration v20] 开始贴纸系统初始化');
|
||||
|
||||
try {
|
||||
// 添加 stickerId 列
|
||||
try {
|
||||
await db.execute(`ALTER TABLE keyword_groups ADD COLUMN stickerId TEXT`);
|
||||
console.log('[Migration v20] ✅ 为 keyword_groups 添加 stickerId 列');
|
||||
} catch (error: any) {
|
||||
if (error.message?.includes('duplicate')) {
|
||||
console.log('[Migration v20] ℹ️ stickerId 列已存在,跳过');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加 stickerConfig 列
|
||||
try {
|
||||
await db.execute(`ALTER TABLE keyword_groups ADD COLUMN stickerConfig TEXT`);
|
||||
console.log('[Migration v20] ✅ 为 keyword_groups 添加 stickerConfig 列');
|
||||
} catch (error: any) {
|
||||
if (error.message?.includes('duplicate')) {
|
||||
console.log('[Migration v20] ℹ️ stickerConfig 列已存在,跳过');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加 effectDuration 列
|
||||
try {
|
||||
await db.execute(`ALTER TABLE keyword_groups ADD COLUMN effectDuration REAL`);
|
||||
console.log('[Migration v20] ✅ 为 keyword_groups 添加 effectDuration 列');
|
||||
} catch (error: any) {
|
||||
if (error.message?.includes('duplicate')) {
|
||||
console.log('[Migration v20] ℹ️ effectDuration 列已存在,跳过');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[Migration v20] ✅ 贴纸系统初始化完成');
|
||||
} catch (error: any) {
|
||||
console.error('[Migration v20] ❌ 迁移失败:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 21,
|
||||
up: async (db: DB) => {
|
||||
console.log('[Migration v21] 开始更新平台账号昵称');
|
||||
|
||||
try {
|
||||
// 更新所有平台账号的昵称为"用户"
|
||||
await db.execute(`
|
||||
UPDATE platform_accounts
|
||||
SET nickname = '用户'
|
||||
WHERE nickname IN ('抖音用户', '快手用户', '视频号用户', '小红书用户')
|
||||
`);
|
||||
|
||||
console.log('[Migration v21] ✅ 已更新所有平台账号昵称为"用户"');
|
||||
} catch (error: any) {
|
||||
console.error('[Migration v21] ❌ 迁移失败:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 22,
|
||||
up: async (db: DB) => {
|
||||
console.log('[Migration v22] 开始创建用户认证系统表');
|
||||
|
||||
try {
|
||||
// 创建 users 表 - 用户基本信息
|
||||
await db.execute(`CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
avatar TEXT,
|
||||
role TEXT DEFAULT 'user',
|
||||
status TEXT DEFAULT 'active',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login_at DATETIME
|
||||
)`);
|
||||
console.log('[Migration v22] ✅ 创建 users 表');
|
||||
|
||||
// 创建 user_subscriptions 表 - 用户订阅时长管理
|
||||
await db.execute(`CREATE TABLE IF NOT EXISTS user_subscriptions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER UNIQUE NOT NULL,
|
||||
subscription_type TEXT DEFAULT 'trial',
|
||||
expires_at DATETIME NOT NULL,
|
||||
trial_started_at DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)`);
|
||||
console.log('[Migration v22] ✅ 创建 user_subscriptions 表');
|
||||
|
||||
// 创建 user_time_logs 表 - 时长变更日志
|
||||
await db.execute(`CREATE TABLE IF NOT EXISTS user_time_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
operator_id INTEGER,
|
||||
action TEXT NOT NULL,
|
||||
days_added INTEGER,
|
||||
previous_expires_at DATETIME,
|
||||
new_expires_at DATETIME,
|
||||
reason TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (operator_id) REFERENCES users(id)
|
||||
)`);
|
||||
console.log('[Migration v22] ✅ 创建 user_time_logs 表');
|
||||
|
||||
// 创建 password_resets 表 - 密码重置记录(管理员手动重置)
|
||||
await db.execute(`CREATE TABLE IF NOT EXISTS password_resets (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
operator_id INTEGER,
|
||||
old_password_hash TEXT,
|
||||
reset_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
reason TEXT,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (operator_id) REFERENCES users(id)
|
||||
)`);
|
||||
console.log('[Migration v22] ✅ 创建 password_resets 表');
|
||||
|
||||
console.log('[Migration v22] ✅ 用户认证系统表创建完成');
|
||||
} catch (error: any) {
|
||||
console.error('[Migration v22] ❌ 迁移失败:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 23,
|
||||
up: async (db: DB) => {
|
||||
console.log('[Migration v23] 开始创建字幕模板表和添加 readonly 列');
|
||||
|
||||
try {
|
||||
// 创建 subtitle_templates 表 - 用于保存字幕模板
|
||||
await db.execute(`CREATE TABLE IF NOT EXISTS subtitle_templates (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
config TEXT,
|
||||
is_system INTEGER DEFAULT 0,
|
||||
readonly INTEGER DEFAULT 0,
|
||||
created_at INTEGER,
|
||||
updated_at INTEGER
|
||||
)`);
|
||||
console.log('[Migration v23] ✅ 创建 subtitle_templates 表');
|
||||
|
||||
// 为 cover_templates 表添加 readonly 列(如果不存在)
|
||||
try {
|
||||
await db.execute(`
|
||||
ALTER TABLE cover_templates
|
||||
ADD COLUMN readonly INTEGER DEFAULT 0
|
||||
`);
|
||||
console.log('[Migration v23] ✅ 为 cover_templates 表添加 readonly 列');
|
||||
} catch (error: any) {
|
||||
if (error.message && error.message.includes('duplicate column name')) {
|
||||
console.log('[Migration v23] ℹ️ cover_templates 表已有 readonly 列');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 为 subtitle_templates 表创建索引
|
||||
await db.execute(`CREATE INDEX IF NOT EXISTS idx_subtitle_templates_name ON subtitle_templates(name)`);
|
||||
await db.execute(`CREATE INDEX IF NOT EXISTS idx_subtitle_templates_is_system ON subtitle_templates(is_system)`);
|
||||
console.log('[Migration v23] ✅ 创建 subtitle_templates 表的索引');
|
||||
|
||||
console.log('[Migration v23] ✅ 字幕模板表创建完成');
|
||||
} catch (error: any) {
|
||||
console.error('[Migration v23] ❌ 迁移失败:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 24,
|
||||
up: async (db: DB) => {
|
||||
console.log('[Migration v24] 开始创建素材库表');
|
||||
await db.execute(`CREATE TABLE IF NOT EXISTS data_video_material (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
info TEXT,
|
||||
createdAt INTEGER,
|
||||
updatedAt INTEGER
|
||||
)`);
|
||||
await db.execute(`CREATE INDEX IF NOT EXISTS idx_video_material_updatedAt ON data_video_material(updatedAt DESC)`);
|
||||
console.log('[Migration v24] 素材库表创建完成');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
versions,
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ipcRenderer } from "electron";
|
||||
|
||||
const init = () => { };
|
||||
|
||||
const execute = async (sql: string, params: any = []) => {
|
||||
return ipcRenderer.invoke("db:execute", sql, params);
|
||||
};
|
||||
|
||||
const insert = async (sql: string, params: any = []) => {
|
||||
return ipcRenderer.invoke("db:insert", sql, params);
|
||||
};
|
||||
|
||||
const first = async (sql: string, params: any = []) => {
|
||||
return ipcRenderer.invoke("db:first", sql, params);
|
||||
};
|
||||
|
||||
const select = async (sql: string, params: any = []) => {
|
||||
return ipcRenderer.invoke("db:select", sql, params);
|
||||
};
|
||||
|
||||
const update = async (sql: string, params: any = []) => {
|
||||
return ipcRenderer.invoke("db:update", sql, params);
|
||||
};
|
||||
|
||||
const deletes = async (sql: string, params: any = []) => {
|
||||
return ipcRenderer.invoke("db:delete", sql, params);
|
||||
};
|
||||
|
||||
export default {
|
||||
init,
|
||||
execute,
|
||||
insert,
|
||||
first,
|
||||
select,
|
||||
update,
|
||||
delete: deletes,
|
||||
};
|
||||
@@ -0,0 +1,234 @@
|
||||
/**
|
||||
* 音效数据库操作模块
|
||||
*/
|
||||
|
||||
import type { DB } from './main';
|
||||
|
||||
/**
|
||||
* 音效记录(数据库)
|
||||
*/
|
||||
export interface SoundEffectRecord {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
category: string;
|
||||
type: string;
|
||||
audio_source?: string;
|
||||
volume: number;
|
||||
duration?: number;
|
||||
metadata?: string;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
is_system: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户自定义音效记录(数据库)
|
||||
*/
|
||||
export interface UserSoundEffectRecord {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
file_path: string;
|
||||
duration?: number;
|
||||
file_size?: number;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 音效数据库操作类
|
||||
*/
|
||||
export class SoundEffectDb {
|
||||
private db: DB;
|
||||
|
||||
constructor(db: DB) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
// ==================== 内置音效操作 ====================
|
||||
|
||||
/**
|
||||
* 获取所有系统内置音效
|
||||
*/
|
||||
async getBuiltinEffects(): Promise<SoundEffectRecord[]> {
|
||||
const results = await this.db.select(
|
||||
'SELECT * FROM sound_effects WHERE is_system = 1 ORDER BY created_at ASC'
|
||||
);
|
||||
return results || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有系统内置音效(用于重新初始化)
|
||||
*/
|
||||
async clearBuiltinEffects(): Promise<void> {
|
||||
await this.db.execute('DELETE FROM sound_effects WHERE is_system = 1');
|
||||
console.log('[SoundEffectDb] 已清除所有系统内置音效');
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取音效
|
||||
*/
|
||||
async getSoundEffectById(id: string): Promise<SoundEffectRecord | null> {
|
||||
const result = await this.db.first(
|
||||
'SELECT * FROM sound_effects WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
return result || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存系统音效
|
||||
*/
|
||||
async saveBuiltinEffect(effect: SoundEffectRecord): Promise<void> {
|
||||
const existing = await this.getSoundEffectById(effect.id);
|
||||
|
||||
if (existing) {
|
||||
// 更新
|
||||
await this.db.execute(
|
||||
`UPDATE sound_effects SET
|
||||
name = ?,
|
||||
description = ?,
|
||||
category = ?,
|
||||
type = ?,
|
||||
audio_source = ?,
|
||||
volume = ?,
|
||||
duration = ?,
|
||||
metadata = ?,
|
||||
updated_at = ?
|
||||
WHERE id = ?`,
|
||||
[
|
||||
effect.name,
|
||||
effect.description || null,
|
||||
effect.category,
|
||||
effect.type,
|
||||
effect.audio_source || null,
|
||||
effect.volume,
|
||||
effect.duration || null,
|
||||
effect.metadata || null,
|
||||
Date.now(),
|
||||
effect.id
|
||||
]
|
||||
);
|
||||
} else {
|
||||
// 插入
|
||||
await this.db.execute(
|
||||
`INSERT INTO sound_effects (
|
||||
id, name, description, category, type, audio_source,
|
||||
volume, duration, metadata, created_at, updated_at, is_system
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
effect.id,
|
||||
effect.name,
|
||||
effect.description || null,
|
||||
effect.category,
|
||||
effect.type,
|
||||
effect.audio_source || null,
|
||||
effect.volume,
|
||||
effect.duration || null,
|
||||
effect.metadata || null,
|
||||
Date.now(),
|
||||
Date.now(),
|
||||
1
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 用户自定义音效操作 ====================
|
||||
|
||||
/**
|
||||
* 获取所有用户自定义音效
|
||||
*/
|
||||
async getUserEffects(): Promise<UserSoundEffectRecord[]> {
|
||||
const results = await this.db.select(
|
||||
'SELECT * FROM user_sound_effects ORDER BY created_at DESC'
|
||||
);
|
||||
return results || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取用户音效
|
||||
*/
|
||||
async getUserEffectById(id: string): Promise<UserSoundEffectRecord | null> {
|
||||
const result = await this.db.first(
|
||||
'SELECT * FROM user_sound_effects WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
return result || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存用户自定义音效
|
||||
*/
|
||||
async saveUserEffect(effect: UserSoundEffectRecord): Promise<void> {
|
||||
const existing = await this.getUserEffectById(effect.id);
|
||||
|
||||
if (existing) {
|
||||
// 更新
|
||||
await this.db.execute(
|
||||
`UPDATE user_sound_effects SET
|
||||
name = ?,
|
||||
description = ?,
|
||||
file_path = ?,
|
||||
duration = ?,
|
||||
file_size = ?,
|
||||
updated_at = ?
|
||||
WHERE id = ?`,
|
||||
[
|
||||
effect.name,
|
||||
effect.description || null,
|
||||
effect.file_path,
|
||||
effect.duration || null,
|
||||
effect.file_size || null,
|
||||
Date.now(),
|
||||
effect.id
|
||||
]
|
||||
);
|
||||
} else {
|
||||
// 插入
|
||||
await this.db.execute(
|
||||
`INSERT INTO user_sound_effects (
|
||||
id, name, description, file_path, duration, file_size,
|
||||
created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
effect.id,
|
||||
effect.name,
|
||||
effect.description || null,
|
||||
effect.file_path,
|
||||
effect.duration || null,
|
||||
effect.file_size || null,
|
||||
Date.now(),
|
||||
Date.now()
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户自定义音效
|
||||
*/
|
||||
async deleteUserEffect(id: string): Promise<void> {
|
||||
await this.db.execute(
|
||||
'DELETE FROM user_sound_effects WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查音效名称是否已存在
|
||||
*/
|
||||
async isNameExists(name: string, excludeId?: string): Promise<boolean> {
|
||||
let sql = 'SELECT COUNT(*) as count FROM user_sound_effects WHERE name = ?';
|
||||
const params: any[] = [name];
|
||||
|
||||
if (excludeId) {
|
||||
sql += ' AND id != ?';
|
||||
params.push(excludeId);
|
||||
}
|
||||
|
||||
const result = await this.db.first(sql, params);
|
||||
return result && result.count > 0;
|
||||
}
|
||||
}
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
type DB = {
|
||||
execute(sql: string, params?: any): Promise<any>;
|
||||
insert(sql: string, params?: any): Promise<any>;
|
||||
first(sql: string, params?: any): Promise<any>;
|
||||
select(sql: string, params?: any): Promise<any>;
|
||||
update(sql: string, params?: any): Promise<any>;
|
||||
delete(sql: string, params?: any): Promise<any>;
|
||||
};
|
||||
Reference in New Issue
Block a user