Przeglądaj źródła

新增 创作知识提取-skill(创作帖→ingest payload 的三阶段 skill)

- extraction/phase1-3:读多模态→判几颗→剔制作→框架骨架;类型/业务阶段/
  创作阶段/动作 + 作用域回扣;组装 ingest payload + lint + 自检 + 逃生
- format/ingest-payload.schema.json:机器校验契约
- taxonomy/:知识类型/业务阶段/创作阶段(带边界);作用域不放静态词表,实时回扣 DB
- examples/金标样例.md:How 端到端 + What + Why few-shot
- tools/:lint-payload(金标 0 ERROR/负例抓全)、scope-link(复用 scripts/scope_link
  + scope_trees 火山回扣,反转→1.0)、ingest-post(默认 dry-run)

骨架借鉴 马晗 skill(三阶段/滚动单文件/上下文纪律/inferred 逃生/词表带边界);
差异:输出 ingest payload,滚动产物 framework.json 对齐 frameworks.json。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SamLee 1 miesiąc temu
rodzic
commit
265054cbad

+ 67 - 0
创作知识提取-skill/README.md

@@ -0,0 +1,67 @@
+# 创作知识提取 SKILL · 总览
+
+> 这道 skill 做一件事:**读一篇创作帖/视频,把背后能指导创作的"框架"提出来,组装成 `POST /api/v1/knowledge/ingest` 的请求体(payload.json)**。
+>
+> 本文是总览。具体操作和字段规则在 [extraction/](extraction/) 的三阶段文件里,按阶段读。
+
+---
+
+**本目录是一个自包含 skill**:跑提取需要的所有说明都在这里。外部的帖子原文和你的产物(`outputs/<帖>/`)不算 skill 的一部分。设计依据是仓库根的《创作知识拆解框架.md》(本 skill 是它的可执行化)。
+
+## 输入 / 输出
+
+**输入**:一篇创作帖/视频教程(小红书 / 抖音 …,正文 + 图 / 视频)。取数与多模态读取用仓库基建(`creation_knowledge.integrations.crawler` / `extractor` / `video_extract`)。
+
+**输出**:`outputs/<帖>/payload.json` —— 一个或多个 ingest payload(**一颗知识 = 一个 payload**,一帖通常 1-2 颗,共享 `source.id`)。中间滚动产物是 `framework.json`。
+
+## 只收创作、不收制作
+
+只收**指导"怎么想 / 怎么判断 / 怎么搭框架"**的知识(创作);剔除**"怎么执行 / 怎么操作 / 怎么做出来"**的纯工艺(制作:器材/打光/剪辑软件/调色/导出)。边界按"设计决策 vs 工艺执行"判,不看词面(景别/构图/字幕=设计决策,保留)。
+
+## 概念速览
+
+- **一颗 = 一个完整创作框架**:`purpose`(把某输入一路做成成品)+ 一条不可跳步的 `steps`。一帖通常 1-2 颗,**别拆成碎片**。
+- **How 是骨架**:绝大多数帖归到一个 How 框架;What(是什么/构成)、Why(为什么/原理)多数活在某步里,只有整帖纯界定/纯原理才单独成颗(简化版)。
+- **5 个标注维度**:知识类型(how/what/why)、业务阶段(灵感/选题/脚本)、创作阶段(定向/构思/结构/成文/打磨,逐步)、动作(开放·从内容提炼具体手法)、作用域(5 棵树带值·**回扣**复用现有节点)。
+- **作用域回扣**:值要对到 5 棵分类树的真实节点——对得上复用原名、对不上才新建。用 `tools/scope-link.py`(火山 embedding 最近邻,基建已落地)。
+
+## 目录里有什么
+
+### 操作流程(extraction/ —— 分三阶段,按阶段读)
+
+| 文件 | 内容 | 何时读 |
+|---|---|---|
+| [extraction/phase1-skeleton.md](extraction/phase1-skeleton.md) | 读懂→判几颗→剔制作→出框架骨架(purpose+steps) | 第一阶段 |
+| [extraction/phase2-normalize.md](extraction/phase2-normalize.md) | 知识类型/业务阶段/创作阶段/动作 + 作用域回扣 | 第二阶段 |
+| [extraction/phase3-finalize.md](extraction/phase3-finalize.md) | 组装 payload + lint + 自检 + 逃生 | 第三阶段 |
+
+### 契约 / 词表 / 样例 / 工具
+
+| 路径 | 内容 |
+|---|---|
+| [format/ingest-payload.schema.json](format/ingest-payload.schema.json) | 给机器看的字段清单(最终裁判) |
+| [taxonomy/知识类型.json](taxonomy/知识类型.json) · [业务阶段.json](taxonomy/业务阶段.json) · [创作阶段.json](taxonomy/创作阶段.json) | 受控词表(每条带分类说明+边界) |
+| [taxonomy/作用域.md](taxonomy/作用域.md) | 5 棵树**不放静态词表**——实时回扣 DB(说明在这) |
+| [examples/金标样例.md](examples/金标样例.md) | 创作帖 → 完整 payload 的 few-shot(How / What / Why 各一) |
+| [tools/tools.md](tools/tools.md) | scope-link / lint-payload / ingest-post 接口手册 |
+
+## 操作流程
+
+整个流程围绕**一个滚动文件 `framework.json`**:第一阶段搭骨架,第二阶段就地补标注,第三阶段组装成 `payload.json`。
+
+```
+第一阶段 · 搭框架骨架   读懂(多模态)→判几颗→剔制作→purpose+steps   → framework.json(骨架)
+第二阶段 · 归类标注     类型/业务阶段/创作阶段/动作 + 作用域回扣      → framework.json(就地补)
+第三阶段 · 组装收尾     拼 content+维度 → payload.json → lint → 自检  → payload.json(最终)
+```
+
+## 上下文纪律(重要,照做省预算)
+
+- **本文读一遍,别回头重读**;需要某阶段细节直接读对应阶段文件。
+- **读过的文件别重复 Read**——内容已在记忆里。只想确认某概念用 Grep,只想看目录用 Glob。
+- 进了第二/三阶段**别重读**第一阶段文件。
+- 中断后接着做:用户**可能改过**的产物(framework.json / payload.json)要重读;skill 本身没变,不重读。
+
+## 卡住了怎么办
+
+某阶段过关条件过不去 → **别硬闯下一阶段**,回当前阶段修。修两次还过不去 → 在该颗挂 `"inferred": true, "inferred_reason": "反复过不去,需人工看"`,再往下走。

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

