Explorar o código

松绑 what/why 骨架(锚点+自由小节)+ 卡片级溯源 + 全量重跑

what/why 不再死模板:
- What = 界定(必填锚点)+ 主体[] 自由小节;Why = 主张(必填锚点)+ 支撑[]
- 每节 {小标题, 形式?, 内容?, 条目?};形式为开放提示(清单/分类/判据/机制/影响…)
- 散文与条目二选一或并存;节数 0..N 由内容定,不强制三件套/单要素表

卡片级溯源:① 按卡片归因记 cards(图url/视频时间段+读到内容),
②每 step / 每 what·why 颗带「出处」,前端 🔍 点回原卡片。

代码/前端:
- decompose.py / rebuild_payloads.py:build_content 按自由小节拼;
  新增创作阶段受控守卫(非法值如"定稿/输出"丢弃)
- index.html:WhatCard/WhyCard 渲染 KSection(小标题+形式标+内容/条目表)
- 文档同步:phase1/2/3、金标样例、创作知识拆解框架.md

全量重跑 7 帖(5 小红书 + 1 抖音视频 + 1 实时链接):21 颗,lint 0 ERROR。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SamLee hai 1 mes
pai
achega
1468d88891

+ 32 - 8
scripts/decompose.py

@@ -39,6 +39,7 @@ SOURCES = [
 ]
 SRC2CN = {"substance": "实质", "form": "形式", "feeling": "感受", "effect": "作用", "intent": "意图"}
 TYPE2ATTR = {"how": "how工序", "what": "what构成", "why": "why原理"}
+CSTAGE = {"定向", "构思", "结构", "成文", "打磨"}  # 创作阶段受控 5 值
 REUSE_THRESHOLD = 0.90
 
 
@@ -62,6 +63,9 @@ def read_one(src: dict, post, settings: Settings, extractor: GeminiExtractor):
         pub = f"/data/demo/douyin/dy_{cid}/video.mp4"
         ec = video_extract.extract_video(post, settings=settings, save_path=save, public_url=pub)
         media = {"type": "video", "video_url": pub, "images": []}
+        cmap = {c.index: c.content for c in ec.cards}
+        cards = [{"index": c.index, "content": cmap.get(c.index, ""), "video_url": pub,
+                  "start": c.start, "end": c.end} for c in post.cards]  # 段卡:时间戳 + 读到的内容
     else:                # 图文帖:读图
         ec = extractor.extract(post)
         if src["from"] == "fixture":
@@ -69,11 +73,14 @@ def read_one(src: dict, post, settings: Settings, extractor: GeminiExtractor):
         else:
             imgs = list(post.image_urls)  # 新帖未落盘 → 直接用 CDN url
         media = {"type": "image", "video_url": None, "images": imgs}
+        cmap = {c.index: c.content for c in ec.cards}
+        cards = [{"index": n, "content": cmap.get(n, ""), "image_url": imgs[n - 1] if n <= len(imgs) else None}
+                 for n in range(1, len(imgs) + 1)]  # 每图一张卡:图 url + 读到的内容
     parts = [ec.text]
     if ec.from_image:
         parts.append("【图片要点】\n" + ec.from_image)
     parts += [f"【卡片{c.index}】{c.content}" for c in ec.cards if c.content]
-    return "\n\n".join(p for p in parts if p), bool(ec.is_empty), media
+    return "\n\n".join(p for p in parts if p), bool(ec.is_empty), media, cards
 
 
 # ---------- ② 判颗+成形+轻标签 ----------
@@ -94,7 +101,7 @@ def _slim(knowledges: list[dict]) -> list[dict]:
                            "directive": (s.get("directive") or "")[:500], "output": s.get("output")}
                           for s in k.get("steps", [])]
         else:
-            e["内容"] = {x: k.get(x) for x in ("界定", "构成", "主张", "依据", "对创作的影响") if k.get(x)}
+            e["内容"] = {x: k.get(x) for x in ("界定", "主体", "主张", "支撑") if k.get(x)}
         slim.append(e)
     return slim
 
@@ -146,9 +153,23 @@ def build_content(k: dict) -> str:
                       f"  指引:{s.get('directive','')}", f"  产出:{s.get('output','')}"]
         return "\n".join(lines)
     if t == "what":
