init: 数字员工平台初始代码

包含前端(Vue3 + VueFlow 画布)、后端(Go)、文档体系。
- 工作台画布:节点拖放、连线模式、右键菜单、AI 助手
- 后端:连接器 API、专员种子数据
- 导航:左侧导航、工坊、市场、控制台
This commit is contained in:
eaiadmin
2026-08-18 20:19:58 +08:00
commit 4e8817d768
239 changed files with 48631 additions and 0 deletions
+233
View File
@@ -0,0 +1,233 @@
# BE03 — 素材模块设计
> **版本:V1.1 | 技术:分片上传 + LibreOffice + PyMuPDF**
---
## 1. 模块职责
- 文件上传(直传 + 分片上传)
- 素材审批流(待审批 → 通过/驳回)
- 审批通过后异步文档转换管线
- 文件预览
- 转换状态查询
## 2. 素材状态流转
```
员工提交 / 管理员上传
│
▼
pending(待审批) ──┬─ approve → approved(已通过)
│ │
│ └─→ 触发异步转换管线
│ │
│ ├─ 文档 → LibreOffice 转 PDF → PyMuPDF 提取
│ │ 文本 → 切片写入 knowledge_chunk
│ └─ 视频/图片 → 仅标记预览可用
│
└─ reject → rejected(已驳回,前台不可见)
```
## 3. 上传 API
### 直传(文档 ≤ 200MB)
```python
POST /api/media/upload
Content-Type: multipart/form-data
Parameters:
- file: 文件二进制
- bind_type: company | product | course | none
- bind_id: 绑定实体 ID(可选)
Response:
{
"media_id": 1,
"status": "pending",
"filename": "原始名称.pptx"
}
```
### 分片上传(视频 > 100MB)
```python
# 1. 初始化
POST /api/media/upload-init
{
"filename": "training.mp4",
"file_size": 524288000,
"bind_type": "course",
"bind_id": 1
}
Response: { "upload_id": "uuid", "chunk_size": 5242880, "chunk_count": 100 }
# 2. 上传分片(循环调用)
POST /api/media/upload-chunk
Content-Type: multipart/form-data
{
"upload_id": "uuid",
"chunk_index": 0,
"file": <binary>
}
# 3. 完成合并
POST /api/media/upload-complete
{ "upload_id": "uuid" }
Response: { "media_id": 1, "status": "pending" }
```
## 4. 审批 API
```python
# 管理员
GET /api/media/audit-list?status=pending&page=1&size=20
POST /api/media/audit/{mediaId}
{
"action": "approve", # approve | reject
"reject_reason": "..." # 驳回时必填
}
```
## 5. 异步转换管线
```python
# services/media_service.py
import threading
from datetime import datetime
def _async_convert_pipeline(media_id: int):
"""审批通过后的异步转换管线"""
try:
media = db.query(MediaFile).get(media_id)
# 仅文档需要转换(PPT/Word/PDF)
if media.file_ext in ("ppt", "pptx", "doc", "docx"):
# Step 1: LibreOffice 转 PDF
pdf_path = _libreoffice_to_pdf(media.stored_path)
# Step 2: PyMuPDF 提取文本
text = _pymupdf_extract(pdf_path)
# Step 3: 按段落切片入库
chunks = _split_into_chunks(text)
for i, chunk_text in enumerate(chunks):
db.add(KnowledgeChunk(
media_file_id=media.id,
source_type=media.file_ext,
chunk_index=i,
content=chunk_text,
))
elif media.file_ext == "pdf":
# PDF 直接 PyMuPDF 提取
text = _pymupdf_extract(media.stored_path)
chunks = _split_into_chunks(text)
for i, chunk_text in enumerate(chunks):
db.add(KnowledgeChunk(media_file_id=media.id, ...))
# 视频/图片:不提取文本
media.extracted = True
db.commit()
logger.info(f"转换完成: media_id={media_id}")
except Exception as e:
logger.error(f"转换失败: media_id={media_id}, error={e}")
media.extracted = False # 标记失败可重试
def approve_media(media_id: int, auditor_id: int):
"""审批通过 → 启动异步转换"""
media = db.query(MediaFile).get(media_id)
media.status = "approved"
media.audit_by = auditor_id
media.audit_at = datetime.utcnow()
db.commit()
thread = threading.Thread(target=_async_convert_pipeline, args=(media_id,))
thread.start()
```
## 6. LibreOffice 转换接口
```python
# utils/libreoffice.py
import subprocess
import requests
def libreoffice_convert(input_path: str, output_dir: str) -> str:
"""调用 LibreOffice 容器将文档转 PDF"""
# 方式1:本地安装 libreoffice
subprocess.run([
"libreoffice", "--headless", "--convert-to", "pdf",
"--outdir", output_dir, input_path
], check=True)
# 方式2:Docker 容器 HTTP 接口
# response = requests.post(
# f"{settings.libreoffice_url}/convert",
# files={"file": open(input_path, "rb")}
# )
# return response.json()["pdf_path"]
```
## 7. PyMuPDF 文本提取
```python
# utils/pdf_extractor.py
import fitz # PyMuPDF
def extract_text(pdf_path: str) -> str:
"""提取 PDF 全部文本"""
doc = fitz.open(pdf_path)
text = ""
for page in doc:
text += page.get_text()
doc.close()
return text
def split_into_chunks(text: str, max_chars: int = 1000) -> list[str]:
"""按段落 + 最大字符数切片"""
paragraphs = text.split("\n\n")
chunks = []
current = ""
for p in paragraphs:
if len(current) + len(p) > max_chars:
if current:
chunks.append(current.strip())
current = p
else:
current += "\n\n" + p if current else p
if current:
chunks.append(current.strip())
return chunks
```
## 8. 预览 API
```python
GET /api/media/preview/{mediaId}
# 仅 approved 素材可预览
Response:
{
"preview_url": "/media/upload/uuid-filename.pdf",
"file_ext": "pdf",
"can_preview": true
}
GET /api/media/status/{mediaId}
# 查询素材状态(含提取进度)
Response:
{
"status": "approved",
"extracted": true,
"chunk_count": 42
}
```
## 9. 安全约束
- 扩展名白名单:ppt/pptx/pdf/doc/docx/mp4/png/jpg/jpeg
- 文件名重命名为 UUID,杜绝路径穿越
- 上传目录对静态预览只读,禁止直接执行
- 文件大小:文档 ≤ 200MB,视频 ≤ 2GB
- MIME 类型校验 + 扩展名双重校验