Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
Y
yaoai-video
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
姚珂
yaoai-video
Commits
21e68a93
Commit
21e68a93
authored
Apr 26, 2026
by
yaoke.yk
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
分镜页面优化和运营后台-api
parent
1a494815
Hide whitespace changes
Inline
Side-by-side
Showing
35 changed files
with
1123 additions
and
59 deletions
+1123
-59
values.yaml
yaoai-comic-studio/k8s/values.yaml
+102
-0
pom.xml
yaoai-comic-studio/yaoai-admin/pom.xml
+28
-0
LlmService.java
...e/src/main/java/com/yaoai/ai/core/service/LlmService.java
+9
-0
SeedanceService.java
.../java/com/yaoai/ai/providers/service/SeedanceService.java
+28
-6
SeedreamService.java
.../java/com/yaoai/ai/providers/service/SeedreamService.java
+11
-0
ArkLlmService.java
...va/com/yaoai/ai/providers/service/impl/ArkLlmService.java
+15
-0
SeedanceServiceImpl.java
.../yaoai/ai/providers/service/impl/SeedanceServiceImpl.java
+36
-11
SeedreamServiceImpl.java
.../yaoai/ai/providers/service/impl/SeedreamServiceImpl.java
+10
-0
UsageController.java
...c/main/java/com/yaoai/api/controller/UsageController.java
+35
-0
VideoTaskController.java
...in/java/com/yaoai/api/controller/VideoTaskController.java
+73
-2
AiTaskDTO.java
...oai-api/src/main/java/com/yaoai/api/dto/ai/AiTaskDTO.java
+63
-0
StoryboardDTO.java
...api/src/main/java/com/yaoai/api/dto/ai/StoryboardDTO.java
+29
-0
BillingRecordDTO.java
...main/java/com/yaoai/api/dto/billing/BillingRecordDTO.java
+15
-0
pom.xml
yaoai-comic-studio/yaoai-billing/pom.xml
+4
-0
BillingService.java
...c/main/java/com/yaoai/billing/service/BillingService.java
+36
-6
BillingServiceImpl.java
...va/com/yaoai/billing/service/impl/BillingServiceImpl.java
+110
-14
pom.xml
yaoai-comic-studio/yaoai-bootstrap/pom.xml
+5
-0
application.yml
...studio/yaoai-bootstrap/src/main/resources/application.yml
+8
-1
pom.xml
yaoai-comic-studio/yaoai-domain/pom.xml
+4
-0
AiTask.java
...-domain/src/main/java/com/yaoai/domain/entity/AiTask.java
+19
-1
BillingRecord.java
.../src/main/java/com/yaoai/domain/entity/BillingRecord.java
+27
-0
Storyboard.java
...ain/src/main/java/com/yaoai/domain/entity/Storyboard.java
+6
-0
Tenant.java
...-domain/src/main/java/com/yaoai/domain/entity/Tenant.java
+8
-0
User.java
...ai-domain/src/main/java/com/yaoai/domain/entity/User.java
+2
-0
pom.xml
yaoai-comic-studio/yaoai-pipeline/pom.xml
+4
-0
VideoTaskAsyncProcessor.java
...ava/com/yaoai/pipeline/async/VideoTaskAsyncProcessor.java
+39
-7
ImageGenPipelineService.java
...a/com/yaoai/pipeline/service/ImageGenPipelineService.java
+2
-0
VideoTaskPipelineService.java
.../com/yaoai/pipeline/service/VideoTaskPipelineService.java
+10
-1
AssemblyPipelineServiceImpl.java
...ai/pipeline/service/impl/AssemblyPipelineServiceImpl.java
+14
-1
AssetGenPipelineServiceImpl.java
...ai/pipeline/service/impl/AssetGenPipelineServiceImpl.java
+106
-0
ImageGenPipelineServiceImpl.java
...ai/pipeline/service/impl/ImageGenPipelineServiceImpl.java
+34
-1
OutlinePipelineServiceImpl.java
...oai/pipeline/service/impl/OutlinePipelineServiceImpl.java
+53
-3
StoryboardPipelineServiceImpl.java
.../pipeline/service/impl/StoryboardPipelineServiceImpl.java
+55
-1
VideoTaskPipelineServiceImpl.java
...i/pipeline/service/impl/VideoTaskPipelineServiceImpl.java
+122
-4
SaTokenConfigure.java
...main/java/com/yaoai/security/config/SaTokenConfigure.java
+1
-0
No files found.
yaoai-comic-studio/k8s/values.yaml
View file @
21e68a93
...
...
@@ -83,3 +83,105 @@ podAnnotations:
podDisruptionBudget
:
enabled
:
true
minAvailable
:
1
adminServer
:
enabled
:
true
replicaCount
:
1
image
:
repository
:
registry.cn-beijing.aliyuncs.com/yaoai/comic-studio
pullPolicy
:
IfNotPresent
tag
:
"
latest"
service
:
type
:
ClusterIP
port
:
8081
ingress
:
enabled
:
true
className
:
nginx
annotations
:
nginx.ingress.kubernetes.io/proxy-body-size
:
"
50m"
nginx.ingress.kubernetes.io/proxy-read-timeout
:
"
300"
# Production should restrict this with your office/VPN CIDRs.
# nginx.ingress.kubernetes.io/whitelist-source-range: "10.0.0.0/8"
hosts
:
-
host
:
admin-api.yaoai.com
paths
:
-
path
:
/admin-api
pathType
:
Prefix
tls
:
-
secretName
:
yaoai-admin-tls
hosts
:
-
admin-api.yaoai.com
env
:
SPRING_PROFILES_ACTIVE
:
prod
SERVER_PORT
:
"
8081"
SPRING_FLYWAY_ENABLED
:
"
false"
BILLING_MODE
:
STRICT
resources
:
requests
:
cpu
:
250m
memory
:
512Mi
limits
:
cpu
:
1000m
memory
:
1Gi
livenessProbe
:
httpGet
:
path
:
/actuator/health/liveness
port
:
8081
initialDelaySeconds
:
60
periodSeconds
:
15
failureThreshold
:
3
readinessProbe
:
httpGet
:
path
:
/actuator/health/readiness
port
:
8081
initialDelaySeconds
:
30
periodSeconds
:
10
failureThreshold
:
3
adminWeb
:
enabled
:
true
replicaCount
:
1
image
:
repository
:
registry.cn-beijing.aliyuncs.com/yaoai/admin-web
pullPolicy
:
IfNotPresent
tag
:
"
latest"
service
:
type
:
ClusterIP
port
:
80
ingress
:
enabled
:
true
className
:
nginx
annotations
:
nginx.ingress.kubernetes.io/proxy-body-size
:
"
50m"
# Production should restrict this with your office/VPN CIDRs.
# nginx.ingress.kubernetes.io/whitelist-source-range: "10.0.0.0/8"
hosts
:
-
host
:
admin.yaoai.com
paths
:
-
path
:
/
pathType
:
Prefix
tls
:
-
secretName
:
yaoai-admin-web-tls
hosts
:
-
admin.yaoai.com
resources
:
requests
:
cpu
:
50m
memory
:
128Mi
limits
:
cpu
:
500m
memory
:
512Mi
livenessProbe
:
httpGet
:
path
:
/
port
:
80
initialDelaySeconds
:
10
periodSeconds
:
15
failureThreshold
:
3
readinessProbe
:
httpGet
:
path
:
/
port
:
80
initialDelaySeconds
:
5
periodSeconds
:
10
failureThreshold
:
3
yaoai-comic-studio/yaoai-admin/pom.xml
View file @
21e68a93
...
...
@@ -23,5 +23,33 @@
<groupId>
com.yaoai
</groupId>
<artifactId>
yaoai-billing
</artifactId>
</dependency>
<dependency>
<groupId>
com.yaoai
</groupId>
<artifactId>
yaoai-security
</artifactId>
</dependency>
<dependency>
<groupId>
com.yaoai
</groupId>
<artifactId>
yaoai-common
</artifactId>
</dependency>
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-web
</artifactId>
</dependency>
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-aop
</artifactId>
</dependency>
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-validation
</artifactId>
</dependency>
<dependency>
<groupId>
cn.dev33
</groupId>
<artifactId>
sa-token-spring-boot3-starter
</artifactId>
</dependency>
<dependency>
<groupId>
cn.dev33
</groupId>
<artifactId>
sa-token-redis-jackson
</artifactId>
</dependency>
</dependencies>
</project>
yaoai-comic-studio/yaoai-ai-core/src/main/java/com/yaoai/ai/core/service/LlmService.java
View file @
21e68a93
...
...
@@ -22,4 +22,13 @@ public interface LlmService {
ChatMessage
.
user
(
userMessage
)))
.
build
());
}
/** 默认文本模型 ID,用于计费明细 */
String
getModelId
();
/** 模型展示名称,用于计费明细 */
String
getModelName
();
/** Provider 标识,例:volcengine / qwen / openai */
String
getProvider
();
}
yaoai-comic-studio/yaoai-ai-providers/src/main/java/com/yaoai/ai/providers/service/SeedanceService.java
View file @
21e68a93
...
...
@@ -14,22 +14,44 @@ public interface SeedanceService {
* @return 外部任务 ID
*/
default
String
submitVideoTask
(
String
imageUrl
,
String
prompt
)
{
return
submitVideoTask
(
List
.
of
(
imageUrl
),
prompt
,
5
,
false
);
return
submitVideoTask
(
List
.
of
(
imageUrl
),
prompt
,
5
,
false
,
"16:9"
,
null
);
}
/**
* 提交图生视频任务(多图 + 参数)
* 提交图生视频任务(多图 + 参数,model 由实现兜底)
*/
default
String
submitVideoTask
(
List
<
String
>
imageUrls
,
String
prompt
,
int
durationSeconds
,
boolean
generateAudio
,
String
ratio
)
{
return
submitVideoTask
(
imageUrls
,
prompt
,
durationSeconds
,
generateAudio
,
ratio
,
null
);
}
/**
* 提交图生视频任务(多图 + 参数 + 显式模型)
*
* @param imageUrls 参考图 URL 列表,顺序对应 @图1、@图2...(至少 1 个)
* @param prompt 视频描述提示词,可含 @图N 引用
* @param imageUrls
参考图 URL 列表,顺序对应 @图1、@图2...(至少 1 个)
* @param prompt
视频描述提示词,可含 @图N 引用
* @param durationSeconds 视频时长(秒)
* @param generateAudio 是否生成配音
* @param generateAudio 是否生成配音
* @param ratio 视频宽高比,如 "16:9" / "9:16" / "1:1";空/非法值由实现兜底为 "16:9"
* @param model Seedance 模型 ID;空/null 时由实现回退到 ArkProperties.videoModel
* @return 外部任务 ID
*/
String
submitVideoTask
(
List
<
String
>
imageUrls
,
String
prompt
,
int
durationSeconds
,
boolean
generateAudio
);
String
submitVideoTask
(
List
<
String
>
imageUrls
,
String
prompt
,
int
durationSeconds
,
boolean
generateAudio
,
String
ratio
,
String
model
);
/**
* 查询任务状态
*/
VideoTaskResult
getTaskStatus
(
String
externalTaskId
);
/** 当前使用的 Seedance 模型 ID,用于计费明细 */
String
getModelId
();
/** 模型展示名 */
String
getModelName
();
/** Provider 标识 */
default
String
getProvider
()
{
return
"volcengine"
;
}
}
yaoai-comic-studio/yaoai-ai-providers/src/main/java/com/yaoai/ai/providers/service/SeedreamService.java
View file @
21e68a93
...
...
@@ -9,4 +9,15 @@ public interface SeedreamService {
* @return 生成图片的 URL
*/
String
generateImage
(
String
prompt
);
/** 当前使用的 Seedream 模型 ID,用于计费明细 */
String
getModelId
();
/** 模型展示名 */
String
getModelName
();
/** Provider 标识 */
default
String
getProvider
()
{
return
"volcengine"
;
}
}
yaoai-comic-studio/yaoai-ai-providers/src/main/java/com/yaoai/ai/providers/service/impl/ArkLlmService.java
View file @
21e68a93
...
...
@@ -54,4 +54,19 @@ public class ArkLlmService implements LlmService {
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
"AI 服务调用失败: "
+
e
.
getMessage
());
}
}
@Override
public
String
getModelId
()
{
return
properties
.
getTextModel
();
}
@Override
public
String
getModelName
()
{
return
"豆包 Pro 文本生成"
;
}
@Override
public
String
getProvider
()
{
return
"volcengine"
;
}
}
yaoai-comic-studio/yaoai-ai-providers/src/main/java/com/yaoai/ai/providers/service/impl/SeedanceServiceImpl.java
View file @
21e68a93
...
...
@@ -33,20 +33,34 @@ public class SeedanceServiceImpl implements SeedanceService {
.
build
();
}
private
static
final
java
.
util
.
Set
<
String
>
SUPPORTED_RATIOS
=
java
.
util
.
Set
.
of
(
"16:9"
,
"9:16"
,
"1:1"
);
@Override
public
String
submitVideoTask
(
List
<
String
>
imageUrls
,
String
prompt
,
int
durationSeconds
,
boolean
generateAudio
)
{
public
String
submitVideoTask
(
List
<
String
>
imageUrls
,
String
prompt
,
int
durationSeconds
,
boolean
generateAudio
,
String
ratio
,
String
model
)
{
if
(
imageUrls
==
null
||
imageUrls
.
isEmpty
())
{
throw
new
BizException
(
ErrorCode
.
INVALID_PARAM
,
"至少需要一张参考图"
);
}
List
<
Map
<
String
,
Object
>>
content
=
new
ArrayList
<>();
String
safeRatio
=
(
ratio
!=
null
&&
SUPPORTED_RATIOS
.
contains
(
ratio
))
?
ratio
:
"16:9"
;
int
safeDuration
=
durationSeconds
>
0
?
durationSeconds
:
5
;
String
safeModel
=
(
model
!=
null
&&
!
model
.
isBlank
())
?
model
:
properties
.
getVideoModel
();
// 1. 文本 prompt 放在最前(Seedance 2.0 约定),保留 @图N 原始标注
// Seedance 2.0 优先走 body 字段(generate_audio / ratio / duration / watermark),
// 文本后缀作冗余兜底(部分老接入点仍按 --rt/--dur/--wm 解析)。
StringBuilder
textBuilder
=
new
StringBuilder
();
if
(
prompt
!=
null
&&
!
prompt
.
isBlank
())
{
content
.
add
(
Map
.
of
(
"type"
,
"text"
,
"text"
,
prompt
));
textBuilder
.
append
(
prompt
.
trim
(
));
}
textBuilder
.
append
(
" --rt "
).
append
(
safeRatio
)
.
append
(
" --dur "
).
append
(
safeDuration
)
.
append
(
" --wm false"
);
String
finalText
=
textBuilder
.
toString
();
List
<
Map
<
String
,
Object
>>
content
=
new
ArrayList
<>();
content
.
add
(
Map
.
of
(
"type"
,
"text"
,
"text"
,
finalText
));
//
2.
按顺序添加参考图 → 对应 @图1、@图2、@图3...
// 按顺序添加参考图 → 对应 @图1、@图2、@图3...
for
(
String
url
:
imageUrls
)
{
content
.
add
(
Map
.
of
(
"type"
,
"image_url"
,
...
...
@@ -56,15 +70,16 @@ public class SeedanceServiceImpl implements SeedanceService {
}
Map
<
String
,
Object
>
body
=
new
java
.
util
.
LinkedHashMap
<>();
body
.
put
(
"model"
,
properties
.
getVideoModel
()
);
body
.
put
(
"model"
,
safeModel
);
body
.
put
(
"content"
,
content
);
body
.
put
(
"generate_audio"
,
generateAudio
);
body
.
put
(
"ratio"
,
"16:9"
);
body
.
put
(
"duration"
,
durationSeconds
>
0
?
durationSeconds
:
5
);
body
.
put
(
"ratio"
,
safeRatio
);
body
.
put
(
"duration"
,
safeDuration
);
body
.
put
(
"watermark"
,
false
);
body
.
put
(
"generate_audio"
,
generateAudio
);
log
.
info
(
"Seedance submit: model={}, images={}, duration={}s, audio={}"
,
properties
.
getVideoModel
(),
imageUrls
.
size
(),
durationSeconds
,
generateAudio
);
log
.
info
(
"Seedance submit: model={}, images={}, duration={}s, audio={}, ratio={}, finalTextSuffix='{}'"
,
safeModel
,
imageUrls
.
size
(),
safeDuration
,
generateAudio
,
safeRatio
,
finalText
.
length
()
>
200
?
finalText
.
substring
(
finalText
.
length
()
-
200
)
:
finalText
);
try
{
TaskSubmitResponse
resp
=
restClient
.
post
()
.
uri
(
"/contents/generations/tasks"
)
...
...
@@ -125,6 +140,16 @@ public class SeedanceServiceImpl implements SeedanceService {
}
}
@Override
public
String
getModelId
()
{
return
properties
.
getVideoModel
();
}
@Override
public
String
getModelName
()
{
return
"Seedance 图生视频"
;
}
// ---- helpers ----
/**
...
...
yaoai-comic-studio/yaoai-ai-providers/src/main/java/com/yaoai/ai/providers/service/impl/SeedreamServiceImpl.java
View file @
21e68a93
...
...
@@ -60,6 +60,16 @@ public class SeedreamServiceImpl implements SeedreamService {
}
}
@Override
public
String
getModelId
()
{
return
properties
.
getImageModel
();
}
@Override
public
String
getModelName
()
{
return
"Seedream 文生图"
;
}
// ---- internal response models ----
@Data
...
...
yaoai-comic-studio/yaoai-api/src/main/java/com/yaoai/api/controller/UsageController.java
View file @
21e68a93
...
...
@@ -2,10 +2,13 @@ package com.yaoai.api.controller;
import
com.yaoai.api.dto.billing.BalanceDTO
;
import
com.yaoai.api.dto.billing.BillingRecordDTO
;
import
com.yaoai.billing.service.BillingCostService
;
import
com.yaoai.billing.service.BillingService
;
import
com.yaoai.common.exception.BizException
;
import
com.yaoai.common.exception.ErrorCode
;
import
com.yaoai.common.response.ApiResponse
;
import
com.yaoai.domain.entity.Project
;
import
com.yaoai.domain.mapper.ProjectMapper
;
import
com.yaoai.security.context.TenantContext
;
import
io.swagger.v3.oas.annotations.Operation
;
import
io.swagger.v3.oas.annotations.tags.Tag
;
...
...
@@ -15,6 +18,10 @@ import org.springframework.web.bind.annotation.*;
import
java.math.BigDecimal
;
import
java.util.List
;
import
java.util.Map
;
import
java.util.Objects
;
import
java.util.Set
;
import
java.util.function.Function
;
import
java.util.stream.Collectors
;
@Tag
(
name
=
"用量统计"
)
@RestController
...
...
@@ -23,6 +30,8 @@ import java.util.Map;
public
class
UsageController
{
private
final
BillingService
billingService
;
private
final
BillingCostService
billingCostService
;
private
final
ProjectMapper
projectMapper
;
@Operation
(
summary
=
"查询积分余额"
)
@GetMapping
(
"/balance"
)
...
...
@@ -31,6 +40,12 @@ public class UsageController {
return
ApiResponse
.
success
(
BalanceDTO
.
from
(
billingService
.
getBalance
(
tenantId
)));
}
@Operation
(
summary
=
"查询计费价目(operation -> credits)"
)
@GetMapping
(
"/costs"
)
public
ApiResponse
<
Map
<
String
,
BigDecimal
>>
getCosts
()
{
return
ApiResponse
.
success
(
billingCostService
.
getCosts
());
}
@Operation
(
summary
=
"购买积分资源包(演示:免支付直接到账)"
)
@PostMapping
(
"/purchase"
)
public
ApiResponse
<
BalanceDTO
>
purchase
(
@RequestBody
Map
<
String
,
Object
>
body
)
{
...
...
@@ -55,7 +70,27 @@ public class UsageController {
Long
tenantId
=
TenantContext
.
get
();
List
<
BillingRecordDTO
>
list
=
billingService
.
getRecords
(
tenantId
,
limit
,
offset
)
.
stream
().
map
(
BillingRecordDTO:
:
from
).
toList
();
Map
<
Long
,
Project
>
projects
=
loadProjects
(
tenantId
,
list
);
list
.
forEach
(
record
->
{
Project
project
=
projects
.
get
(
record
.
getProjectId
());
if
(
project
!=
null
)
{
record
.
setProjectName
(
project
.
getName
());
}
});
long
total
=
billingService
.
countRecords
(
tenantId
);
return
ApiResponse
.
success
(
Map
.
of
(
"total"
,
total
,
"items"
,
list
));
}
private
Map
<
Long
,
Project
>
loadProjects
(
Long
tenantId
,
List
<
BillingRecordDTO
>
records
)
{
Set
<
Long
>
ids
=
records
.
stream
()
.
map
(
BillingRecordDTO:
:
getProjectId
)
.
filter
(
Objects:
:
nonNull
)
.
collect
(
Collectors
.
toSet
());
if
(
ids
.
isEmpty
())
{
return
Map
.
of
();
}
return
projectMapper
.
selectBatchIds
(
ids
).
stream
()
.
filter
(
project
->
tenantId
.
equals
(
project
.
getTenantId
()))
.
collect
(
Collectors
.
toMap
(
Project:
:
getId
,
Function
.
identity
()));
}
}
yaoai-comic-studio/yaoai-api/src/main/java/com/yaoai/api/controller/VideoTaskController.java
View file @
21e68a93
package
com
.
yaoai
.
api
.
controller
;
import
cn.dev33.satoken.stp.StpUtil
;
import
com.yaoai.api.dto.ai.AiTaskDTO
;
import
com.yaoai.api.dto.ai.StructuredVideoGenerateRequest
;
import
com.yaoai.api.dto.ai.UserPrompt
;
import
com.yaoai.common.exception.BizException
;
import
com.yaoai.common.exception.ErrorCode
;
import
com.yaoai.common.response.ApiResponse
;
import
com.yaoai.pipeline.dto.StructuredVideoSubmission
;
import
com.yaoai.pipeline.service.VideoTaskPipelineService
;
import
com.yaoai.security.context.TenantContext
;
import
io.swagger.v3.oas.annotations.Operation
;
import
io.swagger.v3.oas.annotations.tags.Tag
;
import
jakarta.validation.Valid
;
import
lombok.RequiredArgsConstructor
;
import
org.springframework.web.bind.annotation.*
;
import
java.util.Collections
;
import
java.util.LinkedHashMap
;
import
java.util.List
;
import
java.util.Map
;
import
java.util.stream.Collectors
;
...
...
@@ -24,7 +30,72 @@ public class VideoTaskController {
private
final
VideoTaskPipelineService
videoTaskPipelineService
;
@Operation
(
summary
=
"文生图→图生视频(异步,立即返回 pending 任务,后台执行图片生成)"
)
@Operation
(
summary
=
"结构化视频生成(推荐):左图(角色/场景/道具/风格)+ 右结构化提示词"
)
@PostMapping
(
"/generate-structured"
)
public
ApiResponse
<
AiTaskDTO
>
generateStructured
(
@PathVariable
Long
projectId
,
@RequestBody
@Valid
StructuredVideoGenerateRequest
req
)
{
UserPrompt
up
=
req
.
getUserPrompt
();
boolean
hasFreeform
=
req
.
getFreeformPrompt
()
!=
null
&&
!
req
.
getFreeformPrompt
().
isBlank
();
boolean
hasStructured
=
up
!=
null
&&
up
.
hasAnyContent
();
if
(!
hasFreeform
&&
!
hasStructured
)
{
throw
new
BizException
(
ErrorCode
.
INVALID_PARAM
,
"请填写分镜提示词"
);
}
List
<
String
>
characterKeys
=
req
.
getCharacterImageKeys
();
if
((
characterKeys
==
null
||
characterKeys
.
isEmpty
())
&&
(
req
.
getCharacterImageKey
()
==
null
||
req
.
getCharacterImageKey
().
isBlank
()))
{
throw
new
BizException
(
ErrorCode
.
INVALID_PARAM
,
"请至少选择一个出镜角色"
);
}
Long
tenantId
=
TenantContext
.
get
();
StructuredVideoSubmission
submission
=
StructuredVideoSubmission
.
builder
()
.
userId
(
StpUtil
.
getLoginIdAsLong
())
.
episodeId
(
req
.
getEpisodeId
())
.
storyboardId
(
req
.
getStoryboardId
())
.
characterImageKey
(
req
.
getCharacterImageKey
())
.
characterImageKeys
(
characterKeys
)
.
sceneImageKey
(
req
.
getSceneImageKey
())
.
propImageKeys
(
req
.
getPropImageKeys
())
.
styleImageKey
(
req
.
getStyleImageKey
())
.
characterAction
(
up
!=
null
?
up
.
getCharacterAction
()
:
null
)
.
sceneEvent
(
up
!=
null
?
up
.
getSceneEvent
()
:
null
)
.
cameraMovement
(
up
!=
null
?
up
.
getCameraMovement
()
:
null
)
.
lightingAtmosphere
(
up
!=
null
?
up
.
getLightingAtmosphere
()
:
null
)
.
videoStyle
(
up
!=
null
?
up
.
getVideoStyle
()
:
null
)
.
freeformPrompt
(
req
.
getFreeformPrompt
())
.
userPromptRaw
(
buildUserPromptMap
(
up
,
req
.
getFreeformPrompt
()))
.
durationSeconds
(
req
.
getDuration
())
.
ratio
(
req
.
getRatio
())
.
model
(
req
.
getModel
())
.
generateAudio
(
req
.
getGenerateAudio
())
.
build
();
return
ApiResponse
.
success
(
AiTaskDTO
.
from
(
videoTaskPipelineService
.
submitStructuredVideoTask
(
projectId
,
tenantId
,
submission
)
));
}
private
static
Map
<
String
,
String
>
buildUserPromptMap
(
UserPrompt
up
,
String
freeformPrompt
)
{
Map
<
String
,
String
>
m
=
new
LinkedHashMap
<>();
if
(
freeformPrompt
!=
null
&&
!
freeformPrompt
.
isBlank
())
{
m
.
put
(
"freeformPrompt"
,
freeformPrompt
);
}
if
(
up
!=
null
)
{
if
(
up
.
getCharacterAction
()
!=
null
)
m
.
put
(
"characterAction"
,
up
.
getCharacterAction
());
if
(
up
.
getSceneEvent
()
!=
null
)
m
.
put
(
"sceneEvent"
,
up
.
getSceneEvent
());
if
(
up
.
getCameraMovement
()
!=
null
)
m
.
put
(
"cameraMovement"
,
up
.
getCameraMovement
());
if
(
up
.
getLightingAtmosphere
()
!=
null
)
m
.
put
(
"lightingAtmosphere"
,
up
.
getLightingAtmosphere
());
if
(
up
.
getVideoStyle
()
!=
null
)
m
.
put
(
"videoStyle"
,
up
.
getVideoStyle
());
}
return
m
;
}
/**
* @deprecated 由 {@code POST /generate-structured} 替代;保留 1 个版本周期供前端回滚。
* 旧路径:prompt 内嵌 @图N + image_keys 顺序绑定。
*/
@Deprecated
@Operation
(
summary
=
"[DEPRECATED] 文生图→图生视频(异步,自然语言 @图N 方式)"
)
@PostMapping
(
"/generate"
)
public
ApiResponse
<
AiTaskDTO
>
generateFromText
(
@PathVariable
Long
projectId
,
@RequestBody
Map
<
String
,
Object
>
body
)
{
...
...
@@ -53,7 +124,7 @@ public class VideoTaskController {
Long
tenantId
=
TenantContext
.
get
();
return
ApiResponse
.
success
(
AiTaskDTO
.
from
(
videoTaskPipelineService
.
submitTextToVideoAsync
(
projectId
,
tenantId
,
episodeId
,
storyboardId
,
videoTaskPipelineService
.
submitTextToVideoAsync
(
projectId
,
tenantId
,
StpUtil
.
getLoginIdAsLong
(),
episodeId
,
storyboardId
,
prompt
,
videoPrompt
,
preImageKeys
,
duration
)
));
}
...
...
yaoai-comic-studio/yaoai-api/src/main/java/com/yaoai/api/dto/ai/AiTaskDTO.java
View file @
21e68a93
package
com
.
yaoai
.
api
.
dto
.
ai
;
import
com.fasterxml.jackson.core.type.TypeReference
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
import
com.yaoai.domain.entity.AiTask
;
import
lombok.Data
;
import
lombok.extern.slf4j.Slf4j
;
import
java.time.LocalDateTime
;
import
java.util.List
;
import
java.util.Map
;
@Slf4j
@Data
public
class
AiTaskDTO
{
private
static
final
ObjectMapper
OBJECT_MAPPER
=
new
ObjectMapper
();
private
static
final
TypeReference
<
List
<
String
>>
LIST_STRING_TYPE
=
new
TypeReference
<>()
{};
private
static
final
TypeReference
<
Map
<
String
,
String
>>
MAP_STRING_TYPE
=
new
TypeReference
<>()
{};
private
Long
id
;
private
Long
projectId
;
private
Long
episodeId
;
...
...
@@ -19,6 +29,22 @@ public class AiTaskDTO {
private
LocalDateTime
createdAt
;
private
LocalDateTime
updatedAt
;
/** 系统拼接后提交给 Seedance 的最终 Prompt(含 @图N) */
private
String
prompt
;
/** 结构化字段:仅 task_type=structured_video 时非空 */
private
String
characterImageKey
;
/** V16:多角色出镜列表(优先于 characterImageKey) */
private
List
<
String
>
characterImageKeys
;
private
String
sceneImageKey
;
private
List
<
String
>
propImageKeys
;
private
String
styleImageKey
;
/** 用户原始结构化输入 */
private
Map
<
String
,
String
>
userPrompt
;
private
Integer
videoDuration
;
private
String
videoRatio
;
private
Boolean
generateAudio
;
public
static
AiTaskDTO
from
(
AiTask
t
)
{
AiTaskDTO
dto
=
new
AiTaskDTO
();
dto
.
setId
(
t
.
getId
());
...
...
@@ -31,6 +57,43 @@ public class AiTaskDTO {
dto
.
setErrorMessage
(
t
.
getErrorMessage
());
dto
.
setCreatedAt
(
t
.
getCreatedAt
());
dto
.
setUpdatedAt
(
t
.
getUpdatedAt
());
dto
.
setPrompt
(
t
.
getPrompt
());
dto
.
setCharacterImageKey
(
t
.
getCharacterImageKey
());
dto
.
setSceneImageKey
(
t
.
getSceneImageKey
());
dto
.
setStyleImageKey
(
t
.
getStyleImageKey
());
dto
.
setVideoDuration
(
t
.
getVideoDuration
());
dto
.
setVideoRatio
(
t
.
getVideoRatio
());
dto
.
setGenerateAudio
(
t
.
getGenerateAudio
());
dto
.
setPropImageKeys
(
parseList
(
t
.
getPropImageKeys
(),
"propImageKeys"
,
t
.
getId
()));
dto
.
setCharacterImageKeys
(
parseList
(
t
.
getCharacterImageKeys
(),
"characterImageKeys"
,
t
.
getId
()));
dto
.
setUserPrompt
(
parseMap
(
t
.
getUserPrompt
(),
"userPrompt"
,
t
.
getId
()));
return
dto
;
}
private
static
List
<
String
>
parseList
(
String
json
,
String
fieldName
,
Long
taskId
)
{
if
(
json
==
null
||
json
.
isBlank
())
{
return
null
;
}
try
{
return
OBJECT_MAPPER
.
readValue
(
json
,
LIST_STRING_TYPE
);
}
catch
(
Exception
e
)
{
log
.
warn
(
"Failed to parse {} for task {}: {}"
,
fieldName
,
taskId
,
e
.
getMessage
());
return
null
;
}
}
private
static
Map
<
String
,
String
>
parseMap
(
String
json
,
String
fieldName
,
Long
taskId
)
{
if
(
json
==
null
||
json
.
isBlank
())
{
return
null
;
}
try
{
return
OBJECT_MAPPER
.
readValue
(
json
,
MAP_STRING_TYPE
);
}
catch
(
Exception
e
)
{
log
.
warn
(
"Failed to parse {} for task {}: {}"
,
fieldName
,
taskId
,
e
.
getMessage
());
return
null
;
}
}
}
yaoai-comic-studio/yaoai-api/src/main/java/com/yaoai/api/dto/ai/StoryboardDTO.java
View file @
21e68a93
package
com
.
yaoai
.
api
.
dto
.
ai
;
import
com.fasterxml.jackson.core.type.TypeReference
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
import
com.yaoai.domain.entity.Storyboard
;
import
lombok.Data
;
import
lombok.extern.slf4j.Slf4j
;
import
java.time.LocalDateTime
;
import
java.util.List
;
@Slf4j
@Data
public
class
StoryboardDTO
{
private
static
final
ObjectMapper
OBJECT_MAPPER
=
new
ObjectMapper
();
private
static
final
TypeReference
<
List
<
String
>>
LIST_STRING_TYPE
=
new
TypeReference
<>()
{};
private
Long
id
;
private
Long
episodeId
;
private
Integer
sequenceNum
;
...
...
@@ -26,6 +34,12 @@ public class StoryboardDTO {
private
LocalDateTime
createdAt
;
private
LocalDateTime
updatedAt
;
/** V16:分镜上落地的素材选择 */
private
List
<
String
>
characterImageKeys
;
private
String
sceneImageKey
;
private
List
<
String
>
propImageKeys
;
private
String
styleImageKey
;
public
static
StoryboardDTO
from
(
Storyboard
s
)
{
StoryboardDTO
dto
=
new
StoryboardDTO
();
dto
.
setId
(
s
.
getId
());
...
...
@@ -45,6 +59,21 @@ public class StoryboardDTO {
dto
.
setStatus
(
s
.
getStatus
());
dto
.
setCreatedAt
(
s
.
getCreatedAt
());
dto
.
setUpdatedAt
(
s
.
getUpdatedAt
());
dto
.
setCharacterImageKeys
(
parseList
(
s
.
getCharacterImageKeys
(),
"characterImageKeys"
,
s
.
getId
()));
dto
.
setSceneImageKey
(
s
.
getSceneImageKey
());
dto
.
setPropImageKeys
(
parseList
(
s
.
getPropImageKeys
(),
"propImageKeys"
,
s
.
getId
()));
dto
.
setStyleImageKey
(
s
.
getStyleImageKey
());
return
dto
;
}
private
static
List
<
String
>
parseList
(
String
json
,
String
fieldName
,
Long
id
)
{
if
(
json
==
null
||
json
.
isBlank
())
return
null
;
try
{
return
OBJECT_MAPPER
.
readValue
(
json
,
LIST_STRING_TYPE
);
}
catch
(
Exception
e
)
{
log
.
warn
(
"Failed to parse {} for storyboard {}: {}"
,
fieldName
,
id
,
e
.
getMessage
());
return
null
;
}
}
}
yaoai-comic-studio/yaoai-api/src/main/java/com/yaoai/api/dto/billing/BillingRecordDTO.java
View file @
21e68a93
...
...
@@ -11,7 +11,15 @@ public class BillingRecordDTO {
private
Long
id
;
private
Long
projectId
;
private
String
projectName
;
private
String
operation
;
private
String
modality
;
private
String
modelProvider
;
private
String
modelId
;
private
String
modelName
;
private
Integer
unitCount
;
private
Boolean
success
;
private
String
billingMeta
;
private
BigDecimal
credits
;
private
String
billingMode
;
private
String
refId
;
...
...
@@ -22,6 +30,13 @@ public class BillingRecordDTO {
dto
.
setId
(
r
.
getId
());
dto
.
setProjectId
(
r
.
getProjectId
());
dto
.
setOperation
(
r
.
getOperation
());
dto
.
setModality
(
r
.
getModality
());
dto
.
setModelProvider
(
r
.
getModelProvider
());
dto
.
setModelId
(
r
.
getModelId
());
dto
.
setModelName
(
r
.
getModelName
());
dto
.
setUnitCount
(
r
.
getUnitCount
());
dto
.
setSuccess
(
r
.
getSuccess
());
dto
.
setBillingMeta
(
r
.
getBillingMeta
());
dto
.
setCredits
(
r
.
getCredits
());
dto
.
setBillingMode
(
r
.
getBillingMode
());
dto
.
setRefId
(
r
.
getRefId
());
...
...
yaoai-comic-studio/yaoai-billing/pom.xml
View file @
21e68a93
...
...
@@ -28,6 +28,10 @@
<artifactId>
spring-boot-starter
</artifactId>
</dependency>
<dependency>
<groupId>
com.fasterxml.jackson.core
</groupId>
<artifactId>
jackson-databind
</artifactId>
</dependency>
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-configuration-processor
</artifactId>
<optional>
true
</optional>
...
...
yaoai-comic-studio/yaoai-billing/src/main/java/com/yaoai/billing/service/BillingService.java
View file @
21e68a93
package
com
.
yaoai
.
billing
.
service
;
import
com.yaoai.billing.dto.BillingChargeRequest
;
import
com.yaoai.domain.entity.BillingRecord
;
import
com.yaoai.domain.entity.TenantBalance
;
...
...
@@ -9,18 +10,47 @@ import java.util.List;
public
interface
BillingService
{
/**
* 消费计费:SOFT 只记录;STRICT 先扣余额再记录;OFF 跳过。
* 模型粒度计费(M14 起的主入口):记录模型/用户/单元数/成功标记/JSON 元信息。
* SOFT 只记录;STRICT 先扣余额再记录;OFF 跳过。
*/
void
charge
(
BillingChargeRequest
request
);
/**
* Estimate credits for a billing request without writing records or changing balance.
*/
BigDecimal
estimateCost
(
BillingChargeRequest
request
);
/**
* Check current balance before running an external model call. No deduction is performed.
*/
void
checkBalance
(
BillingChargeRequest
request
);
/**
* Check current balance against an already estimated total. No deduction is performed.
*/
void
checkBalance
(
Long
tenantId
,
BigDecimal
credits
);
/**
* 旧调用兼容:自动映射到 charge(BillingChargeRequest)。仅记录 modality/operation,缺失模型信息。
*
* @param tenantId 租户 ID
* @param projectId 关联项目(可为 null)
* @param operation 操作名(outline_generate / image_generate / video_generate / assembly)
* @param refId 关联业务 ID,用于追溯
* @deprecated 使用 {@link #charge(BillingChargeRequest)},能落到模型粒度便于运营查询
*/
void
charge
(
Long
tenantId
,
Long
projectId
,
String
operation
,
String
refId
);
@Deprecated
default
void
charge
(
Long
tenantId
,
Long
projectId
,
String
operation
,
String
refId
)
{
charge
(
BillingChargeRequest
.
builder
()
.
tenantId
(
tenantId
)
.
projectId
(
projectId
)
.
operation
(
operation
)
.
refId
(
refId
)
.
build
());
}
/** 充值(管理后台 / Webhook 调用) */
void
recharge
(
Long
tenantId
,
BigDecimal
amount
,
String
refId
);
/** 运营手动充值,记录运营 ID */
void
rechargeByAdmin
(
Long
tenantId
,
BigDecimal
amount
,
Long
adminId
,
String
refId
);
/** 初始化余额行(租户注册时调用,已存在则幂等) */
void
initBalance
(
Long
tenantId
,
BigDecimal
initialCredits
);
...
...
yaoai-comic-studio/yaoai-billing/src/main/java/com/yaoai/billing/service/impl/BillingServiceImpl.java
View file @
21e68a93
package
com
.
yaoai
.
billing
.
service
.
impl
;
import
com.fasterxml.jackson.core.JsonProcessingException
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
import
com.yaoai.billing.config.BillingConfig
;
import
com.yaoai.billing.dto.BillingChargeRequest
;
import
com.yaoai.billing.service.BillingCostService
;
import
com.yaoai.billing.service.BillingService
;
import
com.yaoai.common.exception.BizException
;
import
com.yaoai.common.exception.ErrorCode
;
...
...
@@ -26,52 +30,128 @@ public class BillingServiceImpl implements BillingService {
private
final
BillingConfig
billingConfig
;
private
final
TenantBalanceMapper
tenantBalanceMapper
;
private
final
BillingRecordMapper
billingRecordMapper
;
private
final
ObjectMapper
objectMapper
;
private
final
BillingCostService
billingCostService
;
@Override
@Transactional
@CacheEvict
(
value
=
"balance"
,
key
=
"#tenantId"
)
public
void
charge
(
Long
tenantId
,
Long
projectId
,
String
operation
,
String
refId
)
{
@CacheEvict
(
value
=
"balance"
,
key
=
"#
request.
tenantId"
)
public
void
charge
(
BillingChargeRequest
request
)
{
if
(
billingConfig
.
isOff
())
return
;
BigDecimal
cost
=
billingConfig
.
getCost
(
operation
);
if
(
cost
.
compareTo
(
BigDecimal
.
ZERO
)
<=
0
)
return
;
boolean
isSuccess
=
isSuccess
(
request
);
BigDecimal
cost
=
estimateCost
(
request
)
;
if
(
billingConfig
.
isStrict
()
)
{
int
rows
=
tenantBalanceMapper
.
tryDeduct
(
tenantId
,
cost
);
if
(
cost
.
compareTo
(
BigDecimal
.
ZERO
)
>
0
&&
billingConfig
.
isStrict
()
&&
isSuccess
)
{
int
rows
=
tenantBalanceMapper
.
tryDeduct
(
request
.
getTenantId
()
,
cost
);
if
(
rows
==
0
)
{
throw
new
BizException
(
ErrorCode
.
INVALID_PARAM
,
"积分余额不足,请充值后重试"
);
throw
InsufficientBalance
(
);
}
}
BillingRecord
r
=
new
BillingRecord
();
r
.
setTenantId
(
request
.
getTenantId
());
r
.
setUserId
(
request
.
getUserId
());
r
.
setProjectId
(
request
.
getProjectId
());
r
.
setOperation
(
request
.
getOperation
());
r
.
setModality
(
request
.
getModality
());
r
.
setModelProvider
(
request
.
getModelProvider
());
r
.
setModelId
(
request
.
getModelId
());
r
.
setModelName
(
request
.
getModelName
());
r
.
setUnitCount
(
request
.
getUnitCount
()
==
null
?
1
:
request
.
getUnitCount
());
r
.
setSuccess
(
isSuccess
);
r
.
setCredits
(
cost
);
r
.
setBillingMode
(
billingConfig
.
getMode
());
r
.
setRefId
(
request
.
getRefId
());
if
(
request
.
getMeta
()
!=
null
)
{
r
.
setBillingMeta
(
toJson
(
request
));
}
billingRecordMapper
.
insert
(
r
);
log
.
debug
(
"Billed: tenant={}, op={}, modality={}, model={}, credits={}, success={}"
,
request
.
getTenantId
(),
request
.
getOperation
(),
request
.
getModality
(),
request
.
getModelId
(),
cost
,
isSuccess
);
}
@Override
@Deprecated
@Transactional
@CacheEvict
(
value
=
"balance"
,
key
=
"#tenantId"
)
public
void
charge
(
Long
tenantId
,
Long
projectId
,
String
operation
,
String
refId
)
{
BillingService
.
super
.
charge
(
tenantId
,
projectId
,
operation
,
refId
);
}
@Override
public
BigDecimal
estimateCost
(
BillingChargeRequest
request
)
{
if
(
billingConfig
.
isOff
()
||
!
isSuccess
(
request
))
{
return
BigDecimal
.
ZERO
;
}
return
request
.
getCredits
()
!=
null
?
request
.
getCredits
()
:
billingCostService
.
getCost
(
request
.
getOperation
());
}
@Override
public
void
checkBalance
(
BillingChargeRequest
request
)
{
checkBalance
(
request
.
getTenantId
(),
estimateCost
(
request
));
}
@Override
public
void
checkBalance
(
Long
tenantId
,
BigDecimal
credits
)
{
if
(
billingConfig
.
isOff
()
||
!
billingConfig
.
isStrict
())
{
return
;
}
if
(
credits
==
null
||
credits
.
compareTo
(
BigDecimal
.
ZERO
)
<=
0
)
{
return
;
}
TenantBalance
balance
=
tenantBalanceMapper
.
findByTenantId
(
tenantId
);
BigDecimal
available
=
balance
==
null
||
balance
.
getBalance
()
==
null
?
BigDecimal
.
ZERO
:
balance
.
getBalance
();
if
(
available
.
compareTo
(
credits
)
<
0
)
{
throwInsufficientBalance
();
}
}
@Override
@Transactional
@CacheEvict
(
value
=
"balance"
,
key
=
"#tenantId"
)
public
void
recharge
(
Long
tenantId
,
BigDecimal
amount
,
String
refId
)
{
ensureBalanceRow
(
tenantId
);
tenantBalanceMapper
.
recharge
(
tenantId
,
amount
);
BillingRecord
record
=
new
BillingRecord
();
record
.
setTenantId
(
tenantId
);
record
.
setProjectId
(
projectId
);
record
.
setOperation
(
operation
);
record
.
setCredits
(
cost
);
record
.
setOperation
(
"recharge"
);
record
.
setCredits
(
amount
);
record
.
setBillingMode
(
billingConfig
.
getMode
());
record
.
setRefId
(
refId
);
billingRecordMapper
.
insert
(
record
);
log
.
debug
(
"Billed: tenant={}, op={}, credits={}, mode={}, ref={}"
,
tenantId
,
operation
,
cost
,
billingConfig
.
getMode
(),
refId
);
log
.
info
(
"Recharged: tenant={}, amount={}, ref={}"
,
tenantId
,
amount
,
refId
);
}
@Override
@Transactional
@CacheEvict
(
value
=
"balance"
,
key
=
"#tenantId"
)
public
void
recharge
(
Long
tenantId
,
BigDecimal
amount
,
String
refId
)
{
public
void
recharge
ByAdmin
(
Long
tenantId
,
BigDecimal
amount
,
Long
adminId
,
String
refId
)
{
ensureBalanceRow
(
tenantId
);
tenantBalanceMapper
.
recharge
(
tenantId
,
amount
);
BillingRecord
record
=
new
BillingRecord
();
record
.
setTenantId
(
tenantId
);
record
.
setCreatedByAdminId
(
adminId
);
record
.
setOperation
(
"recharge"
);
record
.
setModality
(
"RECHARGE"
);
record
.
setCredits
(
amount
);
record
.
setBillingMode
(
billingConfig
.
getMode
());
record
.
setRefId
(
refId
);
record
.
setSuccess
(
true
);
record
.
setUnitCount
(
1
);
billingRecordMapper
.
insert
(
record
);
log
.
info
(
"
Recharged: tenant={}, amount={}, ref={}"
,
tenantId
,
amount
,
refId
);
log
.
info
(
"
Admin recharge: tenant={}, amount={}, admin={}, ref={}"
,
tenantId
,
amount
,
adminId
,
refId
);
}
@Override
...
...
@@ -123,6 +203,14 @@ public class BillingServiceImpl implements BillingService {
return
billingRecordMapper
.
countByTenant
(
tenantId
);
}
private
String
toJson
(
BillingChargeRequest
request
)
{
try
{
return
objectMapper
.
writeValueAsString
(
request
.
getMeta
());
}
catch
(
JsonProcessingException
e
)
{
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
"计费元数据序列化失败: "
+
e
.
getMessage
());
}
}
private
void
ensureBalanceRow
(
Long
tenantId
)
{
if
(
tenantBalanceMapper
.
findByTenantId
(
tenantId
)
==
null
)
{
TenantBalance
b
=
new
TenantBalance
();
...
...
@@ -134,4 +222,12 @@ public class BillingServiceImpl implements BillingService {
tenantBalanceMapper
.
insert
(
b
);
}
}
private
boolean
isSuccess
(
BillingChargeRequest
request
)
{
return
request
.
getSuccess
()
==
null
||
request
.
getSuccess
();
}
private
void
throwInsufficientBalance
()
{
throw
new
BizException
(
ErrorCode
.
INVALID_PARAM
,
"积分余额不足,请充值后重试"
);
}
}
yaoai-comic-studio/yaoai-bootstrap/pom.xml
View file @
21e68a93
...
...
@@ -21,6 +21,11 @@
<artifactId>
yaoai-api
</artifactId>
</dependency>
<dependency>
<groupId>
com.yaoai
</groupId>
<artifactId>
yaoai-admin
</artifactId>
</dependency>
<!-- ===== Spring Boot Starters ===== -->
<dependency>
<groupId>
org.springframework.boot
</groupId>
...
...
yaoai-comic-studio/yaoai-bootstrap/src/main/resources/application.yml
View file @
21e68a93
...
...
@@ -26,6 +26,7 @@ spring:
locations
:
classpath:db/migration
baseline-on-migrate
:
true
validate-on-migrate
:
true
out-of-order
:
true
data
:
redis
:
...
...
@@ -125,13 +126,19 @@ sa-token:
is-read-url
:
true
billing
:
mode
:
${BILLING_MODE:S
OF
T}
mode
:
${BILLING_MODE:S
TRIC
T}
costs
:
outline_generate
:
${BILLING_COST_OUTLINE:5}
episode_generate
:
${BILLING_COST_EPISODE:3}
image_generate
:
${BILLING_COST_IMAGE:2}
video_generate
:
${BILLING_COST_VIDEO:20}
assembly
:
${BILLING_COST_ASSEMBLY:5}
character_extract
:
${BILLING_COST_CHARACTER_EXTRACT:3}
scene_extract
:
${BILLING_COST_SCENE_EXTRACT:3}
storyboard_generate
:
${BILLING_COST_STORYBOARD:5}
storyboard_prompt_generate
:
${BILLING_COST_STORYBOARD_PROMPT:1}
character_image_generate
:
${BILLING_COST_CHARACTER_IMAGE:6}
scene_image_generate
:
${BILLING_COST_SCENE_IMAGE:2}
logging
:
config
:
classpath:logback-spring.xml
yaoai-comic-studio/yaoai-domain/pom.xml
View file @
21e68a93
...
...
@@ -27,5 +27,9 @@
<groupId>
cn.hutool
</groupId>
<artifactId>
hutool-all
</artifactId>
</dependency>
<dependency>
<groupId>
com.fasterxml.jackson.core
</groupId>
<artifactId>
jackson-annotations
</artifactId>
</dependency>
</dependencies>
</project>
yaoai-comic-studio/yaoai-domain/src/main/java/com/yaoai/domain/entity/AiTask.java
View file @
21e68a93
...
...
@@ -19,7 +19,7 @@ public class AiTask {
private
Long
episodeId
;
private
Long
storyboardId
;
/** image_to_video */
/** image_to_video
/ text_to_video / ref_image_to_video / structured_video
*/
private
String
taskType
;
private
String
externalTaskId
;
...
...
@@ -27,12 +27,30 @@ public class AiTask {
/** pending / submitted / running / succeeded / failed */
private
String
status
;
/** 旧版单图任务的输入;新结构化任务为 null */
private
String
inputImageKey
;
/** 最终提交给 Seedance 的完整 Prompt(含 @图N),新旧任务通用 */
private
String
prompt
;
private
String
resultVideoUrl
;
private
String
resultTosKey
;
private
String
errorMessage
;
/** 结构化视频生成字段(V14 起,task_type=structured_video 时使用) */
private
String
characterImageKey
;
/** V16 起:多角色出镜,JSON 数组字符串,优先于旧 characterImageKey */
private
String
characterImageKeys
;
private
String
sceneImageKey
;
/** JSON 数组字符串,运行时用 ObjectMapper 序列化/反序列化 */
private
String
propImageKeys
;
private
String
styleImageKey
;
/** 用户原始结构化输入(JSON 字符串),与 prompt 字段配对存证 */
private
String
userPrompt
;
private
Integer
videoDuration
;
private
String
videoRatio
;
private
Boolean
generateAudio
;
private
LocalDateTime
createdAt
;
private
LocalDateTime
updatedAt
;
}
yaoai-comic-studio/yaoai-domain/src/main/java/com/yaoai/domain/entity/BillingRecord.java
View file @
21e68a93
...
...
@@ -18,9 +18,36 @@ public class BillingRecord {
private
Long
tenantId
;
private
Long
projectId
;
/** 租户内发起此操作的用户 */
private
Long
userId
;
/** 运营手动充值时记录运营 ID */
private
Long
createdByAdminId
;
/** outline_generate / episode_generate / image_generate / video_generate / assembly / recharge */
private
String
operation
;
/** TEXT / IMAGE / VIDEO / AUDIO / COMPOSE / RECHARGE */
private
String
modality
;
/** volcengine / qwen / openai / ffmpeg */
private
String
modelProvider
;
/** 模型唯一 ID,如 doubao-seedream-5-0-260128 */
private
String
modelId
;
/** 模型展示名称 */
private
String
modelName
;
/** 单次操作的单元数(图数 / 视频段数 / token 倍率) */
private
Integer
unitCount
;
/** 1=成功 0=失败;失败也记一笔 credits=0 用于审计 */
private
Boolean
success
;
/** JSON 元数据:尺寸、时长、token 数等 */
private
String
billingMeta
;
private
BigDecimal
credits
;
/** SOFT / STRICT */
...
...
yaoai-comic-studio/yaoai-domain/src/main/java/com/yaoai/domain/entity/Storyboard.java
View file @
21e68a93
...
...
@@ -36,6 +36,12 @@ public class Storyboard {
private
String
endFramePrompt
;
private
String
notes
;
/** V16:分镜上落地的素材选择(编辑态记忆) */
private
String
characterImageKeys
;
private
String
sceneImageKey
;
private
String
propImageKeys
;
private
String
styleImageKey
;
/** draft / review / approved / generating / generated / failed */
private
String
status
;
...
...
yaoai-comic-studio/yaoai-domain/src/main/java/com/yaoai/domain/entity/Tenant.java
View file @
21e68a93
...
...
@@ -29,6 +29,14 @@ public class Tenant {
private
BigDecimal
frozenCredits
;
private
LocalDateTime
suspendedAt
;
private
Long
suspendedBy
;
private
String
suspendedReason
;
private
String
notes
;
private
LocalDateTime
createdAt
;
private
LocalDateTime
updatedAt
;
...
...
yaoai-comic-studio/yaoai-domain/src/main/java/com/yaoai/domain/entity/User.java
View file @
21e68a93
...
...
@@ -3,6 +3,7 @@ package com.yaoai.domain.entity;
import
com.baomidou.mybatisplus.annotation.IdType
;
import
com.baomidou.mybatisplus.annotation.TableId
;
import
com.baomidou.mybatisplus.annotation.TableName
;
import
com.fasterxml.jackson.annotation.JsonIgnore
;
import
lombok.Data
;
import
java.time.LocalDateTime
;
...
...
@@ -24,6 +25,7 @@ public class User {
private
String
avatarUrl
;
@JsonIgnore
private
String
passwordHash
;
/** admin / member */
...
...
yaoai-comic-studio/yaoai-pipeline/pom.xml
View file @
21e68a93
...
...
@@ -36,6 +36,10 @@
<artifactId>
spring-boot-starter
</artifactId>
</dependency>
<dependency>
<groupId>
cn.dev33
</groupId>
<artifactId>
sa-token-spring-boot3-starter
</artifactId>
</dependency>
<dependency>
<groupId>
com.yaoai
</groupId>
<artifactId>
yaoai-billing
</artifactId>
</dependency>
...
...
yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/async/VideoTaskAsyncProcessor.java
View file @
21e68a93
...
...
@@ -2,6 +2,7 @@ package com.yaoai.pipeline.async;
import
com.yaoai.ai.providers.model.VideoTaskResult
;
import
com.yaoai.ai.providers.service.SeedanceService
;
import
com.yaoai.billing.dto.BillingChargeRequest
;
import
com.yaoai.billing.service.BillingService
;
import
com.yaoai.domain.entity.AiTask
;
import
com.yaoai.domain.mapper.AiTaskMapper
;
...
...
@@ -12,8 +13,10 @@ import lombok.extern.slf4j.Slf4j;
import
org.springframework.scheduling.annotation.Async
;
import
org.springframework.stereotype.Component
;
import
java.math.BigDecimal
;
import
java.util.ArrayList
;
import
java.util.List
;
import
java.util.Map
;
@Slf4j
@Component
...
...
@@ -33,14 +36,43 @@ public class VideoTaskAsyncProcessor {
*
* @param preImageKeys 按 @图1/@图2... 顺序排列的 TOS key 列表,空列表则走文生图
* @param durationSeconds 视频时长(秒),<=0 则使用默认值 5
* @param ratio 视频宽高比,如 "16:9" / "9:16" / "1:1",空/非法值由下游兜底
* @param generateAudio 是否生成配音
*/
@Async
public
void
processTextToVideo
(
Long
taskId
,
Long
tenantId
,
Long
projectId
,
public
void
processTextToVideo
(
Long
taskId
,
Long
tenantId
,
Long
userId
,
Long
projectId
,
String
imagePrompt
,
String
videoPrompt
,
List
<
String
>
preImageKeys
,
int
durationSeconds
)
{
log
.
info
(
"Async text-to-video started: taskId={}, preImages={}, duration={}s"
,
taskId
,
preImageKeys
.
size
(),
durationSeconds
);
List
<
String
>
preImageKeys
,
int
durationSeconds
,
String
ratio
,
boolean
generateAudio
,
String
model
)
{
log
.
info
(
"Async text-to-video started: taskId={}, preImages={}, duration={}s, ratio={}, audio={}, model={}"
,
taskId
,
preImageKeys
.
size
(),
durationSeconds
,
ratio
,
generateAudio
,
model
);
try
{
BillingChargeRequest
videoCharge
=
BillingChargeRequest
.
builder
()
.
tenantId
(
tenantId
)
.
userId
(
userId
)
.
projectId
(
projectId
)
.
operation
(
"video_generate"
)
.
modality
(
"VIDEO"
)
.
modelProvider
(
seedanceService
.
getProvider
())
.
modelId
(
seedanceService
.
getModelId
())
.
modelName
(
seedanceService
.
getModelName
())
.
unitCount
(
1
)
.
refId
(
String
.
valueOf
(
taskId
))
.
build
();
BigDecimal
estimatedCost
=
billingService
.
estimateCost
(
videoCharge
);
if
(
preImageKeys
.
isEmpty
())
{
estimatedCost
=
estimatedCost
.
add
(
billingService
.
estimateCost
(
BillingChargeRequest
.
builder
()
.
tenantId
(
tenantId
)
.
userId
(
userId
)
.
projectId
(
projectId
)
.
operation
(
"image_generate"
)
.
modality
(
"IMAGE"
)
.
unitCount
(
1
)
.
meta
(
Map
.
of
(
"size"
,
"2048x2048"
))
.
build
()));
}
billingService
.
checkBalance
(
tenantId
,
estimatedCost
);
List
<
String
>
presignedUrls
=
new
ArrayList
<>();
String
primaryImageKey
;
...
...
@@ -53,14 +85,14 @@ public class VideoTaskAsyncProcessor {
log
.
info
(
"Using {} pre-existing reference image(s): taskId={}"
,
preImageKeys
.
size
(),
taskId
);
}
else
{
// 无预设参考图:调用 Seedream 文生图
primaryImageKey
=
imageGenPipelineService
.
generateAndStore
(
tenantId
,
projectId
,
imagePrompt
);
primaryImageKey
=
imageGenPipelineService
.
generateAndStore
(
tenantId
,
userId
,
projectId
,
imagePrompt
);
presignedUrls
.
add
(
tosService
.
presignedGetUrl
(
primaryImageKey
,
3600
));
log
.
info
(
"Image generated and stored: taskId={}, key={}"
,
taskId
,
primaryImageKey
);
}
// 提交 Seedance,传入所有参考图 + 时长
int
duration
=
durationSeconds
>
0
?
durationSeconds
:
5
;
String
externalTaskId
=
seedanceService
.
submitVideoTask
(
presignedUrls
,
videoPrompt
,
duration
,
true
);
String
externalTaskId
=
seedanceService
.
submitVideoTask
(
presignedUrls
,
videoPrompt
,
duration
,
generateAudio
,
ratio
,
model
);
log
.
info
(
"Seedance task submitted: taskId={}, externalId={}, images={}, duration={}s"
,
taskId
,
externalTaskId
,
presignedUrls
.
size
(),
duration
);
...
...
@@ -72,7 +104,7 @@ public class VideoTaskAsyncProcessor {
update
.
setExternalTaskId
(
externalTaskId
);
aiTaskMapper
.
updateById
(
update
);
billingService
.
charge
(
tenantId
,
projectId
,
"video_generate"
,
String
.
valueOf
(
taskId
)
);
billingService
.
charge
(
videoCharge
);
// 轮询 Ark 直到任务完成(最多等 10 分钟,每 5 秒查一次)
pollUntilDone
(
taskId
,
externalTaskId
);
...
...
yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/service/ImageGenPipelineService.java
View file @
21e68a93
...
...
@@ -11,4 +11,6 @@ public interface ImageGenPipelineService {
* @return TOS object key
*/
String
generateAndStore
(
Long
tenantId
,
Long
projectId
,
String
prompt
);
String
generateAndStore
(
Long
tenantId
,
Long
userId
,
Long
projectId
,
String
prompt
);
}
yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/service/VideoTaskPipelineService.java
View file @
21e68a93
package
com
.
yaoai
.
pipeline
.
service
;
import
com.yaoai.domain.entity.AiTask
;
import
com.yaoai.pipeline.dto.StructuredVideoSubmission
;
import
java.util.List
;
...
...
@@ -18,12 +19,20 @@ public interface VideoTaskPipelineService {
*
* @param preImageKeys 按 @图N 顺序排列的 TOS key 列表,空列表则走文生图
* @param durationSeconds 视频时长(秒),<=0 则使用默认值 5
* @deprecated 由 {@link #submitStructuredVideoTask} 替代;保留 1 个版本周期供回滚
*/
AiTask
submitTextToVideoAsync
(
Long
projectId
,
Long
tenantId
,
Long
episodeId
,
@Deprecated
AiTask
submitTextToVideoAsync
(
Long
projectId
,
Long
tenantId
,
Long
userId
,
Long
episodeId
,
Long
storyboardId
,
String
imagePrompt
,
String
videoPrompt
,
List
<
String
>
preImageKeys
,
int
durationSeconds
);
/**
* 结构化视频生成(左图 + 右提示词):把素材选择 + 五段提示词转成 Seedance 调用。
* 立即创建 pending 任务并返回,后台异步提交 Seedance 并轮询。
*/
AiTask
submitStructuredVideoTask
(
Long
projectId
,
Long
tenantId
,
StructuredVideoSubmission
submission
);
/**
* 轮询任务状态:查询 Seedance API,更新 DB
*/
AiTask
pollTaskStatus
(
Long
taskId
,
Long
tenantId
);
...
...
yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/service/impl/AssemblyPipelineServiceImpl.java
View file @
21e68a93
package
com
.
yaoai
.
pipeline
.
service
.
impl
;
import
cn.dev33.satoken.stp.StpUtil
;
import
com.yaoai.billing.dto.BillingChargeRequest
;
import
com.yaoai.billing.service.BillingService
;
import
com.yaoai.domain.entity.AiTask
;
import
com.yaoai.domain.entity.AssemblyTask
;
...
...
@@ -109,7 +111,18 @@ public class AssemblyPipelineServiceImpl implements AssemblyPipelineService {
task
.
setResultTosKey
(
tosKey
);
task
.
setStatus
(
"succeeded"
);
assemblyTaskMapper
.
updateById
(
task
);
billingService
.
charge
(
task
.
getTenantId
(),
task
.
getProjectId
(),
"assembly"
,
String
.
valueOf
(
assemblyTaskId
));
billingService
.
charge
(
BillingChargeRequest
.
builder
()
.
tenantId
(
task
.
getTenantId
())
.
userId
(
StpUtil
.
getLoginIdAsLong
())
.
projectId
(
task
.
getProjectId
())
.
operation
(
"assembly"
)
.
modality
(
"COMPOSE"
)
.
modelProvider
(
"ffmpeg"
)
.
modelId
(
"ffmpeg-compose"
)
.
modelName
(
"FFmpeg 视频合成"
)
.
unitCount
(
1
)
.
refId
(
String
.
valueOf
(
assemblyTaskId
))
.
build
());
log
.
info
(
"Assembly task {} succeeded, tosKey={}"
,
assemblyTaskId
,
tosKey
);
}
catch
(
IOException
|
InterruptedException
e
)
{
...
...
yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/service/impl/AssetGenPipelineServiceImpl.java
View file @
21e68a93
package
com
.
yaoai
.
pipeline
.
service
.
impl
;
import
cn.dev33.satoken.stp.StpUtil
;
import
com.fasterxml.jackson.core.type.TypeReference
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
import
com.yaoai.ai.core.model.ChatMessage
;
import
com.yaoai.ai.core.model.ChatRequest
;
import
com.yaoai.ai.core.service.LlmService
;
import
com.yaoai.ai.providers.service.SeedreamService
;
import
com.yaoai.billing.dto.BillingChargeRequest
;
import
com.yaoai.billing.service.BillingService
;
import
com.yaoai.common.exception.BizException
;
import
com.yaoai.common.exception.ErrorCode
;
import
com.yaoai.domain.entity.Character
;
...
...
@@ -82,6 +85,7 @@ public class AssetGenPipelineServiceImpl implements AssetGenPipelineService {
private
final
EpisodeMapper
episodeMapper
;
private
final
ProjectMapper
projectMapper
;
private
final
ObjectMapper
objectMapper
;
private
final
BillingService
billingService
;
private
final
HttpClient
httpClient
=
HttpClient
.
newHttpClient
();
@Override
...
...
@@ -91,6 +95,19 @@ public class AssetGenPipelineServiceImpl implements AssetGenPipelineService {
String
context
=
buildCharacterExtractionContext
(
projectId
,
tenantId
,
visualStyle
);
log
.
info
(
"Extracting characters: projectId={}"
,
projectId
);
billingService
.
checkBalance
(
BillingChargeRequest
.
builder
()
.
tenantId
(
tenantId
)
.
userId
(
StpUtil
.
getLoginIdAsLong
())
.
projectId
(
projectId
)
.
operation
(
"character_extract"
)
.
modality
(
"TEXT"
)
.
modelProvider
(
llmService
.
getProvider
())
.
modelId
(
llmService
.
getModelId
())
.
modelName
(
llmService
.
getModelName
())
.
unitCount
(
1
)
.
refId
(
String
.
valueOf
(
projectId
))
.
build
());
String
raw
=
llmService
.
chat
(
ChatRequest
.
builder
()
.
messages
(
List
.
of
(
ChatMessage
.
system
(
CHARACTER_SYSTEM
),
ChatMessage
.
user
(
context
)))
.
build
());
...
...
@@ -114,6 +131,18 @@ public class AssetGenPipelineServiceImpl implements AssetGenPipelineService {
characterMapper
.
insert
(
character
);
result
.
add
(
character
);
}
billingService
.
charge
(
BillingChargeRequest
.
builder
()
.
tenantId
(
tenantId
)
.
userId
(
StpUtil
.
getLoginIdAsLong
())
.
projectId
(
projectId
)
.
operation
(
"character_extract"
)
.
modality
(
"TEXT"
)
.
modelProvider
(
llmService
.
getProvider
())
.
modelId
(
llmService
.
getModelId
())
.
modelName
(
llmService
.
getModelName
())
.
unitCount
(
result
.
size
())
.
refId
(
String
.
valueOf
(
projectId
))
.
build
());
return
result
;
}
catch
(
Exception
e
)
{
log
.
error
(
"Character extraction failed"
,
e
);
...
...
@@ -126,6 +155,19 @@ public class AssetGenPipelineServiceImpl implements AssetGenPipelineService {
String
context
=
buildProjectContext
(
projectId
,
tenantId
);
log
.
info
(
"Extracting scenes: projectId={}"
,
projectId
);
billingService
.
checkBalance
(
BillingChargeRequest
.
builder
()
.
tenantId
(
tenantId
)
.
userId
(
StpUtil
.
getLoginIdAsLong
())
.
projectId
(
projectId
)
.
operation
(
"scene_extract"
)
.
modality
(
"TEXT"
)
.
modelProvider
(
llmService
.
getProvider
())
.
modelId
(
llmService
.
getModelId
())
.
modelName
(
llmService
.
getModelName
())
.
unitCount
(
1
)
.
refId
(
String
.
valueOf
(
projectId
))
.
build
());
String
raw
=
llmService
.
chat
(
ChatRequest
.
builder
()
.
messages
(
List
.
of
(
ChatMessage
.
system
(
SCENE_SYSTEM
),
ChatMessage
.
user
(
context
)))
.
build
());
...
...
@@ -145,6 +187,18 @@ public class AssetGenPipelineServiceImpl implements AssetGenPipelineService {
sceneMapper
.
insert
(
scene
);
result
.
add
(
scene
);
}
billingService
.
charge
(
BillingChargeRequest
.
builder
()
.
tenantId
(
tenantId
)
.
userId
(
StpUtil
.
getLoginIdAsLong
())
.
projectId
(
projectId
)
.
operation
(
"scene_extract"
)
.
modality
(
"TEXT"
)
.
modelProvider
(
llmService
.
getProvider
())
.
modelId
(
llmService
.
getModelId
())
.
modelName
(
llmService
.
getModelName
())
.
unitCount
(
result
.
size
())
.
refId
(
String
.
valueOf
(
projectId
))
.
build
());
return
result
;
}
catch
(
Exception
e
)
{
log
.
error
(
"Scene extraction failed"
,
e
);
...
...
@@ -161,6 +215,20 @@ public class AssetGenPipelineServiceImpl implements AssetGenPipelineService {
throw
new
BizException
(
ErrorCode
.
INVALID_PARAM
,
"角色缺少图片 Prompt"
);
}
billingService
.
checkBalance
(
BillingChargeRequest
.
builder
()
.
tenantId
(
tenantId
)
.
userId
(
StpUtil
.
getLoginIdAsLong
())
.
projectId
(
character
.
getProjectId
())
.
operation
(
"character_image_generate"
)
.
modality
(
"IMAGE"
)
.
modelProvider
(
seedreamService
.
getProvider
())
.
modelId
(
seedreamService
.
getModelId
())
.
modelName
(
seedreamService
.
getModelName
())
.
unitCount
(
3
)
.
meta
(
Map
.
of
(
"views"
,
List
.
of
(
"front"
,
"side"
,
"back"
)))
.
refId
(
String
.
valueOf
(
characterId
))
.
build
());
character
.
setStatus
(
"generating"
);
characterMapper
.
updateById
(
character
);
...
...
@@ -180,6 +248,19 @@ public class AssetGenPipelineServiceImpl implements AssetGenPipelineService {
character
.
setStatus
(
"ready"
);
characterMapper
.
updateById
(
character
);
billingService
.
charge
(
BillingChargeRequest
.
builder
()
.
tenantId
(
tenantId
)
.
userId
(
StpUtil
.
getLoginIdAsLong
())
.
projectId
(
character
.
getProjectId
())
.
operation
(
"character_image_generate"
)
.
modality
(
"IMAGE"
)
.
modelProvider
(
seedreamService
.
getProvider
())
.
modelId
(
seedreamService
.
getModelId
())
.
modelName
(
seedreamService
.
getModelName
())
.
unitCount
(
3
)
.
meta
(
Map
.
of
(
"views"
,
List
.
of
(
"front"
,
"side"
,
"back"
)))
.
refId
(
String
.
valueOf
(
characterId
))
.
build
());
return
character
;
}
catch
(
BizException
e
)
{
character
.
setStatus
(
"failed"
);
...
...
@@ -202,6 +283,19 @@ public class AssetGenPipelineServiceImpl implements AssetGenPipelineService {
throw
new
BizException
(
ErrorCode
.
INVALID_PARAM
,
"场景缺少图片 Prompt"
);
}
billingService
.
checkBalance
(
BillingChargeRequest
.
builder
()
.
tenantId
(
tenantId
)
.
userId
(
StpUtil
.
getLoginIdAsLong
())
.
projectId
(
scene
.
getProjectId
())
.
operation
(
"scene_image_generate"
)
.
modality
(
"IMAGE"
)
.
modelProvider
(
seedreamService
.
getProvider
())
.
modelId
(
seedreamService
.
getModelId
())
.
modelName
(
seedreamService
.
getModelName
())
.
unitCount
(
1
)
.
refId
(
String
.
valueOf
(
sceneId
))
.
build
());
scene
.
setStatus
(
"generating"
);
sceneMapper
.
updateById
(
scene
);
...
...
@@ -215,6 +309,18 @@ public class AssetGenPipelineServiceImpl implements AssetGenPipelineService {
scene
.
setImageTosKey
(
key
);
scene
.
setStatus
(
"ready"
);
sceneMapper
.
updateById
(
scene
);
billingService
.
charge
(
BillingChargeRequest
.
builder
()
.
tenantId
(
tenantId
)
.
userId
(
StpUtil
.
getLoginIdAsLong
())
.
projectId
(
scene
.
getProjectId
())
.
operation
(
"scene_image_generate"
)
.
modality
(
"IMAGE"
)
.
modelProvider
(
seedreamService
.
getProvider
())
.
modelId
(
seedreamService
.
getModelId
())
.
modelName
(
seedreamService
.
getModelName
())
.
unitCount
(
1
)
.
refId
(
String
.
valueOf
(
sceneId
))
.
build
());
return
scene
;
}
catch
(
BizException
e
)
{
scene
.
setStatus
(
"failed"
);
...
...
yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/service/impl/ImageGenPipelineServiceImpl.java
View file @
21e68a93
package
com
.
yaoai
.
pipeline
.
service
.
impl
;
import
cn.dev33.satoken.stp.StpUtil
;
import
com.yaoai.ai.providers.service.SeedreamService
;
import
com.yaoai.billing.dto.BillingChargeRequest
;
import
com.yaoai.billing.service.BillingService
;
import
com.yaoai.common.exception.BizException
;
import
com.yaoai.common.exception.ErrorCode
;
...
...
@@ -15,6 +17,7 @@ import java.net.URI;
import
java.net.http.HttpClient
;
import
java.net.http.HttpRequest
;
import
java.net.http.HttpResponse
;
import
java.util.Map
;
@Slf4j
@Service
...
...
@@ -28,6 +31,24 @@ public class ImageGenPipelineServiceImpl implements ImageGenPipelineService {
@Override
public
String
generateAndStore
(
Long
tenantId
,
Long
projectId
,
String
prompt
)
{
return
generateAndStore
(
tenantId
,
StpUtil
.
getLoginIdAsLong
(),
projectId
,
prompt
);
}
@Override
public
String
generateAndStore
(
Long
tenantId
,
Long
userId
,
Long
projectId
,
String
prompt
)
{
billingService
.
checkBalance
(
BillingChargeRequest
.
builder
()
.
tenantId
(
tenantId
)
.
userId
(
userId
)
.
projectId
(
projectId
)
.
operation
(
"image_generate"
)
.
modality
(
"IMAGE"
)
.
modelProvider
(
seedreamService
.
getProvider
())
.
modelId
(
seedreamService
.
getModelId
())
.
modelName
(
seedreamService
.
getModelName
())
.
unitCount
(
1
)
.
meta
(
Map
.
of
(
"size"
,
"2048x2048"
))
.
build
());
// 1. 调用 Seedream 生成图片,获取临时 URL
String
imageUrl
=
seedreamService
.
generateImage
(
prompt
);
log
.
info
(
"Seedream image URL: {}"
,
imageUrl
);
...
...
@@ -39,7 +60,19 @@ public class ImageGenPipelineServiceImpl implements ImageGenPipelineService {
String
key
=
TosService
.
buildKey
(
tenantId
,
projectId
,
"scene"
,
"scene.jpg"
);
tosService
.
upload
(
key
,
new
ByteArrayInputStream
(
imageBytes
),
imageBytes
.
length
,
"image/jpeg"
);
log
.
info
(
"Scene image stored: key={}"
,
key
);
billingService
.
charge
(
tenantId
,
projectId
,
"image_generate"
,
key
);
billingService
.
charge
(
BillingChargeRequest
.
builder
()
.
tenantId
(
tenantId
)
.
userId
(
userId
)
.
projectId
(
projectId
)
.
operation
(
"image_generate"
)
.
modality
(
"IMAGE"
)
.
modelProvider
(
seedreamService
.
getProvider
())
.
modelId
(
seedreamService
.
getModelId
())
.
modelName
(
seedreamService
.
getModelName
())
.
unitCount
(
1
)
.
meta
(
Map
.
of
(
"size"
,
"2048x2048"
))
.
refId
(
key
)
.
build
());
return
key
;
}
...
...
yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/service/impl/OutlinePipelineServiceImpl.java
View file @
21e68a93
package
com
.
yaoai
.
pipeline
.
service
.
impl
;
import
cn.dev33.satoken.stp.StpUtil
;
import
com.fasterxml.jackson.core.type.TypeReference
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
import
com.yaoai.ai.core.model.ChatMessage
;
import
com.yaoai.ai.core.model.ChatRequest
;
import
com.yaoai.ai.core.service.LlmService
;
import
com.yaoai.billing.dto.BillingChargeRequest
;
import
com.yaoai.billing.service.BillingService
;
import
com.yaoai.common.exception.BizException
;
import
com.yaoai.common.exception.ErrorCode
;
import
com.yaoai.domain.entity.Episode
;
import
com.yaoai.domain.entity.Outline
;
import
com.yaoai.domain.mapper.EpisodeMapper
;
import
com.yaoai.domain.mapper.OutlineMapper
;
import
com.yaoai.billing.service.BillingService
;
import
com.yaoai.pipeline.service.OutlinePipelineService
;
import
lombok.RequiredArgsConstructor
;
import
lombok.extern.slf4j.Slf4j
;
...
...
@@ -69,6 +71,19 @@ public class OutlinePipelineServiceImpl implements OutlinePipelineService {
))
.
build
();
billingService
.
checkBalance
(
BillingChargeRequest
.
builder
()
.
tenantId
(
tenantId
)
.
userId
(
StpUtil
.
getLoginIdAsLong
())
.
projectId
(
projectId
)
.
operation
(
"outline_generate"
)
.
modality
(
"TEXT"
)
.
modelProvider
(
llmService
.
getProvider
())
.
modelId
(
llmService
.
getModelId
())
.
modelName
(
llmService
.
getModelName
())
.
unitCount
(
1
)
.
refId
(
String
.
valueOf
(
projectId
))
.
build
());
String
raw
=
llmService
.
chat
(
request
);
log
.
debug
(
"Outline LLM response: {}"
,
raw
);
...
...
@@ -89,7 +104,18 @@ public class OutlinePipelineServiceImpl implements OutlinePipelineService {
outline
.
setEpisodeCount
(
epCount
instanceof
Number
?
((
Number
)
epCount
).
intValue
()
:
1
);
outlineMapper
.
insert
(
outline
);
billingService
.
charge
(
tenantId
,
projectId
,
"outline_generate"
,
String
.
valueOf
(
projectId
));
billingService
.
charge
(
BillingChargeRequest
.
builder
()
.
tenantId
(
tenantId
)
.
userId
(
StpUtil
.
getLoginIdAsLong
())
.
projectId
(
projectId
)
.
operation
(
"outline_generate"
)
.
modality
(
"TEXT"
)
.
modelProvider
(
llmService
.
getProvider
())
.
modelId
(
llmService
.
getModelId
())
.
modelName
(
llmService
.
getModelName
())
.
unitCount
(
1
)
.
refId
(
String
.
valueOf
(
projectId
))
.
build
());
log
.
info
(
"Outline saved: id={}, title={}"
,
outline
.
getId
(),
outline
.
getTitle
());
return
outline
;
}
catch
(
Exception
e
)
{
...
...
@@ -119,6 +145,19 @@ public class OutlinePipelineServiceImpl implements OutlinePipelineService {
))
.
build
();
billingService
.
checkBalance
(
BillingChargeRequest
.
builder
()
.
tenantId
(
tenantId
)
.
userId
(
StpUtil
.
getLoginIdAsLong
())
.
projectId
(
projectId
)
.
operation
(
"episode_generate"
)
.
modality
(
"TEXT"
)
.
modelProvider
(
llmService
.
getProvider
())
.
modelId
(
llmService
.
getModelId
())
.
modelName
(
llmService
.
getModelName
())
.
unitCount
(
1
)
.
refId
(
String
.
valueOf
(
outlineId
))
.
build
());
String
raw
=
llmService
.
chat
(
request
);
log
.
debug
(
"Episode LLM response: {}"
,
raw
);
...
...
@@ -140,7 +179,18 @@ public class OutlinePipelineServiceImpl implements OutlinePipelineService {
episodeMapper
.
insert
(
ep
);
episodes
.
add
(
ep
);
}
billingService
.
charge
(
tenantId
,
projectId
,
"episode_generate"
,
String
.
valueOf
(
outlineId
));
billingService
.
charge
(
BillingChargeRequest
.
builder
()
.
tenantId
(
tenantId
)
.
userId
(
StpUtil
.
getLoginIdAsLong
())
.
projectId
(
projectId
)
.
operation
(
"episode_generate"
)
.
modality
(
"TEXT"
)
.
modelProvider
(
llmService
.
getProvider
())
.
modelId
(
llmService
.
getModelId
())
.
modelName
(
llmService
.
getModelName
())
.
unitCount
(
1
)
.
refId
(
String
.
valueOf
(
outlineId
))
.
build
());
log
.
info
(
"Episodes saved: count={}"
,
episodes
.
size
());
return
episodes
;
}
catch
(
Exception
e
)
{
...
...
yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/service/impl/StoryboardPipelineServiceImpl.java
View file @
21e68a93
package
com
.
yaoai
.
pipeline
.
service
.
impl
;
import
cn.dev33.satoken.stp.StpUtil
;
import
com.fasterxml.jackson.core.type.TypeReference
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
import
com.yaoai.ai.core.model.ChatMessage
;
import
com.yaoai.ai.core.model.ChatRequest
;
import
com.yaoai.ai.core.service.LlmService
;
import
com.yaoai.billing.dto.BillingChargeRequest
;
import
com.yaoai.billing.service.BillingService
;
import
com.yaoai.common.exception.BizException
;
import
com.yaoai.common.exception.ErrorCode
;
import
com.yaoai.domain.entity.Character
;
...
...
@@ -35,6 +38,7 @@ public class StoryboardPipelineServiceImpl implements StoryboardPipelineService
private
final
CharacterMapper
characterMapper
;
private
final
SceneMapper
sceneMapper
;
private
final
ObjectMapper
objectMapper
;
private
final
BillingService
billingService
;
private
static
final
String
SYSTEM_PROMPT_BASE
=
"""
你是一位专业的影视分镜师。根据分集脚本,将其拆分为若干个连续镜头(shot)。
...
...
@@ -84,6 +88,19 @@ public class StoryboardPipelineServiceImpl implements StoryboardPipelineService
))
.
build
();
billingService
.
checkBalance
(
BillingChargeRequest
.
builder
()
.
tenantId
(
tenantId
)
.
userId
(
StpUtil
.
getLoginIdAsLong
())
.
projectId
(
projectId
)
.
operation
(
"storyboard_generate"
)
.
modality
(
"TEXT"
)
.
modelProvider
(
llmService
.
getProvider
())
.
modelId
(
llmService
.
getModelId
())
.
modelName
(
llmService
.
getModelName
())
.
unitCount
(
1
)
.
refId
(
String
.
valueOf
(
episodeId
))
.
build
());
String
raw
=
llmService
.
chat
(
request
);
log
.
debug
(
"Storyboard LLM response length={}"
,
raw
.
length
());
...
...
@@ -112,6 +129,18 @@ public class StoryboardPipelineServiceImpl implements StoryboardPipelineService
storyboardMapper
.
insert
(
sb
);
result
.
add
(
sb
);
}
billingService
.
charge
(
BillingChargeRequest
.
builder
()
.
tenantId
(
tenantId
)
.
userId
(
StpUtil
.
getLoginIdAsLong
())
.
projectId
(
projectId
)
.
operation
(
"storyboard_generate"
)
.
modality
(
"TEXT"
)
.
modelProvider
(
llmService
.
getProvider
())
.
modelId
(
llmService
.
getModelId
())
.
modelName
(
llmService
.
getModelName
())
.
unitCount
(
result
.
size
())
.
refId
(
String
.
valueOf
(
episodeId
))
.
build
());
log
.
info
(
"Storyboards generated: count={}"
,
result
.
size
());
return
result
;
}
catch
(
Exception
e
)
{
...
...
@@ -268,7 +297,32 @@ public class StoryboardPipelineServiceImpl implements StoryboardPipelineService
log
.
info
(
"Generating storyboard prompt: storyboardId={}, episodeId={}, characters={}, scenes={}"
,
storyboardId
,
sb
.
getEpisodeId
(),
characters
.
size
(),
scenes
.
size
());
return
llmService
.
chat
(
request
);
billingService
.
checkBalance
(
BillingChargeRequest
.
builder
()
.
tenantId
(
tenantId
)
.
userId
(
StpUtil
.
getLoginIdAsLong
())
.
projectId
(
sb
.
getProjectId
())
.
operation
(
"storyboard_prompt_generate"
)
.
modality
(
"TEXT"
)
.
modelProvider
(
llmService
.
getProvider
())
.
modelId
(
llmService
.
getModelId
())
.
modelName
(
llmService
.
getModelName
())
.
unitCount
(
1
)
.
refId
(
String
.
valueOf
(
storyboardId
))
.
build
());
String
prompt
=
llmService
.
chat
(
request
);
billingService
.
charge
(
BillingChargeRequest
.
builder
()
.
tenantId
(
tenantId
)
.
userId
(
StpUtil
.
getLoginIdAsLong
())
.
projectId
(
sb
.
getProjectId
())
.
operation
(
"storyboard_prompt_generate"
)
.
modality
(
"TEXT"
)
.
modelProvider
(
llmService
.
getProvider
())
.
modelId
(
llmService
.
getModelId
())
.
modelName
(
llmService
.
getModelName
())
.
unitCount
(
1
)
.
refId
(
String
.
valueOf
(
storyboardId
))
.
build
());
return
prompt
;
}
private
static
String
nullSafe
(
String
s
)
{
...
...
yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/service/impl/VideoTaskPipelineServiceImpl.java
View file @
21e68a93
package
com
.
yaoai
.
pipeline
.
service
.
impl
;
import
cn.dev33.satoken.stp.StpUtil
;
import
com.fasterxml.jackson.core.JsonProcessingException
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
import
com.yaoai.ai.providers.model.VideoTaskResult
;
import
com.yaoai.ai.providers.service.SeedanceService
;
import
com.yaoai.ai.providers.util.SeedancePromptBuilder
;
import
com.yaoai.billing.dto.BillingChargeRequest
;
import
com.yaoai.billing.service.BillingService
;
import
com.yaoai.common.exception.BizException
;
import
com.yaoai.common.exception.ErrorCode
;
import
com.yaoai.domain.entity.AiTask
;
import
com.yaoai.domain.mapper.AiTaskMapper
;
import
com.yaoai.pipeline.async.VideoTaskAsyncProcessor
;
import
com.yaoai.pipeline.dto.StructuredVideoSubmission
;
import
com.yaoai.pipeline.service.VideoTaskPipelineService
;
import
com.yaoai.storage.service.TosService
;
import
lombok.RequiredArgsConstructor
;
...
...
@@ -28,12 +34,26 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService {
private
final
BillingService
billingService
;
private
final
VideoTaskAsyncProcessor
asyncProcessor
;
private
static
final
ObjectMapper
OBJECT_MAPPER
=
new
ObjectMapper
();
@Override
public
AiTask
submitVideoTask
(
Long
projectId
,
Long
tenantId
,
Long
episodeId
,
Long
storyboardId
,
String
imageKey
,
String
prompt
)
{
String
presignedUrl
=
tosService
.
presignedGetUrl
(
imageKey
,
3600
);
log
.
info
(
"Submitting video task: projectId={}, episodeId={}, storyboardId={}"
,
projectId
,
episodeId
,
storyboardId
);
billingService
.
checkBalance
(
BillingChargeRequest
.
builder
()
.
tenantId
(
tenantId
)
.
userId
(
StpUtil
.
getLoginIdAsLong
())
.
projectId
(
projectId
)
.
operation
(
"video_generate"
)
.
modality
(
"VIDEO"
)
.
modelProvider
(
seedanceService
.
getProvider
())
.
modelId
(
seedanceService
.
getModelId
())
.
modelName
(
seedanceService
.
getModelName
())
.
unitCount
(
1
)
.
build
());
String
externalTaskId
=
seedanceService
.
submitVideoTask
(
presignedUrl
,
prompt
);
AiTask
task
=
new
AiTask
();
...
...
@@ -48,7 +68,18 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService {
task
.
setPrompt
(
prompt
);
aiTaskMapper
.
insert
(
task
);
billingService
.
charge
(
tenantId
,
projectId
,
"video_generate"
,
String
.
valueOf
(
task
.
getId
()));
billingService
.
charge
(
BillingChargeRequest
.
builder
()
.
tenantId
(
tenantId
)
.
userId
(
StpUtil
.
getLoginIdAsLong
())
.
projectId
(
projectId
)
.
operation
(
"video_generate"
)
.
modality
(
"VIDEO"
)
.
modelProvider
(
seedanceService
.
getProvider
())
.
modelId
(
seedanceService
.
getModelId
())
.
modelName
(
seedanceService
.
getModelName
())
.
unitCount
(
1
)
.
refId
(
String
.
valueOf
(
task
.
getId
()))
.
build
());
log
.
info
(
"AiTask created: id={}, externalId={}"
,
task
.
getId
(),
externalTaskId
);
return
task
;
}
...
...
@@ -80,7 +111,7 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService {
}
@Override
public
AiTask
submitTextToVideoAsync
(
Long
projectId
,
Long
tenantId
,
Long
episodeId
,
public
AiTask
submitTextToVideoAsync
(
Long
projectId
,
Long
tenantId
,
Long
userId
,
Long
episodeId
,
Long
storyboardId
,
String
imagePrompt
,
String
videoPrompt
,
List
<
String
>
preImageKeys
,
int
durationSeconds
)
{
// 1. 立即创建 pending 任务并返回
...
...
@@ -97,11 +128,98 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService {
// 2. 后台异步执行(如有参考图则跳过 Seedream)
List
<
String
>
keys
=
preImageKeys
!=
null
?
preImageKeys
:
java
.
util
.
Collections
.
emptyList
();
asyncProcessor
.
processTextToVideo
(
task
.
getId
(),
tenantId
,
projectId
,
imagePrompt
,
videoPrompt
!=
null
?
videoPrompt
:
imagePrompt
,
keys
,
durationSeconds
);
asyncProcessor
.
processTextToVideo
(
task
.
getId
(),
tenantId
,
userId
,
projectId
,
imagePrompt
,
videoPrompt
!=
null
?
videoPrompt
:
imagePrompt
,
keys
,
durationSeconds
,
"16:9"
,
true
,
null
);
return
task
;
}
@Override
public
AiTask
submitStructuredVideoTask
(
Long
projectId
,
Long
tenantId
,
StructuredVideoSubmission
s
)
{
// 角色:优先使用 characterImageKeys 列表,回退到旧单图字段
List
<
String
>
characterKeys
=
s
.
getCharacterImageKeys
();
if
(
characterKeys
==
null
||
characterKeys
.
isEmpty
())
{
if
(
s
.
getCharacterImageKey
()
==
null
||
s
.
getCharacterImageKey
().
isBlank
())
{
throw
new
BizException
(
ErrorCode
.
INVALID_PARAM
,
"角色图不能为空"
);
}
characterKeys
=
java
.
util
.
List
.
of
(
s
.
getCharacterImageKey
());
}
if
(
s
.
getSceneImageKey
()
==
null
||
s
.
getSceneImageKey
().
isBlank
())
{
throw
new
BizException
(
ErrorCode
.
INVALID_PARAM
,
"场景图不能为空"
);
}
// 1. 拼装 final prompt + 图片顺序
int
propCount
=
s
.
getPropImageKeys
()
!=
null
?
s
.
getPropImageKeys
().
size
()
:
0
;
boolean
hasStyle
=
s
.
getStyleImageKey
()
!=
null
&&
!
s
.
getStyleImageKey
().
isBlank
();
String
finalPrompt
;
if
(
s
.
getFreeformPrompt
()
!=
null
&&
!
s
.
getFreeformPrompt
().
isBlank
())
{
finalPrompt
=
SeedancePromptBuilder
.
buildPromptFreeform
(
s
.
getFreeformPrompt
(),
characterKeys
.
size
(),
propCount
,
hasStyle
);
}
else
{
finalPrompt
=
SeedancePromptBuilder
.
buildPrompt
(
new
SeedancePromptBuilder
.
UserPromptParts
(
s
.
getCharacterAction
(),
s
.
getSceneEvent
(),
s
.
getCameraMovement
(),
s
.
getLightingAtmosphere
(),
s
.
getVideoStyle
()
),
characterKeys
.
size
(),
propCount
,
hasStyle
);
}
List
<
String
>
orderedKeys
=
SeedancePromptBuilder
.
buildOrderedImageKeys
(
characterKeys
,
s
.
getSceneImageKey
(),
s
.
getPropImageKeys
(),
s
.
getStyleImageKey
()
);
// 2. 创建 pending 任务(落库结构化字段)
AiTask
task
=
new
AiTask
();
task
.
setTenantId
(
tenantId
);
task
.
setProjectId
(
projectId
);
task
.
setEpisodeId
(
s
.
getEpisodeId
());
task
.
setStoryboardId
(
s
.
getStoryboardId
());
task
.
setTaskType
(
"structured_video"
);
task
.
setStatus
(
"pending"
);
task
.
setPrompt
(
finalPrompt
);
task
.
setCharacterImageKey
(
characterKeys
.
get
(
0
));
task
.
setCharacterImageKeys
(
toJson
(
characterKeys
));
task
.
setSceneImageKey
(
s
.
getSceneImageKey
());
task
.
setPropImageKeys
(
toJson
(
s
.
getPropImageKeys
()));
task
.
setStyleImageKey
(
s
.
getStyleImageKey
());
task
.
setUserPrompt
(
toJson
(
s
.
getUserPromptRaw
()));
task
.
setVideoDuration
(
s
.
getDurationSeconds
());
task
.
setVideoRatio
(
s
.
getRatio
());
task
.
setGenerateAudio
(
Boolean
.
TRUE
.
equals
(
s
.
getGenerateAudio
()));
aiTaskMapper
.
insert
(
task
);
log
.
info
(
"Structured video task created: id={}, characters={}, images={}, duration={}s, ratio={}"
,
task
.
getId
(),
characterKeys
.
size
(),
orderedKeys
.
size
(),
s
.
getDurationSeconds
(),
s
.
getRatio
());
// 3. 复用 async processor:将拼接好的 prompt + 有序 key 列表交给后台
int
duration
=
s
.
getDurationSeconds
()
!=
null
&&
s
.
getDurationSeconds
()
>
0
?
s
.
getDurationSeconds
()
:
15
;
String
ratio
=
s
.
getRatio
()
!=
null
&&
!
s
.
getRatio
().
isBlank
()
?
s
.
getRatio
()
:
"16:9"
;
boolean
generateAudio
=
Boolean
.
TRUE
.
equals
(
s
.
getGenerateAudio
());
asyncProcessor
.
processTextToVideo
(
task
.
getId
(),
tenantId
,
s
.
getUserId
(),
projectId
,
finalPrompt
,
finalPrompt
,
orderedKeys
,
duration
,
ratio
,
generateAudio
,
s
.
getModel
());
return
task
;
}
private
static
String
toJson
(
Object
value
)
{
if
(
value
==
null
)
return
null
;
if
(
value
instanceof
List
<?>
list
&&
list
.
isEmpty
())
return
null
;
try
{
return
OBJECT_MAPPER
.
writeValueAsString
(
value
);
}
catch
(
JsonProcessingException
e
)
{
log
.
warn
(
"Failed to serialize structured video field: {}"
,
e
.
getMessage
());
return
null
;
}
}
@Override
public
void
deleteTask
(
Long
taskId
,
Long
tenantId
)
{
AiTask
task
=
aiTaskMapper
.
selectById
(
taskId
);
...
...
yaoai-comic-studio/yaoai-security/src/main/java/com/yaoai/security/config/SaTokenConfigure.java
View file @
21e68a93
...
...
@@ -22,6 +22,7 @@ public class SaTokenConfigure implements WebMvcConfigurer {
"/actuator/**"
,
"/ping"
,
"/system/**"
,
"/admin-api/**"
,
"/agent/runs/*/events"
// SSE: token 通过 query param 传入,由 controller 手动校验
};
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment