feat(release): migrate GeekAI v4.3.0 to open source

- Sync backend and frontend from GeekAI Plus v4.3.0

- Remove commercial License flows and update open-source deployment defaults

- Preserve Docker Compose deployment and bump image tags to v4.3.0

BREAKING CHANGE: commercial License configuration and related endpoints are removed
This commit is contained in:
RockYang
2026-08-11 14:51:20 +08:00
parent 37e024acea
commit 9ccff4efbc
219 changed files with 29212 additions and 10802 deletions
-510
View File
@@ -1,510 +0,0 @@
## 开发 3D 图片生成功能
对接 3D 图片生成接口,为当前系统添加 3D 模型生成功能,默认支持腾讯云和 Gitee 的图生 3D API 接口。
## 要求
1. 完成数据库设计,后端 API 设计,前端页面设计。
2. 完成前端功能页面以及后台管理页面,具体设计结构可以参考即梦 AI,在对应的模块建立独立的模块 。
3. 页面设计要精美,但是整体风格要跟整站风格一致。
4. 支持前端 3D 模型预览,支持 3D 模型下载。
## 腾讯云图生 3D API 接口文档
1. 提交任务: https://cloud.tencent.com/document/product/1804/120826
2. 查询任务: https://cloud.tencent.com/document/product/1804/120827
3. Golang SDK https://gitee.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/ai3d/v20250513/client.go 依赖已经安装到本地了,如果你无法读取远程文件,下面是文件主要内容:
client.go
```go
// Copyright (c) 2017-2025 Tencent. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package v20250513
import (
"context"
"errors"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
tchttp "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
)
const APIVersion = "2025-05-13"
type Client struct {
common.Client
}
// Deprecated
func NewClientWithSecretId(secretId, secretKey, region string) (client *Client, err error) {
cpf := profile.NewClientProfile()
client = &Client{}
client.Init(region).WithSecretId(secretId, secretKey).WithProfile(cpf)
return
}
func NewClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *Client, err error) {
client = &Client{}
client.Init(region).
WithCredential(credential).
WithProfile(clientProfile)
return
}
func NewQueryHunyuanTo3DJobRequest() (request *QueryHunyuanTo3DJobRequest) {
request = &QueryHunyuanTo3DJobRequest{
BaseRequest: &tchttp.BaseRequest{},
}
request.Init().WithApiInfo("ai3d", APIVersion, "QueryHunyuanTo3DJob")
return
}
func NewQueryHunyuanTo3DJobResponse() (response *QueryHunyuanTo3DJobResponse) {
response = &QueryHunyuanTo3DJobResponse{
BaseResponse: &tchttp.BaseResponse{},
}
return
}
// QueryHunyuanTo3DJob
// 混元生3D接口,基于混元大模型,根据输入的文本描述/图片智能生成3D。
//
// 默认提供1个并发,代表最多能同时处理1个已提交的任务,上一个任务处理完毕后,才能开始处理下一个任务。
func (c *Client) QueryHunyuanTo3DJob(request *QueryHunyuanTo3DJobRequest) (response *QueryHunyuanTo3DJobResponse, err error) {
return c.QueryHunyuanTo3DJobWithContext(context.Background(), request)
}
// QueryHunyuanTo3DJob
// 混元生3D接口,基于混元大模型,根据输入的文本描述/图片智能生成3D。
//
// 默认提供1个并发,代表最多能同时处理1个已提交的任务,上一个任务处理完毕后,才能开始处理下一个任务。
func (c *Client) QueryHunyuanTo3DJobWithContext(ctx context.Context, request *QueryHunyuanTo3DJobRequest) (response *QueryHunyuanTo3DJobResponse, err error) {
if request == nil {
request = NewQueryHunyuanTo3DJobRequest()
}
c.InitBaseRequest(&request.BaseRequest, "ai3d", APIVersion, "QueryHunyuanTo3DJob")
if c.GetCredential() == nil {
return nil, errors.New("QueryHunyuanTo3DJob require credential")
}
request.SetContext(ctx)
response = NewQueryHunyuanTo3DJobResponse()
err = c.Send(request, response)
return
}
func NewSubmitHunyuanTo3DJobRequest() (request *SubmitHunyuanTo3DJobRequest) {
request = &SubmitHunyuanTo3DJobRequest{
BaseRequest: &tchttp.BaseRequest{},
}
request.Init().WithApiInfo("ai3d", APIVersion, "SubmitHunyuanTo3DJob")
return
}
func NewSubmitHunyuanTo3DJobResponse() (response *SubmitHunyuanTo3DJobResponse) {
response = &SubmitHunyuanTo3DJobResponse{
BaseResponse: &tchttp.BaseResponse{},
}
return
}
// SubmitHunyuanTo3DJob
// 混元生3D接口,基于混元大模型,根据输入的文本描述/图片智能生成3D。
//
// 默认提供1个并发,代表最多能同时处理1个已提交的任务,上一个任务处理完毕后,才能开始处理下一个任务。
func (c *Client) SubmitHunyuanTo3DJob(request *SubmitHunyuanTo3DJobRequest) (response *SubmitHunyuanTo3DJobResponse, err error) {
return c.SubmitHunyuanTo3DJobWithContext(context.Background(), request)
}
// SubmitHunyuanTo3DJob
// 混元生3D接口,基于混元大模型,根据输入的文本描述/图片智能生成3D。
//
// 默认提供1个并发,代表最多能同时处理1个已提交的任务,上一个任务处理完毕后,才能开始处理下一个任务。
func (c *Client) SubmitHunyuanTo3DJobWithContext(ctx context.Context, request *SubmitHunyuanTo3DJobRequest) (response *SubmitHunyuanTo3DJobResponse, err error) {
if request == nil {
request = NewSubmitHunyuanTo3DJobRequest()
}
c.InitBaseRequest(&request.BaseRequest, "ai3d", APIVersion, "SubmitHunyuanTo3DJob")
if c.GetCredential() == nil {
return nil, errors.New("SubmitHunyuanTo3DJob require credential")
}
request.SetContext(ctx)
response = NewSubmitHunyuanTo3DJobResponse()
err = c.Send(request, response)
return
}
```
model.go
```go
// Copyright (c) 2017-2025 Tencent. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package v20250513
import (
tcerr "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors"
tchttp "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/json"
)
type File3D struct {
// 文件格式
Type *string `json:"Type,omitnil,omitempty" name:"Type"`
// 文件的Url(有效期24小时)
Url *string `json:"Url,omitnil,omitempty" name:"Url"`
// 预览图片Url
PreviewImageUrl *string `json:"PreviewImageUrl,omitnil,omitempty" name:"PreviewImageUrl"`
}
// Predefined struct for user
type QueryHunyuanTo3DJobRequestParams struct {
// 任务ID。
JobId *string `json:"JobId,omitnil,omitempty" name:"JobId"`
}
type QueryHunyuanTo3DJobRequest struct {
*tchttp.BaseRequest
// 任务ID。
JobId *string `json:"JobId,omitnil,omitempty" name:"JobId"`
}
func (r *QueryHunyuanTo3DJobRequest) ToJsonString() string {
b, _ := json.Marshal(r)
return string(b)
}
// FromJsonString It is highly **NOT** recommended to use this function
// because it has no param check, nor strict type check
func (r *QueryHunyuanTo3DJobRequest) FromJsonString(s string) error {
f := make(map[string]interface{})
if err := json.Unmarshal([]byte(s), &f); err != nil {
return err
}
delete(f, "JobId")
if len(f) > 0 {
return tcerr.NewTencentCloudSDKError("ClientError.BuildRequestError", "QueryHunyuanTo3DJobRequest has unknown keys!", "")
}
return json.Unmarshal([]byte(s), &r)
}
// Predefined struct for user
type QueryHunyuanTo3DJobResponseParams struct {
// 任务状态。WAIT:等待中,RUN:执行中,FAIL:任务失败,DONE:任务成功
Status *string `json:"Status,omitnil,omitempty" name:"Status"`
// 错误码
ErrorCode *string `json:"ErrorCode,omitnil,omitempty" name:"ErrorCode"`
// 错误信息
ErrorMessage *string `json:"ErrorMessage,omitnil,omitempty" name:"ErrorMessage"`
// 生成的3D文件数组。
ResultFile3Ds []*File3D `json:"ResultFile3Ds,omitnil,omitempty" name:"ResultFile3Ds"`
// 唯一请求 ID,由服务端生成,每次请求都会返回(若请求因其他原因未能抵达服务端,则该次请求不会获得 RequestId)。定位问题时需要提供该次请求的 RequestId。
RequestId *string `json:"RequestId,omitnil,omitempty" name:"RequestId"`
}
type QueryHunyuanTo3DJobResponse struct {
*tchttp.BaseResponse
Response *QueryHunyuanTo3DJobResponseParams `json:"Response"`
}
func (r *QueryHunyuanTo3DJobResponse) ToJsonString() string {
b, _ := json.Marshal(r)
return string(b)
}
// FromJsonString It is highly **NOT** recommended to use this function
// because it has no param check, nor strict type check
func (r *QueryHunyuanTo3DJobResponse) FromJsonString(s string) error {
return json.Unmarshal([]byte(s), &r)
}
// Predefined struct for user
type SubmitHunyuanTo3DJobRequestParams struct {
// 文生3D,3D内容的描述,中文正向提示词。
// 最多支持1024个 utf-8 字符。
// 文生3D, image、image_url和 prompt必填其一,且prompt和image/image_url不能同时存在。
Prompt *string `json:"Prompt,omitnil,omitempty" name:"Prompt"`
// 输入图 Base64 数据。
// 大小:单边分辨率要求不小于128,不大于5000。大小不超过8m(base64编码后会大30%左右,建议实际输入图片不超过6m)
// 格式:jpgpngjpegwebp。
// ImageBase64、ImageUrl和 Prompt必填其一,且Prompt和ImageBase64/ImageUrl不能同时存在。
ImageBase64 *string `json:"ImageBase64,omitnil,omitempty" name:"ImageBase64"`
// 输入图Url。
// 大小:单边分辨率要求不小于128,不大于5000。大小不超过8m(base64编码后会大30%左右,建议实际输入图片不超过6m)
// 格式:jpgpngjpegwebp。
// ImageBase64/ImageUrl和 Prompt必填其一,且Prompt和ImageBase64/ImageUrl不能同时存在。
ImageUrl *string `json:"ImageUrl,omitnil,omitempty" name:"ImageUrl"`
// 多视角的模型图片,视角参考值:
// left:左视图;
// right:右视图;
// back:后视图;
//
// 每个视角仅限制一张图片。
// ●图片大小限制:编码后大小不可超过8M。
// ●图片分辨率限制:单边分辨率小于5000且大于128。
// ●支持图片格式:支持jpg或png
MultiViewImages []*ViewImage `json:"MultiViewImages,omitnil,omitempty" name:"MultiViewImages"`
// 生成模型的格式,仅限制生成一种格式。
// 生成模型文件组默认返回obj格式。
// 可选值:OBJGLBSTLUSDZFBXMP4。
ResultFormat *string `json:"ResultFormat,omitnil,omitempty" name:"ResultFormat"`
// 是否开启 PBR材质生成,默认 false。
EnablePBR *bool `json:"EnablePBR,omitnil,omitempty" name:"EnablePBR"`
}
type SubmitHunyuanTo3DJobRequest struct {
*tchttp.BaseRequest
// 文生3D,3D内容的描述,中文正向提示词。
// 最多支持1024个 utf-8 字符。
// 文生3D, image、image_url和 prompt必填其一,且prompt和image/image_url不能同时存在。
Prompt *string `json:"Prompt,omitnil,omitempty" name:"Prompt"`
// 输入图 Base64 数据。
// 大小:单边分辨率要求不小于128,不大于5000。大小不超过8m(base64编码后会大30%左右,建议实际输入图片不超过6m)
// 格式:jpgpngjpegwebp。
// ImageBase64、ImageUrl和 Prompt必填其一,且Prompt和ImageBase64/ImageUrl不能同时存在。
ImageBase64 *string `json:"ImageBase64,omitnil,omitempty" name:"ImageBase64"`
// 输入图Url。
// 大小:单边分辨率要求不小于128,不大于5000。大小不超过8m(base64编码后会大30%左右,建议实际输入图片不超过6m)
// 格式:jpgpngjpegwebp。
// ImageBase64/ImageUrl和 Prompt必填其一,且Prompt和ImageBase64/ImageUrl不能同时存在。
ImageUrl *string `json:"ImageUrl,omitnil,omitempty" name:"ImageUrl"`
// 多视角的模型图片,视角参考值:
// left:左视图;
// right:右视图;
// back:后视图;
//
// 每个视角仅限制一张图片。
// ●图片大小限制:编码后大小不可超过8M。
// ●图片分辨率限制:单边分辨率小于5000且大于128。
// ●支持图片格式:支持jpg或png
MultiViewImages []*ViewImage `json:"MultiViewImages,omitnil,omitempty" name:"MultiViewImages"`
// 生成模型的格式,仅限制生成一种格式。
// 生成模型文件组默认返回obj格式。
// 可选值:OBJGLBSTLUSDZFBXMP4。
ResultFormat *string `json:"ResultFormat,omitnil,omitempty" name:"ResultFormat"`
// 是否开启 PBR材质生成,默认 false。
EnablePBR *bool `json:"EnablePBR,omitnil,omitempty" name:"EnablePBR"`
}
func (r *SubmitHunyuanTo3DJobRequest) ToJsonString() string {
b, _ := json.Marshal(r)
return string(b)
}
// FromJsonString It is highly **NOT** recommended to use this function
// because it has no param check, nor strict type check
func (r *SubmitHunyuanTo3DJobRequest) FromJsonString(s string) error {
f := make(map[string]interface{})
if err := json.Unmarshal([]byte(s), &f); err != nil {
return err
}
delete(f, "Prompt")
delete(f, "ImageBase64")
delete(f, "ImageUrl")
delete(f, "MultiViewImages")
delete(f, "ResultFormat")
delete(f, "EnablePBR")
if len(f) > 0 {
return tcerr.NewTencentCloudSDKError("ClientError.BuildRequestError", "SubmitHunyuanTo3DJobRequest has unknown keys!", "")
}
return json.Unmarshal([]byte(s), &r)
}
// Predefined struct for user
type SubmitHunyuanTo3DJobResponseParams struct {
// 任务ID(有效期24小时)
JobId *string `json:"JobId,omitnil,omitempty" name:"JobId"`
// 唯一请求 ID,由服务端生成,每次请求都会返回(若请求因其他原因未能抵达服务端,则该次请求不会获得 RequestId)。定位问题时需要提供该次请求的 RequestId。
RequestId *string `json:"RequestId,omitnil,omitempty" name:"RequestId"`
}
type SubmitHunyuanTo3DJobResponse struct {
*tchttp.BaseResponse
Response *SubmitHunyuanTo3DJobResponseParams `json:"Response"`
}
func (r *SubmitHunyuanTo3DJobResponse) ToJsonString() string {
b, _ := json.Marshal(r)
return string(b)
}
// FromJsonString It is highly **NOT** recommended to use this function
// because it has no param check, nor strict type check
func (r *SubmitHunyuanTo3DJobResponse) FromJsonString(s string) error {
return json.Unmarshal([]byte(s), &r)
}
type ViewImage struct {
// 视角类型。
// 取值:back、left、right
ViewType *string `json:"ViewType,omitnil,omitempty" name:"ViewType"`
// 图片Url地址
ViewImageUrl *string `json:"ViewImageUrl,omitnil,omitempty" name:"ViewImageUrl"`
}
```
## Gitee 图生 3D API 接口文档
1. 提交任务: https://ai.gitee.com/docs/openapi/v1#tag/3d-%E7%94%9F%E6%88%90/post/async/image-to-3d
2. 查询任务:https://ai.gitee.com/docs/openapi/v1#tag/%E5%BC%82%E6%AD%A5%E4%BB%BB%E5%8A%A1/get/task/{task_id}/get
首先,你需要认真阅读上述接口文档,然后按照接口文档的示例代码实现腾讯云和 Gitee 的图生 3D API 接口,并且将接口集成到现有的系统中。
📋 功能概述
为现有的GeekAI-Plus系统添加3D图片生成功能,集成腾讯云和Gitee的图生3D API接口,包含完整的前后端功能和管理界面。
🗄️ 数据库设计
新增数据表:geekai_3d_jobs
- id (uint): 主键
- type (string): API类型 (tencent/gitee)
- user_id (uint): 用户ID
- power (int): 消耗算力
- task_id (string): 第三方任务ID
- img_url (string): 生成的3D模型文件地址
- model (string): 使用的3D模型类型
- status (string): 任务状态
- err_msg (string): 错误信息
- params (JSON): 任务参数(包含输入图片、提示词等所有参数)
- created_at (int64): 创建时间
🔧 后端API实现
路由结构:/api/3d/*
- POST /api/3d/generate - 创建3D生成任务
- GET /api/3d/jobs - 获取任务列表
- GET /api/3d/job/{id} - 获取任务详情
- GET /api/3d/download/{id} - 下载3D模型
- DELETE /api/3d/job/{id} - 删除任务
核心服务:
- service/3d/tencent_client.go - 腾讯云3D API客户端
- service/3d/gitee_client.go - Gitee 3D API客户端
- service/3d/service.go - 3D生成服务统一接口
- handler/3d_handler.go - HTTP处理器
- store/vo/3d_job.go - 数据模型
🎨 前端界面设计
用户端页面:/3d - 3D生成主页面
- 参考JiMeng.vue的设计风格和布局
- 使用CustomTab组件分离平台参数:
- Tab 1: "魔力方舟" (Gitee平台参数)
- Tab 2: "腾讯混元" (腾讯云平台参数)
- 每个Tab内包含:
- 图片上传区域
- 模型选择下拉框
- 算力消耗实时显示
- 平台特定的参数配置
- 生成按钮
- 任务列表和状态显示
- 集成3D模型预览器 (three.js)
- 模型下载功能
移动端适配:
- mobile/3dCreate.vue - 移动端3D生成页面
- 保持Tab切换功能
- 响应式设计,触控优化
🛠️ 管理后台
管理功能:
- admin/3d/3dJobs.vue - 任务管理列表
- admin/3d/3dSetting.vue - API配置页面
- 模型配置管理:
- 分平台配置API秘钥和模型列表
- 设置每个模型的算力消耗值
- API密钥和端点配置
🔌 API集成方案
腾讯云集成:
- 使用官方Golang SDK
- 支持异步任务提交和状态查询
Gitee集成:
- HTTP客户端实现
- 标准化响应处理
🎯 核心功能特性
- 平台切换:通过CustomTab在魔力方舟和腾讯混元间切换
- 模型选择:每个平台支持不同的3D模型
- 动态算力:切换模型时实时更新算力消耗显示
- 参数隔离:不同平台的参数配置完全分离
- 3D预览:集成Three.js实现模型预览
- 统一体验:保持与JiMeng.vue相似的交互风格
📱 用户体验
- JiMeng.vue风格的简洁界面
- Tab切换流畅的平台选择
- 模型选择时算力消耗实时更新
- 支持拖拽上传图片
- 实时任务状态显示
- 3D模型交互式预览
这个设计将创建一个与现有JiMeng功能风格一致的3D生成模块,通过Tab分离实现平台参数的清晰管理。
整个实现严格按照现有系统的代码规范和架构模式,与 JiMeng 等模块保持一致的用户体验!
+54
View File
@@ -0,0 +1,54 @@
# PPT 断点续生成(简化方案 · 修订版)
## 设计原则(按产品确认)
- **不新增数据库字段**,只复用现有 `geekai_ppt_jobs.slides`JSON)。
- 分镜结构已在 [`vo.PPTSlideData`](api/store/vo/ppt_job.go) 中体现:`theme/title/points/image_prompt` + `image_url`
- **大模型生成分镜后立刻整表写入 `slides`**:每条记录带完整分镜字段,**`image_url` 为空**,表示「待生图」。
- **列表「继续生成」条件**`status !== 'completed'` 即可展示(实现时需排除无意义的态:如从未写入分镜的异常数据;**`processing` 时按钮应禁用或显示加载**,避免与进行中的任务重复提交)。
- **继续生成**:读出 `slides`**有 `image_url` 则跳过**,无则取当条分镜调生图 API;**每成功一张就更新一次数据库**。
- **并发更新 `slides`**:多 goroutine 同时写同一任务的 `slides` 字段时,必须 **加锁**(推荐在 [`PptService`](api/service/ppt/ppt_service.go) 内 **`sync.Mutex``task_id` 维度**保护「读 JSON → 改一条 slide → 写回」的整段逻辑,避免后写覆盖先写)。
## 后端改动要点
### 1. `RunTask` 流程调整([`ppt_service.go`](api/service/ppt/ppt_service.go)
1. `GenerateSlides` 成功后,将 `[]slidePlan` 转为 `[]SlideData` / `vo.PPTSlides`**全部 `ImageURL` 置空**,一次性 `Save` / `Updates` 写入 `slides`,并 `SetTotalSlides(total)`
2. **`completed_slides` 语义**:表示「已有配图」的页数,等于 `slides``image_url` 非空的条数(或单独在更新时维护,与 `len(slides)` 区分)。
3. 抽取 **`runSlideImageGeneration`**:对「需要生图」的条目并发生图;每条成功后调用新的 **`applySlideImage(taskID, slideIndex, url)`**(内部持锁、按 index 更新对应元素的 `image_url`,再扣算力)。
### 2. 替换「仅 append」的 `UpdateProgress`
- 现有 `UpdateProgress`**append** 一条 slide,与「先写满占位再填图」冲突。
- 改为 **按 `slide_index` 原地更新** 指定项的 `image_url`(及必要时 `thumb`),并在持锁下 **整份序列化写回**
### 3. `ResumeTask` / `POST .../resume`
- 校验用户、任务存在;若 `status === processing` 建议直接拒绝或返回「任务进行中」。
-`status !== completed`:加载 `slides`,筛出 `image_url` 为空的项,**仅对这些项**走与 `RunTask` 相同的生图 + `applySlideImage`
- 全部非空后:`UpdateStatus(completed)`,清空或保留 `err_msg` 按产品定。
- 若某次仍失败:可 `MarkAsFailed` 保留已生成页(与现网一致)。
### 4. 失败路径
- 任一页生图失败时,仍可 `MarkAsFailed`,但 **分镜与已生成图均已在 `slides` 中**,续跑只需补空 `image_url`
## 前端([`PPTCreate.vue`](web/src/views/PPTCreate.vue)
- 列表与详情:当 **`status !== 'completed'`**(且建议 **`status !== 'processing'` 才可点**)显示「继续生成」,**`el-tooltip`** 文案:继续生成尚未完成的幻灯片,已有页面保持不变。
- 调用 `POST /api/v1/tasks/:id/resume`(路由注册方式同既有 export)。
## 与旧方案差异
- **不增加** `slide_plans` 等字段;**不重复存储**分镜,全部以 `slides` 中「无图占位」表达待生成状态。
## 验证
- 人为让第 N 页失败:列表出现继续生成,点击后仅补第 N 页及之后空图项。
- 并发压测:多页同时完成时 `slides` JSON 无丢失、无覆盖。
## 实施状态(已实现)
- 后端:`saveSlidesOutline` + `ApplySlideImage``sync.Map``task_id` 互斥)、`runSlideImageJobs``ResumeTask``POST /api/v1/tasks/:task_id/resume`
- 前端:`PPTCreate.vue` 列表/详情「继续生成」+ `el-tooltip``processing` 时禁用或 loading。
- **旧任务**:若 `slides` 条数小于 `total_slides`(无完整占位),`ResumeTask` 返回不可续跑。
+122
View File
@@ -0,0 +1,122 @@
# 📄 PRD: 智能演示文稿 (PPT) 生成后端 API (Agentic Workflow)
## 1. 项目概述 (Project Overview)
本项目旨在开发一个基于 Go (Gin 框架) 的后端 API 服务。该服务接收用户提供的「文本大纲」和「设计提示词(可选)」,通过大语言模型 (LLM) 进行结构化内容拆解与提示词工程,并利用并发调度图像生成模型 (如 Nano Banana 2) 生成幻灯片配图。
为保证系统稳定性和极佳的用户体验,系统采用**异步任务+前端定时轮询(3秒/次)**的架构。后端负责核心的并发控制、速率限制(Rate Limiting)和状态管理。
## 2. 技术栈约束 (Tech Stack)
- **语言/框架**: Go 1.21+, Gin Web Framework
- **并发控制**: `golang.org/x/sync/errgroup`
- **速率限制**: `golang.org/x/time/rate`
- **状态管理**: `sync.Map` (内存级,后续可扩展至 Redis)
- **数据交互**: JSON 格式,RESTful 风格
## 3. 核心 API 规范 (API Specification)
### 3.1 创建生成任务 (Create Task)
接收用户的原始文本和要求,立即返回任务 ID,不阻塞等待生成结果。
- **Endpoint**: `POST /api/v1/tasks/generate-slides`
- **Request Body**:
```json
{
"content": "用户的 Markdown 笔记或大纲内容 (必填)",
"prompt": "用户的附加设计要求,如:极简商务风、卡通手绘风 (选填)",
"language": "用户选择的语言,如:中文、英文,必填",
"pages": "用户选择的页数,如:5-20,必填"
}
```
- **Response Body**:
```json
{
"code": 200,
"message": "Task created successfully",
"data": {
"task_id": "uuid-v4-string",
"status": "pending"
}
}
```
### 3.2 查询任务进度 (Query Task Progress)
前端每隔 3 秒调用一次此接口,获取最新进度和已生成的幻灯片数据。
- **Endpoint**: `GET /api/v1/tasks/:task_id`
- **Response Body**:
```json
{
"code": 200,
"data": {
"task_id": "uuid-v4-string",
"status": "processing", // 状态枚举: pending, processing, completed, failed
"progress": {
"total_slides": 10,
"completed_slides": 3,
"percentage": 30
},
"slides": [
{
"slide_index": 1,
"theme": "早晨环节",
"title": "第一关:起床咕噜咕噜喝温水!",
"points": ["水量:300-500ml", "唤醒身体小怪兽"],
"image_url": "https://example.com/generated-image-url-1.png"
}
],
"error_message": "" // 仅在 status 为 failed 时返回具体错误
}
}
```
## 4. 核心工作流与状态机 (State Machine & Workflow)
### 4.1 任务初始化阶段
1. 接收 `POST` 请求,生成全局唯一的 `task_id`
2.`TaskManager` (基于 `sync.Map`) 中初始化任务实例,初始化状态 `status = pending`,并加上并发安全锁 (`sync.Mutex`)。
3. 启动独立的后台 Goroutine 接管耗时业务,主线程立即向客户端返回 `task_id`
### 4.2 后台异步执行阶段
1. **LLM 解析 (状态变更: processing)**
调用大语言模型解析传入的 `content``prompt`。必须注入以下 System Prompt 常量约束大模型输出:
> # Role
> 你是一位顶级的专业演示文稿(PPT)策划专家和 AI 图像提示词(Prompt)工程师。任务是根据用户提供的「内容大纲」或「设计要求」,生成一套逻辑清晰、视觉风格高度统一的幻灯片分镜数据。
> # Rules
> 1. 全局风格锚定:根据大纲推断或遵循用户要求的全局视觉风格。所有配图必须严格遵循此风格。
> 2. 结构化拆解:合理拆分为多张幻灯片,单页最多 3-4 个简短要点。
> 3. 视觉转译:为每页构思具体的画面描述 (image_prompt)。必须包含前缀 `[全局风格描述]`,必须包含后缀 `[画面左侧或右侧留出干净的纯色或虚化空间,用于排版文字。绝对不要在图片中生成任何英文字母、汉字或乱码。]`
> 4. 严格输出合法的纯 JSON 数组 `[{"slide_index": 1, "theme": "...", "title": "...", "points": ["..."], "image_prompt": "..."}]`,禁止使用 Markdown 标记。
2. **获取总数**
解析成功后,更新该任务的 `total_slides` 字段,`completed_slides` 保持为 0。
3. **并发绘图与限流调度 (核心挑战)**
- 启动 `errgroup.WithContext` 进行并发调度,设置 `g.SetLimit(3)` 控制最大并发线程。
- 引入令牌桶限流器 `rate.NewLimiter(rate.Every(1*time.Second), 1)` 控制 API 外部请求频率(QPS = 1)。
- 必须实现**指数退避重试 (Exponential Backoff)**:遇到 HTTP 429 错误时,等待 2s, 4s, 8s 后重试,最高重试 3 次。
- **进度更新**:每成功生成一张图片,必须获取 `Task` 实例的写锁,将 `completed_slides` 加 1,并将完整的 `SlideData` 追加到任务的 `slides` 数组中,以便前端下一秒轮询时能拿到最新切片。
### 4.3 任务收尾阶段
- 正常结束:`errgroup.Wait()` 返回 nil,状态变更为 `completed`
- 异常中断:发生超过最大重试次数的错误或解析失败,状态变更为 `failed`,并记录错误原因至 `error_message`
请按照以下顺序逐步实现 Go 后端代码,每完成一步请与我确认:
1. **核心模型与接口定义**:定义 `Task` 结构体(包含 sync.Mutex)、DTO 结构体以及 Gin 的路由组搭建。
2. **状态管理器实现**:实现一个基于 `sync.Map``TaskManager`,提供安全的 `CreateTask``GetTask``UpdateProgress``MarkAsFailed` 方法。
3. **限流与重试 HTTP 客户端**:实现 `callImageAPIWithRetry` 函数,内部封装 `rate.Limiter` 的等待逻辑和针对 429 状态码的指数退避重试逻辑(可先用 Mock 数据代替真实 HTTP 请求)。
4. **组装核心 Handler**:实现 `POST``GET` 控制器。在 `POST` 的后台 Goroutine 中,串联 LLM Mock 解析和 `errgroup` 并发画图流程,确保在循环中安全地调用 `TaskManager.UpdateProgress` 更新进度和结果数组。