prepare-live-S2.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. """Prepare a source-backed S2 draft and delivery package; never saves production data."""
  2. from pathlib import Path
  3. import hashlib
  4. import json
  5. import sys
  6. import zipfile
  7. ROOT = Path(__file__).resolve().parents[1]
  8. WORK = ROOT / 'work'
  9. DOCS = Path(r'F:\文档\工作坊当前资料')
  10. TEMPLATE = WORK / 'first-live-S2-template.json'
  11. TASK_KEY = 'KSF-真实资料-20261102-01:S2'
  12. SESSION = 'btrFqkz0yw'
  13. REQUEST_ID = TASK_KEY + ':save:v1'
  14. REMOTE_TEMPLATE = '/opt/data/ksf-executor-20261102/first-live-S2/S2-save.template.json'
  15. REMOTE_INPUT = '/opt/data/ksf-workshop-private/S2-live-save-01.json'
  16. REMOTE_STATE = '/opt/data/ksf-workshop-private/S2-live-01'
  17. sha = lambda b: hashlib.sha256(b).hexdigest()
  18. encode = lambda value: (json.dumps(value, ensure_ascii=False, indent=2) + '\n').encode('utf-8')
  19. def retain(path, blob):
  20. if path.exists():
  21. assert path.read_bytes() == blob, f'Preserve different existing file: {path}'
  22. else:
  23. path.parent.mkdir(parents=True, exist_ok=True)
  24. with path.open('xb') as f:
  25. f.write(blob)
  26. def prepare():
  27. verified = json.loads((WORK / 'consumer-quotes-readback-20261102-complete.json').read_text('utf-8'))
  28. assert verified['ok'] and verified['cq04EvidenceIndependentlyVerified']
  29. assert verified['sources'] == verified['evidence'] == 4 and verified['businessWrites'] == 0
  30. collection = json.loads((WORK / 'consumer-research-codex-20261102/delivery/consumer-quotes.json').read_text('utf-8'))
  31. by_key = {r['id']: r for r in collection['records']}
  32. imported = verified['imports']
  33. rows = []
  34. evidence = {}
  35. claims = {
  36. 'cq-01': '仅记录香辣拌面作者留水改善口感的个人经验;不证明普遍嫌干,也不推广到燃魂拌面。',
  37. 'cq-02': '仅记录燃魂拌面作者购前未注意煮制要求;同段肯定简单和口感,不证明普遍误导。',
  38. 'cq-03': '仅记录燃魂拌面作者认为面量不够并自行加面;份量问题和自加动机需补充验证。',
  39. 'cq-04': '仅记录燃魂拌面作者肯定味道并指出面量不大、需要煮;不代表市场普遍需求。',
  40. }
  41. for ids in imported:
  42. key = ids['key']
  43. original = by_key[key]
  44. input_task = json.loads((WORK / 'consumer-quote-inputs-20261102' / (key + '.input.json')).read_text('utf-8'))
  45. assert original['quote'] == input_task['records'][0]['data']['quote']
  46. assert original['completeMainBody'] == input_task['source']['rawText']
  47. rows.append([key, '香辣拌面;系列/SKU未核实' if key == 'cq-01' else '燃魂拌面;SKU未核实',
  48. original['authorDisplay'] + ';' + original['publishedAtReported'],
  49. original['quote'], original['context']])
  50. evidence['consumer-' + key] = {
  51. 'sourceId': ids['sourceId'], 'claim': claims[key],
  52. 'quotes': [{'text': original['quote'],
  53. 'ref': original['sourceUrl'] + ';页面发表时间=' + original['publishedAtReported']
  54. + '(时区未知);2026-11-02采集;evidenceId=' + ids['evidenceId']}],
  55. }
  56. questions = [
  57. '能否补充近两年、不同平台的具体使用体验,并确认涉及的产品是否已换版?现有4篇最新为2024-10-10。',
  58. '对明确到规格的燃魂拌面,受访者实际在什么场景吃、期待多少份量?自行加面或配料是习惯还是为补足食量?',
  59. '用户购买前能否辨认燃魂拌面的煮制要求?当前包装和商品页面如何说明?不把1篇购前未注意推为普遍误导。',
  60. '对同一款香辣拌面,在可比条件下留水量与拌匀、干燥口感有什么关系?不将其混同燃魂拌面的煮制过程。',
  61. '对青豆口感的单例不满,是否有更多正反体验、具体产品版本与配料信息支持进一步研究?',
  62. '目标人群、具体用餐场景、问题频率、影响程度与支付意愿是什么?需补访谈或问卷,不凭小样本给KANO分档。',
  63. ]
  64. report = {
  65. 'subtitle': '4篇历史公开用户自述;用于提出问题,尚未验证市场需求',
  66. 'method': '按可访问且有具体使用体验的原帖取便利样本;原话、上下文、出处和截图核对后入库。',
  67. 'sources': '什么值得买4篇、4个不同显示账号;发表时间2023—2024年,实际采集时间2026-11-02。',
  68. 'executive': [
  69. '已保存4篇公开用户使用体验及4条主引文,来自同一平台。购买记录、作者真实身份与商业关系未独立认证,不能据此估计人群比例。',
  70. '3篇涉及燃魂拌面,1篇涉及香辣拌面,精确SKU及与当前商品的匹配未知,不合并为同一款产品。',
  71. '样本内2篇明确说量少,3篇提及需要煮,其中仅1篇说购买前没注意;另1篇提出留少量水改善口感。这些是个案描述,不是已验证的普遍痛点。',
  72. '最新一篇发表于2024-10-10,距2026-11-02已超过2年。人群特征、具体用餐场景、需求强度、支付意愿和当前商品参数仍待补。',
  73. ],
  74. 'chapters': [
  75. {'id': 's2-original-experiences-v1', 'title': '用户实际说了什么', 'stage': 'S2', 'sections': [
  76. {'kind': 'para', 'title': '阅读范围', 'text': '只陈述这4篇里的话,保留正负上下文。帖子的旧价格和克重属于作者历史自述,不当作当前参数。原文“添加剂”字词原样保留;“作为一餐是否足够”是研究问题,不是作者逐字场景。'},
  77. {'kind': 'table', 'title': '4篇主引文与上下文', 'data': {'cols': ['条目', '产品范围', '显示作者与发表时间', '逐字原话', '必要上下文'], 'rows': rows}},
  78. ]},
  79. {'id': 's2-questions-v1', 'title': '值得继续核实的问题', 'stage': 'S2', 'sections': [
  80. {'kind': 'table', 'title': '从原话到问题', 'data': {
  81. 'cols': ['问题', '依据', '目前不能认定'],
  82. 'rows': [
  83. ['燃魂拌面的煮制要求在购买前是否清楚?', 'cq-02购前未注意;cq-03、cq-04也提到需要煮。', '不能认定消费者普遍被误导,cq-02同时肯定操作简单、Q弹爽口。'],
  84. ['份量是否满足具体用餐需求?', 'cq-03和cq-04共2篇说量少;出现自加面或食材。', '不认定普遍要求加量;自加动机、用餐场景和具体规格未知。'],
  85. ['香辣拌面留水量是否影响干燥口感?', 'cq-01建议沥水时留少量水,并说香辣味好吃。', '不认定普遍嫌干,也不把经验推广到燃魂或全部康师傅拌面。'],
  86. ['青豆口感是否值得进一步验证?', 'cq-03完整来源正文对青豆有单例不满,同时明确批评面量。', '只有1篇提及,列为低证据线索;不认定普遍配料缺陷。'],
  87. ],
  88. }},
  89. {'kind': 'para', 'title': '保留已有的具体叙述', 'text': '原帖出现吃腻泡面后尝鲜、元旦前购买、自加挂面和配菜等自述。可以保留这些个案信息;不能据此补写学生宿舍、加班夜宵等人群与场景。'},
  90. ]},
  91. {'id': 's2-unknowns-v1', 'title': '尚未知道什么、接下来补什么', 'stage': 'S2', 'sections': [
  92. {'kind': 'table', 'title': '关键未知项', 'data': {
  93. 'cols': ['项目', '当前状态'],
  94. 'rows': [
  95. ['年龄、性别、城市及家庭结构', '未知;账号显示名不等于已核实的真实消费者身份。'],
  96. ['具体用餐时间、地点、目的与自加动机', '未明确部分保持未知,不从购买日期推出食用场景。'],
  97. ['需求频率、强度、支付意愿与KANO分档', '未知;未做访谈、代表性调查或KANO问卷。'],
  98. ['当前商品匹配、规格、库存及可比单价', '未知;不与苏宁、华姐商品或已有牛肉主线产品记录硬关联。'],
  99. ],
  100. }},
  101. {'kind': 'table', 'title': '待补问题', 'data': {'cols': ['顺序', '问题', '状态'], 'rows': [[str(i + 1), q, '待验证'] for i, q in enumerate(questions)]}},
  102. {'kind': 'para', 'title': '本阶段边界', 'text': '本小结记录历史个案与待验证问题。资料取得模式为live,但不意味着内容新近或需求已经验证;尚未生成机会卡、建立项目、开展企业正式审批或发布正式报告。S1第2版作为既有阶段记录保留。'},
  103. ]},
  104. ],
  105. 'evidence': evidence,
  106. }
  107. task = {'sessionId': None, 'taskKey': TASK_KEY, 'version': 1, 'stageCode': 'S2',
  108. 'title': 'S2用户需求与痛点:4篇历史使用体验与待验证问题',
  109. 'artifactType': 'pain_list', 'category': '拌面',
  110. 'sourceIds': [r['sourceId'] for r in imported], 'pendingQuestions': questions, 'report': report}
  111. paragraphs = [task['title'], *report['executive']]
  112. for chapter in report['chapters']:
  113. paragraphs.append(chapter['title'])
  114. for section in chapter['sections']:
  115. paragraphs.append(section['title'])
  116. if section['kind'] == 'para':
  117. paragraphs.append(section['text'])
  118. else:
  119. paragraphs.append(' | '.join(section['data']['cols']))
  120. paragraphs.extend(' | '.join(row) for row in section['data']['rows'])
  121. for entry in evidence.values():
  122. paragraphs.append(entry['claim'])
  123. paragraphs.extend(q['text'] + '\n' + q['ref'] for q in entry['quotes'])
  124. task['rawText'] = '\n\n'.join(paragraphs)
  125. retain(TEMPLATE, encode(task))
  126. review = '''# S2小结审阅稿
  127. 拟保存:S2用户需求与痛点,第1版,沿用现有live会话。当前只完成模板准备,尚未保存。
  128. 已经知道:4篇什么值得买历史使用体验已入库,并由Codex独立核对通过。3篇涉及燃魂拌面,1篇涉及香辣拌面;样本内2篇说量少,3篇提及需要煮,其中1篇说购前没注意;1篇给出留水改善口感的经验。青豆不满只有单例,保留为低证据线索。
  129. 这些是个案。保留味道不错、操作简单等正面上下文,不把“看着还可以吧”当作整款产品的总体评价。自加面和配菜的动机、具体用餐场景仍未知,“一餐够不够”只是待验证问题。
  130. 仍不知道:更近期及不同平台的体验、当前商品版本和SKU、人群特征、需求频率与强度、支付意愿。原帖发表于2023—2024年,最新一篇已超过2年;实际采集时间为2026年11月2日,二者分开。没有已购或真实身份认证,也没有KANO问卷结果。
  131. 本次保存将引用已入库4来源、4条主引文,形成可查看的课堂过程小结。保留S1第2版,不重导资料,不生成机会卡、不建项目,不代表市场需求已验证或正式报告已发布。保存成功后再独立核对后台和电脑/手机页面。
  132. '''
  133. retain(WORK / 'first-live-S2-review.md', review.encode('utf-8'))
  134. retain(DOCS / 'S2小结第1版-审阅稿.md', review.encode('utf-8'))
  135. return task, verified, review
  136. def package(task, verified, review):
  137. check_path = WORK / 'first-live-S2-local-check.json'
  138. local = json.loads(check_path.read_text('utf-8'))
  139. assert local['ok'] and local['productionBusinessWrites'] == 0
  140. assert local['templateSha256'] == sha(TEMPLATE.read_bytes())
  141. ops = f'''运维001:准备真实S2小结第1版,只装模板和只读核查,不代跑保存。
  142. 包:ksf-live-S2-20261102.zip。核FILES.sha256全部项;将S2-save.template.json原样安装到{REMOTE_TEMPLATE},root:root 0644、hermes可读。模板sessionId=null,不可直接提交。沿用现有secure入口,不改Skill/Function/schema,不重启。
  143. 沿用现有live会话{SESSION},workshopKey=KSF-真实资料-20261102-01,已有S1报告G8IrkkDG7X。只读核对当前Agent、员工与真实会话归属一致,不猜账号、不新登记身份;确认4来源2mzBadmvws/Pi04eTwTQ3/m1OQx9aynA/QdeMcyTTjX及4证据FO47R0yR8D/rD8XZgV1Xo/_9mUml0wmh/2bFGP7axMo存在且live。Codex已完成独立回读,包括此前未完成的cq-04,禁止重导任何一份。
  144. 检查报告系列{TASK_KEY}、version=1、requestId={REQUEST_ID}的服务端历史/本地回执/在途。若已有任何执行或结果不明先回读报告,不重放、不换键。本地拟生成输入{REMOTE_INPUT}及S2回执文件均须原本不存在;如已有则停下核查。
  145. 以hermes(10000)、HOME=/opt/data建{REMOTE_STATE},0700;目标已存在则不清空或换目录,先核状态。私有保存输入由王刚001独占新建为0600,本轮运维不生成可执行保存输入、不代跑业务。
  146. 回报模板哈希/权限、身份会话归属、源证据及S2历史核对、留存目录及在途状态。所有旧S1、消费者原话及0条采集回执保留。原cq-04读失败回执保留,无需补跑原命令。随后交王刚001按包内保存单执行一次。
  147. '''
  148. wang = f'''王刚001:4份消费者原话及导入回执已经由Codex独立核对通过,含cq-04证据2bFGP7axMo。原失败回执保留,不再重试或重导入。
  149. 本轮在新的真实消息任务保存S2第1版,沿用live会话{SESSION}。先使用现有无参数ksfregistercurrent_employee工具取得本条真实身份回执,确认当前Agent及员工与既有会话、S1 G8IrkkDG7X归属一致。不创建新会话,不用旧私有身份文件冒充当前消息。
  150. 运维准备核查通过后,固定taskKey={TASK_KEY},version=1,stageCode=S2,artifactType=pain_list,requestId={REQUEST_ID}。若已有该系列版本、requestId回执或在途,停止并回读,不换键重放。
  151. 以hermes(10000)、HOME=/opt/data读{REMOTE_TEMPLATE}到内存,仅填sessionId={SESSION},其余字段逐项不改;差异必须只有sessionId。独占新建{REMOTE_INPUT},hermes/0600,已存在就停,不覆盖,不打印完整输入。
  152. 执行一次:
  153. node /opt/data/skills/ksf-workshop-runtime/scripts/ksf-secure.mjs workshop-save --agent-profile <本条已核实的agentProfileKey> --input {REMOTE_INPUT} --request-id {REQUEST_ID}
  154. stdout/stderr/退出码以S2-v1-save前缀独占留存到{REMOTE_STATE}/,0600。核外层ok和entryResult.ok均为true后,取entryResult.result的sessionId/artifactId/sourceId。失败、超时或不确定立即停,保留原requestId和所有编号,不自动重试/换键/删记录;错误不等于零写入。
  155. 成功后用实际新artifactId独占写0600只读输入{REMOTE_STATE}/S2-v1-read.json,内容为{{"className":"KfArtifact","objectId":"<实际artifactId>"}},执行:
  156. 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
  157. 核两层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。
  158. 回报sessionId、artifactId、生成正文sourceId、reportUrl、版本/阶段/dataMode、boardVisible、formalReportPublished、employeeConfirmed、两层invocationId及精确回读。未打开浏览器则browserVerified=false,页面由Codex后续独立验收。
  159. 本次不重导资料、不建产品/分类/机会卡/项目,不改S1或旧样例。S2是4篇历史个案与待验证问题;2篇量少,1篇购前未注意煮制,1篇留水经验,不据此宣布普遍需求或市场机会。保存可能使会话显示当前S2,并生成本小结正文来源,这是本次正常结果,不是重新抓取。
  160. '''
  161. closure = '''消费者原话入库:独立核对通过
  162. 4来源、4条主引文及4个导入任务均通过。cq-04证据2bFGP7axMo已由Codex经ksfGetRecord按编号回读,quote、locator、作者、时间及来源关联与固定输入一致。其余3份也已独立对照,原件哈希与字符定位一致;4个任务均succeeded,每来源只有1条对应证据,无重复源或额外产品。
  163. 王刚001保留原cq-04失败回执即可,不必重发原只读命令,更不能重跑导入。原REMOTE_REQUEST_UNCERTAIN的具体原因没有据本次成功回读反推,仍记未查明。Codex本轮只读,业务写入0。
  164. S1第2版G8IrkkDG7X仍与原模板一致,S2尚未保存。下一步准备S2第1版,见23号运维说明与24号保存单。
  165. '''
  166. retain(DOCS / '22-消费者原话入库核对结果.txt', closure.encode('utf-8'))
  167. retain(DOCS / '23-发给运维001-S2第1版准备.txt', ops.encode('utf-8'))
  168. retain(DOCS / '24-发给王刚001-S2第1版保存.txt', wang.encode('utf-8'))
  169. members = {
  170. 'S2-save.template.json': TEMPLATE.read_bytes(), 'S2小结第1版-审阅稿.md': review.encode('utf-8'),
  171. '给运维001.txt': ops.encode('utf-8'), '给王刚001.txt': wang.encode('utf-8'),
  172. 'consumer-quotes-independent-readback.json': encode(verified), 'local-verification.json': check_path.read_bytes(),
  173. }
  174. members['FILES.sha256'] = ''.join(sha(b) + ' ' + name + '\n' for name, b in members.items()).encode('utf-8')
  175. target = DOCS / 'ksf-live-S2-20261102.zip'
  176. assert not target.exists(), 'Preserve existing package'
  177. with zipfile.ZipFile(target, 'x', compression=zipfile.ZIP_DEFLATED) as z:
  178. for name, blob in members.items():
  179. z.writestr(name, blob)
  180. with zipfile.ZipFile(target) as z:
  181. assert z.testzip() is None
  182. for line in z.read('FILES.sha256').decode('utf-8').splitlines():
  183. expected, name = line.split(' ', 1)
  184. assert sha(z.read(name)) == expected
  185. receipt = {'ok': True, 'file': str(target), 'bytes': target.stat().st_size, 'sha256': sha(target.read_bytes()),
  186. 'templateSha256': sha(TEMPLATE.read_bytes()), 'manifestChecked': len(members) - 1,
  187. 'taskKey': TASK_KEY, 'requestId': REQUEST_ID, 'version': 1, 'stageCode': 'S2',
  188. 'sourceIds': task['sourceIds'], 'reportQuotes': 4, 'pendingQuestions': 6,
  189. 'productionBusinessWrites': 0, 'targetSaved': False, 'browserVerified': False}
  190. retain(WORK / 'first-live-S2-package-receipt.json', encode(receipt))
  191. return receipt
  192. if __name__ == '__main__':
  193. sys.stdout.reconfigure(encoding='utf-8')
  194. values = prepare()
  195. if '--package' in sys.argv:
  196. print(json.dumps(package(*values), ensure_ascii=False, indent=2))
  197. else:
  198. print(json.dumps({'ok': True, 'template': str(TEMPLATE), 'sha256': sha(TEMPLATE.read_bytes()),
  199. 'sourceIds': values[0]['sourceIds'], 'reportQuotes': 4,
  200. 'pendingQuestions': 6, 'productionBusinessWrites': 0}, ensure_ascii=False, indent=2))