| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233 |
- """Prepare fixed consumer-quote inputs and handoff files. No network or business writes."""
- from pathlib import Path
- import hashlib
- import json
- import sys
- import zipfile
- ROOT = Path(__file__).resolve().parents[1]
- SOURCE = ROOT / 'work/consumer-research-codex-20261102/delivery'
- ARCHIVE = ROOT / 'work/consumer-quotes-codex-20261102.zip'
- DEST = ROOT / 'work/consumer-quote-inputs-20261102'
- DOCS = Path(r'F:\文档\工作坊当前资料')
- TASK = 'KSF-消费者原话-20261102-01'
- ARCHIVE_SHA = 'df741b09e0540c21cc311ea81e560beb87c0ec7a85a1872ad704f17a7b5e4ac1'
- REMOTE_INPUTS = '/opt/data/ksf-executor-20261102/consumer-quotes-01'
- REMOTE_STATE = '/opt/data/ksf-workshop-private/consumer-quotes/KSF-consumer-quotes-20261102-01'
- REMOTE_ARCHIVE = '/opt/data/ksf-workshop-private/archive-packages/consumer-quotes-codex-20261102.zip'
- PACKAGE = DOCS / 'ksf-consumer-quotes-import-20261102.zip'
- sha = lambda blob: hashlib.sha256(blob).hexdigest()
- encode = lambda value: (json.dumps(value, ensure_ascii=False, indent=2) + '\n').encode('utf-8')
- def retain(path, blob):
- if path.exists():
- assert path.read_bytes() == blob, f'Existing file differs; preserve it: {path}'
- else:
- path.parent.mkdir(parents=True, exist_ok=True)
- with path.open('xb') as f:
- f.write(blob)
- def prepare():
- archive = ARCHIVE.read_bytes()
- assert len(archive) == 1287263 and sha(archive) == ARCHIVE_SHA
- with zipfile.ZipFile(ARCHIVE) as z:
- assert z.testzip() is None and len(z.namelist()) == 30
- manifest = z.read('FILES.sha256').decode('utf-8').splitlines()
- assert len(manifest) == 29
- for line in manifest:
- expected, name = line.split(' ', 1)
- assert sha(z.read(name)) == expected
- assert z.read(name) == (SOURCE / name).read_bytes()
- data = json.loads(z.read('consumer-quotes.json'))
- assert len(data['records']) == 4 and data['businessDataWritten'] is False
- inputs = {}
- for row in data['records']:
- key = row['id']
- original_locator = row['locator']
- raw = row['completeMainBody']
- start, end = original_locator['startCharacter'], original_locator['endCharacter']
- assert raw[start:end] == row['quote']
- assert sha(raw.encode('utf-8')) == row['hashes']['mainTextSha256']
- locator = {
- 'url': row['sourceUrl'], 'section': '原帖主文章正文中的使用体验自述',
- 'startCharacter': start, 'endCharacter': end,
- 'unit': 'Unicode code point', 'startBase': 0, 'endExclusive': True,
- 'publishedAtReported': row['publishedAtReported'], 'publicationTimezone': 'unknown',
- 'httpCollectedAt': row['httpCollectedAt'], 'browserReadAt': row['browserCollectedAt'],
- 'archiveSha256': ARCHIVE_SHA,
- 'textOrigin': {'file': original_locator['file'], 'sha256': row['hashes']['mainTextSha256'],
- 'extraction': original_locator['extraction']},
- 'responseOriginal': {'file': row['files']['response'], 'sha256': row['hashes']['originalResponseSha256']},
- 'screenshot': {'file': row['files']['screenshot'], 'sha256': row['hashes']['screenshotSha256']},
- 'articleDom': {'file': row['files']['articleDom'], 'sha256': row['hashes']['articleDomSha256']},
- 'browserReadFile': row['files']['browserRead'],
- 'collectionPerformedBy': 'Codex',
- 'productScope': row['productScope'], 'productIdentificationBasis': row['productIdentificationBasis'],
- 'context': row['context'], 'purchaseVerified': False, 'realIdentityVerified': False,
- 'commercialRelationship': 'unknown', 'visibleTopic': row['visibleTopic'],
- 'limitations': data['limits'],
- }
- item = {
- 'itemKey': key + '-experience', 'className': 'KfEvidence',
- 'externalKey': TASK + ':' + key + ':experience:v1',
- 'data': {
- 'dataMode': 'live', 'quote': row['quote'],
- 'evidenceKind': 'public_user_experience_self_report',
- 'platform': 'smzdm', 'authorLabel': row['authorDisplay'],
- 'parentObjectId': row['sourceUrl'].rstrip('/').split('/')[-1],
- 'quoteForm': 'verbatim_excerpt',
- 'verificationState': 'archived_text_and_screenshot_checked',
- 'publishedAt': None, 'collectedAt': row['browserCollectedAt'],
- 'locator': locator,
- },
- }
- task = {
- 'source': {'title': row['title'], 'sourceKind': 'social', 'dataMode': 'live',
- 'sourceUrl': row['sourceUrl'], 'platform': 'smzdm',
- 'rawText': raw, 'collectedAt': row['httpCollectedAt']},
- 'records': [item],
- }
- assert 'productRef' not in item['data'] and 'sourceRef' not in item['data']
- inputs[key + '.input.json'] = task
- retain(DEST / (key + '.input.json'), encode(task))
- review = {
- 'ok': True, 'operation': 'consumer-quote-draft-reviewed',
- 'basis': '王刚001草稿由用户转述;原话与固定本地原件独立对照。远端文件权限和其哈希验算只记为王刚001回报。',
- 'taskKey': TASK, 'originalArchiveSha256': ARCHIVE_SHA, 'originalArchiveBytes': len(archive),
- 'sourceCount': 4, 'evidenceCount': 4, 'platformCount': 1,
- 'corrections': [
- '明确表达面量少的是①cq-03、②cq-04,共2篇;③cq-02没有说量少。问题表中“③②同系列”应为“①②”。',
- '①既说“面量真少”“真不够吃”,也批评青豆;不能说唯一明确批评是青豆,也不将“看着还可以吧”写成整体正面。',
- '“作为一餐是否足够”是研究问题,不是①②的逐字原话;自加食材的动机与具体用餐场景保持待验证。',
- '最新帖发表于2024-10-10,距2026-11-02已超过2年。“近两年没有样本”成立;“快2年”应改为“超过2年”。',
- '④是作者留水改善口感的经验,不能推断所有留水量的效果,也不能因作者语气正面就断言其从未感觉干。',
- '“吃腻泡面想尝鲜”“元旦前买”等明确自述可保留,人口特征、真实用餐场景、动机及需求强度不能补写。',
- ],
- 'sampleObservations': {'portionComments': ['cq-03', 'cq-04'],
- 'boilingMentioned': ['cq-02', 'cq-03', 'cq-04'],
- 'unnoticedBeforePurchase': ['cq-02'],
- 'drainingWaterAdvice': ['cq-01'], 'peaDislike': ['cq-03']},
- 'sampleProportionsArePopulationEstimates': False,
- 'draftReadyForInputPreparation': True, 's2Saved': False,
- 'inputHashes': {name: sha(encode(value)) for name, value in inputs.items()},
- 'businessWrites': 0,
- }
- retain(DEST / 'draft-review.json', encode(review))
- review_text = '''消费者原话草稿核对结论
- 草稿可以用于继续准备资料入库,尚不是已保存的S2成果。四篇原话、出处与产品区分符合原件,保留正负上下文、小样本和历史时间边界。
- 保存前修正以下表述:
- 1. 量少的是①和②,共2篇,不是3篇;问题表中“③②”应改成“①②”。
- 2. ①明确批评了面量和青豆,不能说唯一批评是青豆。“看着还可以吧”指加料后的外观,不足以断定整体正面。
- 3. “作为一餐是否足够”是我们提出的研究问题,不是作者逐字说的场景。自加挂面、蛋、菜的实际动机待验证。
- 4. 最新一篇为2024年10月10日,距2026年11月2日已超过2年;“近两年没有样本”成立,“快2年”应改为“超过2年”。
- 5. 留水建议属于作者改善口感的经验,不推出普遍嫌干或普遍不嫌干。已有的尝鲜、元旦前购买等原话可保留,未明示的人群和用餐场景保持未知。
- 下一步只导入4份完整正文及4条主引文,不创建产品或分类,不把燃魂与香辣合成SKU。来源为公开用户自述,购买、真实身份及商业关系未认证。发表时间只保留页面原字符串,时区未知;本次取得时间使用原采集日志,不混淆两者。
- 固定输入、原件哈希与本地隔离预检完成后交运维准备,再由王刚001真实消息任务执行。入库后独立回读通过,才准备S2保存模板;S1第2版G8IrkkDG7X保持原样。
- '''
- retain(DOCS / '19-消费者原话草稿核对结果.txt', review_text.encode('utf-8'))
- return inputs, review, review_text
- def package(inputs, review, review_text):
- verification = ROOT / 'work/consumer-quote-inputs-verified-20261102.json'
- local = json.loads(verification.read_text('utf-8'))
- assert local['ok'] and local['productionBusinessWrites'] == 0 and len(local['checks']) == 4
- assert local['inputHashes'] == review['inputHashes']
- ops = f'''运维001:准备4篇消费者原话入库。只落地固定输入和留存目录,不代跑业务。
- 包:ksf-consumer-quotes-import-20261102.zip。先核FILES.sha256全部项。inputs下4份cq-01至cq-04.input.json原样放到{REMOTE_INPUTS}/,root:root 0644、hermes可读。沿用现有ksf-secure.mjs,不安装新执行器,不改Skill/Function/schema,不重启。
- 包内archives/consumer-quotes-codex-20261102.zip为原采集包原字节,1287263B,SHA-256={ARCHIVE_SHA}。保存到{REMOTE_ARCHIVE},hermes:hermes 0600,父目录0700。若该路径已有同字节原包,核对后直接复用;已有不同文件则保留现场并停止,不覆盖、不重打包。
- 王刚001已回报解压原件目录/opt/data/ksf-workshop-private/consumer-quotes-codex-20261102/;若存在,核其29项FILES.sha256与原包一致后复用,不覆盖。若不存在,用已核原包在全新目录解压,目录0700、文件0600。不得将既有0条采集目录改写或清空。
- 只读核对固定任务键{TASK}的历史、本地回执与在途,并按输入4个sourceUrl、source.rawText内容哈希及4个externalKey查服务端。已有相同资料可复用;如发现该任务已执行或在途,回报实际编号并暂停,不换键重跑。
- 以hermes(10000)、HOME=/opt/data准备留存父目录及{REMOTE_STATE},0700。目标若已存在,不清空或换目录,先核历史。本批只计划KfSource+KfEvidence,不需要新身份文件。
- 回报:4份输入哈希/权限、同一原包实际路径/字节/完整哈希及hermes实读、原件清单核对、历史/在途与留存目录状态。没有执行workshop-import就是预期。准备完成后才交王刚001按包内执行单入库一次;无需立即保存S2。
- '''
- wang = f'''王刚001:运维准备完成后,在一条新的真实消息任务里,使用本条已核实的现有agentProfileKey,通过现有secure入口导入4份固定消费者原话输入。你不需要重新设计或修改内容。
- 草稿修正:量少为①②共2篇;①同时批评面量和青豆;“作为一餐”是研究问题;最新帖距今超过2年。完整核对见包内草稿核对结果.txt。
- 固定任务键{TASK}。沿用身份,不登记新Agent或员工。先查本任务无既有执行回执、无在途;若有则只回报,不重放。以hermes(10000)、HOME=/opt/data执行。
- 按cq-01、cq-02、cq-03、cq-04顺序逐份执行。前一份导入及来源/证据精确回读都通过后才做下一份。下面以cq-01为例,其余只将命令和回执前缀中的cq-01替换为对应cq-02/03/04;不改输入、不换任务键。
- 1. 只读查重:
- node /opt/data/skills/ksf-workshop-runtime/scripts/ksf-secure.mjs workshop-dedup --input {REMOTE_INPUTS}/cq-01.input.json --request-id {TASK}:cq-01:preflight:v1
- 新来源no-exact-match、条目deferred/SOURCE_NOT_REGISTERED是正常组合;来源unchanged和条目reuse也是可接受组合。先确认外层ok和entryResult.ok都为true,只有以上可解释结果才继续,其他异常停止回报。
- 2. 导入一次:
- node /opt/data/skills/ksf-workshop-runtime/scripts/ksf-secure.mjs workshop-import --agent-profile <本条已核实agentProfileKey> --input {REMOTE_INPUTS}/cq-01.input.json --request-id {TASK}:cq-01:import:v1
- 核外层ok和entryResult.ok均为true,取entryResult.result的sourceId、sourceOperation、items[0].objectId、items[0].jobId、operation及counts。不以退出码替代两层回执核对。
- 3. 分别用真实返回编号独占写两份只读输入(0600),格式为{{"className":"KfSource","objectId":"<实际sourceId>"}}和{{"className":"KfEvidence","objectId":"<实际evidenceId>"}},然后:
- node /opt/data/skills/ksf-workshop-runtime/scripts/ksf-secure.mjs workshop-read --input <来源回读输入绝对路径> --request-id {TASK}:cq-01:read-source:v1
- node /opt/data/skills/ksf-workshop-runtime/scripts/ksf-secure.mjs workshop-read --input <证据回读输入绝对路径> --request-id {TASK}:cq-01:read-evidence:v1
- 核对来源输入各字段逐项一致:social/live/smzdm、标题、URL、完整正文、collectedAt;证据输入各字段逐项一致:quote、locator、authorLabel、parentObjectId、evidenceKind=public_user_experience_self_report、verificationState=archived_text_and_screenshot_checked、quoteForm=verbatim_excerpt、publishedAt=null和实际collectedAt。sourceRef指回本次来源,productRef为空。locator按Unicode码点切片等于quote,原包/正文/截图哈希与实际文件吻合。发表时间保留locator.publishedAtReported原字符串,时区未知;不得用采集时间替代发表时间。不能只看“读接口成功”就算内容通过。
- 两份回读均通过后再处理下一份。所有命令各执行一次。输入快照、只读输入、stdout/stderr、退出码和两层回执独占留存到{REMOTE_STATE}/,0600,不覆盖既有文件。查重、写入、回读任何一步失败即停,保留原requestId和已取得编号;不自动重试/换键/删记录,错误不代表零写入。
- 成功回报4篇各自sourceId、evidenceId、jobId、sourceOperation、created/unchanged、import两层invocationId及逐项回读。可为unchanged,不为凑新增改键。collectionPerformedBy为Codex,实际导入执行者为你,不声称你刚刚重新抓取了平台。
- 本次只做原话资料入库:4来源、每来源1条主引文;不建KfCategory/KfProduct,不保存S2,不生成机会卡/项目,不改S1 G8IrkkDG7X,不重跑旧K轮或旧导入。3篇燃魂、1篇香辣不合SKU,均不挂到-KbX0Agka-。不称已验证购买、真实身份或普遍需求,也不据历史帖子认定当前商品价格/规格/库存。
- '''
- retain(DOCS / '20-发给运维001-消费者原话入库准备.txt', ops.encode('utf-8'))
- retain(DOCS / '21-发给王刚001-消费者原话入库.txt', wang.encode('utf-8'))
- blobs = {'inputs/' + name: encode(value) for name, value in inputs.items()}
- blobs.update({
- 'archives/consumer-quotes-codex-20261102.zip': ARCHIVE.read_bytes(),
- 'draft-review.json': encode(review),
- '草稿核对结果.txt': review_text.encode('utf-8'),
- '给运维001.txt': ops.encode('utf-8'), '给王刚001.txt': wang.encode('utf-8'),
- 'local-verification.json': verification.read_bytes(),
- })
- history_file = ROOT / 'work/consumer-quote-import-history-20261102.json'
- if history_file.exists():
- blobs['readonly-history.json'] = history_file.read_bytes()
- blobs['FILES.sha256'] = ''.join(sha(b) + ' ' + n + '\n' for n, b in blobs.items()).encode('utf-8')
- if PACKAGE.exists():
- with zipfile.ZipFile(PACKAGE) as z:
- assert set(z.namelist()) == set(blobs)
- for n, b in blobs.items():
- assert z.read(n) == b
- else:
- with zipfile.ZipFile(PACKAGE, 'x', compression=zipfile.ZIP_DEFLATED) as z:
- for name, blob in blobs.items():
- z.writestr(name, blob)
- with zipfile.ZipFile(PACKAGE) as z:
- assert z.testzip() is None
- for line in z.read('FILES.sha256').decode('utf-8').splitlines():
- expected, name = line.split(' ', 1)
- assert sha(z.read(name)) == expected
- receipt = {'ok': True, 'operation': 'consumer-quote-inputs-packaged',
- 'taskKey': TASK, 'file': str(PACKAGE), 'bytes': PACKAGE.stat().st_size,
- 'sha256': sha(PACKAGE.read_bytes()), 'manifestChecked': len(blobs) - 1,
- 'sourcesPlanned': 4, 'evidencePlanned': 4, 'productsPlanned': 0,
- 'reportsPlanned': 0, 'targetExecuted': False, 'businessWrites': 0,
- 'originalArchiveSha256': ARCHIVE_SHA, 'inputHashes': review['inputHashes']}
- retain(ROOT / 'work/consumer-quote-import-package-receipt-20261102.json', encode(receipt))
- return receipt
- if __name__ == '__main__':
- sys.stdout.reconfigure(encoding='utf-8')
- prepared = prepare()
- if '--package' in sys.argv:
- print(json.dumps(package(*prepared), ensure_ascii=False, indent=2))
- else:
- print(json.dumps({'ok': True, 'operation': 'consumer-quote-inputs-prepared',
- 'taskKey': TASK, 'inputHashes': prepared[1]['inputHashes'],
- 'businessWrites': 0}, ensure_ascii=False, indent=2))
|