Files
ttstd-infinite-canvas/backend/internal/app/cloud_agent_json.go
T

141 lines
4.9 KiB
Go

package app
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"reflect"
"strings"
)
var errCloudAgentJSONSingleObject = errors.New("参数必须是单个 JSON 对象")
// Only explicitly classified input errors may be repaired by the model;
// authorization and unsupported mutations still fail before any write/approval.
type cloudAgentArgumentError struct{ error }
func (e *cloudAgentArgumentError) Unwrap() error { return e.error }
type cloudAgentFieldArgumentError struct {
error
Field string
Issue string
}
func (e *cloudAgentFieldArgumentError) Unwrap() error { return e.error }
func cloudAgentFieldError(field, issue, message string) error {
return &cloudAgentFieldArgumentError{
error: &cloudAgentArgumentError{BadAuthRequest(message)}, Field: field, Issue: issue,
}
}
// Decode operations individually so feedback identifies the failing array item.
// Only field names declared by our structs are echoed; arbitrary keys/values
// supplied by a model are never used as diagnostics.
func decodeCloudAgentCanvasArgs(raw string) (agentCanvasArgs, error) {
var envelope struct {
SnapshotHash string `json:"snapshotHash"`
Ops []json.RawMessage `json:"ops"`
}
args := agentCanvasArgs{}
if err := decodeCloudAgentJSONObject(raw, &envelope); err != nil {
return args, cloudAgentJSONArgumentError(err)
}
if envelope.SnapshotHash == "" {
return args, cloudAgentFieldError("snapshotHash", "required", "画布参数 snapshotHash 不能为空,请先读取画布")
}
if len(envelope.Ops) < 1 || len(envelope.Ops) > 20 {
return args, cloudAgentFieldError("ops", "item_count", "画布参数 ops 必须包含1到20项操作")
}
args.SnapshotHash = envelope.SnapshotHash
for i, rawOp := range envelope.Ops {
path := fmt.Sprintf("ops[%d]", i)
var op agentCanvasOp
if err := decodeCloudAgentJSONObject(string(rawOp), &op); err != nil {
field, issue := path, "invalid_value"
name := ""
var typeErr *json.UnmarshalTypeError
if errors.As(err, &typeErr) {
issue = "type_mismatch"
name = typeErr.Field
} else if strings.HasPrefix(err.Error(), "json: unknown field ") {
issue = "unexpected_field"
name = strings.Trim(strings.TrimPrefix(err.Error(), "json: unknown field "), "\"")
}
knownField:
for _, typ := range []reflect.Type{reflect.TypeOf(agentCanvasArgs{}), reflect.TypeOf(agentCanvasOp{})} {
for j := 0; j < typ.NumField(); j++ {
if typ.Field(j).Tag.Get("json") == name {
field += "." + name
break knownField
}
}
}
return args, cloudAgentFieldError(field, issue, fmt.Sprintf("画布参数 %s 无效:%s", field, cloudAgentSafeToolError(cloudAgentJSONArgumentError(err))))
}
required := ""
switch {
case op.Type == "":
required = "type"
case op.ID == "":
required = "id"
case op.Type == "add_node" && op.NodeType == "":
required = "nodeType"
case op.Type == "update_node" && len(op.Patch) == 0:
required = "patch"
case op.Type == "connect_nodes" && op.FromNodeID == "":
required = "fromNodeId"
case op.Type == "connect_nodes" && op.ToNodeID == "":
required = "toNodeId"
}
if required != "" {
field := path + "." + required
return args, cloudAgentFieldError(field, "required", fmt.Sprintf("画布参数 %s 不能为空", field))
}
args.Ops = append(args.Ops, op)
}
return args, nil
}
// Report the failure category without echoing model-controlled keys or values.
// The runtime attaches the advertised schema so the model can correct its call.
func cloudAgentJSONArgumentError(err error) error {
message := "工具参数 JSON 格式无效,请按参数规范修正后重试"
var typeErr *json.UnmarshalTypeError
switch {
case errors.Is(err, errCloudAgentJSONSingleObject):
message = "工具参数必须是单个 JSON 对象,不能是字符串、数组、null 或多个对象;无参数时传 {}"
case errors.As(err, &typeErr):
message = "工具参数字段类型不匹配,请按参数规范使用整数、字符串或数组,不要把数字或数组写成字符串"
case strings.HasPrefix(err.Error(), "json: unknown field "):
message = "工具参数含有不支持的字段,请移除参数规范以外的字段后重试"
}
return &cloudAgentArgumentError{BadAuthRequest(message)}
}
// decodeCloudAgentJSONObject is used for model tool arguments. Tool arguments
// are an untrusted protocol boundary: reject non-objects, unknown fields and
// trailing JSON instead of silently accepting an ambiguous payload.
func decodeCloudAgentJSONObject(raw string, target any) error {
data := bytes.TrimSpace([]byte(raw))
if len(data) == 0 || data[0] != '{' {
return errCloudAgentJSONSingleObject
}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
if err := decoder.Decode(target); err != nil {
return err
}
var extra any
if err := decoder.Decode(&extra); err != io.EOF {
if err == nil {
return errCloudAgentJSONSingleObject
}
return err
}
return nil
}