Files
2026-06-19 18:45:55 +08:00

1302 lines
54 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 封面模板管理 - IPC Handlers
* 提供封面模板的CRUD操作
*/
import { ipcMain } from "electron";
import { Log } from "../log/main";
import * as path from "path";
import * as fs from "fs";
import { AppEnv } from "../env";
import DB from "../db/main";
import { spawn } from "child_process";
import { getPythonPath } from "../../lib/python-util";
import { getPythonScriptPath, getCommonResourcePath } from "../../lib/resource-path";
import { getFFmpegExecutablePath, resourceExists } from "../../lib/resource-path";
import { spawnPython } from "../../lib/python-spawn-util";
import { normalizePath, normalizeOutputPath } from "../../lib/path-util";
// UUID生成函数
const generateUUID = (): string => {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
};
// 获取输出目录
const getOutputDir = () => {
const outputDir = path.join(AppEnv.dataRoot, 'output');
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
return outputDir;
};
// 转换 thumbnailPath 为可访问的路径
const convertThumbnailPath = (thumbnailPath: string | null): string | null => {
if (!thumbnailPath) {
return null;
}
// 如果已经是 data: URL,直接返回
if (thumbnailPath.startsWith('data:')) {
return thumbnailPath;
}
// 如果是绝对路径,读取并转换为 data: URL
if (path.isAbsolute(thumbnailPath)) {
try {
if (fs.existsSync(thumbnailPath)) {
const fileBuffer = fs.readFileSync(thumbnailPath);
const base64 = fileBuffer.toString('base64');
const ext = path.extname(thumbnailPath).toLowerCase();
let mimeType = 'image/png';
if (ext === '.jpg' || ext === '.jpeg') {
mimeType = 'image/jpeg';
} else if (ext === '.gif') {
mimeType = 'image/gif';
} else if (ext === '.webp') {
mimeType = 'image/webp';
}
const dataUrl = `data:${mimeType};base64,${base64}`;
return dataUrl;
}
} catch (error) {
Log.warn("convertThumbnailPath.absolutePath.error", { thumbnailPath, error: error?.message });
}
return thumbnailPath;
}
// 处理相对路径
try {
// 🔧 修复:移除可能存在的 extra/common/ 前缀
let relativePath = thumbnailPath;
if (relativePath.startsWith('extra/common/')) {
relativePath = relativePath.substring('extra/common/'.length);
}
const fullPath = getCommonResourcePath(relativePath);
console.log(`[convertThumbnailPath] 相对路径: ${thumbnailPath} -> 完整路径: ${fullPath}`);
if (fs.existsSync(fullPath)) {
// 读取文件并转换为 base64 data: URL
const fileBuffer = fs.readFileSync(fullPath);
const base64 = fileBuffer.toString('base64');
// 根据文件扩展名判断 MIME 类型
const ext = path.extname(fullPath).toLowerCase();
let mimeType = 'image/png';
if (ext === '.jpg' || ext === '.jpeg') {
mimeType = 'image/jpeg';
} else if (ext === '.gif') {
mimeType = 'image/gif';
} else if (ext === '.webp') {
mimeType = 'image/webp';
}
const dataUrl = `data:${mimeType};base64,${base64}`;
console.log(`[convertThumbnailPath] ✅ 成功转换为 base64, 大小: ${Math.round(base64.length / 1024)}KB`);
return dataUrl;
} else {
console.warn(`[convertThumbnailPath] ❌ 文件不存在: ${fullPath}`);
Log.warn("convertThumbnailPath.fileNotFound", { thumbnailPath, fullPath });
}
} catch (error) {
Log.warn("convertThumbnailPath.error", { thumbnailPath, error: error?.message });
console.error(`[convertThumbnailPath] 转换失败:`, error);
}
return thumbnailPath;
};
// 注册封面模板相关的IPC handlers
export const registerCoverTemplateHandlers = () => {
// 保存封面模板
ipcMain.handle("ipAgent:saveCoverTemplate", async (event, params: {
name: string;
config: any;
thumbnailPath?: string;
id?: string;
isDev?: boolean; // ✅ 添加开发模式标志
}) => {
try {
const { name, config, thumbnailPath, id, isDev } = params; // ✅ 解构 isDev
Log.info("ipAgent.saveCoverTemplate - START", {
name,
id,
idType: typeof id,
hasThumbnail: !!thumbnailPath,
hasConfig: !!config,
configKeys: config ? Object.keys(config).length : 0
});
console.log('[CoverTemplate saveCoverTemplate] 接收到参数:', {
name,
id,
hasThumbnail: !!thumbnailPath,
configKeys: config ? Object.keys(config).length : 0,
configKeysArray: config ? Object.keys(config).slice(0, 5) : []
});
// 验证参数
if (!name || !config) {
console.error('[CoverTemplate saveCoverTemplate] ❌ 参数验证失败:', {
nameEmpty: !name,
configEmpty: !config,
configValue: config
});
return {
success: false,
message: "模板名称和配置不能为空"
};
}
const now = Date.now();
const configJson = JSON.stringify(config);
// 详细日志:记录配置内容的关键字段
Log.info("ipAgent.saveCoverTemplate.config", {
hasId: !!id,
configKeys: Object.keys(config),
hasTitleFontFamily: !!config.titleFontFamily,
hasTitleBackgroundEnabled: !!config.titleBackgroundEnabled,
configLength: configJson.length
});
// 如果提供了id,先检查该id是否存在
if (id) {
const existing = await DB.first(
`SELECT id, name, COALESCE(is_system, 0) as is_system FROM cover_templates WHERE id = ?`,
[id]
);
if (existing) {
// ✅ 修复:仅在非开发模式下保护系统模板,开发模式下允许修改
if (existing.is_system === 1 && !isDev) {
Log.warn("ipAgent.saveCoverTemplate.systemTemplate", { id, name, isDev });
return {
success: false,
message: "系统默认模板不能修改"
};
}
// 如果修改了名称,检查新名称是否与其他模板冲突
if (existing.name !== name) {
const nameConflict = await DB.first(
`SELECT id FROM cover_templates WHERE name = ? AND id != ?`,
[name, id]
);
if (nameConflict) {
return {
success: false,
message: "模板名称已存在,请使用其他名称"
};
}
}
// 更新现有模板
await DB.execute(
`UPDATE cover_templates
SET name = ?, config = ?, thumbnail_path = ?, updated_at = ?
WHERE id = ?`,
[name, configJson, thumbnailPath || null, now, id]
);
Log.info("ipAgent.saveCoverTemplate.updated", { name, id });
return {
success: true,
id,
message: "模板更新成功"
};
}
}
// 🔧 防止误用已存在的ID进行创建操作
// 如果创建新模板时提供了ID,需要检查这个ID是否已经存在
// ✅ 修复:检查 id 是否为非空字符串(排除空字符串 "" 或 null/undefined
if (id && typeof id === 'string' && id.trim()) {
// 能进入这个分支,说明在前面的第68行已经检查过,这个ID在数据库中不存在
// 但仍然需要确保逻辑清晰:不允许用ID创建,只允许用ID更新
// 直接返回错误,建议用户不指定ID或直接更新现有模板
Log.warn("ipAgent.saveCoverTemplate.idProvisionedForCreation", { id, name });
return {
success: false,
message: "创建新模板不应该指定ID,请不指定ID让系统自动生成,或使用更新接口修改现有模板"
};
}
// 检查是否已存在同名模板
const existingByName = await DB.first(
`SELECT id FROM cover_templates WHERE name = ?`,
[name]
);
if (existingByName) {
return {
success: false,
message: "模板名称已存在,请使用其他名称"
};
}
// 检查模板数量限制(最多24个用户模板,不包括系统模板)
// ✅ 修复:只计数用户模板(is_system=0或NULL),不包括系统模板(is_system=1
const count = await DB.first(
`SELECT COUNT(*) as count FROM cover_templates WHERE is_system = 0 OR is_system IS NULL`
);
if (count && count.count >= 24) {
return {
success: false,
message: "最多只能保存24个自定义模板,请先删除一些模板"
};
}
// 创建新模板 - 生成新UUID
const newId = generateUUID();
await DB.execute(
`INSERT INTO cover_templates (id, name, config, thumbnail_path, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)`,
[newId, name, configJson, thumbnailPath || null, now, now]
);
Log.info("ipAgent.saveCoverTemplate.created", { name, id: newId });
return {
success: true,
id: newId,
message: "模板保存成功"
};
} catch (error) {
Log.error("ipAgent.saveCoverTemplate.error", error);
return {
success: false,
message: "保存模板失败: " + (error?.message || error)
};
}
});
// 获取所有封面模板
ipcMain.handle("ipAgent:getCoverTemplates", async (event, params?: { metadataOnly?: boolean }) => {
try {
const metadataOnly = params?.metadataOnly === true;
Log.info("ipAgent.getCoverTemplates - START", { metadataOnly });
console.log(`[CoverTemplate] 开始获取封面模板列表 (metadataOnly=${metadataOnly})`);
// 先尝试查询,如果 is_system 字段不存在则使用备用查询
let templates;
try {
// 🔧 性能优化:如果只需要元数据,不查询config字段
const selectFields = metadataOnly
? `id, name, thumbnail_path, created_at, updated_at, COALESCE(is_system, 0) as is_system`
: `id, name, thumbnail_path, config, created_at, updated_at, COALESCE(is_system, 0) as is_system`;
templates = await DB.select(
`SELECT ${selectFields}
FROM cover_templates
ORDER BY created_at DESC
LIMIT 24`
);
} catch (columnError) {
// 如果 is_system 列不存在,使用不包含该列的查询
console.warn("[CoverTemplate] is_system 列不存在,使用备用查询");
const selectFields = metadataOnly
? `id, name, thumbnail_path, created_at, updated_at`
: `id, name, thumbnail_path, config, created_at, updated_at`;
templates = await DB.select(
`SELECT ${selectFields}
FROM cover_templates
ORDER BY created_at DESC
LIMIT 24`
);
// 为所有模板添加默认的 is_system = 0
templates = templates.map(t => ({ ...t, is_system: 0 }));
}
console.log(`[CoverTemplate] 查询到 ${templates.length} 个模板`);
Log.info("ipAgent.getCoverTemplates.queryResult", { count: templates.length, metadataOnly });
// 🔧 性能优化:如果只需要元数据,直接返回,不解析config
if (metadataOnly) {
const metadataTemplates = templates.map(t => ({
id: t.id,
name: t.name,
thumbnailPath: convertThumbnailPath(t.thumbnail_path),
createdAt: t.created_at,
updatedAt: t.updated_at,
is_system: t.is_system || 0
}));
console.log(`[CoverTemplate] 返回 ${metadataTemplates.length} 个模板(仅元数据)`);
Log.info("ipAgent.getCoverTemplates.metadataOnly", { count: metadataTemplates.length });
return {
success: true,
templates: metadataTemplates
};
}
// 解析config JSON并合并所有字段(完整模式)
const parsedTemplates = templates.map(t => {
try {
const config = JSON.parse(t.config);
// 将config中的所有段展开,并确保id、name等字段正确
let thumbnailPath = convertThumbnailPath(t.thumbnail_path);
// 如果数据库中没有缩略图路径,使用映射表中的默认图片
const imageMap: { [key: string]: string } = {
'485183be-6eed-4eae-b74a-8591f08b69fa': 'huangbai-shishi.jpg',
'6404718a-1b1c-4148-9807-ad3c57a53e0c': 'landi-baizi.jpg',
'705fa010-5bf3-4041-b624-0fd364bea5ad': 'dasi-baobao.jpg',
'8f17c467-c69c-45b2-8011-94a6c3071d16': 'xiehanghuangbai.jpg',
'43t1uykj8zg-mkdite6i': 'fenghong-xubai.jpg',
'v08ezr6v3x-mkdite6i': 'renyixujzhong.jpg',
'1xwrrcvf7a5-mkdite6i': 'qinglan-xushi.png',
'sb8mm7l4bjf-mkdite6i': 'honghang-xushi.png'
};
if (!thumbnailPath && imageMap[t.id]) {
const imageName = imageMap[t.id];
const fullPath = getCommonResourcePath(`cover-templates/${imageName}`);
console.log(`[CoverTemplate] 检查图片: ${t.name}, id=${t.id}, 文件=${imageName}, 路径=${fullPath}, 存在=${fs.existsSync(fullPath)}`);
if (fs.existsSync(fullPath)) {
const fileBuffer = fs.readFileSync(fullPath);
const base64 = fileBuffer.toString('base64');
const ext = path.extname(fullPath).toLowerCase();
let mimeType = 'image/png';
if (ext === '.jpg' || ext === '.jpeg') {
mimeType = 'image/jpeg';
}
thumbnailPath = `data:${mimeType};base64,${base64}`;
console.log(`[CoverTemplate] ✅ 使用默认图片: ${t.id} -> ${imageName}`);
} else {
console.log(`[CoverTemplate] ❌ 文件不存在: ${fullPath}`);
}
}
const template = {
...config,
id: t.id,
name: t.name,
thumbnailPath: thumbnailPath,
createdAt: t.created_at,
updatedAt: t.updated_at,
is_system: t.is_system || 0
};
console.log(`[CoverTemplate] 解析模板: ${t.name}, is_system: ${t.is_system}`);
return template;
} catch (error) {
Log.error("ipAgent.getCoverTemplates.parseError", {
templateId: t.id,
error: error
});
console.error(`[CoverTemplate] 解析模板失败: ${t.id}`, error);
// 返回基本信息,避免整个列表加载失败
return {
id: t.id,
name: t.name,
thumbnailPath: convertThumbnailPath(t.thumbnail_path),
createdAt: t.created_at,
updatedAt: t.updated_at,
is_system: t.is_system || 0
};
}
});
// 详细日志:记录每个模板的关键信息
Log.info("ipAgent.getCoverTemplates.success", {
count: parsedTemplates.length,
templates: parsedTemplates.map(t => ({
id: t.id,
name: t.name,
is_system: t.is_system,
hasTitleFontFamily: !!t.titleFontFamily,
hasTitleBackgroundEnabled: !!t.titleBackgroundEnabled,
hasPersonSize: !!t.personSize,
configKeys: Object.keys(t).length
}))
});
console.log(`[CoverTemplate] 成功返回 ${parsedTemplates.length} 个模板`);
return {
success: true,
templates: parsedTemplates
};
} catch (error) {
console.error("[CoverTemplate] 获取模板列表失败:", error);
Log.error("ipAgent.getCoverTemplates.error", error);
return {
success: false,
message: "获取模板列表失败: " + (error?.message || error),
templates: []
};
}
});
// 删除封面模板
ipcMain.handle("ipAgent:deleteCoverTemplate", async (event, params: {
id: string;
isDev?: boolean; // ✅ 添加开发模式标志
}) => {
try {
const { id, isDev } = params; // ✅ 解构 isDev
Log.info("ipAgent.deleteCoverTemplate", { id });
if (!id) {
return {
success: false,
message: "模板ID不能为空"
};
}
// 检查是否为系统模板
let template;
try {
template = await DB.first(
`SELECT is_system FROM cover_templates WHERE id = ?`,
[id]
);
} catch (columnError) {
// 如果 is_system 列不存在,则不是系统模板,允许删除
console.warn("[CoverTemplate] is_system 列不存在,跳过系统模板检查");
template = null;
}
// ✅ 修复:仅在非开发模式下保护系统模板,开发模式下允许删除
if (template && template.is_system === 1 && !isDev) {
Log.warn("ipAgent.deleteCoverTemplate.systemTemplate", { id, isDev });
return {
success: false,
message: "系统默认模板不能删除"
};
}
await DB.execute(
`DELETE FROM cover_templates WHERE id = ?`,
[id]
);
Log.info("ipAgent.deleteCoverTemplate.success", { id });
return {
success: true,
message: "模板删除成功"
};
} catch (error) {
Log.error("ipAgent.deleteCoverTemplate.error", error);
return {
success: false,
message: "删除模板失败: " + (error?.message || error)
};
}
});
// 更新封面模板
ipcMain.handle("ipAgent:updateCoverTemplate", async (event, params: {
id: string;
name?: string;
config?: any;
thumbnailPath?: string;
is_system?: number; // ✅ 添加设置 is_system 的能力
isDev?: boolean; // ✅ 添加开发模式标志
}) => {
try {
const { id, name, config, thumbnailPath, is_system, isDev } = params; // ✅ 解构新参数
Log.info("ipAgent.updateCoverTemplate", { id, hasName: !!name, hasConfig: !!config });
if (!id) {
return {
success: false,
message: "模板ID不能为空"
};
}
// ✅ 修复:仅在非开发模式下保护系统模板,开发模式下允许修改
let template;
try {
template = await DB.first(
`SELECT is_system FROM cover_templates WHERE id = ?`,
[id]
);
} catch (columnError) {
console.warn("[CoverTemplate] is_system 列不存在,跳过系统模板检查");
template = null;
}
if (template && template.is_system === 1 && !isDev) {
Log.warn("ipAgent.updateCoverTemplate.systemTemplate", { id, isDev });
return {
success: false,
message: "系统默认模板不能修改"
};
}
const updates: string[] = [];
const values: any[] = [];
if (name !== undefined) {
updates.push('name = ?');
values.push(name);
}
if (config !== undefined) {
updates.push('config = ?');
values.push(JSON.stringify(config));
}
if (thumbnailPath !== undefined) {
updates.push('thumbnail_path = ?');
values.push(thumbnailPath);
}
// ✅ 添加设置 is_system 的能力
if (is_system !== undefined) {
updates.push('is_system = ?');
values.push(is_system);
}
if (updates.length === 0) {
return {
success: false,
message: "没有要更新的内容"
};
}
updates.push('updated_at = ?');
values.push(Date.now());
values.push(id);
await DB.execute(
`UPDATE cover_templates
SET ${updates.join(', ')}
WHERE id = ?`,
values
);
Log.info("ipAgent.updateCoverTemplate.success", { id });
return {
success: true,
message: "模板更新成功"
};
} catch (error) {
Log.error("ipAgent.updateCoverTemplate.error", error);
return {
success: false,
message: "更新模板失败: " + (error?.message || error)
};
}
});
// 获取单个封面模板
ipcMain.handle("ipAgent:getCoverTemplate", async (event, params: { id: string }) => {
try {
const { id } = params;
Log.info("ipAgent.getCoverTemplate", { id });
if (!id) {
return {
success: false,
message: "模板ID不能为空"
};
}
const template = await DB.first(
`SELECT id, name, thumbnail_path, config, created_at, updated_at
FROM cover_templates
WHERE id = ?`,
[id]
);
if (!template) {
return {
success: false,
message: "模板不存在"
};
}
try {
const config = JSON.parse(template.config);
// 返回完整的模板对象,包含元数据和配置
// 前端会负责分离元数据和配置
const parsedTemplate = {
...config, // 展开所有配置字段
id: template.id,
name: template.name,
thumbnailPath: convertThumbnailPath(template.thumbnail_path),
createdAt: template.created_at,
updatedAt: template.updated_at
};
Log.info("ipAgent.getCoverTemplate.success", {
id,
hasTitleFontFamily: !!config.titleFontFamily,
hasTitleBackgroundEnabled: !!config.titleBackgroundEnabled,
configKeys: Object.keys(config).length
});
return {
success: true,
template: parsedTemplate
};
} catch (error) {
Log.error("ipAgent.getCoverTemplate.parseError", {
id,
error: error
});
return {
success: false,
message: "模板数据解析失败"
};
}
} catch (error) {
Log.error("ipAgent.getCoverTemplate.error", error);
return {
success: false,
message: "获取模板失败: " + (error?.message || error)
};
}
});
// 预览封面效果(不保存,仅生成临时预览图)
ipcMain.handle("ipAgent:previewCoverTemplate", async (event, params: {
videoPath: string;
titleText: string;
config: any;
}) => {
try {
const { videoPath, titleText, config } = params;
Log.info("ipAgent.previewCoverTemplate", { videoPath, titleText });
// 验证参数
if (!videoPath || !fs.existsSync(videoPath)) {
return {
success: false,
message: "视频文件不存在"
};
}
if (!titleText || !titleText.trim()) {
return {
success: false,
message: "标题文字不能为空"
};
}
const pythonScript = getPythonScriptPath('advanced_cover_generator.py');
const outputDir = getOutputDir();
const timestamp = Date.now();
const previewPath = path.join(outputDir, `cover_preview_${timestamp}.jpg`);
const safeVideoPath = normalizePath(videoPath);
const safePreviewPath = normalizeOutputPath(previewPath);
const configJson = JSON.stringify(config);
return new Promise(async (resolve) => {
try {
const result = await spawnPython([
pythonScript,
'--video', safeVideoPath,
'--title', titleText,
'--output', safePreviewPath,
'--config', configJson,
'--preview'
], {
timeout: 120000, // 2分钟超时
addFFmpegToPath: true
});
if (fs.existsSync(previewPath)) {
Log.info("ipAgent.previewCoverTemplate.success", { previewPath });
resolve({
success: true,
previewPath,
message: "预览生成成功"
});
} else {
Log.error("ipAgent.previewCoverTemplate.noOutput", { result });
resolve({
success: false,
message: `预览生成失败: 未生成输出文件`
});
}
} catch (error: any) {
Log.error("ipAgent.previewCoverTemplate.error", error);
resolve({
success: false,
message: `预览生成失败: ${error.message || '未知错误'}`
});
}
});
} catch (error) {
Log.error("ipAgent.previewCoverTemplate.error", error);
return {
success: false,
message: "预览生成失败: " + (error?.message || error)
};
}
});
// 清理损坏的模板数据(修复嵌套循环引用问题)
ipcMain.handle("ipAgent:cleanupCoverTemplates", async (event) => {
try {
Log.info("ipAgent.cleanupCoverTemplates.start");
const templates = await DB.select(
`SELECT id, name, config FROM cover_templates`
);
let cleanedCount = 0;
let errorCount = 0;
for (const template of templates) {
try {
const config = JSON.parse(template.config);
// 检查是否存在嵌套的 config 字段
if (config.config) {
Log.info("ipAgent.cleanupCoverTemplates.foundNested", {
id: template.id,
name: template.name
});
// 递归提取最内层的配置(不包含元数据字段)
let cleanConfig = config;
while (cleanConfig.config) {
cleanConfig = cleanConfig.config;
}
// 移除元数据字段
const { id, name, thumbnailPath, createdAt, updatedAt, ...pureConfig } = cleanConfig;
// 更新数据库
await DB.execute(
`UPDATE cover_templates SET config = ? WHERE id = ?`,
[JSON.stringify(pureConfig), template.id]
);
cleanedCount++;
Log.info("ipAgent.cleanupCoverTemplates.cleaned", {
id: template.id,
name: template.name
});
}
} catch (error) {
errorCount++;
Log.error("ipAgent.cleanupCoverTemplates.templateError", {
id: template.id,
error: error
});
}
}
Log.info("ipAgent.cleanupCoverTemplates.complete", {
total: templates.length,
cleaned: cleanedCount,
errors: errorCount
});
return {
success: true,
total: templates.length,
cleaned: cleanedCount,
errors: errorCount,
message: `清理完成:共 ${templates.length} 个模板,修复 ${cleanedCount} 个,失败 ${errorCount}`
};
} catch (error) {
Log.error("ipAgent.cleanupCoverTemplates.error", error);
return {
success: false,
message: "清理失败: " + (error?.message || error)
};
}
});
// 批量标记所有现有模板为系统模板
ipcMain.handle("ipAgent:markAllTemplatesAsSystem", async (event) => {
try {
Log.info("ipAgent.markAllTemplatesAsSystem - START");
console.log("[CoverTemplate] 批量标记所有模板为系统模板");
await DB.execute(
`UPDATE cover_templates SET is_system = 1 WHERE is_system IS NULL OR is_system = 0`
);
const templates = await DB.select(
`SELECT id, name, is_system FROM cover_templates`
);
console.log(`[CoverTemplate] 已标记 ${templates.length} 个模板为系统模板`);
console.table(templates);
return {
success: true,
count: templates.length,
message: `已将 ${templates.length} 个模板标记为系统模板`
};
} catch (error) {
Log.error("ipAgent.markAllTemplatesAsSystem.error", error);
console.error("[CoverTemplate] 批量标记失败:", error);
return {
success: false,
message: "操作失败: " + (error?.message || error)
};
}
});
// 导出系统模板到配置文件(供新用户自动导入)
ipcMain.handle("ipAgent:exportCoverTemplatesToConfig", async (event) => {
try {
Log.info("ipAgent.exportCoverTemplatesToConfig - START");
console.log("[CoverTemplate] 开始导出所有系统模板到配置文件");
// 🔒 正确的8个系统模板ID(白名单)- 与图片映射中的ID必须一致
const validSystemTemplateIds = [
'485183be-6eed-4eae-b74a-8591f08b69fa', // 黄白实描
'6404718a-1b1c-4148-9807-ad3c57a53e0c', // 蓝底白字
'705fa010-5bf3-4041-b624-0fd364bea5ad', // 大四字报
'8f17c467-c69c-45b2-8011-94a6c3071d16', // 斜黄白
'43t1uykj8zg-mkdite6i', // 粉红虚白
'v08ezr6v3x-mkdite6i', // 人像居中
'1xwrrcvf7a5-mkdite6i', // 青蓝虚描
'sb8mm7l4bjf-mkdite6i' // 红黄虚描
];
// 查询数据库中所有 is_system=1 的模板
const allSystemTemplates = await DB.select(
`SELECT id, name, config FROM cover_templates WHERE is_system = 1 ORDER BY created_at ASC`
);
// ✅ 只保留白名单中的模板(过滤掉错误的系统模板标记)
const templates = allSystemTemplates.filter((t: any) => validSystemTemplateIds.includes(t.id));
// ⚠️ 警告:如果数据库中有多余的 is_system=1 标记
if (allSystemTemplates.length > templates.length) {
const invalidCount = allSystemTemplates.length - templates.length;
console.warn(`[CoverTemplate] ⚠️ 数据库中有 ${invalidCount} 个错误的系统模板标记被忽略`);
Log.warn("ipAgent.exportCoverTemplatesToConfig.invalidTemplates", {
totalInDb: allSystemTemplates.length,
validCount: templates.length,
invalidCount
});
}
// 获取导出的系统模板 ID 列表(用于后续过滤)
const systemTemplateIds = templates.map((t: any) => t.id);
if (templates.length === 0) {
return {
success: false,
message: "未找到要导出的系统模板"
};
}
// 使用统一的配置文件路径(用于打包和开发)
const configPath = getCommonResourcePath('config/system-templates.json');
console.log(`[CoverTemplate] 配置文件路径: ${configPath}`);
let config: any = {
version: "1.0.0",
description: "系统默认模板配置 - 应用启动时自动初始化",
subtitleTemplates: [],
coverTemplates: [],
subtitleStyles: []
};
if (fs.existsSync(configPath)) {
const content = fs.readFileSync(configPath, 'utf-8');
config = JSON.parse(content);
// ✅ 确保保留字幕模板,只更新封面模板
}
// 转换模板格式
const exportedTemplates = templates.map((template: any) => {
const tplConfig = JSON.parse(template.config);
// ✅ 确保所有必需的参数都存在,补充缺失的默认值
const completeConfig = {
// 背景相关
backgroundEnabled: tplConfig.backgroundEnabled ?? false,
backgroundImagePath: tplConfig.backgroundImagePath ?? null,
backgroundBlurEnabled: tplConfig.backgroundBlurEnabled ?? false,
backgroundBlurIntensity: tplConfig.backgroundBlurIntensity ?? 10,
backgroundSize: tplConfig.backgroundSize ?? 100,
backgroundPosition: tplConfig.backgroundPosition ?? { x: 50, y: 50 },
// 人物相关
personImagePath: tplConfig.personImagePath ?? null,
personSize: tplConfig.personSize ?? 80,
personPosition: tplConfig.personPosition ?? { x: 50, y: 50 },
personRotation: tplConfig.personRotation ?? 0,
personBorderEnabled: tplConfig.personBorderEnabled ?? false,
personBorderStyle: tplConfig.personBorderStyle ?? "solid",
personBorderColor: tplConfig.personBorderColor ?? "#FFFFFF",
personBorderWidth: tplConfig.personBorderWidth ?? 8,
personBorderDashLength: tplConfig.personBorderDashLength ?? null,
personBorderGapLength: tplConfig.personBorderGapLength ?? null,
// 文本相关
autoSplitText: tplConfig.autoSplitText ?? true,
titleMaxLength: tplConfig.titleMaxLength ?? 4,
subtitleMaxLength: tplConfig.subtitleMaxLength ?? 10,
// 主标题相关
titleText: tplConfig.titleText ?? "",
titleFontFamily: tplConfig.titleFontFamily ?? "NotoSerifCJK-VF",
titleFontSize: tplConfig.titleFontSize ?? 120,
titleFontWeight: tplConfig.titleFontWeight ?? 700,
titleColor: tplConfig.titleColor ?? "#FFFFFF",
titleStrokeColor: tplConfig.titleStrokeColor ?? "#000000",
titleStrokeWidth: tplConfig.titleStrokeWidth ?? 2,
titleShadowColor: tplConfig.titleShadowColor ?? "#000000",
titleShadowOffsetX: tplConfig.titleShadowOffsetX ?? 0,
titleShadowOffsetY: tplConfig.titleShadowOffsetY ?? 0,
titleShadowBlur: tplConfig.titleShadowBlur ?? 0,
titleShadowEnabled: tplConfig.titleShadowEnabled ?? false,
titleShadowLayers: tplConfig.titleShadowLayers ?? [],
titlePosition: tplConfig.titlePosition ?? { x: 50, y: 80 },
titleRotation: tplConfig.titleRotation ?? 0,
titleDirection: tplConfig.titleDirection ?? "horizontal",
titleCharSpacing: tplConfig.titleCharSpacing ?? 24,
titleLineSpacing: tplConfig.titleLineSpacing ?? 144,
titleMaxCharsPerLine: tplConfig.titleMaxCharsPerLine ?? 10,
titleBackgroundEnabled: tplConfig.titleBackgroundEnabled ?? false,
titleBackgroundColor: tplConfig.titleBackgroundColor ?? "#000000",
titleBackgroundOpacity: tplConfig.titleBackgroundOpacity ?? 70,
titleBackgroundShape: tplConfig.titleBackgroundShape ?? "rectangle",
titleBackgroundSize: tplConfig.titleBackgroundSize ?? { width: 30, height: 10 },
titleBackgroundPosition: tplConfig.titleBackgroundPosition ?? { x: 50, y: 30 },
titleBackgroundPoints: tplConfig.titleBackgroundPoints ?? [],
titleBackgroundRadius: tplConfig.titleBackgroundRadius ?? 10,
titleBackgroundRotation: tplConfig.titleBackgroundRotation ?? 0,
// 副标题相关
subtitleText: tplConfig.subtitleText ?? "",
subtitleFontFamily: tplConfig.subtitleFontFamily ?? "NotoSerifCJK-VF",
subtitleFontSize: tplConfig.subtitleFontSize ?? 60,
subtitleFontWeight: tplConfig.subtitleFontWeight ?? 500,
subtitleColor: tplConfig.subtitleColor ?? "#FFFFFF",
subtitleStrokeColor: tplConfig.subtitleStrokeColor ?? "#000000",
subtitleStrokeWidth: tplConfig.subtitleStrokeWidth ?? 1,
subtitleShadowColor: tplConfig.subtitleShadowColor ?? "#000000",
subtitleShadowOffsetX: tplConfig.subtitleShadowOffsetX ?? 0,
subtitleShadowOffsetY: tplConfig.subtitleShadowOffsetY ?? 0,
subtitleShadowBlur: tplConfig.subtitleShadowBlur ?? 0,
subtitleShadowEnabled: tplConfig.subtitleShadowEnabled ?? false,
subtitleShadowLayers: tplConfig.subtitleShadowLayers ?? [],
subtitlePosition: tplConfig.subtitlePosition ?? { x: 50, y: 90 },
subtitleRotation: tplConfig.subtitleRotation ?? 0,
subtitleDirection: tplConfig.subtitleDirection ?? "horizontal",
subtitleCharSpacing: tplConfig.subtitleCharSpacing ?? 12,
subtitleLineSpacing: tplConfig.subtitleLineSpacing ?? 72,
subtitleMaxCharsPerLine: tplConfig.subtitleMaxCharsPerLine ?? 15,
subtitleBackgroundEnabled: tplConfig.subtitleBackgroundEnabled ?? false,
subtitleBackgroundColor: tplConfig.subtitleBackgroundColor ?? "#000000",
subtitleBackgroundOpacity: tplConfig.subtitleBackgroundOpacity ?? 70,
subtitleBackgroundShape: tplConfig.subtitleBackgroundShape ?? "rectangle",
subtitleBackgroundSize: tplConfig.subtitleBackgroundSize ?? { width: 30, height: 10 },
subtitleBackgroundPosition: tplConfig.subtitleBackgroundPosition ?? { x: 50, y: 60 },
subtitleBackgroundPoints: tplConfig.subtitleBackgroundPoints ?? [],
subtitleBackgroundRadius: tplConfig.subtitleBackgroundRadius ?? 10,
subtitleBackgroundRotation: tplConfig.subtitleBackgroundRotation ?? 0,
// 蒙版相关
maskEnabled: tplConfig.maskEnabled ?? false,
maskImagePath: tplConfig.maskImagePath ?? null,
maskSize: tplConfig.maskSize ?? 100,
maskPosition: tplConfig.maskPosition ?? { x: 50, y: 50 },
maskColor: tplConfig.maskColor ?? null,
maskOpacity: tplConfig.maskOpacity ?? 100,
maskShape: tplConfig.maskShape ?? "rectangle"
};
return {
id: template.id,
name: template.name,
description: `系统模板 - ${template.name}`,
is_system: 1,
config: completeConfig
};
});
// ✨ 改进:只保留数据库导出的系统模板,不混入配置文件里的其他模板
// 原因:配置文件是用于新用户初始化的,只应包含系统模板,用户自定义模板应在应用中创建
config.coverTemplates = exportedTemplates;
// 写回配置文件
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
console.log(`[CoverTemplate] ✅ 已导出 ${exportedTemplates.length} 个系统模板到配置文件`);
console.log(`[CoverTemplate] 配置文件已更新: ${configPath}`);
Log.info("ipAgent.exportCoverTemplatesToConfig.success", {
count: exportedTemplates.length,
configPath: configPath,
templates: exportedTemplates.map(t => ({ id: t.id, name: t.name }))
});
return {
success: true,
count: exportedTemplates.length,
message: `已将 ${exportedTemplates.length} 个模板导出到配置文件`,
configPath: configPath,
templates: exportedTemplates.map(t => ({ id: t.id, name: t.name }))
};
} catch (error) {
Log.error("ipAgent.exportCoverTemplatesToConfig.error", error);
console.error("[CoverTemplate] 导出配置文件失败:", error);
return {
success: false,
message: "导出失败: " + (error?.message || error)
};
}
});
// 导出单个封面模板为JSON
ipcMain.handle("ipAgent:exportCoverTemplateById", async (event, params: { id: string }) => {
try {
const { id } = params;
if (!id) {
return {
success: false,
message: "模板ID不能为空"
};
}
Log.info("ipAgent.exportCoverTemplateById", { id });
console.log('[CoverTemplate] 开始导出单个模板:', id);
const template = await DB.first(
`SELECT id, name, config FROM cover_templates WHERE id = ?`,
[id]
);
if (!template) {
return {
success: false,
message: "模板不存在"
};
}
const config = typeof template.config === 'string' ? JSON.parse(template.config) : template.config;
const exportData = {
version: "1.0.0",
description: "封面模板导出配置",
timestamp: new Date().toISOString(),
coverTemplate: {
id: template.id,
name: template.name,
config: config
}
};
console.log(`[CoverTemplate] 导出单个模板成功: ${template.name}`);
Log.info("ipAgent.exportCoverTemplateById.success", { id, name: template.name });
return {
success: true,
data: exportData,
message: "导出成功"
};
} catch (error) {
console.error("[CoverTemplate] 导出单个模板失败:", error);
Log.error("ipAgent.exportCoverTemplateById.error", error);
return {
success: false,
message: "导出失败: " + (error?.message || error),
data: null
};
}
});
// 注意:这个接口已废弃,前端 render.ts 中的 exportAllCoverTemplates 已指向 exportCoverTemplatesToConfig
// 保留此接口是为了向后兼容,但实际上前端不会调用到这里
ipcMain.handle("ipAgent:exportAllCoverTemplates", async (event) => {
// 直接重定向到正确的导出接口
console.log('[CoverTemplate] ⚠️ 警告:调用了废弃的 exportAllCoverTemplates 接口,重定向到 exportCoverTemplatesToConfig');
return event.sender.invoke("ipAgent:exportCoverTemplatesToConfig");
});
// 加载封面模板图片文件
ipcMain.handle("ipAgent:getCoverTemplateImage", async (event, params: { templateId: string }) => {
try {
const { templateId } = params;
// 根据 templateId 映射到图片文件
const imageMap: { [key: string]: string } = {
'485183be-6eed-4eae-b74a-8591f08b69fa': 'huangbai-shishi.jpg', // 黄白实描
'6404718a-1b1c-4148-9807-ad3c57a53e0c': 'landi-baizi.jpg', // 蓝底白字
'705fa010-5bf3-4041-b624-0fd364bea5ad': 'dasi-baobao.jpg', // 大四字报
'8f17c467-c69c-45b2-8011-94a6c3071d16': 'xiehanghuangbai.jpg', // 斜黄白
'43t1uykj8zg-mkdite6i': 'fenghong-xubai.jpg', // 粉红虚白
'v08ezr6v3x-mkdite6i': 'renyixujzhong.jpg', // 人像居中
'1xwrrcvf7a5-mkdite6i': 'qinglan-xushi.png', // 青蓝虚描
'sb8mm7l4bjf-mkdite6i': 'honghang-xushi.png' // 红黄虚描
};
const imageName = imageMap[templateId];
if (!imageName) {
Log.warn("ipAgent.getCoverTemplateImage.noMap", { templateId });
return null;
}
const imagePath = getCommonResourcePath(`cover-templates/${imageName}`);
if (!fs.existsSync(imagePath)) {
Log.warn("ipAgent.getCoverTemplateImage.notFound", { templateId, imagePath });
return null;
}
console.log(`[CoverTemplate] 读取图片: ${imagePath}`);
// 读取文件并转换为 base64
const fileBuffer = fs.readFileSync(imagePath);
const base64 = fileBuffer.toString('base64');
// 判断 MIME 类型
const ext = path.extname(imagePath).toLowerCase();
let mimeType = 'image/png';
if (ext === '.jpg' || ext === '.jpeg') {
mimeType = 'image/jpeg';
}
const dataUrl = `data:${mimeType};base64,${base64}`;
console.log(`[CoverTemplate] 图片 base64 长度: ${dataUrl.length}`);
return dataUrl;
} catch (error) {
Log.error("ipAgent.getCoverTemplateImage.error", error);
console.error("[CoverTemplate] 加载图片失败:", error);
return null;
}
});
// 强制重新初始化系统模板(用于运行时更新)
ipcMain.handle("ipAgent:reinitSystemTemplates", async (event) => {
try {
Log.info("ipAgent.reinitSystemTemplates - START");
console.log("[CoverTemplate] 开始强制重新初始化系统模板");
// 动态导入 initSystemTemplates 函数
const { initSystemTemplates } = await import("../db/initSystemTemplates");
// 执行初始化
await initSystemTemplates();
Log.info("ipAgent.reinitSystemTemplates - SUCCESS");
console.log("[CoverTemplate] ✅ 系统模板强制重新初始化完成");
return {
success: true,
message: "系统模板已重新初始化,请刷新页面查看更新"
};
} catch (error) {
Log.error("ipAgent.reinitSystemTemplates.error", error);
console.error("[CoverTemplate] 强制重新初始化失败:", error);
return {
success: false,
message: "重新初始化失败: " + (error?.message || error)
};
}
});
// 复制封面模板
ipcMain.handle("ipAgent:duplicateCoverTemplate", async (event, params: {
id: string;
newName?: string;
}) => {
try {
const { id, newName } = params;
Log.info("ipAgent.duplicateCoverTemplate", { id, newName });
console.log('[CoverTemplate duplicateCoverTemplate] 开始复制模板:', id);
if (!id) {
return {
success: false,
message: "模板ID不能为空"
};
}
// 获取原模板
const sourceTemplate = await DB.first(
`SELECT id, name, config, thumbnail_path, is_system FROM cover_templates WHERE id = ?`,
[id]
);
if (!sourceTemplate) {
return {
success: false,
message: "源模板不存在"
};
}
// 生成新模板名称
let duplicateName = newName || `${sourceTemplate.name} - 副本`;
// 检查名称是否已存在,如果存在则添加数字后缀
let counter = 1;
let finalName = duplicateName;
while (true) {
const existing = await DB.first(
`SELECT id FROM cover_templates WHERE name = ?`,
[finalName]
);
if (!existing) break;
finalName = `${duplicateName} (${counter})`;
counter++;
}
// 创建新模板
const newId = generateUUID();
const now = Date.now();
await DB.execute(
`INSERT INTO cover_templates (id, name, config, thumbnail_path, is_system, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[newId, finalName, sourceTemplate.config, sourceTemplate.thumbnail_path, 0, now, now]
);
Log.info("ipAgent.duplicateCoverTemplate.success", {
sourceId: id,
newId,
sourceName: sourceTemplate.name,
newName: finalName
});
console.log(`[CoverTemplate] ✅ 复制成功: ${sourceTemplate.name}${finalName}`);
return {
success: true,
id: newId,
name: finalName,
message: "模板复制成功"
};
} catch (error) {
Log.error("ipAgent.duplicateCoverTemplate.error", error);
console.error("[CoverTemplate] 复制模板失败:", error);
return {
success: false,
message: "复制模板失败: " + (error?.message || error)
};
}
});
Log.info("ipAgent.coverTemplate.handlers.registered");
};
export default {
registerCoverTemplateHandlers
};