refactor(task-5): add usecase layer
- Migrate agent runner from internal/agent - Migrate sanitizer from internal/sanitize - Migrate toolcall from internal/toolcall - Update import paths to use pkg/usecase
This commit is contained in:
parent
d4fcb8d3b8
commit
5866a5b9c9
|
|
@ -0,0 +1,28 @@
|
||||||
|
package usecase
|
||||||
|
|
||||||
|
import "cursor-api-proxy/internal/config"
|
||||||
|
|
||||||
|
func BuildAgentFixedArgs(cfg config.BridgeConfig, workspaceDir, model string, stream bool) []string {
|
||||||
|
args := []string{"--print"}
|
||||||
|
if cfg.ApproveMcps {
|
||||||
|
args = append(args, "--approve-mcps")
|
||||||
|
}
|
||||||
|
if cfg.Force {
|
||||||
|
args = append(args, "--force")
|
||||||
|
}
|
||||||
|
if cfg.ChatOnlyWorkspace {
|
||||||
|
args = append(args, "--trust")
|
||||||
|
}
|
||||||
|
args = append(args, "--workspace", workspaceDir)
|
||||||
|
args = append(args, "--model", model)
|
||||||
|
if stream {
|
||||||
|
args = append(args, "--stream-partial-output", "--output-format", "stream-json")
|
||||||
|
} else {
|
||||||
|
args = append(args, "--output-format", "text")
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildAgentCmdArgs(cfg config.BridgeConfig, workspaceDir, model, prompt string, stream bool) []string {
|
||||||
|
return append(BuildAgentFixedArgs(cfg, workspaceDir, model, stream), prompt)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
package usecase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getCandidates(agentScriptPath, configDirOverride string) []string {
|
||||||
|
if configDirOverride != "" {
|
||||||
|
return []string{filepath.Join(configDirOverride, "cli-config.json")}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []string
|
||||||
|
|
||||||
|
if dir := os.Getenv("CURSOR_CONFIG_DIR"); dir != "" {
|
||||||
|
result = append(result, filepath.Join(dir, "cli-config.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if agentScriptPath != "" {
|
||||||
|
agentDir := filepath.Dir(agentScriptPath)
|
||||||
|
result = append(result, filepath.Join(agentDir, "..", "data", "config", "cli-config.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
if home == "" {
|
||||||
|
home = os.Getenv("USERPROFILE")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
local := os.Getenv("LOCALAPPDATA")
|
||||||
|
if local == "" {
|
||||||
|
local = filepath.Join(home, "AppData", "Local")
|
||||||
|
}
|
||||||
|
result = append(result, filepath.Join(local, "cursor-agent", "cli-config.json"))
|
||||||
|
case "darwin":
|
||||||
|
result = append(result, filepath.Join(home, "Library", "Application Support", "cursor-agent", "cli-config.json"))
|
||||||
|
default:
|
||||||
|
xdg := os.Getenv("XDG_CONFIG_HOME")
|
||||||
|
if xdg == "" {
|
||||||
|
xdg = filepath.Join(home, ".config")
|
||||||
|
}
|
||||||
|
result = append(result, filepath.Join(xdg, "cursor-agent", "cli-config.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunMaxModePreflight(agentScriptPath, configDirOverride string) {
|
||||||
|
for _, candidate := range getCandidates(agentScriptPath, configDirOverride) {
|
||||||
|
data, err := os.ReadFile(candidate)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip BOM if present
|
||||||
|
if len(data) >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF {
|
||||||
|
data = data[3:]
|
||||||
|
}
|
||||||
|
|
||||||
|
var raw map[string]interface{}
|
||||||
|
if err := json.Unmarshal(data, &raw); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if raw == nil || len(raw) <= 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
raw["maxMode"] = true
|
||||||
|
if model, ok := raw["model"].(map[string]interface{}); ok {
|
||||||
|
model["maxMode"] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := json.MarshalIndent(raw, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(candidate, out, 0644); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
package usecase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"cursor-api-proxy/internal/config"
|
||||||
|
"cursor-api-proxy/pkg/infrastructure/process"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
process.MaxModeFn = RunMaxModePreflight
|
||||||
|
}
|
||||||
|
|
||||||
|
func cacheTokenForAccount(configDir string) {
|
||||||
|
if configDir == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
token := ReadKeychainToken()
|
||||||
|
if token != "" {
|
||||||
|
WriteCachedToken(configDir, token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AccountsDir() string {
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
if home == "" {
|
||||||
|
home = os.Getenv("USERPROFILE")
|
||||||
|
}
|
||||||
|
return filepath.Join(home, ".cursor-api-proxy", "accounts")
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunAgentSync(cfg config.BridgeConfig, workspaceDir string, cmdArgs []string, tempDir, configDir string, ctx context.Context) (process.RunResult, error) {
|
||||||
|
opts := process.RunOptions{
|
||||||
|
Cwd: workspaceDir,
|
||||||
|
TimeoutMs: cfg.TimeoutMs,
|
||||||
|
MaxMode: cfg.MaxMode,
|
||||||
|
ConfigDir: configDir,
|
||||||
|
Ctx: ctx,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := process.Run(cfg.AgentBin, cmdArgs, opts)
|
||||||
|
|
||||||
|
cacheTokenForAccount(configDir)
|
||||||
|
if tempDir != "" {
|
||||||
|
os.RemoveAll(tempDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunAgentStreamWithContext(cfg config.BridgeConfig, workspaceDir string, cmdArgs []string, onLine func(string), tempDir, configDir string, ctx context.Context) (process.StreamResult, error) {
|
||||||
|
opts := process.RunStreamingOptions{
|
||||||
|
RunOptions: process.RunOptions{
|
||||||
|
Cwd: workspaceDir,
|
||||||
|
TimeoutMs: cfg.TimeoutMs,
|
||||||
|
MaxMode: cfg.MaxMode,
|
||||||
|
ConfigDir: configDir,
|
||||||
|
Ctx: ctx,
|
||||||
|
},
|
||||||
|
OnLine: onLine,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := process.RunStreaming(cfg.AgentBin, cmdArgs, opts)
|
||||||
|
|
||||||
|
cacheTokenForAccount(configDir)
|
||||||
|
if tempDir != "" {
|
||||||
|
os.RemoveAll(tempDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
package usecase
|
||||||
|
|
||||||
|
import "regexp"
|
||||||
|
|
||||||
|
type rule struct {
|
||||||
|
pattern *regexp.Regexp
|
||||||
|
replacement string
|
||||||
|
}
|
||||||
|
|
||||||
|
var rules = []rule{
|
||||||
|
{regexp.MustCompile(`(?i)x-anthropic-billing-header:[^\n]*\n?`), ""},
|
||||||
|
{regexp.MustCompile(`(?i)\bcc_version=[^\s;,\n]+[;,]?\s*`), ""},
|
||||||
|
{regexp.MustCompile(`(?i)\bcc_entrypoint=[^\s;,\n]+[;,]?\s*`), ""},
|
||||||
|
{regexp.MustCompile(`(?i)\bcch=[a-f0-9]+[;,]?\s*`), ""},
|
||||||
|
{regexp.MustCompile(`\bClaude Code\b`), "Cursor"},
|
||||||
|
{regexp.MustCompile(`(?i)Anthropic['']s official CLI for Claude`), "Cursor AI assistant"},
|
||||||
|
{regexp.MustCompile(`\bAnthropic\b`), "Cursor"},
|
||||||
|
{regexp.MustCompile(`(?i)anthropic\.com`), "cursor.com"},
|
||||||
|
{regexp.MustCompile(`(?i)claude\.ai`), "cursor.sh"},
|
||||||
|
{regexp.MustCompile(`^[;,\s]+`), ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
func SanitizeText(text string) string {
|
||||||
|
for _, r := range rules {
|
||||||
|
text = r.pattern.ReplaceAllString(text, r.replacement)
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
func SanitizeMessages(messages []interface{}) []interface{} {
|
||||||
|
result := make([]interface{}, len(messages))
|
||||||
|
for i, raw := range messages {
|
||||||
|
if raw == nil {
|
||||||
|
result[i] = raw
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m, ok := raw.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
result[i] = raw
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newMsg := make(map[string]interface{}, len(m))
|
||||||
|
for k, v := range m {
|
||||||
|
newMsg[k] = v
|
||||||
|
}
|
||||||
|
switch c := m["content"].(type) {
|
||||||
|
case string:
|
||||||
|
newMsg["content"] = SanitizeText(c)
|
||||||
|
case []interface{}:
|
||||||
|
newParts := make([]interface{}, len(c))
|
||||||
|
for j, p := range c {
|
||||||
|
if pm, ok := p.(map[string]interface{}); ok && pm["type"] == "text" {
|
||||||
|
if t, ok := pm["text"].(string); ok {
|
||||||
|
newPart := make(map[string]interface{}, len(pm))
|
||||||
|
for k, v := range pm {
|
||||||
|
newPart[k] = v
|
||||||
|
}
|
||||||
|
newPart["text"] = SanitizeText(t)
|
||||||
|
newParts[j] = newPart
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newParts[j] = p
|
||||||
|
}
|
||||||
|
newMsg["content"] = newParts
|
||||||
|
}
|
||||||
|
result[i] = newMsg
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func SanitizeSystem(system interface{}) interface{} {
|
||||||
|
switch v := system.(type) {
|
||||||
|
case string:
|
||||||
|
return SanitizeText(v)
|
||||||
|
case []interface{}:
|
||||||
|
result := make([]interface{}, len(v))
|
||||||
|
for i, p := range v {
|
||||||
|
if pm, ok := p.(map[string]interface{}); ok && pm["type"] == "text" {
|
||||||
|
if t, ok := pm["text"].(string); ok {
|
||||||
|
newPart := make(map[string]interface{}, len(pm))
|
||||||
|
for k, val := range pm {
|
||||||
|
newPart[k] = val
|
||||||
|
}
|
||||||
|
newPart["text"] = SanitizeText(t)
|
||||||
|
result[i] = newPart
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result[i] = p
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return system
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
package usecase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSanitizeTextAnthropicBilling(t *testing.T) {
|
||||||
|
input := "x-anthropic-billing-header: abc123\nHello"
|
||||||
|
got := SanitizeText(input)
|
||||||
|
if strings.Contains(got, "x-anthropic-billing-header") {
|
||||||
|
t.Errorf("billing header not removed: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSanitizeTextClaudeCode(t *testing.T) {
|
||||||
|
input := "I am Claude Code assistant"
|
||||||
|
got := SanitizeText(input)
|
||||||
|
if strings.Contains(got, "Claude Code") {
|
||||||
|
t.Errorf("'Claude Code' not replaced: %q", got)
|
||||||
|
}
|
||||||
|
if !strings.Contains(got, "Cursor") {
|
||||||
|
t.Errorf("'Cursor' not present in output: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSanitizeTextAnthropic(t *testing.T) {
|
||||||
|
input := "Powered by Anthropic's technology and anthropic.com"
|
||||||
|
got := SanitizeText(input)
|
||||||
|
if strings.Contains(got, "Anthropic") {
|
||||||
|
t.Errorf("'Anthropic' not replaced: %q", got)
|
||||||
|
}
|
||||||
|
if strings.Contains(got, "anthropic.com") {
|
||||||
|
t.Errorf("'anthropic.com' not replaced: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSanitizeTextNoChange(t *testing.T) {
|
||||||
|
input := "Hello, this is a normal message about cursor."
|
||||||
|
got := SanitizeText(input)
|
||||||
|
if got != input {
|
||||||
|
t.Errorf("unexpected change: %q -> %q", input, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSanitizeMessages(t *testing.T) {
|
||||||
|
messages := []interface{}{
|
||||||
|
map[string]interface{}{"role": "user", "content": "Ask Claude Code something"},
|
||||||
|
map[string]interface{}{"role": "system", "content": "Use Anthropic's tools"},
|
||||||
|
}
|
||||||
|
result := SanitizeMessages(messages)
|
||||||
|
|
||||||
|
for _, raw := range result {
|
||||||
|
m := raw.(map[string]interface{})
|
||||||
|
c := m["content"].(string)
|
||||||
|
if strings.Contains(c, "Claude Code") || strings.Contains(c, "Anthropic") {
|
||||||
|
t.Errorf("found unsanitized content: %q", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package usecase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const tokenFile = ".cursor-token"
|
||||||
|
|
||||||
|
func ReadCachedToken(configDir string) string {
|
||||||
|
p := filepath.Join(configDir, tokenFile)
|
||||||
|
data, err := os.ReadFile(p)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteCachedToken(configDir, token string) {
|
||||||
|
p := filepath.Join(configDir, tokenFile)
|
||||||
|
_ = os.WriteFile(p, []byte(token), 0600)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadKeychainToken() string {
|
||||||
|
if runtime.GOOS != "darwin" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
out, err := exec.Command("security", "find-generic-password", "-s", "cursor-access-token", "-w").Output()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(out))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
package usecase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ToolCall struct {
|
||||||
|
Name string
|
||||||
|
Arguments string // JSON string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParsedResponse struct {
|
||||||
|
TextContent string
|
||||||
|
ToolCalls []ToolCall
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ParsedResponse) HasToolCalls() bool {
|
||||||
|
return len(p.ToolCalls) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modified regex to handle nested JSON
|
||||||
|
var toolCallTagRe = regexp.MustCompile(`(?s)行政法规\s*(\{(?:[^{}]|\{[^{}]*\})*\})\s*ugalakh`)
|
||||||
|
var antmlFunctionCallsRe = regexp.MustCompile(`(?s)<function_calls>\s*(.*?)\s*</function_calls>`)
|
||||||
|
var antmlInvokeRe = regexp.MustCompile(`(?s)<invoke\s+name="([^"]+)">\s*(.*?)\s*</invoke>`)
|
||||||
|
var antmlParamRe = regexp.MustCompile(`(?s)<parameter\s+name="([^"]+)">(.*?)</parameter>`)
|
||||||
|
|
||||||
|
func ExtractToolCalls(text string, toolNames map[string]bool) *ParsedResponse {
|
||||||
|
result := &ParsedResponse{}
|
||||||
|
|
||||||
|
if locs := toolCallTagRe.FindAllStringSubmatchIndex(text, -1); len(locs) > 0 {
|
||||||
|
var calls []ToolCall
|
||||||
|
var textParts []string
|
||||||
|
last := 0
|
||||||
|
for _, loc := range locs {
|
||||||
|
if loc[0] > last {
|
||||||
|
textParts = append(textParts, text[last:loc[0]])
|
||||||
|
}
|
||||||
|
jsonStr := text[loc[2]:loc[3]]
|
||||||
|
if tc := parseToolCallJSON(jsonStr, toolNames); tc != nil {
|
||||||
|
calls = append(calls, *tc)
|
||||||
|
} else {
|
||||||
|
textParts = append(textParts, text[loc[0]:loc[1]])
|
||||||
|
}
|
||||||
|
last = loc[1]
|
||||||
|
}
|
||||||
|
if last < len(text) {
|
||||||
|
textParts = append(textParts, text[last:])
|
||||||
|
}
|
||||||
|
if len(calls) > 0 {
|
||||||
|
result.TextContent = strings.TrimSpace(strings.Join(textParts, ""))
|
||||||
|
result.ToolCalls = calls
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if locs := antmlFunctionCallsRe.FindAllStringSubmatchIndex(text, -1); len(locs) > 0 {
|
||||||
|
var calls []ToolCall
|
||||||
|
var textParts []string
|
||||||
|
last := 0
|
||||||
|
for _, loc := range locs {
|
||||||
|
if loc[0] > last {
|
||||||
|
textParts = append(textParts, text[last:loc[0]])
|
||||||
|
}
|
||||||
|
block := text[loc[2]:loc[3]]
|
||||||
|
invokes := antmlInvokeRe.FindAllStringSubmatch(block, -1)
|
||||||
|
for _, inv := range invokes {
|
||||||
|
name := inv[1]
|
||||||
|
if toolNames != nil && len(toolNames) > 0 && !toolNames[name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
body := inv[2]
|
||||||
|
args := map[string]interface{}{}
|
||||||
|
params := antmlParamRe.FindAllStringSubmatch(body, -1)
|
||||||
|
for _, p := range params {
|
||||||
|
paramName := p[1]
|
||||||
|
paramValue := strings.TrimSpace(p[2])
|
||||||
|
var jsonVal interface{}
|
||||||
|
if err := json.Unmarshal([]byte(paramValue), &jsonVal); err == nil {
|
||||||
|
args[paramName] = jsonVal
|
||||||
|
} else {
|
||||||
|
args[paramName] = paramValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
argsJSON, _ := json.Marshal(args)
|
||||||
|
calls = append(calls, ToolCall{Name: name, Arguments: string(argsJSON)})
|
||||||
|
}
|
||||||
|
last = loc[1]
|
||||||
|
}
|
||||||
|
if last < len(text) {
|
||||||
|
textParts = append(textParts, text[last:])
|
||||||
|
}
|
||||||
|
if len(calls) > 0 {
|
||||||
|
result.TextContent = strings.TrimSpace(strings.Join(textParts, ""))
|
||||||
|
result.ToolCalls = calls
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.TextContent = text
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseToolCallJSON(jsonStr string, toolNames map[string]bool) *ToolCall {
|
||||||
|
var raw map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(jsonStr), &raw); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
name, _ := raw["name"].(string)
|
||||||
|
if name == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if toolNames != nil && len(toolNames) > 0 && !toolNames[name] {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var argsStr string
|
||||||
|
switch a := raw["arguments"].(type) {
|
||||||
|
case string:
|
||||||
|
argsStr = a
|
||||||
|
case map[string]interface{}, []interface{}:
|
||||||
|
b, _ := json.Marshal(a)
|
||||||
|
argsStr = string(b)
|
||||||
|
default:
|
||||||
|
if p, ok := raw["parameters"]; ok {
|
||||||
|
b, _ := json.Marshal(p)
|
||||||
|
argsStr = string(b)
|
||||||
|
} else {
|
||||||
|
argsStr = "{}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &ToolCall{Name: name, Arguments: argsStr}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CollectToolNames(tools []interface{}) map[string]bool {
|
||||||
|
names := map[string]bool{}
|
||||||
|
for _, t := range tools {
|
||||||
|
m, ok := t.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if m["type"] == "function" {
|
||||||
|
if fn, ok := m["function"].(map[string]interface{}); ok {
|
||||||
|
if name, ok := fn["name"].(string); ok {
|
||||||
|
names[name] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if name, ok := m["name"].(string); ok {
|
||||||
|
names[name] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue