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 生成用)。
424 lines
9.9 KiB
Go
424 lines
9.9 KiB
Go
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
|
||
}
|
||
|
||
// 归一化 DataType:json → 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)
|
||
}
|