Initial clean project import
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const Database = require('better-sqlite3');
|
||||
|
||||
console.log('=== 全面分析所有配置源和数据库中的模板差异 ===\n');
|
||||
|
||||
// 1. 收集所有配置文件
|
||||
const configFiles = [
|
||||
{ name: 'C盘根目录', path: 'C:\\system-templates.json' },
|
||||
{ name: '项目打包配置', path: 'electron/resources/extra/common/config/system-templates.json' },
|
||||
{ name: '项目开发配置', path: 'electron/config/system-templates.json' },
|
||||
{ name: '历史配置v2', path: 'electron/config/default-subtitle-templates-v2.json' },
|
||||
{ name: 'TypeScript源文件', path: 'src/config/systemSubtitleTemplates.ts' },
|
||||
];
|
||||
|
||||
// 2. 查找数据库
|
||||
const possibleDbPaths = [
|
||||
path.join(process.env.APPDATA || '', 'aigc-tools-panel', 'database.db'),
|
||||
path.join(process.env.APPDATA || '', 'zhenqianba', 'data.db'),
|
||||
path.join(process.env.LOCALAPPDATA || '', 'aigc-tools-panel', 'database.db'),
|
||||
path.join(process.env.LOCALAPPDATA || '', 'zhenqianba', 'data.db'),
|
||||
path.join(__dirname, '../userData/data.db'),
|
||||
path.join(__dirname, '../data.db'),
|
||||
];
|
||||
|
||||
let dbPath = null;
|
||||
for (const dbp of possibleDbPaths) {
|
||||
if (fs.existsSync(dbp)) {
|
||||
dbPath = dbp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 读取所有配置源
|
||||
const configs = {};
|
||||
|
||||
// 读取JSON配置文件
|
||||
configFiles.forEach(cf => {
|
||||
if (fs.existsSync(cf.path)) {
|
||||
try {
|
||||
if (cf.path.endsWith('.ts')) {
|
||||
// TypeScript文件需要特殊处理
|
||||
const content = fs.readFileSync(cf.path, 'utf-8');
|
||||
const match = content.match(/export const SYSTEM_TEMPLATES = (\[[\s\S]*?\]);/);
|
||||
if (match) {
|
||||
// 简单提取,实际应该用AST解析
|
||||
configs[cf.name] = { type: 'ts', path: cf.path, content };
|
||||
}
|
||||
} else {
|
||||
const data = JSON.parse(fs.readFileSync(cf.path, 'utf-8'));
|
||||
configs[cf.name] = { type: 'json', path: cf.path, data };
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`⚠️ 读取 ${cf.name} 失败: ${e.message}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`⚠️ ${cf.name} 不存在: ${cf.path}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 读取数据库
|
||||
if (dbPath) {
|
||||
try {
|
||||
const db = new Database(dbPath, { readonly: true });
|
||||
const templates = db.prepare(`
|
||||
SELECT id, name, description, config, is_system, created_at
|
||||
FROM subtitle_templates
|
||||
WHERE is_system = 1
|
||||
ORDER BY created_at ASC
|
||||
`).all();
|
||||
|
||||
configs['数据库'] = {
|
||||
type: 'db',
|
||||
path: dbPath,
|
||||
templates: templates.map(t => ({
|
||||
...t,
|
||||
config: JSON.parse(t.config)
|
||||
}))
|
||||
};
|
||||
db.close();
|
||||
} catch (e) {
|
||||
console.log(`⚠️ 读取数据库失败: ${e.message}`);
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ 未找到数据库文件');
|
||||
}
|
||||
|
||||
// 4. 分析每个配置源的模板
|
||||
console.log('\n=== 各配置源的模板标题字幕配置 ===\n');
|
||||
|
||||
const analysis = {};
|
||||
|
||||
Object.keys(configs).forEach(sourceName => {
|
||||
const config = configs[sourceName];
|
||||
const templates = [];
|
||||
|
||||
if (config.type === 'json') {
|
||||
const data = config.data;
|
||||
if (data.subtitleTemplates) {
|
||||
data.subtitleTemplates.forEach(t => {
|
||||
const title = t.config && t.config.titleSubtitleConfig;
|
||||
templates.push({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
hasTitle: !!title,
|
||||
maxCharsPerLine: title ? title.maxCharsPerLine : null,
|
||||
lines: title ? title.lines.length : 0,
|
||||
linesText: title ? title.lines.map(l => l.text).join('|') : '',
|
||||
fontNames: title ? title.lines.map(l => l.fontName).join(',') : '',
|
||||
});
|
||||
});
|
||||
}
|
||||
} else if (config.type === 'db') {
|
||||
config.templates.forEach(t => {
|
||||
const title = t.config && t.config.titleSubtitleConfig;
|
||||
templates.push({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
hasTitle: !!title,
|
||||
maxCharsPerLine: title ? title.maxCharsPerLine : null,
|
||||
lines: title ? title.lines.length : 0,
|
||||
linesText: title ? title.lines.map(l => l.text).join('|') : '',
|
||||
fontNames: title ? title.lines.map(l => l.fontName).join(',') : '',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
analysis[sourceName] = templates;
|
||||
|
||||
console.log(`【${sourceName}】`);
|
||||
templates.forEach(t => {
|
||||
if (t.hasTitle) {
|
||||
console.log(` 模板 ${t.name}: ${t.maxCharsPerLine}字符/行, ${t.lines}行`);
|
||||
} else {
|
||||
console.log(` 模板 ${t.name}: 无标题配置`);
|
||||
}
|
||||
});
|
||||
console.log('');
|
||||
});
|
||||
|
||||
// 5. 找出相同配置的模板组
|
||||
console.log('\n=== 各配置源中相同配置的模板组 ===\n');
|
||||
|
||||
Object.keys(analysis).forEach(sourceName => {
|
||||
const templates = analysis[sourceName];
|
||||
const groups = {};
|
||||
|
||||
templates.forEach(t => {
|
||||
if (t.hasTitle) {
|
||||
const key = `${t.maxCharsPerLine}-${t.lines}-${t.linesText}`;
|
||||
if (!groups[key]) groups[key] = [];
|
||||
groups[key].push(t.name);
|
||||
}
|
||||
});
|
||||
|
||||
const sameGroups = Object.keys(groups).filter(k => groups[k].length > 1);
|
||||
if (sameGroups.length > 0) {
|
||||
console.log(`【${sourceName}】`);
|
||||
sameGroups.forEach(key => {
|
||||
console.log(` 模板 ${groups[key].join(', ')} 配置相同`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
});
|
||||
|
||||
// 6. 对比不同配置源
|
||||
console.log('\n=== 配置源之间的差异 ===\n');
|
||||
|
||||
const sources = Object.keys(analysis);
|
||||
if (sources.length > 1) {
|
||||
const baseSource = sources[0];
|
||||
const baseTemplates = analysis[baseSource];
|
||||
|
||||
sources.slice(1).forEach(sourceName => {
|
||||
const compareTemplates = analysis[sourceName];
|
||||
console.log(`【${baseSource} vs ${sourceName}】`);
|
||||
|
||||
baseTemplates.forEach(baseT => {
|
||||
const compareT = compareTemplates.find(t => t.id === baseT.id || t.name === baseT.name);
|
||||
if (!compareT) {
|
||||
console.log(` 模板 ${baseT.name}: 在 ${sourceName} 中不存在`);
|
||||
} else if (baseT.hasTitle !== compareT.hasTitle) {
|
||||
console.log(` 模板 ${baseT.name}: 标题配置存在性不同 (${baseSource}: ${baseT.hasTitle}, ${sourceName}: ${compareT.hasTitle})`);
|
||||
} else if (baseT.hasTitle && compareT.hasTitle) {
|
||||
const baseKey = `${baseT.maxCharsPerLine}-${baseT.lines}-${baseT.linesText}`;
|
||||
const compareKey = `${compareT.maxCharsPerLine}-${compareT.lines}-${compareT.linesText}`;
|
||||
if (baseKey !== compareKey) {
|
||||
console.log(` 模板 ${baseT.name}: 配置不同`);
|
||||
console.log(` ${baseSource}: ${baseT.maxCharsPerLine}字符/行, ${baseT.lines}行`);
|
||||
console.log(` ${sourceName}: ${compareT.maxCharsPerLine}字符/行, ${compareT.lines}行`);
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log('');
|
||||
});
|
||||
}
|
||||
|
||||
// 7. 总结
|
||||
console.log('\n=== 总结 ===\n');
|
||||
console.log('检查的配置源:');
|
||||
Object.keys(configs).forEach(name => {
|
||||
console.log(` - ${name}: ${configs[name].type === 'db' ? '数据库' : '文件'}`);
|
||||
});
|
||||
console.log('\n建议:');
|
||||
console.log('1. 检查哪个配置源是"正确的"(应该包含8个不同的模板)');
|
||||
console.log('2. 将正确的配置同步到所有配置源');
|
||||
console.log('3. 确保数据库中的配置与配置文件一致');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 读取 TypeScript 配置文件
|
||||
const tsFile = path.join(__dirname, '../src/config/systemSubtitleTemplates.ts');
|
||||
const content = fs.readFileSync(tsFile, 'utf-8');
|
||||
|
||||
// 提取每个模板的关键信息
|
||||
const templates = [];
|
||||
const templateRegex = /id: "template_system_(\d+)",[\s\S]*?name: "(\d+)",[\s\S]*?titleSubtitleConfig: \{[\s\S]*?maxCharsPerLine: (\d+),[\s\S]*?sourceTitle: "([^"]+)",[\s\S]*?lines: \[([\s\S]*?)\],[\s\S]*?\},[\s\S]*?\},[\s\S]*?isSystem: true/g;
|
||||
|
||||
let match;
|
||||
while ((match = templateRegex.exec(content)) !== null) {
|
||||
const id = match[1];
|
||||
const name = match[2];
|
||||
const maxCharsPerLine = parseInt(match[3]);
|
||||
const sourceTitle = match[4];
|
||||
const linesContent = match[5];
|
||||
|
||||
// 提取行信息
|
||||
const lines = [];
|
||||
const fontNames = [];
|
||||
const lineRegex = /\{[^}]*text: "([^"]+)"[^}]*fontName: "([^"]+)"[^}]*fontColor: "([^"]+)"[^}]*\}/g;
|
||||
let lineMatch;
|
||||
while ((lineMatch = lineRegex.exec(linesContent)) !== null) {
|
||||
lines.push({
|
||||
text: lineMatch[1],
|
||||
fontName: lineMatch[2],
|
||||
fontColor: lineMatch[3]
|
||||
});
|
||||
if (!fontNames.includes(lineMatch[2])) {
|
||||
fontNames.push(lineMatch[2]);
|
||||
}
|
||||
}
|
||||
|
||||
templates.push({
|
||||
id,
|
||||
name,
|
||||
maxCharsPerLine,
|
||||
sourceTitle,
|
||||
lines,
|
||||
fontNames
|
||||
});
|
||||
}
|
||||
|
||||
console.log('=== 模板标题字幕配置对比 ===\n');
|
||||
|
||||
templates.forEach(t => {
|
||||
console.log(`模板 ${t.name}:`);
|
||||
console.log(` - 每行字符数: ${t.maxCharsPerLine}`);
|
||||
console.log(` - 行数: ${t.lines.length}`);
|
||||
console.log(` - 字体: ${t.fontNames.join(', ')}`);
|
||||
t.lines.forEach((line, idx) => {
|
||||
console.log(` - 第${idx + 1}行: "${line.text}" (${line.fontName}, ${line.fontColor})`);
|
||||
});
|
||||
console.log('');
|
||||
});
|
||||
|
||||
// 找出相同的模板
|
||||
console.log('\n=== 相同配置的模板组 ===\n');
|
||||
|
||||
const groups = {};
|
||||
templates.forEach(t => {
|
||||
const key = `${t.maxCharsPerLine}-${t.lines.length}-${t.fontNames.join(',')}-${t.lines.map(l => `${l.text}:${l.fontName}:${l.fontColor}`).join('|')}`;
|
||||
if (!groups[key]) {
|
||||
groups[key] = [];
|
||||
}
|
||||
groups[key].push(t.name);
|
||||
});
|
||||
|
||||
Object.keys(groups).forEach(key => {
|
||||
if (groups[key].length > 1) {
|
||||
console.log(`模板 ${groups[key].join(', ')} 配置相同`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
const buildDir = path.join(root, 'dist-release-final');
|
||||
const bundleDir = path.join(buildDir, 'resource-bundles');
|
||||
const releaseDir = path.join(buildDir, 'release-assets');
|
||||
const legacyPrefixes = ['尚创IP智能体V8'];
|
||||
const currentPrefix = 'SC-IP-Agent';
|
||||
const manifestPath = path.join(buildDir, 'resource-manifest.json');
|
||||
const isOemFullOfflineBuild = process.env.OEM_BATCH_BUILD === '1';
|
||||
|
||||
const readJson = (file, fallback) => {
|
||||
if (!fs.existsSync(file)) return fallback;
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
|
||||
fs.mkdirSync(releaseDir, { recursive: true });
|
||||
|
||||
const waitForFile = (filePath, maxWait = 10000) => {
|
||||
const startTime = Date.now();
|
||||
while (Date.now() - startTime < maxWait) {
|
||||
try {
|
||||
const fd = fs.openSync(filePath, 'r+');
|
||||
fs.closeSync(fd);
|
||||
return true;
|
||||
} catch {
|
||||
if (Date.now() - startTime < maxWait) {
|
||||
const sleepMs = 200;
|
||||
const sleepUntil = Date.now() + sleepMs;
|
||||
while (Date.now() < sleepUntil) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
console.warn(`[assemble] Warning: ${filePath} still locked after ${maxWait}ms, attempting copy anyway...`);
|
||||
return false;
|
||||
};
|
||||
|
||||
const copyIfNeeded = (sourcePath, destPath) => {
|
||||
if (fs.existsSync(destPath)) {
|
||||
const srcBuffer = fs.readFileSync(sourcePath);
|
||||
const destBuffer = fs.readFileSync(destPath);
|
||||
if (srcBuffer.length === destBuffer.length && srcBuffer.equals(destBuffer)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
waitForFile(sourcePath);
|
||||
fs.copyFileSync(sourcePath, destPath);
|
||||
return true;
|
||||
};
|
||||
|
||||
const compatAliasesFor = (name) => {
|
||||
for (const legacyPrefix of legacyPrefixes) {
|
||||
const prefix = `${legacyPrefix}-`;
|
||||
if (name.startsWith(prefix)) {
|
||||
return [`${currentPrefix}-${name.slice(prefix.length)}`];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const releaseExtensions = ['.exe', '.blockmap', '.yml', '.7z'];
|
||||
const hasRootInstaller = fs.readdirSync(buildDir)
|
||||
.some(name => {
|
||||
const full = path.join(buildDir, name);
|
||||
return fs.statSync(full).isFile()
|
||||
&& /setup.*\.exe$/i.test(name)
|
||||
&& !name.startsWith('__uninstaller')
|
||||
&& fs.statSync(full).size > 1024 * 1024;
|
||||
});
|
||||
const hasNsisWebOutput = !hasRootInstaller && fs.existsSync(path.join(buildDir, 'nsis-web'));
|
||||
const copyReleaseFiles = (dir) => {
|
||||
if (!fs.existsSync(dir)) return;
|
||||
if (path.resolve(dir) === path.resolve(releaseDir)) return;
|
||||
for (const name of fs.readdirSync(dir)) {
|
||||
const full = path.join(dir, name);
|
||||
const stat = fs.statSync(full);
|
||||
if (stat.isDirectory()) {
|
||||
if (path.resolve(dir) === path.resolve(buildDir) && name === 'nsis-web') {
|
||||
copyReleaseFiles(full);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!stat.isFile()) continue;
|
||||
if (!releaseExtensions.some(ext => name.endsWith(ext))) continue;
|
||||
if (hasNsisWebOutput && path.resolve(dir) === path.resolve(buildDir)) continue;
|
||||
const dest = path.join(releaseDir, name);
|
||||
try {
|
||||
copyIfNeeded(full, dest);
|
||||
} catch (err) {
|
||||
if (err.code === 'EBUSY') {
|
||||
console.warn(`[assemble] ${name} is locked, skipping (likely being scanned by antivirus)`);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
copyReleaseFiles(buildDir);
|
||||
|
||||
for (const name of fs.readdirSync(releaseDir)) {
|
||||
const full = path.join(releaseDir, name);
|
||||
if (!fs.statSync(full).isFile()) continue;
|
||||
for (const alias of compatAliasesFor(name)) {
|
||||
const aliasPath = path.join(releaseDir, alias);
|
||||
if (aliasPath === full || fs.existsSync(aliasPath)) continue;
|
||||
fs.copyFileSync(full, aliasPath);
|
||||
console.log(`[assemble] Created compatibility alias: ${alias}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isOemFullOfflineBuild && fs.existsSync(manifestPath)) {
|
||||
fs.copyFileSync(manifestPath, path.join(releaseDir, 'resource-manifest.json'));
|
||||
}
|
||||
|
||||
if (!isOemFullOfflineBuild && fs.existsSync(bundleDir)) {
|
||||
const manifest = readJson(manifestPath, { bundles: {} });
|
||||
const currentBundleArchives = new Set(
|
||||
Object.values(manifest.bundles || {})
|
||||
.map(bundle => bundle && bundle.archive)
|
||||
.filter(Boolean)
|
||||
);
|
||||
|
||||
for (const name of fs.readdirSync(bundleDir)) {
|
||||
const full = path.join(bundleDir, name);
|
||||
const shouldCopy = name.endsWith('.json') || currentBundleArchives.has(name);
|
||||
if (fs.statSync(full).isFile() && shouldCopy) {
|
||||
fs.copyFileSync(full, path.join(releaseDir, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isOemFullOfflineBuild) {
|
||||
for (const name of fs.readdirSync(releaseDir)) {
|
||||
if (name.endsWith('.zip') || name === 'resource-manifest.json' || name === 'changed-bundles.json') {
|
||||
fs.rmSync(path.join(releaseDir, name), { force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[release-assemble] output:', releaseDir);
|
||||
@@ -0,0 +1,241 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
const packageJson = require(path.join(root, 'package.json'));
|
||||
const buildDir = path.join(root, 'dist-release-final');
|
||||
const unpackedDir = path.join(buildDir, 'win-unpacked');
|
||||
const bundleDir = path.join(buildDir, 'resource-bundles');
|
||||
const manifestPath = path.join(buildDir, 'resource-manifest.json');
|
||||
const tempDir = path.join(root, 'build', 'oem-batch-temp');
|
||||
|
||||
function findIscc() {
|
||||
const candidates = [
|
||||
process.env.INNO_SETUP_ISCC,
|
||||
'C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe',
|
||||
'C:\\Program Files\\Inno Setup 6\\ISCC.exe',
|
||||
'ISCC.exe',
|
||||
].filter(Boolean);
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (candidate.includes('\\') || candidate.includes('/')) {
|
||||
if (fs.existsSync(candidate)) return candidate;
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = spawnSync(candidate, ['/Qp'], {
|
||||
encoding: 'utf8',
|
||||
windowsHide: true,
|
||||
});
|
||||
if (result.status === 0 || result.status === 1) return candidate;
|
||||
}
|
||||
|
||||
throw new Error('Inno Setup ISCC.exe not found.');
|
||||
}
|
||||
|
||||
function getInnoInstallDir(isccPath) {
|
||||
if (isccPath.includes('\\') || isccPath.includes('/')) {
|
||||
return path.dirname(isccPath);
|
||||
}
|
||||
|
||||
const candidates = [
|
||||
'C:\\Program Files (x86)\\Inno Setup 6',
|
||||
'C:\\Program Files\\Inno Setup 6',
|
||||
];
|
||||
return candidates.find(candidate => fs.existsSync(path.join(candidate, 'ISCC.exe'))) || '';
|
||||
}
|
||||
|
||||
function getLanguagesSection(isccPath) {
|
||||
const innoDir = getInnoInstallDir(isccPath);
|
||||
const chineseLanguageFile = innoDir ? path.join(innoDir, 'Languages', 'ChineseSimplified.isl') : '';
|
||||
if (chineseLanguageFile && fs.existsSync(chineseLanguageFile)) {
|
||||
return '[Languages]\nName: "chinesesimp"; MessagesFile: "compiler:Languages\\\\ChineseSimplified.isl"';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function escapeInnoString(value) {
|
||||
return String(value).replace(/"/g, '""');
|
||||
}
|
||||
|
||||
function readJson(file, fallback) {
|
||||
if (!fs.existsSync(file)) return fallback;
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function findAppExe() {
|
||||
if (!fs.existsSync(unpackedDir)) {
|
||||
throw new Error(`win-unpacked not found: ${unpackedDir}`);
|
||||
}
|
||||
|
||||
const exeFiles = fs.readdirSync(unpackedDir)
|
||||
.filter(name => name.toLowerCase().endsWith('.exe'))
|
||||
.filter(name => !/^unins/i.test(name));
|
||||
|
||||
if (exeFiles.length === 0) {
|
||||
throw new Error(`No app exe found in ${unpackedDir}`);
|
||||
}
|
||||
|
||||
exeFiles.sort((a, b) => {
|
||||
const aStat = fs.statSync(path.join(unpackedDir, a));
|
||||
const bStat = fs.statSync(path.join(unpackedDir, b));
|
||||
return bStat.size - aStat.size;
|
||||
});
|
||||
|
||||
return exeFiles[0];
|
||||
}
|
||||
|
||||
function getCurrentResourceArchives() {
|
||||
const manifest = readJson(manifestPath, { bundles: {} });
|
||||
const archives = Object.values(manifest.bundles || {})
|
||||
.map(bundle => bundle && bundle.archive)
|
||||
.filter(Boolean);
|
||||
|
||||
const archivePaths = Array.from(new Set(archives))
|
||||
.map(archive => path.join(bundleDir, archive))
|
||||
.sort();
|
||||
const missing = archivePaths.filter(archivePath => !fs.existsSync(archivePath));
|
||||
|
||||
if (archivePaths.length === 0) {
|
||||
throw new Error(`No resource archives found in manifest: ${manifestPath}`);
|
||||
}
|
||||
if (missing.length > 0) {
|
||||
throw new Error(`Missing resource archives:\n${missing.join('\n')}`);
|
||||
}
|
||||
|
||||
return archivePaths;
|
||||
}
|
||||
|
||||
function getCurrentResourceArchiveEntries() {
|
||||
const manifest = readJson(manifestPath, { bundles: {} });
|
||||
return Object.entries(manifest.bundles || {})
|
||||
.map(([name, bundle]) => ({
|
||||
name,
|
||||
archive: bundle && bundle.archive,
|
||||
archivePath: bundle && bundle.archive ? path.join(bundleDir, bundle.archive) : '',
|
||||
}))
|
||||
.filter(entry => entry.archive && fs.existsSync(entry.archivePath))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
function ensureOfflineBundlesHydrated() {
|
||||
const scriptPath = path.join(__dirname, 'hydrate-oem-portable-bundles.cjs');
|
||||
const result = spawnSync(process.execPath, [scriptPath], {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
stdio: 'inherit',
|
||||
windowsHide: true,
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`hydrate-oem-portable-bundles failed with exit code ${result.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
function dirSizeBytes(dir) {
|
||||
let total = 0;
|
||||
const walk = current => {
|
||||
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
||||
const full = path.join(current, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walk(full);
|
||||
} else if (entry.isFile()) {
|
||||
total += fs.statSync(full).size;
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(dir);
|
||||
return total;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const appExe = findAppExe();
|
||||
ensureOfflineBundlesHydrated();
|
||||
const resourceArchives = getCurrentResourceArchives();
|
||||
const appName = path.basename(appExe, '.exe');
|
||||
const version = process.env.OEM_VERSION || process.env.APP_VERSION || packageJson.version || '1.0.0';
|
||||
const outputBaseName = `${appName}-${version}-win-setup-x64`;
|
||||
const issPath = path.join(tempDir, 'oem-full-offline-installer.iss');
|
||||
const iscc = findIscc();
|
||||
const languagesSection = getLanguagesSection(iscc);
|
||||
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
fs.mkdirSync(buildDir, { recursive: true });
|
||||
|
||||
const previousOutput = path.join(buildDir, `${outputBaseName}.exe`);
|
||||
if (fs.existsSync(previousOutput)) {
|
||||
fs.rmSync(previousOutput, { force: true });
|
||||
}
|
||||
|
||||
const iss = `
|
||||
[Setup]
|
||||
AppId={{68912956-3e71-5e51-8eba-ab7cdc795c8c}
|
||||
AppName=${escapeInnoString(appName)}
|
||||
AppVersion=${escapeInnoString(version)}
|
||||
AppPublisher=ModStartLib
|
||||
DefaultDirName={autopf}\\${escapeInnoString(appName)}
|
||||
DefaultGroupName=${escapeInnoString(appName)}
|
||||
DisableDirPage=no
|
||||
DisableProgramGroupPage=yes
|
||||
UsePreviousAppDir=no
|
||||
OutputDir=${escapeInnoString(buildDir)}
|
||||
OutputBaseFilename=${escapeInnoString(outputBaseName)}
|
||||
SetupIconFile=${escapeInnoString(path.join(root, 'electron', 'resources', 'build', 'logo.ico'))}
|
||||
UninstallDisplayIcon={app}\\${escapeInnoString(appExe)}
|
||||
Compression=lzma2/max
|
||||
SolidCompression=no
|
||||
LZMAUseSeparateProcess=yes
|
||||
LZMADictionarySize=65536
|
||||
DiskSpanning=no
|
||||
ArchitecturesAllowed=x64compatible
|
||||
ArchitecturesInstallIn64BitMode=x64compatible
|
||||
WizardStyle=modern
|
||||
PrivilegesRequired=lowest
|
||||
CloseApplications=yes
|
||||
RestartApplications=no
|
||||
|
||||
${languagesSection}
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"
|
||||
|
||||
[Files]
|
||||
Source: "${escapeInnoString(path.join(unpackedDir, '*'))}"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "${escapeInnoString(manifestPath)}"; DestDir: "{app}\\resources"; Flags: ignoreversion
|
||||
[Icons]
|
||||
Name: "{group}\\${escapeInnoString(appName)}"; Filename: "{app}\\${escapeInnoString(appExe)}"; WorkingDir: "{app}"
|
||||
Name: "{autodesktop}\\${escapeInnoString(appName)}"; Filename: "{app}\\${escapeInnoString(appExe)}"; WorkingDir: "{app}"; Tasks: desktopicon
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\\${escapeInnoString(appExe)}"; Description: "{cm:LaunchProgram,${escapeInnoString(appName)}}"; Flags: nowait postinstall skipifsilent
|
||||
`.trimStart();
|
||||
|
||||
fs.writeFileSync(issPath, iss, 'utf8');
|
||||
|
||||
const unpackedGb = dirSizeBytes(unpackedDir) / 1024 / 1024 / 1024;
|
||||
const archivesMb = resourceArchives.reduce((total, archivePath) => total + fs.statSync(archivePath).size, 0) / 1024 / 1024;
|
||||
console.log(`[inno] source: ${unpackedDir} (${unpackedGb.toFixed(2)} GB)`);
|
||||
console.log(`[inno] validated resource archives: ${resourceArchives.length} (${archivesMb.toFixed(2)} MB)`);
|
||||
console.log('[inno] resource payload mode: full offline resources copied from win-unpacked');
|
||||
console.log(`[inno] output: ${previousOutput}`);
|
||||
|
||||
const result = spawnSync(iscc, [issPath], {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
stdio: 'inherit',
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`ISCC failed with exit code ${result.status}`);
|
||||
}
|
||||
|
||||
const stat = fs.statSync(previousOutput);
|
||||
console.log(`[inno] built: ${previousOutput} (${(stat.size / 1024 / 1024).toFixed(2)} MB)`);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,146 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const archiver = require('archiver');
|
||||
const config = require('./resource-bundles.config.cjs');
|
||||
|
||||
function ensureDir(dir) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
function listFiles(dir) {
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const files = [];
|
||||
const walk = (current) => {
|
||||
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
||||
const full = path.join(current, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walk(full);
|
||||
} else if (entry.isFile()) {
|
||||
files.push(full);
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(dir);
|
||||
files.sort();
|
||||
return files;
|
||||
}
|
||||
|
||||
function hashDirectory(dir) {
|
||||
const hash = crypto.createHash('sha256');
|
||||
for (const file of listFiles(dir)) {
|
||||
const rel = path.relative(dir, file).replace(/\\/g, '/');
|
||||
const stat = fs.statSync(file);
|
||||
hash.update(rel);
|
||||
hash.update(String(stat.size));
|
||||
hash.update(fs.readFileSync(file));
|
||||
}
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
function hashFile(file) {
|
||||
const hash = crypto.createHash('sha256');
|
||||
hash.update(fs.readFileSync(file));
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
function readJson(file, fallback) {
|
||||
if (!fs.existsSync(file)) return fallback;
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function zipDirectory(sourceDir, outFile) {
|
||||
return new Promise((resolve, reject) => {
|
||||
ensureDir(path.dirname(outFile));
|
||||
const output = fs.createWriteStream(outFile);
|
||||
const archive = archiver('zip', { zlib: { level: 9 } });
|
||||
output.on('close', resolve);
|
||||
archive.on('error', reject);
|
||||
archive.pipe(output);
|
||||
archive.directory(sourceDir, false);
|
||||
archive.finalize();
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
ensureDir(config.outputDir);
|
||||
ensureDir(path.dirname(config.releaseManifestPath));
|
||||
ensureDir(path.dirname(config.embeddedManifestPath));
|
||||
ensureDir(path.dirname(config.buildStatePath));
|
||||
|
||||
const previousState = readJson(config.buildStatePath, { bundles: {} });
|
||||
const nextState = { generatedAt: new Date().toISOString(), bundles: {} };
|
||||
const manifest = {
|
||||
manifestVersion: 1,
|
||||
generatedAt: new Date().toISOString(),
|
||||
appVersion: process.env.npm_package_version || '',
|
||||
bundles: {},
|
||||
};
|
||||
|
||||
const changedBundles = [];
|
||||
|
||||
for (const bundle of config.bundles) {
|
||||
if (!fs.existsSync(bundle.source)) {
|
||||
if (bundle.required) {
|
||||
throw new Error(`Required resource bundle source missing: ${bundle.name} (${bundle.source})`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const sourceFiles = listFiles(bundle.source);
|
||||
if (bundle.required && sourceFiles.length === 0) {
|
||||
throw new Error(`Required resource bundle source is empty: ${bundle.name} (${bundle.source})`);
|
||||
}
|
||||
|
||||
const version = hashDirectory(bundle.source).slice(0, 16);
|
||||
const archive = `${bundle.name}-${version}.zip`;
|
||||
const archivePath = path.join(config.outputDir, archive);
|
||||
const previous = previousState.bundles?.[bundle.name];
|
||||
|
||||
if (!previous || previous.version !== version || !fs.existsSync(archivePath)) {
|
||||
await zipDirectory(bundle.source, archivePath);
|
||||
changedBundles.push(bundle.name);
|
||||
}
|
||||
|
||||
const size = fs.existsSync(archivePath) ? fs.statSync(archivePath).size : 0;
|
||||
const sha256 = fs.existsSync(archivePath) ? hashFile(archivePath) : '';
|
||||
if (bundle.required && (!fs.existsSync(archivePath) || size <= 0 || !sha256)) {
|
||||
throw new Error(`Required resource bundle archive was not created: ${bundle.name} (${archivePath})`);
|
||||
}
|
||||
|
||||
const archiveUrl = process.env[config.releaseBaseUrlEnv] || config.releaseBaseUrl
|
||||
? `${(process.env[config.releaseBaseUrlEnv] || config.releaseBaseUrl).replace(/\/$/, '')}/${archive}`
|
||||
: archive;
|
||||
|
||||
manifest.bundles[bundle.name] = {
|
||||
version,
|
||||
archive,
|
||||
url: archiveUrl,
|
||||
sha256,
|
||||
size,
|
||||
required: !!bundle.required,
|
||||
extractTo: bundle.extractTo,
|
||||
source: path.relative(path.join(__dirname, '..'), bundle.source).replace(/\\/g, '/'),
|
||||
};
|
||||
|
||||
nextState.bundles[bundle.name] = { version, archive, sha256, size };
|
||||
}
|
||||
|
||||
fs.writeFileSync(config.releaseManifestPath, JSON.stringify(manifest, null, 2));
|
||||
fs.writeFileSync(config.embeddedManifestPath, JSON.stringify(manifest, null, 2));
|
||||
fs.writeFileSync(config.buildStatePath, JSON.stringify(nextState, null, 2));
|
||||
|
||||
const summaryPath = path.join(config.outputDir, 'changed-bundles.json');
|
||||
fs.writeFileSync(summaryPath, JSON.stringify({ changedBundles, manifest: path.basename(config.releaseManifestPath) }, null, 2));
|
||||
|
||||
console.log('[resource-build] changed bundles:', changedBundles.join(', ') || 'none');
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('[resource-build] failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
const common = require("./common.cjs");
|
||||
|
||||
console.log("BuildOptimize", {
|
||||
name: common.platformName(),
|
||||
arch: common.platformArch(),
|
||||
});
|
||||
|
||||
exports.default = async function (context) {
|
||||
console.log("BuildOptimize.output", {
|
||||
context: context,
|
||||
root: context.appOutDir,
|
||||
});
|
||||
// copy extra electron/resources/extra/[name]-[arch] to extra
|
||||
const platformName = common.platformName();
|
||||
const platformArch = common.platformArch();
|
||||
const name = platformName + "-" + platformArch;
|
||||
|
||||
const srcDir = `electron/resources/extra/${name}`;
|
||||
let destDir = null;
|
||||
if (platformName === 'osx') {
|
||||
destDir = common.pathResolve(
|
||||
context.appOutDir,
|
||||
`${context.packager.appInfo.productFilename}.app`,
|
||||
"Contents",
|
||||
"Resources",
|
||||
"extra",
|
||||
name
|
||||
);
|
||||
} else if (platformName === 'win') {
|
||||
destDir = common.pathResolve(context.appOutDir, "resources", "extra", name);
|
||||
} else if (platformName === 'linux') {
|
||||
destDir = common.pathResolve(context.appOutDir, "resources", "extra", name);
|
||||
}
|
||||
|
||||
console.log("BuildOptimize.copy", {
|
||||
platformName,
|
||||
platformArch,
|
||||
srcDir,
|
||||
destDir,
|
||||
});
|
||||
|
||||
if (srcDir && common.exists(srcDir)) {
|
||||
if (process.env.OEM_BATCH_BUILD === "1" && platformName === "win" && platformArch === "x86") {
|
||||
console.log("OEM batch build skips FFmpeg in NSIS payload; it is delivered as resource-bundles/ffmpeg");
|
||||
return;
|
||||
}
|
||||
console.log(`Copying from ${srcDir} to ${destDir}`);
|
||||
common.copy(srcDir, destDir, true);
|
||||
console.log(`Copy completed`);
|
||||
} else {
|
||||
console.log(`No matching source directory found for platform: ${platformName}-${platformArch}`);
|
||||
}
|
||||
|
||||
// common.listFiles(context.appOutDir, true).forEach((p) => {
|
||||
// console.log('BuildOptimize.path', (p.isDir ? 'D:' : 'F:') + p.path);
|
||||
// })
|
||||
// const localeDir = context.appOutDir + "/AigcPanel.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Resources/";
|
||||
// console.log(`localeDir: ${localeDir}`);
|
||||
// fs.readdir(localeDir, function (err, files) {
|
||||
// if (!(files && files.length)) {
|
||||
// return;
|
||||
// }
|
||||
// for (let f of files) {
|
||||
// if (f.endsWith('.lproj')) {
|
||||
// if (!(f.startsWith("en") || f.startsWith("zh"))) {
|
||||
// const p = localeDir + f;
|
||||
// console.log(`removeFile: ${p}`);
|
||||
// fs.rmdirSync(p, {recursive: true});
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
const fs = require('fs');
|
||||
|
||||
const content = fs.readFileSync('electron/resources/extra/common/config/system-templates.json', 'utf-8');
|
||||
const data = JSON.parse(content);
|
||||
|
||||
console.log('模板数量:', data.subtitleTemplates.length);
|
||||
console.log('');
|
||||
|
||||
data.subtitleTemplates.forEach(t => {
|
||||
const hasTitle = !!(t.config && t.config.titleSubtitleConfig);
|
||||
console.log(`模板 ${t.name}: ${hasTitle ? '有标题配置' : '无标题配置'}`);
|
||||
if (hasTitle) {
|
||||
const title = t.config.titleSubtitleConfig;
|
||||
console.log(` - ${title.maxCharsPerLine}字符/行, ${title.lines.length}行`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
const targets = [
|
||||
path.join(root, 'dist'),
|
||||
path.join(root, 'dist-electron'),
|
||||
];
|
||||
|
||||
for (const target of targets) {
|
||||
if (fs.existsSync(target)) {
|
||||
fs.rmSync(target, { recursive: true, force: true });
|
||||
console.log(`[clean-build-artifacts] removed ${target}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
const outputDir = path.join(root, 'dist-release-final');
|
||||
|
||||
const exactNames = new Set([
|
||||
'win-unpacked',
|
||||
'release-assets',
|
||||
'nsis-web',
|
||||
]);
|
||||
|
||||
const removableFilePatterns = [
|
||||
/-win-setup-x64\.exe$/i,
|
||||
/-win-setup-x64\.exe\.baiduyun\.uploading\.cfg$/i,
|
||||
/^__uninstaller-nsis-.*\.exe$/i,
|
||||
/\.nsis\.7z$/i,
|
||||
/\.blockmap$/i,
|
||||
];
|
||||
|
||||
function remove(target) {
|
||||
if (fs.existsSync(target)) {
|
||||
fs.rmSync(target, { recursive: true, force: true });
|
||||
console.log(`[clean-oem-output] removed ${target}`);
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
for (const name of fs.readdirSync(outputDir)) {
|
||||
const full = path.join(outputDir, name);
|
||||
const stat = fs.statSync(full);
|
||||
if (stat.isDirectory() && exactNames.has(name)) {
|
||||
remove(full);
|
||||
continue;
|
||||
}
|
||||
if (stat.isFile() && removableFilePatterns.some(pattern => pattern.test(name))) {
|
||||
remove(full);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,402 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
GPU显存清理脚本(强化版)
|
||||
在任务完成后调用,彻底释放GPU显存和内存
|
||||
支持强制杀死占用GPU的Python进程
|
||||
"""
|
||||
|
||||
import sys
|
||||
import subprocess
|
||||
import os
|
||||
import gc
|
||||
import time
|
||||
|
||||
def get_gpu_processes():
|
||||
"""获取占用GPU的进程列表"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
"nvidia-smi --query-compute-apps=pid,process_name,used_memory --format=csv,noheader",
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
|
||||
processes = []
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
if line.strip():
|
||||
parts = [p.strip() for p in line.split(',')]
|
||||
if len(parts) >= 3:
|
||||
pid, name, memory = parts
|
||||
processes.append({
|
||||
'pid': int(pid),
|
||||
'name': name,
|
||||
'memory': memory
|
||||
})
|
||||
return processes
|
||||
except Exception as e:
|
||||
print("[GPU清理] 获取GPU进程失表: {}".format(e))
|
||||
return []
|
||||
|
||||
def get_all_python_processes():
|
||||
"""获取所有Python进程列表(包括孤儿进程)"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
"tasklist /FI \"IMAGENAME eq python.exe\" /FO CSV /NH",
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
|
||||
processes = []
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
if line.strip():
|
||||
parts = [p.strip('"') for p in line.split(',')]
|
||||
if len(parts) >= 2:
|
||||
name = parts[0]
|
||||
pid = parts[1]
|
||||
try:
|
||||
processes.append({
|
||||
'pid': int(pid),
|
||||
'name': name
|
||||
})
|
||||
except:
|
||||
pass
|
||||
return processes
|
||||
except Exception as e:
|
||||
print("[GPU清理] 获取Python进程失败: {}".format(e))
|
||||
return []
|
||||
|
||||
def should_kill_process(pid, model_paths=None):
|
||||
"""判断进程是否应该被清理(只清理AI模型相关进程)
|
||||
|
||||
Args:
|
||||
pid: 进程ID
|
||||
model_paths: 模型路径列表,如果提供,只清理这些路径下的进程
|
||||
"""
|
||||
try:
|
||||
# 获取进程命令行
|
||||
cmdline_result = subprocess.run(
|
||||
"wmic process where ProcessId={} get CommandLine /format:list".format(pid),
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=2
|
||||
)
|
||||
|
||||
if cmdline_result.returncode != 0 or not cmdline_result.stdout:
|
||||
return False
|
||||
|
||||
cmdline = cmdline_result.stdout.lower()
|
||||
|
||||
# 白名单(包含这些关键词的进程不清理)
|
||||
whitelist_keywords = [
|
||||
'直播伴侣',
|
||||
'finderliveobs',
|
||||
'xwechat',
|
||||
'obs',
|
||||
'obs-studio',
|
||||
'streamlabs',
|
||||
'douyin',
|
||||
'tiktok',
|
||||
'bilibili',
|
||||
'抖音',
|
||||
'kuaishou',
|
||||
'快手',
|
||||
'pycharm',
|
||||
'vscode',
|
||||
'visual studio code',
|
||||
'jupyter',
|
||||
'spyder',
|
||||
'anaconda',
|
||||
'conda',
|
||||
'sublime',
|
||||
'notepad++',
|
||||
'wechat',
|
||||
'weixin',
|
||||
'微信',
|
||||
'qq',
|
||||
'asr',
|
||||
'funasr',
|
||||
'aigcpanel',
|
||||
'whisper',
|
||||
'cosyvoice',
|
||||
]
|
||||
|
||||
# 检查白名单
|
||||
for keyword in whitelist_keywords:
|
||||
if keyword in cmdline:
|
||||
# 提取进程名称(从命令行中获取)
|
||||
process_name = "未知"
|
||||
if "python" in cmdline:
|
||||
process_name = "Python进程"
|
||||
elif "finderliveobs" in cmdline or "xwechat" in cmdline:
|
||||
process_name = "视频号直播伴侣"
|
||||
elif "obs" in cmdline:
|
||||
process_name = "OBS直播软件"
|
||||
elif "wechat" in cmdline or "weixin" in cmdline:
|
||||
process_name = "微信"
|
||||
|
||||
print("[GPU清理] [保护] 跳过白名单进程 PID={} ({}, 匹配关键词: {})".format(pid, process_name, keyword))
|
||||
return False
|
||||
|
||||
# 🔧 必须提供模型路径才进行清理(安全模式)
|
||||
if model_paths and len(model_paths) > 0:
|
||||
for model_path in model_paths:
|
||||
# 标准化路径格式(统一使用小写和反斜杠)
|
||||
normalized_path = model_path.lower().replace('/', '\\')
|
||||
if normalized_path in cmdline:
|
||||
print("[GPU清理] 匹配模型路径 PID={} (路径: {})".format(pid, model_path))
|
||||
return True
|
||||
# 如果提供了路径但都不匹配,不清理
|
||||
return False
|
||||
else:
|
||||
# 🔧 安全修复:没有提供路径时,不清理任何进程(防止误杀)
|
||||
# 之前的关键词匹配模式已移除,确保只清理模型路径下的进程
|
||||
print("[GPU清理] 警告: 未提供模型路径,跳过进程 PID={}".format(pid))
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print("[GPU清理] 检查进程 PID={} 失败: {}".format(pid, e))
|
||||
return False
|
||||
|
||||
def cleanup_gpu(force_kill=True, model_paths=None):
|
||||
"""彻底清理GPU显存和内存(默认强力模式)
|
||||
|
||||
Args:
|
||||
force_kill: 是否强制清理
|
||||
model_paths: 模型路径列表,只清理这些路径下的进程
|
||||
"""
|
||||
try:
|
||||
print("[GPU清理] ========== 开始GPU显存和内存清理 ==========")
|
||||
if model_paths and len(model_paths) > 0:
|
||||
print("[GPU清理] [模式] 路径过滤:只清理模型路径下的Python进程")
|
||||
print("[GPU清理] [路径] 共 {} 个模型路径:".format(len(model_paths)))
|
||||
for path in model_paths:
|
||||
print("[GPU清理] - {}".format(path))
|
||||
else:
|
||||
print("[GPU清理] [模式] 智能清理:只清理AI模型进程,保护其他Python应用")
|
||||
|
||||
# ==================== 步骤1:PyTorch GPU深度清理 ====================
|
||||
print("[GPU清理] [步骤1] PyTorch GPU深度清理...")
|
||||
try:
|
||||
import torch
|
||||
|
||||
if torch.cuda.is_available():
|
||||
# 显示清理前的显存状态
|
||||
allocated_before = torch.cuda.memory_allocated() / 1024**3
|
||||
reserved_before = torch.cuda.memory_reserved() / 1024**3
|
||||
print("[GPU清理] 清理前: 已分配={:.2f}GB, 已保留={:.2f}GB".format(allocated_before, reserved_before))
|
||||
|
||||
# 1. 清空CUDA缓存(多次循环确保彻底清理)
|
||||
print("[GPU清理] 正在清空CUDA缓存...")
|
||||
for i in range(5):
|
||||
torch.cuda.empty_cache()
|
||||
gc.collect()
|
||||
time.sleep(0.1)
|
||||
|
||||
# 2. 同步所有CUDA设备
|
||||
print("[GPU清理] 同步CUDA设备...")
|
||||
torch.cuda.synchronize()
|
||||
|
||||
# 3. 重置内存统计
|
||||
torch.cuda.reset_peak_memory_stats()
|
||||
torch.cuda.reset_accumulated_memory_stats()
|
||||
|
||||
# 4. 尝试释放所有未使用的缓存内存
|
||||
try:
|
||||
# 清理内存分配器的缓存
|
||||
torch.cuda.memory.empty_cache()
|
||||
except:
|
||||
pass
|
||||
|
||||
# 5. 设置内存分配器配置(优化内存使用)
|
||||
try:
|
||||
os.environ['PYTORCH_CUDA_ALLOC_CONF'] = 'max_split_size_mb:512'
|
||||
except:
|
||||
pass
|
||||
|
||||
# 显示清理后的显存状态
|
||||
allocated_after = torch.cuda.memory_allocated() / 1024**3
|
||||
reserved_after = torch.cuda.memory_reserved() / 1024**3
|
||||
freed_memory = (reserved_before - reserved_after)
|
||||
print("[GPU清理] 清理后: 已分配={:.2f}GB, 已保留={:.2f}GB".format(allocated_after, reserved_after))
|
||||
print("[GPU清理] 释放显存: {:.2f}GB".format(freed_memory))
|
||||
|
||||
print("[GPU清理] [OK] PyTorch GPU清理完成")
|
||||
else:
|
||||
print("[GPU清理] CUDA不可用,跳过GPU清理")
|
||||
except ImportError:
|
||||
print("[GPU清理] PyTorch未安装,跳过GPU清理")
|
||||
except Exception as e:
|
||||
print("[GPU清理] PyTorch清理出错: {}".format(e))
|
||||
|
||||
# ==================== 步骤2:强制系统内存清理 ====================
|
||||
print("[GPU清理] [步骤2] 执行强制系统内存清理...")
|
||||
|
||||
# 1. 多轮垃圾回收(清理不同代的对象)
|
||||
total_collected = 0
|
||||
for i in range(5):
|
||||
collected = gc.collect(generation=2) # 清理所有代
|
||||
total_collected += collected
|
||||
time.sleep(0.05)
|
||||
|
||||
print("[GPU清理] 垃圾回收: 清理了 {} 个对象".format(total_collected))
|
||||
|
||||
# 2. 清理Python内部缓存
|
||||
try:
|
||||
import ctypes
|
||||
# 尝试释放Python未使用的内存回操作系统
|
||||
if hasattr(ctypes, 'windll'):
|
||||
# Windows平台
|
||||
ctypes.windll.kernel32.SetProcessWorkingSetSize(-1, -1, -1)
|
||||
except:
|
||||
pass
|
||||
|
||||
print("[GPU清理] [OK] 系统内存清理完成")
|
||||
|
||||
# ==================== 步骤3:显示当前状态 ====================
|
||||
print("[GPU清理] [步骤3] 检查GPU状态...")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
"nvidia-smi --query-gpu=index,memory.used,memory.free,memory.total --format=csv,noheader",
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
print("[GPU清理] ========== GPU 显存状态 ==========")
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
if line.strip():
|
||||
print("[GPU清理] {}".format(line.strip()))
|
||||
except:
|
||||
pass
|
||||
|
||||
# ==================== 步骤4:清理所有Python进程(包括孤儿进程) ====================
|
||||
print("[GPU清理] [步骤4] 检查并清理所有Python进程...")
|
||||
|
||||
# 获取所有Python进程(包括GPU和非GPU进程)
|
||||
all_python_processes = get_all_python_processes()
|
||||
|
||||
if all_python_processes:
|
||||
print("[GPU清理] 检测到 {} 个Python进程(包括AI任务和孤儿进程)".format(len(all_python_processes)))
|
||||
|
||||
# 过滤需要清理的进程
|
||||
print("[GPU清理] [智能过滤] 正在识别需要清理的进程...")
|
||||
processes_to_kill = []
|
||||
skipped_count = 0
|
||||
|
||||
for proc in all_python_processes:
|
||||
if should_kill_process(proc['pid'], model_paths):
|
||||
processes_to_kill.append(proc)
|
||||
print("[GPU清理] PID={}, 将被清理 (AI模型进程)".format(proc['pid']))
|
||||
else:
|
||||
skipped_count += 1
|
||||
|
||||
print("[GPU清理] 识别完成: {} 个进程将被清理, {} 个进程已跳过".format(
|
||||
len(processes_to_kill), skipped_count
|
||||
))
|
||||
|
||||
# 清理识别出的AI模型进程
|
||||
if processes_to_kill:
|
||||
print("[GPU清理] [智能清理] 开始清理AI模型进程...")
|
||||
killed_count = 0
|
||||
failed_pids = []
|
||||
|
||||
for proc in processes_to_kill:
|
||||
try:
|
||||
# 使用 PowerShell 强制杀死进程(更可靠)
|
||||
result = subprocess.run(
|
||||
'powershell -Command "Stop-Process -Id {} -Force"'.format(proc['pid']),
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
timeout=3
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
print("[GPU清理] [OK] 已清理进程 PID={}".format(proc['pid']))
|
||||
killed_count += 1
|
||||
else:
|
||||
failed_pids.append(proc['pid'])
|
||||
print("[GPU清理] [FAIL] 清理进程 PID={} 失败".format(proc['pid']))
|
||||
|
||||
time.sleep(0.2)
|
||||
except Exception as e:
|
||||
failed_pids.append(proc['pid'])
|
||||
print("[GPU清理] [ERROR] 清理进程PID={} 异常: {}".format(proc['pid'], str(e)))
|
||||
|
||||
print("[GPU清理] [完成] 成功清理 {} 个进程, 失败 {} 个".format(killed_count, len(failed_pids)))
|
||||
else:
|
||||
print("[GPU清理] [OK] 没有需要清理的AI模型进程")
|
||||
|
||||
# 清理后再次检查
|
||||
time.sleep(1)
|
||||
remaining = get_all_python_processes()
|
||||
remaining_ai_processes = [p for p in remaining if should_kill_process(p['pid'], model_paths)]
|
||||
|
||||
if not remaining_ai_processes:
|
||||
print("[GPU清理] [OK] AI模型进程已清理完成")
|
||||
else:
|
||||
print("[GPU清理] [提示] 仍有 {} 个AI模型进程存在".format(len(remaining_ai_processes)))
|
||||
for proc in remaining_ai_processes:
|
||||
print("[GPU清理] 残留进程 PID={}".format(proc['pid']))
|
||||
else:
|
||||
print("[GPU清理] [OK] 没有检测到Python进程")
|
||||
|
||||
# ==================== 步骤5:最终GPU显存清理 ====================
|
||||
print("[GPU清理] [步骤5] 最终GPU显存清理...")
|
||||
try:
|
||||
import torch
|
||||
if torch.cuda.is_available():
|
||||
# 进程清理后,再次清理GPU缓存
|
||||
for i in range(3):
|
||||
torch.cuda.empty_cache()
|
||||
gc.collect()
|
||||
time.sleep(0.1)
|
||||
|
||||
# 显示最终状态
|
||||
final_allocated = torch.cuda.memory_allocated() / 1024**3
|
||||
final_reserved = torch.cuda.memory_reserved() / 1024**3
|
||||
print("[GPU清理] 最终状态: 已分配={:.2f}GB, 已保留={:.2f}GB".format(final_allocated, final_reserved))
|
||||
print("[GPU清理] [OK] 最终清理完成")
|
||||
else:
|
||||
print("[GPU清理] CUDA不可用")
|
||||
except:
|
||||
pass
|
||||
|
||||
print("[GPU清理] ========== GPU和内存清理完成 ==========\n")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print("[GPU清理] [ERROR] 清理失败: {}".format(str(e)))
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 解析命令行参数获取模型路径
|
||||
model_paths = []
|
||||
if len(sys.argv) > 1:
|
||||
# 从命令行参数获取模型路径(以逗号分隔)
|
||||
paths_arg = sys.argv[1]
|
||||
if paths_arg and paths_arg.strip():
|
||||
model_paths = [p.strip() for p in paths_arg.split(',') if p.strip()]
|
||||
|
||||
if model_paths:
|
||||
print("[GPU清理] [信息] 路径过滤模式:只清理指定模型路径下的Python进程")
|
||||
print("[GPU清理] [信息] 受保护的应用:直播伴侣、OBS、IDE等不会被清理")
|
||||
print("[GPU清理] [信息] 模型路径数: {}\n".format(len(model_paths)))
|
||||
else:
|
||||
print("[GPU清理] [警告] 未提供模型路径参数!")
|
||||
print("[GPU清理] [说明] 脚本仅在提供模型路径时才进行进程清理")
|
||||
print("[GPU清理] [说明] 用法: python cleanup_gpu.py \"path1,path2,path3\"")
|
||||
print("[GPU清理] [信息] 将执行GPU显存和系统内存清理(不清理任何进程)\n")
|
||||
|
||||
cleanup_gpu(force_kill=True, model_paths=model_paths)
|
||||
sys.exit(0)
|
||||
@@ -0,0 +1,137 @@
|
||||
const fs = require("node:fs");
|
||||
const {resolve, join} = require("node:path");
|
||||
const crypto = require("node:crypto");
|
||||
|
||||
const dir = (p) => {
|
||||
p = p || ''
|
||||
return join(__dirname, "../" + p)
|
||||
}
|
||||
|
||||
const distReleaseDir = (p) => {
|
||||
if (p) {
|
||||
return dir("dist-release/" + p)
|
||||
} else {
|
||||
return dir("dist-release")
|
||||
}
|
||||
}
|
||||
|
||||
function calcSha256File(filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const hash = crypto.createHash("sha256");
|
||||
const stream = fs.createReadStream(filePath);
|
||||
stream.on("data", (data) => hash.update(data));
|
||||
stream.on("end", () => resolve(hash.digest("hex")));
|
||||
stream.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const platformName = () => {
|
||||
switch (process.platform) {
|
||||
case "darwin":
|
||||
return "osx";
|
||||
case "win32":
|
||||
return "win";
|
||||
case "linux":
|
||||
return "linux";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const platformArch = () => {
|
||||
switch (process.arch) {
|
||||
case "x64":
|
||||
return "x86";
|
||||
case "arm64":
|
||||
return "arm64";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const listFiles = (dir, recursive, regex) => {
|
||||
regex = regex || null
|
||||
recursive = recursive || false
|
||||
const files = fs.readdirSync(dir);
|
||||
const list = [];
|
||||
for (let f of files) {
|
||||
const p = resolve(dir, f);
|
||||
if (regex) {
|
||||
if (!regex.test(p)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const stat = fs.statSync(p);
|
||||
list.push({
|
||||
isDir: stat.isDirectory(),
|
||||
name: f,
|
||||
path: p
|
||||
});
|
||||
if (recursive && stat.isDirectory()) {
|
||||
list.push(...listFiles(p, recursive));
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
const copy = (src, dest, print) => {
|
||||
print = print || false
|
||||
if (!fs.existsSync(src)) {
|
||||
console.warn(`Source path does not exist: ${src}`);
|
||||
return;
|
||||
}
|
||||
if (fs.statSync(src).isDirectory()) {
|
||||
fs.mkdirSync(dest, {recursive: true});
|
||||
const files = fs.readdirSync(src);
|
||||
for (const file of files) {
|
||||
copy(join(src, file), join(dest, file));
|
||||
}
|
||||
} else {
|
||||
if (print) {
|
||||
console.log(`Copying file from ${src} to ${dest}`);
|
||||
}
|
||||
fs.copyFileSync(src, dest);
|
||||
}
|
||||
}
|
||||
|
||||
const pathResolve = (...args)=>{
|
||||
return resolve(...args)
|
||||
}
|
||||
|
||||
const exists = (p) => {
|
||||
try {
|
||||
return fs.existsSync(p);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function calcSha256() {
|
||||
console.log('calcSha256.start')
|
||||
const results = []
|
||||
const files = listFiles(distReleaseDir(), false, /\.(exe|dmg|AppImage|deb)$/)
|
||||
for (const p of files) {
|
||||
const sha256 = await calcSha256File(p.path);
|
||||
results.push({
|
||||
name: p.name,
|
||||
sha256: sha256
|
||||
})
|
||||
}
|
||||
const target = distReleaseDir(`sha256-${platformName()}-${platformArch()}.yml`)
|
||||
const content = results.map((r) => {
|
||||
return `${r.name}: ${r.sha256}`
|
||||
}).join("\n")
|
||||
fs.writeFileSync(target, content);
|
||||
console.log('calcSha256.end', target, results)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
dir,
|
||||
distReleaseDir,
|
||||
platformName,
|
||||
platformArch,
|
||||
listFiles,
|
||||
copy,
|
||||
pathResolve,
|
||||
exists,
|
||||
calcSha256,
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 读取 C 盘根目录的配置文件
|
||||
const cDriveFile = 'C:\\system-templates.json';
|
||||
const projectFile = path.join(__dirname, '../electron/resources/extra/common/config/system-templates.json');
|
||||
|
||||
console.log('=== 对比 C 盘配置 vs 项目配置 ===\n');
|
||||
|
||||
if (!fs.existsSync(cDriveFile)) {
|
||||
console.log('❌ C 盘根目录下未找到 system-templates.json');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const cDriveConfig = JSON.parse(fs.readFileSync(cDriveFile, 'utf-8'));
|
||||
const projectConfig = JSON.parse(fs.readFileSync(projectFile, 'utf-8'));
|
||||
|
||||
const cTemplates = cDriveConfig.subtitleTemplates || [];
|
||||
const pTemplates = projectConfig.subtitleTemplates || [];
|
||||
|
||||
console.log(`C 盘配置: ${cTemplates.length} 个模板`);
|
||||
console.log(`项目配置: ${pTemplates.length} 个模板\n`);
|
||||
|
||||
// 对比每个模板的标题字幕配置
|
||||
cTemplates.forEach(cTemplate => {
|
||||
const pTemplate = pTemplates.find(t => t.id === cTemplate.id);
|
||||
|
||||
if (!pTemplate) {
|
||||
console.log(`⚠️ 模板 ${cTemplate.name} (${cTemplate.id}) 在项目配置中不存在`);
|
||||
return;
|
||||
}
|
||||
|
||||
const cTitle = cTemplate.config.titleSubtitleConfig;
|
||||
const pTitle = pTemplate.config.titleSubtitleConfig;
|
||||
|
||||
if (!cTitle || !pTitle) {
|
||||
console.log(`⚠️ 模板 ${cTemplate.name} 缺少标题字幕配置`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否相同
|
||||
const cKey = `${cTitle.maxCharsPerLine}-${cTitle.lines.length}-${cTitle.lines.map(l => `${l.text}:${l.fontName}:${l.fontColor}`).join('|')}`;
|
||||
const pKey = `${pTitle.maxCharsPerLine}-${pTitle.lines.length}-${pTitle.lines.map(l => `${l.text}:${l.fontName}:${l.fontColor}`).join('|')}`;
|
||||
|
||||
if (cKey === pKey) {
|
||||
console.log(`✅ 模板 ${cTemplate.name}: 配置相同`);
|
||||
} else {
|
||||
console.log(`❌ 模板 ${cTemplate.name}: 配置不同`);
|
||||
console.log(` C盘: ${cTitle.maxCharsPerLine}字符/行, ${cTitle.lines.length}行`);
|
||||
console.log(` 项目: ${pTitle.maxCharsPerLine}字符/行, ${pTitle.lines.length}行`);
|
||||
cTitle.lines.forEach((l, i) => {
|
||||
const pLine = pTitle.lines[i];
|
||||
if (pLine) {
|
||||
if (l.text !== pLine.text || l.fontName !== pLine.fontName || l.fontColor !== pLine.fontColor) {
|
||||
console.log(` 第${i+1}行:`);
|
||||
console.log(` C盘: "${l.text}" (${l.fontName}, ${l.fontColor})`);
|
||||
console.log(` 项目: "${pLine.text}" (${pLine.fontName}, ${pLine.fontColor})`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 检查关键词组样式
|
||||
console.log('\n=== 关键词组样式对比 ===\n');
|
||||
|
||||
cTemplates.forEach(cTemplate => {
|
||||
const pTemplate = pTemplates.find(t => t.id === cTemplate.id);
|
||||
if (!pTemplate) return;
|
||||
|
||||
const cKeywords = cTemplate.config.keywordGroupsStyles || [];
|
||||
const pKeywords = pTemplate.config.keywordGroupsStyles || [];
|
||||
|
||||
let hasDiff = false;
|
||||
cKeywords.forEach((cKw, idx) => {
|
||||
const pKw = pKeywords[idx];
|
||||
if (!pKw) return;
|
||||
|
||||
if (cKw.groupName === pKw.groupName) {
|
||||
const cFont = cKw.styleOverride?.fontName || 'N/A';
|
||||
const pFont = pKw.styleOverride?.fontName || 'N/A';
|
||||
const cEffect = cKw.effectId || 'N/A';
|
||||
const pEffect = pKw.effectId || 'N/A';
|
||||
|
||||
if (cFont !== pFont || cEffect !== pEffect) {
|
||||
if (!hasDiff) {
|
||||
console.log(`模板 ${cTemplate.name}:`);
|
||||
hasDiff = true;
|
||||
}
|
||||
console.log(` ${cKw.groupName}:`);
|
||||
if (cFont !== pFont) {
|
||||
console.log(` 字体: ${cFont} (C盘) vs ${pFont} (项目)`);
|
||||
}
|
||||
if (cEffect !== pEffect) {
|
||||
console.log(` 特效: ${cEffect} (C盘) vs ${pEffect} (项目)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!hasDiff && cKeywords.length > 0) {
|
||||
console.log(`✅ 模板 ${cTemplate.name}: 关键词组样式相同`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
const fs = require('fs');
|
||||
|
||||
const cDriveFile = 'C:\\system-templates.json';
|
||||
const projectFile = 'electron/resources/extra/common/config/system-templates.json';
|
||||
|
||||
const cConfig = JSON.parse(fs.readFileSync(cDriveFile, 'utf-8'));
|
||||
const pConfig = JSON.parse(fs.readFileSync(projectFile, 'utf-8'));
|
||||
|
||||
const cTemplates = cConfig.subtitleTemplates || [];
|
||||
const pTemplates = pConfig.subtitleTemplates || [];
|
||||
|
||||
console.log('=== C盘配置 vs 项目配置对比 ===\n');
|
||||
|
||||
cTemplates.forEach(cTemplate => {
|
||||
const pTemplate = pTemplates.find(t => t.id === cTemplate.id);
|
||||
if (!pTemplate) {
|
||||
console.log(`模板 ${cTemplate.name}: 在项目配置中不存在`);
|
||||
return;
|
||||
}
|
||||
|
||||
const cTitle = cTemplate.config && cTemplate.config.titleSubtitleConfig;
|
||||
const pTitle = pTemplate.config && pTemplate.config.titleSubtitleConfig;
|
||||
|
||||
if (!cTitle || !pTitle) {
|
||||
console.log(`模板 ${cTemplate.name}: 标题配置缺失 (C盘: ${!!cTitle}, 项目: ${!!pTitle})`);
|
||||
return;
|
||||
}
|
||||
|
||||
const cKey = `${cTitle.maxCharsPerLine}-${cTitle.lines.length}-${cTitle.lines.map(l => l.text).join('|')}`;
|
||||
const pKey = `${pTitle.maxCharsPerLine}-${pTitle.lines.length}-${pTitle.lines.map(l => l.text).join('|')}`;
|
||||
|
||||
if (cKey === pKey) {
|
||||
console.log(`模板 ${cTemplate.name}: 标题配置相同 (${cTitle.maxCharsPerLine}字符/行, ${cTitle.lines.length}行)`);
|
||||
} else {
|
||||
console.log(`模板 ${cTemplate.name}: 标题配置不同`);
|
||||
console.log(` C盘: ${cTitle.maxCharsPerLine}字符/行, ${cTitle.lines.length}行`);
|
||||
console.log(` 项目: ${pTitle.maxCharsPerLine}字符/行, ${pTitle.lines.length}行`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\n=== 相同配置的模板组 (C盘) ===\n');
|
||||
const cGroups = {};
|
||||
cTemplates.forEach(t => {
|
||||
const title = t.config && t.config.titleSubtitleConfig;
|
||||
if (title && title.lines) {
|
||||
const key = `${title.maxCharsPerLine}-${title.lines.length}-${title.lines.map(l => l.text).join('|')}`;
|
||||
if (!cGroups[key]) cGroups[key] = [];
|
||||
cGroups[key].push(t.name);
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(cGroups).forEach(key => {
|
||||
if (cGroups[key].length > 1) {
|
||||
console.log(`模板 ${cGroups[key].join(', ')} 配置相同`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\n=== 相同配置的模板组 (项目) ===\n');
|
||||
const pGroups = {};
|
||||
pTemplates.forEach(t => {
|
||||
const title = t.config && t.config.titleSubtitleConfig;
|
||||
if (title && title.lines) {
|
||||
const key = `${title.maxCharsPerLine}-${title.lines.length}-${title.lines.map(l => l.text).join('|')}`;
|
||||
if (!pGroups[key]) pGroups[key] = [];
|
||||
pGroups[key].push(t.name);
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(pGroups).forEach(key => {
|
||||
if (pGroups[key].length > 1) {
|
||||
console.log(`模板 ${pGroups[key].join(', ')} 配置相同`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 读取历史配置
|
||||
const historicalFile = path.join(__dirname, '../electron/config/default-subtitle-templates-v2.json');
|
||||
const historical = JSON.parse(fs.readFileSync(historicalFile, 'utf-8'));
|
||||
|
||||
// 读取当前配置
|
||||
const currentFile = path.join(__dirname, '../electron/resources/extra/common/config/system-templates.json');
|
||||
const current = JSON.parse(fs.readFileSync(currentFile, 'utf-8'));
|
||||
|
||||
console.log('=== 历史配置 vs 当前配置对比 ===\n');
|
||||
|
||||
const historicalTemplates = historical.subtitleTemplates || [];
|
||||
const currentTemplates = current.subtitleTemplates || [];
|
||||
|
||||
// 按ID匹配模板
|
||||
const historicalMap = {};
|
||||
historicalTemplates.forEach(t => {
|
||||
historicalMap[t.id] = t;
|
||||
});
|
||||
|
||||
const currentMap = {};
|
||||
currentTemplates.forEach(t => {
|
||||
currentMap[t.id] = t;
|
||||
});
|
||||
|
||||
// 对比每个模板
|
||||
['template_system_11', 'template_system_22', 'template_system_33', 'template_system_44',
|
||||
'template_system_55', 'template_system_66', 'template_system_77', 'template_system_88'].forEach(templateId => {
|
||||
const hist = historicalMap[templateId];
|
||||
const curr = currentMap[templateId];
|
||||
|
||||
if (!hist || !curr) {
|
||||
console.log(`模板 ${templateId}: 缺失配置`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`\n=== 模板 ${hist.name} (${templateId}) ===`);
|
||||
|
||||
// 对比关键词组样式
|
||||
console.log('\n关键词组样式差异:');
|
||||
const histKeywords = hist.config.keywordGroupsStyles || [];
|
||||
const currKeywords = curr.config.keywordGroupsStyles || [];
|
||||
|
||||
histKeywords.forEach((histKw, idx) => {
|
||||
const currKw = currKeywords[idx];
|
||||
if (!currKw) return;
|
||||
|
||||
if (histKw.groupName === currKw.groupName) {
|
||||
const histFont = histKw.styleOverride?.fontName || 'N/A';
|
||||
const currFont = currKw.styleOverride?.fontName || 'N/A';
|
||||
const histEffect = histKw.effectId || 'N/A';
|
||||
const currEffect = currKw.effectId || 'N/A';
|
||||
const histColor = histKw.color || 'N/A';
|
||||
const currColor = currKw.color || 'N/A';
|
||||
|
||||
if (histFont !== currFont || histEffect !== currEffect || histColor !== currColor) {
|
||||
console.log(` ${histKw.groupName}:`);
|
||||
if (histFont !== currFont) {
|
||||
console.log(` 字体: ${histFont} -> ${currFont}`);
|
||||
}
|
||||
if (histEffect !== currEffect) {
|
||||
console.log(` 特效: ${histEffect} -> ${currEffect}`);
|
||||
}
|
||||
if (histColor !== currColor) {
|
||||
console.log(` 颜色: ${histColor} -> ${currColor}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 对比标题字幕配置
|
||||
console.log('\n标题字幕配置:');
|
||||
const histTitle = hist.config.titleSubtitleConfig;
|
||||
const currTitle = curr.config.titleSubtitleConfig;
|
||||
|
||||
if (histTitle && currTitle) {
|
||||
console.log(` 每行字符数: ${histTitle.maxCharsPerLine} (历史) vs ${currTitle.maxCharsPerLine} (当前)`);
|
||||
console.log(` 行数: ${histTitle.lines.length} (历史) vs ${currTitle.lines.length} (当前)`);
|
||||
|
||||
if (histTitle.lines.length === currTitle.lines.length) {
|
||||
histTitle.lines.forEach((histLine, idx) => {
|
||||
const currLine = currTitle.lines[idx];
|
||||
if (currLine) {
|
||||
if (histLine.fontName !== currLine.fontName || histLine.fontColor !== currLine.fontColor) {
|
||||
console.log(` 第${idx + 1}行:`);
|
||||
if (histLine.fontName !== currLine.fontName) {
|
||||
console.log(` 字体: ${histLine.fontName} -> ${currLine.fontName}`);
|
||||
}
|
||||
if (histLine.fontColor !== currLine.fontColor) {
|
||||
console.log(` 颜色: ${histLine.fontColor} -> ${currLine.fontColor}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\n\n=== 总结 ===');
|
||||
console.log('历史配置文件中,标题字幕配置:');
|
||||
console.log('- 模板 11, 33, 55, 77: 8字符/行,2行');
|
||||
console.log('- 模板 22, 44, 66, 88: 15字符/行,1行');
|
||||
console.log('\n历史配置文件中,关键词组样式使用 "Noto Serif CJK" 字体');
|
||||
console.log('当前配置文件中,关键词组样式已更新为不同字体');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 读取 JSON 配置文件
|
||||
const jsonFile = path.join(__dirname, '../electron/resources/extra/common/config/system-templates.json');
|
||||
const data = JSON.parse(fs.readFileSync(jsonFile, 'utf-8'));
|
||||
|
||||
console.log('=== 模板标题字幕配置对比 ===\n');
|
||||
|
||||
const templates = data.subtitleTemplates || [];
|
||||
|
||||
templates.forEach(t => {
|
||||
const title = t.config.titleSubtitleConfig;
|
||||
if (title) {
|
||||
console.log(`模板 ${t.name}:`);
|
||||
console.log(` - 每行字符数: ${title.maxCharsPerLine}`);
|
||||
console.log(` - 行数: ${title.lines.length}`);
|
||||
title.lines.forEach((l, i) => {
|
||||
console.log(` - 第${i + 1}行: "${l.text}" (${l.fontName}, ${l.fontColor})`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
});
|
||||
|
||||
// 找出相同的模板
|
||||
console.log('\n=== 相同配置的模板组 ===\n');
|
||||
|
||||
const groups = {};
|
||||
templates.forEach(t => {
|
||||
const title = t.config.titleSubtitleConfig;
|
||||
if (title) {
|
||||
const key = `${title.maxCharsPerLine}-${title.lines.length}-${title.lines.map(l => `${l.text}:${l.fontName}:${l.fontColor}`).join('|')}`;
|
||||
if (!groups[key]) {
|
||||
groups[key] = [];
|
||||
}
|
||||
groups[key].push(t.name);
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(groups).forEach(key => {
|
||||
if (groups[key].length > 1) {
|
||||
console.log(`模板 ${groups[key].join(', ')} 配置相同`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* 复制 Playwright 浏览器到项目打包资源目录
|
||||
* 从开发环境的缓存直接复制到打包目录
|
||||
*/
|
||||
|
||||
async function copyPlaywrightBrowser() {
|
||||
console.log('🚀 开始复制 Playwright 浏览器到打包资源目录\n');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
// 目标目录:项目打包资源
|
||||
const targetDir = path.join(__dirname, '../electron/resources/extra/common/playwright');
|
||||
await fs.ensureDir(targetDir);
|
||||
|
||||
// 源目录:Playwright 的缓存位置(开发环境)
|
||||
const sourcePaths = [
|
||||
// Windows: %APPDATA%/ms-playwright
|
||||
path.join(process.env.APPDATA || '', 'ms-playwright'),
|
||||
// Windows: %LOCALAPPDATA%/ms-playwright
|
||||
path.join(process.env.LOCALAPPDATA || '', 'ms-playwright'),
|
||||
// Mac/Linux: ~/.cache/ms-playwright
|
||||
path.join(process.env.HOME || '', '.cache/ms-playwright'),
|
||||
];
|
||||
|
||||
let sourceDir = null;
|
||||
for (const p of sourcePaths) {
|
||||
if (fs.existsSync(p)) {
|
||||
console.log(`✅ 找到 Playwright 缓存目录: ${p}`);
|
||||
sourceDir = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sourceDir) {
|
||||
console.error('❌ 未找到 Playwright 浏览器缓存目录');
|
||||
console.error('\n请先运行以下命令下载 Playwright 浏览器:');
|
||||
console.error(' npx playwright install chromium');
|
||||
console.error('\n或者运行一次开发环境,Playwright 会自动下载浏览器');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 查找 chromium 浏览器版本
|
||||
console.log('\n🔍 查找 Chromium 浏览器版本...');
|
||||
const files = await fs.readdir(sourceDir);
|
||||
const chromiumDirs = files.filter(f => f.startsWith('chromium-'));
|
||||
|
||||
if (chromiumDirs.length === 0) {
|
||||
console.error('❌ 未找到 Chromium 浏览器');
|
||||
console.error('请先运行: npx playwright install chromium');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`✅ 找到 ${chromiumDirs.length} 个 Chromium 版本:`);
|
||||
chromiumDirs.forEach((dir, index) => {
|
||||
console.log(` ${index + 1}. ${dir}`);
|
||||
});
|
||||
|
||||
// 只复制最新版本以减小打包体积
|
||||
const latestChromium = chromiumDirs.sort().reverse()[0];
|
||||
console.log(`\n📌 只打包最新版本: ${latestChromium}`);
|
||||
console.log(' (其他版本将被跳过以减小打包体积)\n');
|
||||
|
||||
console.log('📋 开始复制浏览器文件...');
|
||||
console.log('⚠️ 这可能需要几分钟,请耐心等待...\n');
|
||||
|
||||
let totalSize = 0;
|
||||
let fileCount = 0;
|
||||
|
||||
for (const chromiumDir of [latestChromium]) {
|
||||
const sourceChromiumPath = path.join(sourceDir, chromiumDir);
|
||||
const targetChromiumPath = path.join(targetDir, chromiumDir);
|
||||
|
||||
console.log(`📦 复制: ${chromiumDir}`);
|
||||
console.log(` 源: ${sourceChromiumPath}`);
|
||||
console.log(` 目标: ${targetChromiumPath}`);
|
||||
|
||||
try {
|
||||
// 复制整个目录
|
||||
await fs.copy(sourceChromiumPath, targetChromiumPath, {
|
||||
overwrite: true,
|
||||
filter: (src) => {
|
||||
// 排除不必要的文件以减小体积
|
||||
const excludePatterns = [
|
||||
'.isolatedStorage',
|
||||
'CrashpadMetrics',
|
||||
'.lock',
|
||||
'SingletonLock',
|
||||
'SingletonSocket',
|
||||
'SingletonCookie'
|
||||
];
|
||||
|
||||
const shouldInclude = !excludePatterns.some(pattern => src.includes(pattern));
|
||||
return shouldInclude;
|
||||
}
|
||||
});
|
||||
|
||||
// 计算大小
|
||||
const size = await getDirSize(targetChromiumPath);
|
||||
totalSize += size;
|
||||
|
||||
const count = await countFiles(targetChromiumPath);
|
||||
fileCount += count;
|
||||
|
||||
console.log(` ✅ 复制完成 (${(size / 1024 / 1024).toFixed(2)} MB, ${count} 个文件)\n`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(` ❌ 复制失败:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('='.repeat(60));
|
||||
console.log('🎉 Playwright 浏览器复制完成!');
|
||||
console.log(`\n📊 统计信息:`);
|
||||
console.log(` 总大小: ${(totalSize / 1024 / 1024).toFixed(2)} MB`);
|
||||
console.log(` 文件数: ${fileCount} 个`);
|
||||
console.log(` 目标目录: ${targetDir}`);
|
||||
|
||||
// 验证浏览器可执行文件
|
||||
console.log('\n🔍 验证浏览器可执行文件...');
|
||||
for (const chromiumDir of chromiumDirs) {
|
||||
const chromePath = path.join(targetDir, chromiumDir, 'chrome-win64/chrome.exe');
|
||||
const chromePathAlt = path.join(targetDir, chromiumDir, 'chrome-win/chrome.exe');
|
||||
|
||||
if (fs.existsSync(chromePath)) {
|
||||
console.log(` ✅ ${chromiumDir}/chrome-win64/chrome.exe`);
|
||||
} else if (fs.existsSync(chromePathAlt)) {
|
||||
console.log(` ✅ ${chromiumDir}/chrome-win/chrome.exe`);
|
||||
} else {
|
||||
console.log(` ❌ ${chromiumDir} - 未找到 chrome.exe`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n='.repeat(60));
|
||||
console.log('✅ 完成!现在可以运行打包命令了:');
|
||||
console.log(' npm run prepare-package # 准备打包资源');
|
||||
console.log(' npm run build:win # 开始打包');
|
||||
console.log('='.repeat(60) + '\n');
|
||||
}
|
||||
|
||||
// 辅助函数:计算目录大小
|
||||
async function getDirSize(dir) {
|
||||
let size = 0;
|
||||
|
||||
async function walk(directory) {
|
||||
try {
|
||||
const files = await fs.readdir(directory);
|
||||
for (const file of files) {
|
||||
const filePath = path.join(directory, file);
|
||||
const stat = await fs.stat(filePath);
|
||||
if (stat.isDirectory()) {
|
||||
await walk(filePath);
|
||||
} else {
|
||||
size += stat.size;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略权限错误
|
||||
}
|
||||
}
|
||||
|
||||
if (await fs.pathExists(dir)) {
|
||||
await walk(dir);
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
// 辅助函数:统计文件数量
|
||||
async function countFiles(dir) {
|
||||
let count = 0;
|
||||
|
||||
async function walk(directory) {
|
||||
try {
|
||||
const files = await fs.readdir(directory);
|
||||
for (const file of files) {
|
||||
const filePath = path.join(directory, file);
|
||||
const stat = await fs.stat(filePath);
|
||||
if (stat.isDirectory()) {
|
||||
await walk(filePath);
|
||||
} else {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略权限错误
|
||||
}
|
||||
}
|
||||
|
||||
if (await fs.pathExists(dir)) {
|
||||
await walk(dir);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
copyPlaywrightBrowser().catch(console.error);
|
||||
@@ -0,0 +1,66 @@
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { spawn, spawnSync } = require("node:child_process");
|
||||
const waitOn = require("wait-on");
|
||||
|
||||
const projectRoot = path.resolve(__dirname, "..");
|
||||
const serverUrl = process.env.VITE_DEV_SERVER_URL || "http://127.0.0.1:3354";
|
||||
const mainBundle = path.join(projectRoot, "dist-electron", "main", "index.js");
|
||||
const preloadBundle = path.join(projectRoot, "dist-electron", "preload", "index.js");
|
||||
|
||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
async function waitForSyntaxReady(filePath, label) {
|
||||
let lastSize = -1;
|
||||
|
||||
for (let attempt = 0; attempt < 240; attempt++) {
|
||||
if (fs.existsSync(filePath)) {
|
||||
const stat = fs.statSync(filePath);
|
||||
if (stat.size > 0 && stat.size === lastSize) {
|
||||
const check = spawnSync(process.execPath, ["--check", filePath], {
|
||||
stdio: "pipe",
|
||||
encoding: "utf8",
|
||||
});
|
||||
if (check.status === 0) {
|
||||
return;
|
||||
}
|
||||
process.stdout.write(
|
||||
`[dev-electron] waiting for valid ${label} bundle: ${check.stderr || check.stdout}\n`
|
||||
);
|
||||
}
|
||||
lastSize = stat.size;
|
||||
}
|
||||
|
||||
await sleep(250);
|
||||
}
|
||||
|
||||
throw new Error(`[dev-electron] timed out waiting for ${label} bundle: ${filePath}`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await waitOn({
|
||||
resources: [serverUrl],
|
||||
timeout: 120000,
|
||||
validateStatus: status => status >= 200 && status < 400,
|
||||
});
|
||||
|
||||
await waitForSyntaxReady(mainBundle, "main");
|
||||
await waitForSyntaxReady(preloadBundle, "preload");
|
||||
|
||||
const electronBinary = require("electron");
|
||||
const child = spawn(electronBinary, ["."], {
|
||||
cwd: projectRoot,
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
child.on("exit", code => {
|
||||
process.exit(code ?? 0);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error("[dev-electron] failed to start Electron");
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
@echo off
|
||||
echo ========================================
|
||||
echo 数字人合成卡死诊断工具
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
echo [1] 检查GPU显存占用...
|
||||
nvidia-smi --query-gpu=index,memory.used,memory.total --format=csv,noheader
|
||||
echo.
|
||||
|
||||
echo [2] 检查Chromium/Chrome进程数量...
|
||||
tasklist | findstr /I "chromium chrome" | find /C "exe"
|
||||
echo 个Chromium相关进程
|
||||
echo.
|
||||
|
||||
echo [3] 检查Python进程数量...
|
||||
tasklist | findstr /I "python" | find /C "exe"
|
||||
echo 个Python进程
|
||||
echo.
|
||||
|
||||
echo [4] 检查占用GPU的进程...
|
||||
nvidia-smi --query-compute-apps=pid,process_name,used_memory --format=csv,noheader
|
||||
echo.
|
||||
|
||||
echo [5] 检查内存占用Top 5进程...
|
||||
powershell "Get-Process | Sort-Object WS -Descending | Select-Object -First 5 | Format-Table Name, @{Label='Memory(MB)';Expression={[int]($_.WS / 1MB)}} -AutoSize"
|
||||
echo.
|
||||
|
||||
echo ========================================
|
||||
echo 诊断完成!
|
||||
echo.
|
||||
echo 如果发现问题:
|
||||
echo - GPU显存^>90%% → 运行 cleanup_gpu.py --force
|
||||
echo - Chromium进程^>5个 → 运行 taskkill /F /IM chromium.exe
|
||||
echo - Python进程^>3个 → 检查是否有残留进程
|
||||
echo ========================================
|
||||
pause
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
数字人处理脚本
|
||||
用于调用 Gradio API 生成数字人视频
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 4:
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"success": False,
|
||||
"error": "参数不足: 需要 api_url, audio_file, video_file",
|
||||
}
|
||||
)
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
api_url = sys.argv[1]
|
||||
audio_file = sys.argv[2]
|
||||
video_file = sys.argv[3]
|
||||
|
||||
try:
|
||||
from gradio_client import Client, handle_file
|
||||
|
||||
client = Client(api_url)
|
||||
result = client.predict(
|
||||
audio_file=handle_file(audio_file),
|
||||
video_file={"video": handle_file(video_file)},
|
||||
api_name="/process_single",
|
||||
)
|
||||
|
||||
if isinstance(result, dict) and result.get("video"):
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"videoPath": result["video"],
|
||||
"subtitles": result.get("subtitles"),
|
||||
}
|
||||
)
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{"success": False, "error": f"未获取到视频结果: {str(result)[:500]}"}
|
||||
)
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
print(json.dumps({"success": False, "error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,320 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const BASE_DIR = path.join(__dirname, '../electron/resources/extra');
|
||||
|
||||
// 下载进度显示
|
||||
function downloadFile(url, destPath, description) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`\n📥 开始下载: ${description}`);
|
||||
console.log(` URL: ${url}`);
|
||||
console.log(` 目标: ${destPath}`);
|
||||
|
||||
const protocol = url.startsWith('https') ? https : http;
|
||||
const file = fs.createWriteStream(destPath);
|
||||
|
||||
protocol.get(url, (response) => {
|
||||
if (response.statusCode === 302 || response.statusCode === 301) {
|
||||
// 处理重定向
|
||||
file.close();
|
||||
fs.unlinkSync(destPath);
|
||||
return downloadFile(response.headers.location, destPath, description)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
}
|
||||
|
||||
const totalSize = parseInt(response.headers['content-length'], 10);
|
||||
let downloadedSize = 0;
|
||||
let lastPercent = 0;
|
||||
|
||||
response.on('data', (chunk) => {
|
||||
downloadedSize += chunk.length;
|
||||
const percent = Math.floor((downloadedSize / totalSize) * 100);
|
||||
if (percent > lastPercent && percent % 10 === 0) {
|
||||
console.log(` 进度: ${percent}% (${(downloadedSize / 1024 / 1024).toFixed(2)} MB / ${(totalSize / 1024 / 1024).toFixed(2)} MB)`);
|
||||
lastPercent = percent;
|
||||
}
|
||||
});
|
||||
|
||||
response.pipe(file);
|
||||
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
console.log(` ✅ 下载完成: ${(downloadedSize / 1024 / 1024).toFixed(2)} MB`);
|
||||
resolve();
|
||||
});
|
||||
|
||||
file.on('error', (err) => {
|
||||
fs.unlinkSync(destPath);
|
||||
reject(err);
|
||||
});
|
||||
}).on('error', (err) => {
|
||||
fs.unlinkSync(destPath);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 解压 ZIP 文件
|
||||
function extractZip(zipPath, destDir) {
|
||||
console.log(`\n📦 解压文件: ${zipPath}`);
|
||||
console.log(` 目标目录: ${destDir}`);
|
||||
|
||||
try {
|
||||
execSync(`powershell -command "Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force"`, {
|
||||
stdio: 'inherit'
|
||||
});
|
||||
console.log(' ✅ 解压完成');
|
||||
} catch (error) {
|
||||
console.error(' ❌ 解压失败:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('🚀 开始下载 ZhenQianBa 打包所需的外部依赖(国内镜像版本)\n');
|
||||
console.log('=' .repeat(60));
|
||||
|
||||
// 1. 下载 Python 3.12 嵌入式版本(使用淘宝镜像)
|
||||
console.log('\n📦 步骤 1/5: 下载 Python 3.12 嵌入式版本(国内镜像)');
|
||||
const pythonDir = path.join(BASE_DIR, 'common/python');
|
||||
await fs.ensureDir(pythonDir);
|
||||
|
||||
if (!fs.existsSync(path.join(pythonDir, 'python.exe'))) {
|
||||
const pythonZip = path.join(BASE_DIR, 'python-3.12.0-embed-amd64.zip');
|
||||
|
||||
// 尝试多个镜像源
|
||||
const mirrors = [
|
||||
'https://registry.npmmirror.com/-/binary/python/3.12.0/python-3.12.0-embed-amd64.zip',
|
||||
'https://npm.taobao.org/mirrors/python/3.12.0/python-3.12.0-embed-amd64.zip',
|
||||
];
|
||||
|
||||
let downloaded = false;
|
||||
for (const mirror of mirrors) {
|
||||
try {
|
||||
console.log(`\n 尝试镜像: ${mirror}`);
|
||||
await downloadFile(mirror, pythonZip, 'Python 3.12 嵌入式版本 (约 15MB)');
|
||||
downloaded = true;
|
||||
break;
|
||||
} catch (error) {
|
||||
console.log(` ❌ 该镜像下载失败,尝试下一个...`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!downloaded) {
|
||||
console.log('\n ❌ 所有镜像下载失败!');
|
||||
console.log(' 请手动下载:');
|
||||
console.log(' 1. 浏览器访问: https://registry.npmmirror.com/binary.html?path=python/');
|
||||
console.log(' 2. 找到 3.12.0/python-3.12.0-embed-amd64.zip');
|
||||
console.log(' 3. 下载后解压到: ' + pythonDir);
|
||||
console.log('');
|
||||
} else {
|
||||
extractZip(pythonZip, pythonDir);
|
||||
fs.unlinkSync(pythonZip);
|
||||
|
||||
console.log('\n 🔧 配置 Python 环境...');
|
||||
|
||||
// 修改 python312._pth 以启用 site-packages
|
||||
const pthFile = path.join(pythonDir, 'python312._pth');
|
||||
if (fs.existsSync(pthFile)) {
|
||||
let content = fs.readFileSync(pthFile, 'utf-8');
|
||||
content = content.replace('#import site', 'import site');
|
||||
if (!content.includes('Lib\\site-packages')) {
|
||||
content = content.trim() + '\nLib\\site-packages\n';
|
||||
}
|
||||
fs.writeFileSync(pthFile, content);
|
||||
console.log(' ✅ python312._pth 配置完成');
|
||||
}
|
||||
|
||||
// 下载 get-pip.py(使用国内镜像)
|
||||
console.log('\n 📥 下载 pip 安装脚本(国内镜像)...');
|
||||
const getPipPath = path.join(pythonDir, 'get-pip.py');
|
||||
|
||||
try {
|
||||
await downloadFile(
|
||||
'https://bootstrap.pypa.io/get-pip.py',
|
||||
getPipPath,
|
||||
'get-pip.py'
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(' ⚠️ 下载失败,使用备用地址...');
|
||||
await downloadFile(
|
||||
'https://registry.npmmirror.com/-/binary/python-get-pip/get-pip.py',
|
||||
getPipPath,
|
||||
'get-pip.py (备用)'
|
||||
);
|
||||
}
|
||||
|
||||
// 安装 pip
|
||||
console.log('\n 🔧 安装 pip...');
|
||||
execSync(`"${path.join(pythonDir, 'python.exe')}" "${getPipPath}"`, {
|
||||
stdio: 'inherit',
|
||||
cwd: pythonDir
|
||||
});
|
||||
|
||||
console.log('\n ✅ Python 环境准备完成');
|
||||
}
|
||||
} else {
|
||||
console.log(' ⏭️ Python 已存在,跳过下载');
|
||||
}
|
||||
|
||||
// 2. 配置 pip 使用国内镜像
|
||||
console.log('\n📦 步骤 2/5: 配置 pip 使用国内镜像');
|
||||
|
||||
const pipConfDir = path.join(pythonDir, 'pip');
|
||||
const pipConfFile = path.join(pipConfDir, 'pip.ini');
|
||||
|
||||
await fs.ensureDir(pipConfDir);
|
||||
|
||||
const pipConfig = `[global]
|
||||
index-url = https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
trusted-host = pypi.tuna.tsinghua.edu.cn
|
||||
`;
|
||||
|
||||
fs.writeFileSync(pipConfFile, pipConfig);
|
||||
console.log(' ✅ pip 配置完成(使用清华镜像)');
|
||||
|
||||
// 3. 安装 Python 依赖
|
||||
console.log('\n📦 步骤 3/5: 安装 Python 依赖(使用清华镜像)');
|
||||
const requirementsPath = path.join(__dirname, '../python/requirements.txt');
|
||||
|
||||
if (fs.existsSync(requirementsPath) && fs.existsSync(path.join(pythonDir, 'python.exe'))) {
|
||||
console.log(' 📋 从 requirements.txt 安装依赖...');
|
||||
console.log(' ⚠️ 这可能需要 10-20 分钟,请耐心等待...\n');
|
||||
|
||||
try {
|
||||
execSync(
|
||||
`"${path.join(pythonDir, 'python.exe')}" -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r "${requirementsPath}" --target="${path.join(pythonDir, 'Lib/site-packages')}"`,
|
||||
{
|
||||
stdio: 'inherit',
|
||||
cwd: pythonDir
|
||||
}
|
||||
);
|
||||
console.log('\n ✅ Python 依赖安装完成');
|
||||
} catch (error) {
|
||||
console.error('\n ❌ Python 依赖安装失败');
|
||||
console.error(' 请手动执行:');
|
||||
console.error(` cd "${pythonDir}"`);
|
||||
console.error(` python.exe -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r "${requirementsPath}" --target="./Lib/site-packages"`);
|
||||
}
|
||||
} else {
|
||||
console.log(' ⚠️ 未找到 requirements.txt 或 Python,跳过依赖安装');
|
||||
}
|
||||
|
||||
// 4. FFmpeg 手动下载提示
|
||||
console.log('\n📦 步骤 4/5: FFmpeg(需要手动下载)');
|
||||
const ffmpegDir = path.join(BASE_DIR, 'win-x86');
|
||||
await fs.ensureDir(ffmpegDir);
|
||||
|
||||
if (!fs.existsSync(path.join(ffmpegDir, 'ffmpeg.exe'))) {
|
||||
console.log('\n ⚠️ FFmpeg 需要手动下载(文件较大 ~100MB)');
|
||||
console.log('\n 方案1 - 使用 GitHub 代理下载:');
|
||||
console.log(' https://ghproxy.com/https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip');
|
||||
console.log('\n 方案2 - 从百度网盘/阿里云盘分享获取(如果有的话)');
|
||||
console.log('\n 下载后:');
|
||||
console.log(' 1. 解压 zip 文件');
|
||||
console.log(' 2. 进入 bin 目录');
|
||||
console.log(' 3. 复制 ffmpeg.exe 和 ffprobe.exe 到:');
|
||||
console.log(` ${ffmpegDir}`);
|
||||
console.log('');
|
||||
} else {
|
||||
console.log(' ⏭️ FFmpeg 已存在,跳过下载');
|
||||
}
|
||||
|
||||
// 5. Chromium
|
||||
console.log('\n📦 步骤 5/5: Chromium 浏览器');
|
||||
const chromiumDir = path.join(BASE_DIR, 'common/chromium');
|
||||
await fs.ensureDir(chromiumDir);
|
||||
|
||||
if (!fs.existsSync(path.join(chromiumDir, 'chrome-win'))) {
|
||||
console.log(' 🔧 尝试从 puppeteer 复制 Chromium...');
|
||||
|
||||
try {
|
||||
const puppeteerPath = path.join(__dirname, '../node_modules/puppeteer');
|
||||
if (!fs.existsSync(puppeteerPath)) {
|
||||
console.log(' ⚠️ 未找到 puppeteer');
|
||||
console.log('\n 请使用淘宝镜像安装 puppeteer:');
|
||||
console.log(' set PUPPETEER_DOWNLOAD_HOST=https://registry.npmmirror.com/-/binary/chromium-browser-snapshots');
|
||||
console.log(' npm install puppeteer');
|
||||
console.log(' 然后重新运行此脚本\n');
|
||||
} else {
|
||||
const localChromium = path.join(puppeteerPath, '.local-chromium');
|
||||
if (fs.existsSync(localChromium)) {
|
||||
const chromiumVersions = fs.readdirSync(localChromium);
|
||||
if (chromiumVersions.length > 0) {
|
||||
const latestVersion = chromiumVersions[0];
|
||||
const chromeSrc = path.join(localChromium, latestVersion, 'chrome-win');
|
||||
|
||||
if (fs.existsSync(chromeSrc)) {
|
||||
console.log(` 📋 复制 Chromium...`);
|
||||
await fs.copy(chromeSrc, path.join(chromiumDir, 'chrome-win'));
|
||||
console.log(' ✅ Chromium 准备完成');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(' ⚠️ puppeteer 未下载 Chromium');
|
||||
console.log(' 解决方案: 运行一次开发环境,Chromium 会自动下载');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(' ❌ Chromium 准备失败:', error.message);
|
||||
}
|
||||
} else {
|
||||
console.log(' ⏭️ Chromium 已存在,跳过');
|
||||
}
|
||||
|
||||
// 6. AI 模型
|
||||
console.log('\n📦 步骤 6/6: AI 模型');
|
||||
const modelsDir = path.join(BASE_DIR, 'common/models');
|
||||
await fs.ensureDir(modelsDir);
|
||||
|
||||
const u2netPath = path.join(modelsDir, 'u2net.onnx');
|
||||
if (!fs.existsSync(u2netPath)) {
|
||||
console.log(' ⚠️ U2-Net 模型需要在安装完 Python 依赖后下载');
|
||||
console.log('\n 请执行以下命令:');
|
||||
console.log(` "${path.join(pythonDir, 'python.exe')}" -c "from rembg import remove, new_session; session = new_session('u2net')"`);
|
||||
console.log('\n 模型会下载到: %USERPROFILE%\\.u2net\\u2net.onnx');
|
||||
console.log(' 然后复制到: ' + u2netPath);
|
||||
console.log('');
|
||||
} else {
|
||||
console.log(' ⏭️ U2-Net 模型已存在');
|
||||
}
|
||||
|
||||
// 最终检查
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('📊 外部依赖下载状态检查:');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
const checks = [
|
||||
{ name: 'Python', path: path.join(pythonDir, 'python.exe') },
|
||||
{ name: 'pip', path: path.join(pythonDir, 'Scripts/pip.exe') },
|
||||
{ name: 'FFmpeg', path: path.join(ffmpegDir, 'ffmpeg.exe') },
|
||||
{ name: 'FFprobe', path: path.join(ffmpegDir, 'ffprobe.exe') },
|
||||
{ name: 'Chromium', path: path.join(chromiumDir, 'chrome-win/chrome.exe') },
|
||||
{ name: 'U2-Net 模型', path: u2netPath }
|
||||
];
|
||||
|
||||
let allComplete = true;
|
||||
for (const check of checks) {
|
||||
const exists = fs.existsSync(check.path);
|
||||
console.log(`${exists ? '✅' : '❌'} ${check.name}: ${exists ? '已准备' : '缺失'}`);
|
||||
if (!exists) allComplete = false;
|
||||
}
|
||||
|
||||
console.log('\n' + '='.repeat(60));
|
||||
if (allComplete) {
|
||||
console.log('🎉 所有外部依赖准备完成!');
|
||||
console.log('\n下一步:');
|
||||
console.log('1. 运行: npm run prepare-package (复制项目资源)');
|
||||
console.log('2. 运行: npm run build:win (开始打包)');
|
||||
} else {
|
||||
console.log('⚠️ 部分依赖需要手动准备');
|
||||
console.log('请查看上面的提示信息完成剩余步骤');
|
||||
}
|
||||
console.log('='.repeat(60) + '\n');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,274 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const BASE_DIR = path.join(__dirname, '../electron/resources/extra');
|
||||
|
||||
// 下载进度显示
|
||||
function downloadFile(url, destPath, description) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`\n📥 开始下载: ${description}`);
|
||||
console.log(` URL: ${url}`);
|
||||
console.log(` 目标: ${destPath}`);
|
||||
|
||||
const protocol = url.startsWith('https') ? https : http;
|
||||
const file = fs.createWriteStream(destPath);
|
||||
|
||||
protocol.get(url, (response) => {
|
||||
if (response.statusCode === 302 || response.statusCode === 301) {
|
||||
// 处理重定向
|
||||
file.close();
|
||||
fs.unlinkSync(destPath);
|
||||
return downloadFile(response.headers.location, destPath, description)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
}
|
||||
|
||||
const totalSize = parseInt(response.headers['content-length'], 10);
|
||||
let downloadedSize = 0;
|
||||
let lastPercent = 0;
|
||||
|
||||
response.on('data', (chunk) => {
|
||||
downloadedSize += chunk.length;
|
||||
const percent = Math.floor((downloadedSize / totalSize) * 100);
|
||||
if (percent > lastPercent && percent % 10 === 0) {
|
||||
console.log(` 进度: ${percent}% (${(downloadedSize / 1024 / 1024).toFixed(2)} MB / ${(totalSize / 1024 / 1024).toFixed(2)} MB)`);
|
||||
lastPercent = percent;
|
||||
}
|
||||
});
|
||||
|
||||
response.pipe(file);
|
||||
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
console.log(` ✅ 下载完成: ${(downloadedSize / 1024 / 1024).toFixed(2)} MB`);
|
||||
resolve();
|
||||
});
|
||||
|
||||
file.on('error', (err) => {
|
||||
fs.unlinkSync(destPath);
|
||||
reject(err);
|
||||
});
|
||||
}).on('error', (err) => {
|
||||
fs.unlinkSync(destPath);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 解压 ZIP 文件
|
||||
function extractZip(zipPath, destDir) {
|
||||
console.log(`\n📦 解压文件: ${zipPath}`);
|
||||
console.log(` 目标目录: ${destDir}`);
|
||||
|
||||
try {
|
||||
// 使用 PowerShell 解压(Windows 内置)
|
||||
execSync(`powershell -command "Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force"`, {
|
||||
stdio: 'inherit'
|
||||
});
|
||||
console.log(' ✅ 解压完成');
|
||||
} catch (error) {
|
||||
console.error(' ❌ 解压失败:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('🚀 开始下载 ZhenQianBa 打包所需的外部依赖\n');
|
||||
console.log('=' .repeat(60));
|
||||
|
||||
// 1. 下载 Python 3.12 嵌入式版本
|
||||
console.log('\n📦 步骤 1/4: 下载 Python 3.12 嵌入式版本');
|
||||
const pythonDir = path.join(BASE_DIR, 'common/python');
|
||||
await fs.ensureDir(pythonDir);
|
||||
|
||||
if (!fs.existsSync(path.join(pythonDir, 'python.exe'))) {
|
||||
const pythonZip = path.join(BASE_DIR, 'python-3.12.0-embed-amd64.zip');
|
||||
|
||||
await downloadFile(
|
||||
'https://www.python.org/ftp/python/3.12.0/python-3.12.0-embed-amd64.zip',
|
||||
pythonZip,
|
||||
'Python 3.12 嵌入式版本 (约 15MB)'
|
||||
);
|
||||
|
||||
extractZip(pythonZip, pythonDir);
|
||||
fs.unlinkSync(pythonZip);
|
||||
|
||||
console.log('\n 🔧 配置 Python 环境...');
|
||||
|
||||
// 修改 python312._pth 以启用 site-packages
|
||||
const pthFile = path.join(pythonDir, 'python312._pth');
|
||||
if (fs.existsSync(pthFile)) {
|
||||
let content = fs.readFileSync(pthFile, 'utf-8');
|
||||
content = content.replace('#import site', 'import site');
|
||||
if (!content.includes('Lib\\site-packages')) {
|
||||
content = content.trim() + '\nLib\\site-packages\n';
|
||||
}
|
||||
fs.writeFileSync(pthFile, content);
|
||||
console.log(' ✅ python312._pth 配置完成');
|
||||
}
|
||||
|
||||
// 下载 get-pip.py
|
||||
console.log('\n 📥 下载 pip 安装脚本...');
|
||||
const getPipPath = path.join(pythonDir, 'get-pip.py');
|
||||
await downloadFile(
|
||||
'https://bootstrap.pypa.io/get-pip.py',
|
||||
getPipPath,
|
||||
'get-pip.py'
|
||||
);
|
||||
|
||||
// 安装 pip
|
||||
console.log('\n 🔧 安装 pip...');
|
||||
execSync(`"${path.join(pythonDir, 'python.exe')}" "${getPipPath}"`, {
|
||||
stdio: 'inherit',
|
||||
cwd: pythonDir
|
||||
});
|
||||
|
||||
console.log('\n ✅ Python 环境准备完成');
|
||||
} else {
|
||||
console.log(' ⏭️ Python 已存在,跳过下载');
|
||||
}
|
||||
|
||||
// 2. 安装 Python 依赖
|
||||
console.log('\n📦 步骤 2/4: 安装 Python 依赖');
|
||||
const requirementsPath = path.join(__dirname, '../python/requirements.txt');
|
||||
|
||||
if (fs.existsSync(requirementsPath)) {
|
||||
console.log(' 📋 从 requirements.txt 安装依赖...');
|
||||
console.log(' ⚠️ 这可能需要 5-10 分钟,请耐心等待...\n');
|
||||
|
||||
try {
|
||||
execSync(
|
||||
`"${path.join(pythonDir, 'python.exe')}" -m pip install -r "${requirementsPath}" --target="${path.join(pythonDir, 'Lib/site-packages')}"`,
|
||||
{
|
||||
stdio: 'inherit',
|
||||
cwd: pythonDir
|
||||
}
|
||||
);
|
||||
console.log('\n ✅ Python 依赖安装完成');
|
||||
} catch (error) {
|
||||
console.error('\n ❌ Python 依赖安装失败');
|
||||
console.error(' 请手动执行:');
|
||||
console.error(` cd "${pythonDir}"`);
|
||||
console.error(` python.exe -m pip install -r "${requirementsPath}" --target="./Lib/site-packages"`);
|
||||
}
|
||||
} else {
|
||||
console.log(' ⚠️ 未找到 requirements.txt,跳过依赖安装');
|
||||
}
|
||||
|
||||
// 3. 下载 FFmpeg
|
||||
console.log('\n📦 步骤 3/4: 下载 FFmpeg');
|
||||
const ffmpegDir = path.join(BASE_DIR, 'win-x86');
|
||||
await fs.ensureDir(ffmpegDir);
|
||||
|
||||
if (!fs.existsSync(path.join(ffmpegDir, 'ffmpeg.exe'))) {
|
||||
console.log('\n ⚠️ FFmpeg 需要手动下载');
|
||||
console.log(' 原因: GitHub Releases 文件太大,需要浏览器下载');
|
||||
console.log('\n 请按照以下步骤操作:');
|
||||
console.log(' 1. 打开浏览器访问: https://github.com/BtbN/FFmpeg-Builds/releases');
|
||||
console.log(' 2. 下载: ffmpeg-master-latest-win64-gpl.zip');
|
||||
console.log(' 3. 解压后,将 bin 目录下的文件复制到:');
|
||||
console.log(` ${ffmpegDir}`);
|
||||
console.log(' 4. 确保包含: ffmpeg.exe 和 ffprobe.exe\n');
|
||||
} else {
|
||||
console.log(' ⏭️ FFmpeg 已存在,跳过下载');
|
||||
}
|
||||
|
||||
// 4. 下载 Chromium (通过 puppeteer)
|
||||
console.log('\n📦 步骤 4/4: 准备 Chromium 浏览器');
|
||||
const chromiumDir = path.join(BASE_DIR, 'common/chromium');
|
||||
await fs.ensureDir(chromiumDir);
|
||||
|
||||
if (!fs.existsSync(path.join(chromiumDir, 'chrome-win'))) {
|
||||
console.log(' 🔧 通过 puppeteer 下载 Chromium...');
|
||||
console.log(' ⚠️ 这可能需要几分钟...\n');
|
||||
|
||||
try {
|
||||
// 检查 puppeteer 是否已安装
|
||||
const puppeteerPath = path.join(__dirname, '../node_modules/puppeteer');
|
||||
if (!fs.existsSync(puppeteerPath)) {
|
||||
console.log(' ❌ 未找到 puppeteer,请先运行: npm install');
|
||||
} else {
|
||||
// 查找 puppeteer 下载的 Chromium
|
||||
const localChromium = path.join(puppeteerPath, '.local-chromium');
|
||||
if (fs.existsSync(localChromium)) {
|
||||
// 复制 Chromium
|
||||
const chromiumVersions = fs.readdirSync(localChromium);
|
||||
if (chromiumVersions.length > 0) {
|
||||
const latestVersion = chromiumVersions[0];
|
||||
const chromeSrc = path.join(localChromium, latestVersion, 'chrome-win');
|
||||
|
||||
if (fs.existsSync(chromeSrc)) {
|
||||
console.log(` 📋 复制 Chromium 从 puppeteer...`);
|
||||
await fs.copy(chromeSrc, path.join(chromiumDir, 'chrome-win'));
|
||||
console.log(' ✅ Chromium 准备完成');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(' ⚠️ puppeteer 未下载 Chromium');
|
||||
console.log(' 解决方案: 运行一次开发环境,Chromium 会自动下载');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(' ❌ Chromium 准备失败:', error.message);
|
||||
}
|
||||
} else {
|
||||
console.log(' ⏭️ Chromium 已存在,跳过下载');
|
||||
}
|
||||
|
||||
// 5. 下载 AI 模型
|
||||
console.log('\n📦 步骤 5/5: 下载 AI 模型');
|
||||
const modelsDir = path.join(BASE_DIR, 'common/models');
|
||||
await fs.ensureDir(modelsDir);
|
||||
|
||||
const u2netPath = path.join(modelsDir, 'u2net.onnx');
|
||||
if (!fs.existsSync(u2netPath)) {
|
||||
console.log(' ⚠️ U2-Net 模型需要通过 rembg 触发下载');
|
||||
console.log('\n 请按照以下步骤操作:');
|
||||
console.log(' 1. 确保上面的 Python 依赖已安装完成');
|
||||
console.log(' 2. 运行以下命令触发模型下载:');
|
||||
console.log(` "${path.join(pythonDir, 'python.exe')}" -c "from rembg import remove, new_session; session = new_session('u2net')"`);
|
||||
console.log(' 3. 模型会下载到用户目录,然后复制到:');
|
||||
console.log(` ${u2netPath}`);
|
||||
console.log(' 4. Windows 模型位置通常在: %USERPROFILE%\\.u2net\\u2net.onnx\n');
|
||||
} else {
|
||||
console.log(' ⏭️ U2-Net 模型已存在,跳过下载');
|
||||
}
|
||||
|
||||
// 最终检查
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('📊 外部依赖下载状态检查:');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
const checks = [
|
||||
{ name: 'Python', path: path.join(pythonDir, 'python.exe') },
|
||||
{ name: 'pip', path: path.join(pythonDir, 'Scripts/pip.exe') },
|
||||
{ name: 'FFmpeg', path: path.join(ffmpegDir, 'ffmpeg.exe') },
|
||||
{ name: 'FFprobe', path: path.join(ffmpegDir, 'ffprobe.exe') },
|
||||
{ name: 'Chromium', path: path.join(chromiumDir, 'chrome-win/chrome.exe') },
|
||||
{ name: 'U2-Net 模型', path: u2netPath }
|
||||
];
|
||||
|
||||
let allComplete = true;
|
||||
for (const check of checks) {
|
||||
const exists = fs.existsSync(check.path);
|
||||
console.log(`${exists ? '✅' : '❌'} ${check.name}: ${exists ? '已准备' : '缺失'}`);
|
||||
if (!exists) allComplete = false;
|
||||
}
|
||||
|
||||
console.log('\n' + '='.repeat(60));
|
||||
if (allComplete) {
|
||||
console.log('🎉 所有外部依赖准备完成!');
|
||||
console.log('\n下一步:');
|
||||
console.log('1. 运行: npm run prepare-package (复制项目资源)');
|
||||
console.log('2. 运行: npm run build:win (开始打包)');
|
||||
} else {
|
||||
console.log('⚠️ 部分依赖需要手动准备');
|
||||
console.log('请查看上面的提示信息完成剩余步骤');
|
||||
}
|
||||
console.log('='.repeat(60) + '\n');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,60 @@
|
||||
import json
|
||||
import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main() -> int:
|
||||
repo_root = Path(__file__).resolve().parent.parent
|
||||
db_path = repo_root / "data" / "database.db"
|
||||
out_path = repo_root / "build" / "runtime-system-subtitle-templates.json"
|
||||
|
||||
if not db_path.exists():
|
||||
print(f"[export-runtime-system-templates] database not found: {db_path}")
|
||||
return 0
|
||||
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT id, name, description, config, created_at
|
||||
FROM subtitle_templates
|
||||
WHERE is_system = 1
|
||||
ORDER BY created_at ASC
|
||||
"""
|
||||
).fetchall()
|
||||
|
||||
templates = []
|
||||
for row in rows:
|
||||
config = row[3]
|
||||
templates.append(
|
||||
{
|
||||
"id": row[0],
|
||||
"name": row[1],
|
||||
"description": row[2],
|
||||
"config": json.loads(config) if isinstance(config, str) else config,
|
||||
"createdAt": row[4],
|
||||
"isSystem": True,
|
||||
"is_system": 1,
|
||||
}
|
||||
)
|
||||
|
||||
if len(templates) != 16:
|
||||
print(
|
||||
f"[export-runtime-system-templates] expected 16 system templates, got {len(templates)}; skipping export"
|
||||
)
|
||||
return 0
|
||||
|
||||
out_path.write_text(json.dumps(templates, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
print(
|
||||
f"[export-runtime-system-templates] exported {len(templates)} templates to {out_path}"
|
||||
)
|
||||
return 0
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 导出系统模板配置脚本
|
||||
* 从数据库导出所有系统模板配置到 system-templates.json
|
||||
*
|
||||
* 使用方法:
|
||||
* node scripts/export-system-templates.js
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const Database = require('better-sqlite3');
|
||||
|
||||
async function exportSystemTemplates() {
|
||||
console.log('📦 开始导出系统模板配置...\n');
|
||||
|
||||
try {
|
||||
// 确定数据库路径
|
||||
let dbPath;
|
||||
const possiblePaths = [
|
||||
path.join(__dirname, '../userData/data.db'),
|
||||
path.join(__dirname, '../data.db'),
|
||||
path.join(process.env.APPDATA || process.env.HOME, 'zhenqianba/data.db'),
|
||||
];
|
||||
|
||||
for (const tryPath of possiblePaths) {
|
||||
if (fs.existsSync(tryPath)) {
|
||||
dbPath = tryPath;
|
||||
console.log(`✅ 找到数据库: ${dbPath}\n`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!dbPath) {
|
||||
console.error('❌ 未找到数据库文件,请确保应用已启动过一次');
|
||||
console.error('尝试的路径:', possiblePaths);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 打开数据库
|
||||
const db = new Database(dbPath, { readonly: true });
|
||||
|
||||
// 导出字幕模板
|
||||
console.log('1️⃣ 导出字幕模板...');
|
||||
const subtitleTemplates = db.prepare(`
|
||||
SELECT id, name, description, config, is_system, readonly, created_at, updated_at
|
||||
FROM subtitle_templates
|
||||
WHERE is_system = 1
|
||||
ORDER BY created_at ASC
|
||||
`).all();
|
||||
|
||||
const subtitleTemplatesFormatted = subtitleTemplates.map(t => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
isSystem: Boolean(t.is_system),
|
||||
readonly: Boolean(t.readonly),
|
||||
createdAt: t.created_at,
|
||||
config: typeof t.config === 'string' ? JSON.parse(t.config) : t.config
|
||||
}));
|
||||
|
||||
console.log(` ✅ 导出 ${subtitleTemplatesFormatted.length} 个字幕模板`);
|
||||
|
||||
// 导出封面模板
|
||||
console.log('\n2️⃣ 导出封面模板...');
|
||||
const coverTemplates = db.prepare(`
|
||||
SELECT id, name, description, config, thumbnail_path, is_system, readonly, created_at, updated_at
|
||||
FROM cover_templates
|
||||
WHERE is_system = 1
|
||||
ORDER BY created_at ASC
|
||||
`).all();
|
||||
|
||||
const coverTemplatesFormatted = coverTemplates.map(t => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
is_system: t.is_system,
|
||||
readonly: Boolean(t.readonly),
|
||||
config: typeof t.config === 'string' ? JSON.parse(t.config) : t.config,
|
||||
thumbnailPath: t.thumbnail_path,
|
||||
createdAt: t.created_at
|
||||
}));
|
||||
|
||||
console.log(` ✅ 导出 ${coverTemplatesFormatted.length} 个封面模板`);
|
||||
|
||||
// 生成配置对象
|
||||
const systemTemplatesConfig = {
|
||||
version: "1.0.0",
|
||||
description: "系统内置模板配置 - 包含8套字幕模板和8套封面模板(开发模式可编辑,生产模式只读)",
|
||||
timestamp: new Date().toISOString(),
|
||||
subtitleTemplates: subtitleTemplatesFormatted,
|
||||
coverTemplates: coverTemplatesFormatted
|
||||
};
|
||||
|
||||
// 保存到文件
|
||||
const configPath = path.join(__dirname, '../electron/config/system-templates.json');
|
||||
fs.writeFileSync(
|
||||
configPath,
|
||||
JSON.stringify(systemTemplatesConfig, null, 2),
|
||||
'utf-8'
|
||||
);
|
||||
|
||||
console.log(`\n✅ 配置已保存到: ${configPath}`);
|
||||
console.log(` - 字幕模板: ${subtitleTemplatesFormatted.length} 套`);
|
||||
console.log(` - 封面模板: ${coverTemplatesFormatted.length} 套`);
|
||||
console.log(` - 文件大小: ${(fs.statSync(configPath).size / 1024).toFixed(2)} KB`);
|
||||
|
||||
db.close();
|
||||
console.log('\n✨ 导出完成!');
|
||||
process.exit(0);
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ 导出失败:', error.message);
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
exportSystemTemplates();
|
||||
@@ -0,0 +1,92 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
console.log('=== 查找8个模板的不同配置(普通字幕字体、特效字幕字体、颜色都不同)===\n');
|
||||
|
||||
// 检查所有可能的配置文件
|
||||
const filesToCheck = [
|
||||
{ name: 'default-ipagent-config.json', path: 'electron/config/default-ipagent-config.json' },
|
||||
{ name: 'default-ipagent-config.json (resources)', path: 'electron/resources/extra/common/config/default-ipagent-config.json' },
|
||||
{ name: 'default-subtitle-templates.json', path: 'electron/config/default-subtitle-templates.json' },
|
||||
{ name: 'C盘配置', path: 'C:\\system-templates.json' },
|
||||
];
|
||||
|
||||
filesToCheck.forEach(fileInfo => {
|
||||
if (!fs.existsSync(fileInfo.path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(fileInfo.path, 'utf-8'));
|
||||
|
||||
if (data.subtitleTemplates) {
|
||||
const templates = data.subtitleTemplates.filter(t =>
|
||||
t.id && t.id.match(/template_system_(11|22|33|44|55|66|77|88)/)
|
||||
);
|
||||
|
||||
if (templates.length === 8) {
|
||||
console.log(`\n✅ 找到包含8个模板的配置文件: ${fileInfo.name}`);
|
||||
console.log(` 路径: ${fileInfo.path}\n`);
|
||||
|
||||
// 检查每个模板的配置是否不同
|
||||
const configs = {};
|
||||
let allDifferent = true;
|
||||
|
||||
templates.forEach(t => {
|
||||
const templateNum = t.name;
|
||||
|
||||
// 普通字幕字体和颜色
|
||||
const normalFont = t.config?.subtitleStyle?.fontName ||
|
||||
(t.config?.subtitleStyleId ? `ID: ${t.config.subtitleStyleId}` : 'N/A');
|
||||
const normalColor = t.config?.subtitleStyle?.fontColor || 'N/A';
|
||||
|
||||
// 特效字幕字体和颜色(取第一个关键词组的配置作为代表)
|
||||
const effectFont = t.config?.keywordGroupsStyles?.[0]?.styleOverride?.fontName ||
|
||||
t.config?.keywordGroups?.[0]?.styleOverride?.fontName || 'N/A';
|
||||
const effectColor = t.config?.keywordGroupsStyles?.[0]?.color ||
|
||||
t.config?.keywordGroups?.[0]?.color || 'N/A';
|
||||
|
||||
const key = `${normalFont}-${normalColor}-${effectFont}-${effectColor}`;
|
||||
|
||||
if (configs[key]) {
|
||||
allDifferent = false;
|
||||
console.log(` ❌ 模板 ${templateNum} 与模板 ${configs[key]} 配置相同`);
|
||||
} else {
|
||||
configs[key] = templateNum;
|
||||
}
|
||||
|
||||
console.log(` 模板 ${templateNum}:`);
|
||||
console.log(` 普通字幕: 字体=${normalFont}, 颜色=${normalColor}`);
|
||||
console.log(` 特效字幕: 字体=${effectFont}, 颜色=${effectColor}`);
|
||||
});
|
||||
|
||||
if (allDifferent) {
|
||||
console.log(`\n ✅ 所有8个模板的配置都不同!`);
|
||||
console.log(`\n 这就是你要找的配置文件: ${fileInfo.path}`);
|
||||
} else {
|
||||
console.log(`\n ⚠️ 部分模板配置相同`);
|
||||
}
|
||||
|
||||
console.log('\n' + '='.repeat(60) + '\n');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略错误
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\n检查完成。如果找到8个不同配置的模板,会在上面显示。');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
console.log('=== 查找8个模板的不同配置 ===\n');
|
||||
|
||||
// 检查所有可能的配置文件
|
||||
const filesToCheck = [
|
||||
{ name: 'default-templates.json (subtitleStyles)', path: 'electron/resources/extra/common/config/default-templates.json' },
|
||||
{ name: 'default-subtitle-templates.json', path: 'electron/config/default-subtitle-templates.json' },
|
||||
{ name: 'C盘配置', path: 'C:\\system-templates.json' },
|
||||
];
|
||||
|
||||
filesToCheck.forEach(fileInfo => {
|
||||
if (!fs.existsSync(fileInfo.path)) {
|
||||
console.log(`⚠️ ${fileInfo.name} 不存在\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(fileInfo.path, 'utf-8'));
|
||||
|
||||
// 检查 subtitleStyles
|
||||
if (data.subtitleStyles) {
|
||||
console.log(`\n【${fileInfo.name}】- subtitleStyles:`);
|
||||
const styles = data.subtitleStyles.filter(s => s.id && s.id.match(/system-subtitle-(11|22|33|44|55|66|77|88)/));
|
||||
if (styles.length > 0) {
|
||||
styles.forEach(s => {
|
||||
console.log(` 模板 ${s.id.replace('system-subtitle-', '')}:`);
|
||||
console.log(` 字体: ${s.fontName || 'N/A'}`);
|
||||
console.log(` 颜色: ${s.fontColor || 'N/A'}`);
|
||||
console.log(` 描边: ${s.outlineColor || 'N/A'}, 宽度: ${s.outlineWidth || 0}`);
|
||||
});
|
||||
} else {
|
||||
console.log(' 未找到系统模板样式');
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 subtitleTemplates
|
||||
if (data.subtitleTemplates) {
|
||||
console.log(`\n【${fileInfo.name}】- subtitleTemplates:`);
|
||||
const templates = data.subtitleTemplates.filter(t => t.id && t.id.match(/template_system_(11|22|33|44|55|66|77|88)/));
|
||||
if (templates.length > 0) {
|
||||
templates.forEach(t => {
|
||||
console.log(`\n 模板 ${t.name} (${t.id}):`);
|
||||
|
||||
// 普通字幕字体(subtitleStyle)
|
||||
if (t.config && t.config.subtitleStyle) {
|
||||
console.log(` 普通字幕字体: ${t.config.subtitleStyle.fontName || 'N/A'}`);
|
||||
console.log(` 普通字幕颜色: ${t.config.subtitleStyle.fontColor || 'N/A'}`);
|
||||
} else if (t.config && t.config.subtitleStyleId) {
|
||||
console.log(` 普通字幕样式ID: ${t.config.subtitleStyleId}`);
|
||||
}
|
||||
|
||||
// 特效字幕字体和颜色(keywordGroupsStyles)
|
||||
if (t.config && t.config.keywordGroupsStyles) {
|
||||
console.log(` 特效字幕配置:`);
|
||||
t.config.keywordGroupsStyles.forEach((kg, idx) => {
|
||||
if (kg.styleOverride && kg.styleOverride.fontName) {
|
||||
console.log(` ${kg.groupName}: 字体=${kg.styleOverride.fontName}, 颜色=${kg.color || kg.styleOverride.fontColor}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log(' 未找到系统模板');
|
||||
}
|
||||
}
|
||||
|
||||
console.log('');
|
||||
} catch (e) {
|
||||
console.log(`❌ 读取 ${fileInfo.name} 失败: ${e.message}\n`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env node
|
||||
const Database = require('better-sqlite3');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function fixCoverTemplates() {
|
||||
console.log('🔧 开始修复封面模板...\n');
|
||||
|
||||
try {
|
||||
let dbPath;
|
||||
const possiblePaths = [
|
||||
path.join(__dirname, '../userData/data.db'),
|
||||
path.join(__dirname, '../data.db'),
|
||||
'C:\\aigcpanel-main\\dist-release\\win-unpacked\\data\\data\\database.db',
|
||||
path.join(process.env.APPDATA || process.env.HOME, 'zhenqianba/data.db'),
|
||||
];
|
||||
|
||||
for (const tryPath of possiblePaths) {
|
||||
if (fs.existsSync(tryPath)) {
|
||||
dbPath = tryPath;
|
||||
console.log(`✅ 找到数据库: ${dbPath}\n`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!dbPath) {
|
||||
console.error('❌ 未找到数据库文件');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const db = new Database(dbPath);
|
||||
|
||||
const correctSystemIds = [
|
||||
'485183be-6eed-4eae-b74a-8591f08b69fa',
|
||||
'6404718a-1b1c-4148-9807-ad3c57a53e0c',
|
||||
'705fa010-5bf3-4041-b624-0fd364bea5ad',
|
||||
'8f17c467-c69c-45b2-8011-94a6c3071d16',
|
||||
'1rv2nxmt9td-mkdite60',
|
||||
'4ndc1cf5lwo-mkdite61',
|
||||
'sjohrqfff7-mkdite61',
|
||||
'rxfnphz2eb-mkdite61'
|
||||
];
|
||||
|
||||
console.log('1️⃣ 查询所有模板...');
|
||||
const allTemplates = db.prepare(`
|
||||
SELECT id, name, is_system FROM cover_templates ORDER BY created_at ASC
|
||||
`).all();
|
||||
|
||||
console.log(` 找到 ${allTemplates.length} 个模板\n`);
|
||||
|
||||
const toFix = allTemplates.filter(t => t.is_system === 1 && !correctSystemIds.includes(t.id));
|
||||
|
||||
if (toFix.length === 0) {
|
||||
console.log('✅ 数据库已正确,无需修复\n');
|
||||
db.close();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(`2️⃣ 发现 ${toFix.length} 个错误的系统模板:\n`);
|
||||
toFix.forEach(t => {
|
||||
console.log(` ⚠️ "${t.name}" (ID: ${t.id})`);
|
||||
});
|
||||
|
||||
console.log('\n3️⃣ 修复中...\n');
|
||||
|
||||
toFix.forEach(template => {
|
||||
db.prepare(`UPDATE cover_templates SET is_system = 0 WHERE id = ?`).run(template.id);
|
||||
console.log(` ✅ 已修复: "${template.name}"`);
|
||||
});
|
||||
|
||||
console.log('\n4️⃣ 验证结果...');
|
||||
const systemTemplates = db.prepare(`
|
||||
SELECT id, name FROM cover_templates WHERE is_system = 1
|
||||
`).all();
|
||||
|
||||
console.log(` 系统模板数量: ${systemTemplates.length}/8`);
|
||||
systemTemplates.forEach((t, i) => {
|
||||
console.log(` ${i+1}. ${t.name}`);
|
||||
});
|
||||
|
||||
console.log('\n✨ 修复完成!');
|
||||
console.log('下一步:重启应用后点击"导出全部"\n');
|
||||
|
||||
db.close();
|
||||
process.exit(0);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 修复失败:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fixCoverTemplates();
|
||||
@@ -0,0 +1,53 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const outputDir = path.join(__dirname, '..', 'dist-release-final');
|
||||
const ymlPath = path.join(outputDir, 'latest.yml');
|
||||
|
||||
if (!fs.existsSync(ymlPath)) {
|
||||
console.log('[fix-latest-yml] latest.yml not found, skipping');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let content = fs.readFileSync(ymlPath, 'utf-8');
|
||||
|
||||
if (content.includes('blockMapSize')) {
|
||||
console.log('[fix-latest-yml] blockMapSize already exists, skipping');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const lines = content.split('\n');
|
||||
const newLines = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
newLines.push(line);
|
||||
|
||||
if (line.includes('url:') && line.includes('.exe')) {
|
||||
const urlMatch = line.match(/url:\s*(.+\.exe)/);
|
||||
if (urlMatch) {
|
||||
const exeName = urlMatch[1].trim();
|
||||
const blockmapName = exeName + '.blockmap';
|
||||
const blockmapPath = path.join(outputDir, blockmapName);
|
||||
|
||||
if (fs.existsSync(blockmapPath)) {
|
||||
const stat = fs.statSync(blockmapPath);
|
||||
const nextLine = lines[i + 1] || '';
|
||||
if (nextLine.includes('size:')) {
|
||||
newLines.push(nextLine);
|
||||
newLines.push(` blockMapSize: ${stat.size}`);
|
||||
i++;
|
||||
console.log('[fix-latest-yml] Added blockMapSize:', stat.size, 'for', exeName);
|
||||
} else {
|
||||
newLines.push(` blockMapSize: ${stat.size}`);
|
||||
console.log('[fix-latest-yml] Added blockMapSize:', stat.size, 'for', exeName);
|
||||
}
|
||||
} else {
|
||||
console.log('[fix-latest-yml] blockmap file not found:', blockmapName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(ymlPath, newLines.join('\n'), 'utf-8');
|
||||
console.log('[fix-latest-yml] latest.yml fixed');
|
||||
@@ -0,0 +1,184 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
console.log('=== 8个模板配置差异全面分析报告 ===\n');
|
||||
|
||||
// 读取所有配置源
|
||||
const sources = {
|
||||
'C盘根目录': 'C:\\system-templates.json',
|
||||
'项目打包配置': 'electron/resources/extra/common/config/system-templates.json',
|
||||
'项目开发配置': 'electron/config/system-templates.json',
|
||||
'历史配置v2': 'electron/config/default-subtitle-templates-v2.json',
|
||||
'TypeScript源': 'src/config/systemSubtitleTemplates.ts',
|
||||
};
|
||||
|
||||
const configs = {};
|
||||
|
||||
// 读取JSON文件
|
||||
Object.keys(sources).forEach(name => {
|
||||
const filePath = sources[name];
|
||||
if (fs.existsSync(filePath)) {
|
||||
try {
|
||||
if (filePath.endsWith('.ts')) {
|
||||
// TypeScript文件,提取配置
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
// 简单提取,实际应该用AST
|
||||
const templates = [];
|
||||
const regex = /"id": "template_system_(\d+)",[\s\S]*?"name": "(\d+)",[\s\S]*?titleSubtitleConfig: \{[\s\S]*?maxCharsPerLine: (\d+),[\s\S]*?lines: \[([\s\S]*?)\],/g;
|
||||
let match;
|
||||
while ((match = regex.exec(content)) !== null) {
|
||||
const id = match[1];
|
||||
const name = match[2];
|
||||
const maxChars = parseInt(match[3]);
|
||||
const linesContent = match[4];
|
||||
const lines = [];
|
||||
const lineRegex = /text: "([^"]+)"/g;
|
||||
let lineMatch;
|
||||
while ((lineMatch = lineRegex.exec(linesContent)) !== null) {
|
||||
lines.push(lineMatch[1]);
|
||||
}
|
||||
templates.push({ id: `template_system_${id}`, name, maxCharsPerLine: maxChars, lines: lines.length, linesText: lines.join('|') });
|
||||
}
|
||||
configs[name] = templates;
|
||||
} else {
|
||||
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
||||
const templates = (data.subtitleTemplates || []).map(t => {
|
||||
const title = t.config && t.config.titleSubtitleConfig;
|
||||
return {
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
hasTitle: !!title,
|
||||
maxCharsPerLine: title ? title.maxCharsPerLine : null,
|
||||
lines: title ? title.lines.length : 0,
|
||||
linesText: title ? title.lines.map(l => l.text).join('|') : '',
|
||||
};
|
||||
});
|
||||
configs[name] = templates;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`⚠️ 读取 ${name} 失败: ${e.message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 分析每个配置源
|
||||
console.log('【1】各配置源的模板标题字幕配置详情\n');
|
||||
|
||||
Object.keys(configs).forEach(sourceName => {
|
||||
const templates = configs[sourceName];
|
||||
console.log(`\n${sourceName}:`);
|
||||
templates.forEach(t => {
|
||||
if (t.hasTitle !== false && t.maxCharsPerLine) {
|
||||
console.log(` 模板 ${t.name}: ${t.maxCharsPerLine}字符/行, ${t.lines}行`);
|
||||
} else {
|
||||
console.log(` 模板 ${t.name}: 无标题配置`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 找出相同配置的模板组
|
||||
console.log('\n\n【2】各配置源中相同配置的模板组\n');
|
||||
|
||||
Object.keys(configs).forEach(sourceName => {
|
||||
const templates = configs[sourceName];
|
||||
const groups = {};
|
||||
|
||||
templates.forEach(t => {
|
||||
if (t.maxCharsPerLine !== null && t.maxCharsPerLine !== undefined) {
|
||||
const key = `${t.maxCharsPerLine}-${t.lines}-${t.linesText}`;
|
||||
if (!groups[key]) groups[key] = [];
|
||||
groups[key].push(t.name);
|
||||
}
|
||||
});
|
||||
|
||||
const sameGroups = Object.keys(groups).filter(k => groups[k].length > 1);
|
||||
if (sameGroups.length > 0) {
|
||||
console.log(`\n${sourceName}:`);
|
||||
sameGroups.forEach(key => {
|
||||
console.log(` ❌ 模板 ${groups[key].join(', ')} 配置相同 (${key.split('-')[0]}字符/行, ${key.split('-')[1]}行)`);
|
||||
});
|
||||
} else {
|
||||
console.log(`\n${sourceName}:`);
|
||||
console.log(` ✅ 所有模板配置都不同`);
|
||||
}
|
||||
});
|
||||
|
||||
// 对比不同配置源
|
||||
console.log('\n\n【3】配置源之间的差异对比\n');
|
||||
|
||||
const sourceNames = Object.keys(configs);
|
||||
if (sourceNames.length > 1) {
|
||||
const baseSource = sourceNames[0];
|
||||
const baseTemplates = configs[baseSource];
|
||||
|
||||
sourceNames.slice(1).forEach(sourceName => {
|
||||
const compareTemplates = configs[sourceName];
|
||||
console.log(`\n${baseSource} vs ${sourceName}:`);
|
||||
|
||||
let hasDiff = false;
|
||||
baseTemplates.forEach(baseT => {
|
||||
const compareT = compareTemplates.find(t => t.id === baseT.id || t.name === baseT.name);
|
||||
if (!compareT) {
|
||||
console.log(` ⚠️ 模板 ${baseT.name}: 在 ${sourceName} 中不存在`);
|
||||
hasDiff = true;
|
||||
} else {
|
||||
const baseKey = baseT.maxCharsPerLine !== null ? `${baseT.maxCharsPerLine}-${baseT.lines}-${baseT.linesText}` : 'no-title';
|
||||
const compareKey = compareT.maxCharsPerLine !== null ? `${compareT.maxCharsPerLine}-${compareT.lines}-${compareT.linesText}` : 'no-title';
|
||||
if (baseKey !== compareKey) {
|
||||
console.log(` ❌ 模板 ${baseT.name}: 配置不同`);
|
||||
if (baseT.maxCharsPerLine) {
|
||||
console.log(` ${baseSource}: ${baseT.maxCharsPerLine}字符/行, ${baseT.lines}行`);
|
||||
} else {
|
||||
console.log(` ${baseSource}: 无标题配置`);
|
||||
}
|
||||
if (compareT.maxCharsPerLine) {
|
||||
console.log(` ${sourceName}: ${compareT.maxCharsPerLine}字符/行, ${compareT.lines}行`);
|
||||
} else {
|
||||
console.log(` ${sourceName}: 无标题配置`);
|
||||
}
|
||||
hasDiff = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!hasDiff) {
|
||||
console.log(` ✅ 配置相同`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 总结
|
||||
console.log('\n\n【4】总结\n');
|
||||
|
||||
console.log('检查的配置源:');
|
||||
sourceNames.forEach(name => {
|
||||
const templates = configs[name];
|
||||
const hasTitleCount = templates.filter(t => t.maxCharsPerLine !== null).length;
|
||||
console.log(` - ${name}: ${templates.length}个模板, ${hasTitleCount}个有标题配置`);
|
||||
});
|
||||
|
||||
console.log('\n发现的问题:');
|
||||
console.log('1. ❌ 所有有标题配置的源中,模板都分为两组:');
|
||||
console.log(' - 模板 11, 33, 55, 77 配置相同(8字符/行,2行)');
|
||||
console.log(' - 模板 22, 44, 66, 88 配置相同(15字符/行,1行)');
|
||||
console.log('2. ❌ 项目打包配置和项目开发配置中缺少 titleSubtitleConfig');
|
||||
console.log('3. ⚠️ 需要确认:这8个模板原本是否应该有不同的配置?');
|
||||
|
||||
console.log('\n建议:');
|
||||
console.log('1. 如果8个模板应该不同,需要为每个模板创建不同的标题字幕配置');
|
||||
console.log('2. 将完整的配置同步到所有配置源(项目打包配置、项目开发配置)');
|
||||
console.log('3. 确保数据库初始化时使用正确的配置');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const yauzl = require('yauzl');
|
||||
const config = require('./resource-bundles.config.cjs');
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
const portableBundleRoot = path.join(root, 'dist-release-final', 'win-unpacked', 'resources', 'resources-bundles');
|
||||
const manifestPath = path.join(root, 'dist-release-final', 'resource-manifest.json');
|
||||
const retryableFsCodes = new Set(['EBUSY', 'EACCES', 'EPERM', 'ENOTEMPTY']);
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function isRetryableFsError(error) {
|
||||
return error && retryableFsCodes.has(error.code);
|
||||
}
|
||||
|
||||
async function retryFsOperation(label, operation, attempts = 6) {
|
||||
let lastError = null;
|
||||
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
||||
try {
|
||||
return await operation(attempt);
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (!isRetryableFsError(error) || attempt === attempts) {
|
||||
throw error;
|
||||
}
|
||||
const delayMs = 350 * attempt;
|
||||
console.warn(`[hydrate-oem-portable] ${label} failed with ${error.code}; retry ${attempt}/${attempts - 1} after ${delayMs}ms`);
|
||||
await sleep(delayMs);
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
function removeIfExists(target) {
|
||||
if (fs.existsSync(target)) {
|
||||
fs.rmSync(target, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function uniqueTempPath(target) {
|
||||
return `${target}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
}
|
||||
|
||||
async function replaceWithRetry(source, destination) {
|
||||
await retryFsOperation(`replace ${destination}`, async () => {
|
||||
removeIfExists(destination);
|
||||
fs.renameSync(source, destination);
|
||||
});
|
||||
}
|
||||
|
||||
function readJson(file, fallback) {
|
||||
if (!fs.existsSync(file)) return fallback;
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function extractZipOnce(zipPath, destination) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const finalDestinationRoot = path.resolve(destination);
|
||||
const stagingDestination = uniqueTempPath(destination);
|
||||
removeIfExists(stagingDestination);
|
||||
fs.mkdirSync(stagingDestination, { recursive: true });
|
||||
const destinationRoot = path.resolve(stagingDestination);
|
||||
|
||||
yauzl.open(zipPath, { lazyEntries: true }, (openError, zipFile) => {
|
||||
if (openError || !zipFile) {
|
||||
reject(openError || new Error(`Cannot open zip: ${zipPath}`));
|
||||
return;
|
||||
}
|
||||
|
||||
let settled = false;
|
||||
const finish = (error) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
try {
|
||||
zipFile.close();
|
||||
} catch {
|
||||
// Ignore close errors after the original extraction failure.
|
||||
}
|
||||
if (error) {
|
||||
removeIfExists(stagingDestination);
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
replaceWithRetry(stagingDestination, finalDestinationRoot)
|
||||
.then(() => resolve())
|
||||
.catch(replaceError => {
|
||||
removeIfExists(stagingDestination);
|
||||
reject(replaceError);
|
||||
});
|
||||
};
|
||||
|
||||
zipFile.readEntry();
|
||||
zipFile.on('entry', (entry) => {
|
||||
if (settled) return;
|
||||
const normalizedName = entry.fileName.replace(/\\/g, '/');
|
||||
const destPath = path.resolve(destinationRoot, normalizedName);
|
||||
if (!destPath.startsWith(destinationRoot + path.sep) && destPath !== destinationRoot) {
|
||||
finish(new Error(`Unsafe zip entry: ${entry.fileName}`));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (/\/$/.test(normalizedName)) {
|
||||
fs.mkdirSync(destPath, { recursive: true });
|
||||
zipFile.readEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
||||
} catch (error) {
|
||||
finish(error);
|
||||
return;
|
||||
}
|
||||
|
||||
zipFile.openReadStream(entry, (streamError, readStream) => {
|
||||
if (streamError || !readStream) {
|
||||
finish(streamError || new Error(`Cannot read zip entry: ${entry.fileName}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const writeStream = fs.createWriteStream(destPath);
|
||||
readStream.pipe(writeStream);
|
||||
writeStream.on('finish', () => {
|
||||
if (!settled) zipFile.readEntry();
|
||||
});
|
||||
writeStream.on('error', finish);
|
||||
readStream.on('error', finish);
|
||||
});
|
||||
});
|
||||
|
||||
zipFile.on('end', () => finish());
|
||||
zipFile.on('error', finish);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function extractZip(zipPath, destination) {
|
||||
return retryFsOperation(
|
||||
`extract ${path.basename(zipPath)} to ${destination}`,
|
||||
() => extractZipOnce(zipPath, destination)
|
||||
);
|
||||
}
|
||||
|
||||
async function copyDirectory(source, destination) {
|
||||
if (!fs.existsSync(source)) return false;
|
||||
await retryFsOperation(`copy ${source} to ${destination}`, async () => {
|
||||
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
||||
removeIfExists(destination);
|
||||
fs.cpSync(source, destination, { recursive: true, force: true });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function pruneWindowsOnlyBundles() {
|
||||
const chromiumRoot = path.join(portableBundleRoot, 'playwright', 'chromium-1200');
|
||||
const macChromium = path.join(chromiumRoot, 'chrome-mac');
|
||||
const winChromium = path.join(chromiumRoot, 'chrome-win64');
|
||||
if (process.platform === 'win32' && fs.existsSync(winChromium)) {
|
||||
removeIfExists(macChromium);
|
||||
}
|
||||
}
|
||||
|
||||
function directoryHasFiles(dir) {
|
||||
if (!fs.existsSync(dir)) return false;
|
||||
const walk = (current) => {
|
||||
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
||||
const full = path.join(current, entry.name);
|
||||
if (entry.isFile()) return true;
|
||||
if (entry.isDirectory() && walk(full)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
return walk(dir);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const unpackedDir = path.join(root, 'dist-release-final', 'win-unpacked');
|
||||
if (!fs.existsSync(unpackedDir)) {
|
||||
throw new Error(`win-unpacked does not exist: ${unpackedDir}`);
|
||||
}
|
||||
|
||||
const manifest = readJson(manifestPath, { bundles: {} });
|
||||
fs.mkdirSync(portableBundleRoot, { recursive: true });
|
||||
const hydrated = [];
|
||||
|
||||
for (const bundle of config.bundles) {
|
||||
const destination = path.join(portableBundleRoot, bundle.name);
|
||||
const archiveName = manifest.bundles?.[bundle.name]?.archive;
|
||||
const archivePath = archiveName ? path.join(config.outputDir, archiveName) : '';
|
||||
|
||||
if (archivePath && fs.existsSync(archivePath)) {
|
||||
await extractZip(archivePath, destination);
|
||||
hydrated.push(bundle.name);
|
||||
} else if (await copyDirectory(bundle.source, destination)) {
|
||||
hydrated.push(bundle.name);
|
||||
} else if (bundle.required) {
|
||||
throw new Error(`Required resource bundle is missing: ${bundle.name}. Expected archive ${archivePath || '(none)'} or source ${bundle.source}`);
|
||||
}
|
||||
|
||||
if (bundle.required && !directoryHasFiles(destination)) {
|
||||
throw new Error(`Required resource bundle was not hydrated: ${bundle.name} (${destination})`);
|
||||
}
|
||||
}
|
||||
|
||||
pruneWindowsOnlyBundles();
|
||||
console.log(`[hydrate-oem-portable] hydrated bundles: ${hydrated.join(', ') || 'none'}`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('[hydrate-oem-portable] failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/bin/bash
|
||||
|
||||
# prepare
|
||||
# brew install --cask inkscape
|
||||
|
||||
echo "Convert icon"
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
PROJECT_ROOT=$(realpath "${DIR}/..")
|
||||
echo "PROJECT_ROOT: ${PROJECT_ROOT}"
|
||||
|
||||
path_svg="${PROJECT_ROOT}/public/logo.svg"
|
||||
path_white_svg="${PROJECT_ROOT}/public/logo-white.svg"
|
||||
path_build="${PROJECT_ROOT}/electron/resources/build"
|
||||
path_extra="${PROJECT_ROOT}/electron/resources/extra"
|
||||
path_source_png="${path_build}/logo_1024x1024.png"
|
||||
|
||||
cp -a "${path_svg}" "${PROJECT_ROOT}/src/assets/image/logo.svg"
|
||||
cp -a "${path_white_svg}" "${PROJECT_ROOT}/src/assets/image/logo-white.svg"
|
||||
|
||||
inkscape "${path_svg}" --export-type=png --export-filename="${path_source_png}" -w 1024 -h 1024
|
||||
|
||||
size=(16 32 44 48 64 128 150 256 512)
|
||||
for i in "${size[@]}"; do
|
||||
path_png="${path_build}/logo@${i}x$i.png"
|
||||
echo "Generate: logo@${i}x$i.png"
|
||||
inkscape --export-type="png" --export-filename="${path_png}" -w $i -h $i "${path_source_png}"
|
||||
done
|
||||
|
||||
path_ico="${path_build}/logo.ico"
|
||||
echo "Generate: logo.ico"
|
||||
magick "${path_source_png}" -define icon:auto-resize=256,48,32,16 "${path_ico}"
|
||||
|
||||
echo "Generate: logo.png"
|
||||
rm -rf "${path_build}/logo.png"
|
||||
cp -a "${path_build}/logo@256x256.png" "${path_build}/logo.png"
|
||||
|
||||
echo "Generate: logo.icns"
|
||||
path_iconset="${path_build}/icon.iconset"
|
||||
rm -rf "${path_iconset}"
|
||||
mkdir -p "${path_iconset}"
|
||||
cp -a "${path_build}/logo@256x256.png" "${path_iconset}/icon_256x256.png"
|
||||
cp -a "${path_build}/logo@32x32.png" "${path_iconset}/icon_32x32.png"
|
||||
cp -a "${path_build}/logo@16x16.png" "${path_iconset}/icon_16x16.png"
|
||||
iconutil -c icns "${path_iconset}" -o "${path_build}/logo.icns"
|
||||
|
||||
echo "Generate: appx/StoreLogo.png"
|
||||
cp -a "${path_build}/logo@256x256.png" "${path_build}/appx/StoreLogo.png"
|
||||
echo "Generate: appx/Square44x44Logo.png"
|
||||
cp -a "${path_build}/logo@44x44.png" "${path_build}/appx/Square44x44Logo.png"
|
||||
echo "Generate: appx/Square150x150Logo.png"
|
||||
cp -a "${path_build}/logo@150x150.png" "${path_build}/appx/Square150x150Logo.png"
|
||||
echo "Generate: appx/Wide310x150Logo.png"
|
||||
magick "${path_build}/logo@150x150.png" -resize 310x150 -background none -gravity center -extent 310x150 "${path_build}/appx/Wide310x150Logo.png"
|
||||
|
||||
echo "Generate: common/tray/icon.png"
|
||||
mkdir -p "${path_extra}/common/tray"
|
||||
cp -a "${path_build}/logo@256x256.png" "${path_extra}/common/tray/icon.png"
|
||||
echo "Generate: common/tray/icon.ico"
|
||||
cp -a "${path_build}/logo.ico" "${path_extra}/common/tray/icon.ico"
|
||||
|
||||
echo "Generate: osx/tray/iconTemplate.png"
|
||||
mkdir -p "${path_extra}/osx/tray"
|
||||
magick "${path_build}/logo-gray.png" -resize 16x16 -background none -gravity center "${path_extra}/osx/tray/iconTemplate.png"
|
||||
echo "Generate: osx/tray/iconTemplate@2x.png"
|
||||
magick "${path_build}/logo-gray.png" -resize 32x32 -background none -gravity center "${path_extra}/osx/tray/iconTemplate@2x.png"
|
||||
echo "Generate: osx/tray/iconTemplate@4x.png"
|
||||
magick "${path_build}/logo-gray.png" -resize 64x64 -background none -gravity center "${path_extra}/osx/tray/iconTemplate@4x.png"
|
||||
|
||||
rm -rf "${path_iconset}"
|
||||
rm -rf ${path_build}/logo@*
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_URL="https://github.com/modstart-lib/share-binary"
|
||||
REPO_DIR="share-binary"
|
||||
|
||||
if [ ! -d "$REPO_DIR/.git" ]; then
|
||||
echo "🔹 目录不存在,正在克隆仓库..."
|
||||
git clone "$REPO_URL"
|
||||
else
|
||||
echo "🔹 仓库已存在,进入目录并更新..."
|
||||
cd "$REPO_DIR"
|
||||
git pull origin main
|
||||
cd ..
|
||||
fi
|
||||
|
||||
rm -rfv electron/resources/extra/osx-arm64
|
||||
mkdir -p electron/resources/extra/osx-arm64
|
||||
cp -a share-binary/osx-arm64/ffmpeg electron/resources/extra/osx-arm64/ffmpeg
|
||||
cp -a share-binary/osx-arm64/ffprobe electron/resources/extra/osx-arm64/ffprobe
|
||||
|
||||
rm -rfv electron/resources/extra/osx-x86
|
||||
mkdir -p electron/resources/extra/osx-x86
|
||||
cp -a share-binary/osx-x86/ffmpeg electron/resources/extra/osx-x86/ffmpeg
|
||||
cp -a share-binary/osx-x86/ffprobe electron/resources/extra/osx-x86/ffprobe
|
||||
|
||||
rm -rfv electron/resources/extra/linux-arm64
|
||||
mkdir -p electron/resources/extra/linux-arm64
|
||||
cp -a share-binary/linux-arm64/ffmpeg electron/resources/extra/linux-arm64/ffmpeg
|
||||
cp -a share-binary/linux-arm64/ffprobe electron/resources/extra/linux-arm64/ffprobe
|
||||
|
||||
rm -rfv electron/resources/extra/linux-x86
|
||||
mkdir -p electron/resources/extra/linux-x86
|
||||
cp -a share-binary/linux-x86/ffmpeg electron/resources/extra/linux-x86/ffmpeg
|
||||
cp -a share-binary/linux-x86/ffprobe electron/resources/extra/linux-x86/ffprobe
|
||||
|
||||
rm -rfv electron/resources/extra/win-x86
|
||||
mkdir -p electron/resources/extra/win-x86
|
||||
cp -a share-binary/win-x86/ffmpeg.exe electron/resources/extra/win-x86/ffmpeg.exe
|
||||
cp -a share-binary/win-x86/ffprobe.exe electron/resources/extra/win-x86/ffprobe.exe
|
||||
|
||||
ls -R electron/resources/extra
|
||||
@@ -0,0 +1,347 @@
|
||||
!include "LogicLib.nsh"
|
||||
|
||||
!define LEGACY_APP_GUID "3a51f54f-dd33-55a2-a302-8f716620a296"
|
||||
!define LEGACY_INSTALL_KEY "Software\${LEGACY_APP_GUID}"
|
||||
!define LEGACY_UNINSTALL_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${LEGACY_APP_GUID}"
|
||||
!define LEGACY_UNINSTALL_KEY_WOW64 "Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\${LEGACY_APP_GUID}"
|
||||
|
||||
Var OldInstallDir
|
||||
Var OldBundleDir
|
||||
Var BundleBackupDir
|
||||
Var UpgradePreparationDone
|
||||
|
||||
Function LegacyGetInQuotes
|
||||
Exch $R0
|
||||
Push $R1
|
||||
Push $R2
|
||||
Push $R3
|
||||
|
||||
StrCpy $R2 -1
|
||||
IntOp $R2 $R2 + 1
|
||||
StrCpy $R3 $R0 1 $R2
|
||||
StrCmp $R3 "" 0 +3
|
||||
StrCpy $R0 ""
|
||||
Goto legacy_get_in_quotes_done
|
||||
StrCmp $R3 '"' 0 -5
|
||||
|
||||
IntOp $R2 $R2 + 1
|
||||
StrCpy $R0 $R0 "" $R2
|
||||
|
||||
StrCpy $R2 0
|
||||
IntOp $R2 $R2 + 1
|
||||
StrCpy $R3 $R0 1 $R2
|
||||
StrCmp $R3 "" 0 +3
|
||||
StrCpy $R0 ""
|
||||
Goto legacy_get_in_quotes_done
|
||||
StrCmp $R3 '"' 0 -5
|
||||
|
||||
StrCpy $R0 $R0 $R2
|
||||
legacy_get_in_quotes_done:
|
||||
|
||||
Pop $R3
|
||||
Pop $R2
|
||||
Pop $R1
|
||||
Exch $R0
|
||||
FunctionEnd
|
||||
|
||||
Function LegacyGetFileParent
|
||||
Exch $R0
|
||||
Push $R1
|
||||
Push $R2
|
||||
Push $R3
|
||||
|
||||
StrCpy $R1 0
|
||||
StrLen $R2 $R0
|
||||
|
||||
legacy_get_parent_loop:
|
||||
IntOp $R1 $R1 + 1
|
||||
IntCmp $R1 $R2 legacy_get_parent_done 0 legacy_get_parent_done
|
||||
StrCpy $R3 $R0 1 -$R1
|
||||
StrCmp $R3 "\" legacy_get_parent_done
|
||||
Goto legacy_get_parent_loop
|
||||
|
||||
legacy_get_parent_done:
|
||||
StrCpy $R0 $R0 -$R1
|
||||
|
||||
Pop $R3
|
||||
Pop $R2
|
||||
Pop $R1
|
||||
Exch $R0
|
||||
FunctionEnd
|
||||
|
||||
Function ResolveLegacyInstallDirFromUninstallString
|
||||
Exch $0
|
||||
Push $1
|
||||
Push $2
|
||||
|
||||
StrCpy $1 ""
|
||||
${If} $0 == ""
|
||||
Goto resolve_legacy_dir_done
|
||||
${EndIf}
|
||||
|
||||
StrCpy $2 $0 1
|
||||
${If} $2 == '"'
|
||||
Push $0
|
||||
Call LegacyGetInQuotes
|
||||
Pop $1
|
||||
${Else}
|
||||
StrCpy $1 $0
|
||||
${EndIf}
|
||||
|
||||
${If} $1 != ""
|
||||
Push $1
|
||||
Call LegacyGetFileParent
|
||||
Pop $1
|
||||
${EndIf}
|
||||
|
||||
resolve_legacy_dir_done:
|
||||
Pop $2
|
||||
Exch $1
|
||||
FunctionEnd
|
||||
|
||||
Function ApplyLegacyInstallDirCandidate
|
||||
Exch $0
|
||||
|
||||
${If} $0 != ""
|
||||
${AndIf} ${FileExists} "$0\*.*"
|
||||
StrCpy $OldInstallDir "$0"
|
||||
StrCpy $OldBundleDir "$OldInstallDir\resources-bundles"
|
||||
StrCpy $INSTDIR "$OldInstallDir"
|
||||
${EndIf}
|
||||
|
||||
Exch $0
|
||||
FunctionEnd
|
||||
|
||||
Function ResolveLegacyInstallDir
|
||||
${If} $OldInstallDir != ""
|
||||
${AndIf} ${FileExists} "$OldInstallDir\*.*"
|
||||
StrCpy $INSTDIR "$OldInstallDir"
|
||||
Return
|
||||
${EndIf}
|
||||
|
||||
StrCpy $OldInstallDir ""
|
||||
StrCpy $OldBundleDir ""
|
||||
|
||||
ReadRegStr $0 HKCU "${LEGACY_INSTALL_KEY}" "InstallLocation"
|
||||
Push $0
|
||||
Call ApplyLegacyInstallDirCandidate
|
||||
Pop $0
|
||||
${If} $OldInstallDir == ""
|
||||
ReadRegStr $0 HKLM "${LEGACY_INSTALL_KEY}" "InstallLocation"
|
||||
Push $0
|
||||
Call ApplyLegacyInstallDirCandidate
|
||||
Pop $0
|
||||
${EndIf}
|
||||
|
||||
${If} $OldInstallDir == ""
|
||||
ReadRegStr $0 HKCU "${LEGACY_UNINSTALL_KEY}" "InstallLocation"
|
||||
Push $0
|
||||
Call ApplyLegacyInstallDirCandidate
|
||||
Pop $0
|
||||
${EndIf}
|
||||
${If} $OldInstallDir == ""
|
||||
ReadRegStr $0 HKLM "${LEGACY_UNINSTALL_KEY}" "InstallLocation"
|
||||
Push $0
|
||||
Call ApplyLegacyInstallDirCandidate
|
||||
Pop $0
|
||||
${EndIf}
|
||||
${If} $OldInstallDir == ""
|
||||
ReadRegStr $0 HKLM "${LEGACY_UNINSTALL_KEY_WOW64}" "InstallLocation"
|
||||
Push $0
|
||||
Call ApplyLegacyInstallDirCandidate
|
||||
Pop $0
|
||||
${EndIf}
|
||||
|
||||
${If} $OldInstallDir == ""
|
||||
ReadRegStr $0 HKCU "${LEGACY_UNINSTALL_KEY}" "UninstallString"
|
||||
Push $0
|
||||
Call ResolveLegacyInstallDirFromUninstallString
|
||||
Pop $0
|
||||
Push $0
|
||||
Call ApplyLegacyInstallDirCandidate
|
||||
Pop $0
|
||||
${EndIf}
|
||||
${If} $OldInstallDir == ""
|
||||
ReadRegStr $0 HKLM "${LEGACY_UNINSTALL_KEY}" "UninstallString"
|
||||
Push $0
|
||||
Call ResolveLegacyInstallDirFromUninstallString
|
||||
Pop $0
|
||||
Push $0
|
||||
Call ApplyLegacyInstallDirCandidate
|
||||
Pop $0
|
||||
${EndIf}
|
||||
${If} $OldInstallDir == ""
|
||||
ReadRegStr $0 HKLM "${LEGACY_UNINSTALL_KEY_WOW64}" "UninstallString"
|
||||
Push $0
|
||||
Call ResolveLegacyInstallDirFromUninstallString
|
||||
Pop $0
|
||||
Push $0
|
||||
Call ApplyLegacyInstallDirCandidate
|
||||
Pop $0
|
||||
${EndIf}
|
||||
|
||||
${If} $OldInstallDir != ""
|
||||
DetailPrint "Legacy install dir: $OldInstallDir"
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
Function ClearLegacyRegistry
|
||||
DeleteRegKey HKCU "${LEGACY_UNINSTALL_KEY}"
|
||||
DeleteRegKey HKLM "${LEGACY_UNINSTALL_KEY}"
|
||||
DeleteRegKey HKLM "${LEGACY_UNINSTALL_KEY_WOW64}"
|
||||
DeleteRegKey HKCU "${LEGACY_INSTALL_KEY}"
|
||||
DeleteRegKey HKLM "${LEGACY_INSTALL_KEY}"
|
||||
FunctionEnd
|
||||
|
||||
Function RemoveDirectoryDeep
|
||||
Exch $0
|
||||
|
||||
${If} $0 == ""
|
||||
Return
|
||||
${EndIf}
|
||||
|
||||
ClearErrors
|
||||
RMDir /r "$0"
|
||||
ClearErrors
|
||||
nsExec::ExecToLog '"$SYSDIR\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -Command "$$target = ''\\?\'' + [System.IO.Path]::GetFullPath(''$0''); try { Remove-Item -LiteralPath $$target -Recurse -Force -ErrorAction Stop } catch {}"'
|
||||
Pop $1
|
||||
|
||||
Exch $0
|
||||
FunctionEnd
|
||||
|
||||
Function KillDirectAppProcesses
|
||||
nsExec::ExecToLog 'taskkill /F /IM "${PRODUCT_FILENAME}.exe" /T'
|
||||
Pop $0
|
||||
nsExec::ExecToLog 'taskkill /F /IM "SC-IP-Agent.exe" /T'
|
||||
Pop $0
|
||||
nsExec::ExecToLog 'taskkill /F /IM "尚创IP智能体V8.exe" /T'
|
||||
Pop $0
|
||||
FunctionEnd
|
||||
|
||||
Function KillProcessesUnderLegacyInstall
|
||||
Call KillDirectAppProcesses
|
||||
|
||||
${If} $OldInstallDir == ""
|
||||
Return
|
||||
${EndIf}
|
||||
|
||||
DetailPrint "Closing processes under $OldInstallDir"
|
||||
nsExec::ExecToLog '"$SYSDIR\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -Command "$$root = [System.IO.Path]::GetFullPath(''$OldInstallDir''); Get-Process -ErrorAction SilentlyContinue | Where-Object { try { $$path = $$_.Path; if (-not $$path) { return $$false }; [System.IO.Path]::GetFullPath($$path).StartsWith($$root, [System.StringComparison]::OrdinalIgnoreCase) } catch { $$false } } | ForEach-Object { try { Stop-Process -Id $$_.Id -Force -ErrorAction Stop } catch {} }"'
|
||||
Pop $0
|
||||
Sleep 1200
|
||||
|
||||
Call KillDirectAppProcesses
|
||||
Sleep 800
|
||||
FunctionEnd
|
||||
|
||||
Function BackupLegacyResourceBundles
|
||||
${If} $OldBundleDir == ""
|
||||
Return
|
||||
${EndIf}
|
||||
|
||||
StrCpy $BundleBackupDir "$OldInstallDir.__resources-bundles-backup"
|
||||
${If} ${FileExists} "$BundleBackupDir\*.*"
|
||||
${IfNot} ${FileExists} "$OldBundleDir\*.*"
|
||||
DetailPrint "Reusing preserved resource bundles from $BundleBackupDir"
|
||||
Return
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
|
||||
${IfNot} ${FileExists} "$OldBundleDir\*.*"
|
||||
Return
|
||||
${EndIf}
|
||||
|
||||
Push "$BundleBackupDir"
|
||||
Call RemoveDirectoryDeep
|
||||
|
||||
DetailPrint "Preserving resource bundles to $BundleBackupDir"
|
||||
Rename "$OldBundleDir" "$BundleBackupDir"
|
||||
${If} ${Errors}
|
||||
ClearErrors
|
||||
nsExec::ExecToLog 'robocopy "$OldBundleDir" "$BundleBackupDir" /E /R:1 /W:1 /NFL /NDL /NJH /NJS /NP'
|
||||
Pop $0
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
Function RestorePreservedResourceBundles
|
||||
${If} $BundleBackupDir == ""
|
||||
Return
|
||||
${EndIf}
|
||||
|
||||
${IfNot} ${FileExists} "$BundleBackupDir\*.*"
|
||||
Return
|
||||
${EndIf}
|
||||
|
||||
DetailPrint "Merging preserved resource bundles"
|
||||
CreateDirectory "$INSTDIR\resources-bundles"
|
||||
nsExec::ExecToLog 'robocopy "$BundleBackupDir" "$INSTDIR\resources-bundles" /E /XC /XN /XO /R:1 /W:1 /NFL /NDL /NJH /NJS /NP'
|
||||
Pop $0
|
||||
Push "$BundleBackupDir"
|
||||
Call RemoveDirectoryDeep
|
||||
FunctionEnd
|
||||
|
||||
Function RunUpgradePreparation
|
||||
${If} $UpgradePreparationDone == "true"
|
||||
Return
|
||||
${EndIf}
|
||||
|
||||
Call ResolveLegacyInstallDir
|
||||
${If} $OldInstallDir == ""
|
||||
StrCpy $UpgradePreparationDone "true"
|
||||
Return
|
||||
${EndIf}
|
||||
|
||||
Call KillProcessesUnderLegacyInstall
|
||||
Call BackupLegacyResourceBundles
|
||||
Call ClearLegacyRegistry
|
||||
StrCpy $UpgradePreparationDone "true"
|
||||
FunctionEnd
|
||||
|
||||
!macro customCheckAppRunning
|
||||
!ifdef BUILD_UNINSTALLER
|
||||
DetailPrint "Closing running application..."
|
||||
nsExec::ExecToLog 'taskkill /F /IM "${PRODUCT_FILENAME}.exe" /T'
|
||||
Pop $0
|
||||
nsExec::ExecToLog 'taskkill /F /IM "SC-IP-Agent.exe" /T'
|
||||
Pop $0
|
||||
nsExec::ExecToLog 'taskkill /F /IM "尚创IP智能体V8.exe" /T'
|
||||
Pop $0
|
||||
!else
|
||||
DetailPrint "Preparing in-place upgrade..."
|
||||
Call RunUpgradePreparation
|
||||
DetailPrint "Upgrade preparation complete"
|
||||
!endif
|
||||
!macroend
|
||||
|
||||
!macro customInit
|
||||
Call ResolveLegacyInstallDir
|
||||
|
||||
IfSilent skip_vcredist 0
|
||||
ReadRegStr $0 HKLM "SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64" "Installed"
|
||||
StrCmp $0 "1" skip_vcredist
|
||||
MessageBox MB_YESNO "检测到系统缺少 Visual C++ 运行库,是否安装?" IDYES install_vcredist IDNO skip_vcredist
|
||||
install_vcredist:
|
||||
SetOutPath "$TEMP"
|
||||
File "${BUILD_RESOURCES_DIR}\vc_redist.x64.exe"
|
||||
ExecWait '"$TEMP\vc_redist.x64.exe" /install /quiet /norestart' $1
|
||||
Delete "$TEMP\vc_redist.x64.exe"
|
||||
skip_vcredist:
|
||||
|
||||
Call RunUpgradePreparation
|
||||
!macroend
|
||||
|
||||
!macro customInstall
|
||||
DetailPrint "Running final upgrade checks..."
|
||||
Call KillProcessesUnderLegacyInstall
|
||||
Call RestorePreservedResourceBundles
|
||||
!macroend
|
||||
|
||||
!macro customUnInit
|
||||
DetailPrint "Closing running application..."
|
||||
nsExec::ExecToLog 'taskkill /F /IM "${PRODUCT_FILENAME}.exe" /T'
|
||||
Pop $0
|
||||
nsExec::ExecToLog 'taskkill /F /IM "SC-IP-Agent.exe" /T'
|
||||
Pop $0
|
||||
nsExec::ExecToLog 'taskkill /F /IM "尚创IP智能体V8.exe" /T'
|
||||
Pop $0
|
||||
!macroend
|
||||
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
JumpCutter Wrapper Script for AIGCPanel
|
||||
This script wraps the jumpcutter Python library to integrate with AIGCPanel's video processing workflow.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
# Add jumpcutter to path if it exists in a subdirectory
|
||||
script_dir = Path(__file__).parent
|
||||
jumpcutter_path = script_dir / "jumpcutter"
|
||||
if jumpcutter_path.exists():
|
||||
sys.path.insert(0, str(jumpcutter_path))
|
||||
|
||||
# Try to import jumpcutter
|
||||
try:
|
||||
from jumpcutter.clip import Clip
|
||||
except ImportError as e:
|
||||
print(f"Error importing jumpcutter: {e}", file=sys.stderr)
|
||||
print("Please ensure jumpcutter is installed or the jumpcutter directory contains the required files", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="JumpCutter wrapper for AIGCPanel")
|
||||
|
||||
# Required arguments
|
||||
parser.add_argument("--input", "-i", required=True, help="Path to input video file")
|
||||
parser.add_argument("--output", "-o", required=True, help="Path to output video file")
|
||||
|
||||
# JumpCutter specific arguments
|
||||
parser.add_argument("--cut", "-c",
|
||||
choices=["silent", "voiced", "both"],
|
||||
default="silent",
|
||||
help="Parts to cut: silent, voiced, or both")
|
||||
|
||||
parser.add_argument("--magnitude-threshold-ratio", "-m",
|
||||
type=float,
|
||||
default=0.02,
|
||||
help="Audio signal magnitude threshold ratio")
|
||||
|
||||
parser.add_argument("--duration-threshold", "-d",
|
||||
type=float,
|
||||
default=0.5,
|
||||
help="Minimum duration of silence in seconds")
|
||||
|
||||
parser.add_argument("--failure-tolerance-ratio", "-f",
|
||||
type=float,
|
||||
default=0.1,
|
||||
help="Failure tolerance ratio")
|
||||
|
||||
parser.add_argument("--space-on-edges", "-s",
|
||||
type=float,
|
||||
default=0.1,
|
||||
help="Space to leave on edges in seconds")
|
||||
|
||||
parser.add_argument("--silence-part-speed", "-x",
|
||||
type=int,
|
||||
help="Speed up silent parts instead of cutting")
|
||||
|
||||
parser.add_argument("--min-loud-part-duration", "-l",
|
||||
type=int,
|
||||
default=-1,
|
||||
help="Minimum duration of loud parts in seconds")
|
||||
|
||||
parser.add_argument("--codec",
|
||||
type=str,
|
||||
help="Video codec")
|
||||
|
||||
parser.add_argument("--bitrate",
|
||||
type=str,
|
||||
help="Video bitrate")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
# Validate input file exists
|
||||
if not os.path.exists(args.input):
|
||||
print(f"Error: Input file does not exist: {args.input}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Ensure output directory exists
|
||||
output_dir = os.path.dirname(os.path.abspath(args.output))
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Create Clip object and process
|
||||
clip = Clip(
|
||||
args.input,
|
||||
min_loud_part_duration=args.min_loud_part_duration,
|
||||
silence_part_speed=args.silence_part_speed
|
||||
)
|
||||
|
||||
cuts = [args.cut] if args.cut != "both" else ["silent", "voiced"]
|
||||
|
||||
print(f"Processing video: {args.input}")
|
||||
print(f"Output: {args.output}")
|
||||
print(f"Cut mode: {args.cut}")
|
||||
print(f"Magnitude threshold ratio: {args.magnitude_threshold_ratio}")
|
||||
print(f"Duration threshold: {args.duration_threshold}s")
|
||||
print(f"Failure tolerance ratio: {args.failure_tolerance_ratio}")
|
||||
print(f"Space on edges: {args.space_on_edges}s")
|
||||
|
||||
if args.silence_part_speed:
|
||||
print(f"Silence part speed: {args.silence_part_speed}x")
|
||||
|
||||
if args.min_loud_part_duration > -1:
|
||||
print(f"Min loud part duration: {args.min_loud_part_duration}s")
|
||||
|
||||
# Process the video
|
||||
outputs = clip.jumpcut(
|
||||
cuts,
|
||||
args.magnitude_threshold_ratio,
|
||||
args.duration_threshold,
|
||||
args.failure_tolerance_ratio,
|
||||
args.space_on_edges
|
||||
)
|
||||
|
||||
# Save the results
|
||||
for cut_type, jumpcutted_clip in outputs.items():
|
||||
if len(outputs) == 2:
|
||||
output_path = f"{os.path.splitext(args.output)[0]}_{cut_type}_parts_cut{os.path.splitext(args.output)[1]}"
|
||||
else:
|
||||
output_path = args.output
|
||||
|
||||
kwargs = {}
|
||||
if args.codec:
|
||||
kwargs['codec'] = args.codec
|
||||
if args.bitrate:
|
||||
kwargs['bitrate'] = args.bitrate
|
||||
|
||||
print(f"Saving processed video: {output_path}")
|
||||
jumpcutted_clip.write_videofile(output_path, **kwargs)
|
||||
|
||||
# Create output stats file
|
||||
stats_file = f"{os.path.splitext(args.output)[0]}_stats.json"
|
||||
stats = {
|
||||
"input": args.input,
|
||||
"output": args.output,
|
||||
"cut_mode": args.cut,
|
||||
"params": {
|
||||
"magnitude_threshold_ratio": args.magnitude_threshold_ratio,
|
||||
"duration_threshold": args.duration_threshold,
|
||||
"failure_tolerance_ratio": args.failure_tolerance_ratio,
|
||||
"space_on_edges": args.space_on_edges,
|
||||
"silence_part_speed": args.silence_part_speed,
|
||||
"min_loud_part_duration": args.min_loud_part_duration,
|
||||
"codec": args.codec,
|
||||
"bitrate": args.bitrate
|
||||
}
|
||||
}
|
||||
|
||||
with open(stats_file, 'w') as f:
|
||||
json.dump(stats, f, indent=2)
|
||||
|
||||
print(f"Processing complete: {args.output}")
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error processing video: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 杀死占用特定端口的进程
|
||||
* 使用方式: node kill-port.js 5173
|
||||
*/
|
||||
|
||||
import { exec } from 'child_process';
|
||||
import { platform } from 'os';
|
||||
|
||||
const port = process.argv[2] || 5173;
|
||||
|
||||
function killPort(port) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (platform() === 'win32') {
|
||||
// Windows 系统
|
||||
exec(`netstat -ano | findstr :${port}`, (error, stdout, stderr) => {
|
||||
if (stdout) {
|
||||
const lines = stdout.trim().split('\n');
|
||||
lines.forEach(line => {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
const pid = parts[parts.length - 1];
|
||||
if (pid && pid !== 'PID') {
|
||||
exec(`taskkill /PID ${pid} /F`, (err) => {
|
||||
if (!err) {
|
||||
console.log(`✅ 已杀死进程 PID ${pid}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
setTimeout(resolve, 1000);
|
||||
});
|
||||
} else {
|
||||
// macOS/Linux 系统
|
||||
exec(`lsof -i :${port} | grep LISTEN | awk '{print $2}' | xargs kill -9`, (error) => {
|
||||
if (!error || error.code === 1) {
|
||||
// 即使出错也继续(可能是没有找到进程)
|
||||
console.log(`✅ 端口 ${port} 已检查`);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🔍 正在检查端口 ${port}...`);
|
||||
killPort(port)
|
||||
.then(() => {
|
||||
console.log(`✅ 准备启动应用...`);
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`⚠️ 错误:`, err);
|
||||
process.exit(0); // 即使出错也继续启动
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const configPath = path.join(__dirname, '../electron/config/default-templates.json');
|
||||
const content = fs.readFileSync(configPath, 'utf8');
|
||||
const config = JSON.parse(content);
|
||||
|
||||
console.log('Cover Templates:');
|
||||
config.coverTemplates.forEach((t, i) => {
|
||||
console.log(`[${i}] ID: ${t.id}, Name: ${t.name}, IsSystem: ${t.is_system}`);
|
||||
});
|
||||
|
||||
console.log('\nSubtitle Styles:');
|
||||
if (config.subtitleStyles) {
|
||||
config.subtitleStyles.forEach((s, i) => {
|
||||
console.log(`[${i}] ID: ${s.id}, Name: ${s.name}, IsSystem: ${s.is_system}`);
|
||||
});
|
||||
} else {
|
||||
console.log('No subtitleStyles found in config.');
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const JSON5 = require('json5');
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
const sourcePath = path.join(root, 'electron-builder.json5');
|
||||
const outputPath = path.join(root, 'tools', 'oem-batch-builder', 'electron-builder.oem.generated.json');
|
||||
|
||||
function isLargeBundledResource(entry) {
|
||||
const from = String(entry && entry.from || '').replace(/\\/g, '/');
|
||||
const to = String(entry && entry.to || '').replace(/\\/g, '/');
|
||||
return to.startsWith('resources-bundles/')
|
||||
|| from === 'ziti'
|
||||
|| from === 'bgm'
|
||||
|| from.includes('/common/playwright')
|
||||
|| from.includes('/common/python')
|
||||
|| from.includes('/common/models')
|
||||
|| from.includes('/common/fonts');
|
||||
}
|
||||
|
||||
const config = JSON5.parse(fs.readFileSync(sourcePath, 'utf8'));
|
||||
const oemCoverTemplateFiles = [
|
||||
'common/cover-templates/huangbai-shishi.jpg',
|
||||
'common/cover-templates/landi-baizi.jpg',
|
||||
'common/cover-templates/dasi-baobao.jpg',
|
||||
'common/cover-templates/xiehanghuangbai.jpg',
|
||||
'common/cover-templates/fenghong-xubai.jpg',
|
||||
'common/cover-templates/renyixujzhong.jpg',
|
||||
'common/cover-templates/qinglan-xushi.png',
|
||||
'common/cover-templates/honghang-xushi.png',
|
||||
'common/cover-templates/baihang-xushi.png',
|
||||
];
|
||||
for (const templateId of ['11', '22', '33', '44', '55', '66', '77', '88']) {
|
||||
oemCoverTemplateFiles.push(`common/cover-templates/subtitle-preview-template_system_${templateId}.png`);
|
||||
oemCoverTemplateFiles.push(`common/cover-templates/subtitle-preview-template_system_classic_${templateId}.png`);
|
||||
}
|
||||
const oemAsarExcludes = [
|
||||
'!node_modules/date-fns{,/**/*}',
|
||||
'!node_modules/fluent-ffmpeg{,/**/*}',
|
||||
'!node_modules/ttf2woff2{,/**/*}',
|
||||
'!node_modules/vue-timeago3{,/**/*}',
|
||||
'!node_modules/pdfkit{,/**/*}',
|
||||
'!node_modules/fontkit{,/**/*}',
|
||||
'!node_modules/@resvg{,/**/*}',
|
||||
'!node_modules/moment{,/**/*}',
|
||||
'!node_modules/tldts{,/**/*}',
|
||||
'!node_modules/polished{,/**/*}',
|
||||
'!node_modules/caniuse-lite{,/**/*}',
|
||||
'!node_modules/csso{,/**/*}',
|
||||
'!node_modules/cheerio{,/**/*}',
|
||||
'!node_modules/svgo{,/**/*}',
|
||||
'!node_modules/@jimp{,/**/*}',
|
||||
'!node_modules/jimp{,/**/*}',
|
||||
'!node_modules/lit{,/**/*}',
|
||||
'!node_modules/emoji-mart{,/**/*}',
|
||||
'!node_modules/@mswjs{,/**/*}',
|
||||
'!node_modules/graphql{,/**/*}',
|
||||
'!node_modules/mermaid{,/**/*}',
|
||||
'!node_modules/@lobehub{,/**/*}',
|
||||
'!node_modules/@emoji-mart{,/**/*}',
|
||||
'!node_modules/@arco-design{,/**/*}',
|
||||
'!node_modules/lucide-react{,/**/*}',
|
||||
'!node_modules/typescript{,/**/*}',
|
||||
'!node_modules/@shikijs{,/**/*}',
|
||||
'!node_modules/@vue{,/**/*}',
|
||||
'!node_modules/@babel{,/**/*}',
|
||||
'!node_modules/@mermaid-js{,/**/*}',
|
||||
'!node_modules/@splinetool{,/**/*}',
|
||||
'!node_modules/concurrently{,/**/*}',
|
||||
'!node_modules/tailwindcss{,/**/*}',
|
||||
'!node_modules/msw{,/**/*}',
|
||||
'!node_modules/cytoscape{,/**/*}',
|
||||
'!node_modules/cytoscape-fcose{,/**/*}',
|
||||
'!node_modules/framer-motion{,/**/*}',
|
||||
'!node_modules/katex{,/**/*}',
|
||||
'!node_modules/rxjs{,/**/*}',
|
||||
'!node_modules/langium{,/**/*}',
|
||||
'!node_modules/openai{,/**/*}',
|
||||
'!node_modules/@emotion{,/**/*}',
|
||||
'!node_modules/d3{,/**/*}',
|
||||
'!node_modules/vue{,/**/*}',
|
||||
'!node_modules/vue-router{,/**/*}',
|
||||
'!node_modules/vue-i18n{,/**/*}',
|
||||
'!node_modules/pinia{,/**/*}',
|
||||
'!node_modules/xgplayer{,/**/*}',
|
||||
'!node_modules/wavesurfer.js{,/**/*}',
|
||||
'!node_modules/showdown{,/**/*}',
|
||||
'!node_modules/dompurify{,/**/*}',
|
||||
'!node_modules/file-saver{,/**/*}',
|
||||
'!node_modules/qrcode{,/**/*}',
|
||||
'!node_modules/canvg{,/**/*}',
|
||||
'!node_modules/svg2img{,/**/*}',
|
||||
'!node_modules/svgtofont{,/**/*}',
|
||||
'!node_modules/mddir{,/**/*}',
|
||||
'!node_modules/@types{,/**/*}',
|
||||
'!node_modules/vite{,/**/*}',
|
||||
'!node_modules/vite-plugin-electron{,/**/*}',
|
||||
'!node_modules/vite-plugin-electron-renderer{,/**/*}',
|
||||
'!node_modules/@vitejs{,/**/*}',
|
||||
'!node_modules/vue-tsc{,/**/*}',
|
||||
'!node_modules/less{,/**/*}',
|
||||
'!node_modules/postcss{,/**/*}',
|
||||
'!node_modules/autoprefixer{,/**/*}',
|
||||
'!node_modules/sass-embedded{,/**/*}',
|
||||
'!node_modules/sass-embedded-win32-x64{,/**/*}',
|
||||
'!node_modules/electron-builder{,/**/*}',
|
||||
'!node_modules/app-builder-bin{,/**/*}',
|
||||
'!node_modules/7zip-bin{,/**/*}',
|
||||
'!node_modules/wait-on{,/**/*}',
|
||||
];
|
||||
|
||||
if (config.win && Array.isArray(config.win.extraResources)) {
|
||||
config.win.extraResources = config.win.extraResources
|
||||
.filter(entry => !isLargeBundledResource(entry))
|
||||
.map(entry => {
|
||||
const from = String(entry && entry.from || '').replace(/\\/g, '/');
|
||||
if (from.endsWith('electron/resources/extra') && Array.isArray(entry.filter)) {
|
||||
return {
|
||||
...entry,
|
||||
filter: entry.filter
|
||||
.filter(pattern => String(pattern).replace(/\\/g, '/') !== 'common/config/**/*')
|
||||
.filter(pattern => String(pattern).replace(/\\/g, '/') !== 'common/cover-templates/**/*')
|
||||
.filter(pattern => !String(pattern).replace(/\\/g, '/').startsWith('win-x86'))
|
||||
.concat([
|
||||
'common/config/default-templates.json',
|
||||
'common/config/default-subtitle-templates.json',
|
||||
'common/config/preset_templates.json',
|
||||
'common/config/default-ipagent-config.json',
|
||||
'common/config/system-templates.json',
|
||||
'common/python-scripts/**/*',
|
||||
'common/soundEffects/**/*',
|
||||
'stickers/**/*',
|
||||
])
|
||||
.concat(oemCoverTemplateFiles)
|
||||
};
|
||||
}
|
||||
if (from.endsWith('electron/resources/build') && Array.isArray(entry.filter)) {
|
||||
return {
|
||||
...entry,
|
||||
filter: entry.filter.filter(pattern => String(pattern).replace(/\\/g, '/') !== 'vc_redist.x64.exe'),
|
||||
};
|
||||
}
|
||||
return entry;
|
||||
});
|
||||
}
|
||||
|
||||
if (Array.isArray(config.files)) {
|
||||
config.files = config.files
|
||||
.filter(entry => !(typeof entry === 'string' && entry.replace(/\\/g, '/') === 'node_modules'))
|
||||
.concat(oemAsarExcludes);
|
||||
}
|
||||
|
||||
if (config.win) {
|
||||
config.win.target = [
|
||||
{
|
||||
target: 'dir',
|
||||
arch: ['x64'],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (config.nsis) {
|
||||
config.nsis.differentialPackage = false;
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
||||
fs.writeFileSync(outputPath, JSON.stringify(config, null, 2), 'utf8');
|
||||
console.log(`[oem-builder-config] wrote ${outputPath}`);
|
||||
@@ -0,0 +1,52 @@
|
||||
const {notarize} = require("@electron/notarize");
|
||||
const common = require('./common.cjs')
|
||||
|
||||
exports.default = async function notarizing(context) {
|
||||
const appName = context.packager.appInfo.productFilename;
|
||||
const {electronPlatformName, appOutDir} = context;
|
||||
console.log(` • Notarization Start`);
|
||||
// We skip notarization if the process is not running on MacOS and
|
||||
// if the enviroment variable SKIP_NOTARIZE is set to `true`
|
||||
// This is useful for local testing where notarization is useless
|
||||
if (
|
||||
electronPlatformName !== "darwin" ||
|
||||
process.env.SKIP_NOTARIZE === "true"
|
||||
) {
|
||||
console.log(` • Skipping notarization`);
|
||||
return;
|
||||
}
|
||||
|
||||
// THIS MUST BE THE SAME AS THE `appId` property
|
||||
// in your electron builder configuration
|
||||
const appId = "AigcPanel";
|
||||
|
||||
let appPath = `${appOutDir}/${appName}.app`;
|
||||
let {APPLE_ID, APPLE_ID_PASSWORD, APPLE_TEAM_ID} = process.env;
|
||||
if (!APPLE_ID) {
|
||||
console.info(" • Notarization ignore: APPLE_ID is empty");
|
||||
await common.calcSha256()
|
||||
return;
|
||||
}
|
||||
const notarizeOption = {
|
||||
tool: "notarytool",
|
||||
appBundleId: appId,
|
||||
appPath,
|
||||
appleId: APPLE_ID,
|
||||
appleIdPassword: APPLE_ID_PASSWORD,
|
||||
teamId: APPLE_TEAM_ID,
|
||||
verbose: true,
|
||||
}
|
||||
console.log(` • Notarizing`, `appPath:${appPath} notarizeOption:${JSON.stringify(notarizeOption)}`);
|
||||
try {
|
||||
const result = await notarize(notarizeOption);
|
||||
console.log(" • Notarization successful!");
|
||||
await common.calcSha256()
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(" • Notarization failed:", error.message);
|
||||
console.error(" • Stack trace:", error.stack);
|
||||
await common.calcSha256()
|
||||
throw new Error(`Notarization failed: ${error.message}`);
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,346 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { syncSystemSubtitleSources } = require('./sync-system-subtitle-sources.cjs');
|
||||
|
||||
async function preparePackage() {
|
||||
console.log('📦 开始准备打包资源...\n');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
const extraDir = path.join(__dirname, '../electron/resources/extra');
|
||||
const commonDir = path.join(extraDir, 'common');
|
||||
|
||||
// 0. 发布前同步系统字幕模板,避免安装包带入旧候选模板或旧音效配置
|
||||
console.log('\n0锔忊儯 鍚屾绯荤粺瀛楀箷妯℃澘...');
|
||||
const syncResult = syncSystemSubtitleSources();
|
||||
console.log(` 鉁?宸插悓姝? ${syncResult.count} 涓郴缁熷瓧骞曟ā鏉垮埌鍙戝竷閰嶇疆\n`);
|
||||
|
||||
// 1. 确保目录存在
|
||||
console.log('\n1️⃣ 确保目录结构存在...');
|
||||
await fs.ensureDir(path.join(commonDir, 'python'));
|
||||
await fs.ensureDir(path.join(commonDir, 'chromium'));
|
||||
await fs.ensureDir(path.join(commonDir, 'python-scripts'));
|
||||
await fs.ensureDir(path.join(commonDir, 'models'));
|
||||
await fs.ensureDir(path.join(commonDir, 'fonts'));
|
||||
await fs.ensureDir(path.join(commonDir, 'bgm'));
|
||||
await fs.ensureDir(path.join(commonDir, 'demo'));
|
||||
await fs.ensureDir(path.join(commonDir, 'config'));
|
||||
await fs.ensureDir(path.join(commonDir, 'cover-templates'));
|
||||
await fs.ensureDir(path.join(extraDir, 'win-x86'));
|
||||
console.log(' ✅ 目录结构准备完成\n');
|
||||
|
||||
// 2. 复制 Python 脚本
|
||||
console.log('2️⃣ 复制 Python 脚本...');
|
||||
const pythonSrc = path.join(__dirname, '../python');
|
||||
const pythonDest = path.join(commonDir, 'python-scripts');
|
||||
const pythonFilter = (src) => {
|
||||
const normalizedSrc = src.replace(/\\/g, '/');
|
||||
const fileName = path.basename(normalizedSrc).toLowerCase();
|
||||
|
||||
if (normalizedSrc.includes('/__pycache__/') || fileName.endsWith('.pyc')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
fileName.includes('.backup') ||
|
||||
fileName.endsWith('.bak') ||
|
||||
fileName.includes('backup_') ||
|
||||
fileName.startsWith('test_')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
if (fs.existsSync(pythonSrc)) {
|
||||
await copyWithLockTolerance(pythonSrc, pythonDest, {
|
||||
overwrite: true,
|
||||
filter: pythonFilter
|
||||
});
|
||||
const fileCount = await countFiles(pythonDest);
|
||||
console.log(` ✅ Python 脚本复制到 python-scripts 完成 (${fileCount} 个文件)`);
|
||||
|
||||
const pythonRuntimeDir = path.join(commonDir, 'python');
|
||||
await copyWithLockTolerance(pythonSrc, pythonRuntimeDir, {
|
||||
overwrite: true,
|
||||
filter: pythonFilter
|
||||
});
|
||||
console.log(` ✅ Python 脚本复制到 python 运行时目录完成\n`);
|
||||
} else {
|
||||
console.log(' ⚠️ Python 脚本目录不存在,跳过\n');
|
||||
}
|
||||
|
||||
// 3. 复制字体
|
||||
console.log('3️⃣ 复制字体文件...');
|
||||
const fontSrc = path.join(__dirname, '../ziti');
|
||||
const fontDest = path.join(commonDir, 'fonts/ziti');
|
||||
|
||||
if (fs.existsSync(fontSrc)) {
|
||||
if (process.env.OEM_BATCH_BUILD === '1' && await fs.pathExists(fontDest)) {
|
||||
const existingFileCount = await countFiles(fontDest);
|
||||
if (existingFileCount > 0) {
|
||||
console.log(` [SKIP] OEM batch build reuses prepared fonts (${existingFileCount} files)\n`);
|
||||
} else {
|
||||
await copyWithLockTolerance(fontSrc, fontDest, { overwrite: true });
|
||||
}
|
||||
} else {
|
||||
const sourceFileCount = await countFiles(fontSrc);
|
||||
const sourceTotalSize = await getDirSize(fontSrc);
|
||||
const destReady = await fs.pathExists(fontDest)
|
||||
&& await countFiles(fontDest) === sourceFileCount
|
||||
&& await getDirSize(fontDest) === sourceTotalSize;
|
||||
|
||||
if (destReady) {
|
||||
console.log(` [SKIP] Fonts already prepared (${sourceFileCount} files, ${(sourceTotalSize / 1024 / 1024).toFixed(2)} MB)\n`);
|
||||
} else {
|
||||
await copyWithLockTolerance(fontSrc, fontDest, { overwrite: true });
|
||||
}
|
||||
}
|
||||
const fileCount = await countFiles(fontDest);
|
||||
const totalSize = await getDirSize(fontDest);
|
||||
console.log(` ✅ 字体复制完成 (${fileCount} 个文件, ${(totalSize / 1024 / 1024).toFixed(2)} MB)\n`);
|
||||
} else {
|
||||
console.log(' ⚠️ 字体目录不存在,跳过\n');
|
||||
}
|
||||
|
||||
// 4. 复制 BGM
|
||||
console.log('4️⃣ 复制背景音乐...');
|
||||
const bgmSrc = path.join(__dirname, '../bgm');
|
||||
const bgmDest = path.join(commonDir, 'bgm');
|
||||
|
||||
if (fs.existsSync(bgmSrc)) {
|
||||
if (process.env.OEM_BATCH_BUILD === '1' && await fs.pathExists(bgmDest)) {
|
||||
const existingFileCount = await countFiles(bgmDest);
|
||||
if (existingFileCount > 0) {
|
||||
console.log(` [SKIP] OEM batch build reuses prepared BGM (${existingFileCount} files)\n`);
|
||||
} else {
|
||||
await copyWithLockTolerance(bgmSrc, bgmDest, { overwrite: true });
|
||||
}
|
||||
} else {
|
||||
await copyWithLockTolerance(bgmSrc, bgmDest, { overwrite: true });
|
||||
}
|
||||
const fileCount = await countFiles(bgmDest);
|
||||
const totalSize = await getDirSize(bgmDest);
|
||||
console.log(` ✅ BGM 复制完成 (${fileCount} 个文件, ${(totalSize / 1024 / 1024).toFixed(2)} MB)\n`);
|
||||
} else {
|
||||
console.log(' ⚠️ BGM 目录不存在,跳过\n');
|
||||
}
|
||||
|
||||
// 5. 复制 Demo
|
||||
console.log('5️⃣ 复制演示文件...');
|
||||
const demoSrc = path.join(__dirname, '../demo');
|
||||
const demoDest = path.join(commonDir, 'demo');
|
||||
|
||||
if (fs.existsSync(demoSrc)) {
|
||||
if (process.env.OEM_BATCH_BUILD === '1' && await fs.pathExists(demoDest)) {
|
||||
const existingFileCount = await countFiles(demoDest);
|
||||
if (existingFileCount > 0) {
|
||||
console.log(` [SKIP] OEM batch build reuses prepared Demo (${existingFileCount} files)\n`);
|
||||
} else {
|
||||
await copyWithLockTolerance(demoSrc, demoDest, { overwrite: true });
|
||||
}
|
||||
} else {
|
||||
await copyWithLockTolerance(demoSrc, demoDest, { overwrite: true });
|
||||
}
|
||||
const fileCount = await countFiles(demoDest);
|
||||
const totalSize = await getDirSize(demoDest);
|
||||
console.log(` ✅ Demo 复制完成 (${fileCount} 个文件, ${(totalSize / 1024 / 1024).toFixed(2)} MB)\n`);
|
||||
} else {
|
||||
console.log(' ⚠️ Demo 目录不存在,跳过\n');
|
||||
}
|
||||
|
||||
// 6. 复制配置文件
|
||||
console.log('6️⃣ 复制配置文件...');
|
||||
const configDest = path.join(commonDir, 'config');
|
||||
|
||||
const configFiles = [
|
||||
{
|
||||
src: path.join(__dirname, '../electron/config/default-templates.json'),
|
||||
dest: path.join(configDest, 'default-templates.json'),
|
||||
name: 'default-templates.json'
|
||||
},
|
||||
{
|
||||
src: path.join(__dirname, '../electron/config/default-subtitle-templates.json'),
|
||||
dest: path.join(configDest, 'default-subtitle-templates.json'),
|
||||
name: 'default-subtitle-templates.json'
|
||||
},
|
||||
{
|
||||
src: path.join(__dirname, '../python/preset_templates.json'),
|
||||
dest: path.join(configDest, 'preset_templates.json'),
|
||||
name: 'preset_templates.json'
|
||||
},
|
||||
{
|
||||
src: path.join(__dirname, '../electron/config/default-ipagent-config.json'),
|
||||
dest: path.join(configDest, 'default-ipagent-config.json'),
|
||||
name: 'default-ipagent-config.json'
|
||||
},
|
||||
{
|
||||
src: path.join(__dirname, '../electron/resources/extra/common/config/system-templates.json'),
|
||||
dest: path.join(configDest, 'system-templates.json'),
|
||||
name: 'system-templates.json (系统模板配置 - 8套字幕+8套封面)'
|
||||
}
|
||||
];
|
||||
|
||||
let copiedCount = 0;
|
||||
for (const file of configFiles) {
|
||||
if (fs.existsSync(file.src)) {
|
||||
const samePath = path.resolve(file.src) === path.resolve(file.dest);
|
||||
if (samePath) {
|
||||
const size = (await fs.stat(file.src)).size;
|
||||
console.log(` ℹ️ ${file.name} 已在目标位置 (${(size / 1024).toFixed(2)} KB)`);
|
||||
copiedCount++;
|
||||
continue;
|
||||
}
|
||||
await copyWithLockTolerance(file.src, file.dest, { overwrite: true });
|
||||
const size = (await fs.stat(file.dest)).size;
|
||||
console.log(` ✅ ${file.name} (${(size / 1024).toFixed(2)} KB)`);
|
||||
copiedCount++;
|
||||
} else {
|
||||
console.log(` ⚠️ ${file.name} - 源文件不存在`);
|
||||
}
|
||||
}
|
||||
console.log(` 📊 配置文件复制完成 (${copiedCount}/${configFiles.length})\n`);
|
||||
|
||||
// 7. 检查外部依赖
|
||||
console.log('7️⃣ 检查外部依赖...');
|
||||
|
||||
// FFmpeg DLL 文件列表
|
||||
const ffmpegDlls = [
|
||||
'avcodec-61.dll',
|
||||
'avdevice-61.dll',
|
||||
'avfilter-10.dll',
|
||||
'avformat-61.dll',
|
||||
'avutil-59.dll',
|
||||
'postproc-58.dll',
|
||||
'swresample-5.dll',
|
||||
'swscale-8.dll'
|
||||
];
|
||||
|
||||
// 动态查找最新的 Playwright Chromium 版本
|
||||
const playwrightDir = path.join(commonDir, 'playwright');
|
||||
let playwrightChromiumCheck = null;
|
||||
if (await fs.pathExists(playwrightDir)) {
|
||||
const chromiumDirs = (await fs.readdir(playwrightDir))
|
||||
.filter(f => f.startsWith('chromium-'))
|
||||
.sort()
|
||||
.reverse();
|
||||
if (chromiumDirs.length > 0) {
|
||||
const latestChromium = chromiumDirs[0];
|
||||
const chromePath = path.join(playwrightDir, latestChromium, 'chrome-win64/chrome.exe');
|
||||
const chromePathAlt = path.join(playwrightDir, latestChromium, 'chrome-win/chrome.exe');
|
||||
playwrightChromiumCheck = {
|
||||
name: `Playwright Chromium (${latestChromium})`,
|
||||
path: await fs.pathExists(chromePath) ? chromePath : chromePathAlt
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const checks = [
|
||||
{ name: 'Python 环境', path: path.join(commonDir, 'python/python.exe') },
|
||||
{ name: 'Python 依赖 (opencv)', path: path.join(commonDir, 'python/Lib/site-packages/cv2') },
|
||||
{ name: 'FFmpeg (win-x86)', path: path.join(extraDir, 'win-x86/ffmpeg.exe') },
|
||||
{ name: 'FFprobe (win-x86)', path: path.join(extraDir, 'win-x86/ffprobe.exe') },
|
||||
...ffmpegDlls.map(dll => ({ name: `FFmpeg DLL (${dll})`, path: path.join(extraDir, `win-x86/${dll}`) })),
|
||||
...(playwrightChromiumCheck ? [playwrightChromiumCheck] : []),
|
||||
{ name: 'U2-Net 模型', path: path.join(commonDir, 'models/u2net.onnx') },
|
||||
];
|
||||
|
||||
let allOk = true;
|
||||
for (const check of checks) {
|
||||
const exists = await fs.pathExists(check.path);
|
||||
if (exists) {
|
||||
console.log(` ✅ ${check.name}`);
|
||||
} else {
|
||||
console.log(` ❌ ${check.name} - 缺失`);
|
||||
allOk = false;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n' + '='.repeat(60));
|
||||
|
||||
if (!allOk) {
|
||||
console.log('⚠️ 警告:某些外部依赖缺失');
|
||||
console.log('\n请先运行: node scripts/download-dependencies.js');
|
||||
console.log('参考文档: C:\\aigcpanel-main\\打包方案.md 的 "阶段 1: 准备外部依赖"\n');
|
||||
console.log('='.repeat(60));
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('✅ 所有资源准备完成!可以开始打包了');
|
||||
console.log('\n下一步:');
|
||||
console.log(' npm run build:win # 开始打包 Windows 版本');
|
||||
console.log(' npm run build:mac # 开始打包 macOS 版本');
|
||||
console.log('\n📝 字幕模板说明:');
|
||||
console.log(' 系统默认字幕模板配置:');
|
||||
console.log(' • 位置: electron/config/default-subtitle-templates.json');
|
||||
console.log(' • 功能: 包含所有参数(普通字幕、关键词特效、标题字幕、音效)');
|
||||
console.log('\n导出字幕模板功能:');
|
||||
console.log(' • 用户在应用中导出字幕模板为 JSON 文件');
|
||||
console.log(' • 其他用户可通过导入功能导入这些 JSON 文件');
|
||||
console.log(' • 支持备份、分享、版本控制');
|
||||
console.log('\n='.repeat(60));
|
||||
}
|
||||
}
|
||||
|
||||
async function copyWithLockTolerance(src, dest, options = {}) {
|
||||
try {
|
||||
await fs.copy(src, dest, options);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error && error.code === 'EPERM') {
|
||||
console.warn(` ⚠️ 跳过被占用的复制目标: ${dest}`);
|
||||
console.warn(` 原因: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数:统计文件数量
|
||||
async function countFiles(dir) {
|
||||
let count = 0;
|
||||
|
||||
async function walk(directory) {
|
||||
const files = await fs.readdir(directory);
|
||||
for (const file of files) {
|
||||
const filePath = path.join(directory, file);
|
||||
const stat = await fs.stat(filePath);
|
||||
if (stat.isDirectory()) {
|
||||
await walk(filePath);
|
||||
} else {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (await fs.pathExists(dir)) {
|
||||
await walk(dir);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
// 辅助函数:计算目录大小
|
||||
async function getDirSize(dir) {
|
||||
let size = 0;
|
||||
|
||||
async function walk(directory) {
|
||||
const files = await fs.readdir(directory);
|
||||
for (const file of files) {
|
||||
const filePath = path.join(directory, file);
|
||||
const stat = await fs.stat(filePath);
|
||||
if (stat.isDirectory()) {
|
||||
await walk(filePath);
|
||||
} else {
|
||||
size += stat.size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (await fs.pathExists(dir)) {
|
||||
await walk(dir);
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
preparePackage().catch(console.error);
|
||||
@@ -0,0 +1,100 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
const buildDir = path.join(root, 'dist-release-final');
|
||||
const releaseAssetsDir = path.join(buildDir, 'release-assets');
|
||||
const packageJson = require(path.join(root, 'package.json'));
|
||||
|
||||
const currentVersion = packageJson.version;
|
||||
const currentInstallerName = `SC-IP-Agent-${currentVersion}-win-setup-x64.exe`;
|
||||
const currentBlockmapName = `${currentInstallerName}.blockmap`;
|
||||
|
||||
function ensurePathExists(filePath) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`Missing required path: ${filePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateReleaseArtifacts() {
|
||||
console.log(`[prepare-release] Current version: ${currentVersion}`);
|
||||
|
||||
ensurePathExists(buildDir);
|
||||
ensurePathExists(releaseAssetsDir);
|
||||
ensurePathExists(path.join(buildDir, currentInstallerName));
|
||||
ensurePathExists(path.join(buildDir, currentBlockmapName));
|
||||
ensurePathExists(path.join(buildDir, 'latest.yml'));
|
||||
ensurePathExists(path.join(buildDir, 'resource-manifest.json'));
|
||||
ensurePathExists(path.join(releaseAssetsDir, currentInstallerName));
|
||||
ensurePathExists(path.join(releaseAssetsDir, currentBlockmapName));
|
||||
|
||||
const latestYmlContent = fs.readFileSync(path.join(buildDir, 'latest.yml'), 'utf8');
|
||||
if (!latestYmlContent.includes(`version: ${currentVersion}`)) {
|
||||
throw new Error(`latest.yml version mismatch, expected ${currentVersion}`);
|
||||
}
|
||||
|
||||
console.log('[prepare-release] Required artifacts are present');
|
||||
}
|
||||
|
||||
function listDifferentialArtifacts() {
|
||||
return fs.readdirSync(releaseAssetsDir)
|
||||
.filter(name => {
|
||||
return /^SC-IP-Agent-\d+\.\d+\.\d+-win-setup-x64\.exe(\.blockmap)?$/.test(name) || name === 'latest.yml';
|
||||
})
|
||||
.sort();
|
||||
}
|
||||
|
||||
function listHistoricalBlockmaps() {
|
||||
return fs.readdirSync(releaseAssetsDir)
|
||||
.filter(name => /^SC-IP-Agent-\d+\.\d+\.\d+-win-setup-x64\.exe\.blockmap$/.test(name))
|
||||
.sort();
|
||||
}
|
||||
|
||||
function listResourceArtifacts() {
|
||||
return fs.readdirSync(releaseAssetsDir)
|
||||
.filter(name => name.endsWith('.zip') || name === 'resource-manifest.json' || name === 'changed-bundles.json')
|
||||
.sort();
|
||||
}
|
||||
|
||||
function printSummary() {
|
||||
const installerPath = path.join(buildDir, currentInstallerName);
|
||||
const installerSizeMb = (fs.statSync(installerPath).size / 1024 / 1024).toFixed(2);
|
||||
const differentialArtifacts = listDifferentialArtifacts();
|
||||
const historicalBlockmaps = listHistoricalBlockmaps();
|
||||
const resourceArtifacts = listResourceArtifacts();
|
||||
|
||||
if (differentialArtifacts.length === 0) {
|
||||
throw new Error('No SC-IP-Agent differential artifacts found in release-assets');
|
||||
}
|
||||
|
||||
console.log('\n[prepare-release] ========== RELEASE SUMMARY ==========');
|
||||
console.log(`Version: ${currentVersion}`);
|
||||
console.log(`Installer: ${currentInstallerName}`);
|
||||
console.log(`Installer size: ${installerSizeMb} MB`);
|
||||
|
||||
console.log('\nUpload to COS updates/ for this release:');
|
||||
console.log(` - ${currentInstallerName}`);
|
||||
console.log(` - ${currentBlockmapName}`);
|
||||
console.log(' - latest.yml');
|
||||
|
||||
console.log('\nKeep on COS for differential installer updates:');
|
||||
historicalBlockmaps.forEach(name => console.log(` - ${name}`));
|
||||
|
||||
console.log('\nUpload resource delta files when changed:');
|
||||
resourceArtifacts.forEach(name => console.log(` - ${name}`));
|
||||
|
||||
console.log('\nStorage policy:');
|
||||
console.log(' - Historical SC-IP-Agent *.exe files can be deleted from COS if you do not offer old installers for manual download.');
|
||||
console.log(' - Historical SC-IP-Agent *.exe.blockmap files should be kept for the versions you still want to support differential updates from.');
|
||||
console.log(' - If a client is too old and its blockmap was removed, that client will fall back to a full installer download once.');
|
||||
console.log('[prepare-release] ====================================\n');
|
||||
}
|
||||
|
||||
try {
|
||||
validateReleaseArtifacts();
|
||||
printSummary();
|
||||
console.log('[prepare-release] Release preparation complete');
|
||||
} catch (error) {
|
||||
console.error('[prepare-release] ERROR:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* 批量缩小字幕模板的字体大小
|
||||
* 包括:标题字幕和关键词字幕
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const configPath = path.join(__dirname, '../electron/resources/extra/common/config/system-templates.json');
|
||||
|
||||
console.log('📖 读取配置文件:', configPath);
|
||||
|
||||
// 读取配置文件
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
||||
|
||||
console.log(`✅ 找到 ${config.subtitleTemplates.length} 个字幕模板\n`);
|
||||
|
||||
// 字体缩放比例
|
||||
const SCALE_RATIO = 0.7; // 缩小到原来的 70%
|
||||
|
||||
let totalChanges = 0;
|
||||
|
||||
// 遍历所有字幕模板
|
||||
config.subtitleTemplates.forEach((template, index) => {
|
||||
console.log(`\n📝 处理模板 ${index + 1}: ${template.name}`);
|
||||
|
||||
const cfg = template.config;
|
||||
let changes = 0;
|
||||
|
||||
// 1. 调整基础字幕样式的字体大小
|
||||
if (cfg.subtitleStyle && cfg.subtitleStyle.fontSize) {
|
||||
const oldSize = cfg.subtitleStyle.fontSize;
|
||||
const newSize = Math.round(oldSize * SCALE_RATIO);
|
||||
cfg.subtitleStyle.fontSize = newSize;
|
||||
console.log(` - 基础字幕: ${oldSize} → ${newSize}`);
|
||||
changes++;
|
||||
}
|
||||
|
||||
// 2. 调整标题字幕的字体大小
|
||||
if (cfg.titleSubtitleConfig && cfg.titleSubtitleConfig.lines) {
|
||||
cfg.titleSubtitleConfig.lines.forEach((line, lineIndex) => {
|
||||
if (line.fontSize) {
|
||||
const oldSize = line.fontSize;
|
||||
const newSize = Math.round(oldSize * SCALE_RATIO);
|
||||
line.fontSize = newSize;
|
||||
console.log(` - 标题行 ${lineIndex + 1}: ${oldSize} → ${newSize}`);
|
||||
changes++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 3. 调整关键词字幕的字体大小
|
||||
if (cfg.keywordGroupsStyles) {
|
||||
cfg.keywordGroupsStyles.forEach((group) => {
|
||||
if (group.styleOverride && group.styleOverride.fontSize) {
|
||||
const oldSize = group.styleOverride.fontSize;
|
||||
const newSize = Math.round(oldSize * SCALE_RATIO);
|
||||
group.styleOverride.fontSize = newSize;
|
||||
console.log(` - 关键词"${group.groupName}": ${oldSize} → ${newSize}`);
|
||||
changes++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 4. 调整描边宽度(字体变小后,描边也应该相应变细)
|
||||
if (cfg.subtitleStyle && cfg.subtitleStyle.outlineWidth) {
|
||||
const oldWidth = cfg.subtitleStyle.outlineWidth;
|
||||
const newWidth = Math.max(1, Math.round(oldWidth * SCALE_RATIO));
|
||||
cfg.subtitleStyle.outlineWidth = newWidth;
|
||||
console.log(` - 基础描边: ${oldWidth} → ${newWidth}`);
|
||||
changes++;
|
||||
}
|
||||
|
||||
if (cfg.titleSubtitleConfig && cfg.titleSubtitleConfig.lines) {
|
||||
cfg.titleSubtitleConfig.lines.forEach((line, lineIndex) => {
|
||||
if (line.outlineWidth) {
|
||||
const oldWidth = line.outlineWidth;
|
||||
const newWidth = Math.max(1, Math.round(oldWidth * SCALE_RATIO));
|
||||
line.outlineWidth = newWidth;
|
||||
console.log(` - 标题行 ${lineIndex + 1} 描边: ${oldWidth} → ${newWidth}`);
|
||||
changes++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (cfg.keywordGroupsStyles) {
|
||||
cfg.keywordGroupsStyles.forEach((group) => {
|
||||
if (group.styleOverride && group.styleOverride.outlineWidth) {
|
||||
const oldWidth = group.styleOverride.outlineWidth;
|
||||
const newWidth = Math.max(1, Math.round(oldWidth * SCALE_RATIO));
|
||||
group.styleOverride.outlineWidth = newWidth;
|
||||
console.log(` - 关键词"${group.groupName}" 描边: ${oldWidth} → ${newWidth}`);
|
||||
changes++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log(` ✅ 共修改 ${changes} 处`);
|
||||
totalChanges += changes;
|
||||
});
|
||||
|
||||
// 备份原文件
|
||||
const backupPath = configPath + '.backup-' + Date.now();
|
||||
fs.copyFileSync(configPath, backupPath);
|
||||
console.log(`\n💾 已备份原文件到: ${backupPath}`);
|
||||
|
||||
// 保存修改后的配置
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
console.log(`\n✅ 已保存修改后的配置文件`);
|
||||
console.log(`📊 总共修改了 ${totalChanges} 处字体大小和描边宽度`);
|
||||
console.log(`📏 缩放比例: ${SCALE_RATIO * 100}%`);
|
||||
|
||||
console.log('\n🎯 下一步操作:');
|
||||
console.log('1. 重启应用,系统会自动同步新的字体大小到数据库');
|
||||
console.log('2. 如果需要恢复,可以从备份文件恢复');
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 进一步缩小标题字幕的字体大小
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const configPath = path.join(__dirname, '../electron/resources/extra/common/config/system-templates.json');
|
||||
|
||||
console.log('📖 读取配置文件:', configPath);
|
||||
|
||||
// 读取配置文件
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
||||
|
||||
console.log(`✅ 找到 ${config.subtitleTemplates.length} 个字幕模板\n`);
|
||||
|
||||
// 标题字幕缩放比例(在当前基础上再缩小)
|
||||
const TITLE_SCALE_RATIO = 0.75; // 缩小到当前的 75%
|
||||
|
||||
let totalChanges = 0;
|
||||
|
||||
// 遍历所有字幕模板
|
||||
config.subtitleTemplates.forEach((template, index) => {
|
||||
console.log(`\n📝 处理模板 ${index + 1}: ${template.name}`);
|
||||
|
||||
const cfg = template.config;
|
||||
let changes = 0;
|
||||
|
||||
// 只调整标题字幕的字体大小
|
||||
if (cfg.titleSubtitleConfig && cfg.titleSubtitleConfig.lines) {
|
||||
cfg.titleSubtitleConfig.lines.forEach((line, lineIndex) => {
|
||||
if (line.fontSize) {
|
||||
const oldSize = line.fontSize;
|
||||
const newSize = Math.round(oldSize * TITLE_SCALE_RATIO);
|
||||
line.fontSize = newSize;
|
||||
console.log(` - 标题行 ${lineIndex + 1}: ${oldSize} → ${newSize}`);
|
||||
changes++;
|
||||
}
|
||||
|
||||
// 同时调整描边宽度
|
||||
if (line.outlineWidth) {
|
||||
const oldWidth = line.outlineWidth;
|
||||
const newWidth = Math.max(1, Math.round(oldWidth * TITLE_SCALE_RATIO));
|
||||
line.outlineWidth = newWidth;
|
||||
console.log(` - 标题行 ${lineIndex + 1} 描边: ${oldWidth} → ${newWidth}`);
|
||||
changes++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (changes > 0) {
|
||||
console.log(` ✅ 共修改 ${changes} 处`);
|
||||
} else {
|
||||
console.log(` ⏭️ 无标题字幕需要调整`);
|
||||
}
|
||||
totalChanges += changes;
|
||||
});
|
||||
|
||||
// 备份原文件
|
||||
const backupPath = configPath + '.backup-title-' + Date.now();
|
||||
fs.copyFileSync(configPath, backupPath);
|
||||
console.log(`\n💾 已备份原文件到: ${backupPath}`);
|
||||
|
||||
// 保存修改后的配置
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
console.log(`\n✅ 已保存修改后的配置文件`);
|
||||
console.log(`📊 总共修改了 ${totalChanges} 处标题字幕大小和描边宽度`);
|
||||
console.log(`📏 缩放比例: ${TITLE_SCALE_RATIO * 100}%`);
|
||||
|
||||
console.log('\n📐 调整后的标题字幕大小范围:');
|
||||
console.log(' - 约 37-53px(原来是 49-70px)');
|
||||
|
||||
console.log('\n🎯 下一步操作:');
|
||||
console.log('1. 重启应用,系统会自动同步新的字体大小到数据库');
|
||||
console.log('2. 如果还是太大,可以修改脚本中的 TITLE_SCALE_RATIO 参数再次运行');
|
||||
console.log('3. 如果需要恢复,可以从备份文件恢复');
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 清空数据库中的系统模板,让应用重新从配置文件初始化
|
||||
* 使用方法:node scripts/reset-system-templates.cjs
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
|
||||
console.log('========================================');
|
||||
console.log('清空系统模板数据库工具');
|
||||
console.log('========================================\n');
|
||||
|
||||
// 查找数据库文件
|
||||
const possiblePaths = [
|
||||
// 开发环境
|
||||
path.join(__dirname, '../database.db'),
|
||||
path.join(__dirname, '../dist-release/win-unpacked/data/data/database.db'),
|
||||
// 用户数据目录
|
||||
path.join(os.homedir(), 'AppData', 'Roaming', 'aigcpanel', 'database.db'),
|
||||
path.join(os.homedir(), 'AppData', 'Roaming', 'Electron', 'database.db'),
|
||||
];
|
||||
|
||||
console.log('搜索数据库文件...\n');
|
||||
let dbPath = null;
|
||||
for (const tryPath of possiblePaths) {
|
||||
console.log('尝试路径:', tryPath);
|
||||
if (fs.existsSync(tryPath)) {
|
||||
dbPath = tryPath;
|
||||
console.log('✅ 找到数据库文件!\n');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!dbPath) {
|
||||
console.error('❌ 未找到数据库文件!');
|
||||
console.error('\n请手动删除以下位置的 database.db 文件:');
|
||||
possiblePaths.forEach(p => console.error(' - ' + p));
|
||||
console.error('\n或提供数据库文件的完整路径作为参数:');
|
||||
console.error(' node scripts/reset-system-templates.cjs <数据库路径>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('找到数据库:', dbPath);
|
||||
console.log('\n========================================');
|
||||
console.log('选项1(推荐):直接删除数据库文件');
|
||||
console.log('========================================');
|
||||
console.log('');
|
||||
console.log('最简单的方法是直接删除数据库文件:');
|
||||
console.log(` rm "${dbPath}"`);
|
||||
console.log('');
|
||||
console.log('然后重启应用,它会重新创建数据库并从配置文件初始化。');
|
||||
console.log('');
|
||||
console.log('========================================');
|
||||
console.log('选项2:使用 SQLite 命令行工具');
|
||||
console.log('========================================');
|
||||
console.log('');
|
||||
console.log('如果你安装了 SQLite,可以运行:');
|
||||
console.log(` sqlite3 "${dbPath}" "DELETE FROM subtitle_templates WHERE is_system = 1;"`);
|
||||
console.log(` sqlite3 "${dbPath}" "DELETE FROM cover_templates WHERE is_system = 1;"`);
|
||||
console.log(` sqlite3 "${dbPath}" "DELETE FROM subtitle_styles WHERE is_system = 1;"`);
|
||||
console.log('');
|
||||
console.log('然后重启应用。');
|
||||
console.log('');
|
||||
console.log('========================================\n');
|
||||
|
||||
// 询问用户是否要自动删除
|
||||
console.log('是否要自动删除数据库文件?(y/n)');
|
||||
console.log('注意:这会清空所有数据,包括用户创建的模板!');
|
||||
console.log('建议先备份数据库文件。\n');
|
||||
|
||||
// 由于这是自动化脚本,我们只提供信息,不自动执行删除
|
||||
console.log('💡 建议步骤:');
|
||||
console.log('1. 关闭应用');
|
||||
console.log('2. 备份数据库文件(可选)');
|
||||
console.log(`3. 删除文件: ${dbPath}`);
|
||||
console.log('4. 重启应用');
|
||||
console.log('5. 应用会自动从配置文件初始化8个正确的模板\n');
|
||||
@@ -0,0 +1,56 @@
|
||||
const path = require('path');
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
|
||||
module.exports = {
|
||||
releaseBaseUrlEnv: 'RESOURCE_BASE_URL',
|
||||
releaseBaseUrl: 'https://xiazaigengxin-1417293730.cos.ap-guangzhou.myqcloud.com/updates',
|
||||
outputDir: path.join(root, 'dist-release-final', 'resource-bundles'),
|
||||
releaseManifestPath: path.join(root, 'dist-release-final', 'resource-manifest.json'),
|
||||
embeddedManifestPath: path.join(root, 'electron', 'resources', 'extra', 'common', 'resource-manifest.json'),
|
||||
buildStatePath: path.join(root, 'build', 'resource-build-state.json'),
|
||||
bundles: [
|
||||
{
|
||||
name: 'playwright',
|
||||
source: path.join(root, 'electron', 'resources', 'extra', 'common', 'playwright'),
|
||||
required: true,
|
||||
extractTo: 'resources-bundles/playwright',
|
||||
},
|
||||
{
|
||||
name: 'python-runtime',
|
||||
source: path.join(root, 'electron', 'resources', 'extra', 'common', 'python'),
|
||||
required: true,
|
||||
extractTo: 'resources-bundles/python-runtime',
|
||||
},
|
||||
{
|
||||
name: 'ffmpeg',
|
||||
source: path.join(root, 'electron', 'resources', 'extra', 'win-x86'),
|
||||
required: true,
|
||||
extractTo: 'resources-bundles/ffmpeg',
|
||||
},
|
||||
{
|
||||
name: 'models',
|
||||
source: path.join(root, 'electron', 'resources', 'extra', 'common', 'models'),
|
||||
required: true,
|
||||
extractTo: 'resources-bundles/models',
|
||||
},
|
||||
{
|
||||
name: 'fonts',
|
||||
source: path.join(root, 'electron', 'resources', 'extra', 'common', 'fonts'),
|
||||
required: false,
|
||||
extractTo: 'resources-bundles/fonts',
|
||||
},
|
||||
{
|
||||
name: 'ziti',
|
||||
source: path.join(root, 'ziti'),
|
||||
required: true,
|
||||
extractTo: 'resources-bundles/ziti',
|
||||
},
|
||||
{
|
||||
name: 'bgm',
|
||||
source: path.join(root, 'bgm'),
|
||||
required: false,
|
||||
extractTo: 'resources-bundles/bgm',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import paramiko
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect('152.136.232.83', port=22, username='root', password='Jianhua105', timeout=10)
|
||||
|
||||
def run(cmd):
|
||||
print(f"\n>>> {cmd[:120]}")
|
||||
stdin, stdout, stderr = client.exec_command(cmd, timeout=15)
|
||||
out = stdout.read().decode('utf-8', errors='replace').strip()
|
||||
if out: print(out[-600:])
|
||||
return out
|
||||
|
||||
# Check admin page
|
||||
run("ls /www/wwwroot/ipzhinengti/public/ 2>/dev/null | head -20")
|
||||
run("grep -n 'cloud_beauty\\|CONFIG\\|system_config' /www/wwwroot/ipzhinengti/public/admin.html 2>/dev/null | head -20")
|
||||
|
||||
# Check the admin page location
|
||||
run("grep -rn 'admin' /www/wwwroot/ipzhinengti/server.js | head -10")
|
||||
|
||||
# Check db path
|
||||
run("grep -n 'sqlite\\|database\\|db.*=' /www/wwwroot/ipzhinengti/server.js | head -10")
|
||||
|
||||
client.close()
|
||||
@@ -0,0 +1,15 @@
|
||||
import paramiko, json
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect('152.136.232.83', port=22, username='root', password='Jianhua105', timeout=10)
|
||||
|
||||
stdin, stdout, stderr = client.exec_command("cat /www/wwwroot/zhinengtiapp/data/sysConfig.json", timeout=10)
|
||||
out = stdout.read().decode('utf-8', errors='replace').strip()
|
||||
print("sysConfig.json:", out)
|
||||
|
||||
stdin, stdout, stderr = client.exec_command("curl -s http://localhost:3001/api/system/config/public", timeout=10)
|
||||
out = stdout.read().decode('utf-8', errors='replace').strip()
|
||||
print("\nEndpoint response:", out)
|
||||
|
||||
client.close()
|
||||
@@ -0,0 +1,33 @@
|
||||
import paramiko
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect('152.136.232.83', port=22, username='root', password='Jianhua105', timeout=10)
|
||||
|
||||
def run(cmd):
|
||||
print(f"\n>>> {cmd[:120]}")
|
||||
stdin, stdout, stderr = client.exec_command(cmd, timeout=15)
|
||||
out = stdout.read().decode('utf-8', errors='replace').strip()
|
||||
err = stderr.read().decode('utf-8', errors='replace').strip()
|
||||
if out: print(out[-600:])
|
||||
if err: print(f"ERR: {err[-200:]}")
|
||||
return out
|
||||
|
||||
# Check what user-server (3002) is
|
||||
run("pm2 show 2 | head -15")
|
||||
|
||||
# Check if user-server has config/public
|
||||
run("grep -n 'config/public' /www/wwwroot/zhinengtiapp/user-server/*.js 2>/dev/null || echo 'no match'")
|
||||
run("find /www/wwwroot -name '*.js' -path '*/user-server/*' 2>/dev/null | head -5")
|
||||
|
||||
# Check user-server path
|
||||
run("pm2 show 2 | grep 'script path'")
|
||||
|
||||
# Check if 3001 is accessible externally
|
||||
run("curl -s http://152.136.232.83:3001/api/system/config/public 2>&1 | head -5")
|
||||
run("curl -s http://152.136.232.83:3002/api/system/config/public 2>&1 | head -5")
|
||||
|
||||
# Check nginx/firewall for port 3001
|
||||
run("ss -tlnp | grep -E '3001|3002'")
|
||||
|
||||
client.close()
|
||||
@@ -0,0 +1,27 @@
|
||||
import paramiko
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect('152.136.232.83', port=22, username='root', password='Jianhua105', timeout=10)
|
||||
|
||||
def run(cmd):
|
||||
print(f"\n>>> {cmd[:120]}")
|
||||
stdin, stdout, stderr = client.exec_command(cmd, timeout=15)
|
||||
out = stdout.read().decode('utf-8', errors='replace').strip()
|
||||
if out: print(out[-600:])
|
||||
return out
|
||||
|
||||
# Check user-server's config/public endpoint
|
||||
run("grep -n 'config/public' /www/wwwroot/ipzhinengti/server.js")
|
||||
|
||||
# Check user-server's sysConfig handling
|
||||
run("grep -n 'sysConfig\\|SYS_CONFIG\\|sys_config' /www/wwwroot/ipzhinengti/server.js | head -10")
|
||||
|
||||
# Check user-server's .env
|
||||
run("grep COS_ /www/wwwroot/ipzhinengti/.env 2>/dev/null || echo 'no .env'")
|
||||
run("ls /www/wwwroot/ipzhinengti/data/ 2>/dev/null")
|
||||
|
||||
# Check how the existing /api/config/public returns
|
||||
run("grep -n -B2 -A20 'config/public' /www/wwwroot/ipzhinengti/server.js | head -30")
|
||||
|
||||
client.close()
|
||||
@@ -0,0 +1,43 @@
|
||||
import paramiko
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect('152.136.232.83', port=22, username='root', password='Jianhua105', timeout=10)
|
||||
|
||||
def run(cmd):
|
||||
print(f"\n>>> {cmd[:120]}")
|
||||
stdin, stdout, stderr = client.exec_command(cmd, timeout=15)
|
||||
out = stdout.read().decode('utf-8', errors='replace').strip()
|
||||
err = stderr.read().decode('utf-8', errors='replace').strip()
|
||||
if out: print(out[-600:])
|
||||
if err: print(f"ERR: {err[-200:]}")
|
||||
return out
|
||||
|
||||
# Write sysConfig with update COS settings
|
||||
set_config = r'''const fs = require('fs');
|
||||
const p = '/www/wwwroot/zhinengtiapp/data/sysConfig.json';
|
||||
let cfg = {};
|
||||
try { cfg = JSON.parse(fs.readFileSync(p, 'utf8')); } catch(e) {}
|
||||
cfg.UPDATE_COS_BUCKET = 'xiazaigengxin-1417293730';
|
||||
cfg.UPDATE_COS_REGION = 'ap-guangzhou';
|
||||
cfg.UPDATE_COS_PATH = 'updates';
|
||||
fs.writeFileSync(p, JSON.stringify(cfg, null, 2), 'utf8');
|
||||
console.log('OK: sysConfig updated');
|
||||
console.log(JSON.stringify(cfg, null, 2));
|
||||
'''
|
||||
|
||||
sftp = client.open_sftp()
|
||||
with sftp.open('/tmp/set_config.js', 'w') as f:
|
||||
f.write(set_config)
|
||||
sftp.close()
|
||||
|
||||
run("node /tmp/set_config.js")
|
||||
|
||||
# Restart service
|
||||
run("pm2 restart all 2>/dev/null || (pkill -f 'node.*zhinengtiapp' && sleep 1 && cd /www/wwwroot/zhinengtiapp && nohup node index.js > output.log 2>&1 &)")
|
||||
|
||||
# Test
|
||||
run("sleep 3 && curl -s http://localhost:3002/api/system/config/public")
|
||||
|
||||
client.close()
|
||||
print("\n=== DONE ===")
|
||||
@@ -0,0 +1,25 @@
|
||||
import paramiko
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect('152.136.232.83', port=22, username='root', password='Jianhua105', timeout=10)
|
||||
|
||||
def run(cmd):
|
||||
print(f"\n>>> {cmd[:120]}")
|
||||
stdin, stdout, stderr = client.exec_command(cmd, timeout=15)
|
||||
out = stdout.read().decode('utf-8', errors='replace').strip()
|
||||
if out: print(out[-600:])
|
||||
return out
|
||||
|
||||
# Find database path in server.js
|
||||
run("grep -n 'sqlite\\|database\\|\\.db' /www/wwwroot/ipzhinengti/server.js | head -10")
|
||||
|
||||
# Find all .db files
|
||||
run("find /www/wwwroot/ipzhinengti -name '*.db' 2>/dev/null")
|
||||
run("find /www/wwwroot/ipzhinengti -name '*.sqlite' 2>/dev/null")
|
||||
|
||||
# Check data dir
|
||||
run("ls -la /www/wwwroot/ipzhinengti/data/ 2>/dev/null || echo 'no data dir'")
|
||||
run("ls -la /www/wwwroot/ipzhinengti/*.db 2>/dev/null || echo 'no .db in root'")
|
||||
|
||||
client.close()
|
||||
@@ -0,0 +1,33 @@
|
||||
import paramiko
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect('152.136.232.83', port=22, username='root', password='Jianhua105', timeout=10)
|
||||
|
||||
def run(cmd):
|
||||
print(f"\n>>> {cmd[:120]}")
|
||||
stdin, stdout, stderr = client.exec_command(cmd, timeout=15)
|
||||
out = stdout.read().decode('utf-8', errors='replace').strip()
|
||||
err = stderr.read().decode('utf-8', errors='replace').strip()
|
||||
if out: print(out[-800:])
|
||||
if err: print(f"ERR: {err[-200:]}")
|
||||
return out
|
||||
|
||||
# Check who's on port 3002
|
||||
run("lsof -i :3002 2>/dev/null || ss -tlnp | grep 3002")
|
||||
|
||||
# Check ai-video error log
|
||||
run("pm2 logs ai-video --lines 30 --nostream 2>/dev/null")
|
||||
|
||||
# The actual process running zhinengtiapp
|
||||
run("ps aux | grep zhinengtiapp | grep -v grep")
|
||||
|
||||
# Check what pm2 ai-video runs
|
||||
run("pm2 show 1 2>/dev/null | head -20")
|
||||
|
||||
# Restart ai-video
|
||||
run("pm2 restart ai-video 2>/dev/null")
|
||||
run("sleep 3 && pm2 list")
|
||||
run("sleep 1 && curl -s http://localhost:3002/api/system/config/public | head -200")
|
||||
|
||||
client.close()
|
||||
@@ -0,0 +1,37 @@
|
||||
import paramiko
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect('152.136.232.83', port=22, username='root', password='Jianhua105', timeout=10)
|
||||
|
||||
def run(cmd):
|
||||
print(f"\n>>> {cmd[:120]}")
|
||||
stdin, stdout, stderr = client.exec_command(cmd, timeout=20)
|
||||
out = stdout.read().decode('utf-8', errors='replace').strip()
|
||||
err = stderr.read().decode('utf-8', errors='replace').strip()
|
||||
if out: print(out[-800:])
|
||||
if err: print(f"ERR: {err[-200:]}")
|
||||
return out
|
||||
|
||||
# Check error log
|
||||
run("tail -30 /root/.pm2/logs/ai-video-error.log")
|
||||
|
||||
# Which port is each app on?
|
||||
run("grep -n 'PORT\\|listen' /www/wwwroot/zhinengtiapp/index.js | head -5")
|
||||
run("grep -n 'PORT\\|listen' /www/wwwroot/aihuitu/server/index.cjs | head -5")
|
||||
|
||||
# Check .env PORT
|
||||
run("grep PORT /www/wwwroot/zhinengtiapp/.env")
|
||||
run("grep PORT /www/wwwroot/aihuitu/server/.env")
|
||||
|
||||
# What's listening on 3001/3002?
|
||||
run("ss -tlnp | grep -E '3001|3002|3003'")
|
||||
|
||||
# Fix: restart ai-video from zhinengtiapp dir
|
||||
run("cd /www/wwwroot/zhinengtiapp && pm2 delete ai-video 2>/dev/null; pm2 start index.js --name ai-video 2>&1")
|
||||
run("sleep 3 && pm2 list")
|
||||
|
||||
# Test the endpoint
|
||||
run("curl -s http://localhost:3001/api/system/config/public 2>/dev/null || curl -s http://localhost:3002/api/system/config/public 2>/dev/null")
|
||||
|
||||
client.close()
|
||||
@@ -0,0 +1,25 @@
|
||||
import paramiko
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect('152.136.232.83', port=22, username='root', password='Jianhua105', timeout=10)
|
||||
|
||||
def run(cmd):
|
||||
print(f"\n>>> {cmd[:120]}")
|
||||
stdin, stdout, stderr = client.exec_command(cmd, timeout=15)
|
||||
out = stdout.read().decode('utf-8', errors='replace').strip()
|
||||
err = stderr.read().decode('utf-8', errors='replace').strip()
|
||||
if out: print(out[-800:])
|
||||
if err: print(f"ERR: {err[-200:]}")
|
||||
return out
|
||||
|
||||
# Check loadSysConfig function
|
||||
run("grep -n -A 10 'function loadSysConfig\\|loadSysConfig' /www/wwwroot/zhinengtiapp/index.js | head -20")
|
||||
|
||||
# Check the path it reads from
|
||||
run("grep -n 'sysConfig.json\\|data/' /www/wwwroot/zhinengtiapp/index.js | head -10")
|
||||
|
||||
# Test loadSysConfig directly
|
||||
run("""node -e "const path=require('path');const fs=require('fs');const p=path.join('/www/wwwroot/zhinengtiapp','data','sysConfig.json');console.log('path:',p);try{const d=fs.readFileSync(p,'utf8');console.log('data:',d);}catch(e){console.log('err:',e.message);}" """)
|
||||
|
||||
client.close()
|
||||
@@ -0,0 +1,19 @@
|
||||
import paramiko
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect('152.136.232.83', port=22, username='root', password='Jianhua105', timeout=10)
|
||||
|
||||
def run(cmd):
|
||||
print(f"\n>>> {cmd[:120]}")
|
||||
stdin, stdout, stderr = client.exec_command(cmd, timeout=15)
|
||||
out = stdout.read().decode('utf-8', errors='replace').strip()
|
||||
if out: print(out[-800:])
|
||||
return out
|
||||
|
||||
run("grep -n 'SYS_CONFIG_FILE' /www/wwwroot/zhinengtiapp/index.js | head -5")
|
||||
|
||||
# Check our endpoint code
|
||||
run("sed -n '1080,1120p' /www/wwwroot/zhinengtiapp/index.js")
|
||||
|
||||
client.close()
|
||||
@@ -0,0 +1,25 @@
|
||||
import paramiko
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect('152.136.232.83', port=22, username='root', password='Jianhua105', timeout=10)
|
||||
|
||||
def run(cmd):
|
||||
print(f"\n>>> {cmd[:120]}")
|
||||
stdin, stdout, stderr = client.exec_command(cmd, timeout=20)
|
||||
out = stdout.read().decode('utf-8', errors='replace').strip()
|
||||
err = stderr.read().decode('utf-8', errors='replace').strip()
|
||||
if out: print(out[-800:])
|
||||
if err: print(f"ERR: {err[-200:]}")
|
||||
return out
|
||||
|
||||
# Full error log
|
||||
run("tail -50 /root/.pm2/logs/ai-video-error.log")
|
||||
|
||||
# Also check out log
|
||||
run("tail -30 /root/.pm2/logs/ai-video-out.log")
|
||||
|
||||
# Check if index.js has syntax errors by parsing it
|
||||
run("node --check /www/wwwroot/zhinengtiapp/index.js 2>&1")
|
||||
|
||||
client.close()
|
||||
@@ -0,0 +1,50 @@
|
||||
import paramiko, json
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect('152.136.232.83', port=22, username='root', password='Jianhua105', timeout=10)
|
||||
|
||||
def run(cmd):
|
||||
print(f"\n>>> {cmd[:120]}")
|
||||
stdin, stdout, stderr = client.exec_command(cmd, timeout=15)
|
||||
out = stdout.read().decode('utf-8', errors='replace').strip()
|
||||
err = stderr.read().decode('utf-8', errors='replace').strip()
|
||||
if out: print(out[-600:])
|
||||
if err: print(f"ERR: {err[-200:]}")
|
||||
return out
|
||||
|
||||
# Check current sys_config.json (correct filename)
|
||||
run("cat /www/wwwroot/zhinengtiapp/data/sys_config.json 2>/dev/null || echo 'FILE NOT FOUND'")
|
||||
|
||||
# Check wrong filename
|
||||
run("cat /www/wwwroot/zhinengtiapp/data/sysConfig.json 2>/dev/null || echo 'FILE NOT FOUND'")
|
||||
|
||||
# Write to the CORRECT file: sys_config.json
|
||||
set_config = r'''const fs = require('fs');
|
||||
const p = '/www/wwwroot/zhinengtiapp/data/sys_config.json';
|
||||
let cfg = {};
|
||||
try { cfg = JSON.parse(fs.readFileSync(p, 'utf8')); } catch(e) {}
|
||||
cfg.UPDATE_COS_BUCKET = 'xiazaigengxin-1417293730';
|
||||
cfg.UPDATE_COS_REGION = 'ap-guangzhou';
|
||||
cfg.UPDATE_COS_PATH = 'updates';
|
||||
fs.writeFileSync(p, JSON.stringify(cfg, null, 2), 'utf8');
|
||||
console.log('OK: sys_config.json updated');
|
||||
console.log(JSON.stringify(cfg, null, 2));
|
||||
'''
|
||||
|
||||
sftp = client.open_sftp()
|
||||
with sftp.open('/tmp/fix_config.js', 'w') as f:
|
||||
f.write(set_config)
|
||||
sftp.close()
|
||||
|
||||
run("node /tmp/fix_config.js")
|
||||
|
||||
# Restart to pick up changes
|
||||
run("pm2 restart ai-video 2>&1")
|
||||
run("sleep 3 && pm2 list")
|
||||
|
||||
# Test endpoint
|
||||
run("curl -s http://localhost:3001/api/system/config/public")
|
||||
|
||||
client.close()
|
||||
print("\n=== FIX DONE ===")
|
||||
@@ -0,0 +1,66 @@
|
||||
import paramiko
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect('152.136.232.83', port=22, username='root', password='Jianhua105', timeout=10)
|
||||
|
||||
def run(cmd):
|
||||
print(f"\n>>> {cmd[:120]}")
|
||||
stdin, stdout, stderr = client.exec_command(cmd, timeout=20)
|
||||
out = stdout.read().decode('utf-8', errors='replace').strip()
|
||||
err = stderr.read().decode('utf-8', errors='replace').strip()
|
||||
if out: print(out[-600:])
|
||||
if err: print(f"ERR: {err[-200:]}")
|
||||
return out
|
||||
|
||||
# Check where sqlite3 is
|
||||
run("ls /www/wwwroot/ipzhinengti/node_modules/sqlite3 2>/dev/null && echo 'FOUND' || echo 'NOT FOUND'")
|
||||
run("ls /www/wwwroot/ipzhinengti/node_modules/better-sqlite3 2>/dev/null && echo 'FOUND' || echo 'NOT FOUND'")
|
||||
|
||||
# Check db file
|
||||
run("ls -la /www/wwwroot/ipzhinengti/data/ 2>/dev/null")
|
||||
|
||||
# Try running from the project dir
|
||||
insert_db = r'''const sqlite3 = require('sqlite3');
|
||||
const path = require('path');
|
||||
const dbPath = path.join(__dirname, 'data', 'database.db');
|
||||
console.log('DB path:', dbPath);
|
||||
const db = new sqlite3.Database(dbPath);
|
||||
|
||||
const configs = [
|
||||
['UPDATE_COS_BUCKET', 'xiazaigengxin-1417293730'],
|
||||
['UPDATE_COS_REGION', 'ap-guangzhou'],
|
||||
['UPDATE_COS_PATH', 'updates']
|
||||
];
|
||||
|
||||
let done = 0;
|
||||
configs.forEach(([key, value]) => {
|
||||
db.run(
|
||||
'INSERT OR REPLACE INTO system_config (config_key, config_value) VALUES (?, ?)',
|
||||
[key, value],
|
||||
(err) => {
|
||||
if (err) console.log('ERR:', key, err.message);
|
||||
else console.log('OK:', key, '=', value);
|
||||
done++;
|
||||
if (done === configs.length) {
|
||||
db.close();
|
||||
console.log('ALL DONE');
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
'''
|
||||
|
||||
sftp = client.open_sftp()
|
||||
with sftp.open('/www/wwwroot/ipzhinengti/insert_config.js', 'w') as f:
|
||||
f.write(insert_db)
|
||||
sftp.close()
|
||||
|
||||
run("cd /www/wwwroot/ipzhinengti && node insert_config.js")
|
||||
run("rm /www/wwwroot/ipzhinengti/insert_config.js")
|
||||
|
||||
# Restart
|
||||
run("pm2 restart user-server 2>&1")
|
||||
run("sleep 3 && curl -s http://localhost:3002/api/system/config/public")
|
||||
|
||||
client.close()
|
||||
@@ -0,0 +1,59 @@
|
||||
import paramiko
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect('152.136.232.83', port=22, username='root', password='Jianhua105', timeout=10)
|
||||
|
||||
def run(cmd):
|
||||
print(f"\n>>> {cmd[:120]}")
|
||||
stdin, stdout, stderr = client.exec_command(cmd, timeout=20)
|
||||
out = stdout.read().decode('utf-8', errors='replace').strip()
|
||||
err = stderr.read().decode('utf-8', errors='replace').strip()
|
||||
if out: print(out[-600:])
|
||||
if err: print(f"ERR: {err[-200:]}")
|
||||
return out
|
||||
|
||||
insert_db = r'''const sqlite3 = require('sqlite3');
|
||||
const path = require('path');
|
||||
const dbPath = path.join(__dirname, 'users.db');
|
||||
console.log('DB path:', dbPath);
|
||||
const db = new sqlite3.Database(dbPath);
|
||||
|
||||
const configs = [
|
||||
['UPDATE_COS_BUCKET', 'xiazaigengxin-1417293730'],
|
||||
['UPDATE_COS_REGION', 'ap-guangzhou'],
|
||||
['UPDATE_COS_PATH', 'updates']
|
||||
];
|
||||
|
||||
let done = 0;
|
||||
configs.forEach(([key, value]) => {
|
||||
db.run(
|
||||
'INSERT OR REPLACE INTO system_config (config_key, config_value) VALUES (?, ?)',
|
||||
[key, value],
|
||||
(err) => {
|
||||
if (err) console.log('ERR:', key, err.message);
|
||||
else console.log('OK:', key, '=', value);
|
||||
done++;
|
||||
if (done === configs.length) {
|
||||
db.close();
|
||||
console.log('ALL DONE');
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
'''
|
||||
|
||||
sftp = client.open_sftp()
|
||||
with sftp.open('/www/wwwroot/ipzhinengti/insert_config.js', 'w') as f:
|
||||
f.write(insert_db)
|
||||
sftp.close()
|
||||
|
||||
run("cd /www/wwwroot/ipzhinengti && node insert_config.js")
|
||||
run("rm -f /www/wwwroot/ipzhinengti/insert_config.js")
|
||||
|
||||
# Restart
|
||||
run("pm2 restart user-server 2>&1")
|
||||
run("sleep 3 && curl -s http://localhost:3002/api/system/config/public")
|
||||
|
||||
client.close()
|
||||
print("\n=== DONE ===")
|
||||
@@ -0,0 +1,101 @@
|
||||
import paramiko
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect('152.136.232.83', port=22, username='root', password='Jianhua105', timeout=10)
|
||||
|
||||
def run(cmd):
|
||||
print(f"\n>>> {cmd[:120]}")
|
||||
stdin, stdout, stderr = client.exec_command(cmd, timeout=20)
|
||||
out = stdout.read().decode('utf-8', errors='replace').strip()
|
||||
err = stderr.read().decode('utf-8', errors='replace').strip()
|
||||
if out: print(out[-600:])
|
||||
if err: print(f"ERR: {err[-200:]}")
|
||||
return out
|
||||
|
||||
patch_code = r"""const fs = require('fs');
|
||||
const filePath = '/www/wwwroot/zhinengtiapp/index.js';
|
||||
let content = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
const marker = "// \u6587\u6848\u751f\u6210\uff08\u5f02\u6b65\uff1a\u7acb\u5373\u8fd4\u56de\uff0c\u540e\u53f0\u5904\u7406\uff09";
|
||||
|
||||
if (content.includes('/api/system/config/public')) {
|
||||
console.log('SKIP: endpoint already exists');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const newEndpoint = `
|
||||
|
||||
// ========== \u81ea\u52a8\u66f4\u65b0\u914d\u7f6e\u63a5\u53e3 ==========
|
||||
app.get('/api/system/config/public', (req, res) => {
|
||||
const sysCfg = loadSysConfig()
|
||||
const envPath = path.join(__dirname, '.env')
|
||||
let cosBucket = ''
|
||||
let cosRegion = ''
|
||||
try {
|
||||
const envContent = fs.readFileSync(envPath, 'utf-8')
|
||||
envContent.split(String.fromCharCode(10)).forEach(line => {
|
||||
line = line.trim()
|
||||
if (!line || line.startsWith('#')) return
|
||||
const eqIdx = line.indexOf('=')
|
||||
if (eqIdx > 0) {
|
||||
const key = line.substring(0, eqIdx).trim()
|
||||
const val = line.substring(eqIdx + 1).trim()
|
||||
if (key === 'COS_BUCKET') cosBucket = val
|
||||
if (key === 'COS_REGION') cosRegion = val
|
||||
}
|
||||
})
|
||||
} catch (e) {}
|
||||
cosBucket = sysCfg.UPDATE_COS_BUCKET || cosBucket
|
||||
cosRegion = sysCfg.UPDATE_COS_REGION || cosRegion
|
||||
const updatePath = sysCfg.UPDATE_COS_PATH || 'updates'
|
||||
let updateUrl = ''
|
||||
if (cosBucket && cosRegion) {
|
||||
updateUrl = 'https://' + cosBucket + '.cos.' + cosRegion + '.myqcloud.com/' + updatePath
|
||||
}
|
||||
res.json({
|
||||
success: true,
|
||||
update_url: updateUrl
|
||||
})
|
||||
})
|
||||
|
||||
`;
|
||||
|
||||
const idx = content.indexOf(marker);
|
||||
if (idx === -1) {
|
||||
console.log('ERROR: marker not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
content = content.slice(0, idx) + newEndpoint + content.slice(idx);
|
||||
fs.writeFileSync(filePath, content, 'utf-8');
|
||||
console.log('OK: patched index.js');
|
||||
"""
|
||||
|
||||
sftp = client.open_sftp()
|
||||
with sftp.open('/tmp/patch_index2.js', 'w') as f:
|
||||
f.write(patch_code)
|
||||
sftp.close()
|
||||
print("Uploaded patch_index2.js")
|
||||
|
||||
# Backup current (already restored) file
|
||||
run("cp /www/wwwroot/zhinengtiapp/index.js /www/wwwroot/zhinengtiapp/index.js.bak2")
|
||||
|
||||
# Run patch
|
||||
run("node /tmp/patch_index2.js")
|
||||
|
||||
# Verify syntax
|
||||
run("node --check /www/wwwroot/zhinengtiapp/index.js 2>&1")
|
||||
|
||||
# Check endpoint exists
|
||||
run("grep -n 'system/config/public' /www/wwwroot/zhinengtiapp/index.js")
|
||||
|
||||
# Restart
|
||||
run("pm2 restart ai-video 2>&1")
|
||||
run("sleep 3 && pm2 list")
|
||||
|
||||
# Test
|
||||
run("curl -s http://localhost:3001/api/system/config/public")
|
||||
|
||||
client.close()
|
||||
print("\n=== PATCH DONE ===")
|
||||
@@ -0,0 +1,209 @@
|
||||
import paramiko
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect('152.136.232.83', port=22, username='root', password='Jianhua105', timeout=10)
|
||||
|
||||
def run(cmd):
|
||||
print(f"\n>>> {cmd[:120]}")
|
||||
stdin, stdout, stderr = client.exec_command(cmd, timeout=20)
|
||||
out = stdout.read().decode('utf-8', errors='replace').strip()
|
||||
err = stderr.read().decode('utf-8', errors='replace').strip()
|
||||
if out: print(out[-600:])
|
||||
if err: print(f"ERR: {err[-200:]}")
|
||||
return out
|
||||
|
||||
# Backup
|
||||
run("cp /www/wwwroot/ipzhinengti/server.js /www/wwwroot/ipzhinengti/server.js.bak.update")
|
||||
run("cp /www/wwwroot/ipzhinengti/public/admin.html /www/wwwroot/ipzhinengti/public/admin.html.bak.update")
|
||||
|
||||
# === Patch 1: server.js - add UPDATE_COS keys to publicKeys and compute update_url ===
|
||||
patch_server = r'''const fs = require('fs');
|
||||
const filePath = '/www/wwwroot/ipzhinengti/server.js';
|
||||
let content = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
if (content.includes('UPDATE_COS_BUCKET')) {
|
||||
console.log('SKIP: already patched');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// 1. Add UPDATE_COS keys to publicKeys array
|
||||
const marker1 = " 'cloud_beauty_enabled'";
|
||||
const replacement1 = marker1 + ",\n 'UPDATE_COS_BUCKET',\n 'UPDATE_COS_REGION',\n 'UPDATE_COS_PATH'";
|
||||
content = content.replace(marker1, replacement1);
|
||||
|
||||
// 2. Replace the res.json to add update_url computation
|
||||
const marker2 = "res.json({ success: true, config })";
|
||||
const replacement2 = `const cosBucket = config.UPDATE_COS_BUCKET || ''
|
||||
const cosRegion = config.UPDATE_COS_REGION || ''
|
||||
const updatePath = config.UPDATE_COS_PATH || 'updates'
|
||||
let updateUrl = ''
|
||||
if (cosBucket && cosRegion) {
|
||||
updateUrl = 'https://' + cosBucket + '.cos.' + cosRegion + '.myqcloud.com/' + updatePath
|
||||
}
|
||||
res.json({ success: true, config, update_url: updateUrl })`;
|
||||
content = content.replace(marker2, replacement2);
|
||||
|
||||
fs.writeFileSync(filePath, content, 'utf-8');
|
||||
console.log('OK: patched server.js');
|
||||
'''
|
||||
|
||||
sftp = client.open_sftp()
|
||||
with sftp.open('/tmp/patch_server.js', 'w') as f:
|
||||
f.write(patch_server)
|
||||
sftp.close()
|
||||
|
||||
run("node /tmp/patch_server.js")
|
||||
run("node --check /www/wwwroot/ipzhinengti/server.js 2>&1")
|
||||
|
||||
# === Patch 2: admin.html - add UPDATE_COS config section ===
|
||||
patch_admin = r'''const fs = require('fs');
|
||||
const filePath = '/www/wwwroot/ipzhinengti/public/admin.html';
|
||||
let content = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
if (content.includes('UPDATE_COS_BUCKET')) {
|
||||
console.log('SKIP: already patched');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// 1. Add to CONFIG_KEYS array
|
||||
const keysMarker = "'cloud_beauty_enabled'";
|
||||
content = content.replace(keysMarker, keysMarker + ", 'UPDATE_COS_BUCKET', 'UPDATE_COS_REGION', 'UPDATE_COS_PATH'");
|
||||
|
||||
// 2. Add save values
|
||||
const saveMarker = "cloud_beauty_enabled: document.getElementById('cfg_cloud_beauty_enabled').checked ? 'true' : 'false',";
|
||||
const saveReplacement = saveMarker + "\n UPDATE_COS_BUCKET: document.getElementById('cfg_UPDATE_COS_BUCKET').value,\n UPDATE_COS_REGION: document.getElementById('cfg_UPDATE_COS_REGION').value,\n UPDATE_COS_PATH: document.getElementById('cfg_UPDATE_COS_PATH').value,";
|
||||
content = content.replace(saveMarker, saveReplacement);
|
||||
|
||||
// 3. Add HTML section after cloud_beauty section
|
||||
const htmlMarker = "document.getElementById('cfg_cloud_beauty_enabled').checked = config.cloud_beauty_enabled === 'true';";
|
||||
const htmlSection = htmlMarker + `
|
||||
document.getElementById('cfg_UPDATE_COS_BUCKET').value = config.UPDATE_COS_BUCKET || '';
|
||||
document.getElementById('cfg_UPDATE_COS_REGION').value = config.UPDATE_COS_REGION || '';
|
||||
document.getElementById('cfg_UPDATE_COS_PATH').value = config.UPDATE_COS_PATH || '';`;
|
||||
content = content.replace(htmlMarker, htmlSection);
|
||||
|
||||
// 4. Add form HTML - find the closing tag after cloud_beauty section
|
||||
const formMarker = "<label>\\u542f\\u7528\\u4e91\\u7f8e\\u989c</label>";
|
||||
const formSection = formMarker + `</div>
|
||||
</div>
|
||||
<h3 style="grid-column:1/-1;margin:15px 0 8px;padding-bottom:8px;border-bottom:1px solid #e2e8f0;">\\u81ea\\u52a8\\u66f4\\u65b0 COS \\u914d\\u7f6e</h3>
|
||||
<div class="form-group"><label>\\u66f4\\u65b0\\u6876\\u540d</label><input type="text" id="cfg_UPDATE_COS_BUCKET" placeholder="xiazaigengxin-1417293730"></div>
|
||||
<div class="form-group"><label>\\u5730\\u57df</label><input type="text" id="cfg_UPDATE_COS_REGION" placeholder="ap-guangzhou"></div>
|
||||
<div class="form-group"><label>\\u66f4\\u65b0\\u8def\\u5f84</label><input type="text" id="cfg_UPDATE_COS_PATH" placeholder="updates"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
// Actually, let's find a better insertion point - the last form-group before save button
|
||||
// Simpler: just append before the save button div
|
||||
|
||||
fs.writeFileSync(filePath, content, 'utf-8');
|
||||
console.log('OK: patched admin.html');
|
||||
'''
|
||||
|
||||
# Actually the admin.html patch is complex, let me use a simpler approach
|
||||
# Just add the form fields after the cloud_beauty section
|
||||
patch_admin2 = r'''const fs = require('fs');
|
||||
const filePath = '/www/wwwroot/ipzhinengti/public/admin.html';
|
||||
let content = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
if (content.includes('UPDATE_COS_BUCKET')) {
|
||||
console.log('SKIP: already patched');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// 1. Add to CONFIG_KEYS
|
||||
content = content.replace(
|
||||
"'cloud_beauty_enabled'",
|
||||
"'cloud_beauty_enabled', 'UPDATE_COS_BUCKET', 'UPDATE_COS_REGION', 'UPDATE_COS_PATH'"
|
||||
);
|
||||
|
||||
// 2. Add save values after cloud_beauty_enabled save line
|
||||
content = content.replace(
|
||||
"cloud_beauty_enabled: document.getElementById('cfg_cloud_beauty_enabled').checked ? 'true' : 'false',",
|
||||
`cloud_beauty_enabled: document.getElementById('cfg_cloud_beauty_enabled').checked ? 'true' : 'false',
|
||||
UPDATE_COS_BUCKET: document.getElementById('cfg_UPDATE_COS_BUCKET').value,
|
||||
UPDATE_COS_REGION: document.getElementById('cfg_UPDATE_COS_REGION').value,
|
||||
UPDATE_COS_PATH: document.getElementById('cfg_UPDATE_COS_PATH').value,`
|
||||
);
|
||||
|
||||
// 3. Add load values
|
||||
content = content.replace(
|
||||
"document.getElementById('cfg_cloud_beauty_enabled').checked = config.cloud_beauty_enabled === 'true';",
|
||||
`document.getElementById('cfg_cloud_beauty_enabled').checked = config.cloud_beauty_enabled === 'true';
|
||||
document.getElementById('cfg_UPDATE_COS_BUCKET').value = config.UPDATE_COS_BUCKET || '';
|
||||
document.getElementById('cfg_UPDATE_COS_REGION').value = config.UPDATE_COS_REGION || '';
|
||||
document.getElementById('cfg_UPDATE_COS_PATH').value = config.UPDATE_COS_PATH || '';`
|
||||
);
|
||||
|
||||
// 4. Add HTML form fields - insert before the save button
|
||||
// Find the save button and insert before it
|
||||
const saveBtnPattern = '<button onclick="saveSystemConfig()"';
|
||||
const insertHTML = `<div style="grid-column:1/-1;margin:15px 0 8px;padding-bottom:8px;border-bottom:1px solid #e2e8f0;font-weight:bold;">\u81ea\u52a8\u66f4\u65b0 COS \u914d\u7f6e</div>
|
||||
<div class="form-group"><label>\u66f4\u65b0\u6876\u540d</label><input type="text" id="cfg_UPDATE_COS_BUCKET" placeholder="xiazaigengxin-1417293730"></div>
|
||||
<div class="form-group"><label>\u5730\u57df</label><input type="text" id="cfg_UPDATE_COS_REGION" placeholder="ap-guangzhou"></div>
|
||||
<div class="form-group"><label>\u66f4\u65b0\u8def\u5f84</label><input type="text" id="cfg_UPDATE_COS_PATH" placeholder="updates"></div>
|
||||
`;
|
||||
content = content.replace(saveBtnPattern, insertHTML + '\n ' + saveBtnPattern);
|
||||
|
||||
fs.writeFileSync(filePath, content, 'utf-8');
|
||||
console.log('OK: patched admin.html');
|
||||
'''
|
||||
|
||||
sftp = client.open_sftp()
|
||||
with sftp.open('/tmp/patch_admin2.js', 'w') as f:
|
||||
f.write(patch_admin2)
|
||||
sftp.close()
|
||||
|
||||
run("node /tmp/patch_admin2.js")
|
||||
|
||||
# === Insert UPDATE_COS config into database ===
|
||||
insert_db = r'''const sqlite3 = require('sqlite3');
|
||||
const path = require('path');
|
||||
const dbPath = path.join('/www/wwwroot/ipzhinengti', 'data', 'database.db');
|
||||
console.log('DB path:', dbPath);
|
||||
const db = new sqlite3.Database(dbPath);
|
||||
|
||||
const configs = [
|
||||
['UPDATE_COS_BUCKET', 'xiazaigengxin-1417293730'],
|
||||
['UPDATE_COS_REGION', 'ap-guangzhou'],
|
||||
['UPDATE_COS_PATH', 'updates']
|
||||
];
|
||||
|
||||
let done = 0;
|
||||
configs.forEach(([key, value]) => {
|
||||
db.run(
|
||||
'INSERT OR REPLACE INTO system_config (config_key, config_value) VALUES (?, ?)',
|
||||
[key, value],
|
||||
(err) => {
|
||||
if (err) console.log('ERR:', key, err.message);
|
||||
else console.log('OK:', key, '=', value);
|
||||
done++;
|
||||
if (done === configs.length) {
|
||||
db.close();
|
||||
console.log('ALL DONE');
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
'''
|
||||
|
||||
sftp = client.open_sftp()
|
||||
with sftp.open('/tmp/insert_config.js', 'w') as f:
|
||||
f.write(insert_db)
|
||||
sftp.close()
|
||||
|
||||
run("node /tmp/insert_config.js")
|
||||
|
||||
# Restart user-server
|
||||
run("pm2 restart user-server 2>&1")
|
||||
run("sleep 3 && pm2 list")
|
||||
|
||||
# Test endpoint
|
||||
run("curl -s http://localhost:3002/api/system/config/public")
|
||||
|
||||
client.close()
|
||||
print("\n=== ALL PATCHED ===")
|
||||
@@ -0,0 +1,30 @@
|
||||
import paramiko
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect('152.136.232.83', port=22, username='root', password='Jianhua105', timeout=10)
|
||||
|
||||
def run(cmd):
|
||||
print(f"\n>>> {cmd[:120]}")
|
||||
stdin, stdout, stderr = client.exec_command(cmd, timeout=20)
|
||||
out = stdout.read().decode('utf-8', errors='replace').strip()
|
||||
err = stderr.read().decode('utf-8', errors='replace').strip()
|
||||
if out: print(out[-800:])
|
||||
if err: print(f"ERR: {err[-200:]}")
|
||||
return out
|
||||
|
||||
# Restore backup
|
||||
run("cp /www/wwwroot/zhinengtiapp/index.js.bak.update /www/wwwroot/zhinengtiapp/index.js")
|
||||
|
||||
# Verify syntax
|
||||
run("node --check /www/wwwroot/zhinengtiapp/index.js 2>&1")
|
||||
|
||||
# Restart
|
||||
run("cd /www/wwwroot/zhinengtiapp && pm2 delete ai-video 2>/dev/null; pm2 start index.js --name ai-video 2>&1")
|
||||
run("sleep 3 && pm2 list")
|
||||
|
||||
# Test it's running
|
||||
run("curl -s http://localhost:3001/api/health 2>/dev/null || curl -s http://localhost:3001/ 2>/dev/null | head -100")
|
||||
|
||||
client.close()
|
||||
print("\n=== RESTORE DONE ===")
|
||||
@@ -0,0 +1,11 @@
|
||||
import paramiko
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect('152.136.232.83', port=22, username='root', password='Jianhua105', timeout=10)
|
||||
|
||||
stdin, stdout, stderr = client.exec_command("curl -s http://localhost:3002/api/system/config/public", timeout=15)
|
||||
out = stdout.read().decode('utf-8', errors='replace').strip()
|
||||
print(out)
|
||||
|
||||
client.close()
|
||||
@@ -0,0 +1,32 @@
|
||||
import paramiko
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect('152.136.232.83', port=22, username='root', password='Jianhua105', timeout=10)
|
||||
|
||||
def run(cmd):
|
||||
print(f"\n>>> {cmd[:120]}")
|
||||
stdin, stdout, stderr = client.exec_command(cmd, timeout=15)
|
||||
out = stdout.read().decode('utf-8', errors='replace').strip()
|
||||
err = stderr.read().decode('utf-8', errors='replace').strip()
|
||||
if out: print(out[-800:])
|
||||
if err: print(f"ERR: {err[-200:]}")
|
||||
return out
|
||||
|
||||
# Check if our endpoint exists
|
||||
run("grep -n 'system/config/public' /www/wwwroot/zhinengtiapp/index.js")
|
||||
|
||||
# Check what pm2 is running
|
||||
run("pm2 list")
|
||||
|
||||
# Check the actual loaded file
|
||||
run("pm2 show 0 2>/dev/null | grep 'script path'")
|
||||
|
||||
# Check the cosBucket value
|
||||
run("node -e \"const fs=require('fs');const c=fs.readFileSync('/www/wwwroot/zhinengtiapp/data/sysConfig.json','utf8');console.log(c);\"")
|
||||
|
||||
# Check the .env file COS entries
|
||||
run("grep COS_ /www/wwwroot/zhinengtiapp/.env")
|
||||
|
||||
client.close()
|
||||
print("\n=== DONE ===")
|
||||
@@ -0,0 +1,32 @@
|
||||
import paramiko
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect('152.136.232.83', port=22, username='root', password='Jianhua105', timeout=10)
|
||||
|
||||
def run(cmd):
|
||||
print(f"\n>>> {cmd[:120]}")
|
||||
stdin, stdout, stderr = client.exec_command(cmd, timeout=15)
|
||||
out = stdout.read().decode('utf-8', errors='replace').strip()
|
||||
err = stderr.read().decode('utf-8', errors='replace').strip()
|
||||
if out: print(out[-600:])
|
||||
if err: print(f"ERR: {err[-200:]}")
|
||||
return out
|
||||
|
||||
# Check the .env file
|
||||
run("grep -n 'COS_BUCKET\\|COS_REGION' /www/wwwroot/zhinengtiapp/.env")
|
||||
|
||||
# Check sysConfig file
|
||||
run("cat /www/wwwroot/zhinengtiapp/data/sysConfig.json 2>/dev/null || echo 'no sysConfig'")
|
||||
|
||||
# Check what the endpoint actually returns
|
||||
run("curl -s http://localhost:3002/api/system/config/public | python3 -m json.tool 2>/dev/null || curl -s http://localhost:3002/api/system/config/public")
|
||||
|
||||
# Check the new endpoint exists in index.js
|
||||
run("grep -n 'system/config/public' /www/wwwroot/zhinengtiapp/index.js")
|
||||
|
||||
# Check admin.html changes
|
||||
run("grep -n 'UPDATE_COS' /www/wwwroot/zhinengtiapp/public/admin.html")
|
||||
|
||||
client.close()
|
||||
print("\n=== DONE ===")
|
||||
@@ -0,0 +1,32 @@
|
||||
import paramiko
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect('152.136.232.83', port=22, username='root', password='Jianhua105', timeout=10)
|
||||
|
||||
def run(cmd):
|
||||
print(f"\n>>> {cmd[:120]}")
|
||||
stdin, stdout, stderr = client.exec_command(cmd, timeout=15)
|
||||
out = stdout.read().decode('utf-8', errors='replace').strip()
|
||||
err = stderr.read().decode('utf-8', errors='replace').strip()
|
||||
if out: print(out[-800:])
|
||||
if err: print(f"ERR: {err[-300:]}")
|
||||
return out
|
||||
|
||||
# Verify index.js patch
|
||||
run("grep -n 'system/config/public' /www/wwwroot/zhinengtiapp/index.js")
|
||||
|
||||
# Verify admin.html patch
|
||||
run("grep -n 'UPDATE_COS' /www/wwwroot/zhinengtiapp/public/admin.html")
|
||||
|
||||
# Test endpoint
|
||||
run("curl -s http://localhost:3002/api/system/config/public")
|
||||
|
||||
# Check .env for COS_BUCKET
|
||||
run("grep COS_ /www/wwwroot/zhinengtiapp/.env")
|
||||
|
||||
# Check sysConfig
|
||||
run("cat /www/wwwroot/zhinengtiapp/data/sysConfig.json 2>/dev/null | head -20")
|
||||
|
||||
client.close()
|
||||
print("\n=== DONE ===")
|
||||
@@ -0,0 +1,11 @@
|
||||
import paramiko
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect('152.136.232.83', port=22, username='root', password='Jianhua105', timeout=10)
|
||||
|
||||
stdin, stdout, stderr = client.exec_command("sed -n '1075,1110p' /www/wwwroot/zhinengtiapp/index.js", timeout=15)
|
||||
out = stdout.read().decode('utf-8', errors='replace').strip()
|
||||
print(out)
|
||||
|
||||
client.close()
|
||||
@@ -0,0 +1,11 @@
|
||||
import paramiko
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect('152.136.232.83', port=22, username='root', password='Jianhua105', timeout=10)
|
||||
|
||||
stdin, stdout, stderr = client.exec_command("sed -n '890,940p' /www/wwwroot/ipzhinengti/server.js", timeout=15)
|
||||
out = stdout.read().decode('utf-8', errors='replace').strip()
|
||||
print(out)
|
||||
|
||||
client.close()
|
||||
@@ -0,0 +1,132 @@
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const net = require("node:net");
|
||||
const { spawn, spawnSync } = require("node:child_process");
|
||||
|
||||
const projectRoot = path.resolve(__dirname, "..");
|
||||
const serverUrl = process.env.VITE_DEV_SERVER_URL || "http://127.0.0.1:3354";
|
||||
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
|
||||
const requiredNodeMajor = 20;
|
||||
const checkOnly = process.argv.includes("--check");
|
||||
|
||||
function log(message = "") {
|
||||
process.stdout.write(`${message}\n`);
|
||||
}
|
||||
|
||||
function fail(message) {
|
||||
process.stderr.write(`\n[start-local] ${message}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function parseServerUrl(url) {
|
||||
try {
|
||||
return new URL(url);
|
||||
} catch {
|
||||
fail(`VITE_DEV_SERVER_URL 无效:${url}`);
|
||||
}
|
||||
}
|
||||
|
||||
function checkCommand(command, args, label) {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: projectRoot,
|
||||
encoding: "utf8",
|
||||
stdio: "pipe",
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
|
||||
if (result.error || result.status !== 0) {
|
||||
fail(`未检测到 ${label},请先安装后再运行。`);
|
||||
}
|
||||
|
||||
return (result.stdout || result.stderr || "").trim();
|
||||
}
|
||||
|
||||
function checkNodeVersion() {
|
||||
const major = Number(process.versions.node.split(".")[0]);
|
||||
if (!Number.isFinite(major) || major < requiredNodeMajor) {
|
||||
fail(`当前 Node.js 为 ${process.version},建议使用 .nvmrc 指定的 Node.js ${requiredNodeMajor}。`);
|
||||
}
|
||||
}
|
||||
|
||||
function checkDependencies() {
|
||||
const viteBin = path.join(projectRoot, "node_modules", ".bin", process.platform === "win32" ? "vite.cmd" : "vite");
|
||||
const electronPackage = path.join(projectRoot, "node_modules", "electron", "package.json");
|
||||
|
||||
if (!fs.existsSync(viteBin) || !fs.existsSync(electronPackage)) {
|
||||
fail("依赖尚未安装,请先执行:npm install");
|
||||
}
|
||||
}
|
||||
|
||||
function assertPortAvailable(host, port) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer();
|
||||
|
||||
server.once("error", error => {
|
||||
if (error.code === "EADDRINUSE") {
|
||||
reject(new Error(`端口 ${port} 已被占用,请先关闭已有 Vite/Electron 开发进程。`));
|
||||
return;
|
||||
}
|
||||
reject(error);
|
||||
});
|
||||
|
||||
server.once("listening", () => {
|
||||
server.close(resolve);
|
||||
});
|
||||
|
||||
server.listen(port, host);
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
process.chdir(projectRoot);
|
||||
|
||||
const parsedUrl = parseServerUrl(serverUrl);
|
||||
const host = parsedUrl.hostname || "127.0.0.1";
|
||||
const port = Number(parsedUrl.port || 3344);
|
||||
|
||||
log("========================================");
|
||||
log(" 本地快速启动:Vite + Electron");
|
||||
log("========================================");
|
||||
log(`项目目录:${projectRoot}`);
|
||||
log(`开发地址:${serverUrl}`);
|
||||
log("");
|
||||
|
||||
checkNodeVersion();
|
||||
const npmVersion = checkCommand(npmCommand, ["--version"], "npm");
|
||||
checkDependencies();
|
||||
await assertPortAvailable(host, port);
|
||||
|
||||
log(`Node.js:${process.version}`);
|
||||
log(`npm:${npmVersion}`);
|
||||
log("");
|
||||
|
||||
if (checkOnly) {
|
||||
log("自检通过:可以执行 npm run start:local 或双击 start-local.bat 启动。");
|
||||
return;
|
||||
}
|
||||
|
||||
log("正在启动应用,关闭此窗口即可停止开发服务。");
|
||||
log("");
|
||||
|
||||
const child = spawn(npmCommand, ["run", "dev:electron"], {
|
||||
cwd: projectRoot,
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
VITE_DEV_SERVER_URL: serverUrl,
|
||||
},
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
|
||||
child.on("exit", code => {
|
||||
process.exit(code ?? 0);
|
||||
});
|
||||
|
||||
child.on("error", error => {
|
||||
fail(`启动失败:${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
fail(error.message);
|
||||
});
|
||||
@@ -0,0 +1,207 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = path.join(__dirname, '..');
|
||||
const tsSourcePath = path.join(repoRoot, 'src', 'config', 'systemSubtitleTemplates.ts');
|
||||
const runtimeDatabasePath = path.join(repoRoot, 'data', 'database.db');
|
||||
const runtimeExportPath = path.join(repoRoot, 'build', 'runtime-system-subtitle-templates.json');
|
||||
const previewAssetDir = path.join(repoRoot, 'electron', 'resources', 'extra', 'common', 'cover-templates');
|
||||
const packagedPreviewPrefix = 'extra/common/cover-templates';
|
||||
const inlineTemplateIds = 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 defaultTriggeredSoundConfig = {
|
||||
volume: 0.7,
|
||||
pitch: 1,
|
||||
fadeIn: 0,
|
||||
fadeOut: 50,
|
||||
triggerTime: 'start',
|
||||
};
|
||||
|
||||
const systemTemplateJsonPaths = [
|
||||
path.join(repoRoot, 'electron', 'config', 'system-templates.json'),
|
||||
path.join(repoRoot, 'electron', 'resources', 'extra', 'common', 'config', 'system-templates.json'),
|
||||
];
|
||||
|
||||
const defaultSubtitleTemplateJsonPaths = [
|
||||
path.join(repoRoot, 'electron', 'config', 'default-subtitle-templates.json'),
|
||||
path.join(repoRoot, 'electron', 'resources', 'extra', 'common', 'config', 'default-subtitle-templates.json'),
|
||||
];
|
||||
|
||||
const isAbsoluteLocalPath = (value) => /^[a-zA-Z]:[\\/]/.test(value) || value.startsWith('\\\\');
|
||||
|
||||
const stripMachineSpecificFontPaths = (value) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(stripMachineSpecificFontPaths);
|
||||
}
|
||||
|
||||
if (!value || typeof value !== 'object') {
|
||||
return value;
|
||||
}
|
||||
|
||||
const next = {};
|
||||
for (const [key, child] of Object.entries(value)) {
|
||||
if (key === 'fontPath' && typeof child === 'string' && isAbsoluteLocalPath(child)) {
|
||||
continue;
|
||||
}
|
||||
next[key] = stripMachineSpecificFontPaths(child);
|
||||
}
|
||||
return next;
|
||||
};
|
||||
|
||||
const ensureBundledPreviewImage = (templateId, imagePath) => {
|
||||
if (!imagePath || typeof imagePath !== 'string') {
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
const ext = path.extname(imagePath) || '.png';
|
||||
const fileName = `subtitle-preview-${templateId}${ext.toLowerCase()}`;
|
||||
const bundledRelativePath = `${packagedPreviewPrefix}/${fileName}`;
|
||||
const bundledAbsolutePath = path.join(previewAssetDir, fileName);
|
||||
|
||||
fs.mkdirSync(previewAssetDir, { recursive: true });
|
||||
|
||||
if (isAbsoluteLocalPath(imagePath)) {
|
||||
if (fs.existsSync(imagePath)) {
|
||||
const shouldCopy =
|
||||
!fs.existsSync(bundledAbsolutePath) ||
|
||||
fs.statSync(bundledAbsolutePath).size !== fs.statSync(imagePath).size;
|
||||
if (shouldCopy) {
|
||||
fs.copyFileSync(imagePath, bundledAbsolutePath);
|
||||
}
|
||||
return bundledRelativePath;
|
||||
}
|
||||
|
||||
if (fs.existsSync(bundledAbsolutePath)) {
|
||||
return bundledRelativePath;
|
||||
}
|
||||
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
return imagePath;
|
||||
};
|
||||
|
||||
const normalizeTemplateForPackaging = (template) => {
|
||||
const normalized = stripMachineSpecificFontPaths(JSON.parse(JSON.stringify(template)));
|
||||
const previewImage = normalized?.config?.previewConfig?.backgroundImage;
|
||||
const bundledPreview = ensureBundledPreviewImage(normalized.id, previewImage);
|
||||
if (bundledPreview) {
|
||||
normalized.config.previewConfig.backgroundImage = bundledPreview;
|
||||
}
|
||||
|
||||
if (inlineTemplateIds.has(normalized.id) && Array.isArray(normalized?.config?.keywordGroupsStyles)) {
|
||||
const soundEffects = ['bubble', null, null, 'ding'];
|
||||
normalized.config.keywordGroupsStyles = normalized.config.keywordGroupsStyles.map((group, index) => {
|
||||
if (index > 3 || !group || typeof group !== 'object') {
|
||||
return group;
|
||||
}
|
||||
|
||||
const soundEffectId = soundEffects[index];
|
||||
return {
|
||||
...group,
|
||||
soundEffectId,
|
||||
soundEffectConfig: soundEffectId ? { ...defaultTriggeredSoundConfig } : null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return normalized;
|
||||
};
|
||||
|
||||
const readTemplatesFromRuntimeDatabase = () => {
|
||||
if (!fs.existsSync(runtimeExportPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const templates = JSON.parse(fs.readFileSync(runtimeExportPath, 'utf8'));
|
||||
if (!Array.isArray(templates) || templates.length !== 16) {
|
||||
return null;
|
||||
}
|
||||
return templates;
|
||||
} catch (error) {
|
||||
console.warn('[sync-system-subtitle-sources] failed to read runtime export:', error?.message || error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const readTemplatesFromTsSource = () => {
|
||||
const source = fs.readFileSync(tsSourcePath, 'utf8');
|
||||
const match = source.match(/export const SYSTEM_TEMPLATES = (\[[\s\S]*?\]);/);
|
||||
if (!match) {
|
||||
throw new Error(`Failed to locate SYSTEM_TEMPLATES in ${tsSourcePath}`);
|
||||
}
|
||||
|
||||
const templates = JSON.parse(match[1]);
|
||||
if (!Array.isArray(templates) || templates.length !== 16) {
|
||||
throw new Error(`Expected 16 canonical system templates, received ${Array.isArray(templates) ? templates.length : 'invalid data'}`);
|
||||
}
|
||||
|
||||
return templates;
|
||||
};
|
||||
|
||||
const readCanonicalTemplates = () => {
|
||||
const dbTemplates = readTemplatesFromRuntimeDatabase();
|
||||
if (dbTemplates) {
|
||||
return {
|
||||
source: 'runtime-db-export',
|
||||
templates: dbTemplates.map(normalizeTemplateForPackaging),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
source: 'ts-source',
|
||||
templates: readTemplatesFromTsSource().map(normalizeTemplateForPackaging),
|
||||
};
|
||||
};
|
||||
|
||||
const writeJsonFile = (filePath, updater) => {
|
||||
const existing = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
const next = updater(existing);
|
||||
fs.writeFileSync(filePath, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
|
||||
};
|
||||
|
||||
const syncSystemSubtitleSources = () => {
|
||||
const { source, templates } = readCanonicalTemplates();
|
||||
const syncedAt = new Date().toISOString();
|
||||
|
||||
for (const filePath of systemTemplateJsonPaths) {
|
||||
writeJsonFile(filePath, (json) => ({
|
||||
...json,
|
||||
subtitleTemplates: templates,
|
||||
timestamp: syncedAt,
|
||||
}));
|
||||
}
|
||||
|
||||
for (const filePath of defaultSubtitleTemplateJsonPaths) {
|
||||
writeJsonFile(filePath, (json) => ({
|
||||
...json,
|
||||
subtitleTemplates: templates,
|
||||
timestamp: syncedAt,
|
||||
}));
|
||||
}
|
||||
|
||||
return {
|
||||
count: templates.length,
|
||||
ids: templates.map((template) => template.id),
|
||||
syncedAt,
|
||||
source,
|
||||
};
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
const result = syncSystemSubtitleSources();
|
||||
console.log('[sync-system-subtitle-sources] synced canonical system subtitle templates', result);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
syncSystemSubtitleSources,
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const rootDir = path.resolve(__dirname, '..');
|
||||
|
||||
function isSemverLike(version) {
|
||||
return /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(String(version || '').trim());
|
||||
}
|
||||
|
||||
function readJson(filePath) {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
}
|
||||
|
||||
function writeJson(filePath, value) {
|
||||
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
||||
}
|
||||
|
||||
function getAppPackageDirs() {
|
||||
const dirs = [rootDir];
|
||||
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.name === 'node_modules' || entry.name === 'tmp' || entry.name.startsWith('.')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const dir = path.join(rootDir, entry.name);
|
||||
if (
|
||||
fs.existsSync(path.join(dir, 'package.json')) &&
|
||||
fs.existsSync(path.join(dir, 'src', 'config.ts'))
|
||||
) {
|
||||
dirs.push(dir);
|
||||
}
|
||||
}
|
||||
|
||||
return dirs;
|
||||
}
|
||||
|
||||
function syncVersions(version, options = {}) {
|
||||
const targetVersion = String(version || '').trim();
|
||||
if (!isSemverLike(targetVersion)) {
|
||||
throw new Error(`Invalid version "${version}". Expected x.y.z, for example 4.5.34.`);
|
||||
}
|
||||
|
||||
const packageDirs = getAppPackageDirs();
|
||||
const changed = [];
|
||||
|
||||
for (const dir of packageDirs) {
|
||||
const packagePath = path.join(dir, 'package.json');
|
||||
const pkg = readJson(packagePath);
|
||||
const before = pkg.version;
|
||||
|
||||
if (before !== targetVersion) {
|
||||
pkg.version = targetVersion;
|
||||
writeJson(packagePath, pkg);
|
||||
changed.push(path.relative(rootDir, packagePath) || 'package.json');
|
||||
}
|
||||
|
||||
const configPath = path.join(dir, 'src', 'config.ts');
|
||||
if (fs.existsSync(configPath)) {
|
||||
let content = fs.readFileSync(configPath, 'utf8');
|
||||
const nextContent = content.replace(
|
||||
/version:\s*(?:packageJson\.version|["'][^"']*["'])/,
|
||||
'version: packageJson.version'
|
||||
);
|
||||
if (nextContent !== content) {
|
||||
fs.writeFileSync(configPath, nextContent, 'utf8');
|
||||
changed.push(path.relative(rootDir, configPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!options.silent) {
|
||||
console.log(`[sync-version] version=${targetVersion}`);
|
||||
console.log(`[sync-version] app packages=${packageDirs.length}`);
|
||||
if (changed.length === 0) {
|
||||
console.log('[sync-version] no changes');
|
||||
} else {
|
||||
changed.forEach((file) => console.log(`[sync-version] updated ${file}`));
|
||||
}
|
||||
}
|
||||
|
||||
return { version: targetVersion, packageCount: packageDirs.length, changed };
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
const argVersion = process.argv[2] || process.env.APP_VERSION || process.env.OEM_VERSION;
|
||||
try {
|
||||
syncVersions(argVersion);
|
||||
} catch (error) {
|
||||
console.error(`[sync-version] ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
syncVersions,
|
||||
getAppPackageDirs
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 读取 TypeScript 配置文件
|
||||
const tsFilePath = path.join(__dirname, '../src/config/systemSubtitleTemplates.ts');
|
||||
const jsonFilePath = path.join(__dirname, '../electron/resources/extra/common/config/system-templates.json');
|
||||
|
||||
console.log('读取 TypeScript 配置文件:', tsFilePath);
|
||||
const tsContent = fs.readFileSync(tsFilePath, 'utf-8');
|
||||
|
||||
// 提取 SYSTEM_TEMPLATES 数组
|
||||
const match = tsContent.match(/export const SYSTEM_TEMPLATES = (\[[\s\S]*?\]);/);
|
||||
if (!match) {
|
||||
console.error('无法找到 SYSTEM_TEMPLATES 定义');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 使用 eval 解析(因为这是 TypeScript 文件,但结构是 JSON)
|
||||
const templates = eval(match[1]);
|
||||
|
||||
console.log(`找到 ${templates.length} 个模板`);
|
||||
|
||||
// 读取现有的 system-templates.json
|
||||
console.log('读取现有的 system-templates.json:', jsonFilePath);
|
||||
const jsonContent = fs.readFileSync(jsonFilePath, 'utf-8');
|
||||
const jsonData = JSON.parse(jsonContent);
|
||||
|
||||
// 更新字幕模板配置
|
||||
console.log('更新字幕模板配置...');
|
||||
jsonData.subtitleTemplates = templates.map(template => ({
|
||||
id: template.id,
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
createdAt: template.createdAt,
|
||||
isSystem: template.isSystem !== undefined ? template.isSystem : true,
|
||||
config: template.config
|
||||
}));
|
||||
|
||||
// 保存更新后的配置
|
||||
console.log('保存更新后的配置...');
|
||||
fs.writeFileSync(jsonFilePath, JSON.stringify(jsonData, null, 2), 'utf-8');
|
||||
|
||||
console.log('✅ 更新完成!');
|
||||
console.log(`已更新 ${jsonData.subtitleTemplates.length} 个字幕模板`);
|
||||
|
||||
// 验证每个模板的配置
|
||||
jsonData.subtitleTemplates.forEach(t => {
|
||||
const hasTitleConfig = !!t.config.titleSubtitleConfig;
|
||||
const keywordCount = t.config.keywordGroupsStyles?.length || 0;
|
||||
console.log(` - ${t.id} (${t.name}): keywordGroupsStyles=${keywordCount}, titleSubtitleConfig=${hasTitleConfig}`);
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const jsonFilePath = path.join(__dirname, '../electron/resources/extra/common/config/system-templates.json');
|
||||
const data = JSON.parse(fs.readFileSync(jsonFilePath, 'utf-8'));
|
||||
|
||||
console.log('验证字幕模板配置:\n');
|
||||
data.subtitleTemplates.forEach(t => {
|
||||
const title = t.config.titleSubtitleConfig?.sourceTitle || '无标题';
|
||||
const keywordCount = t.config.keywordGroupsStyles?.length || 0;
|
||||
const hasTitleConfig = !!t.config.titleSubtitleConfig;
|
||||
console.log(`${t.id} (${t.name}):`);
|
||||
console.log(` 标题: ${title.substring(0, 30)}`);
|
||||
console.log(` 关键词组: ${keywordCount}`);
|
||||
console.log(` 标题配置: ${hasTitleConfig ? '有' : '无'}`);
|
||||
console.log('');
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user