Files
ele-HeiXiu/electron/main/ipc/canvas/scan-roots.ts
YoungestSongMo 136a17c0bf 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 行
2026-07-03 11:13:36 +08:00

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;
}