- 任务右键菜单:实现打开输出目录(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 行
190 lines
6.0 KiB
TypeScript
190 lines
6.0 KiB
TypeScript
// ============================================================
|
|
// 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;
|
|
}
|