☰
MooTool next-compose DIFF-526 实战解析:HTTP 公网 smoke 白名单、PDF 合并表单保留与 P7 离线门禁
2026/10/15 3:06:57 网站建设 项目流程
  • 桌面应用
  • 开发工具

【免费下载链接】MooTool

Handy tool set for developers

项目地址:https://gitcode.com/gh_mirrors/mo/MooTool
点击查看免费下载

本篇技术指南以 next-compose 开发切片文档 526-http-pdf-merge-smoke-p7-slice.md 为核心,系统讲解 MooTool 下一代 Compose 桌面版(next-compose)在 F09 HTTP、F24 PDF、F17 二维码三个功能模块上的行为约定与验证方案,并说明其与 P7 产品证据脚本、离线测试门禁的关系。读完本文,你将掌握:如何用环境变量开关跑通公网 smoke 测试、理解 PDF 合并/拆分后 AcroForm 表单与签名字段为何能保留而书签却不被复制、掌握二维码尺寸360.4 → 360归一化对齐的细节,以及如何把“可选公网测试”与“离线必跑门禁”正确登记到验收文档中。

一、切片背景:DIFF-525 的收尾与定位

本切片(DIFF-526)是开发切片序列中针对 DIFF-525「未做」事项的收敛切片,集中补齐四类工作:

  • HTTP 公网走查:F09 工具对httpbin.org与localhost/127.0.0.1的公开 smoke 测试;
  • PDF 合并表单保留:F24 工具在合并/拆分后对页级表单与签名字段的保留行为;
  • 产品证据脚本登记:把可选 HTTP smoke 与 P7 离线门禁关系写进证据脚本;
  • P7 说明文档:明确哪些事项仍属人工验收。

切片明确声明不重复DIFF-525 中的非对称 Tab、本机 Cookie 单测,聚焦在“公网走查 + 结构保留 + 脚本登记”这条独立主线上。相关切片均存放在 next-compose/docs/diff/ 目录,与 DIFF-012(PDFBox importPage 基础)等历史切片共同构成 next-compose 的演进记录。

二、F09 HTTP:公网 smoke 的白名单机制与环境变量开关

2.1 白名单实现:仅放行 httpbin.org 与 localhost

切片要求HttpEngine.isPublicSmokeUrlAllowed/resolvePublicSmokeUrl仅允许httpbin.org(含子域)与localhost/127.0.0.1。源码实现位于 HttpEngine.kt:

private val publicSmokeHosts = setOf("httpbin.org", "localhost", "127.0.0.1") /** Only httpbin.org and localhost are allowed for optional public HTTP smoke (see `HttpEngineTest`). */ fun isPublicSmokeUrlAllowed(url: String): Boolean { val uri = runCatching { URI(url.trim()) }.getOrNull() ?: return false if (uri.scheme?.lowercase() !in setOf("http", "https")) return false val host = uri.host?.lowercase() ?: return false if (host in publicSmokeHosts) return true return host.endsWith(".httpbin.org") }

该函数有三层校验:

  1. URL 必须能被URI解析;
  2. scheme 只能是http或https(ftp等一律拒绝);
  3. host 必须命中httpbin.org、localhost、127.0.0.1三个精确值之一,或以.httpbin.org结尾的子域。

默认公网 smoke 地址定义在同文件顶部(HttpEngine.kt):

const val DEFAULT_PUBLIC_SMOKE_URL = "https://httpbin.org/get?source=mootool-next-compose" const val DEFAULT_PUBLIC_MULTIPART_SMOKE_URL = "https://httpbin.org/post?source=mootool-next-compose"

resolvePublicSmokeUrl()读取环境变量MOOTOOL_HTTP_SMOKE_URL,未设置时回落到默认地址;无论走哪条路径,最终都会再经过isPublicSmokeUrlAllowed强校验,不通过直接check抛出异常,从机制上杜绝 CI 或开发机误连第三方站点。

2.2 测试双轨:可选公网 smoke 与离线必跑 allowlist

切片点名的两个测试全部位于 HttpEngineTest.kt:

publicSmokeAllowlistPermitsHttpBinAndLocalhostOnly(离线必跑,L446-L454):不依赖任何网络,纯本地断言白名单行为:

assertTrue(HttpEngine.isPublicSmokeUrlAllowed("https://httpbin.org/get")) assertTrue(HttpEngine.isPublicSmokeUrlAllowed("http://127.0.0.1:8080/echo")) assertTrue(HttpEngine.isPublicSmokeUrlAllowed("http://localhost/health")) assertFalse(HttpEngine.isPublicSmokeUrlAllowed("https://example.com/")) assertFalse(HttpEngine.isPublicSmokeUrlAllowed("ftp://httpbin.org/get")) assertEquals(HttpEngine.DEFAULT_PUBLIC_SMOKE_URL, HttpEngine.resolvePublicSmokeUrl())

