fix(devdoc): fallback empty rag article search

This commit is contained in:
shangguanxuan.sgx
2026-06-18 13:08:34 +08:00
parent 493ca36e08
commit 6067906c55
10 changed files with 393 additions and 34 deletions
+9 -2
View File
@@ -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)
}
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
}
+61 -16
View File
@@ -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, "未找到指定工具")
}
+166 -5
View File
@@ -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)
}
}
+73 -1
View File
@@ -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])
+68
View File
@@ -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
+3 -3
View File
@@ -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 {
+10 -4
View File
@@ -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)
}