package-expanded-review-intake.py 7.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. """Package second-round review source batches and a precise publication handoff."""
  2. import hashlib
  3. import json
  4. from pathlib import Path
  5. import zipfile
  6. ROOT = Path(__file__).resolve().parents[1]
  7. WORK = ROOT / 'work/product-reviews/round2/intake'
  8. DOCS = ROOT / 'docs/industry-product-reviews-20261106/round2'
  9. OUT = ROOT / 'artifacts/商品评价扩充-20261106-r2'
  10. def sha(raw):
  11. return hashlib.sha256(raw).hexdigest()
  12. def main():
  13. report = json.loads((WORK / 'verification.json').read_text(encoding='utf-8'))
  14. rebuilding = json.loads((WORK / 'source-rebuild-verification.json').read_text(encoding='utf-8'))
  15. assert report['ok'] and rebuilding['verifiedReviews'] == report['selectedReviews']
  16. OUT.mkdir(parents=True, exist_ok=True)
  17. DOCS.mkdir(parents=True, exist_ok=True)
  18. archive = OUT / 'ksf-review-coverage-20261106-r2.zip'
  19. if archive.exists():
  20. raise ValueError('RELEASE_ALREADY_EXISTS')
  21. instruction = f'''# 第二批商品评价扩充:真实来源已准备,待生产发布
  22. 本批由Codex直接采集,用户要求继续补充“很多商品没有评价”的缺口。生产盘点4674件商品,仅54件有收录评价,其余4620件未收录;不能将未收录解读为平台零评价。
  23. 本轮从历史展示评价量一万以上的未收录商品中,按康师傅优先、品牌及店铺/品类分散选取58件。每件只请求一页、不重试;按实际点赞数在本页内排序,最多取20条。当前选中 **{report['sourceVerifiedProducts']}件商品、{report['selectedReviews']}条原评**,正点赞{report['positiveLikeReviews']}条,样本最高{report['maxObservedLikes']}赞,不称平台全量高赞。
  24. 本轮请求{report['collectionCalls']}次,成功{report['successfulCalls']}次,已核费用{report['confirmedNewCostCny']:.2f}元、未决预留{report['unsettledNewReservationCny']:.2f}元;仍计入原50元总预算。此前任务9.90元保守占用、第一批评价2.10元已核加预留均保留。失败/超时不自动重发。
  25. ## 数据与质量
  26. - 每条按完整平台商品ID及reviewId关联,核请求ID与返回ID一致,排除模板、空内容、错商品和重复记录。
  27. - 真实点赞数、评论总量展示及来源时间在来源JSON保留;reviews表没有点赞列,未知默认状态与评分量表仍留空,不能为了显示在分析范围而填false/造五星量表。
  28. - 新评论发布后应在“全部评价(含待确认)”检查;“推荐用于分析”可能不展示这批未知默认状态记录。
  29. - 商品上下文来自固定生产代际,既有商品标题/店铺/平台原值保留。构建器会生成标题候选规格,生产dry需核是否重复,不能替换已验证规格或擦除旧业务字段。
  30. - 媒体manifest只用于追溯接口替换过的图片URL,头像/评价附图不当商品主图;本包不导入图片类。
  31. - 所有来源已做文件SHA、JSON Pointer、商品归属及暂存回读;已从传输包重新构建验证,不包含活动数据库或凭据。
  32. ## 发布步骤
  33. 本机仍没有生产受限发布通道;上一次数字人实例通道可用。请沿用该实例既有KSF_LIBRARY_CHANNEL及ksf-voc-library Skill,**无需重新采集、无需新增费用**。
  34. 1. 校验根FILES.sha256;每个intake/batch-XX.zip是library_channel.pack产出的原始来源包,内含batch.json,解压到各自新的私有目录。
  35. 2. 准备时生产代际:`{report['expectedProductionGeneration']}`。先读实际当前代际。并发更新需重核本批商品当前记录及合并计划,不能盲目改generation覆盖。
  36. 3. 本包分为{len(report['batches'])}个小批次。每批按来源预检、新建暂存、发布dry,核商品保留、评价净新增/重复/冲突及候选规格;按用户已有入库授权apply一次,随后status并回读新代际。**前一批发布后,下一批必须读取新的实际generation并重新dry。**
  37. 4. 网络结果不明时以对应来源包SHA查status,不重复数据请求或盲目apply。保留旧代际、原件及合并取舍。
  38. 5. 最后回读全批评价ID、原文、商品归属以及看板“全部评价”,回报净新增评价数、有评价商品数、冲突和剩余未收录商品数。只有生产回读成功后称已入库。
  39. 示例(每批使用目标实例已有脚本路径):
  40. ```sh
  41. python3 -B scripts/agent_batch.py --batch /本批/batch.json --output /本批/new-staging.sqlite
  42. python3 -B scripts/agent_batch.py --batch /本批/batch.json --output /本批/new-staging.sqlite --apply
  43. python3 -B scripts/library_channel.py publish --batch /本批/batch.json --expected-generation <实际当前代际>
  44. python3 -B scripts/library_channel.py publish --batch /本批/batch.json --expected-generation <同一已核dry代际> --apply
  45. python3 -B scripts/library_channel.py status --archive-sha256 <清单内该批SHA>
  46. ```
  47. ## 本地显示修复
  48. Codex已将4181普通预览从旧2360评价快照改为生产只读数据,实测与生产2484条同代际,浏览器可见上批速达面馆10条原评。显式KSF_LIBRARY_REGISTRY/KSF_VOC_DB仍使用指定本地快照。此开发服务器修改不需要生产部署,不影响此次生产数据发布。
  49. 当前本批状态:prepared-not-published。请勿重复发布第一批104条评价。
  50. '''
  51. (DOCS / 'README.md').write_text(instruction, encoding='utf-8')
  52. files = {'交接说明.md': instruction.encode(), 'evidence/verification.json': (WORK / 'verification.json').read_bytes(),
  53. 'evidence/source-rebuild-verification.json': (WORK / 'source-rebuild-verification.json').read_bytes(),
  54. 'evidence/collection-audit.json': (WORK / 'collection-audit.json').read_bytes()}
  55. for ordinal, r in enumerate(report['batches'], 1):
  56. folder = WORK / ('batch-' + str(ordinal).zfill(2))
  57. raw = (folder / 'channel-source.zip').read_bytes()
  58. assert sha(raw) == r['channelArchiveSha256']
  59. files['intake/batch-' + str(ordinal).zfill(2) + '.zip'] = raw
  60. files['evidence/batch-' + str(ordinal).zfill(2) + '.json'] = (folder / 'verification.json').read_bytes()
  61. for name in ['expand-product-review-coverage.py', 'build-expanded-review-intake.py']:
  62. files['reproduction/' + name] = (ROOT / 'scripts' / name).read_bytes()
  63. files['FILES.sha256'] = ''.join(sha(raw) + ' ' + name + '\n' for name, raw in sorted(files.items())).encode()
  64. with zipfile.ZipFile(archive, 'x', zipfile.ZIP_DEFLATED, compresslevel=9) as z:
  65. for name, raw in files.items():
  66. z.writestr(name, raw)
  67. with zipfile.ZipFile(archive) as z:
  68. assert z.testzip() is None
  69. for line in z.read('FILES.sha256').decode().splitlines():
  70. expected, name = line.split(' ', 1)
  71. assert sha(z.read(name)) == expected
  72. receipt = {'ok': True, 'file': str(archive), 'sha256': sha(archive.read_bytes()), 'bytes': archive.stat().st_size,
  73. 'sourceBatches': len(report['batches']), 'products': report['sourceVerifiedProducts'], 'reviews': report['selectedReviews'],
  74. 'productionImported': False, 'filesVerified': len(files) - 1}
  75. (DOCS / 'delivery-receipt.json').write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
  76. print(json.dumps(receipt, ensure_ascii=False, indent=2))
  77. if __name__ == '__main__':
  78. main()