软件包准备就绪,并不意味着发布信息也处于同一版本。对于同时维护中文和英文更新说明的应用,最容易漏掉的不是整段译文,而是那些隐藏在句子里的变量。产品经理更新了新功能名称,开发人员改了客户端版本号,运营把上一轮测试邀请里的提示复制到了正式发布稿。每个人完成的工作可能都是对的,合在一起却可能出现“两个语言版本指向不同内容”的发布事故。

这一次不从审核流程表格切入,而是先定义一份只负责文本一致性、不碰发布权限的离线合同:应用发版前,检查两种语言的占位符集合是否完整,检查生产渠道是否混入测试专用文案,并拒绝旧修订稿替换当前候选。实现使用Node.js对本地JSON夹具运行纯函数,输出可解释的差异列表。不会调用AGC账号、上传软件包,也不声称平台具有本文自定义的门禁规则。

一、发布稿最怕“看起来像同一版”

在AppGallery Connect的实际业务中,提交软件包和维护应用的本地化信息属于不同的操作内容。华为应用市场对开发者说明的发布流程包括填写应用资料、上传应用包和提交审核,AGC提供面向应用生命周期的服务。这里可以据此建立内部发布前检查流程,但不能由此推导出平台自动识别某个产品自定义模板变量,也不能把没有提交审核的本地JSON称为平台审核通过。

本案例聚焦一种具体错误:中文更新说明写“已为您启用{feature},当前版本{version}”,英文更新说明因复制替换操作丢了{feature}。肉眼检查很容易只看到英语句子还算通顺,却忽略动态字段不见了;如果客户端最终采用模板渲染,缺少变量可能导致版本说明不完整。相比之下,界面截图缺口、软件包签名、权限声明都属于其他检查层,本文不会混为一谈。

另一个特殊错误来自渠道隔离。测试渠道可能允许写“请到beta专用页面反馈”,但生产发布说明不应出现内部测试标识。无论平台是否对这些词强制审核,产品团队都有理由在自己的准入规则里限制它们。最后是修订号问题:同样的版本号2.6.0,历史回滚模板可能仍停在修订25,而当前候选要求修订26。版本名相同并不代表稿件是同一次审定结果。

二、冻结版本、语言与八条候选记录

工程命名为ReleaseNoteParity,任务号LOC-1011-35,测试文件release_copy_08。候选产品版本为2.6.0、自定义数值版本码260,当前文本修订号26。本轮只模拟zh-CN和en-US两种语言,同时保留production、beta、rollback三种业务渠道标识。它们都是项目自己的内容发布分类,不代表AGC后台存在同名接口或者相同字段结构。

八条候选记录中,K01中文正式、K02英文正式、K03中文测试、K04英文测试、K05中文回滚都带完整占位符、正确渠道和修订号,因此返回PASS。K06英文正式稿缺少{feature},返回TOKEN_MISSING;K07中文正式稿含内部beta-only提示,返回CHANNEL_LEAK;K08英文回滚稿修订号为25,返回REVISION_STALE。最终五条接受、三条阻断,综合状态COPY_HOLD。

这些结果不能简单理解成“有五个语种可发布”,因为候选是八份待检查稿件,并非八种语言。相同语言与渠道可以包含多个编辑候选,检查器必须为每份稿件保留独立的候选编号,不能用locale单独作为唯一键覆盖较早的记录。正式流程还需要由业务选出最终生效稿,并保证生产渠道实际只使用被批准的一份;本文输出的是校验结论,不替代审批与发布操作。

三、把文本字段的责任边界写进契约

固定输入为本项目自定义JSON:每条候选包含id、locale、channel、revision和text。这里没有任何AGC专有字段,也没有承诺JSON可以直接导入应用市场控制台。占位符的约定是成对花括号格式{version}和{feature};两者必须都出现,重复使用可以被记录但不一定被拒绝。未知占位符另行告警,不能把所有花括号都默认为不安全代码执行。

为避免规则依赖翻译顺序,检查器从文本中提取占位符集合,再与预期集合比较。集合比较不会关心中文句子里变量出现的前后顺序,也不会因英文多一个空格改变结果。发布稿的实际可读性、表达是否通顺仍需要人工校对,纯字符串算法无法替代审校人员。把自动检查职责限定在机械性错误,能更清楚地衡量它是否真的减少了发版风险。

