Merge pull request #26 from wxianfeng/main

add helper overrides for all task subcommands
This commit is contained in:
coffeeBigSir
2026-03-29 22:32:53 +08:00
committed by GitHub
6 changed files with 718 additions and 42 deletions
+1 -1
View File
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="108" height="20" role="img" aria-label="coverage: 50.0%"><title>coverage: 50.0%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="108" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="61" height="20" fill="#555"/><rect x="61" width="47" height="20" fill="#e05d44"/><rect width="108" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="315" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="510">coverage</text><text x="315" y="140" transform="scale(.1)" fill="#fff" textLength="510">coverage</text><text aria-hidden="true" x="835" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="370">50.0%</text><text x="835" y="140" transform="scale(.1)" fill="#fff" textLength="370">50.0%</text></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="108" height="20" role="img" aria-label="coverage: 53.9%"><title>coverage: 53.9%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="108" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="61" height="20" fill="#555"/><rect x="61" width="47" height="20" fill="#e05d44"/><rect width="108" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="315" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="510">coverage</text><text x="315" y="140" transform="scale(.1)" fill="#fff" textLength="510">coverage</text><text aria-hidden="true" x="835" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="370">53.9%</text><text x="835" y="140" transform="scale(.1)" fill="#fff" textLength="370">53.9%</text></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

