| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208 |
- """Organize the existing report site without regenerating research or Skill releases."""
- import hashlib
- import html
- import json
- import re
- import shutil
- from pathlib import Path
- from urllib.parse import unquote, urlsplit
- ROOT = Path(__file__).resolve().parents[1]
- OUT = ROOT / 'docs/workshop-quality-r13-20261110'
- BACKUP = ROOT / 'work/workshop-navigation-20261110/before'
- MANUAL = '康师傅工作坊操作手册.html'
- OLD_MANUAL = '康师傅工作坊操作手册-r13本地修订.html'
- EVIDENCE = '证据底表.html'
- ORDER = ['category', 'rules', 'competitors', 'tasks', 'research', 'accept', 'voc',
- 'matrix', 'opportunities', 'priority', 'feasibility', 'value', 'proposal']
- GROUPS = [('market', '第一个半天'), ('needs', '第二个半天'), ('concept', '第三个半天')]
- e = lambda value: html.escape(str(value), quote=True)
- sha = lambda path: hashlib.sha256(path.read_bytes()).hexdigest()
- def dump(path, value):
- path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + '\n', encoding='utf8')
- def data_block(text, marker):
- start = text.index(marker) + len(marker)
- value, length = json.JSONDecoder().raw_decode(text[start:])
- return value, start, length
- def navigation(steps, base, active):
- result = []
- for group, label in GROUPS:
- result.append(f'<div class="site-nav-label">{label}</div>')
- for step in steps:
- if step['id'] in ORDER and step['group'] == group:
- current = ' aria-current="page"' if step['id'] == active else ''
- result.append(f'<a href="{base}reports/{step["id"]}.html"{current}>'
- f'<span>{step["number"]}</span><span>{e(step["title"])}</span></a>')
- result.append('<div class="secondary-links"><div class="site-nav-label">研究参考</div>')
- for name, label in [('参考学习与类型对应.html', '参考报告与方法'), ('选材与分析规则.html', '选材与分析规则')]:
- current = ' aria-current="page"' if active == name else ''
- result.append(f'<a href="{base}{name}"{current}>{label}</a>')
- return ''.join(result) + '</div>'
- def shell(title, body, steps, active='', base='', intro='', actions='', pager=''):
- section = '报告案例' if active in ORDER else title
- links = []
- for rel, label, selected in [('index.html', '首页', active == 'home'),
- (MANUAL, '操作手册', False),
- ('案例报告合集.html', '报告案例', active in ORDER or active == 'reports')]:
- links.append(f'<a href="{base}{rel}"' + (' aria-current="page"' if selected else '') + f'>{label}</a>')
- links.append('<a href="https://kangshifu.fmode.cn/" target="_blank" rel="noopener">成果看板 ↗</a>')
- breadcrumb = f'<a href="{base}index.html">工作坊首页</a>'
- if active in ORDER:
- breadcrumb += f'<span>/</span><a href="{base}案例报告合集.html">报告案例</a>'
- breadcrumb += f'<span>/</span>{e(title)}' if active != 'home' else ''
- return f'''<!doctype html>
- <html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
- <title>{e(title)} · 康师傅工作坊</title><link rel="stylesheet" href="{base}assets/workshop-reports.css">
- <script src="{base}assets/workshop-reports.js" defer></script></head><body>
- <a class="skip-link" href="#content">跳转到正文</a><div class="site-layout">
- <aside class="site-sidebar"><a class="site-brand" href="{base}index.html"><span class="brand-mark">康</span><span>康师傅<small>产品创新工作坊</small></span></a>
- <details id="site-menu" class="site-menu" open><summary>报告目录 · {e(section)}</summary><nav class="site-navigation" aria-label="报告目录">{navigation(steps, base, active)}</nav></details>
- <div class="sidebar-note">按研究顺序查看案例<br>每篇报告可返回对应任务提示词</div></aside>
- <div class="site-content"><header class="site-topbar"><span>产品创新工作坊</span><nav class="site-links" aria-label="网站导航">{''.join(links)}</nav></header>
- <main id="content" class="reading"><div class="breadcrumb">{breadcrumb}</div><div class="page-heading"><h1>{e(title)}</h1>{f'<p>{intro}</p>' if intro else ''}{f'<div class="page-actions">{actions}</div>' if actions else ''}</div>
- {body}{pager}<footer class="site-footer">康师傅产品创新工作坊 · 案例用于理解研究方法与交付形式;实际结论以各组证据和验证为准。</footer></main></div></div></body></html>'''
- def card(step):
- return f'<a class="card" href="reports/{step["id"]}.html"><small>步骤 {step["number"]}</small><strong>{e(step["title"])}</strong><span>{e(step["question"])}</span></a>'
- def source_item(record, base):
- key = record['key']
- title = record.get('title') or record.get('purchasedSku') or record.get('productId') or record['recordId']
- text = f'<blockquote>{e(record["quote"])}</blockquote>' if record.get('quote') else ''
- if record.get('appendText'):
- text += f'<p>追评:{e(record["appendText"])}</p>'
- date = record.get('dateRaw') or record.get('sourceTime') or '时间未标明'
- note = record.get('reason') or record.get('note') or record.get('qualification') or '商品信息用于本报告对照,具体规格及价格以相应原件为准。'
- text += f'<p class="source-meta">对象:{e(record.get("productId") or record["recordId"])} · 时间:{e(date)}</p><p>{e(note)}</p>'
- source = record.get('source') or {}
- if source.get('asset_path'):
- text += f'<p class="source-meta">原件:{e(source["asset_path"])} · 第 {e(source.get("line_number", "未知"))} 行</p>'
- if record.get('url'):
- text += f'<p><a href="{e(record["url"])}" target="_blank" rel="noopener">原始页面 ↗</a></p>'
- text += f'<p class="source-meta"><a href="{base}data/citations/{key}.json">查看留存记录</a></p>'
- return f'<details class="source-item" id="source-{key}"><summary>{key} · {e(title)}</summary><div class="source-body">{text}</div></details>'
- def inline_citations(body, records, base):
- keys = []
- def replace(match):
- href = html.unescape(match.group(1))
- parsed = urlsplit(href)
- key = parsed.fragment
- if unquote(parsed.path).split('/')[-1] in [EVIDENCE, '证据索引.html']:
- if not key:
- return ''
- assert key in records, key
- if key not in keys:
- keys.append(key)
- return f'<a href="#source-{key}">{match.group(2)}</a>'
- return match.group(0)
- body = re.sub(r'<a\s+href="([^"]+)"[^>]*>(.*?)</a>', replace, body, flags=re.S)
- if keys:
- body += '<section class="report-sources"><h2>本篇引用</h2><p class="muted">点击正文编号可在本页展开对应原文。以下材料的使用范围与限制保持原记录。</p>'
- body += ''.join(source_item(records[k], base) for k in keys) + '</section>'
- return body, keys
- def main():
- BACKUP.mkdir(parents=True, exist_ok=True)
- for path in list(OUT.glob('*.html')) + list((OUT / 'reports').glob('*.html')) + list((OUT / 'manual').glob('*')):
- if path.is_file():
- saved = BACKUP / path.relative_to(OUT)
- saved.parent.mkdir(parents=True, exist_ok=True)
- if not saved.exists():
- shutil.copy2(path, saved)
- text = (OUT / MANUAL).read_text('utf8')
- data, _, _ = data_block(text, 'window.WORKSHOP_MANUAL_DATA = ')
- mapping, _, _ = data_block(text, ' const promptMap = ')
- assert data['steps'][0]['id'] == 'topic' and len(data['steps']) == 14
- steps = data['steps']
- ledger_path = OUT / '证据与编码-r13.json'
- ledger_sha = sha(ledger_path)
- ledger = json.loads(ledger_path.read_bytes())
- records = {r['key']: r for group in ['oldReviews', 'addedReviews', 'oldProducts', 'addedProducts', 'social'] for r in ledger[group]}
- citations = OUT / 'data/citations'
- citations.mkdir(exist_ok=True)
- used = set()
- by_id = {s['id']: s for s in steps}
- for index, identifier in enumerate(ORDER):
- step = by_id[identifier]
- raw = (BACKUP / f'reports/{identifier}.html').read_text('utf8')
- body = re.search(r'<div class="reading"><article>(.*)</article></div></main>', raw, re.S).group(1)
- body = re.sub(r'^<div class="kicker">.*?</div>', '', body, count=1, flags=re.S)
- heading = re.search(r'<h2>(.*?)</h2>', body, re.S)
- title = html.unescape(re.sub('<[^>]+>', '', heading.group(1)))
- body = body[:heading.start()] + body[heading.end():]
- body = re.sub(r'<p class="muted"><a href="../证据底表.html">.*?</p>', '', body, flags=re.S)
- body = body.replace(OLD_MANUAL, MANUAL)
- body, keys = inline_citations(body, records, '../')
- used.update(keys)
- actions = f'<a class="action-link primary" href="../{MANUAL}#{identifier}">返回 {step["number"]} 任务提示词</a><a class="action-link" href="../案例报告合集.html">全部报告</a>'
- previous = f'<a href="{ORDER[index-1]}.html">← {e(by_id[ORDER[index-1]]["title"])}</a>' if index else '<a href="../案例报告合集.html">← 报告目录</a>'
- following = f'<a href="{ORDER[index+1]}.html">{e(by_id[ORDER[index+1]]["title"])} →</a>' if index < len(ORDER)-1 else f'<a href="../{MANUAL}">返回操作手册 →</a>'
- pager = f'<nav class="report-pager" aria-label="前后报告">{previous}{following}</nav>'
- page = shell(title, '<article id="report-content">' + body + '</article>', steps, identifier, '../',
- intro=f'步骤 {step["number"]} · 宿舍常备拌面研究案例', actions=actions, pager=pager)
- (OUT / f'reports/{identifier}.html').write_text(page, encoding='utf8')
- for key in sorted(used):
- dump(citations / f'{key}.json', records[key])
- groups = []
- for group, label in GROUPS:
- cards = ''.join(card(s) for s in steps if s['id'] in ORDER and s['group'] == group)
- groups.append(f'<section class="panel"><h2>{label}</h2><div class="grid">{cards}</div></section>')
- intro = '按三个半天的研究顺序查看 13 篇报告案例;从每篇报告可以返回对应提示词。'
- (OUT / '案例报告合集.html').write_text(shell('报告案例', ''.join(groups), steps, 'reports', intro=intro), encoding='utf8')
- home = f'''<section class="panel"><h2>开始工作坊</h2><div class="grid"><a class="card" href="{MANUAL}"><small>01—14 · 三个半天</small><strong>操作手册 →</strong><span>沿本组课题逐节研究,检查报告并接续下一步。</span></a><a class="card" href="案例报告合集.html"><small>13 篇研究报告</small><strong>报告案例 →</strong><span>查看图谱、竞品、需求、机会、可行性和最终提案。</span></a></div></section>
- <section class="panel"><h2>案例研究主线</h2><p>宿舍常备拌面:从风味选择与准备体验出发,比较已有供给、用户任务和产品改良方向。</p><p class="muted">案例使用留存商品与用户表达展开探索分析;产品原型设定、研究推断及尚未完成的实测,在各篇报告中分别说明。</p><a class="action-link" href="reports/category.html">从品类全景开始</a> <a class="action-link" href="reports/proposal.html">查看最终概念提案</a></section>'''
- (OUT / 'index.html').write_text(shell('产品创新工作坊', home, steps, 'home', intro='操作手册与研究报告案例'), encoding='utf8')
- support = [('参考学习与类型对应.html', '参考报告与方法'), ('选材与分析规则.html', '选材与分析规则'),
- ('商业回推演算器.html', '商业回推演算器'), ('旧稿逐项复核.html', '案例复核记录')]
- for name, title in support:
- raw = (BACKUP / name).read_text('utf8')
- body = re.search(r'<div class="reading">(.*)</div></main>', raw, re.S).group(1)
- body = body.replace(OLD_MANUAL, MANUAL)
- (OUT / name).write_text(shell(title, body, steps, name), encoding='utf8')
- # Keep the latest 14 prompts as the sole manual source; report links return here.
- dump(OUT / 'manual-source.json', data)
- dump(OUT / 'prompts-and-outputs.json', {'version': data['release']['contentVersion'],
- 'steps': [{**s, 'fullPrompt': mapping[s['id']]} for s in steps]})
- manual_dir = OUT / 'manual'
- index_text = (manual_dir / 'index.html').read_text('utf8')
- index_text = index_text.replace('href="#install"', 'href="../index.html"').replace('href="#topic"', 'href="../index.html"')
- index_text = index_text.replace('0 / 15 项已核对', '0 / 14 项已核对')
- index_text = index_text.replace('<span>产品创新 / <strong>工作坊操作手册</strong></span>', '<span><strong>工作坊操作手册</strong></span>')
- if '>报告案例</a>' not in index_text:
- index_text = index_text.replace(' <a href="https://kangshifu.fmode.cn/"', ' <a href="../index.html">首页</a><a href="../案例报告合集.html">报告案例</a>\n <a href="https://kangshifu.fmode.cn/"')
- (manual_dir / 'index.html').write_text(index_text, encoding='utf8')
- from update_workshop_manual_install_r13 import render_manual
- render_manual(data, json.loads((OUT / 'prompts-and-outputs.json').read_bytes()))
- # Remove the former page after every incoming citation has a local replacement.
- if (OUT / EVIDENCE).exists():
- (OUT / EVIDENCE).unlink()
- assert sha(ledger_path) == ledger_sha
- routes = {'home': 'index.html', 'manual': MANUAL, 'reports': '案例报告合集.html',
- 'reportPages': [dict(id=s['id'], number=s['number'], path=f'reports/{s["id"]}.html', manual=f'{MANUAL}#{s["id"]}') for s in steps if s['id'] in ORDER]}
- dump(OUT / 'site-routes.json', routes)
- receipt = dict(ok=True, reportPages=13, manualSteps=14, removedPage=EVIDENCE,
- citationsInReportPages=True, preservedSourceRecords=len(used), originalLedgerSha256=ledger_sha,
- manualPromptsUnchanged=True, backup=str(BACKUP), manualPublished=False, productionWrites=0)
- dump(OUT / '页面导航整理核验.json', receipt)
- manifest_path = OUT / '本地发行清单.json'
- manifest = json.loads(manifest_path.read_bytes())
- manifest.update(manualSha256=sha(OUT / MANUAL), steps=14, siteRoutes='site-routes.json')
- dump(manifest_path, manifest)
- print(json.dumps(receipt, ensure_ascii=False))
- if __name__ == '__main__':
- main()
|