另外一个容易被遗漏的环节是字符归一化。文案可能从办公软件复制出来,混有全角空格、零宽字符和不同的换行习惯。工程上可以先统一换行符、常见空格以及Unicode规范形式,但不要过度“清洗”用户可见内容。特别是引号、标点和中文数字属于文案表达的一部分,不应为了通过规则直接改写原稿。检查器最好分别保存原始文本摘要和归一化后文本摘要,方便事后复核哪一步改变了比对结果。

四、先实现占位符提取,而不是急着接AGC

第一个代码块解决的是“如何保证占位符校验确定且可解释”。函数只接受业务数据,不读取文件,也不使用网络;requiredTokens在整个批次中固定,避免某条候选自己声明需要哪些变量后又自己通过。对缺失项返回有序数组,便于命令行和诊断页展示。此处的正则是本文的模板语法定义,不应当成AGC占位符规则。

const REQUIRED_TOKENS = ['{version}', '{feature}'];
function collectTokens(text) {
  return [...new Set(text.match(/\{[A-Za-z][A-Za-z0-9_]*\}/g) ?? [])].sort();
}
function missingTokens(text, required = REQUIRED_TOKENS) {
  const present = new Set(collectTokens(text));
  return required.filter(token => !present.has(token));
}
function canonicalText(text) {
  return text.normalize('NFC').replace(/\r\n?/g, '\n').trim();
}

这里刻意没有把变量替换成真实功能名再比较。不同语言的译文可能在词序、词形和语序上差别明显,用整段字符串对等来判断一致性会制造大量误报。只校验相同的变量“槽位”,是将机器可核对的合同从自然语言里抽出来。后续如果项目确实需要数值占位符、复数形式或条件段落,应升级为显式的模板语法和解析器,而不是让正则悄悄吞掉未知结构。

错误处理也不能只写“invalid”。K06真正缺少的是{feature},记录这个具体缺失项,修订人员就不需要逐句反复比对两个语言版本。反之,假如出现未声明的新变量{branch},系统可以给出独立的UNKNOWN_TOKEN提示,但是否阻断要由发布策略决定。本文的固定八条输入只覆盖三类硬阻断,不把以后可能新增的告警类别算进现有测试结果。

五、渠道与修订号比对必须先于最终接收

接下来解决“测试提示不应该出现在正式更新说明”。内部禁用短语在正式渠道上拒绝,在beta渠道内只记录,而不是对所有环境一律删除。这个策略属于业务组织的内部规范,不代表应用市场自动识别beta-only或使用同样的字段。将生产与测试的词汇风险区分开,可以让产品在内测期间保留必要的说明,又不会把它们原样带到正式发布稿。

function auditCandidate(row, expectedRevision = 26) {
  if (row.revision !== expectedRevision) return 'REVISION_STALE';
  if (row.channel === 'production' && /beta-only/i.test(row.text)) {
    return 'CHANNEL_LEAK';
  }
  if (missingTokens(row.text).length > 0) return 'TOKEN_MISSING';
  return 'PASS';
}

判定顺序需要提前写明。本篇先做修订号,再做渠道泄露,最后检测占位符;每份候选只生成一个主阻断原因,其他问题可以作为次级提示保存。真实系统如果希望一次暴露全部问题,也可以改成返回错误数组,但汇总口径必须随之改变。若把每个错误标签都计为一次拒绝,就可能出现“八份稿件、十个失败”的表面矛盾。本文坚持以候选为计数单位,因此总数始终是五加三等于八。

正则对beta-only的判断刻意简单。它可以发现固定样例中显性的内测关键词,却无法捕获“仅研发同学点击下方按钮”等语义相近表述,更无法判断一段翻译是否涉及隐私、广告承诺或法律资质。业务有需要时应扩展规则表并走人工抽检;不能用一个小正则取代运营审核,更不能把“通过脚本”当成“符合应用市场全部规范”。

六、文件读取、结构校验与退出码是另一个故障层

规则函数没有输入输出依赖,调用层才负责解析文件并处理异常。这样一旦夹具路径不存在、JSON格式错误或字段缺失,检查器可以退出为输入错误,而不是把文件系统异常误标为TOKEN_MISSING。对流水线来说,未能完成检查与检查后发现三条违规是两种不同结果:前者是检查基础设施不可靠,后者是候选内容确有问题。

