From a0c31d7435f730b215833fe1511e12b6ccae7eba Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 13:41:52 +0000 Subject: [PATCH] =?UTF-8?q?fix(pack-update):=20DRY=20fileId=20=E5=B7=B2?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E5=88=A4=E6=96=AD=EF=BC=8COCP=20=E6=94=B6?= =?UTF-8?q?=E6=8B=A2=20treatAsUpdate=20RP=20bump?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 525e69f 修复「同版本未 apply 也需同步」后,编排层仍复刻 versionPolicy 字段计算 shouldBumpRp,且 lastApplied 判定多处手写。抽 isFileIdApplied, 扩展 decideVersionPolicy({ treatAsUpdate }) 为唯一策略源,并补契约测。 Co-authored-by: Shiroha --- sfmc/pack-update-policy.test.mjs | 30 +++++++++++ sfmc/src/pack-update/service.ts | 72 +++++++++++--------------- sfmc/src/pack-update/version-policy.ts | 11 ++-- 3 files changed, 67 insertions(+), 46 deletions(-) diff --git a/sfmc/pack-update-policy.test.mjs b/sfmc/pack-update-policy.test.mjs index 02e5f577..d1c63fbf 100644 --- a/sfmc/pack-update-policy.test.mjs +++ b/sfmc/pack-update-policy.test.mjs @@ -43,6 +43,36 @@ describe("version-policy", () => { assert.equal(d.shouldBumpRp, false); }); + it("同版本未更高时默认不 bump;treatAsUpdate 强制同步仍可 bump RP", () => { + const policy = { + authority: "behavior_pack", + onUpdateOverwriteBoth: true, + rpBumpWhenSameMajor: true, + rpBumpComponent: "patch", + majorHigherSkipRpBump: true, + }; + const plain = decideVersionPolicy([1, 0, 0], [1, 0, 0], policy); + assert.equal(plain.remoteNewer, false); + assert.equal(plain.shouldBumpRp, false); + + const forced = decideVersionPolicy([1, 0, 0], [1, 0, 0], policy, { treatAsUpdate: true }); + assert.equal(forced.remoteNewer, false); + assert.equal(forced.shouldBumpRp, true); + }); + + it("远程更旧但 treatAsUpdate 时仍按同 major 规则 bump RP", () => { + const d = decideVersionPolicy([1, 2, 0], [1, 1, 0], { + authority: "behavior_pack", + onUpdateOverwriteBoth: true, + rpBumpWhenSameMajor: true, + rpBumpComponent: "patch", + majorHigherSkipRpBump: true, + }, { treatAsUpdate: true }); + assert.equal(d.remoteNewer, false); + assert.equal(d.majorHigher, false); + assert.equal(d.shouldBumpRp, true); + }); + it("nextVersionGreaterThan", () => { assert.deepEqual(nextVersionGreaterThan([1, 0, 5], [1, 0, 8], "patch"), [1, 0, 9]); }); diff --git a/sfmc/src/pack-update/service.ts b/sfmc/src/pack-update/service.ts index 52f7c6cc..5bd14ac4 100644 --- a/sfmc/src/pack-update/service.ts +++ b/sfmc/src/pack-update/service.ts @@ -89,6 +89,11 @@ function touchBinding(bpUuid: string, binding: PackSourceBinding, patch?: Partia setBinding(bpUuid, binding); } +/** 该 fileId 是否已成功写入世界目录(DRY:prepareCheck 多处勿手写 null/!==) */ +function isFileIdApplied(binding: PackSourceBinding, fileId: number): boolean { + return binding.lastAppliedFileId != null && binding.lastAppliedFileId === fileId; +} + /** 多查询搜索 + 综合打分排序(probe / searchRemote 共用) */ async function searchAndRankHits( provider: PackSourceProvider, @@ -411,8 +416,8 @@ async function prepareCheck( ); } - /* 已成功应用(或确认无需覆盖)同一 fileId:不再下载 */ - if (binding.lastAppliedFileId != null && binding.lastAppliedFileId === file.fileId) { + /* 已成功应用同一 fileId:不再下载 */ + if (isFileIdApplied(binding, file.fileId)) { touchBinding(bpUuid, binding, { lastFileId: file.fileId }); return withLocalBp( baseCheckFields(bpUuid, name, localVer, binding, { @@ -424,15 +429,12 @@ async function prepareCheck( } if (!downloadArchive) { - /* 仅文件 id 比较的轻量检查 */ - const updateAvailable = binding.lastAppliedFileId == null || binding.lastAppliedFileId !== file.fileId; + /* 仅文件 id 比较的轻量检查(未 apply 即视为有更新) */ return withLocalBp( baseCheckFields(bpUuid, name, localVer, binding, { - updateAvailable, + updateAvailable: true, fileId: file.fileId, - message: updateAvailable - ? t("packUpdate.fileNewer", { file: file.fileName, id: String(file.fileId) }) - : t("packUpdate.upToDate"), + message: t("packUpdate.fileNewer", { file: file.fileName, id: String(file.fileId) }), }), localBp ); @@ -461,44 +463,28 @@ async function prepareCheck( ); } - const decision = decideVersionPolicy(localVer, remoteBpInfo.version, cfg.versionPolicy); - /* - * 是否需要写入世界目录: - * - 远程 BP 版本更高 → 要覆盖 - * - 或同一 fileId 从未成功 apply 过 → 也要覆盖 - * (汉化包等可能与 CF 同版本号但内容不同;旧逻辑在「版本未更高」时直接记 lastApplied 会跳过安装) - * 注意:绝不能在未 install 成功时写入 lastAppliedFileId。 + * 能走到此处说明该 fileId 尚未成功 apply(上方已 early-return)。 + * - 远程 BP 版本更高 → 覆盖 + * - 版本未更高 → 仍覆盖(汉化包等同版本号不同内容;勿在未 install 时写 lastAppliedFileId) + * RP bump 规则集中在 decideVersionPolicy(OCP/DRY:勿在编排层复刻 policy 字段)。 */ - const fileNotApplied = - binding.lastAppliedFileId == null || binding.lastAppliedFileId !== file.fileId; - const updateAvailable = decision.remoteNewer || fileNotApplied; - const shouldBumpRp = decision.remoteNewer - ? decision.shouldBumpRp - : Boolean( - fileNotApplied && - cfg.versionPolicy.onUpdateOverwriteBoth && - cfg.versionPolicy.rpBumpWhenSameMajor && - !decision.majorHigher - ); + const decision = decideVersionPolicy(localVer, remoteBpInfo.version, cfg.versionPolicy, { + treatAsUpdate: true, + }); touchBinding(bpUuid, binding, { lastFileId: file.fileId }); - let message: string; - if (decision.remoteNewer) { - message = t("packUpdate.bpNewer", { - local: fmtVer(localVer), - remote: fmtVer(remoteBpInfo.version), - }); - } else if (fileNotApplied) { - message = t("packUpdate.needSync", { - local: fmtVer(localVer), - remote: fmtVer(remoteBpInfo.version), - id: String(file.fileId), - }); - } else { - message = t("packUpdate.upToDate"); - } + const message = decision.remoteNewer + ? t("packUpdate.bpNewer", { + local: fmtVer(localVer), + remote: fmtVer(remoteBpInfo.version), + }) + : t("packUpdate.needSync", { + local: fmtVer(localVer), + remote: fmtVer(remoteBpInfo.version), + id: String(file.fileId), + }); return withLocalBp( { @@ -506,9 +492,9 @@ async function prepareCheck( name, localVer, remoteVer: remoteBpInfo.version, - updateAvailable, + updateAvailable: true, majorHigher: decision.majorHigher, - shouldBumpRp, + shouldBumpRp: decision.shouldBumpRp, fileId: file.fileId, message, binding, diff --git a/sfmc/src/pack-update/version-policy.ts b/sfmc/src/pack-update/version-policy.ts index 36b887b7..b0f28c03 100644 --- a/sfmc/src/pack-update/version-policy.ts +++ b/sfmc/src/pack-update/version-policy.ts @@ -15,16 +15,21 @@ export function isNewer(remote: SemVer3, local: SemVer3): boolean { return compareSemVer3(remote, local) > 0; } -/** 根据本地/远程 BP 版本与策略,决定是否更新以及是否额外 bump RP */ +/** + * 根据本地/远程 BP 版本与策略,决定是否更新以及是否额外 bump RP。 + * `treatAsUpdate`:同版本/未更高但需强制同步写入时(如 fileId 尚未 apply)也走更新侧 RP bump 规则(OCP:策略集中于此,勿在编排层复刻)。 + */ export function decideVersionPolicy( localBp: SemVer3, remoteBp: SemVer3, - policy: VersionPolicyConfig + policy: VersionPolicyConfig, + opts?: { treatAsUpdate?: boolean } ): VersionCompareResult { const remoteNewer = isNewer(remoteBp, localBp); const majorHigher = remoteBp[0] > localBp[0]; + const asUpdate = remoteNewer || Boolean(opts?.treatAsUpdate); let shouldBumpRp = false; - if (remoteNewer && policy.onUpdateOverwriteBoth) { + if (asUpdate && policy.onUpdateOverwriteBoth) { if (majorHigher && policy.majorHigherSkipRpBump) { shouldBumpRp = false; } else if (policy.rpBumpWhenSameMajor) {