bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
121 lines
3.8 KiB
Go
121 lines
3.8 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package sheets
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/internal/util"
|
|
"github.com/larksuite/cli/internal/validate"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
// ToolKind selects the One-OpenAPI endpoint and its rate-limit bucket.
|
|
//
|
|
// - ToolKindRead → POST .../tools/invoke_read (scope sheets:spreadsheet:read, 10 qps)
|
|
// - ToolKindWrite → POST .../tools/invoke_write (scope sheets:spreadsheet:write_only, 5 qps)
|
|
type ToolKind string
|
|
|
|
const (
|
|
ToolKindRead ToolKind = "read"
|
|
ToolKindWrite ToolKind = "write"
|
|
)
|
|
|
|
// toolInvokePath returns the full One-OpenAPI invoke path for the given
|
|
// spreadsheet token + tool kind. Network-free, safe in DryRun.
|
|
func toolInvokePath(token string, kind ToolKind) string {
|
|
suffix := "invoke_read"
|
|
if kind == ToolKindWrite {
|
|
suffix = "invoke_write"
|
|
}
|
|
return fmt.Sprintf("/open-apis/sheet_ai/v2/spreadsheets/%s/tools/%s",
|
|
validate.EncodePathSegment(token), suffix)
|
|
}
|
|
|
|
// buildToolBody constructs the One-OpenAPI request body for a tool invocation.
|
|
// `input` is serialized to a JSON string per the API contract; callers pass
|
|
// a typed Go map and never need to handle JSON encoding themselves.
|
|
func buildToolBody(toolName string, input map[string]interface{}) (map[string]interface{}, error) {
|
|
inputJSON, err := json.Marshal(input)
|
|
if err != nil {
|
|
return nil, errs.NewInternalError(errs.SubtypeSDKError, "encode tool input: %v", err).WithCause(err)
|
|
}
|
|
return map[string]interface{}{
|
|
"tool_name": toolName,
|
|
"input": string(inputJSON),
|
|
}, nil
|
|
}
|
|
|
|
// callTool invokes a sheet-ai tool via the One-OpenAPI endpoint and decodes
|
|
// the JSON-string `output` field into a generic Go value (typically
|
|
// map[string]interface{}). When the tool returns an empty `output`, callTool
|
|
// returns nil with no error.
|
|
//
|
|
// kind must match the tool's read/write classification — passing a read tool
|
|
// to invoke_write (or vice versa) results in a 403 from the gateway.
|
|
func callTool(
|
|
ctx context.Context,
|
|
runtime *common.RuntimeContext,
|
|
token string,
|
|
kind ToolKind,
|
|
toolName string,
|
|
input map[string]interface{},
|
|
) (interface{}, error) {
|
|
body, err := buildToolBody(toolName, input)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
raw, err := runtime.RawAPI("POST", toolInvokePath(token, kind), nil, body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
envelope, ok := raw.(map[string]interface{})
|
|
if !ok {
|
|
return nil, errs.NewInternalError(errs.SubtypeInvalidResponse,
|
|
"tool %q: unexpected non-JSON-object response: %v", toolName, raw)
|
|
}
|
|
code, _ := util.ToFloat64(envelope["code"])
|
|
if code != 0 {
|
|
msg, _ := envelope["msg"].(string)
|
|
return nil, errs.NewAPIError(errs.SubtypeServerError, "tool %q failed: [%d] %s", toolName, int(code), msg).
|
|
WithCode(int(code))
|
|
}
|
|
data, _ := envelope["data"].(map[string]interface{})
|
|
rawOutput, _ := data["output"].(string)
|
|
if rawOutput == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
var out interface{}
|
|
if err := json.Unmarshal([]byte(rawOutput), &out); err != nil {
|
|
return nil, errs.NewInternalError(errs.SubtypeInvalidResponse,
|
|
"tool %q returned invalid JSON output: %v", toolName, err).WithCause(err)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// invokeToolDryRun renders the One-OpenAPI request the shortcut would send.
|
|
// The wire-format body (with input serialized to a JSON string) is preserved
|
|
// for fidelity, and a decoded tool_input map is surfaced alongside so humans
|
|
// don't have to mentally unmarshal the string field.
|
|
func invokeToolDryRun(
|
|
token string,
|
|
kind ToolKind,
|
|
toolName string,
|
|
input map[string]interface{},
|
|
) *common.DryRunAPI {
|
|
wireBody, _ := buildToolBody(toolName, input)
|
|
return common.NewDryRunAPI().
|
|
POST(toolInvokePath(token, kind)).
|
|
Body(wireBody).
|
|
Set("spreadsheet_token", token).
|
|
Set("tool_name", toolName).
|
|
Set("tool_input", input)
|
|
}
|