prepare-review-display-hotfix.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. """Build a single-file review display patch against the observed public module."""
  2. import argparse
  3. import difflib
  4. import hashlib
  5. import json
  6. from pathlib import Path
  7. import subprocess
  8. import zipfile
  9. ROOT = Path(__file__).resolve().parents[1]
  10. WORK = ROOT / 'work/review-display-hotfix-20261106-r1'
  11. PACKAGE = WORK / 'package'
  12. OUT = ROOT / 'artifacts/商品评价默认展示修复-20261106-r1'
  13. DOCS = ROOT / 'docs/industry-product-reviews-20261106/display-hotfix'
  14. RELEASE = 'ksf-review-display-20261106-r1'
  15. MODULE = 'src/pages/library-product-detail.js'
  16. BASELINE_SHA = '505c98d3794666c6860de27bd74d0d16e10db76bda734fa4d7445d3744835662'
  17. def sha(raw):
  18. return hashlib.sha256(raw).hexdigest()
  19. def save(relative, raw):
  20. path = PACKAGE / relative
  21. path.parent.mkdir(parents=True, exist_ok=True)
  22. path.write_bytes(raw.encode() if isinstance(raw, str) else raw)
  23. def js(relative, value):
  24. save(relative, json.dumps(value, ensure_ascii=False, indent=2) + '\n')
  25. def prepare():
  26. assert not PACKAGE.exists(), 'PACKAGE_ALREADY_EXISTS'
  27. raw = (WORK / 'production-before' / MODULE).read_bytes()
  28. assert sha(raw) == BASELINE_SHA
  29. before = raw.decode('utf-8')
  30. after = before
  31. patches = [
  32. (" _count(entity) {", " get reviewQuality() {\n"
  33. " if (['analysis', 'audit'].includes(this.query.relatedQuality)) return this.query.relatedQuality;\n"
  34. " return this._count('reviews') > 0 && this.product?.overview?.reviews?.eligible === 0 ? 'audit' : 'analysis';\n"
  35. " }\n _count(entity) {"),
  36. (" const request = libraryProductRequest(this.product?.id, this.query);",
  37. " const request = libraryProductRequest(this.product?.id, this.query);\n"
  38. " if (request?.entity === 'reviews') request.quality = this.reviewQuality;"),
  39. (".value=${this.query.relatedQuality || 'analysis'}", ".value=${this.reviewQuality}"),
  40. (" ${this.panel === 'prices' ? html`<p class=\"scope-note\">",
  41. " ${this.panel === 'reviews' && this.reviewQuality === 'audit' && this.product?.overview?.reviews?.eligible === 0 && this._count('reviews') > 0 ? html`<p class=\"scope-note\">已收录评价仍有信息待确认,先展示原文;这些记录尚未计入分析概况。</p>` : ''}\n"
  42. " ${this.panel === 'prices' ? html`<p class=\"scope-note\">")]
  43. for old, new in patches:
  44. assert after.count(old) == 1, 'PATCH_CONTEXT_MISMATCH'
  45. after = after.replace(old, new)
  46. assert after.split(' static styles = css`', 1)[1] == before.split(' static styles = css`', 1)[1]
  47. payload = after.encode()
  48. save('payload/' + MODULE, payload)
  49. save('evidence/review-display.patch', ''.join(difflib.unified_diff(before.splitlines(True), after.splitlines(True),
  50. fromfile='production/' + MODULE, tofile='patched/' + MODULE)))
  51. homepage = (WORK / 'production-before/index.html').read_bytes()
  52. manifest = {'version': 'ksf.review-display.publish.v1', 'release': RELEASE,
  53. 'origin': 'https://kangshifu.fmode.cn', 'homepageSha256': sha(homepage),
  54. 'files': [{'path': MODULE, 'sha256': sha(payload),
  55. 'priorNormalizedSha256': [sha(raw.replace(b'\r\n', b'\n'))],
  56. 'allowMissing': False, 'observedProductionSha256': sha(raw)}]}
  57. js('deploy/review-display-files.json', manifest)
  58. publisher = (ROOT / 'scripts/publish-opportunity-ui.mjs').read_text(encoding='utf-8')
  59. start = publisher.index('export const FILES=Object.freeze([')
  60. end = publisher.index(']);', start) + 3
  61. publisher = publisher[:start] + 'export const FILES=Object.freeze(' + json.dumps([MODULE]) + ');' + publisher[end:]
  62. publisher = publisher.replace('ksf.opportunity-ui.publish.v1', manifest['version']).replace('ksf.opportunity-ui.20261103.v1', RELEASE)
  63. publisher = publisher.replace('filesChecked:6', 'filesChecked:FILES.length').replace('ksf-opportunity-ui', 'ksf-review-display')
  64. publisher = publisher.replace("sourceDir:root,manifestPath:path.join(root,'deploy/opportunity-ui-files-20261103.json')",
  65. "sourceDir:path.join(root,'payload'),manifestPath:path.join(root,'deploy/review-display-files.json')")
  66. save('scripts/publish-review-display.mjs', publisher)
  67. for relative in ['payload/' + MODULE, 'scripts/publish-review-display.mjs']:
  68. subprocess.run(['node', '--check', str(PACKAGE / relative)], check=True, capture_output=True)
  69. instruction = '''# 商品评价默认展示修复
  70. 512条新评价已入库,生产共有2996条评价。当前线上前端默认只显示可分析记录,使新增商品点开显示0条。此包基于2026-11-06实际线上商品详情模块制作,只更新一个文件。
  71. 更新后:已有原评、可分析数为0且用户未指定查看范围时,直接展示“全部评价(含待确认)”;明确手动选择“推荐用于分析”时仍按该范围读取。待确认原评附说明,不计入分析概况。既有可分析评价的商品仍默认分析范围。
  72. ## 通过既有静态发布入口执行
  73. 用户已要求继续,沿用既有运维入口部署,不再重新采集或导入评价。本机提供的数据库配置没有静态发布连接,因此此包制作时尚未上线。
  74. 1. 解压到站点外的新目录,核FILES.sha256和deploy/review-display-files.json。发布前核实际Web目录;下面/opt/kangshifu-web为此前执行单记录,需现场核实。
  75. 2. 只读预检:`node /本包/scripts/publish-review-display.mjs --target-dir /实际站点目录`。
  76. 3. 预检只应有`src/pages/library-product-detail.js`一项修改。当前详情基线SHA为`505c98d3794666c6860de27bd74d0d16e10db76bda734fa4d7445d3744835662`。若其他窗口已发布新版,停止并集成该版,不能擅改校验基线或覆盖。
  77. 4. 正式发布:`node /本包/scripts/publish-review-display.mjs --target-dir /实际站点目录 --apply`。发布器先备份,按白名单写入、回读;失败尝试回退。再次执行相同包为no-op。回报备份目录与外网JS SHA,并沿既有方式刷新该JS缓存。
  78. 5. 真实浏览器从不带relatedQuality的入口打开,确认10条评价直接显示,刷新后保持;手动改分析范围为0、切回全部为10。另核有18条可分析评价的商品默认仍18,以及无原评商品提示准确。桌面1440和手机390/320验证。
  79. 入口:https://kangshifu.fmode.cn/library/products/jd%3A100017664018?panel=reviews
  80. 已有可分析评价:https://kangshifu.fmode.cn/library/products/jd%3A100005702440?panel=reviews
  81. 本包不更新数据库、首页、Skill、服务、身份与其他页面,也不包含未发布的商品图片/详情布局调整。不可把浏览器临时加载补丁的验收称已上线;上线成功须外网模块SHA和无拦截浏览器结果都通过。
  82. '''
  83. save('交接说明.md', instruction)
  84. DOCS.mkdir(parents=True, exist_ok=True)
  85. (DOCS / 'README.md').write_text(instruction, encoding='utf-8')
  86. (DOCS / 'review-display.patch').write_bytes((PACKAGE / 'evidence/review-display.patch').read_bytes())
  87. js('RELEASE.json', {'release': RELEASE, 'files': 1, 'productionBaselineSha256': BASELINE_SHA,
  88. 'payloadSha256': sha(payload), 'productionInstalled': False, 'databaseIncluded': False,
  89. 'credentialsIncluded': False, 'base': 'observed-production-not-unrelated-workspace-changes'})
  90. print(json.dumps({'ok': True, 'prepared': str(PACKAGE), 'payloadSha256': sha(payload), 'productionWrites': 0}, ensure_ascii=False))
  91. def package():
  92. assert (WORK / 'browser-verification.json').is_file()
  93. assert (WORK / 'publisher-verification.json').is_file()
  94. for name in ['browser-verification.json', 'publisher-verification.json']:
  95. raw = (WORK / name).read_bytes()
  96. assert json.loads(raw)['ok']
  97. save('evidence/' + name, raw)
  98. (DOCS / name).write_bytes(raw)
  99. for name in ['patched-reviews-1440.png', 'patched-reviews-390.png']:
  100. save('evidence/' + name, (WORK / name).read_bytes())
  101. entries = sorted(p for p in PACKAGE.rglob('*') if p.is_file() and p.name != 'FILES.sha256')
  102. assert not any(p.suffix.lower() in {'.sqlite', '.db', '.key', '.pem', '.pyc'} for p in entries)
  103. save('FILES.sha256', ''.join(sha(p.read_bytes()) + ' ' + p.relative_to(PACKAGE).as_posix() + '\n' for p in entries))
  104. OUT.mkdir(parents=True, exist_ok=True)
  105. archive = OUT / (RELEASE + '.zip')
  106. with zipfile.ZipFile(archive, 'x', zipfile.ZIP_DEFLATED, compresslevel=9) as z:
  107. for p in [*entries, PACKAGE / 'FILES.sha256']:
  108. z.write(p, p.relative_to(PACKAGE).as_posix())
  109. with zipfile.ZipFile(archive) as z:
  110. assert z.testzip() is None
  111. for line in z.read('FILES.sha256').decode().splitlines():
  112. expected, name = line.split(' ', 1)
  113. assert sha(z.read(name)) == expected
  114. receipt = {'ok': True, 'path': str(archive), 'sha256': sha(archive.read_bytes()),
  115. 'bytes': archive.stat().st_size, 'filesVerified': len(entries), 'staticFiles': 1,
  116. 'productionInstalled': False, 'productionWrites': 0}
  117. (DOCS / 'delivery-receipt.json').write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
  118. print(json.dumps(receipt, ensure_ascii=False, indent=2))
  119. if __name__ == '__main__':
  120. parser = argparse.ArgumentParser(description=__doc__)
  121. parser.add_argument('--package', action='store_true')
  122. args = parser.parse_args()
  123. package() if args.package else prepare()