Commit 9bda1904 authored by yaoke.yk's avatar yaoke.yk

chore: keep frontend and backend only

parent 24c4cc97
TZ=Asia/Shanghai
MYSQL_PORT=13306
MYSQL_ROOT_PASSWORD=root123
MYSQL_DATABASE=yaoai_comic
MYSQL_USER=yaoai
MYSQL_PASSWORD=yaoai123
REDIS_PORT=16379
BACKEND_PORT=8080
FRONTEND_PORT=3000
SPRING_PROFILES_ACTIVE=prod
VITE_API_BASE_URL=/api
VOLCENGINE_ARK_API_KEY=
VOLCENGINE_ARK_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
VOLCENGINE_ARK_TEXT_MODEL=doubao-seed-2-0-code-preview-260215
VOLCENGINE_ARK_IMAGE_MODEL=doubao-seedream-5-0-260128
VOLCENGINE_ARK_VIDEO_MODEL=doubao-seedance-2-0-fast-260128
VOLCENGINE_TOS_ACCESS_KEY=
VOLCENGINE_TOS_SECRET_KEY=
VOLCENGINE_TOS_ENDPOINT=https://tos-cn-beijing.volces.com
VOLCENGINE_TOS_REGION=cn-beijing
VOLCENGINE_TOS_BUCKET=
# YaoAI Video
当前仓库已经整理为一个单仓库项目,核心只保留两部分:
- 前端工程:`doc/html`
- 后端工程:`yaoai-comic-studio`
## 开发态启动
这部分保持你现在本地已经跑通的方式,不改习惯。
1. 启动后端依赖服务:
```bash
cd yaoai-comic-studio
docker compose up -d
```
2. 编译后端:
```bash
cd yaoai-comic-studio
mvn package -pl yaoai-bootstrap -am -DskipTests
```
3. 启动后端 Jar:
```bash
cd yaoai-comic-studio
java -jar yaoai-bootstrap\target\yaoai-bootstrap-0.1.0-SNAPSHOT.jar
```
4. 启动前端开发服务:
```bash
cd doc/html
npm install
npm run dev
```
默认开发访问地址:
- 前端:`http://localhost:5173`
- 后端:`http://localhost:8080`
- Swagger:`http://localhost:8080/swagger-ui/index.html`
## 发布态部署
根目录的 `docker-compose.yml` 是单独给部署使用的,会一起构建前端和后端镜像。
1. 复制环境变量模板:
```bash
cp .env.example .env
```
2. 补全 `.env` 中的数据库密码、火山引擎 Ark/TOS 等配置。
3. 在仓库根目录执行:
```bash
docker compose up -d --build
```
默认发布访问地址:
- 前端:`http://localhost:3000`
- 后端:`http://localhost:8080`
发布态下,前端容器会通过 Nginx 将 `/api` 反向代理到后端容器,不需要额外处理跨域。
## 文件说明
- 根目录 `docker-compose.yml`:发布态一键部署
- `yaoai-comic-studio/docker-compose.yml`:开发态依赖服务
- `.env.example`:发布态环境变量模板
## 推送到 GitHub
如果你要新建 GitHub 仓库并推送当前代码,可以直接在根目录执行:
```bash
git add .
git commit -m "chore: keep frontend and backend only"
git branch -M main
git remote remove origin
git remote add origin <your-github-repo-url>
git push -u origin main
```
# AI 漫剧 SaaS 平台 — 设计文档索引
> 生成时间: 2026-04-16
> 技术栈: Java 17 + Spring Boot 3.x + MyBatis-Plus + Spring AI Alibaba + MySQL + Redis Stack (缓存 + 向量) + 阿里云 OSS / 火山引擎 TOS(主)+ MinIO(备选)
> 前端参考: `doc/html` (Vite + React + Shadcn)
> 开源参考: Toonflow-app、AIComicBuilder、waoowaoo
## 文档结构
| # | 文档 | 说明 |
|---|------|------|
| 01 | [产品定位与 SaaS 化策略](./01-产品定位与SaaS化策略.md) | 目标用户、商业模式、核心差异化 |
| 02 | [前端功能与页面清单](./02-前端功能与页面清单.md) | 基于 doc/html 原型的完整功能规格 |
| 03 | [三开源项目对比与吸收](./03-三开源项目对比与吸收.md) | Toonflow / AIComicBuilder / waoowaoo 取长补短 |
| 04 | [核心业务流程与 AI 流水线](./04-核心业务流程与AI流水线.md) | 剧本→大纲→分集→角色→分镜→视频 端到端流程 |
| 05 | [后端架构设计](./05-后端架构设计.md) | Spring Boot 模块划分、分层、核心组件 |
| 06 | [数据库设计](./06-数据库设计.md) | MySQL 表结构 + Redis Stack 向量存储 |
| 07 | [API 接口规范](./07-API接口规范.md) | REST + WebSocket/SSE 全量接口定义 |
| 08 | [异步任务与 AI 工作流](./08-异步任务与AI工作流.md) | 三层 Agent + 任务队列 + Skill 系统 |
| 09 | [计费与多租户基础设施](./09-计费与多租户SaaS基础设施.md) | 余额冻结、Saga 事务、租户隔离 |
| 10 | [部署与运维](./10-部署与运维.md) | Docker、监控、可观测性 |
| 11 | [实施路线图](./11-实施路线图与里程碑.md) | 分阶段交付计划 |
| 12 | [前端原型复盘与流程验证](./12-前端原型复盘与流程验证.md) | 17 页面清单、设计文档对齐度、技术债务 |
| 13 | [多 Agent 功能复盘与落地方案](./13-多Agent功能复盘与落地方案.md) | 三项目 Agent 源码分析、Java 落地方案、吸收策略 |
| 14 | [前端原型截图复盘与最终确认](./14-前端原型截图复盘与最终确认.md) | 34 截图映射、页面覆盖率、原型与设计文档差异 |
| 15 | [执行任务列表](./15-执行任务列表.md) | M0~M9 全量任务清单,~302 项,完成即标记 |
## 一句话总结
**做一个面向短剧工作室和内容创作者的 Web SaaS,核心能力是"剧本一键产出完整动画短剧视频"。后端用 Java + Spring AI Alibaba(通义系列)承载核心 AI 流水线,吸收 Toonflow 的三层 Agent 架构、AIComicBuilder 的角色一致性方案、waoowaoo 的 SaaS 工程化基础设施,实现 `doc/html` 原型定义的全部功能。**
# 01 · 产品定位与 SaaS 化策略
++ /dev/null
# 01 · 产品定位与 SaaS 化策略
## 1. 产品定义
**产品名(暂定)**: YaoAI 漫剧工坊 (YaoAI Comic Studio)
**一句话定位**: 给内容创作者的 AI 漫剧生产 SaaS —— 上传小说/剧本,自动产出完整动画短剧视频,从剧本解析、角色设定、分镜制作到视频合成全流程可视化、可编辑、可协作。
**对标**:
- 国外: Runway、Pika、LTX Studio
- 国内: 即梦短剧、AIComicBuilder(开源)、Toonflow(开源)
- 我们的差异化: **中文短剧场景深度优化 + 企业级协作 + 通义系列原生集成 + 资产复用库**
## 2. 目标用户
| 层级 | 画像 | 关键诉求 |
|------|------|----------|
| **C 端创作者** | 网文作者、短视频博主、独立漫画师 | 低成本、快速出片、无需美术基础 |
| **小型工作室** | 5~20 人短剧 MCN、同人社团 | 多人协作、资产复用、品质可控 |
| **企业客户** | 出版社、游戏公司、教育机构 | 私有化部署、品牌安全、批量产出 |
## 3. 核心价值主张
1. **剧本一键开工**: 上传小说 → AI 自动提取角色/场景/关系,智能分集,无需手动拆分。
2. **角色一致性**: 借鉴 AIComicBuilder 的"四视图参考 + 文字约束"双轨制,角色在 1000+ 镜头中保持同一外形。
3. **三层 Agent 质量管控**: 借鉴 Toonflow,决策/执行/监督三层 Agent 协作,AI 产出有审核有修订。
4. **所见即所得的分镜工作台**: 拖拽排序、批量生成、实时预览、版本回溯。
5. **企业级 SaaS 基础**: 多租户隔离、按量计费、团队权限、资产库共享、审计日志。
## 4. 商业模式
### 4.1 套餐
| 套餐 | 月费 | 积分 | 团队人数 | 功能 |
|------|------|------|---------|------|
| Free | ¥0 | 100 | 1 | 基础生成、带水印 |
| Pro | ¥99 | 3000 | 5 | 去水印、资产库、参考图模式 |
| Team | ¥499 | 20000 | 20 | 团队协作、角色版本管理、API |
| Enterprise | 定制 | 定制 | 无限 | 私有部署、SLA、专属客服、定制模型 |
### 4.2 计费单位 (积分)
| 操作 | 单价(积分) | 说明 |
|------|-----------|------|
| 文本生成(大纲/分镜拆分) | 1~5 | 按 token |
| 角色四视图生成 | 20 | 一次性 |
| 场景/道具图 | 10 | 单张 |
| 分镜首尾帧 | 15 × 2 | 首+尾 |
| 视频生成 (5s, 720p) | 50 | 单镜头 |
| 视频生成 (8s, 1080p) | 120 | 单镜头 |
| TTS 配音 | 0.1 / 字符 | |
| 口型同步 | 30 | 单镜头 |
**补充充值**: ¥1 = 10 积分
## 5. 核心差异化(vs 三个开源项目)
| 维度 | Toonflow | AIComicBuilder | waoowaoo | **我们** |
|------|----------|----------------|----------|----------|
| 部署形态 | Electron 桌面 | Web 本地 | Web 本地 | **云 SaaS + 私有化可选** |
| 多租户 | ❌ | ❌ | ⭐ 基础 | ⭐⭐⭐ 企业级 |
| 计费 | ❌ | ❌ | ⭐ 账户冻结 | ⭐⭐⭐ 完整订阅+按量 |
| Agent 架构 | ⭐⭐⭐ 三层 | ⭐ 单层 | ⭐⭐ LangGraph | **⭐⭐⭐ 三层 + 可观测** |
| 角色一致性 | ⭐⭐ | ⭐⭐⭐ 双轨制 | ⭐⭐ | **⭐⭐⭐ 四视图+向量检索** |
| 视频模型 | Sora/豆包 | Seedance/Veo/Kling | FAL/百炼 | **通义万相+可插拔多模型** |
| 中文优化 | ⭐⭐ | ⭐ | ⭐⭐ | **⭐⭐⭐ 原生中文短剧** |
| 团队协作 | ❌ | ❌ | ⭐ | **⭐⭐⭐ 角色/权限/资产库** |
| 资产复用库 | ⭐ | ⭐ | ⭐⭐ Global Assets | **⭐⭐⭐ 个人库+团队库+市场** |
## 6. 关键产品假设
1. **短剧市场足够大**: 2025 年国内短剧市场 500 亿+,AI 化是大势。
2. **创作者愿意为速度付费**: 人工制作 1 集需 3~7 天,AI 只需 1~2 小时,时间价值远超积分费。
3. **质量门槛可接受**: AI 生成质量目前足以应对 60% 的场景(对话、转场、中景),人工补 40% 特写/动作戏。
4. **资产复用是 retention 核心**: 用户做第二个项目时,角色库/场景库复用率 > 50%,形成粘性。
## 7. 不做的事(范围边界)
- ❌ 不做真人视频换脸、不做成人内容(合规风险)
- ❌ 不做实时互动直播(技术栈不同)
- ❌ 不做专业级后期特效(让用户导出到 AE/PR)
- ❌ 第一版不做移动端 App(Web 响应式优先)
# 02 · 前端功能与页面清单
++ /dev/null
# 02 · 前端功能与页面清单
> 基于 `doc/html` 原型(Vite + React 18 + Shadcn + React Router 7)整理。后端需对齐这里定义的全部功能和数据模型。
## 1. 技术栈速览(前端原型)
- Vite 6 + React 18 + TypeScript + React Router 7
- Tailwind 4 + Shadcn (Radix) + MUI 7(部分)
- Recharts 2(图表)、react-dnd(拖拽)、react-hook-form、sonner
- 状态管理: 纯 React Hooks(生产需引入 Zustand / TanStack Query)
- 当前无真实后端调用,均为 mock 数据
## 2. 路由表
```
/ Dashboard(项目看板)
/new-project 新建项目(上传剧本)
/team 企业级团队管理
/usage 使用统计/积分管理
/models 模型配置(语言/图片/视频)
/settings AI 水印设置(全局)
/assets/* 资产库(角色/场景/道具)
/project/:projectId 项目详情
/style 风格选择
/outline 大纲生成
/episodes 分集生成
/characters 角色生成
/scenes 场景生成
/props 道具生成
/storyboard/:episodeId? 分镜工作台
/video 视频生成与预览
/settings 项目成员与权限
```
## 3. 页面功能详表
### 3.1 Dashboard `/`
- 项目网格(3 列)、统计卡片(总项目/进行中/成员/总集数)
- 搜索:名称模糊 + 日期范围 + 团队成员多选
- 项目卡片: 封面、标题、描述、3 个标签、集数、成员数、更新时间、进度条、状态(outline/storyboard/video)
- **API**: `GET /api/projects`(分页+筛选)、`GET /api/projects/stats`
### 3.2 NewProject `/new-project`
- 拖拽上传 txt/doc/docx/pdf/md(≤20MB)
- 三选项卡: 纵横比(16:9 / 9:16)、分辨率(720p/1080p)、风格(2D日漫等)
- **API**: `POST /api/projects` (multipart)、`POST /api/projects/{id}/parse-script`
### 3.3 StyleSelection `/project/:id/style`
- 6 种预设风格卡片(都市情感/玄幻修仙/甜宠恋爱/古装宫廷/悬疑推理/轻喜剧)
- **API**: `GET /api/styles``PATCH /api/projects/{id}` (style 字段)
### 3.4 OutlineGeneration `/project/:id/outline`
- 显示剧名、类型、梗概、核心主题、4 个故事篇章
- 支持编辑大纲
- **API**: `POST /api/projects/{id}/outline:generate` (异步 jobId)、`GET /api/projects/{id}/outline``PUT /api/projects/{id}/outline`
### 3.5 EpisodeGeneration `/project/:id/episodes`
- 分集网格(5 列),每集标题/时长/梗概
- **API**: `POST /api/projects/{id}/episodes:generate``GET /api/projects/{id}/episodes`
### 3.6 CharacterGeneration `/project/:id/characters`
- 角色 Tab + 场景 Tab + 道具 Tab 切换
- 角色卡片: 头像、类型(女/男主/反派/配角)、性别、年龄、人格、背景、配音音色
- 正面大头照 + 全身三视图拼图
- 服装变体: 同一角色不同服装形态作为独立角色变体生成(如"张三-校服"、"张三-西装"),每个变体有独立四视图
- 操作: 编辑/删除/上传参考图/AI 生成形象/新增/创建服装变体
- **API**: CRUD `/api/projects/{id}/characters` + `POST /api/characters/{cid}/images:generate`
### 3.7 SceneGeneration `/project/:id/scenes`
- 场景卡: 名称、类型(室内/室外)、描述、使用场景、图像
- **API**: CRUD `/api/projects/{id}/scenes` + `POST /api/scenes/{sid}/image:generate`
### 3.8 PropsGeneration `/project/:id/props`
- 道具卡: 名称、分类、描述、使用场景列表、图像
- **API**: CRUD `/api/projects/{id}/props` + `POST /api/props/{pid}/image:generate`
### 3.9 StoryboardWorkspace `/project/:id/storyboard/:episodeId?` ⭐ 核心
- 左侧: 分集选择器(可折叠)
- 中间: 分镜详情编辑
- 分镜信息区:
- 分镜描述(文本)
- 出镜角色(可增删更换,每个角色可选择不同音色)
- 分镜场景(可更换)
- 道具(可增删更换)
- 视频生成区:
- 单一提示词编辑框: 镜头调度、灯光、画风、镜头描述等所有生成指令统一在一个文本框内编写,无其他分散编辑项
- 智能体选择器: 可选择不同的 AI 智能体(如通义万相、Seedance、Kling 等)来生成视频
- 积分预估: 根据所选智能体和视频时长,实时计算预计消耗积分
- 说明: 无独立服装选项,角色的不同服装形态在角色生成阶段作为独立角色变体创建
- 右侧: 预览区(视频/首帧图)
- 底部工具栏: AI 检查、批量操作、生成视频
- 操作: 新增/复制/删除/展开折叠/拖拽排序
- **API**: CRUD `/api/projects/{id}/episodes/{eid}/storyboards`, `PUT .../reorder`, `POST /api/storyboards/{sid}/video:generate`, `POST /api/storyboards/batch:generate`, `POST /api/storyboards/{sid}/ai-check`
### 3.10 VideoGeneration `/project/:id/video`
- 左侧分集列表
- 中间视频播放器 + 分镜时间线
- 右侧分镜缩略图网格(状态: generating/completed/failed/queued/not_started)
- 进度统计
- **API**: `GET /api/projects/{id}/episodes/{eid}/segments``GET /api/storyboards/{sid}/video-status`(SSE 推荐)
### 3.11 ProjectDetail `/project/:id`
- 顶部: 封面、标题、标签、集数、屏幕方向、纵横比、分辨率、小组
- 底部悬浮工具栏: AI 模型选择、积分余额、快速生成按钮
- Tab: 剧本大纲 / 人物小传 / 分集列表
- **API**: `GET /api/projects/{id}` + `GET /api/projects/{id}/full-data`
### 3.12 ProjectSettings `/project/:id/settings`
- 项目团队成员(owner/admin/editor/viewer 四级角色)
- 邀请成员、改角色、移除
- **API**: `GET/POST/PUT/DELETE /api/projects/{id}/members``POST /api/projects/{id}/invitations`
### 3.13 TeamManagement `/team`(企业级)
- 员工 Tab: 列表、编辑、删除、批量导入、邀请
- 小组 Tab: 仙侠/都市/日漫/古装/悬疑 5 个预设,卡片显示成员数和权限
- **API**: `/api/admin/employees``/api/admin/groups`
### 3.14 UsageManagement `/usage`
- 积分卡片(总/已用/剩余/子账户)
- 30 天趋势折线图(实际+预测)
- 使用记录表(类型/项目/用户/小组/积分/时间),支持筛选
- **API**: `/api/usage/stats``/api/usage/daily``/api/usage/records`
### 3.15 AssetsManagement `/assets/*`
- 个人库 / 团队库切换
- 类型筛选(角色/场景/道具)
- 搜索、标签、创建者、风格筛选
- 卡片: 缩略图、名称、标签、创建者、收藏、操作菜单
- 快速复用到当前项目
- **API**: `/api/assets` CRUD、`POST /api/assets/{aid}/clone`
### 3.16 ModelSettings `/models`
- 默认模型选择器: 语言模型 / 图片模型 / 视频模型 三栏独立配置默认模型
- 供应商管理: 按能力类型(语言/图片/视频)分 Tab 显示已配置供应商
- 供应商卡片: 名称、协议标签、Base URL、API Key 状态、已启用模型数
- 模型列表: 每个供应商下展示可用模型,支持搜索、勾选启用
- 添加/编辑供应商弹窗: 名称、协议(OpenAI兼容/DashScope/Gemini/Seedance/Kling)、Base URL、API Key、Secret Key(可灵AK/SK)、手动添加模型 ID / 自动获取模型列表
- 预置供应商: 通义千问(LLM)、通义万相(图片)、OpenAI、Gemini、Seedance(视频)、可灵(视频)
- **API**: `GET/PUT /api/settings/models`, `POST /api/settings/models/providers`, `GET /api/settings/models/fetch`
### 3.17 AIWatermarkSettings `/settings`
- 启用/禁用、文字、位置(5 选)、大小(3 档)、透明度滑块、Logo 上传
- **API**: `GET/PUT /api/settings/watermark`
## 4. 全量数据模型(前端视角)
详见 [06-数据库设计](./06-数据库设计.md),这里仅列核心实体:
```
User / Tenant / Organization
└─ Project (title, description, style, aspectRatio, resolution, status, progress)
├─ Outline (summary, themes[], arcs[])
├─ Episode[] (number, title, description, duration)
│ └─ Storyboard[] (sceneNumber, detailedDescription, location, dialogue[], camera, duration, status, creditCost)
│ └─ ShotAsset[] (type: first_frame|last_frame|video|audio, fileUrl, version)
├─ Character[] (name, role, gender, personality, costume, voice, images{portrait, fullBody, reference})
│ └─ CharacterAppearance[] (version)
├─ Scene[] (name, type, description, image)
├─ Prop[] (name, category, description, scenes[], image)
├─ Member[] (userId, role[owner|admin|editor|viewer])
└─ WatermarkSettings
Asset (shared library, type=char|scene|prop, library=personal|team, tags[])
UsageRecord (type, projectId, userId, credits, timestamp)
BalanceTransaction (type, amount, balanceAfter, taskId)
GenerationTask (type, status, progress, result, errorMessage)
Notification (userId, type, title, read)
```
## 5. 交互与状态管理建议(给前端重构用)
- 引入 **Zustand** 管理 UI 状态(抽屉、选中分镜、上传进度)
- 引入 **TanStack Query** 管理服务器状态(缓存、失效、乐观更新)
- WebSocket/SSE 推送 AI 任务进度,避免轮询
- 分镜工作台使用虚拟列表(react-window)应对 500+ 分镜
- 大文件上传: 分片 + 断点续传(tus-js-client)
# 03 · 三开源项目对比与吸收
++ /dev/null
# 03 · 三开源项目对比与吸收
## 1. 三个项目一句话画像
| 项目 | 路径 | 定位 | 最大亮点 |
|------|------|------|----------|
| **Toonflow-app** | `Toonflow-app/` | Electron 桌面 AI 短剧工程 | **三层 Agent 协作 + Skill 外化 + 可编程供应商系统** |
| **AIComicBuilder** | `aibuildvideo/AIComicBuilder/` | Next.js 漫剧 Web 全栈 | **角色一致性双轨制 + 12 个提示词插槽化管理 + 首尾帧+视频插值** |
| **waoowaoo** | `waoowaoo/` | Next.js 企业级 AI 视频 SaaS | **BullMQ 任务队列 + 余额冻结计费 + 多租户 + 国际化 + 代码卫士** |
## 2. 功能矩阵
| 维度 | Toonflow | AIComicBuilder | waoowaoo | 吸收到我们项目 |
|------|----------|----------------|----------|----------------|
| 剧本解析 | ✓ 事件图谱驱动 | ✓ 四步导入流水线 | ✓ | **四步导入 + 事件提取**(AICB+Toonflow) |
| 自动分集 | ✓ 前 10 集黄金结构 | ✓ | ✗ | **采用 Toonflow 的付费卡点策略**(10/30/50/70/90%) |
| 角色四视图 | ✗ | ✓ 正面/3/4侧/侧/背 | ✗ | **必做,AICB 方案** |
| 角色参考图注入 | ✓ | ✓ Gemini 参考图 | ✓ | **必做,用通义万相参考图模式** |
| 角色关系图 | ✗ | ✓ characterRelations 表 | ✗ | **必做,影响构图和视线** |
| 分镜智能规划 | ✓ 导演 Agent | ✓ 非常细致 | ✓ | **合并 Toonflow 导演手册 + AICB shot-split** |
| 首尾帧+视频插值 | 部分 | ✓ 主力模式 | ✗ | **主力模式采用** |
| 多模态参考模式 | ✓ | ✓ Veo 3.1 | ✗ | **Pro 用户可选** |
| 三层 Agent | ✓⭐⭐⭐ | ✗ | LangGraph | **采用 Toonflow 三层架构** |
| Agent 记忆 | ✓ 向量+摘要 | ✗ | ✗ | **用 Redis Stack (RediSearch) 实现** |
| Skill 文件外化 | ✓ 159 个 .md | ✓ 12 个提示词 | ✗ | **采用 Markdown Skill 库 + 插槽化编辑** |
| 提示词版本管理 | ✗ | ✓ 完整 | ✗ | **采用 AICB 方案** |
| 可编程供应商 | ✓ VM 沙盒 | ✗ | ✓ 配置化 | **用 Spring 策略模式 + SPI** |
| 资产版本 | ✗ | ✓ shot_assets 表 | ✓ MediaObject 哈希去重 | **合并: 版本表 + 哈希去重** |
| 任务队列 | Socket.IO 简单 | 自研 | ✓ BullMQ + 4 队列 | **采用 RabbitMQ/Kafka + 队列分型** |
| 任务心跳/重试 | 部分 | ✗ | ✓ 完整 | **必做** |
| 多租户 | ✗ | ✗ | ✓ userId 隔离 | **强化: tenantId + Spring Security** |
| 计费系统 | ✗ | ✗ | ✓⭐⭐⭐ 余额冻结 | **直接采用 waoowaoo 方案** |
| 幂等性 | ✗ | ✗ | ✓ idempotencyKey | **必做** |
| 审计日志 | ✗ | ✗ | ✓ 结构化+脱敏 | **必做** |
| 国际化 | ✓ 7 种 | 4 种 | 2 种 | **中英双语起步** |
| 代码卫士 | ✗ | ✗ | ✓ 10+ 守护脚本 | **必做** |
| 测试分层 | ✗ | 部分 | ✓ 单元/集成/系统/回归 | **必做** |
| 可观测性 | ✗ | ✗ | ✓ Bull Board | **Prometheus + Grafana + Spring Actuator** |
| FFmpeg 合成 | ✓ | ✓ 字幕/转场/BGM | ✓ Remotion | **采用 AICB FFmpeg 方案** |
| TTS | 框架 | 未集成 | ✓ FAL/百炼 | **必做: 百炼 Qwen TTS** |
| 口型同步 | ✗ | ✗ | ✓ | **Pro 功能** |
## 3. 各项目核心亮点详述
### 3.1 Toonflow 亮点(架构启发)
**三层 Agent 协作体系** — 这是 Toonflow 最大的技术遗产,值得完整移植:
```
决策层 Agent
├─ 理解用户意图
├─ 任务拆解
├─ 调度执行/监督层
└─ 质量把关(最终通过/驳回)
执行层 SubAgent (6 个)
├─ 故事骨架搭建(三幕 + 付费卡点)
├─ 改编策略制定
├─ 剧本编写
├─ 衍生资产分析
├─ 导演规划
└─ 分镜生成
监督层 Agent
└─ 审核执行层产出,反馈修订
```
**Skill 文件外化**: 159 个 Markdown 文件分别存储提示词、艺术手册、导演约束,运行时动态加载,改动无需重启。
**艺术风格手册**: `art_skills/2D_90s_japanese_anime/` 下有完整的角色/场景/道具约束文档,包含鹅蛋脸、6-6.5 头身、樱桃唇等具体描述 —— 直接复用!
**跨会话记忆**: 基于 ONNX all-MiniLM 做向量化,按项目 ID 隔离,支持 RAG 召回。我们改用 Redis Stack(RediSearch,HNSW + COSINE)+ 通义 embedding,Spring AI `RedisVectorStore` 直连,未来超过 64GB 再迁 Milvus(接口零改动)。
### 3.2 AIComicBuilder 亮点(工程细节)
**角色一致性双轨制**:
1. 视觉轨道: 四视图参考图 → 每次生成首尾帧时作为 reference image 注入
2. 文字轨道: 分镜拆分时强制使用完整角色名,禁止缩写和代词
**12 个提示词 + 插槽化编辑**:
- script_generate / script_parse / script_split
- character_extract / import_character_extract / character_image
- shot_split
- frame_generate_first / frame_generate_last / scene_frame_generate
- video_generate / ref_video_prompt_generate
三层优先级: **项目级覆盖 > 全局级覆盖 > 代码默认值**,且支持**版本历史+一键回滚**
**shot_assets 统一资产表**: 一张表存 first_frame/last_frame/reference/keyframe_video/reference_video 五种类型,配合 assetVersion + isActive 实现版本管理,这个设计直接搬过来。
**四步剧本导入**: 文本抽取 → 角色提取(AI 去重) → 智能分集 → 一键创建,每步可视化、可修改、可回退。
### 3.3 waoowaoo 亮点(SaaS 基础设施)
**余额冻结机制**(核心借鉴):
```
Task 提交 → prepareTaskBilling → 估算成本 → 冻结金额(BalanceFreeze)
Task 执行 → withBilling 包装 → 收集实际用量
Task 完成 → settleTaskBilling → 解冻 + 扣费 + 写 BalanceTransaction
Task 失败 → rollbackTaskBilling → 解冻,不扣费
```
- **原子性**: 冻结即锁定余额,防止超支
- **幂等性**: idempotencyKey 防重复扣费
- **可追溯**: 每笔扣费都有 Transaction 流水
- **自动清理**: 过期冻结自动释放
**MediaObject 统一媒体管理**: 单一媒体表 + sha256 内容哈希 + 40+ 种关系,DRY 原则。
**代码卫士(Guards)**:
- `check:api-handler` — API 签名规范
- `check:prompt-i18n` — 提示词本地化
- 禁止 LLM 直接写在 API(必须走 worker)
- 禁止模型配置硬编码
**测试分层**: 单元 + 集成 + 系统 + 回归 + 并发 + 合约,CI 守护质量。
## 4. 我们的吸收策略
### 4.1 直接搬运(少改)
- AIComicBuilder 的 `shot_assets` 表设计
- AIComicBuilder 的 12 个提示词分类 + 插槽化编辑 + 版本历史
- AIComicBuilder 的四步剧本导入 UX
- AIComicBuilder 的角色四视图生成提示词
- waoowaoo 的 `UserBalance` / `BalanceFreeze` / `BalanceTransaction` 三表结构
- waoowaoo 的 `MediaObject` 统一媒体表
- Toonflow 的 `art_skills/` 艺术手册全套 Markdown 文件
- Toonflow 的故事骨架提示词(三幕 + 付费卡点)
### 4.2 架构移植(Node → Java)
- Toonflow 三层 Agent → Spring AI Alibaba + Tool Use + 多 Chain
- Toonflow 供应商 VM 系统 → Java SPI + Strategy Pattern + `@Component`
- Toonflow Agent Memory → Redis Stack(RediSearch)+ 通义 embedding + Spring AI `RedisVectorStore`
- waoowaoo BullMQ → RabbitMQ/Kafka + Spring AMQP + 多队列(image/video/voice/text)
- waoowaoo NextAuth → Sa-Token(Redis 会话)+ Sa-Token-OAuth2
- waoowaoo Prisma → MyBatis-Plus + Flyway
- waoowaoo next-intl → Spring MessageSource + i18n
### 4.3 自研强化
- **多租户**: 强化到 tenantId + RLS(行级安全)或租户库分库
- **订阅支付**: 集成支付宝/微信/Stripe,waoowaoo 只做了按量
- **权限模型**: RBAC + ABAC,细到分镜级别
- **可观测性**: Prometheus + Grafana + SkyWalking 链路追踪
- **资产市场**: 社区分享+付费购买资产(差异化功能)
## 5. 明确不用的设计
- ❌ Toonflow 的 SQLite — 直接上 MySQL
- ❌ Toonflow 的 Electron 打包 — 我们是 Web SaaS
- ❌ Toonflow 的 VM2 供应商系统 — 安全隐患,改 Java SPI
- ❌ waoowaoo 的 Remotion — 用 FFmpeg 够了,Remotion 是 React 依赖
- ❌ AIComicBuilder 的 SQLite + Drizzle — 同上
- ❌ 三个项目的单机部署 — 我们要分布式
# 04 · 核心业务流程与 AI 流水线
++ /dev/null
# 04 · 核心业务流程与 AI 流水线
## 1. 端到端业务流程
```
[用户注册/登录]
[创建项目] ─ 上传剧本 + 选择纵横比/分辨率/风格
[剧本解析] ─ 文本抽取 → 角色提取 → 关系推理 → 智能分集
[风格确认] ─ 选择艺术风格手册(2D日漫/国风/3D/水墨...)
[大纲生成] ─ 三幕结构 + 付费卡点 + 篇章划分
[资产建设] ─ 角色四视图 / 场景图 / 道具图(可并行)
[分镜生成] ─ 按集拆分 → 每集 N 个 shot(首尾帧描述 + 对白 + 相机指令)
[首尾帧生成] ─ 注入角色参考图 + 风格约束 + 色板约束
[视频生成] ─ 首尾帧插值或参考图模式(通义万相/可插拔)
[TTS 配音] ─ 按角色音色生成对白音频
[口型同步] ─ 可选(Pro 功能)
[视频合成] ─ FFmpeg 拼接 + 字幕烧录 + BGM 混音 + 转场
[导出/发布] ─ MP4 下载 / 水印处理 / 团队审核
```
## 2. AI 流水线分层
### Stage 1 · 剧本解析流水线
| 步骤 | 输入 | 模型 | 输出 | 存储 |
|------|------|------|------|------|
| 1.1 文本抽取 | 上传文件 | Apache POI / PDFBox | 纯文本 | `projects.script_text` |
| 1.2 事件提取 | 纯文本 | Qwen-Max | 章节 + 事件表 | `novel_events` |
| 1.3 角色提取 | 纯文本 + 事件 | Qwen-Max | 角色卡列表(含去重) | `characters` |
| 1.4 关系推理 | 角色 + 剧本 | Qwen-Max | 关系图 | `character_relations` |
| 1.5 智能分集 | 事件 + 目标集数 | Qwen-Max | 分集方案(三幕 + 卡点) | `episodes` |
**提示词来源**: 借鉴 AIComicBuilder 的 `import_character_extract` + Toonflow 的 `script_execution_skeleton.md`
### Stage 2 · 资产生成流水线
| 步骤 | 输入 | 模型 | 输出 | 特殊约束 |
|------|------|------|------|---------|
| 2.1 角色四视图 | 角色描述 + 风格手册 | 通义万相 | 4 张参考图(正/3/4侧/侧/背) | 单次生成 16:9 画布 |
| 2.2 衍生资产 | 剧本分析 | Qwen-Max | 服装变体/状态变体列表 | 仅状态变化才衍生 |
| 2.3 衍生资产图 | 父资产图 + 变体提示 | 通义万相 | 变体图 | 参考父资产 |
| 2.4 场景图 | 场景描述 + 色板 | 通义万相 | 场景底图 | 支持室内/室外 |
| 2.5 道具图 | 道具描述 | 通义万相 | 道具素材 | 可选 |
**并发**: 10 张/批,RabbitMQ `image.generate` 队列。
### Stage 3 · 分镜生成流水线
| 步骤 | 输入 | 模型 | 输出 |
|------|------|------|------|
| 3.1 导演规划 | 剧本 + 角色 + 关系图 + 风格 | Qwen-Max | 拍摄计划(景别/机位/运动) |
| 3.2 场景分组 | 剧本 + 导演计划 | Qwen-Max | 场景分组 + 每场镜头数 |
| 3.3 镜头拆分 | 场景 + 时长目标 | Qwen-Max | shot 列表(见下) |
| 3.4 监督审核 | shot 列表 | Qwen-Max | 审核报告 + 修订建议 |
**Shot 字段** (借鉴 AIComicBuilder shot-split):
```json
{
"sequence": 1,
"prompt": "张三从树后走出,警惕扫视",
"startFrame": "张三正面从树后探身,左手扶剑柄,夕阳斜照",
"endFrame": "张三走到空地中心,目光锁定前方",
"motionScript": "0-2s: 右肩先露出... 2-4s: 身体侧转...",
"videoScript": "精炼 40-70 字散文提示词",
"duration": 8,
"cameraDirection": "slow zoom in",
"compositionGuide": "rule_of_thirds",
"focalPoint": "张三面部",
"depthOfField": "shallow",
"dialogues": [{"character": "张三", "text": "谁在那里?"}],
"characters": ["张三"],
"transitionIn": "cut",
"transitionOut": "dissolve",
"soundDesign": "夜风、远处蝉鸣",
"musicCue": "紧张低弦乐"
}
```
### Stage 4 · 视觉生成流水线
```
for shot in episode.shots:
first_frame_prompt = build_first_frame(shot, characters, style)
first_frame = call_image_model(
prompt=first_frame_prompt,
reference_images=[char.turnaround_image for char in shot.characters],
size=project.resolution,
ratio=project.aspect_ratio
)
save_asset(shot_id, type='first_frame', url=first_frame, version=v+1)
last_frame_prompt = build_last_frame(shot, first_frame)
last_frame = call_image_model(
prompt=last_frame_prompt,
reference_images=[first_frame] + [char.turnaround]
)
save_asset(shot_id, type='last_frame', ...)
```
**模型选择**(可插拔):
| 模型 | 输入模式 | 质量 | 速度 | 成本 |
|------|---------|------|------|------|
| 通义万相 wanx2.1 | text + ref | ⭐⭐⭐ | 快 | 低 |
| Seedance 2.0 | text + ref + audio | ⭐⭐⭐⭐ | 中 | 中 |
| Veo 3.1 | text + 3 ref | ⭐⭐⭐⭐⭐ | 慢 | 高 |
| Kling v3 | text + ref | ⭐⭐⭐ | 中 | 中 |
### Stage 5 · 视频生成流水线
```
for shot in episode.shots:
video_task = submit_video_task(
model=project.video_model,
first_frame=shot.first_frame_url,
last_frame=shot.last_frame_url,
prompt=shot.videoScript,
duration=shot.duration,
resolution=project.resolution
)
# 异步轮询 5s x 120 次
video_url = poll_until_done(video_task.external_id)
save_asset(shot_id, type='keyframe_video', ...)
```
### Stage 6 · 音频流水线
```
for voice_line in episode.dialogues:
audio = qwen_tts(
text=voice_line.text,
voice=character.voice_id,
emotion=voice_line.emotion,
rate=voice_line.speed
)
save_asset(voice_line_id, type='audio', ...)
```
### Stage 7 · 合成流水线 (FFmpeg)
```
1. 按 shot.sequence 排序所有 keyframe_video
2. xfade 转场拼接(cut/dissolve/fade,默认 0.5s)
3. 生成 SRT 字幕(基于 dialogues 时间比例)
4. 字幕硬编码烧录
5. 混入对白音频(按时间戳)
6. 混入 BGM(volume=0.3)
7. 添加片头标题卡(3s) + 片尾(2s)
8. 输出 /projects/{id}/episodes/{eid}/final.mp4
```
## 3. 三层 Agent 协作(借鉴 Toonflow)
### 3.1 ScriptAgent(剧本 Agent)
```
用户输入 / 剧本上传
[决策层 DecisionAgent]
解析意图 → 拆解任务 → 调度执行/监督
[执行层] 串行调用:
SkeletonExecutor (故事骨架)
↓ 输出到工作区
[监督层 SupervisionAgent] 审核 → 通过/驳回/重做
AdaptationExecutor (改编策略)
[监督层] 审核
ScriptExecutor (剧本编写)
[监督层] 审核
[决策层] 汇总 → 返回用户
```
### 3.2 ProductionAgent(制作 Agent)
```
[决策层]
DeriveAssetsExecutor (衍生资产分析)
GenerateAssetsExecutor (资产图生成, 异步)
DirectorPlanExecutor (导演规划)
↓ [监督]
StoryboardTableExecutor (分镜表)
↓ [监督]
StoryboardPanelExecutor (分镜面板写入)
StoryboardImageExecutor (首尾帧生成, 异步并发)
VideoExecutor (视频生成, 异步并发 + 轮询)
AssemblyExecutor (FFmpeg 合成)
```
### 3.3 实现要点
- 决策层用 Spring AI Alibaba `ChatClient` + `@Tool` 定义子 Agent 为可调用工具
- 执行层每个 Executor 是独立 `@Service`,提示词从 `skills/` 加载
- 监督层可选开启(Free 套餐关闭以省成本)
- 跨 Agent 上下文通过 Redis Stack 向量检索(RediSearch)+ 短期摘要
## 4. 并发与异步策略
| 队列 | 并发 | 超时 | 重试 |
|------|------|------|------|
| `text.generate` | 50 | 60s | 3 次 |
| `image.generate` | 20 | 120s | 3 次 |
| `video.generate` | 10 | 900s | 2 次(昂贵) |
| `voice.tts` | 30 | 30s | 3 次 |
| `video.assemble` | 5 | 1800s | 1 次 |
| `lipsync` | 5 | 600s | 2 次 |
所有任务**必须**通过 RabbitMQ,**禁止**直接在 HTTP 线程里调 AI(借鉴 waoowaoo 代码卫士)。
## 5. 进度推送
**Server-Sent Events (SSE)** 主推送:
```
GET /api/projects/{id}/events (text/event-stream)
event: task.progress
data: {"taskId":"...","type":"video","progress":45,"currentShot":12,"totalShots":50}
event: task.completed
data: {"taskId":"...","resultUrl":"..."}
event: task.failed
data: {"taskId":"...","error":"..."}
```
WebSocket 留给后续实时协作(多人同时编辑分镜)。
This diff is collapsed.
This diff is collapsed.
# 07 · API 接口规范
++ /dev/null
# 07 · API 接口规范
## 1. 通用约定
- **BaseURL**: `https://api.yaoai.com/v1`
- **格式**: JSON(UTF-8),时间 ISO-8601
- **鉴权**: `Authorization: Bearer <jwt>`,租户从 JWT claims 解析
- **请求 ID**: 每个响应带 `X-Request-Id`
- **分页**: `?page=1&size=20`,响应 `{items[], total, page, size}`
- **错误**: 统一结构
```json
{"error":{"code":"INVALID_PARAM","message":"...","requestId":"..."}}
```
- **幂等**: 写操作支持 `Idempotency-Key`
- **限流**: 返回 `Retry-After`
## 2. 认证
| Method | Path | 说明 |
|--------|------|------|
| POST | `/auth/register` | 注册(email/username + password) |
| POST | `/auth/login` | 登录 {accessToken, refreshToken, user} |
| POST | `/auth/logout` | 注销 |
| POST | `/auth/refresh` | 刷新 token |
| GET | `/auth/me` | 当前用户信息 |
| POST | `/auth/oauth/{provider}/callback` | 第三方登录 |
| POST | `/auth/password/reset-request` | 发送重置邮件 |
| POST | `/auth/password/reset` | token 重置 |
## 3. 租户与成员
| Method | Path | 说明 |
|--------|------|------|
| GET | `/tenants/current` | 当前租户 |
| PATCH | `/tenants/current` | 更新名称/Logo |
| GET | `/tenants/members` | 成员列表 |
| POST | `/tenants/invitations` | 邀请成员 |
## 4. 项目
| Method | Path | 说明 |
|--------|------|------|
| GET | `/projects?page=&search=&status=&from=&to=&members=` | 列表 |
| GET | `/projects/stats` | Dashboard 统计 |
| POST | `/projects` | 创建(multipart: file + meta) |
| GET | `/projects/{id}` | 详情 |
| GET | `/projects/{id}/full-data` | 含大纲/人物/分集聚合 |
| PATCH | `/projects/{id}` | 更新字段 |
| DELETE | `/projects/{id}` | 删除 |
| POST | `/projects/{id}:archive` | 归档 |
### 项目成员
| Method | Path |
|--------|------|
| GET | `/projects/{id}/members` |
| POST | `/projects/{id}/invitations` |
| PATCH | `/projects/{id}/members/{mid}` |
| DELETE | `/projects/{id}/members/{mid}` |
## 5. 剧本解析 (四步导入)
| Method | Path | 说明 |
|--------|------|------|
| POST | `/projects/{id}/import/step1-extract` | 文本抽取 |
| POST | `/projects/{id}/import/step2-characters` | AI 角色提取 |
| GET/PATCH/DELETE | `/projects/{id}/characters/{cid}` | 用户修改角色 |
| POST | `/projects/{id}/import/step3-episodes` | 智能分集 |
| POST | `/projects/{id}/import/step4-confirm` | 一键创建 |
## 6. 风格与大纲
| Method | Path |
|--------|------|
| GET | `/styles` |
| POST | `/projects/{id}:set-style` |
| POST | `/projects/{id}/outline:generate` `taskId` |
| GET | `/projects/{id}/outline` |
| PUT | `/projects/{id}/outline` |
## 7. 分集
| Method | Path |
|--------|------|
| GET | `/projects/{id}/episodes` |
| POST | `/projects/{id}/episodes:generate` |
| PATCH | `/projects/{id}/episodes/{eid}` |
| DELETE | `/projects/{id}/episodes/{eid}` |
## 8. 角色/场景/道具
每种资源一套相同风格:
```
GET /projects/{id}/characters
POST /projects/{id}/characters
GET /projects/{id}/characters/{cid}
PATCH /projects/{id}/characters/{cid}
DELETE /projects/{id}/characters/{cid}
POST /projects/{id}/characters/{cid}:generate-turnaround # 四视图
POST /projects/{id}/characters/{cid}:upload-reference # 参考图
POST /projects/{id}/characters/{cid}/appearances # 新增变体
GET /projects/{id}/characters/{cid}/appearances
```
scenes / props 同构。
## 9. 分镜
| Method | Path | 说明 |
|--------|------|------|
| GET | `/projects/{id}/episodes/{eid}/storyboards` | 列表 |
| POST | `/projects/{id}/episodes/{eid}/storyboards:generate` | AI 批量生成分镜 |
| POST | `/projects/{id}/storyboards` | 手动创建 |
| GET | `/projects/{id}/storyboards/{sid}` | 详情 |
| PATCH | `/projects/{id}/storyboards/{sid}` | 更新 |
| DELETE | `/projects/{id}/storyboards/{sid}` | 删除 |
| POST | `/projects/{id}/storyboards/{sid}:duplicate` | 复制 |
| PUT | `/projects/{id}/episodes/{eid}/storyboards:reorder` | 拖拽重排序 |
| POST | `/projects/{id}/storyboards/{sid}:ai-check` | AI 合规检查 |
### 分镜资产
| Method | Path |
|--------|------|
| GET | `/projects/{id}/storyboards/{sid}/assets?type=FIRST_FRAME&active=true` |
| POST | `/projects/{id}/storyboards/{sid}/assets/first-frame:generate` |
| POST | `/projects/{id}/storyboards/{sid}/assets/last-frame:generate` |
| POST | `/projects/{id}/storyboards/{sid}/assets/video:generate` |
| POST | `/projects/{id}/storyboards/{sid}/assets/audio:generate` |
| PATCH | `/projects/{id}/storyboards/{sid}/assets/{aid}:activate` | 回滚版本 |
### 批量
| Method | Path |
|--------|------|
| POST | `/projects/{id}/episodes/{eid}/storyboards:batch-generate` body={storyboardIds[], targets:[first_frame,last_frame,video]} |
## 10. 视频合成
| Method | Path |
|--------|------|
| POST | `/projects/{id}/episodes/{eid}:assemble` | 合成最终视频(FFmpeg) |
| GET | `/projects/{id}/episodes/{eid}/segments` | 分镜段状态 |
| GET | `/projects/{id}/episodes/{eid}/final` | 最终 MP4 下载 URL |
## 11. 任务与进度
| Method | Path |
|--------|------|
| GET | `/tasks/{taskId}` | 单任务详情 |
| GET | `/tasks?type=&status=&projectId=` | 列表 |
| POST | `/tasks/{taskId}:cancel` | 取消 |
| GET | `/tasks/{taskId}/events` (**SSE**) | 进度事件流 |
| GET | `/projects/{id}/events` (**SSE**) | 项目级事件聚合 |
**SSE 事件格式**:
```
event: task.progress
data: {"taskId":"01HX...","type":"VIDEO_GEN","progress":42,"stage":"polling","message":"Shot 12/50"}
event: task.completed
data: {"taskId":"...","result":{...}}
event: task.failed
data: {"taskId":"...","error":{"code":"...","message":"..."}}
event: credit.consumed
data: {"amount":50,"balance":2340}
```
## 12. 计费
| Method | Path |
|--------|------|
| GET | `/billing/balance` | 当前余额+冻结+已消费 |
| GET | `/billing/transactions?page=&type=` | 流水 |
| POST | `/billing/recharge` body={amount, paymentMethod} → 支付链接 |
| POST | `/billing/subscription` body={plan} → 升级 |
| GET | `/billing/subscription` | 当前订阅 |
| POST | `/billing/subscription:cancel` |
| GET | `/usage/stats?from=&to=` | 积分统计 |
| GET | `/usage/daily?from=&to=` | 30 天趋势 |
| GET | `/usage/records?...` | 明细 |
## 13. 资产库
| Method | Path |
|--------|------|
| GET | `/assets?library=personal&type=character&tags=&search=` |
| POST | `/assets` | 上传 |
| GET | `/assets/{aid}` |
| PATCH | `/assets/{aid}` |
| DELETE | `/assets/{aid}` |
| POST | `/assets/{aid}:favorite` |
| POST | `/assets/{aid}:clone-to-project` body={projectId} |
## 14. 设置与提示词
| Method | Path |
|--------|------|
| GET/PUT | `/settings/watermark` |
| GET | `/prompt-templates?scope=&key=` |
| POST | `/prompt-templates` |
| PATCH | `/prompt-templates/{id}` |
| GET | `/prompt-templates/{id}/versions` |
| POST | `/prompt-templates/{id}:rollback?version=` |
| GET/POST | `/prompt-presets` |
## 15. 企业管理(`/admin/*`)
需要 `ADMIN``OWNER` 角色:
| Method | Path |
|--------|------|
| GET | `/admin/employees?search=&status=` |
| POST | `/admin/employees` |
| PATCH | `/admin/employees/{id}` |
| DELETE | `/admin/employees/{id}` |
| POST | `/admin/employees:bulk-import` (CSV) |
| GET | `/admin/groups` |
| POST | `/admin/groups` |
| PATCH | `/admin/groups/{id}` |
| GET | `/admin/audit-logs?...` |
## 16. 系统
| Method | Path |
|--------|------|
| GET | `/system/boot-id` | 健康检查 |
| GET | `/system/version` | 版本 |
| GET | `/system/notifications` | 全局公告 |
## 17. OpenAPI 规范
- 使用 `springdoc-openapi` 自动生成 `/v3/api-docs` 和 Swagger UI
- 所有 DTO 用 Bean Validation 注解(`@NotNull`/`@Size`/`@Email`)
- Controller 强制使用 `ResponseEntity<ApiResponse<T>>` 统一包装
- 错误码集中维护在 `ErrorCode` 枚举
This diff is collapsed.
This diff is collapsed.
# 10 · 部署与运维
++ /dev/null
# 10 · 部署与运维
## 1. 环境矩阵
| 环境 | 用途 | 规模 |
|------|------|------|
| **local** | 开发者本地 | Docker Compose 一键起 |
| **dev** | 集成测试、PR 预览 | 单节点 K8s / docker-compose |
| **staging** | 预发布、压测 | 2 节点 K8s,真实数据缩减版 |
| **prod** | 生产 | 多可用区 K8s + 完整灾备 |
## 2. Docker Compose(本地开发)
```yaml
# docker-compose.yml
version: "3.8"
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: yaoai123
MYSQL_DATABASE: yaoai
ports: ["13306:3306"]
volumes: ["mysql_data:/var/lib/mysql"]
redis:
# Redis Stack = Redis + RediSearch(向量)+ RedisJSON + RedisInsight
image: redis/redis-stack:7.2.0-v10
command: redis-stack-server --appendonly yes
ports: ["16379:6379", "18001:8001"] # 8001 是 RedisInsight 控制台
volumes: ["redis_data:/data"]
rabbitmq:
image: rabbitmq:3.13-management
ports: ["5672:5672", "15672:15672"]
volumes: ["rabbit_data:/var/lib/rabbitmq"]
# 对象存储:统一使用阿里云 OSS / 火山引擎 TOS,不在 docker-compose 内起
# 本地开发直连开发环境 OSS Bucket(yaoai-dev),通过 AK/SK 读写
# 未来若需完全离线开发,再按 TODO 加回 MinIO(S3 兼容)
# 注:Elasticsearch 暂不引入,MVP 用 MySQL FULLTEXT;M8 团队协作/资产库阶段再评估
api:
build: .
depends_on: [mysql, redis, rabbitmq]
environment:
SPRING_PROFILES_ACTIVE: local
MYSQL_HOST: mysql
REDIS_HOST: redis # 同一 Redis Stack 实例同时承载缓存 + 向量
RABBIT_HOST: rabbitmq
YAOAI_STORAGE_PROVIDER: oss # oss | tos(MinIO 作为未来 TODO)
YAOAI_VECTOR_PROVIDER: redis
OSS_ENDPOINT: ${OSS_ENDPOINT} # 生产:oss-cn-hangzhou.aliyuncs.com
OSS_ACCESS_KEY: ${OSS_ACCESS_KEY}
OSS_SECRET_KEY: ${OSS_SECRET_KEY}
QWEN_API_KEY: ${QWEN_API_KEY}
ports: ["18080:8080"]
command: java -jar app.jar --spring.profiles.active=local,api
worker:
build: .
depends_on: [mysql, redis, rabbitmq]
environment: (同上)
command: java -jar app.jar --spring.profiles.active=local,worker
deploy:
replicas: 2
volumes:
mysql_data: {}
redis_data: {}
rabbit_data: {}
```
## 3. Dockerfile(多阶段构建)
```dockerfile
# 构建阶段
FROM eclipse-temurin:17-jdk-alpine AS builder
WORKDIR /build
COPY . .
RUN ./mvnw -B -DskipTests package -pl yaoai-bootstrap -am
# 运行阶段
FROM eclipse-temurin:17-jre-alpine
# 安装 FFmpeg(Assemble Worker 需要)
RUN apk add --no-cache ffmpeg
WORKDIR /app
COPY --from=builder /build/yaoai-bootstrap/target/yaoai-bootstrap-*.jar app.jar
COPY skills/ ./skills/
EXPOSE 8080
ENTRYPOINT ["java","-XX:+UseContainerSupport","-jar","app.jar"]
```
## 4. Kubernetes 部署(生产)
### 4.1 部署单元
```
Deployment: yaoai-api replicas=3 (HPA by CPU 70%)
Deployment: yaoai-worker-text replicas=2 (HPA by queue depth)
Deployment: yaoai-worker-image replicas=3 (HPA by queue depth)
Deployment: yaoai-worker-video replicas=2 (HPA by queue depth, GPU)
Deployment: yaoai-worker-voice replicas=2
Deployment: yaoai-worker-assemble replicas=1 (FFmpeg,CPU 密集)
CronJob: yaoai-scheduler (定时任务,单实例)
StatefulSet: mysql (或用云 RDS)
StatefulSet: redis-stack (或云 Tair / Redis Enterprise 带 RediSearch 模块)
StatefulSet: rabbitmq 集群
对象存储: 阿里云 OSS / 火山引擎 TOS(托管,不在 K8s 内)
Ingress: yaoai.com / api.yaoai.com
```
### 4.2 HPA 按队列深度扩缩
```yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: yaoai-worker-video
spec:
minReplicas: 2
maxReplicas: 20
metrics:
- type: External
external:
metric:
name: rabbitmq_queue_depth
selector:
matchLabels:
queue: video.generate
target:
type: AverageValue
averageValue: "5" # 每 5 个任务扩 1 个 Pod
```
### 4.3 Secret 管理
- 用 ExternalSecrets + HashiCorp Vault 或 AWS Secrets Manager
- API Key、DB 密码、Sa-Token `token-name`/加密盐绝不进 Git
## 5. CI/CD
```
[ Push to main ]
GitHub Actions
├─ mvn test(单元 + 集成 with Testcontainers)
├─ ESLint / SonarQube 扫描
├─ Docker buildx → push to registry
├─ helm upgrade yaoai-staging
├─ 冒烟测试(Playwright)
[ Manual Approve ]
helm upgrade yaoai-prod (蓝绿 / 滚动)
```
## 6. 数据迁移
- **Flyway** 管理 schema,强制版本化
- 每次上线先跑 `V{n}__description.sql` 再发 app(新字段 + 默认值兼容旧代码)
- 大表 DDL 用 gh-ost / pt-online-schema-change 避免锁表
- 回滚方案:每个版本都有 `Undo_V{n}.sql`(可选)
## 7. 监控与告警
### 7.1 关键指标
| 类别 | 指标 | 告警阈值 |
|------|------|----------|
| API | p99 延迟 | > 2s |
| API | 5xx 率 | > 1% |
| MQ | 队列积压 | video > 50 / image > 100 |
| Worker | 失败率 | > 5% / 10min |
| DB | 慢查询 | > 1s 超过 10 条 /分钟 |
| AI | Provider 错误率 | > 10% |
| 业务 | 余额扣减异常 | 任何负数 |
| 业务 | 任务 stuck | processing 超过 30min |
### 7.2 告警通道
- 即时: 钉钉/企业微信 机器人
- 持续: PagerDuty / Opsgenie
- 日报: 每日 09:00 推送昨日核心指标
## 8. 备份与灾备
| 数据 | 策略 |
|------|------|
| MySQL | 每日全量 + binlog 增量,异地冷存 30 天 |
| Redis Stack(缓存 + 向量) | AOF 持久化 + 每日 RDB 快照;向量索引实例与缓存实例分开部署,向量实例优先保证持久化 |
| RabbitMQ | 队列持久化 + 定期导出定义 |
| OSS / TOS | 版本化 Bucket + 跨区域复制 + 生命周期归档 |
| 配置 | Git 版本化,不可变 |
RTO < 4h,RPO < 15min。
## 9. 安全基线
- [ ] HTTPS 全站(Let's Encrypt)
- [ ] WAF(Aliyun / Cloudflare)
- [ ] SQL 注入防护(参数化查询,禁 `@Query` 字符串拼接)
- [ ] XSS 防护(DTO 输出 escape)
- [ ] CSRF(Sa-Token 前后端分离通过请求头传 Token,天然免疫)
- [ ] 依赖扫描(Dependabot + Snyk)
- [ ] 敏感日志脱敏(统一 `LogRedactor`)
- [ ] API Key 加密存储(Jasypt + 主密钥放 KMS)
- [ ] 定期渗透测试(半年一次)
- [ ] 漏洞奖励计划(产品稳定后)
## 10. 运营手册(Runbook)片段
### 10.1 视频生成任务卡住
1. `kubectl logs -l app=worker-video --tail=200`
2.`tasks where status='PROCESSING' and heartbeat_at < NOW() - INTERVAL 5 MINUTE`
3. 查第三方 Provider 状态页
4. 临时措施: 手动 `markDead` → 重入队
5. 根因: 如果是 Provider 故障,切 fallback provider
### 10.2 余额异常
1.`user_balances``balance``frozen_amount` 为负的行
2. 对照 `balance_transactions` 重算
3. 临时措施: 冻结账户,人工补偿
4. 根因: 定位是否有竞态(没走乐观锁)
### 10.3 Worker OOM
1. `kubectl top pod`
2. 检查是否在单任务下载 1GB+ 视频到堆
3. 修复: 流式下载直接写 OSS,不全量加载
# 11 · 实施路线图与里程碑
++ /dev/null
# 11 · 实施路线图与里程碑
## 0. 总原则
- **单人/小团队起步**: 前 2 个月目标是把 MVP 跑通,而不是造全部功能
- **端到端优先**: 每个里程碑都要有可演示的用户价值,而不是只补基础设施
- **吸收 > 重造**: 有现成开源代码/提示词/skill 的地方直接搬,不要重新发明
- **前端保留原型**: `doc/html` 作为 UI 底稿,只做必要的接入层改造(加 TanStack Query + Zustand),不重写 UI
## 1. 里程碑总览
| 阶段 | 周期 | 目标 | 产出 |
|------|------|------|------|
| **M0 · 基建** | 第 1~2 周 | 工程骨架跑通 | 空 API + Swagger + Docker |
| **M1 · 认证与项目 CRUD** | 第 3~4 周 | 用户能登录、建项目 | Dashboard + NewProject 可用 |
| **M2 · 剧本解析流水线** | 第 5~7 周 | 上传剧本 → 出角色/分集 | 四步导入 UI + 异步任务 |
| **M3 · 资产生成** | 第 8~10 周 | 角色四视图 + 场景 + 道具 | Character/Scene/Props 可用 |
| **M4 · 分镜工作台** | 第 11~13 周 | AI 生成分镜 + 手动编辑 | StoryboardWorkspace 可用 |
| **M5 · 视觉生成** | 第 14~16 周 | 首尾帧 + 视频生成 | 单镜头视频可生成 |
| **M6 · 合成与导出** | 第 17~18 周 | FFmpeg 拼成完整集 | 可下载完整 MP4 |
| **M7 · 计费与 SaaS** | 第 19~21 周 | 余额、订阅、支付 | 真正 SaaS 化 |
| **M8 · 团队协作** | 第 22~24 周 | 多人协作、资产库、权限 | 企业功能 |
| **M9 · 上线与优化** | 第 25~26 周 | 压测、监控、灰度 | 生产发布 |
**总计约 6 个月**(1~2 名后端 + 1 名前端,MVP 可提前到 3 个月但砍 M7~M9)。
## 2. 阶段详解
### M0 · 基建(第 1~2 周)
**交付清单**:
- [ ] Maven 多模块骨架(按 05-后端架构设计.md 的模块划分)
- [ ] Spring Boot 3 + Spring AI Alibaba 依赖跑通
- [ ] Flyway 初始化 + 空表结构
- [ ] Docker Compose 一键启动(mysql + redis-stack + rabbitmq;对象存储直连开发环境 OSS Bucket)
- [ ] Actuator + Prometheus endpoint
- [ ] Swagger / OpenAPI 自动文档
- [ ] 统一异常处理 + ApiResponse<T> 包装
- [ ] Logback JSON + MDC(requestId)
- [ ] GitHub Actions: PR 跑 test + build
- [ ] 前端 `doc/html` 接入 TanStack Query + Zustand + API client
**完成标准**: `docker compose up` 后能访问 Swagger UI 并看到 1 个 `/ping` 接口。
### M1 · 认证与项目 CRUD(第 3~4 周)
- [ ] Sa-Token 1.39+(`sa-token-spring-boot3-starter` + `sa-token-redis-jackson`),Token 存 Redis
- [ ] `/auth/*` 全套(注册/登录/刷新/登出/me)
- [ ] IP 登录限流(Bucket4j + Redis)
- [ ] Tenant 自动创建(注册即建个人租户)
- [ ] `TenantContextFilter` + MyBatis-Plus `TenantLineInnerInterceptor`
- [ ] Projects 表 + CRUD API
- [ ] `/projects/stats` 统计接口
- [ ] 前端 Dashboard + NewProject 真实接入(去 mock)
- [ ] 前端 Auth Guard
**完成标准**: 用户注册→登录→创建一个空项目→在 Dashboard 看到它。
### M2 · 剧本解析流水线(第 5~7 周)
- [ ] 文件上传 + 阿里云 OSS / 火山引擎 TOS 接入(`StorageProvider` 抽象接口,MinIO 作为未来 TODO 备选)+ MediaObject 表
- [ ] 文本抽取(POI + PDFBox,支持 txt/doc/docx/pdf/md)
- [ ] RabbitMQ 接入 + Task 表 + TaskService + 通用 Worker 模板
- [ ] Spring AI Alibaba 通义千问接入(chat/embedding)
- [ ] `SkillLoader` + 从 Toonflow 拷贝的 `script_execution_skeleton.md`
- [ ] 实现 Executor: `ScriptParseExecutor` + `CharacterExtractExecutor` + `EpisodeSplitExecutor`
- [ ] 四步导入 API + SSE 进度
- [ ] 前端 NewProject 的四步向导(借鉴 AIComicBuilder UX)
- [ ] Character/Scene/Props 表 + 基础 CRUD
**完成标准**: 上传一篇 5000 字的小说,能自动出角色列表和分集列表,全程 SSE 推进度。
### M3 · 资产生成(第 8~10 周)
- [ ] `AiProvider` 接口 + `WanxImageProvider`(通义万相文生图)
- [ ] 兜底 Provider: OpenAI DALL-E 或 Gemini Imagen(可配置)
- [ ] 从 Toonflow 拷 `art_skills/2d_anime/` 全套艺术手册
- [ ] `CharacterTurnaroundExecutor`(四视图,借鉴 AIComicBuilder)
- [ ] `SceneImageExecutor` / `PropImageExecutor`
- [ ] `shot_assets` 表 + 资产版本管理
- [ ] 前端 CharacterGeneration / SceneGeneration / PropsGeneration 页接入
- [ ] 参考图上传 + 注入到生成请求
- [ ] 初级计费(SOFT 模式)
**完成标准**: 项目里每个角色都能一键生成四视图,前端能看到并支持重新生成。
### M4 · 分镜工作台(第 11~13 周)
- [ ] `Storyboard` 表完整字段(参考 AIComicBuilder shot 结构)
- [ ] `DirectorPlanExecutor`(借鉴 Toonflow 导演手册)
- [ ] `StoryboardSplitExecutor`(借鉴 AIComicBuilder shot-split.ts)
- [ ] `SupervisionAgent` 雏形(可选开关)
- [ ] Storyboard CRUD + 批量生成 + 排序 + 复制
- [ ] 前端 StoryboardWorkspace 完整接入
- [ ] 拖拽排序 API(批量 sequence 更新)
- [ ] AI 合规检查 API(调 LLM 做敏感词+内容审核)
**完成标准**: 某集的全部分镜可一键 AI 生成,然后用户可手动编辑每个镜头。
### M5 · 视觉生成(第 14~16 周)
- [ ] `FrameGenerateExecutor`(首尾帧,参考图注入)
- [ ] `VideoProvider` 接口 + `WanxVideoProvider`
- [ ] 备选: SeedanceProvider / KlingProvider
- [ ] `VideoGenerateExecutor`(异步提交 + 轮询)
- [ ] 任务心跳机制上线
- [ ] 失败重试 + Provider fallback
- [ ] 前端 StoryboardWorkspace 右侧预览接入
- [ ] 前端 VideoGeneration 页面接入(分镜网格状态)
**完成标准**: 选中一个分镜,点"生成视频",10 分钟内能看到 5~8 秒短视频,参考图约束生效(角色相对一致)。
### M6 · 合成与导出(第 17~18 周)
- [ ] FFmpeg 调度封装(`FfmpegService`)
- [ ] `AssembleExecutor`:拼接 + 转场 + 字幕 + BGM
- [ ] TTS 接入(Qwen-TTS):`VoiceTtsExecutor`
- [ ] 全集合成 API + 下载链接签名
- [ ] 水印烧录(全局设置)
**完成标准**: 一集的所有分镜生成完成后,点"合成",得到一个带字幕、配音、BGM 的完整 MP4。
### M7 · 计费与 SaaS(第 19~21 周)
- [ ] `user_balances` / `balance_freezes` / `balance_transactions` 三表上线
- [ ] `BillingService`(prepare/settle/rollback)集成到所有任务
- [ ] BILLING_MODE=STRICT 切换
- [ ] 订阅表 + 套餐定义
- [ ] 支付宝 + 微信支付接入(Webhook)
- [ ] Stripe(海外)
- [ ] 充值页面 + 订阅页面
- [ ] `/usage/*` 统计 API
- [ ] 前端 UsageManagement 接入
- [ ] 定时任务:过期冻结清理、订阅续费
**完成标准**: 用户能充值、消费会扣积分、余额不足被拦截、流水可查。
### M8 · 团队协作与资产库(第 22~24 周)
- [ ] 项目成员邀请 + RBAC 4 角色
- [ ] 企业员工/小组(`/team` 页)
- [ ] 资产库:个人 + 团队(`global_assets` 表)
- [ ] 资产收藏、复用到项目
- [ ] 通知系统(`notifications` 表 + WebPush 可选)
- [ ] 审计日志(`audit_logs` + `@Audited` 切面)
- [ ] 前端 TeamManagement / AssetsManagement 接入
- [ ] 提示词模板的项目级覆盖 UI
**完成标准**: 团队成员可被邀请到项目,权限隔离正确,资产能跨项目复用。
### M9 · 上线与优化(第 25~26 周)
- [ ] 压测(JMeter/k6)
- [ ] 性能优化(慢 SQL、N+1、缓存)
- [ ] Prometheus + Grafana 监控面板
- [ ] SkyWalking 链路追踪
- [ ] 告警接入钉钉
- [ ] K8s Helm chart
- [ ] 灰度发布方案
- [ ] 运营后台基础(用户/订单查询)
- [ ] 用户文档、API 文档
- [ ] 上线!
## 3. 风险与缓解
| 风险 | 影响 | 缓解 |
|------|------|------|
| 通义万相/视频模型配额不足 | 阻塞 M5 | 并行接入 2 家 Provider,签商务合同 |
| 角色一致性效果不达标 | 用户体验差 | 四视图 + 强制命名 + 必要时引入 LoRA 微调 |
| 视频生成单价过高 | 毛利低 | 精确计费、短视频优先、非关键镜头降级 |
| FFmpeg 性能瓶颈 | 合成慢 | Worker 独立扩容 + GPU 编码 |
| 多租户数据泄漏 | 法律风险 | 代码卫士强制 Filter + 自动化测试 |
| Agent 幻觉导致低质产出 | 用户不满 | 监督层 + 用户最终审核 |
## 4. 后续(v2+)想做的事
- 移动端 App(React Native)
- 实时协作编辑分镜(CRDT / Yjs + WebSocket)
- 模型市场(用户上传 LoRA)
- 资产市场(付费分享角色/场景)
- AI 审核合规自动化(全量扫敏感内容)
- 多语言配音(英日韩)
- 真人参考视频驱动(动作迁移)
- 导出到剪映/PR 的 XML 工程
- 开放 API(面向开发者)
# 12 · 前端原型复盘与流程验证
++ /dev/null
# 12 · 前端原型复盘与流程验证
> 最近一次复盘日期: 2026-04-16
## 1. 端到端用户流程
```
Dashboard (/)
│ 点击"新建项目"
NewProject (/new-project)
│ 上传剧本(.txt/.doc/.docx/.pdf/.md) + 选纵横比/分辨率/风格
│ 点击"创建项目"
StyleSelection (/project/:id/style)
│ 选择6种艺术风格之一
│ 点击"继续"
OutlineGeneration (/project/:id/outline)
│ AI生成大纲(三幕结构+篇章) → 可编辑
│ 点击"继续"
EpisodeGeneration (/project/:id/episodes)
│ AI生成分集列表(80集网格)
│ 点击"继续生成角色设定"
CharacterGeneration (/project/:id/characters)
│ 角色卡 + 四视图 + 服装变体(独立角色形态)
│ 可切Tab到场景/道具
│ 点击"继续生成场景设定"
SceneGeneration (/project/:id/scenes)
│ 场景卡 + AI生成场景图
│ 点击"继续生成道具设定"
PropsGeneration (/project/:id/props)
│ 道具卡 + AI生成道具图
│ 点击"生成分镜"
StoryboardWorkspace (/project/:id/storyboard/:episodeId)
│ 左侧: 分集选择器
│ 中间: 分镜列表(分镜信息区 + 视频生成区)
│ 分镜信息区: 描述、出镜角色(增删+音色)、场景(更换)、道具(增删)
│ 视频生成区: 单一提示词编辑框 + 智能体选择器 + 积分预估
│ 右侧: 视频预览
│ 通过顶部导航Tab切换到视频页
VideoGeneration (/project/:id/video)
│ 视频播放器 + 分镜时间线 + 状态网格
│ 可返回分镜编辑
ProjectDetail (/project/:id)
│ 总览: 大纲/角色/分集/分镜/视频各Tab
```
## 2. 页面清单(17个)
| # | 页面 | 路由 | UI完整 | 流程衔接 | 备注 |
|---|---|---|---|---|---|
| 1 | Dashboard | `/` | ✅ | ✅→NewProject/ProjectDetail | 项目网格+统计+搜索 |
| 2 | NewProject | `/new-project` | ✅ | ✅→Style | 上传剧本+三选项 |
| 3 | StyleSelection | `/project/:id/style` | ✅ | ✅→Outline | 6种预设风格 |
| 4 | OutlineGeneration | `/project/:id/outline` | ✅ | ✅→Episodes | AI生成+可编辑 |
| 5 | EpisodeGeneration | `/project/:id/episodes` | ✅ | ✅→Characters | 分集网格 |
| 6 | CharacterGeneration | `/project/:id/characters` | ✅ | ✅→Scenes | 角色+服装变体 |
| 7 | SceneGeneration | `/project/:id/scenes` | ✅ | ✅→Props | 场景卡+AI生成 |
| 8 | PropsGeneration | `/project/:id/props` | ✅ | ✅→Storyboard | 道具卡+AI生成 |
| 9 | StoryboardWorkspace | `/project/:id/storyboard/:eid` | ✅ | ✅ TopNav | 分镜编辑+视频生成 |
| 10 | VideoGeneration | `/project/:id/video` | ✅ | ✅ 可返回 | 播放器+时间线 |
| 11 | ProjectDetail | `/project/:id` | ✅ | ✅ 多Tab | 项目总览 |
| 12 | ProjectSettings | `/project/:id/settings` | ✅ | ✅ | 团队成员+权限 |
| 13 | TeamManagement | `/team` | ✅ | ✅ | 企业团队管理 |
| 14 | UsageManagement | `/usage` | ✅ | ✅ | 用量统计+图表 |
| 15 | AssetsManagement | `/assets/*` | ✅ | ✅ | 个人/团队资产库 |
| 16 | ModelSettings | `/models` | ✅ | ✅ | 语言/图片/视频模型配置 |
| 17 | AIWatermarkSettings | `/settings` | ✅ | ✅ | 水印设置+预览 |
## 3. 设计文档对齐度
| 设计文档要求 | 原型实现 | 状态 |
|---|---|---|
| 单一提示词编辑框 | StoryboardWorkspace detailedDescription textarea | ✅ |
| 智能体选择器(通义万相/Seedance/Kling) | 底部工具栏 select | ✅ |
| 积分预估(按智能体+时长) | 生成按钮显示消耗积分 | ✅ |
| 出镜角色增删+音色选择 | 分镜信息区 characters + voice select | ✅ |
| 场景可更换 | 场景卡片 + "更换"按钮 | ✅ |
| 道具可增删 | 道具网格 + "添加"按钮 | ✅ |
| 服装变体机制 | CharacterGeneration 变体列表+创建弹窗 | ✅ |
| 角色变体格式("林若曦·白色晚礼服") | Storyboard 角色下拉已联动 | ✅ |
| 模型配置页(三类独立) | ModelSettings 语言/图片/视频 | ✅ |
| 文件上传支持 .md | NewProject accept 已添加 | ✅ |
| 模型名使用真实名称 | 全部替换为通义万相/Seedance/Kling | ✅ |
| 登录/注册页 | 缺失(M1阶段做) | ⏳ |
| 充值/订阅页 | 缺失(M7阶段做) | ⏳ |
## 4. 已修复问题
| 问题 | 修复内容 |
|---|---|
| PropsGeneration "生成分镜"按钮无导航 | 添加 navigate 跳转 storyboard/1 |
| NewProject 不支持 .md 格式 | accept 添加 .md |
| 多页面模型名使用假数据 | SceneGeneration/PropsGeneration/ProjectDetail 替换为真实模型名 |
| 角色页无服装变体机制 | 新增 CharacterVariant 数据结构+创建变体UI+管理交互 |
| 分镜页角色选择无变体联动 | 角色下拉改为"角色·变体"格式 |
| 缺少模型配置页 | 新增 ModelSettings 页(参考 AIComicBuilder) |
## 5. 技术债务(原型阶段可接受)
- 所有数据为 mock,无真实 API 调用(M0 接入)
- 拖拽排序仅引入 react-dnd 依赖,未实现拖拽逻辑
- 无表单验证提示(仅部分按钮 disabled)
- 无错误边界/Loading skeleton
- 状态管理为纯 useState,生产需迁移 Zustand + TanStack Query
- 无登录/注册页(M1)、无充值/订阅页(M7)
This diff is collapsed.
This diff is collapsed.
---
++ /dev/null
---
今天完成(M0)
- 16模块 Maven 多模块项目从零搭建完毕
- 核心模块:common / api / bootstrap,含统一响应体、全局异常、Swagger、请求ID过滤器
- Flyway 数据库迁移(tenants / users / tenant_members)
- docker-compose:MySQL / Redis / RabbitMQ
- Dockerfile 多阶段构建
- Git 初始化 + 2 个 commit
- 前端:TanStack Query + Zustand + Axios 骨架接入
- Swagger UI 验证通过,/ping 正常响应
---
---
M1 完成情况总结:
后端:
- yaoai-domain:Tenant / User / TenantMember 实体 + MyBatis-Plus Mapper
- yaoai-security:TenantContext(ThreadLocal)、Sa-Token 拦截器(保护除 /auth/**
以外的所有路由)、AuthService(注册/登录/登出/me)
- yaoai-api:AuthController(4 个端点)、更新 GlobalExceptionHandler(处理 401/403)
- yaoai-bootstrap:Redis + MyBatis-Plus + Sa-Token 配置写入 application.yml、DevDataInitializer(local profile
首次启动创建 admin@yaoai.com / Admin@123)
前端:
- src/lib/api/auth.ts:API 调用层
- src/hooks/useAuth.ts:TanStack Query mutations + Zustand store 联动
- src/app/pages/LoginPage.tsx / RegisterPage.tsx:登录/注册表单
- src/app/components/PrivateRoute.tsx:路由保护
- routes.tsx:添加 /login、/register,所有已有路由被 PrivateRoute 包裹
验证方式:
1. 启动 Docker:docker compose up -d
2. 运行(须用 Java 17):JAVA_HOME="D:/Program Files/Java/jdk-17" mvn package -pl yaoai-bootstrap -am -DskipTests &&
java -jar yaoai-bootstrap/target/yaoai-bootstrap-0.1.0-SNAPSHOT.jar
3. 测试:POST /auth/login {"email":"admin@yaoai.com","password":"Admin@123"} → 收到 token
4. 用 token 访问 GET /auth/me → 返回用户信息
5. 不带 token 访问 /ping → 200(开放);不带 token 访问其他接口 → 401
This diff is collapsed.
This diff is collapsed.
node_modules
dist
.env
.env.local
npm-debug.log
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
ARG VITE_API_BASE_URL=/api
ENV VITE_API_BASE_URL=${VITE_API_BASE_URL}
RUN npm run build
FROM nginx:1.27-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location /api/ {
proxy_pass http://backend:8080/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
try_files $uri $uri/ /index.html;
}
}
......@@ -15,6 +15,7 @@ import {
Sliders,
Clapperboard,
Video,
Bot,
Cpu,
LogOut,
User,
......@@ -70,6 +71,7 @@ export function Layout() {
// Determine active section based on current path
const getActiveSection = () => {
const path = location.pathname;
if (path.includes("/agent")) return "agent";
if (path.includes("/storyboard")) return "storyboard";
if (path.includes("/video")) return "video";
if (
......@@ -93,6 +95,7 @@ export function Layout() {
const activeSection = getActiveSection();
const projectSections = [
{ id: "agent", label: "Agent制作", icon: Bot, path: `/project/${projectId}/agent` },
{ id: "script", label: "剧本", icon: FileText, path: `/project/${projectId}` },
{
id: "settings",
......
This diff is collapsed.
......@@ -21,6 +21,7 @@ import { TeamManagement } from "./pages/TeamManagement";
import { UsageManagement } from "./pages/UsageManagement";
import { AIWatermarkSettings } from "./pages/AIWatermarkSettings";
import { ModelSettings } from "./pages/ModelSettings";
import { AgentStudio } from "./pages/AgentStudio";
export const router = createBrowserRouter([
{ path: "/login", element: <LoginPage /> },
......@@ -50,6 +51,7 @@ export const router = createBrowserRouter([
{ path: "project/:projectId/storyboard/:episodeId?", element: <StoryboardWorkspace /> },
{ path: "project/:projectId/video", element: <VideoGeneration /> },
{ path: "project/:projectId/settings", element: <ProjectSettings /> },
{ path: "project/:projectId/agent", element: <AgentStudio /> },
],
},
]);
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useEffect, useRef, useState } from "react";
import { agentApi, type AgentRunDTO, type AgentStepDTO } from "../lib/api/agent";
const runKey = (projectId: string) => ["agent-run", projectId];
export function useLatestAgentRun(projectId: string) {
return useQuery({
queryKey: runKey(projectId),
queryFn: () => agentApi.getLatestRun(projectId),
enabled: !!projectId,
refetchInterval: false,
});
}
export function useStartAgentRun(projectId: string) {
const qc = useQueryClient();
return useMutation({
mutationFn: (goal: string) => agentApi.startRun(projectId, goal),
onSuccess: (run) => {
qc.setQueryData(runKey(projectId), run);
},
});
}
export function usePauseAgentRun(projectId: string) {
const qc = useQueryClient();
return useMutation({
mutationFn: (runId: number) => agentApi.pauseRun(runId),
onSuccess: () => qc.invalidateQueries({ queryKey: runKey(projectId) }),
});
}
export function useResumeAgentRun(projectId: string) {
const qc = useQueryClient();
return useMutation({
mutationFn: (runId: number) => agentApi.resumeRun(runId),
onSuccess: () => qc.invalidateQueries({ queryKey: runKey(projectId) }),
});
}
export interface SseLog {
id: number;
time: string;
level: "info" | "warn" | "error" | "agent";
message: string;
}
interface SseEvent {
type: string;
data: Record<string, unknown>;
}
/** 连接 SSE 并实时更新 run 状态。当 runId 为 null 时不连接 */
export function useAgentSse(runId: number | null, projectId: string) {
const qc = useQueryClient();
const [logs, setLogs] = useState<SseLog[]>([]);
const logIdRef = useRef(0);
useEffect(() => {
// 从 localStorage 直接取原始 token(不带 Bearer 前缀)
const token = localStorage.getItem("yaoai_token");
if (!runId || !token) return;
const BASE = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8080";
const url = `${BASE}/agent/runs/${runId}/events?Authorization=${encodeURIComponent(token)}`;
const es = new EventSource(url);
const addLog = (level: SseLog["level"], message: string) => {
setLogs((prev) => [
...prev,
{
id: ++logIdRef.current,
time: new Date().toLocaleTimeString("zh-CN", { hour12: false }),
level,
message,
},
]);
};
const refreshRun = () => {
qc.invalidateQueries({ queryKey: runKey(projectId) });
};
es.onmessage = (e) => {
try {
const evt: SseEvent = JSON.parse(e.data);
const { type, data } = evt;
if (type === "log") {
addLog((data.level as SseLog["level"]) || "info", data.message as string);
} else if (type === "step.start") {
addLog("info", `步骤开始:${data.stepKey}`);
refreshRun();
} else if (type === "step.done") {
addLog("agent", `步骤完成:${data.stepKey}${data.durationMs}ms)`);
refreshRun();
} else if (type === "step.failed") {
addLog("error", `步骤失败:${data.stepKey}${data.error}`);
refreshRun();
} else if (type === "run.done") {
addLog("agent", "制作完成!");
refreshRun();
es.close();
} else if (type === "run.failed") {
addLog("error", `制作失败:${data.error}`);
refreshRun();
es.close();
} else if (type === "run.paused") {
addLog("info", "制作已暂停");
refreshRun();
} else if (type === "run.resumed") {
addLog("info", "制作已继续");
refreshRun();
}
} catch {
// ignore parse errors
}
};
let errCount = 0;
es.onerror = () => {
errCount++;
if (errCount === 1) addLog("warn", "SSE 连接断开,尝试重连...");
// 如果连续出错说明鉴权失败或服务不可用,停止重连
if (errCount >= 3) {
addLog("error", "SSE 无法连接,请刷新页面重试");
es.close();
}
};
return () => es.close();
// token 每次从 localStorage 读取,不需要作为依赖
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [runId, projectId]);
return { logs };
}
import { apiClient } from "./client";
export interface AgentStepDTO {
id: number;
stepKey: string;
stepName: string;
agentType: "DECISION" | "EXECUTOR" | "SUPERVISION";
status: "PENDING" | "RUNNING" | "DONE" | "FAILED" | "SKIPPED";
score?: "A" | "B" | "C" | "D";
scoreDetail?: string;
output?: string;
durationMs?: number;
startedAt?: string;
doneAt?: string;
seq: number;
}
export interface AgentRunDTO {
id: number;
projectId: number;
goal: string;
status: "PENDING" | "RUNNING" | "PAUSED" | "DONE" | "FAILED";
createdAt: string;
steps: AgentStepDTO[];
}
export const agentApi = {
startRun: (projectId: string, goal: string) =>
apiClient
.post<{ data: AgentRunDTO }>(`/agent/projects/${projectId}/runs`, { goal })
.then((r) => r.data.data),
getLatestRun: (projectId: string) =>
apiClient
.get<{ data: AgentRunDTO | null }>(`/agent/projects/${projectId}/runs/latest`)
.then((r) => r.data.data ?? null),
getRun: (runId: number) =>
apiClient
.get<{ data: AgentRunDTO }>(`/agent/runs/${runId}`)
.then((r) => r.data.data),
pauseRun: (runId: number) =>
apiClient.post(`/agent/runs/${runId}/pause`),
resumeRun: (runId: number) =>
apiClient.post(`/agent/runs/${runId}/resume`),
};
import axios from "axios";
const BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8080";
const BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "/api";
export const apiClient = axios.create({
baseURL: BASE_URL,
......
This diff is collapsed.
services:
mysql:
image: mysql:8.0
container_name: yaoai-mysql
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root123}
MYSQL_DATABASE: ${MYSQL_DATABASE:-yaoai_comic}
MYSQL_USER: ${MYSQL_USER:-yaoai}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-yaoai123}
TZ: ${TZ:-Asia/Shanghai}
ports:
- "${MYSQL_PORT:-13306}:3306"
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --default-time-zone=+08:00
volumes:
- mysql-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-u${MYSQL_USER:-yaoai}", "-p${MYSQL_PASSWORD:-yaoai123}"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
restart: unless-stopped
redis:
image: redis:7.2-alpine
container_name: yaoai-redis
ports:
- "${REDIS_PORT:-16379}:6379"
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 10
restart: unless-stopped
backend:
build:
context: ./yaoai-comic-studio
dockerfile: Dockerfile
container_name: yaoai-backend
environment:
TZ: ${TZ:-Asia/Shanghai}
SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-prod}
SERVER_PORT: 8080
DB_URL: jdbc:mysql://mysql:3306/${MYSQL_DATABASE:-yaoai_comic}?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8
DB_USERNAME: ${MYSQL_USER:-yaoai}
DB_PASSWORD: ${MYSQL_PASSWORD:-yaoai123}
REDIS_HOST: redis
REDIS_PORT: 6379
VOLCENGINE_ARK_API_KEY: ${VOLCENGINE_ARK_API_KEY:-}
VOLCENGINE_ARK_BASE_URL: ${VOLCENGINE_ARK_BASE_URL:-https://ark.cn-beijing.volces.com/api/v3}
VOLCENGINE_ARK_TEXT_MODEL: ${VOLCENGINE_ARK_TEXT_MODEL:-doubao-seed-2-0-code-preview-260215}
VOLCENGINE_ARK_IMAGE_MODEL: ${VOLCENGINE_ARK_IMAGE_MODEL:-doubao-seedream-5-0-260128}
VOLCENGINE_ARK_VIDEO_MODEL: ${VOLCENGINE_ARK_VIDEO_MODEL:-doubao-seedance-2-0-fast-260128}
VOLCENGINE_TOS_ACCESS_KEY: ${VOLCENGINE_TOS_ACCESS_KEY:-}
VOLCENGINE_TOS_SECRET_KEY: ${VOLCENGINE_TOS_SECRET_KEY:-}
VOLCENGINE_TOS_ENDPOINT: ${VOLCENGINE_TOS_ENDPOINT:-https://tos-cn-beijing.volces.com}
VOLCENGINE_TOS_REGION: ${VOLCENGINE_TOS_REGION:-cn-beijing}
VOLCENGINE_TOS_BUCKET: ${VOLCENGINE_TOS_BUCKET:-}
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
ports:
- "${BACKEND_PORT:-8080}:8080"
restart: unless-stopped
frontend:
build:
context: ./doc/html
dockerfile: Dockerfile
args:
VITE_API_BASE_URL: ${VITE_API_BASE_URL:-/api}
container_name: yaoai-frontend
depends_on:
- backend
ports:
- "${FRONTEND_PORT:-3000}:80"
restart: unless-stopped
volumes:
mysql-data:
redis-data:
《重生之豪门千金》剧本大纲
第一集:重生
【场景:医院病房,夜晚】
林晓雨,28岁,睁开眼睛,发现自己躺在医院床上。
护士走进来:"小姐,你醒了!你昏迷了三天,家人都很担心你。"
林晓雨看向镜子,镜中是18岁的自己——她重生了。
【内心独白】
"前世我被闺蜜背叛,被未婚夫抛弃,最终落得人财两空。
这一世,我绝不会再让那些人得逞。"
第二集:归来
【场景:林氏集团大厅】
林晓雨走进父亲的公司,神态从容。
秘书:"林小姐,您今天怎么来公司了?"
林晓雨微笑:"我来帮我父亲解决一个麻烦。"
【场景:会议室】
林晓雨拿出一份文件,指出财务漏洞。
林父震惊:"你怎么知道这些?"
林晓雨:"直觉。"
第三集:对决
【场景:豪华餐厅】
前世闺蜜苏曼走来,皮笑肉不笑:"晓雨,好久不见。"
林晓雨淡然一笑:"是啊,好久不见。你最近和陈总走得很近?"
苏曼脸色一变。
林晓雨站起身:"这局棋,我们重新来过。"
.git
.idea
logs
target
**/target
**/.env
**/.env.local
**/application-local.yml
# ===== Server =====
SERVER_PORT=8080
SPRING_PROFILES_ACTIVE=local
# ===== Database =====
DB_URL=jdbc:mysql://localhost:13306/yaoai_comic?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8
DB_USERNAME=yaoai
DB_PASSWORD=yaoai123
# ===== Volcano Engine ARK (LLM / Video Generation) =====
VOLCENGINE_ARK_API_KEY=your-ark-api-key
VOLCENGINE_ARK_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
VOLCENGINE_ARK_MODEL=doubao-seedance-2-0-260128
# ===== Volcano Engine TOS (Object Storage) =====
VOLCENGINE_TOS_ACCESS_KEY=your-access-key
VOLCENGINE_TOS_SECRET_KEY=your-secret-key
VOLCENGINE_TOS_ENDPOINT=https://tos-cn-beijing.volces.com
VOLCENGINE_TOS_REGION=cn-beijing
VOLCENGINE_TOS_BUCKET=your-bucket-name
# ===== Redis =====
REDIS_HOST=localhost
REDIS_PORT=16379
# ===== RabbitMQ =====
RABBITMQ_HOST=localhost
RABBITMQ_PORT=5672
RABBITMQ_USERNAME=yaoai
RABBITMQ_PASSWORD=yaoai123
RABBITMQ_VHOST=yaoai
# Normalize line endings to LF on check-in, keep LF on checkout (cross-platform)
* text=auto eol=lf
# Force LF for these types regardless
*.java text eol=lf
*.xml text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.sql text eol=lf
*.ts text eol=lf
*.tsx text eol=lf
*.json text eol=lf
*.md text eol=lf
*.sh text eol=lf
# Binary — do not mangle
*.jar binary
*.png binary
*.jpg binary
*.gif binary
*.ico binary
# ===== Build output =====
target/
*.class
*.jar
*.war
*.ear
*.nar
# ===== Maven =====
.mvn/wrapper/maven-wrapper.jar
!.mvn/wrapper/maven-wrapper.properties
# ===== IDE =====
.idea/
*.iws
*.iml
*.ipr
.vscode/
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# ===== OS =====
.DS_Store
Thumbs.db
ehthumbs.db
# ===== Logs =====
logs/
*.log
*.log.gz
# ===== Spring Boot =====
spring-shell.log
# ===== Environment / Secrets =====
.env
.env.local
.env.*.local
application-local.yml
application-prod.yml
application-staging.yml
# ===== Docker =====
.docker/
# ===== Node / Frontend =====
node_modules/
dist/
.pnpm-store/
*.local
# ===== Test output =====
surefire-reports/
failsafe-reports/
coverage/
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar
distributionType=bin
# ===== Stage 1: Build =====
FROM eclipse-temurin:17-jdk-jammy AS builder
WORKDIR /workspace
# Cache Maven dependencies
COPY .mvn/ .mvn/
COPY mvnw pom.xml ./
COPY yaoai-common/pom.xml yaoai-common/
COPY yaoai-domain/pom.xml yaoai-domain/
COPY yaoai-security/pom.xml yaoai-security/
COPY yaoai-billing/pom.xml yaoai-billing/
COPY yaoai-storage/pom.xml yaoai-storage/
COPY yaoai-ai-core/pom.xml yaoai-ai-core/
COPY yaoai-ai-providers/pom.xml yaoai-ai-providers/
COPY yaoai-agent/pom.xml yaoai-agent/
COPY yaoai-pipeline/pom.xml yaoai-pipeline/
COPY yaoai-task/pom.xml yaoai-task/
COPY yaoai-media/pom.xml yaoai-media/
COPY yaoai-api/pom.xml yaoai-api/
COPY yaoai-admin/pom.xml yaoai-admin/
COPY yaoai-worker/pom.xml yaoai-worker/
COPY yaoai-bootstrap/pom.xml yaoai-bootstrap/
COPY yaoai-test/pom.xml yaoai-test/
RUN ./mvnw dependency:go-offline -B -q
# Build
COPY . .
RUN ./mvnw package -pl yaoai-bootstrap -am -DskipTests -B -q
# ===== Stage 2: Runtime =====
FROM eclipse-temurin:17-jre-jammy
LABEL maintainer="YaoAI Team <yaoke251@gmail.com>"
LABEL org.opencontainers.image.title="YaoAI Comic Studio"
LABEL org.opencontainers.image.version="0.1.0"
# Non-root user
RUN groupadd -r yaoai && useradd -r -g yaoai yaoai
WORKDIR /app
COPY --from=builder /workspace/yaoai-bootstrap/target/*.jar app.jar
RUN mkdir -p logs && chown -R yaoai:yaoai /app
USER yaoai
EXPOSE 8080
ENV JAVA_OPTS="-Xms256m -Xmx512m -XX:+UseG1GC -Djava.security.egd=file:/dev/./urandom"
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
@echo off
@REM -------------------------------------------------------
@REM YaoAI Comic Studio — Windows build helper
@REM Overrides JAVA_HOME to JDK 17, then delegates to mvn.
@REM -------------------------------------------------------
set "JAVA_HOME=D:\Program Files\Java\jdk-17"
set "PATH=%JAVA_HOME%\bin;%PATH%"
echo Using Java: %JAVA_HOME%
"%JAVA_HOME%\bin\java.exe" -version
mvn %*
version: "3.9"
services:
mysql:
image: mysql:8.0
container_name: yaoai-mysql
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: yaoai_comic
MYSQL_USER: yaoai
MYSQL_PASSWORD: yaoai123
TZ: Asia/Shanghai
ports:
- "13306:3306"
volumes:
- yaoai-mysql-data:/var/lib/mysql
command: >
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
--default-time-zone=+08:00
--innodb-buffer-pool-size=256M
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "yaoai", "-pyaoai123"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
restart: unless-stopped
redis:
image: redis/redis-stack:7.2.0-v11
container_name: yaoai-redis
ports:
- "16379:6379"
- "18001:8001"
volumes:
- yaoai-redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
restart: unless-stopped
rabbitmq:
image: rabbitmq:3.13-management
container_name: yaoai-rabbitmq
environment:
RABBITMQ_DEFAULT_USER: yaoai
RABBITMQ_DEFAULT_PASS: yaoai123
RABBITMQ_DEFAULT_VHOST: yaoai
ports:
- "5672:5672"
- "15672:15672"
volumes:
- yaoai-rabbitmq-data:/var/lib/rabbitmq
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "ping"]
interval: 15s
timeout: 10s
retries: 5
restart: unless-stopped
prometheus:
image: prom/prometheus:v2.51.2
container_name: yaoai-prometheus
ports:
- "19090:9090"
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- yaoai-prometheus-data:/prometheus
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
- "--storage.tsdb.retention.time=7d"
- "--web.enable-lifecycle"
restart: unless-stopped
grafana:
image: grafana/grafana:10.4.2
container_name: yaoai-grafana
ports:
- "13000:3000"
environment:
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: admin123
GF_USERS_ALLOW_SIGN_UP: "false"
volumes:
- yaoai-grafana-data:/var/lib/grafana
depends_on:
- prometheus
restart: unless-stopped
volumes:
yaoai-mysql-data:
yaoai-redis-data:
yaoai-rabbitmq-data:
yaoai-prometheus-data:
yaoai-grafana-data:
networks:
default:
name: yaoai-net
apiVersion: v2
name: yaoai-comic-studio
description: YaoAI Comic Studio - AI漫剧SaaS平台
type: application
version: 0.1.0
appVersion: "0.1.0"
keywords:
- yaoai
- comic
- saas
- ai
maintainers:
- name: YaoAI Team
{{/*
Expand the name of the chart.
*/}}
{{- define "yaoai.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "yaoai.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "yaoai.labels" -}}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app.kubernetes.io/name: {{ include "yaoai.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "yaoai.selectorLabels" -}}
app.kubernetes.io/name: {{ include "yaoai.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "yaoai.fullname" . }}
labels:
{{- include "yaoai.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "yaoai.selectorLabels" . | nindent 6 }}
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
template:
metadata:
labels:
{{- include "yaoai.selectorLabels" . | nindent 8 }}
annotations:
{{- toYaml .Values.podAnnotations | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
terminationGracePeriodSeconds: 60
containers:
- name: api
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: 8080
protocol: TCP
env:
{{- range $key, $val := .Values.env }}
- name: {{ $key }}
value: {{ $val | quote }}
{{- end }}
{{- range $key := keys .Values.secrets }}
- name: {{ $key }}
valueFrom:
secretKeyRef:
name: {{ include "yaoai.fullname" $ }}-secrets
key: {{ $key }}
{{- end }}
livenessProbe:
{{- toYaml .Values.livenessProbe | nindent 12 }}
readinessProbe:
{{- toYaml .Values.readinessProbe | nindent 12 }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "yaoai.fullname" . }}
labels:
{{- include "yaoai.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "yaoai.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "yaoai.fullname" . }}
labels:
{{- include "yaoai.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- toYaml .Values.ingress.tls | nindent 4 }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "yaoai.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}
{{- if .Values.podDisruptionBudget.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "yaoai.fullname" . }}
labels:
{{- include "yaoai.labels" . | nindent 4 }}
spec:
minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
selector:
matchLabels:
{{- include "yaoai.selectorLabels" . | nindent 6 }}
{{- end }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "yaoai.fullname" . }}-secrets
labels:
{{- include "yaoai.labels" . | nindent 4 }}
type: Opaque
stringData:
{{- range $key, $val := .Values.secrets }}
{{ $key }}: {{ $val | quote }}
{{- end }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "yaoai.fullname" . }}
labels:
{{- include "yaoai.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
selector:
{{- include "yaoai.selectorLabels" . | nindent 4 }}
ports:
- name: http
port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
replicaCount: 2
image:
repository: registry.cn-beijing.aliyuncs.com/yaoai/comic-studio
pullPolicy: IfNotPresent
tag: "latest"
imagePullSecrets: []
service:
type: ClusterIP
port: 8080
ingress:
enabled: true
className: nginx
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
hosts:
- host: api.yaoai.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: yaoai-tls
hosts:
- api.yaoai.com
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: 2000m
memory: 2Gi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 8
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: 80
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 15
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
env:
SPRING_PROFILES_ACTIVE: prod
SERVER_PORT: "8080"
BILLING_MODE: STRICT
# Secrets — set via --set or external secrets operator in prod
secrets:
DB_URL: ""
DB_USERNAME: ""
DB_PASSWORD: ""
REDIS_HOST: ""
REDIS_PORT: "6379"
VOLCENGINE_TOS_ACCESS_KEY: ""
VOLCENGINE_TOS_SECRET_KEY: ""
VOLCENGINE_TOS_BUCKET: ""
VOLCENGINE_ARK_API_KEY: ""
podAnnotations:
prometheus.io/scrape: "true"
prometheus.io/path: /actuator/prometheus
prometheus.io/port: "8080"
podDisruptionBudget:
enabled: true
minAvailable: 1
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
env: local
scrape_configs:
- job_name: yaoai-comic-studio
metrics_path: /actuator/prometheus
static_configs:
- targets:
- host.docker.internal:8080
relabel_configs:
- source_labels: [__address__]
target_label: instance
replacement: yaoai-api
- job_name: prometheus
static_configs:
- targets:
- localhost:9090
@REM ----------------------------------------------------------------------------
@REM Apache Maven Wrapper startup batch script, version 3.3.2
@REM ----------------------------------------------------------------------------
@echo off
@setlocal
@REM ==== START VALIDATION ====
if not "%JAVA_HOME%"=="" goto OkJHome
@REM Try common Java 17 locations on this machine
if exist "D:\Program Files\Java\jdk-17\bin\java.exe" (
set "JAVA_HOME=D:\Program Files\Java\jdk-17"
goto OkJHome
)
if exist "C:\Program Files\Java\jdk-17\bin\java.exe" (
set "JAVA_HOME=C:\Program Files\Java\jdk-17"
goto OkJHome
)
if exist "C:\Program Files\Eclipse Adoptium\jdk-17\bin\java.exe" (
set "JAVA_HOME=C:\Program Files\Eclipse Adoptium\jdk-17"
goto OkJHome
)
echo.
echo Error: JAVA_HOME is not set and Java 17 was not found in common locations.
echo Set JAVA_HOME to point to your Java 17 JDK installation.
echo.
exit /B 1
:OkJHome
if exist "%JAVA_HOME%\bin\java.exe" goto Init
echo.
echo Error: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
exit /B 1
:Init
set _JAVACMD=%JAVA_HOME%\bin\java.exe
set MAVEN_WRAPPER_JAR=%~dp0.mvn\wrapper\maven-wrapper.jar
set MAVEN_WRAPPER_PROPERTIES=%~dp0.mvn\wrapper\maven-wrapper.properties
@REM Download wrapper jar if missing
if not exist "%MAVEN_WRAPPER_JAR%" (
echo Downloading Maven Wrapper...
"%_JAVACMD%" -classpath "%MAVEN_WRAPPER_JAR%" "-Dmaven.multiModuleProjectDirectory=%~dp0" ^
org.apache.maven.wrapper.MavenWrapperMain %* 2>nul
if errorlevel 1 (
"%_JAVACMD%" ^
-Dmaven.multiModuleProjectDirectory="%~dp0" ^
-Dmaven.wrapper.launcher.url="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar" ^
org.apache.maven.wrapper.MavenWrapperBootstrap %*
if errorlevel 1 goto error
)
)
@REM Execute Maven via wrapper
"%_JAVACMD%" ^
-classpath "%MAVEN_WRAPPER_JAR%" ^
"-Dmaven.multiModuleProjectDirectory=%~dp0" ^
org.apache.maven.wrapper.MavenWrapperMain %*
if errorlevel 1 goto error
goto end
:error
set ERROR_CODE=1
:end
@endlocal & exit /B %ERROR_CODE%
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.6</version>
<relativePath/>
</parent>
<groupId>com.yaoai</groupId>
<artifactId>yaoai-comic-studio</artifactId>
<version>0.1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>YaoAI Comic Studio</name>
<description>AI 漫剧 SaaS 平台 — 剧本一键产出完整动画短剧视频</description>
<modules>
<module>yaoai-common</module>
<module>yaoai-domain</module>
<module>yaoai-security</module>
<module>yaoai-billing</module>
<module>yaoai-storage</module>
<module>yaoai-ai-core</module>
<module>yaoai-ai-providers</module>
<module>yaoai-agent</module>
<module>yaoai-pipeline</module>
<module>yaoai-task</module>
<module>yaoai-media</module>
<module>yaoai-api</module>
<module>yaoai-admin</module>
<module>yaoai-worker</module>
<module>yaoai-bootstrap</module>
<module>yaoai-test</module>
</modules>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- Library versions -->
<spring-ai.version>1.0.0</spring-ai.version>
<mybatis-plus.version>3.5.9</mybatis-plus.version>
<sa-token.version>1.39.0</sa-token.version>
<springdoc.version>2.6.0</springdoc.version>
<hutool.version>5.8.26</hutool.version>
<volcengine-tos.version>2.8.9</volcengine-tos.version>
<bucket4j.version>8.10.1</bucket4j.version>
<ulid-creator.version>5.1.0</ulid-creator.version>
<poi.version>5.3.0</poi.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- ===== Internal Modules ===== -->
<dependency>
<groupId>com.yaoai</groupId>
<artifactId>yaoai-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.yaoai</groupId>
<artifactId>yaoai-domain</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.yaoai</groupId>
<artifactId>yaoai-security</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.yaoai</groupId>
<artifactId>yaoai-billing</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.yaoai</groupId>
<artifactId>yaoai-storage</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.yaoai</groupId>
<artifactId>yaoai-ai-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.yaoai</groupId>
<artifactId>yaoai-ai-providers</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.yaoai</groupId>
<artifactId>yaoai-agent</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.yaoai</groupId>
<artifactId>yaoai-pipeline</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.yaoai</groupId>
<artifactId>yaoai-task</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.yaoai</groupId>
<artifactId>yaoai-media</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.yaoai</groupId>
<artifactId>yaoai-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.yaoai</groupId>
<artifactId>yaoai-admin</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.yaoai</groupId>
<artifactId>yaoai-worker</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.yaoai</groupId>
<artifactId>yaoai-bootstrap</artifactId>
<version>${project.version}</version>
</dependency>
<!-- ===== Spring AI BOM ===== -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- ===== MyBatis-Plus ===== -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-extension</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- ===== Sa-Token ===== -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>${sa-token.version}</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- ===== SpringDoc OpenAPI ===== -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<!-- ===== Hutool ===== -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- ===== Volcano Engine TOS (替代阿里云 OSS) ===== -->
<dependency>
<groupId>com.volcengine</groupId>
<artifactId>ve-tos-java-sdk</artifactId>
<version>${volcengine-tos.version}</version>
</dependency>
<!-- ===== Bucket4j (rate limiting) ===== -->
<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j-core</artifactId>
<version>${bucket4j.version}</version>
</dependency>
<!-- ===== ULID generator ===== -->
<dependency>
<groupId>com.github.f4b6a3</groupId>
<artifactId>ulid-creator</artifactId>
<version>${ulid-creator.version}</version>
</dependency>
<!-- ===== Apache POI (Word 文档解析) ===== -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>${poi.version}</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>${poi.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<!-- Common dependencies for ALL modules -->
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
@echo off
java -Dfile.encoding=UTF-8 -Dstdout.encoding=UTF-8 -jar yaoai-bootstrap\target\yaoai-bootstrap-0.1.0-SNAPSHOT.jar
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.yaoai</groupId>
<artifactId>yaoai-comic-studio</artifactId>
<version>0.1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>yaoai-admin</artifactId>
<name>YaoAI - Admin</name>
<description>运营后台 API:租户管理、积分充值、系统配置(M4 填充)</description>
<dependencies>
<dependency>
<groupId>com.yaoai</groupId>
<artifactId>yaoai-domain</artifactId>
</dependency>
<dependency>
<groupId>com.yaoai</groupId>
<artifactId>yaoai-billing</artifactId>
</dependency>
</dependencies>
</project>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.yaoai</groupId>
<artifactId>yaoai-comic-studio</artifactId>
<version>0.1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>yaoai-agent</artifactId>
<name>YaoAI - Agent</name>
<description>三层 Agent 框架:决策层/执行层/监督层,Spring AI @Tool 注解(M3 填充)</description>
<dependencies>
<dependency>
<groupId>com.yaoai</groupId>
<artifactId>yaoai-ai-core</artifactId>
</dependency>
<dependency>
<groupId>com.yaoai</groupId>
<artifactId>yaoai-domain</artifactId>
</dependency>
<dependency>
<groupId>com.yaoai</groupId>
<artifactId>yaoai-pipeline</artifactId>
</dependency>
<dependency>
<groupId>com.yaoai</groupId>
<artifactId>yaoai-billing</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
package com.yaoai.agent.service;
import com.yaoai.domain.entity.GraphRun;
import com.yaoai.domain.entity.GraphStep;
import java.util.List;
public interface AgentRunService {
/** 创建并异步启动一个 Agent 制作流程 */
GraphRun startRun(Long projectId, Long tenantId, String goal);
GraphRun getRun(Long runId, Long tenantId);
/** 获取项目最新的 run */
GraphRun getLatestRun(Long projectId, Long tenantId);
List<GraphStep> getSteps(Long runId, Long tenantId);
/** 暂停(仅标记,下一个步骤开始前检查) */
void pauseRun(Long runId, Long tenantId);
/** 继续(恢复标记,下一步正常执行) */
void resumeRun(Long runId, Long tenantId);
}
package com.yaoai.agent.sse;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
@Slf4j
@Component
@RequiredArgsConstructor
public class AgentSseManager {
private final ObjectMapper objectMapper;
private final Map<Long, List<SseEmitter>> emitters = new ConcurrentHashMap<>();
public SseEmitter subscribe(Long runId) {
SseEmitter emitter = new SseEmitter(300_000L);
List<SseEmitter> list = emitters.computeIfAbsent(runId, k -> new CopyOnWriteArrayList<>());
list.add(emitter);
emitter.onCompletion(() -> remove(runId, emitter));
emitter.onTimeout(() -> remove(runId, emitter));
emitter.onError(e -> remove(runId, emitter));
log.debug("SSE subscribed: runId={}, total={}", runId, list.size());
return emitter;
}
public void emit(Long runId, String type, Object data) {
List<SseEmitter> list = emitters.get(runId);
if (list == null || list.isEmpty()) return;
try {
String json = objectMapper.writeValueAsString(
Map.of("type", type, "data", data == null ? Map.of() : data)
);
list.removeIf(emitter -> {
try {
emitter.send(SseEmitter.event().name(type).data(json));
return false;
} catch (Exception e) {
log.debug("Removed broken SSE emitter for runId={}", runId);
return true;
}
});
} catch (Exception e) {
log.error("SSE emit failed: runId={}, type={}", runId, type, e);
}
}
public void emitLog(Long runId, String level, String message) {
emit(runId, "log", Map.of("level", level, "message", message,
"time", java.time.LocalTime.now().toString().substring(0, 8)));
}
private void remove(Long runId, SseEmitter emitter) {
List<SseEmitter> list = emitters.get(runId);
if (list != null) list.remove(emitter);
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.yaoai</groupId>
<artifactId>yaoai-comic-studio</artifactId>
<version>0.1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>yaoai-ai-core</artifactId>
<name>YaoAI - AI Core</name>
<description>AI 核心抽象:Prompt 模板、ChatClient 封装、工具调用接口(M2 填充)</description>
<dependencies>
<dependency>
<groupId>com.yaoai</groupId>
<artifactId>yaoai-common</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
</project>
package com.yaoai.ai.core.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ChatMessage {
/** system / user / assistant */
private String role;
private String content;
public static ChatMessage system(String content) {
return new ChatMessage("system", content);
}
public static ChatMessage user(String content) {
return new ChatMessage("user", content);
}
}
package com.yaoai.ai.core.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class ChatRequest {
private String model;
private List<ChatMessage> messages;
@Builder.Default
private double temperature = 0.7;
@JsonProperty("max_tokens")
@Builder.Default
private int maxTokens = 4096;
}
package com.yaoai.ai.core.model;
import lombok.Data;
import java.util.List;
/** OpenAI-compatible chat completions response */
@Data
public class ChatResponse {
private String id;
private List<Choice> choices;
private Usage usage;
@Data
public static class Choice {
private int index;
private ChatMessage message;
@com.fasterxml.jackson.annotation.JsonProperty("finish_reason")
private String finishReason;
}
@Data
public static class Usage {
@com.fasterxml.jackson.annotation.JsonProperty("prompt_tokens")
private int promptTokens;
@com.fasterxml.jackson.annotation.JsonProperty("completion_tokens")
private int completionTokens;
@com.fasterxml.jackson.annotation.JsonProperty("total_tokens")
private int totalTokens;
}
public String firstContent() {
if (choices == null || choices.isEmpty()) return "";
ChatMessage msg = choices.get(0).getMessage();
return msg != null ? msg.getContent() : "";
}
}
package com.yaoai.ai.core.service;
import com.yaoai.ai.core.model.ChatMessage;
import com.yaoai.ai.core.model.ChatRequest;
import java.util.List;
public interface LlmService {
/**
* 发起一次对话,返回模型回复文本
*/
String chat(ChatRequest request);
/**
* 快捷方式:system prompt + user 消息,使用默认模型
*/
default String chat(String systemPrompt, String userMessage) {
return chat(ChatRequest.builder()
.messages(List.of(
ChatMessage.system(systemPrompt),
ChatMessage.user(userMessage)))
.build());
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.yaoai</groupId>
<artifactId>yaoai-comic-studio</artifactId>
<version>0.1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>yaoai-ai-providers</artifactId>
<name>YaoAI - AI Providers</name>
<description>AI Provider 适配:火山方舟 ARK API(OpenAI 兼容)、视频生成 Seedance(M2 填充)</description>
<dependencies>
<dependency>
<groupId>com.yaoai</groupId>
<artifactId>yaoai-ai-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
package com.yaoai.ai.providers.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "volcengine.ark")
public class ArkProperties {
private String apiKey;
private String baseUrl = "https://ark.cn-beijing.volces.com/api/v3";
/** 文本生成模型(用于大纲、分集等),需在火山方舟控制台创建推理接入点 */
private String textModel = "doubao-pro-32k-240828";
/** 图片生成模型(Seedream text-to-image) */
private String imageModel = "doubao-seedream-5-0-260128";
/** 视频生成模型(Seedance image-to-video) */
private String videoModel = "doubao-seedance-2-0-fast-260128";
}
package com.yaoai.ai.providers.model;
import lombok.Data;
@Data
public class VideoTaskResult {
private String taskId;
/** queued / running / succeeded / failed */
private String status;
/** 生成完成后的视频 URL */
private String videoUrl;
private String errorMessage;
public boolean isCompleted() {
return "succeeded".equals(status) || "failed".equals(status);
}
public boolean isSucceeded() {
return "succeeded".equals(status);
}
public static VideoTaskResult queued(String taskId) {
VideoTaskResult r = new VideoTaskResult();
r.setTaskId(taskId);
r.setStatus("queued");
return r;
}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment