Files
dingtalk-workspace-cli/internal/app/direct_runtime.go
T
audanye-sudo c1893f40fc feat: add tool-level endpoint routing for multi-endpoint products
- Build tool → endpoint mapping from CLI tools and overrides
- directRuntimeEndpoint resolves by tool name before product fallback
- Support dynamic command routing with compat layer updates
2026-03-29 16:29:35 +08:00

175 lines
4.8 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package app
import (
"os"
"strings"
"sync"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/market"
)
var (
dynamicMu sync.RWMutex
dynamicEndpoints map[string]string
dynamicProducts map[string]bool
dynamicAliases map[string]string
dynamicToolEndpoints map[string]string // tool name → endpoint
)
var legacyDirectRuntimeAliases = map[string]string{
"tb": "teambition",
"dingtalk-discovery": "discovery",
"dingtalk-oa-plus": "oa",
"dingtalk-ai-sincere-hire": "ai-sincere-hire",
}
// SetDynamicServers injects server data discovered from servers.json.
// All product endpoints are resolved dynamically from this data.
func SetDynamicServers(servers []market.ServerDescriptor) {
dynamicMu.Lock()
defer dynamicMu.Unlock()
endpoints := make(map[string]string)
products := make(map[string]bool)
aliases := make(map[string]string)
toolEndpoints := make(map[string]string)
for _, server := range servers {
if server.CLI.Skip {
continue
}
id := strings.TrimSpace(server.CLI.ID)
endpoint := strings.TrimSpace(server.Endpoint)
if id != "" && endpoint != "" {
endpoints[id] = endpoint
products[id] = true
}
cmd := strings.TrimSpace(server.CLI.Command)
if cmd != "" && cmd != id && endpoint != "" {
endpoints[cmd] = endpoint
products[cmd] = true
}
for _, alias := range server.CLI.Aliases {
alias = strings.TrimSpace(alias)
if alias != "" && endpoint != "" {
endpoints[alias] = endpoint
products[alias] = true
// Build alias → CLI.ID mapping
aliases[alias] = id
}
}
// Build tool → endpoint mapping from CLI tools and overrides.
if endpoint != "" {
for _, tool := range server.CLI.Tools {
toolName := strings.TrimSpace(tool.Name)
if toolName != "" {
toolEndpoints[toolName] = endpoint
}
}
for toolName := range server.CLI.ToolOverrides {
toolName = strings.TrimSpace(toolName)
if toolName != "" {
toolEndpoints[toolName] = endpoint
}
}
}
}
dynamicEndpoints = endpoints
dynamicProducts = products
dynamicAliases = aliases
dynamicToolEndpoints = toolEndpoints
}
func shouldUseDirectRuntime(invocation executor.Invocation) bool {
if strings.TrimSpace(os.Getenv(cli.CatalogFixtureEnv)) != "" {
return false
}
switch invocation.Kind {
case "compat_invocation", "helper_invocation":
return true
default:
return false
}
}
func directRuntimeEndpoint(productID, toolName string) (string, bool) {
// Priority 0: env-var override always wins (DINGTALK_<PRODUCT>_MCP_URL).
normalized := normalizeDirectRuntimeProductID(productID)
for _, candidate := range []string{strings.TrimSpace(productID), normalized} {
if candidate == "" {
continue
}
if override, ok := productEndpointOverride(candidate); ok {
return override, true
}
}
dynamicMu.RLock()
de := dynamicEndpoints
te := dynamicToolEndpoints
dynamicMu.RUnlock()
// Priority 1: tool-level endpoint (resolves multi-endpoint products).
if tool := strings.TrimSpace(toolName); tool != "" && te != nil {
if endpoint, ok := te[tool]; ok {
return endpoint, true
}
}
// Priority 2: product-level endpoint.
for _, candidate := range []string{strings.TrimSpace(productID), normalized} {
if candidate == "" {
continue
}
if de != nil {
if endpoint, ok := de[candidate]; ok {
return endpoint, true
}
}
}
return "", false
}
// DirectRuntimeProductIDs returns the set of product IDs that have direct
// runtime endpoints configured, sourced from dynamic server discovery.
func DirectRuntimeProductIDs() map[string]bool {
dynamicMu.RLock()
dp := dynamicProducts
dynamicMu.RUnlock()
ids := make(map[string]bool, len(dp))
for key := range dp {
ids[key] = true
}
return ids
}
func normalizeDirectRuntimeProductID(productID string) string {
dynamicMu.RLock()
da := dynamicAliases
dynamicMu.RUnlock()
trimmed := strings.TrimSpace(productID)
if da != nil {
if normalizedID, ok := da[trimmed]; ok && normalizedID != "" {
return normalizedID
}
}
if normalizedID, ok := legacyDirectRuntimeAliases[trimmed]; ok {
return normalizedID
}
return trimmed
}