release: 发布 v0.6.4 环境自适应与 macOS 打包

新增能力:桌面端新增 macOS 打包支持,electron-builder 增加独立 mac 配置块,
产出 dmg,按 arm64、x64 分别单独构建、不产出 universal 包;Pandoc 运行时清单
新增 darwin/arm64、darwin/x64 两个官方发行项(含独立许可证文件来源),桌面
Pandoc 候选路径与 prepare-pandoc-runtime.mjs 均已泛化为按 --platform/--arch
参数选取目标,不再只认 Windows x64。AGENTS.md 不再硬编码 Windows 绝对路径与
强制 PowerShell 语法,改为按当前 Shell 与仓库实际位置自适应。

问题修复:修复解压内置 Pandoc 时未保留 Unix 可执行权限位的问题(unzipSync
不保留压缩包内权限,已补 chmod 0o755);修复根 package.json 中
build:web-runtime/dev/desktop:dev 三个脚本的构建顺序错误
(@md-to-pdf/application 被排在其依赖的 @md-to-pdf/preview-engine 之前,在
没有历史 dist 残留的全新环境中会导致类型解析失败);修正
packages/docx-engine/tests/pandoc-runtime.test.ts 与
apps/desktop/tests/markdown-file.test.ts 中依赖宿主 OS 或路径分隔符的测试
断言,避免这些用例只能在特定平台上通过。

验证结果:docx-engine 包 93 项测试、desktop 包 62 项测试(61 通过 + 1 项
Windows 专属用例按预期跳过)均通过;全项目类型检查、生产构建通过,
git diff --check 无空白错误。跳过依赖 Git 忽略的真实客户文档语料(tmp/)的
test:docx-real-world-corpus 与 test:docx-release-gate-suites 后,其余全部
工作区测试均通过;这两步需要接入真实语料的机器上单独执行。本机 macOS
(Apple Silicon)实测 npm run package:mac:arm64 全链路成功,内置 Pandoc
3.9.0.2 完成下载、哈希校验与真实 DOCX 冒烟转换;实际截图确认窗口、macOS
原生菜单栏、编辑器工具栏、中文字体与实时预览渲染正常。

