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

141 lines
4.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 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
}