下方第三段代码使用Node.js内置fs/promises读取固定输入,并为每条记录保留原始编号。它没有使用AGC API,也不会把原文案上传网络。若生产CI要持久化诊断JSON,应另外加上文件写入的原子替换和敏感信息脱敏,避免审稿内容被意外曝光在公开日志里。

import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
async function auditFile(fileName) {
  const text = await readFile(resolve(fileName), 'utf8');
  const input = JSON.parse(text);
  if (!Array.isArray(input.candidates)) throw new Error('invalid candidates');
  const result = input.candidates.map(row => ({
    id: row.id, locale: row.locale, channel: row.channel,
    revision: row.revision, status: auditCandidate(row),
    missing: missingTokens(row.text)
  }));
  const blocked = result.filter(item => item.status !== 'PASS').length;
  return { task: 'LOC-1011-35', checked: result.length,
    accepted: result.length - blocked, blocked, result };
}

这段代码特意用resolve限定的是当前工作目录下的路径解释方式,并不代表已实现安全路径沙箱。若候选文件名来自不可信用户输入,还需要限制文件根目录并核对真实路径,防止读到构建机上的其他内容。当前任务只有预置文件,不从远程接收路径,因此把危险边界留在接口说明里,而不制造与本轮业务无关的虚假测试结论。Node.js运行环境应固定版本,CI脚本的退出码约定和产物保存路径也应写入流水线配置。

七、八条候选的预期结果如何落到页面

在ReleaseNoteParity中,K01~K05返回PASS,另外三条一一对应固定阻断原因。K06的en-US/production/rev26缺少{feature},虽然它仍包含{version},也不应被当成完整更新说明。K07的zh-CN/production/rev26带beta-only,触发CHANNEL_LEAK。K08的en-US/rollback/rev25与目标修订26不符,触发REVISION_STALE。

报告汇总显示8 checked / 5 accepted / 3 blocked,分项TOKEN_MISSING=1、CHANNEL_LEAK=1、REVISION_STALE=1,状态COPY_HOLD。这里用HOLD而不用FAILED_UPLOAD,是因为没有进行任何平台上传。用户应从文案候选列表进入“差异诊断”,看到阻断条目、实际修订、缺失变量与建议修复动作;修复后重新生成新修订,而不是就地把被拒稿件的状态改成通过。

本轮配图里约定展示时间09:41和电量91%,这只是统一的视觉数据契约,不用于计算修订有效期。DevEco白色主题示意图右侧的手机页面用于演示审核台布局,底部HiLog为固定夹具产出的样例日志;并未运行真实模拟器、应用程序或AGC发布接口。把这些限制写在图注和正文旁边,能避免读者以为看到绿色“通过”就意味着应用商店已经认可了某个候选包。

八、失败分支给开发、翻译和运营各自什么信息

面对TOKEN_MISSING,开发侧首先检查模板合同有没有同时升级;翻译侧再检查译文是否遗漏占位符;运营侧确认最终发布稿是否取自同一修订号。不要以为把缺失的花括号生硬地插到句末就算修复,因为句法位置可能影响阅读体验。更稳妥的做法是恢复合法模板,做一次变量替换预览,然后由双语审稿人员确认句子仍然自然。

CHANNEL_LEAK通常需要确定词汇来自哪里。可能是运营从beta公告复制,也可能是内容管理系统将草稿错误合并到了生产版本。直接在输出阶段删除beta-only会掩盖来源,导致下一次仍然混入。更好的流程是从源数据修订,记录候选编号和修订人,再重新生成可核对的产物。本文不虚构真实企业内部人员操作,上述只属于常见工程路径及处理建议。

REVISION_STALE涉及发布与回滚的关系。回滚软件包不代表更新说明可以不受修订控制;真正需要恢复旧版本时,仍要针对当前发布决策重新确认文本。文本修订号26不必等于软件包版本码260,但二者必须在本次发布合同里有清楚的对应关系。若采用语义化版本和独立内容版本,最好在发布工单里显式记录链接,不要靠文件名拼接推断。

九、为什么要保留失败候选,而不是只输出五条通过项

自动化门禁如果只返回通过项,后续很难解释第六条为什么没有出现在最终清单。保留全部八条结果并存储每条候选的拒绝原因,可以让负责人快速定位一次改稿引入了哪些问题。需要注意,原始文案可能包含尚未公开的功能信息,所以台账适合存放ID、必要的字段和摘要,完整内容应由受控的内容仓库保存。公开分享的演示图不应暴露未发布产品的真实文案。