+3 -1
View File
@@ -24,4 +24,6 @@ test/cli_compat/testdata/
.env.*
*.pem
*.key
credentials*
credentials*
plans
_docs
+336 -4
View File
@@ -14,13 +14,15 @@
package helpers
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cobracmd"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/i18n"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/i18n"
"github.com/spf13/cobra"
)
@@ -41,7 +43,8 @@ func (todoHandler) Name() string {
func (todoHandler) Command(runner executor.Runner) *cobra.Command {
root := &cobra.Command{
Use: "todo",
Short: "Todo helper overrides",
Short: i18n.T("待办任务管理"),
Long: i18n.T("管理钉钉个人待办:创建、查询列表、查看详情、修改、标记完成、删除。"),
Args: cobra.NoArgs,
TraverseChildren: true,
DisableAutoGenTag: true,
@@ -60,11 +63,94 @@ func (todoHandler) Command(runner executor.Runner) *cobra.Command {
return cmd.Help()
},
}
task.AddCommand(newTodoTaskListCommand(runner))
task.AddCommand(
newTodoTaskCreateCommand(runner),
newTodoTaskListCommand(runner),
newTodoTaskUpdateCommand(runner),
newTodoTaskDoneCommand(runner),
newTodoTaskGetCommand(runner),
newTodoTaskDeleteCommand(runner),
)
root.AddCommand(task)
return root
}
// ── create ─────────────────────────────────────────────────
func newTodoTaskCreateCommand(runner executor.Runner) *cobra.Command {
cmd := &cobra.Command{
Use: "create",
Short: i18n.T("创建待办"),
Example: ` dws todo task create --title "修复线上Bug" --executors userId1,userId2 --priority 40
dws todo task create --title "提交报告" --executors userId1 --due "2026-03-10T18:00:00+08:00"
# 查询 userId: dws contact user search --keyword "姓名"`,
Args: cobra.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
title := flagOrFallback(cmd, "title", "subject", "content")
if strings.TrimSpace(title) == "" {
return apperrors.NewValidation("--title is required")
}
executorsStr, _ := cmd.Flags().GetString("executors")
if strings.TrimSpace(executorsStr) == "" {
return apperrors.NewValidation("--executors is required")
}
executorIds := parseExecutorIds(executorsStr)
vo := map[string]any{
"subject": title,
"executorIds": executorIds,
}
if v, _ := cmd.Flags().GetString("due"); v != "" {
ms, err := parseISOTimeToMillis("due", v)
if err != nil {
return err
}
vo["dueTime"] = ms
}
if v, _ := cmd.Flags().GetString("priority"); v != "" {
if n, err := strconv.Atoi(v); err == nil {
vo["priority"] = n
}
}
if v, _ := cmd.Flags().GetString("recurrence"); v != "" {
vo["recurrence"] = v
}
params := map[string]any{"PersonalTodoCreateVO": vo}
invocation := executor.NewHelperInvocation(
cobracmd.LegacyCommandPath(cmd),
"todo",
"create_personal_todo",
params,
)
invocation.DryRun = commandDryRun(cmd)
result, err := runner.Run(cmd.Context(), invocation)
if err != nil {
return err
}
return writeCommandPayload(cmd, result)
},
}
preferLegacyLeaf(cmd)
cmd.Flags().String("title", "", i18n.T("待办标题 (必填)"))
cmd.Flags().String("executors", "", i18n.T("执行者 userId 列表 (必填)"))
cmd.Flags().String("due", "", i18n.T("截止时间 ISO-8601 (如 2026-03-10T18:00:00+08:00)"))
cmd.Flags().String("priority", "", i18n.T("优先级: 10低/20普通/30较高/40紧急"))
cmd.Flags().String("recurrence", "", i18n.T("循环待办 (需先设置 --due); 格式: DTSTART:...\\nRRULE:FREQ=DAILY;INTERVAL=1"))
cmd.Flags().String("subject", "", i18n.T("--title 的别名"))
cmd.Flags().String("content", "", i18n.T("--title 的别名"))
_ = cmd.Flags().MarkHidden("subject")
_ = cmd.Flags().MarkHidden("content")
return cmd
}
// ── list ───────────────────────────────────────────────────
func newTodoTaskListCommand(runner executor.Runner) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
@@ -159,6 +245,252 @@ func newTodoTaskListCommand(runner executor.Runner) *cobra.Command {
return cmd
}
// ── update ─────────────────────────────────────────────────
func newTodoTaskUpdateCommand(runner executor.Runner) *cobra.Command {
cmd := &cobra.Command{
Use: "update",
Short: i18n.T("修改待办任务"),
Example: ` dws todo task update --task-id <taskId> --title "新标题"
dws todo task update --task-id <taskId> --priority 40 --due "2026-03-10T18:00:00+08:00"
dws todo task update --task-id <taskId> --done true
# 查询 taskId: dws todo task list`,
Args: cobra.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
taskID, _ := cmd.Flags().GetString("task-id")
if strings.TrimSpace(taskID) == "" {
return apperrors.NewValidation("--task-id is required")
}
inner := map[string]any{
"taskId": taskID,
}
if v, _ := cmd.Flags().GetString("title"); v != "" {
inner["subject"] = v
}
if v, _ := cmd.Flags().GetString("due"); v != "" {
ms, err := parseISOTimeToMillis("due", v)
if err != nil {
return err
}
inner["dueTime"] = ms
}
if v, _ := cmd.Flags().GetString("priority"); v != "" {
if n, err := strconv.Atoi(v); err == nil {
inner["priority"] = n
}
}
if v, _ := cmd.Flags().GetString("done"); v != "" {
inner["isDone"] = v == "true"
}
params := map[string]any{"TodoUpdateRequest": inner}
invocation := executor.NewHelperInvocation(
cobracmd.LegacyCommandPath(cmd),
"todo",
"update_todo_task",
params,
)
invocation.DryRun = commandDryRun(cmd)
result, err := runner.Run(cmd.Context(), invocation)
if err != nil {
return err
}
return writeCommandPayload(cmd, result)
},
}
preferLegacyLeaf(cmd)
cmd.Flags().String("task-id", "", i18n.T("待办任务 ID (必填)"))
cmd.Flags().String("title", "", i18n.T("新标题"))
cmd.Flags().String("due", "", i18n.T("截止时间 ISO-8601 (如 2026-03-10T18:00:00+08:00)"))
cmd.Flags().String("priority", "", i18n.T("优先级: 10低/20普通/30较高/40紧急"))
cmd.Flags().String("done", "", i18n.T("完成状态: true/false"))
return cmd
}
// ── done ───────────────────────────────────────────────────
func newTodoTaskDoneCommand(runner executor.Runner) *cobra.Command {
cmd := &cobra.Command{
Use: "done",
Short: i18n.T("修改执行者的待办完成状态"),
Example: ` dws todo task done --task-id <taskId> --status true
dws todo task done --task-id <taskId> --status false
# 查询 taskId: dws todo task list`,
Args: cobra.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
taskID, _ := cmd.Flags().GetString("task-id")
if strings.TrimSpace(taskID) == "" {
return apperrors.NewValidation("--task-id is required")
}
status, _ := cmd.Flags().GetString("status")
if strings.TrimSpace(status) == "" {
return apperrors.NewValidation("--status is required")
}
params := map[string]any{
"taskId": taskID,
"isDone": status,
}
invocation := executor.NewHelperInvocation(
cobracmd.LegacyCommandPath(cmd),
"todo",
"update_todo_done_status",
params,
)
invocation.DryRun = commandDryRun(cmd)
result, err := runner.Run(cmd.Context(), invocation)
if err != nil {
return err
}
return writeCommandPayload(cmd, result)
},
}
preferLegacyLeaf(cmd)
cmd.Flags().String("task-id", "", i18n.T("待办任务 ID (必填)"))
cmd.Flags().String("status", "", i18n.T("完成状态: true=已完成, false=未完成 (必填)"))
return cmd
}
// ── get ────────────────────────────────────────────────────
func newTodoTaskGetCommand(runner executor.Runner) *cobra.Command {
cmd := &cobra.Command{
Use: "get",
Short: i18n.T("待办详情"),
Example: ` dws todo task get --task-id <taskId>
# 查询 taskId: dws todo task list`,
Args: cobra.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
taskID, _ := cmd.Flags().GetString("task-id")
if strings.TrimSpace(taskID) == "" {
return apperrors.NewValidation("--task-id is required")
}
params := map[string]any{
"taskId": taskID,
}
invocation := executor.NewHelperInvocation(
cobracmd.LegacyCommandPath(cmd),
"todo",
"query_todo_detail",
params,
)
invocation.DryRun = commandDryRun(cmd)
result, err := runner.Run(cmd.Context(), invocation)
if err != nil {
return err
}
return writeCommandPayload(cmd, result)
},
}
preferLegacyLeaf(cmd)
cmd.Flags().String("task-id", "", i18n.T("待办任务 ID (必填)"))
return cmd
}
// ── delete ─────────────────────────────────────────────────
func newTodoTaskDeleteCommand(runner executor.Runner) *cobra.Command {
cmd := &cobra.Command{
Use: "delete",
Short: i18n.T("删除待办"),
Example: ` dws todo task delete --task-id <taskId>
dws todo task delete --task-id <taskId> --yes
# 查询 taskId: dws todo task list`,
Args: cobra.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
taskID, _ := cmd.Flags().GetString("task-id")
if strings.TrimSpace(taskID) == "" {
return apperrors.NewValidation("--task-id is required")
}
if !confirmDeletePrompt(cmd, i18n.T("待办"), taskID) {
return nil
}
params := map[string]any{
"taskId": taskID,
}
invocation := executor.NewHelperInvocation(
cobracmd.LegacyCommandPath(cmd),
"todo",
"delete_todo",
params,
)
invocation.DryRun = commandDryRun(cmd)
result, err := runner.Run(cmd.Context(), invocation)
if err != nil {
return err
}
return writeCommandPayload(cmd, result)
},
}
preferLegacyLeaf(cmd)
cmd.Flags().String("task-id", "", i18n.T("待办任务 ID (必填)"))
cmd.Flags().Bool("yes", false, i18n.T("跳过确认直接删除"))
return cmd
}
// ── helpers ────────────────────────────────────────────────
// flagOrFallback returns the first non-empty value among the given flag names.
func flagOrFallback(cmd *cobra.Command, names ...string) string {
for _, name := range names {
if v, _ := cmd.Flags().GetString(name); strings.TrimSpace(v) != "" {
return v
}
}
return ""
}
// parseExecutorIds splits "id1,id2" into []string for the MCP executorIds array.
func parseExecutorIds(s string) []string {
s = strings.TrimSpace(s)
if s == "" {
return nil
}
parts := strings.Split(s, ",")
ids := make([]string, 0, len(parts))
for _, p := range parts {
if id := strings.TrimSpace(p); id != "" {
ids = append(ids, id)
}
}
return ids
}
// parseISOTimeToMillis parses an ISO-8601 datetime string and returns Unix
// milliseconds. It supports timezone offsets (e.g. +08:00) and UTC "Z" suffix.
func parseISOTimeToMillis(flagName, value string) (int64, error) {
formats := []string{
time.RFC3339,
"2006-01-02T15:04:05Z07:00",
"2006-01-02T15:04:05",
"2006-01-02 15:04:05",
}
for _, layout := range formats {
if t, err := time.Parse(layout, value); err == nil {
return t.UnixMilli(), nil
}
}
return 0, apperrors.NewValidation(
fmt.Sprintf("--%s format error, use ISO-8601 e.g. 2026-03-10T18:00:00+08:00", flagName),
)
}
// ── list pagination helpers ────────────────────────────────
func normalizePage(raw string) string {
if trimmed := strings.TrimSpace(raw); trimmed != "" {
return trimmed
+52 -33
View File
@@ -9,49 +9,51 @@
"AI诚聘": "AI Recruitment",
"Base ID (必填)": "Base ID (required)",
"MCP 服务返回了无法解析的协议响应;检查服务版本或上游代理。": "MCP service returned an unparseable protocol response; check service version or upstream proxy.",
"OSS 上传失败 HTTP %d: %s": "OSS uploadfailed HTTP %d: %s",
"OSS 上传失败 HTTP %d: %s": "OSS upload failed HTTP %d: %s",
"[%d] 轮询中... (%ds)": "[%d] polling ... (%ds)",
"prepare_attachment_upload 返回格式异常": "prepare_attachment_upload returned abnormal format",
"refresh_token 刷新失败": "refresh_token refreshfailed",
"refresh_token 刷新失败": "refresh_token refresh failed",
"refresh_token 刷新失败,将尝试扫码登录": "refresh_token refresh failed, will attempt QR code login",
"true=已完成, false=未完成": "true=alreadycompleted, false=un-completed",
"true=已完成, false=未完成": "true=completed, false=not completed",
"⏳ 等待授权中...": "⏳ Waiting for authorization...",
"⚠️ 即将删除 %s: %s\\\n": "⚠️ About to delete %s: %s\\\n",
"上传失败: %w": "uploadfailed: %w",
"上传失败: %w": "upload failed: %w",
"上游服务异常;可稍后重试,若持续失败请查看 recovery snapshot。": "Upstream service error; retry later. If persistent, check recovery snapshot.",
"下载失败 (HTTP %d): %s": "downloadfailed (HTTP %d): %s",
"下载失败 (HTTP %d): %s": "download failed (HTTP %d): %s",
"下载失败: %w": "Download failed: %w",
"不是文件: ": "notisfile:",
"不是文件: ": "not a file: ",
"人才查询": "Talent query",
"使用授权码换取 Access Token...": "Exchanging authorization code for Access Token...",
"保存 token 失败": "Failed to save token",
"保存Keyword subscription规则": "saveKeyword subscriptionrule",
"写入文件失败: %w": "writefilefailed: %w",
"准备上传失败: %w": "prepareuploadfailed: %w",
"分页查询审批实例": "paginated queryapproval instance",
"创建 / 查询 / 更新 / 删除待办": "create / query / update / deletetodo",
"创建文件失败: %w": "createfilefailed: %w",
"保存Keyword subscription规则": "Save keyword subscription rule",
"写入文件失败: %w": "write file failed: %w",
"准备上传失败: %w": "prepare upload failed: %w",
"分页查询审批实例": "Paginated query approval instances",
"创建 / 查询 / 更新 / 删除待办": "Create / query / update / delete todo",
"创建待办": "Create todo",
"创建文件失败: %w": "create file failed: %w",
"创建请求失败": "Failed to create request",
"删除 AI 表格": "delete AI table",
"删除原因(可选)": "deletereason(optional)",
"删除字段": "deletefield",
"删除待办": "Delete todo",
"删除 AI 表格": "Delete AI table",
"删除原因(可选)": "Delete reason (optional)",
"删除字段": "Delete field",
"删除指定 Base(高风险、不可逆)。使用 --yes 跳过确认。": "Delete specified Base (high risk, irreversible). Use --yes to skip confirmation.",
"删除指定字段(高风险、不可逆)。使用 --yes 跳过确认。": "Delete specified field (high risk, irreversible). Use --yes to skip confirmation.",
"删除指定数据表(高风险、不可逆)。使用 --yes 跳过确认。": "Delete specified data table (high risk, irreversible). Use --yes to skip confirmation.",
"删除数据表": "deletedata table",
"删除数据表": "Delete data table",
"删除行记录": "Delete row records",
"参数不符合工具输入 schema;请检查 --json/--params/flags。": "Parameters do not match tool input schema; check --json/--params/flags.",
"发送请求失败": "Failed to send request",
"回调中未收到授权码": "callback un-receivetoauthorization code",
"回调中未收到授权码": "Callback did not receive authorization code",
"如果浏览器未自动打开,请手动访问:\n %s\n\n": "If the browser did not open automatically, please visit:\n %s\n\n",
"字段": "field",
"字段 ID (必填)": "field ID (required)",
"字段管理": "fieldmanagement",
"审批实例管理": "approval instancemanagement",
"字段管理": "Field management",
"审批实例管理": "Approval instance management",
"岗位查询": "Position query",
"工具协议不兼容;请检查服务版本、工具名或刷新发现缓存。": "Tool protocol incompatible; check service version, tool name, or refresh discovery cache.",
"工具调用失败;请检查参数和上游服务状态。": "Tool invocation failed; check parameters and upstream service status.",
"已取消操作": "alreadycanceloperations",
"已取消操作": "Operation cancelled",
"当前平台 %s 没有可用的预编译二进制": "No pre-built binary available for platform %s",
"或者直接打开以下链接:": "Or open the following link:",
"所有凭证已失效,请运行 dws auth login 重新登录": "All credentials have expired, please run dws auth login to re-authenticate",
@@ -65,9 +67,9 @@
"授权超时(5分钟),请重试": "Authorization timeout (5 minutes), please retry",
"数据表": "data table",
"数据表 ID (必填)": "data table ID (required)",
"数据表管理": "data tablemanagement",
"文件不存在: ": "filenot found: ",
"文件为空": "fileis empty",
"数据表管理": "Data table management",
"文件不存在: ": "File not found: ",
"文件为空": "File is empty",
"文件过大 (%d 字节,限制 %d 字节)": "File too large (%d bytes, limit %d bytes)",
"无法打开文件: %w": "Cannot open file: %w",
"无法自动打开浏览器": "Cannot automatically open browser",
@@ -82,7 +84,7 @@
"服务返回了空结果;请稍后重试,必要时查看 recovery snapshot。": "Service returned empty results; retry later. Check recovery snapshot if persistent.",
"未找到 MCP Server URL": "MCP Server URL not found",
"未找到认证信息,请运行 dws auth login": "No credentials found, please run dws auth login",
"未登录,请运行 dws auth login": "not logged in,please run dws auth login",
"未登录,请运行 dws auth login": "Not logged in, please run dws auth login",
"未知错误: %s": "Unknown error: %s",
"本地文件一键上传到 AITable 附件字段": "One-click upload local file to AITable attachment field",
"本地文件路径 (必填)": "Local file path (required)",
@@ -90,36 +92,51 @@
"构建上传请求失败: %w": "Failed to build upload request: %w",
"查询入职人才": "Query onboarding candidates",
"查询在招岗位": "Query open positions",
"修改待办任务": "Update todo task",
"修改执行者的待办完成状态": "Update executor todo done status",
"优先级: 10低/20普通/30较高/40紧急": "Priority: 10=low/20=normal/30=high/40=urgent",
"查询待办列表": "List todos",
"待办": "todo",
"待办任务 ID (必填)": "Todo task ID (required)",
"待办任务管理": "Todo task management",
"待办标题 (必填)": "Todo title (required)",
"待办详情": "Todo detail",
"循环待办 (需先设置 --due); 格式: DTSTART:...\\nRRULE:FREQ=DAILY;INTERVAL=1": "Recurring todo (requires --due); format: DTSTART:...\\nRRULE:FREQ=DAILY;INTERVAL=1",
"截止时间 ISO-8601 (如 2026-03-10T18:00:00+08:00)": "Due time ISO-8601 (e.g. 2026-03-10T18:00:00+08:00)",
"执行者 userId 列表 (必填)": "Executor userId list (required)",
"检查登录状态后重试": "Check login status and retry",
"检查服务连通性后重试;如持续失败,请确认 MCP 服务响应正常。": "Check service connectivity and retry; if persistent, verify MCP service is responding.",
"检查服务连通性和协议版本后重试": "Check service connectivity and protocol version, then retry",
"检查认证、权限和参数后重试原命令": "Check authentication, permissions, and parameters, then retry",
"新标题": "New title",
"管理钉钉个人待办:创建、查询列表、查看详情、修改、标记完成、删除。": "Manage DingTalk personal todos: create, list, view detail, update, mark done, delete.",
"步骤 1/3: 准备上传 %s (%d 字节, %s)...\\\n": "Step 1/3: Preparing upload %s (%d bytes, %s)...\\\n",
"步骤 2/3: 上传文件到 OSS...": "Step 2/3: Uploading file to OSS...",
"步骤 3/3: 上传完成!": "Step 3/3: Upload complete!",
"用户拒绝了授权请求": "User rejected the authorization request",
"确认删除? (yes/no): ": "confirmdelete? (yes/no): ",
"完成状态: true/false": "Done status: true/false",
"完成状态: true=已完成, false=未完成 (必填)": "Done status: true=completed, false=not completed (required)",
"确认删除? (yes/no): ": "Confirm delete? (yes/no): ",
"等待用户授权...": "Waiting for user authorization...",
"结果格式与客户端预期不一致;请检查服务协议变更或回退到最近可用版本。": "Result format does not match client expectations; check service protocol changes or rollback to latest working version.",
"网络错误,继续重试...": "Network error, retrying...",
"获取使用指引": "Get usage guide",
"获取审批实例详情": "getapproval instancedetails",
"获取审批实例详情": "Get approval instance details",
"获取数量,超过 20 自动分页 (默认 20)": "Fetch count, auto-paginate if over 20 (default 20)",
"获取版本信息失败 (HTTP %d)": "getversioninfofailed (HTTP %d)",
"获取版本信息失败 (HTTP %d)": "Get version info failed (HTTP %d)",
"解析令牌数据失败": "Failed to parse token data",
"解析响应失败": "Failed to parse response",
"解析版本信息失败: %w": "parseversioninfofailed: %w",
"解析版本信息失败: %w": "Parse version info failed: %w",
"解析设备授权数据失败": "Failed to parse device authorization data",
"认证信息已失效,请重新执行上一条命令(最多重试两次)": "Credentials expired, re-execute the last command (max 2 retries)",
"认证失败;请检查登录状态或产品 URL 覆盖。": "Authentication failed; check login status or product URL override.",
"记录": "record",
"记录 ID 列表,逗号分隔 (必填)": "record ID list,comma-separated (required)",
"记录管理": "recordmanagement",
"记录 ID 列表,逗号分隔 (必填)": "Record ID list, comma-separated (required)",
"记录管理": "Record management",
"设备授权流程失败(已重试 %d 次)": "Device authorization flow failed (retried %d times)",
"设备授权码已过期": "Device authorization code has expired",
"设备授权码已过期(%d 秒),请重试": "Device authorization code expired (%d seconds), please retry",
"设置权限失败: %w": "settingspermissionfailed: %w",
"设置权限失败: %w": "Set permission failed: %w",
"请在浏览器中完成扫码授权。": "Please complete QR code authorization in the browser.",
"请在浏览器中打开以下链接,并输入授权码:": "Please open the following link in your browser and enter the authorization code:",
"请检查服务 endpoint 是否为空或格式不合法。": "Please check if the service endpoint is empty or has invalid format.",
@@ -130,11 +147,13 @@
"请求设备授权码失败": "Failed to request device authorization code",
"读取 zip 条目失败: %w": "Failed to read zip entry: %w",
"读取响应失败": "Failed to read response",
"读取版本信息失败: %w": "readversioninfofailed: %w",
"读取版本信息失败: %w": "Read version info failed: %w",
"跳过确认直接删除": "Skip confirmation and delete directly",
"--title 的别名": "Alias for --title",
"调用被拒绝;请检查认证状态、租户身份或访问权限。": "Call rejected; check authentication status, tenant identity, or access permissions.",
"轮询过快,间隔增加至 %ds": "Polling too fast, interval increased to %ds",
"返回数据缺少 uploadUrl 或 fileToken": "Response data missing uploadUrl or fileToken",
"附件工作流": "attachmentworkflow",
"附件工作流": "Attachment workflow",
"页码 (必填)": "page number (required)",
"🔐 登录钉钉": "🔐 Login to DingTalk"
}
+19
View File
@@ -30,8 +30,10 @@
"准备上传失败: %w": "准备上传失败: %w",
"分页查询审批实例": "分页查询审批实例",
"创建 / 查询 / 更新 / 删除待办": "创建 / 查询 / 更新 / 删除待办",
"创建待办": "创建待办",
"创建文件失败: %w": "创建文件失败: %w",
"创建请求失败": "创建请求失败",
"删除待办": "删除待办",
"删除 AI 表格": "删除 AI 表格",
"删除原因(可选)": "删除原因(可选)",
"删除字段": "删除字段",
@@ -90,15 +92,30 @@
"构建上传请求失败: %w": "构建上传请求失败: %w",
"查询入职人才": "查询入职人才",
"查询在招岗位": "查询在招岗位",
"修改待办任务": "修改待办任务",
"修改执行者的待办完成状态": "修改执行者的待办完成状态",
"优先级: 10低/20普通/30较高/40紧急": "优先级: 10低/20普通/30较高/40紧急",
"查询待办列表": "查询待办列表",
"待办": "待办",
"待办任务 ID (必填)": "待办任务 ID (必填)",
"待办任务管理": "待办任务管理",
"待办标题 (必填)": "待办标题 (必填)",
"待办详情": "待办详情",
"循环待办 (需先设置 --due); 格式: DTSTART:...\\nRRULE:FREQ=DAILY;INTERVAL=1": "循环待办 (需先设置 --due); 格式: DTSTART:...\\nRRULE:FREQ=DAILY;INTERVAL=1",
"截止时间 ISO-8601 (如 2026-03-10T18:00:00+08:00)": "截止时间 ISO-8601 (如 2026-03-10T18:00:00+08:00)",
"执行者 userId 列表 (必填)": "执行者 userId 列表 (必填)",
"检查登录状态后重试": "检查登录状态后重试",
"检查服务连通性后重试;如持续失败,请确认 MCP 服务响应正常。": "检查服务连通性后重试;如持续失败,请确认 MCP 服务响应正常。",
"检查服务连通性和协议版本后重试": "检查服务连通性和协议版本后重试",
"检查认证、权限和参数后重试原命令": "检查认证、权限和参数后重试原命令",
"新标题": "新标题",
"管理钉钉个人待办:创建、查询列表、查看详情、修改、标记完成、删除。": "管理钉钉个人待办:创建、查询列表、查看详情、修改、标记完成、删除。",
"步骤 1/3: 准备上传 %s (%d 字节, %s)...\\\n": "步骤 1/3: 准备上传 %s (%d 字节, %s)...\\\n",
"步骤 2/3: 上传文件到 OSS...": "步骤 2/3: 上传文件到 OSS...",
"步骤 3/3: 上传完成!": "步骤 3/3: 上传完成!",
"用户拒绝了授权请求": "用户拒绝了授权请求",
"完成状态: true/false": "完成状态: true/false",
"完成状态: true=已完成, false=未完成 (必填)": "完成状态: true=已完成, false=未完成 (必填)",
"确认删除? (yes/no): ": "确认删除? (yes/no): ",
"等待用户授权...": "等待用户授权...",
"结果格式与客户端预期不一致;请检查服务协议变更或回退到最近可用版本。": "结果格式与客户端预期不一致;请检查服务协议变更或回退到最近可用版本。",
@@ -131,6 +148,8 @@
"读取 zip 条目失败: %w": "读取 zip 条目失败: %w",
"读取响应失败": "读取响应失败",
"读取版本信息失败: %w": "读取版本信息失败: %w",
"跳过确认直接删除": "跳过确认直接删除",
"--title 的别名": "--title 的别名",
"调用被拒绝;请检查认证状态、租户身份或访问权限。": "调用被拒绝;请检查认证状态、租户身份或访问权限。",
"轮询过快,间隔增加至 %ds": "轮询过快,间隔增加至 %ds",
"返回数据缺少 uploadUrl 或 fileToken": "返回数据缺少 uploadUrl 或 fileToken",
+307 -3
View File
@@ -6,12 +6,316 @@ import (
// ── task create ─────────────────────────────────────────────
func TestTodoTaskCreate_should_call_create_todo_task(t *testing.T) {
func TestTodoTaskCreate_should_call_create_personal_todo(t *testing.T) {
cap := setupTestDeps(t, "todo")
root := buildRoot()
err := execCmd(t, root, []string{"todo", "task", "create"}, nil)
err := execCmd(t, root, []string{"todo", "task", "create"}, map[string]string{
"title": "修复线上Bug",
"executors": "userId1,userId2",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
assertToolName(t, cap, "create_todo_task")
assertToolName(t, cap, "create_personal_todo")
}
func TestTodoTaskCreate_should_pass_subject_and_executorIds(t *testing.T) {
cap := setupTestDeps(t, "todo")
root := buildRoot()
_ = execCmd(t, root, []string{"todo", "task", "create"}, map[string]string{
"title": "提交报告",
"executors": "userId1",
})
assertToolArg(t, cap, "PersonalTodoCreateVO", map[string]any{
"subject": "提交报告",
"executorIds": []any{"userId1"},
})
}
func TestTodoTaskCreate_should_pass_priority(t *testing.T) {
cap := setupTestDeps(t, "todo")
root := buildRoot()
_ = execCmd(t, root, []string{"todo", "task", "create"}, map[string]string{
"title": "紧急任务",
"executors": "userId1",
"priority": "40",
})
last := cap.last()
if last == nil {
t.Fatal("no MCP call captured")
}
vo, ok := last.Args["PersonalTodoCreateVO"].(map[string]any)
if !ok {
t.Fatalf("expected PersonalTodoCreateVO map, got %T", last.Args["PersonalTodoCreateVO"])
}
if vo["priority"] != float64(40) {
t.Errorf("expected priority=40, got %v", vo["priority"])
}
}
func TestTodoTaskCreate_should_error_when_title_missing(t *testing.T) {
_ = setupTestDeps(t, "todo")
root := buildRoot()
err := execCmd(t, root, []string{"todo", "task", "create"}, map[string]string{
"executors": "userId1",
})
if err == nil {
t.Fatal("expected error when --title is missing")
}
}
func TestTodoTaskCreate_should_error_when_executors_missing(t *testing.T) {
_ = setupTestDeps(t, "todo")
root := buildRoot()
err := execCmd(t, root, []string{"todo", "task", "create"}, map[string]string{
"title": "测试待办",
})
if err == nil {
t.Fatal("expected error when --executors is missing")
}
}
func TestTodoTaskCreate_should_accept_subject_alias(t *testing.T) {
cap := setupTestDeps(t, "todo")
root := buildRoot()
err := execCmd(t, root, []string{"todo", "task", "create"}, map[string]string{
"subject": "通过别名创建",
"executors": "userId1",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
assertToolName(t, cap, "create_personal_todo")
}
func TestTodoTaskCreate_should_not_call_mcp_in_dry_run(t *testing.T) {
cap := setupTestDepsWithDryRun(t, "todo")
root := buildRoot()
_ = execCmd(t, root, []string{"todo", "task", "create"}, map[string]string{
"title": "Dry Run",
"executors": "userId1",
})
assertCallCount(t, cap, 0)
}
// ── task list ───────────────────────────────────────────────
func TestTodoTaskList_should_call_get_user_todos(t *testing.T) {
cap := setupTestDeps(t, "todo")
root := buildRoot()
err := execCmd(t, root, []string{"todo", "task", "list"}, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
assertToolName(t, cap, "get_user_todos_in_current_org")
}
func TestTodoTaskList_should_pass_status_filter(t *testing.T) {
cap := setupTestDeps(t, "todo")
root := buildRoot()
_ = execCmd(t, root, []string{"todo", "task", "list"}, map[string]string{
"status": "false",
})
assertToolArg(t, cap, "isDone", "false")
assertToolArg(t, cap, "todoStatus", "false")
}
// ── task update ─────────────────────────────────────────────
func TestTodoTaskUpdate_should_call_update_todo_task(t *testing.T) {
cap := setupTestDeps(t, "todo")
root := buildRoot()
err := execCmd(t, root, []string{"todo", "task", "update"}, map[string]string{
"task-id": "TASK_001",
"title": "新标题",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
assertToolName(t, cap, "update_todo_task")
}
func TestTodoTaskUpdate_should_pass_TodoUpdateRequest(t *testing.T) {
cap := setupTestDeps(t, "todo")
root := buildRoot()
_ = execCmd(t, root, []string{"todo", "task", "update"}, map[string]string{
"task-id": "TASK_001",
"title": "更新标题",
})
last := cap.last()
if last == nil {
t.Fatal("no MCP call captured")
}
req, ok := last.Args["TodoUpdateRequest"].(map[string]any)
if !ok {
t.Fatalf("expected TodoUpdateRequest map, got %T", last.Args["TodoUpdateRequest"])
}
if req["taskId"] != "TASK_001" {
t.Errorf("expected taskId=TASK_001, got %v", req["taskId"])
}
if req["subject"] != "更新标题" {
t.Errorf("expected subject=更新标题, got %v", req["subject"])
}
}
func TestTodoTaskUpdate_should_pass_done_flag(t *testing.T) {
cap := setupTestDeps(t, "todo")
root := buildRoot()
_ = execCmd(t, root, []string{"todo", "task", "update"}, map[string]string{
"task-id": "TASK_001",
"done": "true",
})
last := cap.last()
if last == nil {
t.Fatal("no MCP call captured")
}
req, ok := last.Args["TodoUpdateRequest"].(map[string]any)
if !ok {
t.Fatalf("expected TodoUpdateRequest map, got %T", last.Args["TodoUpdateRequest"])
}
if req["isDone"] != true {
t.Errorf("expected isDone=true, got %v", req["isDone"])
}
}
func TestTodoTaskUpdate_should_error_when_task_id_missing(t *testing.T) {
_ = setupTestDeps(t, "todo")
root := buildRoot()
err := execCmd(t, root, []string{"todo", "task", "update"}, map[string]string{
"title": "新标题",
})
if err == nil {
t.Fatal("expected error when --task-id is missing")
}
}
func TestTodoTaskUpdate_should_not_call_mcp_in_dry_run(t *testing.T) {
cap := setupTestDepsWithDryRun(t, "todo")
root := buildRoot()
_ = execCmd(t, root, []string{"todo", "task", "update"}, map[string]string{
"task-id": "TASK_001",
"title": "Dry Run",
})
assertCallCount(t, cap, 0)
}
// ── task done ───────────────────────────────────────────────
func TestTodoTaskDone_should_call_update_todo_done_status(t *testing.T) {
cap := setupTestDeps(t, "todo")
root := buildRoot()
err := execCmd(t, root, []string{"todo", "task", "done"}, map[string]string{
"task-id": "TASK_001",
"status": "true",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
assertToolName(t, cap, "update_todo_done_status")
}
func TestTodoTaskDone_should_pass_taskId_and_isDone(t *testing.T) {
cap := setupTestDeps(t, "todo")
root := buildRoot()
_ = execCmd(t, root, []string{"todo", "task", "done"}, map[string]string{
"task-id": "TASK_001",
"status": "true",
})
assertToolArg(t, cap, "taskId", "TASK_001")
assertToolArg(t, cap, "isDone", "true")
}
func TestTodoTaskDone_should_error_when_task_id_missing(t *testing.T) {
_ = setupTestDeps(t, "todo")
root := buildRoot()
err := execCmd(t, root, []string{"todo", "task", "done"}, map[string]string{
"status": "true",
})
if err == nil {
t.Fatal("expected error when --task-id is missing")
}
}
func TestTodoTaskDone_should_error_when_status_missing(t *testing.T) {
_ = setupTestDeps(t, "todo")
root := buildRoot()
err := execCmd(t, root, []string{"todo", "task", "done"}, map[string]string{
"task-id": "TASK_001",
})
if err == nil {
t.Fatal("expected error when --status is missing")
}
}
// ── task get ────────────────────────────────────────────────
func TestTodoTaskGet_should_call_query_todo_detail(t *testing.T) {
cap := setupTestDeps(t, "todo")
root := buildRoot()
err := execCmd(t, root, []string{"todo", "task", "get"}, map[string]string{
"task-id": "TASK_001",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
assertToolName(t, cap, "query_todo_detail")
}
func TestTodoTaskGet_should_pass_taskId(t *testing.T) {
cap := setupTestDeps(t, "todo")
root := buildRoot()
_ = execCmd(t, root, []string{"todo", "task", "get"}, map[string]string{
"task-id": "TASK_002",
})
assertToolArg(t, cap, "taskId", "TASK_002")
}
func TestTodoTaskGet_should_error_when_task_id_missing(t *testing.T) {
_ = setupTestDeps(t, "todo")
root := buildRoot()
err := execCmd(t, root, []string{"todo", "task", "get"}, nil)
if err == nil {
t.Fatal("expected error when --task-id is missing")
}
}
// ── task delete ─────────────────────────────────────────────
func TestTodoTaskDelete_should_call_delete_todo(t *testing.T) {
cap := setupTestDepsAutoConfirm(t, "todo")
root := buildRoot()
err := execCmd(t, root, []string{"todo", "task", "delete"}, map[string]string{
"task-id": "TASK_DEL",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
assertToolName(t, cap, "delete_todo")
}
func TestTodoTaskDelete_should_pass_taskId(t *testing.T) {
cap := setupTestDepsAutoConfirm(t, "todo")
root := buildRoot()
_ = execCmd(t, root, []string{"todo", "task", "delete"}, map[string]string{
"task-id": "TASK_DEL",
})
assertToolArg(t, cap, "taskId", "TASK_DEL")
}
func TestTodoTaskDelete_should_error_when_task_id_missing(t *testing.T) {
_ = setupTestDeps(t, "todo")
root := buildRoot()
err := execCmd(t, root, []string{"todo", "task", "delete"}, nil)
if err == nil {
t.Fatal("expected error when --task-id is missing")
}
}
func TestTodoTaskDelete_should_not_call_mcp_in_dry_run(t *testing.T) {
cap := setupTestDepsWithDryRun(t, "todo")
root := buildRoot()
_ = execCmd(t, root, []string{"todo", "task", "delete"}, map[string]string{
"task-id": "TASK_001",
})
assertCallCount(t, cap, 0)
}