-        return "\n".join([f"界定:{k.get('界定','')}", "构成:"]
-                         + [f"- {c.get('要素','')}:{c.get('说明','')}" for c in k.get("构成", [])])
-    return f"主张:{k.get('主张','')}\n依据:{k.get('依据','')}\n对创作的影响:{k.get('对创作的影响','')}"
+        return "\n".join([f"界定:{k.get('界定','')}"] + _sections(k.get("主体")))
+    return "\n".join([f"主张:{k.get('主张','')}"] + _sections(k.get("支撑")))
+
+
+def _sections(blocks) -> list[str]:
+    """把 what.主体 / why.支撑 的自由小节拼成文本行。"""
+    out = []
+    for b in blocks or []:
+        head = b.get("小标题") or ""
+        form = b.get("形式")
+        out.append(f"【{head}】" + (f"({form})" if form else ""))
+        if b.get("内容"):
+            out.append(f"  {b['内容']}")
+        for it in b.get("条目") or []:
+            word = it.get("词") or it.get("要素") or ""
+            out.append(f"  - {word}:{it.get('说明','')}" if word else f"  - {it.get('说明','')}")
+    return out
 
 
 def build_payload(post, k: dict, how_titles: dict | None = None) -> dict:
@@ -195,15 +216,15 @@ def main() -> None:
         print(f"\n=== {src['platform']} {cid[:10]} ({src['from']}) ===")
         try:
             post = load_post(src, settings)
-            read, is_empty, media = read_one(src, post, settings, extractor)
+            read, is_empty, media, cards = read_one(src, post, settings, extractor)
         except Exception as exc:
             print(f"  ✗ 取数/读懂失败:{exc}")
             posts_out.append({"post_id": cid, "source_id": cid, "title": f"(取数失败 {cid})",
                               "platform": src["platform"], "url": "", "media": {"type": "image", "images": []},
-                              "error": str(exc)[:200], "knowledges": []})
+                              "cards": [], "error": str(exc)[:200], "knowledges": []})
             continue
         meta = {"post_id": cid, "source_id": post.id, "title": post.title or "",
-                "platform": post.platform, "url": post.url, "media": media}
+                "platform": post.platform, "url": post.url, "media": media, "cards": cards}
         if is_empty:  # ① 总闸
             print(f"  ① 读懂 {len(read)} 字 → 判定无可提取的创作知识,跳过拆颗")
             posts_out.append({**meta, "no_knowledge": True, "knowledges": []})
@@ -214,6 +235,9 @@ def main() -> None:
         how_titles = {k.get("id"): k.get("title") for k in knowledges if k.get("type") == "how"}
         for k in knowledges:
             k["业务阶段"] = [b for b in (k.get("业务阶段") or []) if b in ("灵感", "选题", "脚本")]  # 守卫:只留合法业务阶段
+            for s in k.get("steps", []):  # 守卫:创作阶段只留合法 5 值,非法(如"定稿/输出")丢弃
+                if s.get("创作阶段") not in CSTAGE:
+                    s["创作阶段"] = None
             if k.get("role") == "组件" and (k.get("parent") or {}).get("how_id") not in how_ids:  # 守卫:组件 parent 必指向同帖 how
                 k["role"] = "主"; k["parent"] = None
         print(f"  ② {len(knowledges)} 颗:" + ", ".join(f"{k.get('type')}/{k.get('role')}" for k in knowledges))

+ 19 - 3
scripts/rebuild_payloads.py

@@ -16,6 +16,20 @@ PL = ROOT / "web/payloads.json"
 TYPE2ATTR = {"how": "how工序", "what": "what构成", "why": "why原理"}
 
 
+def _sections(blocks) -> list:
+    out = []
+    for b in blocks or []:
+        head = b.get("小标题") or ""
+        form = b.get("形式")
+        out.append(f"【{head}】" + (f"({form})" if form else ""))
+        if b.get("内容"):
+            out.append(f"  {b['内容']}")
+        for it in b.get("条目") or []:
+            word = it.get("词") or it.get("要素") or ""
+            out.append(f"  - {word}:{it.get('说明','')}" if word else f"  - {it.get('说明','')}")
+    return out
+
+
 def content(k: dict) -> str:
     t = k["type"]
     if t == "how":
@@ -25,9 +39,8 @@ def content(k: dict) -> str:
                   f"  指引:{s.get('directive','')}", f"  产出:{s.get('output','')}"]
         return "\n".join(L)
     if t == "what":
-        return "\n".join([f"界定:{k.get('界定','')}", "构成:"]
-                         + [f"- {c.get('要素','')}:{c.get('说明','')}" for c in k.get("构成", [])])
-    return f"主张:{k.get('主张','')}\n依据:{k.get('依据','')}\n对创作的影响:{k.get('对创作的影响','')}"
+        return "\n".join([f"界定:{k.get('界定','')}"] + _sections(k.get("主体")))
+    return "\n".join([f"主张:{k.get('主张','')}"] + _sections(k.get("支撑")))
 
 
 def main() -> None:
