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