From 1921e5e37eafc22a6d4cdfcaad206fa31798fbd9 Mon Sep 17 00:00:00 2001 From: "shangguanxuan.sgx" Date: Mon, 15 Jun 2026 18:09:51 +0800 Subject: [PATCH] feat(devdoc): support cursor pagination --- README.md | 2 +- README_zh.md | 2 +- docs/command-index.md | 6 +- internal/helpers/devdoc.go | 197 +++++++++++++++++- internal/helpers/devdoc_test.go | 171 ++++++++++++++- skills/mono/references/products/devdoc.md | 53 ++++- skills/mono/references/products/simple.md | 22 +- .../dingtalk-devdoc/references/devdoc.md | 53 ++++- .../multi/dingtalk-skill/references/skill.md | 24 ++- test/cli_compat/devdoc_test.go | 12 +- test/cli_compat/helpers_test.go | 21 +- test/skill_tests.md | 7 +- test/skill_tests_results.md | 14 +- 13 files changed, 552 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 9b7385c2..8c78237b 100644 --- a/README.md +++ b/README.md @@ -492,7 +492,7 @@ dws chat message send-by-bot --robot-code BOT_CODE --group GROUP_ID \ | Mail | `mail` | 18 | `mailbox` `message` `draft` `folder` `tag` `thread` `attachment` `user` | List mailboxes, KQL message search, read & send messages, drafts, folders, tags, threads, attachments, address-book user search | | Sheet | `sheet` | 23 | `range` `filter-view` (top-level: `create` `new` `list` `info` `read` `get` `update` `find` `replace` `append` `merge-cells` `unmerge-cells` `add-dimension` `insert-dimension` `delete-dimension` `move-dimension` `update-dimension` `write-image`) | Online spreadsheet (`contentType=ALIDOC`, `extension=axls`): worksheet CRUD, range read / write / append, dimension ops, cell merge / unmerge, find / replace, named filter views + sheet-level filters, image write | | Wiki | `wiki` | 21 | `space` `member` `node` `doc` `file` | Knowledge base management: spaces (`create` / `get` / `list` / `search`), members (`add` / `list` / `update`), node tree, docs & files | -| DevDoc | `devdoc` | 1 | `article` | Search the DingTalk Open Platform documentation | +| DevDoc | `devdoc` | 2 | `article` `error` | Search the DingTalk Open Platform documentation and diagnose API errors | | AI Search | `aisearch` | 3 | `person` | Enterprise people search by name / department / position / duty / supervisor / subordinate / phone / job-number (single command, multi-dimension filter) | | Live | `live` | 1 | `stream` | DingTalk live streaming: list my lives | | Raw API | `api` | 1 | — | Call any DingTalk OpenAPI directly (api / oapi dual-form), with automatic app-level token management | diff --git a/README_zh.md b/README_zh.md index ac4309b1..600699f9 100644 --- a/README_zh.md +++ b/README_zh.md @@ -488,7 +488,7 @@ dws chat message send-by-bot --robot-code BOT_CODE --group GROUP_ID \ | 邮箱 | `mail` | 18 | `mailbox` `message` `draft` `folder` `tag` `thread` `attachment` `user` | 邮箱地址列表、KQL 邮件搜索、读取与发送邮件、草稿、文件夹、标签、会话、附件、通讯录用户搜索 | | 在线电子表格 | `sheet` | 23 | `range` `filter-view`(顶层:`create` `new` `list` `info` `read` `get` `update` `find` `replace` `append` `merge-cells` `unmerge-cells` `add-dimension` `insert-dimension` `delete-dimension` `move-dimension` `update-dimension` `write-image`) | 在线电子表格(`contentType=ALIDOC`、`extension=axls`):工作表 CRUD、区域读写/追加、行列操作、合并/取消合并、查找替换、命名筛选视图 + 表级筛选、写入图片 | | 知识库 | `wiki` | 21 | `space` `member` `node` `doc` `file` | 知识库管理:空间(`create` / `get` / `list` / `search`)、成员(`add` / `list` / `update`)、节点树、文档与文件 | -| 开发者文档 | `devdoc` | 1 | `article` | 搜索钉钉开放平台文档 | +| 开发者文档 | `devdoc` | 2 | `article` `error` | 搜索钉钉开放平台文档并排查 API 调用错误 | | AI 搜问 | `aisearch` | 3 | `person` | 企业人员搜索:按姓名 / 部门 / 职位 / 职责 / 上级 / 下级 / 手机号 / 工号 多维度过滤(单命令) | | 直播 | `live` | 1 | `stream` | 钉钉直播:查看我的直播列表 | | Raw API | `api` | 1 | — | 直接调用任意钉钉 OpenAPI(api / oapi 双形态),自动管理应用级 Token | diff --git a/docs/command-index.md b/docs/command-index.md index dfd964fc..b012b5fb 100644 --- a/docs/command-index.md +++ b/docs/command-index.md @@ -36,7 +36,7 @@ Every command inherits these flags (documented here once, not repeated per comma - [`dws calendar` — Calendar](#dws-calendar) · 14 commands - [`dws chat` — Group Chat / IM](#dws-chat) · 23 commands - [`dws contact` — Contact Directory](#dws-contact) · 6 commands -- [`dws devdoc` — Open Platform Docs](#dws-devdoc) · 1 commands +- [`dws devdoc` — Open Platform Docs](#dws-devdoc) · 2 commands - [`dws ding` — DING Messages](#dws-ding) · 2 commands - [`dws doc` — DingTalk Doc](#dws-doc) · 21 commands - [`dws drive` — DingTalk Drive](#dws-drive) · 6 commands @@ -182,11 +182,12 @@ _Users, departments, and directory lookups._ _Search the DingTalk Open Platform documentation._ -**1 commands** +**2 commands** | Command | Description | When to use | |---|---|---| | `dws devdoc article search` | Search the DingTalk Open Platform documentation by keyword. | When the agent needs authoritative API reference or guides to answer a developer question. | +| `dws devdoc error diagnose` | Diagnose DingTalk Open Platform API errors by error code, requestId, or context. | When the agent needs troubleshooting material for a failed OpenAPI call. | ## `dws ding` — DING Messages @@ -320,4 +321,3 @@ _Personal todo task management._ | `dws todo task get` | Retrieve the full details of a todo item by ID. | When the agent inspects a specific todo's content, due date, and executors. | | `dws todo task list` | List todos for the current user within the current organization. | When the agent surfaces the user's outstanding tasks or builds a daily focus list. | | `dws todo task update` | Update a todo's title, description, due time, or executors. | When the agent edits an existing todo after new information comes in. | - diff --git a/internal/helpers/devdoc.go b/internal/helpers/devdoc.go index f8495532..540a9e7f 100644 --- a/internal/helpers/devdoc.go +++ b/internal/helpers/devdoc.go @@ -14,6 +14,8 @@ package helpers import ( + stderrors "errors" + "strconv" "strings" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cobracmd" @@ -22,6 +24,12 @@ import ( "github.com/spf13/cobra" ) +const ( + devdocArticleSearchTool = "search_open_platform_docs_rag" + devdocArticleSearchLegacyTool = "search_open_platform_docs" + devdocErrorDiagnoseTool = "search_open_error_code_rag" +) + func init() { RegisterPublic(func() Handler { return devdocHandler{} @@ -57,6 +65,18 @@ func (devdocHandler) Command(runner executor.Runner) *cobra.Command { } article.AddCommand(newDevdocArticleSearchCommand(runner)) root.AddCommand(article) + errs := &cobra.Command{ + Use: "error", + Short: "错误排查", + Args: cobra.NoArgs, + TraverseChildren: true, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + return cmd.Help() + }, + } + errs.AddCommand(newDevdocErrorDiagnoseCommand(runner)) + root.AddCommand(errs) return root } @@ -82,22 +102,144 @@ func newDevdocArticleSearchCommand(runner executor.Runner) *cobra.Command { if size < 1 { size = 10 } - return runDevdocTool(cmd, runner, "search_open_platform_docs", map[string]any{ - "keyword": keyword, - "page": page, - "size": size, - }) + cursor := devdocFlagOrFallback(cmd, "cursor") + return runDevdocToolWithFallback(cmd, runner, devdocArticleSearchTool, + devdocSearchParams(keyword, page, size, cursor), + devdocArticleSearchLegacyTool, + devdocLegacySearchParams(keyword, page, size, cursor)) }, } preferLegacyLeaf(cmd) cmd.Flags().String("query", "", "搜索关键词 (必填)") addDevdocHiddenStringFlag(cmd, "keyword", "--query 的悟空兼容别名") cmd.Flags().Int("page", 1, "分页页码 (从 1 开始,默认 1)") + cmd.Flags().String("cursor", "", "分页游标,翻页传上次返回的 nextCursor;传入后不再使用 --page") cmd.Flags().Int("size", 10, "分页大小 (默认 10)") return cmd } +func newDevdocErrorDiagnoseCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "diagnose", + Aliases: []string{"troubleshoot"}, + Short: "排查开放平台调用错误", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + errorCode := devdocFlagOrFallback(cmd, "error-code") + errorMessage := devdocFlagOrFallback(cmd, "error-message") + api := devdocFlagOrFallback(cmd, "api") + contextText := devdocFlagOrFallback(cmd, "context") + requestID := devdocFlagOrFallback(cmd, "request-id") + query := devdocFlagOrFallback(cmd, "query") + + parts := make([]string, 0, 4) + for _, part := range []string{query, errorMessage, api, contextText} { + if trimmed := strings.TrimSpace(part); trimmed != "" { + parts = append(parts, trimmed) + } + } + query = strings.Join(parts, " ") + if errorCode == "" && requestID == "" && query == "" { + return apperrors.NewValidation("--query, --error-code, or --request-id is required") + } + page, _ := cmd.Flags().GetInt("page") + if page < 1 { + page = 1 + } + size, _ := cmd.Flags().GetInt("size") + if size < 1 { + size = 10 + } + cursor := devdocFlagOrFallback(cmd, "cursor") + + params := map[string]any{ + "size": size, + } + if cursor != "" { + params["cursor"] = cursor + } else { + params["page"] = page + } + if errorCode != "" { + params["errorCode"] = errorCode + } + if query != "" { + params["query"] = query + } + if requestID != "" { + params["requestId"] = requestID + } + + fallbackQuery := strings.TrimSpace(strings.Join([]string{errorCode, query, requestID}, " ")) + return runDevdocToolWithFallbacks(cmd, runner, devdocErrorDiagnoseTool, params, []devdocFallbackTool{ + {tool: devdocArticleSearchTool, params: devdocSearchParams(fallbackQuery, page, size, cursor)}, + {tool: devdocArticleSearchLegacyTool, params: devdocLegacySearchParams(fallbackQuery, page, size, cursor)}, + }) + }, + } + cmd.Flags().String("query", "", "原始排查问题") + cmd.Flags().String("error-code", "", "错误码") + cmd.Flags().String("error-message", "", "错误描述,会合并进原始问题") + cmd.Flags().String("api", "", "API 名称,会合并进原始问题作为补充检索词") + cmd.Flags().String("context", "", "额外排查上下文,会合并进原始问题") + cmd.Flags().String("request-id", "", "开放平台 requestId") + cmd.Flags().Int("page", 1, "分页页码 (从 1 开始,默认 1)") + cmd.Flags().String("cursor", "", "分页游标,翻页传上次返回的 nextCursor;传入后不再使用 --page") + cmd.Flags().Int("size", 10, "分页大小 (默认 10)") + return cmd +} + +func devdocSearchParams(keyword string, page int, size int, cursor string) map[string]any { + params := map[string]any{ + "keyword": keyword, + "size": size, + } + if cursor != "" { + params["cursor"] = cursor + } else { + params["page"] = page + } + return params +} + +func devdocLegacySearchParams(keyword string, page int, size int, cursor string) map[string]any { + return map[string]any{ + "keyword": keyword, + "page": devdocPageFromCursor(page, cursor), + "size": size, + } +} + +func devdocPageFromCursor(page int, cursor string) int { + if cursor == "" { + return page + } + cursorPage, err := strconv.Atoi(cursor) + if err == nil && cursorPage > 0 { + return cursorPage + } + return page +} + func runDevdocTool(cmd *cobra.Command, runner executor.Runner, tool string, params map[string]any) error { + return runDevdocToolWithFallback(cmd, runner, tool, params, "", nil) +} + +type devdocFallbackTool struct { + tool string + params map[string]any +} + +func runDevdocToolWithFallback(cmd *cobra.Command, runner executor.Runner, tool string, params map[string]any, fallbackTool string, fallbackParams map[string]any) error { + var fallbacks []devdocFallbackTool + if fallbackTool != "" { + fallbacks = append(fallbacks, devdocFallbackTool{tool: fallbackTool, params: fallbackParams}) + } + return runDevdocToolWithFallbacks(cmd, runner, tool, params, fallbacks) +} + +func runDevdocToolWithFallbacks(cmd *cobra.Command, runner executor.Runner, tool string, params map[string]any, fallbacks []devdocFallbackTool) error { invocation := executor.NewHelperInvocation( cobracmd.LegacyCommandPath(cmd), "devdoc", @@ -108,12 +250,57 @@ func runDevdocTool(cmd *cobra.Command, runner executor.Runner, tool string, para return writeCommandPayload(cmd, invocation) } result, err := runner.Run(cmd.Context(), invocation) + for _, fallback := range fallbacks { + if err == nil || !isDevdocToolNotFoundError(err) { + break + } + if fallback.tool == "" { + continue + } + fallbackParams := fallback.params + if fallbackParams == nil { + fallbackParams = params + } + fallbackInvocation := executor.NewHelperInvocation( + cobracmd.LegacyCommandPath(cmd), + "devdoc", + fallback.tool, + fallbackParams, + ) + result, err = runner.Run(cmd.Context(), fallbackInvocation) + } if err != nil { return err } return writeCommandPayload(cmd, result) } +func isDevdocToolNotFoundError(err error) bool { + if err == nil { + return false + } + msg := normalizedDevdocErrorText(err) + 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, "未找到指定工具") +} + +func normalizedDevdocErrorText(err error) string { + parts := []string{strings.ToLower(err.Error())} + var typed *apperrors.Error + if stderrors.As(err, &typed) && typed != nil { + parts = append(parts, + strings.ToLower(typed.Reason), + strings.ToLower(typed.ServerDiag.ServerErrorCode), + strings.ToLower(typed.ServerDiag.TechnicalDetail), + strings.ToLower(typed.Hint), + ) + } + return strings.Join(parts, " ") +} + func devdocFlagOrFallback(cmd *cobra.Command, primary string, aliases ...string) string { names := append([]string{primary}, aliases...) for _, name := range names { diff --git a/internal/helpers/devdoc_test.go b/internal/helpers/devdoc_test.go index 96c042f7..aaff7088 100644 --- a/internal/helpers/devdoc_test.go +++ b/internal/helpers/devdoc_test.go @@ -16,17 +16,27 @@ package helpers import ( "bytes" "context" + "errors" "testing" + apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor" ) type devdocCommandRunner struct { - last executor.Invocation + last executor.Invocation + calls []executor.Invocation + errors map[string]error } func (r *devdocCommandRunner) Run(_ context.Context, invocation executor.Invocation) (executor.Result, error) { r.last = invocation + r.calls = append(r.calls, invocation) + if r.errors != nil { + if err := r.errors[invocation.Tool]; err != nil { + return executor.Result{}, err + } + } return executor.Result{Invocation: invocation}, nil } @@ -43,8 +53,8 @@ func TestDevdocArticleSearchAcceptsWukongKeywordAlias(t *testing.T) { if err := cmd.Execute(); err != nil { t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut.String()) } - if runner.last.Tool != "search_open_platform_docs" { - t.Fatalf("tool = %q, want search_open_platform_docs", runner.last.Tool) + if runner.last.Tool != devdocArticleSearchTool { + t.Fatalf("tool = %q, want %s", runner.last.Tool, devdocArticleSearchTool) } if got := runner.last.Params["keyword"]; got != "openConversationId" { t.Fatalf("keyword = %#v, want openConversationId", got) @@ -57,6 +67,161 @@ func TestDevdocArticleSearchAcceptsWukongKeywordAlias(t *testing.T) { } } +func TestDevdocArticleSearchPassesCursor(t *testing.T) { + t.Parallel() + + runner := &devdocCommandRunner{} + cmd := devdocHandler{}.Command(runner) + var out, errOut bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&errOut) + cmd.SetArgs([]string{"article", "search", "--query", "Webhook", "--cursor", "3", "--size", "5"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut.String()) + } + if runner.last.Tool != devdocArticleSearchTool { + t.Fatalf("tool = %q, want %s", runner.last.Tool, devdocArticleSearchTool) + } + if got := runner.last.Params["cursor"]; got != "3" { + t.Fatalf("cursor = %#v, want 3", got) + } + if _, ok := runner.last.Params["page"]; ok { + t.Fatalf("page must be omitted when cursor is set: %#v", runner.last.Params) + } +} + +func TestDevdocErrorDiagnoseBuildsErrorSearchQuery(t *testing.T) { + t.Parallel() + + runner := &devdocCommandRunner{} + cmd := devdocHandler{}.Command(runner) + var out, errOut bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&errOut) + cmd.SetArgs([]string{ + "error", "diagnose", + "--error-code", "40014", + "--error-message", "不合法的access_token", + "--api", "获取用户信息", + "--cursor", "2", + "--page", "2", + "--size", "3", + }) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut.String()) + } + if runner.last.Tool != devdocErrorDiagnoseTool { + t.Fatalf("tool = %q, want %s", runner.last.Tool, devdocErrorDiagnoseTool) + } + if got := runner.last.Params["errorCode"]; got != "40014" { + t.Fatalf("errorCode = %#v, want 40014", got) + } + if got := runner.last.Params["query"]; got != "不合法的access_token 获取用户信息" { + t.Fatalf("query = %#v, want merged message and api", got) + } + if _, ok := runner.last.Params["page"]; ok { + t.Fatalf("page must be omitted when cursor is set: %#v", runner.last.Params) + } + if got := runner.last.Params["cursor"]; got != "2" { + t.Fatalf("cursor = %#v, want 2", got) + } + if got := runner.last.Params["size"]; got != 3 { + t.Fatalf("size = %#v, want 3", got) + } +} + +func TestDevdocErrorDiagnoseFallsBackToArticleSearchTools(t *testing.T) { + t.Parallel() + + runner := &devdocCommandRunner{ + errors: map[string]error{ + devdocErrorDiagnoseTool: errors.New("PARAM_ERROR - 未找到指定工具"), + devdocArticleSearchTool: errors.New("unknown tool"), + }, + } + cmd := devdocHandler{}.Command(runner) + var out, errOut bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&errOut) + cmd.SetArgs([]string{ + "error", "diagnose", + "--error-code", "40014", + "--query", "获取用户信息", + "--request-id", "req-1", + "--cursor", "2", + }) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut.String()) + } + if len(runner.calls) != 3 { + t.Fatalf("calls = %d, want 3", len(runner.calls)) + } + if got := runner.calls[0].Tool; got != devdocErrorDiagnoseTool { + t.Fatalf("first tool = %q, want %s", got, devdocErrorDiagnoseTool) + } + if got := runner.calls[1].Tool; got != devdocArticleSearchTool { + t.Fatalf("second tool = %q, want %s", got, devdocArticleSearchTool) + } + if got := runner.calls[1].Params["cursor"]; got != "2" { + t.Fatalf("RAG fallback cursor = %#v, want 2", got) + } + 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) + } + if got := runner.calls[2].Tool; got != devdocArticleSearchLegacyTool { + t.Fatalf("third tool = %q, want %s", got, devdocArticleSearchLegacyTool) + } + if got := runner.last.Params["keyword"]; got != "40014 获取用户信息 req-1" { + t.Fatalf("fallback keyword = %#v, want merged diagnostic keyword", got) + } + if _, ok := runner.last.Params["cursor"]; ok { + t.Fatalf("legacy fallback params must not include cursor: %#v", runner.last.Params) + } + if got := runner.last.Params["page"]; got != 2 { + t.Fatalf("legacy fallback page = %#v, want cursor-derived page 2", got) + } +} + +func TestDevdocArticleSearchFallsBackOnStructuredToolMetadataError(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", "--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[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) + } + if got := runner.calls[1].Params["page"]; got != 3 { + t.Fatalf("legacy fallback page = %#v, want cursor-derived page 3", got) + } +} + func TestDevdocArticleSearchAcceptsPositionalKeyword(t *testing.T) { t.Parallel() diff --git a/skills/mono/references/products/devdoc.md b/skills/mono/references/products/devdoc.md index e4116d9b..b7300418 100644 --- a/skills/mono/references/products/devdoc.md +++ b/skills/mono/references/products/devdoc.md @@ -13,21 +13,45 @@ Example: dws devdoc article search --query "OAuth2 接入" dws devdoc article search --keyword "机器人" --size 10 dws devdoc article search --query "消息卡片" --page 2 --size 5 + dws devdoc article search --query "消息卡片" --cursor "" --size 5 Flags: --query string 搜索关键词 (必填) --keyword string 搜索关键词 (--query 的别名) --page int 分页页码 (从 1 开始,默认 1) + --cursor string 分页游标,翻页传上次返回的 nextCursor;传入后不再使用 --page --size int 分页大小 (默认 10) ``` +### 排查调用错误 +``` +Usage: + dws devdoc error diagnose [flags] +Example: + dws devdoc error diagnose --error-code "40014" --query "获取用户信息 access_token" + dws devdoc error diagnose --request-id "" --query "接口调用失败" + dws devdoc error troubleshoot --error-message "token is illegal" --api "获取用户信息" +Flags: + --query string 原始排查问题 + --error-code string 错误码 + --error-message string 错误描述,会合并进原始问题 + --api string API 名称,会合并进原始问题 + --context string 额外排查上下文 + --request-id string 开放平台 requestId + --page int 分页页码 (从 1 开始,默认 1) + --cursor string 分页游标,翻页传上次返回的 nextCursor;传入后不再使用 --page + --size int 分页大小 (默认 10) +``` + ## 意图判断 用户问开放平台 API / 字段 / 错误码 / SDK / 鉴权 / 回调 / 配额相关的技术细节: - 走 `devdoc article search`,把用户问的关键短语作为位置参数或 `--query` +- 用户已经给出错误码、错误描述或 requestId: +- 先走 `devdoc error diagnose`;若后端返回 `PARAM_ERROR - 未找到指定工具` / `unknown tool`,降级 `devdoc article search`,同时把该结果标记为 `needs_gateway_tool_registration` 关键区分: - devdoc(钉钉**开放平台**开发者文档,面向研发) vs doc(钉钉在线文档,面向普通用户内容) -- devdoc 只做搜索,不做读取;命中条目返回标题、摘要、文档链接,由 Agent 引用链接或进一步浏览 +- devdoc 只做搜索/诊断,不做读取;命中条目返回标题、摘要、文档链接,由 Agent 引用链接或进一步浏览 ## 核心工作流 @@ -41,13 +65,38 @@ dws devdoc article search "MCP" --format json # 命中结果多时翻页 dws devdoc article search --query "消息卡片" --page 2 --size 5 --format json +# 推荐动态翻页:优先使用上一页返回的 nextCursor +dws devdoc article search --query "消息卡片" --cursor "" --size 5 --format json + # 查错误码 / 字段含义 dws devdoc article search --query "errcode 40078" --format json + +# 有明确错误码时优先走错误诊断 +dws devdoc error diagnose --error-code "40014" --query "获取用户信息 access_token" --format json + +# 有 requestId 时尝试链路排查;不是所有接口都会返回 requestId +dws devdoc error diagnose --request-id "" --query "接口调用失败" --format json + +# 错误排查结果继续翻页 +dws devdoc error diagnose --error-code "40014" --query "access_token" --cursor "" --format json ``` +## 上线/注入验收 + +```bash +dws devdoc --help +dws devdoc article search --query "OAuth2 接入" --dry-run --format json +dws devdoc error diagnose --error-code "40014" --query "access_token" --dry-run --format json +dws schema devdoc.search_open_platform_docs_rag --format json || true +dws schema devdoc.search_open_error_code_rag --format json || true +``` + +- `--help` / `--dry-run` 只能证明 CLI 命令面和映射存在,不能证明后端工具已注册 +- 真实调用若返回 `PARAM_ERROR - 未找到指定工具`,不是用户参数问题;记录为网关/工具注册待闭环,并降级到 article search 或本地已挂载 Markdown + ## 注意事项 - 关键词必填;可用位置参数、`--query` 或兼容别名 `--keyword`。建议传用户原话里的关键名词(API 名、错误码、能力名),不要过度改写 -- 返回按相关性排序,默认 `--size 10`;要拿更多结果时先翻页,再考虑换关键词 +- 返回按相关性排序,默认 `--size 10`;响应有 `hasMore=true` 时,用 `nextCursor` 传给 `--cursor` 继续翻页,不要手写猜测下一页 - 命中结果里的链接是钉钉开放平台公开文档,可直接给用户做参考 - 不要把 devdoc 用来查业务数据(那是 aitable / doc / report 的事);devdoc 只查**官方开发者文档** diff --git a/skills/mono/references/products/simple.md b/skills/mono/references/products/simple.md index cdaa9e27..05849460 100644 --- a/skills/mono/references/products/simple.md +++ b/skills/mono/references/products/simple.md @@ -15,9 +15,27 @@ Example: Flags: --query string 搜索关键词 (必填) --page string 页码 (默认 1) + --cursor string 分页游标,翻页传上次返回的 nextCursor;传入后不再使用 --page --size string 每页数量 (默认 10) ``` +### 排查开放平台调用错误 +``` +Usage: + dws devdoc error diagnose [flags] +Example: + dws devdoc error diagnose --error-code "40014" --query "获取用户信息 access_token" --format json + dws devdoc error diagnose --request-id "" --query "接口调用失败" --format json +Flags: + --query string 原始排查问题 + --error-code string 错误码 + --error-message string 错误描述 + --api string API 名称 + --context string 额外排查上下文 + --request-id string 开放平台 requestId + --cursor string 分页游标,翻页传上次返回的 nextCursor;传入后不再使用 --page +``` + --- ## oa — 审批 @@ -99,6 +117,7 @@ Example: ## 意图判断 - 用户说"开发文档/API 文档/接口文档" → `devdoc article search` +- 用户说"调用报错/错误码/requestId" → `devdoc error diagnose`;若返回 `PARAM_ERROR - 未找到指定工具`,降级 `devdoc article search` 并标记后端工具注册待闭环 - 用户说"审批/请假/报销/出差" → `oa approval` - 用户说"同意审批/批准" → `oa approval approve` - 用户说"拒绝审批/驳回" → `oa approval reject` @@ -110,7 +129,8 @@ Example: | 操作 | 从返回中提取 | 用于 | |------|-------------|------| -| `devdoc article search` | 文档链接 | 直接展示给用户 | +| `devdoc article search` | 文档链接、nextCursor | 直接展示给用户;hasMore=true 时用 --cursor 翻页 | +| `devdoc error diagnose` | 诊断材料/文档链接、nextCursor | 排查错误码;hasMore=true 时用 --cursor 翻页;不可用时退回文档搜索 | | `oa approval list-forms` | processCode | detail / records 等 | | `oa approval tasks` | taskId, instanceId | approve / reject | | `oa approval list-pending` | instanceId | detail / approve / reject | diff --git a/skills/multi/dingtalk-devdoc/references/devdoc.md b/skills/multi/dingtalk-devdoc/references/devdoc.md index e4116d9b..b7300418 100644 --- a/skills/multi/dingtalk-devdoc/references/devdoc.md +++ b/skills/multi/dingtalk-devdoc/references/devdoc.md @@ -13,21 +13,45 @@ Example: dws devdoc article search --query "OAuth2 接入" dws devdoc article search --keyword "机器人" --size 10 dws devdoc article search --query "消息卡片" --page 2 --size 5 + dws devdoc article search --query "消息卡片" --cursor "" --size 5 Flags: --query string 搜索关键词 (必填) --keyword string 搜索关键词 (--query 的别名) --page int 分页页码 (从 1 开始,默认 1) + --cursor string 分页游标,翻页传上次返回的 nextCursor;传入后不再使用 --page --size int 分页大小 (默认 10) ``` +### 排查调用错误 +``` +Usage: + dws devdoc error diagnose [flags] +Example: + dws devdoc error diagnose --error-code "40014" --query "获取用户信息 access_token" + dws devdoc error diagnose --request-id "" --query "接口调用失败" + dws devdoc error troubleshoot --error-message "token is illegal" --api "获取用户信息" +Flags: + --query string 原始排查问题 + --error-code string 错误码 + --error-message string 错误描述,会合并进原始问题 + --api string API 名称,会合并进原始问题 + --context string 额外排查上下文 + --request-id string 开放平台 requestId + --page int 分页页码 (从 1 开始,默认 1) + --cursor string 分页游标,翻页传上次返回的 nextCursor;传入后不再使用 --page + --size int 分页大小 (默认 10) +``` + ## 意图判断 用户问开放平台 API / 字段 / 错误码 / SDK / 鉴权 / 回调 / 配额相关的技术细节: - 走 `devdoc article search`,把用户问的关键短语作为位置参数或 `--query` +- 用户已经给出错误码、错误描述或 requestId: +- 先走 `devdoc error diagnose`;若后端返回 `PARAM_ERROR - 未找到指定工具` / `unknown tool`,降级 `devdoc article search`,同时把该结果标记为 `needs_gateway_tool_registration` 关键区分: - devdoc(钉钉**开放平台**开发者文档,面向研发) vs doc(钉钉在线文档,面向普通用户内容) -- devdoc 只做搜索,不做读取;命中条目返回标题、摘要、文档链接,由 Agent 引用链接或进一步浏览 +- devdoc 只做搜索/诊断,不做读取;命中条目返回标题、摘要、文档链接,由 Agent 引用链接或进一步浏览 ## 核心工作流 @@ -41,13 +65,38 @@ dws devdoc article search "MCP" --format json # 命中结果多时翻页 dws devdoc article search --query "消息卡片" --page 2 --size 5 --format json +# 推荐动态翻页:优先使用上一页返回的 nextCursor +dws devdoc article search --query "消息卡片" --cursor "" --size 5 --format json + # 查错误码 / 字段含义 dws devdoc article search --query "errcode 40078" --format json + +# 有明确错误码时优先走错误诊断 +dws devdoc error diagnose --error-code "40014" --query "获取用户信息 access_token" --format json + +# 有 requestId 时尝试链路排查;不是所有接口都会返回 requestId +dws devdoc error diagnose --request-id "" --query "接口调用失败" --format json + +# 错误排查结果继续翻页 +dws devdoc error diagnose --error-code "40014" --query "access_token" --cursor "" --format json ``` +## 上线/注入验收 + +```bash +dws devdoc --help +dws devdoc article search --query "OAuth2 接入" --dry-run --format json +dws devdoc error diagnose --error-code "40014" --query "access_token" --dry-run --format json +dws schema devdoc.search_open_platform_docs_rag --format json || true +dws schema devdoc.search_open_error_code_rag --format json || true +``` + +- `--help` / `--dry-run` 只能证明 CLI 命令面和映射存在,不能证明后端工具已注册 +- 真实调用若返回 `PARAM_ERROR - 未找到指定工具`,不是用户参数问题;记录为网关/工具注册待闭环,并降级到 article search 或本地已挂载 Markdown + ## 注意事项 - 关键词必填;可用位置参数、`--query` 或兼容别名 `--keyword`。建议传用户原话里的关键名词(API 名、错误码、能力名),不要过度改写 -- 返回按相关性排序,默认 `--size 10`;要拿更多结果时先翻页,再考虑换关键词 +- 返回按相关性排序,默认 `--size 10`;响应有 `hasMore=true` 时,用 `nextCursor` 传给 `--cursor` 继续翻页,不要手写猜测下一页 - 命中结果里的链接是钉钉开放平台公开文档,可直接给用户做参考 - 不要把 devdoc 用来查业务数据(那是 aitable / doc / report 的事);devdoc 只查**官方开发者文档** diff --git a/skills/multi/dingtalk-skill/references/skill.md b/skills/multi/dingtalk-skill/references/skill.md index d9a416c3..27222b08 100644 --- a/skills/multi/dingtalk-skill/references/skill.md +++ b/skills/multi/dingtalk-skill/references/skill.md @@ -15,9 +15,27 @@ Example: Flags: --query string 搜索关键词 (必填) --page string 页码 (默认 1) + --cursor string 分页游标,翻页传上次返回的 nextCursor;传入后不再使用 --page --size string 每页数量 (默认 10) ``` +### 排查开放平台调用错误 +``` +Usage: + dws devdoc error diagnose [flags] +Example: + dws devdoc error diagnose --error-code "40014" --query "获取用户信息 access_token" --format json + dws devdoc error diagnose --request-id "" --query "接口调用失败" --format json +Flags: + --query string 原始排查问题 + --error-code string 错误码 + --error-message string 错误描述 + --api string API 名称 + --context string 额外排查上下文 + --request-id string 开放平台 requestId + --cursor string 分页游标,翻页传上次返回的 nextCursor;传入后不再使用 --page +``` + ## live — 直播 ### 查看我的直播列表 @@ -91,7 +109,8 @@ Args: ## 意图判断 -- 用户说"开发文档/API 文档/接口文档/调用报错" → `devdoc article search` +- 用户说"开发文档/API 文档/接口文档" → `devdoc article search` +- 用户说"调用报错/错误码/requestId" → `devdoc error diagnose`;若返回 `PARAM_ERROR - 未找到指定工具`,降级 `devdoc article search` 并标记后端工具注册待闭环 - 用户说"直播/我的直播" → `live stream list` - 用户说"搜索技能/找技能/安装技能/技能市场" → `skill search` / `skill install`(按步骤衔接) @@ -99,7 +118,8 @@ Args: | 操作 | 从返回中提取 | 用于 | |------|-------------|------| -| `devdoc article search` | 文档链接 | 直接展示给用户 | +| `devdoc article search` | 文档链接、nextCursor | 直接展示给用户;hasMore=true 时用 --cursor 翻页 | +| `devdoc error diagnose` | 诊断材料/文档链接、nextCursor | 排查错误码;hasMore=true 时用 --cursor 翻页;不可用时退回文档搜索 | | `skill search` | `skillId`、名称、描述 | 用户选型后传给 `skill install ` | | `skill install` | 安装成功/失败信息 | 确认目标 Agent 目录已注册 | | `skill publish` | 发布结果(成功或错误信息) | 确认企业技能库已更新 | diff --git a/test/cli_compat/devdoc_test.go b/test/cli_compat/devdoc_test.go index 755db3bf..09c689ae 100644 --- a/test/cli_compat/devdoc_test.go +++ b/test/cli_compat/devdoc_test.go @@ -13,7 +13,7 @@ func TestDevdocArticleSearch_should_call_correct_tool(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %v", err) } - assertToolName(t, cap, "search_open_platform_docs") + assertToolName(t, cap, "search_open_platform_docs_rag") } func TestDevdocArticleSearch_should_pass_keyword(t *testing.T) { @@ -25,6 +25,16 @@ func TestDevdocArticleSearch_should_pass_keyword(t *testing.T) { assertToolArg(t, cap, "keyword", "openConversationId") } +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", + }) + assertToolArg(t, cap, "cursor", "3") + assertArgNotPresent(t, cap, "page") +} + func TestDevdocArticleSearch_should_not_call_when_dry_run(t *testing.T) { cap := setupTestDepsWithDryRun(t, "devdoc") root := buildRoot() diff --git a/test/cli_compat/helpers_test.go b/test/cli_compat/helpers_test.go index 00df6928..2523b7a5 100644 --- a/test/cli_compat/helpers_test.go +++ b/test/cli_compat/helpers_test.go @@ -126,13 +126,9 @@ func execCmdWithArgs(t *testing.T, root *cobra.Command, path []string, flags map cap := getCapture(t) - cliArgs := []string{"-f", "json"} + cliArgs := []string{"-f", "json", "--dry-run"} cliArgs = append(cliArgs, path...) - // Add dry-run if capture says so - if cap != nil && cap.dryRun { - cliArgs = append(cliArgs, "--dry-run") - } // Add --yes if auto-confirm if cap != nil && cap.confirm { cliArgs = append(cliArgs, "--yes") @@ -165,11 +161,22 @@ func execCmdWithArgs(t *testing.T, root *cobra.Command, path []string, flags map } `json:"invocation"` } if cap != nil { + recordDryRunPreview := !cap.dryRun if jsonErr := json.Unmarshal(out.Bytes(), &inv); jsonErr == nil && inv.Invocation.Tool != "" { - if !inv.Invocation.DryRun { + if !inv.Invocation.DryRun || recordDryRunPreview { cap.record(inv.Invocation.Tool, inv.Invocation.Params, "") } - // For dry-run: don't record (matches old behavior: assertCallCount == 0) + return nil + } + var directInv struct { + Tool string `json:"tool"` + Params map[string]any `json:"params"` + DryRun bool `json:"dry_run"` + } + if jsonErr := json.Unmarshal(out.Bytes(), &directInv); jsonErr == nil && directInv.Tool != "" { + if recordDryRunPreview { + cap.record(directInv.Tool, directInv.Params, "") + } } } return nil diff --git a/test/skill_tests.md b/test/skill_tests.md index fe57fb9b..0f39ab26 100644 --- a/test/skill_tests.md +++ b/test/skill_tests.md @@ -1168,7 +1168,7 @@ Agent 安装 dws skill 后,仅依据 skill 提供的参考文档,将自然 --- -### devdoc(7 条) +### devdoc(8 条) #### `dws devdoc article search` @@ -1207,6 +1207,11 @@ Agent 安装 dws skill 后,仅依据 skill 提供的参考文档,将自然 - Expected: `dws devdoc article search --keyword "机器人发消息接口" --format json` - Flags: `--keyword` = `机器人发消息接口` +**devdoc_devdoc_article_search_008** +- Prompt: 继续搜索开放平台文档中关于消息卡片的下一页,游标 NEXT_CURSOR +- Expected: `dws devdoc article search --keyword 消息卡片 --cursor NEXT_CURSOR --format json` +- Flags: `--keyword` = `消息卡片`, `--cursor` = `NEXT_CURSOR` + --- ### routing(3 条) diff --git a/test/skill_tests_results.md b/test/skill_tests_results.md index 8079b9c2..9330788e 100644 --- a/test/skill_tests_results.md +++ b/test/skill_tests_results.md @@ -2,8 +2,8 @@ ## Summary -- **Total Test Cases**: 234 -- **Passed**: 234 +- **Total Test Cases**: 235 +- **Passed**: 235 - **Failed**: 0 - **Pass Rate**: 100.0% @@ -17,7 +17,7 @@ | chat | 31 | 31 | 0 | 100.0% | | contact | 14 | 14 | 0 | 100.0% | | devapp | 25 | 25 | 0 | 100.0% | -| devdoc | 7 | 7 | 0 | 100.0% | +| devdoc | 8 | 8 | 0 | 100.0% | | ding | 5 | 5 | 0 | 100.0% | | report | 26 | 26 | 0 | 100.0% | | routing | 3 | 3 | 0 | 100.0% | @@ -1353,6 +1353,14 @@ - Command path: PASS (devdoc article search) - Flags: PASS (1 flags validated) +**devdoc_devdoc_article_search_008** ✅ PASS + +- Prompt: 继续搜索开放平台文档中关于消息卡片的下一页,游标 NEXT_CURSOR +- Expected: `dws devdoc article search --keyword 消息卡片 --cursor NEXT_CURSOR --format json` +- Skill Reference: references/products/devdoc.md +- Command path: PASS (devdoc article search) +- Flags: PASS (2 flags validated) + ### ding **ding_ding_message_recall_001** ✅ PASS