@@ -0,0 +1,117 @@
+# 金标样例 · 创作帖 → IngestPayload
+
+三个成品样例(How 端到端、What、Why)。提取时对照它们的粒度与字段。
+
+---
+
+## 样例一(How)· 撕裂共识选题框架
+
+一篇"怎么找能爆的选题"的帖子 → **一颗 How 框架**(不是 4 个碎片,4 步是同一框架的内部步骤)。
+
+### 第一、二阶段的 framework.json(节选一颗)
+
+```json
+{
+  "id": "k1",
+  "knowledge_type": "how",
+  "title": "撕裂共识选题框架",
+  "purpose": "把一个赛道,按「列共识→定失灵场景→撕裂裂缝写选题→羞耻感验收」做成一个能引爆传播的选题",
+  "业务阶段": ["选题"],
+  "steps": [
+    { "id": "s1", "intent": "分析,锁定赛道里被反复重复的正确共识",
+      "directive": "列出本赛道被反复说、大家默认正确的共识。例:『护肤赛道——早C晚A、含酒精的就是不好』",
+      "output": "赛道共识清单", "出处": "正文",
+      "创作阶段": "定向", "动作": "列赛道共识",
+      "scopes": [{ "scope_type": "substance", "value": "赛道共识" }] },
+    { "id": "s2", "intent": "构思,找这个共识在哪些场景会失灵",
+      "directive": "对每条共识,找它不成立/反例的具体场景,越具体越好",
+      "output": "失灵场景", "出处": "正文",
+      "创作阶段": "构思", "动作": "定失灵场景",
+      "scopes": [{ "scope_type": "effect", "value": "撕裂共识" }] },
+    { "id": "s3", "intent": "成文,在裂缝处写出撕裂共识的选题",
+      "directive": "把失灵场景写成一个挑战默认共识的选题句,制造认知冲突",
+      "output": "撕裂式选题", "出处": "正文",
+      "创作阶段": "成文", "动作": "撕裂共识写选题",
+      "scopes": [{ "scope_type": "feeling", "value": "羞耻感/冒犯" }] },
+    { "id": "s4", "intent": "打磨,用羞耻感验收选题够不够冒犯",
+      "directive": "自检:这个选题会不会让坚持旧共识的人感到被冒犯/羞耻?不会就再撕深一点",
+      "output": "定稿选题", "出处": "正文",
+      "创作阶段": "打磨", "动作": "羞耻感验收",
+      "scopes": [{ "scope_type": "intent", "value": "引爆传播" }] }
+  ],
+  "dropped": ["『用某排版工具做封面』—— 制作工艺执行,剔除"]
+}
+```
+
+### 第三阶段组装出的 payload.json(这颗 → 一个 payload)
+
+```json
+{
+  "source": { "id": "xhs_699308fa0000000016009697", "source_type": "post",
+              "title": "真正能爆的选题,都在撕裂共识", "author": "某博主",
+              "source_metadata": { "platform": "小红书", "url": "…", "date": "2026 上半年" } },
+  "title": "撕裂共识选题框架",
+  "content": "目标:把一个赛道,按「列共识→定失灵场景→撕裂裂缝写选题→羞耻感验收」做成一个能引爆传播的选题\n步骤1(目的:分析,锁定赛道里被反复重复的正确共识)\n  指引:列出本赛道被反复说、大家默认正确的共识。例:『护肤赛道——早C晚A、含酒精的就是不好』\n  产出:赛道共识清单\n步骤2(目的:构思,找这个共识在哪些场景会失灵)\n  指引:对每条共识,找它不成立/反例的具体场景,越具体越好\n  产出:失灵场景\n步骤3(目的:成文,在裂缝处写出撕裂共识的选题)\n  指引:把失灵场景写成一个挑战默认共识的选题句,制造认知冲突\n  产出:撕裂式选题\n步骤4(目的:打磨,用羞耻感验收选题够不够冒犯)\n  指引:自检这个选题会不会让坚持旧共识的人感到被冒犯/羞耻;不会就再撕深一点\n  产出:定稿选题",
+  "dim_creations": ["创作"],
+  "dim_attributes": ["how工序"],
+  "scopes": [
+    { "scope_type": "substance", "value": "赛道共识" },
+    { "scope_type": "effect", "value": "撕裂共识" },
+    { "scope_type": "feeling", "value": "羞耻感/冒犯" },
+    { "scope_type": "intent", "value": "引爆传播" }
+  ],
+  "custom_ext": [
+    { "key": "业务阶段", "type": "str", "value": "选题" },
+    { "key": "创作阶段", "type": "str", "value": "定向" },
+    { "key": "创作阶段", "type": "str", "value": "构思" },
+    { "key": "创作阶段", "type": "str", "value": "成文" },
+    { "key": "创作阶段", "type": "str", "value": "打磨" },
+    { "key": "动作", "type": "str", "value": "列赛道共识" },
+    { "key": "动作", "type": "str", "value": "定失灵场景" },
+    { "key": "动作", "type": "str", "value": "撕裂共识写选题" },
+    { "key": "动作", "type": "str", "value": "羞耻感验收" }
+  ]
+}
+```
+
+要点:4 步压成 1 颗;scopes = 各步去重并集;创作阶段逐步去重;动作每步一个具体手法;制作项进 dropped。
+
+---
+
+## 样例二(What)· 短视频脚本三要素
+
+整帖只是**界定/列举**"脚本由哪几块组成",没有"怎么一步步做" → 纯 What,简化版(无 steps、无创作阶段/动作)。
+
+```json
+{
+  "source": { "id": "xhs_xxx", "source_type": "post", "title": "脚本就这三样" },
+  "title": "短视频脚本三要素",
+  "content": "界定:一份短视频脚本的内容骨架由三类要素构成。\n构成:\n- 人物:谁出镜、出镜形象\n- 场景:室内 / 室外\n- 事件:整条脚本的故事内容",
+  "dim_creations": ["创作"],
+  "dim_attributes": ["what构成"],
+  "scopes": [ { "scope_type": "substance", "value": "脚本要素" },
+              { "scope_type": "form", "value": "分镜骨架" } ],
+  "custom_ext": [ { "key": "业务阶段", "type": "str", "value": "脚本" } ]
+}
+```
+
+---
+
+## 样例三(Why)· 标题决定打开,与内容质量无关
+
+整帖**只讲原理**,没有工序 → 纯 Why,简化版。
+
+```json
+{
+  "source": { "id": "xhs_yyy", "source_type": "post", "title": "标题才是第一生产力" },
+  "title": "标题决定打开,与内容质量无关",
+  "content": "主张:标题的唯一作用是让人有欲望点开;点不点开,和内容好不好没关系。\n依据:信息流里用户先看到的是标题,内容再好没人点开等于零(平台分发逻辑)。\n对创作的影响:把『起标题』当成独立的、第一优先的创作决策,而非内容概括。",
+  "dim_creations": ["创作"],
+  "dim_attributes": ["why原理"],
+  "scopes": [ { "scope_type": "effect", "value": "吸引点击" },
+              { "scope_type": "intent", "value": "提升打开率" } ],
+  "custom_ext": [ { "key": "业务阶段", "type": "str", "value": "选题" } ]
+}
+```
+
+> What/Why 只保留「业务阶段」,**没有**创作阶段/动作(没步骤)。其余字段(source/dim_creations/scopes 带值回扣)与 How 一致。

+ 106 - 0
创作知识提取-skill/extraction/phase1-skeleton.md

@@ -0,0 +1,106 @@
+# 第一阶段 · 搭框架骨架
+
+要做的事:读懂帖子(含图/视频)→ 判这帖出几颗知识 → 剔掉制作/碎片 → 把每颗写成一个完整创作框架(目标 + 有序步骤)。产物:`framework.json`(骨架版,第二阶段在它上面就地补字段)。
+
+## 步骤
+
+| 小步 | 做什么 | 产出 |
+|---|---|---|
+| **1.1** | **读懂 + 判几颗** —— 通读正文 + 图(图文帖逐图看)/ 视频整段;想清楚这帖有**几颗独立知识**(通常 1-2 颗),每颗的框架名、最终交付物、大概几步。同时**剔除**制作/碎片/过宽内容。 | `framework.json` 的 `frameworks[]` 条数 + 各 `dropped` |
+| **1.2** | **出每颗的框架骨架** —— 写出 `framework.json`:每颗 `purpose` + 有序 `steps`(每步 `intent` / `directive` / `output` / `出处`)。 | `framework.json`(骨架版) |
+
+---
+
+## 一颗知识的粒度(铁律,最容易错)
+
+**一颗知识 = 一个完整创作框架** = 一个 `purpose`(把某输入一路做成可交付成品)+ 一条**不可跳步**的 `steps` 链。**一帖通常 1-2 颗。** 绝不把框架内部的步骤/要点拆成独立颗。
+
+- **判 step(压进框架、不单列)**:①产出半成品 ②被同帖另一块当输入消费 ③拿走则本颗成品残缺。满足任一即为 step。
+- **判独立颗**:①有自己的 purpose 句 ②产出能独立交付 ③脱离本框架换题材照样跑通。三条都满足才另立一颗。
+- **清单/导图型**:一堆并列要素(N 选 1)是「一个 step 内的菜单」,压进所属 step,**不按项数拆颗**。
+- 踩过的坑:早期把一帖拆成 5-6 个碎片是错的——碎片其实是同一框架内部的步骤/指引细节。
+
+---
+
+## How / What / Why:优先按 How 提
+
+- **优先 How**:绝大多数创作教学帖都能归到**一个 How 框架**(purpose + 有序 steps)。先试着按 How 提。
+- **What / Why 多数活在 How 里**,不单独成颗:某步「产出一个有构成的东西」= 该步的 What 性质,写进 `output`;directive 里「为什么这样选/原理/标准」= Why,写进 `directive`。
+- **例外(单独成颗,用简化骨架)**:整帖只是**列举/界定**(纯 What)或**只讲原理**(纯 Why),套不进 purpose+steps。这时这颗没有 `steps`,改填:
+  - What:`purpose` 写「界定:<一句话这是什么>」+ `构成`: [{要素, 说明}, …]
+  - Why:`purpose` 写「主张:<核心观点>」+ `依据` + `对创作的影响`
+
+判到底是 how/what/why 先记在该颗的 `knowledge_type`(第二阶段会复核并决定 dim_attributes)。
+
+---
+
+## 创作 vs 制作的边界(剔除时按这条判)
+
+**收**(创作知识):普适的创作路径/方法/工序、垂类技巧、形式偏好与禁忌——能一步步指导创作、**产出因人而异**。
+
+**剔除**(写进 `dropped`,附一句理由):
+- **制作 = 纯工艺执行**:器材 / 光圈ISO / 打光 / 收音设备 / 剪辑软件操作 / 调色参数 / 导出压制("拿方案去操作设备软件实现它")。
+- **具体素材/案例本身**:"此类内容具体讲了什么"的内容本体。
+- **过于宽泛**:"内容要有吸引力"这类无法约束某个具体创作决策的话。
+
+**关键边界(按"设计决策 vs 工艺执行"判,不看词面):** 景别/角度/运镜/构图/字幕/配乐/时长——**名字像制作、实为"把创意翻译成镜头语言"的设计决策**,写在脚本/分镜上 → **保留为框架内 step**;只有"拿方案操作设备软件"那层才剔除。
+
+---
+
+## 忠实原则(directive 怎么写)
+
+- `directive` 说清这一步**怎么做、关键操作原则/判断标准**;用自然语言,别写成数据流公式。
+- **原帖给出的具体示例(例句、案例、样本)必须原文保留**,格式 `例:『…』`,紧跟对应要点之后。
+- **不要编原文没有的例子**;判断不出的字段留空,别硬填假值。
+
+## 隐含步骤要补(inferred)
+
+不可跳步但原帖没明说的中间步骤要补上,并标 `"inferred": true, "inferred_reason": "…"`(让后面复核)。只补**工艺上必然需要**的;只是换个说法、或归类标注(第二阶段的活)不算补。
+
+---
+
+## framework.json 骨架模板(第一阶段产出)
+
+复制改即可。`scopes / 创作阶段 / 动作 / 业务阶段 / 作用域并集` 留给第二阶段,这里先不填。
+
+```json
+{
+  "source": {
+    "id": "<xhs_<content_id> 等>",
+    "source_type": "post",
+    "title": "<原帖标题>",
+    "author": "<作者>",
+    "source_metadata": { "platform": "<小红书/抖音…>", "url": "<…>", "date": "<…>" }
+  },
+  "frameworks": [
+    {
+      "id": "k1",
+      "knowledge_type": "<how | what | why>",
+      "title": "<框架名,如 撕裂共识选题框架>",
+      "purpose": "<how: 一句话目标(把…一路做成…);what: 界定:…;why: 主张:…>",
+      "想要": "<可选:这颗帮用户得到什么>",
+      "steps": [
+        {
+          "id": "s1",
+          "intent": "<一句话目的,≤25字,第二阶段会复核>",
+          "directive": "<怎么做+判断标准;原帖示例用 例:『…』 嵌入>",
+          "output": "<这步的产出物>",
+          "出处": "<正文 / 图3 / 视频 02:10 等>"
+        }
+      ],
+      "构成": "<仅 what 颗:[{\"要素\":\"…\",\"说明\":\"…\"}],无 steps 时用>",
+      "依据": "<仅 why 颗>",
+      "对创作的影响": "<仅 why 颗>",
+      "dropped": ["<剔除项:xxx —— 理由(制作/碎片/过宽)>"]
+    }
+  ]
+}
+```
+
+> 纯 What/Why 颗没有 `steps`,用 `构成` 或 `依据`+`对创作的影响` 代替。
+
+## 过关条件 → 进第二阶段
+
+- 颗数合理(1-2,每颗是完整框架不是碎片);制作/碎片/过宽都进了 `dropped`。
+- How 颗:每步 `intent`/`directive`/`output`/`出处` 都填了;步骤不可跳步、数据接得上。
+- directive 忠实(保留了原帖示例、没编造)。

