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
69bfd17b
Commit
69bfd17b
authored
Apr 28, 2026
by
yaoke.yk
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Agent制作优化上下文错误和剧本集数可修改
parent
824a0e6d
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
411 additions
and
34 deletions
+411
-34
ProjectDetail.tsx
doc/html/src/app/pages/ProjectDetail.tsx
+253
-30
useAi.ts
doc/html/src/hooks/useAi.ts
+18
-1
ai.ts
doc/html/src/lib/api/ai.ts
+8
-0
Dockerfile
yaoai-comic-studio/Dockerfile
+5
-1
AgentSseManager.java
...nt/src/main/java/com/yaoai/agent/sse/AgentSseManager.java
+43
-2
OutlineController.java
...main/java/com/yaoai/api/controller/OutlineController.java
+20
-0
GlobalExceptionHandler.java
...in/java/com/yaoai/api/handler/GlobalExceptionHandler.java
+7
-0
logback-spring.xml
...dio/yaoai-bootstrap/src/main/resources/logback-spring.xml
+1
-0
OutlinePipelineService.java
...va/com/yaoai/pipeline/service/OutlinePipelineService.java
+10
-0
OutlinePipelineServiceImpl.java
...oai/pipeline/service/impl/OutlinePipelineServiceImpl.java
+46
-0
No files found.
doc/html/src/app/pages/ProjectDetail.tsx
View file @
69bfd17b
...
...
@@ -13,9 +13,19 @@ import {
Users
,
Clapperboard
,
Video
,
Plus
,
X
,
}
from
"lucide-react"
;
import
{
useProject
}
from
"../../hooks/useProjects"
;
import
{
useOutline
,
useEpisodes
,
useCharacters
,
useScenes
,
useVideoTasks
}
from
"../../hooks/useAi"
;
import
{
useOutline
,
useEpisodes
,
useCharacters
,
useScenes
,
useVideoTasks
,
useCreateEpisode
,
useUpdateEpisode
,
}
from
"../../hooks/useAi"
;
type
Tab
=
"outline"
|
"characters"
|
"storyboard"
|
"video"
;
...
...
@@ -216,43 +226,256 @@ function OutlineTab({
</
div
>
{
/* Episode List */
}
{
episodes
.
length
>
0
&&
(
<
div
>
<
h2
className=
"text-lg font-semibold text-foreground mb-4"
>
分集列表 (
{
episodes
.
length
}
集)
</
h2
>
<
div
className=
"grid grid-cols-3 gap-4"
>
{
episodes
.
map
((
ep
)
=>
(
<
div
key=
{
ep
.
id
}
onClick=
{
()
=>
onNavigateEpisode
(
ep
.
id
)
}
className=
"rounded-xl border border-border bg-card p-4 hover:border-primary transition-colors cursor-pointer"
<
EpisodeListSection
episodes=
{
episodes
}
projectId=
{
projectId
}
outlineExists=
{
!!
outline
}
onNavigateEpisode=
{
onNavigateEpisode
}
onNavigateOutline=
{
onNavigateOutline
}
/>
</
div
>
);
}
// ---- Episode List with Edit/Create ----
type
EpisodeDraft
=
{
id
?:
string
;
episodeNumber
:
number
;
title
:
string
;
summary
:
string
;
script
:
string
;
};
function
EpisodeListSection
({
episodes
,
projectId
,
outlineExists
,
onNavigateEpisode
,
onNavigateOutline
,
}:
{
episodes
:
import
(
"../../lib/api/ai"
).
Episode
[];
projectId
?:
string
;
outlineExists
:
boolean
;
onNavigateEpisode
:
(
epId
:
string
)
=>
void
;
onNavigateOutline
:
()
=>
void
;
})
{
const
pid
=
projectId
??
""
;
const
createMutation
=
useCreateEpisode
(
pid
);
const
updateMutation
=
useUpdateEpisode
(
pid
);
const
[
draft
,
setDraft
]
=
useState
<
EpisodeDraft
|
null
>
(
null
);
const
nextNumber
=
episodes
.
length
===
0
?
1
:
Math
.
max
(...
episodes
.
map
((
e
)
=>
e
.
episodeNumber
||
0
))
+
1
;
const
openCreate
=
()
=>
setDraft
({
episodeNumber
:
nextNumber
,
title
:
""
,
summary
:
""
,
script
:
""
});
const
openEdit
=
(
ep
:
import
(
"../../lib/api/ai"
).
Episode
)
=>
setDraft
({
id
:
ep
.
id
,
episodeNumber
:
ep
.
episodeNumber
,
title
:
ep
.
title
??
""
,
summary
:
ep
.
summary
??
""
,
script
:
ep
.
script
??
""
,
});
const
submit
=
async
()
=>
{
if
(
!
draft
)
return
;
if
(
!
draft
.
episodeNumber
||
draft
.
episodeNumber
<=
0
)
return
;
const
payload
=
{
episodeNumber
:
draft
.
episodeNumber
,
title
:
draft
.
title
,
summary
:
draft
.
summary
,
script
:
draft
.
script
,
};
if
(
draft
.
id
)
{
await
updateMutation
.
mutateAsync
({
id
:
draft
.
id
,
patch
:
payload
});
}
else
{
await
createMutation
.
mutateAsync
(
payload
);
}
setDraft
(
null
);
};
if
(
episodes
.
length
===
0
)
{
return
(
<>
{
outlineExists
&&
(
<
div
className=
"rounded-xl border border-dashed border-border bg-card p-8 text-center"
>
<
p
className=
"text-sm text-muted-foreground mb-4"
>
大纲已生成,可由 AI 生成分集,或手动新增
</
p
>
<
div
className=
"flex items-center gap-3 justify-center"
>
<
button
onClick=
{
onNavigateOutline
}
className=
"px-5 py-2 rounded-lg bg-gradient-to-r from-[#5b5ff9] to-[#8b5ff9] text-white hover:shadow-md transition-all text-sm flex items-center gap-2"
>
<
div
className=
"flex items-center gap-2 mb-2"
>
<
span
className=
"text-xs font-medium text-muted-foreground bg-accent px-2 py-0.5 rounded"
>
第
{
ep
.
episodeNumber
}
集
</
span
>
</
div
>
<
h3
className=
"font-medium text-foreground text-sm mb-1"
>
{
ep
.
title
}
</
h3
>
<
p
className=
"text-xs text-muted-foreground line-clamp-3"
>
{
ep
.
summary
}
</
p
>
</
div
>
))
}
<
Sparkles
className=
"w-4 h-4"
/>
生成分集
</
button
>
<
button
onClick=
{
openCreate
}
className=
"px-5 py-2 rounded-lg border border-border bg-card text-foreground hover:bg-muted transition-all text-sm flex items-center gap-2"
>
<
Plus
className=
"w-4 h-4"
/>
手动新增一集
</
button
>
</
div
>
</
div
>
</
div
>
)
}
{
draft
&&
(
<
EpisodeEditModal
draft=
{
draft
}
setDraft=
{
setDraft
}
onClose=
{
()
=>
setDraft
(
null
)
}
onSubmit=
{
submit
}
saving=
{
createMutation
.
isPending
||
updateMutation
.
isPending
}
/>
)
}
</>
);
}
return
(
<
div
>
<
div
className=
"flex items-center justify-between mb-4"
>
<
h2
className=
"text-lg font-semibold text-foreground"
>
分集列表 (
{
episodes
.
length
}
集)
</
h2
>
<
button
onClick=
{
openCreate
}
className=
"text-sm text-primary hover:underline flex items-center gap-1"
>
<
Plus
className=
"w-4 h-4"
/>
新增一集
</
button
>
</
div
>
<
div
className=
"grid grid-cols-3 gap-4"
>
{
episodes
.
map
((
ep
)
=>
(
<
div
key=
{
ep
.
id
}
onClick=
{
()
=>
onNavigateEpisode
(
ep
.
id
)
}
className=
"relative rounded-xl border border-border bg-card p-4 hover:border-primary transition-colors cursor-pointer"
>
<
button
onClick=
{
(
e
)
=>
{
e
.
stopPropagation
();
openEdit
(
ep
);
}
}
className=
"absolute top-2 right-2 p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
title=
"编辑本集"
>
<
Edit2
className=
"w-3.5 h-3.5"
/>
</
button
>
<
div
className=
"flex items-center gap-2 mb-2"
>
<
span
className=
"text-xs font-medium text-muted-foreground bg-accent px-2 py-0.5 rounded"
>
第
{
ep
.
episodeNumber
}
集
</
span
>
</
div
>
<
h3
className=
"font-medium text-foreground text-sm mb-1 pr-6"
>
{
ep
.
title
}
</
h3
>
<
p
className=
"text-xs text-muted-foreground line-clamp-3"
>
{
ep
.
summary
}
</
p
>
</
div
>
))
}
</
div
>
{
draft
&&
(
<
EpisodeEditModal
draft=
{
draft
}
setDraft=
{
setDraft
}
onClose=
{
()
=>
setDraft
(
null
)
}
onSubmit=
{
submit
}
saving=
{
createMutation
.
isPending
||
updateMutation
.
isPending
}
/>
)
}
</
div
>
);
}
{
episodes
.
length
===
0
&&
outline
&&
(
<
div
className=
"rounded-xl border border-dashed border-border bg-card p-8 text-center"
>
<
p
className=
"text-sm text-muted-foreground mb-4"
>
大纲已生成,点击前往生成分集内容
</
p
>
function
EpisodeEditModal
({
draft
,
setDraft
,
onClose
,
onSubmit
,
saving
,
}:
{
draft
:
EpisodeDraft
;
setDraft
:
(
d
:
EpisodeDraft
)
=>
void
;
onClose
:
()
=>
void
;
onSubmit
:
()
=>
void
;
saving
:
boolean
;
})
{
const
isEdit
=
!!
draft
.
id
;
return
(
<
div
className=
"fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center z-50 p-6"
>
<
div
className=
"bg-card rounded-2xl border border-border w-full max-w-xl max-h-[90vh] flex flex-col"
>
<
div
className=
"flex items-center justify-between p-5 border-b border-border flex-shrink-0"
>
<
h3
className=
"text-base font-semibold text-foreground"
>
{
isEdit
?
"编辑分集"
:
"新增分集"
}
</
h3
>
<
button
onClick=
{
on
NavigateOutlin
e
}
className=
"p
x-5 py-2 rounded-lg bg-gradient-to-r from-[#5b5ff9] to-[#8b5ff9] text-white hover:shadow-md transition-all text-sm flex items-center gap-2 mx-auto
"
onClick=
{
on
Clos
e
}
className=
"p
-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors
"
>
<
Sparkles
className=
"w-4 h-4"
/>
生成分集
<
X
className=
"w-4 h-4"
/>
</
button
>
</
div
>
)
}
<
div
className=
"p-5 space-y-4 overflow-auto"
>
<
div
>
<
label
className=
"block text-xs font-medium text-foreground mb-1.5"
>
集数
</
label
>
<
input
type=
"number"
min=
{
1
}
value=
{
draft
.
episodeNumber
}
onChange=
{
(
e
)
=>
setDraft
({
...
draft
,
episodeNumber
:
Number
(
e
.
target
.
value
)
||
0
})
}
className=
"w-32 px-3 py-2 rounded-lg border border-border bg-background text-foreground text-sm focus:outline-none focus:border-primary"
/>
<
p
className=
"text-xs text-muted-foreground mt-1"
>
允许重复,请自行确保唯一
</
p
>
</
div
>
<
div
>
<
label
className=
"block text-xs font-medium text-foreground mb-1.5"
>
本集标题
</
label
>
<
input
type=
"text"
value=
{
draft
.
title
}
onChange=
{
(
e
)
=>
setDraft
({
...
draft
,
title
:
e
.
target
.
value
})
}
className=
"w-full px-3 py-2 rounded-lg border border-border bg-background text-foreground text-sm focus:outline-none focus:border-primary"
/>
</
div
>
<
div
>
<
label
className=
"block text-xs font-medium text-foreground mb-1.5"
>
剧情摘要
</
label
>
<
textarea
value=
{
draft
.
summary
}
onChange=
{
(
e
)
=>
setDraft
({
...
draft
,
summary
:
e
.
target
.
value
})
}
rows=
{
3
}
className=
"w-full px-3 py-2 rounded-lg border border-border bg-background text-foreground text-sm focus:outline-none focus:border-primary resize-none"
/>
</
div
>
<
div
>
<
label
className=
"block text-xs font-medium text-foreground mb-1.5"
>
本集剧本正文
</
label
>
<
textarea
value=
{
draft
.
script
}
onChange=
{
(
e
)
=>
setDraft
({
...
draft
,
script
:
e
.
target
.
value
})
}
rows=
{
8
}
className=
"w-full px-3 py-2 rounded-lg border border-border bg-background text-foreground text-sm focus:outline-none focus:border-primary resize-none font-mono"
/>
</
div
>
</
div
>
<
div
className=
"flex items-center justify-end gap-2 p-5 border-t border-border flex-shrink-0"
>
<
button
onClick=
{
onClose
}
className=
"px-4 py-2 rounded-lg border border-border text-foreground hover:bg-muted transition-colors text-sm"
>
取消
</
button
>
<
button
onClick=
{
onSubmit
}
disabled=
{
saving
||
!
draft
.
episodeNumber
}
className=
"px-4 py-2 rounded-lg bg-gradient-to-r from-[#5b5ff9] to-[#8b5ff9] text-white hover:shadow-md transition-all text-sm flex items-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed"
>
{
saving
&&
<
Loader2
className=
"w-3.5 h-3.5 animate-spin"
/>
}
保存
</
button
>
</
div
>
</
div
>
</
div
>
);
}
...
...
doc/html/src/hooks/useAi.ts
View file @
69bfd17b
import
{
useMutation
,
useQuery
,
useQueryClient
}
from
"@tanstack/react-query"
;
import
{
aiApi
}
from
"../lib/api/ai"
;
import
type
{
Character
,
Scene
,
Storyboard
,
StructuredVideoRequest
}
from
"../lib/api/ai"
;
import
type
{
Character
,
Episode
,
Scene
,
Storyboard
,
StructuredVideoRequest
}
from
"../lib/api/ai"
;
// ---- Characters ----
const
charactersKey
=
(
pid
:
string
)
=>
[
"characters"
,
pid
];
...
...
@@ -102,6 +102,23 @@ export function useGenerateEpisodes(projectId: string) {
});
}
export
function
useCreateEpisode
(
projectId
:
string
)
{
const
qc
=
useQueryClient
();
return
useMutation
({
mutationFn
:
(
req
:
Partial
<
Episode
>
)
=>
aiApi
.
createEpisode
(
projectId
,
req
),
onSuccess
:
()
=>
qc
.
invalidateQueries
({
queryKey
:
episodesKey
(
projectId
)
}),
});
}
export
function
useUpdateEpisode
(
projectId
:
string
)
{
const
qc
=
useQueryClient
();
return
useMutation
({
mutationFn
:
({
id
,
patch
}:
{
id
:
string
;
patch
:
Partial
<
Episode
>
})
=>
aiApi
.
updateEpisode
(
projectId
,
id
,
patch
),
onSuccess
:
()
=>
qc
.
invalidateQueries
({
queryKey
:
episodesKey
(
projectId
)
}),
});
}
// ---- Storyboards ----
export
function
useStoryboards
(
projectId
:
string
,
episodeId
:
string
)
{
return
useQuery
({
...
...
doc/html/src/lib/api/ai.ts
View file @
69bfd17b
...
...
@@ -160,6 +160,14 @@ export const aiApi = {
const
r
=
await
apiClient
.
get
(
`/projects/
${
projectId
}
/episodes`
);
return
r
.
data
.
data
;
},
createEpisode
:
async
(
projectId
:
string
,
req
:
Partial
<
Episode
>
):
Promise
<
Episode
>
=>
{
const
r
=
await
apiClient
.
post
(
`/projects/
${
projectId
}
/episodes`
,
req
);
return
r
.
data
.
data
;
},
updateEpisode
:
async
(
projectId
:
string
,
episodeId
:
string
,
patch
:
Partial
<
Episode
>
):
Promise
<
Episode
>
=>
{
const
r
=
await
apiClient
.
put
(
`/projects/
${
projectId
}
/episodes/
${
episodeId
}
`
,
patch
);
return
r
.
data
.
data
;
},
// ---- Storyboards ----
createStoryboard
:
async
(
projectId
:
string
,
episodeId
:
string
,
req
:
Partial
<
Storyboard
>
):
Promise
<
Storyboard
>
=>
{
...
...
yaoai-comic-studio/Dockerfile
View file @
69bfd17b
...
...
@@ -35,6 +35,10 @@ LABEL maintainer="YaoAI Team <yaoke251@gmail.com>"
LABEL
org.opencontainers.image.title="YaoAI Comic Studio"
LABEL
org.opencontainers.image.version="0.1.0"
ENV
LANG=C.UTF-8 \
LANGUAGE=C.UTF-8 \
LC_ALL=C.UTF-8
RUN
apt-get update
\
&&
apt-get
install
-y
--no-install-recommends
ffmpeg
\
&&
rm
-rf
/var/lib/apt/lists/
*
...
...
@@ -52,6 +56,6 @@ USER yaoai
EXPOSE
8080
ENV
JAVA_OPTS="-Xms256m -Xmx512m -XX:+UseG1GC -Djava.security.egd=file:/dev/./urandom"
ENV
JAVA_OPTS="-Xms256m -Xmx512m -XX:+UseG1GC -Djava.security.egd=file:/dev/./urandom
-Dfile.encoding=UTF-8 -Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8
"
ENTRYPOINT
["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
yaoai-comic-studio/yaoai-agent/src/main/java/com/yaoai/agent/sse/AgentSseManager.java
View file @
69bfd17b
package
com
.
yaoai
.
agent
.
sse
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
import
jakarta.annotation.PostConstruct
;
import
jakarta.annotation.PreDestroy
;
import
lombok.RequiredArgsConstructor
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.stereotype.Component
;
...
...
@@ -10,21 +12,48 @@ import java.util.List;
import
java.util.Map
;
import
java.util.concurrent.ConcurrentHashMap
;
import
java.util.concurrent.CopyOnWriteArrayList
;
import
java.util.concurrent.Executors
;
import
java.util.concurrent.ScheduledExecutorService
;
import
java.util.concurrent.TimeUnit
;
@Slf4j
@Component
@RequiredArgsConstructor
public
class
AgentSseManager
{
private
static
final
long
HEARTBEAT_INTERVAL_SECONDS
=
15
;
private
final
ObjectMapper
objectMapper
;
private
final
Map
<
Long
,
List
<
SseEmitter
>>
emitters
=
new
ConcurrentHashMap
<>();
private
final
ScheduledExecutorService
heartbeatExecutor
=
Executors
.
newSingleThreadScheduledExecutor
(
r
->
{
Thread
t
=
new
Thread
(
r
,
"agent-sse-heartbeat"
);
t
.
setDaemon
(
true
);
return
t
;
});
@PostConstruct
void
startHeartbeat
()
{
heartbeatExecutor
.
scheduleAtFixedRate
(
this
::
heartbeatAll
,
HEARTBEAT_INTERVAL_SECONDS
,
HEARTBEAT_INTERVAL_SECONDS
,
TimeUnit
.
SECONDS
);
}
@PreDestroy
void
shutdownHeartbeat
()
{
heartbeatExecutor
.
shutdownNow
();
}
public
SseEmitter
subscribe
(
Long
runId
)
{
SseEmitter
emitter
=
new
SseEmitter
(
300_00
0L
);
SseEmitter
emitter
=
new
SseEmitter
(
0L
);
List
<
SseEmitter
>
list
=
emitters
.
computeIfAbsent
(
runId
,
k
->
new
CopyOnWriteArrayList
<>());
list
.
add
(
emitter
);
emitter
.
onCompletion
(()
->
remove
(
runId
,
emitter
));
emitter
.
onTimeout
(()
->
remove
(
runId
,
emitter
));
emitter
.
onTimeout
(()
->
{
log
.
debug
(
"SSE timeout: runId={}"
,
runId
);
remove
(
runId
,
emitter
);
emitter
.
complete
();
});
emitter
.
onError
(
e
->
remove
(
runId
,
emitter
));
log
.
debug
(
"SSE subscribed: runId={}, total={}"
,
runId
,
list
.
size
());
return
emitter
;
...
...
@@ -57,6 +86,18 @@ public class AgentSseManager {
"time"
,
java
.
time
.
LocalTime
.
now
().
toString
().
substring
(
0
,
8
)));
}
private
void
heartbeatAll
()
{
emitters
.
forEach
((
runId
,
list
)
->
list
.
removeIf
(
emitter
->
{
try
{
emitter
.
send
(
SseEmitter
.
event
().
comment
(
"ping"
));
return
false
;
}
catch
(
Exception
e
)
{
log
.
debug
(
"Removed heartbeat-broken SSE emitter for runId={}"
,
runId
);
return
true
;
}
}));
}
private
void
remove
(
Long
runId
,
SseEmitter
emitter
)
{
List
<
SseEmitter
>
list
=
emitters
.
get
(
runId
);
if
(
list
!=
null
)
list
.
remove
(
emitter
);
...
...
yaoai-comic-studio/yaoai-api/src/main/java/com/yaoai/api/controller/OutlineController.java
View file @
69bfd17b
...
...
@@ -5,6 +5,7 @@ import com.yaoai.api.dto.ai.OutlineDTO;
import
com.yaoai.common.exception.BizException
;
import
com.yaoai.common.exception.ErrorCode
;
import
com.yaoai.common.response.ApiResponse
;
import
com.yaoai.domain.entity.Episode
;
import
com.yaoai.domain.entity.Outline
;
import
com.yaoai.domain.mapper.StoryboardMapper
;
import
com.yaoai.pipeline.service.OutlinePipelineService
;
...
...
@@ -87,4 +88,23 @@ public class OutlineController {
.
collect
(
Collectors
.
toList
())
);
}
@Operation
(
summary
=
"手动新增一集"
)
@PostMapping
(
"/episodes"
)
public
ApiResponse
<
EpisodeDTO
>
createEpisode
(
@PathVariable
Long
projectId
,
@RequestBody
Episode
req
)
{
Long
tenantId
=
TenantContext
.
get
();
Episode
ep
=
outlinePipelineService
.
createEpisode
(
projectId
,
tenantId
,
req
);
return
ApiResponse
.
success
(
EpisodeDTO
.
from
(
ep
,
0
));
}
@Operation
(
summary
=
"编辑单集(episodeNumber/title/summary/script)"
)
@PutMapping
(
"/episodes/{episodeId}"
)
public
ApiResponse
<
EpisodeDTO
>
updateEpisode
(
@PathVariable
Long
projectId
,
@PathVariable
Long
episodeId
,
@RequestBody
Episode
req
)
{
Long
tenantId
=
TenantContext
.
get
();
Episode
ep
=
outlinePipelineService
.
updateEpisode
(
projectId
,
tenantId
,
episodeId
,
req
);
return
ApiResponse
.
success
(
EpisodeDTO
.
from
(
ep
,
storyboardMapper
.
countByEpisode
(
ep
.
getId
(),
tenantId
)));
}
}
yaoai-comic-studio/yaoai-api/src/main/java/com/yaoai/api/handler/GlobalExceptionHandler.java
View file @
69bfd17b
...
...
@@ -12,6 +12,7 @@ import org.springframework.web.bind.MethodArgumentNotValidException;
import
org.springframework.web.bind.annotation.ExceptionHandler
;
import
org.springframework.web.bind.annotation.ResponseStatus
;
import
org.springframework.web.bind.annotation.RestControllerAdvice
;
import
org.springframework.web.context.request.async.AsyncRequestTimeoutException
;
import
java.util.stream.Collectors
;
...
...
@@ -56,6 +57,12 @@ public class GlobalExceptionHandler {
return
ApiResponse
.
error
(
ErrorCode
.
INVALID_PARAM
.
getCode
(),
e
.
getMessage
());
}
@ExceptionHandler
(
AsyncRequestTimeoutException
.
class
)
@ResponseStatus
(
HttpStatus
.
NO_CONTENT
)
public
void
handleAsyncTimeout
(
AsyncRequestTimeoutException
e
)
{
log
.
debug
(
"Async request timeout"
,
e
);
}
@ExceptionHandler
(
Exception
.
class
)
@ResponseStatus
(
HttpStatus
.
INTERNAL_SERVER_ERROR
)
public
ApiResponse
<?>
handleException
(
Exception
e
)
{
...
...
yaoai-comic-studio/yaoai-bootstrap/src/main/resources/logback-spring.xml
View file @
69bfd17b
...
...
@@ -43,6 +43,7 @@
<!-- ===== non-local: file only ===== -->
<springProfile
name=
"!local"
>
<root
level=
"INFO"
>
<appender-ref
ref=
"CONSOLE"
/>
<appender-ref
ref=
"ASYNC_FILE"
/>
</root>
</springProfile>
...
...
yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/service/OutlinePipelineService.java
View file @
69bfd17b
...
...
@@ -20,4 +20,14 @@ public interface OutlinePipelineService {
Outline
getOutline
(
Long
projectId
,
Long
tenantId
);
List
<
Episode
>
getEpisodes
(
Long
projectId
,
Long
tenantId
);
/**
* 手动新增一集(不调 LLM)。若 req.episodeNumber 为空,自动取当前最大 +1。
*/
Episode
createEpisode
(
Long
projectId
,
Long
tenantId
,
Episode
req
);
/**
* 手动编辑一集(episodeNumber/title/summary/script,非 null 字段才更新)。
*/
Episode
updateEpisode
(
Long
projectId
,
Long
tenantId
,
Long
episodeId
,
Episode
req
);
}
yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/service/impl/OutlinePipelineServiceImpl.java
View file @
69bfd17b
...
...
@@ -209,6 +209,52 @@ public class OutlinePipelineServiceImpl implements OutlinePipelineService {
return
episodeMapper
.
findByProject
(
projectId
,
tenantId
);
}
@Override
public
Episode
createEpisode
(
Long
projectId
,
Long
tenantId
,
Episode
req
)
{
Outline
outline
=
outlineMapper
.
findLatestByProject
(
projectId
,
tenantId
);
if
(
outline
==
null
)
{
throw
new
BizException
(
ErrorCode
.
NOT_FOUND
,
"请先生成或保存大纲"
);
}
Integer
epNum
=
req
.
getEpisodeNumber
();
if
(
epNum
==
null
||
epNum
<=
0
)
{
int
max
=
episodeMapper
.
findByProject
(
projectId
,
tenantId
).
stream
()
.
mapToInt
(
e
->
e
.
getEpisodeNumber
()
==
null
?
0
:
e
.
getEpisodeNumber
())
.
max
().
orElse
(
0
);
epNum
=
max
+
1
;
}
Episode
ep
=
new
Episode
();
ep
.
setOutlineId
(
outline
.
getId
());
ep
.
setProjectId
(
projectId
);
ep
.
setTenantId
(
tenantId
);
ep
.
setEpisodeNumber
(
epNum
);
ep
.
setTitle
(
req
.
getTitle
()
==
null
?
""
:
req
.
getTitle
());
ep
.
setSummary
(
req
.
getSummary
()
==
null
?
""
:
req
.
getSummary
());
ep
.
setScript
(
req
.
getScript
()
==
null
?
""
:
req
.
getScript
());
ep
.
setStatus
(
"ready"
);
episodeMapper
.
insert
(
ep
);
log
.
info
(
"Episode manually created: id={}, episodeNumber={}"
,
ep
.
getId
(),
epNum
);
return
ep
;
}
@Override
public
Episode
updateEpisode
(
Long
projectId
,
Long
tenantId
,
Long
episodeId
,
Episode
req
)
{
Episode
existing
=
episodeMapper
.
selectById
(
episodeId
);
if
(
existing
==
null
||
!
tenantId
.
equals
(
existing
.
getTenantId
())
||
!
projectId
.
equals
(
existing
.
getProjectId
()))
{
throw
new
BizException
(
ErrorCode
.
NOT_FOUND
,
"分集不存在"
);
}
if
(
req
.
getEpisodeNumber
()
!=
null
&&
req
.
getEpisodeNumber
()
>
0
)
{
existing
.
setEpisodeNumber
(
req
.
getEpisodeNumber
());
}
if
(
req
.
getTitle
()
!=
null
)
existing
.
setTitle
(
req
.
getTitle
());
if
(
req
.
getSummary
()
!=
null
)
existing
.
setSummary
(
req
.
getSummary
());
if
(
req
.
getScript
()
!=
null
)
existing
.
setScript
(
req
.
getScript
());
episodeMapper
.
updateById
(
existing
);
log
.
info
(
"Episode updated: id={}"
,
episodeId
);
return
existing
;
}
private
String
extractJson
(
String
raw
)
{
// strip markdown code fences if present
String
s
=
raw
.
strip
();
...
...
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