Initial clean project import

This commit is contained in:
cat-shark
2026-06-19 18:41:41 +08:00
commit a13b804c7a
1306 changed files with 220568 additions and 0 deletions
+221
View File
@@ -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. 确保数据库中的配置与配置文件一致');
+89
View File
@@ -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(', ')} 配置相同`);
}
});
+148
View File
@@ -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);
+241
View File
@@ -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();
+146
View File
@@ -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);
});
+73
View File
@@ -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});
// }
// }
// }
// });
};
+30
View File
@@ -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}`);
}
});
+15
View File
@@ -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}`);
}
}
+44
View File
@@ -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();
+402
View File
@@ -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应用")
# ==================== 步骤1PyTorch 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)
+137
View File
@@ -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,
}
+118
View File
@@ -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}: 关键词组样式相同`);
}
});
+74
View File
@@ -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(', ')} 配置相同`);
}
});
+120
View File
@@ -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('当前配置文件中,关键词组样式已更新为不同字体');
+58
View File
@@ -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(', ')} 配置相同`);
}
});
+198
View File
@@ -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);
+66
View File
@@ -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);
});
+37
View File
@@ -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
+62
View File
@@ -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()
+320
View File
@@ -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);
+274
View File
@@ -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())
+119
View File
@@ -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();
+92
View File
@@ -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个不同配置的模板,会在上面显示。');
+87
View File
@@ -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`);
}
});
+93
View File
@@ -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();
+53
View File
@@ -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. 确保数据库初始化时使用正确的配置');
+221
View File
@@ -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);
});
+71
View File
@@ -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@*
+43
View File
@@ -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
+347
View File
@@ -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
+167
View File
@@ -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())
+54
View File
@@ -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); // 即使出错也继续启动
});
+20
View File
@@ -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.');
}
+168
View File
@@ -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}`);
+52
View File
@@ -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}`);
}
};
+346
View File
@@ -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);
+100
View File
@@ -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);
}
+115
View File
@@ -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. 如果需要恢复,可以从备份文件恢复');
+76
View File
@@ -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. 如果需要恢复,可以从备份文件恢复');
+78
View File
@@ -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');
+56
View File
@@ -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',
},
],
};
+24
View File
@@ -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()
+15
View File
@@ -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()
+33
View File
@@ -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()
+27
View File
@@ -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()
+43
View File
@@ -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 ===")
+25
View File
@@ -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()
+33
View File
@@ -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()
+37
View File
@@ -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()
+25
View File
@@ -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()
+19
View File
@@ -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()
+25
View File
@@ -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()
+50
View File
@@ -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 ===")
+66
View File
@@ -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()
+59
View File
@@ -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 ===")
+101
View File
@@ -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 ===")
+209
View File
@@ -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 ===")
+30
View File
@@ -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 ===")
+11
View File
@@ -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()
+32
View File
@@ -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 ===")
+32
View File
@@ -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 ===")
+32
View File
@@ -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 ===")
+11
View File
@@ -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()
+11
View File
@@ -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()
+132
View File
@@ -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);
});
+207
View File
@@ -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,
};
+105
View File
@@ -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}`);
});
+32
View File
@@ -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('');
});