+ 66 - 0
创作知识提取-skill/extraction/phase2-normalize.md

@@ -0,0 +1,66 @@
+# 第二阶段 · 归类标注 + 作用域回扣
+
+在第一阶段的 `framework.json` 骨架上**就地补字段**(Edit,不另存)。给每颗补 5 类标注。
+
+### 起手:把词表读进来(各读一遍,别重读)
+
+- `taxonomy/知识类型.json`(how/what/why → 定 dim_attributes + 形态)
+- `taxonomy/业务阶段.json`(灵感/选题/脚本,整颗多值)
+- `taxonomy/创作阶段.json`(定向/构思/结构/成文/打磨,逐步)
+- `taxonomy/作用域.md`(5 棵树 + 回扣怎么用)
+
+---
+
+## 1. 知识类型(整颗一个)—— 复核 + 定形态
+
+复核第一阶段的 `knowledge_type`,确认 how/what/why。它决定后面 `dim_attributes`(how工序/what构成/why原理)和 payload 形态。**绝大多数是 how**;只有套不进 purpose+steps 的纯界定/纯原理才是 what/why。
+
+## 2. 业务阶段(整颗,可多值)
+
+这颗框架覆盖了哪些业务环节,对到 `灵感/选题/脚本` 的一个或多个。整颗框架常跨阶段(如选题框架可能同时落「灵感+选题」)。写到该颗的 `业务阶段: [...]`。
+
+## 3. 创作阶段(逐步,仅 How 颗)
+
+How 颗的**每个 step** 命中创作阶段一个标准词(`定向/构思/结构/成文/打磨`)。写到 `step.创作阶段`。**必须命中**;对不上说明第一阶段这步切错了,回去改。
+- 别把「动作」当「阶段」——"撕裂共识写选题"是动作,它所在的步可能处在「构思」阶段。
+
+## 4. 动作(逐步,仅 How 颗,开放·从内容提炼)
+
+How 颗的**每个 step** 提炼一个**具体创作手法**,写到 `step.动作`。**不锁词表**——用帖子自己的语言、具体招式(`列赛道共识` / `撕裂共识写选题` / `塑三维人物` / `套三幕结构` / `羞耻感验收`),别抽象成「决策/撰写」这种空词。这是创作里最有信息量的东西,ingest 会对它自动 embed 成「手法库」可语义检索。
+
+## 5. 作用域(带值 + 回扣)—— 命门
+
+给作用域 5 类挑相关的(不必 5 类都给),每个给一个**具体值**:
+- How 颗:**逐步**标 `step.scopes: [{scope_type, value}]`(这步主要涉及实质/形式/感受/作用/意图里的哪些)。
+- What/Why 颗:整颗标 `作用域并集: [{scope_type, value}]`。
+
+| scope_type | 判别 | 例 |
+|---|---|---|
+| substance 实质 | 讲什么(题材/对象) | 赛道共识、脚本要素 |
+| form 形式 | 怎么呈现(结构/体裁/手法) | 三幕结构、分镜骨架 |
+| feeling 感受 | 勾什么情绪 | 羞耻感/冒犯、代入共鸣 |
+| effect 作用 | 起什么表达功能 | 撕裂共识、吸引点击 |
+| intent 意图 | 创作者图什么 | 引爆传播、提升打开率 |
+
+**每个值都要回扣**(避免同义词污染 5 棵树):
+
+```bash
+python 创作知识提取-skill/tools/scope-link.py "撕裂共识" --type effect --top-k 5
+```
+- score ≥ ~0.90:直接**复用返回的 `name`**(value 写这个原名,ingest 会按名挂靠现有节点)。
+- 0.75–0.90:看 top-K 里有没有真正同义的,有就复用 name,没有就用你的候选值(新值,丰富树)。
+- < 0.75:用你的候选值(新值)。
+
+> 批量回扣:把这颗所有候选值列出来逐个跑 scope-link,再回填。值用中文短词,别用长句。
+
+---
+
+## 落盘
+
+在 `framework.json` 上 **Edit** 补:每颗的 `knowledge_type`(复核)/`业务阶段`;How 每步的 `创作阶段`/`动作`/`scopes`;What·Why 颗的 `作用域并集`。**不要写 Python 脚本去改 framework.json**(容易弄坏),逐字段 Edit。
+
+## 过关条件 → 进第三阶段
+
+- 每颗 knowledge_type 定了、业务阶段填了(≥1)。
+- How 每步:创作阶段命中 5 词之一、动作是具体手法(非空词)、scopes 带值且值已回扣。
+- What/Why 颗:作用域并集带值且已回扣。

+ 72 - 0
创作知识提取-skill/extraction/phase3-finalize.md

