hookehuyr

feat(doc-parser): 添加文档解析系统架构文档和豆包预处理支持

## 新增
- 文档解析系统架构文档 (docs/doc-parser-architecture.md)
  - 完整的三层架构说明
  - 8 种字段提取模式详解
  - 优缺点分析和优化建议

- 豆包预处理快速通道
  - 新增 preprocessed/ 目录支持
  - 自动识别文档来源
  - 优化 MD 文件解析提示

- 混合解析方案
  - 少量文档用豆包预处理
  - 批量文档用 MCP 直接解析
  - 按来源分组显示文档列表

## 更新
- README.md: 添加文档解析工具说明
- docs/to-parse/README.md: 添加豆包预处理指南和对比表

## 移除
- scripts/doc-parser/QUICKSTART.md (内容已整合)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
4 4
5 ## 📚 项目文档 5 ## 📚 项目文档
6 6
7 +- **[文档解析系统架构](docs/doc-parser-architecture.md)** - 计划书配置自动化生成工具
7 - **[经验教训总结](docs/lessons-learned.md)** - Taro 项目开发经验、最佳实践和常见陷阱 8 - **[经验教训总结](docs/lessons-learned.md)** - Taro 项目开发经验、最佳实践和常见陷阱
8 - **[CLAUDE.md](CLAUDE.md)** - 项目开发指南(供 Claude Code 使用) 9 - **[CLAUDE.md](CLAUDE.md)** - 项目开发指南(供 Claude Code 使用)
9 - **[文档导航](docs/README.md)** - 项目文档索引与使用建议 10 - **[文档导航](docs/README.md)** - 项目文档索引与使用建议
...@@ -55,7 +56,7 @@ pnpm lint ...@@ -55,7 +56,7 @@ pnpm lint
55 56
56 ### 近期亮点 57 ### 近期亮点
57 58
58 -- **多产品文档解析** - 支持自动识别和分割包含多个保险产品的文档 59 +- **文档解析系统** - 从 PDF/DOCX 自动生成计划书配置(支持多产品文档分割)
59 - **计划书 Schema 驱动** - 储蓄类/人寿/重疾模板字段配置化 60 - **计划书 Schema 驱动** - 储蓄类/人寿/重疾模板字段配置化
60 - **Git 工作流标准化** - 使用 standard-version + Conventional Commits 61 - **Git 工作流标准化** - 使用 standard-version + Conventional Commits
61 - **认证系统完善** - 401 自动刷新、登录权限检查、TabBar 红点 62 - **认证系统完善** - 401 自动刷新、登录权限检查、TabBar 红点
...@@ -270,7 +271,44 @@ export default { ...@@ -270,7 +271,44 @@ export default {
270 - ✅ 所有参数都有 `@param` 说明 271 - ✅ 所有参数都有 `@param` 说明
271 - ✅ 返回值有 `@returns` 说明 272 - ✅ 返回值有 `@returns` 说明
272 273
273 -## 🔧 可选功能 274 +## 🔧 开发工具
275 +
276 +### 文档解析工具
277 +
278 +自动从保险产品文档(PDF/DOCX)中提取配置,生成计划书模板:
279 +
280 +```bash
281 +# 解析所有待处理文档
282 +pnpm parse:docs
283 +
284 +# 解析指定文件
285 +pnpm parse:docs -- --file=产品说明书.pdf
286 +
287 +# 查看待处理文档列表
288 +pnpm parse:docs -- --list
289 +
290 +# 应用审核通过的配置
291 +pnpm parse:docs -- --apply=计划书模版4
292 +
293 +# 预览变更(不实际修改)
294 +pnpm parse:docs -- --apply=计划书模版4 --dry-run
295 +
296 +# 查看配置状态
297 +pnpm parse:docs -- --status
298 +```
299 +
300 +**核心能力**
301 +- 📄 支持 PDF、DOCX、TXT、MD 格式
302 +- 🔄 自动识别并分割多产品文档
303 +- 🤖 智能字段提取(8 个核心字段)
304 +- ✅ 人工审核流程
305 +- 💾 自动备份和回滚
306 +
307 +**详细文档**: [文档解析系统架构](docs/doc-parser-architecture.md)
308 +
309 +---
310 +
311 +### 可选功能组件
274 312
275 以下功能可以根据项目需求选择使用或移除: 313 以下功能可以根据项目需求选择使用或移除:
276 314
...@@ -281,11 +319,24 @@ export default { ...@@ -281,11 +319,24 @@ export default {
281 319
282 ## ✅ 优化建议 320 ## ✅ 优化建议
283 321
284 -- 建议将文档解析脚本接入真实 AI 解析服务以替代 mock 配置 322 +### 文档解析系统
285 -- 建议为 parse:docs 增加一键校验配置合法性的脚本输出 323 +
324 +| 优先级 | 优化项 | 说明 |
325 +|--------|--------|------|
326 +| 🔴 P0 | 启用 AI 服务 | 配置 `AI_SERVICE_TYPE` 提升复杂文档解析准确率 |
327 +| 🟡 P1 | 完善 .doc 支持 | 使用 antiword 或 LibreOffice 转换 |
328 +| 🟡 P1 | 增加自动化测试 | 补充 parse-docs.test.js 测试用例 |
329 +| 🟢 P2 | 添加 OCR 能力 | 支持扫描件解析(Tesseract.js) |
330 +
331 +### 项目整体
332 +
333 +1. 持续维护 API 集成日志与页面模块对应关系
334 +2. 文档预览与视频播放页面补充更多异常场景说明
335 +3. 页面入口与权限策略保持同步,避免入口显示但权限不一致
286 336
287 ## 📚 相关文档 337 ## 📚 相关文档
288 338
339 +- **[文档解析系统架构](docs/doc-parser-architecture.md)** - 计划书配置自动化工具详解
289 - **[经验教训总结](docs/lessons-learned.md)** - Taro 项目开发经验、最佳实践和常见陷阱 340 - **[经验教训总结](docs/lessons-learned.md)** - Taro 项目开发经验、最佳实践和常见陷阱
290 - **[CLAUDE.md](CLAUDE.md)** - 项目开发指南(供 Claude Code 使用) 341 - **[CLAUDE.md](CLAUDE.md)** - 项目开发指南(供 Claude Code 使用)
291 - **[文档解析待处理说明](docs/to-parse/README.md)** - 文档解析样本与脚本使用方式 342 - **[文档解析待处理说明](docs/to-parse/README.md)** - 文档解析样本与脚本使用方式
......
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
1 -# OpenAPI 转 API 文档生成器 - 快速开始
2 -
3 -## 🎯 一分钟快速上手
4 -
5 -### 1️⃣ 创建 OpenAPI 文档
6 -
7 -`docs/api-specs/` 目录下创建模块和接口文档:
8 -
9 -```bash
10 -# 创建新模块
11 -mkdir -p docs/api-specs/product
12 -
13 -# 创建接口文档
14 -touch docs/api-specs/product/getList.md
15 -```
16 -
17 -### 2️⃣ 编写 OpenAPI 规范
18 -
19 -编辑 `getList.md`
20 -
21 -```markdown
22 -# 获取商品列表
23 -
24 -## OpenAPI Specification
25 -
26 -\```yaml
27 -openapi: 3.0.1
28 -info:
29 - title: ''
30 - version: 1.0.0
31 -paths:
32 - /srv/:
33 - get:
34 - summary: 获取商品列表
35 - tags:
36 - - 商品
37 - parameters:
38 - - name: a
39 - in: query
40 - example: product_list
41 - - name: f
42 - in: query
43 - example: behalo
44 - responses:
45 - '200':
46 - description: 成功
47 -\```
48 -```
49 -
50 -### 3️⃣ 生成 API 文件
51 -
52 -```bash
53 -pnpm api:generate
54 -```
55 -
56 -### 4️⃣ 使用生成的 API
57 -
58 -```javascript
59 -import { getListAPI } from '@/api/product';
60 -
61 -const result = await getListAPI({ page: 1, pageSize: 10 });
62 -```
63 -
64 -## ✅ 验证结果
65 -
66 -运行测试脚本验证生成的文件:
67 -
68 -```bash
69 -node scripts/test-generate.js
70 -```
71 -
72 -## 📂 文件结构
73 -
74 -```
75 -manulife-weapp/
76 -├── docs/
77 -│ ├── api-specs/ # API 规范文档源目录
78 -│ │ └── user/ # 模块目录
79 -│ │ └── getUserInfo.md
80 -│ ├── OPENAPI_TO_API_GUIDE.md # 详细使用指南
81 -│ └── API_USAGE_EXAMPLES.md # API 使用示例
82 -├── scripts/
83 -│ ├── generateApiFromOpenAPI.js # 生成器核心脚本
84 -│ └── test-generate.js # 测试脚本
85 -├── src/
86 -│ └── api/ # 生成的 API 文件目录
87 -│ ├── user.js # 自动生成
88 -│ ├── wx/
89 -│ └── index.js
90 -└── package.json # 包含 api:generate 命令
91 -```
92 -
93 -## 🔄 工作流程
94 -
95 -```mermaid
96 -graph LR
97 - A[编写 OpenAPI 文档] --> B[运行 pnpm api:generate]
98 - B --> C[生成 API 文件]
99 - C --> D[在项目中使用]
100 - D --> E[需要修改接口]
101 - E --> A
102 -```
103 -
104 -## 🎨 常见场景
105 -
106 -### 场景 1: 批量生成多个接口
107 -
108 -```bash
109 -docs/api-specs/
110 -├── user/
111 -│ ├── getUserInfo.md
112 -│ ├── updateProfile.md
113 -│ └── changePassword.md
114 -└── order/
115 - ├── getList.md
116 - └── getDetail.md
117 -```
118 -
119 -运行 `pnpm api:generate` 后生成:
120 -
121 -```
122 -src/api/
123 -├── user.js # 包含 3 个接口
124 -└── order.js # 包含 2 个接口
125 -```
126 -
127 -### 场景 2: 更新已有接口
128 -
129 -1. 修改 `docs/api-specs/user/getUserInfo.md`
130 -2. 运行 `pnpm api:generate`
131 -3. `src/api/user.js` 自动更新
132 -
133 -### 场景 3: 添加新模块
134 -
135 -1. 创建 `docs/api-specs/payment/`
136 -2. 添加接口文档
137 -3. 运行生成命令
138 -4. 自动生成 `src/api/payment.js`
139 -
140 -## ⚙️ 配置和自定义
141 -
142 -### 修改输出目录
143 -
144 -编辑 `scripts/generateApiFromOpenAPI.js`
145 -
146 -```javascript
147 -const outputDir = path.resolve(__dirname, '../src/api');
148 -// 改为你想要的目录
149 -const outputDir = path.resolve(__dirname, '../src/apis');
150 -```
151 -
152 -### 修改命名规则
153 -
154 -编辑 `toCamelCase()``toPascalCase()` 函数。
155 -
156 -### 修改生成模板
157 -
158 -编辑 `generateApiFileContent()` 函数。
159 -
160 -## 🐛 调试技巧
161 -
162 -### 启用详细日志
163 -
164 -在脚本中添加更多 console.log:
165 -
166 -```javascript
167 -console.log('解析的 API 信息:', JSON.stringify(apiInfo, null, 2));
168 -```
169 -
170 -### 单独测试某个模块
171 -
172 -修改脚本中的模块过滤逻辑。
173 -
174 -### 查看生成的中间数据
175 -
176 -添加调试输出查看 YAML 解析结果。
177 -
178 -## 📞 获取帮助
179 -
180 -- 详细指南:[OpenAPI 转 API 文档生成器指南](./OPENAPI_TO_API_GUIDE.md)
181 -- 使用示例:[API 使用示例](./API_USAGE_EXAMPLES.md)
182 -- 项目架构:[CLAUDE.md](../CLAUDE.md)
183 -
184 -## 🎉 开始使用
185 -
186 -现在你已经准备好了!开始创建你的第一个 OpenAPI 文档吧。
187 -
188 -```bash
189 -# 1. 创建模块目录
190 -mkdir -p docs/api-specs/your-module
191 -
192 -# 2. 创建接口文档(参考 docs/api-specs/user/getUserInfo.md)
193 -
194 -# 3. 生成 API
195 -pnpm api:generate
196 -
197 -# 4. 查看生成的文件
198 -cat src/api/your-module.js
199 -
200 -# 5. 开始使用
201 -```
202 -
203 -祝你编码愉快!🚀
...@@ -42,6 +42,8 @@ import { splitByProducts, findProductTitles, generateSplitReport } from './produ ...@@ -42,6 +42,8 @@ import { splitByProducts, findProductTitles, generateSplitReport } from './produ
42 // ========== 配置区 ========== 42 // ========== 配置区 ==========
43 43
44 const DOCS_DIR = path.resolve(process.cwd(), 'docs/to-parse') 44 const DOCS_DIR = path.resolve(process.cwd(), 'docs/to-parse')
45 +const DOCS_PREPROCESSED_DIR = path.resolve(process.cwd(), 'docs/to-parse/preprocessed')
46 +const DOCS_RAW_DIR = path.resolve(process.cwd(), 'docs/to-parse/raw')
45 const DOCS_ARCHIVE_DIR = path.resolve(process.cwd(), 'docs/to-parse/archived') 47 const DOCS_ARCHIVE_DIR = path.resolve(process.cwd(), 'docs/to-parse/archived')
46 const CONFIG_FILE = path.resolve(process.cwd(), 'src/config/plan-templates.js') 48 const CONFIG_FILE = path.resolve(process.cwd(), 'src/config/plan-templates.js')
47 const BACKUP_DIR = path.resolve(process.cwd(), 'docs/parsed-backup') 49 const BACKUP_DIR = path.resolve(process.cwd(), 'docs/parsed-backup')
...@@ -49,6 +51,29 @@ const BACKUP_DIR = path.resolve(process.cwd(), 'docs/parsed-backup') ...@@ -49,6 +51,29 @@ const BACKUP_DIR = path.resolve(process.cwd(), 'docs/parsed-backup')
49 // 支持的文档格式 51 // 支持的文档格式
50 const SUPPORTED_EXTENSIONS = ['.pdf', '.doc', '.docx', '.txt', '.md'] 52 const SUPPORTED_EXTENSIONS = ['.pdf', '.doc', '.docx', '.txt', '.md']
51 53
54 +/**
55 + * 检测文档来源
56 + *
57 + * @description 判断文档是预处理过的 MD 文件还是原始文档
58 + * @param {string} filePath - 文档路径
59 + * @returns {{source: string, type: string}} 来源信息
60 + */
61 +function detectDocumentSource(filePath) {
62 + if (filePath.includes('preprocessed')) {
63 + return { source: 'preprocessed', type: 'markdown' }
64 + }
65 + if (filePath.includes('raw')) {
66 + return { source: 'raw', type: 'original' }
67 + }
68 + // 根据文件扩展名推断
69 + const ext = path.extname(filePath).toLowerCase()
70 + if (ext === '.md') {
71 + // MD 文件可能是预处理过的
72 + return { source: 'likely-preprocessed', type: 'markdown' }
73 + }
74 + return { source: 'unknown', type: 'original' }
75 +}
76 +
52 const ajv = new Ajv({ allErrors: true, strict: false }) 77 const ajv = new Ajv({ allErrors: true, strict: false })
53 const parseConfigSchema = { 78 const parseConfigSchema = {
54 type: 'object', 79 type: 'object',
...@@ -214,23 +239,45 @@ function writeFile(filePath, content) { ...@@ -214,23 +239,45 @@ function writeFile(filePath, content) {
214 239
215 /** 240 /**
216 * 获取所有待处理的文档 241 * 获取所有待处理的文档
242 + *
243 + * @description 扫描多个目录获取待处理文档,按优先级排序
244 + * @returns {Array<{name: string, fullPath: string, ext: string, size: number, source: string}>} 文档列表
217 */ 245 */
218 function getDocsToParse() { 246 function getDocsToParse() {
219 - if (!fs.existsSync(DOCS_DIR)) { 247 + const docs = []
220 - console.log('📂 文档夹不存在:', DOCS_DIR) 248 + const directories = [
221 - return [] 249 + { path: DOCS_DIR, source: 'root' },
250 + { path: DOCS_PREPROCESSED_DIR, source: 'preprocessed' },
251 + { path: DOCS_RAW_DIR, source: 'raw' }
252 + ]
253 +
254 + for (const dir of directories) {
255 + if (!fs.existsSync(dir.path)) {
256 + continue
222 } 257 }
223 258
224 - const files = fs.readdirSync(DOCS_DIR) 259 + const files = fs.readdirSync(dir.path)
225 - return files 260 + const dirDocs = files
226 .filter(file => SUPPORTED_EXTENSIONS.includes(path.extname(file).toLowerCase())) 261 .filter(file => SUPPORTED_EXTENSIONS.includes(path.extname(file).toLowerCase()))
227 .filter(file => file !== 'README.md') 262 .filter(file => file !== 'README.md')
228 .map(file => ({ 263 .map(file => ({
229 name: file, 264 name: file,
230 - fullPath: path.join(DOCS_DIR, file), 265 + fullPath: path.join(dir.path, file),
231 ext: path.extname(file).toLowerCase(), 266 ext: path.extname(file).toLowerCase(),
232 - size: fs.statSync(path.join(DOCS_DIR, file)).size 267 + size: fs.statSync(path.join(dir.path, file)).size,
268 + source: dir.source
233 })) 269 }))
270 +
271 + docs.push(...dirDocs)
272 + }
273 +
274 + // 优先处理预处理的 MD 文件,然后是原始文档
275 + docs.sort((a, b) => {
276 + const priorityOrder = { preprocessed: 1, root: 2, raw: 3 }
277 + return priorityOrder[a.source] - priorityOrder[b.source]
278 + })
279 +
280 + return docs
234 } 281 }
235 282
236 /** 283 /**
...@@ -367,10 +414,15 @@ function formatSize(size) { ...@@ -367,10 +414,15 @@ function formatSize(size) {
367 */ 414 */
368 async function parseDocumentWithMarkitdown(docPath) { 415 async function parseDocumentWithMarkitdown(docPath) {
369 const ext = path.extname(docPath).toLowerCase() 416 const ext = path.extname(docPath).toLowerCase()
417 + const sourceInfo = detectDocumentSource(docPath)
370 418
371 // MD 和 TXT 文件直接读取,不需要 markitdown 419 // MD 和 TXT 文件直接读取,不需要 markitdown
372 if (ext === '.md' || ext === '.txt') { 420 if (ext === '.md' || ext === '.txt') {
421 + if (sourceInfo.source === 'preprocessed' || sourceInfo.source === 'likely-preprocessed') {
422 + console.log(`⚡ 预处理 MD 文件,跳过 markitdown: ${path.basename(docPath)}`)
423 + } else {
373 console.log(`📄 直接读取文本文件: ${path.basename(docPath)}`) 424 console.log(`📄 直接读取文本文件: ${path.basename(docPath)}`)
425 + }
374 return buildExtractResult(docPath, fs.readFileSync(docPath, 'utf-8'), []) 426 return buildExtractResult(docPath, fs.readFileSync(docPath, 'utf-8'), [])
375 } 427 }
376 428
...@@ -707,8 +759,17 @@ function inferCurrency(content) { ...@@ -707,8 +759,17 @@ function inferCurrency(content) {
707 */ 759 */
708 async function parseSingleFile(filePath) { 760 async function parseSingleFile(filePath) {
709 const fileName = path.basename(filePath) 761 const fileName = path.basename(filePath)
762 + const sourceInfo = detectDocumentSource(filePath)
763 + const sourceLabel = {
764 + preprocessed: '⚡ 预处理文档',
765 + raw: '📄 原始文档',
766 + root: '📂 根目录文档',
767 + 'likely-preprocessed': '⚡ MD 文档',
768 + unknown: '📄 文档'
769 + }[sourceInfo.source] || '📄 文档'
770 +
710 console.log("\n" + "=".repeat(60)) 771 console.log("\n" + "=".repeat(60))
711 - console.log("📄 处理文件: " + fileName) 772 + console.log(`📄 ${sourceLabel}: ${fileName}`)
712 console.log("=".repeat(60)) 773 console.log("=".repeat(60))
713 774
714 // 解析文档(可能返回单个 config 或 configs 数组) 775 // 解析文档(可能返回单个 config 或 configs 数组)
...@@ -1799,15 +1860,33 @@ async function main() { ...@@ -1799,15 +1860,33 @@ async function main() {
1799 applyAuditFile(auditFileName, applyOptions) 1860 applyAuditFile(auditFileName, applyOptions)
1800 } else if (listMode) { 1861 } else if (listMode) {
1801 // 列出模式 1862 // 列出模式
1802 - const docs = getDocsToParse()
1803 console.log("\n📋 待处理文档列表:") 1863 console.log("\n📋 待处理文档列表:")
1804 if (docs.length === 0) { 1864 if (docs.length === 0) {
1805 console.log(' (无文档)') 1865 console.log(' (无文档)')
1806 } else { 1866 } else {
1807 - docs.forEach((doc, index) => { 1867 + // 按来源分组显示
1808 - console.log(" " + (index + 1) + ". " + doc.name + " (" + formatSize(doc.size) + ")") 1868 + const grouped = {
1869 + preprocessed: docs.filter(d => d.source === 'preprocessed'),
1870 + root: docs.filter(d => d.source === 'root'),
1871 + raw: docs.filter(d => d.source === 'raw')
1872 + }
1873 +
1874 + for (const [source, sourceDocs] of Object.entries(grouped)) {
1875 + if (sourceDocs.length === 0) continue
1876 +
1877 + const sourceLabel = {
1878 + preprocessed: '⚡ 预处理 (preprocessed/)',
1879 + root: '📂 根目录 (docs/to-parse/)',
1880 + raw: '📄 原始文档 (raw/)'
1881 + }[source]
1882 +
1883 + console.log(`\n${sourceLabel}`)
1884 + sourceDocs.forEach((doc, index) => {
1885 + const sourceTag = doc.ext === '.md' ? ' [MD]' : ''
1886 + console.log(` ${index + 1}. ${doc.name}${sourceTag} (${formatSize(doc.size)})`)
1809 }) 1887 })
1810 } 1888 }
1889 + }
1811 } else if (fileMode) { 1890 } else if (fileMode) {
1812 // 单文件模式 1891 // 单文件模式
1813 const fileName = fileMode.split('=')[1] 1892 const fileName = fileMode.split('=')[1]
......