optionalHttpBinPublicGetSmoke(需开关、可跳过,L456-L474):通过 JUnitAssume.assumeTrue实现两级跳过:

Assume.assumeTrue( "Set MOOTOOL_HTTP_PUBLIC_SMOKE=1 to run httpbin/localhost smoke", System.getenv("MOOTOOL_HTTP_PUBLIC_SMOKE") == "1", ) val url = HttpEngine.resolvePublicSmokeUrl() val result = HttpEngine.send( HttpEngine.emptyDraft().copy(url = url, method = HttpMethod.GET), requestId = "public-smoke", timeoutMs = 15_000, ) Assume.assumeTrue( "Network unavailable; record as 未测 in acceptance", result.ok || result.errorCode != HttpErrorCode.NETWORK, ) assertTrue(result.ok, "status=${result.status} error=${result.errorCode}") assertTrue(result.body.contains("httpbin.org") || result.body.contains("mootool-next-compose"))

这里体现了切片的完整语义:

  • 默认 CI /desktopTest --offline跳过:未设MOOTOOL_HTTP_PUBLIC_SMOKE=1时第一个Assume直接跳过,全程零网络;
  • 无网时优雅降级:即使开关打开但本机无网络,第二个Assume会把结果标记为跳过,并在验收文档中登记为未测,而不是让测试失败污染门禁;
  • 真正的断言:请求成功且响应体包含httpbin.org或mootool-next-compose溯源参数。

此外切片还覆盖了 multipart 公网走查变体optionalHttpBinMultipartPostSmoke(L476-L507),由MOOTOOL_HTTP_MULTIPART_SMOKE=1独立开关控制,POST 到DEFAULT_PUBLIC_MULTIPART_SMOKE_URL。

2.3 与主请求引擎的关系

公网 smoke 复用的是主引擎HttpEngine.send(...)(HttpEngine.kt),因此白名单治理的是“测试目标”,而发送链路本身仍受既有约束保护:10 MiB 响应上限MAX_RESPONSE_BYTES、clampTimeout(1_000–120_000 ms)超时钳制、cancel(requestId)取消能力,以及HttpErrorCode(ABORTED/TIMEOUT/NETWORK/INVALID_REQUEST/RESPONSE_TOO_LARGE)错误分类。

三、F24 PDF:合并/拆分后的表单与签名字段保留

3.1 技术路线:importPage 子集 + AcroForm 修复处理器

切片对 F24 的行为描述是:合并/拆分时,importPage子集经PDFMergerUtility.appendDocument+AcroFormOrphanWidgetsProcessor/AcroFormDefaultFixup保留页级表单/签名字段 widget(对齐 inspect 计数)。对应实现见 PdfEngine.kt 的merge:

opened.forEachIndexed { index, document -> if (cancelled()) throw PdfException("cancelled", "Cancelled") val pages = parsePageSelection(sources[index].pages, document.numberOfPages) if (pages.isEmpty()) throw PdfException("empty-pages", "No pages selected") PDDocument().use { subset -> pages.forEach { subset.importPage(document.getPage(it - 1)) } repairImportedInteractiveForms(subset) merger.appendDocument(destination, subset) } pageCount += pages.size }

核心修复函数repairImportedInteractiveForms(PdfEngine.kt)是关键所在:

/** Re-link widget annotations after `importPage` so inspect/merge retain AcroForm fields. */ private fun repairImportedInteractiveForms(document: PDDocument) { if (document.documentCatalog.acroForm == null) { document.documentCatalog.acroForm = PDAcroForm(document) } AcroFormOrphanWidgetsProcessor(document).process() AcroFormDefaultFixup(document).apply() }

其原理可以这样理解:

  • PDDocument.importPage只搬运页面及页面级注解,AcroForm 字段与 widget 注解之间的关联关系会丢失,形成“孤儿 widget”;
  • AcroFormOrphanWidgetsProcessor负责把这些孤儿 widget 重新注册回 AcroForm;
  • AcroFormDefaultFixup做兜底修复,确保合并产物中表单结构自洽;
  • 合并前对每个子集先修复一次、合并完成后再对目标文档修复一次,保证最终输出 inspect 计数与源文档对齐。

拆分路径writePages(PdfEngine.kt)采用完全相同的“子集 importPage → 修复 →appendDocument→ 再修复”流程,因此拆分输出的表单/签名同样保留。

3.2 书签不复制:与 Electron pdf-lib 的对齐约定

切片明确:文档级书签仍不复制(与 Electronpdf-libcopyPages一致)。这一约定被固化为常量:

/** Document outlines/bookmarks are not copied on split/merge (same as Electron `pdf-lib` `copyPages`). */ const val ELECTRON_PARITY_OUTLINES_NOT_COPIED = true

也就是说:表单、签名字段是“页级交互对象”,随importPage携带并应保留;而书签(document outline)是“文档级结构”,在页级搬运语义下本就不复制。这是 next-compose 与 Electron 版本刻意对齐的跨技术栈行为契约。

3.3 结构断言:inspect 计数与单测验证

切片点名的两个单测均在 PdfEngineTest.kt:

mergePreservesImportedPageFormAndSignatureWidgets(L111-L141)构造一个含 1 个PDTextField(name)与 1 个PDSignatureField(sig)、并带 1 个书签的结构化 PDF,与一个普通单页 PDF 合并后断言:

assertEquals(sourceInfo.formFieldCount, mergedInfo.formFieldCount, "AcroFormDefaultFixup should register widgets copied with importPage") assertEquals(sourceInfo.signatureFieldCount, mergedInfo.signatureFieldCount) assertEquals(0, mergedInfo.bookmarkCount, "Page-only merge does not copy document outlines (same as Electron pdf-lib copyPages)")

inspectCountsFormsBookmarksAndSignatureFields(L143-L171)强化拆分/合并的结构断言:inspect对源文档计数表单 2、书签 1、签名 1,拆分后表单/签名计数不变,书签归零,并断言ELECTRON_PARITY_OUTLINES_NOT_COPIED为真。

两个测试覆盖了切片要求的两层目标:对齐 inspect 计数(合并后formFieldCount/signatureFieldCount与源一致)与结构不膨胀(书签明确不复制)。PdfEngine.inspect/analyzeStructure的结构统计实现见 PdfEngine.kt。

四、F17 二维码:normalizeSize 与 Electron 对齐

4.1 Double 重载与 roundToInt 语义

切片要求QrEngine.normalizeSize(Double)对齐 ElectronqrTools.test.ts的360.4 → 360。实现见 QrEngine.kt:

fun normalizeSize(size: Int): Int = size.coerceIn(MIN_SIZE, MAX_SIZE) fun normalizeSize(size: Double): Int { if (!size.isFinite()) return DEFAULT_SIZE return normalizeSize(size.roundToInt()) }

细节要点:

  • Int重载直接coerceIn(120, 2000),其中MIN_SIZE = 120、MAX_SIZE = 2000、DEFAULT_SIZE = 300;
  • Double重载先做有限性检查(NaN/Infinity回落到默认 300),再roundToInt()(四舍五入)后走 Int 钳制;
  • 360.4.roundToInt()结果为360,正是与 Electron 对齐的关键点;生成时generatePng同样通过normalizeSize钳制尺寸(QrEngine.kt)。

4.2 单测与跨版本 fixture

单测normalizesSizeAndRejectsEmptyOrBrokenImages(QrEngineTest.kt)同时覆盖边界钳制与异常路径:

assertEquals(120, QrEngine.normalizeSize(20)) // 下限钳制 assertEquals(360, QrEngine.normalizeSize(360)) assertEquals(360, QrEngine.normalizeSize(360.4)) // 对齐 Electron assertEquals(2000, QrEngine.normalizeSize(9999)) // 上限钳制 // empty / invalid-image / not-found 三个异常码

切片提到的 fixture 位于 docs/fixtures/electron-next-qrTools-vitest.md,其中将 ElectronqrTools.test.ts的normalizes QR sizes to the settings boundary(20→120、360.4→360、9999→2000)与QrEngineTest.normalizesSizeAndRejectsEmptyOrBrokenImages一一映射,是 next-compose 与 Electron 行为对账的落点之一。

五、产品证据脚本与 P7 离线门禁

5.1 证据脚本登记

切片要求三个证据脚本登记可选 HTTP smoke 与 P7 离线门禁关系:

  • product-evidence-common.sh 新增mootool_evidence_print_http_public_smoke_hint,输出可直接复制的运行提示:
# Optional F09 httpbin/localhost only (default CI skips): # MOOTOOL_HTTP_PUBLIC_SMOKE=1 MOOTOOL_HTTP_SMOKE_URL="https://httpbin.org/get" # cd <compose_root> && ./gradlew :composeApp:desktopTest \ # --tests com.rememberber.mootool.next.compose.domain.HttpEngineTest.optionalHttpBinPublicGetSmoke --offline