@@ -0,0 +1,72 @@
+# 第三阶段 · 组装 payload + 校验 + 收尾
+
+把 `framework.json` 的每颗组装成一个 ingest payload,写到 `payload.json`(数组,一颗一个,同帖共享 source)。然后 lint + 自检。
+
+## 1. 组装(每颗 → 一个 payload)
+
+字段映射(详见 `format/ingest-payload.schema.json` 与《创作知识拆解框架.md》§3):
+
+| payload 字段 | 怎么填 |
+|---|---|
+| `source` | 照抄 framework.json 的 `source`(id/source_type/title/author/source_metadata) |
+| `title` | 该颗 `title`(框架名) |
+| `dim_creations` | `["创作"]`(写死) |
+| `dim_attributes` | 由 knowledge_type:how→`["how工序"]`,what→`["what构成"]`,why→`["why原理"]` |
+| `content` | 见下「content 拼法」 |
+| `scopes` | How:各步 `scopes` 值**去重并集**;What/Why:`作用域并集` |
+| `custom_ext` | 业务阶段(多值各一条);How 另加 创作阶段(各步去重) + 动作(各步各一条) |
+
+### content 拼法
+
+**How**(拍平全文):
+```
+目标:<purpose>
+步骤1(目的:<intent>)
+  指引:<directive>
+  产出:<output>
+步骤2(目的:…)
+  指引:…
+  产出:…
+```
+**What**:`界定:<purpose 的界定句>\n构成:\n- <要素>:<说明>\n- …`
+**Why**:`主张:<purpose 的主张句>\n依据:<依据>\n对创作的影响:<对创作的影响>`
+
+### custom_ext 拼法
+
+- 业务阶段:该颗 `业务阶段` 每个值一条 `{"key":"业务阶段","type":"str","value":"选题"}`。
+- 创作阶段(仅 How):各步 `创作阶段` **去重**后每个一条。
+- 动作(仅 How):各步 `动作` 每个一条(不去重——每步的手法都留)。
+- What/Why **只有业务阶段**,不要创作阶段/动作。
+
+> 完整 How 与 What/Why 的成品样例见 `examples/金标样例.md`。
+
+## 2. 校验
+
+```bash
+python 创作知识提取-skill/tools/lint-payload.py outputs/<帖>/payload.json
+```
+有 ERROR 必须修(退出码 1);WARN 逐条看是否合理。
+
+## 3. 自检 / 对抗一遍
+
+- **粒度**:每颗是一个完整框架(1 颗非碎片)?被拆碎了就回第一阶段合并。
+- **创作非制作**:content 里没混进纯工艺执行(器材/软件操作/参数)?混了就剔到 dropped。
+- **directive 忠实**:没编原文没有的例子?原帖示例都保留了?
+- **scope 值合理**:值是具体短词、都回扣过、对得上的复用了原名?
+- **dim_creations 恒 ["创作"]、dim_attributes 与 knowledge_type 一致。**
+
+## 卡住了怎么办(逃生)
+
+某阶段过关条件反复过不去 → 别硬闯。回当前阶段修;**修两次还过不去**,就在该颗挂 `"inferred": true, "inferred_reason": "反复过不去,需人工看"`,再往下走,别卡死整帖。
+
+## 4.(可选)入库
+
+确认无误后:
+```bash
+python 创作知识提取-skill/tools/ingest-post.py outputs/<帖>/payload.json --url <ingest-api> --post
+```
+默认 dry-run,加 `--post` 才真发。
+
+---
+
+**产物**:`outputs/<帖>/framework.json`(滚动中间产物)+ `outputs/<帖>/payload.json`(最终入库体)。

+ 85 - 0
创作知识提取-skill/format/ingest-payload.schema.json

@@ -0,0 +1,85 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "$id": "ingest-payload.schema.json",
+  "title": "创作知识 Ingest Payload",
+  "$comment": "POST /api/v1/knowledge/ingest 的请求体。一颗知识 = 一个 payload;一帖通常 1-2 颗,共享 source.id。How=完整工序版,What/Why=简化版(content 不同、dim_attributes 不同、custom_ext 无创作阶段/动作)。Phase3 组装后用 lint-payload.py 校验。",
+  "type": "object",
+  "required": ["source", "title", "content", "dim_creations", "dim_attributes", "scopes"],
+  "additionalProperties": false,
+  "properties": {
+    "source": {
+      "type": "object",
+      "required": ["id", "source_type", "title"],
+      "additionalProperties": true,
+      "properties": {
+        "id": { "type": "string", "$comment": "xhs_<content_id> / dy_<id> 等;同帖多颗共享" },
+        "source_type": { "type": "string", "$comment": "通常 post" },
+        "title": { "type": "string", "$comment": "原帖标题" },
+        "author": { "type": "string" },
+        "source_metadata": {
+          "type": "object",
+          "additionalProperties": true,
+          "properties": {
+            "platform": { "type": "string" },
+            "url": { "type": "string" },
+            "date": { "type": "string" }
+          }
+        }
+      }
+    },
+    "title": { "type": "string", "minLength": 1, "$comment": "知识名(框架名),如 撕裂共识选题框架" },
+    "content": {
+      "type": "string",
+      "minLength": 1,
+      "$comment": "How:目标(=purpose) + 步骤N(目的/指引/产出) 拍平全文。What:界定+构成。Why:主张+依据+对创作的影响。"
+    },
+    "dim_creations": {
+      "type": "array",
+      "items": { "const": "创作" },
+      "minItems": 1,
+      "maxItems": 1,
+      "$comment": "恒 [\"创作\"],写死"
+    },
+    "dim_attributes": {
+      "type": "array",
+      "items": {
+        "type": "string",
+        "pattern": "^(how|what|why)",
+        "$comment": "how工序 / what构成(或 what清单/what界定) / why原理(或 why标准/why心法)"
+      },
+      "minItems": 1,
+      "maxItems": 1
+    },
+    "scopes": {
+      "type": "array",
+      "items": {
+        "type": "object",
+        "required": ["scope_type", "value"],
+        "additionalProperties": false,
+        "properties": {
+          "scope_type": {
+            "type": "string",
+            "enum": ["substance", "form", "feeling", "effect", "intent"]
+          },
+          "value": { "type": "string", "minLength": 1, "$comment": "带值;回扣到 5 棵树现有节点,对得上复用原名" }
+        }
+      },
+      "minItems": 1,
+      "$comment": "整颗 = 各步 scope 值去重并集"
+    },
+    "custom_ext": {
+      "type": "array",
+      "items": {
+        "type": "object",
+        "required": ["key", "type", "value"],
+        "additionalProperties": false,
+        "properties": {
+          "key": { "type": "string", "enum": ["业务阶段", "创作阶段", "动作"] },
+          "type": { "const": "str" },
+          "value": { "type": "string", "minLength": 1 }
+        }
+      },
+      "$comment": "业务阶段(灵感/选题/脚本) + 创作阶段(定向/构思/结构/成文/打磨,仅How) + 动作(开放,从内容提炼,仅How);多值各一条"
+    }
+  }
+}

+ 26 - 0
创作知识提取-skill/taxonomy/业务阶段.json

