重构: 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:
2026-07-22 09:13:05 +08:00
parent 53169264da
commit 9446571363
20 changed files with 2882 additions and 1427 deletions
+423
View File
@@ -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
}
// 归一化 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)
}