From 239c1bcd37b5d9cccde58e8831c377273391ae4a Mon Sep 17 00:00:00 2001 From: dyhhhhh <63825522+dyhhhhhh@users.noreply.github.com> Date: Fri, 14 Aug 2026 11:54:34 +0800 Subject: [PATCH] =?UTF-8?q?fix(prompt):=20=E8=A7=92=E8=89=B2=E6=8F=90?= =?UTF-8?q?=E5=8F=96=20-=20=E8=A7=A3=E6=9E=90=E4=BB=A3=E7=A0=81=E5=9D=97?= =?UTF-8?q?=E4=B8=8E=E5=89=8D=E7=BC=80=E4=B8=AD=E7=9A=84=E6=9C=89=E6=95=88?= =?UTF-8?q?=20JSON?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dyhhhhhqwer <1613203335@qq.com> Original-PR: https://github.com/ddcat-ai/open-ai-canvas/pull/221 --- .../service/prompt_template_json_test.go | 27 ++++++++ backend/internal/service/service.go | 65 ++++++++++++++++--- 2 files changed, 83 insertions(+), 9 deletions(-) create mode 100644 backend/internal/service/prompt_template_json_test.go diff --git a/backend/internal/service/prompt_template_json_test.go b/backend/internal/service/prompt_template_json_test.go new file mode 100644 index 000000000..79d76bf93 --- /dev/null +++ b/backend/internal/service/prompt_template_json_test.go @@ -0,0 +1,27 @@ +package service + +import "testing" + +func TestExtractJSONTextSkipsProseAndMarkdownFence(t *testing.T) { + raw := "先说明一下,{这不是有效 JSON}。\n```json\n{\"characters\":[{\"name\":\"林夏\",\"aliases\":[]}]}\n```\n" + got, err := extractJSONText(raw) + if err != nil { + t.Fatalf("extractJSONText() error = %v", err) + } + want := `{"characters":[{"name":"林夏","aliases":[]}]}` + if got != want { + t.Fatalf("extractJSONText() = %q, want %q", got, want) + } +} + +func TestExtractJSONTextHandlesBracesInJSONString(t *testing.T) { + raw := "```json\n{\"characters\":[{\"name\":\"林夏\",\"role\":\"拿着{小夜灯}的租客\"}]}\n```" + got, err := extractJSONText(raw) + if err != nil { + t.Fatalf("extractJSONText() error = %v", err) + } + want := `{"characters":[{"name":"林夏","role":"拿着{小夜灯}的租客"}]}` + if got != want { + t.Fatalf("extractJSONText() = %q, want %q", got, want) + } +} diff --git a/backend/internal/service/service.go b/backend/internal/service/service.go index b601072a6..ec4b59598 100644 --- a/backend/internal/service/service.go +++ b/backend/internal/service/service.go @@ -1490,16 +1490,63 @@ func storyboardCameraMovementCount(value string) int { } func extractJSONText(raw string) (string, error) { - trimmed := strings.TrimSpace(raw) - trimmed = strings.TrimPrefix(trimmed, "```json") - trimmed = strings.TrimPrefix(trimmed, "```") - trimmed = strings.TrimSuffix(trimmed, "```") - start := strings.Index(trimmed, "{") - end := strings.LastIndex(trimmed, "}") - if start < 0 || end < start { - return "", errors.New("模型返回的不是 JSON") + // Text models may prepend an explanation or append a Markdown fence despite + // the JSON-only contract. Scan complete JSON values instead of pairing the + // first opening brace with the final closing brace, which can merge prose and + // make an otherwise valid character breakdown fail validation. + for start := 0; start < len(raw); start++ { + if raw[start] != '{' && raw[start] != '[' { + continue + } + end := jsonValueEnd(raw, start) + if end < start { + continue + } + candidate := raw[start : end+1] + var decoded interface{} + if json.Unmarshal([]byte(candidate), &decoded) == nil { + return candidate, nil + } } - return trimmed[start : end+1], nil + return "", errors.New("模型返回的不是 JSON") +} + +func jsonValueEnd(source string, start int) int { + stack := make([]byte, 0, 8) + inString := false + escaped := false + for index := start; index < len(source); index++ { + value := source[index] + if inString { + if escaped { + escaped = false + } else if value == '\\' { + escaped = true + } else if value == '"' { + inString = false + } + continue + } + switch value { + case '"': + inString = true + case '{', '[': + stack = append(stack, value) + case '}', ']': + if len(stack) == 0 { + return -1 + } + opener := stack[len(stack)-1] + if (value == '}' && opener != '{') || (value == ']' && opener != '[') { + return -1 + } + stack = stack[:len(stack)-1] + if len(stack) == 0 { + return index + } + } + } + return -1 } func (s *Service) buildAgentStoryboardResult(task model.Task, plan agentStoryboardPlan, assets []storyboardAsset, projectStyle storyboardProjectStyle) (map[string]interface{}, []map[string]interface{}, error) {