@@ -0,0 +1,26 @@
+{
+  "$comment": "业务阶段 受控词表(固定 3,由本项目定)。一整颗框架常跨多个业务阶段——多值。Phase2 读。落到 custom_ext(key=业务阶段, type=str, 多值)。",
+  "$kind": "taxonomy",
+  "$dimension": "业务阶段",
+  "$field": "业务阶段",
+  "落到": "custom_ext.业务阶段",
+  "粒度": "整颗框架级(看这颗框架覆盖了哪些业务环节,可多值)",
+  "最终分类树": [
+    {
+      "分类名称": "灵感",
+      "分类说明": "帮助发现方向 / 素材 / 切口 / 洞察——还没定写什么,先找『有什么可写、从哪冒出念头』。典型:找选题灵感来源、积累素材、捕捉洞察。判别口诀:这颗知识在帮人『发现』,而不是『判断写哪个』或『怎么组织表达』。",
+      "分类性质": "业务环节"
+    },
+    {
+      "分类名称": "选题",
+      "分类说明": "帮助判断写什么 / 拍什么 / 从哪个角度切入——在候选里定下要做的那个,含起标题(标题也算选题)。典型:选题判断框架、角度选择、标题方法。判别口诀:这颗知识在帮人『决定做哪个、从哪个角度切』。",
+      "分类性质": "业务环节"
+    },
+    {
+      "分类名称": "脚本",
+      "分类说明": "帮助组织表达顺序 / 文案 / 镜头 / 结构——选题已定,开始把内容搭出来、写出来。典型:脚本结构、分镜、叙事顺序、文案撰写。判别口诀:这颗知识在帮人『把定了的题组织成具体内容』。",
+      "分类性质": "业务环节"
+    }
+  ],
+  "$leaves": ["灵感", "选题", "脚本"]
+}

+ 28 - 0
创作知识提取-skill/taxonomy/作用域.md

@@ -0,0 +1,28 @@
+# 作用域(5 棵树)—— 不放静态词表,实时回扣
+
+作用域是 5 棵分类树(不是固定 enum),节点上千且**不断生长**,所以这里**不放静态 JSON**——Phase2 实时回扣到 DB 里的真实节点。
+
+| scope_type | 中文 | 判别(带值时各给一个具体词) |
+|---|---|---|
+| `substance` | 实质 | 讲**什么**(题材 / 对象 / 内容实质),如 `赛道共识`、`脚本要素` |
+| `form` | 形式 | **怎么呈现**(结构 / 体裁 / 表现手法),如 `三幕结构`、`分镜骨架` |
+| `feeling` | 感受 | 勾**什么情绪**(读者/观众的情感反应),如 `羞耻感/冒犯`、`角色代入共鸣` |
+| `effect` | 作用 | 起**什么表达功能**(在内容里干什么活),如 `撕裂共识`、`吸引点击` |
+| `intent` | 意图 | 创作者**图什么**(最终目的),如 `引爆传播`、`提升打开率` |
+
+## 回扣(scope-link)怎么用
+
+值要**回扣到 5 棵树的现有节点**——对得上就复用原名(避免同义词污染树),对不上才保留为新值(顺带丰富树)。基建已落地:
+
+```bash
+# 候选值 + 树名 → 火山 embedding 余弦最近邻 top-K
+python 创作知识提取-skill/tools/scope-link.py "撕裂共识" --type effect --top-k 5
+```
+
+- 命中 ≥ ~0.90 → 直接复用返回的 `name`(payload 的 scope value 写这个原名)。
+- 0.75–0.90 → 看 top-K 里有没有真同义的,有就复用,没有就用自己的候选值(新值)。
+- < 0.75 → 保留候选值为新值。
+
+底层:`scripts/scope_link.py`(ScopeLinker)+ `creation_knowledge/embedding.py`(火山 Doubao-embedding-vision,2048 维)+ `scope_trees/`(1855 个 live 节点的本地向量缓存)。树更新后重跑 `scripts/dump_trees.py` + `scripts/embed_trees.py` 刷新缓存。
+
+> `--type` 用英文 key(substance/form/feeling/effect/intent)或中文(实质/形式/感受/作用/意图)都行;底层按 source_type 中文名过滤。

+ 36 - 0
创作知识提取-skill/taxonomy/创作阶段.json

@@ -0,0 +1,36 @@
+{
+  "$comment": "创作阶段 受控词表(固定 5,不开放)。逐步标——框架里每个 step 命中一个。与业务阶段分工:业务阶段是粗的业务环节(整颗多值),创作阶段是框架内每步的细工序位(每步一个)。Phase2 读。落到 custom_ext(key=创作阶段, type=str, 每步一个、整颗去重多值)。",
+  "$kind": "taxonomy",
+  "$dimension": "创作阶段",
+  "$field": "创作阶段",
+  "落到": "custom_ext.创作阶段",
+  "粒度": "step 级(每步一个;整颗 = 各步去重并集)",
+  "最终分类树": [
+    {
+      "分类名称": "定向",
+      "分类说明": "定主题 / 方向 / 受众——尚未开始具体创作。这一步在锁定『要做什么、给谁』。典型:锁定赛道、确定目标人群、定主题方向。",
+      "分类性质": "工序位"
+    },
+    {
+      "分类名称": "构思",
+      "分类说明": "生成核心创意 / 钩子 / 角度 / 冲突——方向已定,想法待产。这一步在『憋出那个点子』。典型:想钩子、设冲突、找反差角度。",
+      "分类性质": "工序位"
+    },
+    {
+      "分类名称": "结构",
+      "分类说明": "搭骨架 / 大纲 / 顺序——想法有了,组成骨架。这一步在『排出先后模块』。典型:搭三幕、排叙事顺序、列大纲。",
+      "分类性质": "工序位"
+    },
+    {
+      "分类名称": "成文",
+      "分类说明": "把结构写成具体文字 / 镜头——从框架到内容。这一步在『落成可见的成品文字/分镜』。典型:写文案、写分镜台词、把大纲扩写成稿。",
+      "分类性质": "工序位"
+    },
+    {
+      "分类名称": "打磨",
+      "分类说明": "修改 / 精炼 / 优化 / 定稿——从有到更好。这一步在『把已有内容改到位』。典型:精炼标题、删冗、用标准验收稿子。",
+      "分类性质": "工序位"
+    }
+  ],
+  "$leaves": ["定向", "构思", "结构", "成文", "打磨"]
+}

+ 31 - 0
创作知识提取-skill/taxonomy/知识类型.json