对于重复候选,还应区别候选编号与“最终生效身份”。一个语种在某个渠道可能有多份候选,全部通过也不代表可以同时上传;批准动作需要挑选唯一候选并记录其摘要。本文的模型没有扩展到唯一性决策,因此K02和K06同为英文生产候选是允许存在的测试数据,其中只有前者通过。后续产品化时可以另设“每个locale/channel只能有一个active版本”的聚合门禁,但不能悄悄把它计入这次三项失败原因。

再往前延伸,发布前应检查软件包内的实际版本名、更新说明变量值以及应用信息页的可见文本是否来自同一次构建。当前脚本只有本地文案记录,故意不接触软件包或版本服务;如果整合到CI,就需要配置可信来源,且通过证据要带构建哈希。不能把任何本地JSON字段叫作AGC接口返回值,也不能拿本地规则的通过率预测平台审核时间。

十、审计日志里应当有证据,而不只是颜色

建议每次执行记录任务号LOC-1011-35、输入文件release_copy_08、目标版本2.6.0(260)、期望修订26、候选数量与阻断分布。差异项应包含失败规则、候选编号、语言与渠道;正常项可以仅保存精简摘要,避免日志过于冗长。配图里的八张候选卡与诊断详情都使用这份固定合同,从而让图中红色错误与正文的代码分支可以一一对应。

生成文件时还要警惕时区与构建目录。发布任务触发时间属于日志元信息,不应拿它替代候选修订号;CI机器的本地时区可能不同,计算时间戳应采用明确时区。示例时间只是视觉合同,因此不把09:41解释为真实上传或审核时间。对比缓存快照时,应该以候选ID和内容摘要核对,而不是以文件最后修改时间代表业务状态。

一旦日志出现“实际检查了七条”,即使被检查的七条全部通过,也必须标记检查不完整。输入记录被删、解析中途异常或重复ID覆盖,都不能合并进“全部通过”。当前示例固定八条,CI应断言checked===8,并核对每个K编号恰好出现一次。这个动作只保障固定输入完整性,真实项目的预期候选数量应由清单或任务系统提供。

十一、将风险控制限制在我们确实能证明的范围

这套校验能证明的是三个具体业务不变量:受管模板变量完整、生产稿不含约定的内部测试标识、候选修订号与发布合同一致。它不能证明隐私政策合规、译文没有歧义、AppGallery审核必然通过,更不能说明真实软件包签名正确或API权限已申请。AGC平台规则可能更新,用户仍需要以官方最新要求与实际控制台提交结果为准。

实际工程上线前建议准备两类测试:固定八条输入验证回归分类,另以受控的随机文本生成测试零宽字符、嵌套花括号、未知变量、大小写变化与不同语言换行。再使用真实双语文本进行人工可读性校验,而不是仅追求正则覆盖所有自然语言错误。生产脚本应输出JSON与非零退出码,并由流水线在下一步禁止推进发布;目前这些CI动作仍属设计建议,尚未实际接入平台。

十二、收尾:让内容版本与代码版本在同一张工单里对账

开发者容易关注代码能否打包、依赖是否锁定、设备能否安装,而忽视更新说明本身也属于需要版本管理的发布资产。本轮ReleaseNoteParity用八条本地候选留下五条可用、三条明确的拒绝原因,把“英文少一个变量”和“测试渠道词混进生产稿”转化为可重复执行的检查。它与此前的截图尺寸门禁、候选包版本码门禁不是同一道题:这里保护的是跨语言文案的语义占位合同与修订来源。

如果后续要接入真正的AGC发布链路,需要再设计账号权限、服务接口、手工审批与平台回执的证据结构。当前阶段坚持FIXTURE_ONLY、AGC_UPLOAD=NOT_RUN、APP_BINARY=NOT_RUN,比给示意图画一枚绿色“审核通过”更有工程价值。能够清晰区分哪些问题已经用代码证明、哪些仍需人工与平台确认,才是一套内容发布前门禁应有的完成条件。

官方参考:华为开发者 AppGallery Connect 简介;华为应用市场上架说明。 华为开发者《AppGallery Connect简介》《华为应用市场》应用发布流程。本文的{version}、{feature}、beta-only、修订号与所有状态名为自定义业务规则,不是官方强制文本字段。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