feat: 任务列表右键菜单回调 + 主进程下载 + sharp 缩略图 + IPC 精简重构
- 任务右键菜单:实现打开输出目录(inputs/outputs/.cache 结构)、
复制资源链接、重新编辑、再次生成、拉取结果、重新下载、收藏、删除记录
- 主进程下载:通过 Electron net 模块绕过 CORS,支持 CDN 媒体文件下载
- sharp 缩略图:替换 nativeImage,支持 4K/8K+ 大尺寸素材的流式处理
- IPC 精简:40+ 冗余通道 → 15 个分组通道,移除未使用的 preload 桥接方法
- 任务存储:新增收藏字段、schema 迁移、task-store 持久化
- 清理废弃模块:canvas-ipc / updater / request / pricing
refactor: IPC/Preload/Services 模块化拆分 — 单一职责 + 向后兼容
主要变更:
- shared/constants/ipc-channels.ts → ipc/ 模块 (base/storage/canvas/updater)
- electron/preload.ts → preload/ 模块 (base/safe-storage/file-storage/canvas)
- electron/main/ipc/canvas-ipc.ts → canvas/ 模块 (types/walk/sidecar/mime/thumbnail)
- electron/main/ipc/storage-ipc.ts → 提取 utils/ (path/download/thumbnail)
- electron/main/updater.ts → updater/ 模块 (provider/platform-updaters)
- src/services/request.ts → request/ 模块 (token/interceptors/methods)
- src/shared/utils/pricing.ts → pricing/ 模块 (types/helpers/format/calculate/fields)
- src/modules/home/center/views/ModelInputForm.tsx → 提取 utils + DependentFormItem
架构优化:
- 所有拆分均保持向后兼容(旧导入路径仍可用)
- TypeScript 编译通过 ✓
- 每个模块单一职责,平均 ~130 行
This commit is contained in:
189
electron/main/ipc/canvas/scan-roots.ts
Normal file
189
electron/main/ipc/canvas/scan-roots.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
// ============================================================
|
||||
// canvas/scan-roots.ts — 扫描根目录计算
|
||||
//
|
||||
// 职责:
|
||||
// - 根据项目路径、阶段、资产类型计算扫描根目录列表
|
||||
// - 支持 shots 和 assets 两种目录结构
|
||||
// ============================================================
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { logger } from '../../logger';
|
||||
import type { StageType, AssetTypeFilter } from './types';
|
||||
|
||||
/** 阶段 → 允许的扩展名 */
|
||||
export function getStageExtensions(stage: StageType): string[] {
|
||||
switch (stage) {
|
||||
case 'Storyboard':
|
||||
case 'Keyframe':
|
||||
return ['jpg', 'jpeg', 'png', 'webp', 'bmp', 'gif'];
|
||||
case 'Video':
|
||||
return ['mp4', 'webm', 'mov', 'avi', 'mkv'];
|
||||
case 'Audio':
|
||||
case 'LipSync':
|
||||
return ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a'];
|
||||
case 'Assets':
|
||||
return ['jpg', 'jpeg', 'png', 'webp', 'bmp', 'gif', 'mp4', 'webm'];
|
||||
}
|
||||
}
|
||||
|
||||
/** 镜头名匹配查询 */
|
||||
export function shotNameMatchesQuery(shotName: string, query: string): boolean {
|
||||
if (!query) return true;
|
||||
const upperShot = shotName.toUpperCase();
|
||||
const upperQuery = query.toUpperCase();
|
||||
if (upperShot.includes(upperQuery)) return true;
|
||||
if (/^\d+$/.test(query)) {
|
||||
const digits = upperShot.replace(/\D/g, '');
|
||||
if (digits.includes(query)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算扫描根目录列表。
|
||||
*
|
||||
* @param params - 扫描参数
|
||||
* @returns 扫描根目录路径列表
|
||||
*/
|
||||
export function buildScanRoots(params: {
|
||||
projectPath: string;
|
||||
stage: StageType;
|
||||
assetTypeFilter: AssetTypeFilter;
|
||||
shotQuery: string;
|
||||
}): string[] {
|
||||
const { projectPath, stage, assetTypeFilter, shotQuery } = params;
|
||||
|
||||
// ── 诊断:检查 projectPath 本身是否存在 ──
|
||||
const projectExists = fs.existsSync(projectPath);
|
||||
logger.info('canvas', 'buildScanRoots 诊断 — projectPath', {
|
||||
projectPath,
|
||||
exists: projectExists,
|
||||
});
|
||||
|
||||
if (!projectExists) {
|
||||
logger.warn('canvas', '⚠️ 项目路径不可访问', undefined, { projectPath });
|
||||
// 尝试列出父目录内容以便排查
|
||||
const parent = path.dirname(projectPath);
|
||||
try {
|
||||
const siblings = fs.readdirSync(parent).filter((n) => !n.startsWith('.'));
|
||||
logger.info('canvas', `父目录 "${parent}" 内容(前 20 项)`, {
|
||||
count: siblings.length,
|
||||
items: siblings.slice(0, 20),
|
||||
});
|
||||
} catch {
|
||||
logger.warn('canvas', `无法读取父目录 "${parent}"`);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// ── 诊断:列出项目根目录内容 ──
|
||||
let projectEntries: string[] = [];
|
||||
try {
|
||||
projectEntries = fs.readdirSync(projectPath).filter((n) => !n.startsWith('.'));
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
logger.info('canvas', 'buildScanRoots 诊断 — 项目根目录内容(前 30 项)', {
|
||||
count: projectEntries.length,
|
||||
items: projectEntries.slice(0, 30),
|
||||
});
|
||||
|
||||
if (stage === 'Assets') {
|
||||
const assetsDir = path.join(projectPath, 'assets');
|
||||
logger.info('canvas', 'buildScanRoots — Assets 模式', {
|
||||
assetsDir,
|
||||
exists: fs.existsSync(assetsDir),
|
||||
assetTypeFilter,
|
||||
});
|
||||
switch (assetTypeFilter) {
|
||||
case '角色': return [path.join(assetsDir, 'chr')];
|
||||
case '道具': return [path.join(assetsDir, 'prp')];
|
||||
case '场景': return [path.join(assetsDir, 'set')];
|
||||
default: return [assetsDir];
|
||||
}
|
||||
}
|
||||
|
||||
const shotsDir = path.join(projectPath, 'shots');
|
||||
const shotsExists = fs.existsSync(shotsDir);
|
||||
logger.info('canvas', 'buildScanRoots 诊断 — shotsDir', {
|
||||
shotsDir,
|
||||
exists: shotsExists,
|
||||
});
|
||||
|
||||
if (!shotsExists) {
|
||||
// 列出可能存在的子目录帮助用户对比
|
||||
const dirsInRoot = projectEntries.filter((name) => {
|
||||
try {
|
||||
return fs.statSync(path.join(projectPath, name)).isDirectory();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
logger.warn('canvas', '⚠️ shots 目录不存在,项目根下的子目录:', undefined, {
|
||||
subdirs: dirsInRoot,
|
||||
});
|
||||
return [];
|
||||
}
|
||||
|
||||
const roots: string[] = [];
|
||||
let episodes: string[];
|
||||
try {
|
||||
episodes = fs.readdirSync(shotsDir).filter((name) => {
|
||||
const full = path.join(shotsDir, name);
|
||||
return !name.startsWith('.') && fs.statSync(full).isDirectory();
|
||||
});
|
||||
} catch (err) {
|
||||
logger.warn('canvas', '无法读取 shots 目录', err instanceof Error ? err : undefined, {
|
||||
error: (err as Error).message,
|
||||
});
|
||||
return [];
|
||||
}
|
||||
|
||||
logger.info('canvas', `buildScanRoots — 发现 ${episodes.length} 个剧集目录`, {
|
||||
episodes,
|
||||
});
|
||||
|
||||
for (const ep of episodes) {
|
||||
const epDir = path.join(shotsDir, ep);
|
||||
let shotNames: string[];
|
||||
try {
|
||||
shotNames = fs.readdirSync(epDir).filter((name) => {
|
||||
const full = path.join(epDir, name);
|
||||
return !name.startsWith('.') && fs.statSync(full).isDirectory();
|
||||
});
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
'canvas',
|
||||
`无法读取剧集目录 "${ep}"`,
|
||||
err instanceof Error ? err : undefined,
|
||||
{ error: (err as Error).message },
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.info('canvas', ` 📂 ${ep} → ${shotNames.length} 个镜头`, {
|
||||
shots: shotNames.slice(0, 20),
|
||||
});
|
||||
|
||||
for (const shot of shotNames) {
|
||||
if (shotQuery && !shotNameMatchesQuery(shot, shotQuery)) continue;
|
||||
const stageDir = path.join(epDir, shot, stage);
|
||||
const stageExists = fs.existsSync(stageDir);
|
||||
if (stageExists) {
|
||||
roots.push(stageDir);
|
||||
}
|
||||
// 仅在前几个镜头时记录阶段目录存在性
|
||||
if (roots.length <= 3 && !stageExists) {
|
||||
logger.info('canvas', ` 阶段目录不存在: ${stageDir}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('canvas', `buildScanRoots 最终结果:${roots.length} 个扫描根`, {
|
||||
roots: roots.slice(0, 10),
|
||||
stage,
|
||||
});
|
||||
|
||||
return roots;
|
||||
}
|
||||
Reference in New Issue
Block a user