@@ -0,0 +1,31 @@
+{
+  "$comment": "知识类型 受控词表(创作知识版)。三个扁平类型 how / what / why。决定 ① dim_attributes 取值;② payload 形态:how→完整工序版,what/why→简化版。Phase2 读。",
+  "$kind": "taxonomy",
+  "$dimension": "知识类型",
+  "$field": "知识类型",
+  "落到": "dim_attributes",
+  "最终分类树": [
+    {
+      "分类名称": "how",
+      "dim_attributes值": "how工序",
+      "payload形态": "完整(目标 purpose + 有序 steps,每步 目的/指引/产出)",
+      "分类说明": "这颗知识是『怎么做』——一条不可跳步的创作工序:把某种输入一路做成一个可交付成品。典型:选题框架、脚本结构搭建法、分镜创作流程。判别口诀:读完主要收获是『知道一步步怎么搭出这个东西』,有明确先后顺序。与 what 边界:how 是过程/步骤,what 是界定/列举。与 why 边界:how 是执行层(具体怎么做),why 是原理层(为什么这样做)。绝大多数创作教学帖都能归到一个 how 框架——优先按 how 提。",
+      "分类性质": "工序"
+    },
+    {
+      "分类名称": "what",
+      "dim_attributes值": "what构成",
+      "payload形态": "简化(界定 + 构成,无 steps)",
+      "分类说明": "这颗知识是『是什么 / 由什么构成』——对某创作对象的界定、要素清单、模板、风格示例,但不讲怎么一步步做。典型:脚本三要素、爆款标题的构成要素、某风格的特征清单。判别口诀:读完主要收获是『了解这东西是什么、由哪几块组成』,是界定性/列举性的。注意:若这些要素其实是某工序里某一步的产出类型,应并进那颗 how 框架的对应 step,不单独成颗——只有整帖就是纯界定/清单、套不进 purpose+steps 时才用 what。",
+      "分类性质": "构成"
+    },
+    {
+      "分类名称": "why",
+      "dim_attributes值": "why原理",
+      "payload形态": "简化(主张 + 依据 + 对创作的影响,无 steps)",
+      "分类说明": "这颗知识是『为什么』——解释某种创作做法/判断背后的原理、依据、平台逻辑、读者心理。典型:为什么标题决定打开率、为什么先写冲突、为什么这种结构传播好。判别口诀:读完主要收获是『理解了某件事为什么这样做』,是解释性/原理性的,较少具体步骤。注意:若原理是某步 directive 里『为什么这样选』的判断依据,应写进那颗 how 框架对应步的指引,不单独成颗——只有整帖就是纯讲道理、没有工序时才用 why。",
+      "分类性质": "原理"
+    }
+  ],
+  "$leaves": ["how", "what", "why"]
+}

+ 46 - 0
创作知识提取-skill/tools/ingest-post.py

@@ -0,0 +1,46 @@
+#!/usr/bin/env python3
+"""可选:把 payload.json POST 到 /api/v1/knowledge/ingest。
+
+默认 dry-run(只打印将发什么);加 --post 才真正发送。
+端点/令牌从参数或 env(INGEST_API / INGEST_TOKEN)取;先跑 lint-payload.py 校验过再发。
+用法:
+    python 创作知识提取-skill/tools/ingest-post.py outputs/<帖>/payload.json --url https://host/api/v1/knowledge/ingest --post
+"""
+from __future__ import annotations
+
+import argparse
+import json
+import os
+from pathlib import Path
+
+import httpx
+
+
+def main() -> None:
+    ap = argparse.ArgumentParser()
+    ap.add_argument("payload")
+    ap.add_argument("--url", default=os.getenv("INGEST_API", ""))
+    ap.add_argument("--token", default=os.getenv("INGEST_TOKEN", ""))
+    ap.add_argument("--post", action="store_true", help="真正发送(默认 dry-run)")
+    args = ap.parse_args()
+
+    data = json.loads(Path(args.payload).read_text(encoding="utf-8"))
+    payloads = data if isinstance(data, list) else [data]
+    print(f"{len(payloads)} 颗知识待入库 → {args.url or '(未指定 --url)'}")
+
+    if not args.post:
+        print("dry-run:未发送。确认无误后加 --post。")
+        return
+    if not args.url:
+        raise SystemExit("缺 --url / INGEST_API")
+    headers = {"Content-Type": "application/json"}
+    if args.token:
+        headers["Authorization"] = f"Bearer {args.token}"
+    for i, p in enumerate(payloads, 1):
+        r = httpx.post(args.url, json=p, headers=headers, timeout=30)
+        print(f"  [{i}] {r.status_code} {r.text[:200]}")
+        r.raise_for_status()
+
+
+if __name__ == "__main__":
+    main()

+ 111 - 0
创作知识提取-skill/tools/lint-payload.py

@@ -0,0 +1,111 @@
+#!/usr/bin/env python3
+"""校验创作知识 ingest payload —— 结构 + How/What/Why 一致性。
+
+接受单个 payload 对象或 payload 数组(一帖多颗)。报 ERROR(必须修)/ WARN(建议看)。
+用法:
+    python 创作知识提取-skill/tools/lint-payload.py outputs/<帖>/payload.json
+"""
+from __future__ import annotations
+
+import json
+import sys
+from pathlib import Path
+
+SCOPE_TYPES = {"substance", "form", "feeling", "effect", "intent"}
+EXT_KEYS = {"业务阶段", "创作阶段", "动作"}
+业务阶段 = {"灵感", "选题", "脚本"}
+创作阶段 = {"定向", "构思", "结构", "成文", "打磨"}
+
+
+def lint_one(p: dict, idx: int) -> tuple[list[str], list[str]]:
+    err: list[str] = []
+    warn: list[str] = []
+    tag = f"[#{idx} {p.get('title', '?')}]"
+
+    # 必填
+    for f in ("source", "title", "content", "dim_creations", "dim_attributes", "scopes"):
+        if not p.get(f):
+            err.append(f"{tag} 缺必填字段 {f}")
+    src = p.get("source") or {}
+    for f in ("id", "source_type", "title"):
+        if not src.get(f):
+            err.append(f"{tag} source.{f} 缺失")
+
+    # dim_creations 写死
+    if p.get("dim_creations") != ["创作"]:
+        err.append(f"{tag} dim_creations 必须恒为 [\"创作\"],实为 {p.get('dim_creations')}")
+
+    # dim_attributes 单值 + 类型前缀
+    da = p.get("dim_attributes") or []
+    if len(da) != 1:
+        err.append(f"{tag} dim_attributes 应恰好 1 个,实为 {da}")
+    ktype = (da[0][:3] if da else "")  # how / wha / why
+    kind = {"how": "how", "wha": "what", "why": "why"}.get(ktype)
+    if kind is None:
+        err.append(f"{tag} dim_attributes[0] 须以 how/what/why 开头,实为 {da}")
+
+    # scopes
+    for s in p.get("scopes") or []:
+        if s.get("scope_type") not in SCOPE_TYPES:
+            err.append(f"{tag} scope_type 非法:{s.get('scope_type')}")
+        if not s.get("value"):
+            err.append(f"{tag} scope 缺 value(须带值)")
+
+    # custom_ext
+    ext = p.get("custom_ext") or []
+    keys_present = set()
+    for e in ext:
+        k = e.get("key")
+        keys_present.add(k)
+        if k not in EXT_KEYS:
+            err.append(f"{tag} custom_ext.key 非法:{k}")
+        if e.get("type") != "str":
+            err.append(f"{tag} custom_ext.type 须为 str,实为 {e.get('type')}")
+        v = e.get("value")
+        if not v:
+            err.append(f"{tag} custom_ext[{k}] 缺 value")
+        elif k == "业务阶段" and v not in 业务阶段:
+            err.append(f"{tag} 业务阶段 非法值:{v}(应 ∈ {sorted(业务阶段)})")
+        elif k == "创作阶段" and v not in 创作阶段:
+            err.append(f"{tag} 创作阶段 非法值:{v}(应 ∈ {sorted(创作阶段)})")
+
+    # How vs What/Why 形态一致性
+    if kind == "how":
+        if "创作阶段" not in keys_present:
+            warn.append(f"{tag} how 颗通常每步带『创作阶段』,custom_ext 里没有")
+        if "动作" not in keys_present:
+            warn.append(f"{tag} how 颗通常带『动作』(从内容提炼),custom_ext 里没有")
+        c = p.get("content", "")
+        if "步骤" not in c and "目标" not in c:
+            warn.append(f"{tag} how 的 content 建议含『目标…』+『步骤N…』结构")
+    elif kind in ("what", "why"):
+        bad = keys_present & {"创作阶段", "动作"}
+        if bad:
+            err.append(f"{tag} {kind} 简化版不应有 {bad}(只保留业务阶段)")
+    if ext and "业务阶段" not in keys_present:
+        warn.append(f"{tag} custom_ext 建议至少有一个『业务阶段』")
+
+    return err, warn
+
+
+def main() -> None:
+    if len(sys.argv) < 2:
+        print("用法:lint-payload.py <payload.json>")
+        sys.exit(2)
+    data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
+    payloads = data if isinstance(data, list) else [data]
+    all_err, all_warn = [], []
+    for i, p in enumerate(payloads, 1):
+        e, w = lint_one(p, i)
+        all_err += e
+        all_warn += w
+    for w in all_warn:
+        print("WARN ", w)
+    for e in all_err:
+        print("ERROR", e)
+    print(f"\n{len(payloads)} 颗知识:{len(all_err)} ERROR / {len(all_warn)} WARN")
+    sys.exit(1 if all_err else 0)
+
+
+if __name__ == "__main__":
+    main()

