chore: 工作台产品化进行中的改动
把工作区里其余在制品一并入库,主要是工作台产品化的推进:
后端:新增 capability_definition / project / my_app_center / office_skill
接口与 action_definition / skill_definition / project / user_app_center
模型,config 加路由健康上报。
前端:新增 frontend/src/skills(Office 技能与 workbuddy 复刻)、
项目管理、应用中心、能力目录页,以及配套 api / store / config;
聊天侧新增 SpecialistChip / SpecialistPanel / SkillStrip / AppChatRail
等组件。
清理:移除旧 views/tools 下的单页工具(已并入工作台)、_frozen 冻结组件、
cmd/inspect_oa_debug 调试入口,以及两份调试笔记。
其它:文档与启动脚本同步。
(这批改动与上一提交的 SY23 工作并行进行,此前已在同一工作区内交织。)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
# AR01 — 后端架构设计
|
||||
|
||||
> **版本:V1.1 | 框架:FastAPI + SQLAlchemy + MySQL 8.0**
|
||||
> **参考:pj006-zhilianyuan2 的 BE01_backend_arch + main.py 装配模式**
|
||||
> **版本:V2.0 | 框架:Go + Gin + GORM + SQLite(modernc)**
|
||||
> **参考:eai_agentplatform/backend-go/internal/api/ + internal/model/ + internal/store/**
|
||||
>
|
||||
> **⚠️ 本文档为 V1.1 设计期历史快照,不再反映当前实现。** 后端已重写为 **Go + Gin + GORM + MySQL 8.0 + FAISS**,以 `docs/changelog.md`(V1.2)、`docs/db_schema.md`、`docs/deploy.md` 为准;下文 FastAPI/Python 结构与 `backend/` 路径仅作设计参考。
|
||||
> **当前实现**:Go 单二进制 + systemd + Clonezilla 整盘克隆,`backend-go/internal/` 为标准源码布局。
|
||||
|
||||
---
|
||||
|
||||
@@ -11,163 +11,179 @@
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ API 路由层 (routes) │
|
||||
│ API 路由层 (api/) │
|
||||
│ auth / company_train / product / course │
|
||||
│ exam / media / ai_chat / system │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ Pydantic 模型层 (schemas) │
|
||||
│ 请求/响应模型,统一响应格式 Envelope │
|
||||
│ Gin Context + 请求/响应结构体 │
|
||||
│ web.OK / web.Fail 统一响应 │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ 服务层 (services) │
|
||||
│ exam_service / media_service / ai_service │
|
||||
│ 业务逻辑 (api/ 内函数) │
|
||||
│ exam.go / media.go / ai_chat.go │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ SQLAlchemy ORM 模型层 (models) │
|
||||
│ GORM 模型层 (model/) │
|
||||
│ User / Product / Course / MediaFile / ... │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ 核心层 (core) │
|
||||
│ config / security / deps │
|
||||
│ 数据库层 (store/) │
|
||||
│ db.go: SQLite Init + AutoMigrate │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ MySQL 8.0 + data/media │
|
||||
│ SQLite + data/media │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 2. 目录结构
|
||||
|
||||
```
|
||||
backend/
|
||||
├── app/
|
||||
│ ├── main.py # FastAPI 应用装配 + CORS + 异常处理
|
||||
│ ├── api/ # 路由层
|
||||
│ │ ├── auth.py # /api/auth/* — 登录/注册/me
|
||||
│ │ ├── company_train.py # /api/company-train/*
|
||||
│ │ ├── product.py # /api/products/*
|
||||
│ │ ├── sales_train.py # /api/courses/*
|
||||
│ │ ├── exam.py # /api/exam/* — 题库/组卷/考试/记录
|
||||
│ │ ├── media.py # /api/media/* — 上传/预览/审批
|
||||
│ │ ├── ai_chat.py # /api/ai-chat/* — PathCoach SSE
|
||||
│ │ └── system.py # /api/system/* — 用户/成绩/配置
|
||||
│ ├── models/ # SQLAlchemy ORM 模型
|
||||
│ │ ├── user.py
|
||||
│ │ ├── product.py
|
||||
│ │ ├── course.py
|
||||
│ │ ├── media_file.py
|
||||
│ │ ├── knowledge_chunk.py
|
||||
│ │ ├── question.py
|
||||
│ │ ├── exam_paper.py
|
||||
│ │ └── exam_record.py
|
||||
│ ├── schemas/ # Pydantic 请求/响应模型
|
||||
│ ├── services/ # 业务逻辑层
|
||||
│ │ ├── media_service.py # 上传/转换/提取
|
||||
│ │ ├── ai_service.py # LLM 调用 + 知识检索
|
||||
│ │ └── exam_service.py # 题库/组卷/判分/记录
|
||||
│ ├── core/ # 核心基础设施
|
||||
│ │ ├── config.py # .env + 系统参数读取
|
||||
│ │ ├── security.py # JWT 签发/校验 + bcrypt
|
||||
│ │ └── deps.py # FastAPI Depends(get_db / get_current_user)
|
||||
│ └── utils/ # 工具函数
|
||||
├── data/media/ # 文件存储(git忽略)
|
||||
│ ├── upload/
|
||||
│ └── _preview_cache/
|
||||
├── requirements.txt
|
||||
└── .env
|
||||
backend-go/
|
||||
├── cmd/
|
||||
│ └── server.go # 入口:gin.Engine + systemd 裸进程
|
||||
├── internal/
|
||||
│ ├── api/ # 路由层(Gin handlers)
|
||||
│ │ ├── auth.go # /api/auth/* — 登录/注册/me
|
||||
│ │ ├── company_train.go # /api/company-train/*
|
||||
│ │ ├── product.go # /api/products/*
|
||||
│ │ ├── exam.go # /api/exam/* — 题库/组卷/考试/记录
|
||||
│ │ ├── media.go # /api/media/* — 上传/预览/审批
|
||||
│ │ ├── ai_chat.go # /api/ai-chat/* — PathCoach SSE
|
||||
│ │ ├── system.go # /api/system/* — 用户/成绩/配置
|
||||
│ │ ├── router.go # RegisterRoutes 总路由
|
||||
│ │ └── (其余为功能模块)
|
||||
│ ├── model/ # GORM 模型
|
||||
│ │ ├── user.go
|
||||
│ │ ├── product.go
|
||||
│ │ ├── course.go
|
||||
│ │ ├── media_file.go
|
||||
│ │ ├── knowledge_chunk.go
|
||||
│ │ ├── question.go
|
||||
│ │ ├── exam_paper.go
|
||||
│ │ └── exam_record.go
|
||||
│ ├── store/ # 数据库层
|
||||
│ │ └── db.go # SQLite Init + AutoMigrate
|
||||
│ ├── ai/ # AI 客户端与检索
|
||||
│ │ ├── llm.go # OpenAI 兼容 LLM 调用
|
||||
│ │ └── retrieve.go # 知识检索
|
||||
│ ├── config/ # 配置加载
|
||||
│ └── middleware/ # 中间件(JWT 认证等)
|
||||
├── knowledge_source/ # 知识源 Markdown(运行时)
|
||||
├── data/ # 运行时数据(db+media,git 忽略)
|
||||
└── deploy/ # systemd 单元 + DELIVERY.md
|
||||
```
|
||||
|
||||
## 3. 应用装配模式(main.py)
|
||||
## 3. 应用装配模式(RegisterRoutes)
|
||||
|
||||
参考 zhilianyuan2 的模式,每个模块的 router 独立注册:
|
||||
入口 `cmd/server.go` 创建 `gin.Engine`,调用 `api.RegisterRoutes` 注册全部路由:
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI
|
||||
from app.api import auth, company_train, product, sales_train
|
||||
from app.api import exam, media, ai_chat, system
|
||||
```go
|
||||
// backend-go/cmd/server.go
|
||||
package main
|
||||
|
||||
app = FastAPI(title="eai_agentplatform_app", version="1.1.0")
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"eai_agentplatform/backend/internal/api"
|
||||
"eai_agentplatform/backend/internal/config"
|
||||
)
|
||||
|
||||
# 异常处理器
|
||||
@app.exception_handler(AppError)
|
||||
def handle_app_error(request, exc):
|
||||
return JSONResponse(status_code=exc.status_code, content={...})
|
||||
func main() {
|
||||
cfg := config.Load()
|
||||
r := gin.Default()
|
||||
|
||||
# 路由注册
|
||||
app.include_router(auth.router)
|
||||
app.include_router(company_train.router)
|
||||
app.include_router(product.router)
|
||||
app.include_router(sales_train.router)
|
||||
app.include_router(exam.router)
|
||||
app.include_router(media.router)
|
||||
app.include_router(ai_chat.router)
|
||||
app.include_router(system.router)
|
||||
// 公开静态文件(已审批素材)
|
||||
r.Static("/media", cfg.KBDataDir+"/approved")
|
||||
|
||||
// 注册全部路由
|
||||
api.RegisterRoutes(r, cfg)
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 依赖注入模式
|
||||
路由注册(`api/router.go`):
|
||||
|
||||
参考 zhilianyuan2 的 `auth/dependencies.py`:
|
||||
```go
|
||||
// api/router.go
|
||||
func RegisterRoutes(r *gin.Engine, cfg *config.Config) {
|
||||
// 健康检查
|
||||
r.GET("/api/health", HealthCheck)
|
||||
|
||||
```python
|
||||
# core/deps.py
|
||||
async def get_current_user(
|
||||
credentials: HTTPAuthorizationCredentials | None = Depends(HTTPBearer(auto_error=False)),
|
||||
db: Session = Depends(get_db),
|
||||
) -> User:
|
||||
"""解析 JWT → 校验用户状态 → 返回 User"""
|
||||
if credentials is None:
|
||||
raise AuthError("缺少 Authorization Bearer 令牌")
|
||||
payload = decode_access_token(credentials.credentials, settings)
|
||||
user = db.query(User).filter(User.username == payload["sub"]).first()
|
||||
if user is None or user.status != "active":
|
||||
raise AuthError("用户不存在或已禁用")
|
||||
return user
|
||||
// 公开 + 员工
|
||||
r.GET("/api/company-train", middleware.Auth(cfg), GetCompanyTrain)
|
||||
r.GET("/api/specialists", middleware.Auth(cfg), ListSpecialists)
|
||||
|
||||
def require_admin(user: User = Depends(get_current_user)) -> User:
|
||||
"""管理员角色守卫"""
|
||||
if user.role != "admin":
|
||||
raise ForbiddenError("需要管理员权限")
|
||||
return user
|
||||
// 员工端考试
|
||||
r.GET("/api/exam/list", middleware.Auth(cfg), ExamList)
|
||||
r.GET("/api/exam/cover", middleware.Auth(cfg), ExamCover)
|
||||
r.POST("/api/exam/start", middleware.Auth(cfg), ExamStart)
|
||||
r.POST("/api/exam/submit", middleware.Auth(cfg), ExamSubmit)
|
||||
|
||||
// 管理员
|
||||
admin := r.Group("/api").Use(middleware.Auth(cfg), middleware.RequireAdmin())
|
||||
{
|
||||
admin.POST("/products", CreateProduct)
|
||||
admin.POST("/courses", CreateCourse)
|
||||
admin.POST("/media/audit/:mediaId", AuditMedia)
|
||||
admin.POST("/knowledge/scan", KnowledgeScan)
|
||||
admin.GET("/system/users", ListUsers)
|
||||
admin.GET("/system/config", GetConfig)
|
||||
admin.GET("/system/exam-records", ListExamRecords)
|
||||
}
|
||||
|
||||
// AI 对话
|
||||
r.POST("/api/ai-chat/message", middleware.Auth(cfg), ChatMessage)
|
||||
r.GET("/api/ai-chat/quick-actions", middleware.Auth(cfg), QuickActions)
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 中间件模式
|
||||
|
||||
```go
|
||||
// internal/middleware/auth.go
|
||||
func Auth(cfg *config.Config) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
claims, err := auth.ParseToken(
|
||||
c.GetHeader("Authorization"), cfg.JWTSecret,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(401, gin.H{"error": "unauthorized"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Set("user", claims)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func RequireAdmin() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
user := c.MustGet("user").(jwt.MapClaims)
|
||||
if role, ok := user["role"].(string); !ok || role != "admin" {
|
||||
c.JSON(403, gin.H{"error": "forbidden"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5. API 路由前缀
|
||||
|
||||
| 路由前缀 | 模块 | 说明 |
|
||||
|---------|------|------|
|
||||
| `/api/auth/*` | auth | 登录/注册/当前用户 |
|
||||
| `/api/company-train/*` | company_train | 公司介绍内容 |
|
||||
| `/api/products/*` | product | 产品 CRUD + 导入 |
|
||||
| `/api/courses/*` | sales_train | 课程 CRUD + 绑定产品 |
|
||||
| `/api/exam/*` | exam | 题库/组卷/考试/记录 |
|
||||
| `/api/media/*` | media | 上传/预览/审批/状态 |
|
||||
| `/api/ai-chat/*` | ai_chat | PathCoach 流式对话 |
|
||||
| `/api/system/*` | system | 用户/成绩/配置 |
|
||||
| `/api/auth/*` | auth.go | 登录/注册/当前用户 |
|
||||
| `/api/company-train/*` | company_train.go | 公司介绍内容 |
|
||||
| `/api/products/*` | product.go | 产品 CRUD + 导入 |
|
||||
| `/api/courses/*` | courses.go | 课程 CRUD + 绑定产品 |
|
||||
| `/api/exam/*` | exam.go | 题库/组卷/考试/记录 |
|
||||
| `/api/media/*` | media.go | 上传/预览/审批/状态 |
|
||||
| `/api/ai-chat/*` | ai_chat.go | PathCoach 流式对话 |
|
||||
| `/api/system/*` | system.go | 用户/成绩/配置 |
|
||||
| `/api/health` | — | 健康检查 |
|
||||
|
||||
## 6. 异步任务模式
|
||||
## 6. 单二进制交付
|
||||
|
||||
文档转换管线(审批通过后异步执行):
|
||||
```bash
|
||||
CGO_ENABLED=0 go build -o bin/eai_agentplatform-server ./cmd/server
|
||||
# 输出:statically linked,无运行时依赖
|
||||
```
|
||||
|
||||
```python
|
||||
# services/media_service.py
|
||||
import threading
|
||||
|
||||
def _async_convert_and_extract(media_file_id: int):
|
||||
"""审批通过后异步执行:文档转 PDF → 文本提取 → 切片入库"""
|
||||
with Session() as db:
|
||||
media = db.query(MediaFile).get(media_file_id)
|
||||
# 1. 调用 LibreOffice 转 PDF
|
||||
pdf_path = libreoffice_convert(media.stored_path)
|
||||
# 2. PyMuPDF 提取文本
|
||||
text = pymupdf_extract(pdf_path)
|
||||
# 3. 按段落切片写入 knowledge_chunk
|
||||
chunks = split_into_chunks(text)
|
||||
for i, chunk in enumerate(chunks):
|
||||
db.add(KnowledgeChunk(media_file_id=media.id, ...))
|
||||
media.extracted = True
|
||||
db.commit()
|
||||
|
||||
def approve_media(media_file_id: int, auditor_id: int):
|
||||
"""审批通过 → 触发异步转换"""
|
||||
media.status = "approved"
|
||||
media.audit_by = auditor_id
|
||||
media.audit_at = datetime.utcnow()
|
||||
db.commit()
|
||||
# 启动异步任务
|
||||
threading.Thread(target=_async_convert_and_extract, args=(media_file_id,)).start()
|
||||
```
|
||||
部署:`systemd` 直接拉起单二进制,`data/` 目录下 SQLite 数据库自动建表。
|
||||
|
||||
Reference in New Issue
Block a user