@@ -40,6 +53,9 @@ def main() -> None:
         how_titles = {k["id"]: k.get("title") for k in ks if k["type"] == "how"}
         for k in ks:
             k["业务阶段"] = [b for b in (k.get("业务阶段") or []) if b in ("灵感", "选题", "脚本")]
+            for s in k.get("steps", []):
+                if s.get("创作阶段") not in ("定向", "构思", "结构", "成文", "打磨"):
+                    s["创作阶段"] = None
             if k.get("role") == "组件" and (k.get("parent") or {}).get("how_id") not in how_ids:
                 k["role"] = "主"
                 k["parent"] = None

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 515 - 801
web/frameworks.json


+ 42 - 13
web/index.html

@@ -56,6 +56,12 @@
   .ktitle{font-size:14px;font-weight:600}
   .krole{font-size:10.5px;padding:1px 6px;border-radius:3px;background:#f1f5f9;color:#64748b}
   .kref{font-size:11px;color:#0e7490;cursor:pointer}.kref:hover{text-decoration:underline}
+  .srcbtn{cursor:pointer;color:#0e7490;margin-left:3px;user-select:none}.srcbtn:hover{color:#155e75}
+  .srccard{border:1px solid #e2e8f0;border-radius:6px;padding:8px 10px;margin:8px 0;background:#f8fafc}
+  .srccard .ci{font-size:11.5px;color:#6b7280;font-weight:600;margin-bottom:5px}
+  .srccard img{max-width:100%;max-height:360px;border-radius:5px;border:1px solid #e2e8f0;cursor:zoom-in}
+  .srccard video{max-width:100%;max-height:300px;border-radius:5px}
+  .srcread{font-size:12.5px;color:#1f2937;line-height:1.6;margin-top:6px;background:#fff;border-left:3px solid #0e7490;padding:6px 9px}
   .kbiz{font-size:11px;padding:1px 7px;border-radius:3px;background:#fef3c7;color:#92400e}
   .kacts{margin-left:auto;display:flex;gap:6px}
   .kbody{padding:6px 14px 13px}
@@ -84,11 +90,15 @@
   .act-chip{display:inline-block;font-size:11.5px;padding:1px 7px;border-radius:4px;background:#fff7ed;color:#9a3412;border:1px solid #fed7aa}
   .dir-txt{font-size:11.5px;color:#33312c;line-height:1.65}.out-txt{font-size:11.5px;color:#166534}
   .from{font-size:11px;color:#9ca3af}
-  /* what 要素表 */
+  /* what/why 自由小节 */
   .def{font-size:13px;background:#fdf4ff;border-left:3px solid #a21caf;border-radius:0 6px 6px 0;padding:8px 12px;margin:6px 0 10px}
+  .ksec{margin:9px 0;padding-left:11px;border-left:3px solid #e2e8f0}
+  .ksec-h{display:flex;align-items:center;gap:7px;margin-bottom:4px}
+  .ksec-t{font-size:13px;font-weight:600;color:#334155}
+  .ksec-f{font-size:10.5px;padding:1px 7px;border-radius:9px;background:#f1f5f9;color:#64748b}
+  .ksec-c{font-size:12.5px;line-height:1.65;color:#1f2937;margin-bottom:4px}
   table.elem{border-collapse:collapse;width:100%;font-size:12.5px}
-  table.elem th,table.elem td{border:1px solid #f0e6f5;padding:6px 10px;text-align:left;vertical-align:top}
-  table.elem th{background:#fae8ff;color:#86198f;font-weight:600;width:130px}
+  table.elem td{border:1px solid #f0e6f5;padding:6px 10px;text-align:left;vertical-align:top}
   table.elem td.el{font-weight:600;color:#86198f;background:#fdf4ff;width:130px}
   /* why 论点卡 */
   .why-row{display:flex;gap:10px;margin:7px 0}
@@ -173,7 +183,7 @@ function KScopes({list,onScope}){
     {list.map((x,i)=><ScopeChip key={i} x={{...x}} onScope={onScope} showType={true}/>)}</div>;
 }
 
-function HowTable({k,onScope,onPrompt}){
+function HowTable({k,onScope,onPrompt,onSource}){
   const steps=k.steps||[];
   const scOf=(s,t)=>(s.作用域||[]).filter(x=>x.scope_type===t);
   return <div className="twrap"><table className="proc">
@@ -186,7 +196,7 @@ function HowTable({k,onScope,onPrompt}){
     </thead>
     <tbody>{steps.map((s,i)=><tr className="step" key={i}>
       <td className="idx">{i+1}</td>
-      <td className="intent">{s.intent}</td>
+      <td className="intent">{s.intent}{s.出处&&s.出处.length>0&&<span className="srcbtn" title={"看原文出处 "+s.出处.join(" ")} onClick={()=>onSource(s.出处)}>🔍</span>}</td>
       <td className="cstage">{s.创作阶段&&<span className="cstage-chip">{s.创作阶段}</span>}</td>
       <td className="act">{s.动作&&<span className="act-chip">{s.动作}</span>}</td>
       <td className="dir"><span className="dir-txt">{s.directive}</span></td>
@@ -195,36 +205,43 @@ function HowTable({k,onScope,onPrompt}){
     </tr>)}</tbody>
   </table></div>;
 }
+function KSection({b}){
+  const items=b.条目||[];
+  return <div className="ksec">
+    <div className="ksec-h">{b.小标题&&<span className="ksec-t">{b.小标题}</span>}{b.形式&&<span className="ksec-f">{b.形式}</span>}</div>
+    {b.内容&&<div className="ksec-c">{b.内容}</div>}
+    {items.length>0&&<table className="elem"><tbody>{items.map((c,i)=><tr key={i}><td className="el">{c.词||c.要素}</td><td>{c.说明}</td></tr>)}</tbody></table>}
+  </div>;
+}
 function WhatCard({k,onScope}){
   return <div>
     {k.界定&&<div className="def">界定:{k.界定}</div>}
-    <table className="elem"><thead><tr><th>要素</th><th>说明</th></tr></thead>
-      <tbody>{(k.构成||[]).map((c,i)=><tr key={i}><td className="el">{c.要素}</td><td>{c.说明}</td></tr>)}</tbody></table>
+    {(k.主体||[]).map((b,i)=><KSection key={i} b={b}/>)}
     <KScopes list={k.作用域} onScope={onScope}/>
   </div>;
 }
 function WhyCard({k,onScope}){
   return <div>
     {k.主张&&<div className="why-row"><span className="why-lab wl-claim">主张</span><span className="why-txt">{k.主张}</span></div>}
-    {k.依据&&<div className="why-row"><span className="why-lab wl-basis">依据</span><span className="why-txt">{k.依据}</span></div>}
-    {k["对创作的影响"]&&<div className="why-row"><span className="why-lab wl-impact">影响</span><span className="why-txt">{k["对创作的影响"]}</span></div>}
+    {(k.支撑||[]).map((b,i)=><KSection key={i} b={b}/>)}
     <KScopes list={k.作用域} onScope={onScope}/>
   </div>;
 }
 
-function KnowledgeCard({k,idx,onScope,onPrompt,onJson,payload,how_titles}){
+function KnowledgeCard({k,idx,onScope,onPrompt,onJson,onSource,payload,how_titles}){
   const par=k.parent;
   return <div className="kcard" id={"k-"+k.id}>
     <div className="khead">
       <span className="kno">第{idx+1}颗</span>
       <span className={"ktype ty-"+k.type}>{TYPECN[k.type]}</span>
       <span className="ktitle">{k.title}</span>
+      {k.出处&&k.出处.length>0&&<span className="kref" title={"看原文出处 "+k.出处.join(" ")} onClick={()=>onSource(k.出处)}>🔍 出处 {k.出处.join(" ")}</span>}
       {par&&<span className="kref" onClick={()=>{var e=document.getElementById("k-"+par.how_id);if(e)e.scrollIntoView({behavior:"smooth",block:"center"});}}>↑ 出自「{how_titles[par.how_id]||par.how_id}」第{par.step}步</span>}
       {(k.业务阶段||[]).map((b,i)=><span key={i} className="kbiz">业务阶段:{b}</span>)}
       <span className="kacts">{payload&&<button className="lbtn" onClick={()=>onJson(payload)}>⚙️ 入库 payload</button>}</span>
     </div>
     <div className="kbody">
-      {k.type==="how"&&<HowTable k={k} onScope={onScope} onPrompt={onPrompt}/>}
+      {k.type==="how"&&<HowTable k={k} onScope={onScope} onPrompt={onPrompt} onSource={onSource}/>}
       {k.type==="what"&&<WhatCard k={k} onScope={onScope}/>}
       {k.type==="why"&&<WhyCard k={k} onScope={onScope}/>}
     </div>
@@ -238,6 +255,7 @@ function App(){
   const [pm,setPm]=useState(null);
   const [json,setJson]=useState(null);
   const [zoom,setZoom]=useState(null);
+  const [src,setSrc]=useState(null);
   const [hl,setHl]=useState(false);
   useEffect(()=>{Promise.all([j("/frameworks.json"),j("/payloads.json").catch(()=>[])]).then(([f,pl])=>{
     setItems((f.posts||[]).map(p=>({...p,payloads:(pl||[]).filter(x=>(x.source||{}).id===(p.source_id||("xhs_"+p.post_id)))})));});},[]);
@@ -255,6 +273,9 @@ function App(){
   const how_titles={}; ks.forEach(k=>{if(k.type==="how")how_titles[k.id]=k.title;});
   const payloadOf=k=>(it.payloads||[]).find(p=>p.title===k.title);
   const media=it.media||{}; const imgs=media.images||[]; const videoUrl=media.video_url;
+  const cardsById={}; (it.cards||[]).forEach(c=>cardsById[c.index]=c);
+  const openSource=list=>{var cs=(list||[]).map(s=>{var m=String(s).match(/\d+/);return m?cardsById[parseInt(m[0])]:null;}).filter(Boolean);if(cs.length)setSrc({cards:cs});};
+  const fmtT=s=>{s=Math.floor(s||0);return Math.floor(s/60)+":"+String(s%60).padStart(2,"0");};
   return <div className="app">
     <header className="top">
       <h1>创作知识的解构</h1>
@@ -272,11 +293,11 @@ function App(){
         </div>
       </details>
       <div className="barhint" style={{display:"flex",gap:8,alignItems:"center"}}>
-        <span>这一帖拆出 <b>{ks.length}</b> 颗知识(how 工序表 / what 要素表 / why 论点卡);组件颗标「出自某 how 第 N 步」可点跳。</span>
+        <span>这一帖拆出 <b>{ks.length}</b> 颗知识(how 工序表 / what 构成卡 / why 论点卡);组件颗标「出自某 how 第 N 步」可点跳。</span>
         <button className={"lbtn"+(hl?" on":"")} onClick={()=>setHl(!hl)} style={{marginLeft:"auto"}}>高亮「新值」</button>
       </div>
       {ks.length===0 && <div className="kcard"><div className="kbody" style={{padding:"20px 16px",color:"#9ca3af",fontSize:"13px"}}>🚫 这帖<b style={{color:"#6b7280"}}>没有可提取的创作知识</b>(纯作品 / 展示 / 无关)——① 读懂时 <code>is_empty=true</code>,后面拆颗/归类/入库已自动跳过。</div></div>}
-      {ks.map((k,i)=><KnowledgeCard key={i} k={k} idx={i} onScope={setScope} onPrompt={openPrompt} onJson={setJson} payload={payloadOf(k)} how_titles={how_titles}/>)}
+      {ks.map((k,i)=><KnowledgeCard key={i} k={k} idx={i} onScope={setScope} onPrompt={openPrompt} onJson={setJson} onSource={openSource} payload={payloadOf(k)} how_titles={how_titles}/>)}
     </div>
     {scope&&<div><div className="ov" onClick={()=>setScope(null)}></div>
       <div className="drawer"><h2>作用域回扣详情<span className="x" onClick={()=>setScope(null)}>×</span></h2>
@@ -296,6 +317,14 @@ function App(){
     {json&&<div className="modal" onClick={()=>setJson(null)}><div className="mbox" onClick={e=>e.stopPropagation()}>
       <h2>⚙️ ingest payload(给机器看)<span className="x" onClick={()=>setJson(null)}>×</span></h2>
       <div className="body"><pre>{JSON.stringify(json,null,2)}</pre></div></div></div>}
+    {src&&<div className="modal" onClick={()=>setSrc(null)}><div className="mbox" onClick={e=>e.stopPropagation()}>
+      <h2>🔍 原文出处(① 读到的卡片)<span className="x" onClick={()=>setSrc(null)}>×</span></h2>
+      <div className="body">{src.cards.map((c,i)=><div className="srccard" key={i}>
+        <div className="ci">卡片{c.index}{c.start!=null?(" · "+fmtT(c.start)+"–"+fmtT(c.end)):""}</div>
+        {c.image_url&&<img src={c.image_url} onClick={()=>setZoom(c.image_url)} onError={e=>e.target.style.display='none'}/>}
+        {c.video_url&&<video src={c.video_url+(c.start!=null?("#t="+Math.floor(c.start)+","+Math.floor(c.end)):"")} controls/>}
+        {c.content?<div className="srcread">① 在这张卡读到的:{c.content}</div>:<div style={{color:"#9ca3af",fontSize:"12px",marginTop:5}}>(这张卡没读到要点)</div>}
+      </div>)}</div></div></div>}
     {zoom&&<div className="lb" onClick={()=>setZoom(null)}><img src={zoom}/></div>}
   </div>;
 }

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 187 - 354
web/payloads.json


+ 5 - 5
创作知识拆解框架.md

@@ -21,8 +21,8 @@
 | 类型 | 它是什么 | 成形模板 | dim_attributes |
 |---|---|---|---|
 | **How** 工序 | 不可跳步地把某输入做成成品 | `目标(purpose)` + 有序 `steps`(每步 目的 / 怎么做 / 产出) | `how工序` |
-| **What** 构成 | 界定 / 列举某创作对象由什么组成 | `界定` + `构成要素[{要素, 说明}]`(**要素表**,无步骤) | `what构成` |
-| **Why** 原理 | 解释某做法 / 判断背后的道理 | `主张` + `依据` + `对创作的影响`(**论点卡**,无步骤) | `why原理` |
+| **What** 构成 | 界定 / 列举 / 分类 / 判据某创作对象 | `界定`(必填锚点) + `主体[{小标题, 形式?, 内容?, 条目?}]`(**自由小节**,无步骤) | `what构成` |
+| **Why** 原理 | 解释某做法 / 判断背后的道理 | `主张`(必填锚点) + `支撑[{小标题, 形式?, 内容?, 条目?}]`(**自由小节**,无步骤) | `why原理` |
 
 ---
 
@@ -110,7 +110,7 @@
 |---|---|---|
 | **① 读懂** | 图 / 视频 → 文字(忠实,不判断) | 视觉 LLM(基建 extractor) |
 | **②a 判颗 + 找 how 骨架** | 几颗、各颗类型;how 抽骨架并识别内部 what/why 组件;orphan what/why;无 how 则纯 what/why(**第三节那道闸**) | 判断 LLM |
-| **②b 按类型成形 + 轻标签** | How→目标+步骤(+逐步 创作阶段/动作);What→界定+构成;Why→主张+依据+影响;颗级业务阶段;组件颗标 cross-ref | 判断 LLM |
+| **②b 按类型成形 + 轻标签** | How→目标+步骤(+逐步 创作阶段/动作);What→界定+主体小节;Why→主张+支撑小节(小节形态自由,0..N);颗级业务阶段;组件颗标 cross-ref | 判断 LLM |
 | **③ 作用域** | 5 类带值 + 互斥边界 + 回扣(How 逐步 / What·Why 颗级) | 判断 LLM + 回扣工具(代码) |
 | **⑤ 组装 + lint** | 拼 payload(content 按类型不同、scopes 并集),校验 | 纯代码 |
 
@@ -120,7 +120,7 @@
 
 | payload 字段 | How | What | Why |
 |---|---|---|---|
-| `content` | 目标 + 步骤(目的/怎么做/产出) 拍平 | 界定 + 构成清单 | 主张 + 依据 + 对创作的影响 |
+| `content` | 目标 + 步骤(目的/怎么做/产出) 拍平 | 界定 + 主体各小节 | 主张 + 支撑各小节 |
 | `dim_attributes` | `["how工序"]` | `["what构成"]` | `["why原理"]` |
 | `scopes` | 各步作用域并集 | 颗级作用域 | 颗级作用域 |
 | `custom_ext` | 业务阶段 + 创作阶段 + 动作 | **仅业务阶段** | **仅业务阶段** |
@@ -131,7 +131,7 @@
 ## 十、前端 —— 一页多颗、按类型渲染
 
 - 选一帖 → 列出**它的全部 N 颗**(可混类型)。
-- 每颗按类型渲染:**How → 工序表**;**What → 要素表**(界定 + 构成清单);**Why → 论点卡**(主张 / 依据 / 影响)。
+- 每颗按类型渲染:**How → 工序表**;**What → 构成卡**(界定 + 自由小节,每节 小标题 + 形式标 + 内容/条目);**Why → 论点卡**(主张 + 自由小节)。
 - **组件颗** cross-ref 到所属 how(标"出自 <how> 第 N 步",可互相跳转);**orphan 颗**独立列出。
 - 每颗都带:**作用域回扣 drawer**(候选 → 最近邻 top-K → 复用/新建)、**提示词溯源**(区块/表头点开产出它的那道工序的提示词)。
 

+ 2 - 2
创作知识提取-skill/examples/金标样例.md

@@ -86,7 +86,7 @@
 {
   "source": { "id": "xhs_xxx", "source_type": "post", "title": "脚本就这三样" },
   "title": "短视频脚本三要素",
-  "content": "界定:一份短视频脚本的内容骨架由三类要素构成。\n构成:\n- 人物:谁出镜、出镜形象\n- 场景:室内 / 室外\n- 事件:整条脚本的故事内容",
+  "content": "界定:一份短视频脚本的内容骨架由三类要素构成。\n【三类要素】(清单)\n  - 人物:谁出镜、出镜形象\n  - 场景:室内 / 室外\n  - 事件:整条脚本的故事内容",
   "dim_creations": ["创作"],
   "dim_attributes": ["what构成"],
   "scopes": [ { "scope_type": "substance", "value": "脚本要素" },
@@ -105,7 +105,7 @@
 {
   "source": { "id": "xhs_yyy", "source_type": "post", "title": "标题才是第一生产力" },
   "title": "标题决定打开,与内容质量无关",
-  "content": "主张:标题的唯一作用是让人有欲望点开;点不点开,和内容好不好没关系。\n依据:信息流里用户先看到的是标题,内容再好没人点开等于零(平台分发逻辑)。\n对创作的影响:把『起标题』当成独立的、第一优先的创作决策,而非内容概括。",
+  "content": "主张:标题的唯一作用是让人有欲望点开;点不点开,和内容好不好没关系。\n【为什么成立】(依据)\n  信息流里用户先看到的是标题,内容再好没人点开等于零(平台分发逻辑)。\n【对创作的影响】(影响)\n  把『起标题』当成独立的、第一优先的创作决策,而非内容概括。",
   "dim_creations": ["创作"],
   "dim_attributes": ["why原理"],
   "scopes": [ { "scope_type": "effect", "value": "吸引点击" },

+ 25 - 9
创作知识提取-skill/extraction/phase1-frame.md

@@ -34,12 +34,19 @@
 - `output` 产出
 - `出处`(图N / 视频时间)
 
-### What(构成)— role 主 或 组件
-`界定`(一句话这是什么)+ `构成`:`[{要素, 说明}]`。无 steps。
+> **每颗都带 `出处`(溯源用)**:How 颗的 `出处` 在每个 step 上(这步来自哪些卡);What/Why 颗在颗级给 `出处`(这颗来自哪些卡,如 ["图3","卡片2"])。出处填卡片号(图N=第N张图,卡片N=视频第N段),让前端能点回原图/原视频段。
+
+### What(是什么)— role 主 或 组件
+`界定`(**唯一必填**,一句话这是什么,做检索/预览锚点)+ `主体`:**0..N 个自由小节**,每节 `{小标题, 形式?, 内容?, 条目?}`。无 steps。+ 颗级 `出处`。
+- `形式`(可选·开放提示):这节是什么形态——清单 / 分类 / 判据 / 对照 / 定义 / 步骤要点…(从内容判,没把握就省,别硬塞)。
+- `内容`(散文)与 `条目`(`[{词, 说明}]` 结构化列表)**二选一或并存**;有就写、没有不写。
+- **别套模板**:一个 what 可能只有「界定 + 一节清单」,也可能「界定 + 三节(清单 + 对照 + 判据)」。节数/形态由本帖内容决定。
 > 组件 what:它是某 how 某步的产出类型,但本身可独立复用(如某步产出的一份「X 要素」清单)。
 
-### Why(原理)— role 主 或 组件
-`主张`(核心观点)+ `依据`(原理/平台逻辑/读者心理)+ `对创作的影响`(取舍含义)。无 steps。
+### Why(为什么)— role 主 或 组件
+`主张`(**唯一必填**,核心观点,做锚点)+ `支撑`:**0..N 个自由小节**,每节 `{小标题, 形式?, 内容?, 条目?}`。无 steps。+ 颗级 `出处`。
+- `形式`(可选·开放提示):依据 / 机制 / 影响 / 例证 / 前提 / 反例…(从内容判)。
+- **不再强制「依据 + 影响」三件套**:有的 why 只有主张 + 一条依据;有的还带平台机制、读者心理、对创作取舍的影响——**有几条写几条**。
 > 组件 why:它是某 how 某步 directive 里"为什么这样选"的判断依据,但本身可独立复用。
 
 ---
@@ -95,14 +102,23 @@
     {
       "id": "k2", "type": "what", "role": "组件", "parent": {"how_id":"k1","step":2},
       "title": "<某 how 步骤产出的、可独立复用的清单/构成名>", "业务阶段": ["<…>"],
-      "界定": "<一句话这是什么>",
-      "构成": [{"要素":"<要素名>","说明":"<一句话说明>"}],
+      "出处": ["<这颗来自哪些卡片,如 图3 / 卡片2>"],
+      "界定": "<一句话这是什么(必填)>",
+      "主体": [
+        {"小标题":"<这节讲什么>","形式":"<清单/分类/判据/对照/定义,可省>",
+         "条目":[{"词":"<要点名>","说明":"<一句话>"}]},
+        {"小标题":"<另一节,可省>","形式":"对照","内容":"<散文式说明,没有结构化条目时用>"}
+      ],
       "作用域": []
     },
     {
       "id": "k3", "type": "why", "role": "主", "parent": null,
-      "title": "<原理名>", "业务阶段": ["<…>"],
-      "主张": "<核心观点>", "依据": "<原理/逻辑>", "对创作的影响": "<取舍含义>",
+      "title": "<原理名>", "业务阶段": ["<…>"], "出处": ["<这颗来自哪些卡片>"],
+      "主张": "<核心观点(必填)>",
+      "支撑": [
+        {"小标题":"<为什么成立>","形式":"依据","内容":"<原理/平台逻辑/读者心理>"},
+        {"小标题":"<对创作的取舍,可省>","形式":"影响","内容":"<取舍含义>"}
+      ],
       "作用域": []
     }
   ]
@@ -111,6 +127,6 @@
 
 ## 过关条件 → 进 ③
 - 颗数合理(how 主颗 + 组件颗 + orphan,可混);每颗有 type/role/parent。
-- how 颗:每步 intent/directive/output/出处 + 创作阶段 + 动作;what 颗:界定+构成;why 颗:主张+依据+影响
+- how 颗:每步 intent/directive/output/出处 + 创作阶段 + 动作;what 颗:界定(必填)+ 主体小节(0..N);why 颗:主张(必填)+ 支撑小节(0..N)
 - 组件颗有 parent cross-ref;颗级业务阶段都填了。
 - 所有颗的 `作用域` 留空(含 how 的每步),③ 来填。

+ 2 - 2
创作知识提取-skill/extraction/phase2-scope.md

@@ -9,7 +9,7 @@
 
 **按类型分粒度**:
 - **How 颗**:**逐步**标——每个 step 一组作用域(写回 `step.作用域`)。
-- **What / Why 颗**:**颗级**标——整颗一组作用域(写回 `knowledge.作用域`);读 What 的 `界定/构成`、Why 的 `主张/依据/对创作的影响` 来判。
+- **What / Why 颗**:**颗级**标——整颗一组作用域(写回 `knowledge.作用域`);读 What 的 `界定` + `主体`各小节、Why 的 `主张` + `支撑`各小节来判。
 
 > 这是全流程最容易"串味"的一刀(5 类边界模糊),所以单独、写厚。判不准就标空 `[]`,不硬塞。
 
@@ -76,7 +76,7 @@
 ## 5. 执行三阶段
 
 ### 阶段一 · 提候选值
-逐步读这颗(how 读 `intent/directive/output`;what/why 读其界定/构成/主张依据)的真实内容,按 §1-§4 标相关的几类,每类一个**从本帖抽的**候选具体值。
+逐步读这颗(how 读 `intent/directive/output`;what/why 读其 界定+主体 / 主张+支撑 各小节)的真实内容,按 §1-§4 标相关的几类,每类一个**从本帖抽的**候选具体值。
 
 ### 阶段二 · 回扣对齐(调工具)
 对每个候选值跑:

+ 4 - 3
创作知识提取-skill/extraction/phase3-assemble.md

@@ -10,14 +10,15 @@
 | `title` | 该颗 `title` | | |
 | `dim_creations` | `["创作"]` 写死 | | |
 | `dim_attributes` | `["how工序"]` | `["what构成"]` | `["why原理"]` |
-| `content` | 目标+步骤(目的/怎么做/产出)拍平 | 界定+构成清单 | 主张+依据+对创作的影响 |
+| `content` | 目标+步骤(目的/怎么做/产出)拍平 | 界定+主体各小节 | 主张+支撑各小节 |
 | `scopes` | **各步 `作用域` value 去重并集** | **颗级 `作用域`** value | 颗级 value |
 | `custom_ext` | 业务阶段 + 创作阶段(各步去重) + 动作(各步) | **仅业务阶段** | **仅业务阶段** |
 
 ### content 拼法
 - **How**:`目标:<purpose>\n步骤1(目的:<intent>)\n  指引:<directive>\n  产出:<output>\n步骤2…`
-- **What**:`界定:<界定>\n构成:\n- <要素>:<说明>`
-- **Why**:`主张:<主张>\n依据:<依据>\n对创作的影响:<对创作的影响>`
+- **What**:`界定:<界定>` + 每个 `主体` 小节 → `【<小标题>】(<形式>)` 后跟 `内容` 散文行 和/或 `条目` 的 `- <词>:<说明>`。
+- **Why**:`主张:<主张>` + 每个 `支撑` 小节 → 同上(`【<小标题>】(<形式>)` + 内容/条目)。
+- 小节数 0..N,有就拼、没有不拼;`形式` 没有则省括号。
 
 ### 组件颗的溯源
 若 `role=="组件"`(parent 非空),在 `custom_ext` 加一条标明出处:

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio