init project
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
# Default ignored files
|
||||
.idea
|
||||
.idea/**
|
||||
node_modules
|
||||
@@ -0,0 +1,160 @@
|
||||
# AGENTS.md — AI 编码助手指引
|
||||
|
||||
> 本文档供 Claude Code / Copilot / Cursor 等 AI 编码助手读取,确保生成的代码符合项目约定。
|
||||
|
||||
---
|
||||
|
||||
## 项目概述
|
||||
|
||||
AI 文档模板生成系统(第一阶段 MVP),定位是一个 **Word 模板标注器**。
|
||||
|
||||
核心流程:上传 Word → 解析标题/段落/表格 → 生成 HTML 预览(含 data-block-id)→ 用户点击区域 → 配置提示词 → 保存配置。
|
||||
|
||||
---
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **前端**:Vue 3 + TypeScript + Vite + Element Plus + Pinia + Vue Router 4
|
||||
- **后端**:Python 3.11+ + FastAPI + SQLAlchemy + Alembic + MySQL 8.0
|
||||
- **存储**:MinIO(文件)/ MySQL(结构化数据)
|
||||
- **关键词**:block_id, template, region_type, upsert
|
||||
|
||||
---
|
||||
|
||||
## 代码约定
|
||||
|
||||
### 后端
|
||||
|
||||
- 路由文件在 `app/api/`,服务逻辑在 `app/services/`,Model 在 `app/models/`
|
||||
- 所有接口返回 JSON,统一格式 `{"data": ..., "message": "ok"}`
|
||||
- 错误返回 `{"detail": "错误信息"}`
|
||||
- 使用 Python类型注解
|
||||
- 数据库 session 通过依赖注入 `get_db` 获取
|
||||
- Model 使用 `sqlalchemy.orm.DeclarativeBase`
|
||||
|
||||
### 前端
|
||||
|
||||
- 组件使用 Composition API + `<script setup lang="ts">`
|
||||
- 全局状态走 Pinia,不滥用 props/emit 跨多层传递
|
||||
- API 请求走 `src/api/base.ts` 的 axios 实例
|
||||
- 样式优先使用全局 CSS 变量(`var(--c-primary)`),避免硬编码色值
|
||||
- 组件文件命名:`PascalCase.vue`
|
||||
|
||||
### 数据库
|
||||
|
||||
- 表名:snake_case(如 `template_block`)
|
||||
- 字段名:snake_case(Model 中用 `__tablename__` 映射)
|
||||
- 迁移使用 Alembic auto-generation
|
||||
|
||||
---
|
||||
|
||||
## 核心数据模型
|
||||
|
||||
### template
|
||||
|
||||
```python
|
||||
class Template(Base):
|
||||
__tablename__ = "template"
|
||||
id: int # PK, auto
|
||||
name: str # 模板名称
|
||||
type: str # 报告类/公文类
|
||||
version: str # v1.0.0
|
||||
original_file_path: str # 文件存储路径
|
||||
status: int # 1=启用 0=停用
|
||||
created_by: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
```
|
||||
|
||||
### template_block
|
||||
|
||||
```python
|
||||
class TemplateBlock(Base):
|
||||
__tablename__ = "template_block"
|
||||
id: int # PK
|
||||
template_id: int # FK → template.id
|
||||
block_id: str # "block_001"
|
||||
parent_block_id: str | None
|
||||
block_type: str # "title" | "heading" | "paragraph" | "table"
|
||||
block_name: str | None
|
||||
text_preview: str | None
|
||||
level: int
|
||||
sort_order: int
|
||||
table_rows: int | None
|
||||
table_cols: int | None
|
||||
```
|
||||
|
||||
### block_config
|
||||
|
||||
```python
|
||||
class BlockConfig(Base):
|
||||
__tablename__ = "block_config"
|
||||
id: int # PK
|
||||
template_id: int # FK
|
||||
block_id: str # "block_003"
|
||||
region_name: str | None
|
||||
region_type: str # "ai_generate" | "manual" | "fixed" | "table"
|
||||
data_sources: str | None # JSON 数组字符串
|
||||
prompt: str | None
|
||||
output_format: str # "formal_paragraph" | ...
|
||||
need_review: int # 0/1
|
||||
remark: str | None
|
||||
enabled: int # 0/1
|
||||
# UNIQUE(template_id, block_id)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 关键接口
|
||||
|
||||
### 上传模板
|
||||
```
|
||||
POST /api/templates/upload
|
||||
multipart/form-data: file + name + type
|
||||
→ {"template_id": 1, "name": "...", "status": "uploaded"}
|
||||
```
|
||||
|
||||
### 模板详情(核心接口)
|
||||
```
|
||||
GET /api/templates/{id}
|
||||
→ {
|
||||
template: {...},
|
||||
preview_html: "<div ...>",
|
||||
blocks: [...],
|
||||
tree: [...],
|
||||
configs: {"block_003": {...}}
|
||||
}
|
||||
```
|
||||
|
||||
### 保存区域配置
|
||||
```
|
||||
POST /api/templates/{id}/blocks/{blockId}/config
|
||||
{region_name, region_type, data_sources, prompt, output_format, need_review, remark, enabled}
|
||||
→ {"message": "saved"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第一阶段不做
|
||||
|
||||
- ❌ 完整在线 Word 编辑
|
||||
- ❌ 多人协同
|
||||
- ❌ 权限审批
|
||||
- ❌ 复杂表格智能填充
|
||||
- ❌ 移动端适配
|
||||
- ❌ 提示词历史版本(后续阶段)
|
||||
|
||||
---
|
||||
|
||||
## 项目启动
|
||||
|
||||
```bash
|
||||
# 后端
|
||||
docker compose up -d mysql minio
|
||||
cd backend && pip install -r requirements.txt
|
||||
alembic upgrade head
|
||||
uvicorn app.main:app --reload
|
||||
|
||||
# 前端
|
||||
cd frontend && npm install && npm run dev
|
||||
```
|
||||
@@ -0,0 +1,196 @@
|
||||
# AI 文档模板生成系统
|
||||
|
||||
> 通用型 AI 文档模板配置与生成平台 — 上传 Word 模板 → 自动解析结构 → 配置 AI 生成规则 → 分章节生成内容 → 回填 Word 模板 → 导出 Word/PDF。
|
||||
|
||||
---
|
||||
|
||||
## 一句话定位
|
||||
|
||||
**Word 模板标注器**:上传 Word 后,系统在页面中间展示文档预览,为每个标题/段落/表格生成唯一 block_id。用户点击预览区某个区域后,左侧显示该区域的提示词配置表单,可设置区域类型、数据来源、提示词、输出格式和审核要求。右侧显示文档结构树,与预览区联动。保存配置后,该区域成为 AI 可生成区域。
|
||||
|
||||
---
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 层 | 技术 | 版本要求 |
|
||||
|---|------|---------|
|
||||
| 前端框架 | Vue 3 | ^3.4 |
|
||||
| 构建工具 | Vite | ^5.x |
|
||||
| UI 组件库 | Element Plus | ^2.8 |
|
||||
| 状态管理 | Pinia | ^2.x |
|
||||
| 路由 | Vue Router 4 | ^4.x |
|
||||
| 后端框架 | Python FastAPI | ^0.110 |
|
||||
| Python 版本 | Python 3.11+ | |
|
||||
| 数据库 | MySQL 8.0 | |
|
||||
| 缓存 | Redis 7.x | 可选 |
|
||||
| 对象存储 | MinIO | |
|
||||
| 文档解析 | python-docx | ^1.1 |
|
||||
| 模板回填 | python-docx-template | ^1.0 |
|
||||
| PDF 转换 | LibreOffice 无头模式 | |
|
||||
| 容器化 | Docker + Docker Compose | |
|
||||
|
||||
---
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
ai-doc-template/
|
||||
├── README.md
|
||||
├── SKILL.md # Hermes 技能文件
|
||||
├── AGENTS.md # AI 编码助手指引
|
||||
├── .gitignore
|
||||
├── .env.example
|
||||
├── docker-compose.yml
|
||||
├── backend/
|
||||
│ ├── Dockerfile
|
||||
│ ├── requirements.txt
|
||||
│ └── app/
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py # 应用入口 + FastAPI 实例
|
||||
│ ├── database.py # 数据库连接 + Session
|
||||
│ ├── models/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── template.py # template 表 Model
|
||||
│ │ ├── template_block.py
|
||||
│ │ └── block_config.py
|
||||
│ ├── api/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── templates.py # 模板上传/查询接口
|
||||
│ │ ├── blocks.py # 区域配置接口
|
||||
│ │ └── data_sources.py # 数据源管理接口
|
||||
│ ├── services/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── storage.py # MinIO 文件存储
|
||||
│ │ ├── template_service.py
|
||||
│ │ ├── doc_parser.py # Word 解析引擎
|
||||
│ │ └── html_generator.py # HTML 预览生成
|
||||
│ └── schemas/
|
||||
│ ├── __init__.py
|
||||
│ └── template.py # Pydantic 模型
|
||||
├── frontend/
|
||||
│ ├── Dockerfile
|
||||
│ ├── package.json
|
||||
│ ├── vite.config.ts
|
||||
│ ├── tsconfig.json
|
||||
│ ├── index.html
|
||||
│ └── src/
|
||||
│ ├── main.ts # 入口 + 插件注册
|
||||
│ ├── App.vue
|
||||
│ ├── router/
|
||||
│ │ └── index.ts # 路由配置
|
||||
│ ├── stores/
|
||||
│ │ ├── templateStore.ts
|
||||
│ │ ├── selectionStore.ts
|
||||
│ │ └── uiStore.ts
|
||||
│ ├── api/
|
||||
│ │ └── base.ts # Axios 实例
|
||||
│ ├── styles/
|
||||
│ │ └── variables.css # 全局 CSS 变量
|
||||
│ ├── views/
|
||||
│ │ ├── TemplateCenter.vue
|
||||
│ │ └── TemplateEdit.vue
|
||||
│ └── components/
|
||||
│ └── TemplateEdit/
|
||||
│ ├── LeftMenu.vue
|
||||
│ ├── TopToolbar.vue
|
||||
│ ├── WordPreview.vue
|
||||
│ ├── ConfigPanel.vue
|
||||
│ └── StructureTree.vue
|
||||
└── docs/
|
||||
└── architecture.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 前置依赖
|
||||
|
||||
- Docker & Docker Compose
|
||||
- Python 3.11+
|
||||
- Node.js 18+
|
||||
- LibreOffice(可选,PDF 导出需要)
|
||||
|
||||
### 启动后端
|
||||
|
||||
```bash
|
||||
# 1. 启动 MySQL + MinIO
|
||||
docker compose up -d mysql minio
|
||||
|
||||
# 2. 安装后端依赖
|
||||
cd backend
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 3. 复制环境变量
|
||||
cp ../.env.example .env
|
||||
|
||||
# 4. 初始化数据库
|
||||
alembic upgrade head
|
||||
|
||||
# 5. 启动后端
|
||||
uvicorn app.main:app --reload --port 8000
|
||||
```
|
||||
|
||||
### 启动前端
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
pnpm install
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
### 访问
|
||||
|
||||
- 前端页面:http://localhost:5173
|
||||
- API 文档:http://localhost:8000/docs
|
||||
- MinIO Console:http://localhost:9001
|
||||
|
||||
---
|
||||
|
||||
## 开发阶段
|
||||
|
||||
| 阶段 | 目标 | 预估 |
|
||||
|------|------|:----:|
|
||||
| 第一阶段:模板标注 MVP | 上传 Word → 解析 → 预览 → 点击配置 → 保存 | 3-4 周 |
|
||||
| 第二阶段:生成测试 | 上传资料 → AI 逐区域生成 → 预览结果 | 1-2 周 |
|
||||
| 第三阶段:回填导出 | block 回填 → Word/PDF 导出 | 1-2 周 |
|
||||
| 第四阶段:高级功能 | 提示词历史、数据源管理、审核流程等 | 2-3 周 |
|
||||
|
||||
---
|
||||
|
||||
## 核心流程
|
||||
|
||||
### 创建模板
|
||||
|
||||
```
|
||||
上传 Word → 保存原始文件 → 解析标题/段落/表格 → 生成 block_id
|
||||
→ 生成 HTML 预览 → 用户点击段落 → 配置提示词 → 保存配置
|
||||
```
|
||||
|
||||
### 生成文档
|
||||
|
||||
```
|
||||
选择模板 → 上传资料 → AI 提取结构化数据 → 按区域逐块生成
|
||||
→ 人工审核修改 → 回填 Word 模板 → 导出 Word/PDF
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据库核心表
|
||||
|
||||
| 表名 | 说明 |
|
||||
|------|------|
|
||||
| template | 模板基本信息、文件路径、版本 |
|
||||
| template_block | 解析出的文档区域(标题/段落/表格),含 block_id 和层级 |
|
||||
| block_config | 用户为每个区域配置的提示词、类型、数据来源等 |
|
||||
| data_source | 数据源配置 |
|
||||
|
||||
---
|
||||
|
||||
## 设计原则
|
||||
|
||||
1. **Word 保持版式,系统管理结构,AI 生成内容**
|
||||
2. **HTML 只负责预览和交互,原始 docx 负责最终导出**
|
||||
3. **block_id 是关键索引**:所有功能(点击、配置、生成、回填)都围绕 block_id
|
||||
4. **先提取、再生成、再审核、再回填**,不把资料一次性扔给 AI
|
||||
5. **第一版不做在线 Word 编辑**,聚焦标注和配置
|
||||
@@ -0,0 +1,185 @@
|
||||
---
|
||||
name: ai-doc-template-system
|
||||
description: "通用型 AI 文档模板生成系统 — Word 模板标注与 AI 文档生成平台。触发:需要建设/开发/维护 AI 文档模板生成平台时使用。"
|
||||
version: 1.0.0
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [文档生成, AI, Word模板, 系统建设, FastAPI, Vue3]
|
||||
related_skills: [requirements-analysis, task-splitting-methodology, prototype-design]
|
||||
---
|
||||
|
||||
# AI 文档模板生成系统
|
||||
|
||||
> 一个面向多类文档的"模板管理 + 提示词配置 + AI 内容生成 + Word 样式回填"平台。
|
||||
|
||||
---
|
||||
|
||||
## 系统定位
|
||||
|
||||
本系统**不是一个**纯 Word 在线编辑器,而是一个 Word 模板标注器:
|
||||
|
||||
1. 用户上传 Word → 系统自动解析标题/段落/表格 → 生成预览 HTML
|
||||
2. 用户点击文档中的某个区域 → 左侧显示配置表单
|
||||
3. 用户配置区域名称、区域类型、数据来源、提示词、输出格式、审核要求
|
||||
4. 保存配置后,该区域成为 AI 可生成区域
|
||||
5. 后续生成文档时,AI 根据各区域提示词生成内容并回填到原 Word
|
||||
|
||||
---
|
||||
|
||||
## 技术架构
|
||||
|
||||
| 层 | 选型 | 说明 |
|
||||
|---|------|------|
|
||||
| 前端 | Vue3 + Vite + Element Plus + Pinia | 组件化单页应用 |
|
||||
| 后端 | Python FastAPI | RESTful API |
|
||||
| 数据库 | MySQL 8.0 + SQLAlchemy + Alembic | 关系型数据 |
|
||||
| 文件存储 | MinIO / 本地文件系统 | 模板和资料文件 |
|
||||
| 文档解析 | python-docx | 读取 Word 结构 |
|
||||
| 模板回填 | python-docx-template | 内容回填 Word |
|
||||
| AI 集成 | 大模型 API + LangChain | 内容生成 |
|
||||
| 容器化 | Docker + Docker Compose | 一键部署 |
|
||||
|
||||
---
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
ai-doc-template/
|
||||
├── backend/
|
||||
│ ├── app/
|
||||
│ │ ├── main.py # FastAPI 入口 + CORS
|
||||
│ │ ├── database.py # MySQL + SQLAlchemy
|
||||
│ │ ├── models/ # SQLAlchemy Model 定义
|
||||
│ │ ├── api/ # RESTful 路由
|
||||
│ │ ├── services/ # 业务逻辑层
|
||||
│ │ └── schemas/ # Pydantic 请求/响应
|
||||
│ ├── requirements.txt
|
||||
│ └── Dockerfile
|
||||
├── frontend/
|
||||
│ ├── src/
|
||||
│ │ ├── views/ # 页面级组件
|
||||
│ │ ├── components/ # 业务组件
|
||||
│ │ ├── stores/ # Pinia 状态管理
|
||||
│ │ ├── api/ # Axios API 客户端
|
||||
│ │ └── styles/ # 全局样式变量
|
||||
│ └── package.json
|
||||
├── docker-compose.yml
|
||||
├── .env.example
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据库表
|
||||
|
||||
详见 `后端开发 > 数据模型`,核心三张表:
|
||||
|
||||
### template — 模板
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| id, name, type, version | 基本信息 |
|
||||
| original_file_path | 原始 docx 存储路径 |
|
||||
| status | 1=启用, 0=停用 |
|
||||
|
||||
### template_block — 文档区域
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| template_id | 所属模板 |
|
||||
| block_id | 唯一标识(block_001) |
|
||||
| block_type | title/heading/paragraph/table |
|
||||
| level | 层级(0/1/2) |
|
||||
| parent_block_id | 父级 |
|
||||
| table_rows, table_cols | 表格行列 |
|
||||
|
||||
### block_config — 区域配置
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| template_id + block_id | 联合唯一键 |
|
||||
| region_type | ai_generate/manual/fixed/table |
|
||||
| prompt | 提示词 |
|
||||
| data_sources | 数据来源列表(JSON) |
|
||||
| need_review | 是否需要审核 |
|
||||
|
||||
---
|
||||
|
||||
## API 接口清单
|
||||
|
||||
### 模板
|
||||
- `POST /api/templates/upload` — 上传 docx
|
||||
- `GET /api/templates/{id}` — 模板详情(含预览 HTML + blocks + 配置)
|
||||
|
||||
### 区域配置
|
||||
- `POST /api/templates/{id}/blocks/{blockId}/config` — 保存/更新区域配置
|
||||
- `GET /api/data-sources` — 数据源列表
|
||||
- `POST /api/data-sources` — 新增数据源
|
||||
|
||||
### 生成(后续阶段)
|
||||
- `POST /api/templates/{id}/generate-test` — 生成测试
|
||||
- `GET /api/tasks/{id}/download-docx` — 下载 Word
|
||||
- `GET /api/tasks/{id}/download-pdf` — 下载 PDF
|
||||
|
||||
---
|
||||
|
||||
## 开发任务(第一阶段 MVP)
|
||||
|
||||
共 **73 个任务**,按模块分组:
|
||||
|
||||
| 模块 | 任务数 | 核心产出 |
|
||||
|------|:------:|---------|
|
||||
| 环境搭建 | 11 | FastAPI + Vue3 项目骨架 + Docker |
|
||||
| 数据库建表 | 3 | template, template_block, block_config |
|
||||
| 模板上传 | 4 | 文件接收校验 → 存储 → 入库 |
|
||||
| Word 解析 | 7 | 标题/段落/表格识别 → block_id → 结构树 |
|
||||
| HTML 预览 | 4 | heading/paragraph/table 转 HTML |
|
||||
| 查询+配置接口 | 4 | 详情查询、配置保存、数据源 |
|
||||
| 四栏布局 | 6 | 菜单/工具栏/预览/配置/树 shell |
|
||||
| 左侧菜单 | 3 | 数据模型、展开收起、高亮 |
|
||||
| 工具栏 | 4 | 面包屑、版本号、保存状态、操作按钮 |
|
||||
| Word 预览区 | 7 | v-html渲染、点击、高亮、颜色、标签、缩放 |
|
||||
| 结构树 | 4 | 递归组件、展开折叠、状态圆点、滚动联动 |
|
||||
| 配置面板 | 9 | 名称/类型/数据源/提示词/审核/备注/保存 |
|
||||
| Pinia Store | 3 | templateStore, selectionStore, uiStore |
|
||||
| 三区联动 | 4 | watch 驱动的配置加载、树同步、loading |
|
||||
|
||||
详见 `docs/tasks-v3.md`
|
||||
|
||||
---
|
||||
|
||||
## 开发阶段规划
|
||||
|
||||
### 第一阶段:模板标注 MVP(3-4 周)
|
||||
|
||||
核心流程:上传 Word → 解析 → 预览 → 点击配置 → 保存
|
||||
|
||||
交付物:
|
||||
- 可用的模板编辑页面(四栏布局)
|
||||
- Word 解析服务(标题/段落/表格)
|
||||
- 区域配置面板(类型/提示词/数据来源)
|
||||
- 结构树联动
|
||||
|
||||
### 第二阶段:生成测试(1-2 周)
|
||||
|
||||
- 上传资料 → AI 逐区域生成 → 预览结果 → 人工编辑
|
||||
|
||||
### 第三阶段:回填导出(1-2 周)
|
||||
|
||||
- block 回填原始 Word → Word/PDF 导出
|
||||
|
||||
### 第四阶段:高级功能(2-3 周)
|
||||
|
||||
- 提示词历史、数据源管理、审核流程
|
||||
|
||||
---
|
||||
|
||||
## 关键设计决策
|
||||
|
||||
| 决策 | 原因 |
|
||||
|------|------|
|
||||
| 不做 Word→HTML→Word 转换 | 格式丢失严重,Word 做底座 |
|
||||
| HTML 只预览,docx 负责导出 | 分离预览和最终输出 |
|
||||
| block_id 作为统一索引 | 解析、配置、生成、回填共享同一定位 |
|
||||
| 分阶段迭代 | MVP 先跑通标注流程 |
|
||||
| python-docx 而非 docxtpl | 第一版需要精确位置控制 |
|
||||
@@ -0,0 +1,26 @@
|
||||
import os
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker, DeclarativeBase
|
||||
|
||||
DB_HOST = os.getenv("DB_HOST", "localhost")
|
||||
DB_PORT = os.getenv("DB_PORT", "3306")
|
||||
DB_USER = os.getenv("DB_USER", "root")
|
||||
DB_PASS = os.getenv("DB_PASS", "root123")
|
||||
DB_NAME = os.getenv("DB_NAME", "ai_doc_template")
|
||||
|
||||
DATABASE_URL = f"mysql+pymysql://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}?charset=utf8mb4"
|
||||
|
||||
engine = create_engine(DATABASE_URL, echo=False)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
@@ -0,0 +1,17 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
app = FastAPI(title="AI 文档模板生成系统", version="0.1.0")
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:5173", "http://localhost:3000"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return {"status": "ok", "service": "ai-doc-template"}
|
||||
@@ -0,0 +1,24 @@
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, UniqueConstraint
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class BlockConfig(Base):
|
||||
__tablename__ = "block_config"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("template_id", "block_id", name="uq_template_block"),
|
||||
)
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
template_id = Column(Integer, ForeignKey("template.id", ondelete="CASCADE"), nullable=False)
|
||||
block_id = Column(String(50), nullable=False)
|
||||
region_name = Column(String(200), nullable=True, comment="区域名称")
|
||||
region_type = Column(String(30), default="ai_generate", comment="ai_generate/manual/fixed/table")
|
||||
data_sources = Column(Text, nullable=True, comment="数据来源 JSON 数组")
|
||||
prompt = Column(Text, nullable=True, comment="提示词")
|
||||
output_format = Column(String(30), default="formal_paragraph", comment="输出格式")
|
||||
need_review = Column(Integer, default=1, comment="1=需要审核")
|
||||
remark = Column(Text, nullable=True, comment="备注")
|
||||
enabled = Column(Integer, default=1, comment="1=启用")
|
||||
created_at = Column(DateTime, default=datetime.now)
|
||||
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
@@ -0,0 +1,17 @@
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class Template(Base):
|
||||
__tablename__ = "template"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String(200), nullable=False, comment="模板名称")
|
||||
type = Column(String(50), default="report", comment="报告类/公文类/总结类/合同类")
|
||||
version = Column(String(20), default="v1.0.0", comment="版本号")
|
||||
original_file_path = Column(String(500), nullable=False, comment="原始 Word 文件路径")
|
||||
status = Column(Integer, default=1, comment="1=启用 0=停用")
|
||||
created_by = Column(String(50), comment="创建人")
|
||||
created_at = Column(DateTime, default=datetime.now)
|
||||
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
@@ -0,0 +1,20 @@
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class TemplateBlock(Base):
|
||||
__tablename__ = "template_block"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
template_id = Column(Integer, ForeignKey("template.id", ondelete="CASCADE"), nullable=False, comment="所属模板")
|
||||
block_id = Column(String(50), nullable=False, comment="唯一标识 block_001")
|
||||
parent_block_id = Column(String(50), nullable=True, comment="父级 block_id")
|
||||
block_type = Column(String(20), nullable=False, comment="title/heading/paragraph/table")
|
||||
block_name = Column(String(200), nullable=True, comment="区域名称")
|
||||
text_preview = Column(String(500), nullable=True, comment="文本预览")
|
||||
level = Column(Integer, default=0, comment="0=文档标题 1=一级 2=二级")
|
||||
sort_order = Column(Integer, default=0, comment="排序号")
|
||||
table_rows = Column(Integer, nullable=True, comment="表格行数")
|
||||
table_cols = Column(Integer, nullable=True, comment="表格列数")
|
||||
created_at = Column(DateTime, default=datetime.now)
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Word 文档解析引擎"""
|
||||
from docx import Document
|
||||
from docx.paragraph import Paragraph
|
||||
from docx.table import Table as DocxTable
|
||||
from typing import List, Dict, Any
|
||||
|
||||
|
||||
def parse_document(file_path: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
解析 Word 文档,返回 block 列表(不含表格数据,仅标记位置)。
|
||||
每个 block 包含:type, text, level, style
|
||||
"""
|
||||
doc = Document(file_path)
|
||||
blocks = []
|
||||
|
||||
for para in doc.paragraphs:
|
||||
block = classify_paragraph(para)
|
||||
if block:
|
||||
blocks.append(block)
|
||||
|
||||
return blocks
|
||||
|
||||
|
||||
def classify_paragraph(para: Paragraph) -> Dict[str, Any] | None:
|
||||
"""识别段落类型"""
|
||||
text = para.text.strip()
|
||||
style_name = para.style.name if para.style else ""
|
||||
|
||||
if not text and "Heading" not in style_name:
|
||||
return None # 跳过空段落
|
||||
|
||||
if "Heading" in style_name:
|
||||
level = int(style_name.replace("Heading", "").strip()) if style_name.replace("Heading", "").strip().isdigit() else 1
|
||||
return {"type": "heading", "text": text, "level": level, "style": style_name}
|
||||
else:
|
||||
return {"type": "paragraph", "text": text, "level": 0, "style": style_name}
|
||||
|
||||
|
||||
def parse_tables(doc: Document) -> List[Dict[str, Any]]:
|
||||
"""解析 Word 表格"""
|
||||
tables = []
|
||||
for i, table in enumerate(doc.tables):
|
||||
headers = [cell.text.strip() for cell in table.rows[0].cells] if table.rows else []
|
||||
tables.append({
|
||||
"type": "table",
|
||||
"text": " | ".join(headers),
|
||||
"level": 0,
|
||||
"rows": len(table.rows),
|
||||
"cols": len(table.columns),
|
||||
"headers": headers,
|
||||
"table_index": i,
|
||||
})
|
||||
return tables
|
||||
|
||||
|
||||
def generate_block_id(index: int) -> str:
|
||||
"""生成 block_id,格式:block_001"""
|
||||
return f"block_{index:03d}"
|
||||
|
||||
|
||||
def build_tree(blocks: List[Dict]) -> Dict:
|
||||
"""构建层级树结构"""
|
||||
tree = []
|
||||
stack = [] # 存父级节点
|
||||
|
||||
for block in blocks:
|
||||
node = {"block_id": block["block_id"], "text": block["text"][:50], "children": []}
|
||||
level = block.get("level", 0)
|
||||
|
||||
# 回退栈
|
||||
while stack and stack[-1]["level"] >= level:
|
||||
stack.pop()
|
||||
|
||||
if stack:
|
||||
parent = stack[-1]["node"]
|
||||
parent["children"].append(node)
|
||||
block["parent_block_id"] = parent["block_id"]
|
||||
else:
|
||||
tree.append(node)
|
||||
block["parent_block_id"] = None
|
||||
|
||||
# 非叶子节点入栈
|
||||
if level < 2 or block["type"] == "heading":
|
||||
stack.append({"level": level, "node": node})
|
||||
|
||||
return {"blocks": tree}
|
||||
@@ -0,0 +1,27 @@
|
||||
"""HTML 预览生成服务"""
|
||||
from typing import List, Dict, Any
|
||||
|
||||
|
||||
def generate_preview_html(blocks: List[Dict[str, Any]]) -> str:
|
||||
"""
|
||||
将 block 列表转为带 data-block-id 的 HTML 预览字符串。
|
||||
每个元素都会带上 data-block-id 属性供前端点击交互。
|
||||
"""
|
||||
html_parts = ['<div class="doc-preview-content">']
|
||||
|
||||
for block in blocks:
|
||||
block_id = block.get("block_id", "")
|
||||
block_type = block.get("type", "")
|
||||
text = block.get("text", "")
|
||||
|
||||
if block_type == "heading":
|
||||
html = f'<h2 data-block-id="{block_id}" style="font-size:16px;font-weight:600;margin:20px 0 10px;padding-bottom:6px;border-bottom:2px solid #1a1d24">{text}</h2>'
|
||||
elif block_type == "table":
|
||||
html = f'<div data-block-id="{block_id}" style="margin:12px 0;padding:8px;background:#f7f8fa;border:1px dashed #ccc;border-radius:4px;color:#5b626e">📊 {text[:80]}</div>'
|
||||
else:
|
||||
html = f'<p data-block-id="{block_id}" style="text-indent:2em;margin:8px 0;line-height:1.8;text-align:justify">{text}</p>'
|
||||
|
||||
html_parts.append(html)
|
||||
|
||||
html_parts.append('</div>')
|
||||
return "\n".join(html_parts)
|
||||
@@ -0,0 +1,28 @@
|
||||
import os
|
||||
from io import BytesIO
|
||||
|
||||
# 先实现本地文件存储,MinIO 作为选项
|
||||
STORAGE_BASE = os.getenv("LOCAL_STORAGE_PATH", "./data/files")
|
||||
|
||||
|
||||
def save_file(content: bytes, file_path: str) -> str:
|
||||
"""保存文件到本地存储,返回完整路径"""
|
||||
full_path = os.path.join(STORAGE_BASE, file_path)
|
||||
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
||||
with open(full_path, "wb") as f:
|
||||
f.write(content)
|
||||
return full_path
|
||||
|
||||
|
||||
def read_file(file_path: str) -> bytes:
|
||||
"""读取文件内容"""
|
||||
full_path = os.path.join(STORAGE_BASE, file_path)
|
||||
with open(full_path, "rb") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def delete_file(file_path: str):
|
||||
"""删除文件"""
|
||||
full_path = os.path.join(STORAGE_BASE, file_path)
|
||||
if os.path.exists(full_path):
|
||||
os.remove(full_path)
|
||||
@@ -0,0 +1,30 @@
|
||||
"""模板相关业务逻辑"""
|
||||
from fastapi import UploadFile
|
||||
from sqlalchemy.orm import Session
|
||||
from ..models.template import Template
|
||||
|
||||
|
||||
ALLOWED_EXTENSIONS = {".docx"}
|
||||
MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB
|
||||
|
||||
|
||||
def validate_file(file: UploadFile):
|
||||
"""校验文件格式和大小"""
|
||||
ext = "." + file.filename.rsplit(".", 1)[-1].lower() if "." in file.filename else ""
|
||||
if ext not in ALLOWED_EXTENSIONS:
|
||||
raise ValueError(f"不支持的文件格式: {ext},仅支持 .docx")
|
||||
# 读取文件头校验大小
|
||||
content = file.file.read()
|
||||
file.file.seek(0)
|
||||
if len(content) > MAX_FILE_SIZE:
|
||||
raise ValueError(f"文件大小超过限制 (50MB)")
|
||||
return content
|
||||
|
||||
|
||||
def create_template_record(db: Session, name: str, file_path: str, type: str = "report") -> Template:
|
||||
"""创建模板记录"""
|
||||
tmpl = Template(name=name, type=type, original_file_path=file_path)
|
||||
db.add(tmpl)
|
||||
db.commit()
|
||||
db.refresh(tmpl)
|
||||
return tmpl
|
||||
@@ -0,0 +1,11 @@
|
||||
fastapi==0.110.0
|
||||
uvicorn[standard]==0.27.0
|
||||
sqlalchemy==2.0.25
|
||||
pymysql==1.1.0
|
||||
cryptography==42.0.0
|
||||
alembic==1.13.0
|
||||
python-multipart==0.0.6
|
||||
python-docx==1.1.0
|
||||
minio==7.2.0
|
||||
python-dotenv==1.0.0
|
||||
pydantic==2.5.0
|
||||
@@ -0,0 +1,33 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
container_name: ai-doc-mysql
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root123
|
||||
MYSQL_DATABASE: ai_doc_template
|
||||
ports:
|
||||
- "3306:3306"
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
|
||||
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
container_name: ai-doc-minio
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MINIO_ROOT_USER: minioadmin
|
||||
MINIO_ROOT_PASSWORD: minioa...n
|
||||
ports:
|
||||
- "9000:9000" # API
|
||||
- "9001:9001" # Console
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
command: server /data --console-address ":9001"
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
minio_data:
|
||||
@@ -0,0 +1,138 @@
|
||||
# 系统架构设计
|
||||
|
||||
> AI 文档模板生成系统 — 架构决策与模块设计
|
||||
|
||||
---
|
||||
|
||||
## 整体架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 前端 (Vue3) │
|
||||
│ TemplateEdit.vue │
|
||||
│ ├─ LeftMenu.vue 左侧菜单 │
|
||||
│ ├─ TopToolbar.vue 顶部工具栏 │
|
||||
│ ├─ WordPreview.vue 中间文档预览 │
|
||||
│ ├─ ConfigPanel.vue 左侧配置面板 │
|
||||
│ └─ StructureTree.vue 右侧结构树 │
|
||||
├────────────────── HTTP/REST ────────────────────────┤
|
||||
│ 后端 (FastAPI) │
|
||||
│ ├─ api/ RESTful 接口 │
|
||||
│ ├─ services/ 业务逻辑层 │
|
||||
│ │ ├─ doc_parser.py Word 解析引擎 │
|
||||
│ │ ├─ html_generator.py HTML 预览生成 │
|
||||
│ │ ├─ template_service.py 模板业务 │
|
||||
│ │ └─ storage.py 文件存储 │
|
||||
│ └─ models/ SQLAlchemy 数据模型 │
|
||||
├─────────────────── 数据层 ───────────────────────────┤
|
||||
│ MySQL 8.0 (结构化) MinIO (文件) │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 核心流程 数据流
|
||||
|
||||
### 上传模板
|
||||
|
||||
```
|
||||
用户 → 上传 .docx
|
||||
→ POST /api/templates/upload
|
||||
→ validate_file() 校验格式和大小
|
||||
→ save_file() 保存到 MinIO/本地
|
||||
→ create_template_record() 写入 template 表
|
||||
→ parse_document() 解析标题/段落
|
||||
→ parse_tables() 解析表格
|
||||
→ generate_block_id() 生成唯一 ID
|
||||
→ save_blocks() 写入 template_block 表
|
||||
← 返回 template_id
|
||||
```
|
||||
|
||||
### 进入编辑页
|
||||
|
||||
```
|
||||
GET /api/templates/{id}
|
||||
→ 查询 template 表 → 基本信息
|
||||
→ 查询 template_block 表 → blocks + tree
|
||||
→ 调用 html_generator → preview_html
|
||||
→ 查询 block_config 表 → 已有配置 keyed by block_id
|
||||
← 返回完整数据
|
||||
前端渲染四栏布局
|
||||
```
|
||||
|
||||
### 保存配置
|
||||
|
||||
```
|
||||
用户配置区域 → 点击保存
|
||||
→ POST /api/templates/{id}/blocks/{blockId}/config
|
||||
→ upsert block_config 表
|
||||
→ 更新 configs 缓存
|
||||
← 返回成功
|
||||
前端更新保存状态 + 树节点颜色
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 关键设计
|
||||
|
||||
### block_id 是统一索引
|
||||
|
||||
```
|
||||
Word 解析 → block_id → 数据库存储
|
||||
点击交互 → block_id → 定位 + 高亮
|
||||
配置保存 → block_id → 存储提示词
|
||||
AI 生成 → block_id → 绑定数据来源
|
||||
Word 回填 → block_id → 定位原始位置
|
||||
```
|
||||
|
||||
所有模块通过 block_id 串联,不依赖 Word 内 XML 路径。
|
||||
|
||||
### HTML 预览 vs Word 回填
|
||||
|
||||
| 维度 | HTML 预览 | Word 回填 |
|
||||
|------|----------|----------|
|
||||
| 用途 | 前端展示+交互 | 最终导出 |
|
||||
| 数据来源 | 解析结果 | 原始 docx |
|
||||
| 样式要求 | 接近即可 | 尽量保留 |
|
||||
| 操作 | 点击选中+配置 | 替换内容 |
|
||||
| 技术 | python-docx → HTML | python-docx 直接操作 |
|
||||
|
||||
### 分阶段策略
|
||||
|
||||
第一阶段只做预览+标注+配置。
|
||||
HTML 预览只需"看到内容+点击",不需要"和 Word 一模一样"。
|
||||
Word 回填保留给第三阶段,那时再精细化处理样式映射。
|
||||
|
||||
---
|
||||
|
||||
## 数据流时序
|
||||
|
||||
### 点击区域 → 配置联动
|
||||
|
||||
```
|
||||
用户点击预览区段落
|
||||
→ WordPreview 捕获 click 事件
|
||||
→ event.target.closest('[data-block-id]')
|
||||
→ selectionStore.selectBlock(blockId)
|
||||
→ watch 触发:
|
||||
├─ ConfigPanel: 加载该 block 的 config (已有/默认)
|
||||
├─ StructureTree: 高亮对应节点
|
||||
└─ WordPreview: 添加高亮样式
|
||||
```
|
||||
|
||||
### 保存配置
|
||||
|
||||
```
|
||||
ConfigPanel 点击保存
|
||||
→ saving=true
|
||||
→ POST /api/templates/{id}/blocks/{blockId}/config
|
||||
→ 成功:
|
||||
├─ saving=false
|
||||
├─ uiStore.saveStatus = 'saved'
|
||||
├─ templateStore.updateConfig(blockId, config)
|
||||
├─ WordPreview 更新区域颜色
|
||||
└─ StructureTree 更新状态圆点
|
||||
→ 失败:
|
||||
├─ saving=false
|
||||
└─ ElMessage.error('保存失败')
|
||||
```
|
||||
Reference in New Issue
Block a user