+ 48 - 0
创作知识提取-skill/tools/scope-link.py

@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+"""作用域回扣 CLI —— 候选值 → 5 棵树本地向量缓存余弦最近邻 top-K。
+
+复用基建:scripts.scope_link.ScopeLinker(+ creation_knowledge.embedding + scope_trees/)。
+从仓库根目录跑(自动 chdir);需 .env 里的 ARK_* 与本地 scope_trees/(先跑 dump_trees+embed_trees)。
+
+用法:
+    python 创作知识提取-skill/tools/scope-link.py "撕裂共识" --type effect --top-k 5
+    python 创作知识提取-skill/tools/scope-link.py "三幕结构" --type 形式
+type 接受英文 key 或中文(不传则跨 5 棵树搜)。
+"""
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import sys
+from pathlib import Path
+
+REPO_ROOT = Path(__file__).resolve().parents[2]
+
+# scope_type 英文 → 树 source_type 中文(trees_index 里存中文)
+_TYPE_MAP = {
+    "substance": "实质", "form": "形式", "feeling": "感受",
+    "effect": "作用", "intent": "意图",
+    "实质": "实质", "形式": "形式", "感受": "感受", "作用": "作用", "意图": "意图",
+}
+
+
+def main() -> None:
+    ap = argparse.ArgumentParser(description="作用域回扣 top-K")
+    ap.add_argument("candidate", help="候选作用域值,如 撕裂共识")
+    ap.add_argument("--type", default=None, help="substance/form/feeling/effect/intent 或中文;不传=跨树")
+    ap.add_argument("--top-k", type=int, default=5)
+    args = ap.parse_args()
+
+    os.chdir(REPO_ROOT)  # ScopeLinker 按 CWD 读 scope_trees/ 与 .env
+    sys.path.insert(0, str(REPO_ROOT))
+    from scripts.scope_link import ScopeLinker
+
+    src = _TYPE_MAP.get(args.type, args.type) if args.type else None
+    hits = ScopeLinker().link(args.candidate, source_type=src, top_k=args.top_k)
+    print(json.dumps({"candidate": args.candidate, "type": src, "hits": hits},
+                     ensure_ascii=False, indent=2))
+
+
+if __name__ == "__main__":
+    main()

+ 35 - 0
创作知识提取-skill/tools/tools.md

@@ -0,0 +1,35 @@
+# tools/ 接口手册
+
+三个脚本,会用就行,**不用读源码**。都从仓库根目录跑(脚本内部会处理路径)。
+
+## scope-link.py —— 作用域回扣(Phase2 用)
+
+候选作用域值 → 5 棵树本地向量缓存余弦最近邻 top-K。判「复用现有节点原名 or 保留新值」。
+
+```bash
+python 创作知识提取-skill/tools/scope-link.py "撕裂共识" --type effect --top-k 5
+python 创作知识提取-skill/tools/scope-link.py "三幕结构" --type 形式
+```
+- `--type`:substance/form/feeling/effect/intent 或中文;不传 = 跨 5 棵树搜。
+- 输出 JSON:`{candidate, type, hits:[{name, path, source_type, score}]}`。
+- 判读:score ≥ ~0.90 复用 `name`;0.75–0.90 看 top-K 有无真同义;< 0.75 用候选值(新值)。
+- 依赖 `scope_trees/` 本地缓存(先 `python scripts/dump_trees.py` + `python scripts/embed_trees.py`,需 PYTHONPATH=. 与 .env)。
+
+## lint-payload.py —— 校验(Phase3 用)
+
+校验单个 payload 或 payload 数组(一帖多颗):结构 + dim_creations 写死 + dim_attributes 类型 + scopes 带值 + custom_ext 枚举 + How/What/Why 形态一致性。
+
+```bash
+python 创作知识提取-skill/tools/lint-payload.py outputs/<帖>/payload.json
+```
+- 输出 WARN(建议看)/ ERROR(必须修);有 ERROR 退出码 1。
+
+## ingest-post.py —— 入库(可选)
+
+把 payload POST 到 ingest API。**默认 dry-run**,加 `--post` 才真发。
+
+```bash
+python 创作知识提取-skill/tools/ingest-post.py outputs/<帖>/payload.json \
+    --url https://<host>/api/v1/knowledge/ingest --post
+```
+- 端点/令牌:`--url`/`--token` 或 env `INGEST_API`/`INGEST_TOKEN`。先 lint 过再发。