141 lines
4.9 KiB
Go
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
|
|
}
|