feat(*): 创作工作台 - 完善媒体上传预览与连线交互并修复任务章节及日志筛选

This commit is contained in:
ddCat committed 2026-09-08 17:26:34 +08:00
1 parent 95b004ec0f
commit 068a8daea6
56 files changed
+3316 -274

No files matched your search

+14 -7
View File
@@ -103,11 +103,12 @@ type AnalyticsFailureRow struct {
type APICallLogQuery struct {
AnalyticsQuery
Keyword string
Status string
IDs []string
Page int
Limit int
RecordType string
Keyword string
Status string
IDs []string
Page int
Limit int
}
type APICallLogPage struct {
@@ -189,8 +190,11 @@ func (s *Service) AdminAPICallLogs(actor *model.User, query APICallLogQuery) (*A
if err := s.RequireAdmin(actor); err != nil {
return nil, err
}
if query.RecordType != "" && query.RecordType != "request" && query.RecordType != "download" && query.RecordType != "all" {
return nil, BadAuthRequest("请求明细类型无效")
}
filter := normalizeAnalyticsFilter(query.AnalyticsQuery)
logs, total, err := s.repo.QueryAPICallLogs(repository.APICallLogFilter{AnalyticsFilter: filter, Keyword: query.Keyword, Status: query.Status, Page: query.Page, Limit: query.Limit})
logs, total, err := s.repo.QueryAPICallLogs(repository.APICallLogFilter{AnalyticsFilter: filter, RecordType: query.RecordType, Keyword: query.Keyword, Status: query.Status, Page: query.Page, Limit: query.Limit})
if err != nil {
return nil, err
}
@@ -378,6 +382,9 @@ func (s *Service) AdminAPICallLogsCSV(actor *model.User, query APICallLogQuery)
if err := s.RequireAdmin(actor); err != nil {
return nil, err
}
if query.RecordType != "" && query.RecordType != "request" && query.RecordType != "download" && query.RecordType != "all" {
return nil, BadAuthRequest("请求明细类型无效")
}
filter := normalizeAnalyticsFilter(query.AnalyticsQuery)
ids := uniqueNonEmpty(query.IDs)
if len(query.IDs) > 0 && len(ids) == 0 {
@@ -386,7 +393,7 @@ func (s *Service) AdminAPICallLogsCSV(actor *model.User, query APICallLogQuery)
if len(ids) > 200 {
return nil, BadAuthRequest("单次最多导出 200 条已选请求明细")
}
logs, err := s.repo.ExportAPICallLogs(repository.APICallLogFilter{AnalyticsFilter: filter, Keyword: query.Keyword, Status: query.Status, IDs: ids}, 10_000)
logs, err := s.repo.ExportAPICallLogs(repository.APICallLogFilter{AnalyticsFilter: filter, RecordType: query.RecordType, Keyword: query.Keyword, Status: query.Status, IDs: ids}, 10_000)
if err != nil {
return nil, err
}
@@ -0,0 +1,27 @@
package service
import (
"encoding/json"
"testing"
)
func TestChapterAssetsPromptContract(t *testing.T) {
if !json.Valid([]byte(chapterAssetsJSONSchema)) {
t.Fatal("invalid chapter assets schema")
}
for _, tc := range []struct {
text string
valid bool
}{
{`{"characters":[],"scenes":[{"name":"车站","description":"空旷站台","prompt":"夜间站台"}],"props":[{"name":"信封","description":"牛皮纸信封","prompt":"信封特写"}]}`, true},
{`{"characters":[],"scenes":[],"props":[]}`, true},
{`{"characters":[]}`, false},
{`{"characters":[],"scenes":[{"name":"车站"}],"props":[]}`, false},
{`{"characters":[{"name":"张三"}],"scenes":[],"props":[]}`, false},
} {
err := validatePromptTemplateResult(promptOperationChapterAssetsExtract, map[string]interface{}{"text": tc.text})
if (err == nil) != tc.valid {
t.Fatalf("valid=%v err=%v", tc.valid, err)
}
}
}
+28 -1
View File
@@ -382,13 +382,40 @@ func validatePromptTemplateResult(operation string, result map[string]interface{
extractor := extractJSONText
// 角色卡契约要求顶层对象,模型正文里若先出现数组(例如先列一遍角色名)会让通用提取命中错误片段,
// 因此这里优先挑出带 characters 键的对象候选。
if operation == promptOperationCharacterExtract {
if operation == promptOperationCharacterExtract || operation == promptOperationChapterAssetsExtract {
extractor = func(raw string) (string, error) { return extractPreferredJSONText(raw, "characters") }
}
jsonText, err := extractor(text)
if err != nil {
return fmt.Errorf("%s 返回内容不符合受保护 JSON 契约:%w", definition.Label, err)
}
if operation == promptOperationChapterAssetsExtract {
var assets struct {
Characters *[]map[string]interface{} `json:"characters"`
Scenes *[]map[string]interface{} `json:"scenes"`
Props *[]map[string]interface{} `json:"props"`
}
if err := json.Unmarshal([]byte(jsonText), &assets); err != nil {
return fmt.Errorf("章节资产提取 JSON 无法解析:%w", err)
}
if assets.Characters == nil || assets.Scenes == nil || assets.Props == nil {
return errors.New("章节资产提取结果必须包含 characters、scenes 和 props 数组")
}
for _, group := range [][]map[string]interface{}{*assets.Scenes, *assets.Props} {
for _, asset := range group {
for _, field := range []string{"name", "description", "prompt"} {
value, ok := asset[field].(string)
if !ok || strings.TrimSpace(value) == "" {
return fmt.Errorf("章节场景或道具缺少有效的 %s", field)
}
}
}
}
if len(*assets.Characters) > 0 {
return validatePromptTemplateResult(promptOperationCharacterExtract, map[string]interface{}{"text": jsonText})
}
return nil
}
if operation != promptOperationCharacterExtract {
return nil
}
@@ -11,6 +11,7 @@ const (
promptOperationStoryboardFirstFrame = "storyboard_first_frame"
promptOperationStoryboardVideo = "storyboard_video"
promptOperationCharacterExtract = "character_extract"
promptOperationChapterAssetsExtract = "chapter_assets_extract"
promptOperationCharacterTurnaround = "character_turnaround"
)
@@ -18,6 +19,12 @@ const legacyStoryboardVideoPromptPreamble = "生成单一连续镜头的视频
func defaultPromptDefinitions() []PromptOperationDefinition {
return []PromptOperationDefinition{
{
Operation: promptOperationChapterAssetsExtract, Label: "章节角色、场景与道具提取", Category: "角色", OutputType: "json", SchemaKey: "chapter-assets/v1",
Description: "从章节正文提取角色、实际发生剧情的场景和影响剧情的道具,分别进入资产待确认列表。",
Variables: []PromptTemplateVariable{{Label: "项目名称", Placeholder: "{{项目名称}}"}, {Label: "章节名称", Placeholder: "{{章节名称}}"}, {Label: "项目画风", Placeholder: "{{项目画风}}"}},
DefaultContent: `你是短剧资产导演。按章节事实提取角色、场景和道具。同一身份的别名合并,不编造正文未出现的资产;未明确的视觉信息写“正文未明确”。角色包含剧情定位、稳定外貌服装体态、性格、一致性提示、三视图提示及语言、声音年龄、音色。场景描述空间布局、环境和光照;道具描述材质、形状及剧情用途。无对应资产时返回空数组。`,
},
{
Operation: promptOperationStoryboardPlan, Label: "分镜规划", Category: "分镜", OutputType: "json", SchemaKey: "storyboard-plan/v3",
Description: "把剧情、项目画风和当前角色版本规划为可执行镜头。",
@@ -105,6 +112,13 @@ func protectedPromptContext(operation string, values map[string]string) string {
return storyboardRepairProtectedContext(values)
case promptOperationCharacterExtract:
return characterExtractProtectedContext(values)
case promptOperationChapterAssetsExtract:
return strings.Join([]string{
fmt.Sprintf("【任务】\n从短剧项目《%s》的章节“%s”提取角色、场景和道具。", values["项目名称"], values["章节名称"]),
"【项目画风】\n" + values["项目画风"],
"【章节正文】\n" + values["章节正文"],
"【受保护输出契约】\n" + promptOutputContract(operation),
}, "\n\n")
case promptOperationCharacterTurnaround:
return strings.Join([]string{"【角色名称】\n" + values["角色名称"], "【项目画风】\n" + values["项目画风"], "【角色设定】\n" + values["角色设定"]}, "\n\n")
default:
@@ -118,6 +132,8 @@ func promptOutputContract(operation string) string {
return "服务端固定 JSON Schema storyboard-plan/v3(不可由运营模板或用户定制覆盖):\n" + storyboardPlanJSONSchema
case promptOperationCharacterExtract:
return "服务端固定 JSON Schema character-breakdown/v1(不可由运营模板或用户定制覆盖):\n" + characterBreakdownJSONSchema
case promptOperationChapterAssetsExtract:
return "服务端固定 JSON Schema chapter-assets/v1(不可由运营模板或用户定制覆盖):\n" + chapterAssetsJSONSchema
default:
return "当前操作输出普通文本提示词,没有 JSON Schema。"
}
@@ -182,6 +198,24 @@ const storyboardPlanJSONSchema = `{
}
}`
const chapterAssetsJSONSchema = `{
"type": "object", "additionalProperties": false,
"required": ["characters", "scenes", "props"],
"properties": {
"characters": {"$ref": "#/$defs/characterBreakdown/properties/characters"},
"scenes": {"type": "array", "items": {"$ref": "#/$defs/asset"}},
"props": {"type": "array", "items": {"$ref": "#/$defs/asset"}}
},
"$defs": {
"asset": {
"type": "object", "additionalProperties": false,
"required": ["name", "description", "prompt"],
"properties": {"name": {"type": "string"}, "description": {"type": "string"}, "prompt": {"type": "string"}}
},
"characterBreakdown": ` + characterBreakdownJSONSchema + `
}
}`
const characterBreakdownJSONSchema = `{
"type": "object",
"additionalProperties": false,