兼容与部署:版本统一为 0.6.4。本版本仅完成开发基础设施与验证,未构建正式
发行文件:没有产出真实签名的 dmg、没有重新构建 Windows 安装包,也没有重新
构建 Docker 镜像;正式 macOS 发行产物与 Windows/Docker 同步验证留待 DOCX
发布门禁阶段完成后一并发布。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K5N6pcbjV4jVfXrcH3NcLP
This commit is contained in:
SkyJourney
2026-08-26 19:32:38 +08:00
co-authored by Claude Sonnet 5
parent effeca7ca8
commit c4df499680
22 changed files with 412 additions and 148 deletions
+88 -20
View File
@@ -1,6 +1,7 @@
import { createHash } from "node:crypto";
import {
access,
chmod,
mkdir,
readFile,
rename,
@@ -43,14 +44,38 @@ async function fileExists(filePath) {
}
}
async function readRuntimeManifest() {
function platformDirectoryName(platform, architecture) {
if (platform === "win32" && architecture === "x64") {
return "windows-x86_64";
}
if (platform === "darwin" && (architecture === "arm64" || architecture === "x64")) {
return `darwin-${architecture}`;
}
throw new Error(`不支持的 Pandoc 桌面内置目标:${platform}/${architecture}`);
}
/** 归档下载得到的、需要从压缩包内部提取的文件名(不含通过 licenseSource 单独下载的许可证文件)。 */
function archiveRequiredNames(artifact) {
const licenseSource = artifact.licenseSource ?? {};
return [
normalizedBaseName(artifact.executableRelativePath),
...artifact.licenseFiles
.filter((fileName) => !licenseSource[fileName])
.map(normalizedBaseName)
];
}
async function readRuntimeManifest({ platform, architecture }) {
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
const artifact = manifest.artifacts?.find(
(candidate) =>
candidate.platform === "win32" && candidate.architecture === "x64"
candidate.platform === platform && candidate.architecture === architecture
);
if (!artifact || artifact.archiveType !== "zip") {
throw new Error("Pandoc 清单缺少 Windows x64 ZIP 发行项");
if (!artifact) {
throw new Error(`Pandoc 清单缺少 ${platform}/${architecture} 发行项`);
}
if (artifact.archiveType !== "zip") {
throw new Error(`Pandoc 清单要求 ${platform}/${architecture} 为 ZIP 发行项`);
}
const requiredNames = [
normalizedBaseName(artifact.executableRelativePath),
@@ -92,6 +117,33 @@ async function downloadArchive(downloadUrl, destination) {
return content;
}
async function downloadLicenseSourceFile(downloadUrl) {
const response = await fetch(downloadUrl, {
redirect: "follow",
signal: AbortSignal.timeout(downloadTimeoutMs)
});
if (!response.ok) {
throw new Error(`Pandoc 许可证文件下载失败:HTTP ${response.status}`);
}
const content = new Uint8Array(await response.arrayBuffer());
if (content.byteLength > maximumExtractedBytes) {
throw new Error("Pandoc 许可证文件超过允许的下载大小");
}
return content;
}
/**
* 部分平台(如 macOS)的官方归档不随附许可证文件,需要按清单声明的
* 独立来源单独下载;返回的文件与归档内提取的文件合并后一并校验哈希。
*/
async function loadLicenseSourceFiles(artifact) {
const files = new Map();
for (const [fileName, source] of Object.entries(artifact.licenseSource ?? {})) {
files.set(fileName, await downloadLicenseSourceFile(source.downloadUrl));
}
return files;
}
async function loadVerifiedArchive(artifact, archivePath) {
if (await fileExists(archivePath)) {
const cached = new Uint8Array(await readFile(archivePath));
@@ -122,10 +174,7 @@ async function loadVerifiedArchive(artifact, archivePath) {
}
function extractRequiredFiles(archive, artifact) {
const requiredNames = new Set([
normalizedBaseName(artifact.executableRelativePath),
...artifact.licenseFiles.map(normalizedBaseName)
]);
const requiredNames = new Set(archiveRequiredNames(artifact));
let selectedBytes = 0;
const entries = unzipSync(archive, {
filter(file) {
@@ -253,9 +302,13 @@ async function verifyPreparedRuntime(targetDirectory, manifest, artifact) {
}
}
async function preparePandocRuntime({ checkOnly = false } = {}) {
const { manifest, artifact } = await readRuntimeManifest();
const platformDirectory = "windows-x86_64";
async function preparePandocRuntime({
checkOnly = false,
platform = process.platform,
architecture = process.arch
} = {}) {
const { manifest, artifact } = await readRuntimeManifest({ platform, architecture });
const platformDirectory = platformDirectoryName(platform, architecture);
const runtimeRoot = path.join(desktopRoot, ".runtime", "pandoc");
const targetDirectory = path.join(
runtimeRoot,
@@ -278,7 +331,10 @@ async function preparePandocRuntime({ checkOnly = false } = {}) {
}
const archivePath = path.join(downloadDirectory, archiveName);
const archive = await loadVerifiedArchive(artifact, archivePath);
const files = extractRequiredFiles(archive, artifact);
const files = new Map([
...extractRequiredFiles(archive, artifact),
...await loadLicenseSourceFiles(artifact)
]);
const temporaryDirectory = path.join(
runtimeRoot,
manifest.version,
@@ -308,6 +364,10 @@ async function preparePandocRuntime({ checkOnly = false } = {}) {
temporaryDirectory,
normalizedBaseName(artifact.executableRelativePath)
);
if (platform !== "win32") {
// unzipSync 不保留压缩包内的可执行权限位,POSIX 平台需要手动补上。
await chmod(executablePath, 0o755);
}
verifyPandocVersion(executablePath, manifest.version);
await smokeTestPandoc(executablePath);
await writeFile(
@@ -315,8 +375,8 @@ async function preparePandocRuntime({ checkOnly = false } = {}) {
`${JSON.stringify(
{
version: manifest.version,
platform: "win32",
architecture: "x64",
platform,
architecture,
license: manifest.license,
projectUrl: manifest.projectUrl,
sourceArchiveUrl: manifest.sourceArchiveUrl,
@@ -338,17 +398,25 @@ async function preparePandocRuntime({ checkOnly = false } = {}) {
return targetDirectory;
}
function readCliFlag(name) {
const prefix = `--${name}=`;
const match = process.argv.find((argument) => argument.startsWith(prefix));
return match ? match.slice(prefix.length) : undefined;
}
const isDirectInvocation =
process.argv[1] &&
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
if (isDirectInvocation) {
preparePandocRuntime({ checkOnly: process.argv.includes("--check") }).catch(
(error) => {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
}
);
preparePandocRuntime({
checkOnly: process.argv.includes("--check"),
platform: readCliFlag("platform") ?? process.platform,
architecture: readCliFlag("arch") ?? process.arch
}).catch((error) => {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
});
}
export {