Initial clean project import

This commit is contained in:
cat-shark
2026-06-19 18:41:41 +08:00
commit a13b804c7a
1306 changed files with 220568 additions and 0 deletions
View File
+96
View File
@@ -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 });
}
};
+852
View File
@@ -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 };
}
};
+289
View File
@@ -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;
+838
View File
@@ -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,
};
+37
View File
@@ -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,
};
+234
View File
@@ -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;
}
}
+8
View File
@@ -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>;
};