feat(devdoc): support cursor pagination

This commit is contained in:
shangguanxuan.sgx committed 2026-06-15 20:41:53 +08:00
1 parent 0a8de62041
commit 1921e5e37e
13 files changed
+552 -32

No files matched your search

+1 -1
View File
@@ -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 |
+1 -1
View File
@@ -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 |
+3 -3
View File
@@ -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. |
+192 -5
View File
@@ -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 {
+168 -3
View File
@@ -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()
+51 -2
View File
@@ -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 "<NEXT_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 "<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 "<NEXT_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 "<REQUEST_ID>" --query "接口调用失败" --format json
# 错误排查结果继续翻页
dws devdoc error diagnose --error-code "40014" --query "access_token" --cursor "<NEXT_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 只查**官方开发者文档**
+21 -1
View File
@@ -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 "<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 |
@@ -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 "<NEXT_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 "<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 "<NEXT_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 "<REQUEST_ID>" --query "接口调用失败" --format json
# 错误排查结果继续翻页
dws devdoc error diagnose --error-code "40014" --query "access_token" --cursor "<NEXT_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 只查**官方开发者文档**
@@ -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 "<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 <skillId> <target>` |
| `skill install` | 安装成功/失败信息 | 确认目标 Agent 目录已注册 |
| `skill publish` | 发布结果(成功或错误信息) | 确认企业技能库已更新 |
+11 -1
View File
@@ -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()
+14 -7
View File
@@ -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
+6 -1
View File
@@ -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 条)
+11 -3
View File
@@ -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