拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AppGallery Connect × Sharp on HarmonyOS 7:多终端商店截图矩阵与本地化素材门禁【鸿蒙心迹】

AppGallery Connect × Sharp on HarmonyOS 7:多终端商店截图矩阵与本地化素材门禁【鸿蒙心迹】

应用包能构建成功,不代表发布素材已经准备好。商店截图常见的问题并不复杂:分辨率错一档、某个语言少一张、横竖图混放、文件超过限制。麻烦在于这些问题分散在不同目录里,人工检查很容易漏。

这篇文章用StoreAssetGate演示怎样把 AppGallery Connect 的素材规格转成一段可执行的门禁。文章中的审核页和日志是演示配图,不冒充真实 AGC 后台,也不虚构已经通过正式审核。规则以官方“Asset Specifications”页面 2026 年 1 月 20 日更新内容为依据,提交前仍应复核当前页面。

一、发布前最后半小时,最容易把素材目录当成普通文件夹

代码进入发布分支后,团队通常会把注意力放在 HAP、签名和版本号。截图由设计或运营补齐,开发者只确认“目录里有图”。等到上传时才发现,中文竖图是 1080×1920,英文第三张却从设计源稿直接导出了 1170×2532;另一张中文横图尺寸正确,但 PNG 达到 5.7 MB。

这类问题看上去只是素材返工,实际上会打断整个发布节奏。更隐蔽的是本地化错位:zh-CN有三张竖图,en-US只有两张;文件名都叫01.png,画面里却保留了另一种语言。尺寸检查通过,也不能证明语言矩阵完整。

官方素材规格页面对 HarmonyOS 手机和平板截图给出了明确约束:横图为 16:9、1920×1080,数量 3~5;竖图为 9:16、1080×1920,数量 3~5;PNG、JPG 或 JPEG 单张最大 5 MB。这里把这几项转成机器可读规则,检查范围限定为手机/平板商店截图,不擅自扩展到其他终端的素材要求。

演示批次编号是ASSET-0055,包含zh-CN与en-US两种语言,每种语言各三张竖图和三张横图,总计 12 张。第一次扫描发现 2 个错误:en-US/phone/portrait/03.png为 1170×2532,zh-CN/phone/landscape/02.png为 5.7 MB。替换后第二次扫描为12 / 12 PASS。

二、规则文件先表达“我们准备发布什么”

脚本不应该从目录结构猜业务意图。一个项目只准备竖图,和一个项目忘记横图,在文件系统里可能完全相同。门禁需要一份清单,明确本批次的语言、终端、方向、数量、尺寸、格式和体积上限。

这段代码解决什么问题:把官方页面中的手机/平板截图要求落成项目级规则,避免扫描器自行猜测。

exporttypeOrientation='portrait'|'landscape'exportinterfaceScreenshotRule{device:'phone'orientation:Orientation width:numberheight:numberminCount:numbermaxCount:numbermaxBytes:numberformats:Array<'png'|'jpeg'>}exportconstreleasePlan={batchId:'ASSET-0055',locales:['zh-CN','en-US'],rules:[{device:'phone',orientation:'portrait',width:1080,height:1920,minCount:3,maxCount:5,maxBytes:5*1024*1024,formats:['png','jpeg']},{device:'phone',orientation:'landscape',width:1920,height:1080,minCount:3,maxCount:5,maxBytes:5*1024*1024,formats:['png','jpeg']}]asScreenshotRule[]}

为什么把规则写在代码里,而不是散落在命令参数中?因为它需要跟发布分支一起评审。以后官方规格变化,修改记录能说明从哪一版开始调整,也能让历史版本继续使用当时的规则。

这里把.jpg和.jpeg都归一为jpeg,并没有把 WebP 纳入演示清单。官方页面还列出 WebP 的单独体积要求,如果项目需要使用,应增加独立格式规则,不能沿用 PNG/JPEG 的 5 MB 上限。

实际项目容易犯的错误,是把 5 MB 写成 5,000,000 字节。平台页面通常以 MB 表达,脚本采用5 * 1024 * 1024时,应在团队内确认口径,并保留接近边界的安全余量。演示把 5 MB 视为硬门槛,但建议设计导出目标控制在 4.5 MB 以下,减少重新编码差异带来的边缘问题。

三、Sharp 只负责读证据,门禁逻辑留在业务层

Sharp 的metadata()可以返回格式、宽高、页数等信息;文件体积则从文件系统读取。它不需要修改原图。发布门禁的第一原则是“检查和修复分开”:脚本发现 1170×2532 后应阻止提交,而不是静默拉伸成 1080×1920。

静默修图看起来省事,实际会把新的风险带进来。截图中的文字可能被缩放发虚,安全区可能被裁掉,横竖构图也可能改变。工具可以生成修复建议,但最终素材应回到设计源文件重新导出。

这段代码解决什么问题:读取每张图片的真实格式、尺寸和体积,输出稳定的错误码。

importsharp,{Metadata}from'sharp'import{stat}from'node:fs/promises'exportinterfaceAssetEvidence{file:stringformat:stringwidth:numberheight:numberbytes:numbererrors:string[]}exportasyncfunctioninspectAsset(file:string,rule:ScreenshotRule):Promise<AssetEvidence>{constmetadata:Metadata=awaitsharp(file,{animated:false,limitInputPixels:40_000_000}).metadata()constinfo=awaitstat(file)constformat=metadata.format==='jpg'?'jpeg':(metadata.format??'')consterrors:string[]=[]if(!rule.formats.includes(formatas'png'|'jpeg')){errors.push(`FORMAT:${format||'UNKNOWN'}`)}if(metadata.width!==rule.width||metadata.height!==rule.height){errors.push(`SIZE:${metadata.width}x${metadata.height}`)}if(info.size>rule.maxBytes){errors.push(`BYTES:${info.size}`)}if((metadata.pages??1)>1){errors.push(`ANIMATED:${metadata.pages}`)}return{file,format,width:metadata.width??0,height:metadata.height??0,bytes:info.size,errors}}

limitInputPixels是工具自身的防御边界,防止异常大图消耗过多内存,不是 AppGallery Connect 的素材规格。animated: false只读取静态页面,但我们仍检查pages,避免动画资源误入截图目录。

状态变化很简单:文件从PENDING进入READING,读取成功后根据errors.length进入PASS或FAIL。读取异常要单独记为UNREADABLE,不能等价为尺寸错误。批量任务结束后再汇总,否则一个损坏文件抛异常会让后续 11 张图都没有报告。

易错点是方向判断。脚本不根据width > height猜landscape,而是由目录和规则共同决定。如果一张 1920×1080 的图被放进portrait,它应该报告与目标规则不匹配,而不是被自动移动。

DevEco 风格演示图中,左侧是scripts/store-asset-gate目录,中间显示inspectAsset(),右侧模拟器展示AssetAuditPage,底部日志对应两条失败记录。图用于解释工程结构,不是实际 IDE 截屏。

四、真正容易漏的是“矩阵缺口”,不是单张图片

单张图片全部通过后,还要检查目录是否完整。zh-CN/phone/portrait有 3 张,en-US/phone/portrait也必须达到计划数量;不能因为总目录凑够了 12 张,就把某个语言的缺口掩盖掉。

目录约定为store-assets/{locale}/phone/{orientation}/。例如中文竖图放在zh-CN/phone/portrait/01.png ... 03.png,英文横图放在en-US/phone/landscape/01.png ... 03.png,其余两个组合遵循相同规则。

文件名使用两位数字,是为了让运营、脚本和上传顺序看到同一套排序。门禁还应检查重复序号、非连续序号和隐藏临时文件。03-final-v2.png对人类很熟悉,对自动上传流程却容易造成顺序不稳定。

这段代码解决什么问题:逐个检查语言 × 方向组合的数量和编号,防止总数正确但局部缺失。

import{readdir}from'node:fs/promises'import{join}from'node:path'interfaceMatrixResult{key:stringfiles:string[]errors:string[]}exportasyncfunctioninspectMatrix(root:string,locale:string,rule:ScreenshotRule):Promise<MatrixResult>{constdir=join(root,locale,rule.device,rule.orientation)constnames=(awaitreaddir(dir)).filter((name:string)=>/^(0[1-9]|[1-9][0-9])\.(png|jpe?g)$/i.test(name)).sort()consterrors:string[]=[]if(names.length<rule.minCount||names.length>rule.maxCount){errors.push(`COUNT:${names.length},EXPECTED:${rule.minCount}-${rule.maxCount}`)}names.forEach((name:string,index:number)=>{constexpected=`${String(index+1).padStart(2,'0')}.`if(!name.startsWith(expected)){errors.push(`ORDER:${name},EXPECTED_PREFIX:${expected}`)}})return{key:`${locale}/${rule.device}/${rule.orientation}`,files:names.map((name:string)=>join(dir,name)),errors}}

这段实现故意没有吞掉readdir异常。目录不存在不是“数量为 0”的普通情况,而是发布计划没有落地,报告中应标成MISSING_DIRECTORY。完整实现可在调用层捕获并归类,但不能静默创建空目录后继续通过。

实际项目还应核对语言内容。纯脚本难以可靠判断画面中文字属于哪种语言,可以在素材旁放置manifest.json,记录页面名、语言、来源设计稿版本和导出时间,再对文件摘要做绑定。OCR 只能作为辅助提示,不能替代设计与运营复核。

五、把失败报告做成“能马上返工”的页面

门禁页不需要展示几十个图表。AssetAuditPage顶部只放批次、规则日期和总状态,中间按语言与方向列出 4 个矩阵,底部给出具体文件与修复建议。

第一次扫描结果为:

  • zh-CN / portrait:3/3,通过;
  • zh-CN / landscape:3/3,其中02.png为 5.7 MB,失败;
  • en-US / portrait:3/3,其中03.png为 1170×2532,失败;
  • en-US / landscape:3/3,通过。

手机运行图时间为 03:15,批次ASSET-0055,总计10 / 12 PASS,状态BLOCKED。红色标注分别指向错误尺寸与超限体积。它让读者看到门禁结果怎样映射到文件,而不是只展示一个漂亮的“审核失败”页面。

设计重新导出素材后,第二次扫描显示12 / 12 PASS,但脚本仍不会声称“审核通过”。它只能证明当前目录满足已编码的素材规则。应用内容合规、隐私声明、截图真实性和其他审核项仍由正式发布流程判断。

六、退出码要服务发布流水线,也要保留人工确认

门禁最终需要给构建流程一个明确结果。演示约定:全部通过返回 0;规则或素材错误返回 2;脚本自身异常返回 3。这样 CI 可以区分“素材不合格”和“检查器坏了”。

这段代码解决什么问题:把扫描结果收口为报告文件与稳定退出码,并确保异常不会误报通过。

import{writeFile}from'node:fs/promises'asyncfunctionmain():Promise<void>{constreport=awaitrunAudit('store-assets',releasePlan)awaitwriteFile('build/reports/store-assets-ASSET-0055.json',JSON.stringify(report,null,2),'utf8')if(report.internalErrors.length>0){process.exitCode=3return}if(report.failedAssets>0||report.failedMatrices>0){process.exitCode=2return}process.exitCode=0}main().catch((error:Error)=>{console.error(`[StoreAssetGate] INTERNAL${error.message}`)process.exitCode=3})

为什么不直接在第一处错误process.exit(2)?因为发布前最怕一轮只修一个问题。完整扫描一次给出两条证据,设计可以同时返工,减少来回次数。状态上,批次从SCANNING进入BLOCKED或READY_FOR_MANUAL_REVIEW;即使退出码为 0,仍然保留人工确认步骤。

脚本需要在package.json或流水线中固定 Sharp 和 Node.js 的运行环境,避免不同机器对图片元数据处理不一致。若要接入 Hvigor,可在打包前任务中调用脚本,但应避免把商店素材强行放进 HAP 资源目录。它们属于发布资产,不应增加应用包体。

七、诊断页比“全部通过”更值得保留

第二次扫描后,诊断页记录两项变化:1170×2532 → 1080×1920,5.7 MB → 4.3 MB。矩阵仍是 12 张,错误从 2 变为 0,状态从BLOCKED变为READY_FOR_MANUAL_REVIEW。

这张图与运行页明显不同:它展示修复前后、规则快照、报告路径和退出码,而不是重复列缩略图。红圈标注用于说明为什么状态改变。报告保留rulesUpdatedAt=2026-01-20,提醒发布者下一次提审前重新核对官方页面。

如果官方规格发生变化,旧报告不能自动代表新版本仍然有效。规则文件应带版本或更新时间,并让 CI 在规则过期时给出提示,而不是自行抓取网页后静默改动门槛。自动更新规则虽然省事,却会让同一提交在不同时间得到不同结果。

八、把工具停在正确的边界上

StoreAssetGate的价值,是把可机械判断的内容提前:数量、尺寸、方向、格式、体积、命名和语言目录。它不会判断截图是否真实反映应用,也不会判断文案是否合规,更不会替代 AppGallery Connect 的正式审核。

这条边界很重要。工具脚本最容易从“减少低级错误”滑向“替团队做发布决定”。当报告为绿色时,最合适的状态名不是APPROVED,而是READY_FOR_MANUAL_REVIEW。它说明机器检查已完成,接下来仍要核对画面、语言、功能和隐私信息。

如果继续扩展,我会优先增加三项:对manifest.json与图片摘要做绑定;为不同终端建立独立规则组;在拉取请求中输出差异报告。不会优先加入自动裁切,因为自动改变商店画面带来的风险,通常高于节省的那几分钟。

参考资料:

  • 华为开发者:AppGallery Connect 素材规格(页面标注 2026-01-20 更新):https://developer.huawei.com/consumer/es/doc/app/agc-help-app-visual-asset-spec-0000002277607976
  • 华为开发者:华为应用市场上架流程与基础信息设置:https://developer.huawei.com/consumer/cn/appgallery
  • Sharp 官方文档:输入元数据metadata():https://sharp.pixelplumbing.com/api-input/
返回列表