RCA EL、力禧特EL、利珀PL、森标 AOI 图片拷贝与 Excel 解析工具,基于 Electron + React 的桌面应用(Windows x64)。
-
图片拷贝要求目标机器上存在名为
DC的根目录,目录层级为:DC/<机台>/EL/<日期 YYYYMMDD>/<班次>/<时段>/<类型>/<图片> -
森标 AOI 的路径为
\\<机台地址>\<SaveImages|SaveSmallImages>\<日期 YYYYMMDD>\<8.5|20.5>\<类型>\<图片>; 第二层由「图片类型」决定:整体读SaveImages(整张扫描图,.bmp),局部读同级的SaveSmallImages(异常区域裁图,.jpg)。入口同样是「根目录 / 东区|西区 / .lnk」, 白班对应8.5、夜班对应20.5,没有时段层;机台列表还可按「检测面」(正检/背检)筛选 -
力禧特EL 的共享名在扫描时探测后固定:逐台比较
el/SaveImages下的A-01-B日期目录,本次日期优先,其次近 30 天;两边无法区分时用SaveImages。 路径不可用或没有本次日期时会自动重新探测,null表示尚未确定 -
利珀PL 的机台标识目录同样按日期选择:优先本次日期,其次近 30 天; 缓存不可用或过期时重新探测候选目录,
null表示尚未确定 -
根目录只需配置一次,会持久化到用户数据目录,再次打开直接复用:
%APPDATA%\QuickFetch\settings.json。扫描历史同样落在同目录的history\下。 配置时自动识别DC下的机台目录,首字母A归东区、B归西区目录名取的是应用名,Electron 启动时读
app.asar\package.json的productName(本项目为QuickFetch)。electron-builder.yml里的productName: QuickFetch只决定 exe 文件名、快捷方式和窗口标题,不参与目录命名。 因此打包版与开发态共用同一个目录,同一台机器上开发调试会读写到现场那份配置。 环境变量使用QUICKFETCH_*。 -
支持扩展名:
.png.jpg.jpeg.webp.gif -
扫描在主进程用
fs.readdir完成(不再受浏览器 File System Access API 限制),分三段执行: 探前缀(机台 → EL → 日期 → 班次)→ 列真实时段与类型后取交集 → 只对命中的类型目录收图。 森标 AOI 没有时段层,班次目录下直接列类型。父级缺失的分支整段跳过,不发无效请求 -
任务状态:待开始 / 进行中 / 已暂停 / 已完成 / 已取消 / 已中断 / 失败。暂停可断点续跑; 连续 5 个目录不可达会自动暂停并提示网络中断。
已中断只出现在历史记录里, 是程序退出时抢救未完成任务的状态 -
扫描结果会写入目标目录的
source.txt(相对路径清单);图片按所选分组方式落盘 (默认按机台)。遇到同名文件默认跳过目标里已有的那一张,同组其余照常复制; 也可在复制面板切换为自动改名(img.jpg→img1.jpg→img2.jpg)或覆盖
Node.js >= 22.12.0(见 engines 与 .nvmrc),pnpm 10.12.3。
pnpm install
pnpm dev # electron-vite 起渲染层 dev server 并拉起 Electron
pnpm typecheck
pnpm format:check # 只检查;写入用 pnpm format:fix若启动报
Electron uninstall,说明依赖的安装脚本被 pnpm 跳过;pnpm dev会通过scripts/ensure-electron.cjs自动补下载。
现象:Vite 的 main / preload 都编译成功、dev server 也起来了,随后 Electron 静默退出,
终端只剩 ELIFECYCLE Command failed with exit code 2147483651.,没有窗口,
也看不到任何主进程日志。
2147483651 就是 0x80000003(STATUS_BREAKPOINT)。Electron 在执行
out/main/index.cjs 之前就崩了,所以业务代码里的日志一行都打不出来。最常见的原因是
项目目录的文件被标成了 Low 完整性级别(Low Mandatory Level):Chromium 的沙箱初始化
需要正常的完整性级别,拿到 Low 会直接触发断点异常。
排查(Windows,在项目根目录执行):
# 正常应直接输出 v45.x.x;崩溃或没有输出说明 Electron 起不来
node_modules\electron\dist\electron.exe --version
# 只有加 --no-sandbox 才正常 → 确认是沙箱初始化失败(只用来定位,不要当修复)
node_modules\electron\dist\electron.exe --no-sandbox --version
# 出现 "Mandatory Label\Low Mandatory Level" 就是根因
icacls . | findstr Mandatory修复:
icacls . /setintegritylevel "(OI)(CI)Medium" /T /C必须加 /T:只改项目根不够,node_modules\electron\dist\electron.exe 本身也得是
Medium。修完重新 pnpm dev 即可。从这种目录复制出来的其它副本(例如桌面上的
项目副本(例如 quickfetch2)会有同样的问题,要一起处理。
pnpm dev / pnpm start 都会先执行 scripts/ensure-electron.cjs,它现在会实际跑一次
electron --version,失败时直接打印上面的排查和修复命令;确认环境无误、需要临时跳过时设
QUICKFETCH_SKIP_ELECTRON_CHECK=1。
这类 Low 完整性 ACL 不是 Windows 或 git 自己产生的,通常来自某个沙箱 / 受限运行工具, 或者用
robocopy /COPYALL、xcopy /O、7-Zip「还原文件安全信息」等保留 ACL 的方式搬过 目录。修复后又变回 Low,可以用 Process Monitor 过滤SetSecurity抓出改它的进程。
macOS 没有 Windows 的完整性级别机制,但同样可能在打开窗口前退出。按顺序排查:
# 架构:Apple Silicon 应输出 arm64,Intel 为 x86_64
file node_modules/electron/dist/Electron.app/Contents/MacOS/Electron
# 隔离属性:有输出说明被 Gatekeeper 标记
xattr -p com.apple.quarantine node_modules/electron/dist/Electron.app
# 签名校验
codesign --verify --deep --strict node_modules/electron/dist/Electron.app
# 执行权限
ls -l node_modules/electron/dist/Electron.app/Contents/MacOS/Electron对应处理:
# 清除下载带来的隔离属性
xattr -dr com.apple.quarantine node_modules/electron/dist/Electron.app
# 开发环境可以 ad-hoc 重签
codesign --force --deep --sign - node_modules/electron/dist/Electron.app
# 补执行权限
chmod +x node_modules/electron/dist/Electron.app/Contents/MacOS/Electron- 进程被系统直接杀掉(shell 显示
Killed: 9、退出码 137)基本是签名或隔离属性问题 Bad CPU type in executable说明拉到的二进制架构和机器不一致,删除node_modules/electron后重新pnpm install- 仍然起不来就看 Console.app 里 Electron 的报错
macOS 只用于开发调试,打包产物只有 Windows 版(
pnpm dist:win)。
pnpm build # 产物输出到 out/(main、preload、renderer)
pnpm dist:win # 校验 → build → NSIS 安装包 + 免安装 zip,输出到 release/release/ 里会同时生成:
QuickFetch-<version>-win-x64.exe:NSIS 安装版,支持自动更新QuickFetch-<version>-win-x64.exe.blockmap:差分更新用latest.yml:electron-updater 的版本清单QuickFetch-<version>-win-x64.zip:免安装版,给禁止安装外部应用的机台
dist:win 会先跑 scripts/check-release.cjs:version 必须是 x.y.z、与 HEAD 的
package.json 一致,且工作区没有未提交改动。这样产物名、asar 内 package.json、
app.getVersion() 与侧边栏显示的版本都对应同一次提交。校验失败时先提交或 git stash;
只有明确要求未提交出包时才用覆盖开关(它仍会检查版本格式):
$env:SKIP_RELEASE_CHECK=1; pnpm dist:win正式发版由 tag 触发:推送 v3.3.0 这类 tag 后,.github/workflows/release.yml 会校验
tag 与版本、跑检查与打包,创建 GitHub Release 并上传安装包、blockmap、latest.yml 和 zip。
tag 必须等于 package.json 的 version,且指向 main 上的提交;workflow 文件本身也要先
随 main 推到远端,tag 才能触发它。手动出包仍用上面的 pnpm dist:win。
git tag v3.3.0
git push origin v3.3.0- 安装版:启动约 10 秒后检查 GitHub Releases(可在设置页关闭),发现新版本后由用户 确认下载,完成后询问是否重启安装;扫描或复制任务运行中会禁止重启。
- 免安装版(zip):只能检查到新版本并提示打开 Releases 页面下载 zip,关闭应用后覆盖 原目录;不会自动安装,也不会把机器上的免安装版变成安装版。
- 安装包未做代码签名,首次安装可能被 SmartScreen 拦截,选择「更多信息 → 仍要运行」即可。
- 升级只替换程序文件,
%APPDATA%\QuickFetch里的设置、历史与日志不受影响。
pnpm start 使用上一次 pnpm build 的产物预览;改完源码要先重新构建。
打包配置见 electron-builder.yml,Windows 图标取 build/icon.ico(16–256 多尺寸)。
标志的源文件、两个配色版本(靛蓝 / 墨黑)与使用规范都在 brand/。
src/
├── main/ # 主进程(领域引擎内聚)
│ ├── index.ts # 窗口创建、应用生命周期
│ ├── ipc/ # 按模块划分的 IPC 注册层(index.ts 聚合 settings/scan/copy/history/excel)
│ ├── services/ # 核心服务
│ │ ├── scanner/ # 扫描任务引擎(任务表、各类 resolver、探测、收图、历史落盘)
│ │ ├── copy/ # 复制任务引擎(平铺复制、命名冲突策略、任务表)
│ │ ├── discovery/ # 机台与快捷方式识别(LXT / PL / lnk / shortcuts)
│ │ ├── settings/ # 配置管理与环境覆盖持久化(userData/settings.json)
│ │ └── update/ # 自动更新(electron-updater 接线、状态机、安装前任务闸门)
│ └── utils/ # 通用工具(自适应并发采样、UNC 路径规范化)
├── preload/index.ts # contextBridge 暴露 window.electronAPI
├── shared/ # 主进程 ↔ preload ↔ 渲染层共用的类型契约(ipc.ts, historyLimit.ts)
└── renderer/ # 渲染层(Feature-First 特性优先架构)
├── index.html
├── main.tsx # 挂载入口(HashRouter)
├── app.tsx # Radix <Theme> Provider + 全局 Toaster
├── routes/ # 路径常量与懒加载路由表(paths.ts、app-routes.tsx)
├── index.css # 全局基线:Radix/font 导入、令牌、reset、共享工具类、sonner
├── layouts/ # 框架布局(app-layout、app-sidebar,样式与组件同目录)
├── features/ # 业务特性模块(高内聚,页面、组件、Hook、Store、样式在一起)
│ ├── scan/ # 图片扫描(EL / 力禧特EL / 利珀PL / 森标 AOI 页面、组件与状态)
│ ├── copy/ # 复制管理(copy-panel、useImageCopy、copyStore)
│ ├── history/ # 历史记录(history-panel、historyStore)
│ ├── excel/ # Excel 解析(上传、解析、筛选、表格与状态)
│ ├── update/ # 自动更新(设置分区、侧边栏标记、状态 store)
│ └── settings/ # 应用设置(settings-page、settingsStore、userPreferencesStore)
├── components/ # 全局跨模块组件(每个组件一个目录:kebab-case 组件文件 + 同名 CSS)
│ ├── choice-list/ # 基于 Radix Themes 的单选/多选组件
│ └── date-picker/ # 基于 Radix Popover + react-day-picker 的日期选择
├── hooks/ # 全局 UI 辅助 Hook(useToast)
├── lib/utils.ts # cn()
├── utils/ # 全局通用工具函数
└── public/img/ # 静态资源
构建产物:out/main、out/preload、out/renderer;安装包、zip 与更新清单输出到 release/。
- 组件库:Radix Themes(
@radix-ui/themes),根节点由<Theme>包裹 - 样式:
@radix-ui/themes/styles.css,使用 Radix 设计令牌 - 字体:拉丁使用
Public Sans Variable,中文回退到系统字体(Windows 微软雅黑、macOS PingFang SC、Linux Noto Sans CJK);不打包 CJK 字体。字体变量需同时覆盖:root与.radix-themes,否则 Radix 组件会使用自己的默认栈 - 布局与文本优先使用
Box/Flex/Grid/Text/Heading;原生标签只保留语义必需的button、label、input、details等 - 设计层:全局令牌、reset、共享工具类与 sonner 覆盖在
src/renderer/index.css;组件 CSS 与组件同目录(渲染层组件目录、组件文件与样式统一 kebab-case),业务 feature CSS 在各自 feature 目录(如features/scan/scan.css)。不引入 reset / 其它组件库 - 页面标题统一由
components/page-header/page-header渲染,AppLayout不再重复标题栏;扫描页的任务进度、统计与控制按钮统一放在features/scan/components/scan-task-panel - 窗口使用自定义顶带:顶部 40px 白带只承载系统窗口控件(macOS 红绿灯 / Windows 右上角按钮),不显示标题,同时兼作拖拽区;页面标题仍由
PageHeader渲染 - 内容区是固定浮起的卡片(四周 12px 间隙、圆角、边框与轻阴影),侧边栏与内容之间不加竖线,靠
--app-canvas底色与间隙分隔;页面在卡片内部滚动 - 选择类控件统一使用
components/choice-list(多选)与ChoiceRadio(单选),整个单元格可点击 - 别名:
@/*→src/renderer/*(tsconfig*.json与electron.vite.config.ts同步配置) - 图标:
@radix-ui/react-icons;提示:sonner;cn()为内部纯逻辑实现
- 渲染层不做任何文件系统操作,全部经
window.electronAPI走 IPC,由主进程用node:fs执行 - 配置(根目录、机台列表)只有主进程一份,落盘在
userData/settings.json;渲染层只持有镜像 - 扫描结果全量留在主进程,按
taskId引用;渲染层只接收统计与前 200 条预览, 复制时传taskId让主进程取全量,避免上万条路径在 IPC 上来回搬 - 主进程/preload 打包为 CommonJS(
.cjs),渲染层为 ESM;仓库本身是type: module - 路由使用
HashRouter:生产环境通过file://加载,history 路由会 404
现场电脑为 8G 内存 + 256G 盘,图片根目录在共享盘上,所以读取强度是刻意压低的:
| 项 | 默认值 | 说明 |
|---|---|---|
| 目录读取并发 | 4 | 每个并发约占一个句柄 + 一条 SMB 请求 |
| 机台分批 | 2 | 一批机台跑完再开下一批,内存峰值只与单批相关 |
| 复制并发 | 6 | Windows 上共享冲突高发,不宜再高 |
| 复制分批 | 50 张 | 批之间让出事件循环,进度也可按批反馈 |
改档位在应用侧边栏底部的设置页里调,落盘到 settings.json,不必重新打包。
EL / 力禧特EL / 利珀PL / 森标 AOI 共用同一套设置,包括分组方式、重名策略与全部性能档位。
临时压降仍可用环境变量 QUICKFETCH_READ_CONCURRENCY、QUICKFETCH_MACHINE_BATCH、
QUICKFETCH_COPY_CONCURRENCY、QUICKFETCH_COPY_BATCH——环境变量优先于设置页,
被接管的项在设置页会标出变量名并禁用输入。
设置页还有调试日志开关(默认关闭,开启后立即生效):日志写到
%APPDATA%\QuickFetch\logs\quickfetch-YYYY-MM-DD.log,记录扫描/复制、网络探测、IPC 与渲染层异常,
按天分文件并保留最近 7 天。现场复现问题前先打开它,出问题后点「打开日志目录」取文件。
三条硬约束:
- 并发闸门不可嵌套。已占用名额的任务里不要再
limiter(...)调用自己, 并发降到 1 时会死锁。所以第 5 层是「先并发读、再串行处理结果」两步走, 机台层也不再包一层 limiter。 - 探测与收尾共用同一个闸门,总并发不会叠加。探测只拿目录名(几 MB),
文件明细统一在
collectDetails补读,避免几万张图的路径同时驻留内存。 - 失败率自适应降档:每 20 次读取统计一次,不可达超过 25% 就降一档(下限 1), 单次超时同步从 20 秒收紧到 8 秒。只降不升——反复试探只会让吃紧的机器更抖。
每次扫描结束(完成 / 取消 / 失败 / 退出时中断)后,主进程把结果写入
userData/history/<taskId>.json,默认保留最近 10 条,可在界面上调整(1–50)。
「0 张图但至少有一个分支真正探测过」的诊断记录也会保留,方便回看失败原因。
- 存全量图片路径,不是内存快照里的 200 条预览。历史任务要能再次复制, 只存预览会让 200 条之后的结果静默消失。
- 落盘在任务真正结束后触发:
cancel()是立即返回canceled的, 执行体还要走几个检查点才退出,runner.waitSettled()保证不会把取消瞬间的 中间结果写成最终结果。 - 原子写(temp + rename),扫描完成时如果正好休眠或断电,不会留下半截 JSON。
- 裁剪只删最旧的:改小保留条数后立即裁剪,改大不会找回已删的记录。
- 列表只带摘要,不带
branches与图片;点开某条才按 id 加载完整结果。 - 恢复历史会把参数回填到筛选面板(机台、日期、班次、时段、类型、厂区; 森标 AOI 没有时段),避免出现「界面显示今天白班、结果却是上周夜班」的错位。
- 单个历史文件损坏只跳过该条,不影响列表。