194 lines
7.1 KiB
Go
194 lines
7.1 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 cli
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
|
)
|
|
|
|
// CatalogDegradedReason identifies why catalog discovery returned empty.
|
|
type CatalogDegradedReason string
|
|
|
|
const (
|
|
DegradedUnauthenticated CatalogDegradedReason = "unauthenticated"
|
|
DegradedMarketUnreachable CatalogDegradedReason = "market_unreachable"
|
|
DegradedRuntimeAllFailed CatalogDegradedReason = "runtime_all_failed"
|
|
)
|
|
|
|
// CatalogDegraded is returned by EnvironmentLoader.Load when discovery
|
|
// fails for a diagnosable reason. Callers that need graceful degradation
|
|
// (e.g. the runtime runner) can check errors.As and fall back to an
|
|
// empty catalog; callers like the schema command can surface the hint.
|
|
type CatalogDegraded struct {
|
|
Reason CatalogDegradedReason
|
|
Hint string
|
|
ServerCount int
|
|
}
|
|
|
|
func (e *CatalogDegraded) Error() string { return string(e.Reason) + ": " + e.Hint }
|
|
|
|
func degradedHint(reason CatalogDegradedReason, serverCount int) string {
|
|
embedded := edition.Get().IsEmbedded
|
|
switch reason {
|
|
case DegradedUnauthenticated:
|
|
if embedded {
|
|
return "未登录,请重新认证"
|
|
}
|
|
return "未登录,无法发现 MCP 服务。请先执行: dws auth login"
|
|
case DegradedMarketUnreachable:
|
|
if embedded {
|
|
return "无法连接 MCP 市场,请检查网络"
|
|
}
|
|
return "无法连接 MCP 市场,请检查网络"
|
|
case DegradedRuntimeAllFailed:
|
|
if embedded {
|
|
return fmt.Sprintf("已发现 %d 个服务但连接全部失败,请稍后重试", serverCount)
|
|
}
|
|
return fmt.Sprintf("已发现 %d 个服务但连接全部失败;静态端点模式下请检查 internal/syncdata 生成物或稍后重试", serverCount)
|
|
default:
|
|
return "MCP 服务发现失败"
|
|
}
|
|
}
|
|
|
|
func newCatalogDegraded(reason CatalogDegradedReason, serverCount int) *CatalogDegraded {
|
|
return &CatalogDegraded{
|
|
Reason: reason,
|
|
Hint: degradedHint(reason, serverCount),
|
|
ServerCount: serverCount,
|
|
}
|
|
}
|
|
|
|
const (
|
|
CatalogFixtureEnv = "DWS_CATALOG_FIXTURE"
|
|
CacheDirEnv = "DWS_CACHE_DIR"
|
|
PluginColdTimeoutEnv = "DWS_PLUGIN_COLD_TIMEOUT"
|
|
)
|
|
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
// Catalog types (formerly in internal/ir, now inlined as minimal stubs)
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
|
|
// Catalog holds the discovered MCP product surface.
|
|
type Catalog struct {
|
|
Products []CanonicalProduct `json:"products"`
|
|
}
|
|
|
|
// FindProduct returns the product with the given ID.
|
|
func (c Catalog) FindProduct(id string) (CanonicalProduct, bool) {
|
|
for _, product := range c.Products {
|
|
if product.ID == id {
|
|
return product, true
|
|
}
|
|
}
|
|
return CanonicalProduct{}, false
|
|
}
|
|
|
|
// CanonicalProduct represents a single MCP product in the catalog.
|
|
type CanonicalProduct struct {
|
|
ID string `json:"id"`
|
|
DisplayName string `json:"display_name"`
|
|
Description string `json:"description,omitempty"`
|
|
ServerKey string `json:"server_key"`
|
|
Endpoint string `json:"endpoint"`
|
|
Source string `json:"source,omitempty"`
|
|
Tools []ToolDescriptor `json:"tools"`
|
|
}
|
|
|
|
// FindTool returns the tool with the given RPC name.
|
|
func (p CanonicalProduct) FindTool(name string) (ToolDescriptor, bool) {
|
|
for _, tool := range p.Tools {
|
|
if tool.RPCName == name {
|
|
return tool, true
|
|
}
|
|
}
|
|
return ToolDescriptor{}, false
|
|
}
|
|
|
|
// ToolDescriptor represents a single tool in a canonical product.
|
|
type ToolDescriptor struct {
|
|
RPCName string `json:"rpc_name"`
|
|
CanonicalPath string `json:"canonical_path"`
|
|
}
|
|
|
|
// CLIFlagHint holds CLI flag alias/shorthand metadata for a tool parameter.
|
|
type CLIFlagHint struct {
|
|
Shorthand string `json:"shorthand,omitempty"`
|
|
Alias string `json:"alias,omitempty"`
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
// CatalogLoader interface
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
|
|
// CatalogLoader loads the canonical catalog.
|
|
type CatalogLoader interface {
|
|
Load(context.Context) (Catalog, error)
|
|
}
|
|
|
|
// StaticLoader returns a pre-built catalog.
|
|
type StaticLoader struct {
|
|
Catalog Catalog
|
|
}
|
|
|
|
func (l StaticLoader) Load(_ context.Context) (Catalog, error) {
|
|
return l.Catalog, nil
|
|
}
|
|
|
|
// CatalogLoaderFrom creates a CatalogLoader that returns a
|
|
// pre-loaded catalog and error.
|
|
func CatalogLoaderFrom(catalog Catalog, err error) CatalogLoader {
|
|
return &preloadedLoader{catalog: catalog, err: err}
|
|
}
|
|
|
|
type preloadedLoader struct {
|
|
catalog Catalog
|
|
err error
|
|
}
|
|
|
|
func (l *preloadedLoader) Load(_ context.Context) (Catalog, error) {
|
|
return l.catalog, l.err
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
// EnvironmentLoader (static endpoint mode — no longer does live discovery)
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
|
|
// EnvironmentLoader provides catalog loading. In the post-discovery
|
|
// architecture this always returns an empty catalog; endpoint resolution
|
|
// is handled by the direct runtime path.
|
|
type EnvironmentLoader struct {
|
|
LookupEnv func(string) (string, bool)
|
|
CatalogBaseURLOverride string
|
|
AuthTokenFunc func(context.Context) string
|
|
LoggerFunc func() *slog.Logger
|
|
}
|
|
|
|
// NewEnvironmentLoader creates an EnvironmentLoader with default settings.
|
|
func NewEnvironmentLoader() EnvironmentLoader {
|
|
return EnvironmentLoader{}
|
|
}
|
|
|
|
// Load returns an empty catalog. All endpoint resolution now uses the
|
|
// direct runtime path (dynamic server registry).
|
|
func (l EnvironmentLoader) Load(_ context.Context) (Catalog, error) {
|
|
return Catalog{}, nil
|
|
}
|
|
|
|
// Ensure unused imports are consumed.
|
|
var _ = newCatalogDegraded
|