| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- """Build an offline handoff for one SKU using existing local evidence only."""
- import argparse
- import base64
- import hashlib
- import html
- import json
- from pathlib import Path
- import shutil
- import struct
- import zipfile
- ROOT = Path(__file__).resolve().parents[1]
- DOCS = ROOT / 'docs/single-sku-validation-20261107-r1'
- DEFAULT_SOURCE = Path(
- 'D:/下载/WXWork/1688857450794210/Cache/File/2026-11/'
- 'ksf-label-evidence-20261107-r1.zip'
- )
- EXPECTED_ARCHIVE_SHA = '6292c2303a391a33a4e6f393527c433f46a82c31b12643c877acfc6a1114429a'
- EXPECTED_GENERATION = '82b104fc024b49a75d2f5376bf9778ced6f63a9433b5fc5a6daa3c458d05d5ea'
- PRODUCT_ID = 'jd:100297147639'
- REVIEW_ID = PRODUCT_ID + ':106531290235443828'
- IMAGE_NAMES = (
- 'images/img1-包装正面.jpg',
- 'images/img2-营养成分表与冲调说明.jpg',
- 'images/img3-配料表.jpg',
- )
- def sha(raw):
- return hashlib.sha256(raw).hexdigest()
- def write_json(path, data):
- path.parent.mkdir(parents=True, exist_ok=True)
- path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
- def jpeg_size(raw):
- """Read dimensions from a JPEG SOF marker without an imaging dependency."""
- if raw[:2] != b'\xff\xd8':
- raise ValueError('IMAGE_NOT_JPEG')
- pos = 2
- while pos < len(raw):
- if raw[pos] != 0xFF:
- raise ValueError('INVALID_JPEG_MARKER')
- while raw[pos] == 0xFF:
- pos += 1
- marker = raw[pos]
- pos += 1
- if marker in (0xD8, 0xD9) or 0xD0 <= marker <= 0xD7:
- continue
- length = struct.unpack('>H', raw[pos:pos + 2])[0]
- if marker in (0xC0, 0xC1, 0xC2):
- height, width = struct.unpack('>HH', raw[pos + 3:pos + 7])
- return width, height
- if marker == 0xDA:
- break
- pos += length
- raise ValueError('JPEG_DIMENSIONS_NOT_FOUND')
- def main():
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('--source', type=Path, default=DEFAULT_SOURCE)
- parser.add_argument('--out', type=Path,
- default=ROOT / 'artifacts/ksf-single-sku-validation-20261107-r1/package')
- args = parser.parse_args()
- out = args.out.resolve()
- if out.exists():
- raise ValueError('NEW_OUTPUT_REQUIRED: existing output will not be overwritten')
- archive_raw = args.source.read_bytes()
- if sha(archive_raw) != EXPECTED_ARCHIVE_SHA:
- raise ValueError('SOURCE_ARCHIVE_CHANGED')
- with zipfile.ZipFile(args.source) as archive:
- checks = []
- for line in archive.read('FILES.sha256').decode('utf-8-sig').splitlines():
- if not line.strip():
- continue
- expected, name = line.split(None, 1)
- name = name.strip()
- actual = sha(archive.read(name))
- if actual != expected:
- raise ValueError('SOURCE_MANIFEST_MISMATCH:' + name)
- checks.append({'path': name, 'sha256': actual, 'ok': True})
- if len(checks) != 7 or len(archive.namelist()) != 8:
- raise ValueError('SOURCE_MANIFEST_SCOPE_CHANGED')
- images = {name: archive.read(name) for name in IMAGE_NAMES}
- review_page_path = ROOT / 'work/opportunity-denominator-20261107-r1/page-5.json'
- review_page_raw = review_page_path.read_bytes()
- review_page = json.loads(review_page_raw)
- if review_page['generation'] != EXPECTED_GENERATION:
- raise ValueError('CACHED_GENERATION_CHANGED')
- matches = [r for r in review_page['records'] if r['record_id'] == REVIEW_ID]
- if len(matches) != 1 or matches[0]['product_id'] != PRODUCT_ID:
- raise ValueError('EXACT_REVIEW_NOT_FOUND')
- review = matches[0]
- if review['is_default'] is not None or review['rating_scale'] is not None:
- raise ValueError('REVIEW_UNKNOWN_STATES_CHANGED')
- dimensions = {name: jpeg_size(raw) for name, raw in images.items()}
- if set(dimensions.values()) != {(608, 1080)}:
- raise ValueError('PHOTO_DIMENSIONS_CHANGED')
- response = json.loads((DOCS / 'current-label-response.template.json').read_text('utf-8'))
- if any(value is not None for value in response['currentLabel'].values()):
- raise ValueError('CURRENT_LABEL_TEMPLATE_MUST_BE_UNKNOWN')
- out.mkdir(parents=True, exist_ok=False)
- (out / 'images').mkdir()
- (out / 'sources').mkdir()
- for source, name in (
- ('README.md', 'README.md'),
- ('product-material-request.md', '产品部资料需求.md'),
- ('comprehension-protocol.md', '说明理解测试方案.md'),
- ('current-label-response.template.json', 'current-label-response.template.json'),
- ):
- shutil.copyfile(DOCS / source, out / name)
- for name, raw in images.items():
- (out / name).write_bytes(raw)
- evidence = {
- 'schema': 'ksf.single-sku-validation-evidence.v1',
- 'preparedAt': '2026-11-07',
- 'productId': PRODUCT_ID,
- 'reviewId': REVIEW_ID,
- 'evidenceGeneration': EXPECTED_GENERATION,
- 'scope': 'one SKU; existing local evidence; no participant results',
- 'status': {
- 'currentLabel': 'NOT_RECEIVED',
- 'versionComparison': 'NOT_STARTED',
- 'productRequest': 'DRAFT_NOT_SENT',
- 'participantTest': 'NOT_STARTED',
- 'participantsObserved': 0,
- },
- 'consumerReview': {
- 'date': review['review_date_raw'],
- 'photoCaptureDate': None,
- 'reportedAction': '少放调料',
- 'statedReason': '可能盐摄入超标(消费者顾虑,非检测结论)',
- 'isDefault': review['is_default'],
- 'ratingScale': review['rating_scale'],
- 'identityState': review['identity_state'],
- 'categoryState': review['category_state'],
- 'ratingRaw': review['rating_raw'],
- 'sourceRecordId': review['source_record_id'],
- },
- 'historicalPhotoLabel': {
- 'verification': 'read_from_consumer_photo; not current-version confirmation',
- 'nutrition': {
- 'servingSizeG': 96,
- 'servingSizeBasis': '每份(96克)',
- 'sodiumMgPerServing': 2414,
- 'sodiumNrvPercent': 121,
- 'derivedSodiumMgPer100g': 2414 / 96 * 100,
- 'displayDerivedSodiumMgPer100g': 2515,
- 'derivation': '2414 / 96 * 100; calculated, not printed per100g or laboratory measured',
- 'netWeightG': None,
- 'servingsPerPackage': None,
- 'sourceImage': IMAGE_NAMES[1],
- },
- 'preparation': {
- 'methodVisible': '方法二:泡面',
- 'waterMlVisible': 460,
- 'waterTemperatureVisible': '沸水',
- 'stepsSummary': '面饼置碗,依次加入粉包、酱包、菜包;后加水',
- 'timeVisibleFragment': '静置4…',
- 'completeMinutes': None,
- 'completeInstructions': None,
- 'doseRatio': None,
- 'allPreparationMethods': None,
- 'sourceImage': IMAGE_NAMES[1],
- },
- 'packagingCodeVisible': 'WXFC-1A',
- 'standardCodeVisible': 'GB 17400',
- 'shelfLifeMonthsVisible': 6,
- 'productionDate': None,
- 'batch': None,
- 'isCurrentVersion': None,
- 'ingredientObservation': '面饼、粉包、酱包均列有食用盐;不可由半包粉包推算整份钠减半',
- 'sourceImageForCodeAndIngredients': IMAGE_NAMES[2],
- },
- 'skuCorrespondence': {
- 'flavor': 'photo and purchased SKU are consistent',
- 'titleDeclaredG': 96,
- 'titleDeclaredPackCount': 24,
- 'photoServingSizeAgreesWithTitleNumber': True,
- 'boxCountVerifiedByPhoto': False,
- 'netWeightVerifiedByPhoto': False,
- 'currentVersionVerified': False,
- },
- 'firstRoundPlan': {
- 'plannedParticipants': 5,
- 'observedParticipants': 0,
- 'recruitmentStarted': False,
- 'method': 'packaging comprehension interview; no preparation or tasting',
- 'results': [],
- 'screeningRule': 'at least 2 of 5 showing the same key misunderstanding is a revision signal, not a prevalence estimate',
- 'secondRoundConditional': '5 different participants after an evidence-backed wording revision',
- },
- 'otherMissingSkuIds': [
- 'jd:100015905411', 'jd:100018673006', 'jd:100061767875',
- 'jd:100001974969', 'jd:5590761', 'jd:5532971', 'jd:6982483',
- ],
- 'photos': [{'path': name, 'sha256': sha(raw), 'bytes': len(raw),
- 'width': dimensions[name][0], 'height': dimensions[name][1]}
- for name, raw in images.items()],
- 'boundaries': {
- 'paidCallsThisBuild': 0,
- 'networkRequestsThisBuild': 0,
- 'productionWritesThisBuild': 0,
- 'externalMessagesSent': 0,
- 'ledgerCarriedForward': {'chargedCny': 46.20, 'pendingCny': 0, 'protectedCny': 3.80},
- 'eightStoreV3Changed': False,
- 'opportunityRankingChanged': False,
- 'formulationDecisionMade': False,
- 'labelPublicationAuthorizedByThisPackage': False,
- },
- }
- write_json(out / 'evidence.json', evidence)
- write_json(out / 'sources/review.json', {
- 'generation': EXPECTED_GENERATION,
- 'cachedPage': review_page_path.relative_to(ROOT).as_posix(),
- 'cachedPageSha256': sha(review_page_raw),
- 'readbackPreviouslySaved': True,
- 'record': review,
- })
- write_json(out / 'sources/archive-verification.json', {
- 'sourceArchive': args.source.name,
- 'bytes': len(archive_raw),
- 'sha256': sha(archive_raw),
- 'archiveEntries': 8,
- 'manifestEntriesChecked': 7,
- 'manifestSelfHashed': False,
- 'allChecksPassed': True,
- 'checks': checks,
- 'selectedOriginalPhotos': list(IMAGE_NAMES),
- 'note': 'source report prose is not copied; corrected facts are separately recorded in evidence.json',
- })
- captions = ('p1 · 包装正面:口味', 'p2 · 营养表与冲调局部', 'p3 · 配料表与包装代码')
- gallery = []
- for name, caption in zip(IMAGE_NAMES, captions):
- encoded = base64.b64encode(images[name]).decode('ascii')
- gallery.append(
- '<figure><img src="data:image/jpeg;base64,' + encoded + '" '
- 'width="608" height="1080" alt="' + html.escape(caption, quote=True) + '">'
- '<figcaption>' + html.escape(caption) + '<br><a href="' + html.escape(name, quote=True) +
- '" target="_blank" rel="noopener">打开原图</a></figcaption></figure>'
- )
- page = (DOCS / 'index.template.html').read_text('utf-8')
- page = page.replace('%%REVIEW_QUOTE%%', html.escape(review['content']))
- page = page.replace('%%GALLERY%%', ''.join(gallery))
- page = page.replace('%%RESPONSE_TEMPLATE%%',
- json.dumps(response, ensure_ascii=False).replace('<', '\\u003c'))
- if '%%' in page:
- raise ValueError('UNRESOLVED_TEMPLATE_TOKEN')
- (out / 'index.html').write_text(page, encoding='utf-8')
- print(json.dumps({'ok': True, 'packageDirectory': str(out),
- 'state': 'BUILT_AWAITING_LOCAL_RENDER_VERIFICATION',
- 'paidCalls': 0, 'productionWrites': 0}, ensure_ascii=False))
- if __name__ == '__main__':
- main()
|