Ctrl K
ComunidadGuías…

electron-builder.config.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// Copyright (c) 2026 开发者猫叔 (DeveloperCatUncle) · 商业使用需授权:COMMERCIAL-LICENSE.md
"use strict";
/**
 * 打安装包的配置。跑 npm run dist:mac / npm run dist:win。
 *
 * 三个不能改的决定,改之前先看理由:
 *
 * 1) asar: false —— 必须关。tools.js 的 run_node 会把 node_modules 软链进
 *    workspace/.tmp 好让生成的脚本 require 到依赖;asar 是个压缩包不是真目录,
 *    软链过去指向一个文件系统上不存在的路径,run_node 会直接失效。
 *    代价是应用包体积大、文件多,换的是「装完就能跑代码」这个核心能力。
 *
 * 2) 技能只打包 git 跟踪的那些。本机 skills/ 下还躺着几个第三方技能(没许可证、
 *    或者查不到上游),它们在 .gitignore 里,不该跟着安装包分发出去。
 *    这里直接问 git 要清单,而不是手写一份——手写的那份迟早跟 .gitignore 对不上。
 *
 * 3) 不签名、不公证(identity: null)。没有 Apple 开发者证书,硬签不了。
 *    后果是用户第一次打开会被 Gatekeeper 拦,README 里写了怎么放行。
 *    真要消掉这一步,得买证书 + 配 APPLE_ID/APPLE_APP_SPECIFIC_PASSWORD 走 notarize。
 */
// identity: null 拦不住签名——electron-builder 会自动去钥匙串里翻可用证书,
// 本机有一张个人的 Apple Development 证书,它就直接拿来签了。公开发的包不该带上任何人的
// 开发者身份(证书里有真实 Apple ID),而且 Development 证书在别人机器上照样过不了 Gatekeeper。
// 只有这个环境变量能真正关掉自动查找,关掉之后走 ad-hoc 签名(签名标识为 "-"),
// 这也是 Apple Silicon 上应用能启动的最低要求。放在配置文件里是为了 mac/Windows 都生效。
process.env.CSC_IDENTITY_AUTO_DISCOVERY = "false";

const { execSync } = require("child_process");
const fs = require("fs");
const path = require("path");

/** 只收 git 跟踪的内置技能;拿不到 git(比如下的是源码 zip)就退回全带上 */
function skillPatterns() {
  let names = [];
  try {
    const out = execSync("git ls-files skills", { cwd: __dirname, encoding: "utf8" });
    names = [...new Set(out.split("\n").filter(Boolean).map((p) => p.split("/")[1]).filter(Boolean))];
  } catch {}
  if (!names.length) {
    console.warn("[打包] 拿不到 git 清单,skills/ 全量打包——发布前确认里面没有不该分发的第三方技能");
    return ["skills/**/*"];
  }
  console.log(`[打包] 内置技能 ${names.length} 个:${names.join(", ")}`);
  return names.map((n) => `skills/${n}/**/*`);
}

/**
 * 打完包重新做一次 ad-hoc 签名。
 * 不做的话 codesign --verify 报「code has no resources but signature indicates they must be present」:
 * Electron 的二进制本身带一个 linker-signed 的 ad-hoc 签名,我们往包里塞了 app/ 之后那个签名就对不上了。
 * 本机双击可能还能开,但用户从网上下下来的包带 quarantine 标记,Gatekeeper 会直接判「已损坏」。
 * 签名标识用 "-" 就是 ad-hoc:不需要任何证书,只是让包内容自洽。
 * 它替代不了 Developer ID + 公证——用户首次打开仍要手动放行,README 里写了步骤。
 */
/** 包里 app/ 在哪:mac 埋在 .app 里,Windows/Linux 在 resources/ 下。找不到就报出找过哪些路径 */
function findAppDir(ctx) {
  const cands = [
    path.join(ctx.appOutDir, ctx.packager.appInfo.productFilename + ".app", "Contents", "Resources", "app"),
    path.join(ctx.appOutDir, "resources", "app"),
    path.join(ctx.appOutDir, "Resources", "app"),
  ];
  const hit = cands.find((d) => fs.existsSync(path.join(d, "package.json")));
  if (!hit) throw new Error("[打包] 找不到包里的 app/ 目录,完整性核对没法做。找过:\n" + cands.map((c) => "  - " + c).join("\n"));
  return hit;
}

async function afterPack(ctx) {
  // 闸门全过了再签名:缺文件、依赖 require 不起来、瘦身没生效——
  // 任何一条都该在这里红掉,别把一个必定打不开的包签得漂漂亮亮发出去
  const gate = require("./scripts/check-package-files");
  const appDir = findAppDir(ctx);
  // mermaid / echarts 只带了单文件,被删掉的那批依赖的许可证原文汇总补写进去(核对时要查它在不在)
  require("./scripts/slim-deps").writeLicenseDigests(appDir);
  gate.assertPackComplete(appDir);
  await gate.assertDepsRequirable(appDir);
  gate.assertSlimmed(appDir);
  gate.assertBundlesSlim(appDir);
  assertSandboxHelper(ctx, appDir);
  await adhocSign(ctx);
}

/**
 * Windows 包里得有本架构的系统沙箱小助手(native/bin,发版流水线现编):缺了,用户机器上沙箱就立不起来。
 * 另一个架构那份顺手删掉,用不上
 */
function assertSandboxHelper(ctx, appDir) {
  if (ctx.electronPlatformName !== "win32") return;
  const arch = { 1: "x64", 3: "arm64" }[ctx.arch]; // electron-builder 的 Arch:x64 = 1,arm64 = 3
  if (!arch) throw new Error(`[打包] 认不出的架构 ${ctx.arch},核不了沙箱小助手`);
  const dir = path.join(appDir, "native", "bin");
  const name = `owb-sandbox-${arch}.exe`;
  if (!fs.existsSync(path.join(dir, name))) throw new Error(`[打包] 包里没有沙箱小助手 native/bin/${name}:先跑 npm run build:sandbox(要装 Go)`);
  for (const f of fs.readdirSync(dir)) if (f !== name) fs.rmSync(path.join(dir, f), { force: true });
  console.log(`[打包] 沙箱小助手:native/bin/${name}`);
}

async function adhocSign(ctx) {
  if (ctx.electronPlatformName !== "darwin") return;
  const app = require("path").join(ctx.appOutDir, ctx.packager.appInfo.productFilename + ".app");
  execSync(`codesign --force --deep --sign - ${JSON.stringify(app)}`, { stdio: "inherit" });
  execSync(`codesign --verify --deep --strict ${JSON.stringify(app)}`, { stdio: "inherit" });
  console.log("[打包] ad-hoc 签名通过:" + app);
}

