重构: 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 生成用)。
This commit is contained in:
@@ -0,0 +1,423 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user