同文件还提供mootool_evidence_print_http_multipart_smoke_hint(L35-L39)与mootool_evidence_print_p7_smoke_hint等姊妹函数。

  • verify-product-evidence-prep.sh 在非交互冒烟中先对prepare-p7-package-smoke.sh做bash -n语法检查,随后 source 公共脚本并打印含mootool_evidence_print_p7_smoke_hint、mootool_evidence_print_http_public_smoke_hint在内的一批提示。该脚本同时验证各类证据预置脚本(Vault 冲突、Git merge/rebase 冲突、IME 样本、托盘截图)产出的真实文件,例如data/vaults/json/sample.json与.git/MERGE_HEAD的存在性。

  • prepare-p7-package-smoke.sh 把可选公网 HTTP smoke 明确标为不属于默认离线门禁:

echo "## Optional F09 public HTTP smoke (not part of default offline gate)" echo "# MOOTOOL_HTTP_PUBLIC_SMOKE=1 runs HttpEngineTest.optionalHttpBinPublicGetSmoke (httpbin/localhost allowlist only)." echo "# Without network, mark 未测 in docs/acceptance.md — see docs/diff/526-http-pdf-merge-smoke-p7-slice.md."

5.2 P7 离线门禁关系

prepare-p7-package-smoke.sh定义的 P7 打包前本地检查序列为:check-toolchain.sh(JDK 21 校验)→:composeApp:printTooling --offline→:composeApp:verifyNativePackageMetadata --offline→:composeApp:desktopTest --offline→(可选MOOTOOL_P7_BUILD_DIST=1时)packageDistributionForCurrentOS --offline。

切片与脚本共同确立的门禁语义是:

  • 离线必跑:./gradlew :composeApp:desktopTest --offline(JDK 21)是 P7 冒烟的事实门禁,HTTP 公网 smoke 默认被Assume跳过;
  • 可选增强:有网且设MOOTOOL_HTTP_PUBLIC_SMOKE=1时补跑 httpbin 走查;
  • 验收边界:无网时在 docs/acceptance.md 登记为未测;P7 的三平台安装/公证/升级/卸载仍为人工验收,脚本不代替。从 acceptance 现有记录看,desktopTest门禁保持“--offline、--no-daemon,公网 smoke 计 skipped”的运行口径(例如 DIFF-667/668/669 批次均为 N/N+2 通过、2 skipped),与本文切片描述一致。

六、验证命令汇总

切片给出的完整验证清单,按离线必跑与可选公网分组如下:

离线必跑(JDK 21):

./gradlew :composeApp:desktopTest --offline

或按类精确跑关键测试:

./gradlew :composeApp:desktopTest \ --tests 'com.rememberber.mootool.next.compose.domain.PdfEngineTest.mergePreservesImportedPageFormAndSignatureWidgets' \ --tests 'com.rememberber.mootool.next.compose.domain.HttpEngineTest.publicSmokeAllowlistPermitsHttpBinAndLocalhostOnly' \ --tests 'com.rememberber.mootool.next.compose.domain.QrEngineTest.normalizesSizeAndRejectsEmptyOrBrokenImages' \ --offline

可选公网(有网时):

MOOTOOL_HTTP_PUBLIC_SMOKE=1 ./gradlew :composeApp:desktopTest \ --tests 'com.rememberber.mootool.next.compose.domain.HttpEngineTest.optionalHttpBinPublicGetSmoke' --offline MOOTOOL_HTTP_MULTIPART_SMOKE=1 ./gradlew :composeApp:desktopTest \ --tests 'com.rememberber.mootool.next.compose.domain.HttpEngineTest.optionalHttpBinMultipartPostSmoke' --offline

无网时按切片约定在docs/acceptance.md登记未测,而不是让 CI 变红。

七、明确未做事项

切片在“未做”节明确列出的后续事项,写作本文时仍属人工/后续切片范畴,不误标为已支持:

  • 六套 CSS 皮肤、产品主窗全 Tab 走查;
  • multipart 文件上传公网走查;
  • PDF 书签/跨文档 AcroForm 完整策略安装镜像验收;
  • P7 三平台安装/公证/升级卸载;
  • 托盘权限对话框与非对称 Tab 逐控件手工走查。

这些边界在 prepare-p7-package-smoke.sh 中同样以“仍属 offline gate / 三平台安装·公证未验收”的注释形式反复声明,阅读时应注意区分“代码已就绪”与“平台验收已通过”两类状态。

  • 桌面应用
  • 开发工具

【免费下载链接】MooTool

Handy tool set for developers

项目地址:https://gitcode.com/gh_mirrors/mo/MooTool
点击查看免费下载
上一篇:终极指南:如何使用 import_3dm 在 Blender 中完美导入 Rhino 3DM 文件
下一篇:VvvebJs 内置 Iconoir 图标库完整指南:1000+ SVG 图标的选择、引入与自定义

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询