hookehuyr

docs(parse): 完善文档解析改造文档与测试验证

### 新增
- 文档解析改造任务清单说明
- 文本抽取管线、结构化校验、写入稳态化等模块说明
- 解析摘要输出与审计日志功能说明
- 计划书模块定位与优化建议

### 修复
- 修复 ESLint 警告

### 测试
- 补充解析流程集成测试与边界测试
- 新增 fixtures 文档样本说明

---

**详细信息**:
- **影响文件**: README.md, docs/CHANGELOG.md, docs/PLAN/plan-form-schema-usage.md, docs/to-parse/README.md, scripts/parse-docs.js, scripts/parse-docs.test.js
- **技术栈**: Node.js, Vitest, 文档维护
- **测试状态**: 已通过 (pnpm test),ESLint 存在现有警告
- **备注**: 每次解析都有可追溯审计记录

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
......@@ -70,6 +70,14 @@ pnpm lint
-**新人指南更新** - 入口文档从工具生成器调整为业务上手流程
-**文档导航同步** - docs/README 快速导航修正与补充
### 文档解析改造
-**任务清单** - 输出文档解析改造任务清单,便于跟踪与回顾
-**文本抽取管线** - 接入 PDF/Docx 文本抽取与统一结构输出
-**结构化校验** - 接入 JSON Schema 校验并阻断非法配置写入
-**写入稳态化** - 结构化插入、重复检测与 dry-run 预览已接入
-**输出结构补齐** - 解析输出 JSON 结构与稳定 form_sn 规则已明确
-**审计与摘要** - 解析摘要与审计日志输出已接入
### 计划书模块定位
-**配置与入口整理** - 补充计划书模块入口、配置与 API 位置说明
-**优化建议** - 新增产品时优先补齐 form_sn 与 plan_config,避免模板缺失
......@@ -365,6 +373,8 @@ export default {
- **[经验教训总结](docs/lessons-learned.md)** - Taro 项目开发经验、最佳实践和常见陷阱
- **[CLAUDE.md](CLAUDE.md)** - 项目开发指南(供 Claude Code 使用)
- **[文档解析待处理说明](docs/to-parse/README.md)** - 文档解析样本与脚本使用方式
- **[文档解析改造任务](docs/tasks/文档解析改造-tasks.md)** - 解析链路改造进度与验收
- [Taro 官方文档](https://docs.taro.zone/)
- [NutUI 文档](https://nutui.jd.com/taro/)
- [Vue 3 文档](https://cn.vuejs.org/)
......
## [2026-02-14] - 运营与审计完善
### 新增
- 解析摘要输出(成功/失败/耗时)并生成审计日志与变更摘要
- 使用说明补充解析摘要与审计日志位置
---
**详细信息**
- **影响文件**: scripts/parse-docs.js, scripts/parse-docs.test.js, docs/to-parse/README.md, docs/tasks/文档解析改造-tasks.md, README.md
- **技术栈**: Node.js, Vitest, 文档维护
- **测试状态**: pnpm test 通过;pnpm lint 30 warnings
- **备注**: 每次解析都有可追溯审计记录
---
## [2026-02-14] - 测试与验证完善
### 新增
- 补充解析流程集成测试与 updateConfigContent 边界测试
- 新增 fixtures 文档样本说明并补齐相关文档入口
---
**详细信息**
- **影响文件**: scripts/parse-docs.js, scripts/parse-docs.test.js, docs/to-parse/README.md, docs/tasks/文档解析改造-tasks.md, README.md
- **技术栈**: Node.js, Vitest, 文档维护
- **测试状态**: 已通过(pnpm test),ESLint 存在现有警告
- **备注**: 解析流程测试可重复运行,覆盖冲突与插入边界路径
---
## [2026-02-14] - 生成与写入稳态化
### 新增
- 结构化定位 PLAN_TEMPLATES 插入位置并支持 dry-run 变更预览
- 增加重复 form_sn 冲突检测与阻断写入
- 完善备份记录并支持回滚入口
---
**详细信息**
- **影响文件**: scripts/parse-docs.js, scripts/parse-docs.test.js, docs/tasks/文档解析改造-tasks.md, README.md
- **技术栈**: Node.js, Vitest
- **测试状态**: 已通过(pnpm test),ESLint 存在现有警告
- **备注**: 解析写入路径更稳定,新增冲突保护与预览模式
---
## [2026-02-14] - 结构化解析校验接入
### 新增
- 接入 JSON Schema 校验并输出缺失字段报告
- 校验失败阻断解析结果写入配置
- 单测覆盖校验通过与失败路径
---
**详细信息**
- **影响文件**: scripts/parse-docs.js, scripts/parse-docs.test.js, package.json, docs/tasks/文档解析改造-tasks.md, README.md
- **技术栈**: Node.js, Ajv, Vitest
- **测试状态**: 已通过(pnpm test),ESLint 存在现有警告
- **备注**: 校验规则覆盖核心字段并保留扩展字段
---
## [2026-02-14] - 文本抽取管线接入
### 新增
- 接入 PDF 文本抽取与页数元信息
- 接入 Docx 文本抽取并输出警告信息
- 统一抽取结果结构并增加抽取失败回退
---
**详细信息**
- **影响文件**: scripts/parse-docs.js, scripts/parse-docs.test.js, package.json, docs/tasks/文档解析改造-tasks.md, README.md
- **技术栈**: Node.js, Vitest
- **测试状态**: 已通过(pnpm test),ESLint 存在现有警告
- **备注**: .doc 文件提示转换为 .docx,OCR 预留未启用
---
## [2026-02-14] - 文档解析输出定义完善
### 更新
- 明确解析输出 JSON 结构并补齐示例与约束
- 生成 form_sn 改为稳定的 slug + hash 规则
- 配置生成支持 form_schema 与 submit_mapping 输出
---
**详细信息**
- **影响文件**: scripts/parse-docs.js, scripts/parse-docs.test.js, docs/plan/plan-form-schema-usage.md, docs/tasks/文档解析改造-tasks.md, README.md
- **技术栈**: Node.js, Vitest, 文档维护
- **测试状态**: 已通过(pnpm test),ESLint 存在现有警告
- **备注**: 解析输出结构对齐 Schema 与提交映射配置
---
## [2026-02-14] - 文档解析改造任务清单
### 新增
- 新增文档解析改造任务清单,细化步骤与验收标准
---
**详细信息**
- **影响文件**: docs/tasks/文档解析改造-tasks.md, README.md
- **技术栈**: 文档维护
- **测试状态**: 不适用
- **备注**: 任务完成后按清单勾选便于回顾
---
## [2026-02-14] - 优化计划书字段配置管理
### 新增
......
......@@ -85,7 +85,37 @@ const submit_mapping = {
}
```
## 8. 使用示例
## 8. 解析输出结构
解析脚本输出 JSON 用于生成 `plan-templates` 配置,字段结构与 `form_schema``submit_mapping` 对齐:
```javascript
{
product_name: '宏挚传承保障计划',
product_type: 'savings',
form_sn: 'savings-hong-zhi-chuan-cheng-abcdef12',
currency: 'USD',
payment_periods: ['整付', '3年', '5年'],
age_range: { min: 0, max: 75 },
insurance_period: '终身',
is_savings: true,
withdrawal_modes: ['年龄指定金额', '最高固定金额'],
withdrawal_periods: ['1年', '3年', '5年', '10年'],
form_schema: { base_fields: [], withdrawal_fields: [], reset_map: {} },
submit_mapping: { coverage: { api_field: 'annual_premium', transform: 'fen_to_yuan' } },
source_file: '产品说明书.pdf',
warnings: []
}
```
字段约束与可选项:
- 必填:`product_name``product_type``currency``payment_periods``age_range``insurance_period`
- 可选:`form_sn``is_savings``withdrawal_modes``withdrawal_periods``form_schema``submit_mapping``source_file``warnings`
- `form_sn` 若未传入,按规则自动生成稳定值
- `payment_periods` 必须为非空数组
- `age_range.min``age_range.max`
- 储蓄产品需提供 `withdrawal_modes``withdrawal_periods`
## 9. 使用示例
```vue
<!-- 储蓄型模板使用示例 -->
<template>
......@@ -111,7 +141,7 @@ const template_config = {
</script>
```
## 8.1 人寿/重疾模板使用示例
## 9.1 人寿/重疾模板使用示例
```vue
<template>
<LifeInsuranceTemplate v-model="form_data" :config="template_config" />
......@@ -129,14 +159,14 @@ const template_config = {
</script>
```
## 9. 新增保险类型流程
## 10. 新增保险类型流程
1.`src/config/plan-templates.js` 新增产品项(配置 form_sn)
2. 为该产品选择已有模板组件或新增模板组件
3. 定义 `form_schema``submit_mapping`
4. 在模板组件内使用 Schema 渲染(仅需接入通用逻辑)
5. 验证校验与提交映射
## 10. 新增产品配置示例
## 11. 新增产品配置示例
```javascript
// 示例:新增储蓄类产品配置
'savings-new': {
......@@ -171,20 +201,20 @@ const template_config = {
}
```
## 11. 常见扩展点
## 12. 常见扩展点
- 新字段:仅在 form_schema 增加字段并补充 submit_mapping
- 新联动:在 show_when 与 reset_map 中定义条件
- 新模板:复用现有字段组件,保持 schema 结构一致
## 12. 计划书模块入口与配置地图
### 12.1 页面入口
## 13. 计划书模块入口与配置地图
### 13.1 页面入口
- 产品详情:`src/pages/product-detail/index.vue`(按钮打开计划书弹窗)
- 产品中心:`src/pages/product-center/index.vue`(列表内“计划书”按钮)
- 搜索页:`src/pages/search/index.vue`(搜索结果卡片“计划书”按钮)
- 计划书列表:`src/pages/plan/index.vue`(查看/删除计划书)
- 提交结果页:`src/pages/plan-submit-result/index.vue`
### 12.2 组件与模板
### 13.2 组件与模板
- 弹窗容器:`src/components/plan/PlanPopupNew.vue`
- 计划书容器:`src/components/plan/PlanFormContainer.vue`
- 模板组件:
......@@ -193,7 +223,7 @@ const template_config = {
- `src/components/plan/PlanTemplates/SavingsTemplate.vue`
- 字段组件:`src/components/plan/PlanFields/*`
### 12.3 配置与数据处理
### 13.3 配置与数据处理
- 模板映射:`src/config/plan-templates.js`
- 字段定义与映射:`src/config/plan-fields.js`
- 字段转换函数:`src/utils/planFieldTransformers.js`
......@@ -202,18 +232,18 @@ const template_config = {
- 字段校验工具:`src/utils/planFieldValidation.js`
- 订单状态常量:`src/config/constants/orderStatus.js`
### 12.4 API 入口
### 13.4 API 入口
- 计划书 API:`src/api/plan.js`
- 新增:`addAPI`
- 列表:`listAPI`
- 删除:`deleteAPI`
- 查看:`viewAPI`
### 12.5 技术书/附件预览关联
### 13.5 技术书/附件预览关联
- 产品详情附件列表:`src/pages/product-detail/index.vue`
- 文件预览能力:`src/composables/useFileOperation.js`
## 13. 计划书模块使用流程
## 14. 计划书模块使用流程
1. 产品详情/产品中心/搜索页获取产品对象(至少包含 `id``form_sn`,可选 `plan_config`
2. 打开 `PlanFormContainer` 并传入 `product`
3. `PlanFormContainer` 根据 `form_sn``plan-templates` 选择模板并合并 `plan_config`
......@@ -222,7 +252,7 @@ const template_config = {
6. 提交完成后通过 `usePlanSubmit` 跳转到提交结果页
7. 在计划书列表中用 `listAPI` 拉取数据,使用 `viewAPI` 标记为已查看
## 14. 计划书容器使用示例
## 15. 计划书容器使用示例
```vue
<template>
<PlanFormContainer
......
# 文档解析改造任务清单
> **创建时间**: 2026-02-14
> **分支**: 当前分支
> **目标**: 文档解析从 mock 走向可用链路
---
## 📊 总体进度
- [x] **第 1 步**: 目标与输出定义
- [ ] **第 2 步**: 文本抽取管线
- [ ] **第 3 步**: 结构化解析与校验
- [x] **第 4 步**: 生成与写入稳态化
- [x] **第 5 步**: 测试与验证
- [x] **第 6 步**: 运营与审计
---
## 📝 任务详情
### 第 1 步:目标与输出定义
**目标**: 明确解析输出结构与计划书配置的对齐规则
**文件**:
- `docs/plan/plan-form-schema-usage.md`
- `scripts/parse-docs.js`
**子任务**:
- [x] 定义解析输出 JSON 结构(字段、类型、必填/可选)
- [x] 对齐 form_schema 与 submit_mapping 规范
- [x] 明确 form_sn 可复现生成规则
- [x] 补齐输出示例与边界约束说明
**验收标准**:
- [x] 输出结构在文档中完整可查
- [x] form_sn 规则具备稳定性与可追溯性
- [x] 解析输出可直接用于配置生成
---
### 第 2 步:文本抽取管线
**目标**: 建立 PDF/Word 文本抽取基础能力
**文件**:
- `scripts/parse-docs.js`
- `package.json`
**子任务**:
- [x] 选择 PDF 文本抽取方案并完成接入
- [x] 选择 Doc/Docx 文本抽取方案并完成接入
- [x] 为扫描文档预留 OCR 接口与降级策略
- [x] 统一抽取结果结构(text/meta/warnings)
- [x] 增加抽取失败的错误提示与回退逻辑
**验收标准**:
- [x] PDF 与 Docx 均可输出可用文本
- [x] 抽取失败可定位原因并不写入配置
- [x] 日志记录包含文件名与失败原因
---
### 第 3 步:结构化解析与校验
**目标**: 将文本解析成结构化配置并进行校验
**文件**:
- `scripts/parse-docs.js`
- `scripts/parse-docs.test.js`
**子任务**:
- [x] 定义 JSON Schema 校验规则
- [x] 接入结构化解析结果校验
- [x] 校验失败输出清晰报告
- [x] 校验失败阻断写入配置
- [x] 增加最小覆盖单测与示例
**验收标准**:
- [x] 不合法配置不会写入 plan-templates
- [x] 校验错误可一眼定位缺失字段
- [x] 单测覆盖关键异常路径
---
### 第 4 步:生成与写入稳态化
**目标**: 输出稳定可控、支持 diff 与回滚
**文件**:
- `scripts/parse-docs.js`
- `src/config/plan-templates.js`
**子任务**:
- [x] form_sn 改为 slug + hash 的稳定规则
- [x] 插入位置改为锚点块或结构化写入
- [x] 增加重复 form_sn 检测与冲突提示
- [x] 支持 dry-run 输出变更 diff
- [x] 备份与回滚记录完善
**验收标准**:
- [x] 重复解析不会产生随机 form_sn
- [x] 插入位置稳定可靠
- [x] dry-run 能清晰展示新增/修改内容
---
### 第 5 步:测试与验证
**目标**: 保证解析流程可回归验证
**文件**:
- `scripts/parse-docs.test.js`
- `docs/to-parse/README.md`
**子任务**:
- [x] 新增 fixtures 文档样本说明
- [x] 增加解析流程集成测试
- [x] 补充 updateConfigContent 边界测试
- [x] 运行测试并记录结果
**验收标准**:
- [x] 解析流程有稳定测试兜底
- [x] 关键边界路径有覆盖
- [x] 测试可重复运行
---
### 第 6 步:运营与审计
**目标**: 便于长期维护与复盘
**文件**:
- `scripts/parse-docs.js`
- `docs/to-parse/README.md`
**子任务**:
- [x] 输出解析摘要(成功/失败/耗时)
- [x] 生成审计日志与变更摘要
- [x] 更新使用说明与注意事项
**验收标准**:
- [x] 每次解析均可追踪结果
- [x] 文档能指导新成员完成解析
---
## 🔍 快速跳转
- [解析脚本](./../../scripts/parse-docs.js)
- [解析测试](./../../scripts/parse-docs.test.js)
- [待解析说明](./../../docs/to-parse/README.md)
- [计划书配置](./../../src/config/plan-templates.js)
- [Schema 使用文档](./../../docs/plan/plan-form-schema-usage.md)
---
## 📝 备注
- 每完成一个子任务,就在对应的 [ ] 中打勾 ✓
- 任务执行过程中的问题与结论直接补充在对应任务下
......@@ -39,6 +39,29 @@ pnpm run parse:docs:file -- --file="产品说明书.pdf"
- ✅ Word (.doc, .docx)
- ✅ 纯本文档 (.txt, .md)
## 🧪 Fixtures 文档样本说明
用于测试的样本文档建议放在此目录,命名规则建议包含产品名与类型,便于回归验证:
```
docs/to-parse/
├── fixtures-life-insurance-sample.pdf
├── fixtures-critical-illness-sample.docx
└── fixtures-savings-sample.txt
```
执行测试前请确认样本文档内容完整且可被抽取为文本。
## 📊 解析摘要与审计日志
每次解析都会输出成功/失败/耗时摘要,并在以下位置记录审计日志:
```
docs/parsed-backup/parse-audit.jsonl
```
日志包含解析汇总与本次变更摘要,便于回溯与排查。
## 🔧 配置 AI 服务
脚本使用 skill 工具调用 AI 服务,支持:
......
......@@ -96,9 +96,12 @@
"eslint-plugin-vue": "^8.0.0",
"happy-dom": "^14.12.0",
"husky": "^9.1.7",
"ajv": "^8.17.1",
"js-yaml": "^4.1.1",
"less": "^4.2.0",
"lint-staged": "^16.2.7",
"mammoth": "^1.9.1",
"pdf-parse": "^2.2.0",
"postcss": "^8.5.6",
"sass": "^1.78.0",
"standard-version": "^9.5.0",
......
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
import { describe, it, expect } from 'vitest'
import { generateFormSn, generateConfigCode, updateConfigContent } from './parse-docs'
import fs from 'fs'
import os from 'os'
import path from 'path'
import { generateFormSn, generateConfigCode, updateConfigContent, extractDocumentText, validateParsedConfig, detectFormSnConflicts, buildDryRunDiff, buildConfigUpdateResult, buildParseSummary } from './parse-docs'
describe('parse-docs 生成逻辑', () => {
it('generateFormSn 使用产品类型前缀', () => {
it('generateFormSn 使用稳定规则生成', () => {
const form_sn = generateFormSn({
product_name: 'WIOP3E 盈传创富保障计划 3 - 优选版',
product_type: 'life-insurance'
})
const form_sn_repeat = generateFormSn({
product_name: 'WIOP3E 盈传创富保障计划 3 - 优选版',
product_type: 'life-insurance'
})
expect(form_sn).toBe(form_sn_repeat)
expect(form_sn.startsWith('life-insurance-')).toBe(true)
expect(form_sn).toMatch(/^life-insurance-[a-z0-9-]+-[a-f0-9]{8}$/)
})
it('generateConfigCode 储蓄配置包含顶层 category', () => {
......@@ -54,4 +63,117 @@ export const FEATURE_FLAGS = {}`
expect(result).toMatch(/'a'[\s\S]*},\n\s+'b'/)
expect(result).toMatch(/'b'[\s\S]*}\n\nexport const FEATURE_FLAGS/)
})
it('updateConfigContent 无模板时返回 null', () => {
const base_content = `export const OTHER = {}`
const result = updateConfigContent(base_content, [
{ code: " 'b': {\n name: 'B'\n }" }
])
expect(result).toBe(null)
})
it('extractDocumentText 统一抽取结构', async () => {
const temp_dir = fs.mkdtempSync(path.join(os.tmpdir(), 'doc-parse-'))
const temp_file = path.join(temp_dir, 'sample.txt')
fs.writeFileSync(temp_file, 'hello parse')
const result = await extractDocumentText(temp_file)
expect(result.text).toBe('hello parse')
expect(result.meta.ext).toBe('.txt')
expect(Array.isArray(result.warnings)).toBe(true)
})
it('validateParsedConfig 能识别缺失字段', () => {
const invalid = validateParsedConfig({
product_type: 'savings',
currency: 'USD'
})
const valid = validateParsedConfig({
product_name: '宏挚传承保障计划',
product_type: 'savings',
currency: 'USD',
form_schema: { base_fields: [], withdrawal_fields: [], reset_map: {} },
submit_mapping: {}
})
expect(invalid.valid).toBe(false)
expect(invalid.errors.length).toBeGreaterThan(0)
expect(valid.valid).toBe(true)
})
it('detectFormSnConflicts 能识别重复 form_sn', () => {
const base_content = `export const PLAN_TEMPLATES = {
'a': {
name: 'A',
component: 'LifeInsuranceTemplate',
config: {
currency: 'USD',
payment_periods: [],
age_range: { min: 0, max: 1 },
insurance_period: '终身'
}
}
}
export const FEATURE_FLAGS = {}`
const conflicts = detectFormSnConflicts(base_content, [
{ formSn: 'a', code: ' ' },
{ formSn: 'b', code: ' ' }
])
expect(conflicts).toEqual(['a'])
})
it('buildDryRunDiff 输出新增内容', () => {
const diff = buildDryRunDiff([
{ formSn: 'b', code: " 'b': {\n name: 'B'\n }" }
])
expect(diff.includes('--- plan-templates.js')).toBe(true)
expect(diff.includes("+++ plan-templates.js")).toBe(true)
expect(diff.includes("+ 'b': {")).toBe(true)
})
it('buildConfigUpdateResult 覆盖冲突与 dry-run', () => {
const base_content = `export const PLAN_TEMPLATES = {
'a': {
name: 'A',
component: 'LifeInsuranceTemplate',
config: {
currency: 'USD',
payment_periods: [],
age_range: { min: 0, max: 1 },
insurance_period: '终身'
}
}
}
export const FEATURE_FLAGS = {}`
const conflict_result = buildConfigUpdateResult(base_content, [
{ formSn: 'a', code: " 'a': {\n name: 'A'\n }" }
])
const dry_run_result = buildConfigUpdateResult(base_content, [
{ formSn: 'b', code: " 'b': {\n name: 'B'\n }" }
], { dry_run: true })
expect(conflict_result.ok).toBe(false)
expect(conflict_result.conflicts).toEqual(['a'])
expect(dry_run_result.ok).toBe(true)
expect(dry_run_result.diff).toContain('+ \'b\': {')
})
it('buildParseSummary 汇总成功失败与耗时', () => {
const summary = buildParseSummary([
{ success: true, formSn: 'a', file: 'a.pdf', config: { product_name: 'A' } },
{ success: false, file: 'b.pdf', reason: 'parse_failed' }
], 1200)
expect(summary.total).toBe(2)
expect(summary.success).toBe(1)
expect(summary.failed).toBe(1)
expect(summary.duration_ms).toBe(1200)
expect(summary.success_list[0].form_sn).toBe('a')
expect(summary.failed_list[0].file).toBe('b.pdf')
})
})
......