"""Prepare a source-backed S2 draft and delivery package; never saves production data.""" from pathlib import Path import hashlib import json import sys import zipfile ROOT = Path(__file__).resolve().parents[1] WORK = ROOT / 'work' DOCS = Path(r'F:\文档\工作坊当前资料') TEMPLATE = WORK / 'first-live-S2-template.json' TASK_KEY = 'KSF-真实资料-20261102-01:S2' SESSION = 'btrFqkz0yw' REQUEST_ID = TASK_KEY + ':save:v1' REMOTE_TEMPLATE = '/opt/data/ksf-executor-20261102/first-live-S2/S2-save.template.json' REMOTE_INPUT = '/opt/data/ksf-workshop-private/S2-live-save-01.json' REMOTE_STATE = '/opt/data/ksf-workshop-private/S2-live-01' sha = lambda b: hashlib.sha256(b).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'Preserve different existing file: {path}' else: path.parent.mkdir(parents=True, exist_ok=True) with path.open('xb') as f: f.write(blob) def prepare(): verified = json.loads((WORK / 'consumer-quotes-readback-20261102-complete.json').read_text('utf-8')) assert verified['ok'] and verified['cq04EvidenceIndependentlyVerified'] assert verified['sources'] == verified['evidence'] == 4 and verified['businessWrites'] == 0 collection = json.loads((WORK / 'consumer-research-codex-20261102/delivery/consumer-quotes.json').read_text('utf-8')) by_key = {r['id']: r for r in collection['records']} imported = verified['imports'] rows = [] evidence = {} claims = { 'cq-01': '仅记录香辣拌面作者留水改善口感的个人经验;不证明普遍嫌干,也不推广到燃魂拌面。', 'cq-02': '仅记录燃魂拌面作者购前未注意煮制要求;同段肯定简单和口感,不证明普遍误导。', 'cq-03': '仅记录燃魂拌面作者认为面量不够并自行加面;份量问题和自加动机需补充验证。', 'cq-04': '仅记录燃魂拌面作者肯定味道并指出面量不大、需要煮;不代表市场普遍需求。', } for ids in imported: key = ids['key'] original = by_key[key] input_task = json.loads((WORK / 'consumer-quote-inputs-20261102' / (key + '.input.json')).read_text('utf-8')) assert original['quote'] == input_task['records'][0]['data']['quote'] assert original['completeMainBody'] == input_task['source']['rawText'] rows.append([key, '香辣拌面;系列/SKU未核实' if key == 'cq-01' else '燃魂拌面;SKU未核实', original['authorDisplay'] + ';' + original['publishedAtReported'], original['quote'], original['context']]) evidence['consumer-' + key] = { 'sourceId': ids['sourceId'], 'claim': claims[key], 'quotes': [{'text': original['quote'], 'ref': original['sourceUrl'] + ';页面发表时间=' + original['publishedAtReported'] + '(时区未知);2026-11-02采集;evidenceId=' + ids['evidenceId']}], } questions = [ '能否补充近两年、不同平台的具体使用体验,并确认涉及的产品是否已换版?现有4篇最新为2024-10-10。', '对明确到规格的燃魂拌面,受访者实际在什么场景吃、期待多少份量?自行加面或配料是习惯还是为补足食量?', '用户购买前能否辨认燃魂拌面的煮制要求?当前包装和商品页面如何说明?不把1篇购前未注意推为普遍误导。', '对同一款香辣拌面,在可比条件下留水量与拌匀、干燥口感有什么关系?不将其混同燃魂拌面的煮制过程。', '对青豆口感的单例不满,是否有更多正反体验、具体产品版本与配料信息支持进一步研究?', '目标人群、具体用餐场景、问题频率、影响程度与支付意愿是什么?需补访谈或问卷,不凭小样本给KANO分档。', ] report = { 'subtitle': '4篇历史公开用户自述;用于提出问题,尚未验证市场需求', 'method': '按可访问且有具体使用体验的原帖取便利样本;原话、上下文、出处和截图核对后入库。', 'sources': '什么值得买4篇、4个不同显示账号;发表时间2023—2024年,实际采集时间2026-11-02。', 'executive': [ '已保存4篇公开用户使用体验及4条主引文,来自同一平台。购买记录、作者真实身份与商业关系未独立认证,不能据此估计人群比例。', '3篇涉及燃魂拌面,1篇涉及香辣拌面,精确SKU及与当前商品的匹配未知,不合并为同一款产品。', '样本内2篇明确说量少,3篇提及需要煮,其中仅1篇说购买前没注意;另1篇提出留少量水改善口感。这些是个案描述,不是已验证的普遍痛点。', '最新一篇发表于2024-10-10,距2026-11-02已超过2年。人群特征、具体用餐场景、需求强度、支付意愿和当前商品参数仍待补。', ], 'chapters': [ {'id': 's2-original-experiences-v1', 'title': '用户实际说了什么', 'stage': 'S2', 'sections': [ {'kind': 'para', 'title': '阅读范围', 'text': '只陈述这4篇里的话,保留正负上下文。帖子的旧价格和克重属于作者历史自述,不当作当前参数。原文“添加剂”字词原样保留;“作为一餐是否足够”是研究问题,不是作者逐字场景。'}, {'kind': 'table', 'title': '4篇主引文与上下文', 'data': {'cols': ['条目', '产品范围', '显示作者与发表时间', '逐字原话', '必要上下文'], 'rows': rows}}, ]}, {'id': 's2-questions-v1', 'title': '值得继续核实的问题', 'stage': 'S2', 'sections': [ {'kind': 'table', 'title': '从原话到问题', 'data': { 'cols': ['问题', '依据', '目前不能认定'], 'rows': [ ['燃魂拌面的煮制要求在购买前是否清楚?', 'cq-02购前未注意;cq-03、cq-04也提到需要煮。', '不能认定消费者普遍被误导,cq-02同时肯定操作简单、Q弹爽口。'], ['份量是否满足具体用餐需求?', 'cq-03和cq-04共2篇说量少;出现自加面或食材。', '不认定普遍要求加量;自加动机、用餐场景和具体规格未知。'], ['香辣拌面留水量是否影响干燥口感?', 'cq-01建议沥水时留少量水,并说香辣味好吃。', '不认定普遍嫌干,也不把经验推广到燃魂或全部康师傅拌面。'], ['青豆口感是否值得进一步验证?', 'cq-03完整来源正文对青豆有单例不满,同时明确批评面量。', '只有1篇提及,列为低证据线索;不认定普遍配料缺陷。'], ], }}, {'kind': 'para', 'title': '保留已有的具体叙述', 'text': '原帖出现吃腻泡面后尝鲜、元旦前购买、自加挂面和配菜等自述。可以保留这些个案信息;不能据此补写学生宿舍、加班夜宵等人群与场景。'}, ]}, {'id': 's2-unknowns-v1', 'title': '尚未知道什么、接下来补什么', 'stage': 'S2', 'sections': [ {'kind': 'table', 'title': '关键未知项', 'data': { 'cols': ['项目', '当前状态'], 'rows': [ ['年龄、性别、城市及家庭结构', '未知;账号显示名不等于已核实的真实消费者身份。'], ['具体用餐时间、地点、目的与自加动机', '未明确部分保持未知,不从购买日期推出食用场景。'], ['需求频率、强度、支付意愿与KANO分档', '未知;未做访谈、代表性调查或KANO问卷。'], ['当前商品匹配、规格、库存及可比单价', '未知;不与苏宁、华姐商品或已有牛肉主线产品记录硬关联。'], ], }}, {'kind': 'table', 'title': '待补问题', 'data': {'cols': ['顺序', '问题', '状态'], 'rows': [[str(i + 1), q, '待验证'] for i, q in enumerate(questions)]}}, {'kind': 'para', 'title': '本阶段边界', 'text': '本小结记录历史个案与待验证问题。资料取得模式为live,但不意味着内容新近或需求已经验证;尚未生成机会卡、建立项目、开展企业正式审批或发布正式报告。S1第2版作为既有阶段记录保留。'}, ]}, ], 'evidence': evidence, } task = {'sessionId': None, 'taskKey': TASK_KEY, 'version': 1, 'stageCode': 'S2', 'title': 'S2用户需求与痛点:4篇历史使用体验与待验证问题', 'artifactType': 'pain_list', 'category': '拌面', 'sourceIds': [r['sourceId'] for r in imported], 'pendingQuestions': questions, 'report': report} paragraphs = [task['title'], *report['executive']] for chapter in report['chapters']: paragraphs.append(chapter['title']) for section in chapter['sections']: paragraphs.append(section['title']) if section['kind'] == 'para': paragraphs.append(section['text']) else: paragraphs.append(' | '.join(section['data']['cols'])) paragraphs.extend(' | '.join(row) for row in section['data']['rows']) for entry in evidence.values(): paragraphs.append(entry['claim']) paragraphs.extend(q['text'] + '\n' + q['ref'] for q in entry['quotes']) task['rawText'] = '\n\n'.join(paragraphs) retain(TEMPLATE, encode(task)) review = '''# S2小结审阅稿 拟保存:S2用户需求与痛点,第1版,沿用现有live会话。当前只完成模板准备,尚未保存。 已经知道:4篇什么值得买历史使用体验已入库,并由Codex独立核对通过。3篇涉及燃魂拌面,1篇涉及香辣拌面;样本内2篇说量少,3篇提及需要煮,其中1篇说购前没注意;1篇给出留水改善口感的经验。青豆不满只有单例,保留为低证据线索。 这些是个案。保留味道不错、操作简单等正面上下文,不把“看着还可以吧”当作整款产品的总体评价。自加面和配菜的动机、具体用餐场景仍未知,“一餐够不够”只是待验证问题。 仍不知道:更近期及不同平台的体验、当前商品版本和SKU、人群特征、需求频率与强度、支付意愿。原帖发表于2023—2024年,最新一篇已超过2年;实际采集时间为2026年11月2日,二者分开。没有已购或真实身份认证,也没有KANO问卷结果。 本次保存将引用已入库4来源、4条主引文,形成可查看的课堂过程小结。保留S1第2版,不重导资料,不生成机会卡、不建项目,不代表市场需求已验证或正式报告已发布。保存成功后再独立核对后台和电脑/手机页面。 ''' retain(WORK / 'first-live-S2-review.md', review.encode('utf-8')) retain(DOCS / 'S2小结第1版-审阅稿.md', review.encode('utf-8')) return task, verified, review def package(task, verified, review): check_path = WORK / 'first-live-S2-local-check.json' local = json.loads(check_path.read_text('utf-8')) assert local['ok'] and local['productionBusinessWrites'] == 0 assert local['templateSha256'] == sha(TEMPLATE.read_bytes()) ops = f'''运维001:准备真实S2小结第1版,只装模板和只读核查,不代跑保存。 包:ksf-live-S2-20261102.zip。核FILES.sha256全部项;将S2-save.template.json原样安装到{REMOTE_TEMPLATE},root:root 0644、hermes可读。模板sessionId=null,不可直接提交。沿用现有secure入口,不改Skill/Function/schema,不重启。 沿用现有live会话{SESSION},workshopKey=KSF-真实资料-20261102-01,已有S1报告G8IrkkDG7X。只读核对当前Agent、员工与真实会话归属一致,不猜账号、不新登记身份;确认4来源2mzBadmvws/Pi04eTwTQ3/m1OQx9aynA/QdeMcyTTjX及4证据FO47R0yR8D/rD8XZgV1Xo/_9mUml0wmh/2bFGP7axMo存在且live。Codex已完成独立回读,包括此前未完成的cq-04,禁止重导任何一份。 检查报告系列{TASK_KEY}、version=1、requestId={REQUEST_ID}的服务端历史/本地回执/在途。若已有任何执行或结果不明先回读报告,不重放、不换键。本地拟生成输入{REMOTE_INPUT}及S2回执文件均须原本不存在;如已有则停下核查。 以hermes(10000)、HOME=/opt/data建{REMOTE_STATE},0700;目标已存在则不清空或换目录,先核状态。私有保存输入由王刚001独占新建为0600,本轮运维不生成可执行保存输入、不代跑业务。 回报模板哈希/权限、身份会话归属、源证据及S2历史核对、留存目录及在途状态。所有旧S1、消费者原话及0条采集回执保留。原cq-04读失败回执保留,无需补跑原命令。随后交王刚001按包内保存单执行一次。 ''' wang = f'''王刚001:4份消费者原话及导入回执已经由Codex独立核对通过,含cq-04证据2bFGP7axMo。原失败回执保留,不再重试或重导入。 本轮在新的真实消息任务保存S2第1版,沿用live会话{SESSION}。先使用现有无参数ksfregistercurrent_employee工具取得本条真实身份回执,确认当前Agent及员工与既有会话、S1 G8IrkkDG7X归属一致。不创建新会话,不用旧私有身份文件冒充当前消息。 运维准备核查通过后,固定taskKey={TASK_KEY},version=1,stageCode=S2,artifactType=pain_list,requestId={REQUEST_ID}。若已有该系列版本、requestId回执或在途,停止并回读,不换键重放。 以hermes(10000)、HOME=/opt/data读{REMOTE_TEMPLATE}到内存,仅填sessionId={SESSION},其余字段逐项不改;差异必须只有sessionId。独占新建{REMOTE_INPUT},hermes/0600,已存在就停,不覆盖,不打印完整输入。 执行一次: node /opt/data/skills/ksf-workshop-runtime/scripts/ksf-secure.mjs workshop-save --agent-profile <本条已核实的agentProfileKey> --input {REMOTE_INPUT} --request-id {REQUEST_ID} stdout/stderr/退出码以S2-v1-save前缀独占留存到{REMOTE_STATE}/,0600。核外层ok和entryResult.ok均为true后,取entryResult.result的sessionId/artifactId/sourceId。失败、超时或不确定立即停,保留原requestId和所有编号,不自动重试/换键/删记录;错误不等于零写入。 成功后用实际新artifactId独占写0600只读输入{REMOTE_STATE}/S2-v1-read.json,内容为{{"className":"KfArtifact","objectId":"<实际artifactId>"}},执行: node /opt/data/skills/ksf-workshop-runtime/scripts/ksf-secure.mjs workshop-read --input {REMOTE_STATE}/S2-v1-read.json --request-id {TASK_KEY}:read:v1 核两层ok,以及version=1、stageCode=S2、dataMode=live、dashboardState=workshop;ownerSession/sessionRef指向{SESSION}。sourceIds含4份已核来源,4段主引文、6条待补问题、完整rawText和report.chapters/evidence与模板逐项相符。服务端会加课堂声明、报告标题和generatedAt,按合同核对;私有meta不可读则交Codex核查,不补造。只读失败也停止并保留回执,交Codex按编号核对,禁止因此再次save。 回报sessionId、artifactId、生成正文sourceId、reportUrl、版本/阶段/dataMode、boardVisible、formalReportPublished、employeeConfirmed、两层invocationId及精确回读。未打开浏览器则browserVerified=false,页面由Codex后续独立验收。 本次不重导资料、不建产品/分类/机会卡/项目,不改S1或旧样例。S2是4篇历史个案与待验证问题;2篇量少,1篇购前未注意煮制,1篇留水经验,不据此宣布普遍需求或市场机会。保存可能使会话显示当前S2,并生成本小结正文来源,这是本次正常结果,不是重新抓取。 ''' closure = '''消费者原话入库:独立核对通过 4来源、4条主引文及4个导入任务均通过。cq-04证据2bFGP7axMo已由Codex经ksfGetRecord按编号回读,quote、locator、作者、时间及来源关联与固定输入一致。其余3份也已独立对照,原件哈希与字符定位一致;4个任务均succeeded,每来源只有1条对应证据,无重复源或额外产品。 王刚001保留原cq-04失败回执即可,不必重发原只读命令,更不能重跑导入。原REMOTE_REQUEST_UNCERTAIN的具体原因没有据本次成功回读反推,仍记未查明。Codex本轮只读,业务写入0。 S1第2版G8IrkkDG7X仍与原模板一致,S2尚未保存。下一步准备S2第1版,见23号运维说明与24号保存单。 ''' retain(DOCS / '22-消费者原话入库核对结果.txt', closure.encode('utf-8')) retain(DOCS / '23-发给运维001-S2第1版准备.txt', ops.encode('utf-8')) retain(DOCS / '24-发给王刚001-S2第1版保存.txt', wang.encode('utf-8')) members = { 'S2-save.template.json': TEMPLATE.read_bytes(), 'S2小结第1版-审阅稿.md': review.encode('utf-8'), '给运维001.txt': ops.encode('utf-8'), '给王刚001.txt': wang.encode('utf-8'), 'consumer-quotes-independent-readback.json': encode(verified), 'local-verification.json': check_path.read_bytes(), } members['FILES.sha256'] = ''.join(sha(b) + ' ' + name + '\n' for name, b in members.items()).encode('utf-8') target = DOCS / 'ksf-live-S2-20261102.zip' assert not target.exists(), 'Preserve existing package' with zipfile.ZipFile(target, 'x', compression=zipfile.ZIP_DEFLATED) as z: for name, blob in members.items(): z.writestr(name, blob) with zipfile.ZipFile(target) 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, 'file': str(target), 'bytes': target.stat().st_size, 'sha256': sha(target.read_bytes()), 'templateSha256': sha(TEMPLATE.read_bytes()), 'manifestChecked': len(members) - 1, 'taskKey': TASK_KEY, 'requestId': REQUEST_ID, 'version': 1, 'stageCode': 'S2', 'sourceIds': task['sourceIds'], 'reportQuotes': 4, 'pendingQuestions': 6, 'productionBusinessWrites': 0, 'targetSaved': False, 'browserVerified': False} retain(WORK / 'first-live-S2-package-receipt.json', encode(receipt)) return receipt if __name__ == '__main__': sys.stdout.reconfigure(encoding='utf-8') values = prepare() if '--package' in sys.argv: print(json.dumps(package(*values), ensure_ascii=False, indent=2)) else: print(json.dumps({'ok': True, 'template': str(TEMPLATE), 'sha256': sha(TEMPLATE.read_bytes()), 'sourceIds': values[0]['sourceIds'], 'reportQuotes': 4, 'pendingQuestions': 6, 'productionBusinessWrites': 0}, ensure_ascii=False, indent=2))