module.exports = {
  afterPack,
  appId: "com.catcatuncle.openworkbuddy",
  productName: "OpenWorkBuddy",
  copyright: "Copyright © 2026 CatCatUncle",
  asar: false, // 见文件头 (1)
  directories: { output: "dist", buildResources: "build" },

  // 白名单:只有列出来的才进包。用户数据(data/ workspace/ config.json backups/ .tmp/
  // plugins/ eval/runs/)一个都不能进——那是本机的账号和聊天记录,装机态它们在 ~/OpenWorkBuddy
  files: [
    // ⚠️ "*.js" 只匹配顶层,不含子目录。v0.1.1 就是漏了当时 engines 目录那 7 个文件:装机后
    // server.js 在 require 引擎时抛 MODULE_NOT_FOUND,端口没人监听、窗口不亮,
    // 用户看到的是「双击没反应 / 任务管理器有进程但没界面」。顶层只剩几个入口文件,
    // 其余源码全在 src/ 下,由 "src/**/*" 一条带上;漏了由 afterPack 的完整性闸门当场拦下
    // (scripts/check-package-files.js 按 SOURCE_DIRS 逐个文件核)。
    "*.js",
    "!electron-builder.config.js",
    "!eslint.config.js", // 开发期的 lint 配置,运行时一个字节都不读,"*.js" 会把它顺手带进包
    "src/**/*",
    "public/**/*",
    // Chrome 扩展:设置 → 浏览器 那颗「打开扩展文件夹」从这儿拷一份到数据目录给 Chrome 加载
    "extension/chrome/**/*",
    "eval/run.js",
    "eval/tasks.js",
    "eval/judge.js",
    "eval/rejudge.js",
    "eval/custom-sets.js",
    "eval/blind.js",
    "experts.json",
    "config.example.json",
    "package.json",
    "LICENSE",
    "COMMERCIAL-LICENSE.md",
    // LICENSE 正文里点名了它(哪些路径额外按 MIT)——被引用的文件不进包,等于那段话指向空气
    // 桌面包 asar:false,别人的 MIT/Apache 代码原样躺在 node_modules 里发出去了。
    // NOTICE.md 是这批第三方署名的唯一一份汇总,它自己不进包 = 分发时没带署名。
    "NOTICE.md",
    "README.md",
    ...skillPatterns(),

    // 依赖里那些「运行时一个字节都不读」的东西,不该进用户的下载包。
    // 量过:生产依赖 280 MB / 16809 个文件里,source map 占 116.8 MB、类型声明占 35 MB。
    // Windows 免安装版每次启动都要把整包解压到 %TEMP%,还要被 Defender 逐个扫,
    // 文件越多首次启动越久——issue #1「任务管理器里有进程、屏幕上没窗口」就是等在这儿。
    // node_modules 不在上面的白名单里(electron-builder 总是自动带上生产依赖),
    // 所以这里只能用排除式写法。
    "!node_modules/**/*.map", // devtools 才读的源码映射
    "!node_modules/@types/**", // TypeScript 类型包,编译期产物
    "!node_modules/**/*.d.ts",
    "!node_modules/**/*.d.mts",
    "!node_modules/**/*.d.cts",
    // ⚠️ 按目录名删(test/doc/example 之类)试过,当场炸:@iconify/utils 的运行时代码就住在
    // @iconify/utils/lib/emoji/test/ 下、exceljs 的核心在 exceljs/lib/doc/ 下,删完 mermaid 和 exceljs 都 require 不起来。
    // 那条规则只省 1 MB,换的是整包打不开——不做。留下的三条只按「运行时永远不读的文件类型」删。
    // LICENSE / *.md 一律留着:MIT 之类的许可证要求随分发附上原文,省这几 MB 不值当
    //
    // mermaid / echarts 运行时各只读一个打包好的单文件,其余源码和只被它俩用到的依赖(d3、cytoscape、zrender……)
    // 一个字节都不读,却占了 node_modules 一半的文件数。删哪些由 scripts/slim-deps.js 按 package-lock 现算,
    // 被删依赖的许可证由 afterPack 汇总写回,再由 assertBundlesSlim 核对删得对不对
    ...require("./scripts/slim-deps").excludePatterns(),
  ],

  mac: {
    fileAssociations: [{ ext: require("./src/desktop/open-files").EXTENSIONS, name: "OpenWorkBuddy Document", role: "Editor", rank: "Alternate" }],
    category: "public.app-category.productivity",
    icon: "build/icon.icns",
    identity: null, // 见文件头 (3)
    target: [
      { target: "dmg", arch: ["arm64", "x64"] },
      { target: "zip", arch: ["arm64", "x64"] },
    ],
    artifactName: "${productName}-${version}-mac-${arch}.${ext}",
    extendInfo: {
      // 这些是 macOS 的权限说明弹窗文案。截图/录屏能力和「打开所在位置」会摸到这些
      NSDesktopFolderUsageDescription: "OpenWorkBuddy 需要访问桌面来读写你交给它的文件",
      NSDocumentsFolderUsageDescription: "OpenWorkBuddy 需要访问文稿来读写你交给它的文件",
      NSDownloadsFolderUsageDescription: "OpenWorkBuddy 需要访问下载文件夹来读写你交给它的文件",
      // 任务里会用 osascript 操作别的应用(安全模块也靠 System Events 数进程)。没有这条说明,
      // macOS 要么不弹授权框直接拒,要么弹一个空白理由的框,用户不知道该不该点允许
      NSAppleEventsUsageDescription: "OpenWorkBuddy 执行你交代的任务时,需要操作访达等其他应用",
      // 下面两条 Electron 自带英文默认值(This app needs access to …),中文用户看到的是一句没头没尾的英文。
      // 现在界面上没有用到麦克风/摄像头的地方,只有 agent 做出来的网页之类真要用时才会问;说清楚拒了也不碍事。
      // 开发壳(scripts/make-mac-app.sh)直接读这三条写进 Info.plist,改这里一处就够
      NSMicrophoneUsageDescription: "OpenWorkBuddy 只在你用到录音功能时才用麦克风,不同意也不影响其他功能",
      NSCameraUsageDescription: "OpenWorkBuddy 只在你用到拍照或录像功能时才用摄像头,不同意也不影响其他功能",
    },
  },
  dmg: {
    title: "${productName} ${version}",
    // 窗口尺寸 = build/background.png 的尺寸(540×420,见 scripts/gen-dmg-bg.py)。
    // 背景图不用在这儿声明:build/ 下有 background.png 就自动认(dmgUtil.computeBackground),
    // 同名 @2x 存在时会被 tiffutil 合成双分辨率。
    // 图标中心压在 y=178:下面 288 那条分隔线以下留给「第一次打开被拦了怎么办」,
    // 图标下面那行文件名(Finder 自己画的)刚好落在两者之间,不会压字。
    contents: [
      { x: 140, y: 178, type: "file" },
      { x: 400, y: 178, type: "link", path: "/Applications" },
    ],
    iconSize: 120,
  },

  win: {
    publish: [{ provider: "github", owner: "CatCatUncle", repo: "openworkbuddy" }],
    icon: "build/icon.ico",
    // 系统沙箱小助手只有 Windows 用(跟上面那份 files 合在一起算)
    files: ["native/bin/*.exe"],
    target: [
      // 纯 JS 依赖、没有原生模块,arm64(骁龙 X / Surface Pro X 一类)直接多打一份
      { target: "nsis", arch: ["x64", "arm64"] },
      { target: "portable", arch: ["x64", "arm64"] },
    ],
    // 多架构 nsis 会合成一个安装包(${arch} 为空),装的时候自动选架构;
    // 名字必须和 portable 分开,不然 v0.1.0 那次两种 .exe 撞名,文档指的「安装包」其实是免安装版
    artifactName: "${productName}-${version}-win-${arch}.${ext}",
  },
  portable: {
    artifactName: "${productName}-${version}-win-${arch}-portable.${ext}",
    // NSIS 系的目标在多架构下默认还会额外产一个「双架构合体包」:v0.1.6 的 Release 里
    // 那个 248 MB 的 OpenWorkBuddy-0.1.6-win-portable.exe 就是它(= x64 120MB + arm64 127MB 摞一起)。
    // 免安装版本来就要每次启动解压整包,合体版等于让用户白等一倍时间解压另一个架构用不上的文件,
    // 而且没有任何文档指向它,纯粹是下载页上一个更大更诱人的错误选项。关掉,只留两个按架构分开的。
    // (安装版 nsis 那个合体是要的:artifactName 里没有 ${arch},只会产一个,装的时候自动选架构。)
    buildUniversalInstaller: false,
    // 每次启动解压到各自的 %TEMP%\nsXXXX.tmp\app,不再共用一个构建时定死的目录。
    // 共用时第二次双击会先把那个目录整个删掉再解压:第一份正开着的代码当场没了,
    // 解压到一半又双击,两份同时在一个目录里写和删。代价是被强杀时临时目录来不及清,
    // 下次启动由 portable-temp.js 收拾。注意只能写 true:false 在 electron-builder 里等同不写。
    unpackDirName: true,
    // 解压那一两分钟屏幕上原来什么都没有,用户以为没点上又双击。图由 scripts/gen-portable-splash.py 生成
    splashImage: "build/portable-splash.bmp",
  },
  nsis: {
    artifactName: "${productName}-${version}-win-setup.${ext}",
    // 向导式:先问「只给我装 / 给这台电脑所有人装」,再让选装在哪个盘哪个文件夹,装的时候有进度条,
    // 最后一页勾着「运行 OpenWorkBuddy」。以前是一键装:双击后一声不吭装进 %LOCALAPPDATA%\Programs、
    // 装完自己启动,用户既选不了位置,也看不出装没装上——报上来就是「双击了没反应」。
    //   · 只给自己装不要管理员权限;给所有人装(默认 Program Files)会弹一次 UAC。装哪儿都行:
    //     应用目录只读不写,用户数据一律在 ~/OpenWorkBuddy(paths.js DATA_DIR)。
    //   · 选了 D:\ 这种盘根,装的时候自动补一层 OpenWorkBuddy 文件夹,不会把一万多个文件撒在盘根。
    //   · 升级时默认装回上次那个位置(注册表 InstallLocation)。一键装那批老用户装在
    //     %LOCALAPPDATA%\Programs\openworkbuddy,跟新默认的 …\OpenWorkBuddy 只差大小写,Windows 上是同一个文件夹。
    oneClick: false,
    perMachine: false,
    allowToChangeInstallationDirectory: true,
    runAfterFinish: true,
    createDesktopShortcut: true,
    createStartMenuShortcut: true,
    shortcutName: "OpenWorkBuddy",
    // 卸载时不碰 ~/OpenWorkBuddy:那里面是用户的工作区和成果文件,卸个应用不该把人家的
    // PPT 一起删了。要彻底清干净得手动删那个目录,README 里写了
    deleteAppDataOnUninstall: false,
  },

  // 这里原来有一段 linux / AppImage 配置,删了。它从来没在流水线上跑过一次
  // (release.yml 的 matrix 只有 macOS 和 Windows,上传通道也只收 dmg/zip/exe),
  // 开发机是 Mac 也造不出来——等于一份没人验证过的配置,却让读代码的人以为有 Linux 安装包。
  // Linux 上的既定路线是跑源码:install.sh 开头就写着「一键安装(macOS / Linux)」,
  // 服务端还有 Docker。真要恢复得四处一起改:matrix 补 ubuntu 腿、upload 的 path 补 *.AppImage
  // (那步是 if-no-files-found: error,不补必红)、README 下载表补一行、Release 正文补一行。
};