From 6067906c5524cdd2acdabb19239484ecc22f85cc Mon Sep 17 00:00:00 2001 From: "shangguanxuan.sgx" Date: Thu, 18 Jun 2026 03:44:22 +0800 Subject: [PATCH] fix(devdoc): fallback empty rag article search --- internal/app/legacy.go | 11 +- internal/app/root.go | 2 +- internal/cli/loader.go | 2 +- internal/discovery/service.go | 2 +- internal/helpers/devdoc.go | 77 +++++++++++--- internal/helpers/devdoc_test.go | 171 ++++++++++++++++++++++++++++++- internal/market/registry.go | 74 ++++++++++++- internal/market/registry_test.go | 68 ++++++++++++ pkg/config/constants.go | 6 +- test/cli_compat/devdoc_test.go | 14 ++- 10 files changed, 393 insertions(+), 34 deletions(-) diff --git a/internal/app/legacy.go b/internal/app/legacy.go index acd6e753..6202de17 100644 --- a/internal/app/legacy.go +++ b/internal/app/legacy.go @@ -241,7 +241,7 @@ func loadDynamicCommands(ctx context.Context, runner executor.Runner) []*cobra.C // no-op: fall through to FallbackServers check below } } else { - servers = market.NormalizeServers(resp, "market") + servers = market.NormalizeServersForBaseURL(resp, "market", registryDiscoveryBaseURL()) if discoveryTraceEnabled() { slog.Info("loadDynamicCommands: sync discovery fetch ok", "partition", partition, @@ -499,6 +499,13 @@ func fetchRegistryServers(ctx context.Context, httpClient *http.Client) (market. return client.FetchServers(ctx, config.DefaultFetchServersLimit) } +func registryDiscoveryBaseURL() string { + if editionURL := strings.TrimSpace(edition.Get().DiscoveryURL); editionURL != "" { + return editionURL + } + return DiscoveryBaseURL() +} + // asyncRevalidateRegistry refreshes the registry cache in the background. // Uses a short timeout derived from the parent context and silently ignores // errors — the next CLI invocation will pick up the refreshed cache or retry. @@ -511,7 +518,7 @@ func asyncRevalidateRegistry(parent context.Context, store *cache.Store, partiti slog.Debug("asyncRevalidateRegistry: fetch failed", "error", err) return } - servers := market.NormalizeServers(resp, "market") + servers := market.NormalizeServersForBaseURL(resp, "market", registryDiscoveryBaseURL()) if saveErr := store.SaveRegistry(partition, cache.RegistrySnapshot{Servers: servers}); saveErr != nil { slog.Debug("asyncRevalidateRegistry: save failed", "error", saveErr) } diff --git a/internal/app/root.go b/internal/app/root.go index 9cfe7ba3..18024a50 100644 --- a/internal/app/root.go +++ b/internal/app/root.go @@ -474,7 +474,7 @@ func newCacheCommand() *cobra.Command { if err != nil { return apperrors.NewDiscovery(fmt.Sprintf("cache refresh: fetch server list failed: %v", err)) } - servers := market.NormalizeServers(resp, "live_market") + servers := market.NormalizeServersForBaseURL(resp, "live_market", registryDiscoveryBaseURL()) _ = store.SaveRegistry(service.CachePartition(), cache.RegistrySnapshot{Servers: servers}) selected := selectServersForProduct(servers, product) diff --git a/internal/cli/loader.go b/internal/cli/loader.go index b67edce9..19dd85bd 100644 --- a/internal/cli/loader.go +++ b/internal/cli/loader.go @@ -279,7 +279,7 @@ func (l EnvironmentLoader) Load(ctx context.Context) (ir.Catalog, error) { // accepted subset. See plan fix-wukong-discovery-missing-servers Phase 4.3. logDiscoveryWarnings(response.Metadata.Warnings) - servers := market.NormalizeServers(response, "live_market") + servers := market.NormalizeServersForBaseURL(response, "live_market", baseURL) _ = store.SaveRegistry(partition, cache.RegistrySnapshot{Servers: servers}) changedKeys := cache.ChangedServerKeysByUpdatedAt(cached.Registry.Servers, servers) diff --git a/internal/discovery/service.go b/internal/discovery/service.go index 39f722c2..ab5c7887 100644 --- a/internal/discovery/service.go +++ b/internal/discovery/service.go @@ -94,7 +94,7 @@ func (s *Service) DiscoverServers(ctx context.Context) ([]market.ServerDescripto response, err := s.MarketClient.FetchServers(ctx, 200) if err == nil { - servers := market.NormalizeServers(response, "live_market") + servers := market.NormalizeServersForBaseURL(response, "live_market", s.MarketClient.BaseURL) _ = s.Cache.SaveRegistry(partition, cache.RegistrySnapshot{Servers: servers}) return servers, nil } diff --git a/internal/helpers/devdoc.go b/internal/helpers/devdoc.go index 81ff0de6..b90bfe65 100644 --- a/internal/helpers/devdoc.go +++ b/internal/helpers/devdoc.go @@ -82,7 +82,7 @@ func (devdocHandler) Command(runner executor.Runner) *cobra.Command { func newDevdocArticleSearchCommand(runner executor.Runner) *cobra.Command { cmd := &cobra.Command{ - Use: "search [keyword]", + Use: "search [query]", Short: "搜索开放平台文档", // Bind the devdoc MCP tool so `dws schema dev.doc.search` can fetch this // command's real parameters live from the devdoc server (mcp-source), @@ -91,11 +91,11 @@ func newDevdocArticleSearchCommand(runner executor.Runner) *cobra.Command { Args: cobra.MaximumNArgs(1), DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { - keyword := devdocFlagOrFallback(cmd, "keyword", "query") - if keyword == "" && len(args) > 0 { - keyword = strings.TrimSpace(args[0]) + query := devdocFlagOrFallback(cmd, "keyword", "query") + if query == "" && len(args) > 0 { + query = strings.TrimSpace(args[0]) } - if keyword == "" { + if query == "" { return apperrors.NewValidation("--keyword is required") } page, _ := cmd.Flags().GetInt("page") @@ -105,17 +105,16 @@ func newDevdocArticleSearchCommand(runner executor.Runner) *cobra.Command { size := devdocPageSize(cmd) cursor := devdocFlagOrFallback(cmd, "cursor") return runDevdocToolWithFallback(cmd, runner, devdocArticleSearchTool, - devdocSearchParams(keyword, page, size, cursor), + devdocSearchParams(query, page, size, cursor), devdocArticleSearchLegacyTool, - devdocLegacySearchParams(keyword, page, size, cursor)) + devdocLegacySearchParams(query, page, size, cursor)) }, } preferLegacyLeaf(cmd) - // Flag names == MCP param names (keyword/size), so `dws schema dev.doc.search` - // shows exactly what the user types. --query/--page-size kept as hidden - // back-compat aliases for existing scripts and `dws devdoc`. + // The published devdoc RAG action accepts keyword. Keep --query as a hidden + // compatibility alias for older scripts. cmd.Flags().String("keyword", "", "搜索关键词 (必填)") - addDevdocHiddenStringFlag(cmd, "query", "--keyword 的别名") + addDevdocHiddenStringFlag(cmd, "query", "--keyword 的兼容别名") cmd.Flags().Int("page", 1, "分页页码 (从 1 开始,默认 1)") cmd.Flags().String("cursor", "", "分页游标,翻页传上次返回的 nextCursor;传入后不再使用 --page") cmd.Flags().Int("size", 20, "单页条数 (默认 20)") @@ -215,10 +214,10 @@ func newDevdocErrorDiagnoseCommand(runner executor.Runner) *cobra.Command { return cmd } -func devdocSearchParams(keyword string, page int, size int, cursor string) map[string]any { +func devdocSearchParams(query string, page int, size int, cursor string) map[string]any { params := map[string]any{ - "keyword": keyword, - "pageSize": size, + "keyword": query, + "size": size, } if cursor != "" { params["cursor"] = cursor @@ -275,8 +274,12 @@ func runDevdocToolWithFallbacks(cmd *cobra.Command, runner executor.Runner, tool return writeCommandPayload(cmd, invocation) } result, err := runner.Run(cmd.Context(), invocation) + currentTool := tool for _, fallback := range fallbacks { - if err == nil || !isDevdocToolNotFoundError(err) { + if err == nil && !shouldFallbackDevdocResult(currentTool, result) { + break + } + if err != nil && !isDevdocToolNotFoundError(err) { break } if fallback.tool == "" { @@ -293,6 +296,7 @@ func runDevdocToolWithFallbacks(cmd *cobra.Command, runner executor.Runner, tool fallbackParams, ) result, err = runner.Run(cmd.Context(), fallbackInvocation) + currentTool = fallback.tool } if err != nil { return err @@ -300,6 +304,48 @@ func runDevdocToolWithFallbacks(cmd *cobra.Command, runner executor.Runner, tool return writeCommandPayload(cmd, result) } +func shouldFallbackDevdocResult(tool string, result executor.Result) bool { + if tool != devdocArticleSearchTool || !result.Invocation.Implemented { + return false + } + content, ok := result.Response["content"].(map[string]any) + if !ok { + return false + } + return isEmptyDevdocArticleRAGContent(content) +} + +func isEmptyDevdocArticleRAGContent(content map[string]any) bool { + if hasDevdocArticleRAGPayload(content) { + return false + } + for _, key := range []string{"materials", "references", "ragContext", "result", "success"} { + if _, ok := content[key]; ok { + return true + } + } + return false +} + +func hasDevdocArticleRAGPayload(content map[string]any) bool { + if hasNonEmptyList(content["materials"]) || hasNonEmptyList(content["references"]) || hasNonEmptyString(content["ragContext"]) { + return true + } + result, _ := content["result"].(map[string]any) + return hasNonEmptyList(result["items"]) || hasNonEmptyList(result["materials"]) || + hasNonEmptyList(result["references"]) || hasNonEmptyString(result["ragContext"]) +} + +func hasNonEmptyList(value any) bool { + items, ok := value.([]any) + return ok && len(items) > 0 +} + +func hasNonEmptyString(value any) bool { + text, ok := value.(string) + return ok && strings.TrimSpace(text) != "" +} + func isDevdocToolNotFoundError(err error) bool { if err == nil { return false @@ -308,7 +354,6 @@ func isDevdocToolNotFoundError(err error) bool { return strings.Contains(msg, "tool not found") || strings.Contains(msg, "unknown tool") || strings.Contains(msg, "mcp_tool_not_found") || - strings.Contains(msg, "tool metadata api error") || strings.Contains(msg, "未找到指定工具") } diff --git a/internal/helpers/devdoc_test.go b/internal/helpers/devdoc_test.go index 2470819b..8fa9b478 100644 --- a/internal/helpers/devdoc_test.go +++ b/internal/helpers/devdoc_test.go @@ -24,9 +24,10 @@ import ( ) type devdocCommandRunner struct { - last executor.Invocation - calls []executor.Invocation - errors map[string]error + last executor.Invocation + calls []executor.Invocation + errors map[string]error + responses map[string]map[string]any } func (r *devdocCommandRunner) Run(_ context.Context, invocation executor.Invocation) (executor.Result, error) { @@ -37,6 +38,15 @@ func (r *devdocCommandRunner) Run(_ context.Context, invocation executor.Invocat return executor.Result{}, err } } + if r.responses != nil { + if content, ok := r.responses[invocation.Tool]; ok { + invocation.Implemented = true + return executor.Result{ + Invocation: invocation, + Response: map[string]any{"content": content}, + }, nil + } + } return executor.Result{Invocation: invocation}, nil } @@ -59,11 +69,20 @@ func TestDevdocArticleSearchAcceptsWukongKeywordAlias(t *testing.T) { if got := runner.last.Params["keyword"]; got != "openConversationId" { t.Fatalf("keyword = %#v, want openConversationId", got) } + if _, ok := runner.last.Params["query"]; ok { + t.Fatalf("query must not be sent to article RAG search: %#v", runner.last.Params) + } if got := runner.last.Params["cursor"]; got != "tok-d" { t.Fatalf("cursor = %#v, want tok-d", got) } - if got := runner.last.Params["pageSize"]; got != 5 { - t.Fatalf("pageSize = %#v, want 5", got) + if got := runner.last.Params["size"]; got != 5 { + t.Fatalf("size = %#v, want 5", got) + } + if _, ok := runner.last.Params["pageSize"]; ok { + t.Fatalf("pageSize must not be sent to RAG search: %#v", runner.last.Params) + } + if _, ok := runner.last.Params["CliRagSearchReqVO"]; ok { + t.Fatalf("CliRagSearchReqVO wrapper must not be sent to RAG search: %#v", runner.last.Params) } } @@ -168,6 +187,12 @@ func TestDevdocErrorDiagnoseFallsBackToArticleSearchTools(t *testing.T) { if got := runner.calls[1].Params["cursor"]; got != "2" { t.Fatalf("RAG fallback cursor = %#v, want 2", got) } + if got := runner.calls[1].Params["keyword"]; got != "40014 获取用户信息 req-1" { + t.Fatalf("RAG fallback keyword = %#v, want merged diagnostic query", got) + } + if _, ok := runner.calls[1].Params["query"]; ok { + t.Fatalf("RAG fallback query must not be sent: %#v", runner.calls[1].Params) + } if _, ok := runner.calls[1].Params["page"]; ok { t.Fatalf("RAG fallback page must be omitted when cursor is set: %#v", runner.calls[1].Params) } @@ -222,6 +247,139 @@ func TestDevdocArticleSearchFallsBackOnStructuredToolMetadataError(t *testing.T) } } +func TestDevdocArticleSearchDoesNotFallbackOnGenericToolMetadataError(t *testing.T) { + t.Parallel() + + runner := &devdocCommandRunner{ + errors: map[string]error{ + devdocArticleSearchTool: apperrors.NewAPI("business error: success=false", + apperrors.WithReason("business_error"), + apperrors.WithServerDiag(apperrors.ServerDiagnostics{ + ServerErrorCode: "PARAM_ERROR", + TechnicalDetail: "Tool metadata API error: PARAM_ERROR - 参数不能为空", + }), + ), + }, + } + cmd := devdocHandler{}.Command(runner) + var out, errOut bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&errOut) + cmd.SetArgs([]string{"article", "search", "--query", "Webhook"}) + + if err := cmd.Execute(); err == nil { + t.Fatalf("Execute() error = nil, want metadata PARAM_ERROR") + } + if len(runner.calls) != 1 { + t.Fatalf("calls = %d, want 1", len(runner.calls)) + } + if got := runner.calls[0].Tool; got != devdocArticleSearchTool { + t.Fatalf("first tool = %q, want %s", got, devdocArticleSearchTool) + } +} + +func TestDevdocArticleSearchFallsBackOnEmptyRAGContent(t *testing.T) { + t.Parallel() + + runner := &devdocCommandRunner{ + responses: map[string]map[string]any{ + devdocArticleSearchTool: { + "materials": []any{}, + "references": []any{}, + "ragContext": nil, + }, + devdocArticleSearchLegacyTool: { + "success": true, + "result": map[string]any{ + "items": []any{map[string]any{"title": "OAuth2.0鉴权 - 开放平台"}}, + }, + }, + }, + } + cmd := devdocHandler{}.Command(runner) + var out, errOut bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&errOut) + cmd.SetArgs([]string{"article", "search", "--keyword", "OAuth2", "--size", "2"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut.String()) + } + if len(runner.calls) != 2 { + t.Fatalf("calls = %d, want 2", len(runner.calls)) + } + if got := runner.calls[0].Tool; got != devdocArticleSearchTool { + t.Fatalf("first tool = %q, want %s", got, devdocArticleSearchTool) + } + if got := runner.calls[1].Tool; got != devdocArticleSearchLegacyTool { + t.Fatalf("second tool = %q, want %s", got, devdocArticleSearchLegacyTool) + } +} + +func TestDevdocArticleSearchFallsBackOnPaginationOnlyRAGContent(t *testing.T) { + t.Parallel() + + runner := &devdocCommandRunner{ + responses: map[string]map[string]any{ + devdocArticleSearchTool: { + "success": true, + "result": map[string]any{"nextCursor": "2"}, + }, + devdocArticleSearchLegacyTool: { + "success": true, + "result": map[string]any{ + "items": []any{map[string]any{"title": "获取用户token - 开放平台"}}, + }, + }, + }, + } + cmd := devdocHandler{}.Command(runner) + var out, errOut bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&errOut) + cmd.SetArgs([]string{"article", "search", "--query", "OAuth2", "--cursor", "3"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut.String()) + } + if len(runner.calls) != 2 { + t.Fatalf("calls = %d, want 2", len(runner.calls)) + } + if got := runner.calls[1].Tool; got != devdocArticleSearchLegacyTool { + t.Fatalf("second tool = %q, want %s", got, devdocArticleSearchLegacyTool) + } + if got := runner.calls[1].Params["page"]; got != 3 { + t.Fatalf("legacy fallback page = %#v, want cursor-derived page 3", got) + } +} + +func TestDevdocArticleSearchKeepsNonEmptyRAGContent(t *testing.T) { + t.Parallel() + + runner := &devdocCommandRunner{ + responses: map[string]map[string]any{ + devdocArticleSearchTool: { + "materials": []any{map[string]any{"title": "OAuth2.0鉴权 - 开放平台"}}, + }, + }, + } + cmd := devdocHandler{}.Command(runner) + var out, errOut bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&errOut) + cmd.SetArgs([]string{"article", "search", "--keyword", "OAuth2"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut.String()) + } + if len(runner.calls) != 1 { + t.Fatalf("calls = %d, want 1", len(runner.calls)) + } + if got := runner.calls[0].Tool; got != devdocArticleSearchTool { + t.Fatalf("first tool = %q, want %s", got, devdocArticleSearchTool) + } +} + func TestDevdocArticleSearchAcceptsPositionalKeyword(t *testing.T) { t.Parallel() @@ -238,4 +396,7 @@ func TestDevdocArticleSearchAcceptsPositionalKeyword(t *testing.T) { if got := runner.last.Params["keyword"]; got != "MCP" { t.Fatalf("keyword = %#v, want MCP", got) } + if _, ok := runner.last.Params["query"]; ok { + t.Fatalf("query must not be sent to article RAG search: %#v", runner.last.Params) + } } diff --git a/internal/market/registry.go b/internal/market/registry.go index 78bd0950..50f0c308 100644 --- a/internal/market/registry.go +++ b/internal/market/registry.go @@ -668,7 +668,12 @@ func isPrivateHost(host string) bool { } func NormalizeServers(response ListResponse, source string) []ServerDescriptor { + return NormalizeServersForBaseURL(response, source, "") +} + +func NormalizeServersForBaseURL(response ListResponse, source string, discoveryBaseURL string) []ServerDescriptor { bestByEndpoint := make(map[string]ServerDescriptor) + forcePreGateway := isPreMCPBaseURL(discoveryBaseURL) for _, envelope := range response.Servers { meta := envelope.Meta.Registry @@ -681,7 +686,7 @@ func NormalizeServers(response ListResponse, source string) []ServerDescriptor { continue } - endpoint := NormalizeEndpoint(remoteURL) + endpoint := normalizeEndpoint(remoteURL, forcePreGateway) descriptor := ServerDescriptor{ Key: ServerKey(endpoint), DisplayName: strings.TrimSpace(envelope.Server.Name), @@ -822,10 +827,15 @@ func hasDeprecatedMarker(displayName string) bool { } func NormalizeEndpoint(raw string) string { + return normalizeEndpoint(raw, false) +} + +func normalizeEndpoint(raw string, forcePreGateway bool) string { parsed, err := url.Parse(raw) if err != nil { return strings.TrimSpace(raw) } + normalizeGatewayHost(parsed, forcePreGateway) parsed.Fragment = "" parsed.Path = strings.TrimRight(parsed.Path, "/") values := parsed.Query() @@ -846,6 +856,68 @@ func NormalizeEndpoint(raw string) string { return parsed.String() } +func normalizeGatewayHost(parsed *url.URL, forcePreGateway bool) { + if parsed == nil { + return + } + host := parsed.Hostname() + nextHost, ok := normalizedGatewayHost(host, forcePreGateway) + if !ok { + return + } + if port := parsed.Port(); port != "" { + parsed.Host = net.JoinHostPort(nextHost, port) + } else { + parsed.Host = nextHost + } +} + +func normalizedGatewayHost(host string, forcePreGateway bool) (string, bool) { + host = strings.TrimSpace(strings.ToLower(host)) + if host == "" { + return "", false + } + if forcePreGateway { + switch { + case host == "mcp.dingtalk.com" || host == "mcp-gw.dingtalk.com" || + host == "pre-mcp.dingtalk.com" || host == "pre-mcp-gw.dingtalk.com": + return "pre-mcp-gw.dingtalk.com", true + case strings.HasPrefix(host, "pre-mcp-gw."): + return host, true + case strings.HasPrefix(host, "pre-mcp."): + return strings.Replace(host, "pre-mcp.", "pre-mcp-gw.", 1), true + case strings.HasPrefix(host, "mcp-gw."): + return strings.Replace(host, "mcp-gw.", "pre-mcp-gw.", 1), true + case strings.HasPrefix(host, "mcp."): + return strings.Replace(host, "mcp.", "pre-mcp-gw.", 1), true + default: + return "", false + } + } + switch { + case host == "mcp.dingtalk.com": + return "mcp-gw.dingtalk.com", true + case strings.HasPrefix(host, "pre-mcp."): + return strings.Replace(host, "pre-mcp.", "pre-mcp-gw.", 1), true + case strings.HasPrefix(host, "mcp."): + return strings.Replace(host, "mcp.", "mcp-gw.", 1), true + default: + return "", false + } +} + +func isPreMCPBaseURL(raw string) bool { + parsed, err := url.Parse(strings.TrimSpace(raw)) + if err != nil { + return false + } + host := strings.ToLower(parsed.Hostname()) + return host == "pre-mcp.dingtalk.com" || + host == "pre-mcp-gw.dingtalk.com" || + strings.HasPrefix(host, "pre-mcp.") || + strings.HasPrefix(host, "pre-mcp-gw.") +} + func ServerKey(endpoint string) string { sum := sha256.Sum256([]byte(endpoint)) return hex.EncodeToString(sum[:8]) diff --git a/internal/market/registry_test.go b/internal/market/registry_test.go index 5b030104..577f4c47 100644 --- a/internal/market/registry_test.go +++ b/internal/market/registry_test.go @@ -101,6 +101,74 @@ func TestNormalizeServersDeduplicatesSameNameAcrossEndpoints(t *testing.T) { } } +func TestNormalizeServersConvertsPreMCPRemoteToGatewayEndpoint(t *testing.T) { + t.Parallel() + + response := ListResponse{ + Servers: []ServerEnvelope{ + { + Server: RegistryServer{ + Name: "钉钉开放平台文档搜索", + Remotes: []RegistryRemote{ + {Type: "streamable-http", URL: "https://pre-mcp.dingtalk.com/server/devdoc?key=b&key=a#fragment"}, + }, + }, + Meta: EnvelopeMeta{ + Registry: RegistryMetadata{Status: "active", UpdatedAt: "2026-06-16T00:00:00Z"}, + CLI: CLIOverlay{ID: "devdoc", Command: "devdoc"}, + }, + }, + }, + } + + servers := NormalizeServers(response, "live_market") + if len(servers) != 1 { + t.Fatalf("NormalizeServers() len = %d, want 1", len(servers)) + } + want := "https://pre-mcp-gw.dingtalk.com/server/devdoc?key=a&key=b" + if servers[0].Endpoint != want { + t.Fatalf("NormalizeServers() endpoint = %q, want %q", servers[0].Endpoint, want) + } +} + +func TestNormalizeServersForBaseURLForcesPreGatewayEndpoint(t *testing.T) { + t.Parallel() + + response := ListResponse{ + Servers: []ServerEnvelope{ + { + Server: RegistryServer{ + Name: "钉钉文档", + Remotes: []RegistryRemote{ + {Type: "streamable-http", URL: "https://mcp-gw.dingtalk.com/server/doc?key=b&key=a#fragment"}, + }, + }, + Meta: EnvelopeMeta{ + Registry: RegistryMetadata{Status: "active", UpdatedAt: "2026-06-16T00:00:00Z"}, + CLI: CLIOverlay{ID: "doc", Command: "doc"}, + }, + }, + }, + } + + prodServers := NormalizeServers(response, "live_market") + if len(prodServers) != 1 { + t.Fatalf("NormalizeServers() len = %d, want 1", len(prodServers)) + } + if want := "https://mcp-gw.dingtalk.com/server/doc?key=a&key=b"; prodServers[0].Endpoint != want { + t.Fatalf("NormalizeServers() endpoint = %q, want %q", prodServers[0].Endpoint, want) + } + + preServers := NormalizeServersForBaseURL(response, "live_market", "https://pre-mcp.dingtalk.com") + if len(preServers) != 1 { + t.Fatalf("NormalizeServersForBaseURL() len = %d, want 1", len(preServers)) + } + want := "https://pre-mcp-gw.dingtalk.com/server/doc?key=a&key=b" + if preServers[0].Endpoint != want { + t.Fatalf("NormalizeServersForBaseURL() endpoint = %q, want %q", preServers[0].Endpoint, want) + } +} + // TestNormalizeServersPreservesDistinctCLIIDsOnSharedEndpoint guards the // bot-root / bot-message / bot-group split (issue: chat bot subtree vanished // from the CLI after NormalizeServers collapsed three envelopes that share a diff --git a/pkg/config/constants.go b/pkg/config/constants.go index c986e45a..0848ab37 100644 --- a/pkg/config/constants.go +++ b/pkg/config/constants.go @@ -161,9 +161,9 @@ const ( // Shared across auth, errors, and device-flow packages. const ( - // DefaultMCPBaseURL is the DingTalk MCP base URL. + // DefaultMCPBaseURL is the DingTalk MCP base URL for the pre-release build. // Override at runtime via ~/.dws/mcp_url file. - DefaultMCPBaseURL = "https://mcp.dingtalk.com" + DefaultMCPBaseURL = "https://pre-mcp.dingtalk.com" // DefaultTerminalBaseURL is the DingTalk developer platform base URL. // Override at runtime via ~/.dws/terminal_url file. @@ -189,7 +189,7 @@ func DefaultConfigDir() string { // GetMCPBaseURL returns the MCP base URL with priority: // 1. ~/.dws/mcp_url file content (for custom environment) -// 2. Default value (https://mcp.dingtalk.com) +// 2. Default value (https://pre-mcp.dingtalk.com) func GetMCPBaseURL() string { mcpURLPath := filepath.Join(DefaultConfigDir(), "mcp_url") if data, err := os.ReadFile(mcpURLPath); err == nil { diff --git a/test/cli_compat/devdoc_test.go b/test/cli_compat/devdoc_test.go index 09c689ae..2b189ec9 100644 --- a/test/cli_compat/devdoc_test.go +++ b/test/cli_compat/devdoc_test.go @@ -8,7 +8,7 @@ func TestDevdocArticleSearch_should_call_correct_tool(t *testing.T) { cap := setupTestDeps(t, "devdoc") root := buildRoot() err := execCmd(t, root, []string{"devdoc", "article", "search"}, map[string]string{ - "keyword": "MCP", "page": "1", "size": "10", + "query": "MCP", "page": "1", "size": "10", }) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -20,16 +20,22 @@ func TestDevdocArticleSearch_should_pass_keyword(t *testing.T) { cap := setupTestDeps(t, "devdoc") root := buildRoot() _ = execCmd(t, root, []string{"devdoc", "article", "search"}, map[string]string{ - "keyword": "openConversationId", "page": "1", "size": "10", + "query": "openConversationId", "page": "1", "size": "10", }) assertToolArg(t, cap, "keyword", "openConversationId") + assertArgNotPresent(t, cap, "query") + assertToolArg(t, cap, "size", float64(10)) + assertArgNotPresent(t, cap, "pageSize") + if _, ok := cap.last().Args["CliRagSearchReqVO"]; ok { + t.Fatalf("CliRagSearchReqVO wrapper must not be sent for RAG search: %v", cap.last().Args) + } } func TestDevdocArticleSearch_should_pass_cursor(t *testing.T) { cap := setupTestDeps(t, "devdoc") root := buildRoot() _ = execCmd(t, root, []string{"devdoc", "article", "search"}, map[string]string{ - "keyword": "Webhook", "cursor": "3", "size": "10", + "query": "Webhook", "cursor": "3", "size": "10", }) assertToolArg(t, cap, "cursor", "3") assertArgNotPresent(t, cap, "page") @@ -39,7 +45,7 @@ func TestDevdocArticleSearch_should_not_call_when_dry_run(t *testing.T) { cap := setupTestDepsWithDryRun(t, "devdoc") root := buildRoot() _ = execCmd(t, root, []string{"devdoc", "article", "search"}, map[string]string{ - "keyword": "MCP", "page": "1", "size": "10", + "query": "MCP", "page": "1", "size": "10", }) assertCallCount(t, cap, 0) }