// Copyright 2026 Alibaba Group // Licensed under the Apache License, Version 2.0 package drive import ( "encoding/json" "fmt" "math" "strconv" "strings" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" ) const driveCompositeInterfaceReason = "Reviewed Drive Shortcut composite: the executable CLI owns strict response validation, optional multi-step orchestration, local I/O, read-back verification, output projection, and confirmation; no single MCP interface represents the complete command contract." func driveContract(command, description, useWhen string, avoidWhen, examples []string, result *contract.ResultSpec, pagination *contract.PaginationSpec, params ...contract.ParamDecl) corecmd.ContractDecl { name := "shortcut_" + strings.ReplaceAll(strings.TrimPrefix(command, "+"), "-", "_") cliPath := "drive " + command return corecmd.ContractDecl{ Description: description, Parameters: params, Result: result, Pagination: pagination, Interface: &contract.InterfaceSpec{ Mode: contract.InterfaceModeComposite, Availability: contract.InterfaceAvailable, Reason: driveCompositeInterfaceReason, }, Selection: contract.SelectionSpec{ AgentSummary: description, UseWhen: []string{useWhen}, AvoidWhen: avoidWhen, Examples: examples, }, Identity: contract.ToolIdentitySpec{ ProductID: "drive", Name: name, CanonicalPath: "drive." + name, CLIPath: cliPath, PrimaryCLIPath: cliPath, }, } } func driveObjectResult(description string) *contract.ResultSpec { return &contract.ResultSpec{ Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess}, DataSchema: json.RawMessage(fmt.Sprintf( `{"type":"object","description":%q,"additionalProperties":true}`, description, )), } } func driveCollectionResult(collection, description string) *contract.ResultSpec { return &contract.ResultSpec{ Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess}, DataSchema: json.RawMessage(fmt.Sprintf( `{"type":"object","description":%q,"properties":{"count":{"type":"integer","description":"本页有效结果数量"},%q:{"type":"array","description":%q,"items":{"type":"object","description":"Drive 资源条目","additionalProperties":true}},"nextCursor":{"type":"string","description":"下一页游标"},"hasMore":{"type":"boolean","description":"服务端是否仍有下一页"}},"required":["count",%q],"additionalProperties":true}`, description, collection, description, collection, )), } } func driveCursorPagination() *contract.PaginationSpec { return &contract.PaginationSpec{ Kind: contract.PaginationKindCursor, CursorParameter: "cursor", MetaPath: contract.PaginationMetaPath, EndpointExhaustedPath: contract.PaginationExhaustedPath, NextTokenPath: contract.PaginationNextTokenPath, } } func requireDriveResponse(data map[string]any, operation string) (map[string]any, error) { if len(data) == 0 { return nil, driveResponseError(operation, "empty_tool_response", "服务返回空响应,无法证明操作成功") } if success, present := data["success"]; present { value, ok := success.(bool) if !ok { return nil, driveResponseError(operation, "malformed_success", "响应 success 字段不是布尔值") } if !value { message := firstString(data, "errorMsg", "message", "error") if message == "" { message = "服务明确返回 success=false" } return nil, driveResponseError(operation, "remote_failure", message) } } return data, nil } func requireDriveObject(data map[string]any, operation string) (map[string]any, error) { data, err := requireDriveResponse(data, operation) if err != nil { return nil, err } if value, present := data["result"]; present { result, ok := value.(map[string]any) if !ok || len(result) == 0 { return nil, driveResponseError(operation, "malformed_result", "响应 result 缺失有效对象") } return result, nil } if value, present := data["data"]; present { result, ok := value.(map[string]any) if !ok || len(result) == 0 { return nil, driveResponseError(operation, "malformed_data", "响应 data 缺失有效对象") } return result, nil } if len(data) == 1 { return nil, driveResponseError(operation, "missing_business_result", "响应没有可验证的业务对象") } return data, nil } func requireDriveWrite(data map[string]any, operation string) (map[string]any, error) { data, err := requireDriveResponse(data, operation) if err != nil { return nil, err } success, ok := data["success"].(bool) if !ok || !success { return nil, driveResponseError(operation, "missing_terminal_success", "写操作响应没有 success=true 终态证据") } return data, nil } func requireDriveCollection(data map[string]any, operation string, keys ...string) ([]any, map[string]any, error) { data, err := requireDriveResponse(data, operation) if err != nil { return nil, nil, err } containers := []map[string]any{data} for _, wrapper := range []string{"result", "data"} { if value, present := data[wrapper]; present { inner, ok := value.(map[string]any) if !ok { return nil, nil, driveResponseError(operation, "malformed_envelope", fmt.Sprintf("响应 %s 字段不是对象", wrapper)) } containers = append(containers, inner) } } for _, container := range containers { for _, key := range keys { value, present := container[key] if !present { continue } items, ok := value.([]any) if !ok { return nil, nil, driveResponseError(operation, "malformed_collection", fmt.Sprintf("响应 %s 字段不是数组", key)) } for index, item := range items { if _, ok := item.(map[string]any); !ok { return nil, nil, driveResponseError(operation, "malformed_collection_item", fmt.Sprintf("响应 %s[%d] 不是对象", key, index)) } } return items, container, nil } } return nil, nil, driveResponseError(operation, "missing_collection", "响应缺少声明的业务数组;不能把缺字段投影成空结果") } func projectDriveRows(items []any, aliases map[string][]string) []map[string]any { rows := make([]map[string]any, 0, len(items)) for _, item := range items { source := item.(map[string]any) row := make(map[string]any) for canonical, candidates := range aliases { for _, candidate := range candidates { if value, ok := source[candidate]; ok && value != nil { row[canonical] = value break } } } rows = append(rows, row) } return rows } func addDrivePagination(out map[string]any, container map[string]any) { for _, pair := range [][2]string{{"nextCursor", "nextCursor"}, {"nextToken", "nextCursor"}, {"nextPageToken", "nextCursor"}, {"hasMore", "hasMore"}} { if value, ok := container[pair[0]]; ok && value != nil { out[pair[1]] = value } } } func firstString(data map[string]any, keys ...string) string { for _, key := range keys { if value, ok := data[key].(string); ok && strings.TrimSpace(value) != "" { return strings.TrimSpace(value) } } return "" } func nestedString(data map[string]any, keys ...string) string { if value := firstString(data, keys...); value != "" { return value } for _, wrapper := range []string{"result", "data"} { if inner, ok := data[wrapper].(map[string]any); ok { if value := firstString(inner, keys...); value != "" { return value } } } return "" } func driveReadbackNameMatches(data map[string]any, requested string) bool { remoteName := firstString(data, "name", "fileName") if remoteName == requested { return true } extension := strings.TrimLeft(firstString(data, "extension", "fileExtension", "ext"), ".") return extension != "" && remoteName+"."+extension == requested } func firstInt64(data map[string]any, keys ...string) (int64, bool) { for _, key := range keys { value, present := data[key] if !present { continue } switch typed := value.(type) { case int: return int64(typed), true case int32: return int64(typed), true case int64: return typed, true case float64: if !math.IsNaN(typed) && !math.IsInf(typed, 0) && typed == math.Trunc(typed) && typed >= math.MinInt64 && typed < math.MaxInt64 { return int64(typed), true } case json.Number: parsed, err := strconv.ParseInt(typed.String(), 10, 64) if err == nil { return parsed, true } case string: parsed, err := strconv.ParseInt(strings.TrimSpace(typed), 10, 64) if err == nil { return parsed, true } } } return 0, false } func driveResponseError(operation, reason, message string) error { return apperrors.NewAPI(message, apperrors.WithOperation(operation), apperrors.WithOrigin("mcp"), apperrors.WithFailureStage("response_validation"), apperrors.WithRetryable(false), apperrors.WithReason(reason), ) }