init project

This commit is contained in:
zwt13703
2026-07-01 19:50:29 +08:00
commit 685d69ebdc
21 changed files with 1002 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
# Default ignored files
.idea
.idea/**
node_modules
+160
View File
@@ -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_caseModel 中用 `__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
```
+196
View File
@@ -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 Consolehttp://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 编辑**,聚焦标注和配置
+185
View File
@@ -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 | 第一版需要精确位置控制 |
View File
View File
+26
View File
@@ -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()
+17
View File
@@ -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"}
View File
+24
View File
@@ -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)
+17
View File
@@ -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)
+20
View File
@@ -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)
View File
View File
+86
View File
@@ -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}
+27
View File
@@ -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)
+28
View File
@@ -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)
+30
View File
@@ -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
+11
View File
@@ -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
+33
View File
@@ -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:
+138
View File
@@ -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('保存失败')
```