tech-book-extractor-skills 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # tech-book-extractor
2
+
3
+ Claude Code 技能:技术书深度萃取——两阶段流水线。
4
+
5
+ ## 结构
6
+
7
+ | 阶段 | 文件 | 职责 |
8
+ |------|------|------|
9
+ | Stage 1 | [stage1/skill-stage1-structure-parser.md](stage1/skill-stage1-structure-parser.md) | 全书骨架解析:标注 type/weight/complexity/keyQuestions/outdatedRisks/阅读路线 |
10
+ | Stage 2 | [stage2/skill-stage2-chapter-extractor.md](stage2/skill-stage2-chapter-extractor.md) | 单章深度萃取:六层结构 + 复杂热点深度脚手架 |
11
+
12
+ ## 安装(Agent Skill)
13
+
14
+ ```bash
15
+ npx tech-book-extractor-skills
16
+ ```
17
+
18
+ 安装完成后即可使用 `/tech-book-stage1` 和 `/tech-book-stage2`。
19
+
20
+ **更新到最新版本:**
21
+
22
+ ```bash
23
+ npx tech-book-extractor-skills@latest
24
+ ```
25
+
26
+ ## 设计思路
27
+
28
+ - **Stage 1 画地图**——读整本书之前,先标注知识价值分布,聚焦高权重章节
29
+ - **Stage 2 搭脚手架**——复杂小节(如 ZGC 染色指针)不精炼提纯,而是分步拆解:做了什么 → 为什么需要 → 不这样做会怎样
30
+ - **复杂度客观检测**——用小节长度、脚注密度、跨章引用数自动判定 complexity,不依赖 LLM 主观判断
31
+ - **一套模板 + 增量**——六层输出格式统一,complexity=high 的小节在心智模型层追加分步拆解,不断开新模板
package/bin/install.js ADDED
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+
7
+ const skillsSource = path.join(__dirname, "..", "skills");
8
+ const skillsDest = path.join(os.homedir(), ".claude", "skills");
9
+ const scriptsSrc = path.join(__dirname, "..", "stage1", "complexity_scanner.py");
10
+ const scriptsDest = path.join(os.homedir(), ".claude", "scripts");
11
+
12
+ fs.mkdirSync(skillsDest, { recursive: true });
13
+ fs.mkdirSync(scriptsDest, { recursive: true });
14
+
15
+ // 复制 skills
16
+ const skills = fs.readdirSync(skillsSource).filter((f) =>
17
+ fs.statSync(path.join(skillsSource, f)).isDirectory()
18
+ );
19
+
20
+ if (skills.length === 0) {
21
+ console.error("No skills found in package.");
22
+ process.exit(1);
23
+ }
24
+
25
+ for (const skill of skills) {
26
+ const src = path.join(skillsSource, skill);
27
+ const dest = path.join(skillsDest, skill);
28
+ fs.mkdirSync(dest, { recursive: true });
29
+
30
+ for (const file of fs.readdirSync(src)) {
31
+ fs.copyFileSync(path.join(src, file), path.join(dest, file));
32
+ }
33
+
34
+ console.log(`✓ skill: ${skill} → ${dest}`);
35
+ }
36
+
37
+ // 复制预处理脚本
38
+ const scannerDest = path.join(scriptsDest, "complexity_scanner.py");
39
+ fs.copyFileSync(scriptsSrc, scannerDest);
40
+ console.log(`✓ script: complexity_scanner.py → ${scannerDest}`);
41
+
42
+ console.log(`\n${skills.length} skill(s) installed.`);
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "tech-book-extractor-skills",
3
+ "version": "1.0.6",
4
+ "description": "Claude Code skills for deep technical book reading — structure parsing (Stage 1) and chapter extraction (Stage 2).",
5
+ "bin": {
6
+ "tech-book-extractor-skills": "bin/install.js"
7
+ },
8
+ "files": [
9
+ "skills/",
10
+ "bin/",
11
+ "stage1/complexity_scanner.py"
12
+ ],
13
+ "scripts": {
14
+ "install-skills": "node bin/install.js",
15
+ "sync": "node scripts/sync.js",
16
+ "release:patch": "npm run sync && npm version patch && npm publish",
17
+ "release:minor": "npm run sync && npm version minor && npm publish"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/cheney12138/tech-book-extractor.git"
22
+ },
23
+ "license": "MIT",
24
+ "publishConfig": {
25
+ "access": "public"
26
+ }
27
+ }
@@ -0,0 +1,234 @@
1
+ ---
2
+ name: tech-book-stage1
3
+ description: "技术书精读前的知识骨架生成器。当用户想在读书前先画地图、分析章节价值分布、标注权重和阅读路线时激活。触发词:/tech-book-stage1、精读准备、生成骨架、分析这本书的结构。先运行 complexity_scanner.py,再生成 stage1-skeleton.json。"
4
+ ---
5
+ # 阶段一 Skill:技术书结构解析器
6
+
7
+ **定位**:读整本书之前,先画地图。避免逐章硬啃,把有限精力聚焦在高价值章节。
8
+
9
+ **复杂度指标由脚本客观计算,不依赖 LLM 主观推断。**
10
+
11
+ ---
12
+
13
+ ## Step 0:运行预处理脚本(必须先做)
14
+
15
+ ```bash
16
+ python ~/.claude/scripts/complexity_scanner.py <书的路径> [输出路径]
17
+ ```
18
+
19
+ 脚本扫描全书文本,计算每个小节的三项客观指标,输出 `complexity-meta.json`。
20
+
21
+ **支持格式**:
22
+
23
+ | 格式 | 依赖 | 复杂度可信度 |
24
+ |------|------|------|
25
+ | `.txt` | 无 | 高 |
26
+ | `.epub` | 无(标准库) | 高 |
27
+ | `.pdf`(文字版) | `brew install poppler` | 中(受排版影响) |
28
+ | `.pdf`(扫描件) | — | 不可用,退回 LLM 推断 |
29
+
30
+ 扫描件场景:输出 JSON 中 `_meta.scan_fallback = true`,所有小节 `complexity = "unknown"`,LLM 需自行推断,并在骨架中标注 `"complexity_source": "llm_inferred"`。
31
+
32
+ ---
33
+
34
+ ## 输入
35
+
36
+ 两种主文本输入模式,**均需先完成 Step 0**:
37
+
38
+ | 模式 | 输入内容 | 适用场景 |
39
+ |------|---------|---------|
40
+ | A(省 token) | 全书目录 + 前言/序言 + 元信息 + **complexity-meta.json** | 已有提取好的目录文本 |
41
+ | B(省人工) | 全书全文 + **complexity-meta.json** | 直接扔原文,Skill 自行提取目录和前言 |
42
+
43
+ > 模式 B 下,Skill 第一步从原文中定位目录(通常在正文前 5%)和前言/序言,再进入骨架生成。Token 成本更高但对用户零预处理要求。
44
+ >
45
+ > **无论哪种模式,`complexityHotspots` 均直接读取 `complexity-meta.json`,不再由 LLM 计算。**
46
+
47
+ ## 输出
48
+
49
+ 一份 JSON 知识骨架。
50
+
51
+ **输出路径**:`{output_dir}/{书名}/stage1-skeleton.json`
52
+
53
+ | 参数 | 说明 | 默认值 |
54
+ |------|------|------|
55
+ | `output_dir` | 笔记根目录,用户可在调用时指定 | 当前工作目录(`.`) |
56
+ | `{书名}` | 从输入元信息提取,去除副标题和特殊字符,如 "深入理解Java虚拟机" | — |
57
+
58
+ 目录不存在时自动创建,无需用户手动建。
59
+
60
+ 完整 schema:
61
+
62
+ ```json
63
+ {
64
+ "book": "深入理解Java虚拟机(第3版)",
65
+ "author": "周志明",
66
+ "versionNote": "基于 JDK 13,部分内容覆盖 JDK 8-17",
67
+ "chapters": [
68
+ {
69
+ "id": "ch03",
70
+ "title": "垃圾收集器与内存分配策略",
71
+ "type": "core_principle",
72
+ "weight": 5,
73
+ "estimatedReading": "2-3h",
74
+ "prerequisites": ["ch02"],
75
+ "practiceDensity": "high",
76
+ "keyQuestions": [
77
+ "G1 和 ZGC 分别适合什么场景?",
78
+ "什么时候该调大 SurvivorRatio?"
79
+ ],
80
+ "outdatedRisks": ["CMS 收集器已在 JDK 14 废弃"],
81
+ "subsections": [
82
+ {
83
+ "title": "3.4 HotSpot的算法实现",
84
+ "practiceChecklist": ["安全点", "安全区域", "记忆集与卡表"],
85
+ "skipIf": "只做应用开发不做 JVM 调优",
86
+ "complexity": "medium"
87
+ }
88
+ ]
89
+ }
90
+ ],
91
+ "complexityHotspots": [
92
+ {
93
+ "section": "3.6.2",
94
+ "topic": "ZGC 染色指针",
95
+ "indicators": { "sectionLength": 10192, "footnoteCount": 11, "crossChapterRefs": 5 }
96
+ }
97
+ ],
98
+ "readingPaths": [
99
+ {
100
+ "name": "应用开发者路线",
101
+ "chapterIds": ["ch02", "ch03", "ch04", "ch07"]
102
+ },
103
+ {
104
+ "name": "调优工程师路线",
105
+ "chapterIds": ["ch02", "ch03", "ch04", "ch05", "ch08"]
106
+ },
107
+ {
108
+ "name": "面试突击路线",
109
+ "chapterIds": ["ch02", "ch03", "ch07", "ch12"]
110
+ }
111
+ ]
112
+ }
113
+ ```
114
+
115
+ ## 字段语义
116
+
117
+ | 字段 | 类型 | 说明 |
118
+ |------|------|------|
119
+ | `id` | string | 章节唯一标识,如 ch03 |
120
+ | `type` | enum | `core_principle` 核心原理 / `practical` 实操为主 / `reference` 速查参考 / `outdated` 已过时。本枚举适用于原理+实践混合型技术书;纯教程类可扩展 `tutorial`,规范/标准类可扩展 `spec` |
121
+ | `weight` | 1-5 | 5=必精读,3=通读,1=可跳过 |
122
+ | `estimatedReading` | string | 预估阅读时长,如 "2-3h"、"45min"、"30min-1h" |
123
+ | `practiceDensity` | enum | high / medium / low |
124
+ | `keyQuestions` | string[] | 读完这章你应该能回答的问题 |
125
+ | `outdatedRisks` | string[] | 过时技术/API/参数提前标注。⚠️ 标注依赖模型知识截止日期,对近两年出版的书可能不完整,建议人工复核 |
126
+ | `skipIf` | string | 什么角色/场景下可以跳过本节 |
127
+ | `prerequisites` | string[] | 前置依赖章节 id 列表。必须无环:若 A 依赖 B,B 不可直接或间接依赖 A |
128
+ | `practiceChecklist` | string[] | 本节核心实践点关键词 |
129
+ | `complexityHotspots` | object[] | 全书中需要深度脚手架(而非精炼提纯)的复杂小节。按客观指标自动计算,不依赖主观判断 |
130
+
131
+ ## 设计原则
132
+
133
+ 1. **不是总结,是导航** —— 不生产知识,只标注知识的价值和位置。骨架是指南针,不是摘要
134
+ 2. **权重分等级** —— 5 分必精读、3 分通读、1 分速查,拒绝平均用力。一本书真正值得精读的通常只有 3-5 章
135
+ 3. **标记过时风险** —— JVM 演进快,CMS、永久代这些内容提前标注,避免白学
136
+ 4. **多阅读路线** —— 不同角色(应用开发者、调优工程师、面试准备)走不同路径
137
+ 5. **前置依赖链** —— 明确章节间的前置关系,防止跳读卡住
138
+ 6. **可跳过提示** —— 每个小节都标注什么情况下可以跳过,最大程度节省时间
139
+ 7. **问题驱动** —— 每章定义关键问题,读完后能回答才算读透了
140
+ 8. **复杂度热点自动检测** —— 复杂小节(原文长 + 脚注多 + 跨章引用多)需要在 Stage 2 启用深度脚手架,而不是精炼提纯。**指标由 `complexity_scanner.py` 客观计算,LLM 直接读取结果,不自行推断。**
141
+
142
+ ### 复杂度热点检测规则(由脚本执行)
143
+
144
+ `complexity_scanner.py` 按以下客观指标计算每个小节的复杂度:
145
+
146
+ | 指标 | 阈值 | 说明 |
147
+ |------|------|------|
148
+ | 原文小节字数 | > 3000 | 作者用篇幅投票,长小节通常概念层数多 |
149
+ | 脚注数 | > 3 个 | 脚注多 = 不额外解释读者无法理解 |
150
+ | 跨章引用次数 | > 2 个 | "见第 X 章"、"参见 §X.X" 出现越多,概念栈越深 |
151
+
152
+ **判定逻辑**:
153
+ - 三项全部命中 → `complexity: "high"`
154
+ - 命中 1-2 项 → `complexity: "medium"`
155
+ - 其余 → `complexity: "low"`
156
+
157
+ 扫描件 PDF 无法计算,所有小节输出 `complexity: "unknown"`,由 LLM 退回推断(标注 `complexity_source: "llm_inferred"`)。
158
+
159
+ Stage 2 对复杂度为 high 的小节,在心智模型层追加"分步拆解"增量块(不改六层格式),每步回答:做了什么 → 为什么需要 → 不这样做会怎样。
160
+
161
+ ## 和阶段二的关系
162
+
163
+ 阶段二(深度萃取 Skill)拿这个骨架 + 具体章节文本,就能精准判断:
164
+
165
+ - 本章是原理型还是实操型?→ 决定萃取模板侧重概念解释还是实践清单
166
+ - 哪些小节可以跳过?→ 直接不喂给阶段二,省 token
167
+ - 哪些小节复杂度高?→ 阶段二启用深度脚手架(分步拆解),而非精炼提纯
168
+ - 有哪些过时风险?→ 萃取时自动标注版本差异
169
+ - 前后依赖什么?→ 如果前置章节还没读,提醒用户先补课
170
+
171
+ ## 输出质量 Checklist
172
+
173
+ 生成骨架后逐项校验,不通过视为不合格输出:
174
+
175
+ - [ ] 每个 chapter 都有 `type` 和 `weight`
176
+ - [ ] `weight=5` 的章节不超过全书的 30%(一本技术书真正值得精读的通常只有 3-5 章)
177
+ - [ ] 三条阅读路线的 `chapterIds` 交集不超过 50%(差异化,避免高度重叠)
178
+ - [ ] 所有 `prerequisites` 指向存在的 `chapter.id`,无悬挂引用
179
+ - [ ] 所有 `prerequisites` 引用链无环
180
+ - [ ] 每个 subsection 标注了 `complexity`,且 high 级别的 subsection 已汇总到顶层 `complexityHotspots`
181
+ - [ ] `complexity` 来自 `complexity-meta.json`(客观计算),或扫描件场景下标注了 `complexity_source: "llm_inferred"`
182
+ - [ ] 每个 `outdatedRisks` 条目都有具体技术/API/参数名称,拒绝泛泛的"部分内容可能过时"
183
+ - [ ] `keyQuestions` 每章 2-3 个,问题具体可验证,拒绝"本章讲了什么"式空洞提问
184
+ - [ ] `subsections` 中每个小节的 `skipIf` 明确了跳过条件(角色/场景),无空值
185
+
186
+ ## Prompt 模板
187
+
188
+ ```
189
+ 你是一位资深技术书籍编辑,擅长快速评估技术书籍的价值分布。
190
+
191
+ 【输入类型】
192
+ {input_mode} // "A" = 已提取的目录+前言, "B" = 全书全文
193
+
194
+ 【输出目录】{output_dir} // 未提供时使用当前工作目录
195
+ 【书名】{book_name}
196
+ 【作者】{author}
197
+ 【版本】{edition}
198
+ 【书籍简介/前言摘要】{preface_summary}
199
+
200
+ 【全书目录】
201
+ {full_toc}
202
+
203
+ 【复杂度元数据】(complexity_scanner.py 输出,必须提供)
204
+ {complexity_meta_json}
205
+
206
+ 【全书原文】(仅模式 B 提供)
207
+ {book_full_text}
208
+
209
+ 【预处理步骤 — 仅模式 B 执行】
210
+ 如果输入是模式 B(全书全文),先完成以下步骤再进入任务:
211
+ 1. 在原文中定位目录页——通常在"目录"标题下,列出全部章节名和页码
212
+ 2. 提取完整的章-节-小节层级(忽略页码),整理为 {full_toc}
213
+ 3. 定位前言/序言章节,提取作者自述的写作意图、读者定位、版本变化
214
+ 4. 用提取的目录和前言替换 {full_toc} 和 {preface_summary},再进入主任务
215
+
216
+ 【主任务】
217
+ 0. 从书名提取目录名(去除副标题和特殊字符),自动创建 `{output_dir}/{书名}/` 目录(`output_dir` 未指定时使用当前工作目录)
218
+ 1. 为每一章标注 type(core_principle/practical/reference/outdated)、weight(1-5)、practiceDensity
219
+ 2. 从 {complexity_meta_json} 读取每个小节的 complexity(high/medium/low),不自行计算。
220
+ 若 `_meta.scan_fallback = true`,则退回自行推断,并在每个 subsection 标注 `"complexity_source": "llm_inferred"`
221
+ 3. 标记过时技术风险(如 CMS 已废弃、永久代已移除等)
222
+ 4. 定义 3 条阅读路线:应用开发者、调优工程师、面试突击
223
+ 5. 为每章生成 2-3 个关键问题
224
+ 6. 明确章节间的前置依赖关系
225
+ 7. 汇总全书的 complexityHotspots(仅 high 级别),记录在顶层数组
226
+
227
+ 【输出要求】
228
+ - 严格按 JSON schema 输出
229
+ - 将结果写入 `{output_dir}/{书名}/stage1-skeleton.json`(目录不存在则自动创建;`output_dir` 未指定时写入当前工作目录)
230
+ - 不要总结章节内容,只做元数据标注
231
+ - 对不确定的过时风险标注 confidence: low
232
+ - 阅读路线要差异化,避免三条路线高度重叠
233
+ - 确保 prerequisites 引用链无环:若 ch03 依赖 ch02,ch02 不可直接或间接依赖 ch03
234
+ ```
@@ -0,0 +1,576 @@
1
+ ---
2
+ name: tech-book-stage2
3
+ description: "技术书章节深度萃取器。当用户想精读某章节、把内容读薄提炼为可用知识时激活。需要 Stage 1 骨架作为输入。触发词:/tech-book-stage2、萃取这章、精读这章、深度提炼。输出六层结构的 Markdown 笔记。"
4
+ ---
5
+ # 阶段二 Skill:技术书章节深度萃取器
6
+
7
+ **定位**:把一章书读薄,只留下能拿来用的和能拿来记的。不是摘要,是知识提纯。
8
+
9
+ ---
10
+
11
+ ## 输入
12
+
13
+ | 字段 | 来源 | 说明 |
14
+ |------|------|------|
15
+ | 章节全文 | 用户提供 | 单章完整原文 |
16
+ | 章节骨架 | Stage 1 产出 | 本章的 type、weight、keyQuestions、outdatedRisks、prerequisites、subsections.skipIf、subsections.complexity |
17
+ | complexityHotspots | Stage 1 产出 | 全书中 complexity=high 的小节列表,Stage 2 对此类小节启用深度脚手架而非精炼提纯 |
18
+ | 前置章节萃取 | Stage 2 先前产出(可选) | 已处理章节的"关键问答",用于检测术语引用一致性 |
19
+
20
+ ---
21
+
22
+ ## 输出
23
+
24
+ 一份 Markdown 文件,六层结构从粗到细。每层独立可用,不强制按顺序阅读。
25
+
26
+ ### 输出格式
27
+
28
+ ```markdown
29
+ # 第 N 章 · 章节标题
30
+
31
+ > 类型:{core_principle/practical/reference/outdated} | 权重:{1-5}/5 | 预估阅读:{estimatedReading}
32
+ > 前置:{prerequisites 章节名}
33
+ > 读完应能回答:{骨架 keyQuestions}
34
+
35
+ ---
36
+
37
+ ## 一、一句话内核
38
+
39
+ {≤80字。首选形式:类比或画面。如果概念本身高度抽象、类比会丢失精确性,可改为"一句话核心命题"——用一句话概括本章最重要的因果关系。禁止写成标题重述或"本章讲了..."}
40
+
41
+ {四种类型的变体:
42
+ - core_principle:类比/画面 → 不行则核心因果关系
43
+ - practical:一句话行动指南
44
+ - reference:一句话定位(这章解答什么问题,什么时候来查)
45
+ - outdated:一句话替代方案(书推荐什么,现在该用什么)
46
+ }
47
+
48
+ ---
49
+
50
+ ## 二、心智模型
51
+
52
+ ### 2.1 {核心概念1}
53
+
54
+ {用费曼技巧重述——用简单语言解释,假设读者完全不懂}
55
+
56
+ ### 2.2 {因果链或组织逻辑}
57
+
58
+ {用箭头串联因果关系,或说明作者的组织逻辑}
59
+ 例:并发标记 → 对象可能漏标 → SATB 快照解决 → 但产生浮动垃圾
60
+ → 浮动垃圾下次 GC 才回收 → Mixed GC 可能连续触发
61
+
62
+ ### 2.3 边界条件
63
+
64
+ | 场景 | 适用 | 不适用 |
65
+ |------|------|--------|
66
+ | {场景描述} | 堆≥4GB | 小堆用Serial/Parallel |
67
+ | ... | ... | ... |
68
+
69
+ ---
70
+
71
+ ## 三、决策规则
72
+
73
+ ### 3.1 {决策场景}
74
+
75
+ **when**:{触发条件}
76
+ **then**:{行动}
77
+ **why**:{一句话理由}
78
+
79
+ ### 3.2 参数速查
80
+
81
+ | 参数 | 默认值 | 调大后果 | 调小后果 | 适用版本 |
82
+ |------|--------|---------|---------|---------|
83
+ | {param} | {default} | {up} | {down} | {JDK version} |
84
+
85
+ > 只收录书中给出明确建议的参数,不收录纯罗列的参数。
86
+
87
+ ---
88
+
89
+ ## 四、诊断手册
90
+
91
+ ### 症状:{症状描述}
92
+
93
+ 1. {排查命令1} ← 看 {指标}
94
+ 2. {排查命令2} ← 看 {指标}
95
+ 3. 判断分支:
96
+ ├─ {条件A} → {根因} → {解决}
97
+ ├─ {条件B} → {根因} → {解决}
98
+ └─ {条件C} → {根因} → {解决}
99
+
100
+ ---
101
+
102
+ ## 五、本章过时标注
103
+
104
+ | 内容 | 状态 | 替代 |
105
+ |------|------|------|
106
+ | {技术/API/参数} | {当前状态} | {替代方案} |
107
+
108
+ ---
109
+
110
+ ## 六、关键问答
111
+
112
+ **Q: {问题 — 必须覆盖骨架 keyQuestions 的每一项}**
113
+ A: {回答 — 包含足够细节,独立可理解}
114
+
115
+ **Q: {扩展问题 — 萃取过程中发现的新问题}**
116
+ A: {回答}
117
+ ```
118
+
119
+ ---
120
+
121
+ ## 输出示例(Few-shot)
122
+
123
+ 以下为 ch02"Java 内存区域与内存溢出异常"的萃取结果,类型为 core_principle,作为 Pass 2 的参考范例。注意标注了 `[✓]` 的值得模仿之处和 `[→ 注意]` 的注意事项。
124
+
125
+ ```markdown
126
+ # 第 2 章 · Java 内存区域与内存溢出异常
127
+
128
+ > 类型:core_principle | 权重:5/5 | 预估阅读:2-3h
129
+ > 前置:无(本章是内存管理入口章)
130
+ > 读完应能回答:Java 运行时数据区域有哪些?OOM 异常怎么排查?
131
+
132
+ ---
133
+
134
+ ## 一、一句话内核
135
+
136
+ [✓ 这是类比式内核,不超过 80 字]
137
+ JVM 的内存像一套分租公寓——每人有自己的私人空间(虚拟机栈、程序计数器),
138
+ 共用客厅和厨房(堆、方法区),直接内存像向物业租的储物间。
139
+
140
+ ---
141
+
142
+ ## 二、心智模型
143
+
144
+ ### 2.1 运行时数据区域的五类空间
145
+
146
+ [✓ 用费曼技巧重述,假设读者不懂]
147
+
148
+ 运行一个 Java 程序时,JVM 向操作系统申请一大块内存,然后内部切分成五个功能区:
149
+
150
+ - **程序计数器**:当前线程执行到第几行字节码的指示器。每个线程一个,是唯一不会 OOM 的区域。
151
+ - **Java 虚拟机栈**:存方法调用的局部变量、操作数栈等。每个方法调用时压入一个栈帧,方法返回时弹出。
152
+ - **本地方法栈**:和虚拟机栈类似,但服务的是 native 方法。
153
+ - **Java 堆**:存所有对象实例。所有线程共享,是 GC 的主战场。
154
+ - **方法区**:存类的元信息(字段、方法、常量池)。JDK 8 前叫永久代(PermGen),JDK 8 后改为元空间(Metaspace),使用本地内存。
155
+
156
+ ### 2.2 内存溢出的因果链
157
+
158
+ [✓ 箭头串联因果关系]
159
+
160
+ 对象分配速率 > GC 回收速率
161
+ → 堆内存被占满 → GC 无法回收足够内存 → OOM: Java heap space
162
+
163
+ 如果堆本身太小 → 调大 -Xmx
164
+ 如果是内存泄漏 → jmap 查大对象 → 查代码
165
+
166
+ 大量类被加载且无法卸载
167
+ → 元空间(方法区)被占满 → OOM: Metaspace / PermGen space
168
+
169
+ -XX:MaxMetaspaceSize 太小 → 调大
170
+ 类加载器泄漏 → 查动态代理/反射使用
171
+
172
+ ### 2.3 边界条件
173
+
174
+ | 场景 | 适用 | 不适用 |
175
+ |------|------|--------|
176
+ | 方法区的 GC 效率 | JDK 8+ 元空间:可自动回收类元数据 | JDK 7 永久代:类卸载条件苛刻,容易 OOM |
177
+ | 直接内存 OOM 排查 | 使用了 NIO/DirectByteBuffer | 没有使用 NIO,OOM 基本来自堆或栈 |
178
+ | 虚拟机栈 OOM | 线程数 × 栈容量 > 可用内存 | 单线程递归过深 → StackOverflowError,不是 OOM |
179
+
180
+ [→ 注意:边界条件不是罗列,而是标注"什么情况下这个原理失效"]
181
+
182
+ ---
183
+
184
+ ## 三、决策规则
185
+
186
+ ### 3.1 堆大小设置
187
+
188
+ [✓ when-then-why 三元组,可执行]
189
+
190
+ **when**:物理内存 8GB 以上,Java 应用为主要服务
191
+ **then**:-Xms 和 -Xmx 设为相同值,约为物理内存的 50-75%(留够 OS 和堆外内存)
192
+ **why**:避免堆动态扩缩容的开销,提前预留避免启动后再申请
193
+
194
+ ### 3.2 参数速查
195
+
196
+ | 参数 | 默认值 | 调大后果 | 调小后果 | 适用版本 |
197
+ |------|--------|---------|---------|---------|
198
+ | `-Xmx` | 物理内存 1/4 | GC 频率降低但单次暂停更长 | 频繁 GC,吞吐下降,可能 OOM | 全版本 |
199
+ | `-Xss` | 1MB (Linux) | 线程数上限降低(内存/栈容量) | 栈溢出风险增加,递归深度受限 | 全版本 |
200
+ | `-XX:MaxMetaspaceSize` | 无限制 | 浪费本地内存 | 类加载失败,OOM: Metaspace | JDK 8+ |
201
+
202
+ > [✓ 只收录有明确建议的参数]
203
+
204
+ ---
205
+
206
+ ## 四、诊断手册
207
+
208
+ ### 症状:堆溢出(OOM: Java heap space)
209
+
210
+ 1. `jstat -gc <pid> 1000 10` ← 看 EU( Eden使用率)、OU( 老年代使用率) 是否持续增长
211
+ 2. `jmap -histo:live <pid> | head -20` ← 看哪些类实例数量异常
212
+ 3. 判断分支:
213
+ ├─ GC 后老年代持续增长 → 内存泄漏 → jmap dump 后用 MAT/JProfiler 分析引用链
214
+ ├─ GC 频繁但每次回收很少 → 堆太小 → 调大 -Xmx
215
+ └─ 大对象直接进老年代 → 查 -XX:PretenureSizeThreshold 或代码中的大数组/大集合
216
+
217
+ ### 症状:元空间溢出(OOM: Metaspace)
218
+
219
+ 1. `jstat -gc <pid>` ← 看 MU( Metaspace使用率) 是否接近最大容量
220
+ 2. 判断分支:
221
+ ├─ -XX:MaxMetaspaceSize 太小 → 调大
222
+ └─ 类加载器泄漏 → 查动态代理、CGLIB、反射频繁创建类 → 使用 Arthas 查 ClassLoader 实例数
223
+
224
+ [✓ 症状描述具体可匹配,排查命令可直接复制执行]
225
+
226
+ ---
227
+
228
+ ## 五、本章过时标注
229
+
230
+ | 内容 | 状态 | 替代 |
231
+ |------|------|------|
232
+ | 永久代(PermGen) | JDK 8 移除 | 元空间(Metaspace),配置参数从 -XX:PermSize 改为 -XX:MetaspaceSize |
233
+ | 运行时常量池在方法区 | JDK 7 起字符串常量池已移至堆 | 字符串常量池在堆,类常量池仍在 Metaspace |
234
+
235
+ [✓ 每条都有具体技术和替代方案,不是泛泛的"可能过时"]
236
+
237
+ ---
238
+
239
+ ## 六、关键问答
240
+
241
+ **Q: Java 运行时数据区域有哪些?各自存放什么数据?哪些是线程私有?**
242
+ A: 线程私有:程序计数器(当前字节码行号)、虚拟机栈(局部变量、操作数栈、方法返回地址)、本地方法栈。线程共享:Java 堆(对象实例)、方法区/元空间(类元信息、静态变量、运行时常量池)、直接内存(NIO Buffer)
243
+
244
+ **Q: 不同内存区域出现 OutOfMemoryError 的典型原因和排查思路是什么?**
245
+ A: 堆溢出:对象太多或内存泄漏 → jstat + jmap dump + MAT 分析 [详见诊断手册 §四]。栈溢出:递归过深或线程数 × 栈容量 > 内存 → 调小 -Xss 或限制线程数。元空间溢出:类加载器泄漏或容量不够 → 调大 MaxMetaspaceSize 或查动态代理。直接内存溢出:NIO 使用不当 → 查 DirectByteBuffer 分配。
246
+
247
+ [✓ 覆盖了骨架 keyQuestions 的全部问题]
248
+ ```
249
+
250
+ ---
251
+
252
+ ## 四种萃取模板
253
+
254
+ 骨架的 `type` 字段决定哪个模板主导,但六层结构保持一致(非核心层可留空标注"本节不适用")。
255
+
256
+ ### 模板 A:核心原理章 (core_principle)
257
+
258
+ 适用章:weight≥3 的 core_principle 章。这是萃取最深的模板。
259
+
260
+ **萃取重点**:
261
+ 1. 一句话内核必须给出——首选类比或画面;概念过于抽象时(如内存模型、并发规范),改为核心因果关系,不超过 80 字
262
+ 2. 因果链必须串联——不是列表,是箭头链。例:A → B → C → 副作用 D
263
+ 3. 边界条件必须标注——这个原理在什么情况下失效?不标注等于没理解
264
+ 4. 下游影响必须说明——理解此原理后,写代码时的行为改变
265
+ 5. "决策规则"节:从原理推演出的实践指导
266
+ 6. "诊断手册"节:基于原理的排查路径
267
+ 7. **复杂度热点处理(新增)**:当小节 `complexity = "high"` 时,在心智模型层追加"分步拆解"增量块——不改变六层格式,只在因果链之后插入。要求:
268
+ - 每步回答三个问题:做了什么 → 为什么需要这一步 → 不这样做会怎样
269
+ - 优先使用具体例子和类比
270
+ - 结尾给一句"读原书提示":读 §X.X 时重点抓住哪 2-3 个点
271
+ - 此增量块的预期效果:读者看完后不再觉得原书枯燥难懂,因为脚手架已经搭好了
272
+
273
+ **Token 预算**:全章原文 + Pass 1 笔记,质量优先
274
+
275
+ ### 模板 B:实操章 (practical)
276
+
277
+ 适用章:type=practical,或 weight≥3 的实践导向章。
278
+
279
+ **萃取重点**:
280
+ 1. "一句话内核"改为"一句话行动指南"——读完这章你应该做什么
281
+ 2. "心智模型"节可用一句话带过,重心在"决策规则"和"诊断手册"
282
+ 3. 决策规则必须可执行:when-then-why 三元组
283
+ 4. 参数速查必须收录有明确建议值的参数,纯罗列的跳过
284
+ 5. 诊断症状必须可匹配:症状描述要足够具体,能让人对号入座
285
+ 6. 常见错误必须从书中提取或合理推断
286
+
287
+ **Token 预算**:全章原文 + Pass 1 笔记
288
+
289
+ ### 模板 C:参考章 (reference)
290
+
291
+ 适用章:type=reference,或 weight=2 的章。
292
+
293
+ **萃取重点**:
294
+ 1. "一句话内核"改为"一句话定位"——这章解答什么问题,什么时候需要来查
295
+ 2. "心智模型"节跳过,改为"术语索引"表
296
+ 3. 术语索引:术语 | 一句话解释 | 所在节号 | 被哪些原理章引用
297
+ 4. "决策规则"节标注"本节为参考内容,无决策规则"
298
+ 5. "关键问答"只保留 1-2 个覆盖骨架问题的 QA 对
299
+
300
+ **Token 预算**:weight=2 的章只喂标题+小节名(不喂全文);weight=3 的参考章喂全文但用轻量模板
301
+
302
+ ### 模板 D:过时章 (outdated)
303
+
304
+ 适用章:type=outdated,或 outdatedRisks≥3 条的章。
305
+
306
+ **萃取重点**:
307
+ 1. "一句话内核"改为"一句话替代方案"——这本书推荐什么,现在该用什么
308
+ 2. "心智模型"节跳过
309
+ 3. 过时项逐条展开:过时内容 → 替代技术 → 替代版本 → 是否仍需了解(考试/维护遗留系统/纯历史)
310
+ 4. 如果考试会考,保留"考试相关知识最小集"
311
+ 5. 其余各节标注"本节内容已过时,跳过"
312
+
313
+ **Token 预算**:最小。只喂标题 + outdatedRisks 列表,不喂全文。
314
+
315
+ ---
316
+
317
+ ## Two-pass 流程
318
+
319
+ 同一章跑两次,两次的目标完全不同。但并非所有章节都需要 two-pass。
320
+
321
+ ### Token 预算与跳过逻辑
322
+
323
+ | 章类型 + 权重 | 流程 | Pass 1 输入 | Pass 2 输入 | 说明 |
324
+ |--------------|------|------------|------------|------|
325
+ | weight=5, core_principle | Two-pass | 全章原文 | 全文 + Pass 1 笔记 | 质量优先,全深度。complexity=high 的小节追加分步拆解 |
326
+ | weight=3-4, 任意 type | Two-pass | 全章原文 | 全文 + Pass 1 笔记 | 标准流程。complexity=high 的小节同样追加分步拆解 |
327
+ | weight=2, 任意 type | **Single-pass** | — | 标题+小节名(不喂全文) | 轻量萃取,省 token |
328
+ | weight=1, 任意 type | **跳过** | — | — | 输出一行"本章跳过" |
329
+ | type=outdated | **Single-pass** | — | 标题 + outdatedRisks 列表 | 不喂全文 |
330
+
331
+ > 原则:weight≥3 的章才有资格用 two-pass 占用双倍 token 预算。一本书按 Stage 1 骨架,通常只有 5-8 章需要 two-pass,其余章合并为 single-pass 或跳过。
332
+
333
+ ### Pass 1:理解
334
+
335
+ **输入**:全章原文
336
+
337
+ **任务**:生成一份内部理解笔记(不输出给用户),包含:
338
+ 1. 本章的核心论点是什么?(1-3 个,不是小节罗列)
339
+ 2. 作者的组织逻辑是什么?(层层递进?分情况枚举?对比论证?)
340
+ 3. 哪些内容是真正的新知识,哪些是填充/过渡/复习?
341
+ 4. 本章的结论或行动建议是什么?
342
+ 5. 技术细节中有哪些精确的数值、参数、版本号需要原样保留?
343
+ 6. 本章与前置依赖章的可能关联点(基于骨架 prerequisites)
344
+
345
+ **产出**:一份结构化的理解笔记(内部使用,不展示)
346
+
347
+ ### Pass 2:萃取
348
+
349
+ **输入**:全章原文 + Pass 1 理解笔记 + 本章骨架片段(type, weight, keyQuestions, outdatedRisks, prerequisites, subsections)
350
+
351
+ **任务**:
352
+ 1. 根据骨架 `type` 选择萃取模板(A/B/C/D)
353
+ 2. 根据骨架 `weight` 决定萃取深度:
354
+ - weight=5:全深度,六层全部展开
355
+ - weight=3-4:标准深度,"诊断手册"和次要概念可精简
356
+ - weight=2:轻量处理,只保留"关键问答"和"过时标注"
357
+ - weight=1:跳过全章,输出一行"本章跳过。原因:{skipIf}"
358
+ 3. 根据骨架 `skipIf` 跳过对应小节的原文(不喂入模型)
359
+ 4. 确保"关键问答"至少覆盖骨架 `keyQuestions` 中的所有问题
360
+ 5. 将骨架 `outdatedRisks` 注入"过时标注"区,并结合 Pass 1 做补充
361
+ 6. 如果引用了不在 `prerequisites` 中的章节概念,标注 `[需前置知识:第X章]`
362
+
363
+ **产出**:Markdown 文件(六层结构,保存为 `{output_dir}/{书名}/chapters/ch{id}-extract.md`;`output_dir` 未指定时写入当前工作目录。目录不存在时自动创建)
364
+
365
+ ---
366
+
367
+ ## 章节间一致性仲裁规则
368
+
369
+ 当多个章节的处理结果出现矛盾时,按以下优先级裁决:
370
+
371
+ ### 冲突类型及仲裁
372
+
373
+ | 冲突类型 | 示例 | 仲裁规则 |
374
+ |---------|------|---------|
375
+ | 同一概念,不同章描述角度不同 | ch03 讲 G1 从回收算法角度,ch08 从停顿时间角度 | **保留各自角度**,在术语处标注"参见 ch03 §3.x 的算法视角"——不同角度是互补的,不冲突 |
376
+ | outdatedRisks 骨架标注 vs Pass 1 新发现矛盾 | 骨架标注"CMS 已废弃",Pass 1 发现"JDK 14 废弃但 JDK 15 才移除" | **Pass 1 发现优先于骨架标注**。骨架是粗略的,Pass 1 读了原文后判断更准。更新 outdatedRisks 条目 |
377
+ | 跨章参数描述不一致 | ch03 说 G1HeapRegionSize 范围 1-32MB,ch05 说 1-64MB | **以书中最新/最权威的章节为准**。标注 `[与 ch03 不一致,以本章值为准]`,供 Stage 3 统一处理 |
378
+ | 术语命名不一致 | 书中同时用"记忆集"/"Remembered Set"/"RSet" | **保留所有名称**,首次出现时标注 `[记忆集 / Remembered Set / RSet,三者等价]` |
379
+
380
+ ### 全局仲裁原则
381
+
382
+ 1. **原书优先**:矛盾双方都来自原书时,以后出现的章节为准(作者在后文更新了观点)
383
+ 2. **新知识优先**:Pass 1 发现的过时信息优先于骨架的粗略标注
384
+ 3. **保留差异,不强行统一**:不同角度的描述不构成矛盾,保留并互引
385
+ 4. **无法裁决的标注** `[待确认]`:不在萃取阶段强行裁决,留给 Stage 3 或人工审核
386
+
387
+ ---
388
+
389
+ ## 和阶段一、阶段三的关系
390
+
391
+ ### 阶段一 → 阶段二
392
+
393
+ - 骨架的 `type` → 决定萃取模板
394
+ - 骨架的 `weight` → 决定萃取深度和 token 预算
395
+ - 骨架的 `skipIf` subsections → 跳过小节,节省 token
396
+ - 骨架的 `keyQuestions` → 注入"关键问答"区
397
+ - 骨架的 `outdatedRisks` → 注入"过时标注"区
398
+ - 骨架的 `prerequisites` → 检查术语引用的前置依赖
399
+
400
+ ### 阶段二 → 阶段三(待定)
401
+
402
+ - 每章的"关键问答" → 跨章概念映射的输入
403
+ - 每章的"术语" → 全局术语表去重
404
+ - 每章的"边界条件"和"过时标注" → 矛盾检测(不同章说法是否一致?)
405
+
406
+ ---
407
+
408
+ ## 质量闸门:三种失败模式及防范
409
+
410
+ ### 失败模式 1:发明事实
411
+
412
+ 模型可能编造精确值或版本号。例:"ZGC 在 JDK 13 生产可用"——实际 JDK 15 才正式生产可用。
413
+
414
+ **防范**:
415
+ - 要求所有事实性陈述引用章节位置(如 §3.6.2)
416
+ - 精确数值、版本号、参数名必须在 Pass 1 中标注 `[精确值,需原样保留]`
417
+ - 不确定的信息标注 `confidence: low/medium/high`
418
+ - Prompt 中明确:"对于你不确定的技术细节,标注 confidence: low,不要编造"
419
+
420
+ ### 失败模式 2:泛化稀释
421
+
422
+ 模型倾向简化技术精确性。例:把"Parallel Scavenge 用复制算法,Parallel Old 用标记-压缩"简化为"Parallel 用复制和标记-压缩"——吃掉了新生代/老年代的区别。
423
+
424
+ **防范**:
425
+ - Prompt 中要求:"保留技术术语的精确性,不要为了可读性牺牲准确度"
426
+ - 决策规则和参数表中的术语必须与书中原文一致
427
+ - 如果书中对同一概念有多个名称,保留所有并标注等价关系
428
+
429
+ ### 失败模式 3:遗漏前置知识
430
+
431
+ 第 8 章可能用第 6 章才引入的术语。如果读者按阅读路线跳过了第 6 章,萃取内容会不可理解。
432
+
433
+ **防范**:
434
+ - Pass 2 接收 prerequisites,检查每个跨章术语
435
+ - 如果使用了不在 prerequisites 中的章节概念,在术语处标注 `[需前置知识:第X章]`
436
+ - 关键问答中的回答必须自包含——不依赖读者已读过前置章节
437
+
438
+ ---
439
+
440
+ ## 输出质量 Checklist
441
+
442
+ 每章萃取完成后逐项校验。标注了检查方式:`[可自动]` 可脚本化,`[需人工]` 必须人眼判断。
443
+
444
+ - [ ] `[可自动]` "一句话内核"存在且 ≤ 80 字(字数统计可自动;是否为类比/定义需人工判断)
445
+ - [ ] `[可自动]` 六层结构每一节都出现(包括标注"本节不适用"的节 —— 用标题正则匹配验证)
446
+ - [ ] `[需人工]` "心智模型"节至少有一个因果链或组织逻辑图(core_principle 模板)
447
+ - [ ] `[可自动]` 参数速查表(如有)每行都有调大/调小列且非空
448
+ - [ ] `[需人工]` 诊断手册中每个症状描述足够具体,排查命令可直接执行
449
+ - [ ] `[可自动]` "关键问答"覆盖了骨架 keyQuestions 的全部问题(关键词匹配 + 人工确认)
450
+ - [ ] `[可自动]` 所有精确数值、版本号、参数名与原文一致(可用 difflib 对比原文)
451
+ - [ ] `[可自动]` 涉及非 prerequisites 章节的术语已标注 `[需前置知识:第X章]`
452
+ - [ ] `[需人工]` 每个事实性陈述有章节位置引用(§x.x)或 confidence 标注
453
+ - [ ] `[可自动]` 过时标注区包含了骨架 outdatedRisks(逐条 grep 匹配)+ Pass 1 新发现的过时项
454
+ - [ ] `[可自动]` 没有超过 100 字的连续原文引用(最长引文长度统计)
455
+
456
+ ---
457
+
458
+ ## Prompt 模板
459
+
460
+ ### Pass 1 Prompt
461
+
462
+ ```
463
+ 你是一位资深技术书籍编辑,正在精读一章技术内容。你的任务不是总结,而是理解——为后续的深度萃取做好准备。
464
+
465
+ 【章节原文】
466
+ {chapter_full_text}
467
+
468
+ 【章节元信息】
469
+ - 章节编号:{chapter_id}
470
+ - 标题:{chapter_title}
471
+ - 类型:{type}
472
+ - 前置依赖:{prerequisites}
473
+
474
+ 【任务】
475
+ 请生成一份"理解笔记",包含以下六个部分:
476
+
477
+ 1. 核心论点(1-3 个)
478
+ 这一章真正想表达什么?用 1-2 句话精确定位每个核心论点。
479
+ 不是小节罗列,是提炼后的论点。
480
+
481
+ 2. 组织逻辑
482
+ 作者用什么逻辑组织这章?
483
+ - 层层递进(先讲 A 再讲 B 再讲 C,不能跳过)
484
+ - 分情况枚举(A/B/C 并列,可独立阅读)
485
+ - 对比论证(A vs B,先讲差异再讲选择)
486
+ - 问题→解决方案→验证
487
+
488
+ 3. 知识价值分类
489
+ 区分三类内容:
490
+ - 新知识(作者原创观点、核心技术原理):列出并标注
491
+ - 填充过渡(背景铺垫、行业共识、复习内容):列出并标注
492
+ - 精确数据(数值、参数、版本号,必须原样保留):逐条列出
493
+
494
+ 4. 结论和行动建议
495
+ 作者最终建议读者做什么?有什么"如果你只记住一件事"的结论?
496
+
497
+ 5. 术语和概念清单
498
+ 本章引入或依赖的关键术语,每个术语标注:
499
+ - 是否在本章首次引入
500
+ - 是否可能引用了前置章节的概念
501
+ - 是否需要读者有额外背景知识
502
+
503
+ 6. 过时风险补充
504
+ 基于你的知识,本章是否有骨架未标注的过时风险?
505
+ 如果有,列出具体技术/API/参数及其当前状态。
506
+
507
+ 【输出要求】
508
+ - 这是内部笔记,不展示给读者,可以写成提纲式
509
+ - 精确数值必须逐字复制原文,不重述
510
+ - 不确定的内容标注 [不确定: ...]
511
+ ```
512
+
513
+ ### Pass 2 Prompt
514
+
515
+ ```
516
+ 你是一位资深技术书籍编辑,擅长把技术内容提炼为可实践的知识。请基于以下材料,生成一份结构化的章节深度萃取 Markdown 文档。
517
+
518
+ 【章节原文】
519
+ {chapter_full_text}
520
+
521
+ 【Pass 1 理解笔记】
522
+ {pass1_notes}
523
+
524
+ 【章节骨架(来自 Stage 1)】
525
+ - id: {chapter_id}
526
+ - title: {chapter_title}
527
+ - type: {type}
528
+ - weight: {weight}
529
+ - keyQuestions: {key_questions}
530
+ - outdatedRisks: {outdated_risks}
531
+ - prerequisites: {prerequisites}
532
+ - 需跳过的 subsections (skipIf): {skipped_subsections}
533
+ - subsections 复杂度: {sections_with_complexity} // complexity=high 的小节需要深度脚手架
534
+ - 前置章节关键问答(如有): {prerequisite_qa}
535
+
536
+ 【萃取模板】
537
+ {template_selection} // A=核心原理, B=实操, C=参考, D=过时
538
+
539
+ 【萃取深度】
540
+ {weight_instruction}
541
+ // weight=5: 全深度,六层全部展开
542
+ // weight=3-4: 标准深度,"诊断手册"和次要概念可精简
543
+ // weight=2: 轻量处理,只保留"关键问答"和"过时标注"
544
+ // weight=1: 跳过,输出一行"本章跳过"
545
+
546
+ 【复杂度热点处理】
547
+ {complexity_instruction}
548
+ // complexity=high 的小节:在心智模型层追加"分步拆解"增量块
549
+ // 每步格式:做了什么 → 为什么需要这一步 → 不这样做会怎样
550
+ // 结尾:一句"读原书提示"——读 §X.X 时重点抓住哪 2-3 个点
551
+ // 优先使用具体例子和类比,不要让读者看完脚手架后回到原书时还是一头雾水
552
+ // complexity=medium/low:标准处理,不追加
553
+
554
+ 【任务】
555
+ 按照选定的模板和萃取深度,生成 Markdown 文档。严格遵循六层结构:
556
+
557
+ 一、一句话内核
558
+ 二、心智模型(complexity=high 的小节追加"分步拆解"增量块,格式同上)
559
+ 三、决策规则
560
+ 四、诊断手册
561
+ 五、本章过时标注
562
+ 六、关键问答
563
+
564
+ 详细格式要求见输出格式规范。
565
+
566
+ 【输出要求】
567
+ 1. Markdown 格式,六层结构完整(非核心层标注"本节不适用",不要留空)
568
+ 2. 将结果写入 `{output_dir}/{书名}/chapters/ch{chapter_id}-extract.md`(目录不存在则自动创建;`output_dir` 未指定时写入当前工作目录)
569
+ 3. 所有事实性陈述引用章节位置(如 §3.6.2)或标注 confidence
570
+ 4. 保留技术术语的精确性,不要为了可读性泛化
571
+ 5. 关键问答必须覆盖骨架 keyQuestions 的全部问题
572
+ 6. 涉及非 prerequisites 章节的术语标注 `[需前置知识:第X章]`
573
+ 7. 不要连续引用原文超过 100 字
574
+ 8. 对于你不确定的技术细节,标注 `[confidence: low]`,不要编造
575
+ 9. 精确数值、参数名、版本号与原文一致
576
+ ```
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ complexity_scanner.py — Stage 1 预处理脚本
4
+
5
+ 客观计算技术书各小节的复杂度指标,输出 complexity-meta.json。
6
+ 由 Stage 1 Skill 调用,替代 LLM 对复杂度的主观推断。
7
+
8
+ 支持格式:txt / epub / pdf(需安装 poppler-utils)
9
+ 扫描件 PDF 无法处理,退回 LLM 推断(会在输出中标注)。
10
+
11
+ 用法:
12
+ python complexity_scanner.py <书的路径> [输出路径]
13
+
14
+ 输出示例:
15
+ {
16
+ "sec_1": {
17
+ "title": "第1章 引言",
18
+ "words": 1200,
19
+ "footnotes": 1,
20
+ "cross_refs": 0,
21
+ "complexity": "low"
22
+ },
23
+ ...
24
+ }
25
+ """
26
+
27
+ import json
28
+ import re
29
+ import sys
30
+ from pathlib import Path
31
+
32
+
33
+ # ── 文本提取 ──────────────────────────────────────────────────────────────────
34
+
35
+ def extract_text_txt(path: Path) -> str:
36
+ return path.read_text(encoding="utf-8", errors="ignore")
37
+
38
+
39
+ def extract_text_epub(path: Path) -> str:
40
+ import zipfile
41
+ from html.parser import HTMLParser
42
+
43
+ class _Stripper(HTMLParser):
44
+ def __init__(self):
45
+ super().__init__()
46
+ self._parts = []
47
+
48
+ def handle_data(self, d):
49
+ self._parts.append(d)
50
+
51
+ def get_data(self):
52
+ return "\n".join(self._parts)
53
+
54
+ texts = []
55
+ try:
56
+ with zipfile.ZipFile(path, "r") as z:
57
+ for name in sorted(z.namelist()):
58
+ if name.endswith((".html", ".xhtml", ".htm")):
59
+ html = z.read(name).decode("utf-8", errors="ignore")
60
+ s = _Stripper()
61
+ s.feed(html)
62
+ texts.append(s.get_data())
63
+ except Exception as e:
64
+ print(f"[WARN] epub 解析失败: {e}", file=sys.stderr)
65
+ return "\n".join(texts)
66
+
67
+
68
+ def extract_text_pdf(path: Path) -> str:
69
+ import subprocess
70
+
71
+ try:
72
+ result = subprocess.run(
73
+ ["pdftotext", "-layout", str(path), "-"],
74
+ capture_output=True,
75
+ text=True,
76
+ timeout=120,
77
+ )
78
+ if result.returncode == 0:
79
+ return result.stdout
80
+ print(f"[WARN] pdftotext 失败: {result.stderr}", file=sys.stderr)
81
+ except FileNotFoundError:
82
+ print(
83
+ "[WARN] 未找到 pdftotext,请安装 poppler-utils(brew install poppler)",
84
+ file=sys.stderr,
85
+ )
86
+ except subprocess.TimeoutExpired:
87
+ print("[WARN] pdftotext 超时", file=sys.stderr)
88
+ return ""
89
+
90
+
91
+ def extract_text(path: str) -> tuple[str, bool]:
92
+ """
93
+ 返回 (文本内容, is_scan)。
94
+ is_scan=True 表示扫描件或提取失败,复杂度指标不可信。
95
+ """
96
+ p = Path(path)
97
+ suffix = p.suffix.lower()
98
+
99
+ if suffix == ".txt":
100
+ return extract_text_txt(p), False
101
+ elif suffix == ".epub":
102
+ text = extract_text_epub(p)
103
+ return text, len(text.strip()) == 0
104
+ elif suffix == ".pdf":
105
+ text = extract_text_pdf(p)
106
+ # 扫描件特征:文本极少(每页平均 < 50 字符)
107
+ try:
108
+ import subprocess
109
+ pages_result = subprocess.run(
110
+ ["pdfinfo", str(p)], capture_output=True, text=True, timeout=10
111
+ )
112
+ pages_line = next(
113
+ (l for l in pages_result.stdout.splitlines() if "Pages:" in l), ""
114
+ )
115
+ pages = int(pages_line.split(":")[-1].strip()) if pages_line else 1
116
+ except Exception:
117
+ pages = max(1, len(text) // 2000)
118
+
119
+ is_scan = len(text.strip()) < pages * 50
120
+ return text, is_scan
121
+ else:
122
+ text = p.read_text(encoding="utf-8", errors="ignore")
123
+ return text, False
124
+
125
+
126
+ # ── 章节分割 ──────────────────────────────────────────────────────────────────
127
+
128
+ _HEADING_RE = re.compile(
129
+ r"("
130
+ r"^第[零一二三四五六七八九十百千\d]+[章节篇][\s ].{0,60}" # 中文章节
131
+ r"|^(?:Chapter|CHAPTER|PART|Part)\s+[\dA-Za-z].{0,60}" # 英文 Chapter/Part
132
+ r"|^\d{1,2}(?:\.\d{1,2}){0,2}[\s ]\S.{0,60}" # 1 / 1.2 / 1.2.3 编号
133
+ r"|^#{1,3}\s+\S.{0,60}" # Markdown 标题
134
+ r")",
135
+ re.MULTILINE,
136
+ )
137
+
138
+
139
+ def split_sections(text: str) -> list[dict]:
140
+ parts = _HEADING_RE.split(text)
141
+ if len(parts) < 3:
142
+ return [{"id": "whole", "title": "whole", "content": text}]
143
+
144
+ sections = []
145
+ idx = 1
146
+ while idx < len(parts) - 1:
147
+ title = parts[idx].strip()
148
+ content = parts[idx + 1] if idx + 1 < len(parts) else ""
149
+ sections.append(
150
+ {"id": f"sec_{len(sections) + 1}", "title": title, "content": content}
151
+ )
152
+ idx += 2
153
+ return sections
154
+
155
+
156
+ # ── 指标计算 ──────────────────────────────────────────────────────────────────
157
+
158
+ _FOOTNOTE_RE = re.compile(
159
+ r"\[\d{1,3}\]" # [1]
160
+ r"|[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮]" # 圈数字
161
+ r"|^\s*注[::].+", # 注:…(行首)
162
+ re.MULTILINE,
163
+ )
164
+
165
+ _CROSS_REF_RE = re.compile(
166
+ r"第[零一二三四五六七八九十百千\d]+[章节篇]"
167
+ r"|Chapter\s+\d+"
168
+ r"|见\s*[第§§]\s*\S+"
169
+ r"|参[见考]\s*[第§]\s*\S+"
170
+ r"|如前[所述文章]"
171
+ r"|后文[将会]介绍",
172
+ re.IGNORECASE,
173
+ )
174
+
175
+
176
+ def word_count(text: str) -> int:
177
+ chinese = len(re.findall(r"[一-鿿]", text))
178
+ english = len(re.findall(r"\b[a-zA-Z]+\b", text))
179
+ return chinese + english
180
+
181
+
182
+ def count_footnotes(text: str) -> int:
183
+ return len(_FOOTNOTE_RE.findall(text))
184
+
185
+
186
+ def count_cross_refs(text: str) -> int:
187
+ return len(_CROSS_REF_RE.findall(text))
188
+
189
+
190
+ def classify_complexity(words: int, footnotes: int, cross_refs: int) -> str:
191
+ hits = (
192
+ (1 if words > 3000 else 0)
193
+ + (1 if footnotes > 3 else 0)
194
+ + (1 if cross_refs > 2 else 0)
195
+ )
196
+ if hits == 3:
197
+ return "high"
198
+ if hits >= 1:
199
+ return "medium"
200
+ return "low"
201
+
202
+
203
+ # ── 主流程 ────────────────────────────────────────────────────────────────────
204
+
205
+ def scan(book_path: str, output_path: str | None = None) -> str:
206
+ print(f"[INFO] 扫描: {book_path}")
207
+ text, is_scan = extract_text(book_path)
208
+
209
+ if not text.strip():
210
+ print("[ERROR] 文本提取失败或为空", file=sys.stderr)
211
+ sys.exit(1)
212
+
213
+ if is_scan:
214
+ print(
215
+ "[WARN] 检测到扫描件 PDF,无法客观计算复杂度指标。"
216
+ " Stage 1 Skill 将退回 LLM 推断,请在 JSON 中注意 scan_fallback 标记。",
217
+ file=sys.stderr,
218
+ )
219
+
220
+ print(f"[INFO] 总字符数: {len(text):,}")
221
+ sections = split_sections(text)
222
+ print(f"[INFO] 识别小节数: {len(sections)}")
223
+
224
+ meta: dict = {"_meta": {"scan_fallback": is_scan, "total_sections": len(sections)}}
225
+
226
+ for sec in sections:
227
+ words = word_count(sec["content"])
228
+ footnotes = count_footnotes(sec["content"])
229
+ cross_refs = count_cross_refs(sec["content"])
230
+ complexity = "unknown" if is_scan else classify_complexity(words, footnotes, cross_refs)
231
+
232
+ meta[sec["id"]] = {
233
+ "title": sec["title"],
234
+ "words": words,
235
+ "footnotes": footnotes,
236
+ "cross_refs": cross_refs,
237
+ "complexity": complexity,
238
+ }
239
+
240
+ if output_path is None:
241
+ stem = Path(book_path).stem
242
+ output_path = str(Path(book_path).parent / f"{stem}-complexity-meta.json")
243
+
244
+ with open(output_path, "w", encoding="utf-8") as f:
245
+ json.dump(meta, f, ensure_ascii=False, indent=2)
246
+
247
+ high = sum(1 for k, v in meta.items() if k != "_meta" and v["complexity"] == "high")
248
+ medium = sum(1 for k, v in meta.items() if k != "_meta" and v["complexity"] == "medium")
249
+ low = sum(1 for k, v in meta.items() if k != "_meta" and v["complexity"] == "low")
250
+ print(f"[INFO] 复杂度分布 — high: {high} medium: {medium} low: {low}")
251
+ print(f"[INFO] 输出: {output_path}")
252
+ return output_path
253
+
254
+
255
+ if __name__ == "__main__":
256
+ if len(sys.argv) < 2:
257
+ print("用法: python complexity_scanner.py <书的路径> [输出路径]")
258
+ sys.exit(1)
259
+
260
+ scan(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else None)