build-workshop-self-run.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. """Render the actual local rehearsal archive; no generated text is a real employee chat."""
  2. import csv
  3. import hashlib
  4. import html
  5. import json
  6. import re
  7. import shutil
  8. from decimal import Decimal
  9. from pathlib import Path
  10. ROOT = Path(__file__).resolve().parents[1]
  11. BASE = ROOT/'docs/workshop-self-run-20261110'
  12. FINAL = ROOT/'docs/workshop-validated-final-20261110'
  13. sha = lambda p: hashlib.sha256(p.read_bytes()).hexdigest()
  14. dump = lambda p,x:p.write_text(json.dumps(x,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
  15. def inline(s):
  16. s = html.escape(s)
  17. s = re.sub(r'`([^`]+)`',r'<code>\1</code>',s)
  18. s = re.sub(r'\[([^\]]+)\]\(([^)]+)\)',r'<a href="\2">\1</a>',s)
  19. return re.sub(r'\*\*([^*]+)\*\*',r'<strong>\1</strong>',s)
  20. def markdown(text):
  21. """The limited Markdown used by these controlled reports: headings, tables, lists, quotes, fences."""
  22. lines=text.splitlines(); result=[];i=0
  23. while i<len(lines):
  24. line=lines[i]
  25. if not line.strip():i+=1;continue
  26. if line.startswith('```'):
  27. buf=[];i+=1
  28. while i<len(lines) and not lines[i].startswith('```'):buf.append(lines[i]);i+=1
  29. result.append('<pre>'+html.escape('\n'.join(buf))+'</pre>');i+=1;continue
  30. m=re.match(r'^(#{1,6}) (.+)',line)
  31. if m:
  32. n=min(len(m[1])+1,6)
  33. key=re.match(r'([EPS]\d{2})[| |]',m[2])
  34. attr=' id="'+key[1]+'"' if key else ''
  35. result.append(f'<h{n}{attr}>'+inline(m[2])+f'</h{n}>');i+=1;continue
  36. if line.startswith('|') and i+1<len(lines) and re.match(r'^\|[\s:|\-]+\|$',lines[i+1]):
  37. cells=lambda l:[inline(c.strip()) for c in l.strip().strip('|').split('|')]
  38. result.append('<div class="table-wrap"><table><thead><tr>'+''.join('<th>'+c+'</th>' for c in cells(line))+'</tr></thead><tbody>');i+=2
  39. while i<len(lines) and lines[i].startswith('|'):
  40. result.append('<tr>'+''.join('<td>'+c+'</td>' for c in cells(lines[i]))+'</tr>');i+=1
  41. result.append('</tbody></table></div>');continue
  42. if re.match(r'^[-*] |^\d+\. ',line):
  43. ordered=bool(re.match(r'^\d+\. ',line)); tag='ol' if ordered else 'ul';result.append('<'+tag+'>')
  44. pattern=r'^\d+\. ' if ordered else r'^[-*] '
  45. while i<len(lines) and re.match(pattern,lines[i]):
  46. result.append('<li>'+inline(re.sub(pattern,'',lines[i]))+'</li>');i+=1
  47. result.append('</'+tag+'>');continue
  48. if line.startswith('> '):
  49. buf=[]
  50. while i<len(lines) and lines[i].startswith('> '):buf.append(lines[i][2:]);i+=1
  51. result.append('<blockquote>'+inline('\n'.join(buf)).replace('\n','<br>')+'</blockquote>');continue
  52. result.append('<p>'+inline(line)+'</p>');i+=1
  53. return '\n'.join(result)
  54. CSS='''*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:#f5f4f1;color:#263530;font:16px/1.85 "Segoe UI","Microsoft YaHei",sans-serif}a{color:#934534;text-underline-offset:3px}a:hover{color:#b52632}button{font:inherit;cursor:pointer}header{border-bottom:1px solid #e0e0d8;padding:26px 34px;background:#fff}header small{letter-spacing:.1em;color:#797e75}h1{font-size:30px;line-height:1.4;margin:8px 0 12px}h2{font-size:24px;line-height:1.5}h3{font-size:21px;margin:26px 0 12px}h4{font-size:18px;margin:23px 0 9px}h5{font-size:16px}p{margin:9px 0}main{max-width:1480px;margin:auto;padding:24px 30px;display:grid;grid-template-columns:240px minmax(0,1fr);gap:28px}nav{position:sticky;top:16px;align-self:start;max-height:95vh;overflow:auto}nav a{display:block;padding:7px 10px;text-decoration:none;font-size:14px;border-radius:5px}nav a:hover{background:#e9e5de}nav b{display:block;color:#7b8179;font-size:12px;padding:17px 10px 5px}.reading{min-width:0}article,.summary,.resources{background:#fff;border:1px solid #e1e0d9;border-radius:12px;padding:26px 30px;margin:0 0 24px;min-width:0;scroll-margin-top:18px}.label{font-size:12px;letter-spacing:.07em;color:#7f867c}.decision{font-size:20px;border-left:4px solid #8a3a2e;padding:10px 18px;background:#faf4ef;margin:16px 0}.notice{font-size:14px;color:#5f6c63}.paths,.cards{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}.cards a{padding:14px;border:1px solid #dce1d7;border-radius:8px;text-decoration:none;background:#f8faf6;font-size:14px}.paths div{padding:12px;border-top:2px solid #b5bdae;background:#f4f6f2;font-size:14px}.table-wrap{max-width:100%;overflow-x:auto;margin:14px 0}table{border-collapse:collapse;width:100%;font-size:14px;line-height:1.8;min-width:560px}td,th{text-align:left;vertical-align:top;padding:11px 13px;border-bottom:1px solid #e2e4dd}th{background:#f2f4ee;font-weight:600}td{min-width:85px}td:first-child{font-weight:500}blockquote{margin:16px 0;padding:10px 18px;background:#f4f7f1;border-left:3px solid #829774;color:#3c513d}code{font:13px/1.8 Consolas,"Microsoft YaHei",monospace;background:#f2f3ef;padding:2px 5px;overflow-wrap:anywhere}pre{white-space:pre-wrap;overflow-wrap:anywhere;background:#f4f5f0;padding:16px;border:1px solid #e1e4da;border-radius:7px;font:14px/1.85 Consolas,"Microsoft YaHei",monospace}details{border-top:1px solid #e2e4dc;padding-top:15px;margin-top:24px}summary{cursor:pointer;color:#805144;font-weight:600}.dialogue{background:#fafaf7;border-radius:8px;padding:15px 18px;margin-top:15px}.tools{display:flex;gap:10px;flex-wrap:wrap;margin:16px 0}.tools a,.tools button,.copy{padding:7px 12px;border:1px solid #d6dbcf;border-radius:6px;background:#fff;text-decoration:none;font-size:13px;color:#535f50}.copy{float:right}ul,ol{padding-left:24px}li{margin:7px 0}.muted{color:#7d847b;font-size:13px}.stage-line{display:flex;gap:8px;flex-wrap:wrap;margin:15px 0}.stage-line span{padding:7px 12px;background:#edf2e8;border-radius:5px;font-size:13px}.source-ref{font-size:.85em;color:#7f4938}footer{font-size:13px;color:#737d70;padding:20px 34px}section,article{overflow-wrap:anywhere}a:focus-visible,button:focus-visible,summary:focus-visible{outline:3px solid #a65f3e;outline-offset:3px}@media(max-width:850px){main{grid-template-columns:1fr;padding:16px;gap:12px}nav{position:static;max-height:230px;border:1px solid #dfe2d7;border-radius:8px;padding:8px;column-count:2;background:#f9faf6}nav b{break-after:avoid}.paths,.cards{grid-template-columns:1fr 1fr}header{padding:22px 20px}article,.summary,.resources{padding:22px 20px}h1{font-size:25px}}@media(max-width:480px){main{padding:10px}header{padding:19px 17px}article,.summary,.resources{padding:19px 16px}.paths{grid-template-columns:1fr}.cards{gap:8px}.cards a{padding:10px;font-size:13px}h2{font-size:22px}h3{font-size:19px}.decision{font-size:18px;padding:8px 12px}table{font-size:14px}pre{font-size:14px;padding:12px}nav a{font-size:12px;padding:5px}body{line-height:1.85}}@media print{nav,.tools,.copy{display:none}main{display:block;padding:0}article,.summary{border:0;break-inside:auto}details{display:block}details>*{display:block}table{min-width:0}body{background:white}header{padding:12px}}'''
  55. def full_prompt(data, s):
  56. return s.get('prompt') or f"{data['common']}\n\n本节要回答:{s['question']}\n\n先复用:{s['inputs']}\n执行细则:{s['skillReference']}\n\n本节任务:\n{s['task']}\n\n本节应交付:\n"+'\n'.join('• '+v for v in s['deliverables'])
  57. def render_run(folder, title, manual, extra=''):
  58. data=json.loads(manual.read_bytes()); chapters=sorted(folder.glob('[0-1][0-9]-*.md'))
  59. assert len(chapters)==15
  60. dialogue=['# '+title+'|完整提示词与自演对话','','记录性质:同一助手分饰研究助手和模拟学员,依据真实只读资料整理的自演记录;不是实际员工聊天、独立模型调用轨迹或真实讲师评审。原始工具请求/时间/响应另见执行证据。','','## 使用版本','',data['release']['contentVersion'],'']
  61. turns=[];nav=[];articles=[];coverage=[]
  62. for s,chapter in zip(data['steps'],chapters):
  63. text=chapter.read_text('utf-8');prompt=full_prompt(data,s)
  64. talk=text.split('## 自演对话\n',1)[1].split('## 实质产出\n',1)[0].strip()
  65. output=text.split('## 实质产出\n',1)[1]
  66. key='s-'+s['id'];nav.append('<a href="#'+key+'">'+html.escape(s['number']+' '+s['title'])+'</a>')
  67. dialogue += ['## '+s['number']+'|'+s['title'],'','### 发起提示词(手册原文)','','```text',prompt,'```','','### 自演对话','',talk,'','### 产出正文','',f'见 [{chapter.name}]({chapter.name})。','',output,'']
  68. turns.append({'stepId':s['id'],'prompt':prompt,'promptSha256':hashlib.sha256(prompt.encode()).hexdigest(),'simulatedDialogue':talk,'outputFile':chapter.name,'outputSha256':sha(chapter),'simulation':True})
  69. refs=sorted(set(re.findall(r'\b[EPS]\d{2}\b',output)))
  70. source_links='<p class="muted">证据快查:'+ ' · '.join('<a href="证据索引.html#'+v+'">'+v+'</a>' for v in refs)+'</p>' if refs else ''
  71. articles.append(f'<article id="{key}" data-step="{s["id"]}"><div class="label">{html.escape(s.get("module","开始之前"))}</div><h2>{html.escape(s["number"]+" · "+s["title"])}</h2>'+markdown(output)+source_links+f'<details open><summary>完整提示词与自演对话 <button class="copy" data-copy="p-{s["id"]}">复制提示词</button></summary><pre id="p-{s["id"]}">'+html.escape(prompt)+'</pre><div class="dialogue">'+markdown(talk)+'</div></details></article>')
  72. coverage.append({'stepId':s['id'],'title':s['title'],'file':chapter.name,'contentPresent':bool(output.strip()),'externalAcceptance':'not-claimed'})
  73. (folder/'完整对话记录.md').write_text('\n'.join(dialogue),encoding='utf-8')
  74. dump(folder/'对话与产出.json',{'notice':'one-assistant self-roleplay; not actual employee dialogue','steps':turns})
  75. dump(folder/'环节覆盖.json',{'contentSteps':coverage,'businessSaved':False,'hostLoadingVerified':False})
  76. seven=[('市场报告','s-tasks'),('社媒场景报告','s-voc'),('用户/竞品证言池','s-voc'),('三源研究卡','s-opportunities'),('论证清单V1–V3','s-feasibility'),('场景买点方案','s-value'),('产品概念提案','s-proposal')]
  77. summary='''<section class="summary" id="overview"><div class="label">本次研究得到什么</div><h2>现品已有人认可便利,先证明值得改的一步</h2><p class="decision">建议先观察现售碗装在目标场景中的实际使用;只有发现可复现的信息理解问题,再比较信息原型。当前依据不支持直接选择新沥水结构。</p><p>课题:宿舍夜宵拌面的体验改良。研究设定为有开水、不用锅,保留酱香与面条口感。真实资料包括7款商品、49条拌面评价和10条定向社媒;业务条件与学员回应为模拟。</p><div class="stage-line"><span>供需与竞品</span><span>证言与反例</span><span>候选全池</span><span>条件取舍</span><span>概念与停止条件</span></div><div class="paths"><div><b>真实执行</b><br>隔离安装、只读资料回读、逐条编码、计算与页面检查。</div><div><b>本地自演</b><br>同一助手扮演学员与研究助手;完整提示词和15节产出可查。</div><div><b>本轮未验证</b><br>真实人员任务/感官测试、宿主自动加载、生产保存及小组归属。</div></div><h3>七类成果直达</h3><div class="cards">'''+''.join('<a data-output="true" href="#'+key+'">'+name+' ↗</a>' for name,key in seven)+'</div></section>'
  78. resources='<section class="resources" id="resources"><h2>完整记录与底表</h2><div class="tools"><a href="完整对话记录.md">完整对话记录</a><a href="证据索引.html">原话与来源</a><a href="评价逐条编码.csv">49条逐条编码 CSV</a><a href="提及率复算.csv">提及率复算 CSV</a><a href="候选机会全池.json">V1/V2/V3全池</a><a href="商业情景复算.csv">商业情景复算</a><a href="执行与核验.json">执行证据索引</a></div><p class="muted">E/P/S与O1–O3均为本地引用键,不是生产业务编号。资料来自固定正式代际;本轮未重哈希服务器原始ZIP,也未观看视频。</p></section>'
  79. script='''document.querySelectorAll('[data-copy]').forEach(b=>b.onclick=async()=>{const t=document.getElementById(b.dataset.copy).textContent;try{await navigator.clipboard.writeText(t);b.textContent='已复制'}catch{const a=document.createElement('textarea');a.value=t;document.body.append(a);a.select();document.execCommand('copy');a.remove();b.textContent='已复制'}});document.getElementById('collapse').onclick=()=>document.querySelectorAll('article details').forEach(d=>d.open=false);document.getElementById('expand').onclick=()=>document.querySelectorAll('article details').forEach(d=>d.open=true);'''
  80. page='<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>'+html.escape(title)+'</title><style>'+CSS+'</style><header><small>康师傅 · 产品创新工作坊 / 本地演练</small><h1>'+html.escape(title)+'</h1><p class="notice">同一助手自演,资料真实,学员回应和业务条件模拟;不是豆包或真实员工验收。</p><div class="tools"><button id="collapse">只看产出</button><button id="expand">展开提示词与对话</button>'+extra+'</div></header><main><nav><a href="#overview">当前判断与成果直达</a>'+''.join(nav)+'<a href="#resources">完整记录与底表</a></nav><div class="reading">'+summary+''.join(articles)+resources+'</div></main><footer>15节内容推演完成与业务端到端验收分别记录;全部文件仅保留本地。</footer><script>'+script+'</script></html>'
  81. (folder/'index.html').write_text(page,encoding='utf-8')
  82. if (folder/'证据索引.md').exists():
  83. evidence_html=markdown((folder/'证据索引.md').read_text('utf-8'))
  84. evidence_html=re.sub(r'<strong>(P\d{2})</strong>',r'<strong id="\1">\1</strong>',evidence_html)
  85. (folder/'证据索引.html').write_text('<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>真实原话与来源</title><style>'+CSS+'main{display:block;max-width:1000px}</style><main><a href="index.html">返回演练</a>'+evidence_html+'</main></html>',encoding='utf-8')
  86. def shared_tables():
  87. pool={}
  88. stages={
  89. 'V1': [('O1','研究线索/待证信息设想','先观察有无问题,有问题才比较引导'),('O2','待证投料设想','缺少真实调整困扰'),('O3','待证结构设想','没有沥水困扰直接依据')],
  90. 'V2': [('O1','保留并收窄','先观察再决定信息改动'),('O2','修改/部分待评','顺序/料量研究保留,分包另核'),('O3','本轮暂缓新开模','模拟约束不新开模,非技术否决')],
  91. 'V3': [('O1','条件式概念讨论','模拟组选择讨论,未获企业批准'),('O2','保留研究线','有相关真实事件才独立验证'),('O3','继续暂缓','条件改变且有问题依据再回评')]}
  92. for version,rs in stages.items():
  93. pool[version]=[{'localKey':k,'status':s,'reason':why,'artifactId':None,'opportunityId':None,'businessSaved':False} for k,s,why in rs]
  94. dump(BASE/'候选机会全池.json',{'topicKey':'LOCAL-REHEARSAL-DORM-20261110','keysAreLocalOnly':True,'snapshots':pool})
  95. scenarios=[]
  96. for key,desc,r,c in [('F0','模拟基准','3.20','2.15'),('F1','信息增量算例','3.20','2.18'),('F2','分包增量算例','3.20','2.25'),('F3','净收入敏感性算例','3.00','2.18'),('F4','净收入敏感性算例','3.40','2.18')]:
  97. R=Decimal(r);C=Decimal(c)
  98. scenarios.append({'key':key,'description':desc,'R':str(R),'C':str(C),'difference':str(R-C),'marginPercent':str((R-C)/R*100),'unit':'人民币元/份,同一假定不含税口径','formula':'difference=R-C; marginPercent=(R-C)/R*100','nature':'纯演算;模拟基准/任意敏感性;无公司报价/预测依据','usedForBusinessRanking':False})
  99. with (BASE/'商业情景复算.csv').open('w',encoding='utf-8-sig',newline='') as f:
  100. w=csv.DictWriter(f,fieldnames=list(scenarios[0]));w.writeheader();w.writerows(scenarios)
  101. dump(BASE/'商业情景复算.json',{'actualCompanyInputs':None,'scenarios':scenarios,'actualTargetMargin':None,'actualThreshold':'Cnew <= Rnew*(1-m), only when actual target m is provided','excludedUnknown':'物流、销售、固定改版、规模、真实出成率','note':'不属于经过财务确认的完整企业利润'})
  102. def archive_evidence():
  103. target=BASE/'evidence';target.mkdir(exist_ok=True)
  104. source=ROOT/'work/workshop-self-run-20261110/evidence'
  105. for p in source.glob('*.json'):shutil.copyfile(p,target/p.name)
  106. # Keep only observed receipts and public responses, not dependencies, credentials or host state.
  107. checks=BASE/'checks';checks.mkdir(exist_ok=True)
  108. for p in (ROOT/'work/workshop-self-run-20261110/checks').glob('*'):
  109. if p.is_file():shutil.copyfile(p,checks/p.name)
  110. shutil.copyfile(ROOT/'work/workshop-self-run-20261110/install-apply/install-receipt.json',checks/'install-receipt.json')
  111. evidence=[]
  112. for p in sorted(target.glob('*.request.json')):
  113. d=json.loads(p.read_bytes()); response=target/(d['name']+'.json')
  114. assert sha(response)==d['responseSha256']
  115. assert d['readOnly'] is True and d['databaseWrites']==0
  116. evidence.append({'receipt':p.relative_to(BASE).as_posix(), 'response':response.relative_to(BASE).as_posix(), 'httpStatus':d['httpStatus'], 'requestedAt':d['requestedAt'], 'responseSha256':d['responseSha256']})
  117. dump(BASE/'执行与核验.json',{'nature':'actual tool receipts, separate from authored self-roleplay','readOnlyApiCalls':len(evidence),'calls':evidence,'businessBridge':'checks/business-bridge.json','installation':'checks/install-receipt.json','directProductPages':'checks/product-page-attempts.json','productionWrites':0,'sourceArchivesRehashed':False})
  118. ledger=json.loads((BASE/'证据与编码.json').read_bytes())
  119. for r in ledger['reviews']:r['responseFile']='evidence/'+Path(r['responseFile']).name
  120. dump(BASE/'证据与编码.json',ledger)
  121. if __name__=='__main__':
  122. shared_tables();archive_evidence()
  123. render_run(BASE,'第一轮|按原手册完整自演',BASE/'manual-source.json','<a href="../workshop-validated-final-20261110/index.html">查看优化后定稿与第二轮 ↗</a>')
  124. print(json.dumps({'ok':True,'chapters':15,'index':str(BASE/'index.html')},ensure_ascii=False))