Files
ttstd-infinite-canvas/backend/internal/prompts/json_extract.go

111 lines
3.0 KiB
Go

package prompts
import (
"encoding/json"
"errors"
)
func extractJSONText(raw string) (string, error) {
// 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 "", errors.New("模型返回的不是 JSON")
}
// ExtractJSONText 从模型正文中抽出第一个完整 JSON 值。
func ExtractJSONText(raw string) (string, error) {
return extractJSONText(raw)
}
// ExtractPreferredJSONText 优先返回包含 preferKey 的顶层 JSON 对象。
func ExtractPreferredJSONText(raw string, preferKey string) (string, error) {
return extractPreferredJSONText(raw, preferKey)
}
// extractPreferredJSONText 与 extractJSONText 一样逐个扫描完整的 JSON 值,但会优先返回顶层对象且
// 包含 preferKey 的候选。模型常在给出契约对象前先用正文列举一遍内容(例如先写一段角色名数组),
// 只取第一个可解析值会命中这些旁枝片段,导致后续校验拿到完全无关的结构。
// 找不到偏好候选时回退到第一个可解析值,保证与 extractJSONText 的默认行为一致。
func extractPreferredJSONText(raw string, preferKey string) (string, error) {
fallback := ""
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 {
continue
}
if obj, ok := decoded.(map[string]interface{}); ok {
if _, ok := obj[preferKey]; ok {
return candidate, nil
}
}
if fallback == "" {
fallback = candidate
}
}
if fallback != "" {
return fallback, 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
}