"""Verify the supplied capture archive and prepare two fixed inputs; no network or DB access.""" from pathlib import Path import collections import hashlib import json import zipfile ROOT = Path(__file__).resolve().parents[1] ARCHIVE = ROOT / 'work/current-product-archive-20261102.zip' CAPTURE = ROOT / 'work/current-product-archive-20261102' DEST = ROOT / 'work/current-product-observations-20261102' DOCS = Path(r'F:\文档\工作坊当前资料') EXPECTED_ARCHIVE = '0ec14dcccbce26bc0fc1a368bfaef4bbc32ea6f0b651ae31df5dd28a8bc0e7a5' TASK = 'KSF-商品页面观察-20261102-01' REPORTED_TIME = '2026-11-02 19:11–19:13 +08:00(采集者回报范围;无精确时间原始日志)' sha = lambda b: hashlib.sha256(b).hexdigest() encode = lambda obj: (json.dumps(obj, ensure_ascii=False, indent=2) + '\n').encode('utf-8') def retain(path, value): if path.exists(): assert path.read_bytes() == value, f'Preserve different existing file: {path}' else: path.parent.mkdir(parents=True, exist_ok=True) with path.open('xb') as out: out.write(value) def main(): archive = ARCHIVE.read_bytes() assert len(archive) == 614046 and sha(archive) == EXPECTED_ARCHIVE manifest = json.loads((CAPTURE / 'originals-manifest.json').read_text('utf-8')) assert len(manifest) == 4 and len({m['file'] for m in manifest}) == 4 files = [] for entry in manifest: assert Path(entry['file']).name == entry['file'] blob = (CAPTURE / 'originals' / entry['file']).read_bytes() assert len(blob) == entry['size'] and sha(blob) == entry['sha256'] files.append({'file': entry['file'], 'bytes': len(blob), 'sha256': sha(blob), 'matched': True}) by_name = {entry['file']: entry for entry in files} card_blob = (CAPTURE / 'originals/suning-card.html').read_bytes() card_text = card_blob.decode('utf-8') card = json.loads(card_text) assert len(card) == 1 and set(card[0]) == {'html', 'text'} assert len(card_text) == 4341 and len(card_blob) == 4681 widths = collections.Counter(len(c.encode('utf-8')) for c in card_text) assert widths == {1: 4170, 2: 2, 3: 169} assert len(card[0]['html']) == 4000 and card[0]['html'].endswith('aria-hidden="tru') suning = card[0]['text'] huajie = (CAPTURE / 'originals/huajie-5904.txt').read_text('utf-8') quote_start = huajie.index('新品 康师傅 红烧牛肉拌面115g') quote_end = huajie.index('进入店铺', quote_start) + len('进入店铺') huajie_quote = huajie[quote_start:quote_end] assert huajie.count('$') == 23 assert all(word not in huajie for word in ['USD', '美元', '人民币']) review = { 'ok': True, 'operation': 'current-product-archive-independently-reviewed', 'archiveSha256': EXPECTED_ARCHIVE, 'archiveBytes': len(archive), 'archiveEntries': 8, 'originalsVerified': files, 'screenshotsVisuallyReviewed': True, 'suningSize': {'bytes': len(card_blob), 'characters': len(card_text), 'utf8WidthCounts': dict(widths), 'explanation': '169 three-byte characters add 338 bytes; two two-byte yen signs add 2 bytes; total difference 340', 'historicalFirstToolOutputAvailable': False}, 'suningFormat': {'container': 'JSON array with html and text', 'htmlCharacters': 4000, 'htmlFragmentTruncated': True, 'completePageHtml': False}, 'huajieCurrency': 'dollar sign only; currency and market region not verified', 'missingSizeNote': {'presentInPackage': False, 'removalActorOrIntentVerified': False, 'originalsCurrentIntegrityBlocked': False}, 'verifiedScope': 'archived page display observations; not current stock, fulfilled orders or verified SKU parameters', 'readOnly': True, 'networkRequests': 0, 'businessWrites': 0, } retain(ROOT / 'work/current-product-archive-review-20261102.json', encode(review)) def task(key, url, platform, title, raw, quote, original, screenshot, limitations): begin = raw.index(quote) record = { 'itemKey': key + '-page-observation', 'className': 'KfEvidence', 'externalKey': TASK + ':' + key + ':page-observation:v1', 'data': { 'dataMode': 'live', 'quote': quote, 'evidenceKind': 'retailer_listing_observation', 'platform': platform, 'quoteForm': 'verbatim_excerpt', 'verificationState': 'archived_text_and_screenshot_checked', 'collectedAt': None, 'locator': { 'url': url, 'section': '已归档商品卡文字' if key == 'suning' else '已归档店铺商品页面文字', 'startCharacter': begin, 'endCharacter': begin + len(quote), 'unit': 'Unicode code point', 'observedAtReported': REPORTED_TIME, 'exactCaptureTimestampVerified': False, 'archiveSha256': EXPECTED_ARCHIVE, 'textOrigin': {'file': original, 'sha256': by_name[original]['sha256'], 'extraction': 'JSON[0].text' if key == 'suning' else 'UTF-8 text file'}, 'screenshot': {'file': screenshot, 'sha256': by_name[screenshot]['sha256']}, 'limitations': limitations, **({'productLink': 'https://product.suning.com/0000000000/12424158513.html', 'deliveryRegionVisible': '江西南昌'} if key == 'suning' else {}), }, }, } assert raw[begin:begin + len(quote)] == quote return { 'source': {'title': title, 'sourceKind': 'ecommerce', 'dataMode': 'live', 'sourceUrl': url, 'platform': platform, 'rawText': raw, 'collectedAt': None}, 'records': [record], } inputs = { 'suning.input.json': task( 'suning', 'https://search.suning.com/%E5%BA%B7%E5%B8%88%E5%82%85%E6%8B%8C%E9%9D%A2/', 'suning', '苏宁搜索商品卡观察(2026-11-02;规格与库存未核实)', suning, suning, 'suning-card.html', 'kangshifu-dangqian-suning-list.png', ['页面展示不证明有货、可下单或当前在售。', '17.70元对应件数未知,不是已核实单碗价或成交价。', '领券200-10只是商品卡提示,适用范围和优惠门槛未独立核实。', '标题与主图不能补足净含量、组合件数、配料或营养参数;不创建或关联既有产品/SKU。', '100+评价只是页面计数提示,没有取得消费者逐字评价。', 'HTML片段已截断;source.rawText仅来自随包JSON导出的text字段。']), 'huajie.input.json': task( 'huajie', 'https://www.huajiemarket.com/goods/info/5904', 'huajiemarket', '华姐零食铺商品页面观察(2026-11-02;币种与市场地区未核实)', huajie, huajie_quote, 'huajie-5904.txt', 'kangshifu-dangqian-huajie-5904.png', ['4.59与5.99使用$符号;具体币种未知,不按美元或人民币换算。', '市场地区和配送地区未核实,不作为中国国内在售或价格对比证据。', '115g只是标题标示;碗形仅为主图图示,非规格参数已核实;无出口包装证据。', '有效期和加入购物车入口只记录页面文字,不证明实物批次、库存或完成交易。', '营销文案属于店铺描述,不是消费者反馈;不创建或关联既有产品/SKU。']), } for name, value in inputs.items(): retain(DEST / name, encode(value)) review_text = '''当前商品原件核对结果(2026年11月2日) 附件614,046字节,包SHA-256与运维回报一致;8个文件齐全,四份原件的大小与完整哈希4/4吻合。Codex已实际查看两张截图并读取文字。 苏宁截图及商品卡文字支持商品标题、17.70元页面报价、苏宁自营标记、券提示和江西南昌收货地区;不支持净含量、组合件数、单碗价、成交价或当前库存。 华姐页面支持标题中的115g、$4.59/$5.99、有效期文字与店铺名称;币种和市场地区未核实。商品主图呈碗形,仅保留为图片观察,不当作规格参数或出口包装证明。 大小差异已独立复现:suning-card.html为4,681字节、4,341个字符。补充说明的字符分类需纠正:实际为169个三字节字符和两个两字节的¥符号,多出169×2+2×1=340字节。可解释当前数字差异,但没有首次工具输出,不能反推全部历史。 文件格式也应准确记录:名为.html的文件实际是含html/text的JSON数组,其中html为4,000字符的截断片段,不是完整网页。现有商品文字仍可与截图互相核对,来源入库时只用实际取得的text字段。 SIZE-DISCREPANCY-NOTE.md不在交付包中,Codex未操作远端目录,无法判断是谁删除、为何删除或内容是否合并;记录为未确认的过程备注。它不影响当前四份原件与清单的核对,不据此断言历史完整。 结论:归档原件核对通过,适合保存为两条独立的“商品页面观察”来源及证据。当前在售、SKU规格和可比较单价仍未核实。固定输入已准备并在本地验证后交付;实际线上入库、产品创建和报告更新均未执行。 ''' retain(DOCS / '11-当前商品原件核对结果.txt', review_text.encode('utf-8')) ops = '''运维001:准备两条商品页面观察入库。只落地固定输入与留存目录,不代跑业务。 包:ksf-page-observations-20261102.zip。核FILES.sha256全部项。 将inputs/suning.input.json、inputs/huajie.input.json放入/opt/data/ksf-executor-20261102/page-observations-01/,root:root 0644、hermes可读;保持内容和文件名不变。沿用现有ksf-secure.mjs,不装新执行器、不改Skill/Function、不重启。 归档原件继续留在原位置;包只包含已核对的固定输入和核对说明,不替换原件。输入仅涉及KfSource+KfEvidence,无KfCategory、KfProduct、会话或报告写入。 确认本任务键KSF-商品页面观察-20261102-01的历史与在途;如已存在相同记录,保留不删,回读后按实际created/unchanged计数。发现已有该任务执行回执或在途则停止交付执行,不换键重跑。 以hermes(10000)、HOME=/opt/data建立全新留存目录/opt/data/ksf-workshop-private/page-observations/KSF-page-observations-20261102-01(父目录与目标0700)。目标如已存在,不清空也不另换目录,先核查已有状态。 回报输入哈希与权限、历史/在途核查和留存目录状态。随后交王刚001在一条新的真实消息任务里执行一次;运维不代跑。无需新建身份或复制员工/会话配置。 ''' wang = '''王刚001:运维准备完成后,使用你本条消息已核实的现有agentProfileKey,通过现有secure入口导入两份固定页面观察输入。输入已经由Codex逐文件及截图核对,不需要你设计或改写。 固定任务键KSF-商品页面观察-20261102-01。先确认无该任务执行回执/在途。以hermes(10000)、HOME=/opt/data执行;本步授权是资料入库,不是当前在售、SKU或价格研究已完成。 第一份(苏宁): node /opt/data/skills/ksf-workshop-runtime/scripts/ksf-secure.mjs workshop-dedup --input /opt/data/ksf-executor-20261102/page-observations-01/suning.input.json --request-id KSF-商品页面观察-20261102-01:suning:preflight:v1 只读查重成功后执行一次: node /opt/data/skills/ksf-workshop-runtime/scripts/ksf-secure.mjs workshop-import --agent-profile <当前已核实agentProfileKey> --input /opt/data/ksf-executor-20261102/page-observations-01/suning.input.json --request-id KSF-商品页面观察-20261102-01:suning:import:v1 先确认外层ok和entryResult.ok均为true,取得entryResult.result里的sourceId、items[0].objectId和operation。只创建KfEvidence条目;不建产品或分类。输入的collectedAt=null是有意保留:原件只留采集者回报的观察时间范围,范围在locator.observedAtReported,不伪造精确时间。 用返回的sourceId和evidenceId分别写只读输入{"className":"KfSource","objectId":"<实际sourceId>"}及{"className":"KfEvidence","objectId":"<实际evidenceId>"}到留存目录(0600、独占创建),调用: node /opt/data/skills/ksf-workshop-runtime/scripts/ksf-secure.mjs workshop-read --input <上述绝对路径> --request-id KSF-商品页面观察-20261102-01:suning:read-source:v1 另一条evidence回读使用同格式命令及requestId KSF-商品页面观察-20261102-01:suning:read-evidence:v1。 确认来源rawText/sourceKind=ecommerce/dataMode=live/平台/URL与输入相同;证据quote/locator/evidenceKind/verificationState与输入逐项一致、sourceRef指回本次来源、productRef为空、collectedAt为空。 苏宁导入与两项回读均通过后,才处理第二份华姐页面:上述各命令的输入文件换为huajie.input.json,requestId里的suning统一换为huajie,其余流程相同。两个来源分开保存,不合并商品。 所有输入、stdout/stderr、退出码、两层调用回执独占留存到/opt/data/ksf-workshop-private/page-observations/KSF-page-observations-20261102-01,0600。任何一步失败即停,包括查重或回读失败,不自动重试/换键/删记录;回报已取得编号。错误不代表零写入。 成功回报每页sourceId、evidenceId、jobId、sourceOperation、created/unchanged、import两层invocationId及精确回读结果。相同资料unchanged属正常,不为增加数量换键。没有新产品、SKU、会话、报告或新版S1是预期;G8IrkkDG7X保持原样,不重跑旧任务。不得说“已核实当前在售”“已验证单碗价”“采集了消费者评价”或“美元价”。 ''' retain(DOCS / '12-发给运维001-商品页面观察准备.txt', ops.encode('utf-8')) retain(DOCS / '13-发给王刚001-商品页面观察入库.txt', wang.encode('utf-8')) package = {**{'inputs/' + name: encode(value) for name, value in inputs.items()}, 'archive-review.json': encode(review), '核对结果.txt': review_text.encode('utf-8'), '给运维001.txt': ops.encode('utf-8'), '给王刚001.txt': wang.encode('utf-8')} package['FILES.sha256'] = ''.join(sha(blob) + ' ' + name + '\n' for name, blob in package.items()).encode() target = DOCS / 'ksf-page-observations-20261102.zip' assert not target.exists(), 'Preserve existing package' with zipfile.ZipFile(target, 'x', compression=zipfile.ZIP_DEFLATED) as z: for name, blob in package.items(): z.writestr(name, blob) with zipfile.ZipFile(target) 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 = {'file': str(target), 'sha256': sha(target.read_bytes()), 'bytes': target.stat().st_size, 'manifestChecked': '6/6', 'taskKey': TASK, 'sourcesPlanned': 2, 'evidencePlanned': 2, 'productsPlanned': 0, 'reportsPlanned': 0, 'businessWrites': 0, 'targetExecuted': False, 'inputHashes': {name: sha(encode(value)) for name, value in inputs.items()}} retain(ROOT / 'work/current-product-observations-package-receipt.json', encode(receipt)) print(json.dumps(receipt, ensure_ascii=True, indent=2)) if __name__ == '__main__': main()