520 lines
12 KiB
Go
520 lines
12 KiB
Go
package antigravity
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
)
|
||
|
||
// CleanJSONSchema 清理 JSON Schema,移除 Antigravity/Gemini 不支持的字段
|
||
// 参考 Antigravity-Manager/src-tauri/src/proxy/common/json_schema.rs 实现
|
||
// 确保 schema 符合 JSON Schema draft 2020-12 且适配 Gemini v1internal
|
||
func CleanJSONSchema(schema map[string]any) map[string]any {
|
||
if schema == nil {
|
||
return nil
|
||
}
|
||
// 0. 预处理:展开 $ref (Schema Flattening)
|
||
// (Go map 是引用的,直接修改 schema)
|
||
flattenRefs(schema, extractDefs(schema))
|
||
|
||
// 递归清理
|
||
cleaned := cleanJSONSchemaRecursive(schema)
|
||
result, ok := cleaned.(map[string]any)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
// extractDefs 提取并移除定义的 helper
|
||
func extractDefs(schema map[string]any) map[string]any {
|
||
defs := make(map[string]any)
|
||
if d, ok := schema["$defs"].(map[string]any); ok {
|
||
for k, v := range d {
|
||
defs[k] = v
|
||
}
|
||
delete(schema, "$defs")
|
||
}
|
||
if d, ok := schema["definitions"].(map[string]any); ok {
|
||
for k, v := range d {
|
||
defs[k] = v
|
||
}
|
||
delete(schema, "definitions")
|
||
}
|
||
return defs
|
||
}
|
||
|
||
// flattenRefs 递归展开 $ref
|
||
func flattenRefs(schema map[string]any, defs map[string]any) {
|
||
if len(defs) == 0 {
|
||
return // 无需展开
|
||
}
|
||
|
||
// 检查并替换 $ref
|
||
if ref, ok := schema["$ref"].(string); ok {
|
||
delete(schema, "$ref")
|
||
// 解析引用名 (例如 #/$defs/MyType -> MyType)
|
||
parts := strings.Split(ref, "/")
|
||
refName := parts[len(parts)-1]
|
||
|
||
if defSchema, exists := defs[refName]; exists {
|
||
if defMap, ok := defSchema.(map[string]any); ok {
|
||
// 合并定义内容 (不覆盖现有 key)
|
||
for k, v := range defMap {
|
||
if _, has := schema[k]; !has {
|
||
schema[k] = deepCopy(v) // 需深拷贝避免共享引用
|
||
}
|
||
}
|
||
// 递归处理刚刚合并进来的内容
|
||
flattenRefs(schema, defs)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 遍历子节点
|
||
for _, v := range schema {
|
||
if subMap, ok := v.(map[string]any); ok {
|
||
flattenRefs(subMap, defs)
|
||
} else if subArr, ok := v.([]any); ok {
|
||
for _, item := range subArr {
|
||
if itemMap, ok := item.(map[string]any); ok {
|
||
flattenRefs(itemMap, defs)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// deepCopy 深拷贝 (简单实现,仅针对 JSON 类型)
|
||
func deepCopy(src any) any {
|
||
if src == nil {
|
||
return nil
|
||
}
|
||
switch v := src.(type) {
|
||
case map[string]any:
|
||
dst := make(map[string]any)
|
||
for k, val := range v {
|
||
dst[k] = deepCopy(val)
|
||
}
|
||
return dst
|
||
case []any:
|
||
dst := make([]any, len(v))
|
||
for i, val := range v {
|
||
dst[i] = deepCopy(val)
|
||
}
|
||
return dst
|
||
default:
|
||
return src
|
||
}
|
||
}
|
||
|
||
// cleanJSONSchemaRecursive 递归核心清理逻辑
|
||
// 返回处理后的值 (通常是 input map,但可能修改内部结构)
|
||
func cleanJSONSchemaRecursive(value any) any {
|
||
schemaMap, ok := value.(map[string]any)
|
||
if !ok {
|
||
return value
|
||
}
|
||
|
||
// 0. [NEW] 合并 allOf
|
||
mergeAllOf(schemaMap)
|
||
|
||
// 1. [CRITICAL] 深度递归处理子项
|
||
if props, ok := schemaMap["properties"].(map[string]any); ok {
|
||
for _, v := range props {
|
||
cleanJSONSchemaRecursive(v)
|
||
}
|
||
// Go 中不需要像 Rust 那样显式处理 nullable_keys remove required,
|
||
// 因为我们在子项处理中会正确设置 type 和 description
|
||
} else if items, ok := schemaMap["items"]; ok {
|
||
// [FIX] Gemini 期望 "items" 是单个 Schema 对象(列表验证),而不是数组(元组验证)。
|
||
if itemsArr, ok := items.([]any); ok {
|
||
// 策略:将元组 [A, B] 视为 A、B 中的最佳匹配项。
|
||
best := extractBestSchemaFromUnion(itemsArr)
|
||
if best == nil {
|
||
// 回退到通用字符串
|
||
best = map[string]any{"type": "string"}
|
||
}
|
||
// 用处理后的对象替换原有数组
|
||
cleanedBest := cleanJSONSchemaRecursive(best)
|
||
schemaMap["items"] = cleanedBest
|
||
} else {
|
||
cleanJSONSchemaRecursive(items)
|
||
}
|
||
} else {
|
||
// 遍历所有值递归
|
||
for _, v := range schemaMap {
|
||
if _, isMap := v.(map[string]any); isMap {
|
||
cleanJSONSchemaRecursive(v)
|
||
} else if arr, isArr := v.([]any); isArr {
|
||
for _, item := range arr {
|
||
cleanJSONSchemaRecursive(item)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 2. [FIX] 处理 anyOf/oneOf 联合类型: 合并属性而非直接删除
|
||
var unionArray []any
|
||
typeStr, _ := schemaMap["type"].(string)
|
||
if typeStr == "" || typeStr == "object" {
|
||
if anyOf, ok := schemaMap["anyOf"].([]any); ok {
|
||
unionArray = anyOf
|
||
} else if oneOf, ok := schemaMap["oneOf"].([]any); ok {
|
||
unionArray = oneOf
|
||
}
|
||
}
|
||
|
||
if len(unionArray) > 0 {
|
||
if bestBranch := extractBestSchemaFromUnion(unionArray); bestBranch != nil {
|
||
if bestMap, ok := bestBranch.(map[string]any); ok {
|
||
// 合并分支内容
|
||
for k, v := range bestMap {
|
||
if k == "properties" {
|
||
targetProps, _ := schemaMap["properties"].(map[string]any)
|
||
if targetProps == nil {
|
||
targetProps = make(map[string]any)
|
||
schemaMap["properties"] = targetProps
|
||
}
|
||
if sourceProps, ok := v.(map[string]any); ok {
|
||
for pk, pv := range sourceProps {
|
||
if _, exists := targetProps[pk]; !exists {
|
||
targetProps[pk] = deepCopy(pv)
|
||
}
|
||
}
|
||
}
|
||
} else if k == "required" {
|
||
targetReq, _ := schemaMap["required"].([]any)
|
||
if sourceReq, ok := v.([]any); ok {
|
||
for _, rv := range sourceReq {
|
||
// 简单的去重添加
|
||
exists := false
|
||
for _, tr := range targetReq {
|
||
if tr == rv {
|
||
exists = true
|
||
break
|
||
}
|
||
}
|
||
if !exists {
|
||
targetReq = append(targetReq, rv)
|
||
}
|
||
}
|
||
schemaMap["required"] = targetReq
|
||
}
|
||
} else if _, exists := schemaMap[k]; !exists {
|
||
schemaMap[k] = deepCopy(v)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3. [SAFETY] 检查当前对象是否为 JSON Schema 节点
|
||
looksLikeSchema := hasKey(schemaMap, "type") ||
|
||
hasKey(schemaMap, "properties") ||
|
||
hasKey(schemaMap, "items") ||
|
||
hasKey(schemaMap, "enum") ||
|
||
hasKey(schemaMap, "anyOf") ||
|
||
hasKey(schemaMap, "oneOf") ||
|
||
hasKey(schemaMap, "allOf")
|
||
|
||
if looksLikeSchema {
|
||
// 4. [ROBUST] 约束迁移
|
||
migrateConstraints(schemaMap)
|
||
|
||
// 5. [CRITICAL] 白名单过滤
|
||
allowedFields := map[string]bool{
|
||
"type": true,
|
||
"description": true,
|
||
"properties": true,
|
||
"required": true,
|
||
"items": true,
|
||
"enum": true,
|
||
"title": true,
|
||
}
|
||
for k := range schemaMap {
|
||
if !allowedFields[k] {
|
||
delete(schemaMap, k)
|
||
}
|
||
}
|
||
|
||
// 6. [SAFETY] 处理空 Object
|
||
if t, _ := schemaMap["type"].(string); t == "object" {
|
||
hasProps := false
|
||
if props, ok := schemaMap["properties"].(map[string]any); ok && len(props) > 0 {
|
||
hasProps = true
|
||
}
|
||
if !hasProps {
|
||
schemaMap["properties"] = map[string]any{
|
||
"reason": map[string]any{
|
||
"type": "string",
|
||
"description": "Reason for calling this tool",
|
||
},
|
||
}
|
||
schemaMap["required"] = []any{"reason"}
|
||
}
|
||
}
|
||
|
||
// 7. [SAFETY] Required 字段对齐
|
||
if props, ok := schemaMap["properties"].(map[string]any); ok {
|
||
if req, ok := schemaMap["required"].([]any); ok {
|
||
var validReq []any
|
||
for _, r := range req {
|
||
if rStr, ok := r.(string); ok {
|
||
if _, exists := props[rStr]; exists {
|
||
validReq = append(validReq, r)
|
||
}
|
||
}
|
||
}
|
||
if len(validReq) > 0 {
|
||
schemaMap["required"] = validReq
|
||
} else {
|
||
delete(schemaMap, "required")
|
||
}
|
||
}
|
||
}
|
||
|
||
// 8. 处理 type 字段 (Lowercase + Nullable 提取)
|
||
isEffectivelyNullable := false
|
||
if typeVal, exists := schemaMap["type"]; exists {
|
||
var selectedType string
|
||
switch v := typeVal.(type) {
|
||
case string:
|
||
lower := strings.ToLower(v)
|
||
if lower == "null" {
|
||
isEffectivelyNullable = true
|
||
selectedType = "string" // fallback
|
||
} else {
|
||
selectedType = lower
|
||
}
|
||
case []any:
|
||
// ["string", "null"]
|
||
for _, t := range v {
|
||
if ts, ok := t.(string); ok {
|
||
lower := strings.ToLower(ts)
|
||
if lower == "null" {
|
||
isEffectivelyNullable = true
|
||
} else if selectedType == "" {
|
||
selectedType = lower
|
||
}
|
||
}
|
||
}
|
||
if selectedType == "" {
|
||
selectedType = "string"
|
||
}
|
||
}
|
||
schemaMap["type"] = selectedType
|
||
} else {
|
||
// 默认 object 如果有 properties (虽然上面白名单过滤可能删了 type 如果它不在... 但 type 必在 allowlist)
|
||
// 如果没有 type,但有 properties,补一个
|
||
if hasKey(schemaMap, "properties") {
|
||
schemaMap["type"] = "object"
|
||
} else {
|
||
// 默认为 string ? or object? Gemini 通常需要明确 type
|
||
schemaMap["type"] = "object"
|
||
}
|
||
}
|
||
|
||
if isEffectivelyNullable {
|
||
desc, _ := schemaMap["description"].(string)
|
||
if !strings.Contains(desc, "nullable") {
|
||
if desc != "" {
|
||
desc += " "
|
||
}
|
||
desc += "(nullable)"
|
||
schemaMap["description"] = desc
|
||
}
|
||
}
|
||
|
||
// 9. Enum 值强制转字符串
|
||
if enumVals, ok := schemaMap["enum"].([]any); ok {
|
||
hasNonString := false
|
||
for i, val := range enumVals {
|
||
if _, isStr := val.(string); !isStr {
|
||
hasNonString = true
|
||
if val == nil {
|
||
enumVals[i] = "null"
|
||
} else {
|
||
enumVals[i] = fmt.Sprintf("%v", val)
|
||
}
|
||
}
|
||
}
|
||
// If we mandated string values, we must ensure type is string
|
||
if hasNonString {
|
||
schemaMap["type"] = "string"
|
||
}
|
||
}
|
||
}
|
||
|
||
return schemaMap
|
||
}
|
||
|
||
func hasKey(m map[string]any, k string) bool {
|
||
_, ok := m[k]
|
||
return ok
|
||
}
|
||
|
||
func migrateConstraints(m map[string]any) {
|
||
constraints := []struct {
|
||
key string
|
||
label string
|
||
}{
|
||
{"minLength", "minLen"},
|
||
{"maxLength", "maxLen"},
|
||
{"pattern", "pattern"},
|
||
{"minimum", "min"},
|
||
{"maximum", "max"},
|
||
{"multipleOf", "multipleOf"},
|
||
{"exclusiveMinimum", "exclMin"},
|
||
{"exclusiveMaximum", "exclMax"},
|
||
{"minItems", "minItems"},
|
||
{"maxItems", "maxItems"},
|
||
{"propertyNames", "propertyNames"},
|
||
{"format", "format"},
|
||
}
|
||
|
||
var hints []string
|
||
for _, c := range constraints {
|
||
if val, ok := m[c.key]; ok && val != nil {
|
||
hints = append(hints, fmt.Sprintf("%s: %v", c.label, val))
|
||
}
|
||
}
|
||
|
||
if len(hints) > 0 {
|
||
suffix := fmt.Sprintf(" [Constraint: %s]", strings.Join(hints, ", "))
|
||
desc, _ := m["description"].(string)
|
||
if !strings.Contains(desc, suffix) {
|
||
m["description"] = desc + suffix
|
||
}
|
||
}
|
||
}
|
||
|
||
// mergeAllOf 合并 allOf
|
||
func mergeAllOf(m map[string]any) {
|
||
allOf, ok := m["allOf"].([]any)
|
||
if !ok {
|
||
return
|
||
}
|
||
delete(m, "allOf")
|
||
|
||
mergedProps := make(map[string]any)
|
||
mergedReq := make(map[string]bool)
|
||
otherFields := make(map[string]any)
|
||
|
||
for _, sub := range allOf {
|
||
if subMap, ok := sub.(map[string]any); ok {
|
||
// Props
|
||
if props, ok := subMap["properties"].(map[string]any); ok {
|
||
for k, v := range props {
|
||
mergedProps[k] = v
|
||
}
|
||
}
|
||
// Required
|
||
if reqs, ok := subMap["required"].([]any); ok {
|
||
for _, r := range reqs {
|
||
if s, ok := r.(string); ok {
|
||
mergedReq[s] = true
|
||
}
|
||
}
|
||
}
|
||
// Others
|
||
for k, v := range subMap {
|
||
if k != "properties" && k != "required" && k != "allOf" {
|
||
if _, exists := otherFields[k]; !exists {
|
||
otherFields[k] = v
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Apply
|
||
for k, v := range otherFields {
|
||
if _, exists := m[k]; !exists {
|
||
m[k] = v
|
||
}
|
||
}
|
||
if len(mergedProps) > 0 {
|
||
existProps, _ := m["properties"].(map[string]any)
|
||
if existProps == nil {
|
||
existProps = make(map[string]any)
|
||
m["properties"] = existProps
|
||
}
|
||
for k, v := range mergedProps {
|
||
if _, exists := existProps[k]; !exists {
|
||
existProps[k] = v
|
||
}
|
||
}
|
||
}
|
||
if len(mergedReq) > 0 {
|
||
existReq, _ := m["required"].([]any)
|
||
var validReqs []any
|
||
for _, r := range existReq {
|
||
if s, ok := r.(string); ok {
|
||
validReqs = append(validReqs, s)
|
||
delete(mergedReq, s) // already exists
|
||
}
|
||
}
|
||
// append new
|
||
for r := range mergedReq {
|
||
validReqs = append(validReqs, r)
|
||
}
|
||
m["required"] = validReqs
|
||
}
|
||
}
|
||
|
||
// extractBestSchemaFromUnion 从 anyOf/oneOf 中选取最佳分支
|
||
func extractBestSchemaFromUnion(unionArray []any) any {
|
||
var bestOption any
|
||
bestScore := -1
|
||
|
||
for _, item := range unionArray {
|
||
score := scoreSchemaOption(item)
|
||
if score > bestScore {
|
||
bestScore = score
|
||
bestOption = item
|
||
}
|
||
}
|
||
return bestOption
|
||
}
|
||
|
||
func scoreSchemaOption(val any) int {
|
||
m, ok := val.(map[string]any)
|
||
if !ok {
|
||
return 0
|
||
}
|
||
typeStr, _ := m["type"].(string)
|
||
|
||
if hasKey(m, "properties") || typeStr == "object" {
|
||
return 3
|
||
}
|
||
if hasKey(m, "items") || typeStr == "array" {
|
||
return 2
|
||
}
|
||
if typeStr != "" && typeStr != "null" {
|
||
return 1
|
||
}
|
||
return 0
|
||
}
|
||
|
||
// DeepCleanUndefined 深度清理值为 "[undefined]" 的字段
|
||
func DeepCleanUndefined(value any) {
|
||
if value == nil {
|
||
return
|
||
}
|
||
switch v := value.(type) {
|
||
case map[string]any:
|
||
for k, val := range v {
|
||
if s, ok := val.(string); ok && s == "[undefined]" {
|
||
delete(v, k)
|
||
continue
|
||
}
|
||
DeepCleanUndefined(val)
|
||
}
|
||
case []any:
|
||
for _, val := range v {
|
||
DeepCleanUndefined(val)
|
||
}
|
||
}
|
||
}
|