Files
dingtalk-workspace-cli/internal/cli/loader.go
T

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