Files
contracts/base/res_api_param.go
T
what 9446571363 重构: base 包资源核心类型迁移到 req/resx,新增 res_watcher/res_api_param
base/resource.go、resource_hooks.go、resource_test.go、query_field.go 删除,ResField 等具体实现搬到 req/resx(见 res_field.go 里的类型别名)。res_listener.go 替换成 res_watcher.go,对应资源变更监听概念改名。新增 res_api_param.go 及配套测试(ResApi 参数建模,给 MCP tool 的 JSON Schema 生成用)。
2026-07-22 09:13:05 +08:00

424 lines
9.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package base
import (
"encoding/json"
"sort"
"strconv"
jschema "github.com/google/jsonschema-go/jsonschema"
"github.com/spf13/cast"
)
type ResApiParam struct {
ID int64 `db:"id" json:"id"`
PID int64 `db:"pid" json:"pid"`
Code string `db:"code" json:"code"`
Name string `db:"name" json:"name"`
Category string `db:"category" json:"category"`
DataType string `db:"type" json:"type"`
Desc string `db:"desc" json:"desc"`
IsRequired bool `db:"isRequired" json:"isRequired"`
Constraints map[string]any `db:"constraints" json:"constraints"`
Rank int `db:"rank" json:"rank"`
RefID int64 `db:"ref_id" json:"ref_id"`
DefaultVal string `db:"df_val" json:"df_val"`
}
type ResApiParams []ResApiParam
// ToJSONSchema 将扁平参数列表还原为 JSON Schema 对象
func (params ResApiParams) ToJSONSchema() *jschema.Schema {
return params.buildObject(0)
}
// ResApiParamsFromJSONSchema 将 JSON Schema 对象解析为扁平参数列表
func ResApiParamsFromJSONSchema(schema *jschema.Schema) ResApiParams {
var params ResApiParams
var seq int64
params.parseObject(schema, 0, &seq)
return params
}
func (params ResApiParams) childrenOf(pid int64) ResApiParams {
var result ResApiParams
for _, p := range params {
if p.PID == pid {
result = append(result, p)
}
}
sort.Slice(result, func(i, j int) bool {
return result[i].Rank < result[j].Rank
})
return result
}
func (params ResApiParams) buildObject(pid int64) *jschema.Schema {
s := &jschema.Schema{Type: "object"}
props := map[string]*jschema.Schema{}
for _, p := range params.childrenOf(pid) {
if p.Code == "items" || p.Code == "oneOf" {
continue
}
props[p.Code] = params.buildProperty(p)
if p.IsRequired {
s.Required = append(s.Required, p.Code)
}
}
if len(props) > 0 {
s.Properties = props
}
sort.Strings(s.Required)
return s
}
func (params ResApiParams) findByID(id int64) (ResApiParam, bool) {
for _, p := range params {
if p.ID == id {
return p, true
}
}
return ResApiParam{}, false
}
// pathOf 计算参数在生成的 JSON Schema 中的路径
func (params ResApiParams) pathOf(id int64) string {
p, ok := params.findByID(id)
if !ok {
return ""
}
if p.PID == 0 {
return "#/properties/" + p.Code
}
parent := params.pathOf(p.PID)
switch p.Code {
case "items":
return parent + "/items"
case "oneOf":
return parent + "/items/oneOf/" + strconv.Itoa(p.Rank)
default:
return parent + "/properties/" + p.Code
}
}
func (params ResApiParams) buildProperty(p ResApiParam) *jschema.Schema {
if p.RefID != 0 {
if _, ok := params.findByID(p.RefID); ok {
return &jschema.Schema{Ref: params.pathOf(p.RefID), Description: p.Desc}
}
}
// any 类型:不设 type,从 oneOf 子节点构建
if p.DataType == "any" {
s := &jschema.Schema{Title: p.Name, Description: p.Desc}
if p.DefaultVal != "" {
var tmp any
if err := json.Unmarshal([]byte(p.DefaultVal), &tmp); err == nil {
s.Default = json.RawMessage(p.DefaultVal)
} else {
s.Default, _ = json.Marshal(p.DefaultVal)
}
}
for _, child := range params.childrenOf(p.ID) {
c := child
if child.Code == "oneOf" {
s.OneOf = append(s.OneOf, params.buildProperty(c))
}
}
params.applyConstraints(s, p.Constraints)
return s
}
// 归一化 DataTypejson → object;空字符串根据子节点推断
dataType := p.DataType
switch dataType {
case "json":
dataType = "object"
case "bool":
dataType = "boolean"
case "":
for _, c := range params.childrenOf(p.ID) {
if c.Code == "oneOf" || c.Code == "items" {
dataType = "array"
break
}
}
if dataType == "" && len(params.childrenOf(p.ID)) > 0 {
dataType = "object"
}
}
s := &jschema.Schema{
Type: dataType,
Title: p.Name,
Description: p.Desc,
}
if p.DefaultVal != "" {
var tmp any
if err := json.Unmarshal([]byte(p.DefaultVal), &tmp); err == nil {
s.Default = json.RawMessage(p.DefaultVal)
} else {
s.Default, _ = json.Marshal(p.DefaultVal)
}
}
params.applyConstraints(s, p.Constraints)
switch dataType {
case "object":
nested := params.buildObject(p.ID)
s.Properties = nested.Properties
s.Required = nested.Required
case "array":
var oneOfSchemas []*jschema.Schema
var itemsChild *ResApiParam
for _, child := range params.childrenOf(p.ID) {
c := child
switch child.Code {
case "oneOf":
oneOfSchemas = append(oneOfSchemas, params.buildProperty(c))
case "items":
itemsChild = &c
}
}
if len(oneOfSchemas) > 0 {
s.Items = &jschema.Schema{OneOf: oneOfSchemas}
} else if itemsChild != nil {
s.Items = params.buildItems(*itemsChild)
}
}
return s
}
func (params ResApiParams) buildItems(p ResApiParam) *jschema.Schema {
if ref, ok := p.Constraints["$ref"].(string); ok {
return &jschema.Schema{Ref: ref}
}
if p.RefID != 0 {
if ref, ok := params.findByID(p.RefID); ok {
path := params.pathOf(p.RefID)
if ref.DataType == "array" {
path += "/items"
}
return &jschema.Schema{Ref: path}
}
}
itemType := p.DataType
if itemType == "json" {
itemType = "object"
} else if itemType == "" && len(params.childrenOf(p.ID)) > 0 {
itemType = "object"
}
s := &jschema.Schema{
Type: itemType,
Title: p.Name,
Description: p.Desc,
}
if s.Description == "" && s.Title != "" {
s.Description = s.Title
}
if itemType == "object" {
nested := params.buildObject(p.ID)
s.Properties = nested.Properties
s.Required = nested.Required
}
params.applyConstraints(s, p.Constraints)
return s
}
func (params ResApiParams) applyConstraints(s *jschema.Schema, c map[string]any) {
for k, v := range c {
switch k {
case "$ref":
if ref, ok := v.(string); ok {
s.Ref = ref
}
case "enum":
if arr, ok := v.([]any); ok {
s.Enum = arr
}
case "format":
if f, ok := v.(string); ok {
s.Format = f
}
case "pattern":
if p, ok := v.(string); ok {
s.Pattern = p
}
case "minLength":
s.MinLength = jschema.Ptr(cast.ToInt(v))
case "maxLength":
s.MaxLength = jschema.Ptr(cast.ToInt(v))
case "minimum":
s.Minimum = jschema.Ptr(cast.ToFloat64(v))
case "maximum":
s.Maximum = jschema.Ptr(cast.ToFloat64(v))
case "multipleOf":
s.MultipleOf = jschema.Ptr(cast.ToFloat64(v))
case "minItems":
s.MinItems = jschema.Ptr(cast.ToInt(v))
case "maxItems":
s.MaxItems = jschema.Ptr(cast.ToInt(v))
case "minProperties":
s.MinProperties = jschema.Ptr(cast.ToInt(v))
case "maxProperties":
s.MaxProperties = jschema.Ptr(cast.ToInt(v))
case "additionalProperties":
switch val := v.(type) {
case bool:
if val {
s.AdditionalProperties = &jschema.Schema{}
} else {
s.AdditionalProperties = &jschema.Schema{Not: &jschema.Schema{}}
}
}
}
}
}
func (params *ResApiParams) parseObject(schema *jschema.Schema, pid int64, seq *int64) {
requiredSet := make(map[string]bool, len(schema.Required))
for _, r := range schema.Required {
requiredSet[r] = true
}
rank := 0
for code, prop := range schema.Properties {
*seq++
id := *seq
p := ResApiParam{
ID: id,
PID: pid,
Code: code,
DataType: prop.Type,
Name: prop.Title,
Desc: prop.Description,
IsRequired: requiredSet[code],
DefaultVal: params.rawToStr(prop.Default),
Rank: rank,
Constraints: params.extractConstraints(prop),
}
*params = append(*params, p)
rank++
switch prop.Type {
case "object":
params.parseObject(prop, id, seq)
case "array":
if prop.Items == nil {
break
}
// items 为纯 oneOf schema:建立 oneOf 子行,不建 items 子行
if len(prop.Items.OneOf) > 0 {
for oneOfRank, sub := range prop.Items.OneOf {
*seq++
oneOfID := *seq
itemType := sub.Type
if itemType == "" {
itemType = "object"
}
*params = append(*params, ResApiParam{
ID: oneOfID,
PID: id,
Code: "oneOf",
DataType: itemType,
Name: sub.Title,
Desc: sub.Description,
Rank: oneOfRank,
Constraints: params.extractConstraints(sub),
})
if itemType == "object" {
params.parseObject(sub, oneOfID, seq)
}
}
break
}
*seq++
itemID := *seq
itemType := prop.Items.Type
if itemType == "" {
itemType = "object"
}
*params = append(*params, ResApiParam{
ID: itemID,
PID: id,
Code: "items",
DataType: itemType,
Name: prop.Items.Title,
Desc: prop.Items.Description,
Constraints: params.extractConstraints(prop.Items),
})
if itemType == "object" {
params.parseObject(prop.Items, itemID, seq)
}
}
}
}
func (params ResApiParams) extractConstraints(s *jschema.Schema) map[string]any {
c := map[string]any{}
if s.Ref != "" {
c["$ref"] = s.Ref
}
if len(s.Enum) > 0 {
c["enum"] = s.Enum
}
if s.Format != "" {
c["format"] = s.Format
}
if s.Pattern != "" {
c["pattern"] = s.Pattern
}
if s.MinLength != nil {
c["minLength"] = *s.MinLength
}
if s.MaxLength != nil {
c["maxLength"] = *s.MaxLength
}
if s.Minimum != nil {
c["minimum"] = *s.Minimum
}
if s.Maximum != nil {
c["maximum"] = *s.Maximum
}
if s.MultipleOf != nil {
c["multipleOf"] = *s.MultipleOf
}
if s.MinItems != nil {
c["minItems"] = *s.MinItems
}
if s.MaxItems != nil {
c["maxItems"] = *s.MaxItems
}
if s.MinProperties != nil {
c["minProperties"] = *s.MinProperties
}
if s.MaxProperties != nil {
c["maxProperties"] = *s.MaxProperties
}
if s.AdditionalProperties != nil {
c["additionalProperties"] = s.AdditionalProperties.Not == nil
}
if len(c) == 0 {
return nil
}
return c
}
func (params ResApiParams) rawToStr(raw json.RawMessage) string {
if len(raw) == 0 {
return ""
}
var s string
if err := json.Unmarshal(raw, &s); err == nil {
return s
}
return string(raw)
}