update_workshop_manual_install_r13.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. """Sync only the current handbook's installation step to a verified publication."""
  2. import hashlib
  3. import json
  4. from pathlib import Path
  5. ROOT = Path(__file__).resolve().parents[1]
  6. OUT = ROOT / 'docs/workshop-quality-r13-20261110'
  7. PUBLICATION = ROOT / 'docs/ksf-workshop-skill-20261110-r13-r2'
  8. KEY_EVIDENCE_FEEDBACK = '''反馈关键分析依据信息:涉及数据判断时,只呈现直接支撑结论或会改变建议的来源、分析对象与时间范围、实际有效样本,并用代表原话、产品对照或关键统计解释依据与判断的关系;比例注明n/N及口径,复用资料与本轮新增采集分开,关键反例和适用限制紧邻结论。未取得的依据如实标明,不补造数字。完整采集明细、逐条处理记录和工具日志按Skill保留供追溯,无需在对话中展开;本节核心报告与成果仍须完整交付。'''
  9. def sha(path):
  10. return hashlib.sha256(path.read_bytes()).hexdigest()
  11. def dump(path, value):
  12. path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + '\n', encoding='utf8')
  13. def configure_install(data):
  14. if data['release'].get('entryMode') == 'fmode-enterprise-agent':
  15. raise ValueError('This handbook uses website onboarding; attendee installation is retired.')
  16. receipt = json.loads((PUBLICATION / 'online-release-receipt.json').read_bytes())
  17. assert receipt['ok'] and receipt['publicResourcesPublished']
  18. assert receipt['workshopVersion'] == 'r13'
  19. archive = receipt['archive']
  20. assert sha(PUBLICATION / 'publish' / archive['path']) == archive['sha256']
  21. prompt = (PUBLICATION / '员工安装正文.txt').read_text('utf8').strip()
  22. assert receipt['guideUrl'] in prompt
  23. data['release'].update(
  24. version='r13', installVersion='r13', guide=receipt['guideUrl'],
  25. installReleaseId=receipt['releaseId'],
  26. deploymentStatus='manual-local-skill-published')
  27. step = next(s for s in data['steps'] if s['id'] == 'install')
  28. step['prompt'] = prompt
  29. step['deliverables'][0] = '工作坊Skill正式r13的安装或核验结果(45个公开文件)'
  30. return receipt
  31. def configure_website_entry(data):
  32. data['release'].update(entryMode='fmode-enterprise-agent', skillManagedBy='organizer')
  33. group = next(g for g in data['groups'] if g['id'] == 'prepare')
  34. group.update(title='进入工作坊,明确本组课题',
  35. description='登录企业网站,在本组独立会话中使用统一的工作坊智能体。')
  36. step = next(s for s in data['steps'] if s['id'] == 'install')
  37. step.clear()
  38. step.update(
  39. id='install', group='prepare', kind='introduction', number='00', title='使用说明',
  40. introduction='本手册采用 fmode 企业网站的统一智能体。工作坊 Skill 和相关工具由组织方统一配置;学员无需安装 Skill,也无需复制安装提示词。',
  41. usage=[
  42. {'title': '登录并进入智能体', 'text': '打开组织方提供的 fmode 企业网站入口,登录后进入指定的工作坊智能体。'},
  43. {'title': '保留本组独立会话', 'text': '各组使用同一个智能体入口,每组保留独立会话。先说明小组名称和研究课题,后续在本组会话中接续,避免混入其他小组的成果。'},
  44. {'title': '从 01 开始逐节推进', 'text': '复制各节提示词发送给智能体,回答必要的业务问题,检查本节产出后再进入下一节。不同小组按各自课题与证据展开研究。'},
  45. {'title': '核对报告与成果', 'text': '每节检查完整报告和关键分析依据;保存后核对成果编号、回读内容和本组归属。若保存未成功,让智能体明确说明。'},
  46. {'title': '确认方案后生成示意图', 'text': '最终方案经本组明确确认后,再由智能体调用 image 技能(image 2.6)生成示意图,并与对应方案一起保存。'}
  47. ],
  48. checks=['已找到组织方指定的工作坊智能体。', '本组使用独立会话,课题和小组名称清楚。'],
  49. prompt='')
  50. for index, item in enumerate(data['steps']):
  51. item['number'] = f'{index:02d}'
  52. def configure_evidence_feedback(data):
  53. if KEY_EVIDENCE_FEEDBACK not in data['common']:
  54. data['common'] += '\n\n' + KEY_EVIDENCE_FEEDBACK
  55. for step in data['steps']:
  56. if step['id'] != 'install' and step.get('prompt') and KEY_EVIDENCE_FEEDBACK not in step['prompt']:
  57. step['prompt'] += '\n\n' + KEY_EVIDENCE_FEEDBACK
  58. def render_manual(data, prompts):
  59. manual = OUT / 'manual'
  60. content = 'window.WORKSHOP_MANUAL_DATA = ' + json.dumps(data, ensure_ascii=False, indent=2) + ';\n'
  61. (manual / 'content.js').write_text(content, encoding='utf8')
  62. js = (manual / 'manual.js').read_text('utf8')
  63. start = js.index(' const promptMap = ')
  64. end = js.index('\n', start)
  65. mapping = {s['id']: s['fullPrompt'] for s in prompts['steps']}
  66. js = js[:start] + ' const promptMap = ' + json.dumps(mapping, ensure_ascii=False) + ';' + js[end:]
  67. (manual / 'manual.js').write_text(js, encoding='utf8')
  68. standalone_js = js.replace('href="../${escape(step.example)}"', 'href="${escape(step.example)}"').replace('href="../参考学习与类型对应.html"', 'href="参考学习与类型对应.html"')
  69. h = (manual / 'index.html').read_text('utf8')
  70. css = (manual / 'manual.css').read_text('utf8')
  71. standalone = h.replace('<link rel="stylesheet" href="manual.css">', '<style>' + css + '</style>').replace('<script src="content.js" defer></script>', '').replace('<script src="manual.js" defer></script>', '')
  72. standalone = standalone.replace('href="../index.html"', 'href="index.html"').replace('href="../案例报告合集.html"', 'href="案例报告合集.html"')
  73. safe = lambda text: text.replace('</script', '<\\/script')
  74. standalone = standalone.replace('</body>', '<script>' + safe(content) + '</script><script>' + safe(standalone_js) + '</script></body>')
  75. html_path = OUT / '康师傅工作坊操作手册.html'
  76. html_path.write_text(standalone, encoding='utf8')
  77. return html_path
  78. def update_key_evidence():
  79. source = OUT / 'manual-source.json'
  80. archive_path = OUT / 'prompts-and-outputs.json'
  81. data = json.loads(source.read_bytes())
  82. prompts = json.loads(archive_path.read_bytes())
  83. before = json.loads(json.dumps(data))
  84. old_common = data['common']
  85. skill = ROOT / 'skills/ksf-workshop-runtime'
  86. skill_hashes = {p.relative_to(skill).as_posix(): sha(p) for p in skill.rglob('*') if p.is_file()}
  87. configure_evidence_feedback(data)
  88. for step, saved in zip(data['steps'], prompts['steps'], strict=True):
  89. assert step['id'] == saved['id']
  90. if step['id'] == 'install':
  91. assert step == before['steps'][0]
  92. assert saved['fullPrompt'] == step['prompt']
  93. continue
  94. if step.get('prompt'):
  95. saved['prompt'] = step['prompt']
  96. saved['fullPrompt'] = step['prompt']
  97. else:
  98. assert saved['fullPrompt'].startswith(old_common)
  99. saved['fullPrompt'] = data['common'] + saved['fullPrompt'][len(old_common):]
  100. assert saved['fullPrompt'].count(KEY_EVIDENCE_FEEDBACK) == 1
  101. for old, new in zip(before['steps'], data['steps'], strict=True):
  102. assert {k: v for k, v in old.items() if k != 'prompt'} == {k: v for k, v in new.items() if k != 'prompt'}
  103. dump(source, data)
  104. dump(archive_path, prompts)
  105. html_path = render_manual(data, prompts)
  106. assert skill_hashes == {p.relative_to(skill).as_posix(): sha(p) for p in skill.rglob('*') if p.is_file()}
  107. manifest_path = OUT / '本地发行清单.json'
  108. manifest = json.loads(manifest_path.read_bytes())
  109. manifest.update(manualSha256=sha(html_path), feedbackRule='key-analysis-evidence')
  110. dump(manifest_path, manifest)
  111. update = dict(ok=True, scope='research-feedback-prompts-only',
  112. feedbackRule=KEY_EVIDENCE_FEEDBACK, researchPromptsUpdated=14,
  113. installPromptUnchanged=True, coreDeliverablesUnchanged=True,
  114. skillFilesUnchanged=len(skill_hashes), manualPublished=False, productionWrites=0)
  115. receipt_path = OUT / '关键分析依据反馈更新核验.json'
  116. dump(receipt_path, update)
  117. validation_path = OUT / '交付文件校验.json'
  118. validation = json.loads(validation_path.read_bytes())
  119. validation['keyEvidenceFeedbackUpdate'] = update
  120. for name in ['manual-source.json', 'prompts-and-outputs.json', 'manual/content.js',
  121. 'manual/manual.js', html_path.name, manifest_path.name, receipt_path.name]:
  122. validation['files'][name] = sha(OUT / name)
  123. dump(validation_path, validation)
  124. print(json.dumps(update, ensure_ascii=False))
  125. def main():
  126. source = OUT / 'manual-source.json'
  127. archive_path = OUT / 'prompts-and-outputs.json'
  128. data = json.loads(source.read_bytes())
  129. prompts = json.loads(archive_path.read_bytes())
  130. before = {s['id']: s for s in prompts['steps'] if s['id'] != 'install'}
  131. before_sha = hashlib.sha256(json.dumps(before, ensure_ascii=False, sort_keys=True).encode()).hexdigest()
  132. receipt = configure_install(data)
  133. install = next(s for s in data['steps'] if s['id'] == 'install')
  134. for i, step in enumerate(prompts['steps']):
  135. if step['id'] == 'install':
  136. prompts['steps'][i] = {**install, 'fullPrompt': install['prompt']}
  137. assert before == {s['id']: s for s in prompts['steps'] if s['id'] != 'install'}
  138. dump(source, data)
  139. dump(archive_path, prompts)
  140. html_path = render_manual(data, prompts)
  141. manifest_path = OUT / '本地发行清单.json'
  142. manifest = json.loads(manifest_path.read_bytes())
  143. manifest.update(manualSha256=sha(html_path), bundledSkillVersion='r13-local-quality',
  144. currentInstallVersion='r13', currentInstallReleaseId=receipt['releaseId'],
  145. currentInstallGuide=receipt['guideUrl'])
  146. dump(manifest_path, manifest)
  147. update = dict(ok=True, scope='installation-step-only', installVersion='r13',
  148. releaseId=receipt['releaseId'], guideUrl=receipt['guideUrl'],
  149. nonInstallStepsUnchanged=len(before), nonInstallStepsSha256=before_sha,
  150. installPromptSha256=hashlib.sha256(install['prompt'].encode()).hexdigest(),
  151. manualPublished=False, productionWrites=0)
  152. dump(OUT / '安装提示词更新核验.json', update)
  153. validation_path = OUT / '交付文件校验.json'
  154. validation = json.loads(validation_path.read_bytes())
  155. validation['installationPromptUpdate'] = update
  156. for name in ['manual-source.json', 'prompts-and-outputs.json', 'manual/content.js',
  157. 'manual/manual.js', html_path.name, manifest_path.name, '安装提示词更新核验.json']:
  158. validation['files'][name] = sha(OUT / name)
  159. dump(validation_path, validation)
  160. print(json.dumps(update, ensure_ascii=False))
  161. if __name__ == '__main__':
  162. import argparse
  163. parser = argparse.ArgumentParser(description=__doc__)
  164. parser.add_argument('--key-evidence', action='store_true')
  165. args = parser.parse_args()
  166. update_key_evidence() if args.key_evidence else main()