diff --git a/.github/badges/coverage.svg b/.github/badges/coverage.svg index c3ca976b..f0d5afcb 100644 --- a/.github/badges/coverage.svg +++ b/.github/badges/coverage.svg @@ -1 +1 @@ -coverage: 57.5%coverage57.5% \ No newline at end of file +coverage: 57.9%coverage57.9% \ No newline at end of file diff --git a/internal/helpers/aitable.go b/internal/helpers/aitable.go index 06b417c5..bc6d06a5 100644 --- a/internal/helpers/aitable.go +++ b/internal/helpers/aitable.go @@ -130,6 +130,11 @@ func (aitableHandler) Command(runner executor.Runner) *cobra.Command { newAitableRecordUpdateCommand(runner), newAitableRecordBatchUpdateCommand(runner), newAitableRecordDeleteCommand(runner), + newAitableRecordHistoryListCommand(runner), + newAitableRecordShareURLCommand(runner), + newAitableRecordUpsertCommand(runner), + newAitableRecordPrimaryDocGetCommand(runner), + newAitableRecordPrimaryDocCreateCommand(runner), newAitableRecordListAlias(runner), ) @@ -264,15 +269,36 @@ func (aitableHandler) Command(runner executor.Runner) *cobra.Command { return cmd.Help() }, } + viewGet := newAitableViewGetCommand(runner) + viewGet.AddCommand( + newAitableViewGetLockCommand(runner), + newAitableViewGetFrozenColsCommand(runner), + newAitableViewGetRowHeightCommand(runner), + newAitableViewGetFillColorRuleCommand(runner), + ) + viewUpdate := newAitableViewUpdateCommand(runner) + viewUpdate.AddCommand( + newAitableViewUpdateFrozenColsCommand(runner), + newAitableViewUpdateRowHeightCommand(runner), + newAitableViewUpdateFillColorRuleCommand(runner), + ) view.AddCommand( - newAitableViewGetCommand(runner), + viewGet, newAitableViewListCommand(runner), newAitableViewCreateCommand(runner), - newAitableViewUpdateCommand(runner), + viewUpdate, newAitableViewDeleteCommand(runner), + newAitableViewLockCommand(runner), + newAitableViewDuplicateCommand(runner), ) - root.AddCommand(base, table, field, record, newAitableFormCommand(runner), template, attachment, export, importCmd, dashboard, chart, view) + root.AddCommand( + base, table, field, record, newAitableFormCommand(runner), + newAitableWorkflowCommand(runner), + template, attachment, export, importCmd, dashboard, chart, view, + newAitableAdvpermCommand(runner), + newAitableSectionCommand(runner), + ) // 顶层别名:dws aitable search/list/create/info → base search/list/create/get // 每个 alias 复用现有 constructor,独立 cobra.Command 实例(避免与 base.* 共享 flag 指针) diff --git a/internal/helpers/aitable_extra.go b/internal/helpers/aitable_extra.go new file mode 100644 index 00000000..b1e14fe7 --- /dev/null +++ b/internal/helpers/aitable_extra.go @@ -0,0 +1,1047 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helpers + +import ( + "fmt" + + 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" +) + +func runAitableHelperTool(cmd *cobra.Command, runner executor.Runner, tool string, params map[string]any) error { + return runAitableProductTool(cmd, runner, "aitable-helper", tool, params) +} + +func newAitableRecordHistoryListCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "history-list", + Short: i18n.T("查询行记录变更历史"), + Example: " dws aitable record history-list --base-id BASE_ID --table-id TABLE_ID --record-id RECORD_ID --limit 50 --offset 0", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, tableID, err := requiredAitableBaseTable(cmd) + if err != nil { + return err + } + recordID, err := aitableRequiredFlag(cmd, "record-id") + if err != nil { + return err + } + params := map[string]any{ + "baseId": baseID, + "tableId": tableID, + "recordId": recordID, + } + if cmd.Flags().Changed("offset") { + offset, _ := cmd.Flags().GetInt("offset") + if offset < 0 { + return apperrors.NewValidation(fmt.Sprintf("--offset must be >= 0, got %d", offset)) + } + params["offset"] = offset + } + if cmd.Flags().Changed("limit") { + limit, _ := cmd.Flags().GetInt("limit") + if limit < 1 || limit > 50 { + return apperrors.NewValidation(fmt.Sprintf("--limit must be in [1, 50], got %d", limit)) + } + params["limit"] = limit + } + return runAitableHelperTool(cmd, runner, "query_record_history", params) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseTableFlags(cmd) + cmd.Flags().String("record-id", "", i18n.T("Record ID (必填)")) + cmd.Flags().Int("offset", 0, i18n.T("分页偏移量,>= 0")) + cmd.Flags().Int("limit", 0, i18n.T("分页大小 [1, 50],不传使用服务端默认值")) + return cmd +} + +func newAitableRecordShareURLCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "share-url", + Short: i18n.T("批量获取记录分享链接"), + Example: " dws aitable record share-url --base-id BASE_ID --table-id TABLE_ID --record-ids rec1,rec2 --view-id VIEW_ID", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, tableID, err := requiredAitableBaseTable(cmd) + if err != nil { + return err + } + recordIDsRaw, err := aitableRequiredFlag(cmd, "record-ids") + if err != nil { + return err + } + recordIDs := parseAitableCSVValues(recordIDsRaw) + if len(recordIDs) == 0 { + return apperrors.NewValidation("--record-ids must contain at least one record ID") + } + if len(recordIDs) > 20 { + return apperrors.NewValidation(fmt.Sprintf("--record-ids exceeds limit: got %d, max 20", len(recordIDs))) + } + params := map[string]any{ + "baseId": baseID, + "tableId": tableID, + "recordIds": recordIDs, + } + if viewID := aitableStringFlag(cmd, "view-id"); viewID != "" { + params["viewId"] = viewID + } + return runAitableHelperTool(cmd, runner, "get_record_share_url", params) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseTableFlags(cmd) + cmd.Flags().String("record-ids", "", i18n.T("Record ID 列表,逗号分隔,单次最多 20 条 (必填)")) + cmd.Flags().String("view-id", "", i18n.T("View ID,可选")) + return cmd +} + +func newAitableRecordUpsertCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "upsert", + Short: i18n.T("批量创建或更新记录"), + Example: " dws aitable record upsert --base-id BASE_ID --table-id TABLE_ID --records '[{\"recordId\":\"rec1\",\"cells\":{\"fld\":\"x\"}},{\"cells\":{\"fld\":\"new\"}}]'", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, tableID, err := requiredAitableBaseTable(cmd) + if err != nil { + return err + } + records, err := resolveAitableRecordsInput(cmd, "records", "fields") + if err != nil { + return err + } + if len(records) == 0 { + return apperrors.NewValidation("--records must contain at least one record") + } + if len(records) > 100 { + return apperrors.NewValidation(fmt.Sprintf("--records exceeds limit: got %d, max 100", len(records))) + } + return runAitableHelperTool(cmd, runner, "record_upsert", map[string]any{ + "baseId": baseID, + "tableId": tableID, + "records": records, + }) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseTableFlags(cmd) + cmd.Flags().String("records", "", i18n.T("记录 JSON 数组 (必填,可改用 --records-file)")) + cmd.Flags().String("records-file", "", i18n.T("从文件读取 records JSON")) + addAitableHiddenStringFlag(cmd, "fields", "--records 的兼容别名") + return cmd +} + +func newAitableRecordPrimaryDocGetCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "primary-doc-get", + Short: i18n.T("查询记录的主键文档"), + Example: " dws aitable record primary-doc-get --base-id BASE_ID --table-id TABLE_ID --record-id RECORD_ID", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, tableID, err := requiredAitableBaseTable(cmd) + if err != nil { + return err + } + recordID, err := aitableRequiredFlag(cmd, "record-id") + if err != nil { + return err + } + return runAitableHelperTool(cmd, runner, "get_primary_doc", map[string]any{ + "baseId": baseID, + "tableId": tableID, + "recordId": recordID, + }) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseTableFlags(cmd) + cmd.Flags().String("record-id", "", i18n.T("Record ID (必填)")) + return cmd +} + +func newAitableRecordPrimaryDocCreateCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "primary-doc-create", + Short: i18n.T("为记录创建主键文档"), + Example: " dws aitable record primary-doc-create --base-id BASE_ID --table-id TABLE_ID --field-id FIELD_ID --record-id RECORD_ID", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, tableID, err := requiredAitableBaseTable(cmd) + if err != nil { + return err + } + fieldID, err := aitableRequiredFlag(cmd, "field-id") + if err != nil { + return err + } + recordID, err := aitableRequiredFlag(cmd, "record-id") + if err != nil { + return err + } + return runAitableHelperTool(cmd, runner, "create_primary_doc", map[string]any{ + "baseId": baseID, + "tableId": tableID, + "fieldId": fieldID, + "recordId": recordID, + }) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseTableFlags(cmd) + cmd.Flags().String("field-id", "", i18n.T("PrimaryDoc 字段 ID (必填)")) + cmd.Flags().String("record-id", "", i18n.T("Record ID (必填)")) + return cmd +} + +func newAitableViewGetLockCommand(runner executor.Runner) *cobra.Command { + return newAitableViewHelperGetCommand(runner, "lock", i18n.T("获取视图锁定状态"), "get_view_lock_status") +} + +func newAitableViewGetFrozenColsCommand(runner executor.Runner) *cobra.Command { + return newAitableViewHelperGetCommand(runner, "frozen-cols", i18n.T("获取视图冻结列数"), "get_frozen_columns_of_view") +} + +func newAitableViewGetRowHeightCommand(runner executor.Runner) *cobra.Command { + return newAitableViewHelperGetCommand(runner, "row-height", i18n.T("获取视图行高"), "get_cell_height_of_view") +} + +func newAitableViewGetFillColorRuleCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "fill-color-rule", + Short: i18n.T("获取视图数据高亮规则"), + Example: " dws aitable view get fill-color-rule --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, tableID, viewID, err := requiredAitableBaseTableView(cmd) + if err != nil { + return err + } + return runAitableTool(cmd, runner, "get_views", map[string]any{ + "baseId": baseID, + "tableId": tableID, + "viewIds": []string{viewID}, + }) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseTableViewFlags(cmd) + return cmd +} + +func newAitableViewHelperGetCommand(runner executor.Runner, use, short, tool string) *cobra.Command { + cmd := &cobra.Command{ + Use: use, + Short: short, + Example: fmt.Sprintf(" dws aitable view get %s --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID", use), + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + params, err := requiredAitableViewParams(cmd) + if err != nil { + return err + } + return runAitableHelperTool(cmd, runner, tool, params) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseTableViewFlags(cmd) + return cmd +} + +func newAitableViewUpdateFrozenColsCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "frozen-cols", + Short: i18n.T("更新视图冻结列数"), + Example: " dws aitable view update frozen-cols --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID --count 1", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + if !cmd.Flags().Changed("count") { + return apperrors.NewValidation("--count is required") + } + count, _ := cmd.Flags().GetInt("count") + if count < 0 { + return apperrors.NewValidation(fmt.Sprintf("--count must be >= 0, got %d", count)) + } + params, err := requiredAitableViewParams(cmd) + if err != nil { + return err + } + params["count"] = count + return runAitableHelperTool(cmd, runner, "set_frozen_columns_of_view", params) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseTableViewFlags(cmd) + cmd.Flags().Int("count", 0, i18n.T("冻结列数,>= 0;0 表示取消冻结 (必填)")) + return cmd +} + +func newAitableViewUpdateRowHeightCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "row-height", + Short: i18n.T("更新视图行高"), + Example: " dws aitable view update row-height --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID --cell-height 56", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + if !cmd.Flags().Changed("cell-height") { + return apperrors.NewValidation("--cell-height is required") + } + cellHeight, _ := cmd.Flags().GetInt("cell-height") + if cellHeight <= 0 { + return apperrors.NewValidation(fmt.Sprintf("--cell-height must be > 0, got %d", cellHeight)) + } + params, err := requiredAitableViewParams(cmd) + if err != nil { + return err + } + params["cellHeight"] = cellHeight + return runAitableHelperTool(cmd, runner, "set_cell_height_of_view", params) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseTableViewFlags(cmd) + cmd.Flags().Int("cell-height", 0, i18n.T("单元格高度,像素值 (必填)")) + return cmd +} + +func newAitableViewUpdateFillColorRuleCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "fill-color-rule", + Short: i18n.T("更新视图数据高亮规则"), + Example: " dws aitable view update fill-color-rule --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID --json '[]'", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + raw, err := aitableRequiredFlag(cmd, "json") + if err != nil { + return err + } + conditionalFormats, err := parseAitableJSONArray(raw, "json") + if err != nil { + return err + } + params, err := requiredAitableViewParams(cmd) + if err != nil { + return err + } + params["conditionalFormats"] = conditionalFormats + return runAitableTool(cmd, runner, "set_view_fill_color_rule", params) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseTableViewFlags(cmd) + cmd.Flags().String("json", "", i18n.T("conditionalFormats JSON 数组 (必填)")) + return cmd +} + +func newAitableViewLockCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "lock", + Short: i18n.T("锁定或解锁视图"), + Example: " dws aitable view lock --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID --off", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + params, err := requiredAitableViewParams(cmd) + if err != nil { + return err + } + action := "lock" + if off, _ := cmd.Flags().GetBool("off"); off { + action = "unlock" + } + params["action"] = action + return runAitableHelperTool(cmd, runner, "lock_or_unlock_view", params) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseTableViewFlags(cmd) + cmd.Flags().Bool("off", false, i18n.T("解锁视图;不传则锁定")) + return cmd +} + +func newAitableViewDuplicateCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "duplicate", + Short: i18n.T("复制视图"), + Example: " dws aitable view duplicate --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID --new-name 副本视图", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, tableID, viewID, err := requiredAitableBaseTableView(cmd) + if err != nil { + return err + } + params := map[string]any{ + "baseId": baseID, + "tableId": tableID, + "sourceViewId": viewID, + } + if newName := aitableStringFlag(cmd, "new-name"); newName != "" { + params["newViewName"] = newName + } + return runAitableHelperTool(cmd, runner, "duplicate_view", params) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseTableViewFlags(cmd) + cmd.Flags().String("new-name", "", i18n.T("新视图名称")) + return cmd +} + +func requiredAitableViewParams(cmd *cobra.Command) (map[string]any, error) { + baseID, tableID, viewID, err := requiredAitableBaseTableView(cmd) + if err != nil { + return nil, err + } + return map[string]any{ + "baseId": baseID, + "tableId": tableID, + "viewId": viewID, + }, nil +} + +func newAitableWorkflowCommand(runner executor.Runner) *cobra.Command { + group := newAitableExtraGroup("workflow", i18n.T("自动化工作流管理")) + group.AddCommand( + newAitableWorkflowEnableCommand(runner), + newAitableWorkflowDisableCommand(runner), + newAitableWorkflowGetCommand(runner), + newAitableWorkflowListCommand(runner), + ) + return group +} + +func newAitableWorkflowEnableCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "enable", + Short: i18n.T("启用指定工作流"), + Example: " dws aitable workflow enable --base-id BASE_ID --workflow-id WORKFLOW_ID", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, err := aitableRequiredFlagOrFallback(cmd, "base-id", "base") + if err != nil { + return err + } + workflowID, err := aitableRequiredFlag(cmd, "workflow-id") + if err != nil { + return err + } + return runAitableHelperTool(cmd, runner, "enable_workflow", map[string]any{ + "baseId": baseID, + "workflowId": workflowID, + }) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseFlag(cmd) + cmd.Flags().String("workflow-id", "", i18n.T("工作流 ID (必填)")) + return cmd +} + +func newAitableWorkflowDisableCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "disable", + Short: i18n.T("禁用指定工作流"), + Example: " dws aitable workflow disable --base-id BASE_ID --workflow-id WORKFLOW_ID --yes", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, err := aitableRequiredFlagOrFallback(cmd, "base-id", "base") + if err != nil { + return err + } + workflowID, err := aitableRequiredFlag(cmd, "workflow-id") + if err != nil { + return err + } + if !confirmDeletePrompt(cmd, i18n.T("工作流"), workflowID) { + return nil + } + return runAitableHelperTool(cmd, runner, "disable_workflow", map[string]any{ + "baseId": baseID, + "workflowId": workflowID, + }) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseFlag(cmd) + cmd.Flags().String("workflow-id", "", i18n.T("工作流 ID (必填)")) + return cmd +} + +func newAitableWorkflowGetCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "get", + Short: i18n.T("获取单个工作流详情"), + Example: " dws aitable workflow get --base-id BASE_ID --workflow-id WORKFLOW_ID", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, err := aitableRequiredFlagOrFallback(cmd, "base-id", "base") + if err != nil { + return err + } + workflowID, err := aitableRequiredFlag(cmd, "workflow-id") + if err != nil { + return err + } + return runAitableHelperTool(cmd, runner, "get_workflow", map[string]any{ + "baseId": baseID, + "workflowId": workflowID, + }) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseFlag(cmd) + cmd.Flags().String("workflow-id", "", i18n.T("工作流 ID (必填)")) + return cmd +} + +func newAitableWorkflowListCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: i18n.T("列出 Base 下的工作流"), + Example: " dws aitable workflow list --base-id BASE_ID --limit 50 --offset 100", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, err := aitableRequiredFlagOrFallback(cmd, "base-id", "base") + if err != nil { + return err + } + params := map[string]any{"baseId": baseID} + if cmd.Flags().Changed("limit") { + limit, _ := cmd.Flags().GetInt("limit") + if limit < 1 || limit > 100 { + return apperrors.NewValidation(fmt.Sprintf("--limit must be in [1, 100], got %d", limit)) + } + params["limit"] = limit + } + if cmd.Flags().Changed("offset") { + offset, _ := cmd.Flags().GetInt("offset") + if offset < 0 { + return apperrors.NewValidation(fmt.Sprintf("--offset must be >= 0, got %d", offset)) + } + params["offset"] = offset + } + return runAitableHelperTool(cmd, runner, "list_workflows", params) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseFlag(cmd) + cmd.Flags().Int("limit", 0, i18n.T("分页大小 [1, 100],不传使用服务端默认值")) + cmd.Flags().Int("offset", 0, i18n.T("分页偏移量,>= 0")) + return cmd +} + +func newAitableAdvpermCommand(runner executor.Runner) *cobra.Command { + group := newAitableExtraGroup("advperm", i18n.T("高级权限管理")) + group.AddCommand( + newAitableAdvpermEnableCommand(runner), + newAitableAdvpermDisableCommand(runner), + newAitableAdvpermRoleListCommand(runner), + newAitableAdvpermRoleGetCommand(runner), + newAitableAdvpermRoleCreateCommand(runner), + newAitableAdvpermRoleUpdateCommand(runner), + newAitableAdvpermRoleDeleteCommand(runner), + ) + return group +} + +func newAitableAdvpermEnableCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "enable", + Short: i18n.T("开启高级权限总开关"), + Example: " dws aitable advperm enable --base-id BASE_ID", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, err := aitableRequiredFlagOrFallback(cmd, "base-id", "base") + if err != nil { + return err + } + return runAitableHelperTool(cmd, runner, "set_advanced_permission", map[string]any{ + "baseId": baseID, + "enabled": true, + }) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseFlag(cmd) + return cmd +} + +func newAitableAdvpermDisableCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "disable", + Short: i18n.T("关闭高级权限总开关"), + Example: " dws aitable advperm disable --base-id BASE_ID --yes", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, err := aitableRequiredFlagOrFallback(cmd, "base-id", "base") + if err != nil { + return err + } + if !confirmDeletePrompt(cmd, i18n.T("高级权限"), baseID) { + return nil + } + return runAitableHelperTool(cmd, runner, "set_advanced_permission", map[string]any{ + "baseId": baseID, + "enabled": false, + }) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseFlag(cmd) + return cmd +} + +func newAitableAdvpermRoleListCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "role-list", + Short: i18n.T("列出 Base 下所有角色"), + Example: " dws aitable advperm role-list --base-id BASE_ID", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, err := aitableRequiredFlagOrFallback(cmd, "base-id", "base") + if err != nil { + return err + } + return runAitableHelperTool(cmd, runner, "list_roles", map[string]any{"baseId": baseID}) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseFlag(cmd) + return cmd +} + +func newAitableAdvpermRoleGetCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "role-get", + Short: i18n.T("获取单个角色完整配置"), + Example: " dws aitable advperm role-get --base-id BASE_ID --role-id ROLE_ID", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, roleID, err := requiredAitableRoleParams(cmd) + if err != nil { + return err + } + return runAitableHelperTool(cmd, runner, "get_role", map[string]any{ + "baseId": baseID, + "roleId": roleID, + }) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseFlag(cmd) + cmd.Flags().String("role-id", "", i18n.T("角色 ID (必填)")) + return cmd +} + +func newAitableAdvpermRoleCreateCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "role-create", + Short: i18n.T("创建自定义角色"), + Example: " dws aitable advperm role-create --base-id BASE_ID --name 市场可读 --sub-roles '[{\"targetId\":\"tbl\",\"targetType\":\"sheet\",\"authLevel\":\"read\"}]'", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, err := aitableRequiredFlagOrFallback(cmd, "base-id", "base") + if err != nil { + return err + } + name, err := aitableRequiredFlag(cmd, "name") + if err != nil { + return err + } + params := map[string]any{ + "baseId": baseID, + "name": name, + } + if err := appendAitableRoleOptionalFlags(cmd, params); err != nil { + return err + } + return runAitableHelperTool(cmd, runner, "create_role", params) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseFlag(cmd) + addAitableRoleMutationFlags(cmd, true) + return cmd +} + +func newAitableAdvpermRoleUpdateCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "role-update", + Short: i18n.T("增量更新自定义角色配置"), + Example: " dws aitable advperm role-update --base-id BASE_ID --role-id ROLE_ID --name 新名字", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, roleID, err := requiredAitableRoleParams(cmd) + if err != nil { + return err + } + params := map[string]any{ + "baseId": baseID, + "roleId": roleID, + } + if name := aitableStringFlag(cmd, "name"); name != "" { + params["name"] = name + } + if err := appendAitableRoleOptionalFlags(cmd, params); err != nil { + return err + } + return runAitableHelperTool(cmd, runner, "patch_role", params) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseFlag(cmd) + cmd.Flags().String("role-id", "", i18n.T("角色 ID (必填)")) + addAitableRoleMutationFlags(cmd, false) + return cmd +} + +func newAitableAdvpermRoleDeleteCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "role-delete", + Short: i18n.T("删除自定义角色"), + Example: " dws aitable advperm role-delete --base-id BASE_ID --role-id ROLE_ID --yes", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, roleID, err := requiredAitableRoleParams(cmd) + if err != nil { + return err + } + if !confirmDeletePrompt(cmd, i18n.T("角色"), roleID) { + return nil + } + return runAitableHelperTool(cmd, runner, "delete_role", map[string]any{ + "baseId": baseID, + "roleId": roleID, + }) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseFlag(cmd) + cmd.Flags().String("role-id", "", i18n.T("角色 ID (必填)")) + return cmd +} + +func requiredAitableRoleParams(cmd *cobra.Command) (string, string, error) { + baseID, err := aitableRequiredFlagOrFallback(cmd, "base-id", "base") + if err != nil { + return "", "", err + } + roleID, err := aitableRequiredFlag(cmd, "role-id") + if err != nil { + return "", "", err + } + return baseID, roleID, nil +} + +func addAitableRoleMutationFlags(cmd *cobra.Command, nameRequired bool) { + label := i18n.T("角色名称") + if nameRequired { + label = i18n.T("角色名称 (必填)") + } + cmd.Flags().String("name", "", label) + cmd.Flags().String("role-type", "", i18n.T("角色类型")) + cmd.Flags().String("flow-type", "", i18n.T("流程类型")) + cmd.Flags().String("sub-roles", "", i18n.T("子角色配置 JSON 数组")) +} + +func appendAitableRoleOptionalFlags(cmd *cobra.Command, params map[string]any) error { + if roleType := aitableStringFlag(cmd, "role-type"); roleType != "" { + params["roleType"] = roleType + } + if flowType := aitableStringFlag(cmd, "flow-type"); flowType != "" { + params["flowType"] = flowType + } + if subRolesRaw := aitableStringFlag(cmd, "sub-roles"); subRolesRaw != "" { + subRoles, err := parseAitableJSONArray(subRolesRaw, "sub-roles") + if err != nil { + return err + } + params["subRoles"] = subRoles + } + return nil +} + +func newAitableSectionCommand(runner executor.Runner) *cobra.Command { + group := newAitableExtraGroup("section", i18n.T("文件夹与节点管理")) + group.AddCommand( + newAitableSectionCreateCommand(runner), + newAitableSectionRenameCommand(runner), + newAitableSectionDeleteCommand(runner), + newAitableSectionReorderCommand(runner), + newAitableSectionListEmptyCommand(runner), + newAitableSectionListNodesCommand(runner), + newAitableSectionMoveNodeCommand(runner), + ) + return group +} + +func newAitableSectionCreateCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: i18n.T("创建文件夹"), + Example: " dws aitable section create --base-id BASE_ID --name 我的文件夹 --parent-section-id SECTION_ID --index 0", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, err := aitableRequiredFlagOrFallback(cmd, "base-id", "base") + if err != nil { + return err + } + name, err := aitableRequiredFlag(cmd, "name") + if err != nil { + return err + } + params := map[string]any{ + "baseId": baseID, + "name": name, + } + if cmd.Flags().Changed("parent-section-id") { + parentSectionID, _ := cmd.Flags().GetString("parent-section-id") + params["parentSectionId"] = parentSectionID + } + if index, _ := cmd.Flags().GetInt("index"); index >= 0 { + params["index"] = index + } + return runAitableHelperTool(cmd, runner, "create_section", params) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseFlag(cmd) + cmd.Flags().String("name", "", i18n.T("文件夹名称 (必填)")) + cmd.Flags().String("parent-section-id", "", i18n.T("父文件夹 ID;空字符串表示根目录")) + cmd.Flags().Int("index", -1, i18n.T("目标位置,0-based;不传则追加")) + return cmd +} + +func newAitableSectionRenameCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "rename", + Short: i18n.T("重命名文件夹"), + Example: " dws aitable section rename --base-id BASE_ID --section-id SECTION_ID --new-name 新名称", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, sectionID, err := requiredAitableSectionParams(cmd) + if err != nil { + return err + } + newName, err := aitableRequiredFlag(cmd, "new-name") + if err != nil { + return err + } + return runAitableHelperTool(cmd, runner, "rename_section", map[string]any{ + "baseId": baseID, + "sectionId": sectionID, + "newName": newName, + }) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseFlag(cmd) + cmd.Flags().String("section-id", "", i18n.T("文件夹 ID (必填)")) + cmd.Flags().String("new-name", "", i18n.T("新文件夹名称 (必填)")) + return cmd +} + +func newAitableSectionDeleteCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "delete", + Short: i18n.T("删除文件夹"), + Example: " dws aitable section delete --base-id BASE_ID --section-id SECTION_ID", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, sectionID, err := requiredAitableSectionParams(cmd) + if err != nil { + return err + } + return runAitableHelperTool(cmd, runner, "delete_section", map[string]any{ + "baseId": baseID, + "sectionId": sectionID, + }) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseFlag(cmd) + cmd.Flags().String("section-id", "", i18n.T("文件夹 ID (必填)")) + return cmd +} + +func newAitableSectionReorderCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "reorder", + Short: i18n.T("调整文件夹顺序"), + Example: " dws aitable section reorder --base-id BASE_ID --section-id SECTION_ID --target-index 0", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, sectionID, err := requiredAitableSectionParams(cmd) + if err != nil { + return err + } + if !cmd.Flags().Changed("target-index") { + return apperrors.NewValidation("--target-index is required") + } + targetIndex, _ := cmd.Flags().GetInt("target-index") + if targetIndex < 0 { + return apperrors.NewValidation(fmt.Sprintf("--target-index must be >= 0, got %d", targetIndex)) + } + return runAitableHelperTool(cmd, runner, "reorder_section", map[string]any{ + "baseId": baseID, + "sectionId": sectionID, + "targetIndex": targetIndex, + }) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseFlag(cmd) + cmd.Flags().String("section-id", "", i18n.T("文件夹 ID (必填)")) + cmd.Flags().Int("target-index", -1, i18n.T("目标位置,0-based (必填)")) + return cmd +} + +func newAitableSectionListEmptyCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "list-empty", + Short: i18n.T("列出空文件夹"), + Example: " dws aitable section list-empty --base-id BASE_ID", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, err := aitableRequiredFlagOrFallback(cmd, "base-id", "base") + if err != nil { + return err + } + return runAitableHelperTool(cmd, runner, "list_empty_sections", map[string]any{"baseId": baseID}) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseFlag(cmd) + return cmd +} + +func newAitableSectionListNodesCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "list-nodes", + Short: i18n.T("列出全部节点"), + Example: " dws aitable section list-nodes --base-id BASE_ID", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, err := aitableRequiredFlagOrFallback(cmd, "base-id", "base") + if err != nil { + return err + } + return runAitableHelperTool(cmd, runner, "list_nsheet_nodes", map[string]any{"baseId": baseID}) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseFlag(cmd) + return cmd +} + +func newAitableSectionMoveNodeCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "move-node", + Short: i18n.T("移动节点"), + Example: " dws aitable section move-node --base-id BASE_ID --node-id NODE_ID --new-parent-section-id SECTION_ID --target-index 0", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + baseID, err := aitableRequiredFlagOrFallback(cmd, "base-id", "base") + if err != nil { + return err + } + nodeID, err := aitableRequiredFlag(cmd, "node-id") + if err != nil { + return err + } + if !cmd.Flags().Changed("new-parent-section-id") { + return apperrors.NewValidation("--new-parent-section-id is required; pass empty string to move to base root") + } + newParentSectionID, _ := cmd.Flags().GetString("new-parent-section-id") + params := map[string]any{ + "baseId": baseID, + "nodeId": nodeID, + "newParentSectionId": newParentSectionID, + } + if targetIndex, _ := cmd.Flags().GetInt("target-index"); targetIndex >= 0 { + params["targetIndex"] = targetIndex + } + return runAitableHelperTool(cmd, runner, "move_nsheet_node", params) + }, + } + preferLegacyLeaf(cmd) + addAitableBaseFlag(cmd) + cmd.Flags().String("node-id", "", i18n.T("要移动的节点 ID (必填)")) + cmd.Flags().String("new-parent-section-id", "", i18n.T("目标父文件夹 ID;空字符串表示根目录 (必填)")) + cmd.Flags().Int("target-index", -1, i18n.T("目标位置,0-based")) + return cmd +} + +func requiredAitableSectionParams(cmd *cobra.Command) (string, string, error) { + baseID, err := aitableRequiredFlagOrFallback(cmd, "base-id", "base") + if err != nil { + return "", "", err + } + sectionID, err := aitableRequiredFlag(cmd, "section-id") + if err != nil { + return "", "", err + } + return baseID, sectionID, nil +} + +func addAitableBaseFlag(cmd *cobra.Command) { + cmd.Flags().String("base-id", "", i18n.T("Base ID (必填)")) + addAitableHiddenStringFlag(cmd, "base", "--base-id 的兼容别名") +} + +func newAitableExtraGroup(use, short string) *cobra.Command { + return &cobra.Command{ + Use: use, + Short: short, + Args: cobra.NoArgs, + TraverseChildren: true, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + return cmd.Help() + }, + } +} diff --git a/internal/helpers/aitable_extra_test.go b/internal/helpers/aitable_extra_test.go new file mode 100644 index 00000000..f26b1f70 --- /dev/null +++ b/internal/helpers/aitable_extra_test.go @@ -0,0 +1,196 @@ +package helpers + +import ( + "bytes" + "testing" + + "github.com/spf13/cobra" +) + +func executeAitableExtraCommand(t *testing.T, cmd *cobra.Command, args ...string) { + t.Helper() + + var out, errOut bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&errOut) + cmd.SetArgs(args) + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut.String()) + } +} + +func TestAitableRecordHistoryListRoutesToHelper(t *testing.T) { + t.Parallel() + + runner := &aitableCommandRunner{} + cmd := newAitableRecordHistoryListCommand(runner) + executeAitableExtraCommand(t, cmd, + "--base-id", "BASE_001", + "--table-id", "TABLE_001", + "--record-id", "REC_001", + "--offset", "10", + "--limit", "30", + ) + + if got := runner.last.CanonicalProduct; got != "aitable-helper" { + t.Fatalf("CanonicalProduct = %q, want aitable-helper", got) + } + if got := runner.last.Tool; got != "query_record_history" { + t.Fatalf("Tool = %q, want query_record_history", got) + } + if got := runner.last.Params["recordId"]; got != "REC_001" { + t.Fatalf("recordId = %#v, want REC_001", got) + } + if got := runner.last.Params["offset"]; got != 10 { + t.Fatalf("offset = %#v, want 10", got) + } + if got := runner.last.Params["limit"]; got != 30 { + t.Fatalf("limit = %#v, want 30", got) + } +} + +func TestAitableRecordUpsertAcceptsFieldsAlias(t *testing.T) { + t.Parallel() + + runner := &aitableCommandRunner{} + cmd := newAitableRecordUpsertCommand(runner) + executeAitableExtraCommand(t, cmd, + "--base-id", "BASE_001", + "--table-id", "TABLE_001", + "--fields", `[{"recordId":"REC_001","cells":{"fld":"updated"}},{"cells":{"fld":"new"}}]`, + ) + + if got := runner.last.CanonicalProduct; got != "aitable-helper" { + t.Fatalf("CanonicalProduct = %q, want aitable-helper", got) + } + if got := runner.last.Tool; got != "record_upsert" { + t.Fatalf("Tool = %q, want record_upsert", got) + } + records, ok := runner.last.Params["records"].([]any) + if !ok { + t.Fatalf("records type = %T, want []any", runner.last.Params["records"]) + } + if len(records) != 2 { + t.Fatalf("records len = %d, want 2", len(records)) + } +} + +func TestAitableViewExtraCommandsRouteToExpectedTools(t *testing.T) { + t.Parallel() + + t.Run("lock unlock", func(t *testing.T) { + t.Parallel() + runner := &aitableCommandRunner{} + cmd := newAitableViewLockCommand(runner) + executeAitableExtraCommand(t, cmd, + "--base-id", "BASE_001", + "--table-id", "TABLE_001", + "--view-id", "VIEW_001", + "--off", + ) + if got := runner.last.CanonicalProduct; got != "aitable-helper" { + t.Fatalf("CanonicalProduct = %q, want aitable-helper", got) + } + if got := runner.last.Tool; got != "lock_or_unlock_view" { + t.Fatalf("Tool = %q, want lock_or_unlock_view", got) + } + if got := runner.last.Params["action"]; got != "unlock" { + t.Fatalf("action = %#v, want unlock", got) + } + }) + + t.Run("fill color rule", func(t *testing.T) { + t.Parallel() + runner := &aitableCommandRunner{} + cmd := newAitableViewUpdateFillColorRuleCommand(runner) + executeAitableExtraCommand(t, cmd, + "--base-id", "BASE_001", + "--table-id", "TABLE_001", + "--view-id", "VIEW_001", + "--json", `[]`, + ) + if got := runner.last.CanonicalProduct; got != "aitable" { + t.Fatalf("CanonicalProduct = %q, want aitable", got) + } + if got := runner.last.Tool; got != "set_view_fill_color_rule" { + t.Fatalf("Tool = %q, want set_view_fill_color_rule", got) + } + if formats, ok := runner.last.Params["conditionalFormats"].([]any); !ok || len(formats) != 0 { + t.Fatalf("conditionalFormats = %#v, want empty []any", runner.last.Params["conditionalFormats"]) + } + }) +} + +func TestAitableWorkflowListRoutesToHelper(t *testing.T) { + t.Parallel() + + runner := &aitableCommandRunner{} + cmd := newAitableWorkflowListCommand(runner) + executeAitableExtraCommand(t, cmd, + "--base-id", "BASE_001", + "--limit", "50", + "--offset", "100", + ) + + if got := runner.last.CanonicalProduct; got != "aitable-helper" { + t.Fatalf("CanonicalProduct = %q, want aitable-helper", got) + } + if got := runner.last.Tool; got != "list_workflows" { + t.Fatalf("Tool = %q, want list_workflows", got) + } + if got := runner.last.Params["limit"]; got != 50 { + t.Fatalf("limit = %#v, want 50", got) + } + if got := runner.last.Params["offset"]; got != 100 { + t.Fatalf("offset = %#v, want 100", got) + } +} + +func TestAitableAdvpermRoleCreateParsesSubRoles(t *testing.T) { + t.Parallel() + + runner := &aitableCommandRunner{} + cmd := newAitableAdvpermRoleCreateCommand(runner) + executeAitableExtraCommand(t, cmd, + "--base-id", "BASE_001", + "--name", "市场可读", + "--sub-roles", `[{"targetId":"TABLE_001","targetType":"sheet","authLevel":"read"}]`, + ) + + if got := runner.last.CanonicalProduct; got != "aitable-helper" { + t.Fatalf("CanonicalProduct = %q, want aitable-helper", got) + } + if got := runner.last.Tool; got != "create_role" { + t.Fatalf("Tool = %q, want create_role", got) + } + subRoles, ok := runner.last.Params["subRoles"].([]any) + if !ok || len(subRoles) != 1 { + t.Fatalf("subRoles = %#v, want single-item []any", runner.last.Params["subRoles"]) + } +} + +func TestAitableSectionMoveNodeAllowsRootParent(t *testing.T) { + t.Parallel() + + runner := &aitableCommandRunner{} + cmd := newAitableSectionMoveNodeCommand(runner) + executeAitableExtraCommand(t, cmd, + "--base-id", "BASE_001", + "--node-id", "NODE_001", + "--new-parent-section-id", "", + "--target-index", "0", + ) + + if got := runner.last.CanonicalProduct; got != "aitable-helper" { + t.Fatalf("CanonicalProduct = %q, want aitable-helper", got) + } + if got := runner.last.Tool; got != "move_nsheet_node" { + t.Fatalf("Tool = %q, want move_nsheet_node", got) + } + if got := runner.last.Params["newParentSectionId"]; got != "" { + t.Fatalf("newParentSectionId = %#v, want empty string", got) + } + if got := runner.last.Params["targetIndex"]; got != 0 { + t.Fatalf("targetIndex = %#v, want 0", got) + } +} diff --git a/internal/helpers/doc.go b/internal/helpers/doc.go index 2f4794ce..9a36a489 100644 --- a/internal/helpers/doc.go +++ b/internal/helpers/doc.go @@ -85,6 +85,7 @@ func (docHandler) Command(runner executor.Runner) *cobra.Command { newDocPermissionAddCommand(runner), newDocPermissionUpdateCommand(runner), newDocPermissionListCommand(runner), + newDocPermissionRemoveCommand(runner), ) export := &cobra.Command{ @@ -174,6 +175,7 @@ func newDocSearchCommand(runner executor.Runner) *cobra.Command { Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { + docDeprecatedNotice(cmd, "drive search or dws wiki node search") params := map[string]any{} if keyword := docFlagOrFallback(cmd, "query", "keyword"); keyword != "" { params["keyword"] = keyword @@ -222,6 +224,7 @@ func newDocListCommand(runner executor.Runner) *cobra.Command { Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { + docDeprecatedNotice(cmd, "drive list or dws wiki node list") params := map[string]any{} if folder := docFlagOrFallback(cmd, "folder", "parent-id", "node", "file-id", "nodee"); folder != "" { params["folderId"] = normalizeDocNodeID(folder) @@ -274,6 +277,9 @@ func newDocReadCommand(runner executor.Runner) *cobra.Command { if output := docStringFlag(cmd, "output"); output != "" { params["__output__"] = output } + if format == "jsonml" { + return runDocReadJSONML(cmd, runner, params) + } return runDocTool(cmd, runner, "doc", "get_document_content", params) }, } @@ -814,6 +820,7 @@ func newDocFileCreateCommand(runner executor.Runner) *cobra.Command { Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { + docDeprecatedNotice(cmd, "wiki node create") name, err := docRequiredFlagOrFallback(cmd, "name", "title") if err != nil { return err @@ -850,6 +857,7 @@ func newDocFolderCreateCommand(runner executor.Runner) *cobra.Command { Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { + docDeprecatedNotice(cmd, "wiki node create --type folder") name, err := docRequiredFlagOrFallback(cmd, "name", "title") if err != nil { return err @@ -889,6 +897,7 @@ func newDocTransferCommand(runner executor.Runner, use, tool string) *cobra.Comm Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { + docDeprecatedNotice(cmd, "drive "+use) nodeID, err := docRequiredNode(cmd) if err != nil { return err @@ -919,6 +928,7 @@ func newDocRenameCommand(runner executor.Runner) *cobra.Command { Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { + docDeprecatedNotice(cmd, "drive rename") nodeID, err := docRequiredNode(cmd) if err != nil { return err @@ -1467,6 +1477,56 @@ func docInvocationResult(cmd *cobra.Command, runner executor.Runner, product, to return runner.Run(cmd.Context(), invocation) } +func runDocReadJSONML(cmd *cobra.Command, runner executor.Runner, params map[string]any) error { + outputPath, _ := params["__output__"].(string) + result, err := docInvocationResult(cmd, runner, "doc", "get_document_content", params) + if err != nil { + return err + } + if !result.Invocation.Implemented { + return writeCommandPayload(cmd, result) + } + payload := normalizeDocReadJSONMLResult(result) + if outputPath != "" { + data, err := json.MarshalIndent(payload, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSONML output: %w", err) + } + if err := os.WriteFile(outputPath, data, 0644); err != nil { + return fmt.Errorf("failed to write output file %s: %w", outputPath, err) + } + fmt.Fprintf(cmd.OutOrStdout(), "[INFO] JSONML 已写入 %s\n", outputPath) + return nil + } + return writeCommandPayload(cmd, payload) +} + +func normalizeDocReadJSONMLResult(result executor.Result) map[string]any { + content := result.Response + if nested, ok := result.Response["content"].(map[string]any); ok { + content = nested + } + out := map[string]any{} + for k, v := range content { + if k == "content" { + continue + } + out[k] = v + } + if raw, ok := out["jsonml"].(string); ok { + var decoded any + if err := json.Unmarshal([]byte(raw), &decoded); err == nil { + out["jsonml"] = decoded + } + } + if revision, ok := out["version"]; ok { + if _, exists := out["revision"]; !exists { + out["revision"] = revision + } + } + return out +} + func addDocNodeFlags(cmd *cobra.Command) { cmd.Flags().String("node", "", i18n.T("文档 nodeId / URL")) addDocHiddenStringFlag(cmd, "url", "--node alias") @@ -2082,6 +2142,7 @@ func newDocPermissionAddCommand(runner executor.Runner) *cobra.Command { Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { + docDeprecatedNotice(cmd, "drive permission add") return runDocPermissionMutation(cmd, runner, "add_permission") }, } @@ -2091,6 +2152,7 @@ func newDocPermissionAddCommand(runner executor.Runner) *cobra.Command { addDocHiddenStringFlag(cmd, "users", "--user alias") cmd.Flags().String("role", "", i18n.T("权限角色: MANAGER / EDITOR / DOWNLOADER / READER (必填,大小写不敏感)")) cmd.Flags().String("workspace", "", i18n.T("目标知识库 ID 或 URL(选填,辅助构造返回的 docUrl)")) + addDocHiddenStringFlag(cmd, "workspace-id", "--workspace alias") return cmd } @@ -2108,6 +2170,7 @@ func newDocPermissionUpdateCommand(runner executor.Runner) *cobra.Command { Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { + docDeprecatedNotice(cmd, "drive permission update") return runDocPermissionMutation(cmd, runner, "update_permission") }, } @@ -2118,6 +2181,7 @@ func newDocPermissionUpdateCommand(runner executor.Runner) *cobra.Command { addDocHiddenStringFlag(cmd, "uid", "--user alias") cmd.Flags().String("role", "", i18n.T("新权限角色: MANAGER / EDITOR / DOWNLOADER / READER (必填)")) cmd.Flags().String("workspace", "", i18n.T("目标知识库 ID 或 URL(选填)")) + addDocHiddenStringFlag(cmd, "workspace-id", "--workspace alias") return cmd } @@ -2135,6 +2199,7 @@ func newDocPermissionListCommand(runner executor.Runner) *cobra.Command { Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { + docDeprecatedNotice(cmd, "drive permission list") nodeID, err := docRequiredNode(cmd) if err != nil { return err @@ -2157,7 +2222,7 @@ func newDocPermissionListCommand(runner executor.Runner) *cobra.Command { } params["filterRoleIds"] = roles } - if v, _ := cmd.Flags().GetString("workspace"); v != "" { + if v := docFlagOrFallback(cmd, "workspace", "workspace-id"); v != "" { params["workspaceId"] = v } if commandDryRun(cmd) { @@ -2183,6 +2248,59 @@ func newDocPermissionListCommand(runner executor.Runner) *cobra.Command { _ = cmd.Flags().MarkHidden("page-size") cmd.Flags().String("filter-role", "", i18n.T("按角色过滤(逗号分隔): OWNER / MANAGER / EDITOR / DOWNLOADER / READER")) cmd.Flags().String("workspace", "", i18n.T("目标知识库 ID 或 URL(选填)")) + addDocHiddenStringFlag(cmd, "workspace-id", "--workspace alias") + return cmd +} + +func newDocPermissionRemoveCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "remove", + Aliases: []string{"rm"}, + Short: i18n.T("移除文档协作者权限"), + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + docDeprecatedNotice(cmd, "drive permission remove") + nodeID, err := docRequiredNode(cmd) + if err != nil { + return err + } + rawUsers := docFlagOrFallback(cmd, "users", "user", "uid") + if strings.TrimSpace(rawUsers) == "" { + return apperrors.NewValidation("--users is required") + } + userIDs, err := parseDocPermissionUsers(rawUsers) + if err != nil { + return err + } + params := map[string]any{ + "nodeId": nodeID, + "userIds": userIDs, + } + if v := docFlagOrFallback(cmd, "workspace", "workspace-id"); v != "" { + params["workspaceId"] = v + } + if commandDryRun(cmd) { + return writeCommandPayload(cmd, executor.NewHelperInvocation( + cobracmd.LegacyCommandPath(cmd), "doc", "remove_permission", params, + )) + } + result, err := runner.Run(cmd.Context(), executor.NewHelperInvocation( + cobracmd.LegacyCommandPath(cmd), "doc", "remove_permission", params, + )) + if err != nil { + return err + } + return writeCommandPayload(cmd, result) + }, + } + preferLegacyLeaf(cmd) + addDocNodeFlags(cmd) + cmd.Flags().String("users", "", i18n.T("被移除用户 userId 列表,逗号分隔,单次最多 30 (必填)")) + addDocHiddenStringFlag(cmd, "user", "--users alias") + addDocHiddenStringFlag(cmd, "uid", "--users alias") + cmd.Flags().String("workspace", "", i18n.T("目标知识库 ID 或 URL(选填)")) + addDocHiddenStringFlag(cmd, "workspace-id", "--workspace alias") return cmd } @@ -2217,7 +2335,7 @@ func runDocPermissionMutation(cmd *cobra.Command, runner executor.Runner, mcpToo "roleId": role, "userIds": userIDs, } - if v, _ := cmd.Flags().GetString("workspace"); v != "" { + if v := docFlagOrFallback(cmd, "workspace", "workspace-id"); v != "" { params["workspaceId"] = v } if commandDryRun(cmd) { @@ -2234,6 +2352,10 @@ func runDocPermissionMutation(cmd *cobra.Command, runner executor.Runner, mcpToo return writeCommandPayload(cmd, result) } +func docDeprecatedNotice(cmd *cobra.Command, replacement string) { + fmt.Fprintf(cmd.ErrOrStderr(), "warning: deprecated: use dws %s instead.\n", replacement) +} + // TRANSITIONAL: 等 mse 把 delete_document 加入 doc toolOverrides(含 // destructive_hint: true)后,本 helper 可删除——CLI discovery 会自动 // 生成等价命令。工单:plan/mse-yuyuan-patch.md 改动 2.2。 diff --git a/internal/helpers/doc_test.go b/internal/helpers/doc_test.go index 45dd40c6..37efacd3 100644 --- a/internal/helpers/doc_test.go +++ b/internal/helpers/doc_test.go @@ -96,6 +96,35 @@ func TestDocPermissionListLimitAliases(t *testing.T) { } } +func TestDocPermissionRemoveRoutesToRemovePermission(t *testing.T) { + t.Parallel() + + runner := &docCommandRunner{} + cmd := newDocTestRoot(runner) + _, errOut, err := executeDocCommand(t, cmd, + "permission", "rm", + "--node", "NODE_001", + "--users", "uid1,uid2", + "--workspace", "WS_001", + ) + if err != nil { + t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut) + } + if runner.last.Tool != "remove_permission" { + t.Fatalf("tool = %q, want remove_permission", runner.last.Tool) + } + if got := runner.last.Params["nodeId"]; got != "NODE_001" { + t.Fatalf("nodeId = %#v, want NODE_001", got) + } + users, ok := runner.last.Params["userIds"].([]string) + if !ok || strings.Join(users, ",") != "uid1,uid2" { + t.Fatalf("userIds = %#v, want uid1,uid2", runner.last.Params["userIds"]) + } + if got := runner.last.Params["workspaceId"]; got != "WS_001" { + t.Fatalf("workspaceId = %#v, want WS_001", got) + } +} + func TestDocPermissionListMaxresultsRejected(t *testing.T) { t.Parallel() diff --git a/internal/helpers/drive.go b/internal/helpers/drive.go index b083c394..ae7a4cf8 100644 --- a/internal/helpers/drive.go +++ b/internal/helpers/drive.go @@ -79,6 +79,12 @@ func (driveHandler) Command(runner executor.Runner) *cobra.Command { newDriveCommitCommand(runner), newDriveUploadCommand(runner), newDriveDeleteCommand(runner), + newDriveSearchCommand(runner), + newDriveCopyCommand(runner), + newDriveMoveCommand(runner), + newDriveRenameCommand(runner), + newDrivePermissionCommand(runner), + newDriveFolderCommand(runner), ) return root } @@ -98,6 +104,17 @@ func newDriveListCommand(runner executor.Runner) *cobra.Command { if maxResults <= 0 { maxResults = 20 } + if workspaceID := driveFlagOrFallback(cmd, "workspace", "workspace-id"); workspaceID != "" { + params := map[string]any{"workspaceId": workspaceID} + if folderID := driveFlagOrFallback(cmd, "folder", "parent-id"); folderID != "" { + params["folderId"] = normalizeDocNodeID(folderID) + } + if maxResults > 0 { + params["pageSize"] = maxResults + } + addDriveStringParam(cmd, params, "pageToken", "cursor", "next-token") + return runDriveInvocation(cmd, runner, "doc", "list_nodes", params) + } params := map[string]any{"maxResults": float64(maxResults)} addDriveStringParam(cmd, params, "spaceId", "space-id") if parentID := driveFlagOrFallback(cmd, "folder", "parent-id"); parentID != "" { @@ -124,6 +141,8 @@ func newDriveListCommand(runner executor.Runner) *cobra.Command { cmd.Flags().String("space-id", "", "空间 ID,不传则使用「我的文件」对应 spaceId (可选)") cmd.Flags().String("folder", "", "父节点 ID (dentryUuid),不传则列出空间根目录 (可选)") addDriveHiddenStringFlag(cmd, "parent-id", "--folder 的兼容别名") + cmd.Flags().String("workspace", "", "文档空间/知识库 ID,传入则路由到文档空间") + addDriveHiddenStringFlag(cmd, "workspace-id", "--workspace 的兼容别名") cmd.Flags().String("cursor", "", "分页游标,首次不传 (可选)") addDriveHiddenStringFlag(cmd, "next-token", "--cursor 的兼容别名") cmd.Flags().String("order-by", "", "排序字段: createTime|modifyTime|name (可选)") @@ -147,6 +166,7 @@ spaceType 筛选规则: Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { + fmt.Fprintln(cmd.ErrOrStderr(), "warning: deprecated: use dws wiki space list instead.") params := map[string]any{} maxResults, _ := cmd.Flags().GetInt("limit") if !cmd.Flags().Changed("limit") { @@ -380,6 +400,9 @@ func newDriveUploadCommand(runner executor.Runner) *cobra.Command { cmd.Flags().String("mime-type", "", "文件 MIME 类型,不传则自动推断 (可选)") cmd.Flags().String("folder", "", "父节点 ID (dentryUuid),不传则上传到空间根目录 (可选)") addDriveHiddenStringFlag(cmd, "parent-id", "--folder 的兼容别名") + cmd.Flags().String("workspace", "", "目标知识库 ID,传入时路由到文档空间上传") + addDriveHiddenStringFlag(cmd, "workspace-id", "--workspace 的兼容别名") + cmd.Flags().Bool("convert", false, "是否转换为钉钉在线文档(文档空间上传时生效)") return cmd } @@ -388,6 +411,9 @@ func runDriveUpload(cmd *cobra.Command, runner executor.Runner) error { if strings.TrimSpace(filePath) == "" { return apperrors.NewValidation("--file is required") } + if workspaceID := driveFlagOrFallback(cmd, "workspace", "workspace-id"); workspaceID != "" { + return runDriveUploadToDoc(cmd, runner, workspaceID) + } absPath, err := filepath.Abs(filePath) if err != nil { @@ -493,6 +519,93 @@ func runDriveUpload(cmd *cobra.Command, runner executor.Runner) error { return writeCommandPayload(cmd, result) } +func runDriveUploadToDoc(cmd *cobra.Command, runner executor.Runner, workspaceID string) error { + filePath, _ := cmd.Flags().GetString("file") + absPath, err := filepath.Abs(filePath) + if err != nil { + return apperrors.NewValidation("无法解析文件路径: " + err.Error()) + } + fi, err := os.Stat(absPath) + if err != nil { + return apperrors.NewValidation("文件不存在或无法读取: " + absPath) + } + if fi.IsDir() { + return apperrors.NewValidation("--file 不能是目录: " + absPath) + } + fileSize := fi.Size() + if fileSize <= 0 { + return apperrors.NewValidation("文件为空") + } + + fileName := driveFlagOrFallback(cmd, "file-name", "name") + if fileName == "" { + fileName = filepath.Base(absPath) + } + folderID := driveFlagOrFallback(cmd, "folder", "parent-id") + if folderID != "" { + folderID = normalizeDocNodeID(folderID) + } + + step1Params := map[string]any{"workspaceId": workspaceID} + if folderID != "" { + step1Params["folderId"] = folderID + } + commitParams := map[string]any{ + "name": fileName, + "fileSize": float64(fileSize), + "workspaceId": workspaceID, + } + if folderID != "" { + commitParams["folderId"] = folderID + } + if convert, _ := cmd.Flags().GetBool("convert"); convert { + commitParams["convertToOnlineDoc"] = true + } + + if commandDryRun(cmd) { + return writeCommandPayload(cmd, map[string]any{ + "dry_run": true, + "step_1_get_file_upload_info": executor.NewHelperInvocation( + cobracmd.LegacyCommandPath(cmd), "doc", "get_file_upload_info", step1Params, + ), + "step_2_http_put_oss": "PUT file bytes to resourceUrl with returned headers", + "step_3_commit_uploaded_file": executor.NewHelperInvocation( + cobracmd.LegacyCommandPath(cmd), "doc", "commit_uploaded_file", commitParams, + ), + "file": absPath, + "name": fileName, + "size": fileSize, + }) + } + + fmt.Fprintf(cmd.ErrOrStderr(), "[1/3] 获取文档空间上传凭证 %s (%d 字节)...\n", fileName, fileSize) + step1Result, err := runner.Run(cmd.Context(), executor.NewHelperInvocation( + cobracmd.LegacyCommandPath(cmd), "doc", "get_file_upload_info", step1Params, + )) + if err != nil { + return fmt.Errorf("获取上传凭证失败: %w", err) + } + resourceURL, uploadKey, headers, err := extractDocFileUploadInfo(step1Result.Response) + if err != nil { + return err + } + + fmt.Fprintln(cmd.ErrOrStderr(), "[2/3] 上传文件到 OSS...") + if err := httpPutDriveFile(cmd.Context(), resourceURL, headers, absPath, fileSize); err != nil { + return err + } + + fmt.Fprintln(cmd.ErrOrStderr(), "[3/3] 提交文件入库...") + commitParams["uploadKey"] = uploadKey + result, err := runner.Run(cmd.Context(), executor.NewHelperInvocation( + cobracmd.LegacyCommandPath(cmd), "doc", "commit_uploaded_file", commitParams, + )) + if err != nil { + return fmt.Errorf("提交文件入库失败: %w", err) + } + return writeCommandPayload(cmd, result) +} + // ── delete (drive surface routed to doc MCP server) ──────── func newDriveDeleteCommand(runner executor.Runner) *cobra.Command { @@ -528,6 +641,303 @@ func newDriveDeleteCommand(runner executor.Runner) *cobra.Command { return cmd } +func newDriveSearchCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "search", + Short: "搜索文件(聚合钉盘和文档空间)", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + keyword := driveFlagOrFallback(cmd, "query", "keyword") + if keyword == "" { + return apperrors.NewValidation("--query is required") + } + target := driveStringFlag(cmd, "target") + driveParams := map[string]any{"keyword": keyword} + if target != "" && target != "all" { + driveParams["searchTarget"] = target + } + addDriveStringSliceParam(cmd, driveParams, "fileTypes", "file-types") + addDriveStringSliceParam(cmd, driveParams, "extensions", "extensions") + addDriveStringSliceParam(cmd, driveParams, "creatorUserIds", "creator-uids") + addDriveInt64Param(cmd, driveParams, "createdTimeFrom", "created-from") + addDriveInt64Param(cmd, driveParams, "createdTimeTo", "created-to") + addDriveInt64Param(cmd, driveParams, "modifiedTimeFrom", "modified-from") + addDriveInt64Param(cmd, driveParams, "modifiedTimeTo", "modified-to") + if pageSize := driveIntFlagOrFallback(cmd, "limit", "page-size"); pageSize > 0 { + driveParams["pageSize"] = float64(pageSize) + } + addDriveStringParam(cmd, driveParams, "pageToken", "cursor", "page-token") + + if target == "file" || target == "space" { + return runDriveInvocation(cmd, runner, "drive", "search_files", driveParams) + } + + driveResult, driveErr := driveInvocationResult(cmd, runner, "drive", "search_files", driveParams) + docParams := map[string]any{"keyword": keyword} + if pageSize := driveIntFlagOrFallback(cmd, "limit", "page-size"); pageSize > 0 { + docParams["pageSize"] = pageSize + } + if extensions, ok := driveParams["extensions"]; ok { + docParams["extensions"] = extensions + } + docResult, docErr := driveInvocationResult(cmd, runner, "doc", "search_documents", docParams) + if driveErr != nil && docErr != nil { + return fmt.Errorf("aggregated search failed: drive: %v; doc: %v", driveErr, docErr) + } + result := map[string]any{} + if driveErr == nil { + result["drive_results"] = driveResult.Response + } + if docErr == nil { + result["doc_results"] = docResult.Response + } + return writeCommandPayload(cmd, map[string]any{"success": true, "result": result}) + }, + } + preferLegacyLeaf(cmd) + cmd.Flags().String("query", "", "搜索关键词 (必填)") + addDriveHiddenStringFlag(cmd, "keyword", "--query 的兼容别名") + cmd.Flags().String("target", "", "搜索范围: all(默认) / file / space") + cmd.Flags().StringSlice("file-types", nil, "按文件内容类型过滤") + cmd.Flags().StringSlice("extensions", nil, "按文件扩展名过滤") + cmd.Flags().StringSlice("creator-uids", nil, "按创建者 userId 过滤") + cmd.Flags().Int64("created-from", 0, "创建时间起始毫秒时间戳") + cmd.Flags().Int64("created-to", 0, "创建时间截止毫秒时间戳") + cmd.Flags().Int64("modified-from", 0, "修改时间起始毫秒时间戳") + cmd.Flags().Int64("modified-to", 0, "修改时间截止毫秒时间戳") + cmd.Flags().Int("limit", 0, "每页返回数量") + cmd.Flags().Int("page-size", 0, "--limit 的兼容别名") + _ = cmd.Flags().MarkHidden("page-size") + cmd.Flags().String("cursor", "", "分页游标") + addDriveHiddenStringFlag(cmd, "page-token", "--cursor 的兼容别名") + return cmd +} + +func newDriveCopyCommand(runner executor.Runner) *cobra.Command { + return newDriveDocTransferCommand(runner, "copy", "copy_document") +} + +func newDriveMoveCommand(runner executor.Runner) *cobra.Command { + return newDriveDocTransferCommand(runner, "move", "move_document") +} + +func newDriveDocTransferCommand(runner executor.Runner, use, tool string) *cobra.Command { + cmd := &cobra.Command{ + Use: use, + Short: use + " 文档空间节点", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + nodeID, err := driveRequiredFlagOrFallback(cmd, "node", "url", "id", "node-id", "doc-id", "file-id") + if err != nil { + return err + } + params := map[string]any{"nodeId": normalizeDocNodeID(nodeID)} + if folder := driveFlagOrFallback(cmd, "folder", "parent-id", "parent-node-id", "parent-folder-id"); folder != "" { + params["targetFolderId"] = normalizeDocNodeID(folder) + } + addDriveStringParam(cmd, params, "workspaceId", "workspace", "workspace-id") + return runDriveInvocation(cmd, runner, "doc", tool, params) + }, + } + preferLegacyLeaf(cmd) + addDriveDocNodeFlags(cmd) + cmd.Flags().String("folder", "", "目标文件夹 nodeId") + addDriveHiddenStringFlag(cmd, "parent-id", "--folder 的兼容别名") + addDriveHiddenStringFlag(cmd, "parent-node-id", "--folder 的兼容别名") + addDriveHiddenStringFlag(cmd, "parent-folder-id", "--folder 的兼容别名") + cmd.Flags().String("workspace", "", "目标知识库 ID") + addDriveHiddenStringFlag(cmd, "workspace-id", "--workspace 的兼容别名") + return cmd +} + +func newDriveRenameCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "rename", + Short: "重命名文档空间节点", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + nodeID, err := driveRequiredFlagOrFallback(cmd, "node", "url", "id", "node-id", "doc-id", "file-id") + if err != nil { + return err + } + name, err := driveRequiredFlagOrFallback(cmd, "name", "title") + if err != nil { + return err + } + return runDriveInvocation(cmd, runner, "doc", "rename_document", map[string]any{ + "nodeId": normalizeDocNodeID(nodeID), + "newName": name, + }) + }, + } + preferLegacyLeaf(cmd) + addDriveDocNodeFlags(cmd) + cmd.Flags().String("name", "", "新名称 (必填)") + addDriveHiddenStringFlag(cmd, "title", "--name 的兼容别名") + return cmd +} + +func newDrivePermissionCommand(runner executor.Runner) *cobra.Command { + root := &cobra.Command{ + Use: "permission", + Aliases: []string{"perm"}, + Short: "文档空间节点权限管理", + Args: cobra.NoArgs, + TraverseChildren: true, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + return cmd.Help() + }, + } + root.AddCommand( + newDrivePermissionMutationCommand(runner, "add", "add_permission", true), + newDrivePermissionMutationCommand(runner, "update", "update_permission", true), + newDrivePermissionListCommand(runner), + newDrivePermissionRemoveCommand(runner), + ) + preferLegacyLeaf(root) + return root +} + +func newDrivePermissionMutationCommand(runner executor.Runner, use, tool string, requireRole bool) *cobra.Command { + cmd := &cobra.Command{ + Use: use, + Short: use + " 文档空间节点权限", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + nodeID, err := driveRequiredFlagOrFallback(cmd, "node", "url", "id", "node-id", "doc-id", "file-id") + if err != nil { + return err + } + rawUsers := driveFlagOrFallback(cmd, "users", "user", "uid") + if rawUsers == "" { + return apperrors.NewValidation("--users is required") + } + userIDs, err := parseDocPermissionUsers(rawUsers) + if err != nil { + return err + } + params := map[string]any{ + "nodeId": normalizeDocNodeID(nodeID), + "userIds": userIDs, + } + if requireRole { + rawRole, err := driveRequiredFlag(cmd, "role") + if err != nil { + return err + } + role, ok := normalizeDocPermissionRole(rawRole) + if !ok { + return apperrors.NewValidation(fmt.Sprintf("invalid --role: %s", rawRole)) + } + params["roleId"] = role + } + addDriveStringParam(cmd, params, "workspaceId", "workspace", "workspace-id") + return runDriveInvocation(cmd, runner, "doc", tool, params) + }, + } + preferLegacyLeaf(cmd) + addDriveDocNodeFlags(cmd) + cmd.Flags().String("users", "", "用户 userId 列表,逗号分隔 (必填)") + addDriveHiddenStringFlag(cmd, "user", "--users 的兼容别名") + addDriveHiddenStringFlag(cmd, "uid", "--users 的兼容别名") + if requireRole { + cmd.Flags().String("role", "", "权限角色: MANAGER / EDITOR / DOWNLOADER / READER (必填)") + } + cmd.Flags().String("workspace", "", "知识库 ID (选填)") + addDriveHiddenStringFlag(cmd, "workspace-id", "--workspace 的兼容别名") + return cmd +} + +func newDrivePermissionListCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "查询文档空间节点协作者", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + nodeID, err := driveRequiredFlagOrFallback(cmd, "node", "url", "id", "node-id", "doc-id", "file-id") + if err != nil { + return err + } + params := map[string]any{"nodeId": normalizeDocNodeID(nodeID)} + if limit := driveIntFlagOrFallback(cmd, "limit", "max-results", "page-size"); limit > 0 { + params["maxResults"] = limit + } + if filterRole := driveStringFlag(cmd, "filter-role"); filterRole != "" { + params["filterRoleIds"] = parseDriveRoleList(filterRole) + } + addDriveStringParam(cmd, params, "workspaceId", "workspace", "workspace-id") + return runDriveInvocation(cmd, runner, "doc", "list_permission", params) + }, + } + preferLegacyLeaf(cmd) + addDriveDocNodeFlags(cmd) + cmd.Flags().Int("limit", 30, "返回成员数上限") + cmd.Flags().Int("max-results", 0, "--limit 的兼容别名") + _ = cmd.Flags().MarkHidden("max-results") + cmd.Flags().Int("page-size", 0, "--limit 的兼容别名") + _ = cmd.Flags().MarkHidden("page-size") + cmd.Flags().String("filter-role", "", "按角色过滤,逗号分隔") + cmd.Flags().String("workspace", "", "知识库 ID (选填)") + addDriveHiddenStringFlag(cmd, "workspace-id", "--workspace 的兼容别名") + return cmd +} + +func newDrivePermissionRemoveCommand(runner executor.Runner) *cobra.Command { + cmd := newDrivePermissionMutationCommand(runner, "remove", "remove_permission", false) + cmd.Aliases = []string{"rm"} + return cmd +} + +func newDriveFolderCommand(runner executor.Runner) *cobra.Command { + root := &cobra.Command{ + Use: "folder", + Short: "文档空间文件夹兼容入口(deprecated)", + Args: cobra.NoArgs, + TraverseChildren: true, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + return cmd.Help() + }, + } + create := &cobra.Command{ + Use: "create", + Short: "创建文档空间文件夹(deprecated)", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Fprintln(cmd.ErrOrStderr(), "warning: deprecated: use dws wiki node create --type folder instead.") + name, err := driveRequiredFlagOrFallback(cmd, "name", "title") + if err != nil { + return err + } + params := map[string]any{"name": name} + if folder := driveFlagOrFallback(cmd, "folder", "parent-id"); folder != "" { + params["folderId"] = normalizeDocNodeID(folder) + } + addDriveStringParam(cmd, params, "workspaceId", "workspace", "workspace-id") + return runDriveInvocation(cmd, runner, "doc", "create_folder", params) + }, + } + preferLegacyLeaf(root) + preferLegacyLeaf(create) + create.Flags().String("name", "", "文件夹名称 (必填)") + addDriveHiddenStringFlag(create, "title", "--name 的兼容别名") + create.Flags().String("folder", "", "父文件夹 nodeId 或 URL") + addDriveHiddenStringFlag(create, "parent-id", "--folder 的兼容别名") + create.Flags().String("workspace", "", "目标知识库 ID") + addDriveHiddenStringFlag(create, "workspace-id", "--workspace 的兼容别名") + root.AddCommand(create) + root.Hidden = true + return root +} + func runDriveInfo(cmd *cobra.Command, runner executor.Runner, params map[string]any) error { result, err := driveInvocationResult(cmd, runner, "drive", "get_file_info", params) if err != nil { @@ -814,6 +1224,64 @@ func addDriveStringParam(cmd *cobra.Command, params map[string]any, paramName st } } +func addDriveStringSliceParam(cmd *cobra.Command, params map[string]any, paramName, flag string) { + if !cmd.Flags().Changed(flag) { + return + } + values, err := cmd.Flags().GetStringSlice(flag) + if err != nil || len(values) == 0 { + return + } + cleaned := make([]string, 0, len(values)) + for _, value := range values { + for _, part := range strings.Split(value, ",") { + if item := strings.TrimSpace(part); item != "" { + cleaned = append(cleaned, item) + } + } + } + if len(cleaned) > 0 { + params[paramName] = cleaned + } +} + +func addDriveInt64Param(cmd *cobra.Command, params map[string]any, paramName, flag string) { + if !cmd.Flags().Changed(flag) { + return + } + value, err := cmd.Flags().GetInt64(flag) + if err == nil && value > 0 { + params[paramName] = value + } +} + +func addDriveDocNodeFlags(cmd *cobra.Command) { + cmd.Flags().String("node", "", "节点 ID 或 URL (必填)") + addDriveHiddenStringFlag(cmd, "url", "--node 的兼容别名") + addDriveHiddenStringFlag(cmd, "id", "--node 的兼容别名") + addDriveHiddenStringFlag(cmd, "node-id", "--node 的兼容别名") + addDriveHiddenStringFlag(cmd, "doc-id", "--node 的兼容别名") + addDriveHiddenStringFlag(cmd, "file-id", "--node 的兼容别名") +} + +func parseDriveRoleList(raw string) []string { + roles := make([]string, 0) + for _, part := range strings.Split(raw, ",") { + role := strings.ToUpper(strings.TrimSpace(part)) + if role == "" { + continue + } + if normalized, ok := normalizeDocPermissionRole(role); ok { + roles = append(roles, normalized) + continue + } + if role == "OWNER" { + roles = append(roles, role) + } + } + return roles +} + func addDriveHiddenStringFlag(cmd *cobra.Command, name, usage string) { cmd.Flags().String(name, "", usage) _ = cmd.Flags().MarkHidden(name) diff --git a/internal/helpers/drive_test.go b/internal/helpers/drive_test.go index 5af1112f..ca144c52 100644 --- a/internal/helpers/drive_test.go +++ b/internal/helpers/drive_test.go @@ -21,19 +21,24 @@ import ( "net/http/httptest" "os" "path/filepath" + "strings" "testing" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor" ) type driveCommandRunner struct { + calls int + all []executor.Invocation last executor.Invocation result executor.Result err error } func (r *driveCommandRunner) Run(_ context.Context, invocation executor.Invocation) (executor.Result, error) { + r.calls++ r.last = invocation + r.all = append(r.all, invocation) if r.err != nil { return executor.Result{}, r.err } @@ -65,6 +70,109 @@ func TestDriveListPageSizeAliasMapsMaxResults(t *testing.T) { } } +func TestDriveListWorkspaceRoutesToDocListNodes(t *testing.T) { + t.Parallel() + + runner := &driveCommandRunner{} + cmd := newDriveListCommand(runner) + var out, errOut bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&errOut) + cmd.SetArgs([]string{"--workspace-id", "WS_001", "--folder", "FOLDER_001", "--limit", "10"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut.String()) + } + if runner.last.CanonicalProduct != "doc" { + t.Fatalf("product = %q, want doc", runner.last.CanonicalProduct) + } + if runner.last.Tool != "list_nodes" { + t.Fatalf("tool = %q, want list_nodes", runner.last.Tool) + } + if got := runner.last.Params["workspaceId"]; got != "WS_001" { + t.Fatalf("workspaceId = %#v, want WS_001", got) + } + if got := runner.last.Params["folderId"]; got != "FOLDER_001" { + t.Fatalf("folderId = %#v, want FOLDER_001", got) + } + if got := runner.last.Params["pageSize"]; got != 10 { + t.Fatalf("pageSize = %#v, want 10", got) + } +} + +func TestDriveCopyAliasesRouteToDoc(t *testing.T) { + t.Parallel() + + runner := &driveCommandRunner{} + cmd := newDriveCopyCommand(runner) + var out, errOut bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&errOut) + cmd.SetArgs([]string{"--file-id", "NODE_001", "--parent-id", "FOLDER_001", "--workspace-id", "WS_001"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut.String()) + } + if runner.last.CanonicalProduct != "doc" || runner.last.Tool != "copy_document" { + t.Fatalf("invocation = %#v, want doc copy_document", runner.last) + } + if got := runner.last.Params["nodeId"]; got != "NODE_001" { + t.Fatalf("nodeId = %#v, want NODE_001", got) + } + if got := runner.last.Params["targetFolderId"]; got != "FOLDER_001" { + t.Fatalf("targetFolderId = %#v, want FOLDER_001", got) + } + if got := runner.last.Params["workspaceId"]; got != "WS_001" { + t.Fatalf("workspaceId = %#v, want WS_001", got) + } +} + +func TestDrivePermissionRemoveRoutesToDoc(t *testing.T) { + t.Parallel() + + runner := &driveCommandRunner{} + cmd := newDrivePermissionCommand(runner) + var out, errOut bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&errOut) + cmd.SetArgs([]string{"remove", "--node", "NODE_001", "--users", "uid1,uid2"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut.String()) + } + if runner.last.CanonicalProduct != "doc" || runner.last.Tool != "remove_permission" { + t.Fatalf("invocation = %#v, want doc remove_permission", runner.last) + } + users, ok := runner.last.Params["userIds"].([]string) + if !ok || strings.Join(users, ",") != "uid1,uid2" { + t.Fatalf("userIds = %#v, want uid1,uid2", runner.last.Params["userIds"]) + } +} + +func TestDriveSearchAggregatesDriveAndDoc(t *testing.T) { + t.Parallel() + + runner := &driveCommandRunner{} + cmd := newDriveSearchCommand(runner) + var out, errOut bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&errOut) + cmd.SetArgs([]string{"--query", "报告", "--extensions", "pdf,docx", "--limit", "5"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut.String()) + } + if len(runner.all) != 2 { + t.Fatalf("calls = %d, want 2", len(runner.all)) + } + if runner.all[0].CanonicalProduct != "drive" || runner.all[0].Tool != "search_files" { + t.Fatalf("first invocation = %#v, want drive search_files", runner.all[0]) + } + if runner.all[1].CanonicalProduct != "doc" || runner.all[1].Tool != "search_documents" { + t.Fatalf("second invocation = %#v, want doc search_documents", runner.all[1]) + } +} + func TestDriveDownloadOutputDirectoryUsesServerFileName(t *testing.T) { t.Parallel() diff --git a/internal/helpers/sheet.go b/internal/helpers/sheet.go new file mode 100644 index 00000000..6edfe061 --- /dev/null +++ b/internal/helpers/sheet.go @@ -0,0 +1,2209 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helpers + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cobracmd" + apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor" + "github.com/spf13/cobra" +) + +func init() { + RegisterPublic(func() Handler { + return sheetHandler{} + }) +} + +type sheetHandler struct{} + +func (sheetHandler) Name() string { + return "sheet" +} + +func (sheetHandler) Command(runner executor.Runner) *cobra.Command { + root := newSheetGroup("sheet", "钉钉电子表格") + root.Long = "管理钉钉在线电子表格:文档与工作表管理、单元格读写、CSV、行列、样式、筛选、条件格式和图片。" + + rangeCmd := newSheetGroup("range", "数据区域操作") + rangeCmd.AddCommand( + newSheetRangeReadCommand(runner), + newSheetRangeUpdateCommand(runner), + newSheetRangeClearCommand(runner), + newSheetRangeSortCommand(runner), + newSheetRangeFillCommand(runner), + newSheetRangeCopyCommand(runner), + newSheetRangeMoveCommand(runner), + newSheetRangeSetStyleCommand(runner), + newSheetRangeBatchSetStyleCommand(runner), + ) + + filterCmd := newSheetGroup("filter", "全局筛选管理") + filterCmd.AddCommand( + newSheetFilterGetCommand(runner), + newSheetFilterCreateCommand(runner), + newSheetFilterDeleteCommand(runner), + newSheetFilterUpdateCommand(runner), + newSheetFilterClearCriteriaCommand(runner), + newSheetFilterSortCommand(runner), + ) + + filterViewCmd := newSheetGroup("filter-view", "筛选视图管理") + filterViewCmd.AddCommand( + newSheetFilterViewListCommand(runner), + newSheetFilterViewCreateCommand(runner), + newSheetFilterViewUpdateCommand(runner), + newSheetFilterViewDeleteCommand(runner), + newSheetFilterViewUpdateCriteriaCommand(runner), + newSheetFilterViewDeleteCriteriaCommand(runner), + newSheetFilterViewInfoCommand(runner), + newSheetFilterViewListCriteriaCommand(runner), + newSheetFilterViewGetCriteriaCommand(runner), + ) + + condFormatCmd := newSheetGroup("cond-format", "条件格式管理") + condFormatCmd.AddCommand( + newSheetCondFormatListCommand(runner), + newSheetCondFormatCreateCommand(runner), + newSheetCondFormatUpdateCommand(runner), + newSheetCondFormatDeleteCommand(runner), + ) + + root.AddCommand( + newSheetCreateCommand(runner), + newSheetListCommand(runner), + newSheetInfoCommand(runner), + newSheetNewCommand(runner), + newSheetUpdateCommand(runner), + newSheetCopyCommand(runner), + newSheetDeleteSheetCommand(runner), + rangeCmd, + newSheetFindCommand(runner), + newSheetAppendCommand(runner), + newSheetCSVPutCommand(runner), + newSheetCSVGetCommand(runner), + newSheetInsertDimensionCommand(runner), + newSheetDeleteDimensionCommand(runner), + newSheetUpdateDimensionCommand(runner), + newSheetMoveDimensionCommand(runner), + newSheetAddDimensionCommand(runner), + newSheetMergeCellsCommand(runner), + newSheetUnmergeCellsCommand(runner), + newSheetSetDropdownCommand(runner), + newSheetGetDropdownCommand(runner), + newSheetDeleteDropdownCommand(runner), + newSheetMediaUploadCommand(runner), + newSheetWriteImageCommand(runner), + newSheetReplaceCommand(runner), + filterCmd, + filterViewCmd, + condFormatCmd, + newSheetCreateFloatImageCommand(runner), + newSheetGetFloatImageCommand(runner), + newSheetListFloatImagesCommand(runner), + newSheetUpdateFloatImageCommand(runner), + newSheetDeleteFloatImageCommand(runner), + newSheetExportCommand(runner), + ) + return root +} + +func newSheetCreateCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("create", "创建钉钉表格文档", func(cmd *cobra.Command, _ []string) error { + name, err := sheetRequiredFlag(cmd, "name") + if err != nil { + return err + } + params := map[string]any{"name": name} + sheetAddStringParam(cmd, params, "folderId", "folder") + sheetAddStringParam(cmd, params, "workspaceId", "workspace", "workspace-id") + return runSheetTool(cmd, runner, "create_workspace_sheet", params) + }) + cmd.Flags().String("name", "", "表格名称 (必填)") + cmd.Flags().String("folder", "", "目标文件夹 ID 或 URL") + cmd.Flags().String("workspace", "", "目标知识库 ID") + addSheetHiddenStringFlag(cmd, "workspace-id", "--workspace alias") + return cmd +} + +func newSheetListCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("list", "获取全部工作表列表", func(cmd *cobra.Command, _ []string) error { + node, err := sheetRequiredFlag(cmd, "node") + if err != nil { + return err + } + return runSheetTool(cmd, runner, "get_all_sheets", map[string]any{"nodeId": node}) + }) + addSheetNodeFlags(cmd) + return cmd +} + +func newSheetInfoCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("info", "获取指定工作表详情", func(cmd *cobra.Command, _ []string) error { + node, err := sheetRequiredFlag(cmd, "node") + if err != nil { + return err + } + params := map[string]any{"nodeId": node} + sheetAddStringParam(cmd, params, "sheetId", "sheet-id") + return runSheetTool(cmd, runner, "get_sheet", params) + }) + addSheetNodeFlags(cmd) + cmd.Flags().String("sheet-id", "", "工作表 ID 或名称") + return cmd +} + +func newSheetNewCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("new", "新建工作表", func(cmd *cobra.Command, _ []string) error { + node, name, err := sheetNodeAndName(cmd) + if err != nil { + return err + } + return runSheetTool(cmd, runner, "create_sheet", map[string]any{"nodeId": node, "name": name}) + }) + addSheetNodeFlags(cmd) + cmd.Flags().String("name", "", "工作表名称 (必填)") + return cmd +} + +func newSheetUpdateCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("update", "更新工作表属性", func(cmd *cobra.Command, _ []string) error { + params, err := sheetBaseParams(cmd) + if err != nil { + return err + } + changed := false + if cmd.Flags().Changed("name") || cmd.Flags().Changed("title") { + params["title"] = sheetNameFlag(cmd) + changed = true + } + if cmd.Flags().Changed("index") { + index, _ := cmd.Flags().GetInt("index") + if index < 0 { + return apperrors.NewValidation("--index must be >= 0") + } + params["index"] = index + changed = true + } + if cmd.Flags().Changed("hidden") { + v, _ := cmd.Flags().GetBool("hidden") + params["hidden"] = v + changed = true + } + if cmd.Flags().Changed("frozen-row-count") { + v, _ := cmd.Flags().GetInt("frozen-row-count") + if v < 0 { + return apperrors.NewValidation("--frozen-row-count must be >= 0") + } + params["frozenRowCount"] = v + changed = true + } + if cmd.Flags().Changed("frozen-column-count") { + v, _ := cmd.Flags().GetInt("frozen-column-count") + if v < 0 { + return apperrors.NewValidation("--frozen-column-count must be >= 0") + } + params["frozenColumnCount"] = v + changed = true + } + if !changed { + return apperrors.NewValidation("at least one of --name, --index, --hidden, --frozen-row-count or --frozen-column-count is required") + } + return runSheetTool(cmd, runner, "update_sheet", params) + }) + addSheetBaseFlags(cmd) + cmd.Flags().String("name", "", "工作表新名称") + cmd.Flags().String("title", "", "--name alias") + _ = cmd.Flags().MarkHidden("title") + cmd.Flags().Int("index", 0, "工作表新位置索引,0-based") + cmd.Flags().Bool("hidden", false, "是否隐藏工作表") + cmd.Flags().Int("frozen-row-count", 0, "冻结行数") + cmd.Flags().Int("frozen-column-count", 0, "冻结列数") + return cmd +} + +func newSheetCopyCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("copy", "复制工作表", func(cmd *cobra.Command, _ []string) error { + params, err := sheetBaseParams(cmd) + if err != nil { + return err + } + if cmd.Flags().Changed("name") || cmd.Flags().Changed("title") { + params["title"] = sheetNameFlag(cmd) + } + if cmd.Flags().Changed("index") { + index, _ := cmd.Flags().GetInt("index") + if index < 0 { + return apperrors.NewValidation("--index must be >= 0") + } + params["index"] = index + } + return runSheetTool(cmd, runner, "copy_sheet", params) + }) + addSheetBaseFlags(cmd) + cmd.Flags().String("name", "", "副本名称") + cmd.Flags().String("title", "", "--name alias") + _ = cmd.Flags().MarkHidden("title") + cmd.Flags().Int("index", 0, "副本位置索引,0-based") + return cmd +} + +func newSheetDeleteSheetCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("delete-sheet", "删除工作表", func(cmd *cobra.Command, _ []string) error { + params, err := sheetBaseParams(cmd) + if err != nil { + return err + } + return runSheetTool(cmd, runner, "delete_sheet", params) + }) + addSheetBaseFlags(cmd) + return cmd +} + +func newSheetRangeReadCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("read", "读取工作表数据", func(cmd *cobra.Command, _ []string) error { + node, err := sheetRequiredFlag(cmd, "node") + if err != nil { + return err + } + params := map[string]any{"nodeId": node} + sheetAddStringParam(cmd, params, "sheetId", "sheet-id") + sheetAddStringParam(cmd, params, "range", "range") + sheetAddStringParam(cmd, params, "valueRenderOption", "value-render-option") + return runSheetTool(cmd, runner, "get_cell_infos", params) + }) + cmd.Aliases = []string{"get"} + addSheetNodeFlags(cmd) + cmd.Flags().String("sheet-id", "", "工作表 ID 或名称") + cmd.Flags().String("range", "", "读取范围,A1 表示法") + cmd.Flags().String("value-render-option", "", "formatted_value | raw_value | formula") + return cmd +} + +func newSheetRangeUpdateCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("update", "更新工作表指定区域内容", func(cmd *cobra.Command, _ []string) error { + if err := sheetValidateRequired(cmd, "node", "sheet-id", "range", "values"); err != nil { + return err + } + var cells [][]any + if err := sheetParseJSONFlag(cmd, "values", &cells); err != nil { + return err + } + for i, row := range cells { + for j, cell := range row { + if cell == nil { + return apperrors.NewValidation(fmt.Sprintf("--values[%d][%d] must be an object, not null", i, j)) + } + if _, ok := cell.(map[string]any); !ok { + return apperrors.NewValidation(fmt.Sprintf("--values[%d][%d] must be an object", i, j)) + } + } + } + return runSheetTool(cmd, runner, "set_cell_range", map[string]any{ + "nodeId": sheetStringFlag(cmd, "node"), + "sheetId": sheetStringFlag(cmd, "sheet-id"), + "rangeAddress": sheetStringFlag(cmd, "range"), + "cells": cells, + }) + }) + addSheetBaseFlags(cmd) + cmd.Flags().String("range", "", "目标单元格区域地址 (必填)") + cmd.Flags().String("values", "", "单元格内容二维 JSON 数组 (必填)") + return cmd +} + +func newSheetRangeClearCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("clear", "清除工作表指定区域", func(cmd *cobra.Command, _ []string) error { + if err := sheetValidateRequired(cmd, "node", "sheet-id", "range"); err != nil { + return err + } + params := map[string]any{ + "nodeId": sheetStringFlag(cmd, "node"), + "sheetId": sheetStringFlag(cmd, "sheet-id"), + "range": sheetStringFlag(cmd, "range"), + } + sheetAddStringParam(cmd, params, "type", "type") + return runSheetTool(cmd, runner, "clear_range", params) + }) + addSheetBaseFlags(cmd) + cmd.Flags().String("range", "", "清除范围,A1 表示法 (必填)") + cmd.Flags().String("type", "", "content | format | all") + return cmd +} + +func newSheetRangeSortCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("sort", "对工作表指定区域排序", func(cmd *cobra.Command, _ []string) error { + if err := sheetValidateRequired(cmd, "node", "sheet-id", "range", "sort-keys"); err != nil { + return err + } + var sortKeys []any + if err := sheetParseJSONFlag(cmd, "sort-keys", &sortKeys); err != nil { + return err + } + params := map[string]any{ + "nodeId": sheetStringFlag(cmd, "node"), + "sheetId": sheetStringFlag(cmd, "sheet-id"), + "range": sheetStringFlag(cmd, "range"), + "sortKeys": sortKeys, + } + if v, _ := cmd.Flags().GetBool("has-header"); v { + params["hasHeader"] = true + } + return runSheetTool(cmd, runner, "sort_range", params) + }) + addSheetBaseFlags(cmd) + cmd.Flags().String("range", "", "排序范围,A1 表示法 (必填)") + cmd.Flags().String("sort-keys", "", "排序规则 JSON 数组 (必填)") + cmd.Flags().Bool("has-header", false, "首行是否为表头") + return cmd +} + +func newSheetRangeFillCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("fill", "自动填充工作表指定区域", func(cmd *cobra.Command, _ []string) error { + if err := sheetValidateRequired(cmd, "node", "sheet-id", "source-range", "target-range"); err != nil { + return err + } + params := map[string]any{ + "nodeId": sheetStringFlag(cmd, "node"), + "sheetId": sheetStringFlag(cmd, "sheet-id"), + "sourceRange": sheetStringFlag(cmd, "source-range"), + "destinationRange": sheetStringFlag(cmd, "target-range"), + } + sheetAddStringParam(cmd, params, "fillType", "fill-type") + return runSheetTool(cmd, runner, "fill_range", params) + }) + addSheetBaseFlags(cmd) + cmd.Flags().String("source-range", "", "源数据范围,A1 表示法 (必填)") + cmd.Flags().String("target-range", "", "目标填充范围,A1 表示法 (必填)") + cmd.Flags().String("fill-type", "", "copy | onlystyle | withoutstyle") + return cmd +} + +func newSheetRangeCopyCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("copy-to", "复制工作表指定区域到目标位置", func(cmd *cobra.Command, _ []string) error { + params, err := sheetRangeTransferParams(cmd) + if err != nil { + return err + } + sheetAddStringParam(cmd, params, "pasteType", "paste-type") + return runSheetTool(cmd, runner, "copy_range", params) + }) + addSheetRangeTransferFlags(cmd) + cmd.Flags().String("paste-type", "", "values | formulas | formats | all") + return cmd +} + +func newSheetRangeMoveCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("move-to", "移动工作表指定区域到目标位置", func(cmd *cobra.Command, _ []string) error { + params, err := sheetRangeTransferParams(cmd) + if err != nil { + return err + } + return runSheetTool(cmd, runner, "move_range", params) + }) + addSheetRangeTransferFlags(cmd) + return cmd +} + +func newSheetFindCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("find", "在工作表中搜索单元格内容", func(cmd *cobra.Command, _ []string) error { + query, err := sheetRequiredFlagOrFallback(cmd, "query", "find") + if err != nil { + return err + } + params, err := sheetBaseParams(cmd) + if err != nil { + return err + } + params["text"] = query + sheetAddStringParam(cmd, params, "range", "range") + for flag, key := range map[string]string{ + "match-case": "matchCase", + "match-entire-cell": "matchEntireCell", + "use-regexp": "useRegExp", + "match-formula": "matchFormulaText", + "include-hidden": "includeHidden", + } { + v, _ := cmd.Flags().GetBool(flag) + params[key] = v + } + return runSheetTool(cmd, runner, "find_cells", params) + }) + addSheetBaseFlags(cmd) + cmd.Flags().String("query", "", "搜索文本 (必填)") + cmd.Flags().String("find", "", "--query alias") + _ = cmd.Flags().MarkHidden("find") + cmd.Flags().String("range", "", "搜索范围,A1 表示法") + cmd.Flags().Bool("match-case", true, "区分大小写") + cmd.Flags().Bool("match-entire-cell", false, "完整单元格匹配") + cmd.Flags().Bool("use-regexp", false, "启用正则表达式搜索") + cmd.Flags().Bool("match-formula", false, "搜索公式文本") + cmd.Flags().Bool("include-hidden", false, "包含隐藏单元格") + return cmd +} + +func newSheetAppendCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("append", "在工作表末尾追加数据", func(cmd *cobra.Command, _ []string) error { + if err := sheetValidateRequired(cmd, "node", "sheet-id", "values"); err != nil { + return err + } + var values [][]any + if err := sheetParseJSONFlag(cmd, "values", &values); err != nil { + return err + } + return runSheetTool(cmd, runner, "append_rows", map[string]any{ + "nodeId": sheetStringFlag(cmd, "node"), + "sheetId": sheetStringFlag(cmd, "sheet-id"), + "values": values, + }) + }) + addSheetBaseFlags(cmd) + cmd.Flags().String("values", "", "追加数据二维 JSON 数组 (必填)") + return cmd +} + +func newSheetCSVPutCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("csv-put", "将 CSV 数据写入表格指定位置", func(cmd *cobra.Command, _ []string) error { + if err := sheetValidateRequired(cmd, "node", "sheet-id", "csv", "start-cell"); err != nil { + return err + } + csvContent := sheetStringFlag(cmd, "csv") + switch { + case csvContent == "-": + data, err := io.ReadAll(os.Stdin) + if err != nil { + return fmt.Errorf("read stdin failed: %w", err) + } + csvContent = string(data) + case strings.HasPrefix(csvContent, "@"): + data, err := os.ReadFile(strings.TrimPrefix(csvContent, "@")) + if err != nil { + return fmt.Errorf("read CSV file failed: %w", err) + } + csvContent = string(data) + } + csvContent = strings.ReplaceAll(csvContent, "\r", "") + csvContent = strings.TrimPrefix(csvContent, "\xef\xbb\xbf") + params := map[string]any{ + "nodeId": sheetStringFlag(cmd, "node"), + "sheetId": sheetStringFlag(cmd, "sheet-id"), + "csv": csvContent, + "startCell": sheetStringFlag(cmd, "start-cell"), + } + if cmd.Flags().Changed("allow-overwrite") { + v, _ := cmd.Flags().GetBool("allow-overwrite") + params["allowOverwrite"] = v + } + return runSheetTool(cmd, runner, "set_range_from_csv", params) + }) + addSheetBaseFlags(cmd) + cmd.Flags().String("csv", "", "CSV 文本、@文件路径 或 - (必填)") + cmd.Flags().String("start-cell", "", "起始单元格,A1 表示法 (必填)") + cmd.Flags().Bool("allow-overwrite", false, "允许覆盖已有数据") + return cmd +} + +func newSheetCSVGetCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("csv-get", "以 CSV 格式读取工作表数据", func(cmd *cobra.Command, _ []string) error { + node, err := sheetRequiredFlag(cmd, "node") + if err != nil { + return err + } + params := map[string]any{"nodeId": node} + sheetAddStringParam(cmd, params, "sheetId", "sheet-id") + sheetAddStringParam(cmd, params, "range", "range") + sheetAddStringParam(cmd, params, "valueRenderOption", "value-render-option") + if cmd.Flags().Changed("max-chars") { + v, _ := cmd.Flags().GetInt("max-chars") + params["maxChars"] = v + } + return runSheetTool(cmd, runner, "get_range_as_csv", params) + }) + addSheetNodeFlags(cmd) + cmd.Flags().String("sheet-id", "", "工作表 ID 或名称") + cmd.Flags().String("range", "", "读取范围,A1 表示法") + cmd.Flags().String("value-render-option", "", "formatted_value | raw_value | formula") + cmd.Flags().Int("max-chars", 0, "CSV 最大字符数") + return cmd +} + +func newSheetInsertDimensionCommand(runner executor.Runner) *cobra.Command { + return newSheetDimensionPositionCommand(runner, "insert-dimension", "在指定位置插入行或列", "insert_dimension") +} + +func newSheetDeleteDimensionCommand(runner executor.Runner) *cobra.Command { + return newSheetDimensionPositionCommand(runner, "delete-dimension", "删除指定位置的行或列", "delete_dimension") +} + +func newSheetDimensionPositionCommand(runner executor.Runner, use, short, tool string) *cobra.Command { + cmd := newSheetLeaf(use, short, func(cmd *cobra.Command, _ []string) error { + if err := sheetValidateRequired(cmd, "node", "sheet-id", "dimension", "position", "length"); err != nil { + return err + } + dimension, err := sheetDimension(cmd) + if err != nil { + return err + } + length, err := sheetPositiveIntStringFlag(cmd, "length", 5000) + if err != nil { + return err + } + return runSheetTool(cmd, runner, tool, map[string]any{ + "nodeId": sheetStringFlag(cmd, "node"), + "sheetId": sheetStringFlag(cmd, "sheet-id"), + "dimension": dimension, + "position": sheetStringFlag(cmd, "position"), + "length": length, + }) + }) + addSheetBaseFlags(cmd) + cmd.Flags().String("dimension", "", "ROWS 或 COLUMNS (必填)") + cmd.Flags().String("position", "", "位置,A1 表示法 (必填)") + cmd.Flags().String("length", "", "数量,正整数 (必填)") + return cmd +} + +func newSheetUpdateDimensionCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("update-dimension", "更新指定范围行/列属性", func(cmd *cobra.Command, _ []string) error { + if err := sheetValidateRequired(cmd, "node", "sheet-id", "dimension", "start-index", "length"); err != nil { + return err + } + dimension, err := sheetDimension(cmd) + if err != nil { + return err + } + length, err := sheetPositiveIntStringFlag(cmd, "length", 5000) + if err != nil { + return err + } + hiddenChanged := cmd.Flags().Changed("hidden") + pixelChanged := cmd.Flags().Changed("pixel-size") + if !hiddenChanged && !pixelChanged { + return apperrors.NewValidation("at least one of --hidden or --pixel-size is required") + } + params := map[string]any{ + "nodeId": sheetStringFlag(cmd, "node"), + "sheetId": sheetStringFlag(cmd, "sheet-id"), + "dimension": dimension, + "startIndex": sheetStringFlag(cmd, "start-index"), + "length": length, + } + if hiddenChanged { + v, _ := cmd.Flags().GetBool("hidden") + params["hidden"] = v + } + if pixelChanged { + v, _ := cmd.Flags().GetInt("pixel-size") + if v < 0 { + return apperrors.NewValidation("--pixel-size must be >= 0") + } + params["pixelSize"] = v + } + return runSheetTool(cmd, runner, "update_dimension", params) + }) + addSheetBaseFlags(cmd) + cmd.Flags().String("dimension", "", "ROWS 或 COLUMNS (必填)") + cmd.Flags().String("start-index", "", "起始位置,A1 表示法 (必填)") + cmd.Flags().String("length", "", "数量,正整数 (必填)") + cmd.Flags().Bool("hidden", false, "是否隐藏") + cmd.Flags().Int("pixel-size", 0, "行高或列宽") + return cmd +} + +func newSheetMoveDimensionCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("move-dimension", "移动行或列到指定位置", func(cmd *cobra.Command, _ []string) error { + if err := sheetValidateRequired(cmd, "node", "sheet-id", "dimension", "start-index", "end-index", "destination-index"); err != nil { + return err + } + dimension, err := sheetDimension(cmd) + if err != nil { + return err + } + return runSheetTool(cmd, runner, "move_dimension", map[string]any{ + "nodeId": sheetStringFlag(cmd, "node"), + "sheetId": sheetStringFlag(cmd, "sheet-id"), + "dimension": dimension, + "startIndex": sheetStringFlag(cmd, "start-index"), + "endIndex": sheetStringFlag(cmd, "end-index"), + "destinationIndex": sheetStringFlag(cmd, "destination-index"), + }) + }) + addSheetBaseFlags(cmd) + cmd.Flags().String("dimension", "", "ROWS 或 COLUMNS (必填)") + cmd.Flags().String("start-index", "", "源起始位置 (必填)") + cmd.Flags().String("end-index", "", "源结束位置 (必填)") + cmd.Flags().String("destination-index", "", "目标位置 (必填)") + return cmd +} + +func newSheetAddDimensionCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("add-dimension", "在末尾追加空行或空列", func(cmd *cobra.Command, _ []string) error { + if err := sheetValidateRequired(cmd, "node", "sheet-id", "dimension"); err != nil { + return err + } + dimension, err := sheetDimension(cmd) + if err != nil { + return err + } + length, _ := cmd.Flags().GetInt("length") + if length < 1 || length > 5000 { + return apperrors.NewValidation("--length must be between 1 and 5000") + } + return runSheetTool(cmd, runner, "add_dimension", map[string]any{ + "nodeId": sheetStringFlag(cmd, "node"), + "sheetId": sheetStringFlag(cmd, "sheet-id"), + "dimension": dimension, + "length": length, + }) + }) + addSheetBaseFlags(cmd) + cmd.Flags().String("dimension", "", "ROWS 或 COLUMNS (必填)") + cmd.Flags().Int("length", 0, "追加数量,正整数 (必填)") + return cmd +} + +func newSheetMergeCellsCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("merge-cells", "合并单元格", func(cmd *cobra.Command, _ []string) error { + params, err := sheetRangeAddressParams(cmd) + if err != nil { + return err + } + sheetAddStringParam(cmd, params, "mergeType", "merge-type") + return runSheetTool(cmd, runner, "merge_cells", params) + }) + addSheetBaseFlags(cmd) + cmd.Flags().String("range", "", "目标单元格区域地址 (必填)") + cmd.Flags().String("merge-type", "", "mergeAll | mergeRows | mergeColumns") + return cmd +} + +func newSheetUnmergeCellsCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("unmerge-cells", "取消合并单元格", func(cmd *cobra.Command, _ []string) error { + params, err := sheetRangeAddressParams(cmd) + if err != nil { + return err + } + return runSheetTool(cmd, runner, "unmerge_range", params) + }) + addSheetBaseFlags(cmd) + cmd.Flags().String("range", "", "取消合并的范围 (必填)") + return cmd +} + +func newSheetSetDropdownCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("set-dropdown", "设置下拉列表", func(cmd *cobra.Command, _ []string) error { + if err := sheetValidateRequired(cmd, "node", "sheet-id", "range", "options"); err != nil { + return err + } + var options []map[string]any + if err := sheetParseJSONFlag(cmd, "options", &options); err != nil { + return err + } + if len(options) == 0 { + return apperrors.NewValidation("--options must contain at least one item") + } + params := map[string]any{ + "nodeId": sheetStringFlag(cmd, "node"), + "sheetId": sheetStringFlag(cmd, "sheet-id"), + "range": sheetStringFlag(cmd, "range"), + "options": options, + } + if v, _ := cmd.Flags().GetBool("multi-select"); v { + params["enableMultiSelect"] = true + } + return runSheetTool(cmd, runner, "set_dropdown_lists", params) + }) + addSheetBaseFlags(cmd) + cmd.Flags().String("range", "", "目标单元格范围 (必填)") + cmd.Flags().String("options", "", "下拉选项 JSON 数组 (必填)") + cmd.Flags().Bool("multi-select", false, "是否允许多选") + return cmd +} + +func newSheetGetDropdownCommand(runner executor.Runner) *cobra.Command { + return newSheetRangeToolCommand(runner, "get-dropdown", "获取下拉列表配置", "get_dropdown_lists", "range") +} + +func newSheetDeleteDropdownCommand(runner executor.Runner) *cobra.Command { + return newSheetRangeToolCommand(runner, "delete-dropdown", "删除下拉列表", "delete_dropdown_lists", "range") +} + +func newSheetReplaceCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("replace", "查找替换文本", func(cmd *cobra.Command, _ []string) error { + if err := sheetValidateRequired(cmd, "node", "sheet-id", "find"); err != nil { + return err + } + if !cmd.Flags().Changed("replacement") { + return apperrors.NewValidation("--replacement is required") + } + params, err := sheetBaseParams(cmd) + if err != nil { + return err + } + params["text"] = sheetStringFlag(cmd, "find") + params["replaceText"] = sheetStringFlag(cmd, "replacement") + sheetAddStringParam(cmd, params, "range", "range") + for flag, key := range map[string]string{ + "match-case": "matchCase", + "match-entire-cell": "matchEntireCell", + "use-regexp": "useRegExp", + "include-hidden": "includeHidden", + } { + v, _ := cmd.Flags().GetBool(flag) + params[key] = v + } + return runSheetTool(cmd, runner, "replace_all", params) + }) + addSheetBaseFlags(cmd) + cmd.Flags().String("find", "", "查找文本 (必填)") + cmd.Flags().String("replacement", "", "替换文本 (必填,可为空)") + cmd.Flags().String("range", "", "替换范围,A1 表示法") + cmd.Flags().Bool("match-case", false, "区分大小写") + cmd.Flags().Bool("match-entire-cell", false, "完整单元格匹配") + cmd.Flags().Bool("use-regexp", false, "启用正则表达式匹配") + cmd.Flags().Bool("include-hidden", false, "包含隐藏行/列") + return cmd +} + +func newSheetFilterGetCommand(runner executor.Runner) *cobra.Command { + return newSheetBaseToolCommand(runner, "get", "获取全局筛选信息", "get_filter") +} + +func newSheetFilterDeleteCommand(runner executor.Runner) *cobra.Command { + return newSheetBaseToolCommand(runner, "delete", "删除全局筛选", "delete_filter") +} + +func newSheetFilterCreateCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("create", "创建全局筛选", func(cmd *cobra.Command, _ []string) error { + if err := sheetValidateRequired(cmd, "node", "sheet-id", "range"); err != nil { + return err + } + params := map[string]any{ + "nodeId": sheetStringFlag(cmd, "node"), + "sheetId": sheetStringFlag(cmd, "sheet-id"), + "range": sheetStringFlag(cmd, "range"), + } + if v := sheetStringFlag(cmd, "criteria"); v != "" { + var criteria []any + if err := json.Unmarshal([]byte(v), &criteria); err != nil { + return fmt.Errorf("--criteria JSON parse failed: %w", err) + } + params["criteria"] = criteria + } + return runSheetTool(cmd, runner, "create_filter", params) + }) + addSheetBaseFlags(cmd) + cmd.Flags().String("range", "", "筛选范围,A1 表示法 (必填)") + cmd.Flags().String("criteria", "", "筛选条件 JSON 数组") + return cmd +} + +func newSheetFilterUpdateCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("update", "批量更新筛选条件", func(cmd *cobra.Command, _ []string) error { + if err := sheetValidateRequired(cmd, "node", "sheet-id", "criteria"); err != nil { + return err + } + var criteria []any + if err := sheetParseJSONFlag(cmd, "criteria", &criteria); err != nil { + return err + } + return runSheetTool(cmd, runner, "update_filter", map[string]any{ + "nodeId": sheetStringFlag(cmd, "node"), + "sheetId": sheetStringFlag(cmd, "sheet-id"), + "criteria": criteria, + }) + }) + addSheetBaseFlags(cmd) + cmd.Flags().String("criteria", "", "筛选条件 JSON 数组 (必填)") + return cmd +} + +func newSheetFilterClearCriteriaCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetColumnCommand(runner, "clear-criteria", "清除单列筛选条件", "clear_filter_criteria") + return cmd +} + +func newSheetFilterSortCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("sort", "筛选排序", func(cmd *cobra.Command, _ []string) error { + params, err := sheetBaseParams(cmd) + if err != nil { + return err + } + column, _ := cmd.Flags().GetInt("column") + ascending, _ := cmd.Flags().GetBool("ascending") + params["field"] = map[string]any{"column": column, "ascending": ascending} + return runSheetTool(cmd, runner, "sort_filter", params) + }) + addSheetBaseFlags(cmd) + cmd.Flags().Int("column", 0, "排序列偏移量,从 0 开始") + cmd.Flags().Bool("ascending", true, "是否升序") + return cmd +} + +func newSheetFilterViewListCommand(runner executor.Runner) *cobra.Command { + return newSheetBaseToolCommand(runner, "list", "获取所有筛选视图", "get_filter_views") +} + +func newSheetFilterViewCreateCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("create", "创建筛选视图", func(cmd *cobra.Command, _ []string) error { + if err := sheetValidateRequired(cmd, "node", "sheet-id", "name", "range"); err != nil { + return err + } + params := map[string]any{ + "nodeId": sheetStringFlag(cmd, "node"), + "sheetId": sheetStringFlag(cmd, "sheet-id"), + "name": sheetStringFlag(cmd, "name"), + "range": sheetStringFlag(cmd, "range"), + } + if v := sheetStringFlag(cmd, "criteria"); v != "" { + var criteria []any + if err := json.Unmarshal([]byte(v), &criteria); err != nil { + return fmt.Errorf("--criteria JSON parse failed: %w", err) + } + params["criteria"] = criteria + } + return runSheetTool(cmd, runner, "create_filter_view", params) + }) + addSheetBaseFlags(cmd) + cmd.Flags().String("name", "", "筛选视图名称 (必填)") + cmd.Flags().String("range", "", "筛选视图范围,A1 表示法 (必填)") + cmd.Flags().String("criteria", "", "筛选条件 JSON 数组") + return cmd +} + +func newSheetFilterViewUpdateCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("update", "更新筛选视图属性", func(cmd *cobra.Command, _ []string) error { + if err := sheetValidateRequired(cmd, "node", "sheet-id", "filter-view-id"); err != nil { + return err + } + params := sheetFilterViewBaseParams(cmd) + changed := false + if cmd.Flags().Changed("name") { + params["name"] = sheetStringFlag(cmd, "name") + changed = true + } + if cmd.Flags().Changed("range") { + params["range"] = sheetStringFlag(cmd, "range") + changed = true + } + if cmd.Flags().Changed("criteria") { + var criteria []any + if err := sheetParseJSONFlag(cmd, "criteria", &criteria); err != nil { + return err + } + params["criteria"] = criteria + changed = true + } + if !changed { + return apperrors.NewValidation("at least one of --name, --range or --criteria is required") + } + return runSheetTool(cmd, runner, "update_filter_view", params) + }) + addSheetFilterViewBaseFlags(cmd) + cmd.Flags().String("name", "", "筛选视图新名称") + cmd.Flags().String("range", "", "筛选视图新范围") + cmd.Flags().String("criteria", "", "筛选条件 JSON 数组") + return cmd +} + +func newSheetFilterViewDeleteCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("delete", "删除筛选视图", func(cmd *cobra.Command, _ []string) error { + if err := sheetValidateRequired(cmd, "node", "sheet-id", "filter-view-id"); err != nil { + return err + } + return runSheetTool(cmd, runner, "delete_filter_view", sheetFilterViewBaseParams(cmd)) + }) + addSheetFilterViewBaseFlags(cmd) + return cmd +} + +func newSheetFilterViewUpdateCriteriaCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("update-criteria", "更新筛选视图列条件", func(cmd *cobra.Command, _ []string) error { + if err := sheetValidateRequired(cmd, "node", "sheet-id", "filter-view-id", "filter-criteria"); err != nil { + return err + } + params := sheetFilterViewBaseParams(cmd) + column, _ := cmd.Flags().GetInt("column") + if column < 0 { + return apperrors.NewValidation("--column must be >= 0") + } + var filterCriteria map[string]any + if err := sheetParseJSONFlag(cmd, "filter-criteria", &filterCriteria); err != nil { + return err + } + params["column"] = column + params["filterCriteria"] = filterCriteria + return runSheetTool(cmd, runner, "set_filter_view_criteria", params) + }) + addSheetFilterViewBaseFlags(cmd) + cmd.Flags().Int("column", 0, "列偏移量,从 0 开始") + cmd.Flags().String("filter-criteria", "", "筛选条件 JSON 对象 (必填)") + return cmd +} + +func newSheetFilterViewDeleteCriteriaCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetColumnCommand(runner, "delete-criteria", "删除筛选视图列条件", "clear_filter_view_criteria") + cmd.Flags().String("filter-view-id", "", "筛选视图 ID (必填)") + return cmd +} + +func newSheetFilterViewInfoCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetFilterViewReadCommand(runner, "info", "获取单个筛选视图详情", "info") + return cmd +} + +func newSheetFilterViewListCriteriaCommand(runner executor.Runner) *cobra.Command { + return newSheetFilterViewReadCommand(runner, "list-criteria", "列出筛选视图所有列条件", "list-criteria") +} + +func newSheetFilterViewGetCriteriaCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetFilterViewReadCommand(runner, "get-criteria", "获取单列筛选条件", "get-criteria") + cmd.Flags().Int("column", 0, "列偏移量,从 0 开始") + return cmd +} + +func newSheetFilterViewReadCommand(runner executor.Runner, use, short, mode string) *cobra.Command { + cmd := newSheetLeaf(use, short, func(cmd *cobra.Command, _ []string) error { + if err := sheetValidateRequired(cmd, "node", "sheet-id", "filter-view-id"); err != nil { + return err + } + result, err := sheetInvocationResult(cmd, runner, "sheet", "get_filter_views", map[string]any{ + "nodeId": sheetStringFlag(cmd, "node"), + "sheetId": sheetStringFlag(cmd, "sheet-id"), + }) + if err != nil { + return err + } + filterViews := sheetResultFilterViews(result.Response) + if len(filterViews) == 0 { + return writeCommandPayload(cmd, result) + } + view, err := sheetFindFilterView(filterViews, sheetStringFlag(cmd, "filter-view-id")) + if err != nil { + return err + } + switch mode { + case "info": + return writeCommandPayload(cmd, view) + case "list-criteria": + if criteria, ok := view["criteria"]; ok { + return writeCommandPayload(cmd, criteria) + } + return writeCommandPayload(cmd, map[string]any{}) + case "get-criteria": + column, _ := cmd.Flags().GetInt("column") + if column < 0 { + return apperrors.NewValidation("--column must be >= 0") + } + criteria, _ := view["criteria"].(map[string]any) + if criteria == nil { + return apperrors.NewValidation("filter view has no criteria") + } + item, ok := criteria[strconv.Itoa(column)] + if !ok { + return apperrors.NewValidation("filter view column criteria not found") + } + return writeCommandPayload(cmd, item) + default: + return writeCommandPayload(cmd, result) + } + }) + addSheetFilterViewBaseFlags(cmd) + return cmd +} + +func newSheetCondFormatListCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("list", "获取条件格式规则", func(cmd *cobra.Command, _ []string) error { + params, err := sheetBaseParams(cmd) + if err != nil { + return err + } + sheetAddStringParam(cmd, params, "ruleId", "rule-id") + return runSheetTool(cmd, runner, "get_cond_format", params) + }) + addSheetBaseFlags(cmd) + cmd.Flags().String("rule-id", "", "条件格式规则 ID") + return cmd +} + +func newSheetCondFormatCreateCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("create", "创建条件格式规则", func(cmd *cobra.Command, _ []string) error { + params, err := sheetCondFormatMutationBase(cmd, false) + if err != nil { + return err + } + return runSheetTool(cmd, runner, "create_cond_format", params) + }) + addSheetBaseFlags(cmd) + cmd.Flags().String("ranges", "", "应用范围 JSON 数组 (必填)") + cmd.Flags().String("condition", "", "条件类型及参数 JSON 对象 (必填)") + cmd.Flags().String("cell-style", "", "单元格样式 JSON 对象") + cmd.Flags().String("data-bar-style", "", "数据条样式 JSON 对象") + return cmd +} + +func newSheetCondFormatUpdateCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("update", "更新条件格式规则", func(cmd *cobra.Command, _ []string) error { + params, err := sheetCondFormatMutationBase(cmd, true) + if err != nil { + return err + } + if len(params) <= 3 { + return apperrors.NewValidation("at least one of --ranges, --condition, --cell-style or --data-bar-style is required") + } + return runSheetTool(cmd, runner, "update_cond_format", params) + }) + addSheetBaseFlags(cmd) + cmd.Flags().String("rule-id", "", "条件格式规则 ID (必填)") + cmd.Flags().String("ranges", "", "应用范围 JSON 数组") + cmd.Flags().String("condition", "", "条件类型及参数 JSON 对象") + cmd.Flags().String("cell-style", "", "单元格样式 JSON 对象") + cmd.Flags().String("data-bar-style", "", "数据条样式 JSON 对象") + return cmd +} + +func newSheetCondFormatDeleteCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("delete", "删除条件格式规则", func(cmd *cobra.Command, _ []string) error { + if ok, _ := cmd.Flags().GetBool("yes"); !ok && !commandDryRun(cmd) { + return apperrors.NewValidation("delete cond-format requires --yes") + } + params, err := sheetBaseParams(cmd) + if err != nil { + return err + } + ruleID, err := sheetRequiredFlag(cmd, "rule-id") + if err != nil { + return err + } + params["ruleId"] = ruleID + return runSheetTool(cmd, runner, "delete_cond_format", params) + }) + addSheetBaseFlags(cmd) + cmd.Flags().String("rule-id", "", "条件格式规则 ID (必填)") + cmd.Flags().Bool("yes", false, "确认删除") + return cmd +} + +func newSheetCreateFloatImageCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("create-float-image", "创建浮动图片", func(cmd *cobra.Command, _ []string) error { + if err := sheetValidateRequired(cmd, "node", "sheet-id", "src", "range"); err != nil { + return err + } + width, height, err := sheetPositiveSize(cmd, true) + if err != nil { + return err + } + params := map[string]any{ + "nodeId": sheetStringFlag(cmd, "node"), + "sheetId": sheetStringFlag(cmd, "sheet-id"), + "src": sheetStringFlag(cmd, "src"), + "range": sheetStringFlag(cmd, "range"), + "width": width, + "height": height, + } + sheetAddChangedIntParam(cmd, params, "offsetX", "offset-x") + sheetAddChangedIntParam(cmd, params, "offsetY", "offset-y") + return runSheetTool(cmd, runner, "create_float_image", params) + }) + addSheetBaseFlags(cmd) + cmd.Flags().String("src", "", "图片资源路径 (必填)") + cmd.Flags().String("range", "", "锚点单元格 (必填)") + cmd.Flags().Int("width", 0, "图片宽度,像素 (必填)") + cmd.Flags().Int("height", 0, "图片高度,像素 (必填)") + cmd.Flags().Int("offset-x", 0, "水平偏移量") + cmd.Flags().Int("offset-y", 0, "垂直偏移量") + return cmd +} + +func newSheetGetFloatImageCommand(runner executor.Runner) *cobra.Command { + return newSheetFloatImageIDCommand(runner, "get-float-image", "获取浮动图片详情", "get_float_image") +} + +func newSheetListFloatImagesCommand(runner executor.Runner) *cobra.Command { + return newSheetBaseToolCommand(runner, "list-float-images", "列出工作表所有浮动图片", "list_float_images") +} + +func newSheetUpdateFloatImageCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("update-float-image", "更新浮动图片属性", func(cmd *cobra.Command, _ []string) error { + params, err := sheetFloatImageBaseParams(cmd) + if err != nil { + return err + } + changed := false + for flag, key := range map[string]string{"src": "src", "range": "range"} { + if cmd.Flags().Changed(flag) { + params[key] = sheetStringFlag(cmd, flag) + changed = true + } + } + for flag, key := range map[string]string{"width": "width", "height": "height", "offset-x": "offsetX", "offset-y": "offsetY"} { + if cmd.Flags().Changed(flag) { + v, _ := cmd.Flags().GetInt(flag) + if (flag == "width" || flag == "height") && v <= 0 { + return apperrors.NewValidation("--" + flag + " must be > 0") + } + if (flag == "offset-x" || flag == "offset-y") && v < 0 { + return apperrors.NewValidation("--" + flag + " must be >= 0") + } + params[key] = v + changed = true + } + } + if !changed { + return apperrors.NewValidation("at least one float image field is required") + } + return runSheetTool(cmd, runner, "update_float_image", params) + }) + addSheetFloatImageBaseFlags(cmd) + cmd.Flags().String("src", "", "新的图片资源路径") + cmd.Flags().String("range", "", "新的锚点单元格") + cmd.Flags().Int("width", 0, "新的图片宽度") + cmd.Flags().Int("height", 0, "新的图片高度") + cmd.Flags().Int("offset-x", 0, "新的水平偏移量") + cmd.Flags().Int("offset-y", 0, "新的垂直偏移量") + return cmd +} + +func newSheetDeleteFloatImageCommand(runner executor.Runner) *cobra.Command { + return newSheetFloatImageIDCommand(runner, "delete-float-image", "删除浮动图片", "delete_float_image") +} + +func newSheetMediaUploadCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("media-upload", "上传附件到表格", func(cmd *cobra.Command, _ []string) error { + return runSheetAttachmentUpload(cmd, runner, false) + }) + addSheetNodeFlags(cmd) + cmd.Flags().String("file", "", "本地文件路径 (必填)") + cmd.Flags().String("name", "", "附件显示名称") + cmd.Flags().String("mime-type", "", "文件 MIME 类型") + return cmd +} + +func newSheetWriteImageCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("write-image", "上传图片并写入表格单元格", func(cmd *cobra.Command, _ []string) error { + return runSheetAttachmentUpload(cmd, runner, true) + }) + addSheetBaseFlags(cmd) + cmd.Flags().String("range", "", "目标单元格区域地址 (必填)") + cmd.Flags().String("file", "", "本地图片文件路径 (必填)") + cmd.Flags().String("name", "", "图片显示名称") + cmd.Flags().String("mime-type", "", "文件 MIME 类型") + cmd.Flags().Int("width", 0, "图片显示宽度") + cmd.Flags().Int("height", 0, "图片显示高度") + return cmd +} + +func newSheetExportCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("export", "导出表格为 xlsx", func(cmd *cobra.Command, _ []string) error { + node, err := sheetRequiredFlag(cmd, "node") + if err != nil { + return err + } + outputPath := sheetStringFlag(cmd, "output") + if commandDryRun(cmd) { + return runSheetTool(cmd, runner, "submit_export_job", map[string]any{"nodeId": node, "exportFormat": "xlsx"}) + } + submit, err := sheetInvocationResult(cmd, runner, "sheet", "submit_export_job", map[string]any{"nodeId": node, "exportFormat": "xlsx"}) + if err != nil { + return err + } + jobID := sheetStringFromResponse(submit.Response, "jobId") + if jobID == "" { + return writeCommandPayload(cmd, submit) + } + downloadURL, status, err := pollSheetExport(cmd, runner, jobID) + if err != nil { + return err + } + out := map[string]any{"jobId": jobID, "status": status} + if downloadURL != "" { + out["downloadUrl"] = downloadURL + } + if outputPath != "" && downloadURL != "" { + if fi, statErr := os.Stat(outputPath); statErr == nil && fi.IsDir() { + outputPath = filepath.Join(outputPath, sheetExportFilename(downloadURL, jobID)) + } + if err := sheetHTTPGet(cmd, downloadURL, outputPath); err != nil { + return err + } + out["output"] = outputPath + } + return writeCommandPayload(cmd, out) + }) + addSheetNodeFlags(cmd) + cmd.Flags().String("output", "", "本地保存路径") + return cmd +} + +func newSheetRangeToolCommand(runner executor.Runner, use, short, tool, rangeFlag string) *cobra.Command { + cmd := newSheetLeaf(use, short, func(cmd *cobra.Command, _ []string) error { + if err := sheetValidateRequired(cmd, "node", "sheet-id", rangeFlag); err != nil { + return err + } + params := map[string]any{ + "nodeId": sheetStringFlag(cmd, "node"), + "sheetId": sheetStringFlag(cmd, "sheet-id"), + "range": sheetStringFlag(cmd, rangeFlag), + } + return runSheetTool(cmd, runner, tool, params) + }) + addSheetBaseFlags(cmd) + cmd.Flags().String(rangeFlag, "", "范围,A1 表示法 (必填)") + return cmd +} + +func newSheetBaseToolCommand(runner executor.Runner, use, short, tool string) *cobra.Command { + cmd := newSheetLeaf(use, short, func(cmd *cobra.Command, _ []string) error { + params, err := sheetBaseParams(cmd) + if err != nil { + return err + } + return runSheetTool(cmd, runner, tool, params) + }) + addSheetBaseFlags(cmd) + return cmd +} + +func newSheetColumnCommand(runner executor.Runner, use, short, tool string) *cobra.Command { + cmd := newSheetLeaf(use, short, func(cmd *cobra.Command, _ []string) error { + params, err := sheetBaseParams(cmd) + if err != nil { + return err + } + if strings.Contains(use, "criteria") && strings.Contains(tool, "filter_view") { + filterViewID, err := sheetRequiredFlag(cmd, "filter-view-id") + if err != nil { + return err + } + params["filterViewId"] = filterViewID + } + column, _ := cmd.Flags().GetInt("column") + if column < 0 { + return apperrors.NewValidation("--column must be >= 0") + } + params["column"] = column + return runSheetTool(cmd, runner, tool, params) + }) + addSheetBaseFlags(cmd) + cmd.Flags().Int("column", 0, "列偏移量,从 0 开始") + return cmd +} + +func newSheetFloatImageIDCommand(runner executor.Runner, use, short, tool string) *cobra.Command { + cmd := newSheetLeaf(use, short, func(cmd *cobra.Command, _ []string) error { + params, err := sheetFloatImageBaseParams(cmd) + if err != nil { + return err + } + return runSheetTool(cmd, runner, tool, params) + }) + addSheetFloatImageBaseFlags(cmd) + return cmd +} + +func newSheetGroup(use, short string) *cobra.Command { + cmd := &cobra.Command{ + Use: use, + Short: short, + Args: cobra.NoArgs, + TraverseChildren: true, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + return cmd.Help() + }, + } + preferLegacyLeaf(cmd) + return cmd +} + +func newSheetLeaf(use, short string, run func(*cobra.Command, []string) error) *cobra.Command { + cmd := &cobra.Command{ + Use: use, + Short: short, + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: run, + } + preferLegacyLeaf(cmd) + return cmd +} + +func runSheetTool(cmd *cobra.Command, runner executor.Runner, tool string, params map[string]any) error { + result, err := sheetInvocationResult(cmd, runner, "sheet", tool, params) + if err != nil { + return err + } + return writeCommandPayload(cmd, result) +} + +func sheetInvocationResult(cmd *cobra.Command, runner executor.Runner, product, tool string, params map[string]any) (executor.Result, error) { + invocation := executor.NewHelperInvocation(cobracmd.LegacyCommandPath(cmd), product, tool, params) + invocation.DryRun = commandDryRun(cmd) + return runner.Run(cmd.Context(), invocation) +} + +func addSheetNodeFlags(cmd *cobra.Command) { + cmd.Flags().String("node", "", "表格文档 ID 或 URL") + addSheetHiddenStringFlag(cmd, "url", "--node alias") + addSheetHiddenStringFlag(cmd, "id", "--node alias") + addSheetHiddenStringFlag(cmd, "node-id", "--node alias") + addSheetHiddenStringFlag(cmd, "doc-id", "--node alias") + addSheetHiddenStringFlag(cmd, "file-id", "--node alias") +} + +func addSheetBaseFlags(cmd *cobra.Command) { + addSheetNodeFlags(cmd) + cmd.Flags().String("sheet-id", "", "工作表 ID 或名称") +} + +func addSheetFilterViewBaseFlags(cmd *cobra.Command) { + addSheetBaseFlags(cmd) + cmd.Flags().String("filter-view-id", "", "筛选视图 ID") +} + +func addSheetFloatImageBaseFlags(cmd *cobra.Command) { + addSheetBaseFlags(cmd) + cmd.Flags().String("float-image-id", "", "浮动图片 ID") +} + +func addSheetRangeTransferFlags(cmd *cobra.Command) { + addSheetBaseFlags(cmd) + cmd.Flags().String("source-range", "", "源范围,A1 表示法 (必填)") + cmd.Flags().String("target-range", "", "目标位置,A1 表示法 (必填)") + cmd.Flags().String("target-sheet-id", "", "目标工作表 ID 或名称") +} + +func addSheetHiddenStringFlag(cmd *cobra.Command, name, usage string) { + cmd.Flags().String(name, "", usage) + _ = cmd.Flags().MarkHidden(name) +} + +func sheetBaseParams(cmd *cobra.Command) (map[string]any, error) { + if err := sheetValidateRequired(cmd, "node", "sheet-id"); err != nil { + return nil, err + } + return map[string]any{ + "nodeId": sheetStringFlag(cmd, "node"), + "sheetId": sheetStringFlag(cmd, "sheet-id"), + }, nil +} + +func sheetFilterViewBaseParams(cmd *cobra.Command) map[string]any { + return map[string]any{ + "nodeId": sheetStringFlag(cmd, "node"), + "sheetId": sheetStringFlag(cmd, "sheet-id"), + "filterViewId": sheetStringFlag(cmd, "filter-view-id"), + } +} + +func sheetFloatImageBaseParams(cmd *cobra.Command) (map[string]any, error) { + if err := sheetValidateRequired(cmd, "node", "sheet-id", "float-image-id"); err != nil { + return nil, err + } + return map[string]any{ + "nodeId": sheetStringFlag(cmd, "node"), + "sheetId": sheetStringFlag(cmd, "sheet-id"), + "floatImageId": sheetStringFlag(cmd, "float-image-id"), + }, nil +} + +func sheetRangeAddressParams(cmd *cobra.Command) (map[string]any, error) { + if err := sheetValidateRequired(cmd, "node", "sheet-id", "range"); err != nil { + return nil, err + } + return map[string]any{ + "nodeId": sheetStringFlag(cmd, "node"), + "sheetId": sheetStringFlag(cmd, "sheet-id"), + "rangeAddress": sheetStringFlag(cmd, "range"), + }, nil +} + +func sheetRangeTransferParams(cmd *cobra.Command) (map[string]any, error) { + if err := sheetValidateRequired(cmd, "node", "sheet-id", "source-range", "target-range"); err != nil { + return nil, err + } + params := map[string]any{ + "nodeId": sheetStringFlag(cmd, "node"), + "sheetId": sheetStringFlag(cmd, "sheet-id"), + "sourceRange": sheetStringFlag(cmd, "source-range"), + "destinationRange": sheetStringFlag(cmd, "target-range"), + } + sheetAddStringParam(cmd, params, "targetSheetId", "target-sheet-id") + return params, nil +} + +func sheetNodeAndName(cmd *cobra.Command) (string, string, error) { + node, err := sheetRequiredFlag(cmd, "node") + if err != nil { + return "", "", err + } + name, err := sheetRequiredFlag(cmd, "name") + if err != nil { + return "", "", err + } + return node, name, nil +} + +func sheetNameFlag(cmd *cobra.Command) string { + if v := sheetStringFlag(cmd, "name"); v != "" { + return v + } + return sheetStringFlag(cmd, "title") +} + +func sheetStringFlag(cmd *cobra.Command, name string) string { + if v, _ := cmd.Flags().GetString(name); v != "" { + return v + } + if name == "node" { + for _, alias := range []string{"url", "id", "node-id", "doc-id", "file-id"} { + if flag := cmd.Flags().Lookup(alias); flag == nil { + continue + } + if v, _ := cmd.Flags().GetString(alias); v != "" { + return v + } + } + } + return "" +} + +func sheetFlagOrFallback(cmd *cobra.Command, primary string, aliases ...string) string { + for _, name := range append([]string{primary}, aliases...) { + if flag := cmd.Flags().Lookup(name); flag == nil { + continue + } + if v := sheetStringFlag(cmd, name); v != "" { + return v + } + } + return "" +} + +func sheetRequiredFlag(cmd *cobra.Command, name string) (string, error) { + if v := sheetStringFlag(cmd, name); v != "" { + return v, nil + } + return "", apperrors.NewValidation("--" + name + " is required") +} + +func sheetRequiredFlagOrFallback(cmd *cobra.Command, primary string, aliases ...string) (string, error) { + if v := sheetFlagOrFallback(cmd, primary, aliases...); v != "" { + return v, nil + } + return "", apperrors.NewValidation("--" + primary + " is required") +} + +func sheetValidateRequired(cmd *cobra.Command, names ...string) error { + for _, name := range names { + if sheetStringFlag(cmd, name) == "" { + return apperrors.NewValidation("--" + name + " is required") + } + } + return nil +} + +func sheetAddStringParam(cmd *cobra.Command, params map[string]any, key string, flags ...string) { + if v := sheetFlagOrFallback(cmd, flags[0], flags[1:]...); v != "" { + params[key] = v + } +} + +func sheetAddChangedIntParam(cmd *cobra.Command, params map[string]any, key, flag string) { + if cmd.Flags().Changed(flag) { + v, _ := cmd.Flags().GetInt(flag) + params[key] = v + } +} + +func sheetParseJSONFlag(cmd *cobra.Command, flag string, out any) error { + raw := sheetStringFlag(cmd, flag) + if err := json.Unmarshal([]byte(raw), out); err != nil { + return fmt.Errorf("--%s JSON parse failed: %w", flag, err) + } + return nil +} + +func sheetDimension(cmd *cobra.Command) (string, error) { + dimension := sheetStringFlag(cmd, "dimension") + switch dimension { + case "ROW": + dimension = "ROWS" + case "COLUMN": + dimension = "COLUMNS" + } + if dimension != "ROWS" && dimension != "COLUMNS" { + return "", apperrors.NewValidation("--dimension must be ROWS or COLUMNS") + } + return dimension, nil +} + +func sheetPositiveIntStringFlag(cmd *cobra.Command, flag string, max int) (int, error) { + raw := sheetStringFlag(cmd, flag) + n, err := strconv.Atoi(raw) + if err != nil || n < 1 { + return 0, apperrors.NewValidation("--" + flag + " must be a positive integer") + } + if max > 0 && n > max { + return 0, apperrors.NewValidation(fmt.Sprintf("--%s must be <= %d", flag, max)) + } + return n, nil +} + +func sheetPositiveSize(cmd *cobra.Command, required bool) (int, int, error) { + width, _ := cmd.Flags().GetInt("width") + height, _ := cmd.Flags().GetInt("height") + if required || cmd.Flags().Changed("width") { + if width <= 0 { + return 0, 0, apperrors.NewValidation("--width must be > 0") + } + } + if required || cmd.Flags().Changed("height") { + if height <= 0 { + return 0, 0, apperrors.NewValidation("--height must be > 0") + } + } + return width, height, nil +} + +func sheetCondFormatMutationBase(cmd *cobra.Command, update bool) (map[string]any, error) { + params, err := sheetBaseParams(cmd) + if err != nil { + return nil, err + } + if update { + ruleID, err := sheetRequiredFlag(cmd, "rule-id") + if err != nil { + return nil, err + } + params["ruleId"] = ruleID + } + if cmd.Flags().Changed("ranges") || !update { + var ranges []string + if err := sheetParseJSONFlag(cmd, "ranges", &ranges); err != nil { + return nil, err + } + if len(ranges) == 0 { + return nil, apperrors.NewValidation("--ranges must contain at least one range") + } + params["ranges"] = ranges + } + if cmd.Flags().Changed("condition") || !update { + var condition map[string]any + if err := sheetParseJSONFlag(cmd, "condition", &condition); err != nil { + return nil, err + } + for k, v := range condition { + params[k] = v + } + } + if cmd.Flags().Changed("cell-style") { + var cellStyle map[string]any + if err := sheetParseJSONFlag(cmd, "cell-style", &cellStyle); err != nil { + return nil, err + } + params["cellStyle"] = cellStyle + } + if cmd.Flags().Changed("data-bar-style") { + var dataBarStyle map[string]any + if err := sheetParseJSONFlag(cmd, "data-bar-style", &dataBarStyle); err != nil { + return nil, err + } + params["dataBarStyle"] = dataBarStyle + } + return params, nil +} + +func runSheetAttachmentUpload(cmd *cobra.Command, runner executor.Runner, writeImage bool) error { + node, err := sheetRequiredFlagOrFallback(cmd, "node", "url", "id", "node-id", "doc-id", "file-id") + if err != nil { + return err + } + filePath, err := sheetRequiredFlag(cmd, "file") + if err != nil { + return err + } + if writeImage { + if err := sheetValidateRequired(cmd, "sheet-id", "range"); err != nil { + return err + } + } + info, err := os.Stat(filePath) + if err != nil { + return fmt.Errorf("cannot read file %s: %w", filePath, err) + } + if info.IsDir() { + return apperrors.NewValidation(filePath + " is a directory") + } + fileName := sheetStringFlag(cmd, "name") + if fileName == "" { + fileName = filepath.Base(filePath) + } else if filepath.Ext(fileName) == "" { + if ext := filepath.Ext(filePath); ext != "" { + fileName += ext + } + } + mimeType := sheetStringFlag(cmd, "mime-type") + if mimeType == "" { + mimeType = detectMIME(fileName) + } + params := map[string]any{ + "nodeId": node, + "fileName": fileName, + "fileSize": float64(info.Size()), + "mimeType": mimeType, + } + if commandDryRun(cmd) { + return runSheetDocTool(cmd, runner, "get_doc_attachment_upload_info", params) + } + credResult, err := sheetInvocationResult(cmd, runner, "doc", "get_doc_attachment_upload_info", params) + if err != nil { + return err + } + uploadURL, resourceID, resourceURL, err := extractDocAttachmentUploadInfo(credResult.Response) + if err != nil { + return err + } + if err := sheetHTTPPut(cmd, uploadURL, filePath, info.Size(), mimeType); err != nil { + return err + } + if !writeImage { + return writeCommandPayload(cmd, map[string]any{ + "resourceId": resourceID, + "resourceUrl": resourceURL, + "fileName": fileName, + "mimeType": mimeType, + "fileSize": info.Size(), + }) + } + writeArgs := map[string]any{ + "nodeId": node, + "sheetId": sheetStringFlag(cmd, "sheet-id"), + "rangeAddress": sheetStringFlag(cmd, "range"), + "resourceId": resourceID, + "resourceUrl": resourceURL, + } + sheetAddChangedIntParam(cmd, writeArgs, "width", "width") + sheetAddChangedIntParam(cmd, writeArgs, "height", "height") + return runSheetTool(cmd, runner, "write_image", writeArgs) +} + +func runSheetDocTool(cmd *cobra.Command, runner executor.Runner, tool string, params map[string]any) error { + result, err := sheetInvocationResult(cmd, runner, "doc", tool, params) + if err != nil { + return err + } + return writeCommandPayload(cmd, result) +} + +func sheetHTTPPut(cmd *cobra.Command, uploadURL, filePath string, size int64, mimeType string) error { + f, err := os.Open(filePath) + if err != nil { + return fmt.Errorf("open file failed: %w", err) + } + defer f.Close() + req, err := http.NewRequestWithContext(cmd.Context(), http.MethodPut, uploadURL, f) + if err != nil { + return fmt.Errorf("build upload request failed: %w", err) + } + req.ContentLength = size + if mimeType != "" { + req.Header.Set("Content-Type", mimeType) + } + resp, err := (&http.Client{Timeout: 5 * time.Minute}).Do(req) + if err != nil { + return fmt.Errorf("OSS upload failed: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 512)) + return fmt.Errorf("OSS upload failed HTTP %d: %s", resp.StatusCode, string(body)) + } + return nil +} + +func sheetHTTPGet(cmd *cobra.Command, rawURL, outputPath string) error { + req, err := http.NewRequestWithContext(cmd.Context(), http.MethodGet, rawURL, nil) + if err != nil { + return err + } + resp, err := (&http.Client{Timeout: 10 * time.Minute}).Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 512)) + return fmt.Errorf("download failed HTTP %d: %s", resp.StatusCode, string(body)) + } + if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil { + return err + } + f, err := os.Create(outputPath) + if err != nil { + return err + } + defer f.Close() + _, err = io.Copy(f, resp.Body) + return err +} + +func pollSheetExport(cmd *cobra.Command, runner executor.Runner, jobID string) (downloadURL, status string, err error) { + intervals := make([]time.Duration, 0, 30) + for i := 0; i < 5; i++ { + intervals = append(intervals, 2*time.Second) + } + for i := 0; i < 5; i++ { + intervals = append(intervals, 5*time.Second) + } + for i := 0; i < 10; i++ { + intervals = append(intervals, 10*time.Second) + } + for i := 0; i < 10; i++ { + intervals = append(intervals, 15*time.Second) + } + for _, wait := range intervals { + timer := time.NewTimer(wait) + select { + case <-cmd.Context().Done(): + timer.Stop() + return "", "", cmd.Context().Err() + case <-timer.C: + } + result, err := sheetInvocationResult(cmd, runner, "sheet", "query_export_job", map[string]any{"jobId": jobID}) + if err != nil { + continue + } + status = strings.ToUpper(strings.TrimSpace(sheetStringFromResponse(result.Response, "status"))) + downloadURL = sheetStringFromResponse(result.Response, "downloadUrl") + switch status { + case "SUCCESS": + return downloadURL, status, nil + case "FAILED", "FAIL", "ERROR": + msg := sheetStringFromResponse(result.Response, "message") + if msg == "" { + msg = "export failed" + } + return "", status, apperrors.NewValidation(msg) + } + } + return "", status, apperrors.NewValidation("export timed out") +} + +func sheetStringFromResponse(resp map[string]any, key string) string { + data := sheetResponseData(resp) + if v, ok := data[key].(string); ok { + return v + } + return "" +} + +func sheetResponseData(resp map[string]any) map[string]any { + if resp == nil { + return map[string]any{} + } + data := resp + for { + next, ok := data["result"].(map[string]any) + if !ok { + next, ok = data["data"].(map[string]any) + } + if !ok { + next, ok = data["content"].(map[string]any) + } + if !ok || len(next) == 0 { + return data + } + data = next + } +} + +func sheetResultFilterViews(resp map[string]any) []map[string]any { + data := sheetResponseData(resp) + raw, _ := data["filterViews"].([]any) + out := make([]map[string]any, 0, len(raw)) + for _, item := range raw { + if m, ok := item.(map[string]any); ok { + out = append(out, m) + } + } + return out +} + +func sheetFindFilterView(views []map[string]any, id string) (map[string]any, error) { + for _, view := range views { + if v, _ := view["id"].(string); v == id { + return view, nil + } + if v, _ := view["filterViewId"].(string); v == id { + return view, nil + } + } + return nil, apperrors.NewValidation("filter view not found: " + id) +} + +func sheetExportFilename(downloadURL, jobID string) string { + if parsed, err := url.Parse(downloadURL); err == nil { + name := filepath.Base(parsed.Path) + if decoded, decodeErr := url.PathUnescape(name); decodeErr == nil && decoded != "" { + name = decoded + } + name = strings.ReplaceAll(name, "\\", "/") + name = filepath.Base(name) + if name != "" && name != "." && name != "/" { + return name + } + } + return "sheet-export-" + jobID + ".xlsx" +} + +type sheetStyleSpec struct { + BgColor string `json:"bgColor,omitempty"` + BgColorsJSON string `json:"bgColorsJson,omitempty"` + FontSize int `json:"fontSize,omitempty"` + FontSizesJSON string `json:"fontSizesJson,omitempty"` + HAlign string `json:"hAlign,omitempty"` + HAlignsJSON string `json:"hAlignsJson,omitempty"` + VAlign string `json:"vAlign,omitempty"` + VAlignsJSON string `json:"vAlignsJson,omitempty"` + FontColor string `json:"fontColor,omitempty"` + FontColorsJSON string `json:"fontColorsJson,omitempty"` + FontWeight string `json:"fontWeight,omitempty"` + FontWeightsJSON string `json:"fontWeightsJson,omitempty"` + WordWrap string `json:"wordWrap,omitempty"` + NumberFormat string `json:"numberFormat,omitempty"` +} + +type sheetBatchStyleItem struct { + SheetID string `json:"sheetId"` + Range string `json:"range"` + sheetStyleSpec +} + +func newSheetRangeSetStyleCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("set-style", "设置指定单元格区域的样式", func(cmd *cobra.Command, _ []string) error { + if err := sheetValidateRequired(cmd, "node", "sheet-id", "range"); err != nil { + return err + } + rows, cols, err := sheetParseA1Range(sheetStringFlag(cmd, "range")) + if err != nil { + return err + } + params := map[string]any{ + "nodeId": sheetStringFlag(cmd, "node"), + "sheetId": sheetStringFlag(cmd, "sheet-id"), + "rangeAddress": sheetStringFlag(cmd, "range"), + } + if err := sheetApplyStyleSpec(sheetReadStyleSpec(cmd), rows, cols, params); err != nil { + return err + } + return runSheetTool(cmd, runner, "update_range", params) + }) + addSheetBaseFlags(cmd) + cmd.Flags().String("range", "", "目标单元格区域地址 (必填)") + bindSheetStyleFlags(cmd) + return cmd +} + +func newSheetRangeBatchSetStyleCommand(runner executor.Runner) *cobra.Command { + cmd := newSheetLeaf("batch-set-style", "按配置文件批量设置样式", func(cmd *cobra.Command, _ []string) error { + if err := sheetValidateRequired(cmd, "node", "batch"); err != nil { + return err + } + data, err := os.ReadFile(sheetStringFlag(cmd, "batch")) + if err != nil { + return err + } + var items []sheetBatchStyleItem + if err := json.Unmarshal(data, &items); err != nil { + return err + } + continueOnErr, _ := cmd.Flags().GetBool("continue-on-error") + var outputs []executor.Result + var firstErr error + for _, item := range items { + if item.SheetID == "" || item.Range == "" { + firstErr = apperrors.NewValidation("batch item requires sheetId and range") + if !continueOnErr { + return firstErr + } + continue + } + rows, cols, err := sheetParseA1Range(item.Range) + if err != nil { + firstErr = err + if !continueOnErr { + return err + } + continue + } + params := map[string]any{"nodeId": sheetStringFlag(cmd, "node"), "sheetId": item.SheetID, "rangeAddress": item.Range} + if err := sheetApplyStyleSpec(&item.sheetStyleSpec, rows, cols, params); err != nil { + firstErr = err + if !continueOnErr { + return err + } + continue + } + result, err := sheetInvocationResult(cmd, runner, "sheet", "update_range", params) + if err != nil { + firstErr = err + if !continueOnErr { + return err + } + continue + } + outputs = append(outputs, result) + } + if firstErr != nil && !continueOnErr { + return firstErr + } + return writeCommandPayload(cmd, map[string]any{"count": len(outputs), "results": outputs}) + }) + addSheetNodeFlags(cmd) + cmd.Flags().String("batch", "", "批次配置 JSON 文件路径 (必填)") + cmd.Flags().Bool("continue-on-error", false, "遇到失败时继续执行") + return cmd +} + +func bindSheetStyleFlags(cmd *cobra.Command) { + cmd.Flags().String("bg-color", "", "背景色") + cmd.Flags().String("bg-colors-json", "", "背景色二维 JSON 数组") + cmd.Flags().Int("font-size", 0, "字号") + cmd.Flags().String("font-sizes-json", "", "字号二维 JSON 数组") + cmd.Flags().String("h-align", "", "水平对齐") + cmd.Flags().String("h-aligns-json", "", "水平对齐二维 JSON 数组") + cmd.Flags().String("v-align", "", "垂直对齐") + cmd.Flags().String("v-aligns-json", "", "垂直对齐二维 JSON 数组") + cmd.Flags().String("font-color", "", "字体颜色") + cmd.Flags().String("font-colors-json", "", "字体颜色二维 JSON 数组") + cmd.Flags().String("font-weight", "", "字体粗细") + cmd.Flags().String("font-weights-json", "", "字体粗细二维 JSON 数组") + cmd.Flags().String("word-wrap", "", "换行方式") + cmd.Flags().String("number-format", "", "数字格式 code") +} + +func sheetReadStyleSpec(cmd *cobra.Command) *sheetStyleSpec { + spec := &sheetStyleSpec{} + spec.BgColor, _ = cmd.Flags().GetString("bg-color") + spec.BgColorsJSON, _ = cmd.Flags().GetString("bg-colors-json") + spec.FontSize, _ = cmd.Flags().GetInt("font-size") + spec.FontSizesJSON, _ = cmd.Flags().GetString("font-sizes-json") + spec.HAlign, _ = cmd.Flags().GetString("h-align") + spec.HAlignsJSON, _ = cmd.Flags().GetString("h-aligns-json") + spec.VAlign, _ = cmd.Flags().GetString("v-align") + spec.VAlignsJSON, _ = cmd.Flags().GetString("v-aligns-json") + spec.FontColor, _ = cmd.Flags().GetString("font-color") + spec.FontColorsJSON, _ = cmd.Flags().GetString("font-colors-json") + spec.FontWeight, _ = cmd.Flags().GetString("font-weight") + spec.FontWeightsJSON, _ = cmd.Flags().GetString("font-weights-json") + spec.WordWrap, _ = cmd.Flags().GetString("word-wrap") + spec.NumberFormat, _ = cmd.Flags().GetString("number-format") + return spec +} + +func sheetApplyStyleSpec(spec *sheetStyleSpec, rows, cols int, params map[string]any) error { + if rows > 1000 || rows*cols > 30000 { + return apperrors.NewValidation("style range is too large") + } + if err := sheetApply2DString(spec.BgColor, spec.BgColorsJSON, rows, cols, "bg-color", "backgroundColors", nil, params); err != nil { + return err + } + if err := sheetApplyFontSize(spec, rows, cols, params); err != nil { + return err + } + if err := sheetApply2DString(spec.HAlign, spec.HAlignsJSON, rows, cols, "h-align", "horizontalAlignments", map[string]bool{"left": true, "center": true, "right": true, "general": true}, params); err != nil { + return err + } + if err := sheetApply2DString(spec.VAlign, spec.VAlignsJSON, rows, cols, "v-align", "verticalAlignments", map[string]bool{"top": true, "middle": true, "bottom": true}, params); err != nil { + return err + } + if err := sheetApply2DString(spec.FontColor, spec.FontColorsJSON, rows, cols, "font-color", "fontColors", nil, params); err != nil { + return err + } + if err := sheetApply2DString(spec.FontWeight, spec.FontWeightsJSON, rows, cols, "font-weight", "fontWeights", map[string]bool{"bold": true, "normal": true}, params); err != nil { + return err + } + if spec.WordWrap != "" { + if !map[string]bool{"overflow": true, "clip": true, "autoWrap": true}[spec.WordWrap] { + return apperrors.NewValidation("--word-wrap has invalid value") + } + params["wordWrap"] = spec.WordWrap + } + if spec.NumberFormat != "" { + params["numberFormat"] = spec.NumberFormat + } + for _, key := range []string{"backgroundColors", "fontSizes", "horizontalAlignments", "verticalAlignments", "fontColors", "fontWeights", "wordWrap", "numberFormat"} { + if _, ok := params[key]; ok { + return nil + } + } + return apperrors.NewValidation("at least one style flag is required") +} + +func sheetApplyFontSize(spec *sheetStyleSpec, rows, cols int, params map[string]any) error { + if spec.FontSize != 0 && spec.FontSizesJSON != "" { + return apperrors.NewValidation("--font-size and --font-sizes-json are mutually exclusive") + } + if spec.FontSize != 0 { + if spec.FontSize < 0 { + return apperrors.NewValidation("--font-size must be positive") + } + params["fontSizes"] = sheetFillIntMatrix(rows, cols, spec.FontSize) + return nil + } + if spec.FontSizesJSON != "" { + var m [][]int + if err := json.Unmarshal([]byte(spec.FontSizesJSON), &m); err != nil { + return err + } + if !sheetMatrixIntShape(m, rows, cols) { + return apperrors.NewValidation("--font-sizes-json shape does not match range") + } + params["fontSizes"] = m + } + return nil +} + +func sheetApply2DString(scalar, jsonStr string, rows, cols int, flagName, key string, enum map[string]bool, params map[string]any) error { + if scalar != "" && jsonStr != "" { + return apperrors.NewValidation("--" + flagName + " and --" + flagName + "s-json are mutually exclusive") + } + if scalar != "" { + if enum != nil && !enum[scalar] { + return apperrors.NewValidation("--" + flagName + " has invalid value") + } + params[key] = sheetFillStringMatrix(rows, cols, scalar) + return nil + } + if jsonStr == "" { + return nil + } + var m [][]string + if err := json.Unmarshal([]byte(jsonStr), &m); err != nil { + return err + } + if !sheetMatrixStringShape(m, rows, cols) { + return apperrors.NewValidation("--" + flagName + "-json shape does not match range") + } + if enum != nil { + for _, row := range m { + for _, v := range row { + if v != "" && !enum[v] { + return apperrors.NewValidation("--" + flagName + "-json has invalid value") + } + } + } + } + params[key] = m + return nil +} + +func sheetParseA1Range(addr string) (rows, cols int, err error) { + if i := strings.Index(addr, "!"); i >= 0 { + addr = addr[i+1:] + } + addr = strings.TrimSpace(strings.ToUpper(addr)) + parts := strings.SplitN(addr, ":", 2) + c1, r1, err := sheetParseA1Cell(parts[0]) + if err != nil { + return 0, 0, err + } + c2, r2 := c1, r1 + if len(parts) == 2 { + c2, r2, err = sheetParseA1Cell(parts[1]) + if err != nil { + return 0, 0, err + } + } + if c2 < c1 { + c1, c2 = c2, c1 + } + if r2 < r1 { + r1, r2 = r2, r1 + } + return r2 - r1 + 1, c2 - c1 + 1, nil +} + +func sheetParseA1Cell(s string) (col, row int, err error) { + for len(s) > 0 && s[0] >= 'A' && s[0] <= 'Z' { + col = col*26 + int(s[0]-'A'+1) + s = s[1:] + } + if col == 0 || s == "" { + return 0, 0, apperrors.NewValidation("invalid A1 cell") + } + row, err = strconv.Atoi(s) + if err != nil || row <= 0 { + return 0, 0, apperrors.NewValidation("invalid A1 cell") + } + return col, row, nil +} + +func sheetFillStringMatrix(rows, cols int, v string) [][]string { + out := make([][]string, rows) + for i := range out { + out[i] = make([]string, cols) + for j := range out[i] { + out[i][j] = v + } + } + return out +} + +func sheetFillIntMatrix(rows, cols, v int) [][]int { + out := make([][]int, rows) + for i := range out { + out[i] = make([]int, cols) + for j := range out[i] { + out[i][j] = v + } + } + return out +} + +func sheetMatrixStringShape(m [][]string, rows, cols int) bool { + if len(m) != rows { + return false + } + for _, row := range m { + if len(row) != cols { + return false + } + } + return true +} + +func sheetMatrixIntShape(m [][]int, rows, cols int) bool { + if len(m) != rows { + return false + } + for _, row := range m { + if len(row) != cols { + return false + } + } + return true +} diff --git a/internal/helpers/sheet_test.go b/internal/helpers/sheet_test.go new file mode 100644 index 00000000..9255a858 --- /dev/null +++ b/internal/helpers/sheet_test.go @@ -0,0 +1,217 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helpers + +import ( + "bytes" + "context" + "testing" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor" +) + +type sheetCommandRunner struct { + last executor.Invocation + calls []executor.Invocation +} + +func (r *sheetCommandRunner) Run(_ context.Context, invocation executor.Invocation) (executor.Result, error) { + r.last = invocation + r.calls = append(r.calls, invocation) + return executor.Result{Invocation: invocation}, nil +} + +func executeSheetCommand(t *testing.T, runner *sheetCommandRunner, args ...string) { + t.Helper() + cmd := sheetHandler{}.Command(runner) + var out, errOut bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&errOut) + cmd.SetArgs(args) + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut.String()) + } +} + +func TestSheetCreateCallsCreateWorkspaceSheet(t *testing.T) { + t.Parallel() + + runner := &sheetCommandRunner{} + executeSheetCommand(t, runner, "create", "--name", "销售数据", "--folder", "FOLDER_001", "--workspace", "WS_001") + + if runner.last.Tool != "create_workspace_sheet" { + t.Fatalf("tool = %q, want create_workspace_sheet", runner.last.Tool) + } + if got := runner.last.Params["name"]; got != "销售数据" { + t.Fatalf("name = %#v", got) + } + if got := runner.last.Params["folderId"]; got != "FOLDER_001" { + t.Fatalf("folderId = %#v", got) + } + if got := runner.last.Params["workspaceId"]; got != "WS_001" { + t.Fatalf("workspaceId = %#v", got) + } +} + +func TestSheetRangeUpdateCallsSetCellRange(t *testing.T) { + t.Parallel() + + runner := &sheetCommandRunner{} + executeSheetCommand(t, runner, + "range", "update", + "--node", "NODE_001", + "--sheet-id", "SHEET_001", + "--range", "A1:B1", + "--values", `[[{"type":"text","text":"姓名"},{"type":"text","text":"分数"}]]`, + ) + + if runner.last.Tool != "set_cell_range" { + t.Fatalf("tool = %q, want set_cell_range", runner.last.Tool) + } + if got := runner.last.Params["rangeAddress"]; got != "A1:B1" { + t.Fatalf("rangeAddress = %#v", got) + } + cells, ok := runner.last.Params["cells"].([][]any) + if !ok || len(cells) != 1 || len(cells[0]) != 2 { + t.Fatalf("cells = %#v", runner.last.Params["cells"]) + } +} + +func TestSheetRangeSetStyleExpandsScalarStyle(t *testing.T) { + t.Parallel() + + runner := &sheetCommandRunner{} + executeSheetCommand(t, runner, + "range", "set-style", + "--node", "NODE_001", + "--sheet-id", "SHEET_001", + "--range", "A1:B2", + "--bg-color", "#FFF2CC", + "--font-weight", "bold", + ) + + if runner.last.Tool != "update_range" { + t.Fatalf("tool = %q, want update_range", runner.last.Tool) + } + bg, ok := runner.last.Params["backgroundColors"].([][]string) + if !ok || len(bg) != 2 || len(bg[0]) != 2 || bg[1][1] != "#FFF2CC" { + t.Fatalf("backgroundColors = %#v", runner.last.Params["backgroundColors"]) + } + weights, ok := runner.last.Params["fontWeights"].([][]string) + if !ok || weights[0][0] != "bold" { + t.Fatalf("fontWeights = %#v", runner.last.Params["fontWeights"]) + } +} + +func TestSheetFilterViewUpdateCriteriaCallsSetCriteria(t *testing.T) { + t.Parallel() + + runner := &sheetCommandRunner{} + executeSheetCommand(t, runner, + "filter-view", "update-criteria", + "--node", "NODE_001", + "--sheet-id", "SHEET_001", + "--filter-view-id", "FV_001", + "--column", "2", + "--filter-criteria", `{"filterType":"values","visibleValues":["销售部"]}`, + ) + + if runner.last.Tool != "set_filter_view_criteria" { + t.Fatalf("tool = %q, want set_filter_view_criteria", runner.last.Tool) + } + if got := runner.last.Params["filterViewId"]; got != "FV_001" { + t.Fatalf("filterViewId = %#v", got) + } + if got := runner.last.Params["column"]; got != 2 { + t.Fatalf("column = %#v", got) + } +} + +func TestSheetCondFormatCreateExpandsCondition(t *testing.T) { + t.Parallel() + + runner := &sheetCommandRunner{} + executeSheetCommand(t, runner, + "cond-format", "create", + "--node", "NODE_001", + "--sheet-id", "SHEET_001", + "--ranges", `["A1:A10"]`, + "--condition", `{"numberCondition":{"operator":"greater","value1":"80"}}`, + "--cell-style", `{"backgroundColor":"#FFCDD2"}`, + ) + + if runner.last.Tool != "create_cond_format" { + t.Fatalf("tool = %q, want create_cond_format", runner.last.Tool) + } + if _, ok := runner.last.Params["numberCondition"]; !ok { + t.Fatalf("numberCondition missing from %#v", runner.last.Params) + } + if _, ok := runner.last.Params["cellStyle"]; !ok { + t.Fatalf("cellStyle missing from %#v", runner.last.Params) + } +} + +func TestSheetCSVCommandsAcceptFileIDAlias(t *testing.T) { + t.Parallel() + + runner := &sheetCommandRunner{} + executeSheetCommand(t, runner, + "csv-get", + "--file-id", "NODE_001", + "--sheet-id", "SHEET_001", + "--range", "A1:B2", + ) + + if runner.last.Tool != "get_range_as_csv" { + t.Fatalf("tool = %q, want get_range_as_csv", runner.last.Tool) + } + if got := runner.last.Params["nodeId"]; got != "NODE_001" { + t.Fatalf("csv-get nodeId = %#v", got) + } + + executeSheetCommand(t, runner, + "csv-put", + "--file-id", "NODE_002", + "--sheet-id", "SHEET_002", + "--csv", "a,b\n1,2", + "--start-cell", "A1", + ) + + if runner.last.Tool != "set_range_from_csv" { + t.Fatalf("tool = %q, want set_range_from_csv", runner.last.Tool) + } + if got := runner.last.Params["nodeId"]; got != "NODE_002" { + t.Fatalf("csv-put nodeId = %#v", got) + } +} + +func TestSheetReplaceAllowsEmptyReplacement(t *testing.T) { + t.Parallel() + + runner := &sheetCommandRunner{} + executeSheetCommand(t, runner, + "replace", + "--node", "NODE_001", + "--sheet-id", "SHEET_001", + "--find", "临时", + "--replacement", "", + ) + + if runner.last.Tool != "replace_all" { + t.Fatalf("tool = %q, want replace_all", runner.last.Tool) + } + if got := runner.last.Params["replaceText"]; got != "" { + t.Fatalf("replaceText = %#v, want empty string", got) + } +} diff --git a/internal/helpers/wiki.go b/internal/helpers/wiki.go index 05811223..692fe3bc 100644 --- a/internal/helpers/wiki.go +++ b/internal/helpers/wiki.go @@ -14,6 +14,8 @@ package helpers import ( + "fmt" + "strconv" "strings" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cobracmd" @@ -64,6 +66,7 @@ func (wikiHandler) Command(runner executor.Runner) *cobra.Command { newWikiSpaceGetCommand(runner), newWikiSpaceListCommand(runner), newWikiSpaceSearchCommand(runner), + newWikiSpaceDeleteCommand(runner), ) root.AddCommand(space) @@ -81,6 +84,7 @@ func (wikiHandler) Command(runner executor.Runner) *cobra.Command { newWikiMemberAddCommand(runner), newWikiMemberUpdateCommand(runner), newWikiMemberListCommand(runner), + newWikiMemberRemoveCommand(runner), ) root.AddCommand(member) return root @@ -150,6 +154,20 @@ func newWikiSpaceListCommand(runner executor.Runner) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { params := map[string]any{} if spaceType := wikiFlagOrFallback(cmd, "type"); spaceType != "" { + if spaceType == "orgSpace" || spaceType == "mySpace" { + driveParams := map[string]any{"spaceType": spaceType} + if limit := wikiFlagOrFallback(cmd, "limit", "page-size"); limit != "" { + if n, err := strconv.Atoi(limit); err == nil { + driveParams["maxResults"] = n + } else { + driveParams["maxResults"] = limit + } + } + if pageToken := wikiFlagOrFallback(cmd, "cursor", "page-token"); pageToken != "" { + driveParams["nextToken"] = pageToken + } + return runWikiProductTool(cmd, runner, "drive", "list_spaces", driveParams) + } params["wikiSpaceType"] = spaceType } if limit := wikiFlagOrFallback(cmd, "limit", "page-size"); limit != "" { @@ -211,6 +229,32 @@ func newWikiSpaceSearchCommand(runner executor.Runner) *cobra.Command { return cmd } +func newWikiSpaceDeleteCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "delete", + Short: "删除知识库", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + workspaceID, err := wikiRequiredFlagOrFallback(cmd, "workspace", "workspace-id", "workspaceId") + if err != nil { + return err + } + if !confirmDeletePrompt(cmd, "知识库", workspaceID) { + return nil + } + return runWikiTool(cmd, runner, "delete_wikiSpace", map[string]any{ + "workspaceId": workspaceID, + }) + }, + } + preferLegacyLeaf(cmd) + cmd.Flags().String("workspace", "", "知识库 ID 或 URL (必填)") + addWikiHiddenStringFlag(cmd, "workspace-id", "--workspace 的兼容别名") + addWikiHiddenStringFlag(cmd, "workspaceId", "--workspace 的兼容别名") + return cmd +} + func newWikiMemberAddCommand(runner executor.Runner) *cobra.Command { cmd := &cobra.Command{ Use: "add", @@ -313,10 +357,233 @@ func newWikiMemberListCommand(runner executor.Runner) *cobra.Command { return cmd } +func newWikiMemberRemoveCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "remove", + Aliases: []string{"rm"}, + Short: "移除知识库成员", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + workspaceID, err := wikiRequiredFlagOrFallback(cmd, "workspace", "workspace-id", "workspaceId") + if err != nil { + return err + } + user, err := wikiRequiredFlagOrFallback(cmd, "users", "user", "uid") + if err != nil { + return err + } + return runWikiTool(cmd, runner, "remove_member", map[string]any{ + "workspaceId": workspaceID, + "userIds": wikiCSV(user), + }) + }, + } + preferLegacyLeaf(cmd) + addWikiMemberListWorkspaceFlag(cmd) + cmd.Flags().String("users", "", "用户 userId 列表,逗号分隔 (必填)") + addWikiHiddenStringFlag(cmd, "user", "--users 的兼容别名") + addWikiHiddenStringFlag(cmd, "uid", "--users 的兼容别名") + return cmd +} + +func newWikiNodeListCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "列出知识库节点", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + workspaceID, err := wikiRequiredFlagOrFallback(cmd, "workspace", "workspace-id", "workspaceId") + if err != nil { + return err + } + params := map[string]any{"workspaceId": workspaceID} + if folder := wikiFlagOrFallback(cmd, "folder", "node", "parent-id"); folder != "" { + params["folderId"] = normalizeDocNodeID(folder) + } + if limit := wikiIntFlagOrFallback(cmd, "limit", "page-size"); limit > 0 { + params["pageSize"] = limit + } + if cursor := wikiFlagOrFallback(cmd, "cursor", "page-token"); cursor != "" { + params["pageToken"] = cursor + } + return runWikiProductTool(cmd, runner, "doc", "list_nodes", params) + }, + } + preferLegacyLeaf(cmd) + addWikiNodeWorkspaceFlag(cmd) + cmd.Flags().String("folder", "", "父节点 nodeId") + addWikiHiddenStringFlag(cmd, "node", "--folder 的兼容别名") + addWikiHiddenStringFlag(cmd, "parent-id", "--folder 的兼容别名") + cmd.Flags().Int("limit", 0, "每页数量") + cmd.Flags().Int("page-size", 0, "--limit 的兼容别名") + _ = cmd.Flags().MarkHidden("page-size") + cmd.Flags().String("cursor", "", "分页游标") + addWikiHiddenStringFlag(cmd, "page-token", "--cursor 的兼容别名") + return cmd +} + +func newWikiNodeCreateCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "在知识库中创建节点", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + workspaceID, err := wikiRequiredFlagOrFallback(cmd, "workspace", "workspace-id", "workspaceId") + if err != nil { + return err + } + name, err := wikiRequiredFlag(cmd, "name") + if err != nil { + return err + } + params := map[string]any{ + "workspaceId": workspaceID, + "name": name, + } + if nodeType := wikiFlagOrFallback(cmd, "type"); nodeType != "" { + params["type"] = nodeType + } + if folder := wikiFlagOrFallback(cmd, "folder", "parent-id"); folder != "" { + params["folderId"] = normalizeDocNodeID(folder) + } + return runWikiProductTool(cmd, runner, "doc", "create_file", params) + }, + } + preferLegacyLeaf(cmd) + addWikiNodeWorkspaceFlag(cmd) + cmd.Flags().String("name", "", "节点名称 (必填)") + cmd.Flags().String("type", "adoc", "节点类型: adoc / asheet / folder / axls") + cmd.Flags().String("folder", "", "父节点 nodeId") + addWikiHiddenStringFlag(cmd, "parent-id", "--folder 的兼容别名") + return cmd +} + +func newWikiNodeCopyCommand(runner executor.Runner) *cobra.Command { + return newWikiNodeTransferCommand(runner, "copy", "copy_document") +} + +func newWikiNodeMoveCommand(runner executor.Runner) *cobra.Command { + return newWikiNodeTransferCommand(runner, "move", "move_document") +} + +func newWikiNodeTransferCommand(runner executor.Runner, use, tool string) *cobra.Command { + cmd := &cobra.Command{ + Use: use, + Short: use + " 知识库节点", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + workspaceID, err := wikiRequiredFlagOrFallback(cmd, "workspace", "workspace-id", "workspaceId") + if err != nil { + return err + } + nodeID, err := wikiRequiredFlagOrFallback(cmd, "node", "node-id", "doc-id", "file-id") + if err != nil { + return err + } + params := map[string]any{ + "nodeId": normalizeDocNodeID(nodeID), + "workspaceId": workspaceID, + } + if folder := wikiFlagOrFallback(cmd, "folder", "parent-id", "parent-node-id", "parent-folder-id"); folder != "" { + params["targetFolderId"] = normalizeDocNodeID(folder) + } + return runWikiProductTool(cmd, runner, "doc", tool, params) + }, + } + preferLegacyLeaf(cmd) + addWikiNodeWorkspaceFlag(cmd) + addWikiNodeIDFlags(cmd) + cmd.Flags().String("folder", "", "目标文件夹 nodeId") + addWikiHiddenStringFlag(cmd, "parent-id", "--folder 的兼容别名") + addWikiHiddenStringFlag(cmd, "parent-node-id", "--folder 的兼容别名") + addWikiHiddenStringFlag(cmd, "parent-folder-id", "--folder 的兼容别名") + return cmd +} + +func newWikiNodeDeleteCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "delete", + Short: "删除知识库节点", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + if _, err := wikiRequiredFlagOrFallback(cmd, "workspace", "workspace-id", "workspaceId"); err != nil { + return err + } + nodeID, err := wikiRequiredFlagOrFallback(cmd, "node", "node-id", "doc-id", "file-id") + if err != nil { + return err + } + if !confirmDeletePrompt(cmd, "知识库节点", nodeID) { + return nil + } + return runWikiProductTool(cmd, runner, "doc", "delete_document", map[string]any{ + "nodeId": normalizeDocNodeID(nodeID), + }) + }, + } + preferLegacyLeaf(cmd) + addWikiNodeWorkspaceFlag(cmd) + addWikiNodeIDFlags(cmd) + cmd.Flags().BoolP("yes", "y", false, "跳过确认直接删除") + return cmd +} + +func newWikiNodeSearchCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "search", + Short: "在知识库中搜索节点", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + workspaceID, err := wikiRequiredFlagOrFallback(cmd, "workspace", "workspace-id", "workspaceId") + if err != nil { + return err + } + query := wikiFlagOrFallback(cmd, "query", "keyword") + if query == "" { + return apperrors.NewValidation("--query is required") + } + params := map[string]any{ + "keyword": query, + "workspaceIds": []string{workspaceID}, + } + if values, _ := cmd.Flags().GetStringSlice("extensions"); len(values) > 0 { + params["extensions"] = splitWikiStringSlice(values) + } + if limit := wikiIntFlagOrFallback(cmd, "limit"); limit > 0 { + params["pageSize"] = limit + } + if cursor := wikiFlagOrFallback(cmd, "cursor", "page-token"); cursor != "" { + params["pageToken"] = cursor + } + return runWikiProductTool(cmd, runner, "doc", "search_documents", params) + }, + } + preferLegacyLeaf(cmd) + addWikiNodeWorkspaceFlag(cmd) + cmd.Flags().String("query", "", "搜索关键词 (必填)") + addWikiHiddenStringFlag(cmd, "keyword", "--query 的兼容别名") + cmd.Flags().StringSlice("extensions", nil, "按扩展名过滤,逗号分隔") + cmd.Flags().Int("limit", 0, "每页数量") + cmd.Flags().String("cursor", "", "分页游标") + addWikiHiddenStringFlag(cmd, "page-token", "--cursor 的兼容别名") + return cmd +} + func runWikiTool(cmd *cobra.Command, runner executor.Runner, tool string, params map[string]any) error { + return runWikiProductTool(cmd, runner, "wiki", tool, params) +} + +func runWikiProductTool(cmd *cobra.Command, runner executor.Runner, product, tool string, params map[string]any) error { invocation := executor.NewHelperInvocation( cobracmd.LegacyCommandPath(cmd), - "wiki", + product, tool, params, ) @@ -344,6 +611,19 @@ func addWikiMemberListWorkspaceFlag(cmd *cobra.Command) { addWikiHiddenStringFlag(cmd, "workspaceId", "--workspace 的兼容别名") } +func addWikiNodeWorkspaceFlag(cmd *cobra.Command) { + cmd.Flags().String("workspace", "", "知识库 ID 或 URL (必填)") + addWikiHiddenStringFlag(cmd, "workspace-id", "--workspace 的兼容别名") + addWikiHiddenStringFlag(cmd, "workspaceId", "--workspace 的兼容别名") +} + +func addWikiNodeIDFlags(cmd *cobra.Command) { + cmd.Flags().String("node", "", "节点 ID 或 URL (必填)") + addWikiHiddenStringFlag(cmd, "node-id", "--node 的兼容别名") + addWikiHiddenStringFlag(cmd, "doc-id", "--node 的兼容别名") + addWikiHiddenStringFlag(cmd, "file-id", "--node 的兼容别名") +} + func addWikiHiddenStringFlag(cmd *cobra.Command, name, usage string) { cmd.Flags().String(name, "", usage) _ = cmd.Flags().MarkHidden(name) @@ -397,3 +677,19 @@ func wikiCSV(raw string) []string { } return values } + +func splitWikiStringSlice(values []string) []string { + out := make([]string, 0, len(values)) + for _, value := range values { + for _, part := range strings.Split(value, ",") { + if item := strings.TrimSpace(part); item != "" { + out = append(out, item) + } + } + } + return out +} + +func wikiUnsupportedCommand(name string) error { + return fmt.Errorf("unsupported wiki command: %s", name) +} diff --git a/internal/helpers/wiki_proxy.go b/internal/helpers/wiki_proxy.go index 11dd187a..f7811610 100644 --- a/internal/helpers/wiki_proxy.go +++ b/internal/helpers/wiki_proxy.go @@ -38,15 +38,19 @@ func addWikiProxyCommands(root *cobra.Command, runner executor.Runner) { newWikiProxyLeaf(runner, "search", wikiProxyTargetSpace, []string{"space", "search"}, wikiProxyOptions{}), newWikiProxyLeaf(runner, "create", wikiProxyTargetSpace, []string{"space", "create"}, wikiProxyOptions{}), newWikiProxyLeaf(runner, "get", wikiProxyTargetSpace, []string{"space", "get"}, wikiProxyOptions{}), + newWikiProxyLeaf(runner, "delete", wikiProxyTargetSpace, []string{"space", "delete"}, wikiProxyOptions{}), ) node := newWikiProxyGroup("node", "知识库节点兼容入口") node.AddCommand( - newWikiProxyLeaf(runner, "list", wikiProxyTargetDoc, []string{"list"}, wikiProxyOptions{}), + newWikiNodeListCommand(runner), newWikiProxyLeaf(runner, "read", wikiProxyTargetDoc, []string{"read"}, wikiProxyOptions{}), newWikiProxyLeaf(runner, "info", wikiProxyTargetDoc, []string{"info"}, wikiProxyOptions{}), - newWikiProxyLeaf(runner, "create", wikiProxyTargetDoc, []string{"create"}, wikiProxyOptions{}), - newWikiProxyLeaf(runner, "search", wikiProxyTargetDoc, []string{"search"}, wikiProxyOptions{workspaceToWorkspaceIDs: true}), + newWikiNodeCreateCommand(runner), + newWikiNodeCopyCommand(runner), + newWikiNodeMoveCommand(runner), + newWikiNodeDeleteCommand(runner), + newWikiNodeSearchCommand(runner), ) file := newWikiProxyGroup("file", "知识库文件兼容入口") @@ -164,6 +168,7 @@ func newWikiProxySpaceTargetRoot(runner executor.Runner) *cobra.Command { newWikiSpaceGetCommand(runner), newWikiSpaceListCommand(runner), newWikiSpaceSearchCommand(runner), + newWikiSpaceDeleteCommand(runner), ) root.AddCommand(space) return root diff --git a/internal/helpers/wiki_test.go b/internal/helpers/wiki_test.go index ad7a3c48..b7cc2502 100644 --- a/internal/helpers/wiki_test.go +++ b/internal/helpers/wiki_test.go @@ -16,6 +16,7 @@ package helpers import ( "bytes" "context" + "reflect" "strconv" "strings" "testing" @@ -259,6 +260,94 @@ func TestWikiMemberAddUsesWorkspaceIDAlias(t *testing.T) { } } +func TestWikiSpaceListDriveTypesRouteToDrive(t *testing.T) { + t.Parallel() + + runner := &wikiCommandRunner{} + cmd := wikiHandler{}.Command(runner) + var out, errOut bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&errOut) + cmd.SetArgs([]string{"space", "list", "--type", "orgSpace", "--limit", "10"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut.String()) + } + if runner.last.CanonicalProduct != "drive" { + t.Fatalf("product = %q, want drive", runner.last.CanonicalProduct) + } + if runner.last.Tool != "list_spaces" { + t.Fatalf("tool = %q, want list_spaces", runner.last.Tool) + } + if got := runner.last.Params["spaceType"]; got != "orgSpace" { + t.Fatalf("spaceType = %#v, want orgSpace", got) + } + if got := runner.last.Params["maxResults"]; got != 10 { + t.Fatalf("maxResults = %#v, want 10", got) + } +} + +func TestWikiNodeSearchRoutesToDoc(t *testing.T) { + t.Parallel() + + runner := &wikiCommandRunner{} + cmd := wikiHandler{}.Command(runner) + var out, errOut bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&errOut) + cmd.SetArgs([]string{ + "node", "search", + "--workspace-id", "WS_001", + "--keyword", "方案", + "--extensions", "adoc,asheet", + "--limit", "5", + }) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut.String()) + } + if runner.last.CanonicalProduct != "doc" || runner.last.Tool != "search_documents" { + t.Fatalf("invocation = %#v, want doc search_documents", runner.last) + } + if got := runner.last.Params["keyword"]; got != "方案" { + t.Fatalf("keyword = %#v, want 方案", got) + } + if got := runner.last.Params["workspaceIds"]; !reflect.DeepEqual(got, []string{"WS_001"}) { + t.Fatalf("workspaceIds = %#v, want []string{WS_001}", got) + } + if got := runner.last.Params["extensions"]; !reflect.DeepEqual(got, []string{"adoc", "asheet"}) { + t.Fatalf("extensions = %#v, want adoc/asheet", got) + } + if got := runner.last.Params["pageSize"]; got != 5 { + t.Fatalf("pageSize = %#v, want 5", got) + } +} + +func TestWikiMemberRemoveRoutesToWiki(t *testing.T) { + t.Parallel() + + runner := &wikiCommandRunner{} + cmd := wikiHandler{}.Command(runner) + var out, errOut bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&errOut) + cmd.SetArgs([]string{"member", "remove", "--workspace-id", "WS_001", "--user", "uid1,uid2"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut.String()) + } + if runner.last.CanonicalProduct != "wiki" || runner.last.Tool != "remove_member" { + t.Fatalf("invocation = %#v, want wiki remove_member", runner.last) + } + if got := runner.last.Params["workspaceId"]; got != "WS_001" { + t.Fatalf("workspaceId = %#v, want WS_001", got) + } + users, ok := runner.last.Params["userIds"].([]string) + if !ok || strings.Join(users, ",") != "uid1,uid2" { + t.Fatalf("userIds = %#v, want uid1,uid2", runner.last.Params["userIds"]) + } +} + func TestWikiMemberUpdateAcceptsWukongUsersAlias(t *testing.T) { t.Parallel() diff --git a/skills/mono/references/products/aitable.md b/skills/mono/references/products/aitable.md index 37361df5..f49cfb0c 100644 --- a/skills/mono/references/products/aitable.md +++ b/skills/mono/references/products/aitable.md @@ -7,9 +7,15 @@ | 资源 | URI 格式 | |------|----------| | Base 文档 | `https://alidocs.dingtalk.com/i/nodes/{baseId}` | +| 指定数据表 | `https://alidocs.dingtalk.com/i/nodes/{baseId}?iframeQuery=sheetId%3D{tableId}` | +| 指定数据表+视图 | `https://alidocs.dingtalk.com/i/nodes/{baseId}?iframeQuery=sheetId%3D{tableId}%26viewId%3D{viewId}` | | 模板预览 | `https://docs.dingtalk.com/table/template/{templateId}` | -> **操作后请返回文档 URI**:每次执行 base list/search/create/get 操作后,从返回数据中提取 `baseId`,拼接为 `https://alidocs.dingtalk.com/i/nodes/{baseId}` 返回给用户。 +> **操作后请返回文档 URI**:返回链接时必须带上当前操作的数据表 tableId,让用户点击后直接看到目标数据表,而不是落在空白的默认表。 +> - 已知 tableId + viewId 时(view create 返回、view get 中提取):拼接 `https://alidocs.dingtalk.com/i/nodes/{baseId}?iframeQuery=sheetId%3D{tableId}%26viewId%3D{viewId}` +> - 已知 tableId 时(table create 返回、base get 中提取、record 操作所用的 tableId):拼接 `https://alidocs.dingtalk.com/i/nodes/{baseId}?iframeQuery=sheetId%3D{tableId}` +> - 仅有 baseId、无明确 tableId 时(如 base list/search):拼接 `https://alidocs.dingtalk.com/i/nodes/{baseId}` +> > 补充:如果 URL 不是来自 `aitable` 命令返回,而是用户直接贴的原始 `alidocs` URL,先按 [链接规范](../url-patterns.md#alidocs-url-类型探测流程) probe,确认是 `able` 后再按 AI 表格处理。 ## 命令索引表 @@ -19,10 +25,9 @@ | 命令 | 用途 | 必填参数 | 路由提醒 | |------|------|----------|----------| | `base list` | 列出最近访问的 Base | — | 仅返回最近访问过的,优先用 `base search` | -| `base search` | 搜索 Base;不传关键词时列出最近 Base | — | 可选 `--query`;不传时走 list_bases | +| `base search` | 按名称搜索 Base | `--query` | 关键词 ≥2 字符 | | `base get` | 获取 Base 信息(含 tables 列表) | `--base-id` | 用户给 URL 时提取末尾 ID | -| `base create` | 创建 Base | `--name` | 创建后直接用返回的 baseId | -| `base copy` | 复制 Base 到目标文件夹 | `--base-id` `--target-folder-id` | 目标必须是 `dws doc folder create/list` 返回的文档文件夹 `nodeId`;不要传钉盘数字 `dentryId`,也不要用手工新建 base/table 代替 | +| `base create` | 创建 Base | `--name` | 创建后直接用返回的 baseId;**默认新建的 base 自带一个空白「数据表」(含 3 行空记录)和一个空白仪表盘**,如需干净的空 base,传 `--template-id 1743` | | `base update` | 更新 Base 名称 | `--base-id` `--name` | — | | `base delete` | 删除 Base | `--base-id` | 不可逆 | @@ -31,8 +36,8 @@ | 命令 | 用途 | 必填参数 | 路由提醒 | |------|------|----------|----------| | `table get` | 获取表结构(字段+视图目录) | `--base-id` | 不传 `--table-ids` 返回全部表 | -| `table create` | 创建数据表 | `--base-id` `--name` | `--fields` 可选;不传时创建空字段表 | -| `table update` | 重命名表 | `--base-id` `--table-id` `--name` | — | +| `table create` | 创建数据表 | `--base-id` `--name` `--fields` | fields 为 JSON 数组,至少 1 个 | +| `table update` | 修改表名 / 备注 / 行命名规则 | `--base-id` `--table-id` + 三选一(`--name` / `--description` / `--record-name-key`) | `--record-name-key` 是固定枚举(如 task/project/event/customer/ji_lu 等),非字段 ID | | `table delete` | 删除表 | `--base-id` `--table-id` | 不可逆 | ### field (字段管理) → 详见 [aitable-field.md](./aitable/aitable-field.md)、[field-properties](./aitable/aitable-field-properties.md) @@ -44,6 +49,30 @@ | `field update` | 更新字段名/配置 | `--base-id` `--table-id` `--field-id` | 不可变更字段类型 | | `field delete` | 删除字段 | `--base-id` `--table-id` `--field-id` | 不可逆 | +#### 搜索字段选项 +``` +Usage: + dws aitable field search-options [flags] +Example: + dws aitable field search-options --base-id --table-id --field-id + dws aitable field search-options --base-id --table-id --field-id --keyword 已完成 + dws aitable field search-options --base-id --table-id --field-id --limit 100 +Flags: + --base-id string Base ID (必填) + --field-id string 目标字段 ID,必须是 singleSelect / multipleSelect 类型 (必填) + --keyword string 模糊搜索关键词,大小写不敏感、contains 匹配 option name;不传返回全部 + --limit int 返回的最大 option 数量,默认 3000(全量),最大 3000 + --table-id string Table ID (必填) +``` + +仅适用于 **singleSelect / multipleSelect** 字段。其他类型(text/number/date/...)调用会返回错误。 + +适用场景: +- options 较多,只想要含某关键词的子集(避免 `field get` 拉取整个字段配置带回所有 options)。 +- 写入 record 前预览选项 id ↔ name 的映射,确认要使用的选项确实存在。 + +> **写 record 时**:`record create / update` 对 singleSelect/multipleSelect 可直接传 option **name**,不需要用本命令。本命令主要用于 **filter** 写法(filters 优先用 option **id**)或选项较多需要精确定位时。 + ### record (记录管理) | 命令 | 用途 | 必读 reference | 路由提醒 | @@ -51,36 +80,66 @@ | `record query` | 查询/搜索记录 | [aitable-record-query.md](./aitable/aitable-record-query.md) | 先 `table get` 拿 fieldId;`--all` 自动翻页;filters 结构见 reference | | `record get` | 按 ID 取记录(`record query --record-ids` 的窄别名) | [aitable-record-query.md](./aitable/aitable-record-query.md) | 已知 recordId 时首选;必填 `--record-ids`(单次最多 100 条);未暴露 filters/sort/query/cursor/limit | | `record create` | 新增记录 | [aitable-record-create.md](./aitable/aitable-record-create.md) | cells key 必须是 fieldId 不是字段名;单次最多 100 条 | -| `record update` | 更新记录(每条独立 cells) | [aitable-record-update.md](./aitable/aitable-record-update.md) | 需先 query 拿 recordId;只传需改字段;`--records` 是 `[{recordId,cells},...]` 数组;同一组值批量更新也用此命令展开 records | +| `record update` | 更新记录(每条独立 cells) | [aitable-record-update.md](./aitable/aitable-record-update.md) | 需先 query 拿 recordId;只传需改字段;`--records` 是 `[{recordId,cells},...]` 数组 | +| `record batch-update` | 批量更新(同一 cells 应用到多条 recordId) | [aitable-record-update.md](./aitable/aitable-record-update.md)、[aitable-cell-value.md](./aitable/aitable-cell-value.md) | 适合"统一标记完成/统一改负责人"等共享 patch 场景;`--cells` 是 JSON object(key=fieldId,value 按字段类型见 cell-value.md),与 record update 的单条 cells 结构完全一致;必填 `--record-ids` `--cells`;单次最多 100 条 | | `record delete` | 删除记录 | [aitable-record-delete.md](./aitable/aitable-record-delete.md) | 不可逆,需先 query 确认 | +| `record history-list` | 查询单条记录的变更历史 | [aitable-record-history.md](./aitable/aitable-record-history.md) | 必填 `--record-id`;分页 `--offset --limit`,limit 范围 [1,50] 默认 20 | +| `record query-empty` | 查询完全没填用户字段的空行 | [aitable-record-query.md](./aitable/aitable-record-query.md) | 一页扫描 `--limit` [1,100] 默认 100;扫完前需用 `--cursor` 翻页(nextCursor 为空才表扫完) | +| `record share-url` | 批量获取记录分享链接 | [aitable-record-share.md](./aitable/aitable-record-share.md) | 必填 `--record-ids`(CSV,单次最多 20 条);可选 `--view-id` 带视图上下文 | +| `record upsert` | 批量创建或更新(按 recordId 是否存在自动拆分) | [aitable-record-upsert.md](./aitable/aitable-record-upsert.md) | --records 同 record update 格式;带 recordId 走 update,不带走 create;单次最多 100 | +| `record primary-doc-get` | 查询记录的主键文档 nodeId | [aitable-primary-doc.md](./aitable/aitable-primary-doc.md) | 返回的 nodeId 可直接用于 `dws doc read/update --node` | +| `record primary-doc-create` | 为记录创建主键文档(幂等) | [aitable-primary-doc.md](./aitable/aitable-primary-doc.md) | fieldId 必须是 primaryDoc 类型;已存在则返回已有 nodeId | ### view (视图管理) | 命令 | 用途 | 必填参数 | 路由提醒 | |------|------|----------|----------| -| `view get` | 获取视图配置 | `--base-id` `--table-id` | 不传 `--view-ids` 返回全部视图 | -| `view list` | 列出全部视图(`view get` 不传 `--view-ids` 的别名) | `--base-id` `--table-id` | 与 `view get` 完全等价;只需视图列表时优先 | -| `view create` | 创建视图 | `--base-id` `--table-id` `--view-type` | 类型: Grid/Kanban/Gantt/Calendar/Gallery/FormDesigner;可选 `--name` 指定视图名称(未传时自动生成)、`--config` 传初始配置 JSON | -| `view update` | 更新视图(**调整字段顺序的入口**) | `--base-id` `--table-id` `--view-id` | `visibleFieldIds` 重排字段顺序 | +| `view get` | 获取视图配置(不传子命令) | `--base-id` `--table-id` | 不传 `--view-ids` 返回全部视图 | +| `view get ` | 获取视图某个属性 | `--view-id` | 12 个:card/timebar/aggregate/filter/sort/group/visible-fields/field-widths(详见 [aitable-view-config.md](./aitable/aitable-view-config.md))+ lock/frozen-cols/row-height/fill-color-rule(详见 [aitable-view-extras.md](./aitable/aitable-view-extras.md)) | +| `view list` | 列出全部视图(`view get` 的别名) | `--base-id` `--table-id` | 与 `view get` 完全等价 | +| `view create` | 创建视图 | `--base-id` `--table-id` `--view-type` | 类型: Grid/Kanban/Gantt/Calendar/Gallery/FormDesigner;**Gantt 创建后必须 `view update timebar` 绑定日期字段** | +| `view update` | 整体更新视图 / 多属性合并更新 | `--base-id` `--table-id` `--view-id` | 可传 `--name --desc --config '{...}'`,**`--config` 路径继续保留** | +| `view update ` | 按属性局部更新(推荐)| `--view-id` + typed flag / `--json` | 12 个:card/timebar/aggregate/field-widths/visible-fields/filter/sort/group/name + frozen-cols/row-height/fill-color-rule | +| `view lock [--off]` | 锁定/解锁视图 | `--base-id` `--table-id` `--view-id` | 默认锁定;`--off` 解锁。详见 [aitable-view-extras.md](./aitable/aitable-view-extras.md) | +| `view duplicate` | 复制视图 | `--base-id` `--table-id` `--view-id` | 可选 `--new-name`;保留源视图全部配置。详见 [aitable-view-extras.md](./aitable/aitable-view-extras.md) | | `view delete` | 删除视图 | `--base-id` `--table-id` `--view-id` | 不可删最后一个/锁定视图 | -> **"移动字段/调整字段顺序"** 在 AI 表格里没有 `field reorder` 命令,必须通过 `view update --config '{"visibleFieldIds":[...]}'` 完成。 +> **优先用 `view get ` / `view update ` 子命令**:每个属性独立命令,typed flag 友好,agent 不必拼 JSON。**`view update --config '{...}'` 仍可用**,适合一次性多属性更新或脚本场景。 -> **view update --config 支持的 key 白名单**(传入其他 key 会报错): -> - `visibleFieldIds` — 视图可见字段列表及顺序(首列字段必须保留在第一位) -> - `filter` — 筛选规则**数组**(⚠️ 注意是数组 `[...]`,不是对象 `{...}`) -> - `sort` — 排序规则**数组** -> - `group` — 分组规则**数组** -> - `fieldWidths` — 列宽映射(仅 Grid 视图有效) -> -> **filter/sort/group 必须传数组格式**,不要和 `record query --filters`(对象格式)混淆。详见 [aitable-filter-sort.md](./aitable/aitable-filter-sort.md) § view update 章节。 -> CLI 会自动容错(对象→数组 wrap),但建议直接使用正确格式。 -> -> 不支持 `formInfo`、`requiredFields`、`conditionalRules` 等 FormDesigner 高级配置,这些 key 会被服务端忽略。 +> **属性按 attr 分类,决定该读哪份子文档**: +> - card / timebar / aggregate / filter / sort / group / visible-fields / field-widths → [aitable-view-config.md](./aitable/aitable-view-config.md) +> - lock / frozen-cols / row-height / fill-color-rule / duplicate → [aitable-view-extras.md](./aitable/aitable-view-extras.md) +> 后一类**不能**塞进 `view update --config '{...}'`,必须用各自专属子命令;如果错传 `flags` / `frozenColCount` / `cellHeight` / `conditionalFormats` 等 key 进 `--config`,CLI 会在 stderr 提示应改用的命令。 -### 表单视图 → 详见 [aitable-form.md](./aitable/aitable-form.md) +> **`view update --config` 支持的 9 个 key**: +> `visibleFieldIds` / `filter` / `sort` / `group` / `fieldWidths`(Grid) / `aggregate`(Grid) / `kanbanCard`(Kanban) / `ganttTimebar`(Gantt) / `galleryCard`(Gallery)。 +> filter/sort/group 必须传**数组**格式(与 `record query --filters` 的对象格式不同;CLI 会自动容错)。其他 key 会被服务端忽略并打 warning。 -悟空命令面不暴露 `form` 命令组;表单按 `viewType=FormDesigner` 的视图处理,创建/查看/更新/删除都使用 `view` 命令。 +### form (表单管理) → 详见 [aitable-form.md](./aitable/aitable-form.md) + +| 命令 | 用途 | 必填参数 | 路由提醒 | +|------|------|----------|----------| +| `form list` | 列出表单视图 | `--base-id` `--table-id` | 详情见 [aitable-form.md](./aitable/aitable-form.md) | +| `form get` | 按 viewId 取单个表单详情 | `--base-id` `--table-id` `--view-id` | — | +| `form create` | 创建表单视图 | `--base-id` `--table-id` `--name` | — | +| `form update` | 更新表单配置 | `--base-id` `--table-id` `--view-id` | title/name/description 至少一项 | +| `form delete` | 删除表单 | `--base-id` `--table-id` `--view-id` | 不可逆 | +| `form field list/update/hide` | 表单字段管理 | — | 详情见子文档 | +| `form questions create/delete` | 题目管理(=field create/delete) | — | 详情见子文档 | +| `form share get/update` | 表单分享配置 | — | 详情见子文档 | + +> **创建表单**有两种等价方式:`form create --name "..."`(推荐)或 `view create --view-type FormDesigner --name "..."`。 + +### workflow (自动化工作流) → 详见 [aitable-workflow.md](./aitable/aitable-workflow.md) + +| 命令 | 用途 | 必填参数 | 路由提醒 | +|------|------|----------|----------| +| `workflow list` | 列出 Base 下所有工作流 | `--base-id` | 支持 `--limit [1,100]` / `--offset >=0`;list 出参字段叫 `flowId` | +| `workflow get` | 获取单个工作流详情(含 flowSchema) | `--base-id` `--workflow-id` | `--workflow-id` 接受 list 里的 `flowId`(同值) | +| `workflow enable` | 启用工作流 | `--base-id` `--workflow-id` | 返回 `{enabled: true}` 是动作确认;要确认真启用看 list 的 `status` | +| `workflow disable` | 禁用工作流(高危) | `--base-id` `--workflow-id` `--yes` | 影响业务自动化,建议二次确认;status 变 STOP | + +> **当前不支持通过 CLI 新建/修改/删除工作流**,请去 AI 表格 Web 端(数据表页面 → 自动化)配置。 ### dashboard & chart → 详见 [aitable-dashboard-chart.md](./aitable/aitable-dashboard-chart.md) @@ -88,6 +147,7 @@ |------|------| | `dashboard get/create/update/delete` | 仪表盘管理 | | `dashboard config-example` | 查看仪表盘配置模板 | +| `dashboard arrange` | 自动重排仪表盘图表布局(智能填满网格,避免空缺) | | `chart get/create/update/delete` | 图表管理 | | `chart widgets-example` | 查看图表 widgets 配置模板 | @@ -111,15 +171,162 @@ |------|------|----------| | `template search` | 搜索模板 | `--query` | -## 评测执行硬约束 +### advperm (高级权限/自定义角色) → 详见 [aitable-advperm.md](./aitable/aitable-advperm.md) + +| 命令 | 用途 | 必填参数 | 路由提醒 | +|------|------|----------|----------| +| `advperm enable` | 开启 Base 高级权限总开关 | `--base-id` | 不开启时角色规则不生效 | +| `advperm disable` | 关闭 Base 高级权限总开关(高危) | `--base-id` `--yes` | 关闭后全员回退默认权限 | +| `advperm role-list` | 列出 Base 下所有角色 | `--base-id` | 同时返回自定义角色和系统角色;`roleType == "custom"` 是自定义,前缀 `system_` 是系统角色 | +| `advperm role-get` | 获取单角色完整配置 | `--base-id` `--role-id` | 含 subRoles 与字段/行级规则 | +| `advperm role-create` | 创建自定义角色 | `--base-id` `--name` | 可选 `--sub-roles` 同时指定子角色权限规则 | +| `advperm role-update` | 增量更新自定义角色(PATCH) | `--base-id` `--role-id` | 未传字段不变;`--sub-roles` 按 (targetId,targetType) 合并 | +| `advperm role-delete` | 删除自定义角色 | `--base-id` `--role-id` `--yes` | 不可逆;系统角色禁删;**调用者必须是该 AI 表格的管理员/Owner**,非管理员会得到 401 AUTH_ERROR | + +> **角色 CRUD 已全支持**:create/get/list/update/delete 都可走 CLI。 +> 所有写命令(enable/disable/role-create/role-update/role-delete)需要 Base 管理员权限;非管理员只能调 `role-list` / `role-get`(只读)。 +> "角色 ↔ 成员"绑定当前 CLI 不支持,仍需在 AI 表格 Web 端 → Base 设置 → 高级权限面板手动完成。 + +### section (文件夹与节点管理) + +> 用于在 Base 的导航树中组织 table / dashboard / 表单视图 / 文档等节点(类似文件夹)。 +> 操作前建议先用 `section list-nodes` 拿到 nodeId / sectionId 与父级关系。 + +#### 创建文件夹 +``` +Usage: + dws aitable section create [flags] +Example: + dws aitable section create --base-id --name 我的文件夹 + dws aitable section create --base-id --name 子文件夹 --parent-section-id --index 0 +Flags: + --base-id string Base ID (必填) + --name string 文件夹名称 (必填) + --parent-section-id string 父文件夹 ID;不传或空字符串表示创建在 Base 根目录下 + --index int 在父文件夹下的目标位置(0-based);不传则追加到末尾 +``` + +返回 `data.sectionId` 与 `data.name`。 + +#### 重命名文件夹 +``` +Usage: + dws aitable section rename [flags] +Example: + dws aitable section rename --base-id --section-id --new-name 新名称 +Flags: + --base-id string Base ID (必填) + --section-id string 目标文件夹 ID (必填) + --new-name string 新的文件夹名称 (必填) +``` + +#### 删除文件夹 +``` +Usage: + dws aitable section delete [flags] +Example: + dws aitable section delete --base-id --section-id +Flags: + --base-id string Base ID (必填) + --section-id string 目标文件夹 ID (必填) +``` + +> **注意**:删除不可逆;删除前可先用 `section list-empty` 确认是否为空文件夹。 + +#### 调整文件夹顺序 +``` +Usage: + dws aitable section reorder [flags] +Example: + dws aitable section reorder --base-id --section-id --target-index 0 +Flags: + --base-id string Base ID (必填) + --section-id string 目标文件夹 ID (必填) + --target-index int 目标位置(0-based)(必填) +``` -- 多轮任务必须执行到用户要求的最后一步;不要只回复"现在开始/下一步执行",也不要在创建 base/table/field 后提前结束。 -- 每个写操作后用 `base get`、`table get`、`field get`、`record query` 或对应 `view get/list` 读回验证真实 ID 与结果。 -- 字段批量 JSON 推荐 `fieldName`;CLI 兼容 `name`,但 skill 生成时不要主动使用 `name`。字段类型统一用小写/规范值,如 `text`、`number`、`singleSelect`、`attachment`。 -- 成员/负责人字段类型使用 `user`,不要生成 `member`。 -- 复制 AI 表格必须调用 `dws aitable base copy --base-id --target-folder-id --format json`。目标目录必须是 `dws doc folder create` 或 `dws doc list` 返回的文档文件夹 `nodeId`;不要传 `drive list` 返回的数字 `dentryId`,不要用新建 base/table 的手工方式代替 `base copy`。 -- 用户未指定目标文件夹时:先 `dws doc info --node --format json` 取 `workspaceId`,再 `dws doc folder create --workspace --name "AI表格副本" --format json` 创建目标文件夹,最后把返回的 `nodeId` 传给 `base copy`。 -- 导入 Excel/CSV 前先用 `find` 或 `ls` 确认真实文件路径;遇到中文文件名乱码或路径不匹配时,重新查找实际文件,不要停在解释阶段。 +> 在**当前父文件夹下**调整展示顺序。跨父级移动请用 `section move-node`。 + +#### 列出空文件夹 +``` +Usage: + dws aitable section list-empty [flags] +Example: + dws aitable section list-empty --base-id +Flags: + --base-id string Base ID (必填) +``` + +返回 `data.items: [{sectionId, name, parentSectionId}]` 与 `data.total`,用于清理或诊断导航树(parentSectionId 为空串表示在根目录下)。 + +#### 列出全部节点 +``` +Usage: + dws aitable section list-nodes [flags] +Example: + dws aitable section list-nodes --base-id +Flags: + --base-id string Base ID (必填) +``` + +返回 `data.items: [{nodeId, nodeType, parentSectionId, name?}]` 与 `data.total`,涵盖文件夹 / AI 表格 / 表单视图 / 仪表盘 / 文档 / 查询视图。 + +> **与其他命令的关联**:是 `section move-node` / `section reorder` 的前置定位命令——先用它拿到 nodeId 与 parentSectionId。 + +#### 移动节点 +``` +Usage: + dws aitable section move-node [flags] +Example: + dws aitable section move-node --base-id --node-id --new-parent-section-id + dws aitable section move-node --base-id --node-id --new-parent-section-id "" --target-index 0 +Flags: + --base-id string Base ID (必填) + --node-id string 要移动的节点 ID(文件夹/AI表格/表单视图/仪表盘/文档/查询视图)(必填) + --new-parent-section-id string 目标父文件夹 ID;空字符串表示移到 Base 根目录 (必填) + --target-index int Base 内节点的全局位置(0-based);不传则不调整 +``` + +> 服务端自动识别节点类型,无需区分文件夹与非文件夹。返回 `data.nodeId / newParentSectionId / nodeType`。 +> 对文件夹节点带 `--target-index` 时会先 move 再 reorder,中间失败会返回 `MOVE_OK_REORDER_FAILED`,可用 `section reorder` 重试。 + +## 复杂操作 + +### 仪表盘 / 图表(建议顺序) + +```bash +# 1) 先看配置模板(JSONC) +dws aitable dashboard config-example --format json +dws aitable chart widgets-example --format json + +# 2) 先拿 dashboard,再拿 chart 详情 +dws aitable dashboard get --base-id --dashboard-id --format json +dws aitable chart get --base-id --dashboard-id --chart-id --format json +``` + +要点: + +- `dashboard get` 返回的 `charts[].chartId` 可直接给 `chart get` 使用。 +- `dashboard share get` 可能返回 `404`(资源不存在或未开通),需按可重试错误处理,不要误判为参数拼错。 +- `chart share get` 可正常返回 `enabled/shareUrl`,用于分享状态判断。 + +### 导出数据(两阶段轮询) + +`export data` 常见为异步任务:首次调用可能只返回 `taskId`,需要继续轮询。 + +```bash +# 第一步:创建任务(按 scope 传必要参数) +dws aitable export data --base-id --scope table --table-id --format excel --timeout-ms 1000 + +# 第二步:拿 taskId 继续轮询,直到返回 downloadUrl +dws aitable export data --base-id --task-id --timeout-ms 3000 +``` + +参数约束 + +- `scope=all`:只需 `base-id` +- `scope=table`:必须 `table-id` +- `scope=view`:必须同时 `table-id + view-id` ## 意图判断 @@ -127,14 +334,14 @@ - 查看/查找/列表 → `base search`(优先)或 `base list`(仅浏览最近访问) - 详情 → `base get` - 创建 → `base create` -- 复制 → `base copy`,必须调用 `dws aitable base copy --base-id --target-folder-id --format json`;若无目标文件夹,先 `doc info --node ` 取 `workspaceId`,再 `doc folder create --workspace ` 创建文档文件夹作为目标。服务端返回 `Invalid target folder ID` 时,改用 `doc folder create` 新建目标文件夹后重试一次;不要手工重建副本。 - 修改 → `base update` - 删除 → `base delete` 用户说"数据表/子表/table": - 查看 → `table get` - 创建 → `table create` -- 重命名 → `table update` +- 重命名 / 改备注 / 改行命名规则 → `table update`(三选一:`--name` / `--description` / `--record-name-key`) +- 用户说"行命名规则/记录别名/卡片显示成 task/project/event 这种" → `table update --record-name-key <枚举键>`,**中文 → 枚举键**对照见 [aitable-record-name-key.md](./aitable/aitable-record-name-key.md) - 删除 → `table delete` 用户说"字段/列/column": @@ -145,19 +352,35 @@ 用户说"记录/行/数据/row": - 查看/搜索 → `record query`(读 [aitable-record-query.md](./aitable/aitable-record-query.md)) +- 找空行 / 没填东西的行 → `record query-empty`(读 [aitable-record-query.md](./aitable/aitable-record-query.md)) - 已知 recordId 反查字段值 → `record get`(按 ID 取专用,等价 `record query --record-ids`) - 添加/写入 → `record create`(读 [aitable-record-create.md](./aitable/aitable-record-create.md)) - 修改/更新(每条独立 cells) → `record update`(读 [aitable-record-update.md](./aitable/aitable-record-update.md)) -- **批量更新同一字段值**(统一标记/统一改值) → `record update --records '[{"recordId":"rec1","cells":{...}},{"recordId":"rec2","cells":{...}}]'` +- **批量更新同一字段值**(统一标记/统一改值) → `record batch-update --record-ids ... --cells '{...}'` - 删除 → `record delete` +- **查记录的字段变更历史 / 操作审计** → `record history-list`(读 [aitable-record-history.md](./aitable/aitable-record-history.md)) +- **取记录分享链接 / 把这行发给同事** → `record share-url`(读 [aitable-record-share.md](./aitable/aitable-record-share.md)) +- **不知道有没有 → 有就改、没有就建** → `record upsert`(读 [aitable-record-upsert.md](./aitable/aitable-record-upsert.md)) 用户说"视图/view": - 列出/查看全部视图 → `view list`(或 `view get` 不传 --view-ids,二者等价) - 看某个视图详情 → `view get --view-ids ` - 创建 → `view create` - 修改(含"调整字段顺序/隐藏字段") → `view update --config '{"visibleFieldIds":[...]}'` +- 修改某一项配置(filter/sort/group/card/timebar/aggregate 等)→ `view update `(读 [aitable-view-config.md](./aitable/aitable-view-config.md)) +- 锁定 / 冻结列 / 行高 / 数据高亮规则 / 复制视图 → 读 [aitable-view-extras.md](./aitable/aitable-view-extras.md) - 删除 → `view delete` +用户说"锁定视图/解锁视图/lock view" → `view lock` / `view lock --off`,详见 [aitable-view-extras.md](./aitable/aitable-view-extras.md) + +用户说"冻结列/冻结首列/frozen columns" → `view update frozen-cols --count N`,详见 [aitable-view-extras.md](./aitable/aitable-view-extras.md) + +用户说"行高/单元格高度/紧凑模式/cell height" → `view update row-height --cell-height N`(合法档位 32/56/88/128),详见 [aitable-view-extras.md](./aitable/aitable-view-extras.md) + +用户说"数据高亮/条件格式/单元格上色/fill color rule" → `view update fill-color-rule --json '[...]'`,详见 [aitable-view-extras.md](./aitable/aitable-view-extras.md) + +用户说"复制视图/duplicate view" → `view duplicate --view-id ... [--new-name ...]`,详见 [aitable-view-extras.md](./aitable/aitable-view-extras.md) + 用户说"筛选/过滤/filter" → 读 [aitable-filter-sort.md](./aitable/aitable-filter-sort.md) 用户说"统计/分析/聚合/TOP N/全量" → 读 [aitable-data-analysis-sop.md](./aitable/aitable-data-analysis-sop.md) @@ -166,16 +389,33 @@ 用户说"查找引用/lookup/filterUp/跨表" → 读 [aitable-formula-guide.md](./aitable/aitable-formula-guide.md)(§5.4 跨表引用) -用户说"表单/form/收集表/问卷/催办填写" → 读 [aitable-form.md](./aitable/aitable-form.md),使用 `view create --view-type FormDesigner` +用户说"表单/form/收集表/问卷/催办填写" → 读 [aitable-form.md](./aitable/aitable-form.md) + +用户说"自动化/工作流/流程/触发/automation/workflow" → 读 [aitable-workflow.md](./aitable/aitable-workflow.md) +- 看 Base 里有哪些流程 / 哪些在跑 → `workflow list`(看 `recordCount` / `runningCount`) +- 看某个流程具体配置(触发条件、动作步骤) → `workflow get` +- 启用流程 → `workflow enable` +- 临时停掉流程(调试 / 数据迁移)→ `workflow disable --yes` +- **新建 / 修改 / 删除流程**:当前不支持,引导用户到 AI 表格 Web 端 → 数据表 → 自动化 面板手动完成 用户说"仪表盘/图表/chart" → 读 [aitable-dashboard-chart.md](./aitable/aitable-dashboard-chart.md) +用户说"仪表盘排版乱了/图表对不齐/重新排布/自动布局/美化仪表盘" → `dashboard arrange`(读 [aitable-dashboard-chart.md](./aitable/aitable-dashboard-chart.md)) + 用户说"附件/上传文件" → 读 [aitable-attachment.md](./aitable/aitable-attachment.md) 用户说"导入/导出/import/export" → 读 [aitable-export-import.md](./aitable/aitable-export-import.md) 用户说"模板" → `template search` +用户说"高级权限/角色/权限控制/谁能看/谁能改" → 读 [aitable-advperm.md](./aitable/aitable-advperm.md) +- 开/关高级权限 → `advperm enable` / `advperm disable --yes` +- 看角色配置 → `advperm role-list` 或 `advperm role-get` +- 建角色(可同时指定子角色权限) → `advperm role-create --name ... --sub-roles '[...]'` +- 改角色名 / 改子角色权限(PATCH 语义,未传字段不变) → `advperm role-update --role-id ... [--name ...] [--sub-roles '[...]']` +- 删角色 → `advperm role-delete --yes` +- **角色 ↔ 成员绑定**:当前 CLI 不支持,仍需在 AI 表格 Web 端面板手动完成 + 命令报错/操作失败 → 读 [aitable-error-recovery.md](./aitable/aitable-error-recovery.md) **关键区分**: base=表格文件, table=数据表, field=列, record=行 @@ -190,7 +430,7 @@ dws aitable base search --query "项目" --format json dws aitable base get --base-id --format json # 3. 获取表结构 — 提取 fieldId -dws aitable table get --base-id --table-ids --format json +dws aitable table get --base-id --table-id --format json # 4. 查询记录 dws aitable record query --base-id --table-id --format json @@ -206,7 +446,8 @@ dws aitable record create --base-id --table-id \ |------|-------------|------| | `base list/search` | `baseId` | 所有后续命令的 --base-id,拼接文档 URI | | `base create` | `baseId` | 后续命令 + 文档 URI | -| `base get` | `tables[].tableId` | --table-id | +| `base get` | `tables[].tableId` | --table-id,拼接指定数据表 URI | +| `table create` | `tableId` | 后续命令 + 拼接指定数据表 URI | | `table get` | `fields[].fieldId` | record 操作的 cells key, field get/update/delete | | `record query` | `recordId` | record update/delete;按 ID 反查字段值用 `record get` | | `template search` | `templateId` | base create --template-id,拼接模板预览 URI | diff --git a/skills/mono/references/products/aitable/aitable-advperm.md b/skills/mono/references/products/aitable/aitable-advperm.md new file mode 100644 index 00000000..84d9dfe6 --- /dev/null +++ b/skills/mono/references/products/aitable/aitable-advperm.md @@ -0,0 +1,251 @@ +# advperm — 高级权限管理 + +控制 Base 的高级权限总开关,并管理自定义角色(增删改查 + 子角色权限规则)。 +适用场景:"如何控制谁能看/改 Base 数据"、"开启/关闭高级权限"、"新建/修改/删除角色"、"按字段或行配置权限"。 + +## 命令一览 + +| 命令 | 用途 | +|------|------| +| `advperm enable` | 开启 Base 高级权限总开关 | +| `advperm disable` | 关闭 Base 高级权限总开关(高危) | +| `advperm role-list` | 列出 Base 下全部角色 | +| `advperm role-get` | 获取单角色完整配置 | +| `advperm role-create` | 创建自定义角色 | +| `advperm role-update` | 增量更新自定义角色(PATCH 语义) | +| `advperm role-delete` | 删除自定义角色(不可逆) | + +> 所有子命令的 `--base-id` 必填,可用隐藏别名 `--base`。 + +## 命令详情 + +### advperm enable — 开启高级权限 + +```bash +dws aitable advperm enable --base-id BASE_ID --format json +``` + +返回 `{baseId, enabled: true}`。 + +只有开启后角色配置才会真正限制成员的可访问范围;关闭状态下角色配置仍可读但不生效。 + +### advperm disable — 关闭高级权限(高危) + +```bash +dws aitable advperm disable --base-id BASE_ID --yes --format json +``` + +返回 `{baseId, enabled: false}`。关闭后所有角色配置即刻失效,全员回退到默认权限。涉及多人协作或敏感数据务必和用户二次确认,建议先 `role-list` 留底。 + +### advperm role-list — 列出全部角色 + +```bash +dws aitable advperm role-list --base-id BASE_ID --format json +``` + +返回结构: + +```json +{ + "data": { + "enabled": true, + "defaultRole": { "mode": 0 }, + "roles": [ + { + "roleId": "10685308981", + "name": "可查看角色", + "roleType": "custom", + "system": false, + "subRoles": [ + { + "authLevel": "read", + "targetId": "HMEaRQ4", + "targetType": "sheet", + "config": { "actions": 268435455 }, + "display": { + "authLevelLabel": "仅查看", + "targetTypeLabel": "数据表", + "permissionScopeNote": "...", + "actionsLabels": ["新增视图", "删除视图", "修改视图"], + "actionsNote": "..." + } + } + ] + } + ] + } +} +``` + +关键字段: + +- `roleType`:`custom`(自定义) / `system_editor` / `system_reader` / `5000`(owner) / `4000`(manager)。 +- `system`:boolean,true 表示系统角色(不可删)。 +- `subRoles[].display.*`:服务端返回的人类可读标签,可直接拼接给用户阅读,无需自行映射枚举。 +- 不返回角色成员列表;如需"成员-角色"映射请去 AI 表格 Web 端。 +- 新建 Base 默认 `enabled=false`,开启后只有 `owner` / `manager` 两个 meta 角色;`system_editor` / `system_reader` 需要在 Web UI 给成员授权"可编辑/可查看"后才会被服务端自动生成。 + +`role-list` / `role-get` 不需要管理员权限,普通成员也可读。 + +### advperm role-get — 获取单角色配置 + +```bash +dws aitable advperm role-get --base-id BASE_ID --role-id ROLE_ID --format json +``` + +返回结构同 `role-list` 中单个 role 对象(含完整 `subRoles[].config` 字段/行级规则与 `display.*` 标签)。 + +### advperm role-create — 创建自定义角色 + +```bash +# 仅指定 name,子角色由服务端按默认(none)填充 +dws aitable advperm role-create --base-id BASE_ID --name "市场可读" --format json + +# 创建时即指定 sub-roles(推荐——避免再走一次 role-update) +dws aitable advperm role-create --base-id BASE_ID --name "市场可读" \ + --sub-roles '[{"targetId":"","targetType":"sheet","authLevel":"read"}]' --format json +``` + +| flag | 必填 | 说明 | +|------|:---:|------| +| `--name` | ✅ | 角色名称 | +| `--role-type` | | 角色类型字符串(留空由服务端决定默认值,如 `custom`) | +| `--flow-type` | | 流程类型字符串(按业务需要) | +| `--sub-roles` | | JSON 数组:`[{targetId, targetType, authLevel, appId?, config?}]`,详见下方"sub-roles 子字段"段 | + +返回新建角色的完整配置(同 `role-get` 出参格式,含自动生成的 default subRoles)。 +系统角色无法通过本命令创建。 + +### advperm role-update — 增量更新自定义角色(PATCH 语义) + +```bash +# 只改名 +dws aitable advperm role-update --base-id BASE_ID --role-id ROLE_ID --name "新名字" + +# 只改 sheet 子角色 authLevel,name 不传保持不变 +dws aitable advperm role-update --base-id BASE_ID --role-id ROLE_ID \ + --sub-roles '[{"targetId":"","targetType":"sheet","authLevel":"edit-own"}]' +``` + +| flag | 必填 | 说明 | +|------|:---:|------| +| `--role-id` | ✅ | 目标自定义角色 ID(数字 long 字符串) | +| `--name` | | 新角色名称;不传不修改 | +| `--role-type` / `--flow-type` | | 可选 | +| `--sub-roles` | | JSON 数组,**PATCH 合并语义**:按 `(targetId, targetType)` 合并到现有 subRoles,入参中的 sub 整体替换该 sub,**入参未提及的 sub 保留不变**(无需先调 `role-get` 自行 merge) | + +**系统角色禁止更新**(包括 owner / manager / system_editor / system_reader)。 + +### sub-roles 子字段 + +每个 sub-role 描述「角色对某个权限目标的访问粒度」: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `targetId` | string | 目标资源 ID(数据表 → `tableId`;仪表盘 → `dashboardId`;应用 → `appId`) | +| `targetType` | string | `sheet` / `dashboard` / `app` | +| `authLevel` | string | `manage` / `edit-own` / `edit-custom-field` / `edit-field-range` / `read` / `none` | +| `appId` | string(可选) | 仅 `targetType=app` 时使用 | +| `config` | object(可选) | 字段/行级细化规则;含 `actions`(位图)/ `rows` / `cells`。结构与 `role-get` 出参 `subRoles[].config` 对齐 | + +### advperm role-delete — 删除自定义角色(不可逆) + +```bash +dws aitable advperm role-delete --base-id BASE_ID --role-id ROLE_ID --yes --format json +``` + +要求同时满足: + +1. 该 Base 已开启高级权限(`role-list` 返回 `enabled=true`)。 +2. 当前 dws 登录用户是该 Base 的管理员/Owner。 +3. `--role-id` 是 `role-list` 返回的数字 long 字符串(如 `"10685308981"`),且对应角色 `system=false`。 + +不可逆,删前先 `role-get` 留底。 + +## 能力边界 + +| 能力 | 状态 | +|------|------| +| 开/关高级权限 | ✅ 需管理员 | +| 列出 / 读取角色 | ✅ 普通成员也可读 | +| 创建自定义角色 | ✅ 需管理员 | +| 增量修改角色(PATCH 语义,不清空未传字段) | ✅ 需管理员 | +| 删除自定义角色 | ✅ 需管理员 | +| 修改/删除系统角色 | ❌ 服务端禁止;只能在 AI 表格 Web 端操作 | +| 角色 ↔ 成员绑定 | ❌ CLI 暂不支持,需在 AI 表格 Web 端 → Base 设置 → 高级权限 → 角色管理面板手动完成 | + +## 错误码速查 + +| 场景 | code | type | message | +|------|------|------|---------| +| advperm 关闭时调用写接口(如 `role-delete` / `role-create` / `role-update`) | `ADVANCED_PERMISSION_DISABLED` | `USER_ERROR` | `Advanced permission is disabled for base , please enable it via setAdvancedPermission before managing roles` | +| 非管理员调用 `enable` / `disable` / `role-create` / `role-update` / `role-delete` | `401` | `AUTH_ERROR` | `the current user must be a manager (administrator) of this base to manage roles or advanced permission` | +| 删除/更新系统角色(`system=true`) | `600` | `USER_ERROR` | `Illegal argument` | +| 操作不存在的数字 roleId(get/update/delete) | `600` | `USER_ERROR` | `Illegal argument` | +| 传非数字 roleId(如 `owner` / `manager`) | `INVALID_PARAMS` | `INPUT_ERROR` | `roleId is required` | +| `role-create` 缺 `--name` | `INVALID_PARAMS` | `INPUT_ERROR` | `name is required` | +| `--sub-roles` JSON 不是数组 / 解析失败 | (CLI 层拦截) | — | `--sub-roles 解析失败 ...` / `--sub-roles 必须是 JSON 数组` | +| `--base-id` 无法解析 | `INVALID_BASE_ID` | `INPUT_ERROR` | `baseId cannot be resolved to docId` | + +> `600 / Illegal argument` 同时覆盖"操作系统角色"和"操作不存在 roleId"两种情况。拿到 `600` 时先 `role-list` 自查目标 roleId 是否存在、是否 `system=true`,再据此引导用户。 + +## 典型工作流 + +### 排查"成员看不到某些字段/记录" + +```bash +dws aitable advperm role-list --base-id BASE_ID --format json +# 若 enabled=false:高级权限未开,所有规则不生效,与用户确认是否需要 enable + +dws aitable advperm enable --base-id BASE_ID --format json +dws aitable advperm role-list --base-id BASE_ID --format json +# 看 roles[] 里有哪些自定义角色 + +dws aitable advperm role-get --base-id BASE_ID --role-id ROLE_ID --format json +# 检查 subRoles[].config 中的字段/行级权限规则 +``` + +### 新建一个"市场可读"角色 + +```bash +# 1. 确保高级权限已开 +dws aitable advperm enable --base-id BASE_ID --format json + +# 2. 拿目标 sheet 的 tableId +dws aitable table get --base-id BASE_ID --format json + +# 3. 创建角色 + 指定 sheet 子角色 authLevel=read +dws aitable advperm role-create --base-id BASE_ID --name "市场可读" \ + --sub-roles '[{"targetId":"","targetType":"sheet","authLevel":"read"}]' \ + --format json +# → 返回新角色完整配置,含 roleId,记下后续 patch / delete 使用 +``` + +### 升级角色权限(read → edit-own),保留其他配置 + +```bash +# 只传 sub-roles,name 等其他字段保持不变(PATCH 语义) +dws aitable advperm role-update --base-id BASE_ID --role-id ROLE_ID \ + --sub-roles '[{"targetId":"","targetType":"sheet","authLevel":"edit-own"}]' \ + --format json +``` + +### 改角色名(不影响权限规则) + +```bash +dws aitable advperm role-update --base-id BASE_ID --role-id ROLE_ID --name "新名字" +``` + +### 清理废弃角色 + +```bash +dws aitable advperm role-list --base-id BASE_ID --format json +dws aitable advperm role-delete --base-id BASE_ID --role-id ROLE_ID --yes --format json +``` + +### 关闭高级权限(恢复全员可见) + +```bash +dws aitable advperm role-list --base-id BASE_ID --format json > /tmp/roles-backup.json +dws aitable advperm disable --base-id BASE_ID --yes --format json +``` diff --git a/skills/mono/references/products/aitable/aitable-attachment.md b/skills/mono/references/products/aitable/aitable-attachment.md index 1648fc87..45e9c2ae 100644 --- a/skills/mono/references/products/aitable/aitable-attachment.md +++ b/skills/mono/references/products/aitable/aitable-attachment.md @@ -1,6 +1,8 @@ # attachment — 附件上传 > **STOP — 不要使用钉盘 (drive) 上传!** 钉盘 fileId 无法写入 attachment 字段。必须使用以下流程。 +> +> **STOP — 严禁在 record create/update 的 cells 里直接传图片 URL!** 直传 `{"url":"https://..."}` 会导致服务端同步下载图片,批量写入时触发 TIMEOUT_ERROR。正确做法:先 `attachment upload` 获取 `fileToken`,再用 `{"fileToken":"ft_xxx"}` 写入。 ## 准备附件上传 @@ -38,8 +40,8 @@ dws aitable record create --base-id --table-id \ dws aitable attachment upload --base-id --file-name report.pdf --size 204800 --format json # → 返回 uploadUrl、fileToken -# 2. PUT 上传(Content-Type 留空) -curl -X PUT "" -H "Content-Type:" --data-binary @report.pdf +# 2. PUT 上传(Content-Type 必须是文件的具体 MIME type) +curl -X PUT "" -H "Content-Type: application/pdf" --data-binary @report.pdf # 3. 写入记录 dws aitable record update --base-id --table-id \ diff --git a/skills/mono/references/products/aitable/aitable-best-practices.md b/skills/mono/references/products/aitable/aitable-best-practices.md index 2c9b2200..db63f634 100644 --- a/skills/mono/references/products/aitable/aitable-best-practices.md +++ b/skills/mono/references/products/aitable/aitable-best-practices.md @@ -4,35 +4,30 @@ | 字段类型 | 可写 | 正确方式 | |----------|------|----------| -| 文本/数字/日期/单选/多选/复选框/URL | 是 | `dws aitable record create` / `dws aitable record update` | -| 附件 | 是,但需先上传 | 先 `dws aitable attachment upload` 取 `uploadUrl/fileToken`,PUT 后把 `fileToken` 写入记录 | -| 创建人/修改人/创建时间/修改时间 | 否 | 系统字段,只读 | -| 公式/查找引用 | 否 | 由系统计算,只读 | -| AI 字段 | 否 | 由 AI 自动计算,只读 | +| 文本/数字/日期/单选/多选/复选框/URL | ✅ | record create/update | +| 附件 | ⚠️ | 必须先走 [attachment upload 流程](./aitable-attachment.md) | +| 创建人/修改人/创建时间/修改时间 | ❌ | 系统字段,只读 | +| 公式/查找引用 | ❌ | 只读,由系统计算 | +| AI 字段 | ❌ | 只读,由 AI 自动计算 | ## 2. 查询执行契约 -1. 优先用 `dws aitable record query --filters` 在服务端过滤,不要先拉全量再在上下文里手动筛选。 -2. 返回 `has_more=true` 时不能做全局结论,数据可能不完整。 -3. 查询前先用 `dws aitable table get --base-id --table-ids ` 获取真实 fieldId,不要猜字段 ID。 -4. 只需要部分字段时,用 `dws aitable record query --field-ids fld1,fld2` 降低响应体积。 -5. 已知 recordId 时,用 `dws aitable record get --record-ids rec1,rec2`,不要构造无意义 filters。 +1. **不要拉全量后在 context 里手动统计** — 优先用 `--filters` 在服务端过滤 +2. **has_more=true 时不能做全局结论** — 数据可能不完整 +3. **优先用 `--filters` 在服务端过滤** — 不要拉全量后在本地 jq/grep +4. **字段名必须来自 `table get` 真实返回** — 不要猜测 fieldId +5. **减少响应体积** — 用 `--field-ids` 仅返回需要的字段 ## 3. 任务选路 | 用户诉求 | 优先方案 | 不要误走 | |---------|----------|----------| -| 查看几条数据 | `dws aitable record query --base-id --table-id ` | 不要默认 `--all` | -| 全量拉取/统计 | `dws aitable record query --base-id --table-id --all` | 不要手动循环 cursor | -| 全量导出 | `dws aitable export data --base-id --scope all --format excel` | 不要 `--all` 拉全量再写文件 | -| 文件级导入 | `dws aitable import upload --base-id --file-name data.xlsx --file-size <字节数>` + `dws aitable import data --import-id ` | 不要手动解析 xlsx 再逐条写入 | -| 批量写入多条不同数据 | `dws aitable record create --base-id --table-id --records '[{"cells":{"":"值"}}]'` | 不要一次超过 100 条 | -| 批量给多条记录写同一组值 | `dws aitable record update --base-id --table-id --records '[{"recordId":"rec1","cells":{"":"值"}},{"recordId":"rec2","cells":{"":"值"}}]'` | 不要使用隐藏兼容命令 | -| 附件上传 | `dws aitable attachment upload --base-id --file-name report.pdf --size <字节数>` + PUT + `record create/update` | 不要用钉盘 drive 上传 | -| 调整字段顺序 | `dws aitable view update --base-id --table-id --view-id --config '{"visibleFieldIds":["fld1","fld2"]}'` | 没有 `field reorder` 命令 | -| 查看视图列表 | `dws aitable view list --base-id --table-id ` | 不需要用 `view get --view-ids` | -| 创建收集表/问卷 | `dws aitable view create --base-id --table-id --view-type FormDesigner --name "表单名"` | 不要使用隐藏兼容命令 | -| 仪表盘/图表 | 先 `dashboard config-example` / `chart widgets-example`,再 create/update | 不要猜 config 结构 | +| 查看几条数据 | `record query` | 不要用 `--all` | +| 全量拉取/统计 | `record query --all` | 不要手动循环 cursor | +| 全量导出为文件 | `export data` | 不要 `--all` 拉全量再写文件 | +| 批量写入 | `record create`(分批 100 条) | 不要一次传超过 100 条 | +| 附件/图片上传 | `attachment upload` 获取 fileToken → `record create/update` 用 fileToken 写入 | **严禁直接传图片 URL 到附件字段**(服务端同步下载会超时) | +| 文件级导入 | `import upload` + `import data` | 不要手动解析 xlsx 再逐条写入 | ## 4. 创建/修改后回读确认 @@ -40,34 +35,12 @@ | 写操作 | 建议回读命令 | 确认内容 | |--------|-------------|----------| -| `dws aitable base create` | `dws aitable base get --base-id ` | base 名称、tables 列表 | -| `dws aitable table create` | `dws aitable table get --base-id --table-ids ` | 表名、字段列表是否符合预期 | -| `dws aitable field create` | `dws aitable field get --base-id --table-id ` | 新字段是否出现在字段列表中 | -| `dws aitable record create/update` | `dws aitable record get --base-id --table-id --record-ids ` | 写入值是否正确 | -| `dws aitable view update` | `dws aitable view get --base-id --table-id --view-ids ` | `visibleFieldIds` 顺序是否正确 | -| `dws aitable view create/update` | `dws aitable view get --base-id --table-id --view-ids ` | 表单视图名称、描述和配置 | +| `table create` | `table get --table-ids <新tableId>` | 表名、字段列表是否符合预期 | +| `field create` | `table get --table-ids ` | 新字段是否出现在字段列表中 | +| `record create/update` | `record query --record-ids <新recordId>` | 写入值是否正确 | -## 5. 导入导出与异步任务 +## 5. AI 字段注意事项 -- `export data` 的 `--format` 是导出格式,不要在此命令上追加全局 `--format json`。 -- 创建导出任务: - ```bash - dws aitable export data --base-id --scope table --table-id \ - --format excel --timeout-ms 1000 - ``` -- 续等已有导出任务: - ```bash - dws aitable export data --base-id --task-id --timeout-ms 3000 - ``` -- 导入本地文件: - ```bash - dws aitable import upload --base-id --file-name data.xlsx --file-size <字节数> --format json - curl -X PUT "" -H "Content-Type:" --data-binary @data.xlsx - dws aitable import data --import-id --format json - ``` - -## 6. AI 字段注意事项 - -- AI 字段的 prompt 必须至少包含一个 `fieldRef` 引用,纯文本 prompt 会被后端拒绝。 -- 先创建/确认被引用字段的 fieldId,再在 prompt 中引用。 -- `outputType` 必须与字段类型一致,例如 `outputType=text` 配 `--type text`。 +- AI 字段的 prompt **必须至少包含一个 `fieldRef` 引用**,纯文本 prompt 会被后端拒绝 +- 先创建/确认被引用字段的 fieldId,再在 prompt 中引用 +- `outputType` 必须与字段类型一致(如 `outputType=text` 配 `--type text`) diff --git a/skills/mono/references/products/aitable/aitable-cell-value.md b/skills/mono/references/products/aitable/aitable-cell-value.md index 9f747bf9..bc3a8125 100644 --- a/skills/mono/references/products/aitable/aitable-cell-value.md +++ b/skills/mono/references/products/aitable/aitable-cell-value.md @@ -86,11 +86,16 @@ {"fldDateId": "2026-03-15T09:00+08:00"} ``` -**读取**:RFC3339 字符串 +**读取**:RFC3339 字符串(带时区) ```json {"fldDateId": "2026-03-15T09:00:00+08:00"} ``` +**过滤**(`record query --filters`):日期字段**只能用日期专用操作符** `date_eq` / `before` / `after` / `not_before` / `not_after` / `exist` / `un_exist`,比较值用日期字符串(如 `"2026-03-15"`)。 +- ❌ 通用 `eq` / `ne` / `gt` / `gte` / `lt` / `lte` / `contain` 对日期字段无效,会静默返回 0 条; +- ❌ 不支持区间 `date_between` 与相对 `from_now`(CLI 会直接拒绝),范围查询用 `not_before` + `not_after` 组合。 +- 详见 [aitable-filter-sort.md](./aitable-filter-sort.md) §日期字段过滤。 + --- ### currency(货币) @@ -236,15 +241,14 @@ ### attachment(附件) -**写入**:对象数组,支持 `fileToken` 或 `url` 形式 +**写入**:对象数组,**必须使用 `fileToken`** ```json {"fldAttachId": [{"fileToken": "ft_xxx"}]} -{"fldAttachId": [{"url": "https://example.com/file.pdf"}]} ``` -> ⚠️ **必须先通过 [attachment upload 流程](./aitable-attachment.md) 获取 `fileToken`**。 -> URL 形式是 best-effort 异步转存,不保证立即可用。 +> ⚠️ **必须先通过 [attachment upload 流程](./aitable-attachment.md) 上传文件获取 `fileToken`,再将 `fileToken` 写入 cells。** +> ❌ **严禁直接传 `{"url": "https://..."}` 形式写入附件/图片字段** — 服务端会同步下载图片,10 条记录即触发 TIMEOUT_ERROR 超时。 > 写入会**整体覆盖**原附件列表,不是追加。 **读取**:对象数组(含下载链接、文件名、大小) @@ -335,7 +339,7 @@ |------|----------| | cells key 用字段名称 `"课程名称"` | 用 fieldId `"fldXXX"` | | progress 写入 `75` | 写入 `0.75`(范围 0~1) | -| attachment 直接传文件路径 | 必须先 upload 获取 fileToken | +| attachment 直接传文件路径或图片 URL | 必须先 `attachment upload` 获取 fileToken,再用 fileToken 写入(直传 URL 会超时) | | user 字段传用户名字符串 | 传对象数组 `[{"userId":"...", "corpId":"..."}]` | | group 字段用 `openConversationId` | 用 `cid` | | singleSelect 传 option id 字符串 | 传 name 字符串或 `{"id":"...", "name":"..."}` 对象 | diff --git a/skills/mono/references/products/aitable/aitable-dashboard-chart.md b/skills/mono/references/products/aitable/aitable-dashboard-chart.md index ae9a3739..94d576e2 100644 --- a/skills/mono/references/products/aitable/aitable-dashboard-chart.md +++ b/skills/mono/references/products/aitable/aitable-dashboard-chart.md @@ -27,17 +27,18 @@ dws aitable chart get --base-id --dashboard-id --chart- | `dashboard update` | 更新仪表盘 | `--base-id` `--dashboard-id` + (`--config` 或 `--name`) | `--name` 仅改名;`--config` 更新完整配置 | | `dashboard delete` | 删除仪表盘 | `--base-id` `--dashboard-id` `--yes` | — | | `dashboard config-example` | 查看仪表盘配置模板 | 无 | 创建前先调此命令了解 config 结构 | +| `dashboard arrange` | 自动重排图表布局 | `--base-id` `--dashboard-id` | 把图表按行铺满网格,避免某行只占半幅、留下大片空白;返回 `{totalColumns, layout, alignedChartCount}` | ## chart 子命令 | 命令 | 用途 | 必填参数 | |------|------|----------| | `chart get` | 获取图表详情 | `--base-id` `--dashboard-id` `--chart-id` | -| `chart create` | 创建图表 | `--base-id` `--dashboard-id` `--config` `--layout` | +| `chart create` | 创建图表 | `--base-id` `--dashboard-id` `--config` | | `chart update` | 更新图表配置 | `--base-id` `--dashboard-id` `--chart-id` `--config` | | `chart delete` | 删除图表 | `--base-id` `--dashboard-id` `--chart-id` `--yes` | | `chart widgets-example` | 查看图表 widgets 配置模板 | 无 | ## 配置获取流程 -创建图表前,必须先调用 `chart widgets-example` 查看配置模板,了解每种图表类型需要的字段结构,然后根据实际 tableId 和 fieldId 填充配置;同时必须传 `--layout` 指定图表位置和尺寸,例如 `--layout '{"x":0,"y":0,"w":6,"h":4}'`。 +创建图表前,必须先调用 `chart widgets-example` 查看配置模板,了解每种图表类型需要的字段结构,然后根据实际 tableId 和 fieldId 填充配置。 diff --git a/skills/mono/references/products/aitable/aitable-error-recovery.md b/skills/mono/references/products/aitable/aitable-error-recovery.md index 947b8fcc..e62fb0a4 100644 --- a/skills/mono/references/products/aitable/aitable-error-recovery.md +++ b/skills/mono/references/products/aitable/aitable-error-recovery.md @@ -63,7 +63,7 @@ | 错误现象 / summary | 原因 | 恢复动作 | |-------------------|------|---------| -| 导出任务超时 | 数据量大,异步任务未完成 | 用 `export data --base-id --task-id ` 轮询直到完成 | +| 导出任务超时 | 数据量大,异步任务未完成 | 用 `export data --task-id ` 轮询直到完成 | | 导入文件格式错误 | 不支持的文件格式或文件损坏 | 确认文件为 .xlsx 格式且未加密 | ## 3. 重试策略 diff --git a/skills/mono/references/products/aitable/aitable-export-import.md b/skills/mono/references/products/aitable/aitable-export-import.md index 2fc9b469..fc755451 100644 --- a/skills/mono/references/products/aitable/aitable-export-import.md +++ b/skills/mono/references/products/aitable/aitable-export-import.md @@ -4,10 +4,10 @@ `export data` 为异步任务:首次调用可能只返回 `taskId`,需要继续轮询。 -> ⚠️ **`export data` 的 `--format` 是导出格式**:需要导出 xlsx/附件时写 `--format excel` / `excel_and_attachment`。不要在这个命令上追加全局 `--format json`。 +> ⚠️ **`--format` 冲突警告**:`export data` 的 `--format` 是**导出格式**(excel/attachment 等),不是全局输出格式。**此命令禁止追加全局 `--format json`**,否则会覆盖导出格式导致 `INVALID_EXPORT_FORMAT` 错误。输出默认就是 JSON,无需额外指定。 ```bash -# 第一步:创建任务(按 scope 传必要参数) +# 第一步:创建任务(按 scope 传必要参数)——注意:不要加 --format json! dws aitable export data --base-id --scope table --table-id --format excel --timeout-ms 1000 # 第二步:拿 taskId 继续轮询,直到返回 downloadUrl diff --git a/skills/mono/references/products/aitable/aitable-field-properties.md b/skills/mono/references/products/aitable/aitable-field-properties.md index 586d58cc..710d685e 100644 --- a/skills/mono/references/products/aitable/aitable-field-properties.md +++ b/skills/mono/references/products/aitable/aitable-field-properties.md @@ -10,7 +10,6 @@ - `field create --name --type --config` 中 config 单独传 JSON 字符串 - `field update --config` 只传 config 部分 - 不需要 config 的类型(如 text、checkbox、attachment)可省略 config 字段 -- 成员/负责人字段类型使用 `user`,不要使用 `member`;字段类型不要写 `Text`/`Number`,统一写规范值 `text`/`number` ## 2. 字段类型速查 @@ -235,6 +234,5 @@ AI 字段不使用 config,而使用独立的 `--ai-config` 参数。详见 [ai | options 更新时只传新增项 | 全量覆盖,旧选项全部丢失 | | formula 字段尝试写入值 | 只读字段,record create/update 会报错 | | linkedTableId 传表名而非 ID | 必须传 tableId(如 `tblXXX`),不接受表名 | -| 成员字段使用 `member` | 不支持;人员/负责人/成员字段统一使用 `user` | | progress 值写入 50 表示 50% | 实际应写入 0.5(range 0~1) | | rating 值超出 max | 写入会报错 | diff --git a/skills/mono/references/products/aitable/aitable-filter-sort.md b/skills/mono/references/products/aitable/aitable-filter-sort.md index 6728fc26..8fabf8b4 100644 --- a/skills/mono/references/products/aitable/aitable-filter-sort.md +++ b/skills/mono/references/products/aitable/aitable-filter-sort.md @@ -1,5 +1,7 @@ # filters & sort — 筛选排序语法参考 +> 视图(view)配置的 filter/sort/group **整体写入**请优先用 `view update filter` / `view update sort` / `view update group` 子命令,详见 [aitable-view-config.md](./aitable-view-config.md)。本文件聚焦于 `record query --filters` 与 view config filter 的语法和差异。 + ## filters 结构规范 ### 强制规则 @@ -49,11 +51,34 @@ CLI 同时兼容两种子条件写法(推荐格式 A): | `exist` / `un_exist` | 有值 / 为空 | `["fieldId"]`(无需第二项) | | `any_of` / `none_of` / `all_of` | 包含任一 / 不包含任一 / 全包含(多选字段) | `["fieldId", "optionName"]` | | `date_eq` / `before` / `after` | 日期等于 / 早于 / 晚于 | `["fieldId", "dateStr"]` | -| `not_before` / `not_after` | 不早于 / 不晚于 | `["fieldId", "dateStr"]` | -| `from_now` | 从现在起 N 天内 | `["fieldId", "天数"]` | -| `date_between` | 日期区间 | `["fieldId", "[startTs, endTs]"]` | +| `not_before` / `not_after` | 不早于(≥) / 不晚于(≤) | `["fieldId", "2026-05-22"]` | > **操作符拼写必须严格匹配上表**,CLI 会在调用前校验,错误拼写会被拒绝。 +> +> **没有 `date_between`(区间)操作符**,也**不支持 `from_now`**——date 字段不支持区间/相对过滤,传了会被 CLI 拒绝。范围查询用 `not_before` + `not_after` 组合,见下方专节。 + +### 日期字段过滤(date / 创建时间 / 修改时间) + +日期类字段的过滤规则与其它字段**不同**,是线上反馈最高频的踩坑点。**经集成测试实测**确认的规则: + +1. **只能用日期专用操作符**:`date_eq` / `before` / `after` / `not_before` / `not_after` / `exist` / `un_exist`(与前端筛选 UI 的「等于 / 早于 / 晚于 / 早于或等于 / 晚于或等于 / 不为空 / 为空」一一对应)。 +2. **比较值用日期字符串**,如 `"2026-05-22"`(也接受 RFC3339 / 毫秒时间戳,内部统一转成毫秒比较)。读取返回的是带时区 RFC3339(如 `"2026-05-22T00:00:00+08:00"`)。 +3. **通用操作符 `eq` / `ne` / `gt` / `gte` / `lt` / `lte` / `contain` 对 date 字段无效**——无论传 ISO 字符串还是毫秒时间戳,都会**静默返回 0 条**。这是后端 date 字段的比较规则,不是 bug,CLI 也无法在本地拦截(不知道字段类型),务必用对操作符。 +4. **没有区间操作符 `date_between`**,也**不支持 `from_now`(相对天数)**——均会静默返回 0 条,CLI 已直接拒绝。范围查询用 `not_before`(≥起点)+ `not_after`(≤终点)两个条件 `and` 组合。 + +| 需求 | 操作符 | 示例 operands | +|------|--------|--------------| +| 等于某天 | `date_eq` | `["fldDate", "2026-05-22"]` | +| 早于 / 晚于(不含当天) | `before` / `after` | `["fldDate", "2026-05-22"]` | +| 不早于(≥) / 不晚于(≤) | `not_before` / `not_after` | `["fldDate", "2026-05-22"]` | +| 有值 / 为空 | `exist` / `un_exist` | `["fldDate"]` | + +**日期区间查询(替代 between)**——查 `2026-05-01 ~ 2026-05-31`(含端点): + +```bash +dws aitable record query --base-id X --table-id Y \ + --filters '{"operator":"and","operands":[{"operator":"not_before","operands":["fldDate","2026-05-01"]},{"operator":"not_after","operands":["fldDate","2026-05-31"]}]}' +``` ### 常见错误拼写(CLI 会自动提示纠正) diff --git a/skills/mono/references/products/aitable/aitable-form.md b/skills/mono/references/products/aitable/aitable-form.md index 83528cd0..2b126d8f 100644 --- a/skills/mono/references/products/aitable/aitable-form.md +++ b/skills/mono/references/products/aitable/aitable-form.md @@ -1,37 +1,120 @@ -# 表单视图 — 使用 view(FormDesigner) +# form — 表单管理 -悟空命令面不暴露独立表单命令组。表单在 AI 表格里按 `viewType=FormDesigner` 的视图处理,所有生成命令都走 `view` 和 `field`。 +## 命令一览 -## 命令路线 +| 命令 | 用途 | +|------|------| +| `form list` | 列出数据表下所有表单视图 | +| `form get` | 按 viewId 取单个表单详情(list_form_views + viewIds 过滤) | +| `form create` | 创建表单视图(等价于 `view create --view-type FormDesigner`) | +| `form update` | 更新表单标题或描述 | +| `form delete` | 删除表单视图(不可逆) | +| `form field list` | 列出表单可见字段 | +| `form field update` | 更新字段必填/描述 | +| `form field hide` | 在表单中隐藏/显示字段(不影响底层数据表字段) | +| `form share get` | 获取分享配置 | +| `form share update` | 开启/关闭分享 | +| `form questions create` | 添加题目(等价于 `field create`,命令位置上的别名) | +| `form questions delete` | 删除题目(等价于 `field delete`,命令位置上的别名) | -| 诉求 | 使用命令 | 说明 | -|------|----------|------| -| 列出表单视图 | `dws aitable view list --base-id --table-id ` | 从返回视图中过滤 `viewType=FormDesigner` | -| 查看表单视图 | `dws aitable view get --base-id --table-id --view-ids ` | 按 viewId 获取 | -| 创建表单视图 | `dws aitable view create --base-id --table-id --view-type FormDesigner --name "表单名"` | 返回 `viewId` | -| 更新表单视图 | `dws aitable view update --base-id --table-id --view-id --name "新名称"` | 描述用 `--desc` JSON | -| 删除表单视图 | `dws aitable view delete --base-id --table-id --view-id --yes` | 不可逆 | -| 添加题目 | `dws aitable field create --base-id --table-id --fields '[...]'` | 题目本质是字段 | -| 删除题目 | `dws aitable field delete --base-id --table-id --field-id --yes` | 不可逆 | +## 建议操作顺序 -## 创建工作流 +```bash +# 1) 列出数据表下的表单视图 +dws aitable form list --base-id BASE_ID --table-id TABLE_ID --format json + +# 2) 查看单个表单详情 +dws aitable form get --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID --format json + +# 3) 查看表单字段配置 +dws aitable form field list --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID --format json + +# 4) 查看分享配置 +dws aitable form share get --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID --format json +``` + +## 要点 + +- **创建表单**有两种等价方式: + - `form create --name "表单名"`(推荐,语义清晰) + - `view create --view-type FormDesigner --name "表单名"`(底层一致) +- `form update` 支持 `--title` 与 `--name` 两个等价参数;至少需传一项 +- `form field update` 必须传 `--required` 或 `--field-description` 至少一项 +- `form field hide` 仅控制字段在表单中的可见性,不影响底层数据表字段 +- **题目管理**与字段管理本质相同(题目 = 表格字段): + - `form questions create` 与 `field create` 入参完全一致(`--fields` JSON 或 `--name --type`) + - `form questions delete` 与 `field delete` 入参完全一致(必传 `--field-id`) + - 设置必填要在 create 后用 `form field update --required true` 单独调一次 + +## form 子命令 + +| 命令 | 用途 | 必填参数 | 说明 | +|------|------|----------|------| +| `form list` | 列出表单视图 | `--base-id` `--table-id` | 返回 viewId/name/title/createdAt | +| `form get` | 按 viewId 取单个表单 | `--base-id` `--table-id` `--view-id` | 内部基于 list_form_views 过滤 | +| `form create` | 创建表单视图 | `--base-id` `--table-id` `--name` | viewType=FormDesigner | +| `form update` | 更新表单 | `--base-id` `--table-id` `--view-id` | `--title`/`--name`(等价)和 `--description` 至少传一项;同时传 title/name 时 title 优先 | +| `form delete` | 删除表单 | `--base-id` `--table-id` `--view-id` `--yes` | 不可逆 | + +## form field 子命令 + +| 命令 | 用途 | 必填参数 | 说明 | +|------|------|----------|------| +| `form field list` | 列出表单字段 | `--base-id` `--table-id` `--view-id` | 返回 fieldId/name/type/required/hidden/description(hidden=true 的字段不在此返回) | +| `form field update` | 更新表单字段 | `--base-id` `--table-id` `--view-id` `--field-id` | `--required` 或 `--field-description` 至少一项 | +| `form field hide` | 切换字段隐藏 | `--base-id` `--table-id` `--view-id` `--field-id` `--hidden` | `--hidden true` 隐藏 / `--hidden false` 显示 | + +## form questions 子命令 + +`form questions create/delete` 与 `field create/delete` 入参、行为完全一致,只是命令位置归属于 `form` 命令组,方便从表单视角操作题目。 + +| 命令 | 用途 | 必填参数 | 说明 | +|------|------|----------|------| +| `form questions create` | 添加题目 | `--base-id` `--table-id` + (`--fields` 或 `--name --type`) | 入参与 `field create` 完全一致 | +| `form questions delete` | 删除题目 | `--base-id` `--table-id` `--field-id` `--yes` | 入参与 `field delete` 完全一致;不可逆;批量需多次调用 | + +## form share 子命令 + +| 命令 | 用途 | 必填参数 | 说明 | +|------|------|----------|------| +| `form share get` | 获取分享配置 | `--base-id` `--table-id` `--view-id` | 返回 enabled/status/shareFormUuid | +| `form share update` | 开启/关闭分享 | `--base-id` `--table-id` `--view-id` `--enabled` | `--enabled true` 开启 / `--enabled false` 关闭。注意:UI 上"发布并分享"按钮是另一概念,本命令只切换内部 enabled 标志,开启后需在 UI 刷新页面才会看到分享面板 | + +## 完整工作流示例 + +> **占位符约定**: +> - `BASE_ID` 来自 `dws aitable base list` / `base search` 返回的 `data.bases[].baseId` +> - `TABLE_ID` 来自 `dws aitable base get --base-id BASE_ID` 返回的 `data.tables[].tableId` +> - `VIEW_ID` 来自步骤 1 `form create` 返回的 `data.viewId` +> - `FIELD_ID` 来自步骤 2 `form questions create` 返回的 `data.results[].fieldId` ```bash -# 1. 创建表单视图 -dws aitable view create --base-id BASE_ID --table-id TABLE_ID \ - --view-type FormDesigner --name "员工信息收集" --format json +# 1) 创建表单 → 取返回的 data.viewId 作为 VIEW_ID +dws aitable form create --base-id BASE_ID --table-id TABLE_ID --name "员工信息收集" --format json -# 2. 添加题目字段 -dws aitable field create --base-id BASE_ID --table-id TABLE_ID \ +# 2) 添加题目 → 取返回的 data.results[].fieldId 作为 FIELD_ID +dws aitable form questions create --base-id BASE_ID --table-id TABLE_ID \ --fields '[{"fieldName":"姓名","type":"text"},{"fieldName":"邮箱","type":"text"}]' --format json -# 3. 回读表单视图 -dws aitable view get --base-id BASE_ID --table-id TABLE_ID --view-ids VIEW_ID --format json +# 3) 配置表单标题与描述 +dws aitable form update --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID \ + --title "员工信息收集" --description "请填写您的基本信息" --format json + +# 4) 设置题目必填(FIELD_ID 来自步骤 2) +dws aitable form field update --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID \ + --field-id FIELD_ID --required true --format json + +# 5) 隐藏不需要的题目 +dws aitable form field hide --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID \ + --field-id FIELD_ID --hidden true --format json + +# 6) 开启分享(注意:开启后需 UI 刷新页面才会看到分享面板) +dws aitable form share update --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID \ + --enabled true --format json ``` -## 注意 +## 返回结构补充 -- 不要生成隐藏兼容的独立表单命令;它不属于悟空对齐的公开命令面。 -- `VIEW_ID` 来自 `view create` 返回。 -- `FIELD_ID` 来自 `field create` 或 `field get` 返回。 -- 字段必填、字段隐藏、分享开关等表单高级配置没有公开的悟空命令入口;不要用隐藏兼容命令替代。 +- `form list` 返回 `data.formViews[]`,**每条仅含** `viewId/name/title/createdAt`;`shareFormUuid` 不在此返回,请用 `form share get` 单独获取。 +- `form get` 返回结构与 `form list` 完全一致(`data.formViews[]`),仅含一条记录(与请求 viewId 一致)。Agent 提取时仍走 `data.formViews[0]`。 +- `form field list` 仅返回**未隐藏**的字段;`hidden=true` 的字段不在此返回,如需查看全部字段请用 `field get`。 diff --git a/skills/mono/references/products/aitable/aitable-primary-doc.md b/skills/mono/references/products/aitable/aitable-primary-doc.md new file mode 100644 index 00000000..eefda277 --- /dev/null +++ b/skills/mono/references/products/aitable/aitable-primary-doc.md @@ -0,0 +1,55 @@ +# 主键文档管理 + +## 适用场景 + +当需要为 AI 表格中的记录创建或查询关联的主键文档时使用。主键文档是 primaryDoc 类型字段对应的钉钉在线文档,可通过 `dws doc` 进行内容读写。 + +## 命令 + +### 查询主键文档 + +```bash +dws aitable record primary-doc-get --base-id BASE_ID --table-id TABLE_ID --record-id RECORD_ID +``` + +**参数:** +- `--base-id`(必填):Base ID +- `--table-id`(必填):Table ID +- `--record-id`(必填):Record ID + +**返回:** `data.nodeId` — 主键文档的 nodeId,可直接传给 `dws doc read/update` 的 `--node` 参数。若该记录尚未创建主键文档,`nodeId` 为 null。 + +### 创建主键文档 + +```bash +dws aitable record primary-doc-create --base-id BASE_ID --table-id TABLE_ID --field-id FIELD_ID --record-id RECORD_ID +``` + +**参数:** +- `--base-id`(必填):Base ID +- `--table-id`(必填):Table ID +- `--field-id`(必填):主键字段 ID,必须是 primaryDoc 类型(通过 `dws aitable table get` 查看字段类型) +- `--record-id`(必填):Record ID + +**返回:** `data.nodeId` — 创建或已存在的主键文档 nodeId。 + +**幂等性:** 若该记录已有主键文档,直接返回已有文档的 nodeId,不会重复创建。 + +## 注意事项 + +- `fieldId` 必须是 primaryDoc 类型,否则返回 `INVALID_FIELD_TYPE` 错误 +- 传入不存在的 `recordId` 会返回 `RECORD_NOT_FOUND` 错误 +- 创建后可通过 `dws doc update --node ` 写入文档内容,或 `dws doc read --node ` 读取 + +## 典型工作流 + +```bash +# 1. 查询表结构,拿到 primaryDoc 字段的 fieldId +dws aitable table get --base-id BASE_ID --table-ids TABLE_ID + +# 2. 为某条记录创建主键文档 +dws aitable record primary-doc-create --base-id BASE_ID --table-id TABLE_ID --field-id FIELD_ID --record-id RECORD_ID + +# 3. 拿到返回的 nodeId,用 dws doc 写入内容 +dws doc update --node --content "# 项目方案\n\n文档正文内容..." +``` diff --git a/skills/mono/references/products/aitable/aitable-record-create.md b/skills/mono/references/products/aitable/aitable-record-create.md index e64f1865..bf605899 100644 --- a/skills/mono/references/products/aitable/aitable-record-create.md +++ b/skills/mono/references/products/aitable/aitable-record-create.md @@ -27,12 +27,13 @@ Flags: | cells key 用字段名 | ❌ cells key 必须是 fieldId(如 `fldXXX`),不是字段名称(如 `"课程名称"`) | | 不先获取 fieldId | ❌ 必须先 `table get` 获取 fieldId,再写入记录 | | 单次超 100 条 | ❌ 单次最多 100 条,超过需分批 | +| 附件/图片字段直传 URL | ❌ 严禁 `{"url":"https://..."}` — 会触发 TIMEOUT_ERROR。必须先 `attachment upload` 获取 `fileToken`,再用 `{"fileToken":"ft_xxx"}` 写入。详见 [aitable-attachment.md](./aitable-attachment.md) | ## 正确流程 ```bash # 先获取 fieldId -dws aitable table get --base-id --table-ids --format json +dws aitable table get --base-id --table-id --format json # 从返回中提取 fieldId(如 fldABC123) # 再用 fieldId 写入记录 diff --git a/skills/mono/references/products/aitable/aitable-record-history.md b/skills/mono/references/products/aitable/aitable-record-history.md new file mode 100644 index 00000000..f02d3d87 --- /dev/null +++ b/skills/mono/references/products/aitable/aitable-record-history.md @@ -0,0 +1,97 @@ +# 行记录变更历史(record history-list) + +按 recordId 查询单条记录的全部变更历史,用于审计、回溯字段变更、定位操作人。 + +## 命令 + +``` +dws aitable record history-list \ + --base-id BASE_ID --table-id TABLE_ID --record-id REC_ID \ + [--offset N] [--limit M] +``` + +| flag | 说明 | +|------|------| +| `--base-id` | 所属 Base ID(必填,可用 `--base` 别名) | +| `--table-id` | 所属 Table ID(必填) | +| `--record-id` | 目标记录 ID(必填,单条;不支持批量) | +| `--offset` | 分页偏移量,默认 0 | +| `--limit` | 每页返回数量,范围 [1, 50],默认 20 | + +## 返回结构 + +```jsonc +{ + "data": { + "histories": [ + { + "type": "field_change", // 变更类型 + "action": "update", // 操作动作: create / update / delete + "newValue": "{\"...\":\"...\"}", // 变更后的值(JSON 字符串) + "oldValue": "{\"...\":\"...\"}", // 变更前的值(JSON 字符串) + "operateTime": 1733123456789, // 操作时间(毫秒级时间戳) + "typeChangedFields": "{...}", // 类型变更的字段信息(JSON 字符串) + "version": 7 // 版本号(单调递增) + } + ] + } +} +``` + +`newValue` / `oldValue` / `typeChangedFields` 是 JSON 字符串(不是 JSON 对象),需要二次 `JSON.parse` 才能拿到结构化值。 + +## 字段含义速查 + +| 字段 | 用途 | +|------|------| +| `type` | 高层分类:`record_create` / `field_change` / `record_delete` 等。先按 type 过滤大类。 | +| `action` | 三态:`create` / `update` / `delete`。比 type 粗,但便于按"动作"统计。 | +| `version` | 单调递增整数;同一 record 越新值越大。**用作"上一条 vs 这一条"的稳定排序键**。 | +| `operateTime` | 毫秒时间戳;可格式化成可读时间。多条同 version 的极端场景用 operateTime 兜底排序。 | + +## 典型用法 + +### 1. 看一条记录被改过几次 + +```bash +dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --format json \ + | jq '.data.histories[] | {version, action, operateTime}' +``` + +### 2. 翻页拉全量历史 + +```bash +# 第 1 页(最新 20 条) +dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --limit 50 --offset 0 + +# 第 2 页 +dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --limit 50 --offset 50 +``` + +`limit` 上限 50,需要更多请增加 `offset` 翻页。 + +### 3. 回溯某字段最近一次值 + +```bash +dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --limit 50 --format json \ + | jq '[.data.histories[] | select(.action == "update")][0].oldValue' +``` + +### 4. 找出删除事件(如果存在 delete history) + +```bash +dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --format json \ + | jq '.data.histories[] | select(.action == "delete") | {version, operateTime}' +``` + +## 注意事项 + +- 一次只能查一条 record;如需批量审计多条记录请循环调用。 +- 仅返回**字段值变更**与**记录生命周期事件**;视图、字段定义、表结构变更不在此 history 里。 +- 历史保留时长由 server 决定,过老的记录可能不再返回。 + +## 与其他 record 命令的关系 + +- 想看记录"现在长什么样" → `record query` / `record get` +- 想看记录"过去长什么样、什么时候改的" → `record history-list`(本命令) +- 想看"这张表整体改过什么" → 当前 CLI 不支持表级 history;只能逐 record 查 diff --git a/skills/mono/references/products/aitable/aitable-record-name-key.md b/skills/mono/references/products/aitable/aitable-record-name-key.md new file mode 100644 index 00000000..65a7fe89 --- /dev/null +++ b/skills/mono/references/products/aitable/aitable-record-name-key.md @@ -0,0 +1,47 @@ +# 行命名规则枚举键(recordNameKey)映射 + +`dws aitable table update --record-name-key <枚举键>` 用于设置数据表的"行命名规则"——卡片/详情页里"行"的展示别名。**取值是固定枚举,不是字段 ID**;传非法值服务端返回 `INVALID_RECORD_NAME_KEY`。 + +## 中文 → 枚举键(按 UI 下拉顺序) + +| 用户说 | --record-name-key | 用户说 | --record-name-key | +|---|---|---|---| +| 记录 | `ji_lu`(默认) | 项目 | `project` | +| 任务 | `task` | 事件 | `event` | +| 请求 | `request` | 活动 | `campaign` | +| 目标 | `objective` | 交付物 | `deliverable` | +| 资产 | `asset` | 客户 | `customer` | +| 订单 | `order` | 联系人 | `contact` | +| 物料/物品 | `item` | 问题 | `question` 或 `issue` | +| 工单 | `ticket` | 候选人 | `candidate` | +| 商机/机会 | `opportunity` | 会议 | `meeting` | +| 成员 | `member` | OKR | `okr` | + +## 其他常用键(按场景分组) + +- **业务流程**:`approval` / `application` / `case` / `decision` / `delivery` / `payment` / `purchase_order` / `quote` / `release` +- **HR / 财务**:`employee` / `expense` / `budget` / `invoice` +- **产品 / 研发**:`feature` / `feedback` / `idea` / `bug` / `requirement` / `risk` / `sprint` / `story` / `subtask` / `epic` +- **CRM**:`account` / `lead` / `prospect` / `deal` +- **运营 / 支持**:`note` / `report` / `topic` / `session` / `service` +- **资源 / 通用**:`file` / `document` / `product` / `team` / `user` / `vendor` / `key_result` / `metric` + +完整集合较大(共 273 个),服务端校验;以上未列出的合法键也可直接传(如 `goal` / `okr` / `pillar` / `phase` / `milestone` 等)。 + +## 使用示例 + +```bash +# 用户说"把这张表的行叫'任务'吧" → 传 task +dws aitable table update --base-id BASE --table-id TBL --record-name-key task + +# 用户说"换成项目" → 传 project +dws aitable table update --base-id BASE --table-id TBL --record-name-key project + +# 用户说"恢复成默认(记录)" → 传 ji_lu +dws aitable table update --base-id BASE --table-id TBL --record-name-key ji_lu +``` + +## 注意 + +- recordNameKey **不会在 `table get` 响应里回显**(`get_tables` DTO 设计上不暴露该字段);写入是否成功以 `table update` 的 set response 是否回填 `recordNameKey` 字段为准。 +- 中文别名是 server 内置 i18n,UI 显示用户对应的国际化文案,CLI 必须传英文枚举键。 diff --git a/skills/mono/references/products/aitable/aitable-record-query.md b/skills/mono/references/products/aitable/aitable-record-query.md index 8d04ab1b..20ed92da 100644 --- a/skills/mono/references/products/aitable/aitable-record-query.md +++ b/skills/mono/references/products/aitable/aitable-record-query.md @@ -72,3 +72,49 @@ dws aitable record query --base-id X --table-id Y --all --cursor "上次返回 - `--sort` 用 `"order":"desc"` → 必须用 `"direction":"desc"` - 不加 `--field-ids` 拉全字段 → 大表响应体积过大 - 全量拉取后在 context 里手动统计 → 应优先用 `--filters` 服务端过滤 + +## record query-empty — 找空行 + +`record query-empty` 是与 `record query` 平行的独立子命令,专门按表内顺序扫描出"完全没填用户字段"的空行。 + +```bash +dws aitable record query-empty --base-id BASE_ID --table-id TABLE_ID +``` + +| flag | 说明 | +|------|------| +| `--base-id` / `--base` | 必填 | +| `--table-id` | 必填 | +| `--limit` | 单次**扫描预算**(不是返回数);范围 [1, 100],默认 100 | +| `--cursor` | 分页游标。响应中 `nextCursor` 非空 → 用它翻页继续扫;nextCursor 为空(或不存在)→ 已扫完整表 | + +返回结构: + +```jsonc +{ "data": { "records": [...], "nextCursor": "..." } } +``` + +### 关键语义 + +1. **`--limit` 是扫描预算不是返回数**:可能扫了 100 条但全部非空,本页 `records: []`。 +2. **本页空 records ≠ 全表无空行**:必须看 `nextCursor`,nextCursor 还在就要继续翻。 +3. **空行定义**:除系统字段(recordId / 创建人 / 创建时间 / 修改人 / 修改时间)外,所有 cell 都是 null、空字符串、空集合或空 Map。一般是用户在 UI 上"插入空行"产生的。 + +### 典型用法 + +```bash +# 扫一页,看本页有没有空行 +dws aitable record query-empty --base-id BASE --table-id TBL + +# 翻页 +dws aitable record query-empty --base-id BASE --table-id TBL --cursor <上次的nextCursor> + +# 把整表扫完(手动循环 cursor) +NC="" +while : ; do + R=$(dws aitable record query-empty --base-id BASE --table-id TBL ${NC:+--cursor "$NC"} --format json) + echo "$R" | jq '.data.records[] | .recordId' + NC=$(echo "$R" | jq -r '.data.nextCursor // empty') + [ -z "$NC" ] && break +done +``` diff --git a/skills/mono/references/products/aitable/aitable-record-share.md b/skills/mono/references/products/aitable/aitable-record-share.md new file mode 100644 index 00000000..6e5eea51 --- /dev/null +++ b/skills/mono/references/products/aitable/aitable-record-share.md @@ -0,0 +1,54 @@ +# 行记录分享链接(record share-url) + +按 recordId 批量获取记录的分享链接,把某行单独发给同事查看。 + +## 命令 + +``` +dws aitable record share-url \ + --base-id BASE_ID --table-id TABLE_ID \ + --record-ids rec1,rec2,rec3 \ + [--view-id VIEW_ID] +``` + +| flag | 说明 | +|------|------| +| `--base-id` | 所属 Base ID(必填,可用 `--base` 别名) | +| `--table-id` | 所属 Table ID(必填) | +| `--record-ids` | 目标 Record ID 列表,CSV 逗号分隔,**单次最多 20 条**(必填) | +| `--view-id` | 视图 ID(可选)。带上后链接打开会落在该视图上下文里 | + +## 返回结构 + +```jsonc +{ + "data": { + "items": [ + { "recordId": "rec1", "shareUrl": "https://..." }, + { "recordId": "rec2", "shareUrl": "https://..." } + ] + } +} +``` + +`shareUrl` 为 null 表示该条获取失败(不影响其他条目)。 + +## 典型用法 + +```bash +# 一次拿一条记录的链接 +dws aitable record share-url --base-id BASE --table-id TBL --record-ids rec1 + +# 批量拿,配合 jq 过滤出 url +dws aitable record share-url --base-id BASE --table-id TBL --record-ids rec1,rec2,rec3 --format json \ + | jq '.data.items[] | {recordId, shareUrl}' + +# 带视图上下文(链接打开时落在指定视图) +dws aitable record share-url --base-id BASE --table-id TBL --record-ids rec1 --view-id viw_VIP +``` + +## 注意事项 + +- **单次最多 20 条**,超出请客户端拆批。 +- 该链接是分享链接(不是源文档链接),打开后看到的是该 record 的只读详情页。 +- 取消单条分享 / 关闭整表分享当前 CLI 不支持,需要在 AI 表格 Web 端操作。 diff --git a/skills/mono/references/products/aitable/aitable-record-upsert.md b/skills/mono/references/products/aitable/aitable-record-upsert.md new file mode 100644 index 00000000..1642282b --- /dev/null +++ b/skills/mono/references/products/aitable/aitable-record-upsert.md @@ -0,0 +1,89 @@ +# 行记录 Upsert(record upsert) + +按 `recordId` 是否存在,自动把入参拆分到 update 链路或 create 链路:批次混合"已存在改 + 新出现建"时用,省掉客户端按 ID 分批的逻辑。 + +## 命令 + +``` +dws aitable record upsert \ + --base-id BASE_ID --table-id TABLE_ID \ + --records '[{"recordId":"<可选>","cells":{...}}, ...]' +``` + +| flag | 说明 | +|------|------| +| `--base-id` | 必填(可用 `--base` 别名) | +| `--table-id` | 必填 | +| `--records` | 待 upsert 的记录 JSON 数组,**单次最多 100 条**(必填)| +| `--records-file` | 从文件读入(命令行 JSON 太长时用),与 `--records` 互斥优先级更高 | + +## --records 结构 + +每项 JSON: + +```jsonc +{ + "recordId": "rec1", // 可选;带 → update,缺省 → create + "cells": { // 必填;key 是 fieldId,value 按字段类型 + "fldTitleId": "新标题", + "fldNumberId": 42 + } +} +``` + +`cells` 写入格式与 `record create` / `record update` **完全一致**(key 必须是 fieldId 不是字段名;按字段类型见 [aitable-cell-value.md](./aitable-cell-value.md))。 + +## 返回结构 + +```jsonc +{ + "data": { + "createdRecordIds": ["recX", "recY"], // 不带 recordId 的项产出 + "updatedRecordIds": ["recA", "recB"] // 带 recordId 的项产出 + } +} +``` + +`createdRecordIds` 顺序对应入参里**不带 recordId**的项(按出现顺序汇总),同理 `updatedRecordIds` 对应**带 recordId**的项。 + +## 典型用法 + +```bash +# 1) 全部新建:所有项都不带 recordId +dws aitable record upsert --base-id BASE --table-id TBL --records '[ + {"cells":{"fldTitleId":"任务1","fldStatusId":"待办"}}, + {"cells":{"fldTitleId":"任务2","fldStatusId":"待办"}} +]' + +# 2) 全部更新:所有项都带 recordId +dws aitable record upsert --base-id BASE --table-id TBL --records '[ + {"recordId":"rec1","cells":{"fldStatusId":"已完成"}}, + {"recordId":"rec2","cells":{"fldStatusId":"已完成"}} +]' + +# 3) 混合:第 1 条更新(带 recordId),第 2 条创建(不带) +dws aitable record upsert --base-id BASE --table-id TBL --records '[ + {"recordId":"rec1","cells":{"fldStatusId":"已完成"}}, + {"cells":{"fldTitleId":"新增任务","fldStatusId":"待办"}} +]' + +# 4) 长 JSON 用文件 +dws aitable record upsert --base-id BASE --table-id TBL --records-file ./batch.json +``` + +## 与 record create / record update 的关系 + +| 场景 | 命令 | +|------|------| +| 确定全是新增 | `record create` | +| 确定全是更新(每条独立 cells) | `record update` | +| 确定全是更新(共享同一 cells) | `record batch-update` | +| **不确定有没有,按 recordId 自动分流** | `record upsert`(本命令) | + +`record upsert` 的 `--records` 入参格式与 `record update` 完全相同,唯一差别是 `recordId` 字段在 upsert 里是可选的。如果批次确定全是更新或全是新建,用专用命令更清晰;批次混合时(典型场景:定时同步外部数据,源里既有已存在的也有新出现的),用 upsert。 + +## 注意事项 + +- **单次最多 100 条**(创建 + 更新合计),超出请客户端拆批。 +- `cells` 的 key 必须是 fieldId 不是字段名(先用 `record query` 或 `field get` 拿 fieldId)。 +- 只读字段(formula / lookup / 系统字段)不能写入 — upsert 链路与 update 链路同样限制。 diff --git a/skills/mono/references/products/aitable/aitable-view-config.md b/skills/mono/references/products/aitable/aitable-view-config.md new file mode 100644 index 00000000..6b22ec03 --- /dev/null +++ b/skills/mono/references/products/aitable/aitable-view-config.md @@ -0,0 +1,203 @@ +# 视图配置(view get/update ) + +按属性局部读/写视图配置。每个属性独立子命令,typed flag 友好,agent 不必拼 JSON。 +向后兼容:`view update --config '{...}'` 一次多属性入口仍可用。 + +## viewType × 支持矩阵 + +| viewType | card | timebar | aggregate | filter / sort / group | visible-fields | field-widths | name | +|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| Grid | | | ✅ | ✅ | ✅ | ✅ | ✅ | +| Kanban | ✅ | | | ✅ | ✅ | | ✅ | +| Gallery | ✅ | | | ✅ | ✅ | | ✅ | +| Gantt | | ✅ | | ✅ | ✅ | | ✅ | +| Calendar | | | | ✅ | ✅ | | ✅ | +| FormDesigner | (走 `form` 系列命令) | | | | | | | + +> card 在 Kanban 走 `kanbanCard`,在 Gallery 走 `galleryCard`,CLI 自动按 viewType dispatch(preflight 1 次 `get_views`)。timebar 仅 Gantt 支持;Calendar 服务端未暴露任何 timebar 配置。 + +> **Gantt 视图必须两步创建**:`view create --view-type Gantt` 只创建空壳(`ganttTimebar: {}`),**必须**紧跟 `view update timebar --start-field <日期字段ID>` 绑定时间轴字段,否则视图打开是空白。`create_view` 的 `--config` 中传入 `ganttTimebar` 会被服务端忽略。 + +## 读取:view get + +所有 `view get ` 共用 `--base-id` / `--table-id` / `--view-id`,输出是该属性子块的 JSON(不存在时输出 `{}`)。viewType 不匹配会报错并指明应该选哪种视图。 + +```bash +dws aitable view get card --view-id VIEW_ID --format json # Kanban / Gallery +dws aitable view get timebar --view-id VIEW_ID --format json # Gantt +dws aitable view get aggregate --view-id VIEW_ID --format json # Grid +dws aitable view get filter --view-id VIEW_ID --format json # 所有 +dws aitable view get sort --view-id VIEW_ID --format json +dws aitable view get group --view-id VIEW_ID --format json +dws aitable view get visible-fields --view-id VIEW_ID --format json +dws aitable view get field-widths --view-id VIEW_ID --format json # Grid +``` + +## 写入:view update + +所有 `view update ` 共用 `--base-id` / `--table-id` / `--view-id`。 +**typed flag + `--json` 可混用**;冲突时 typed flag 优先并 stderr 提示。 +card / timebar / aggregate 三类写入有 viewType 校验(preflight 1 次 get_views)。 + +### view update card(Kanban / Gallery) + +服务端按 viewType 分发到 `kanbanCard` 或 `galleryCard`。typed flag 共享。 + +| flag | 类型 | 说明 | +|------|------|------| +| `--cover-field-id` | string | 封面字段 ID(Kanban / Gallery 通用),与 `--no-cover` 互斥 | +| `--no-cover` | bool | 清除封面(等价 `coverFieldId="NONE"`) | +| `--cover-resize-mode` | string | `cover` / `contain` / `stretch` | +| `--hidden-field-title` | bool | 隐藏字段名标题(仅 Kanban 生效) | +| `--cover-mode` | string | `none` / `auto` / `custom`(仅 Gallery 生效) | +| `--display-field-name` | bool | 是否显示字段名(仅 Gallery 生效) | +| `--json` | JSON | 完整 card 子块对象 | + +```bash +dws aitable view update card --view-id KANBAN_ID --cover-field-id fldAttachment --cover-resize-mode contain +dws aitable view update card --view-id KANBAN_ID --no-cover +dws aitable view update card --view-id GALLERY_ID --cover-mode auto +dws aitable view update card --view-id GALLERY_ID --json '{"coverMode":"custom","coverFieldId":"fldX","displayFieldName":true}' +``` + +### view update timebar(仅 Gantt) + +| flag | 类型 | 说明 | +|------|------|------| +| `--start-field` | string (date fieldId) | 开始日期字段 | +| `--end-field` | string (date fieldId) | 结束日期字段 | +| `--display-field-id` | string | 时间条上显示的标题字段 | +| `--timeline-scale` | string | `year` / `quarter` / `month` / `weeks` | +| `--color-configs` | JSON 数组 | 颜色配置数组(结构由下游协议定义;清空传 `[]`) | +| `--official-holiday` | bool | 是否标注法定节假日 | +| `--json` | JSON | 完整 ganttTimebar 子块 | + +```bash +dws aitable view update timebar --view-id GANTT_ID --start-field fldStart --end-field fldEnd --timeline-scale month +dws aitable view update timebar --view-id GANTT_ID --official-holiday=true +``` + +### view update aggregate(仅 Grid) + +值是 `map[fieldId]→AggregateAction string`;传 null 清除某个字段聚合。 + +| flag | 类型 | 说明 | +|------|------|------| +| `--field-id` | string | 配合 `--action` 设置**单字段**聚合 | +| `--action` | string | `SUM`/`AVG`/`MAX`/`MIN`/`MEDIAN`/`RANGE`/`TOTAL`/`DISTINCT`/`EXIST`/`UN_EXIST`/`CHECKED`/`EARLIEST_DATE` 等(按字段类型可用) | +| `--clear-field-id` | string (CSV) | 一/多个字段 ID,清除其聚合 | +| `--json` | JSON | 完整 aggregate map | + +```bash +dws aitable view update aggregate --view-id GRID_ID --field-id fldX --action SUM +dws aitable view update aggregate --view-id GRID_ID --clear-field-id fldA,fldB +dws aitable view update aggregate --view-id GRID_ID --json '{"fldX":"AVG","fldY":null}' +``` + +### view update field-widths(仅 Grid) + +| flag | 类型 | +|------|------| +| `--field-id` + `--width` | string + int(单字段) | +| `--json` | `{fldId: width, ...}` | + +```bash +dws aitable view update field-widths --view-id GRID_ID --field-id fldX --width 200 +dws aitable view update field-widths --view-id GRID_ID --json '{"fldA":120,"fldB":200}' +``` + +### view update visible-fields(通用) + +整组替换可见字段列表与顺序。首列字段(primaryDoc)必须保留在数组第一位。 + +> ⚠️ 注意:服务端**只接受 reorder,不接受真"隐藏字段"**——如果传入的列表比当前 columns 短,缺失的字段不会被隐藏。需要真正隐藏字段请到 AI 表格 Web UI。 + +| flag | 类型 | +|------|------| +| `--field-ids` | string (CSV) | +| `--json` | string 数组 JSON(与 `--field-ids` 同传时 `--json` 优先) | + +```bash +dws aitable view update visible-fields --view-id VIEW_ID --field-ids fldPrimary,fldA,fldB +dws aitable view update visible-fields --view-id VIEW_ID --json '["fldPrimary","fldA","fldB"]' +``` + +### view update filter / sort / group(通用,纯 --json) + +```bash +dws aitable view update filter --view-id VIEW_ID --json '[{"operator":"and","operands":[{"operator":"eq","operands":["fldX","value"]}]}]' +dws aitable view update sort --view-id VIEW_ID --json '[{"fieldId":"fldX","direction":"asc"}]' +dws aitable view update group --view-id VIEW_ID --json '[{"fieldId":"fldX","direction":"asc"}]' +``` + +> filter/sort/group 入参格式与 `record query --filters`(对象格式)**不同**:view config 这边外层必须是数组。传对象 CLI 会自动 wrap,建议直接用数组。详见 [aitable-filter-sort.md](./aitable-filter-sort.md)。 + +### view update name(重命名) + +```bash +dws aitable view update name --view-id VIEW_ID --name "新视图名" +``` + +等价于 `dws aitable view update --view-id VIEW_ID --name "新视图名"`,无 `config` 参数。 + +## 服务端字段速查(与 dws CLI 关系) + +| dws 子命令 | 服务端 `update_view.config` 子键 | 服务端 Java 模型 | +|---|---|---| +| `view update card`(Kanban) | `kanbanCard` | `KanbanCardUpdateInput` | +| `view update card`(Gallery) | `galleryCard` | `GalleryCardUpdateInput` | +| `view update timebar` | `ganttTimebar` | `GanttTimebarUpdateInput` | +| `view update aggregate` | `aggregate` | `Map` | +| `view update visible-fields` | `visibleFieldIds` | `List` | +| `view update filter / sort / group` | `filter` / `sort` / `group` | `List` | +| `view update field-widths` | `fieldWidths` | `Map` | +| `view update name` | (不在 config 内)`newViewName` 顶层 | — | + +## 典型工作流 + +### 排查"Kanban 卡片为啥不显示封面" + +```bash +dws aitable view get card --view-id KANBAN_ID --format json +# → 看 coverFieldId 是不是 "NONE" 或缺失;不是再看 coverResizeMode 是不是 contain 导致裁掉 +``` + +### 创建可用的 Gantt 视图(必须两步) + +```bash +# 第 1 步:创建 Gantt 视图 +dws aitable view create --base-id BASE_ID --table-id TABLE_ID \ + --view-type Gantt --name "项目甘特图" -f json +# → 记录返回的 viewId + +# 第 2 步(必须):绑定日期字段,否则视图为空 +dws aitable view update timebar --base-id BASE_ID --table-id TABLE_ID \ + --view-id VIEW_ID --start-field fldDateStart +# 可选:加结束日期、标题字段、时间尺度 +# --end-field fldDateEnd --display-field-id fldName --timeline-scale month +``` + +### 把 Gantt 时间轴改成季度尺度并加节假日 + +```bash +dws aitable view update timebar --view-id GANTT_ID \ + --timeline-scale quarter --official-holiday=true +``` + +### 用 dws 脚本批量替换 Kanban 封面字段 + +```bash +for v in viw1 viw2 viw3; do + dws aitable view update card --view-id $v --cover-field-id fldNewCover --cover-resize-mode cover --format json | jq .status +done +``` + +### 一次性多属性更新(仍走 legacy --config) + +```bash +dws aitable view update --view-id VIEW_ID --config '{ + "visibleFieldIds":["fldPrimary","fldA","fldB"], + "filter":[{"operator":"and","operands":[]}], + "kanbanCard":{"coverFieldId":"fldImg","coverResizeMode":"contain"} +}' +``` diff --git a/skills/mono/references/products/aitable/aitable-view-extras.md b/skills/mono/references/products/aitable/aitable-view-extras.md new file mode 100644 index 00000000..23db8059 --- /dev/null +++ b/skills/mono/references/products/aitable/aitable-view-extras.md @@ -0,0 +1,198 @@ +# 视图扩展操作(lock / frozen-cols / row-height / fill-color-rule / duplicate) + +本文档讲 5 项视图操作命令: + +- 锁定 / 解锁视图:`view lock` / `view get lock` +- 冻结列:`view update frozen-cols` / `view get frozen-cols` +- 行高:`view update row-height` / `view get row-height` +- 数据高亮规则(条件填色):`view update fill-color-rule` / `view get fill-color-rule` +- 复制视图:`view duplicate` + +> **与 [aitable-view-config.md](./aitable-view-config.md) 的分工**: +> - `aitable-view-config.md` 讲 `view get/update ` 中 8 个属性:filter / sort / group / visible-fields / field-widths / aggregate / card / timebar。 +> - 本文档讲上面 5 项额外能力(包括 attr 形式的 frozen-cols / row-height / fill-color-rule,以及顶层独立的 lock / duplicate)。 +> 这 5 项**不能**通过 `view update --config '{...}'` 写入,必须用各自专属子命令。 + +## 命令矩阵 + +| 子命令 | 用途 | 必填参数 | 适用 viewType | +|---|---|---|---| +| `view lock [--off]` | 锁定(默认)/ 解锁视图 | `--base-id --table-id --view-id` | 全部 | +| `view get lock` | 读取锁定状态 | `--base-id --table-id --view-id` | 全部 | +| `view update frozen-cols --count N` | 冻结左侧 N 列(0 取消) | `--base-id --table-id --view-id --count` | Grid | +| `view get frozen-cols` | 读取冻结列数 | `--base-id --table-id --view-id` | Grid | +| `view update row-height --cell-height N` | 设置单元格高度(像素) | `--base-id --table-id --view-id --cell-height` | Grid | +| `view get row-height` | 读取单元格高度 | `--base-id --table-id --view-id` | Grid | +| `view update fill-color-rule --json '[...]'` | 全量覆盖条件填色规则 | `--base-id --table-id --view-id --json` | Grid | +| `view get fill-color-rule` | 读取条件填色规则 | `--base-id --table-id --view-id` | 全部(其他视图返回 `[]`) | +| `view duplicate [--new-name X]` | 复制视图 | `--base-id --table-id --view-id` | 全部 | + +## 视图锁定 / 解锁 + +```bash +# 锁定(默认) +dws aitable view lock --view-id VIEW_ID + +# 解锁 +dws aitable view lock --view-id VIEW_ID --off + +# 查询当前是否锁定 +dws aitable view get lock --view-id VIEW_ID --format json +# → {"data": {"baseId": ..., "tableId": ..., "viewId": ..., "locked": true|false}} +``` + +锁定的视图禁止他人修改其配置(filter/sort/group/字段顺序等),但记录读写不受影响。锁定状态可重复 set,幂等。 + +## 冻结列(仅 Grid) + +```bash +# 冻结从首列起 1 列 +dws aitable view update frozen-cols --view-id VIEW_ID --count 1 + +# 取消冻结 +dws aitable view update frozen-cols --view-id VIEW_ID --count 0 + +# 查询当前冻结列数 +dws aitable view get frozen-cols --view-id VIEW_ID --format json +# → {"data": {..., "count": 1}} count 为 null 表示视图未显式设置 +``` + +`--count` 必须 ≥ 0;负数会被拒绝。 + +## 行高(仅 Grid) + +⚠️ **`--cell-height` 只接受 4 档枚举:32 / 56 / 88 / 128**(与前端 CELL_HEIGHTS 约定一致),其他值会被拒绝。默认值为 32。 + +```bash +# 设置行高 — 推荐档位 32 / 56 / 88 / 128 +dws aitable view update row-height --view-id VIEW_ID --cell-height 56 + +# 查询当前行高 +dws aitable view get row-height --view-id VIEW_ID --format json +# → {"data": {..., "cellHeight": 56}} cellHeight 为 null 表示视图未显式设置(前端按 32 渲染) +``` + +## 数据高亮规则(条件填色,仅 Grid) + +`view update fill-color-rule` **整组覆盖**,传 `--json '[]'` 清空所有规则。 + +### 规则结构 + +每条规则 JSON 结构: + +```jsonc +{ + "type": "cell" | "row" | "column" | "preRow", + "formatFieldId": "fldX", // 命中规则后被高亮的字段(cell/column 类型有意义) + "format": { "color": "firstLine5" }, // ⚠️ 必须用 FORMAT_COLORS 代号,不接受 hex + "filters": [ // 当前固定 1 条 + { + "fieldId": "fldX", // ⚠️ 不是 operands[0] + "symbol": "GT", // ⚠️ 不是 operator;大写枚举 + "value": 100 // 部分 symbol(EXIST/UN_EXIST)不需要 value + } + ] +} +``` + +### color 合法值(FORMAT_COLORS) + +`firstLine1` ~ `firstLine11`(共 11 档色码,对应前端调色盘)。**不接受 `#FF0000` 这种 hex**。 + +### filter.symbol 合法值 + +| 类别 | symbol | +|---|---| +| 数值/通用比较 | `GT` / `LT` / `GTE` / `LTE` / `EQ` / `NE` | +| 文本 | `CONTAIN` / `EXCLUSIVE` | +| 存在性(无 value) | `EXIST` / `UN_EXIST` | +| 多选 / 集合 | `ALL_OF` / `ANY_OF` / `NONE_OF` | +| 日期 | `BEFORE` / `AFTER` / `NOT_BEFORE` / `NOT_AFTER` / `DATE_EQ` / `FROM_NOW` / `DATE_BETWEEN` | + +> **与 `record query --filters` / `view update filter` 的格式不同**:那两处用 `{operator, operands}` 结构;这里是 `{fieldId, symbol, value}`。不要混用。 + +### 典型用法 + +```bash +# 1) 给金额字段 > 100 的单元格上 firstLine5 色 +dws aitable view update fill-color-rule --view-id GRID_ID --json '[ + { + "type":"cell", + "formatFieldId":"fldAmount", + "format":{"color":"firstLine5"}, + "filters":[{"fieldId":"fldAmount","symbol":"GT","value":100}] + } +]' + +# 2) 清空所有规则 +dws aitable view update fill-color-rule --view-id GRID_ID --json '[]' + +# 3) 查询当前规则 +dws aitable view get fill-color-rule --view-id GRID_ID --format json +# → {"data": [...]} 数组 +``` + +> **写入后请用 `view get fill-color-rule` 二次确认实际生效**,以读到的 `data` 数组为准。 + +## 复制视图 + +```bash +# 显式命名 +dws aitable view duplicate --view-id VIEW_ID --new-name "副本视图" + +# 系统自动命名(一般是 "原视图名 (副本)") +dws aitable view duplicate --view-id VIEW_ID --format json +# → {"data": {..., "viewId": "<新视图ID>", "sourceViewId": "<原视图ID>", "viewName": "..."}} +``` + +复制会保留源视图的 filter / sort / group / visible-fields / card / timebar 等全部配置;新视图的 viewId 与源视图独立。 + +## 这些字段不能用 `view update --config '{...}'` 写 + +下列字段必须用对应的专属子命令;如果错塞进 `view update --config`,CLI 会在 stderr 提示对应子命令并拒绝把字段当 view config 处理: + +| 错误用法 | 应改用 | +|---|---| +| `--config '{"flags":1}'` | `view lock` / `view lock --off` | +| `--config '{"frozenColCount":2}'` | `view update frozen-cols --count N` | +| `--config '{"cellHeight":56}'` | `view update row-height --cell-height N` | +| `--config '{"rowHeightLevel":"tall"}'` | `view update row-height --cell-height N`(合法档位 32/56/88/128) | +| `--config '{"conditionalFormats":[...]}'` | `view update fill-color-rule --json '[...]'` | + +## 典型工作流 + +### 配置一个"金额超阈值红色高亮"的 Grid 视图 + +```bash +BASE=baseXXX; TABLE=tblYYY; VIEW=viwGridZZ; FLD=fldAmount + +# 1) 关键字段冻结,避免横向滚动看不到 +dws aitable view update frozen-cols --base-id $BASE --table-id $TABLE --view-id $VIEW --count 1 + +# 2) 加大行高让数据更易读 +dws aitable view update row-height --base-id $BASE --table-id $TABLE --view-id $VIEW --cell-height 56 + +# 3) 金额 > 100 的单元格上色 +dws aitable view update fill-color-rule --base-id $BASE --table-id $TABLE --view-id $VIEW --json "[ + {\"type\":\"cell\",\"formatFieldId\":\"$FLD\",\"format\":{\"color\":\"firstLine5\"}, + \"filters\":[{\"fieldId\":\"$FLD\",\"symbol\":\"GT\",\"value\":100}]} +]" + +# 4) 锁定视图,防止他人改坏 +dws aitable view lock --base-id $BASE --table-id $TABLE --view-id $VIEW +``` + +### 复制一个"金牌客户"视图给销售团队 + +```bash +dws aitable view duplicate --view-id viw_VIP_template --new-name "金牌客户-华东区" +# 取返回里 data.viewId 进一步定制 +``` + +### 排查"我设置了高亮规则为啥没生效" + +```bash +# 看实际生效的 conditionalFormats +dws aitable view get fill-color-rule --view-id VIEW_ID --format json +# → 如果是 [] 说明上次写入失败;常见原因:color 用了 hex(必须 firstLineN)/ filter 用了 operator(必须 symbol) +``` diff --git a/skills/mono/references/products/aitable/aitable-workflow.md b/skills/mono/references/products/aitable/aitable-workflow.md new file mode 100644 index 00000000..a223155a --- /dev/null +++ b/skills/mono/references/products/aitable/aitable-workflow.md @@ -0,0 +1,180 @@ +# workflow — 自动化工作流管理 + +启停 / 查看 / 列出 Base 下的自动化工作流(在 AI 表格 Web 端配置的 "当 X 时自动 Y" 流程)。 +适用场景:用户问 "停掉这个流程"、"看下都有哪些自动化流程"、"流程 X 的配置是什么"。 + +## 命令一览 + +| 命令 | 用途 | +|------|------| +| `workflow list` | 列出 Base 下所有工作流(含状态/创建人/最后修改时间),支持分页 | +| `workflow get` | 获取单个工作流详情(含 flowSchema 完整节点定义) | +| `workflow enable` | 启用指定工作流(按配置的触发条件自动执行) | +| `workflow disable` | 禁用指定工作流(高危,建议 `--yes` 二次确认) | + +> 所有子命令的 `--base-id` 必填(可用隐藏别名 `--base`)。 +> 当前**不支持通过 CLI 新建工作流**,请在 AI 表格 Web 端配置好后用 `workflow list` 拿到 ID 再启停。 + +## 命令详情 + +### workflow list — 列出工作流 + +```bash +dws aitable workflow list --base-id BASE_ID --format json +dws aitable workflow list --base-id BASE_ID --limit 50 --offset 100 +``` + +| flag | 说明 | +|------|------| +| `--base-id` | 必填 | +| `--limit` | 可选,分页大小 `[1, 100]`,不传走服务端默认 20 | +| `--offset` | 可选,分页偏移量 `>= 0`,不传走服务端默认 0 | + +返回结构: + +```json +{ + "data": { + "list": [ + { + "flowId": "G-FLOW-XXXXXX", // ★ 注意字段名是 flowId + "name": "流程1", + "description": "当创建记录时,就更新记录", + "status": "RUNNING", // RUNNING / STOP + "creatorStaffId": "281493", + "lastModifier": { "name": "李普阳", "staffId": "281493" }, + "gmtModified": 1780318540000, + "versionId": "G-FLOW-VER-XXXXXX", + "icons": ["..."], // 触发器+动作的图标 + "isSubFlow": false, + "opPermissions": { "canEdit": true } + } + ], + "recordCount": 1, // Base 下总数 + "runningCount": 1 // RUNNING 状态的数量 + } +} +``` + +**注意**: +- 标识字段服务端在 `list` 里叫 **`flowId`**,但在 `enable` / `disable` 出参里叫 **`workflowId`**。CLI `--workflow-id` 传任一即可(同值)。 +- `status` 是字符串枚举:`RUNNING`(启用中)/ `STOP`(已禁用),**不是** boolean。 +- `runningCount` 是当前 Base 下 status=RUNNING 的工作流数,方便快速判断「有几个流程在跑」。 + +### workflow get — 获取单个工作流详情 + +```bash +dws aitable workflow get --base-id BASE_ID --workflow-id WORKFLOW_ID --format json +``` + +| flag | 说明 | +|------|------| +| `--base-id` | 必填 | +| `--workflow-id` | 必填,对应 list 出参里的 `flowId` | + +返回完整工作流配置: + +```json +{ + "data": { + "name": "流程1", + "namespace": "...", + "status": "RUNNING", + "versionId": "G-FLOW-VER-XXXXXX", + "versionNo": 14, + "versionStatus": "...", + "accessor": {...}, // 访问者信息 + "corpId": "...", + "flowAttribute": {...}, // 流程顶层属性 + "flowSchema": {...}, // ★ 流程节点定义(触发器/动作/分支等) + "gmtCreate": 1780317804000, + "gmtModified": 1780318540000 + } +} +``` + +`flowSchema` 是完整的节点 DAG,结构因流程而异(条件触发器 vs 定时触发器、单分支 vs 多分支等)。agent 应按需读取关心字段,不要试图建静态 schema。 + +### workflow enable — 启用工作流 + +```bash +dws aitable workflow enable --base-id BASE_ID --workflow-id WORKFLOW_ID --format json +``` + +返回 `{workflowId, enabled: true}` —— **`enabled: true` 是动作确认,不是当前状态查询**。要确认真启用了,必须再 `workflow list` 看 `status` 是否变成 `"RUNNING"` 或 `runningCount` 是否加 1。 + +### workflow disable — 禁用工作流(高危) + +```bash +dws aitable workflow disable --base-id BASE_ID --workflow-id WORKFLOW_ID --yes --format json +``` + +返回 `{workflowId, disabled: true}` —— 同样是动作确认。禁用后该工作流不再自动触发。 + +**风险**:直接影响业务自动化(如停掉「记录创建后自动发通知」会让通知断流)。建议: +- 操作前先 `workflow get` 留底当前配置 +- 脚本场景显式传 `--yes`;交互场景让用户在 prompt 中再次确认 + +## 能力边界 + +| 能力 | 状态 | +|------|------| +| 列出工作流 | ✅ | +| 看工作流详情(含 flowSchema) | ✅ | +| 启用/禁用 | ✅ | +| 新建工作流 | ❌ 当前不支持,请去 AI 表格 Web 端 → 数据表 → 自动化 创建 | +| 修改工作流配置 | ❌ 同上,需 Web UI 编辑 | +| 删除工作流 | ❌ 同上 | +| 查看运行历史/执行日志 | ❌ 暂未开放 | +| 手动触发/单次运行 | ❌ 暂未开放 | + +## 错误码速查 + +| 场景 | code | type | 备注 | +|------|------|------|------| +| `workflow-id` 不存在调 get | `GET_WORKFLOW_ERROR` | `SYSTEM_ERROR` | message 可能为 null,先 `workflow list` 核对 ID | +| `workflow-id` 不存在调 enable | `ENABLE_WORKFLOW_ERROR` | `SYSTEM_ERROR` | message 含 "场域中不存在该 namespace" | +| `workflow-id` 不存在调 disable | `DISABLE_WORKFLOW_ERROR` | `SYSTEM_ERROR` | 同上 | +| `--limit` < 1 或 > 100 | (CLI 层拦截) | — | `--limit 必须在 [1, 100] 范围内,got N` | +| `--offset` < 0 | (CLI 层拦截) | — | `--offset 必须 >= 0,got N` | + +> 拿到 `*_WORKFLOW_ERROR / SYSTEM_ERROR` 时,先 `workflow list` 自查目标 ID 是否还存在、是否在当前 Base 下。 + +## 典型工作流 + +### 看看 Base 里有哪些自动化在跑 + +```bash +dws aitable workflow list --base-id BASE_ID --format json | jq '.data | {total: .recordCount, running: .runningCount, items: .list | map({name, status, flowId})}' +``` + +### 临时停掉某个流程做调试 + +```bash +# 1. 留底当前状态 +dws aitable workflow get --base-id BASE_ID --workflow-id WORKFLOW_ID --format json > /tmp/wf-backup.json + +# 2. 禁用 +dws aitable workflow disable --base-id BASE_ID --workflow-id WORKFLOW_ID --yes --format json + +# 3. 调试做完后重启 +dws aitable workflow enable --base-id BASE_ID --workflow-id WORKFLOW_ID --format json + +# 4. 确认 status=RUNNING +dws aitable workflow list --base-id BASE_ID --format json | jq '.data.list[] | select(.flowId == "WORKFLOW_ID") | .status' +``` + +### 批量关掉某个 Base 下所有 workflow(调试 / 迁移前清场) + +```bash +for WF in $(dws aitable workflow list --base-id BASE_ID --limit 100 --format json | jq -r '.data.list[] | select(.status == "RUNNING") | .flowId'); do + dws aitable workflow disable --base-id BASE_ID --workflow-id "$WF" --yes --format json | jq .status +done +``` + +## 注意事项 + +- `--workflow-id` 接受的就是 `list` 返回里的 `flowId`(同值,CLI 屏蔽了服务端字段名差异)。 +- enable / disable 出参里的 `enabled` / `disabled` 是 **动作确认 flag**,不是当前状态字段。要确认真生效请走 `workflow list` 查 `status`。 +- `workflow get` 的 `flowSchema` 结构随触发器/动作类型变化,不要假设固定字段。 +- 新建/修改/删除工作流目前必须在 AI 表格 Web 端(数据表页面 → 自动化)完成。 diff --git a/skills/mono/references/products/doc.md b/skills/mono/references/products/doc.md index 1fe88c3d..218915d8 100644 --- a/skills/mono/references/products/doc.md +++ b/skills/mono/references/products/doc.md @@ -35,13 +35,13 @@ dws doc --help # 查看具体命令的完整参数说明 -dws doc list --help +dws doc read --help dws doc create --help dws doc block insert --help # 查看子命令组下的所有命令 dws doc block --help -dws doc permission --help +dws doc media --help ``` 规则: @@ -54,12 +54,10 @@ dws doc permission --help > 命令名 → 单文件,按需加载子文档。复杂任务请优先看下方 §场景索引。 -### 检索 / 阅读 / 元信息 +### 阅读 / 元信息 | 命令 | 用途 | 必填参数 | 详见 | |------|------|----------|------| -| `doc search` | 关键字搜索 / 最近访问 | — | [`doc/doc-search.md`](./doc/doc-search.md) | -| `doc list` | 文件夹遍历 | — | [`doc/doc-list.md`](./doc/doc-list.md) | | `doc info` | 文档元信息(含 contentType / extension) | `--node` | [`doc/doc-info.md`](./doc/doc-info.md) | | `doc read` | 读取正文(markdown 或 jsonml) | `--node` | [`doc/doc-read.md`](./doc/doc-read.md) | @@ -71,23 +69,31 @@ dws doc permission --help | `doc update` | 整篇 / 段落级更新(markdown / jsonml) | `--node` `--mode` | [`doc/doc-update.md`](./doc/doc-update.md) | | `doc block list/insert/update/delete` | 块级精细编辑(含 JSONML 节点操作) | `--node` (+ `--block-id`) | [`doc/doc-block.md`](./doc/doc-block.md) | -### 附件 / 评论 / 权限 / 导出 +### 附件 / 评论 / 导出 | 命令 | 用途 | 必填参数 | 详见 | |------|------|----------|------| | `doc media insert/download` | 附件 / 图片插入与下载 | `--node` `--file` 或 `--resource-id` | [`doc/doc-media.md`](./doc/doc-media.md) | | `doc comment list/create/reply/create-inline` | 文档评论与划词评论 | `--node` (+ ...) | [`doc/doc-comment.md`](./doc/doc-comment.md) | -| `doc permission add/update/list` | 节点级授权 | `--node` `--user` `--role` | [`doc/doc-permission.md`](./doc/doc-permission.md) | | `doc export` / `doc export get` | 在线文档导出 docx | `--node` `--output` | [`doc/doc-export.md`](./doc/doc-export.md) | -### 文件操作 +### 文件操作(已迁移,以下命令 deprecated) -| 命令 | 用途 | 必填参数 | 详见 | -|------|------|----------|------| -| `doc upload` | 上传文件到文档空间/知识库 | `--file` | [`doc/doc-file-ops.md`](./doc/doc-file-ops.md) | -| `doc download` | 下载已有文件(非 ALIDOC) | `--node` `--output` | [`doc/doc-file-ops.md`](./doc/doc-file-ops.md) | -| `doc copy` / `doc move` / `doc rename` / `doc delete` | 复制/移动/重命名/删除 | `--node` (+ ...) | [`doc/doc-file-ops.md`](./doc/doc-file-ops.md) | -| `doc folder create` | 创建文件夹 | `--name` | [`doc/doc-file-ops.md`](./doc/doc-file-ops.md) | +> **迁移提示**:文件管理命令已按产品领域架构重新归属,旧命令在过渡期内仍可使用,运行时会输出 deprecated 警告。 + +| 旧命令 | 推荐命令 | 详见 | +|--------|----------|------| +| `doc upload` | `dws drive upload` | [`drive.md`](./drive.md) | +| `doc download` | `dws drive download` | [`drive.md`](./drive.md) | +| `doc copy` | `dws drive copy` | [`drive.md`](./drive.md) | +| `doc move` | `dws drive move` | [`drive.md`](./drive.md) | +| `doc rename` | `dws drive rename` | [`drive.md`](./drive.md) | +| `doc delete` | `dws drive delete` | [`drive.md`](./drive.md) | +| `doc folder create` | `dws drive folder create` | [`drive.md`](./drive.md) | +| `doc file create` | `dws wiki node create --type ` | [`wiki.md`](./wiki.md) | +| `doc permission *` | `dws drive permission *` | [`drive.md`](./drive.md) | +| `doc list` | `dws drive list --workspace` / `dws wiki node list` | [`drive.md`](./drive.md) / [`wiki.md`](./wiki.md) | +| `doc search` | `dws drive search` / `dws wiki node search` | [`drive.md`](./drive.md) / [`wiki.md`](./wiki.md) | ### 排版规范 / JSONML 参考 @@ -105,7 +111,7 @@ dws doc permission --help | 任务场景 | 一次性读取 | 主命令 | |---------|-----------|--------| -| 定位 nodeId / URL 解析 / 目录遍历 | [`doc-search.md`](./doc/doc-search.md) + [`doc-list.md`](./doc/doc-list.md) + [`doc-info.md`](./doc/doc-info.md) | search | +| 定位 nodeId / URL 解析 | [`doc-info.md`](./doc/doc-info.md)(搜索请用 `dws drive search` / `dws wiki node search`;遍历请用 `dws drive list` / `dws wiki node list`) | `drive search` / `wiki node search` | | 阅读已有文档 | [`doc-info.md`](./doc/doc-info.md) + [`doc-read.md`](./doc/doc-read.md) | read | | 创建新文档 | [`doc-create.md`](./doc/doc-create.md) + [`doc-update.md`](./doc/doc-update.md)(写入管道)+ [`style/doc-create-workflow.md`](./doc/style/doc-create-workflow.md) + [`style/doc-style-guideline.md`](./doc/style/doc-style-guideline.md) | create | | 创建文档且包含图片/截图/图文并茂 | [`doc-create.md`](./doc/doc-create.md) + [`doc-media.md`](./doc/doc-media.md) + [`style/doc-create-workflow.md`](./doc/style/doc-create-workflow.md) + [`style/doc-style-guideline.md`](./doc/style/doc-style-guideline.md) | create → media insert | @@ -114,16 +120,17 @@ dws doc permission --help | 插入富 block(callout / 分栏 / 表格) | [`doc-block.md`](./doc/doc-block.md) + [`format/doc-jsonml-cookbook.md`](./doc/format/doc-jsonml-cookbook.md) + [`style/doc-style-guideline.md`](./doc/style/doc-style-guideline.md) | block insert | | 上传图片 / 附件 | [`doc-media.md`](./doc/doc-media.md) | media insert | | 评论 / 划词评论(含 @人) | [`doc-comment.md`](./doc/doc-comment.md)(+ `dws contact user search` 取 mention 用 userId) | comment create | -| 文档分享 / 节点级权限 | [`doc-permission.md`](./doc/doc-permission.md) | permission add | +| 文档分享 / 节点级权限 | [`drive.md`](./drive.md)(已迁移:`dws drive permission add/update/list/remove`) | `drive permission` | | 导出 PDF / DOCX | [`doc-info.md`](./doc/doc-info.md) + [`doc-export.md`](./doc/doc-export.md) | export | -| 文件下载 / 上传 / 移动 / 重命名 / 复制 | [`doc-file-ops.md`](./doc/doc-file-ops.md) + [`doc-info.md`](./doc/doc-info.md)(download 前判 contentType) | upload / download / move / copy / rename | +| 文件下载 / 上传 / 移动 / 重命名 / 复制 | [`drive.md`](./drive.md)(已迁移:`dws drive upload/download/copy/move/rename/delete`) | `drive *` | ## 意图判断 用户说"找文档/搜文档/最近文档": -- 搜索 → `search` -- 浏览 → `list` +- 全局搜索 → `dws drive search --query "<关键词>"`(聚合钉盘+文档空间) +- 空间内搜索 → `dws wiki node search --workspace --keyword "<关键词>"` +- 遍历文件夹 → `dws drive list --workspace ` 或 `dws wiki node list --workspace ` 用户说"看文档/读内容/文档内容": @@ -144,44 +151,42 @@ dws doc permission --help 用户说"建文件夹/新建目录": -- 创建 → `folder create` +- 创建 → `dws drive folder create` 用户说"上传文件/传文件/上传到文档/上传到知识库": -- 上传 → `upload`(需本地文件路径) -- 上传并转换 → `upload --convert` +- 上传 → `dws drive upload`(需本地文件路径) 用户说"下载/导出/下载到本地/导出文档/导出为Word/导出为docx/把文档导出来": - **必须先判断目标文件类型**,再决定走 `export` 还是 `download`: - - 在线文档 (alidocs/adoc) → **`export`**(格式转换后导出为 docx) - - 已有文件(PDF、图片、附件、视频等非在线文档) → **`download`**(直接下载原始文件) + - 在线文档 (alidocs/adoc) → **`doc export`**(导出是内容层操作,仅对 adoc 有意义) + - 已有文件(PDF、图片、附件、视频等非在线文档) → **`dws drive download`** - 判断方法: - 1. 如果用户明确说了"导出文档"、"导出为Word/docx" → 直接走 `export` - 2. 如果用户明确说了"下载PDF/图片/附件" → 直接走 `download` - 3. 不确定时,先用 `info --node ` 查询节点信息,根据返回的 `contentType` 字段判断: - - `contentType` 为 `ALIDOC` → 走 `export` - - `contentType` 为 `DOCUMENT`/`IMAGE`/`VIDEO` 等 → 走 `download` + 1. 如果用户明确说了"导出文档"、"导出为Word/docx" → 直接走 `doc export` + 2. 如果用户明确说了"下载PDF/图片/附件" → 直接走 `drive download` + 3. 不确定时,先用 `drive info --node ` 查询节点信息,根据返回的 `contentType` 字段判断: + - `contentType` 为 `ALIDOC` → 走 `doc export` + - `contentType` 为 `DOCUMENT`/`IMAGE`/`VIDEO` 等 → 走 `drive download` > **严禁将"导出文档"直接路由到 `download`**。`download` 只能下载已有文件(原样下载),`export` 是将在线文档格式转换后导出为 docx,两者完全不同。 用户说"复制文档/拷贝文件/复制到": -- 复制 → `copy`(需文档 ID 或 URL + 目标位置) +- 复制 → `dws drive copy` 用户说"移动文档/搬到/移到/转移文件": -- 移动 → `move`(需文档 ID 或 URL + 目标位置) +- 移动 → `dws drive move` 用户说"重命名/rename/改名/改文档名/修改文档名称/修改文档标题/把这个文档叫做...": -- 重命名 → `doc rename`(需文档 ID 或 URL + 新名称) -- 只要意图是修改文档在列表和链接中展示的名称,统一路由到 `dws doc rename --node --name "新名称"`;不要走 `drive`、`doc update` 或重新 `doc create`。 +- 重命名 → `dws drive rename --node --name "新名称"` - 只有用户明确说"正文里的标题/章节标题/段落标题/H1 标题"时,才走 `block update`。 用户说"删除文档/删掉这个文件/移到回收站/丢掉这篇文档": -- 删除节点 → `delete`(危险操作,需确认;需文档 ID 或 URL) +- 删除节点 → `dws drive delete` 用户说"插入附件/上传附件到文档/往文档里加文件/加附件": @@ -205,13 +210,14 @@ dws doc permission --help 用户说"给某人开权限/分享给某人/授权某文档/把这篇文档给 xxx 看": -- 新增权限 → `permission add`(需 `--node` + `--user` + `--role`) -- 修改权限 → `permission update` -- 查看谁有权限 → `permission list` +- 新增权限 → `dws drive permission add` +- 修改权限 → `dws drive permission update` +- 查看谁有权限 → `dws drive permission list` +- 移除权限 → `dws drive permission remove` > **关键区分**: > -> - "把**某篇文档**授权给某人" → `doc permission add`(节点级,包括「我的文档」下的文档都支持) +> - "把**某篇文档**授权给某人" → `drive permission add`(节点级,包括「我的文档」下的文档都支持) > - "把**某个知识库**整体授权给某人" → `wiki member add`(容器级,但**「我的文档」个人空间不支持**) > 补充:如果用户直接粘贴的是原始 `alidocs` URL,先按 [链接规范](../url-patterns.md#alidocs-url-类型探测流程) probe;只有 probe 确认是 `adoc` / `file` / `folder` 后,才继续按下列意图执行。 @@ -226,7 +232,7 @@ dws doc permission --help - "帮我看看这个文档" → `read` - "这个文档的信息" → `info` - "往这个文档追加内容" → `update --mode append` -- "把这个文档标题改成 X" / "这个文档改名为 X" → `rename` +- "把这个文档标题改成 X" / "这个文档改名为 X" → `dws drive rename` - "把正文里的一级标题/章节标题改成 X" → `block update` 关键区分: doc(文档编辑/阅读) vs aitable(数据表格操作) vs drive(钉盘文件管理) @@ -236,14 +242,13 @@ dws doc permission --help > 步骤性指引。"读哪些文件"组合参见上方 §场景索引;命令详细参数参见对应子文件。 ```bash -# ── 工作流 1: 浏览并阅读文档 ── -dws doc list --format json # 1. 浏览根目录 -dws doc list --folder --format json # 2. 进入子目录 -dws doc info --node --format json # 3. 元信息(含 contentType) -dws doc read --node --format json # 4. 读 markdown 正文 +# ── 工作流 1: 定位并阅读文档 ── +dws drive search --query "<关键词>" --format json # 1. 搜索定位 nodeId(或 wiki node search) +dws doc info --node --format json # 2. 元信息(含 contentType) +dws doc read --node --format json # 3. 读 markdown 正文 # ── 工作流 2: 创建文档(含分片自动写入)── -dws doc folder create --name "项目资料" --format json # 1. (可选) 文件夹 +dws drive folder create --name "项目资料" --format json # 1. (可选) 创建文件夹 dws doc create --name "项目周报" --content-file /tmp/x.md \ --folder --format json # 2. 创建 + 写入 dws doc read --node --format json # 3. 回读校验(必须) @@ -263,14 +268,14 @@ dws doc update --node --content-file /tmp/doc.json \ dws doc read --node --content-format jsonml # 3. 回读 # 担心被并发覆盖时,可加 --revision 触发并发检查(详见 doc-update.md) -# ── 工作流 5: 上传 vs 插入附件 ── -dws doc upload --file ./report.pdf --folder # 上传作为独立文件 -dws doc media insert --node --file ./report.pdf # 上传并作为附件块插入正文 +# ── 工作流 5: 上传独立文件 vs 插入附件到文档正文 ── +dws drive upload --file ./report.pdf --folder # 上传作为独立文件(存储层) +dws doc media insert --node --file ./report.pdf # 上传并作为附件块插入正文(内容层) # ── 工作流 6: 下载 vs 导出(先 info 判 contentType)── dws doc info --node --format json # 必须先查 contentType -dws doc export --node --output ~/downloads/ # contentType=ALIDOC 走 export -dws doc download --node --output ~/downloads/ # contentType≠ALIDOC 走 download +dws doc export --node --output ~/downloads/ # contentType=ALIDOC 走 export(内容层) +dws drive download --node --output ~/downloads/ # contentType≠ALIDOC 走 drive download(存储层) # ── 工作流 7: 评论 + 划词(@人 需 userId)── dws contact user search --query "张三" --format json # 取 userId @@ -279,38 +284,32 @@ dws doc block list --node --format json # 划词需先 dws doc comment create-inline --node --block-id \ --start 0 --end 10 --content "建议调整" --selected-text "原文" --format json -# ── 工作流 8: 权限授予(节点级)── +# ── 工作流 8: 权限授予(节点级,已迁移到 drive)── dws contact user search --query "张三" --format json # 取 userId -dws doc permission add --node --user , --role EDITOR --format json -dws doc permission list --node --format json # 校验 - -# ── 工作流 9: 文件操作(复制/移动/重命名/删除)── -# 第一步:获取 nodeId(三种方式按场景选一,命中即停,不要冗余调用) -# 方式 A(优先):用户直接提供 URL / nodeId → 直接传 --node,跳过 search/list -# 方式 B:按关键字找:dws doc search --query "项目周报" --format json -# 方式 C:按文件夹遍历:dws doc list --folder --format json -# 第二步:执行(--node 支持 ID 或完整 URL;--folder 支持文件夹 nodeId 或 alidocs 文件夹 URL) -dws doc copy --node --folder --format json # 异步任务 -dws doc move --node --folder --format json -dws doc rename --node --name "新名称" --format json -# 删除是危险操作:必须先向用户展示「即将删除「文档名」到回收站」 → 用户确认 → 才加 --yes -dws doc delete --node --yes --format json +dws drive permission add --node --user , --role EDITOR --format json +dws drive permission list --node --format json # 校验 + +# ── 工作流 9: 文件操作(已迁移到 drive)── +# 第一步:获取 nodeId +# 方式 A(优先):用户直接提供 URL / nodeId → 直接传 --node +# 方式 B:按关键字找:dws drive search --query "项目周报" --format json +# 方式 C:按文件夹遍历:dws drive list --workspace --format json +# 第二步:执行 +dws drive copy --node --folder --format json +dws drive move --node --folder --format json +dws drive rename --node --name "新名称" --format json +dws drive delete --node --yes --format json ``` ## 上下文传递表 | 操作 | 从返回中提取 | 用于 | |------|-------------|------| -| `list` | `nodes[].nodeId` | read / info / update / copy / move / rename / block 操作的 --node | -| `list` | folder 类型的 `nodeId` | list 的 --folder, create / copy / move 的 --folder | -| `search` | 文档 `nodeId` / URL / `createTime` / `creatorUid` | read / info / update / copy / move / rename 的 --node;创建时间与创建者信息 | +| `drive search` / `wiki node search` | 文档 `nodeId` / URL | doc read / info / update 等所有 `--node` 入参 | +| `drive list` / `wiki node list` | `nodes[].nodeId` | doc read / info / update / block 操作的 --node | | `create` | `nodeId` | update / block 操作的 --node | -| `folder create` | `nodeId` | create / list / upload / copy / move 的 --folder | | `block list` | `blockId` | block insert 的 --ref-block, block update/delete 的 --block-id | | `read --content-format jsonml` | `revision` | update --content-format jsonml 的 --revision(可选,并发检查时使用) | -| `upload` | `nodeId` / URL | 上传后文件的访问链接 | -| `download` | 本地文件路径 | 下载后的文件保存位置 | -| `copy` | `nodeId` / URL (异步,不保证返回) | 复制是异步任务,若任务未完成则不会返回新文档 ID;如需获取可稍后通过 list 查询目标文件夹 | | `media insert` | `resourceId` | 附件已插入文档,可通过 block list 查看附件块 | | `media download` | 附件下载链接 `downloadUrl` | 下载文档中的附件资源 | | `block list` | attachment 块的 `resourceId` | media download 的 --resource-id | @@ -319,7 +318,7 @@ dws doc delete --node --yes --format json | `comment create-inline` | `commentKey` | comment reply 的 --comment-key | | `block list` | `blocks[].element.id` | comment create-inline 的 --block-id | | `block list` | `blocks[].element.paragraph.text` | 计算 create-inline 的 --start / --end 偏移量 | -| `contact user search` | `userId` | comment create/reply/create-inline 的 --mention;permission add/update 的 --user | +| `contact user search` | `userId` | comment create/reply/create-inline 的 --mention;drive permission 的 --user | ## 相关产品 diff --git a/skills/mono/references/products/doc/doc-block.md b/skills/mono/references/products/doc/doc-block.md index 439cd021..04e97ac2 100644 --- a/skills/mono/references/products/doc/doc-block.md +++ b/skills/mono/references/products/doc/doc-block.md @@ -2,13 +2,13 @@ > **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件: > 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流 -> 2. [`./style/doc-update-workflow.md`](./style/doc-update-workflow.md) — 改写流程(编辑形态优先级、JSONML normalize/validator 行为) +> 2. [`./style/doc-update-workflow.md`](./style/doc-update-workflow.md) — 改写流程(编辑形态优先级、JSONML validator 行为) > 3. [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md) — JSONML 范例(含 callout / 分栏 / 表格 / 标题等节点的完整命令) > 4. [`./format/doc-jsonml-schema.md`](./format/doc-jsonml-schema.md) — JSONML 节点结构字段定义 > > **同任务常配合**:[`doc-update.md`](./doc-update.md)(整篇 overwrite / 末尾追加纯文本)/ [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md)(JSONML 复制范例) -> **改写已有文档优先 JSONML**:保真度最高、callout / 分栏 / 表格 / @人 / 附件 / 颜色 / 嵌套都能 1:1 round-trip;写入端默认 normalize + validate。详见 [`./style/doc-update-workflow.md` §1.3 编辑形态优先级](./style/doc-update-workflow.md)。 +> **改写已有文档优先 JSONML**:保真度最高、callout / 分栏 / 表格 / @人 / 附件 / 颜色 / 嵌套都能 1:1 round-trip;写入端有 validator 兜底。详见 [`./style/doc-update-workflow.md` §1.3 编辑形态优先级](./style/doc-update-workflow.md)。 --- @@ -88,8 +88,7 @@ Flags: --level int 标题级别 1-6 (配合 --heading,默认 1) --element string 块元素 JSON (高级);content-format=jsonml 时为 JSONML 数组字符串 --content-format string 输入格式: 默认为 element,可选 jsonml - --fix-jsonml 启用全部 JSONML 修复(含 JSON 语法修复 + 结构修复),推荐 agent 调用时使用 - --no-fix-jsonml 关闭全部 JSONML 修复(跳过 JSON 语法修复和结构修复),用于排查原始错误 + --fix-jsonml 启用 JSON 语法修复(括号/逗号补全),推荐 agent 调用时使用 --index int 参照位置索引 (从 0 开始) --where string 插入方向: before / after (默认 after) --ref-block string 参照块 ID (优先级高于 --index) @@ -116,8 +115,7 @@ Flags: --level int 标题级别 1-6 (配合 --heading,默认 1) --element string 块元素 JSON (高级);content-format=jsonml 时为 JSONML 数组字符串 --content-format string 输入格式: 默认为 element,可选 jsonml - --fix-jsonml 启用全部 JSONML 修复(含 JSON 语法修复 + 结构修复),推荐 agent 调用时使用 - --no-fix-jsonml 关闭全部 JSONML 修复(跳过 JSON 语法修复和结构修复),用于排查原始错误 + --fix-jsonml 启用 JSON 语法修复(括号/逗号补全),推荐 agent 调用时使用 ``` > 使用 `--content-format jsonml` 时,`element` 中的 `uuid` **必须**等于 `--block-id`,否则报错。 @@ -169,17 +167,16 @@ dws doc block update --node DOC_ID --block-id UUID --content-format jsonml \ dws doc block delete --node DOC_ID --block-id UUID ``` -> 关于自动修复 / 严格校验:默认会自动注入 uuid、把裸字符串包成 span/leaf;如要禁用,用 `--no-fix-jsonml`。如需同时启用 JSON 语法修复(修复 LLM 遗漏的括号/逗号),用 `--fix-jsonml`。文本结构定义见 [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md)。 +> 关于校验:CLI 不做结构修复,裸字符串、缺 uuid 等错误会被 validator 直接抦下。如需同时启用 JSON 语法修复(修复 LLM 遗漏的括号/逗号),用 `--fix-jsonml`。文本结构定义见 [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md)。 ## 关键说明 - **块类型**:paragraph、heading、blockquote、callout、columns、orderedList、unorderedList、table、sheet、attachment、slot。 - **快捷 vs --element**:`block insert` 优先使用 `--text` 或 `--heading` 快捷方式;复杂块类型(table、callout、columns 等)使用 `--element` JSON 或 `--content-format jsonml`。 - **简单内容追加**:建议用 [`./doc-update.md`](./doc-update.md) `--mode append`,不必走 block insert。 -- **JSONML normalize + validator**(写入端默认行为): - - 缺 `uuid` 的 block 会自动注入;裸字符串自动包成 `["span",{"data-type":"text"},["span",{"data-type":"leaf"},"..."]]`;每条修复以 `[FIX]` 行输出。 - - 结构错误会被 validator 拦下并返回带 path 的错误(如 `$[2][2]: paragraph child must be span wrapper, got raw string.`)。 - - `--no-fix-jsonml` 关闭全部修复(normalize + JSON repair);`--fix-jsonml` 开启全部修复(含 JSON 语法修复),推荐 agent 调用。 +- **JSONML validator**(写入端默认行为): + - 裸字符串、缺 uuid 等结构错误会被 validator 抦下并返回带 path 的错误(如 `$[2][2]: paragraph child must be span wrapper, got raw string.`)。 + - `--fix-jsonml` 开启 JSON 语法修复,推荐 agent 调用。 - **图片插入**:插入图片走 [`./doc-media.md`](./doc-media.md) `media insert`(作为附件块),不走 block insert。 - **分割线**:用 [`./doc-update.md`](./doc-update.md) `--content "---" --mode append`,不走 block insert。 @@ -265,7 +262,7 @@ dws doc block insert --node --content-format jsonml --parent-block **路由前置判断**:用户说「下载/导出」时**必须**先用 [`./doc-info.md`](./doc-info.md) `info --node --format json` 查 `contentType`: > - `contentType` 为 `ALIDOC`(在线文档)→ **必须用 `export`**,禁止用 `download` -> - `contentType` 为 `DOCUMENT`/`IMAGE`/`VIDEO` 等(已有文件)→ 用 [`./doc-file-ops.md`](./doc-file-ops.md) `download` +> - `contentType` 为 `DOCUMENT`/`IMAGE`/`VIDEO` 等(已有文件)→ 用 `dws drive download`(详见 [`../drive.md`](../drive.md)) > -> `download` 只能下载**已有文件**(原样下载),`export` 是将**在线文档格式转换**后导出为 docx,两者完全不同。 +> `drive download` 只能下载**已有文件**(原样下载),`export` 是将**在线文档格式转换**后导出为 docx,两者完全不同。 --- @@ -77,4 +77,4 @@ dws doc export get --job-id --format json - [`../doc.md` §意图判断](../doc.md#意图判断)(如何路由到本命令) - [`./doc-info.md`](./doc-info.md)(前置:判断 contentType=ALIDOC 才走 export) -- [`./doc-file-ops.md`](./doc-file-ops.md)(非 ALIDOC 文件用 download) +- [`../drive.md`](../drive.md)(非 ALIDOC 文件用 `dws drive download`) diff --git a/skills/mono/references/products/doc/doc-info.md b/skills/mono/references/products/doc/doc-info.md index 933101b9..49b7a682 100644 --- a/skills/mono/references/products/doc/doc-info.md +++ b/skills/mono/references/products/doc/doc-info.md @@ -4,7 +4,7 @@ > 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流 > 2. [`../../url-patterns.md`](../../url-patterns.md) — 仅当用户原始 `alidocs` URL 需要 probe 时 > -> **同任务常配合**:[`doc-search.md`](./doc-search.md) / [`doc-list.md`](./doc-list.md)(先拿到 nodeId)/ [`doc-read.md`](./doc-read.md)(确认是 ALIDOC 后读正文) +> **同任务常配合**:`dws drive search` / `dws wiki node search`(先定位 nodeId)/ [`doc-read.md`](./doc-read.md)(确认是 ALIDOC 后读正文) ## 命令格式 @@ -59,7 +59,7 @@ Flags: - `dentryUuid` 是 `alidocs` URL `/i/nodes/{dentryUuid}` 的最后一段,在 `doc` 场景中等价于可传入 CLI 的 `nodeId`;不要把它改写成数字 ID。 - `dentryId` 通常是纯数字,**不是** `doc` 的 `nodeId`,也不是 `doc --folder` 的目标文件夹 ID;不要把数字 `dentryId` 当作 `--node`、`--folder` 或 `--parent-id` 使用。 - `parentId` / `--parent-id` 不是 `doc` 命令参数;`doc` 里目标父文件夹统一使用 `--folder `,目标知识库使用 `--workspace `。 -- 如果上下文只有数字 `dentryId`,但用户要读、改、移动、复制、重命名文档,先通过 `doc search` / `doc list` / 用户提供的 `alidocs` URL 获取 `nodeId` / `dentryUuid`,不要用数字 `dentryId` 重试为父目录参数。 +- 如果上下文只有数字 `dentryId`,但用户要读、改、移动、复制、重命名文档,先通过 `dws drive search` / `dws drive list` / 用户提供的 `alidocs` URL 获取 `nodeId` / `dentryUuid`,不要用数字 `dentryId` 重试为父目录参数。 ## 处理流程 @@ -78,8 +78,8 @@ Flags: | 方式 | 触发条件 | 操作 | |------|----------|------| | **A** | 用户**直接提供文档 URL 或 nodeId** | **直接传给 `--node`**,无需额外查询;优先使用此方式 | -| **B** | 用户给出关键字 / 文档名 | `dws doc search --query "<关键字>" --format json` 从返回的 `nodes[].nodeId` 提取 | -| **C** | 用户指向某个文件夹下的文档 | `dws doc list --folder --format json` 从返回中提取 | +| **B** | 用户给出关键字 / 文档名 | `dws drive search --query "<关键字>" --format json` 或 `dws wiki node search --workspace --keyword "<关键字>"` 从返回中提取 nodeId | +| **C** | 用户指向某个文件夹下的文档 | `dws drive list --workspace --format json` 或 `dws wiki node list --workspace ` 从返回中提取 | > **关键节省**:方式 A 命中时,禁止再调 search/list "确认一下" —— 用户提供的 URL/nodeId 本身就是权威输入。同理,`--folder` 也支持 alidocs 文件夹 URL 直传,不要先 search 把 URL 解析成纯数字 ID 再传。 @@ -104,13 +104,13 @@ dws doc read --node "https://alidocs.dingtalk.com/document/preview?cid=749936706 `--folder` 参数同样支持 alidocs 文件夹 URL 或文档文件夹 nodeId。 -不要把纯数字 `dentryId` 当成这里的 ID。需要父文件夹时,使用文件夹的 `nodeId` / `dentryUuid` / URL 传给 `--folder`;不能改用 `--parent-id`。如果上一步只拿到了 drive/chat 链路里的纯数字 `dentryId`、`spaceId` 或 `parent-id`,说明还没有拿到 doc 文件夹,应该省略 `--folder` 使用默认文档根目录,或先通过 `dws doc list/search` 找到文档文件夹 nodeId。 +不要把纯数字 `dentryId` 当成这里的 ID。需要父文件夹时,使用文件夹的 `nodeId` / `dentryUuid` / URL 传给 `--folder`;不能改用 `--parent-id`。如果上一步只拿到了 drive/chat 链路里的纯数字 `dentryId`、`spaceId` 或 `parent-id`,说明还没有拿到 doc 文件夹,应该省略 `--folder` 使用默认文档根目录,或先通过 `dws drive search` / `dws drive list` 找到文档文件夹 nodeId。 ## 上下文传递 | 从返回中提取 | 用于 | |-------------|------| -| `contentType` + `extension` | 选择 [`./doc-read.md`](./doc-read.md) / `dws sheet ...` / `dws aitable ...` / [`./doc-file-ops.md`](./doc-file-ops.md) 的下载/导出路径 | +| `contentType` + `extension` | 选择 [`./doc-read.md`](./doc-read.md) / `dws sheet ...` / `dws aitable ...` / `dws drive download`(非 ALIDOC 走存储层下载) | | `nodeId` / `docUrl` | 后续所有 `--node` 入参 | ## 常用模板 @@ -145,6 +145,6 @@ dws doc info --node # 错误 ## 参考 - [`../doc.md` §意图判断](../doc.md#意图判断)(如何路由到本命令) -- [`./doc-search.md`](./doc-search.md) / [`./doc-list.md`](./doc-list.md)(前置:拿 nodeId 的入口) +- `dws drive search` / `dws wiki node search`(前置:定位 nodeId 的搜索入口,详见 [`../drive.md`](../drive.md) / [`../wiki.md`](../wiki.md)) - [`./doc-read.md`](./doc-read.md)(contentType=ALIDOC + extension=adoc 的后续命令) - [`../../url-patterns.md`](../../url-patterns.md)(用户原始 alidocs URL 的 probe 流程) diff --git a/skills/mono/references/products/doc/doc-media.md b/skills/mono/references/products/doc/doc-media.md index 8aed95b2..3be8600b 100644 --- a/skills/mono/references/products/doc/doc-media.md +++ b/skills/mono/references/products/doc/doc-media.md @@ -4,7 +4,7 @@ > 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流 > ⚠️ **图片插入硬规则**: -> - 图片来源如果是钉盘/文档空间中的文件,**必须先下载到本地**(`dws doc download --node <图片nodeId> --output /tmp/xxx.png`),再执行 `media insert` +> - 图片来源如果是钉盘/文档空间中的文件,**必须先下载到本地**(`dws drive download --node <图片nodeId> --output /tmp/xxx.png`),再执行 `media insert` > - **禁止**把钉盘/文档节点 URL(如 `alidocs.dingtalk.com/i/nodes/...`)写进 Markdown `![](...)` 图片语法——这些是页面链接,不是可渲染的图片资源 > - 创建文档时需要图文并茂:先用 `doc create` 写入纯文本骨架,再对每张图片执行 `media insert`,最后用 `doc block list` 验证附件块存在 @@ -36,7 +36,7 @@ Flags: ### 关键说明 - `--mime-type` 可选,不指定时根据扩展名自动推断;支持常见文件类型(PDF、Office、图片、视频、压缩包等)。 -- 与 [`./doc-file-ops.md`](./doc-file-ops.md) `doc upload` 的区别:`upload` 将文件上传到文档空间/知识库作为**独立文件**;`media insert` 将文件作为**附件块插入到文档正文中**。 +- 与 `dws drive upload` 的区别:`drive upload` 将文件上传到文档空间/知识库作为**独立文件**;`media insert` 将文件作为**附件块插入到文档正文中**。 --- @@ -94,5 +94,5 @@ dws doc media download --node --resource-id - [`../doc.md` §意图判断](../doc.md#意图判断)(如何路由到本命令族) - [`./doc-block.md`](./doc-block.md)(block list 取 attachment 的 resourceId) -- [`./doc-file-ops.md`](./doc-file-ops.md)(独立文件上传:`doc upload`) +- [`../drive.md`](../drive.md)(独立文件上传:`dws drive upload`) - [`./style/doc-style-guideline.md` §4.9 附件与图片](./style/doc-style-guideline.md)(图示与附件使用规范) diff --git a/skills/mono/references/products/doc/doc-update.md b/skills/mono/references/products/doc/doc-update.md index f40a9b6c..34d1097b 100644 --- a/skills/mono/references/products/doc/doc-update.md +++ b/skills/mono/references/products/doc/doc-update.md @@ -27,8 +27,7 @@ Flags: --mode string 更新模式: overwrite=覆盖, append=追加 (必填) --content-format string 内容格式: 默认为 markdown,可选 jsonml --revision int 文档版本号(仅 --content-format jsonml 时生效,可选);传入后服务端做并发检查,版本不一致时返回 VersionConflict。不传则直接覆盖,不做并发检查 - --fix-jsonml 启用全部 JSONML 修复(含 JSON 语法修复 + 结构修复),推荐 agent 调用时使用 - --no-fix-jsonml 关闭全部 JSONML 修复(跳过 JSON 语法修复和结构修复),用于排查原始错误 + --fix-jsonml 启用 JSON 语法修复(括号/逗号补全),推荐 agent 调用时使用 --index int 插入位置(从 0 开始),仅在 mode=append 时生效。指定将内容插入到文档第几个 block 之前。不传时追加到末尾。block 的 index 可通过 doc block list 获取。插入成功后,该位置及之后所有 block 的 index 会依次 +1 ``` @@ -52,7 +51,7 @@ Flags: ]} ``` -> 默认模式下,CLI 会自动给缺 uuid 的 block 注入 uuid、把 `["p", {}, "hello"]` 之类的裸字符串自动包裹成上述 span/leaf 形式(每条修复都会以 `[FIX]` 行输出)。如需严格按原样发送,加 `--no-fix-jsonml`,结构错误会被 validator 直接拦下。如果输入来自 LLM 生成且可能有 JSON 语法错误(缺括号/逗号),加 `--fix-jsonml` 启用全部修复。 +> CLI 不做结构修复——裸字符串、缺 uuid 等错误会被 validator 直接抦下。body 必须以 `["root", ...]` 为根节点,缺少会报错。如果输入来自 LLM 生成且可能有 JSON 语法错误(缺括号/逗号),加 `--fix-jsonml` 启用 JSON 语法修复。 **典型流程**(无损读改写): diff --git a/skills/mono/references/products/doc/format/doc-jsonml-cookbook.md b/skills/mono/references/products/doc/format/doc-jsonml-cookbook.md index e0883c9f..b0847ca3 100644 --- a/skills/mono/references/products/doc/format/doc-jsonml-cookbook.md +++ b/skills/mono/references/products/doc/format/doc-jsonml-cookbook.md @@ -52,7 +52,7 @@ - 表格用 `table → tr → tc`(无 th/td),表头底色用 `tc` 的 `"fill"` 属性 - 关键数据着色用 leaf 的 `"color"`(绿=好 / 红=风险) - 状态标记用 leaf 的 `"highlight"`(黄=待确认、绿=完成、红=阻塞) -- uuid 可全部省略——CLI normalize 自动补充 +- uuid 必须显式提供——CLI 不再自动补充 ## ⚠️ JSONML 结构严格约束(生成时必须遵守) @@ -96,16 +96,16 @@ ## 核心规则 1. **每个 block 节点应有 uuid**:`["tag", {"uuid": "唯一ID"}, ...children]` - - insert 时可以自行生成任意唯一字符串,后端会自动分配正式 uuid + - insert 时必须提供 uuid(可自行生成任意唯一字符串,后端会自动分配正式 uuid) - update 时 uuid **必须**与 `--block-id` 一致 - - normalize **仅在 attrs 槽完全缺失**(如 `["p", "text"]`)时才补 uuid;如果你写了 `["p", {}, ...]`,normalize 会尊重这个空 attrs 不再补 uuid(这一行为保证 `doc read → doc update` 不污染原文档) + - uuid 必须显式提供,不再自动补充 2. **文本必须用 span + leaf 三层结构**,不要直接写裸字符串 - ✅ `["p", {"uuid": "x"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "hello"]]]` - - ❌ `["p", {"uuid": "x"}, "hello"]` — validator 会报错;默认模式下 CLI 会自动包成 ✅ 的形式并打印 `[FIX]` 行 + - ❌ `["p", {"uuid": "x"}, "hello"]` — validator 会报错,请手动包成 ✅ 的形式 - ⚠️ `["p", {"uuid": "x"}, ["text", {}, "hello"]]` — `text` 是历史 inline tag,validator 不会报错,但建议改写为 ✅ 形式以与 `dws doc read --content-format jsonml` 的输出保持一致 3. **attrs 对象必须存在**(即使为空):`["p", {}, ...]` 不能省略 `{}` -> **自动修复 vs 严格模式**:CLI 默认 normalize 会把 ❌ 的裸字符串自动包成 ✅;如要 1:1 透传原始 JSONML(例如复现服务端报错),用 `--no-fix-jsonml`,此时 validator 会以 `JSONPath + Suggestion` 形式逐条报错。如果输入来自 LLM 且可能有 JSON 语法错误(缺括号/逗号),用 `--fix-jsonml` 启用全部修复。 +> **严格模式(缺省)**:CLI 不做结构修复,裸字符串等错误会被 validator 以 `JSONPath + Suggestion` 形式逐条报错。如果输入来自 LLM 且可能有 JSON 语法错误(缺括号/逗号),用 `--fix-jsonml` 启用 JSON 语法修复。 ## 段落 (p) @@ -118,6 +118,11 @@ dws doc block insert --node --content-format jsonml \ dws doc block insert --node --content-format jsonml \ --element '["p", {"uuid": "new2"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf", "bold": true}, "加粗"], ["span", {"data-type": "leaf"}, "普通"], ["span", {"data-type": "leaf", "italic": true}, "斜体"]]]' +# 多行文本(每行一个 p,同一 p 内的多个 span 不会换行) +dws doc block insert --node --content-format jsonml \ + --element '["p", {"uuid": "line1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf", "bold": true}, "第一行标题"]]]' \ + --element '["p", {"uuid": "line2"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "第二行正文内容"]]]' + # 带链接(link 是与 text 并列的子节点) dws doc block insert --node --content-format jsonml \ --element '["p", {"uuid": "new3"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "请访问"]], ["a", {"href": "https://example.com"}, "链接文字"]]' @@ -126,11 +131,73 @@ dws doc block insert --node --content-format jsonml \ **leaf 支持的格式属性**: - `bold: true` — 加粗 - `italic: true` — 斜体 -- `underline: true` — 下划线 +- `underline: {"value": "single"}` — 下划线(value: `single`/`dash`/`wave`/`double`/`none`,可选 `color`) - `strike: true` — 删除线 -- `color: "#ff0000"` — 文字颜色 +- `dstrike: true` — 双删除线 +- `color: "#ff0000"` — 文字颜色(`#rrggbb` 格式) - `highlight: "#ffff00"` — 高亮背景色 -- `sz: 14` / `szUnit: "pt"` — 字号 +- `sz: 14` / `szUnit: "pt"` — 字号(szUnit 默认 `"px"`,推荐显式写 `"pt"`) + `fonts: {"ascii": "Arial", "eastAsia": "SimHei"}` — 字体(四分区:ascii/hAnsi/cs/eastAsia,值必须使用 font-family 名称,见下方字体表) +- `vertAlign: "superscript"` — 上标(`"subscript"` 下标,`"baseline"` 基线) +- `spacing: 2` — 字间距(单位 pt) + +**字体名称映射**(`fonts` 字段必须使用 font-family 值,不能写中文名): + +| 用户说法 | font-family 值 | 用户说法 | font-family 值 | +|---------|---------------|---------|---------------| +| 宋体 | `SimSun` | 黑体 | `SimHei` | +| 微软雅黑 | `Microsoft YaHei` | 微软雅黑UI | `Microsoft YaHei UI` | +| 仿宋 | `FangSong` | 仿宋_GB2312 | `FangSong_GB2312` | +| 楷体 | `KaiTi` | 楷体_GB2312 | `KaiTi_GB2312` | +| 等线 | `DengXian` | 新宋体 | `NSimSun` | +| 宋体-简 | `SimSun SC` | 宋体-繁 | `SimSun TC` | +| 黑体-简 | `Heiti SC` | 黑体-繁 | `Heiti TC` | +| 华文宋体 | `STSong` | 华文黑体 | `STHeiti` | +| 华文楷体 | `STKaiti` | 华文仿宋 | `STFangsong` | +| 华文中宋 | `STZhongsong` | 华文行楷 | `STXingkai` | +| 华文隶书 | `STLiti` | 华文新魏 | `STXinwei` | +| 华文细黑 | `STXihei` | 华文琥珀 | `STHupo` | +| 苹方-简 | `PingFang SC` | 苹方-繁 | `PingFang TC` | +| 苹方-港 | `PingFang HK` | 冬青黑-简 | `Hiragino Sans GB` | +| 兰亭黑-简 | `Lantinghei SC` | 兰亭黑-繁 | `Lantinghei TC` | +| 凌慧体-简 | `LingWai SC` | 幼圆 | `YouYuan` | +| 思源黑体 | `Source Han Sans CN` | 思源宋体 | `Source Han Serif CN` | +| 思源等宽 | `Source Han Mono SC` | 思源黑体Regular | `Source Han Sans CN Regular` | +| 阿里普惠体2.0 | `"Alibaba PuHuiTi 2.0"` | 阿里普惠体3.0 | `"Alibaba PuHuiTi 3.0"` | +| 钉钉进步体 | `DingTalk JinBuTi` | Adobe仿宋 | `Adobe 仿宋 Std` | +| 方正小标宋_GBK | `FZXiaoBiaoSong-B05` | 方正小标宋简体 | `FZXiaoBiaoSong-B05S` | +| 方正黑体 | `FZHei-B01S` | 方正楷体 | `FZKai-Z03S` | +| 方正仿宋 | `FZFangSong-Z02S` | 方正仿宋_GBK | `FZFangSong-Z02` | +| PMingLiU | `PMingLiU` | — | — | + +**英文字体**(font-family 值即为字体名): +`Arial` ・ `Calibri` ・ `Cambria` ・ `Centaur` ・ `Comfortaa` ・ `Comic Sans MS` ・ `Courier New` ・ `Franklin Gothic` ・ `Garamond` ・ `Georgia` ・ `Helvetica` ・ `Impact` ・ `Lora` ・ `Lucida Sans` ・ `Merriweather` ・ `Montserrat` ・ `Nunito` ・ `Oswald` ・ `Playfair Display` ・ `Roboto` ・ `Spectral` ・ `Times New Roman` ・ `Trebuchet MS` ・ `Verdana` + +> **规则**:优先从上表匹配;用户指定的字体不在列表时,使用该字体在操作系统中的真实 font-family 名称(如"更纱黑体" → `Sarasa Gothic SC`)。 + +**leaf 组合示例**: + +```json +["span", {"data-type": "leaf", "bold": true, "color": "#C62828", "sz": 16, "szUnit": "pt"}, "红色加粗大字"] +["span", {"data-type": "leaf", "strike": true, "color": "#9E9E9E"}, "已废弃内容"] +["span", {"data-type": "leaf", "vertAlign": "superscript"}, "[1]"] +["span", {"data-type": "leaf", "fonts": {"ascii": "Courier New", "eastAsia": "DengXian"}}, "等宽字体"] +``` + +**段落级排版属性**(写在 p/h1-h6 的 attrs 上): +- `jc: "center"` — 对齐(`left`/`center`/`right`/`both`/`justify`) +- `spacing: {"line": 1.5, "lineRule": "auto"}` — 行距(lineRule=auto 时 line 为倍数:1=单倍、1.5=1.5倍、2=双倍) +- `spacing: {"before": 12, "after": 8}` — 段前/段后间距(单位 pt) +- `ind: {"firstLine": 32}` — 首行缩进(≈ 2 中文字符) +- `ind: {"left": 96}` — 左缩进 + +**段落排版示例**: + +```json +["p", {"uuid": "p1", "jc": "center"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "居中段落"]]] +["p", {"uuid": "p2", "spacing": {"line": 1.5, "lineRule": "auto"}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "1.5倍行距"]]] +["p", {"uuid": "p3", "spacing": {"line": 2, "lineRule": "auto", "before": 12, "after": 8}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "双倍行距+段前后间距"]]] +``` ## 标题 (h1-h6) @@ -155,9 +222,11 @@ dws doc block update --node --block-id --content-format json dws doc block insert --node --content-format jsonml \ --element '["p", {"uuid": "li1", "list": {"listId": "mylist1", "level": 0, "isOrdered": false}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "无序列表第一项"]]]' -# 有序列表项 +# 有序列表项(仅第一项设 start,后续项不设,系统自动递增) dws doc block insert --node --content-format jsonml \ --element '["p", {"uuid": "li2", "list": {"listId": "mylist2", "level": 0, "isOrdered": true, "start": 1}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "有序列表第一项"]]]' +dws doc block insert --node --content-format jsonml \ + --element '["p", {"uuid": "li2b", "list": {"listId": "mylist2", "level": 0, "isOrdered": true}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "有序列表第二项"]]]' # 缩进子项(level: 1) dws doc block insert --node --content-format jsonml \ @@ -220,8 +289,10 @@ dws doc block insert --node --content-format jsonml \ ## 表格 (table) +> colsWidth 单位为 **pt**(页宽约 650pt)。如配合 `tblW: {"type": "pct"}` 则为百分比权重。 + ```bash -# 2行2列表格 +# 2行2列表格(各列 200pt) dws doc block insert --node --content-format jsonml \ --element '["table", {"uuid": "tb1", "colsWidth": [200, 200]}, ["tr", {"uuid": "tr1"}, ["tc", {"uuid": "tc1", "colSpan": 1, "rowSpan": 1}, ["p", {"uuid": "tcp1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "标题A"]]]], ["tc", {"uuid": "tc2", "colSpan": 1, "rowSpan": 1}, ["p", {"uuid": "tcp2"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "标题B"]]]]], ["tr", {"uuid": "tr2"}, ["tc", {"uuid": "tc3", "colSpan": 1, "rowSpan": 1}, ["p", {"uuid": "tcp3"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "数据1"]]]], ["tc", {"uuid": "tc4", "colSpan": 1, "rowSpan": 1}, ["p", {"uuid": "tcp4"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "数据2"]]]]]]]' ``` @@ -239,14 +310,19 @@ dws doc block insert --node --content-format jsonml \ ## 分栏布局 (columns) -分栏复用 table tag,通过 `sr: true` 区分。 +分栏复用 table tag,通过 `sr: true` 区分。分栏的 `tc` 可设置 `fill`(背景色)和 `border`(边框)属性提升视觉效果。 ```bash -# 两栏布局 +# 两栏布局(带背景色) dws doc block insert --node --content-format jsonml \ - --element '["table", {"uuid": "col1", "sr": true, "colsWidth": [300, 300]}, ["tr", {"uuid": "coltr"}, ["tc", {"uuid": "coltc1"}, ["p", {"uuid": "colp1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "左栏内容"]]]], ["tc", {"uuid": "coltc2"}, ["p", {"uuid": "colp2"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "右栏内容"]]]]]]' + --element '["table", {"uuid": "col1", "sr": true, "colsWidth": [300, 300]}, ["tr", {"uuid": "coltr"}, ["tc", {"uuid": "coltc1", "fill": "#EEF6FF", "vAlign": "top"}, ["p", {"uuid": "colp1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "左栏内容"]]]], ["tc", {"uuid": "coltc2", "fill": "#FFF3E0", "vAlign": "top"}, ["p", {"uuid": "colp2"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "右栏内容"]]]]]]' ``` +**分栏视觉属性**: +- `fill` — 单元格背景色(推荐淡色,如 `#EEF6FF` / `#FFF8E1` / `#F3E5F5`) +- `border` — 边框配置(可选) +- 分栏建议始终设置 `fill` 背景色,纯白底分栏视觉上与普通段落无异,读者无法感知分栏结构 + ## 嵌入块 (embed) 通用文件/iframe 嵌入。`embed` 是 void 块,仅含 attrs,无子节点。 @@ -374,7 +450,7 @@ dws doc block update --node --block-id --content-format json | 错误写法 | 问题 | 正确写法 | |---------|------|---------| -| `["p", {}, "文字"]` | 裸字符串。validator 会报 `段落子节点不能是裸字符串`;默认 normalize 会自动包成右侧形式(打印 `[FIX]` 行) | `["p", {}, ["span", {"data-type":"text"}, ["span", {"data-type":"leaf"}, "文字"]]]` | +| `["p", {}, "文字"]` | 裸字符串。validator 会报 `段落子节点不能是裸字符串`,请手动包成右侧形式 | `["p", {}, ["span", {"data-type":"text"}, ["span", {"data-type":"leaf"}, "文字"]]]` | | `["p", {}, ["text", {}, "文字"]]` | `text` 是历史 inline tag,validator 不报错但服务端实际渲染的 canonical 形式是 span/leaf;为与 `doc read` 输出一致,建议改写 | 同上,用 span + data-type | | `["callout", {}, ...]` | 不存在 callout tag | `["container", {"subType": "colorBlocks", ...}, ...]` | | `["list", {}, ...]` | 不存在 list tag | `["p", {"list": {...}}, ...]` | diff --git a/skills/mono/references/products/doc/format/doc-jsonml-schema.md b/skills/mono/references/products/doc/format/doc-jsonml-schema.md index ae162665..69375599 100644 --- a/skills/mono/references/products/doc/format/doc-jsonml-schema.md +++ b/skills/mono/references/products/doc/format/doc-jsonml-schema.md @@ -30,28 +30,23 @@ JSONML 是文档内容树的序列化格式: - 第二个元素为文档级属性对象(如 `sectPr` 页面设置),可选 - 后续元素为块级节点(每个 block 节点应带 `uuid`) -全量覆写(overwrite)时,CLI 接受三种 body 形态: +全量覆写(overwrite)时,CLI 要求 body 必须以 `["root", ...]` 为根节点: 1. `["root", {sectPr}, ...blocks]` — 服务端 canonical 形式,`doc read` 输出 -2. `[blocks, ...]` — 纯块数组(缺少 root,validator 会 warn 但不阻断) -3. 单个 `[tag, ...]` — 当作单 block,validator warn +2. `["root", {}, ...blocks]` — 无页面设置时用空 attrs -## CLI 行为概览(validator + normalize) +## CLI 行为概览(validator) -写入端(`doc create/update`、`block insert/update`)默认会走 **normalize → validate** 两步: +写入端(`doc create/update`、`block insert/update`)走 **validate** 一步,不做结构修复: -| 行为 | 缺省 | `--fix-jsonml` | `--no-fix-jsonml` | -|------|------|----------------|-------------------| -| JSON 语法修复(括号/逗号补全) | ✗ | ✓(打印 `[FIX]`) | ✗ | -| 解包单 block 为 body | ✓ | ✓ | ✗ | -| **attrs 槽完全缺失**时补 `attrs` + `uuid` | ✓ | ✓ | ✗ | -| 裸字符串 → `span/text + span/leaf` 自动包裹 | ✓(打印 `[FIX]`) | ✓(打印 `[FIX]`) | ✗ | -| validator 阻断(HasErrors → 拒绝发送) | ✓ | ✓ | ✓ | -| validator 警告(warnings → 仅 stderr) | ✓ | ✓ | ✓ | +| 行为 | 缺省 | `--fix-jsonml` | +|------|------|----------------| +| JSON 语法修复(括号/逗号补全) | ✗ | ✓(打印 `[FIX]`) | +| validator 阻断(HasErrors → 拒绝发送) | ✓ | ✓ | +| validator 警告(warnings → 仅 stderr) | ✓ | ✓ | +| root 校验(仅 doc create/update) | ✓ | ✓ | -> **uuid 注入的边界**:仅当 attrs 槽**完全缺失**(如 `["p", "text"]`)才补 `attrs` + `uuid`。当生产者已显式给出 `attrs`(哪怕是 `{}` 或不含 uuid),normalize 一律不再补 uuid —— 视为生产者明确意图。这避免 `doc read → doc update` 回灌时污染原文档中未修改的节点(真实文档中常见 `["h1", {}, ...]` 形态)。 - -三态设计:缺省 = 结构修复 ON + JSON 语法修复 OFF;`--fix-jsonml` 全开(含 JSON repair,推荐 agent 调用);`--no-fix-jsonml` 全关(用于排查原始错误)。校验始终执行,不可跳过。 +> `doc create/update` 要求 body 必须以 `["root", {attrs?}, ...blocks]` 为根节点。缺少 root 会报错而非自动包装。`doc block insert/update` 不要求 root。 报错格式(面向 agent): @@ -89,12 +84,12 @@ Suggestion: ["span",{"data-type":"text"},["span",{"data-type":"leaf"}," Marks 表的属性集对 canonical 的 leaf 和 legacy 的 text 都适用;差别仅在承载位置(leaf 的 attrs vs text 的 attrs)。 @@ -133,9 +139,9 @@ Suggestion: ["span",{"data-type":"text"},["span",{"data-type":"leaf"},"= 2KB 时必须写入 UTF-8 临时 `.md` 文件 | | 格式 | 按 §JSONML 起稿判定 决定起稿路径:命中 JSONML 起稿条件时**直接用 JSONML 构造**(跳过 markdown);未命中时用 Markdown 起稿,创建后按 [doc-update-workflow.md](./doc-update-workflow.md) 精修 | @@ -75,13 +75,171 @@ --- +## 设计规划(JSONML 起稿前必做) + +判定走 JSONML 路径后,**禁止立即动手写 JSONML**。先完成以下两阶段规划,各自产出一个持久文件作为后续阶段的锚点。 + +### 设计原则(全程生效) + +1. **结构即信息** — 标题层级、表格 vs 分栏 vs 列表、callout 位置都应编码内容逻辑。问自己:“去掉这个结构元素,读者会丢失信息或体验变差吗?”— 丢失信息则必保留;不丢失信息但能提升可读性或美观度(如分割线分隔章节、分栏对比排版)也应保留;既不携带信息也不提升体验的装饰元素才删除。 +2. **视觉层级引导阅读** — 每一屏必须让读者瞥一眼就能回答:“这块最重要的是什么?”字号/粗体/颜色形成明确梯度:标题 > 重点数据 > 正文 > 辅助信息。 +3. **克制产生质感** — 遵循 60-30-10 配色比例:60% 中性底色(白/浅灰)、30% 辅助色、10% 强调色。多色系共存时需保持**同等饱和度**并各有语义角色(如淡蓝=信息、淡黄=提示、淡红=风险),同一色系内深浅变化自由。callout 不超过 2 个。 +4. **设计先于执行** — 从规划阶段起每个结构块的样式就已确定,执行时(无论直接 JSONML 还是脚手架精修)只是落地已有设计,不是边写边想。 +5. **同类同色、一色多阶** — 同类信息必须使用相同色系;单一色系按元素角色展开为深/中/浅/极浅四级(标题文字用深色、强调用中色、高亮/表头用浅色、背景用极浅色),不要全篇只用一个 hex 值。 + +### Phase 1:RFC — 需求理解与设计方向 + +**目标**:明确“做什么”和“为什么这样做”,形成方向性锚点。 + +#### 1.1 需求提取(全量列出用户显式要求) + +通读用户 prompt,抽取两类要求并编为清单: + +**内容要求**: +- 标题、字数、章节划分 +- 数据来源、受众 +- 语气/风格(如“大气”“专业”“轻松”) + +**样式要求**(每一条都必须在最终输出中体现,不得遗漏): +- 字体:映射为 font-family 名称(参照 cookbook 字体映射表),区分“全文字体”和“特定元素字体” +- 字号、行距、对齐、颜色 +- 强调手段(加粗、高亮、配色…) +- 约束(如“每部分不省略”) + +> 用户没有明确指定的维度(如未指定行距、未指定表格样式)由 Phase 2 补充设计决策。 + +#### 1.2 内容-表现适配(每个章节的内容适合用什么元素) + +对每个章节回答:“这个内容的核心是什么类型的信息?”→ 选择最佳元素: + +**块级结构元素**: + +| 信息类型 | 首选元素 | 不适合 | +|----------|---------|--------| +| 多个同类实体对比(≥ 4 项或 ≥ 3 维度) | 彩色表头表格 | 纯文本段落 | +| 少量实体对比(2-3 项× 少量维度) | 分栏(每栏一个实体,可设边框/背景色) | 大宽表格 | +| 时间序列/流程(行程/步骤) | 有序列表 + 粗体时间标签 | 无序列表 | +| 单个结论/推荐/重要提示 | callout(“花大胆”的地方) | 普通段落 | +| 描述性文字(背景/说明) | 正文段落 + 关键词粗体 | 表格 | +| 分类列举(特色/亮点) | 无序列表 | 表格(数据不够多列时) | +| 数值强调(评分/价格/统计) | 加粗 + 着色 | 跳过不强调 | +| 引用原文(用户评价/网友点评/官方说明) | 引用块 | 普通段落 | +| 任务/待办清单 | checklist(`- [ ]`) | 普通列表 | +| 章节分隔/主题转换 | 分割线(`hr`) | 空行 | +| 板块内子区域分隔(同一单元格/容器内多个逻辑段) | hr 内部分隔(在 tc 或 container 内部使用) | 空行或留白 | +| 结构化元信息(人/时间/地点/属性清单) | 键值对表格(窄标签列 ~15-20% + 宽内容列) | 多行段落 | +| 分类标签/状态标记 | 标签元素(tag) | 纯文本标记 | + +**行内强调元素**: +- **emoji** — 用于 callout 前缀、状态标记、H2/H3 标题前;不在普通段落和列表项中滥用 +- **加粗/高亮/着色** — 强调关键数据和结论 +- **highlight 色带** — 在标题 span 上设 `"highlight": "#浅色"` 形成轻量色条标记,比 callout 更轻,适合区分多个并列板块的主题色 +- **灰色辅助文字** — 用浅灰色(如 `#979A9B`)标记示例/说明/占位文字,与正文形成明确的主次层级 +- **图片** — 实景照片、截图、示意图能显著提升理解时使用 + +**分栏选型补充**:分栏栏数无上限,但推荐 2-4 栏(≥5 栏会比较拥挤),可设置边框和背景色。**建议设置 `fill` 背景色**(纯白底分栏视觉上与普通段落无异,读者不易感知分栏结构)。适合场景: +- 2-3 个同类实体并排展示(每栏一个实体,含标题 + 描述 + 关键数据) +- 轻量对比:“优点 vs 缺点”、“方案 A vs B”、“Day 1 vs Day 2” +- 网格布局:多次插入同栏数分栏可形成卡片网格(如 2×3、3×2),适合 4-9 个结构相同、内容等长的卡片式实体。**约束**:每个格子内容必须结构一致且长度相近,否则高低不齐会很丑;实体数不能整除栏数时不使用 + +例如:“5 个实体多维度对比” → **彩色表头表格**;“两组信息并排” → **分栏**;“6 个结构相同的卡片” → **2×3 分栏网格**。 + +#### 1.3 起稿策略选择 + +根据文档复杂度和上方适配结果,确定起稿路径:直接 JSONML / Markdown 脚手架 + JSONML 精修 / 直接 JSONML 分段构造。具体条件和流程见下方「起稿」节的策略表。 + +#### 落盘 + +将以上内容写入 `-rfc.md`,包括: +- 需求摘要(内容要求 + 样式要求) +- 每章展现策略 + 选择理由 +- 起稿策略 + +--- + +### Phase 2:Spec — 精确设计参数 + +**目标**:将 RFC 的方向决策转化为可直接执行的参数。 + +#### 2.1 视觉体系设计(确定全局设计变量) + +在以下四个维度做出明确选择,每个选择都必须能解释为什么适合这篇文档: + +**色彩**(选定主色后展开为色阶): + +用户指定颜色时(如"蓝色""绿色"),不要全篇只用一个 hex 值。将其展开为 4 级色阶,按元素角色分配: + +| 角色 | 用途 | 色阶要求 | +|------|------|----------| +| 深色 | 标题文字、重点数据 `color` | 白底上高对比可读 | +| 中色 | 正文强调、链接 `color` | 辨识度高但不抢标题 | +| 浅色 | highlight 色带、表头 fill | 底色柔和,上方深色文字可读 | +| 极浅 | container bgcolor、大面积背景 | 接近白色,仅提供区域感 | + +**规则**: +- 深→浅的层级关系不可颠倒(不能用极浅色做标题文字、不能用深色做背景) +- 同类板块/同类标题必须使用完全相同的色阶组合,通过色彩的重复形成视觉韵律 +- 不同类别可用不同色系区分(如:任务=蓝系、风险=红系、成果=绿系) +- 遵循 60-30-10 配色比例:60% 中性底色、30% 辅助色、10% 强调色;用户指定的颜色值优先 + +**字体梯度**(形成明确层级): +- 主标题(h2):字体 / 字号 / 粗体 / 颜色 +- 正文:字体 / 字号 / 行距 +- 强调文字:粗体 + 颜色或高亮 +- 辅助信息(注释/来源):字号偏小 / 灰色 + +**表格风格**(有表格时): +- 表头:底色 + 文字色 + 是否粗体 +- 单元格:默认对齐 / 字号 + +**视觉重心**(克制原则落地): +全篇选 1-2 处给予最强视觉处理(配色 callout / 彩色表头 / 分栏对比),其余元素保持朴素。不要处处强调 — 处处强调等于没有强调。 + +callout 可通过 `"showstk": true, "sticker": "图标名"` 配置顶部贴纸图标(如“灯泡”“火”“钉子”),增强语义标识。设置了 sticker 后,高亮块内首个段落不要再以 emoji 开头,避免紧邻的位置出现两个图标。 + +#### 落盘与回验 + +1. 将以上内容写入 `-design.md`,包括: + - 逐章元素映射(用什么标签、什么属性) + - 色阶具体 hex 值 + - 字体梯度参数 + - 表格风格细节 + +2. **回验**:Read `-rfc.md`,逐条确认: + - [ ] RFC 中每条需求在 Spec 中有对应实现 + - [ ] 展现策略在 Spec 中有具体参数支撑 + - [ ] 配色遵循 60-30-10 比例、callout ≤ 2 个、多色系饱和度一致且有语义角色 + +回验通过后进入下一节开始构造 JSONML。 + +--- + ## JSONML 起稿(命中判定时使用) -当判定为 JSONML 起稿时,在本地临时 JSON 文件中直接构造完整的 JSONML 文档树。路径:`/tmp/.json`。 +根据 RFC 中确定的起稿策略执行: + +| 策略 | 条件 | 执行流程 | +|------|------|----------| +| **直接 JSONML** | 短文档(≤ 15 块级节点)且结构简单 | 在 `/tmp/.json` 手写完整 JSONML 树 → `doc create --content-format jsonml` | +| **Markdown 脚手架 + JSONML 精修** | 长文档且结构较线性 | ① Markdown 建立内容骨架 → `doc create --content-format markdown` ② `doc read --node --content-format jsonml --output /tmp/.json` 拉回 JSONML(已含 uuid) ③ **Read `-rfc.md` + `-design.md`** 回顾规划 ④ 逐章对照执行结构变换 + 叠加样式 | +| **直接 JSONML 分段构造** | 长文档且含大量富结构 | 按章节分段构造 JSONML,每段写完校验通过后再写下一段,最后拼接 | + +> **脚手架策略警示**:Markdown 无法表达分栏/callout/色彩表头,拉回的 JSONML 只有纯文本骨架。精修阶段不是“在现有结构上加色”,而是“参照 RFC/Spec 重组结构”。 > **MUST READ**:动手写 JSONML 前,必须先用 Read 工具读取 [doc-jsonml-cookbook.md](../format/doc-jsonml-cookbook.md) — 其中 §决策型文档骨架范例 有可直接复制修改的完整模板。 > 节点类型和属性的权威定义见 [doc-jsonml-schema.md](../format/doc-jsonml-schema.md)。 +### ⚠️ JSONML 降级约束 + +**禁止因一次校验失败就放弃 JSONML 降级为 Markdown。** 当用户需求已触发 JSONML 起稿判定时,JSONML 是实现其样式要求的首选路径。失败时的处理策略: + +1. **校验报错** → 读取错误信息,定位具体节点,修复后重试 +2. **JSON 语法错误** → 检查括号匹配、逗号、引号,修复后重试 +3. **反复失败(≥3 次)** → 尝试简化结构(减少嵌套、拆分复杂节点)再试 +4. **仍然失败** → 退化为「Markdown 脚手架 + JSONML 精修」路径(流程同上方策略表),并告知用户当前状况 + +> “由于 JSONML 结构复杂且容易出错,改用 Markdown” — 这不是合法降级理由。必须先充分重试,且降级后仍需通过精修补回样式。 + ### ⚠️ JSONML 结构严格约束(生成时必须遵守) 每个节点是一个 JSON 数组:`[tagName, attributes?, ...children]` @@ -99,6 +257,10 @@ | 多余逗号 | `["p", {},]` | JSON 解析失败 | | 缺少逗号 | `["p", {} ["span"]]` | JSON 解析失败 | | 引号不匹配 | `["p", {"uuid": "abc}]` | JSON 解析失败 | +| 有序列表每项都设 `start:1` | `{"start":1}` 在每项重复 | 所有项编号重置为 1(显示为 a/a/a) | +| 列表 `level` 从 1 开始 | `"level": 1` 作为顶级 | 顶级列表项多一层缩进;建议:顶级 `"level": 0`,子级 `"level": 1` | +| 用 `fontFamily` 设字体 | `"fontFamily": "Arial"` | 校验报错;正确写法:`"fonts": {"ascii": "Arial", "eastAsia": "..."}` | +| 用多个 span “换行” | 同一 `p` 内放两个 `span` | 不会产生换行;每个换行必须是独立的 `p` 节点 | ### 基础结构 @@ -114,14 +276,20 @@ - 根节点固定 `"root"`(不是 `"body"`) - `--name` 已是 H1,JSONML 从 `h2` 开始 - 表格结构是 `table → tr → tc`(无 `th`/`td`) -- uuid 可省略(CLI normalize 自动补) +- 分栏是 `table` + `"sr": true`,`tc` 建议设 `fill` 背景色 +- 有序列表:仅第一项设 `"start": 1`,后续项不设 `start`(系统自动递增) +- 列表 `level` 建议从 0 开始:顶级项 `"level": 0`,子项 `"level": 1`,以此类推 +- uuid 必须显式提供(CLI 不自动生成) +- **每行内容对应一个 `p` 节点** — 同一 `p` 内的多个 `span` 不会换行,只会横向拼接;需要换行时必须拆分为多个 `p` ### 视觉设计要点 构造时主动使用这些属性实现视觉效果: - **文字着色**:leaf 上 `"color": "#hex"`、`"highlight": "#hex"` +- **highlight 色带**:标题 leaf 上 `"highlight": "#浅色"` 可形成色条效果(比 callout 更轻量的板块标记) - **字号**:leaf 上 `"sz": 14, "szUnit": "pt"` - **callout**:`["container", {"subType": "colorBlocks", "metadata": {"bgcolor": "#E8F5E9", "border": "left"}}, ...blocks]` +- **callout + sticker**:`["container", {"subType": "colorBlocks", "metadata": {"bgcolor": "#FEF3F3", "showstk": true, "sticker": "火"}}, ...blocks]` - **表格单元格底色**:tc 上 `"fill": "#hex"` ### 写入 @@ -147,12 +315,12 @@ dws doc read --node --content-format jsonml --output /tmp/-readba - 只使用用户已提供或对话中已确认的正文素材。 - 如果正文素材不足,先补齐文档目标、受众、章节和缺口;不要在本文中临时扩展跨产品采集流程。 - **先按 [doc-style-guideline.md §2.0 类型判断决策表](./doc-style-guideline.md) 确定文档类型,再用对应类型的骨架样板(§2.1 决策型 / §2.2 执行型 / §2.3 说明型 / §2.4 知识沉淀型)**。不要套通用三段式。 -- **`--name` 已是 H1,正文从 `##` 开始**;正文内不要再写 `#` 一级标题(除非确实需要正文内再造一级 H1 并说明动机)。与 `--name` 相同的首行一级标题会被 CLI 自动移除(stderr 有提示),但不要依赖这个兜底。 +- **`--name` 已是 H1,正文从 `##` 开始**;正文内不要再写 `#` 一级标题(除非确实需要正文内再造一级 H1 并说明动机)。 - 摘要、bullet、引用块、callout 等元素的使用边界以 style-guideline §3-§7 为准。 - 同类信息保持一致:风险、状态、行动项各用一种元素 + 一种视觉语义(style-guideline §1.2 / §5)。 - 临时文件必须保留真实换行,不能把换行写成字面量 `\n`。 - Markdown 草稿阶段**不要**写 callout / 分栏 / 附件——这些留到「创建后的精修」用 `doc block insert` 操作(style-guideline §1.3)。 -- **图片素材闭环(硬规则)**:正文需求含图片/截图/图文并茂时,**禁止**在 Markdown 中写 `![](...)` 图片语法(包括真实存在的 alidocs URL)。正确做法:Markdown 只写文本骨架和图片占位说明(如 `📌 此处插入:xxx 产品截图`),创建文档后逐个执行 `dws doc media insert --node --file <本地图片路径>` 插入,最后用 `dws doc block list --node ` 验证图片块存在。图片来源如果是钉盘文件,必须先 `dws doc download --node <图片nodeId> --output /tmp/xxx.png` 下载到本地再 insert。 +- **图片素材闭环(硬规则)**:正文需求含图片/截图/图文并茂时,**禁止**在 Markdown 中写 `![](...)` 图片语法(包括真实存在的 alidocs URL)。正确做法:Markdown 只写文本骨架和图片占位说明(如 `📌 此处插入:xxx 产品截图`),创建文档后逐个执行 `dws doc media insert --node --file <本地图片路径>` 插入,最后用 `dws doc block list --node ` 验证图片块存在。图片来源如果是钉盘文件,必须先 `dws drive download --node <图片nodeId> --output /tmp/xxx.png` 下载到本地再 insert。 ## 创建写入 @@ -230,7 +398,7 @@ dws doc update --node --content-file /tmp/-resume.md --mode appen 精修常见入口(**按 [doc-update-workflow.md §1.3](./doc-update-workflow.md) 优先级排序:JSONML 首选**): -- 单 block JSONML 精修(首选):`doc block list --node --content-format jsonml --block-id ` 取子树 → `doc block update --node --block-id --content-format jsonml --element '[...]'` 写回(uuid 必须 == --block-id;写入端默认 normalize + validate,详见 [doc-update-workflow.md §4.4](./doc-update-workflow.md)) +- 单 block JSONML 精修(首选):`doc block list --node --content-format jsonml --block-id ` 取子树 → `doc block update --node --block-id --content-format jsonml --element '[...]'` 写回(uuid 必须 == --block-id;写入端默认执行 schema validate,详见 [doc-update-workflow.md §4.4](./doc-update-workflow.md)) - 整篇 JSONML 无损:`doc update --content-format jsonml --mode overwrite`(默认直接覆盖,适合一次改多处或改 root sectPr;担心并发覆盖时加 `--revision ` 触发并发检查) - 插入附件 / 图片:`doc media insert`(无 JSONML 形态,直接走 element) - element JSON 次选:`doc block insert` / `doc block update` 不带 `--content-format jsonml` 时按老接口 JSON 解析;仅在 JSONML 不支持某字段时使用 diff --git a/skills/mono/references/products/doc/style/doc-style-guideline.md b/skills/mono/references/products/doc/style/doc-style-guideline.md index 1e25e87a..1e00cad5 100644 --- a/skills/mono/references/products/doc/style/doc-style-guideline.md +++ b/skills/mono/references/products/doc/style/doc-style-guideline.md @@ -56,7 +56,7 @@ 1. **读者**:谁打开这篇文档?读完要做什么动作(操作 / 选择 / 查参数 / 看推理)? 2. **唯一记忆点**:读者关掉文档后,最想让他记住的一句话是什么?这句话决定开头摘要 / callout 该写什么。 -3. **形态**:按 §2.0 推荐格式列 + [doc-create-workflow.md §JSONML 起稿判定](./doc-create-workflow.md#jsonml-起稿判定) 决定路径。命中判定条件(决策型 / 含对比的知识沉淀型 / 用户意图关键词)→ **直接 JSONML 起稿**;未命中 → markdown 起稿 + 创建后精修。 +3. **形态**:按 §2.0 推荐格式列 + [doc-create-workflow.md §JSONML 起稿判定](./doc-create-workflow.md#jsonml-起稿判定) 决定路径。命中判定条件(决策型 / 含对比的知识沉淀型 / 用户意图关键词)→ **直接 JSONML 起稿**(但必须先完成 [doc-create-workflow.md §设计规划](./doc-create-workflow.md#设计规划jsonml-起稿前必做) 的 4 步规划);未命中 → markdown 起稿 + 创建后精修。 写完自检时回看这三个答案:开头有没有兑现「记忆点」、形态有没有兑现「推荐格式」。两条任一不兑现,按 §8 自检表对应行动。 diff --git a/skills/mono/references/products/doc/style/doc-update-workflow.md b/skills/mono/references/products/doc/style/doc-update-workflow.md index 38e26ef7..45d1cb88 100644 --- a/skills/mono/references/products/doc/style/doc-update-workflow.md +++ b/skills/mono/references/products/doc/style/doc-update-workflow.md @@ -46,7 +46,7 @@ JSONML 模式下这些元素的节点结构见 [doc-jsonml-schema.md](../format/ | 优先级 | 形态 | 适用 | |--------|------|------| -| ① 首选 | `--content-format jsonml` | 保真度最高;callout / 分栏 / 表格 / @人 / 附件 / 颜色 / 嵌套结构都能 1:1 round-trip;写入端有 normalize + validator 兜底(§4.4) | +| ① 首选 | `--content-format jsonml` | 保真度最高;callout / 分栏 / 表格 / @人 / 附件 / 颜色 / 嵌套结构都能 1:1 round-trip;写入端有 validator 兜底(§4.4) | | ② 次选 | `--content-format element`(JSON,老接口) | JSONML 不支持某个块字段时;或快速插入 callout / 分栏不想构造 JSONML 时;不保真改写正文 | | ③ 兜底 | markdown(不带 `--content-format` 即默认)| 纯文本追加、整篇重排骨架;callout / 分栏 / 颜色 / 部分属性会被 markdown 还原过程丢失 | @@ -106,7 +106,7 @@ JSONML 模式下这些元素的节点结构见 [doc-jsonml-schema.md](../format/ ## 四、改写路径详细 -> **首选 JSONML(§4.4)**——保真度最高且 normalize/validator 兜底;本节其余路径(markdown / element)仅在 §1.3 列出的"次选 / 兜底"场景下使用。 +> **首选 JSONML(§4.4)**——保真度最高且 validator 兜底;本节其余路径(markdown / element)仅在 §1.3 列出的"次选 / 兜底"场景下使用。 ### 4.1 段落级 overwrite(markdown 兜底路径) @@ -161,7 +161,7 @@ dws doc block insert --node --ref-block --where after --cont ### 4.4 JSONML 无损改写(**首选路径**) -> 改写已有文档**默认走本节**——保真度最高,callout / 分栏 / 表格 / @人 / 附件 / 颜色 / 嵌套都能 1:1 round-trip;写入端有 normalize + validator 兜底。其他路径(§4.1/4.2/4.3/4.5 markdown)仅在 §1.3 列出的"次选 / 兜底"场景下使用。 +> 改写已有文档**默认走本节**——保真度最高,callout / 分栏 / 表格 / @人 / 附件 / 颜色 / 嵌套都能 1:1 round-trip;写入端有 validator 兜底。其他路径(§4.1/4.2/4.3/4.5 markdown)仅在 §1.3 列出的"次选 / 兜底"场景下使用。 两条子路径: @@ -201,16 +201,15 @@ dws doc update --node --content-file /tmp/doc_modified.json \ > **并发安全模式(担心被并发覆盖时使用)**:如果担心多 agent 同时改这篇文档,可以把第 1 步 read 返回的 `revision` 通过 `--revision ` 透传给第 4 步:服务端会做并发检查,版本不一致返回 `VersionConflict`,此时回到第 1 步重读重写即可。普通单 agent 改写场景默认不传 `--revision`。 -#### JSONML 写入端的 normalize 与 validator +#### JSONML 写入端的 validator -写入命令(`doc create/update` + `doc block insert/update`)默认按 **normalize → validate** 两步处理 JSONML: +写入命令(`doc create/update` + `doc block insert/update`)走 **validate** 一步,不做结构修复: -| 行为 | 缺省 | `--fix-jsonml` | `--no-fix-jsonml` | -|------|------|----------------|-------------------| -| JSON 语法修复(括号/逗号补全) | ✗ | ✓(打印 `[FIX]`) | ✗ | -| 注入缺失的 block `uuid` | ✓ | ✓ | ✗ | -| 裸字符串 → 包成 `["span",{"data-type":"text"},["span",{"data-type":"leaf"},"..."]]` | ✓(stderr 打印 `[FIX]`) | ✓ | ✗ | -| validator 阻断(HasErrors → 拒发) | ✓ | ✓ | ✓ | +| 行为 | 缺省 | `--fix-jsonml` | +|------|------|----------------| +| JSON 语法修复(括号/逗号补全) | ✗ | ✓(打印 `[FIX]`) | +| validator 阻断(HasErrors → 拒发) | ✓ | ✓ | +| root 校验(仅 doc create/update) | ✓ | ✓ | 报错格式(agent 友好): @@ -219,11 +218,11 @@ $[2][2]: paragraph child must be span wrapper, got raw string. Suggestion: ["span",{"data-type":"text"},["span",{"data-type":"leaf"},""]] ``` -三态设计: +设计要点: -- 不传:结构修复 ON + JSON 语法修复 OFF + 校验 ON(推荐人工调用) -- `--fix-jsonml`:全部修复 ON(含 JSON 语法修复,推荐 agent 调用) -- `--no-fix-jsonml`:全部修复 OFF,校验仍 ON;用于排查原始错误 +- 缺省为严格模式:不做结构修复,裸字符串、缺 uuid 等错误会被 validator 抦下。 +- `doc create/update` 要求 body 必须以 `["root", ...]` 为根节点,缺少会报错。`doc block insert/update` 不要求 root。 +- `--fix-jsonml`:启用 JSON 语法修复(修复 LLM 遗漏的括号/逗号),推荐 agent 调用。 **何时不走本节、改用 markdown**:纯文本追加章节(§4.2)、整篇按全新骨架重写(§4.5,且无富结构需要保留时)、只在乎"加一段文字"且确认目标段落无 callout / 分栏 / 颜色 / @人 / 附件。其余场景默认本节。 diff --git a/skills/mono/references/products/drive.md b/skills/mono/references/products/drive.md index 30145280..6304f2c1 100644 --- a/skills/mono/references/products/drive.md +++ b/skills/mono/references/products/drive.md @@ -10,6 +10,7 @@ dws drive --help # 查看具体命令的完整参数说明 dws drive list --help +dws drive search --help dws drive upload --help dws drive download --help ``` @@ -30,7 +31,7 @@ Example: dws drive list --limit 20 dws drive list --limit 20 --folder --order-by name --order asc Flags: - --limit int 每页返回数量,默认 20,最大 100 (可选) + --limit int 每页返回数量,默认 20,最大 50 (可选) --cursor string 分页游标,首次不传 (可选) --order string 排序方向: asc|desc,默认 desc (可选) --order-by string 排序字段: createTime|modifyTime|name (可选) @@ -65,6 +66,43 @@ spaceType 筛选规则: - `spaceType` — 空间类型(如 `orgSpace`) - `nextToken` — 若不为空,表示还有更多空间可查询(仅企业空间) +### 搜索钉盘文件/文件夹/空间 + +按关键词在钉盘中搜索文件、文件夹或团队空间。不同于 `list`(需要明确的 spaceId/parentId 逐层遍历),`search` 用于不知道具体位置、只记得名称/关键词的场景。 + +``` +Usage: + dws drive search [flags] +Example: + dws drive search --query "季度汇报" + dws drive search --query "合同" --target file --extensions pdf,docx + dws drive search --query "项目" --target space + dws drive search --query "方案" --created-from 1700000000000 --created-to 1710000000000 + dws drive search --query "周报" --creator-uids 012345 + dws drive search --query "报告" --limit 30 --cursor +Flags: + --query string 搜索关键词 (必填) + --target string 搜索目标: all(默认) | file | space (可选) + --file-types strings 按文件内容类型过滤,逗号分隔: alidoc,document,image,video,audio,archive (仅 target=file/all 生效) + --extensions strings 按文件扩展名过滤,不含点号,逗号分隔 (如 pdf,docx,adoc) + --creator-uids strings 按创建者用户 ID 过滤,逗号分隔 + --created-from int 创建时间起始 (毫秒时间戳,含) + --created-to int 创建时间截止 (毫秒时间戳,含) + --modified-from int 修改时间起始 (毫秒时间戳,含) + --modified-to int 修改时间截止 (毫秒时间戳,含) + --limit int 每页返回数量(默认 10,最大 30) + --cursor string 分页游标,从上次返回的 nextCursor 获取 (可选) +``` + +搜索目标 (`--target`) 选择规则: +- `all`(默认):同时搜文件与空间,返回混合结果 — 不确定目标是文件还是空间时使用 +- `file`:只搜文件 / 文件夹,支持 `--file-types` / `--extensions` 过滤 — 明确是找文件时使用 +- `space`:只搜团队空间 — 明确知道空间名、需快速定位空间 spaceId/rootFolderId 时使用 + +返回结果中 `type` 字段区分:`SPACE`(空间)、`FILE`(普通文件)、`FOLDER`(文件夹)、`ALIDOC`(钉钉在线文档)。 + +> **提示**:结果按相关性排序,首页未命中时优先调整关键词 / 补充 `--file-types`/`--extensions` 缩小范围 / 加上时间范围,而非反复翻页。 + ### 获取文件元数据信息 ``` @@ -85,8 +123,8 @@ Flags: | extension | 文件类型 | 操作 | 命令 | |-----------|---------|------|------| | adoc | 在线文档 | 在线获取 Markdown 内容 | `dws doc read --node ` | -| axls | 在线表格 | 在线读取表格数据 | `dws sheet list` → `dws sheet range read` | -| able | 多维表格 | 在线查询记录 | `dws aitable table list` → `dws aitable record query` | +| axls | 在线表格 | 在线读取表格数据 | `dws sheet get-all-sheets` → `dws sheet get-range` | +| able | 多维表格 | 在线查询记录 | `dws aitable get-tables` → `dws aitable query-records` | | 其他(pdf/docx/txt/png 等) | 普通文件 | **不支持在线分析**,需用户主动下载后本地查看 | `dws drive download` | ### 下载文件到本地 @@ -161,18 +199,27 @@ Flags: 用户说"我的文件/钉盘/网盘/云盘" → `list` 用户说"钉盘空间/团队文件/有哪些空间/空间列表/团队文件列表" → `list-spaces` +用户说"搜索钉盘文件/钉盘里找个文件/查找某个钉盘文件/钉盘中搜索" → `search` 用户说"文件详情/文件信息" → `info` 用户说"下载文件" → `download` -用户说"新建文件夹/创建目录" → `mkdir` +用户说"新建文件夹/创建目录" → `mkdir`(钉盘空间)/ `folder create`(文档空间) 用户说"上传文件/传文件到钉盘" → `upload`(必须使用此命令,自动完成三步流程) -用户说"复制文件/移动文件/搬到/移到" → 使用 `dws doc copy`/`dws doc move`(详见下方「复制/移动钉盘文件」工作流) +用户说"复制文件/移动文件/搬到/移到" → `copy` / `move` +用户说"重命名/改名" → `rename` 用户说"删除文件/删除文件夹/移到回收站" → `delete`(危险操作,需确认) +用户说"给文档授权/分享权限" → `permission add` + +关键区分: drive(文件管理) vs doc(文档内容读写) vs wiki(空间管理) + +**drive search vs wiki node search**: 用户提到"钉盘/网盘/我的文件里搜" → `drive search`;提到"知识库/文档空间/workspace 里搜" → `wiki node search`;未明确目标时优先问明。 -关键区分: drive(钉盘文件管理) vs doc(文档内容读写) +**drive upload**: 文件上传统一走 `drive upload`。上传到知识库/文档空间时加 `--workspace` 参数。 -**drive upload vs doc upload**: 用户提到"钉盘/网盘/我的文件"→ `drive upload`;提到"知识库/文档空间/workspace"→ `doc upload`;未明确目标时默认 `drive upload` +**drive permission vs wiki member**: "给某篇文档/文件授权" → `drive permission add`(节点级);"给某个知识库整体加成员" → `wiki member add`(空间级) -**钉盘文件复制/移动**: drive 本身没有 copy/move 命令,需使用 `dws doc copy`/`dws doc move` 实现(详见下方工作流) +**创建在线文档/表格/脑图**: drive 不支持创建文件,需走 `wiki node create --type `(创建空节点)或 `doc create`(创建并写入内容)。 + +**导出文档/导出为Word**: 导出是内容层操作,走 `doc export`,不属于 drive。 ## 核心工作流 @@ -201,57 +248,85 @@ dws drive upload --file ./报告.pdf --folder --format json dws drive delete --node --yes --format json ``` -## 复制/移动钉盘文件 +## 文档空间管理命令 + +> 以下命令操作的是**文档空间**(知识库 / 我的文档),底层路由到 doc MCP server。 +> 与钉盘命令(list / mkdir / upload 等)的区别:钉盘命令操作钉盘空间(spaceId 纯数字),文档空间命令操作知识库/我的文档(workspaceId 加密 string)。 + +### 复制/移动/重命名文件 + +``` +Usage: + dws drive copy --node [--folder ] [--workspace ] + dws drive move --node [--folder ] [--workspace ] + dws drive rename --node --name "新名称" +Flags: + --node string 文档/文件 ID 或 URL (必填) + --folder string 目标文件夹 nodeId + --workspace string 目标知识库 ID + --name string 新名称 (仅 rename 必填) +``` + +> **字段选择**:`drive list` 返回中有 `dentryId`(数字格式)和 `fileId`(UUID 格式),**必须使用 `fileId`(UUID 格式)**作为 `--node` 和 `--folder` 参数值。 -钉盘本身没有 copy/move 命令,需使用 `dws doc copy`/`dws doc move` 实现。 +### 创建文件夹(文档空间) -> **注意:字段选择**:`drive list` 返回中有 `dentryId`(数字格式)和 `fileId`(UUID 格式)两个字段,**必须使用 `fileId`(UUID 格式,如 `ZgpG2NdyVXYOR2D5UGDok65MJMwvDqPk`)**作为 `--node` 和 `--folder` 的参数值。**禁止使用 `dentryId`(数字格式,如 `220335325118`),传入数字格式会导致命令失败。** +``` +Usage: + dws drive folder create --name "文件夹名" +Flags: + --name string 名称 (必填) + --folder string 父文件夹 nodeId + --workspace string 目标知识库 ID +``` -> **注意**:钉盘场域下,仅支持将文件复制/移动到文件夹下,不支持文档下嵌套文档。 +### 权限管理(文档节点级) + +> 仅适用于文档空间节点,不适用于钉盘文件。 + +``` +Usage: + dws drive permission add --node --users uid1,uid2 --role READER + dws drive permission update --node --users uid1 --role EDITOR + dws drive permission list --node + dws drive permission remove --node --users uid1 +Flags: + --node string 目标节点 ID 或 URL (必填) + --users string 用户 userId 列表,逗号分隔 + --role string 角色: MANAGER / EDITOR / DOWNLOADER / READER + --limit int 返回成员数上限 (仅 list,默认 30,最大 200) + --filter-role string 按角色过滤 (仅 list) +``` + +> **注意**:`drive export` 不存在。导出仅对自研文档 (adoc) 有意义,属于内容层操作,应使用 `doc export`。 ### 目标位置参数规则 | 目标位置 | 参数传递方式 | 前置步骤 | |---------|-----------|---------| | 未指定目标(默认) | `--folder ` | 先 `dws drive list-spaces --space-type mySpace` 获取「我的文件」的 `rootFolderId` | -| 知识库空间根目录 | `--workspace ` | 无需额外步骤,直接传入 workspaceId | +| 知识库空间根目录 | `--workspace ` | 无需额外步骤 | | 钉盘 space 根目录 | `--folder ` | 先 `dws drive list-spaces` 获取目标 space 的 `rootFolderId` | -| 钉盘 space 下的子文件夹 | `--folder ` | 先 `dws drive list --space-id ` 逐层浏览,获取目标文件夹的 `fileId`(dentryUuid 格式) | +| 钉盘 space 下的子文件夹 | `--folder ` | 先 `dws drive list --space-id ` 逐层浏览 | ### 工作流示例 ```bash -# ── 场景 默认: 用户未指定目标位置 → 复制/移动到「我的文件」根目录 ── -# 1. 获取源文件 dentryUuid +# ── 场景 默认: 复制/移动到「我的文件」根目录 ── dws drive list --space-id --format json -# 2. 获取「我的文件」个人空间的 rootFolderId dws drive list-spaces --space-type mySpace --format json -# 3. 用「我的文件」的 rootFolderId 作为 --folder -dws doc copy --node <源文件dentryUuid> --folder <我的文件rootFolderId> --format json +dws drive copy --node <源文件dentryUuid> --folder <我的文件rootFolderId> --format json -# ── 场景 A: 复制钉盘文件到知识库空间根目录 ── -# 1. 获取源文件 dentryUuid -dws drive list --space-id --format json -# 2. 直接传 workspaceId 即可 -dws doc copy --node <源文件dentryUuid> --workspace --format json +# ── 场景 A: 复制到知识库空间根目录 ── +dws drive copy --node <源文件dentryUuid> --workspace --format json -# ── 场景 B: 移动钉盘文件到另一个钉盘 space 根目录 ── -# 1. 获取源文件 dentryUuid -dws drive list --space-id --format json -# 2. 获取目标 space 的 rootFolderId +# ── 场景 B: 移动到另一个钉盘 space 根目录 ── dws drive list-spaces --format json -# 3. 用 rootFolderId 作为 --folder(不需要传 --workspace) -dws doc move --node <源文件dentryUuid> --folder <目标space的rootFolderId> --format json +dws drive move --node <源文件dentryUuid> --folder <目标space的rootFolderId> --format json -# ── 场景 C: 复制钉盘文件到钉盘 space 下的子文件夹 ── -# 1. 获取源文件 dentryUuid -dws drive list --space-id --format json -# 2. 浏览目标 space 找到目标文件夹的 fileId(dentryUuid 格式) +# ── 场景 C: 复制到钉盘子文件夹 ── dws drive list --space-id --format json -# 若目标在更深层级,继续用 --folder 逐层浏览 -dws drive list --space-id --folder <父文件夹dentryUuid> --format json -# 3. 用目标文件夹的 fileId 作为 --folder -dws doc copy --node <源文件dentryUuid> --folder <目标文件夹fileId> --format json +dws drive copy --node <源文件dentryUuid> --folder <目标文件夹fileId> --format json ``` ## 上下文传递表 @@ -259,10 +334,13 @@ dws doc copy --node <源文件dentryUuid> --folder <目标文件夹fileId> --for | 操作 | 从返回中提取 | 用于 | | ------------- | ---------------------------- | -------------------------------------------------------- | -| `list` | **`fileId`**(UUID 格式,注意:不是 `dentryId`) | info / download / mkdir / delete / list 的 --node 或 --folder;`doc copy/move` 的 --node 或 --folder | +| `list` | **`fileId`**(UUID 格式,注意:不是 `dentryId`) | info / download / mkdir / delete / list 的 --node 或 --folder;`drive copy/move` 的 --node 或 --folder | | `list` | `spaceId` | info / download / mkdir / commit 的 --space-id | -| `list-spaces` | `rootFolderId` | `doc copy/move` 的 --folder(复制/移动到钉盘 space 根目录时) | +| `list-spaces` | `rootFolderId` | `drive copy/move` 的 --folder(复制/移动到钉盘 space 根目录时) | | `list-spaces` | `spaceId` | list / info / download / mkdir / upload 的 --space-id | +| `search` | **`fileId`**(文件/文件夹结果) | info / download / delete 的 --node;list 的 --folder | +| `search` | `spaceId` / `rootFolderId`(空间结果) | list 的 --space-id;`drive copy/move` 的 --folder | +| `search` | `nextCursor` | search 的 --cursor(翻页) | | `mkdir` | `fileId`(UUID 格式) | list 的 --folder | > **重要**:`drive list` 返回结果中同时包含 `dentryId` 和 `fileId` 两个字段。所有需要传 `--node` 的命令(info / download / delete)必须使用 `fileId`(即 dentryUuid),**不要使用** `dentryId`。 @@ -273,6 +351,7 @@ dws doc copy --node <源文件dentryUuid> --folder <目标文件夹fileId> --for - 不传 `--space-id` 时默认使用「我的文件」空间 - 不传 `--folder` 时默认操作空间根目录 - `--folder` 只能使用父文件夹的 `dentryUuid`。不要把 `drive info` 返回的数字型 `dentryId` 当作父目录;`dentryId` 只用于 `chat message send --dentry-id` +- **`--limit` 最大值为 50**,禁止传入超过 50 的值(如 `--limit 100`)。用户要求超过 50 条时,应使用 `--limit 50` 配合 `--cursor` 分页查询,不要直接传大于 50 的值 - `--order-by` 支持: `createTime`、`modifyTime`、`name` - **上传文件必须使用 `dws drive upload` 命令**,禁止使用 `upload-info` + `curl` + `commit` 三步手动流程 - `--file-name` 必须包含扩展名(如 `report.pdf`) diff --git a/skills/mono/references/products/sheet.md b/skills/mono/references/products/sheet.md index edc0bdb3..82022692 100644 --- a/skills/mono/references/products/sheet.md +++ b/skills/mono/references/products/sheet.md @@ -1,1994 +1,92 @@ # 电子表格 (sheet) 命令参考 -## 适用范围(重要) +> **渐进式文档**:本文件为路由层(索引 + 意图判断 + 全局约束),各命令的详细参数、示例和注意事项在 [sheet/](./sheet/) 目录下按需加载。 -`sheet` 产品线仅支持钉钉在线电子表格(`contentType=ALIDOC`、`extension=axls`),不支持上传的 `xlsx` / `xls` / `xlsm` / `csv` 等本地表格文件。 +## 适用范围 + +`sheet` 仅支持钉钉在线电子表格(`contentType=ALIDOC`、`extension=axls`)。 | 文件类型 | 处理方式 | |---------|---------| -| 在线电子表格(`axls`) | 走 `sheet` 全部命令(读/写/筛选/合并/导出等服务端原子操作) | -| `xlsx` / `xls` / `xlsm` / `csv` 等本地表格文件 | 必须用 `dws doc download --node --output <路径>` 先下载到本地再解析处理,禁止调用任何 `sheet` 子命令(sheet 底层 MCP 工具仅认 `axls`,传入 xlsx 节点会直接报错) | -| 想把在线表格导出为 xlsx | 用 `dws sheet export` ——输入是 `axls`,输出是 xlsx(axls → xlsx 的格式转换) | - -> 用户直接粘贴原始 `alidocs` URL 时必须先 probe:先执行 `dws doc info --node --format json`,按 [链接规范](../url-patterns.md#alidocs-url-类型探测流程) 校验 `contentType` 和 `extension`: -> - 仅当 `contentType=ALIDOC` 且 `extension=axls` 时,才继续走 `sheet` -> - 如果是 `xlsx` / `xls` / `xlsm` / `csv`,立即转向 `dws doc download`,并告知用户"这是本地表格文件,已为你下载到本地处理" - -## URL 识别与 NODE_ID 提取 - -当用户输入包含钉钉文档 URL 时,必须先识别并提取 NODE_ID,再判断意图。 - -硬性规则:对用户直接给出的原始 `alidocs` URL,不允许直接走 `sheet` 命令,必须先执行: - -```bash -dws doc info --node "" --format json -``` +| 在线电子表格(`axls`) | 走 `sheet` 全部命令 | +| `xlsx` / `xls` / `xlsm` / `csv` | `dws drive download --node --output <路径>` 下载到本地处理,禁止调用任何 `sheet` 子命令 | +| 本地 xlsx 导入为在线表格 | `dws drive upload --file <路径> --convert`(上传并转换为在线电子表格,转换后可用 `sheet` 命令操作) | +| 在线表格导出为 xlsx | `dws sheet export`(axls → xlsx 格式转换) | -根据返回路由: +用户贴原始 `alidocs` URL 时必须先 probe:`dws doc info --node --format json`,按 [链接规范](../url-patterns.md#alidocs-url-类型探测流程) 校验: - `contentType=ALIDOC` + `extension=axls` → 继续走 `sheet` -- `contentType≠ALIDOC` + `extension=xlsx` / `xls` / `xlsm` / `csv` → 转向 `dws doc download --node --output <路径>`,禁止调用任何 sheet 子命令 -- 其他类型 → 按 [链接规范](../url-patterns.md#alidocs-url-类型探测流程) 路由 - -补充:如果这是用户直接提供的原始 `alidocs` URL,先按 [链接规范](../url-patterns.md#alidocs-url-类型探测流程) probe 一次确认真实类型,再判断是否继续走 `sheet`。 - -### 支持的 URL 格式 - -| 格式 | 示例 | NODE_ID 提取方式 | -|------|------|----------------| -| `alidocs.dingtalk.com/i/nodes/{id}` | `https://alidocs.dingtalk.com/i/nodes/9E05BDRVQePjzLkZt2p2vE7kV63zgkYA` | 取 URL 路径最后一段 | -| `alidocs.dingtalk.com/i/nodes/{id}?queryParams` | `https://alidocs.dingtalk.com/i/nodes/abc123?doc_type=wiki_doc` | 忽略 query 参数,取路径最后一段 | -| `alidocs.dingtalk.com/spreadsheetv2/{key}/...` | `https://alidocs.dingtalk.com/spreadsheetv2/vKJngl50tJN1v3a3/...?dentryKey=vKJngl50tJN1v3a3&type=s` | **不要提取 path segment**,将完整 URL 直接传给 `--node` 参数,由 MCP 服务端解析 | - -### 提取规则 - -1. 匹配 URL 中 `alidocs.dingtalk.com` 域名 -2. **判断 URL 路径格式**: - - 路径包含 `/i/nodes/` → 取 URL path 的最后一段作为 NODE_ID(去掉 query string 和 fragment) - - 路径包含 `/spreadsheetv2/` → **不要提取 path segment**,必须将完整 URL 原样传给 `--node` 参数(因为 path 中的短 ID 不是合法的 nodeId,MCP 服务端会自行解析完整 URL) -3. 对于 `/i/nodes/` 格式,提取出的 NODE_ID 可直接用于所有 `--node` 参数,也可将完整 URL 传给 `--node`(CLI 会自动解析) -4. 对用户直接提供的原始 `alidocs` URL,先按 [链接规范](../url-patterns.md#alidocs-url-类型探测流程) probe;只有 probe 确认 `contentType=ALIDOC` 且 `extension=axls` 时,才继续留在 `sheet`;如果 `extension=xlsx` / `xls` / `xlsm` / `csv`,必须转向 `dws doc download`,不能走任何 sheet 命令 - -## 查询命令帮助 - -当你不确定某个命令的具体参数、格式或可选项时,**优先执行 `--help` 查询**,不要猜测参数名或凭记忆编造。 - -```bash -# 查看 sheet 下所有子命令 -dws sheet --help - -# 查看具体命令的完整参数说明 -dws sheet range update --help -dws sheet filter-view create --help -dws sheet insert-dimension --help - -# 查看子命令组下的所有命令 -dws sheet range --help -dws sheet filter-view --help -``` - -规则: -- 参数名不确定时 → 先 `--help`,再调用 -- 报错 "unknown flag" 时 → `--help` 确认正确的 flag 名称 -- 不确定某个功能是否存在时 → `dws sheet --help` 查看命令列表 - -## 命令速查表 - -| 命令 | 用途 | -|------|------| -| `sheet create` | 创建钉钉表格文档 | -| `sheet list` | 获取全部工作表列表 | -| `sheet info` | 获取指定工作表详情 | -| `sheet new` | 新建工作表 | -| `sheet update` | 更新工作表属性(标题/位置/隐藏/冻结) | -| `sheet copy` | 复制工作表 | -| `sheet range read` | 读取工作表数据(别名: range get) | -| `sheet range update` | 更新指定区域内容(值/公式/超链接) | -| `sheet range set-style` | 设置单元格样式 | -| `sheet range batch-set-style` | 按配置文件批量设置样式 | -| `sheet find` | 搜索单元格内容 | -| `sheet append` | 在末尾追加数据行 | -| `sheet csv-put` | 将 CSV 数据写入指定位置(纯值,自动扩容) | -| `sheet replace` | 全局查找替换文本 | -| `sheet merge-cells` | 合并单元格 | -| `sheet unmerge-cells` | 取消合并单元格 | -| `sheet set-dropdown` | 设置下拉列表 | -| `sheet get-dropdown` | 获取下拉列表配置 | -| `sheet delete-dropdown` | 删除下拉列表 | -| `sheet insert-dimension` | 在指定位置插入行或列 | -| `sheet delete-dimension` | 删除指定位置的行或列 | -| `sheet update-dimension` | 更新行/列属性(显隐/行高/列宽) | -| `sheet move-dimension` | 移动行或列到指定位置 | -| `sheet add-dimension` | 在末尾追加空行或空列 | -| `sheet media-upload` | 上传附件到表格 | -| `sheet write-image` | 上传图片并写入单元格 | -| `sheet create-float-image` | 创建浮动图片 | -| `sheet get-float-image` | 获取浮动图片详情 | -| `sheet list-float-images` | 列出工作表所有浮动图片 | -| `sheet update-float-image` | 更新浮动图片属性 | -| `sheet delete-float-image` | 删除浮动图片 | -| `sheet export` | 导出表格为 xlsx | -| `sheet filter get` | 获取全局筛选信息 | -| `sheet filter create` | 创建全局筛选 | -| `sheet filter delete` | 删除全局筛选 | -| `sheet filter update` | 批量更新筛选条件 | -| `sheet filter clear-criteria` | 清除单列筛选条件 | -| `sheet filter sort` | 筛选排序 | -| `sheet filter-view list` | 获取所有筛选视图 | -| `sheet filter-view create` | 创建筛选视图 | -| `sheet filter-view update` | 更新筛选视图属性 | -| `sheet filter-view delete` | 删除筛选视图 | -| `sheet filter-view update-criteria` | 更新筛选视图列条件 | -| `sheet filter-view delete-criteria` | 删除筛选视图列条件 | -| `sheet filter-view info` | 获取单个筛选视图详情 | -| `sheet filter-view list-criteria` | 列出筛选视图所有列条件 | -| `sheet filter-view get-criteria` | 获取单列筛选条件详情 | - -> 不确定参数?对任意命令执行 `dws sheet <命令> --help` 查看完整用法。 - -## 意图判断 - -### 表格与工作表管理 - -用户说"创建表格/新建电子表格": -- 创建表格文档 → `create` - -用户说"看工作表/有哪些工作表/表格结构": -- 列出工作表 → `list` -- 工作表详情 → `info` - -用户说"加工作表/新增Sheet": -- 新建工作表 → `new` - -用户说"修改工作表名称/重命名工作表/移动工作表位置/隐藏工作表/显示工作表/冻结行/冻结列/取消冻结/更新工作表属性": -- 更新工作表属性 → `update` -- 重命名工作表 → `update --title "新名称"` -- 移动工作表位置 → `update --index N` -- 隐藏工作表 → `update --hidden` -- 显示工作表 → `update --hidden=false` -- 冻结行列 → `update --frozen-row-count N --frozen-column-count M` -- 取消冻结 → `update --frozen-row-count 0 --frozen-column-count 0` - -用户说"复制工作表/拷贝工作表/克隆工作表/工作表副本": -- 复制工作表 → `copy` -- 复制并指定名称 → `copy --title "副本名称"` -- 复制并指定位置 → `copy --index N` - -### 数据读写 - -用户说"读数据/看表格内容": -- 读取数据 → `range read` - -用户说"写数据/填表/更新单元格/写入公式": -- 更新数据 → `range update` -- 【强制】`--sheet-id` 必填:即使是单工作表也不能省略,不要参照 `range read` 的默认行为;未知时先执行 `dws sheet list --node --format json` 获取 `sheetId`,禁止凭空臆测为 `Sheet1`、`sheet1`、`0`、`default` 等 -- 注意:如果用户的目的是替换文本、移动行列或追加空行空列,请勿使用 `range update`,必须使用对应的专用命令(`replace`/`move-dimension`/`add-dimension`) -- **批量纯值写入优先用 `csv-put`**:当写入场景同时满足以下条件时,必须优先使用 `csv-put` 而非 `range update`:(1) 写入的是纯值(不含公式、超链接);(2) 数据量较大(超过 5 行或超过 20 个单元格);(3) 数据来源为表格/CSV 文本/结构化文本。`csv-put` 无需手动构造二维 JSON 数组,直接传 CSV 文本即可,更简洁高效且支持自动扩容 - -用户说"追加数据/添加行/在末尾加数据/新增记录": -- 追加数据 → `append` - -用户说"批量写入CSV/导入CSV/CSV写入表格/把CSV贴到表格里": -- 写入 CSV → `csv-put` -- 与 `range update` 的区别:`csv-put` 接受 CSV 文本直接写入,无需手动构造二维 JSON 数组;适合大批量纯值写入 -- 与 `append` 的区别:`csv-put` 写入指定位置(--start-cell),`append` 在末尾追加 - -用户说"搜索/查找/找单元格/搜内容/精确搜索/精确匹配/完全匹配/全字匹配": -- 搜索单元格 → `find` -- 精确匹配(只匹配完全等于的,不匹配包含的) → `find --match-entire-cell` -- 正则搜索 → `find --use-regexp` -- 搜索公式 → `find --match-formula` -- 不要用 `range read` 读取全量数据后在客户端过滤来替代 `find`,必须使用 `find` 命令的服务端搜索能力 - -用户说"替换/查找替换/全局替换/批量替换/把A替换成B/把所有的X改成Y": -- 查找替换 → `replace` -- 精确匹配后替换(只替换内容完全等于的单元格) → `replace --match-entire-cell` -- 正则替换 → `replace --use-regexp` -- 删除匹配内容 → `replace --replacement ""` -- 请勿用 `find` + `range update`、`range read` + `range update` 等组合来模拟替换,`replace` 是服务端原子操作,效率更高且返回替换计数 - -### 行列操作 - -用户说"插入行/插入列/在某行前插入/在某列前插入": -- 插入行或列 → `insert-dimension` -- 在末尾追加 → `append`(insert-dimension 不支持末尾追加) - -用户说"删除行/删除列/删掉第几行/删掉某列/移除行/移除列": -- 删除行或列 → `delete-dimension` -- 仅清空内容但保留行/列 → `range update --values` 写入空字符串 `""` - -用户说"隐藏行/隐藏列/显示行/显示列/设置行高/设置列宽/调整行高/调整列宽/行列属性": -- 隐藏/显示行或列 → `update-dimension --hidden` / `--hidden=false` -- 设置行高/列宽 → `update-dimension --pixel-size` -- 同时修改尺寸与显隐 → `update-dimension --pixel-size --hidden` - -用户说"移动行/移动列/调整行顺序/调整列顺序/行列拖拽/把第N行移到第M行": -- 移动行或列 → `move-dimension` -- 请勿用 `range read` + `range update` 读取再重写来模拟移动,`move-dimension` 是原子操作,能保留格式和合并状态 - -用户说"追加空行/追加空列/增加行数/增加列数/扩展表格/在末尾加空行": -- 追加空行/空列 → `add-dimension` -- 注意与 `append`(追加数据行)区分:`add-dimension` 追加的是空行/空列,`append` 追加的是带数据的行 -- 请勿用 `range update` 写空数据来模拟追加,`add-dimension` 直接扩展表格维度 - -### 单元格格式 - -用户说"设置样式/改颜色/设背景色/加粗/居中/换行/字体颜色/字号": -- 设置单元格样式 → `range set-style` -- 批量设置不同 range 的样式 → `range batch-set-style --batch ./styles.json`(内部顺序循环调 `update_range`) -- 请勿用 `range update --values` 写空/重写来模拟样式变更;也请勿把样式变更混在 `range update` 里、再故意清空 `--values` - -用户说"设置数字格式/改成百分比/用人民币显示/按日期显示/文本格式/保留几位小数": -- 设置数字格式 → `range set-style --number-format <格式代码>`(如 `0%` / `¥#,##0.00` / `yyyy/m/d` / `@`) -- 请勿用 `range update` 传递数字格式,`range update` 仅负责写入值与超链接;数字格式属于单元格样式,统一走 `range set-style` - -用户说"合并单元格/合并/合并区域/按行合并/按列合并": -- 合并所有单元格 → `merge-cells`(默认 mergeAll) -- 按行合并 → `merge-cells --merge-type mergeRows` -- 按列合并 → `merge-cells --merge-type mergeColumns` - -用户说"取消合并/拆分单元格/还原合并": -- 取消合并单元格 → `unmerge-cells` - -### 下拉列表 - -用户说"设置下拉列表/下拉选项/下拉菜单/添加下拉/配置下拉": -- 设置下拉列表 → `set-dropdown` -- 设置多选下拉 → `set-dropdown --multi-select` - -用户说"查看下拉列表/获取下拉配置/下拉列表有哪些选项": -- 获取下拉列表配置 → `get-dropdown` - -用户说"删除下拉列表/移除下拉/取消下拉/清除下拉": -- 删除下拉列表 → `delete-dropdown` - -### 媒体上传 - -用户说"上传附件/传文件到表格/上传文件到表格/上传到表格": -- 上传附件 → `media-upload`(需表格 ID 或 URL + 本地文件路径) -- 用户指定了上传后的名称 → `media-upload --name "自定义名称"` -- `media-upload` 的 `--name` 参数用于指定附件在表格中显示的名称(不改变本地文件名);不传时默认使用本地文件名 - -用户说"写入图片/插入图片/加图片/放图片到单元格/嵌入图片到表格": -- 写入图片 → `write-image`(需表格 ID + 工作表 ID + 单元格范围 + 本地图片路径) -- 禁止使用 `range update` 写入图片,因为 `update_range` 的 MCP 工具不支持图片类型参数,调用必定失败。必须使用 `write-image` 命令 -- 用户指定了图片尺寸 → `write-image --width N --height M` - -### 浮动图片 - -用户说"浮动图片/悬浮图片/在表格上放一张图/加个浮动的图": -- 创建浮动图片 → 先 `media-upload` 上传图片获取 `resourceUrl`,再 `create-float-image` -- 浮动图片悬浮于单元格之上,不占用单元格内容,与 `write-image`(写入单元格内部的图片)不同 - -用户说"查看浮动图片/有哪些浮动图片/浮动图片列表": -- 列出所有浮动图片 → `list-float-images` -- 查看某个浮动图片详情 → `get-float-image` - -用户说"移动浮动图片/调整浮动图片大小/修改浮动图片/更新浮动图片": -- 更新浮动图片属性 → `update-float-image`(可更新锚点位置、尺寸、偏移量、图片资源路径) - -用户说"删除浮动图片/移除浮动图片": -- 删除浮动图片 → `delete-float-image` - -关键区分:`write-image`(单元格内嵌图片,占据单元格内容)vs `create-float-image`(浮动图片,悬浮于单元格之上,不占内容) - -### 筛选视图 - -用户说"筛选/过滤/只看某些值/只显示满足条件的行/筛选数据/创建筛选/删除筛选/设置筛选条件/清除筛选/排序": -- 查看当前筛选 → `filter get` -- 创建筛选 → `filter create` -- 删除筛选 → `filter delete` -- 批量设置多列条件 → `filter update` -- 清除某一列条件 → `filter clear-criteria` -- 按列排序 → `filter sort` -- **区分全局筛选与筛选视图**:如果用户说"筛选视图"则走 `filter-view` 系列;如果只说"筛选/过滤/只看"则默认走全局 `filter` 系列 -- **禁止替代方案**:当用户要求"筛选/只看/仅保留某些行"时,必须通过 `filter create` / `filter update` 创建真实的筛选器。禁止用"删除不符合条件的行"或"新建工作表只放符合条件的行"来代替——这些做法会让原数据丢失或不可恢复 - -用户说"筛选视图/查看筛选视图/有哪些筛选视图/筛选视图列表": -- 获取所有筛选视图 → `filter-view list` - -用户说"筛选视图详情/查看某个筛选视图/筛选视图信息/筛选视图配置": -- 获取单个筛选视图详情 → `filter-view info` - -用户说"创建筛选视图/新建筛选视图/添加筛选视图": -- 创建筛选视图 → `filter-view create` - -用户说"更新筛选视图/修改筛选视图/改筛选视图名称/改筛选视图范围": -- 更新筛选视图属性 → `filter-view update` - -用户说"删除筛选视图/移除筛选视图": -- 删除筛选视图 → `filter-view delete` - -用户说"设置筛选条件/添加筛选条件/配置筛选视图条件/按值筛选/按条件筛选/按颜色筛选": -- 设置筛选视图列条件 → `filter-view update-criteria` - -用户说"查看筛选条件/有哪些筛选条件/筛选视图设了什么条件/列出筛选条件": -- 列出所有列条件 → `filter-view list-criteria` -- 查看某一列的条件 → `filter-view get-criteria --column N` - -用户说"清除筛选条件/移除筛选条件/取消筛选条件": -- 清除筛选视图列条件 → `filter-view delete-criteria` -- 注意与 `filter-view delete`(删除整个筛选视图)区分:`delete-criteria` 仅清除指定列的条件,不删除筛选视图本身 - -### 导出 - -用户说"导出/下载xlsx/存为Excel/存成表格文件/把表格变成xlsx/导出表格/下载表格/导出为 excel": -- 导出表格 → `export`(单命令一站式,内部自动完成提交、轮询、可选下载) -- 仅需传 `--node`,可选 `--output` 指定本地文件/目录(不传则返回 downloadUrl) -- 需要落盘到本地 → `dws sheet export --node --output `,命令自动下载 xlsx -- 禁止用 `range read` 全量读取后自行拼接 xlsx 来模拟导出,必须使用 `export` 命令(服务端原子导出,保留格式/合并/公式等属性) -- 禁止在 AI Agent 侧实现轮询或重试,CLI 内部已按渐进式退避策略完成(最多 30 次约 5 分钟) - -### URL 粘贴场景 +- `extension=xlsx` / `xls` / `xlsm` / `csv` → 转 `dws drive download`,告知用户"这是本地表格文件,已为你下载到本地处理" + +## URL → NODE_ID + +| URL 格式 | 提取方式 | +|----------|---------| +| `.../i/nodes/{id}` 或 `.../i/nodes/{id}?query` | 取路径末段作 NODE_ID(忽略 query) | +| `.../spreadsheetv2/{key}/...` | **完整 URL 原样传 `--node`**,禁止提取 path segment | + +参数不确定时先 `dws sheet <命令> --help`。 + +## Reference 索引 + +| Reference | 描述 | +|-----------|------| +| [sheet-workbook](./sheet/sheet-workbook.md) | 管理表格文档与工作表。当用户说"创建表格"、"有哪些工作表"、"新建/重命名/隐藏/冻结/复制/删除工作表"时使用。命令:`create`/`list`/`info`/`new`/`update`/`copy`/`delete-sheet` | +| [sheet-read-data](./sheet/sheet-read-data.md) | 读取工作表数据。当用户说"读数据"、"看表格内容"、"查看数据"时使用。推荐 `csv-get`(CSV 格式、token 低、防爆保护);需 value + dataValidation / hyperlink / richText / cellStyles 等 per-cell 元数据时用 `range read`。大范围数据建议分页读取(单次 ≤5000 单元格)。命令:`csv-get`/`range read` | +| [sheet-write-data](./sheet/sheet-write-data.md) | 写入数据到工作表。当用户说"写数据"、"填表"、"更新单元格"、"写公式"、"超链接"、"写值同时设样式/数据验证"、"追加数据"、"导入CSV"时使用。大批量纯值(>5行或>20单元格)必须用 `csv-put` 而非 `range update`。命令:`range update`/`append`/`csv-put` | +| [sheet-search-replace](./sheet/sheet-search-replace.md) | 搜索和替换文本。当用户说"搜索"、"查找"、"替换"、"把A改成B"时使用。禁止用 `range read` 全量读取后客户端过滤代替 `find`,禁止用 `range update` 模拟 `replace`。命令:`find`/`replace` | +| [sheet-range-operations](./sheet/sheet-range-operations.md) | 区域结构性操作。当用户说"清空"、"排序"、"自动填充"、"复制区域到"、"移动数据到"时使用。均为服务端原子操作,禁止 `range read`+`range update` 组合模拟。排序前必须先读前几行判断表头。命令:`range clear`/`range sort`/`range fill`/`range copy-to`/`range move-to` | +| [sheet-dimension-operations](./sheet/sheet-dimension-operations.md) | 行列增删移动与属性设置。当用户说"插入行/列"、"删除行/列"、"隐藏/显示行列"、"设行高/列宽"、"移动行/列"、"追加空行/空列"时使用。命令:`insert-dimension`/`delete-dimension`/`update-dimension`/`move-dimension`/`add-dimension` | +| [sheet-style-format](./sheet/sheet-style-format.md) | 单元格样式与合并。当用户说"设样式"、"改颜色/字体/对齐"、"数字格式(百分比/货币/日期)"、"合并/取消合并"时使用。纯样式/批量样式走 `set-style`;写值同时设置少量 cell 样式可用 `range update` 的 `cellStyles`。命令:`range set-style`/`range batch-set-style`/`merge-cells`/`unmerge-cells` | +| [sheet-dropdown](./sheet/sheet-dropdown.md) | 下拉列表管理。当用户说"设置下拉"、"下拉选项"、"删除下拉"时使用。命令:`set-dropdown`/`get-dropdown`/`delete-dropdown` | +| [sheet-media-image](./sheet/sheet-media-image.md) | 附件上传与图片。当用户说"上传附件"、"写入图片到单元格"、"浮动图片"时使用。单元格图片用 `write-image`(禁止 `range update`);浮动图片需先 `media-upload` 再 `create-float-image`。命令:`media-upload`/`write-image`/`create-float-image`/`get-float-image`/`list-float-images`/`update-float-image`/`delete-float-image` | +| [sheet-filter](./sheet/sheet-filter.md) | 全局筛选。当用户说"筛选"、"过滤"、"只看某些行"(未说"筛选视图")时使用。禁止用"删除不符合条件的行"代替筛选。命令:`filter get`/`create`/`delete`/`update`/`clear-criteria`/`sort` | +| [sheet-filter-view](./sheet/sheet-filter-view.md) | 筛选视图(个人化,不影响协作者)。当用户明确说"筛选视图"时使用,与全局筛选相互独立。命令:`filter-view list`/`create`/`update`/`delete`/`info`/`update-criteria`/`delete-criteria`/`list-criteria`/`get-criteria` | +| [sheet-conditional-format](./sheet/sheet-conditional-format.md) | 条件格式规则。触发词:标红/标黄/高亮/突出/标记/数据条/色阶/颜色随数据变 → **强制**走条件格式,禁止 `range set-style` 静态样式替代。命令:`cond-format list`/`create`/`update`/`delete` | +| [sheet-export](./sheet/sheet-export.md) | 导出表格为 xlsx。当用户说"导出"、"下载xlsx"、"存为Excel"时使用。单命令一站式,CLI 内部自动轮询,禁止 Agent 侧重试。命令:`export` | + +## 全局硬约束 + +1. **`--sheet-id` 禁止臆测**:未知时必须 `dws sheet list --node --format json` 查询,禁止编造 `Sheet1`/`sheet1`/`0`/`default` +2. **合并单元格是结构信息**:`dws sheet info --node --sheet-id --format json` 返回 `mergedRanges`(如 `["C7:D11"]`);不要在 `range read` / `csv-get` 里寻找合并信息 +3. **`range update` 维度校验**:`--values` 行列数必须与 `--range` 完全一致;只接 `--values` 一个数据参数,cell `type` 仅支持 `text` / `richText`;整格超链接通过 cell-level `hyperlink` 表达,富文本片段链接才使用 `richText.texts[].type="link"` +4. **dataValidation 三语义**:不传 `dataValidation` 字段=保留原 DV;`dataValidation:{type:"none"}`=显式清除;`dataValidation:{type:"dropdown"/"checkbox",...}`=覆盖。`{}` 跳过亦保留原 DV +5. **hyperlink 三语义**:不传 `hyperlink` 字段=保留原整格超链接;`hyperlink:{type:"none"}`=显式清除;`hyperlink:{type:"path"/"sheet"/"range",link,...}`=覆盖。Agent 调用不要用 `hyperlink:null`,避免网关/Schema 过滤 null 字段 +6. **样式写法**:cell-level 样式用 `cellStyles` 或 `range set-style`;richText 片段级样式才用子项 `style`。不要在 `type:"text"` 顶层使用旧 `style` 字段 +7. **用专用命令不用组合模拟**:搜索→`find`、替换→`replace`、清空→`range clear`、排序→`range sort`、填充→`range fill`、复制区域→`range copy-to`、移动区域→`range move-to`、移动行列→`move-dimension` +8. **大批量纯值用 `csv-put`**(>5 行或 >20 单元格),不用 `range update` +9. **单元格图片用 `write-image`**(`range update` 不支持图片参数) +10. **`export` 禁止自行轮询**(CLI 内部已完成渐进式退避,最多 30 次约 5 分钟) +11. **单次调用上限**:`range update` / `set-style` 行数 ≤ 1000,单元格总数建议 ≤ 5000(硬限 30000) +12. **关键区分**:sheet(电子表格/单元格读写)vs aitable(AI多维表/结构化记录)vs doc(文档) + +## URL 粘贴场景 用户直接粘贴表格 URL(无其他指令): -- 先 probe:`dws doc info --node --format json` 校验 `contentType` 和 `extension` -- `extension=axls` → `list`(列出工作表)+ `range read`(读取第一个工作表数据) -- `extension=xlsx` / `xls` / `xlsm` / `csv` → 转 `dws doc download --node --output ./`,告知用户"这是本地表格文件,已为你下载到本地",然后基于本地文件继续后续处理 +- 先 probe:`dws doc info --node --format json` +- `extension=axls` → `list` + `range read`(读取第一个工作表数据) +- `extension=xlsx`/`xls`/`xlsm`/`csv` → 转 `dws drive download --node --output ./` 用户粘贴 URL + 附加指令: -- 已 probe 为 `axls` 时: - - "帮我看看这个表格有什么数据" → `range read` - - "这个表格有哪些工作表" → `list` - - "往这个表格写入数据" → `range update` - - "帮我找一下表格里的XXX" → `find` -- probe 为 xlsx/xls/xlsm/csv 时:无论用户说"读数据/查看/分析",先走 `dws doc download` 下载到本地,由用户或后续步骤对本地 xlsx 进行解析,严禁调用 `sheet list` / `range read` 等命令 - -关键区分: sheet(电子表格/单元格读写) vs aitable(AI多维表/结构化记录) vs doc(文档编辑/阅读) - -## 关键注意事项 - -以下是最容易出错的规则,**必须严格遵守**: - -- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) -- ★ **`range update` 维度校验(强制)**:`--values` / `--hyperlinks` 的行列数必须与 `--range` 完全一致。例如 `--range "A1:C3"` → `--values` 必须是 3×3 数组 -- ★ **`range update` 清空规范(强制)**:清空单元格用空字符串 `""`,禁止用 `null`(全 null 会被跳过无效果) -- ★ **单次调用上限(强制)**:`range update` / `set-style` 行数 ≤ 1000,单元格总数建议 ≤ 5000(硬限 30000) -- ★ **大批量纯值写入用 `csv-put` 不用 `range update`**:当写入纯值(无公式/超链接)且数据量较大时(>5 行或 >20 单元格),必须使用 `csv-put`。`csv-put` 接受 CSV 文本直接写入,无需构造二维 JSON 数组,支持自动扩容,更简洁高效。仅在需要写入公式、超链接、或仅更新少量单元格时才使用 `range update` -- ★ **搜索用 `find` 不用 `range read`**:`find` 是服务端搜索,禁止用 `range read` 全量读取后客户端过滤 -- ★ **替换用 `replace` 不用 `range update`**:`replace` 是服务端原子操作,返回替换计数 -- ★ **移动用 `move-dimension` 不用 `range update`**:原子操作,保留格式和合并状态 -- ★ **单元格图片用 `write-image` 不用 `range update`**:`update_range` MCP 不支持图片参数,调用必失败 -- ★ **浮动图片用 `create-float-image` 不用 `write-image`**:两者用途不同——`write-image` 写入单元格内部,`create-float-image` 创建悬浮于单元格之上的浮动图片;`--src` 必须来自 `media-upload` 的 `resourceUrl` -- ★ **`export` 禁止自行轮询/重试**:CLI 内部已完成渐进式退避轮询(最多 30 次约 5 分钟),失败时直接告知用户稍后再试 -- ★ **关键区分**:sheet(在线电子表格/单元格读写) vs aitable(AI多维表/结构化记录/字段定义) vs doc(文档编辑/阅读) - -> 完整注意事项请参见本文档末尾「注意事项(完整版)」章节。 - -## 命令详细参考 - -> 以下为各命令的完整 Usage、Flags 和示例。参数不确定时也可直接执行 `dws sheet <命令> --help` 在线查看。 - -### 创建钉钉表格文档 -``` -Usage: - dws sheet create [flags] -Example: - dws sheet create --name "销售数据" - dws sheet create --name "Q1 数据" --folder - dws sheet create --name "知识库表格" --workspace -Flags: - --name string 表格名称 (必填) - --folder string 目标文件夹 ID (dentryUuid 格式) 或 URL;禁止传入纯数字 dentryId - --workspace string 目标知识库 ID -``` - -> **ID 格式约束**:`--folder` 只接受 UUID 格式的 `fileId`(如 `ZgpG2NdyVXYOR2D5UGDok65MJMwvDqPk`)或 alidocs 文件夹 URL。`drive list` 返回中有 `dentryId`(纯数字,如 `218595998810`)和 `fileId`(UUID 格式)两个字段,**必须使用 `fileId`,禁止使用 `dentryId`**,传入纯数字会导致命令失败。 - -### 获取全部工作表列表 -``` -Usage: - dws sheet list [flags] -Example: - dws sheet list --node - dws sheet list --node "https://alidocs.dingtalk.com/i/nodes/" -Flags: - --node string 表格文档 ID 或 URL (必填) -``` - -### 获取指定工作表详情 -``` -Usage: - dws sheet info [flags] -Example: - dws sheet info --node - dws sheet info --node --sheet-id - dws sheet info --node --sheet-id "Sheet1" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (不传则返回第一个工作表) -``` - -### 新建工作表 -``` -Usage: - dws sheet new [flags] -Example: - dws sheet new --node --name "Sheet2" - dws sheet new --node --name "数据汇总" -Flags: - --node string 表格文档 ID (必填) - --name string 工作表名称 (必填) -``` - -### 更新工作表属性 -``` -Usage: - dws sheet update [flags] -Example: - # 改名 + 调整冻结 - dws sheet update --node --sheet-id --title "汇总表" --frozen-row-count 2 --frozen-column-count 1 - - # 隐藏工作表 - dws sheet update --node --sheet-id --hidden=true - - # 显示工作表 - dws sheet update --node --sheet-id --hidden=false - - # 移动工作表到第一个位置 - dws sheet update --node --sheet-id --index 0 - - # 取消冻结 - dws sheet update --node --sheet-id --frozen-row-count 0 --frozen-column-count 0 -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --title string 新标题,最长 100 字符,不能包含 / \ ? * [ ] : - --index int 新位置(从 0 开始) - --hidden --hidden=true 隐藏,--hidden=false 取消隐藏 - --frozen-row-count int 冻结行数,0 表示取消冻结 - --frozen-column-count int 冻结列数,0 表示取消冻结 -``` - -更新工作表标题、位置、隐藏状态、冻结行列。 -`--title` / `--index` / `--hidden` / `--frozen-row-count` / `--frozen-column-count` 至少提供一个;多个属性可同时传入,将在同一次请求中更新。 - -注意: -- 至少需要保留一个可见的工作表,不能将所有工作表都隐藏 -- 冻结行数/列数不能超过工作表的总行数/列数 - -### 复制工作表 -``` -Usage: - dws sheet copy [flags] -Example: - # 按默认位置复制 - dws sheet copy --node --sheet-id - - # 指定副本名称和位置 - dws sheet copy --node --sheet-id --title "销售副本" --index 2 - - # 只指定名称 - dws sheet copy --node --sheet-id --title "备份" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 源工作表 ID 或名称 (必填) - --title string 副本名称,最长 100 字符,不能包含 / \ ? * [ ] : (不传则系统自动生成) - --index int 副本位置(从 0 开始)(不传则放在源工作表之后) -``` - -复制指定工作表,在同一表格中创建一个副本。 -复制操作会将源工作表的所有内容(包括数据、格式、公式等)完整复制到新工作表中。 -传 `--index` 时,CLI 会先复制,再追加一次位置更新,把副本移动到目标索引。 -名称与已有工作表重复时系统会自动重命名。 - -### 读取工作表数据 -``` -Usage: - dws sheet range read [flags] # 别名: dws sheet range get -Example: - dws sheet range read --node - dws sheet range read --node --sheet-id - dws sheet range read --node --sheet-id "Sheet1" --range "A1:D10" - dws sheet range read --node --range "Sheet1!A1:D10" - - # 使用 get 别名,与 read 等价 - dws sheet range get --node --sheet-id --range "A1:D10" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (不传则默认第一个工作表) - --range string 读取范围,A1 表示法 (如 A1:D10,不传则读取全部数据) -``` - -**超时处理建议**:读取大范围数据时若出现超时或响应过慢,请主动缩小 `--range` 查询范围,**建议单次读取的单元格数量控制在 5000 个以内**(例如 50 行 × 100 列、100 行 × 50 列)。对于大表可采用分页读取策略: -- 先通过 `info` 获取 `rowCount` / `lastNonEmptyRow` / `columnCount` 确定数据边界 -- 按行分批读取,如 `A1:J500`、`A501:J1000`、`A1001:J1500` …… -- 避免不传 `--range` 直接读取整个大工作表 - -### 更新工作表指定区域内容 -``` -Usage: - dws sheet range update [flags] -Example: - # 写入值 - dws sheet range update --node --sheet-id --range "A1:B2" \ - --values '[["姓名","分数"],["张三",90]]' - - # 写入公式 - dws sheet range update --node --sheet-id --range "C2" \ - --values '[["=A2&B2"]]' - - # 写入超链接 - dws sheet range update --node --sheet-id --range "A1" \ - --hyperlinks '[[{"type":"path","link":"https://dingtalk.com","text":"钉钉"}]]' - - # 清空区域(使用空字符串 "") - dws sheet range update --node --sheet-id --range "A1:B3" \ - --values '[["",""],["",""],["",""]]' -Flags: - --node string 表格文档 ID (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 目标单元格区域地址,如 A1:B3 (必填) - --values string 单元格值,二维 JSON 数组 (与 --hyperlinks 至少传一项) - --hyperlinks string 超链接,二维 JSON 数组 (与 --values 至少传一项) -``` - -**单次调用建议**:行数 ≤ 1000,单元格总数(行×列)≤ 5000;超过时请拆分多次调用。 - -**何时该用 `csv-put` 替代**:如果你准备用 `range update` 写入纯值(不含公式和超链接),且数据量超过 5 行或 20 个单元格,应改用 `csv-put`——它接受 CSV 文本直接写入,无需手动拼装二维 JSON 数组,且支持自动扩容行列。仅在需要写入公式(`=SUM(...)`)、超链接(`--hyperlinks`)、或修改少量单元格时才使用 `range update`。 - -**范围职责**:`range update` 仅负责写入单元格的值与超链接,不接受任何样式参数。如需设置数字格式(百分比 / 货币 / 日期 / 文本等)请使用 `dws sheet range set-style --number-format <格式代码>`,可与其他样式参数同时传入。 - -### 设置单元格样式 -``` -Usage: - dws sheet range set-style [flags] -Example: - # 给 A1:B3 打上黄底粗体居中 - dws sheet range set-style --node --sheet-id --range "A1:B3" \ - --bg-color "#FFF2CC" --font-weight bold --h-align center - - # 给 C1:C5 逐单元格设置不同背景色 - dws sheet range set-style --node --sheet-id --range "C1:C5" \ - --bg-colors-json '[["#FF0000"],["#00FF00"],["#0000FF"],["#FFFF00"],["#FF00FF"]]' - - # 整片 range 启用自动换行 - dws sheet range set-style --node --sheet-id --range "A1:E10" --word-wrap autoWrap -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 目标区域,如 A1:B3 (必填) - --bg-color string 背景色(#RRGGBB),一键刷整个 range;与 --bg-colors-json 二选一 - --bg-colors-json string 背景色二维 JSON 数组,维度需与 --range 一致 - --font-size int 字号,一键刷整个 range;与 --font-sizes-json 二选一 - --font-sizes-json string 字号二维 JSON 数组 - --h-align string 水平对齐:left/center/right/general - --h-aligns-json string 水平对齐二维 JSON 数组 - --v-align string 垂直对齐:top/middle/bottom - --v-aligns-json string 垂直对齐二维 JSON 数组 - --font-color string 字体颜色(#RRGGBB) - --font-colors-json string 字体颜色二维 JSON 数组 - --font-weight string 字体粗细:bold/normal - --font-weights-json string 字体粗细二维 JSON 数组 - --word-wrap string 换行方式:overflow/clip/autoWrap(整个 range 共用) - --number-format string 数字格式,如 General/@/#,##0/0%/yyyy/m/d -``` - -**特性说明**: -- 每个样式维度提供两种写法,二选一:`--xxx`(单值刷整个 range,CLI 本地展开为二维数组)vs `--xxx-json`(逐单元格指定,维度需与 `--range` 完全一致) -- 至少需传入一个样式参数。单次调用建议:行数 ≤ 1000,单元格总数 ≤ 5000 -- 枚举值按驼峰书写:`autoWrap`、`bold`、`normal`、`center` 等 - -### 批量设置单元格样式 -``` -Usage: - dws sheet range batch-set-style [flags] -Example: - dws sheet range batch-set-style --node --batch ./styles.json - dws sheet range batch-set-style --node --batch ./styles.json --continue-on-error -Flags: - --node string 表格文档 ID 或 URL (必填) - --batch string 批次配置 JSON 文件路径 (必填) - --continue-on-error 遇到失败时继续执行后续条目(默认遇错即停) -``` - -配置文件格式(JSON 数组,每个元素一条批次项): -```json -[ - { - "sheetId": "Sheet1", - "range": "A1:B3", - "bgColor": "#FFF2CC", - "fontSize": 12, - "hAlign": "center", - "vAlign": "middle", - "fontColor": "#333333", - "fontWeight": "bold", - "wordWrap": "autoWrap", - "numberFormat": "General" - }, - { - "sheetId": "Sheet1", - "range": "C1:C5", - "bgColorsJson": "[[\"#FF0000\"],[\"#00FF00\"],[\"#0000FF\"],[\"#FFFF00\"],[\"#FF00FF\"]]" - } -] -``` - -**特性说明**: -- CLI 侧顺序循环逐条调用 `update_range`(非服务端批量),运行时输出 `[N/M]` 进度 -- 每条记录执行与 `set-style` 一致的校验:至少一项样式字段 + rows ≤ 1000 + rows×cols ≤ 30000 + 枚举合法 -- 默认遇错即停(返回非 0),`--continue-on-error` 时所有条目跑完再返回首个错误 - -### 在工作表中搜索单元格内容 -``` -Usage: - dws sheet find [flags] -Example: - # 基本搜索 - dws sheet find --node --sheet-id --find "销售额" - - # 在指定范围内搜索 - dws sheet find --node --sheet-id --find "合计" --range "A1:D100" - - # 正则表达式搜索(不区分大小写) - dws sheet find --node --sheet-id --find "^total" --use-regexp --match-case=false - - # 精确匹配整个单元格内容 - dws sheet find --node --sheet-id --find "完成" --match-entire-cell - - # 搜索公式文本 - dws sheet find --node --sheet-id --find "SUM" --match-formula -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --find string 搜索文本 (必填) - --range string 搜索范围,A1 表示法 (如 A1:D10) - --match-case 区分大小写 (默认 true) - --match-entire-cell 精确匹配整个单元格内容 - --use-regexp 启用正则表达式搜索 - --match-formula 搜索公式文本而非显示值 - --include-hidden 包含隐藏单元格 -``` - -### 在工作表末尾追加数据 -``` -Usage: - dws sheet append [flags] -Example: - dws sheet append --node --sheet-id --values '[["张三","销售部",50000]]' - dws sheet append --node --sheet-id "Sheet1" \ - --values '[["李四","市场部",38000],["王五","销售部",62000]]' -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --values string 追加数据,二维 JSON 数组 (必填) -``` - -`--values` 为二维 JSON 数组,外层每个元素代表一行,内层每个元素代表一个单元格值。 -追加的数据列数应与工作表已有数据的列数保持一致。 - -### 将 CSV 数据写入指定位置 -``` -Usage: - dws sheet csv-put [flags] -Example: - dws sheet csv-put --node --sheet-id --start-cell A1 \ - --csv 'name,score\nAlice,95\nBob,87' - - dws sheet csv-put --node --sheet-id --start-cell B2 \ - --csv @data.csv --allow-overwrite - - cat data.csv | dws sheet csv-put --node --sheet-id \ - --start-cell A1 --csv - - - dws sheet csv-put --node --sheet-id --start-cell A1 \ - --csv @data.csv --dry-run -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --csv string CSV 文本、@文件路径 或 - 表示 stdin (必填) - --start-cell string 起始单元格,A1 表示法 (必填) - --allow-overwrite 允许覆盖已有数据 (默认 false) -``` - -将 RFC 4180 格式的 CSV 文本写入指定工作表的指定单元格位置。 -- 只写纯值,不支持公式/样式/批注。`=` 开头的内容当文本处理,不会被解析为公式 -- 数字/日期/百分数由表格引擎自动识别类型(如 `95` 存为数字,`2025-03-01` 存为日期) -- 自动扩容行列:CSV 数据超出当前工作表维度时自动追加行/列 -- 目标区域如含合并单元格,合并将被打散,值正常写入 -- `--allow-overwrite` 默认 false,目标区域有数据时需显式传 `--allow-overwrite` 才能覆盖 -- `--csv` 支持三种输入:直接传文本、`@filepath` 从本地文件读取、`-` 从 stdin 管道读取 -- CSV 文本上限 2M 字符,单元格总数上限 30000 -- 特殊字符处理:CLI 会自动过滤 `\r`(Windows 换行符)和 BOM(UTF-8 文件头标记),Excel/Windows 导出的 CSV 可直接使用;如 CSV 数据中含零宽字符(U+200B 等)或 Bidi 控制符,CLI 会拒绝并报错 - -### 在指定位置插入行或列 -``` -Usage: - dws sheet insert-dimension [flags] -Example: - # 在第 3 行之前插入 2 行 - dws sheet insert-dimension --node --sheet-id --dimension ROWS --position "3" --length 2 - - # 在 A 列之前插入 1 列 - dws sheet insert-dimension --node --sheet-id --dimension COLUMNS --position "A" --length 1 - - # 使用工作表前缀(忽略 --sheet-id) - dws sheet insert-dimension --node --sheet-id --dimension ROWS --position "Sheet1!3" --length 5 - - # 在 AB 列之前插入 3 列 - dws sheet insert-dimension --node --sheet-id --dimension COLUMNS --position "AB" --length 3 -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --dimension string 插入维度: ROWS 或 COLUMNS (必填) - --position string 插入位置,A1 表示法 (必填)。ROWS 时为行号如 "3";COLUMNS 时为列字母如 "A" - --length string 插入数量,正整数 (必填),最大 5000 -``` - -在钉钉表格指定工作表的指定位置之前插入若干空行或空列。 -`--dimension ROWS` 时,`--position` 为 1-based 行号字符串;`--dimension COLUMNS` 时,`--position` 为列字母。 -支持在 `--position` 中携带工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id`。 -若需要在末尾追加行/列,请使用 `append` 命令。 - -### 删除指定位置的行或列 - -> **CAUTION:** 不可逆操作 — 执行前必须向用户确认。 - -``` -Usage: - dws sheet delete-dimension [flags] -Example: - # 从第 3 行开始删除 2 行 - dws sheet delete-dimension --node --sheet-id --dimension ROWS --position "3" --length 2 - - # 从 A 列开始删除 1 列 - dws sheet delete-dimension --node --sheet-id --dimension COLUMNS --position "A" --length 1 - - # 使用工作表前缀(忽略 --sheet-id) - dws sheet delete-dimension --node --sheet-id --dimension ROWS --position "Sheet1!3" --length 5 - - # 从 AB 列开始删除 3 列 - dws sheet delete-dimension --node --sheet-id --dimension COLUMNS --position "AB" --length 3 -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --dimension string 删除维度: ROWS 或 COLUMNS (必填) - --position string 删除起始位置,A1 表示法 (必填)。ROWS 时为行号如 "3";COLUMNS 时为列字母如 "A" - --length string 删除数量,正整数 (必填),最大 5000 -``` - -在钉钉表格指定工作表中,从指定位置起删除若干连续的行或列。 -`--dimension ROWS` 时,`--position` 为 1-based 行号字符串;`--dimension COLUMNS` 时,`--position` 为列字母。 -支持在 `--position` 中携带工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id`。 -删除后后续的行/列会向前移动填补空位;若需要仅清空内容但保留行/列占位,请使用 `range update` 将目标区域写入空字符串 `""`。 - -### 更新指定范围行/列属性 -``` -Usage: - dws sheet update-dimension [flags] -Example: - # 隐藏第 3~4 行 - dws sheet update-dimension --node --sheet-id --dimension ROWS --start-index "3" --length 2 --hidden - - # 显示 A~B 列 - dws sheet update-dimension --node --sheet-id --dimension COLUMNS --start-index "A" --length 2 --hidden=false - - # 设置第 1~5 行行高为 40px - dws sheet update-dimension --node --sheet-id --dimension ROWS --start-index "1" --length 5 --pixel-size 40 - - # 设置 C 列列宽为 200px 并隐藏 - dws sheet update-dimension --node --sheet-id --dimension COLUMNS --start-index "C" --length 1 --pixel-size 200 --hidden - - # 使用工作表前缀(忽略 --sheet-id) - dws sheet update-dimension --node --sheet-id --dimension ROWS --start-index "Sheet1!3" --length 2 --hidden -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --dimension string 更新维度: ROWS 或 COLUMNS (必填) - --start-index string 起始位置,A1 表示法 (必填)。ROWS 时为行号如 "3";COLUMNS 时为列字母如 "A" - --length string 更新数量,正整数 (必填),最大 5000 - --hidden 是否隐藏 (true=隐藏, false=显示),与 --pixel-size 至少填其一 - --pixel-size int 行高或列宽(像素),ROWS 时为行高,COLUMNS 时为列宽,与 --hidden 至少填其一 -``` - -批量更新钉钉表格指定工作表中连续多行/多列的属性,支持设置显隐状态(hidden)与行高/列宽(pixelSize)。 -`--dimension ROWS` 时,`--start-index` 为 1-based 行号字符串;`--dimension COLUMNS` 时,`--start-index` 为列字母。 -支持在 `--start-index` 中携带工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id`。 -`--hidden` 与 `--pixel-size` 至少必须提供一个。当同时提供时,将先应用尺寸再应用显隐,任一失败整体失败。 -`--pixel-size` 单位为像素,`dimension=ROWS` 时表示行高、`dimension=COLUMNS` 时表示列宽。 - -### 合并单元格 -``` -Usage: - dws sheet merge-cells [flags] -Example: - # 合并所有单元格(默认) - dws sheet merge-cells --node --sheet-id --range "A1:B3" - - # 按行合并 - dws sheet merge-cells --node --sheet-id --range "A1:C3" --merge-type mergeRows - - # 按列合并 - dws sheet merge-cells --node --sheet-id --range "A1:C3" --merge-type mergeColumns - - # 使用带工作表前缀的范围(忽略 --sheet-id) - dws sheet merge-cells --node --sheet-id --range "Sheet1!A1:B3" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 目标单元格区域地址,如 A1:B3 (必填) - --merge-type string 合并方式: mergeAll(默认)/mergeRows/mergeColumns -``` - -支持三种合并方式: -- `mergeAll`(默认):合并所有单元格,将选定区域内的所有单元格合并成一个 -- `mergeRows`:按行合并,在选定区域内将同一行相邻的单元格合并 -- `mergeColumns`:按列合并,在选定区域内将同一列相邻的单元格合并 - -注意:合并时只保留左上角单元格的值,其他单元格的值会被丢弃。 -`--range` 支持带工作表前缀的写法(如 `Sheet1!A1:B3`),此时将优先使用前缀解析出的工作表,忽略 `--sheet-id`。 - -### 上传附件到表格 -``` -Usage: - dws sheet media-upload [flags] -Example: - dws sheet media-upload --node --file ./report.pdf - dws sheet media-upload --node --file ./data.bin --name "数据文件.dat" --mime-type application/octet-stream -Flags: - --node string 目标表格文档的标识,支持传入 URL 或 ID (必填) - --file string 本地文件路径 (必填) - --name string 附件显示名称 (默认使用文件名) - --mime-type string 文件 MIME 类型 (默认根据扩展名推断) -``` - -### 上传图片并写入表格单元格 -``` -Usage: - dws sheet write-image [flags] -Example: - dws sheet write-image --node --sheet-id --range A1:A1 --file ./chart.png - dws sheet write-image --node --sheet-id --range B2:B2 --file ./logo.png --width 200 --height 100 -Flags: - --node string 目标表格文档的标识,支持传入 URL 或 ID (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 目标单元格区域地址,如 A1:A1 (必填) - --file string 本地图片文件路径 (必填) - --name string 图片显示名称 (默认使用文件名) - --mime-type string 文件 MIME 类型 (默认根据扩展名推断) - --width int 图片显示宽度 (可选) - --height int 图片显示高度 (可选) -``` - -### 创建浮动图片 -``` -Usage: - dws sheet create-float-image [flags] -Example: - # 先上传图片获取 resourceUrl - dws sheet media-upload --node --file ./chart.png - # 输出: resourceUrl: /core/api/resources/img/xxxx... - - # 再创建浮动图片 - dws sheet create-float-image --node --sheet-id \ - --src "/core/api/resources/img/xxxx..." --range A1 --width 400 --height 300 - - # 带偏移量 - dws sheet create-float-image --node --sheet-id \ - --src "/core/api/resources/img/xxxx..." --range B2 --width 200 --height 150 --offset-x 10 --offset-y 20 -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --src string 图片资源路径,通过 media-upload 获取的 resourceUrl (必填) - --range string 锚点单元格,A1 表示法,如 A1、B3 (必填) - --width int 图片宽度,像素,正整数 (必填) - --height int 图片高度,像素,正整数 (必填) - --offset-x int 水平偏移量,像素 (默认 0) - --offset-y int 垂直偏移量,像素 (默认 0) -``` - -浮动图片悬浮于单元格之上,不占用单元格内容,可自由定位和调整大小。 -- `--src` 必须是 `media-upload` 返回的 `resourceUrl`(格式为 `/core/api/resources/img/...`),不能直接传外部 URL -- `--range` 使用 A1 表示法指定锚点单元格(如 `A1`、`B3`),支持带工作表前缀(如 `Sheet1!A1`) -- `--width` / `--height` 为必填,单位像素,必须为正整数 -- `--offset-x` / `--offset-y` 表示相对锚点单元格左上角的偏移量(像素),默认 0,不能为负数 - -### 获取浮动图片详情 -``` -Usage: - dws sheet get-float-image [flags] -Example: - dws sheet get-float-image --node --sheet-id --float-image-id -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --float-image-id string 浮动图片 ID (必填) -``` - -获取单个浮动图片的详细信息,包括 ID、图片资源路径、锚点位置、尺寸和偏移量。 -`--float-image-id` 可通过 `list-float-images` 获取。 - -### 列出工作表所有浮动图片 -``` -Usage: - dws sheet list-float-images [flags] -Example: - dws sheet list-float-images --node --sheet-id -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) -``` - -列出指定工作表中所有浮动图片,返回 `floatImages` 数组和 `totalCount`。 - -### 更新浮动图片属性 -``` -Usage: - dws sheet update-float-image [flags] -Example: - # 移动浮动图片到新位置 - dws sheet update-float-image --node --sheet-id --float-image-id --range C5 - - # 调整尺寸 - dws sheet update-float-image --node --sheet-id --float-image-id --width 600 --height 400 - - # 替换图片(需先 media-upload 新图片获取 resourceUrl) - dws sheet update-float-image --node --sheet-id --float-image-id \ - --src "/core/api/resources/img/xxxx..." -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --float-image-id string 浮动图片 ID (必填) - --src string 新的图片资源路径,通过 media-upload 获取的 resourceUrl - --range string 新的锚点单元格,A1 表示法 - --width int 新的图片宽度,像素 - --height int 新的图片高度,像素 - --offset-x int 新的水平偏移量,像素 - --offset-y int 新的垂直偏移量,像素 -``` - -更新浮动图片的属性,`--src` / `--range` / `--width` / `--height` / `--offset-x` / `--offset-y` 至少传入一个。 -`--float-image-id` 可通过 `list-float-images` 获取。 - -### 删除浮动图片 -``` -Usage: - dws sheet delete-float-image [flags] -Example: - dws sheet delete-float-image --node --sheet-id --float-image-id -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --float-image-id string 浮动图片 ID (必填) -``` - -删除指定的浮动图片,操作不可恢复。`--float-image-id` 可通过 `list-float-images` 获取。 - -### 全局查找替换 -``` -Usage: - dws sheet replace [flags] -Example: - dws sheet replace --node --sheet-id --find "旧文本" --replacement "新文本" - dws sheet replace --node --sheet-id --find "待处理" --replacement "已完成" --match-entire-cell - dws sheet replace --node --sheet-id --find "\\d{4}" --replacement "****" --use-regexp - dws sheet replace --node --sheet-id --find "旧" --replacement "新" --range "A1:D100" - dws sheet replace --node --sheet-id --find "临时" --replacement "" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --find string 查找文本 (必填) - --replacement string 替换文本 (必填,可为空字符串表示删除) - --range string 替换范围,A1 表示法 (如 A1:D100) - --match-case 区分大小写 (默认 false) - --match-entire-cell 完整单元格匹配 - --use-regexp 启用正则表达式匹配 - --include-hidden 包含隐藏行/列 -``` - -返回被替换的单元格数量。`--replacement` 可以为空字符串,表示删除匹配内容。 - -### 移动行或列 -``` -Usage: - dws sheet move-dimension [flags] -Example: - # 将第 2 行移动到第 5 行的位置(索引从 0 开始) - dws sheet move-dimension --node --sheet-id \ - --dimension ROWS --start-index 1 --end-index 1 --destination-index 4 - - # 将第 2~4 行移动到第 1 行之前 - dws sheet move-dimension --node --sheet-id \ - --dimension ROWS --start-index 1 --end-index 3 --destination-index 0 - - # 将 B~C 列移动到 E 列的位置 - dws sheet move-dimension --node --sheet-id \ - --dimension COLUMNS --start-index 1 --end-index 2 --destination-index 4 -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --dimension string 维度类型: ROWS 或 COLUMNS (必填) - --start-index int 源起始索引,0-based (必填) - --end-index int 源结束索引,0-based,包含 (必填) - --destination-index int 目标位置索引,0-based (必填) -``` +- probe 为 `axls` → 按 Reference 索引路由到对应命令 +- probe 为 xlsx/csv → 先 `dws drive download` 下载到本地,严禁调用 sheet 命令 -索引均为 0-based(第 1 行/列的索引为 0)。destination-index 不能在 [start-index, end-index] 范围内。 - -**destination-index 计算规则:** -destination-index 是目标位置的 0-based 索引,即移动到第 n 行/列则传 n-1: -- 通用公式:`destination-index = 目标行号(1-based) - 1` -- 例如:将第 2 行移到第 5 行位置 → `destination-index = 5 - 1 = 4`,即 `start-index=1, end-index=1, destination-index=4` -- 例如:将第 4 行移到第 1 行(最前面)→ `destination-index = 1 - 1 = 0`,即 `start-index=3, end-index=3, destination-index=0` - -### 追加空行或空列 -``` -Usage: - dws sheet add-dimension [flags] -Example: - dws sheet add-dimension --node --sheet-id --dimension ROWS --length 5 - dws sheet add-dimension --node --sheet-id --dimension COLUMNS --length 3 -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --dimension string 维度类型: ROWS 或 COLUMNS (必填) - --length int 追加数量,正整数,最多 5000 (必填) -``` - -在工作表末尾追加指定数量的空行或空列。 - -### 取消合并单元格 -``` -Usage: - dws sheet unmerge-cells [flags] -Example: - dws sheet unmerge-cells --node --sheet-id --range "A1:D5" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 取消合并的范围,A1 表示法 (必填) -``` - -取消指定范围内所有合并的单元格,恢复为独立单元格。 - -### 设置下拉列表 -``` -Usage: - dws sheet set-dropdown [flags] -Example: - # 设置单选下拉列表 - dws sheet set-dropdown --node --sheet-id --range "A2:A100" \ - --options '[{"value":"选项1"},{"value":"选项2"},{"value":"选项3"}]' - - # 设置带颜色的多选下拉列表 - dws sheet set-dropdown --node --sheet-id --range "B2:B50" \ - --options '[{"value":"高","color":"#ff0000"},{"value":"中","color":"#ffaa00"},{"value":"低","color":"#00ff00"}]' \ - --multi-select -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 目标单元格范围,A1 表示法,如 A2:A100 (必填) - --options string 下拉选项 JSON 数组 (必填),如 '[{"value":"选项1","color":"#ff0000"}]' - --multi-select 是否允许多选(默认单选) -``` - -在指定单元格范围内设置下拉列表。设置后用户可从预定义选项中选择值。 -- **用途**:为单元格配置下拉列表,支持自定义选项颜色和多选。 -- **场景**:规范数据输入,如状态选择(完成/进行中/待处理)、优先级(高/中/低)等。 -- **注意**:选项值不能包含英文逗号;如果目标范围已存在下拉列表,会被新配置覆盖。 - -### 获取下拉列表配置 -``` -Usage: - dws sheet get-dropdown [flags] -Example: - dws sheet get-dropdown --node --sheet-id --range "A2:A100" - dws sheet get-dropdown --node --sheet-id --range "A1" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 查询范围,A1 表示法,如 A1:A100 (必填) -``` - -查询指定范围内的下拉列表配置信息,包括选项值、颜色和是否多选。 -- **用途**:查看单元格已设置的下拉列表选项和配置。 -- **场景**:在修改下拉列表前先查询现有配置;确认下拉列表是否设置成功。 -- **返回**:`dataValidations` 数组,相同选项的单元格聚合为一组,每组包含 `conditionValues`(选项值)、`ranges`(覆盖范围)、`options`(含 `enableMultiSelect` 和 `colorValueMap`)。范围内无下拉列表时 `hasDropdown` 为 false。 - -### 删除下拉列表 -``` -Usage: - dws sheet delete-dropdown [flags] -Example: - dws sheet delete-dropdown --node --sheet-id --range "A2:A100" - dws sheet delete-dropdown --node --sheet-id --range "B1:D10" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 要删除下拉列表的范围,A1 表示法 (必填) -``` - -删除指定范围内的下拉列表配置,单元格恢复为普通文本格式。 -- **用途**:移除不再需要的下拉列表约束。 -- **注意**:已填写的单元格值不会被清除;目标范围不存在下拉列表时操作仍返回成功。 - -### 获取筛选信息 -``` -Usage: - dws sheet filter get [flags] -Example: - dws sheet filter get --node --sheet-id - dws sheet filter get --node "https://alidocs.dingtalk.com/i/nodes/" --sheet-id "Sheet1" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) -``` - -获取指定工作表的全局筛选信息,返回筛选范围和各列的筛选条件详情。 -- **用途**:查看当前工作表上是否存在全局筛选及其配置。 -- **场景**:在修改或删除筛选前,先读取当前筛选配置;创建筛选前先确认是否已存在(每个工作表只能有一个筛选)。 -- **区分**:全局筛选(filter)影响所有协作者看到的数据展示;筛选视图(filter-view)是个人化的。 -- **返回**:`range`(筛选范围,A1 表示法)和 `columnFilterCriteria`(各列条件,key 为列偏移量)。如果未设置筛选,返回筛选信息为空。 - -### 创建筛选 -``` -Usage: - dws sheet filter create [flags] -Example: - # 创建筛选框架(不设条件) - dws sheet filter create --node --sheet-id --range "A1:E100" - - # 创建筛选并同时设置条件(按值筛选) - dws sheet filter create --node --sheet-id --range "A1:E100" --criteria '[{"column":1,"filterType":"values","visibleValues":["北京","上海"]}]' - - # 创建筛选并设置条件筛选 - dws sheet filter create --node --sheet-id --range "A1:E100" --criteria '[{"column":2,"filterType":"condition","conditions":[{"operator":"greater","value":"100"}]}]' -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 筛选范围,A1 表示法,须包含表头行 (必填) - --criteria string 筛选条件 JSON 数组 (可选) -``` - -在工作表中创建全局筛选。 -- **用途**:为工作表建立筛选器,使数据可按条件过滤展示。 -- **约束**:每个工作表只能有一个全局筛选,已存在时会报错。应先 `filter get` 确认不存在后再创建。 -- **range 规范**:必须包含表头行(如 `A1:E100`),不能只包含数据行。 -- **criteria 格式**:JSON 数组,每个元素含 `column`(列偏移量,从 0 开始)和筛选条件字段。不传则仅创建空筛选框架,后续可通过 `filter update` 设置条件。 - -### 删除筛选 - -> **CAUTION:** 不可逆操作 — 执行前必须向用户确认。 - -``` -Usage: - dws sheet filter delete [flags] -Example: - dws sheet filter delete --node --sheet-id --yes -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) -``` - -删除工作表的全局筛选。 -- **用途**:移除筛选器,所有被隐藏的行将重新显示。 -- **不可逆**:删除后所有筛选条件丢失,需重新创建。 -- **前置**:工作表没有筛选时调用会报错,应先 `filter get` 确认存在。 - -### 批量更新筛选条件 -``` -Usage: - dws sheet filter update [flags] -Example: - # 同时设置多列的筛选条件 - dws sheet filter update --node --sheet-id --criteria '[{"column":0,"filterType":"values","visibleValues":["已完成","进行中"]},{"column":2,"filterType":"condition","conditions":[{"operator":"greater","value":"50"}]}]' - - # 按颜色筛选 - dws sheet filter update --node --sheet-id --criteria '[{"column":1,"filterType":"color","backgroundColor":"#FF0000"}]' -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --criteria string 筛选条件 JSON 数组 (必填) -``` - -批量更新筛选条件,可同时设置多列的筛选条件。 -- **用途**:一次性设置或替换多列的筛选条件。 -- **前置**:工作表必须已创建筛选(通过 `filter create`)。 -- **覆盖式**:指定列的条件会被替换,未指定的列保持不变。如只想修改某一列,建议先 `filter get` 读取现有配置。 -- **criteria 格式**:JSON 数组,支持三种 `filterType`: - - `values`:按值筛选,指定 `visibleValues` 数组 - - `condition`:按条件筛选,指定 `conditions` 数组(最多 2 个)和可选的 `conditionOperator`(`and`/`or`) - - `color`:按颜色筛选,指定 `backgroundColor` 或 `fontColor`(二选一) - -### 清除单列筛选条件 -``` -Usage: - dws sheet filter clear-criteria [flags] -Example: - # 清除第 2 列(B 列)的筛选条件 - dws sheet filter clear-criteria --node --sheet-id --column 1 - - # 清除第 1 列(A 列)的筛选条件 - dws sheet filter clear-criteria --node --sheet-id --column 0 -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --column number 列偏移量,从 0 开始 (必填) -``` - -清除筛选中某一列的筛选条件。 -- **用途**:移除某列的筛选条件,该列不再参与筛选计算。 -- **区分**:仅清除指定列的条件,不删除整个筛选。如需删除整个筛选,使用 `filter delete`。 -- **幂等**:指定列没有设置筛选条件时调用不会报错。 - -### 筛选排序 -``` -Usage: - dws sheet filter sort [flags] -Example: - # 按第 1 列(A 列)升序排序 - dws sheet filter sort --node --sheet-id --column 0 --ascending - - # 按第 3 列(C 列)降序排序 - dws sheet filter sort --node --sheet-id --column 2 --ascending=false -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --column number 排序列偏移量,从 0 开始 (必填) - --ascending 是否升序,默认 true (可选) -``` - -对筛选范围内的数据按指定列排序。 -- **用途**:对数据行按某一列的值进行升序或降序排列。 -- **前置**:工作表必须已创建筛选(通过 `filter create`)。 -- **注意**:排序会实际改变工作表中数据行的物理顺序,不可撤销。 -- **column**:列偏移量从 0 开始,相对于筛选范围首列。 - -### 获取所有筛选视图 -``` -Usage: - dws sheet filter-view list [flags] -Example: - dws sheet filter-view list --node --sheet-id - dws sheet filter-view list --node "https://alidocs.dingtalk.com/i/nodes/" --sheet-id "Sheet1" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) -``` - -获取指定工作表的所有筛选视图列表,返回每个筛选视图的 ID、名称和范围信息。 -- **用途**:查看当前工作表上已创建的所有筛选视图,获取视图 ID、名称和范围。 -- **场景**:在对筛选视图进行 update / delete / update-criteria 等操作前,先用 list 获取可用的 filterViewId。 -- **区分**:筛选视图(filter-view)是个人化的数据过滤方式,与全局筛选不同。每个用户可以创建自己的筛选视图,互不影响原始数据。如果没有筛选视图,返回空列表。 - -### 创建筛选视图 -``` -Usage: - dws sheet filter-view create [flags] -Example: - # 创建不带筛选条件的筛选视图 - dws sheet filter-view create --node --sheet-id --name "我的视图" --range "A1:E10" - - # 创建带按值筛选条件的筛选视图 - dws sheet filter-view create --node --sheet-id --name "销售筛选" --range "A1:E10" \ - --criteria '[{"column":0,"filterType":"values","visibleValues":["销售部"]}]' - - # 创建带按条件筛选的筛选视图(大于等于 200000) - dws sheet filter-view create --node --sheet-id --name "高预算" --range "A1:C10" \ - --criteria '[{"column":1,"filterType":"condition","conditions":[{"operator":"greater-equal","value":"200000"}]}]' -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --name string 筛选视图名称 (必填) - --range string 筛选视图范围,A1 表示法,如 A1:E10 (必填) - --criteria string 筛选条件,JSON 数组 (可选) -``` - -在指定工作表中创建一个筛选视图。 -- **用途**:为指定数据区域创建一个可命名的个人化筛选视图,可选同时设置筛选条件。 -- **场景**:用户需要针对某个数据区域建立固定的筛选视角(如"高绩效员工""研发部数据"),方便反复查看。 -- **区分**:与全局筛选不同,筛选视图是个人化的,不影响其他用户看到的数据。如果只需创建视图不设条件,后续可通过 `update-criteria` 单独设置;如果要一步到位,可通过 `--criteria` 在创建时直接设置。 -`--criteria` 为 JSON 数组,每个元素包含 `column`(列偏移量,从 0 开始)和筛选条件字段。支持三种筛选类型: -- `values`:按值筛选,通过 `visibleValues` 指定允许显示的值列表 -- `condition`:按条件筛选,通过 `conditions` 指定条件列表(最多 2 个),每个条件包含 `operator` 和 `value`。支持的操作符(kebab-case):`equal`、`not-equal`、`contains`、`not-contains`、`starts-with`、`not-starts-with`、`ends-with`、`not-ends-with`、`greater`、`greater-equal`、`less`、`less-equal`。多条件之间通过 `conditionOperator` 指定逻辑关系:`and`(且,默认)或 `or`(或) -- `color`:按颜色筛选,通过 `backgroundColor` 或 `fontColor` 指定颜色值(十六进制,如 `#FF0000`),二选一 - -### 更新筛选视图属性 -``` -Usage: - dws sheet filter-view update [flags] -Example: - # 更新筛选视图名称 - dws sheet filter-view update --node --sheet-id --filter-view-id --name "新名称" - - # 更新筛选视图范围 - dws sheet filter-view update --node --sheet-id --filter-view-id --range "A1:F20" - - # 更新筛选条件 - dws sheet filter-view update --node --sheet-id --filter-view-id \ - --criteria '[{"column":1,"filterType":"condition","conditions":[{"operator":"greater","value":"100"}]}]' -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --filter-view-id string 筛选视图 ID (必填) - --name string 筛选视图新名称 - --range string 筛选视图新范围,A1 表示法 - --criteria string 筛选条件,JSON 数组 -``` - -更新筛选视图的名称、范围和/或筛选条件,`--name`、`--range`、`--criteria` 至少传入一个。 -- **用途**:修改已有筛选视图的名称、数据范围或筛选条件。 -- **场景**:数据区域扩展后需要扩大筛选视图范围,或重命名视图,或通过 `--criteria` 一次性批量更新多列筛选条件。 -- **区分**:`update` 可同时修改名称、范围和条件,适合批量更新;`update-criteria` 只能设置单列条件,适合精确控制某一列的筛选逻辑。`--criteria` 指定列的条件会被替换,未指定的列保持不变。 - -`--criteria` 为 JSON 数组,格式与 `filter-view create` 的 `--criteria` 相同,支持的筛选类型和操作符参见「创建筛选视图」说明。 - -### 删除筛选视图 - -> **CAUTION:** 不可逆操作 — 执行前必须向用户确认。 - -``` -Usage: - dws sheet filter-view delete [flags] -Example: - dws sheet filter-view delete --node --sheet-id --filter-view-id -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --filter-view-id string 筛选视图 ID (必填) -``` - -删除指定的筛选视图。 -- **用途**:永久删除一个不再需要的筛选视图及其所有筛选条件。 -- **场景**:筛选视图已过时或不再需要时,清理无用的视图。 -- **区分**:`delete` 删除整个筛选视图(包括所有列的条件),操作不可恢复;`delete-criteria` 只删除某一列的筛选条件,视图本身保留。此操作不影响全局筛选或其他筛选视图,也不影响原始数据。 - -### 更新筛选视图列条件 -``` -Usage: - dws sheet filter-view update-criteria [flags] -Example: - # 按值筛选:只显示"销售部"和"市场部" - dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ - --column 0 --filter-criteria '{"filterType":"values","visibleValues":["销售部","市场部"]}' - - # 按条件筛选:大于 100 - dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ - --column 2 --filter-criteria '{"filterType":"condition","conditions":[{"operator":"greater","value":"100"}]}' - - # 按条件筛选:大于等于 200000 - dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ - --column 1 --filter-criteria '{"filterType":"condition","conditions":[{"operator":"greater-equal","value":"200000"}]}' - - # 按条件筛选:小于 100 - dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ - --column 1 --filter-criteria '{"filterType":"condition","conditions":[{"operator":"less","value":"100"}]}' - - # 多条件筛选:大于等于 60 且 小于等于 90 - dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ - --column 2 --filter-criteria '{"filterType":"condition","conditionOperator":"and","conditions":[{"operator":"greater-equal","value":"60"},{"operator":"less-equal","value":"90"}]}' - - # 按颜色筛选:背景色为红色 - dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ - --column 1 --filter-criteria '{"filterType":"color","backgroundColor":"#FF0000"}' -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --filter-view-id string 筛选视图 ID (必填) - --column int 列偏移量,从 0 开始 (必填) - --filter-criteria string 筛选条件,JSON 对象 (必填) -``` - -更新筛选视图中某一列的筛选条件。 -- **用途**:为筛选视图的指定列创建或更新筛选条件,控制该列哪些数据行可见。 -- **场景**:只显示某些特定值的行(如"只看研发部")→ `filterType: values`;按数值条件筛选(如"绩效 ≥ 85")→ `filterType: condition` + `operator: greater-equal`;按文本条件筛选(如"名称包含关键字")→ `filterType: condition` + `operator: contains`。 -- **区分**:`update-criteria` 精确控制单列条件,适合逐列设置不同的筛选逻辑;`filter-view update --criteria` 可以批量更新多列条件;`delete-criteria` 是 `update-criteria` 的逆操作,删除指定列的条件。 - -`--column` 为列偏移量(从 0 开始),相对于筛选视图范围首列。 -例如筛选视图范围为 `B1:E10`,则 `--column 0` 代表 B 列,`--column 1` 代表 C 列。 - -`--filter-criteria` 为 JSON 对象,支持三种筛选类型: -- `values`:按值筛选,通过 `visibleValues` 指定允许显示的值列表 -- `condition`:按条件筛选,通过 `conditions` 指定条件列表(最多 2 个),每个条件包含 `operator` 和 `value`。支持的操作符:`equal`、`not-equal`、`contains`、`not-contains`、`starts-with`、`not-starts-with`、`ends-with`、`not-ends-with`、`greater`、`greater-equal`、`less`、`less-equal`。多条件之间通过 `conditionOperator` 指定逻辑关系:`and`(且,默认)或 `or`(或) -- `color`:按颜色筛选,通过 `backgroundColor` 或 `fontColor` 指定颜色值(十六进制,如 `#FF0000`),二选一 - -### 删除筛选视图列条件 -``` -Usage: - dws sheet filter-view delete-criteria [flags] -Example: - # 删除第 1 列(A 列)的筛选条件 - dws sheet filter-view delete-criteria --node --sheet-id --filter-view-id --column 0 - - # 删除第 3 列(C 列)的筛选条件 - dws sheet filter-view delete-criteria --node --sheet-id --filter-view-id --column 2 -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --filter-view-id string 筛选视图 ID (必填) - --column int 列偏移量,从 0 开始 (必填) -``` - -清除筛选视图中指定列的筛选条件。 -- **用途**:移除筛选视图中指定列的筛选条件,使该列不再参与过滤。 -- **场景**:之前通过 `update-criteria` 设置了某列的筛选条件,现在需要取消该列的筛选以显示全部数据。 -- **区分**:`delete-criteria` 只清除指定列的条件,筛选视图本身和其他列的条件保持不变;`delete` 会删除整个筛选视图。如果指定列没有设置筛选条件,调用此命令不会报错(幂等操作)。 - -### 获取单个筛选视图详情 -``` -Usage: - dws sheet filter-view info [flags] -Example: - # 查看指定筛选视图的详情 - dws sheet filter-view info --node --sheet-id --filter-view-id -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --filter-view-id string 筛选视图 ID (必填) -``` - -获取指定筛选视图的完整信息,包括 ID、名称、范围和筛选条件。 -- **用途**:查看某个筛选视图的当前配置,包括已设置的所有筛选条件详情。 -- **场景**:在修改或删除筛选视图前,先确认其当前状态;或在 `update-criteria` 后验证条件是否生效。 -- **区分**:`info` 返回单个视图的完整信息(含 criteria);`list` 返回所有视图的列表概要。`info` 需要指定 `--filter-view-id`,ID 可通过 `list` 获取。 -- **实现**:内部调用 `get_filter_views` 获取全部列表后按 ID 过滤。 - -### 列出筛选视图所有列条件 -``` -Usage: - dws sheet filter-view list-criteria [flags] -Example: - # 列出筛选视图的所有条件 - dws sheet filter-view list-criteria --node --sheet-id --filter-view-id -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --filter-view-id string 筛选视图 ID (必填) -``` - -列出指定筛选视图中已设置的所有列筛选条件。 -- **用途**:查看某个筛选视图当前设置了哪些列的筛选条件,包括每列的条件类型和具体规则。 -- **场景**:在管理筛选条件(修改/删除特定列条件)前,先了解当前视图有哪些条件;或排查筛选结果不符合预期时检查条件配置。 -- **区分**:`list-criteria` 返回所有列的条件(按列偏移量为 key 的对象);`get-criteria` 只返回指定列的条件。如果没有设置任何条件,返回空对象 `{}`。 -- **实现**:内部调用 `get_filter_views` 获取视图详情后提取 `criteria` 字段。 - -### 获取单列筛选条件 -``` -Usage: - dws sheet filter-view get-criteria [flags] -Example: - # 查看第 1 列(偏移量 0)的筛选条件 - dws sheet filter-view get-criteria --node --sheet-id --filter-view-id --column 0 - - # 查看第 3 列(偏移量 2)的筛选条件 - dws sheet filter-view get-criteria --node --sheet-id --filter-view-id --column 2 -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --filter-view-id string 筛选视图 ID (必填) - --column int 列偏移量,从 0 开始 (必填) -``` - -获取指定筛选视图中某一列的筛选条件详情。 -- **用途**:查看某个筛选视图中指定列当前设置的筛选条件,包括条件类型、运算符和比较值。 -- **场景**:在修改某列条件前,先查看其当前配置;或验证 `update-criteria` 后该列条件是否正确。 -- **区分**:`get-criteria` 只返回指定列的条件;`list-criteria` 返回所有列的条件。`--column` 为列偏移量(从 0 开始),相对于筛选视图范围首列。 -- **实现**:内部调用 `get_filter_views` 获取视图详情后按列偏移量过滤 `criteria` 中的对应条件。 - -### 导出表格为 xlsx(异步任务一站式) -``` -Usage: - dws sheet export [flags] # 一站式:提交 → 轮询 → 可选下载 -Example: - # 仅导出,返回 downloadUrl(链接有时效性,请尽快下载) - dws sheet export --node - dws sheet export --node "https://alidocs.dingtalk.com/i/nodes/" - - # 导出并自动下载为本地文件 - dws sheet export --node --output ./report.xlsx - - # --output 为目录时,自动按下载链接中的文件名保存 - dws sheet export --node --output ./ - -Flags: - --node string 表格文档 ID 或 URL (必填) - --output string 本地保存路径(可选,支持文件路径或目录) -``` - -将钉钉在线电子表格导出为 Office xlsx 格式。**单命令一站式**:命令内部自动完成「提交任务 → 渐进式退避轮询 → (可选)下载文件」全流程,AI Agent 无需自行拆分步骤或实现轮询。 - -**内部流程**: -1. 调 `submit_export_job` 获取 `jobId` -2. 按渐进式退避策略轮询 `query_export_job` 直至任务终态或超时 -3. 任务成功后取得 `downloadUrl`;若指定了 `--output`,自动 HTTP GET 下载 xlsx 到本地文件 - -**内置轮询策略(CLI 内实现,无需关心)**: -- 第 1~5 次:每次间隔 2 秒 -- 第 6~10 次:每次间隔 5 秒 -- 第 11~20 次:每次间隔 10 秒 -- 第 21~30 次:每次间隔 15 秒 -- **硬上限:最多轮询 30 次(约 5 分钟)**,超时后命令返回错误 - -**命令返回**: -- `--output` 未指定:进度日志 + 末尾输出 `jobId` 和 `downloadUrl`(链接有时效性,请尽快下载) -- `--output` 指定为文件路径:下载到该路径并输出 `导出完成: ` -- `--output` 指定为已存在目录:自动从 `downloadUrl` 推断文件名并保存到该目录下 - -**失败处理(命令内部已处理,Agent 仅需转述)**: -- MCP 返回 `FAILED`:命令立即返回错误并附带失败原因,**禁止自动重试 `dws sheet export`**,告知用户稍后再试 -- 轮询 30 次仍 `PROCESSING`:命令返回超时错误,告知用户稍后再试 - -**限制**:仅支持钉钉在线电子表格(alxs)→ xlsx。导出钉钉文字文档请使用 `doc` 产品对应的导出工具。 - -## 核心工作流 - -```bash -# ── 工作流 1: 创建表格并写入数据 ── - -# 1. 创建表格文档 — 提取 nodeId -dws sheet create --name "销售数据" --format json - -# 2. 查看工作表列表 — 提取 sheetId -dws sheet list --node --format json - -# 3. 写入表头和数据 -dws sheet range update --node --sheet-id --range "A1:C1" \ - --values '[["姓名","部门","销售额"]]' --format json - -dws sheet range update --node --sheet-id --range "A2:C4" \ - --values '[["张三","销售部",50000],["李四","市场部",38000],["王五","销售部",62000]]' --format json - -# ── 工作流 2: 读取已有表格数据 ── - -# 1. 获取工作表列表 -dws sheet list --node --format json - -# 2. 查看工作表详情(行列数、最后非空位置等) -dws sheet info --node --sheet-id --format json - -# 3. 读取全部数据 -dws sheet range read --node --sheet-id --format json - -# 4. 读取指定区域 -dws sheet range read --node --sheet-id --range "A1:D10" --format json - -# ── 工作流 3: 多工作表管理 ── - -# 1. 新建工作表 -dws sheet new --node --name "汇总" --format json - -# 2. 在新工作表中写入汇总公式 -dws sheet range update --node --sheet-id --range "A1:B1" \ - --values '[["指标","数值"]]' --format json - -dws sheet range update --node --sheet-id --range "A2:B2" \ - --values '[["总销售额","=SUM(Sheet1!C2:C100)"]]' --format json - -# ── 工作流 4: 写入数据并设置样式 ── - -# 1. 写入数据 -dws sheet range update --node --sheet-id --range "A1:C3" \ - --values '[["商品","单价","数量"],["苹果",5.5,100],["香蕉",3.2,200]]' --format json - -# 2. 设置数字格式(人民币)——请走 set-style,不要放到 range update -dws sheet range set-style --node --sheet-id --range "B2:B3" \ - --number-format "¥#,##0.00" --format json - -# 3. 写入超链接 -dws sheet range update --node --sheet-id --range "D1" \ - --hyperlinks '[[{"type":"path","link":"https://dingtalk.com","text":"详情"}]]' --format json - -# ── 工作流 5: 追加数据 ── - -# 1. 获取工作表列表 -dws sheet list --node --format json - -# 2. 查看工作表详情(确认列结构) -dws sheet info --node --sheet-id --format json - -# 3. 追加单行数据 -dws sheet append --node --sheet-id \ - --values '[["张三","销售部",50000]]' --format json - -# 4. 追加多行数据 -dws sheet append --node --sheet-id \ - --values '[["李四","市场部",38000],["王五","销售部",62000]]' --format json -``` - -```bash -# ── 工作流 6: 插入行或列 ── - -# 1. 获取工作表列表 -dws sheet list --node --format json - -# 2. 在第 3 行之前插入 2 行 -dws sheet insert-dimension --node --sheet-id \ - --dimension ROWS --position "3" --length 2 --format json - -# 3. 在 A 列之前插入 1 列 -dws sheet insert-dimension --node --sheet-id \ - --dimension COLUMNS --position "A" --length 1 --format json - -# 4. 使用工作表前缀指定位置 -dws sheet insert-dimension --node --sheet-id \ - --dimension ROWS --position "Sheet1!5" --length 3 --format json -``` - -```bash -# ── 工作流 6b: 删除行或列 ── - -# 1. 获取工作表列表 -dws sheet list --node --format json - -# 2. 从第 3 行开始删除 2 行 -dws sheet delete-dimension --node --sheet-id \ - --dimension ROWS --position "3" --length 2 --format json - -# 3. 从 A 列开始删除 1 列 -dws sheet delete-dimension --node --sheet-id \ - --dimension COLUMNS --position "A" --length 1 --format json - -# 4. 使用工作表前缀指定位置 -dws sheet delete-dimension --node --sheet-id \ - --dimension ROWS --position "Sheet1!5" --length 3 --format json -``` - -```bash -# ── 工作流 6c: 更新行/列属性(显隐、行高/列宽) ── - -# 1. 获取工作表列表 -dws sheet list --node --format json - -# 2. 隐藏第 3~4 行 -dws sheet update-dimension --node --sheet-id \ - --dimension ROWS --start-index "3" --length 2 --hidden --format json - -# 3. 显示 A~B 列 -dws sheet update-dimension --node --sheet-id \ - --dimension COLUMNS --start-index "A" --length 2 --hidden=false --format json - -# 4. 设置第 1~5 行行高为 40px -dws sheet update-dimension --node --sheet-id \ - --dimension ROWS --start-index "1" --length 5 --pixel-size 40 --format json - -# 5. 设置 C 列列宽为 200px 并隐藏 -dws sheet update-dimension --node --sheet-id \ - --dimension COLUMNS --start-index "C" --length 1 --pixel-size 200 --hidden --format json -``` - -```bash -# ── 工作流 7: 搜索表格数据 ── - -# 1. 获取工作表列表 -dws sheet list --node --format json - -# 2. 基本搜索 — 在指定工作表中查找文本 -dws sheet find --node --sheet-id --find "销售额" --format json - -# 3. 在指定范围内搜索 -dws sheet find --node --sheet-id --find "合计" --range "A1:D100" --format json - -# 4. 正则搜索(不区分大小写) -dws sheet find --node --sheet-id --find "^total" --use-regexp --match-case=false --format json - -# 5. 精确匹配整个单元格 -dws sheet find --node --sheet-id --find "完成" --match-entire-cell --format json - -# 6. 搜索公式文本 -dws sheet find --node --sheet-id --find "SUM" --match-formula --format json -``` - -```bash -# ── 工作流 8: 合并单元格 ── - -# 1. 获取工作表列表 -dws sheet list --node --format json - -# 2. 合并所有单元格(默认 mergeAll) -dws sheet merge-cells --node --sheet-id --range "A1:B3" --format json - -# 3. 按行合并 -dws sheet merge-cells --node --sheet-id --range "A1:C3" --merge-type mergeRows --format json - -# 4. 按列合并 -dws sheet merge-cells --node --sheet-id --range "A1:C3" --merge-type mergeColumns --format json -``` - -```bash -# ── 工作流 9: 上传附件到表格 ── - -# 1. 基本用法: 上传本地文件到表格 -dws sheet media-upload --node --file ./report.pdf -f json - -# 2. 自定义附件显示名称 (--name 指定上传后在表格中显示的名称) -dws sheet media-upload --node --file ./data.csv --name "销售数据.csv" -f json - -# 3. 指定 MIME 类型 (文件扩展名无法推断时) -dws sheet media-upload --node --file ./data.bin --name "导出数据.dat" --mime-type application/octet-stream -f json - -# 4. 完整流程: 创建表格 → 上传附件 -dws sheet create --name "项目资料" -f json -# 提取 nodeId 后: -dws sheet media-upload --node --file ./design.pdf -f json -dws sheet media-upload --node --file ./timeline.xlsx --name "项目时间线.xlsx" -f json - -# ── 工作流 10: 写入图片到表格单元格 ── - -# 1. 基本用法: 写入图片到指定单元格 -dws sheet write-image --node --sheet-id --range A1:A1 --file ./chart.png -f json - -# 2. 指定显示尺寸 -dws sheet write-image --node --sheet-id --range B2:B2 --file ./logo.png --width 200 --height 100 -f json - -# 3. 自定义图片名称 -dws sheet write-image --node --sheet-id --range C3:C3 --file ./photo.jpg --name "产品图.jpg" -f json - -# 4. 完整流程: 创建表格 → 写表头 → 写入图片 -dws sheet create --name "产品目录" -f json -# 提取 nodeId 后: -dws sheet range update --node --sheet-id Sheet1 --range "A1:B1" --values '[["产品名称","产品图片"]]' -f json -dws sheet range update --node --sheet-id Sheet1 --range "A2:A2" --values '[["MacBook Pro"]]' -f json -dws sheet write-image --node --sheet-id Sheet1 --range B2:B2 --file ./macbook.png --width 150 --height 100 -f json -``` - -```bash -# ── 工作流 11: 筛选视图管理 ── - -# 1. 获取工作表列表 -dws sheet list --node -f json - -# 2. 查看已有筛选视图 -dws sheet filter-view list --node --sheet-id -f json - -# 3. 创建筛选视图(不带条件) -dws sheet filter-view create --node --sheet-id \ - --name "我的筛选" --range "A1:E100" -f json - -# 4. 为筛选视图设置列条件(按值筛选) -dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ - --column 0 --filter-criteria '{"filterType":"values","visibleValues":["销售部","市场部"]}' -f json - -# 5. 为筛选视图设置列条件(按条件筛选) -dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ - --column 2 --filter-criteria '{"filterType":"condition","conditions":[{"operator":"greater","value":"100"}]}' -f json - -# 6. 更新筛选视图名称和范围 -dws sheet filter-view update --node --sheet-id --filter-view-id \ - --name "销售数据筛选" --range "A1:F200" -f json - -# 7. 清除某列的筛选条件 -dws sheet filter-view delete-criteria --node --sheet-id --filter-view-id \ - --column 0 -f json - -# 8. 删除筛选视图 -dws sheet filter-view delete --node --sheet-id --filter-view-id -f json -``` +## 导入本地表格 +用户说"导入Excel/把xlsx转为在线表格/上传表格并在线编辑"时: ```bash -# ── 工作流 11b: 创建带条件的筛选视图(一步完成) ── +# 上传并转换为在线电子表格(转换后返回 nodeId,可用 sheet 命令操作) +dws drive upload --file ./data.xlsx --convert -# 创建筛选视图时直接指定筛选条件 -dws sheet filter-view create --node --sheet-id \ - --name "高销售额视图" --range "A1:E100" \ - --criteria '[{"column":0,"filterType":"values","visibleValues":["销售部"]},{"column":2,"filterType":"condition","conditions":[{"operator":"greater","value":"50000"}]}]' \ - -f json -``` - -```bash -# ── 工作流 12: 导出表格为 xlsx(单命令一站式)── - -# 场景 A:仅获取下载链接(命令内部自动完成提交+轮询,最终返回 downloadUrl) -dws sheet export --node --format json -# 传入 URL 也可: -# dws sheet export --node "https://alidocs.dingtalk.com/i/nodes/" --format json +# 指定上传到某个文件夹 +dws drive upload --file ./data.xlsx --folder --convert -# 场景 B:导出并自动下载为本地文件 -dws sheet export --node --output ./report.xlsx - -# 场景 C:下载到目录,自动按链接推断文件名 -dws sheet export --node --output ./ - -# 禁止在 Agent 侧实现任何轮询或重试,CLI 内部已按 2s/5s/10s/15s 渐进式退避自动完成(最多 30 次)。 -# 若命令返回失败或超时,直接告知用户稍后再试,不要自动重调 dws sheet export。 +# 指定上传到知识库 +dws drive upload --file ./data.xlsx --workspace --convert ``` +- `--convert` 是关键参数,不加则仅上传为附件,不会转换为在线电子表格 +- 转换后的文档为 `axls` 格式,可用 `sheet` 全部命令操作 +- 支持 `.xlsx` / `.xls` / `.csv` 等格式 -## 上下文传递表 - -| 操作 | 从返回中提取 | 用于 | -|------|-------------|------| -| `create` | `nodeId` | list / info / new / range read / range update / find 的 --node | -| `list` | 工作表的 `sheetId` | info / range read / range update / find 的 --sheet-id | -| `new` | 新工作表的 `sheetId` | range read / range update / find 的 --sheet-id | -| `info` | `rowCount` / `lastNonEmptyRow` | 确定数据范围、追加写入起始行 | -| `find` | `matchedCells` 中的 `a1Notation` | 定位目标单元格,用于 range read / range update | -| `append` | `a1Notation` 追加数据所在范围 | 确认追加位置 | -| `csv-put` | `a1Notation` 实际写入的单元格范围 | 确认写入位置和范围 | -| `insert-dimension` | `a1Notation` 新插入区域范围 | 确认插入位置和范围 | -| `delete-dimension` | `a1Notation` 被删除区域范围 | 确认删除位置和范围 | -| `update-dimension` | `a1Notation` 被更新区域范围、`hidden` 生效的显隐状态、`pixelSize` 生效的尺寸 | 确认更新结果 | -| `merge-cells` | `a1Notation` 实际被合并的范围、`mergeType` 生效的合并方式 | 确认合并结果 | -| `media-upload` | `resourceId`、`resourceUrl` | 附件已上传到表格;`resourceUrl` 可用于 `create-float-image` 的 `--src` | -| `write-image` | `resourceId` | 图片已写入指定单元格 | -| `create-float-image` | `floatImage`(含 `id`、`src`、`range`、`width`、`height`、`offsetX`、`offsetY`) | `id` 用于后续 get / update / delete 的 `--float-image-id` | -| `get-float-image` | `floatImage`(完整信息) | 查看单个浮动图片详情 | -| `list-float-images` | `floatImages` 数组、`totalCount` | 获取所有浮动图片的 `id`,用于后续操作 | -| `update-float-image` | `floatImage`(更新后的完整信息) | 确认更新结果 | -| `delete-float-image` | `message` | 确认删除完成 | -| `replace` | `replaceCount` 被替换的单元格数量 | 确认替换结果 | -| `move-dimension` | `sheetId` 工作表 ID | 确认操作完成 | -| `add-dimension` | `sheetId` 工作表 ID | 确认操作完成 | -| `unmerge-cells` | `sheetId` 工作表 ID | 确认操作完成 | -| `set-dropdown` | `range` 实际设置范围、`optionCount` 选项数量、`enableMultiSelect` 是否多选 | 确认下拉列表设置成功 | -| `get-dropdown` | `hasDropdown` 是否存在下拉、`dataValidations` 下拉配置列表(含 `conditionValues`、`ranges`、`options`) | 查看已有下拉配置 | -| `delete-dropdown` | `range` 实际删除范围 | 确认下拉列表删除完成 | -| `filter-view list` | `filterViews` 筛选视图列表(含 `id`、`name`、`range`) | 获取 filterViewId 用于 info / update / delete / update-criteria / delete-criteria / list-criteria / get-criteria | -| `filter-view info` | `id`、`name`、`range`、`criteria` | 查看单个视图完整配置,确认条件是否生效 | -| `filter-view create` | `id` 筛选视图 ID、`name`、`range` | 用于后续 update / delete / update-criteria / delete-criteria 的 --filter-view-id | -| `filter-view update` | `id`、`name`、`range`、`criteria` | 确认更新结果 | -| `filter-view delete` | `id` 被删除的筛选视图 ID | 确认删除完成 | -| `filter-view update-criteria` | `id` 筛选视图 ID | 确认条件设置完成 | -| `filter-view delete-criteria` | `id` 筛选视图 ID | 确认条件清除完成 | -| `filter-view list-criteria` | 所有列条件(按列偏移量为 key 的对象) | 了解当前视图已设置哪些列的条件 | -| `filter-view get-criteria` | 指定列的条件详情(`filterType`、`conditions` 等) | 查看某列的具体筛选规则 | -| `export` | `downloadUrl`(未指定 --output)/ `导出完成: `(指定 --output) | 直接下发给用户或告知文件已保存到本地。命令内部已完成轮询,不要再调用其他 export 相关命令 | - -## nodeId 多格式说明 - -所有 `--node` 参数同时支持文档 ID、文档 URL、表格分享链接,系统自动识别。详细格式和提取规则请参见前文「URL 识别与 NODE_ID 提取」章节。 - -> ** 禁止使用 `dentryId`**:`drive list` 返回结果中同时包含 `dentryId`(纯数字,如 `218595998810`)和 `fileId`(UUID 格式,如 `ZgpG2NdyVXYOR2D5UGDok65MJMwvDqPk`)两个字段。sheet 的 `--node` 和 `--folder` 参数**只能使用 `fileId`(UUID 格式)**,不能使用纯数字的 `dentryId`,传入纯数字会导致命令失败。 - -## values 参数格式说明 - -`--values` 为二维 JSON 数组,第一维为行,第二维为列: -- 字符串值: `"文本"` -- 数字值: `100` 或 `3.14` -- 公式: `"=SUM(B2:B4)"`(以 `=` 开头的字符串自动识别为公式) -- 清空单元格: 统一使用空字符串 `""`(不要用 `null` 取代,null 不会保留原值且全 null 会被视为无效调用跳过) - -维度必须与 `--range` 范围一致,例如 `--range "A1:B3"` 需要 3 行 2 列的数组。 - -## hyperlinks 参数格式说明 - -`--hyperlinks` 为二维 JSON 数组,每个元素为对象或 null: -- `type`: 链接类型,可选 `path`(外部链接)、`sheet`(工作表跳转)、`range`(单元格跳转) -- `link`: 链接地址 -- `text`: 显示文本 - -与 `--values` 共存时,hyperlinks 优先级更高。 - -## number-format 常用值 - -适用范围:`number-format` 仅在 `range set-style` / `range batch-set-style` 中接受(CLI 对应 `--number-format`,batch 配置文件对应 `numberFormat`);`range update` 不接受该参数。 - -| 格式代码 | 说明 | 示例 | -|----------|------|------| -| `General` | 常规 | 1234.5 | -| `@` | 文本 | 001234 | -| `#,##0` | 整数千分位 | 1,235 | -| `#,##0.00` | 两位小数 | 1,234.50 | -| `0%` | 百分比 | 85% | -| `yyyy/m/d` | 日期 | 2026/3/15 | -| `hh:mm:ss` | 时间 | 14:30:00 | -| `¥#,##0` | 人民币 | ¥1,235 | - -## 注意事项(完整版) - -> 标 ★ 的条目已在前文「关键注意事项」中列出,此处为完整说明。 +## nodeId 说明 -- ★ `--sheet-id` 获取规范(强制):所有涉及 `--sheet-id` 参数的命令(`info` / `new` / `range read` / `range update` / `find` / `append` / `insert-dimension` / `delete-dimension` / `update-dimension` / `move-dimension` / `add-dimension` / `merge-cells` / `unmerge-cells` / `replace` / `write-image` / `set-dropdown` / `get-dropdown` / `delete-dropdown` / `filter-view *` 等),除非用户主动提供了工作表 ID 或工作表名称,否则在 `sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询真实的 `sheetId` / 工作表名称后再调用,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等);用户仅给出工作表名称时,也应通过 `list` 校验该名称是否存在,避免名称大小写或拼写不一致导致失败 -- ★ `range update` 维度校验(强制):调用 `range update` 写入 `--values` 或 `--hyperlinks` 时,必须严格校验二维 JSON 数组的行数与列数与 `--range` 指定的范围完全一致: - - 例如 `--range "A1:C3"` 表示 3 行 × 3 列,`--values` 必须是 `[[v1,v2,v3],[v4,v5,v6],[v7,v8,v9]]` 这样 3×3 的数组 - - `--range "A1"` 表示 1 行 × 1 列,`--values` 必须是 `[[v]]` - - 行数不足需要用空字符串补齐,列数不足需要补齐到每行相同长度;禁止出现各行列数不一致或与 `--range` 不匹配的情况,否则调用会直接报错 - - 同时传入 `--values` 和 `--hyperlinks` 时,两个二维数组的行列数都必须与 `--range` 严格一致 -- ★ `range update` 清空单元格规范(强制):如需清空单元格内容,统一使用空字符串 `""`。禁止使用 `null`:`null` 不会保留单元格原值,也不存在"选择性保留"场景;且若 `--values` 全部为 `null`,整体调用会被视为无效而跳过,无任何写入效果 -- `create` 不传 `--folder` 和 `--workspace` 时,默认创建在"我的文档"根目录 -- `list` 返回所有工作表的 ID 和名称,是后续操作的必要前置步骤 -- `info` 不传 `--sheet-id` 时默认返回第一个工作表的详情 -- `range read` 不传 `--range` 时默认读取整个工作表的全部非空数据 -- `range read` 的 `--range` 支持 `Sheet1!A1:D10` 格式直接指定工作表(此时忽略 `--sheet-id`) -- `range read` 遇到超时或响应过慢时,应缩小 `--range` 查询范围,**单次读取的单元格数量建议控制在 5000 个以内**;数据量较大时通过 `info` 获取边界后分批读取,避免不传 `--range` 直接读取整个大工作表 -- `range update` 的 `--values` 和 `--hyperlinks` 至少传入一项 -- `range update` 职责边界:`range update` 仅写入单元格的值与超链接,不接受任何样式参数(包括但不限于数字格式 / 背景色 / 字体 / 对齐方式等)。如需设置数字格式,请使用 `dws sheet range set-style --number-format <格式代码>`;批量场景走 `dws sheet range batch-set-style --batch `(配置文件中使用 `numberFormat` 字段)。不要在同一次 `range update` 调用里同时完成写值与格式设置 -- ★ `range update` / `range set-style` / `range batch-set-style` 单次调用上限(强制):行数 ≤ 1000,单元格总数(行×列)建议≤ 5000(底层硬限 30000);超限请拆分多次调用。CLI 会在调用前做本地预校验,底层超 30000 会直接报错 -- `range set-style` / `range batch-set-style` 的样式枚举按驼峰书写:`wordWrap` 取 `overflow`/`clip`/`autoWrap`,`fontWeight` 取 `bold`/`normal`,`hAlign` 取 `left`/`center`/`right`/`general`,`vAlign` 取 `top`/`middle`/`bottom`;背景色/字体颜色统一使用 `#RRGGBB` 格式 -- `new` 创建工作表时,如名称与已有工作表重复,系统会自动重命名 -- `find` 返回匹配单元格的地址(A1 表示法)和值,无匹配时返回空数组 -- `find` 的 `--match-entire-cell` 用于精确匹配:只返回单元格内容完全等于搜索文本的结果,不会匹配包含该文本的单元格(例如搜索"苹果"时,只匹配"苹果",不匹配"苹果手机""苹果汁"等)。用户说"精确搜索/完全匹配/只搜等于XX的"时必须使用此参数 -- `find` 的 `--match-case` 默认为 true(区分大小写),设为 false 可忽略大小写 -- `find` 的 `--use-regexp` 启用后,`--find` 参数作为正则表达式处理 -- ★ 当用户要求搜索/查找表格数据时,使用 `find` 命令,不要用 `range read` 读取全量数据后自行过滤——`find` 支持服务端搜索,效率更高、语义更准确 -- `append` 自动定位到最后一行有数据的位置下方插入,无需手动计算行号 -- `append` 的 `--values` 二维数组中每行的列数必须一致,否则会报错。如果用户提供的数据中各行长度不同,必须先将短行用空字符串 `""` 补齐到与最长行相同的列数后再调用。追加的数据列数也应与工作表已有数据列数保持一致 -- `append` vs `range update`:追加新行用 `append`,修改已有单元格用 `range update` -- `insert-dimension` 在指定位置之前插入空行或空列,不写入数据;如需在末尾追加行/列,使用 `append` -- `insert-dimension` 的 `--dimension` 只接受 `ROWS` 或 `COLUMNS` -- `insert-dimension` 的 `--position` 支持工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id` -- `insert-dimension` 的 `--length` 最大为 5000 -- `delete-dimension` 从指定位置起删除若干连续的行或列,删除后后续行/列向前移动填补空位 -- `delete-dimension` 的 `--dimension` 只接受 `ROWS` 或 `COLUMNS` -- `delete-dimension` 的 `--position` 支持工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id` -- `delete-dimension` 的 `--length` 最大为 5000 -- `delete-dimension` 若需仅清空内容但保留行/列占位,请使用 `range update` 将目标区域写入空字符串 `""`(参见《range update 清空单元格规范》) -- `update-dimension` 批量更新连续行/列的显隐状态与行高/列宽 -- `update-dimension` 的 `--dimension` 只接受 `ROWS` 或 `COLUMNS` -- `update-dimension` 的 `--start-index` 支持工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id` -- `update-dimension` 的 `--length` 最大为 5000 -- `update-dimension` 的 `--hidden` 与 `--pixel-size` 至少必须提供一个 -- `update-dimension` 的 `--pixel-size` 单位为像素,`dimension=ROWS` 时表示行高、`dimension=COLUMNS` 时表示列宽 -- `update-dimension` 当同时提供 `--hidden` 与 `--pixel-size` 时,将先应用尺寸再应用显隐,任一失败整体失败 -- `merge-cells` 合并时只保留左上角单元格的值,其他单元格的值会被丢弃 -- `merge-cells` 的 `--merge-type` 不传时默认为 `mergeAll`(合并所有单元格) -- `merge-cells` 的 `--range` 支持带工作表前缀的写法(如 `Sheet1!A1:B3`),此时忽略 `--sheet-id` -- `merge-cells` 如果目标区域与其他合并单元格、锁定区域或表格区域存在交集,合并将失败 -- `media-upload` 是两步自动完成的流程 (获取附件上传凭证 → OSS 上传),无需手动分步操作 -- `write-image` 是三步自动完成的流程 (获取附件上传凭证 → OSS 上传 → 写入图片到单元格),无需手动分步操作 -- ★ 向表格单元格中写入图片必须使用 `write-image`,禁止使用 `range update`。`range update` 底层调用的 `update_range` MCP 工具不支持图片类型参数,调用会失败 -- `write-image` 与 `media-upload` 的区别:`media-upload` 仅上传附件到表格获取 resourceId;`write-image` 在上传后还会将图片写入指定单元格 -- `create-float-image` 创建浮动图片前必须先通过 `media-upload` 上传图片获取 `resourceUrl`,再将其作为 `--src` 传入。`--src` 的格式为 `/core/api/resources/img/...`,不能直接传外部 URL -- `create-float-image` 的 `--range` 使用 A1 表示法指定锚点单元格(如 `A1`、`B3`),支持带工作表前缀(如 `Sheet1!A1`) -- `create-float-image` 的 `--width` / `--height` 为必填,单位像素,必须为正整数;`--offset-x` / `--offset-y` 可选,默认 0,不能为负数 -- `write-image`(单元格内嵌图片)vs `create-float-image`(浮动图片):`write-image` 将图片写入单元格内部,占据单元格内容;`create-float-image` 创建悬浮于单元格之上的浮动图片,不占用单元格内容,可自由调整位置和大小 -- `update-float-image` 的 `--src` / `--range` / `--width` / `--height` / `--offset-x` / `--offset-y` 至少必须提供一个 -- `list-float-images` 返回 `floatImages` 数组和 `totalCount`,每个元素包含 `id`(用于后续 get / update / delete) -- `delete-float-image` 操作不可恢复,删除后图片将从工作表中移除 -- `replace` 的 `--find` 不能为空字符串,`--replace` 可以为空字符串(表示删除匹配内容) -- `replace` 的 `--match-case` 默认为 false(不区分大小写),与 `find` 的默认行为不同 -- ★ `replace` vs `range update`:需要批量替换文本时,必须使用 `replace` 命令,禁止用 `range update` 手动重写单元格来实现替换效果。`replace` 支持服务端全局替换,效率更高且会返回替换计数 -- ★ `move-dimension` vs `range update`:需要移动行或列时,必须使用 `move-dimension` 命令,禁止用 `range update` 读取数据后手动重写来模拟移动效果。`move-dimension` 是原子操作,能保留单元格的格式、合并状态等属性 -- `move-dimension` 的索引均为 0-based(第 1 行/列的索引为 0),`endIndex` 包含在移动范围内 -- `move-dimension` 的 `--destination-index` 不能在 [start-index, end-index] 范围内 -- `move-dimension` 的移动跨度(end-index - start-index + 1)不超过 5000 -- `move-dimension` 的 `--destination-index` 是目标位置的 0-based 索引,即移动到第 n 行/列则传 `n - 1`(通用公式:`destination-index = 目标行号(1-based) - 1`) -- `add-dimension` vs `range update`:需要在末尾追加空行/空列时,必须使用 `add-dimension` 命令,禁止用 `range update` 写空数据来模拟追加效果 -- `add-dimension` 追加的是空行/空列,与 `append`(追加带数据的行)不同 -- `add-dimension` 的 `--length` 必须为正整数(>= 1),行列均不超过 5000 -- `unmerge-cells` 取消指定范围内所有合并单元格,使用 A1 表示法指定范围 -- `set-dropdown` 在指定范围内设置下拉列表,`--options` 为 JSON 数组,每个元素包含 `value`(必填)和 `color`(可选,`#RRGGBB` 格式)。选项值不能包含英文逗号。`--multi-select` 启用多选模式。如果目标范围已存在下拉列表,会被新配置覆盖 -- `get-dropdown` 查询指定范围内的下拉列表配置,返回 `dataValidations` 数组,相同选项的单元格聚合为一组。无下拉列表时 `hasDropdown` 为 false -- `delete-dropdown` 删除指定范围内的下拉列表配置,单元格恢复为普通文本格式。已填写的值不会被清除。目标范围不存在下拉列表时操作仍返回成功 -- ★ **全局筛选(filter)与筛选视图(filter-view)的区别**:全局筛选影响所有协作者看到的数据展示,每个工作表最多一个;筛选视图是个人化的,互不影响。用户只说"筛选"时默认走 `filter` 系列 -- `filter get` 获取工作表的全局筛选信息,返回 `range`(筛选范围)和 `columnFilterCriteria`(各列条件)。无筛选时返回空 -- `filter create` 创建全局筛选时 `--range` 必须包含表头行(如 `A1:E100`),不能只包含数据行。每个工作表只能有一个筛选,已存在时报错 -- `filter create` 的 `--criteria` 可选,不传则仅创建空筛选框架,后续通过 `filter update` 设置条件 -- `filter delete` 删除后所有筛选条件丢失且所有被隐藏行重新显示,不可恢复 -- `filter delete` 工作表没有筛选时调用会报错,应先 `filter get` 确认存在 -- `filter update` 是覆盖式:指定列的条件会被替换,未指定的列保持不变。如只想修改某一列,建议先 `filter get` 读取现有配置再 patch -- `filter update` 前置:工作表必须已创建筛选 -- `filter clear-criteria` 仅清除指定列的条件,不删除整个筛选。指定列无条件时不报错(幂等) -- `filter sort` 会实际改变数据行的物理顺序,不可撤销。前置:工作表必须已创建筛选 -- ★ **筛选操作规范**(参照飞书 core-operations): - - 当用户要求"筛选/只看/仅保留 X"时,**必须**通过 `filter create` / `filter update` 创建真实的筛选器。**禁止**用"删除不符合条件的行"或"新建工作表只放符合条件的行"来代替 - - 创建/更新筛选后**必须** `filter get` 回读验证配置正确 - - 更新已有筛选前先 `filter get` 读取当前配置,确认目标存在且了解现有条件后再操作 - - 筛选条件的列索引(`column`)必须与实际数据列精确对应,不要凭猜测填写 - - 筛选不支持正则表达式,传入正则会当成普通文本处理 -- `filter-view list` 获取指定工作表的所有筛选视图列表,返回的 `id` 可用于后续 info / update / delete / update-criteria / delete-criteria / list-criteria / get-criteria 的 `--filter-view-id` -- `filter-view info` 获取单个筛选视图的完整信息(含 criteria),内部复用 `get_filter_views` MCP 按 ID 过滤 -- `filter-view list-criteria` 列出指定筛选视图已设置的所有列条件,返回按列偏移量为 key 的对象;无条件时返回空对象 `{}` -- `filter-view get-criteria` 获取指定列的条件详情,`--column` 为列偏移量(从 0 开始);该列无条件时返回错误提示 -- `filter-view create` 创建筛选视图时 `--range` 应包含表头行。`--criteria` 可选,不传则创建后无筛选条件,后续可通过 `filter-view update-criteria` 设置 -- `filter-view update` 的 `--name`、`--range`、`--criteria` 至少需要传入一个,未指定的字段保持不变 -- `filter-view update` 的 `--criteria` 中指定列的条件会被替换,未指定的列保持不变 -- `filter-view delete` 删除后该视图及其所有筛选条件将被永久移除,不可恢复 -- `filter-view delete` 不影响全局筛选或其他筛选视图 -- `filter-view update-criteria` 的 `--column` 为列偏移量(从 0 开始),相对于筛选视图范围首列。例如筛选视图范围为 `B1:E10`,则 `--column 0` 代表 B 列 -- `filter-view update-criteria` 设置条件后立即在该筛选视图中生效,仅影响当前视图,不影响全局筛选或其他筛选视图 -- `filter-view update-criteria` 的 `--filter-criteria` 中 `conditions` 最多 2 个条件,多条件之间通过 `conditionOperator` 指定逻辑关系(`and` 或 `or`) -- `filter-view delete-criteria` 仅清除指定列的条件,不会删除整个筛选视图。如需删除整个筛选视图,请使用 `filter-view delete` -- `filter-view delete-criteria` 如果指定列没有设置筛选条件,调用不会报错 -- 筛选视图相关操作需要"可阅读"权限(list / info / list-criteria / get-criteria)或"可编辑"权限(create / update / delete / update-criteria / delete-criteria),不支持跨组织操作 -- ★ `export` 仅支持钉钉在线电子表格(alxs)→ xlsx;传入钉钉文字文档会报 `invalidRequest.document.typeIllegal` -- ★ `export` 为单命令一站式,CLI 内部已自动完成「提交 → 渐进式退避轮询 → 可选下载」,**Agent 不得在外部实现轮询或重试**;命令返回成功后不再调用其他 export 相关命令 -- `export` 内置轮询策略:1~5 次间隔 2s、6~10 次间隔 5s、11~20 次间隔 10s、21~30 次间隔 15s,硬上限 30 次(约 5 分钟);超时后命令返回错误,告知用户稍后再试即可 -- ★ `export` 命令返回失败或超时时,**禁止自动重调 `dws sheet export`**;直接告知用户导出失败并建议稍后再试 -- `export` 未指定 `--output` 时,返回的 `downloadUrl` 具有时效性,获取后请尽快下载;若用户需要本地文件,优先直接传 `--output` 让 CLI 代为下载 -- `export` 的 `--output` 可为文件路径或已存在目录;为目录时自动从 `downloadUrl` 推断文件名,为文件路径时直接按该路径保存 -- 用户要求"导出表格/下载 xlsx"时,必须使用 `export` 单命令,禁止用 `range read` 读全量数据后自行拼 xlsx 模拟导出(服务端导出会保留格式/合并/公式等完整属性) -- `update` 的 `--title`、`--index`、`--hidden`、`--frozen-row-count`、`--frozen-column-count` 至少必须提供一个 -- `update` 的 `--title` 最长 100 字符,不能包含 `/ \ ? * [ ] :` 等特殊字符 -- `update` 的 `--index` 为 0-based 非负整数,0 表示移动到最前面 -- `update` 的 `--hidden` 设为 true 时,至少需要保留一个可见的工作表,不能将所有工作表都隐藏 -- `update` 的 `--frozen-row-count` / `--frozen-column-count` 为非负整数,不能超过工作表的总行数/列数,设为 0 表示取消冻结 -- `update` 当同时提供多个属性时,所有属性将在同一次请求中更新 -- `copy` 复制操作会将源工作表的所有内容(包括数据、格式、公式等)完整复制到新工作表 -- `copy` 的 `--title` 可选,不传时系统自动生成名称(通常为"源名称 副本"或类似格式) -- `copy` 的 `--title` 最长 100 字符,不能包含 `/ \ ? * [ ] :` 等特殊字符 -- `copy` 当指定名称与已有工作表重复时,系统会自动重命名为合法值 -- `copy` 的 `--index` 可选,不传时副本将放置在源工作表之后的默认位置 -- ★ 关键区分: sheet(电子表格/单元格读写) vs aitable(AI多维表/结构化记录/字段定义) vs doc(文档编辑/阅读) -- sheet 产品线仅支持 `axls`(在线电子表格,`contentType=ALIDOC`),不支持 `xlsx` / `xls` / `xlsm` / `csv` 等本地表格文件 -- 遇到未知 `alidocs` URL 时,必须先 probe(`dws doc info --node --format json`)确认 `contentType` 和 `extension`,才能决定是否走 sheet -- 当节点 `extension=xlsx` / `xls` / `xlsm` / `csv`(`contentType≠ALIDOC`)时,必须用 `dws doc download --node --output <路径>` 先下载到本地再处理,禁止调用任何 sheet 子命令(sheet 底层 MCP 工具只识别 axls,调用 xlsx 节点必失败) -- 要把在线表格导出为 xlsx 文件——走 `dws sheet export`(axls → xlsx 的格式转换);要读已有的 xlsx 文件——走 `dws doc download` 后在本地解析,两者方向相反 +`--node` 同时支持文档 ID、URL、分享链接。`drive list` 返回中必须用 `fileId`(UUID 格式),禁止用 `dentryId`(纯数字)。 diff --git a/skills/mono/references/products/sheet/sheet-conditional-format.md b/skills/mono/references/products/sheet/sheet-conditional-format.md new file mode 100644 index 00000000..b4b7532a --- /dev/null +++ b/skills/mono/references/products/sheet/sheet-conditional-format.md @@ -0,0 +1,271 @@ +# 条件格式 (conditional format) + +## 使用场景 + +### 条件格式 + +#### 强制走条件格式的触发词(硬约束) + +当用户出现以下口语指令时,**强制**走 `cond-format create/update/delete`,**禁止**用 `range set-style` 写静态背景色/字体色代替: + +- **颜色动作**:"标红 / 标黄 / 标绿 / 上色 / 染色 / 涂色" +- **视觉强调**:"高亮 / 突出 / 标记 / 标注 / 区分" +- **条件触发**:"重复的标出来 / 异常的圈出来 / 过期的染红 / 大于 X 的标黄 / 不达标的标红" +- **联动语义**:"颜色随数据变 / 联动 / 自动更新 / 改了数据颜色也跟着变" +- **数值可视化**:"数据条 / 色阶 / 渐变色 / 进度条样式" + +**判断标准**:交付后 `cond-format list` 必须能返回该规则;否则视为违规。 + +> 如果用 `range set-style` 写静态背景色,源数据变化时颜色不会跟着变。典型反例:用户要求"过期单元格标红"时用静态填充——日期变化后颜色不再准确。 + +**大数据量优势**:当数据量 > 1000 行时,条件格式是首选——它由服务端自身渲染,不需要逐行调用 `range set-style`,性能远优于静态样式写入。 + +#### 意图判断 + +用户说"条件格式/条件样式/自动变色/满足条件时高亮/按条件设样式/条件格式规则": +- 查看已有条件格式 → `cond-format list` +- 查看指定规则详情 → `cond-format list --rule-id RULE_ID` + +用户说"创建条件格式/设置条件格式/大于某值时标红/包含某文本时高亮/数据条/图标集/色阶/重复值高亮": +- 创建条件格式规则 → `cond-format create` +- `--condition` 为 JSON 对象,key 为条件类型,value 为条件参数 +- `--cell-style` 为命中时的样式(适用于数值/文本/空值/错误/重复/公式/排名/平均值/标准差类型) +- `--data-bar-style` 为数据条样式(仅数据条类型时使用) +- 色阶和图标集类型不需要 `--cell-style`(样式内置在条件定义中) + +用户说"修改条件格式/更新条件格式规则/改条件/改样式/改范围": +- 更新条件格式规则 → `cond-format update` +- 未传入的字段保持原有值不变 +- 传入 `--condition` 会替换原有条件类型 + +用户说"删除条件格式/移除条件格式/取消条件格式": +- 删除条件格式规则 → `cond-format delete --yes` +- 删除不可恢复,执行前必须向用户确认,同意后才加 `--yes` 执行 +- 规则已不存在时操作仍返回成功 + +#### 常见配置错误(必须注意) + +- **创建后必须验证**:条件格式创建后必须调用 `cond-format list` 验证规则是否生效。如果验证发现规则未生效或配置不正确,应立即修复并重试 +- **范围要精确**:条件格式的应用范围必须精确覆盖用户指定的列/行,不要遗漏也不要过度扩大 +- **`backgroundColor` vs `fontColor` 的中文语义**:用户中文语境下的"标红/高亮/染色/标记"指**单元格背景色**,用 `backgroundColor`;"文字红/字体红/把字变红"才用 `fontColor`。默认无说明时选 `backgroundColor` +- **日期/空值比较必须防空**:用户说"过期的标红"时,公式必须排除空单元格,否则空白格也会被误判为过期而全表标红。正确公式:`=AND(E1<>"", E1<=TODAY())`;错误公式:`=E1<=TODAY()`(空值会被当作 0 判为过期) +- **公式引用方式**:自定义公式条件中的单元格引用需要根据实际场景选择相对/绝对引用(如 `=E1<=TODAY()` 使用相对引用使公式随行变化,而非 `=$E$1<=TODAY()` 只比较一个格) +- **创建前必须确认列对应**:仅读表头不够——如果表头语义含糊,formula 里引用的列字母可能张冠李戴。建议先读 3-5 行数据样本(如 `range read --range "A1:Z5"`)确认列名对应的实际值和数据类型 + +#### 辅助列+条件格式两步走(高频致命错误防护) + +**用户明确要求"辅助列+条件格式"两步走时,禁止用 `formulaCondition` 绕过**: + +当用户说以下任意一种表达时,必须按两步走(先建辅助列 → 再基于辅助列做条件格式),**禁止**直接用一个 `formulaCondition` 公式一步完成: +- "**增加辅助列**,再/然后标记……" +- "**先计算/判断** XX **是否** YY,**再**标记……" +- "**新建一列**放结果,再用结果染色" +- 明确要求用"辅助列"、"辅助字段"、"判断列"、"标记列" + +**正确做法(两步走)**: +```bash +# Step 1: 用 range update 在新列写判断公式(形成"是/否"辅助列) +dws sheet range update --node NODE_ID --sheet-id SHEET_ID --range "H2:H100" \ + --values '[["=IF(A2>B2,\"是\",\"否\")"],...]' + +# Step 2: 基于辅助列值做条件格式(用 formulaCondition 引用辅助列) +dws sheet cond-format create --node NODE_ID --sheet-id SHEET_ID \ + --ranges '["A2:H100"]' \ + --condition '{"formulaCondition":{"formula":"=$H2=\"是\""}}' \ + --cell-style '{"backgroundColor":"#FFECEC"}' +``` + +**错误做法(一步走绕过辅助列)**: +```bash +# 虽然逻辑等价,但产物里缺辅助列 → 用户打开表格看不到"是/否"列 +dws sheet cond-format create --node NODE_ID --sheet-id SHEET_ID \ + --ranges '["A2:H100"]' \ + --condition '{"formulaCondition":{"formula":"=$A2>$B2"}}' \ + --cell-style '{"backgroundColor":"#FFECEC"}' +``` + +**为什么禁止一步走**:用户明确要求辅助列是有**业务意图**的——让人肉眼能在表里看到判断结果列;条件格式只是视觉辅助。一步 `formulaCondition` 虽然效果对了,但用户打开表格看不到辅助列,被视为"操作不完整"。 + +> `formulaCondition` 单独使用的场景是:用户**没有**明确要求辅助列、只要"标红符合条件的行"时。 + +#### 典型工作流 + +``` +1. 先读取现有条件格式了解当前配置 + dws sheet cond-format list --node NODE_ID --sheet-id SHEET_ID + +2. 创建/更新/删除条件格式规则 + dws sheet cond-format create --node NODE_ID --sheet-id SHEET_ID --ranges '["A1:E100"]' \ + --condition '{"numberCondition":{"operator":"greater","value1":"80"}}' \ + --cell-style '{"backgroundColor":"#FFCDD2","fontColor":"#B71C1C","bold":true}' + +3. 再次读取验证结果是否生效 + dws sheet cond-format list --node NODE_ID --sheet-id SHEET_ID +``` + +#### 条件类型参考 + +条件类型(`--condition` JSON 的 key,每次只能选一种): + +| 条件类型 | 说明 | 参数 | +|---------|------|------| +| `numberCondition` | 数值比较 | operator: equal/not-equal/greater/greater-equal/less/less-equal/between/not-between + value1 + value2 | +| `textCondition` | 文本匹配 | operator: contains/not-contains/starts-with/ends-with + value | +| `emptyCondition` | 空值判断 | operator: is-empty/is-not-empty | +| `errorCondition` | 错误值 | operator: error/no-error | +| `duplicateCondition` | 重复/唯一值 | operator: duplicate/unique | +| `formulaCondition` | 自定义公式 | formula: "=A1>100" | +| `rankCondition` | 排名 | value + isPercent + isBottom | +| `averageCondition` | 高于/低于平均值 | isAbove + andEqual | +| `stdevCondition` | 标准差 | value + isAbove + andEqual | +| `dataBarCondition` | 数据条 | minPoint + maxPoint(每个含 type + value) | +| `iconSetCondition` | 图标集 | iconSet(数组)+ showIconOnly | +| `colorScaleCondition` | 色阶 | criterias(数组,每项含 type + value + color) | + +## 命令详细参考 + +### 获取条件格式规则 +``` +Usage: + dws sheet cond-format list [flags] +Example: + # 获取所有条件格式规则 + dws sheet cond-format list --node --sheet-id + + # 获取单个规则的详情 + dws sheet cond-format list --node --sheet-id --rule-id +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --rule-id string 条件格式规则 ID (可选,不传则返回全部) +``` + +- **用途**:查看指定工作表中已有的条件格式规则,或获取单个规则的详情。 +- **场景**:创建/更新/删除条件格式前后验证规则状态;获取 ruleId 供后续 update/delete 使用。 +- **返回**:rules 数组,每条规则包含 id、type、ranges、条件参数、cellStyle/dataBarStyle 等。 + +### 创建条件格式规则 +``` +Usage: + dws sheet cond-format create [flags] +Example: + # 数值条件:大于 80 时标红加粗 + dws sheet cond-format create --node --sheet-id \ + --ranges '["A1:A100"]' \ + --condition '{"numberCondition":{"operator":"greater","value1":"80"}}' \ + --cell-style '{"backgroundColor":"#FFCDD2","fontColor":"#B71C1C","bold":true}' + + # 文本条件:包含"延期"时加删除线 + dws sheet cond-format create --node --sheet-id \ + --ranges '["B1:B50"]' \ + --condition '{"textCondition":{"operator":"contains","value":"延期"}}' \ + --cell-style '{"backgroundColor":"#FFF3E0","strikethrough":true}' + + # 数据条 + dws sheet cond-format create --node --sheet-id \ + --ranges '["C1:C20"]' \ + --condition '{"dataBarCondition":{"minPoint":{"type":"auto"},"maxPoint":{"type":"auto"}}}' \ + --data-bar-style '{"fill":["#4CAF50","#F44336"],"isGradient":true}' + + # 色阶(三色) + dws sheet cond-format create --node --sheet-id \ + --ranges '["D1:D50"]' \ + --condition '{"colorScaleCondition":{"criterias":[{"type":"maxmin","color":"#F44336"},{"type":"percentile","value":"50","color":"#FFEB3B"},{"type":"maxmin","color":"#4CAF50"}]}}' + + # 重复值高亮 + dws sheet cond-format create --node --sheet-id \ + --ranges '["E1:E100"]' \ + --condition '{"duplicateCondition":{"operator":"duplicate"}}' \ + --cell-style '{"backgroundColor":"#FCE4EC"}' +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --ranges string 应用范围 JSON 数组 (必填),如 '["A1:E10"]' + --condition string 条件类型及参数 JSON 对象 (必填) + --cell-style string 单元格样式 JSON 对象 (可选) + --data-bar-style string 数据条样式 JSON 对象 (可选,仅数据条类型) +``` + +- **用途**:在指定工作表中创建一条条件格式规则。 +- **注意事项**: + - 创建后**必须**用 `cond-format list` 验证规则是否生效 + - 中文"标红/高亮/染色"默认指 `backgroundColor`,"字体红"才是 `fontColor` + - 日期/空值公式必须防空:`=AND(E1<>"", E1<=TODAY())` 而非 `=E1<=TODAY()` + - 公式中用相对引用使公式随行变化 + - 创建前建议先 `range read` 读 3-5 行数据确认列对应关系 +- **条件类型**(`--condition` JSON 的 key,每次只能选一种):numberCondition / textCondition / emptyCondition / errorCondition / duplicateCondition / formulaCondition / rankCondition / averageCondition / stdevCondition / dataBarCondition / iconSetCondition / colorScaleCondition + +### 更新条件格式规则 +``` +Usage: + dws sheet cond-format update [flags] +Example: + # 修改条件(改为大于 90) + dws sheet cond-format update --node --sheet-id --rule-id \ + --condition '{"numberCondition":{"operator":"greater","value1":"90"}}' + + # 修改样式 + dws sheet cond-format update --node --sheet-id --rule-id \ + --cell-style '{"backgroundColor":"#C8E6C9","fontColor":"#1B5E20"}' + + # 修改应用范围 + dws sheet cond-format update --node --sheet-id --rule-id \ + --ranges '["A1:F200"]' +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --rule-id string 条件格式规则 ID (必填) + --ranges string 应用范围 JSON 数组 (可选) + --condition string 条件类型及参数 JSON 对象 (可选,传入后替换原有条件) + --cell-style string 单元格样式 JSON 对象 (可选) + --data-bar-style string 数据条样式 JSON 对象 (可选,仅数据条类型) +``` + +- **用途**:更新已有条件格式规则的部分或全部配置。 +- **场景**:修改阈值、切换条件类型、调整样式、扩大应用范围。 +- **注意**:未传入的字段保持原有值不变;`--ranges`/`--condition`/`--cell-style`/`--data-bar-style` 至少传入一个。 +- **ruleId 获取**:通过 `cond-format list` 获取。 + +### 删除条件格式规则 + +> **CAUTION:** 不可逆操作 — 执行前必须向用户确认。 + +``` +Usage: + dws sheet cond-format delete [flags] +Example: + # 删除条件格式规则(必须加 --yes 确认) + dws sheet cond-format delete --node --sheet-id --rule-id --yes +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --rule-id string 条件格式规则 ID (必填) +``` + +- **用途**:删除指定条件格式规则。 +- **幂等性**:规则已不存在时操作仍返回成功。 +- **ruleId 获取**:通过 `cond-format list` 获取。 + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `cond-format list` | `rules` 数组(含 `id`、`type`、`ranges`、条件参数、`cellStyle`/`dataBarStyle`) | 获取 ruleId 供 update / delete 使用;验证规则是否生效 | +| `cond-format create` | 新创建规则的 `id` | 用于后续 update / delete 的 --rule-id | +| `cond-format update` | 更新后的规则信息 | 确认更新结果 | +| `cond-format delete` | 操作结果 | 确认删除完成 | +| `list` | 工作表的 `sheetId` | cond-format list / create / update / delete 的 --sheet-id | + +## 注意事项 + +- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) +- ★ **强制走条件格式的触发词**:用户说"标红/标黄/高亮/突出/标记/数据条/色阶/颜色随数据变"时,强制走 `cond-format create/update/delete`,禁止用 `range set-style` 写静态背景色代替 +- **创建后必须验证**:条件格式创建后必须调用 `cond-format list` 验证规则是否生效 +- **范围要精确**:条件格式的应用范围必须精确覆盖用户指定的列/行,不要遗漏也不要过度扩大 +- **`backgroundColor` vs `fontColor` 的中文语义**:用户中文语境下的"标红/高亮/染色/标记"指**单元格背景色**,用 `backgroundColor`;"文字红/字体红/把字变红"才用 `fontColor`。默认无说明时选 `backgroundColor` +- **日期/空值比较必须防空**:用户说"过期的标红"时,公式必须排除空单元格。正确公式:`=AND(E1<>"", E1<=TODAY())`;错误公式:`=E1<=TODAY()`(空值会被当作 0 判为过期) +- **公式引用方式**:自定义公式条件中的单元格引用需要根据实际场景选择相对/绝对引用(如 `=E1<=TODAY()` 使用相对引用使公式随行变化) +- **创建前必须确认列对应**:仅读表头不够——如果表头语义含糊,formula 里引用的列字母可能张冠李戴。建议先读 3-5 行数据样本(如 `range read --range "A1:Z5"`)确认列名对应的实际值和数据类型 +- **辅助列+条件格式两步走**:用户明确要求"辅助列"时,必须按两步走(先建辅助列 → 再基于辅助列做条件格式),禁止直接用 `formulaCondition` 一步绕过 +- **大数据量优势**:当数据量 > 1000 行时,条件格式是首选——它由服务端自身渲染,不需要逐行调用 `range set-style`,性能远优于静态样式写入 +- **判断标准**:交付后 `cond-format list` 必须能返回该规则;否则视为违规 diff --git a/skills/mono/references/products/sheet/sheet-dimension-operations.md b/skills/mono/references/products/sheet/sheet-dimension-operations.md new file mode 100644 index 00000000..589c157e --- /dev/null +++ b/skills/mono/references/products/sheet/sheet-dimension-operations.md @@ -0,0 +1,275 @@ +# 行列操作 (dimension operations) + +## 使用场景 + +### 行列操作 + +用户说"插入行/插入列/在某行前插入/在某列前插入": +- 插入行或列 → `insert-dimension` +- 在末尾追加 → `append`(insert-dimension 不支持末尾追加) + +用户说"删除行/删除列/删掉第几行/删掉某列/移除行/移除列": +- 删除行或列 → `delete-dimension` +- 仅清空内容但保留行/列 → `range clear`(默认清除值保留格式) + +用户说"隐藏行/隐藏列/显示行/显示列/设置行高/设置列宽/调整行高/调整列宽/行列属性": +- 隐藏/显示行或列 → `update-dimension --hidden` / `--hidden=false` +- 设置行高/列宽 → `update-dimension --pixel-size` +- 同时修改尺寸与显隐 → `update-dimension --pixel-size --hidden` + +用户说"移动行/移动列/调整行顺序/调整列顺序/行列拖拽/把第N行移到第M行": +- 移动行或列 → `move-dimension` +- 请勿用 `range read` + `range update` 读取再重写来模拟移动,`move-dimension` 是原子操作,能保留格式和合并状态 + +用户说"追加空行/追加空列/增加行数/增加列数/扩展表格/在末尾加空行": +- 追加空行/空列 → `add-dimension` +- 注意与 `append`(追加数据行)区分:`add-dimension` 追加的是空行/空列,`append` 追加的是带数据的行 +- 请勿用 `range update` 写空数据来模拟追加,`add-dimension` 直接扩展表格维度 + +**结构预检**:插入、删除、移动行列前,必须先执行 `dws sheet info --node --sheet-id --format json` 查看 `mergedRanges`。合并区域跨过操作位置时,行列变更可能导致表头/分组标题断裂、空白或错位;需要先向用户说明影响,必要时取消合并后操作,再按原模式重新 `merge-cells`。 + +## 命令详细参考 + +### 在指定位置插入行或列 +``` +Usage: + dws sheet insert-dimension [flags] +Example: + # 在第 3 行之前插入 2 行 + dws sheet insert-dimension --node --sheet-id --dimension ROWS --position "3" --length 2 + + # 在 A 列之前插入 1 列 + dws sheet insert-dimension --node --sheet-id --dimension COLUMNS --position "A" --length 1 + + # 使用工作表前缀(忽略 --sheet-id) + dws sheet insert-dimension --node --sheet-id --dimension ROWS --position "Sheet1!3" --length 5 + + # 在 AB 列之前插入 3 列 + dws sheet insert-dimension --node --sheet-id --dimension COLUMNS --position "AB" --length 3 +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --dimension string 插入维度: ROWS 或 COLUMNS (必填) + --position string 插入位置,A1 表示法 (必填)。ROWS 时为行号如 "3";COLUMNS 时为列字母如 "A" + --length string 插入数量,正整数 (必填),最大 5000 +``` + +在钉钉表格指定工作表的指定位置之前插入若干空行或空列。 +`--dimension ROWS` 时,`--position` 为 1-based 行号字符串;`--dimension COLUMNS` 时,`--position` 为列字母。 +支持在 `--position` 中携带工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id`。 +若需要在末尾追加行/列,请使用 `append` 命令。 + +### 删除指定位置的行或列 + +> **CAUTION:** 不可逆操作 — 执行前必须向用户确认。 + +``` +Usage: + dws sheet delete-dimension [flags] +Example: + # 从第 3 行开始删除 2 行 + dws sheet delete-dimension --node --sheet-id --dimension ROWS --position "3" --length 2 + + # 从 A 列开始删除 1 列 + dws sheet delete-dimension --node --sheet-id --dimension COLUMNS --position "A" --length 1 + + # 使用工作表前缀(忽略 --sheet-id) + dws sheet delete-dimension --node --sheet-id --dimension ROWS --position "Sheet1!3" --length 5 + + # 从 AB 列开始删除 3 列 + dws sheet delete-dimension --node --sheet-id --dimension COLUMNS --position "AB" --length 3 +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --dimension string 删除维度: ROWS 或 COLUMNS (必填) + --position string 删除起始位置,A1 表示法 (必填)。ROWS 时为行号如 "3";COLUMNS 时为列字母如 "A" + --length string 删除数量,正整数 (必填),最大 5000 +``` + +在钉钉表格指定工作表中,从指定位置起删除若干连续的行或列。 +`--dimension ROWS` 时,`--position` 为 1-based 行号字符串;`--dimension COLUMNS` 时,`--position` 为列字母。 +支持在 `--position` 中携带工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id`。 +删除后后续的行/列会向前移动填补空位;若需要仅清空内容但保留行/列占位,请使用 `range clear`。 + +### 更新指定范围行/列属性 +``` +Usage: + dws sheet update-dimension [flags] +Example: + # 隐藏第 3~4 行 + dws sheet update-dimension --node --sheet-id --dimension ROWS --start-index "3" --length 2 --hidden + + # 显示 A~B 列 + dws sheet update-dimension --node --sheet-id --dimension COLUMNS --start-index "A" --length 2 --hidden=false + + # 设置第 1~5 行行高为 40px + dws sheet update-dimension --node --sheet-id --dimension ROWS --start-index "1" --length 5 --pixel-size 40 + + # 设置 C 列列宽为 200px 并隐藏 + dws sheet update-dimension --node --sheet-id --dimension COLUMNS --start-index "C" --length 1 --pixel-size 200 --hidden + + # 使用工作表前缀(忽略 --sheet-id) + dws sheet update-dimension --node --sheet-id --dimension ROWS --start-index "Sheet1!3" --length 2 --hidden +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --dimension string 更新维度: ROWS 或 COLUMNS (必填) + --start-index string 起始位置,A1 表示法 (必填)。ROWS 时为行号如 "3";COLUMNS 时为列字母如 "A" + --length string 更新数量,正整数 (必填),最大 5000 + --hidden 是否隐藏 (true=隐藏, false=显示),与 --pixel-size 至少填其一 + --pixel-size int 行高或列宽(像素),ROWS 时为行高,COLUMNS 时为列宽,与 --hidden 至少填其一 +``` + +批量更新钉钉表格指定工作表中连续多行/多列的属性,支持设置显隐状态(hidden)与行高/列宽(pixelSize)。 +`--dimension ROWS` 时,`--start-index` 为 1-based 行号字符串;`--dimension COLUMNS` 时,`--start-index` 为列字母。 +支持在 `--start-index` 中携带工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id`。 +`--hidden` 与 `--pixel-size` 至少必须提供一个。当同时提供时,将先应用尺寸再应用显隐,任一失败整体失败。 +`--pixel-size` 单位为像素,`dimension=ROWS` 时表示行高、`dimension=COLUMNS` 时表示列宽。 + +### 移动行或列 +``` +Usage: + dws sheet move-dimension [flags] +Example: + # 将第 2 行移动到第 5 行的位置 + dws sheet move-dimension --node --sheet-id \ + --dimension ROWS --start-index "2" --end-index "2" --destination-index "5" + + # 将第 2~4 行(共 3 行)移动到第 1 行的位置(最前面) + dws sheet move-dimension --node --sheet-id \ + --dimension ROWS --start-index "2" --end-index "4" --destination-index "1" + + # 将 B~C 列(共 2 列)移动到 D 列的位置 + dws sheet move-dimension --node --sheet-id \ + --dimension COLUMNS --start-index "B" --end-index "C" --destination-index "D" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --dimension string 维度类型: ROWS 或 COLUMNS (必填) + --start-index string 源起始位置,A1 表示法 (必填) + --end-index string 源结束位置,A1 表示法 (必填) + --destination-index string 目标位置,A1 表示法 (必填) +``` + +startIndex、endIndex 和 destinationIndex 均使用 A1 表示法:`--dimension ROWS` 时为 1-based 行号(如 "2"),`--dimension COLUMNS` 时为列字母(如 "B")。 +源行/列将移动到 destinationIndex 所指的位置。destinationIndex 不能落在源范围 [startIndex, endIndex] 内。 + +**合并单元格注意**:如果源范围或目标位置涉及合并单元格,操作会报错中断。移动前先通过 `dws sheet info --node --sheet-id --format json` 查询 `mergedRanges`,必要时先用 `unmerge-cells` 取消合并再移动,移动后再用 `merge-cells` 恢复需要保留的合并区域。 + +### 追加空行或空列 +``` +Usage: + dws sheet add-dimension [flags] +Example: + dws sheet add-dimension --node --sheet-id --dimension ROWS --length 5 + dws sheet add-dimension --node --sheet-id --dimension COLUMNS --length 3 +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --dimension string 维度类型: ROWS 或 COLUMNS (必填) + --length int 追加数量,正整数,最多 5000 (必填) +``` + +在工作表末尾追加指定数量的空行或空列。 + +## 核心工作流 + +```bash +# ── 工作流 6: 插入行或列 ── + +# 1. 获取工作表列表 +dws sheet list --node --format json + +# 2. 在第 3 行之前插入 2 行 +dws sheet insert-dimension --node --sheet-id \ + --dimension ROWS --position "3" --length 2 --format json + +# 3. 在 A 列之前插入 1 列 +dws sheet insert-dimension --node --sheet-id \ + --dimension COLUMNS --position "A" --length 1 --format json + +# 4. 使用工作表前缀指定位置 +dws sheet insert-dimension --node --sheet-id \ + --dimension ROWS --position "Sheet1!5" --length 3 --format json +``` + +```bash +# ── 工作流 6b: 删除行或列 ── + +# 1. 获取工作表列表 +dws sheet list --node --format json + +# 2. 从第 3 行开始删除 2 行 +dws sheet delete-dimension --node --sheet-id \ + --dimension ROWS --position "3" --length 2 --format json + +# 3. 从 A 列开始删除 1 列 +dws sheet delete-dimension --node --sheet-id \ + --dimension COLUMNS --position "A" --length 1 --format json + +# 4. 使用工作表前缀指定位置 +dws sheet delete-dimension --node --sheet-id \ + --dimension ROWS --position "Sheet1!5" --length 3 --format json +``` + +```bash +# ── 工作流 6c: 更新行/列属性(显隐、行高/列宽) ── + +# 1. 获取工作表列表 +dws sheet list --node --format json + +# 2. 隐藏第 3~4 行 +dws sheet update-dimension --node --sheet-id \ + --dimension ROWS --start-index "3" --length 2 --hidden --format json + +# 3. 显示 A~B 列 +dws sheet update-dimension --node --sheet-id \ + --dimension COLUMNS --start-index "A" --length 2 --hidden=false --format json + +# 4. 设置第 1~5 行行高为 40px +dws sheet update-dimension --node --sheet-id \ + --dimension ROWS --start-index "1" --length 5 --pixel-size 40 --format json + +# 5. 设置 C 列列宽为 200px 并隐藏 +dws sheet update-dimension --node --sheet-id \ + --dimension COLUMNS --start-index "C" --length 1 --pixel-size 200 --hidden --format json +``` + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `insert-dimension` | `a1Notation` 新插入区域范围 | 确认插入位置和范围 | +| `delete-dimension` | `a1Notation` 被删除区域范围 | 确认删除位置和范围 | +| `update-dimension` | `a1Notation` 被更新区域范围、`hidden` 生效的显隐状态、`pixelSize` 生效的尺寸 | 确认更新结果 | +| `move-dimension` | `sheetId` 工作表 ID | 确认操作完成 | +| `add-dimension` | `sheetId` 工作表 ID | 确认操作完成 | +| `list` | 工作表的 `sheetId` | info / range read / range update / find 的 --sheet-id | + +## 注意事项 + +- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) +- `sheet info` 的 `mergedRanges` 是行列结构操作的重要预检信息。插入列时尤其要检查多行表头合并区,原有合并区域通常不会自动扩展到新列,必要时需重新设置合并区域 +- `insert-dimension` 在指定位置之前插入空行或空列,不写入数据;如需在末尾追加行/列,使用 `append` +- `insert-dimension` 的 `--dimension` 只接受 `ROWS` 或 `COLUMNS` +- `insert-dimension` 的 `--position` 支持工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id` +- `insert-dimension` 的 `--length` 最大为 5000 +- `delete-dimension` 从指定位置起删除若干连续的行或列,删除后后续行/列向前移动填补空位 +- `delete-dimension` 的 `--dimension` 只接受 `ROWS` 或 `COLUMNS` +- `delete-dimension` 的 `--position` 支持工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id` +- `delete-dimension` 的 `--length` 最大为 5000 +- `delete-dimension` 若需仅清空内容但保留行/列占位,请使用 `range clear`(默认清除值保留格式,比手动构造空数组更简洁) +- `update-dimension` 批量更新连续行/列的显隐状态与行高/列宽 +- `update-dimension` 的 `--dimension` 只接受 `ROWS` 或 `COLUMNS` +- `update-dimension` 的 `--start-index` 支持工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id` +- `update-dimension` 的 `--length` 最大为 5000 +- `update-dimension` 的 `--hidden` 与 `--pixel-size` 至少必须提供一个 +- `update-dimension` 的 `--pixel-size` 单位为像素,`dimension=ROWS` 时表示行高、`dimension=COLUMNS` 时表示列宽 +- `update-dimension` 当同时提供 `--hidden` 与 `--pixel-size` 时,将先应用尺寸再应用显隐,任一失败整体失败 +- ★ `move-dimension` vs `range update`:需要移动行或列时,必须使用 `move-dimension` 命令,禁止用 `range update` 读取数据后手动重写来模拟移动效果。`move-dimension` 是原子操作,能保留单元格的格式、合并状态等属性 +- `move-dimension` 的 `--start-index`、`--end-index` 和 `--destination-index` 均使用 A1 表示法(ROWS 时为 1-based 行号,COLUMNS 时为列字母) +- `move-dimension` 的 `--destination-index` 不能落在源范围 [startIndex, endIndex] 内 +- `move-dimension` 的源范围 [startIndex, endIndex] 最大跨度为 5000 +- `add-dimension` vs `range update`:需要在末尾追加空行/空列时,必须使用 `add-dimension` 命令,禁止用 `range update` 写空数据来模拟追加效果 +- `add-dimension` 追加的是空行/空列,与 `append`(追加带数据的行)不同 +- `add-dimension` 的 `--length` 必须为正整数(>= 1),行列均不超过 5000 diff --git a/skills/mono/references/products/sheet/sheet-dropdown.md b/skills/mono/references/products/sheet/sheet-dropdown.md new file mode 100644 index 00000000..0b135aa4 --- /dev/null +++ b/skills/mono/references/products/sheet/sheet-dropdown.md @@ -0,0 +1,94 @@ +# 下拉列表 (dropdown) + +## 使用场景 + +### 下拉列表 + +用户说"设置下拉列表/下拉选项/下拉菜单/添加下拉/配置下拉": +- 设置下拉列表 → `set-dropdown` +- 设置多选下拉 → `set-dropdown --multi-select` + +用户说"查看下拉列表/获取下拉配置/下拉列表有哪些选项": +- 获取下拉列表配置 → `get-dropdown` + +用户说"删除下拉列表/移除下拉/取消下拉/清除下拉": +- 删除下拉列表 → `delete-dropdown` + +## 命令详细参考 + +### 设置下拉列表 +``` +Usage: + dws sheet set-dropdown [flags] +Example: + # 设置单选下拉列表 + dws sheet set-dropdown --node --sheet-id --range "A2:A100" \ + --options '[{"value":"选项1"},{"value":"选项2"},{"value":"选项3"}]' + + # 设置带颜色的多选下拉列表 + dws sheet set-dropdown --node --sheet-id --range "B2:B50" \ + --options '[{"value":"高","color":"#ff0000"},{"value":"中","color":"#ffaa00"},{"value":"低","color":"#00ff00"}]' \ + --multi-select +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 目标单元格范围,A1 表示法,如 A2:A100 (必填) + --options string 下拉选项 JSON 数组 (必填),如 '[{"value":"选项1","color":"#ff0000"}]' + --multi-select 是否允许多选(默认单选) +``` + +在指定单元格范围内设置下拉列表。设置后用户可从预定义选项中选择值。 +- **用途**:为单元格配置下拉列表,支持自定义选项颜色和多选。 +- **场景**:规范数据输入,如状态选择(完成/进行中/待处理)、优先级(高/中/低)等。 +- **注意**:选项值不能包含英文逗号;如果目标范围已存在下拉列表,会被新配置覆盖。 + +### 获取下拉列表配置 +``` +Usage: + dws sheet get-dropdown [flags] +Example: + dws sheet get-dropdown --node --sheet-id --range "A2:A100" + dws sheet get-dropdown --node --sheet-id --range "A1" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 查询范围,A1 表示法,如 A1:A100 (必填) +``` + +查询指定范围内的下拉列表配置信息,包括选项值、颜色和是否多选。 +- **用途**:查看单元格已设置的下拉列表选项和配置。 +- **场景**:在修改下拉列表前先查询现有配置;确认下拉列表是否设置成功。 +- **返回**:`dataValidations` 数组,相同选项的单元格聚合为一组,每组包含 `conditionValues`(选项值)、`ranges`(覆盖范围)、`options`(含 `enableMultiSelect` 和 `colorValueMap`)。范围内无下拉列表时 `hasDropdown` 为 false。 + +### 删除下拉列表 +``` +Usage: + dws sheet delete-dropdown [flags] +Example: + dws sheet delete-dropdown --node --sheet-id --range "A2:A100" + dws sheet delete-dropdown --node --sheet-id --range "B1:D10" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 要删除下拉列表的范围,A1 表示法 (必填) +``` + +删除指定范围内的下拉列表配置,单元格恢复为普通文本格式。 +- **用途**:移除不再需要的下拉列表约束。 +- **注意**:已填写的单元格值不会被清除;目标范围不存在下拉列表时操作仍返回成功。 + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `set-dropdown` | `range` 实际设置范围、`optionCount` 选项数量、`enableMultiSelect` 是否多选 | 确认下拉列表设置成功 | +| `get-dropdown` | `hasDropdown` 是否存在下拉、`dataValidations` 下拉配置列表(含 `conditionValues`、`ranges`、`options`) | 查看已有下拉配置 | +| `delete-dropdown` | `range` 实际删除范围 | 确认下拉列表删除完成 | +| `list` | 工作表的 `sheetId` | info / range read / range update / find 的 --sheet-id | + +## 注意事项 + +- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) +- `set-dropdown` 在指定范围内设置下拉列表,`--options` 为 JSON 数组,每个元素包含 `value`(必填)和 `color`(可选,`#RRGGBB` 格式)。选项值不能包含英文逗号。`--multi-select` 启用多选模式。如果目标范围已存在下拉列表,会被新配置覆盖 +- `get-dropdown` 查询指定范围内的下拉列表配置,返回 `dataValidations` 数组,相同选项的单元格聚合为一组。无下拉列表时 `hasDropdown` 为 false +- `delete-dropdown` 删除指定范围内的下拉列表配置,单元格恢复为普通文本格式。已填写的值不会被清除。目标范围不存在下拉列表时操作仍返回成功 diff --git a/skills/mono/references/products/sheet/sheet-export.md b/skills/mono/references/products/sheet/sheet-export.md new file mode 100644 index 00000000..359b8c16 --- /dev/null +++ b/skills/mono/references/products/sheet/sheet-export.md @@ -0,0 +1,95 @@ +# 导出 (export) + +## 使用场景 + +### 导出 + +用户说"导出/下载xlsx/存为Excel/存成表格文件/把表格变成xlsx/导出表格/下载表格/导出为 excel": +- 导出表格 → `export`(单命令一站式,内部自动完成提交、轮询、可选下载) +- 仅需传 `--node`,可选 `--output` 指定本地文件/目录(不传则返回 downloadUrl) +- 需要落盘到本地 → `dws sheet export --node --output `,命令自动下载 xlsx +- 禁止用 `range read` 全量读取后自行拼接 xlsx 来模拟导出,必须使用 `export` 命令(服务端原子导出,保留格式/合并/公式等属性) +- 禁止在 AI Agent 侧实现轮询或重试,CLI 内部已按渐进式退避策略完成(最多 30 次约 5 分钟) + +## 命令详细参考 + +### 导出表格为 xlsx(异步任务一站式) +``` +Usage: + dws sheet export [flags] # 一站式:提交 → 轮询 → 可选下载 +Example: + # 仅导出,返回 downloadUrl(链接有时效性,请尽快下载) + dws sheet export --node + dws sheet export --node "https://alidocs.dingtalk.com/i/nodes/" + + # 导出并自动下载为本地文件 + dws sheet export --node --output ./report.xlsx + + # --output 为目录时,自动按下载链接中的文件名保存 + dws sheet export --node --output ./ + +Flags: + --node string 表格文档 ID 或 URL (必填) + --output string 本地保存路径(可选,支持文件路径或目录) +``` + +将钉钉在线电子表格导出为 Office xlsx 格式。**单命令一站式**:命令内部自动完成「提交任务 → 渐进式退避轮询 → (可选)下载文件」全流程,AI Agent 无需自行拆分步骤或实现轮询。 + +**内部流程**: +1. 调 `submit_export_job` 获取 `jobId` +2. 按渐进式退避策略轮询 `query_export_job` 直至任务终态或超时 +3. 任务成功后取得 `downloadUrl`;若指定了 `--output`,自动 HTTP GET 下载 xlsx 到本地文件 + +**内置轮询策略(CLI 内实现,无需关心)**: +- 第 1~5 次:每次间隔 2 秒 +- 第 6~10 次:每次间隔 5 秒 +- 第 11~20 次:每次间隔 10 秒 +- 第 21~30 次:每次间隔 15 秒 +- **硬上限:最多轮询 30 次(约 5 分钟)**,超时后命令返回错误 + +**命令返回**: +- `--output` 未指定:进度日志 + 末尾输出 `jobId` 和 `downloadUrl`(链接有时效性,请尽快下载) +- `--output` 指定为文件路径:下载到该路径并输出 `导出完成: ` +- `--output` 指定为已存在目录:自动从 `downloadUrl` 推断文件名并保存到该目录下 + +**失败处理(命令内部已处理,Agent 仅需转述)**: +- MCP 返回 `FAILED`:命令立即返回错误并附带失败原因,**禁止自动重试 `dws sheet export`**,告知用户稍后再试 +- 轮询 30 次仍 `PROCESSING`:命令返回超时错误,告知用户稍后再试 + +**限制**:仅支持钉钉在线电子表格(alxs)→ xlsx。导出钉钉文字文档请使用 `doc` 产品对应的导出工具。 + +## 核心工作流 + +```bash +# ── 工作流 12: 导出表格为 xlsx(单命令一站式)── + +# 场景 A:仅获取下载链接(命令内部自动完成提交+轮询,最终返回 downloadUrl) +dws sheet export --node --format json +# 传入 URL 也可: +# dws sheet export --node "https://alidocs.dingtalk.com/i/nodes/" --format json + +# 场景 B:导出并自动下载为本地文件 +dws sheet export --node --output ./report.xlsx + +# 场景 C:下载到目录,自动按链接推断文件名 +dws sheet export --node --output ./ + +# 禁止在 Agent 侧实现任何轮询或重试,CLI 内部已按 2s/5s/10s/15s 渐进式退避自动完成(最多 30 次)。 +# 若命令返回失败或超时,直接告知用户稍后再试,不要自动重调 dws sheet export。 +``` + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `export` | `downloadUrl`(未指定 --output)/ `导出完成: `(指定 --output) | 直接下发给用户或告知文件已保存到本地。命令内部已完成轮询,不要再调用其他 export 相关命令 | + +## 注意事项 + +- ★ `export` 仅支持钉钉在线电子表格(alxs)→ xlsx;传入钉钉文字文档会报 `invalidRequest.document.typeIllegal` +- ★ `export` 为单命令一站式,CLI 内部已自动完成「提交 → 渐进式退避轮询 → 可选下载」,**Agent 不得在外部实现轮询或重试**;命令返回成功后不再调用其他 export 相关命令 +- `export` 内置轮询策略:1~5 次间隔 2s、6~10 次间隔 5s、11~20 次间隔 10s、21~30 次间隔 15s,硬上限 30 次(约 5 分钟);超时后命令返回错误,告知用户稍后再试即可 +- ★ `export` 命令返回失败或超时时,**禁止自动重调 `dws sheet export`**;直接告知用户导出失败并建议稍后再试 +- `export` 未指定 `--output` 时,返回的 `downloadUrl` 具有时效性,获取后请尽快下载;若用户需要本地文件,优先直接传 `--output` 让 CLI 代为下载 +- `export` 的 `--output` 可为文件路径或已存在目录;为目录时自动从 `downloadUrl` 推断文件名,为文件路径时直接按该路径保存 +- 用户要求"导出表格/下载 xlsx"时,必须使用 `export` 单命令,禁止用 `range read` 读全量数据后自行拼 xlsx 模拟导出(服务端导出会保留格式/合并/公式等完整属性) diff --git a/skills/mono/references/products/sheet/sheet-filter-view.md b/skills/mono/references/products/sheet/sheet-filter-view.md new file mode 100644 index 00000000..631a6c6f --- /dev/null +++ b/skills/mono/references/products/sheet/sheet-filter-view.md @@ -0,0 +1,344 @@ +# 筛选视图 (filter-view) + +## 使用场景 + +### 筛选视图 + +用户说"筛选视图/查看筛选视图/有哪些筛选视图/筛选视图列表": +- 获取所有筛选视图 → `filter-view list` + +用户说"筛选视图详情/查看某个筛选视图/筛选视图信息/筛选视图配置": +- 获取单个筛选视图详情 → `filter-view info` + +用户说"创建筛选视图/新建筛选视图/添加筛选视图": +- 创建筛选视图 → `filter-view create` + +用户说"更新筛选视图/修改筛选视图/改筛选视图名称/改筛选视图范围": +- 更新筛选视图属性 → `filter-view update` + +用户说"删除筛选视图/移除筛选视图": +- 删除筛选视图 → `filter-view delete` + +用户说"设置筛选条件/添加筛选条件/配置筛选视图条件/按值筛选/按条件筛选/按颜色筛选": +- 设置筛选视图列条件 → `filter-view update-criteria` + +用户说"查看筛选条件/有哪些筛选条件/筛选视图设了什么条件/列出筛选条件": +- 列出所有列条件 → `filter-view list-criteria` +- 查看某一列的条件 → `filter-view get-criteria --column N` + +用户说"清除筛选条件/移除筛选条件/取消筛选条件": +- 清除筛选视图列条件 → `filter-view delete-criteria` +- 注意与 `filter-view delete`(删除整个筛选视图)区分:`delete-criteria` 仅清除指定列的条件,不删除筛选视图本身 + +## 命令详细参考 + +### 获取所有筛选视图 +``` +Usage: + dws sheet filter-view list [flags] +Example: + dws sheet filter-view list --node --sheet-id + dws sheet filter-view list --node "https://alidocs.dingtalk.com/i/nodes/" --sheet-id "Sheet1" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) +``` + +获取指定工作表的所有筛选视图列表,返回每个筛选视图的 ID、名称和范围信息。 +- **用途**:查看当前工作表上已创建的所有筛选视图,获取视图 ID、名称和范围。 +- **场景**:在对筛选视图进行 update / delete / update-criteria 等操作前,先用 list 获取可用的 filterViewId。 +- **区分**:筛选视图(filter-view)是个人化的数据过滤方式,与全局筛选不同。每个用户可以创建自己的筛选视图,互不影响原始数据。如果没有筛选视图,返回空列表。 + +### 创建筛选视图 +``` +Usage: + dws sheet filter-view create [flags] +Example: + # 创建不带筛选条件的筛选视图 + dws sheet filter-view create --node --sheet-id --name "我的视图" --range "A1:E10" + + # 创建带按值筛选条件的筛选视图 + dws sheet filter-view create --node --sheet-id --name "销售筛选" --range "A1:E10" \ + --criteria '[{"column":0,"filterType":"values","visibleValues":["销售部"]}]' + + # 创建带按条件筛选的筛选视图(大于等于 200000) + dws sheet filter-view create --node --sheet-id --name "高预算" --range "A1:C10" \ + --criteria '[{"column":1,"filterType":"condition","conditions":[{"operator":"greater-equal","value":"200000"}]}]' +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --name string 筛选视图名称 (必填) + --range string 筛选视图范围,A1 表示法,如 A1:E10 (必填) + --criteria string 筛选条件,JSON 数组 (可选) +``` + +在指定工作表中创建一个筛选视图。 +- **用途**:为指定数据区域创建一个可命名的个人化筛选视图,可选同时设置筛选条件。 +- **场景**:用户需要针对某个数据区域建立固定的筛选视角(如"高绩效员工""研发部数据"),方便反复查看。 +- **区分**:与全局筛选不同,筛选视图是个人化的,不影响其他用户看到的数据。如果只需创建视图不设条件,后续可通过 `update-criteria` 单独设置;如果要一步到位,可通过 `--criteria` 在创建时直接设置。 +`--criteria` 为 JSON 数组,每个元素包含 `column`(列偏移量,从 0 开始)和筛选条件字段。支持三种筛选类型: +- `values`:按值筛选,通过 `visibleValues` 指定允许显示的值列表 +- `condition`:按条件筛选,通过 `conditions` 指定条件列表(最多 2 个),每个条件包含 `operator` 和 `value`。支持的操作符(kebab-case):`equal`、`not-equal`、`contains`、`not-contains`、`starts-with`、`not-starts-with`、`ends-with`、`not-ends-with`、`greater`、`greater-equal`、`less`、`less-equal`。多条件之间通过 `conditionOperator` 指定逻辑关系:`and`(且,默认)或 `or`(或) +- `color`:按颜色筛选,通过 `backgroundColor` 或 `fontColor` 指定颜色值(十六进制,如 `#FF0000`),二选一 + +### 更新筛选视图属性 +``` +Usage: + dws sheet filter-view update [flags] +Example: + # 更新筛选视图名称 + dws sheet filter-view update --node --sheet-id --filter-view-id --name "新名称" + + # 更新筛选视图范围 + dws sheet filter-view update --node --sheet-id --filter-view-id --range "A1:F20" + + # 更新筛选条件 + dws sheet filter-view update --node --sheet-id --filter-view-id \ + --criteria '[{"column":1,"filterType":"condition","conditions":[{"operator":"greater","value":"100"}]}]' +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --filter-view-id string 筛选视图 ID (必填) + --name string 筛选视图新名称 + --range string 筛选视图新范围,A1 表示法 + --criteria string 筛选条件,JSON 数组 +``` + +更新筛选视图的名称、范围和/或筛选条件,`--name`、`--range`、`--criteria` 至少传入一个。 +- **用途**:修改已有筛选视图的名称、数据范围或筛选条件。 +- **场景**:数据区域扩展后需要扩大筛选视图范围,或重命名视图,或通过 `--criteria` 一次性批量更新多列筛选条件。 +- **区分**:`update` 可同时修改名称、范围和条件,适合批量更新;`update-criteria` 只能设置单列条件,适合精确控制某一列的筛选逻辑。`--criteria` 指定列的条件会被替换,未指定的列保持不变。 + +`--criteria` 为 JSON 数组,格式与 `filter-view create` 的 `--criteria` 相同,支持的筛选类型和操作符参见「创建筛选视图」说明。 + +### 删除筛选视图 + +> **CAUTION:** 不可逆操作 — 执行前必须向用户确认。 + +``` +Usage: + dws sheet filter-view delete [flags] +Example: + dws sheet filter-view delete --node --sheet-id --filter-view-id +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --filter-view-id string 筛选视图 ID (必填) +``` + +删除指定的筛选视图。 +- **用途**:永久删除一个不再需要的筛选视图及其所有筛选条件。 +- **场景**:筛选视图已过时或不再需要时,清理无用的视图。 +- **区分**:`delete` 删除整个筛选视图(包括所有列的条件),操作不可恢复;`delete-criteria` 只删除某一列的筛选条件,视图本身保留。此操作不影响全局筛选或其他筛选视图,也不影响原始数据。 + +### 更新筛选视图列条件 +``` +Usage: + dws sheet filter-view update-criteria [flags] +Example: + # 按值筛选:只显示"销售部"和"市场部" + dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ + --column 0 --filter-criteria '{"filterType":"values","visibleValues":["销售部","市场部"]}' + + # 按条件筛选:大于 100 + dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ + --column 2 --filter-criteria '{"filterType":"condition","conditions":[{"operator":"greater","value":"100"}]}' + + # 按条件筛选:大于等于 200000 + dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ + --column 1 --filter-criteria '{"filterType":"condition","conditions":[{"operator":"greater-equal","value":"200000"}]}' + + # 按条件筛选:小于 100 + dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ + --column 1 --filter-criteria '{"filterType":"condition","conditions":[{"operator":"less","value":"100"}]}' + + # 多条件筛选:大于等于 60 且 小于等于 90 + dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ + --column 2 --filter-criteria '{"filterType":"condition","conditionOperator":"and","conditions":[{"operator":"greater-equal","value":"60"},{"operator":"less-equal","value":"90"}]}' + + # 按颜色筛选:背景色为红色 + dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ + --column 1 --filter-criteria '{"filterType":"color","backgroundColor":"#FF0000"}' +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --filter-view-id string 筛选视图 ID (必填) + --column int 列偏移量,从 0 开始 (必填) + --filter-criteria string 筛选条件,JSON 对象 (必填) +``` + +更新筛选视图中某一列的筛选条件。 +- **用途**:为筛选视图的指定列创建或更新筛选条件,控制该列哪些数据行可见。 +- **场景**:只显示某些特定值的行(如"只看研发部")→ `filterType: values`;按数值条件筛选(如"绩效 ≥ 85")→ `filterType: condition` + `operator: greater-equal`;按文本条件筛选(如"名称包含关键字")→ `filterType: condition` + `operator: contains`。 +- **区分**:`update-criteria` 精确控制单列条件,适合逐列设置不同的筛选逻辑;`filter-view update --criteria` 可以批量更新多列条件;`delete-criteria` 是 `update-criteria` 的逆操作,删除指定列的条件。 + +`--column` 为列偏移量(从 0 开始),相对于筛选视图范围首列。 +例如筛选视图范围为 `B1:E10`,则 `--column 0` 代表 B 列,`--column 1` 代表 C 列。 + +`--filter-criteria` 为 JSON 对象,支持三种筛选类型: +- `values`:按值筛选,通过 `visibleValues` 指定允许显示的值列表 +- `condition`:按条件筛选,通过 `conditions` 指定条件列表(最多 2 个),每个条件包含 `operator` 和 `value`。支持的操作符:`equal`、`not-equal`、`contains`、`not-contains`、`starts-with`、`not-starts-with`、`ends-with`、`not-ends-with`、`greater`、`greater-equal`、`less`、`less-equal`。多条件之间通过 `conditionOperator` 指定逻辑关系:`and`(且,默认)或 `or`(或) +- `color`:按颜色筛选,通过 `backgroundColor` 或 `fontColor` 指定颜色值(十六进制,如 `#FF0000`),二选一 + +### 删除筛选视图列条件 +``` +Usage: + dws sheet filter-view delete-criteria [flags] +Example: + # 删除第 1 列(A 列)的筛选条件 + dws sheet filter-view delete-criteria --node --sheet-id --filter-view-id --column 0 + + # 删除第 3 列(C 列)的筛选条件 + dws sheet filter-view delete-criteria --node --sheet-id --filter-view-id --column 2 +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --filter-view-id string 筛选视图 ID (必填) + --column int 列偏移量,从 0 开始 (必填) +``` + +清除筛选视图中指定列的筛选条件。 +- **用途**:移除筛选视图中指定列的筛选条件,使该列不再参与过滤。 +- **场景**:之前通过 `update-criteria` 设置了某列的筛选条件,现在需要取消该列的筛选以显示全部数据。 +- **区分**:`delete-criteria` 只清除指定列的条件,筛选视图本身和其他列的条件保持不变;`delete` 会删除整个筛选视图。如果指定列没有设置筛选条件,调用此命令不会报错(幂等操作)。 + +### 获取单个筛选视图详情 +``` +Usage: + dws sheet filter-view info [flags] +Example: + # 查看指定筛选视图的详情 + dws sheet filter-view info --node --sheet-id --filter-view-id +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --filter-view-id string 筛选视图 ID (必填) +``` + +获取指定筛选视图的完整信息,包括 ID、名称、范围和筛选条件。 +- **用途**:查看某个筛选视图的当前配置,包括已设置的所有筛选条件详情。 +- **场景**:在修改或删除筛选视图前,先确认其当前状态;或在 `update-criteria` 后验证条件是否生效。 +- **区分**:`info` 返回单个视图的完整信息(含 criteria);`list` 返回所有视图的列表概要。`info` 需要指定 `--filter-view-id`,ID 可通过 `list` 获取。 +- **实现**:内部调用 `get_filter_views` 获取全部列表后按 ID 过滤。 + +### 列出筛选视图所有列条件 +``` +Usage: + dws sheet filter-view list-criteria [flags] +Example: + # 列出筛选视图的所有条件 + dws sheet filter-view list-criteria --node --sheet-id --filter-view-id +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --filter-view-id string 筛选视图 ID (必填) +``` + +列出指定筛选视图中已设置的所有列筛选条件。 +- **用途**:查看某个筛选视图当前设置了哪些列的筛选条件,包括每列的条件类型和具体规则。 +- **场景**:在管理筛选条件(修改/删除特定列条件)前,先了解当前视图有哪些条件;或排查筛选结果不符合预期时检查条件配置。 +- **区分**:`list-criteria` 返回所有列的条件(按列偏移量为 key 的对象);`get-criteria` 只返回指定列的条件。如果没有设置任何条件,返回空对象 `{}`。 +- **实现**:内部调用 `get_filter_views` 获取视图详情后提取 `criteria` 字段。 + +### 获取单列筛选条件 +``` +Usage: + dws sheet filter-view get-criteria [flags] +Example: + # 查看第 1 列(偏移量 0)的筛选条件 + dws sheet filter-view get-criteria --node --sheet-id --filter-view-id --column 0 + + # 查看第 3 列(偏移量 2)的筛选条件 + dws sheet filter-view get-criteria --node --sheet-id --filter-view-id --column 2 +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --filter-view-id string 筛选视图 ID (必填) + --column int 列偏移量,从 0 开始 (必填) +``` + +获取指定筛选视图中某一列的筛选条件详情。 +- **用途**:查看某个筛选视图中指定列当前设置的筛选条件,包括条件类型、运算符和比较值。 +- **场景**:在修改某列条件前,先查看其当前配置;或验证 `update-criteria` 后该列条件是否正确。 +- **区分**:`get-criteria` 只返回指定列的条件;`list-criteria` 返回所有列的条件。`--column` 为列偏移量(从 0 开始),相对于筛选视图范围首列。 +- **实现**:内部调用 `get_filter_views` 获取视图详情后按列偏移量过滤 `criteria` 中的对应条件。 + +## 核心工作流 + +```bash +# ── 工作流 11: 筛选视图管理 ── + +# 1. 获取工作表列表 +dws sheet list --node -f json + +# 2. 查看已有筛选视图 +dws sheet filter-view list --node --sheet-id -f json + +# 3. 创建筛选视图(不带条件) +dws sheet filter-view create --node --sheet-id \ + --name "我的筛选" --range "A1:E100" -f json + +# 4. 为筛选视图设置列条件(按值筛选) +dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ + --column 0 --filter-criteria '{"filterType":"values","visibleValues":["销售部","市场部"]}' -f json + +# 5. 为筛选视图设置列条件(按条件筛选) +dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ + --column 2 --filter-criteria '{"filterType":"condition","conditions":[{"operator":"greater","value":"100"}]}' -f json + +# 6. 更新筛选视图名称和范围 +dws sheet filter-view update --node --sheet-id --filter-view-id \ + --name "销售数据筛选" --range "A1:F200" -f json + +# 7. 清除某列的筛选条件 +dws sheet filter-view delete-criteria --node --sheet-id --filter-view-id \ + --column 0 -f json + +# 8. 删除筛选视图 +dws sheet filter-view delete --node --sheet-id --filter-view-id -f json +``` + +```bash +# ── 工作流 11b: 创建带条件的筛选视图(一步完成) ── + +# 创建筛选视图时直接指定筛选条件 +dws sheet filter-view create --node --sheet-id \ + --name "高销售额视图" --range "A1:E100" \ + --criteria '[{"column":0,"filterType":"values","visibleValues":["销售部"]},{"column":2,"filterType":"condition","conditions":[{"operator":"greater","value":"50000"}]}]' \ + -f json +``` + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `filter-view list` | `filterViews` 筛选视图列表(含 `id`、`name`、`range`) | 获取 filterViewId 用于 info / update / delete / update-criteria / delete-criteria / list-criteria / get-criteria | +| `filter-view info` | `id`、`name`、`range`、`criteria` | 查看单个视图完整配置,确认条件是否生效 | +| `filter-view create` | `id` 筛选视图 ID、`name`、`range` | 用于后续 update / delete / update-criteria / delete-criteria 的 --filter-view-id | +| `filter-view update` | `id`、`name`、`range`、`criteria` | 确认更新结果 | +| `filter-view delete` | `id` 被删除的筛选视图 ID | 确认删除完成 | +| `filter-view update-criteria` | `id` 筛选视图 ID | 确认条件设置完成 | +| `filter-view delete-criteria` | `id` 筛选视图 ID | 确认条件清除完成 | +| `filter-view list-criteria` | 所有列条件(按列偏移量为 key 的对象) | 了解当前视图已设置哪些列的条件 | +| `filter-view get-criteria` | 指定列的条件详情(`filterType`、`conditions` 等) | 查看某列的具体筛选规则 | +| `list` | 工作表的 `sheetId` | info / range read / range update / find 的 --sheet-id | + +## 注意事项 + +- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) +- ★ **全局筛选(filter)与筛选视图(filter-view)的区别**:全局筛选影响所有协作者看到的数据展示,每个工作表最多一个;筛选视图是个人化的,互不影响。用户只说"筛选"时默认走 `filter` 系列 +- `filter-view list` 获取指定工作表的所有筛选视图列表,返回的 `id` 可用于后续 info / update / delete / update-criteria / delete-criteria / list-criteria / get-criteria 的 `--filter-view-id` +- `filter-view info` 获取单个筛选视图的完整信息(含 criteria),内部复用 `get_filter_views` MCP 按 ID 过滤 +- `filter-view list-criteria` 列出指定筛选视图已设置的所有列条件,返回按列偏移量为 key 的对象;无条件时返回空对象 `{}` +- `filter-view get-criteria` 获取指定列的条件详情,`--column` 为列偏移量(从 0 开始);该列无条件时返回错误提示 +- `filter-view create` 创建筛选视图时 `--range` 应包含表头行。`--criteria` 可选,不传则创建后无筛选条件,后续可通过 `filter-view update-criteria` 设置 +- `filter-view update` 的 `--name`、`--range`、`--criteria` 至少需要传入一个,未指定的字段保持不变 +- `filter-view update` 的 `--criteria` 中指定列的条件会被替换,未指定的列保持不变 +- `filter-view delete` 删除后该视图及其所有筛选条件将被永久移除,不可恢复 +- `filter-view delete` 不影响全局筛选或其他筛选视图 +- `filter-view update-criteria` 的 `--column` 为列偏移量(从 0 开始),相对于筛选视图范围首列。例如筛选视图范围为 `B1:E10`,则 `--column 0` 代表 B 列 +- `filter-view update-criteria` 设置条件后立即在该筛选视图中生效,仅影响当前视图,不影响全局筛选或其他筛选视图 +- `filter-view update-criteria` 的 `--filter-criteria` 中 `conditions` 最多 2 个条件,多条件之间通过 `conditionOperator` 指定逻辑关系(`and` 或 `or`) +- `filter-view delete-criteria` 仅清除指定列的条件,不会删除整个筛选视图。如需删除整个筛选视图,请使用 `filter-view delete` +- `filter-view delete-criteria` 如果指定列没有设置筛选条件,调用不会报错 +- 筛选视图相关操作需要"可阅读"权限(list / info / list-criteria / get-criteria)或"可编辑"权限(create / update / delete / update-criteria / delete-criteria),不支持跨组织操作 diff --git a/skills/mono/references/products/sheet/sheet-filter.md b/skills/mono/references/products/sheet/sheet-filter.md new file mode 100644 index 00000000..0e65607b --- /dev/null +++ b/skills/mono/references/products/sheet/sheet-filter.md @@ -0,0 +1,181 @@ +# 全局筛选 (filter) + +## 使用场景 + +### 筛选视图 + +用户说"筛选/过滤/只看某些值/只显示满足条件的行/筛选数据/创建筛选/删除筛选/设置筛选条件/清除筛选/排序": +- 查看当前筛选 → `filter get` +- 创建筛选 → `filter create` +- 删除筛选 → `filter delete` +- 批量设置多列条件 → `filter update` +- 清除某一列条件 → `filter clear-criteria` +- 按列排序 → `filter sort` +- **区分全局筛选与筛选视图**:如果用户说"筛选视图"则走 `filter-view` 系列;如果只说"筛选/过滤/只看"则默认走全局 `filter` 系列 +- **禁止替代方案**:当用户要求"筛选/只看/仅保留某些行"时,必须通过 `filter create` / `filter update` 创建真实的筛选器。禁止用"删除不符合条件的行"或"新建工作表只放符合条件的行"来代替——这些做法会让原数据丢失或不可恢复 + +## 命令详细参考 + +### 获取筛选信息 +``` +Usage: + dws sheet filter get [flags] +Example: + dws sheet filter get --node --sheet-id + dws sheet filter get --node "https://alidocs.dingtalk.com/i/nodes/" --sheet-id "Sheet1" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) +``` + +获取指定工作表的全局筛选信息,返回筛选范围和各列的筛选条件详情。 +- **用途**:查看当前工作表上是否存在全局筛选及其配置。 +- **场景**:在修改或删除筛选前,先读取当前筛选配置;创建筛选前先确认是否已存在(每个工作表只能有一个筛选)。 +- **区分**:全局筛选(filter)影响所有协作者看到的数据展示;筛选视图(filter-view)是个人化的。 +- **返回**:`range`(筛选范围,A1 表示法)和 `columnFilterCriteria`(各列条件,key 为列偏移量)。如果未设置筛选,返回筛选信息为空。 + +### 创建筛选 +``` +Usage: + dws sheet filter create [flags] +Example: + # 创建筛选框架(不设条件) + dws sheet filter create --node --sheet-id --range "A1:E100" + + # 创建筛选并同时设置条件(按值筛选) + dws sheet filter create --node --sheet-id --range "A1:E100" --criteria '[{"column":1,"filterType":"values","visibleValues":["北京","上海"]}]' + + # 创建筛选并设置条件筛选 + dws sheet filter create --node --sheet-id --range "A1:E100" --criteria '[{"column":2,"filterType":"condition","conditions":[{"operator":"greater","value":"100"}]}]' +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 筛选范围,A1 表示法,须包含表头行 (必填) + --criteria string 筛选条件 JSON 数组 (可选) +``` + +在工作表中创建全局筛选。 +- **用途**:为工作表建立筛选器,使数据可按条件过滤展示。 +- **约束**:每个工作表只能有一个全局筛选,已存在时会报错。应先 `filter get` 确认不存在后再创建。 +- **range 规范**:必须包含表头行(如 `A1:E100`),不能只包含数据行。 +- **criteria 格式**:JSON 数组,每个元素含 `column`(列偏移量,从 0 开始)和筛选条件字段。不传则仅创建空筛选框架,后续可通过 `filter update` 设置条件。 + +### 删除筛选 + +> **CAUTION:** 不可逆操作 — 执行前必须向用户确认。 + +``` +Usage: + dws sheet filter delete [flags] +Example: + dws sheet filter delete --node --sheet-id --yes +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) +``` + +删除工作表的全局筛选。 +- **用途**:移除筛选器,所有被隐藏的行将重新显示。 +- **不可逆**:删除后所有筛选条件丢失,需重新创建。 +- **前置**:工作表没有筛选时调用会报错,应先 `filter get` 确认存在。 + +### 批量更新筛选条件 +``` +Usage: + dws sheet filter update [flags] +Example: + # 同时设置多列的筛选条件 + dws sheet filter update --node --sheet-id --criteria '[{"column":0,"filterType":"values","visibleValues":["已完成","进行中"]},{"column":2,"filterType":"condition","conditions":[{"operator":"greater","value":"50"}]}]' + + # 按颜色筛选 + dws sheet filter update --node --sheet-id --criteria '[{"column":1,"filterType":"color","backgroundColor":"#FF0000"}]' +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --criteria string 筛选条件 JSON 数组 (必填) +``` + +批量更新筛选条件,可同时设置多列的筛选条件。 +- **用途**:一次性设置或替换多列的筛选条件。 +- **前置**:工作表必须已创建筛选(通过 `filter create`)。 +- **覆盖式**:指定列的条件会被替换,未指定的列保持不变。如只想修改某一列,建议先 `filter get` 读取现有配置。 +- **criteria 格式**:JSON 数组,支持三种 `filterType`: + - `values`:按值筛选,指定 `visibleValues` 数组 + - `condition`:按条件筛选,指定 `conditions` 数组(最多 2 个)和可选的 `conditionOperator`(`and`/`or`) + - `color`:按颜色筛选,指定 `backgroundColor` 或 `fontColor`(二选一) + +### 清除单列筛选条件 +``` +Usage: + dws sheet filter clear-criteria [flags] +Example: + # 清除第 2 列(B 列)的筛选条件 + dws sheet filter clear-criteria --node --sheet-id --column 1 + + # 清除第 1 列(A 列)的筛选条件 + dws sheet filter clear-criteria --node --sheet-id --column 0 +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --column number 列偏移量,从 0 开始 (必填) +``` + +清除筛选中某一列的筛选条件。 +- **用途**:移除某列的筛选条件,该列不再参与筛选计算。 +- **区分**:仅清除指定列的条件,不删除整个筛选。如需删除整个筛选,使用 `filter delete`。 +- **幂等**:指定列没有设置筛选条件时调用不会报错。 + +### 筛选排序 +``` +Usage: + dws sheet filter sort [flags] +Example: + # 按第 1 列(A 列)升序排序 + dws sheet filter sort --node --sheet-id --column 0 --ascending + + # 按第 3 列(C 列)降序排序 + dws sheet filter sort --node --sheet-id --column 2 --ascending=false +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --column number 排序列偏移量,从 0 开始 (必填) + --ascending 是否升序,默认 true (可选) +``` + +对筛选范围内的数据按指定列排序。 +- **用途**:对数据行按某一列的值进行升序或降序排列。 +- **前置**:工作表必须已创建筛选(通过 `filter create`)。 +- **注意**:排序会实际改变工作表中数据行的物理顺序,不可撤销。 +- **column**:列偏移量从 0 开始,相对于筛选范围首列。 + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `filter get` | `range`(筛选范围)、`columnFilterCriteria`(各列条件) | 查看当前筛选配置,确认筛选是否存在 | +| `filter create` | 筛选创建成功的确认 | 确认筛选已建立,后续可通过 `filter update` 设置条件 | +| `filter delete` | 删除成功的确认 | 确认筛选已删除 | +| `filter update` | 更新成功的确认 | 确认条件已设置 | +| `filter clear-criteria` | 清除成功的确认 | 确认指定列的条件已清除 | +| `filter sort` | 排序成功的确认 | 确认排序已完成 | +| `list` | 工作表的 `sheetId` | info / range read / range update / find 的 --sheet-id | + +## 注意事项 + +- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) +- ★ **全局筛选(filter)与筛选视图(filter-view)的区别**:全局筛选影响所有协作者看到的数据展示,每个工作表最多一个;筛选视图是个人化的,互不影响。用户只说"筛选"时默认走 `filter` 系列 +- `filter get` 获取工作表的全局筛选信息,返回 `range`(筛选范围)和 `columnFilterCriteria`(各列条件)。无筛选时返回空 +- `filter create` 创建全局筛选时 `--range` 必须包含表头行(如 `A1:E100`),不能只包含数据行。每个工作表只能有一个筛选,已存在时报错 +- `filter create` 的 `--criteria` 可选,不传则仅创建空筛选框架,后续通过 `filter update` 设置条件 +- `filter delete` 删除后所有筛选条件丢失且所有被隐藏行重新显示,不可恢复 +- `filter delete` 工作表没有筛选时调用会报错,应先 `filter get` 确认存在 +- `filter update` 是覆盖式:指定列的条件会被替换,未指定的列保持不变。如只想修改某一列,建议先 `filter get` 读取现有配置再 patch +- `filter update` 前置:工作表必须已创建筛选 +- `filter clear-criteria` 仅清除指定列的条件,不删除整个筛选。指定列无条件时不报错(幂等) +- `filter sort` 会实际改变数据行的物理顺序,不可撤销。前置:工作表必须已创建筛选 +- ★ **筛选操作规范**(参照飞书 core-operations): + - 当用户要求"筛选/只看/仅保留 X"时,**必须**通过 `filter create` / `filter update` 创建真实的筛选器。**禁止**用"删除不符合条件的行"或"新建工作表只放符合条件的行"来代替 + - 创建/更新筛选后**必须** `filter get` 回读验证配置正确 + - 更新已有筛选前先 `filter get` 读取当前配置,确认目标存在且了解现有条件后再操作 + - 筛选条件的列索引(`column`)必须与实际数据列精确对应,不要凭猜测填写 + - 筛选不支持正则表达式,传入正则会当成普通文本处理 diff --git a/skills/mono/references/products/sheet/sheet-media-image.md b/skills/mono/references/products/sheet/sheet-media-image.md new file mode 100644 index 00000000..40cf3640 --- /dev/null +++ b/skills/mono/references/products/sheet/sheet-media-image.md @@ -0,0 +1,239 @@ +# 媒体上传与图片 (media & image) + +## 使用场景 + +### 媒体上传 + +用户说"上传附件/传文件到表格/上传文件到表格/上传到表格": +- 上传附件 → `media-upload`(需表格 ID 或 URL + 本地文件路径) +- 用户指定了上传后的名称 → `media-upload --name "自定义名称"` +- `media-upload` 的 `--name` 参数用于指定附件在表格中显示的名称(不改变本地文件名);不传时默认使用本地文件名 + +用户说"写入图片/插入图片/加图片/放图片到单元格/嵌入图片到表格": +- 写入图片 → `write-image`(需表格 ID + 工作表 ID + 单元格范围 + 本地图片路径) +- 禁止使用 `range update` 写入图片,因为 `update_range` 的 MCP 工具不支持图片类型参数,调用必定失败。必须使用 `write-image` 命令 +- 用户指定了图片尺寸 → `write-image --width N --height M` + +### 浮动图片 + +用户说"浮动图片/悬浮图片/在表格上放一张图/加个浮动的图": +- 创建浮动图片 → 先 `media-upload` 上传图片获取 `resourceUrl`,再 `create-float-image` +- 浮动图片悬浮于单元格之上,不占用单元格内容,与 `write-image`(写入单元格内部的图片)不同 + +用户说"查看浮动图片/有哪些浮动图片/浮动图片列表": +- 列出所有浮动图片 → `list-float-images` +- 查看某个浮动图片详情 → `get-float-image` + +用户说"移动浮动图片/调整浮动图片大小/修改浮动图片/更新浮动图片": +- 更新浮动图片属性 → `update-float-image`(可更新锚点位置、尺寸、偏移量、图片资源路径) + +用户说"删除浮动图片/移除浮动图片": +- 删除浮动图片 → `delete-float-image` + +关键区分:`write-image`(单元格内嵌图片,占据单元格内容)vs `create-float-image`(浮动图片,悬浮于单元格之上,不占内容) + +## 命令详细参考 + +### 上传附件到表格 +``` +Usage: + dws sheet media-upload [flags] +Example: + dws sheet media-upload --node --file ./report.pdf + dws sheet media-upload --node --file ./data.bin --name "数据文件.dat" --mime-type application/octet-stream +Flags: + --node string 目标表格文档的标识,支持传入 URL 或 ID (必填) + --file string 本地文件路径 (必填) + --name string 附件显示名称 (默认使用文件名) + --mime-type string 文件 MIME 类型 (默认根据扩展名推断) +``` + +### 上传图片并写入表格单元格 +``` +Usage: + dws sheet write-image [flags] +Example: + dws sheet write-image --node --sheet-id --range A1:A1 --file ./chart.png + dws sheet write-image --node --sheet-id --range B2:B2 --file ./logo.png --width 200 --height 100 +Flags: + --node string 目标表格文档的标识,支持传入 URL 或 ID (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 目标单元格区域地址,如 A1:A1 (必填) + --file string 本地图片文件路径 (必填) + --name string 图片显示名称 (默认使用文件名) + --mime-type string 文件 MIME 类型 (默认根据扩展名推断) + --width int 图片显示宽度 (可选) + --height int 图片显示高度 (可选) +``` + +### 创建浮动图片 +``` +Usage: + dws sheet create-float-image [flags] +Example: + # 先上传图片获取 resourceUrl + dws sheet media-upload --node --file ./chart.png + # 输出: resourceUrl: /core/api/resources/img/xxxx... + + # 再创建浮动图片 + dws sheet create-float-image --node --sheet-id \ + --src "/core/api/resources/img/xxxx..." --range A1 --width 400 --height 300 + + # 带偏移量 + dws sheet create-float-image --node --sheet-id \ + --src "/core/api/resources/img/xxxx..." --range B2 --width 200 --height 150 --offset-x 10 --offset-y 20 +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --src string 图片资源路径,通过 media-upload 获取的 resourceUrl (必填) + --range string 锚点单元格,A1 表示法,如 A1、B3 (必填) + --width int 图片宽度,像素,正整数 (必填) + --height int 图片高度,像素,正整数 (必填) + --offset-x int 水平偏移量,像素 (默认 0) + --offset-y int 垂直偏移量,像素 (默认 0) +``` + +浮动图片悬浮于单元格之上,不占用单元格内容,可自由定位和调整大小。 +- `--src` 必须是 `media-upload` 返回的 `resourceUrl`(格式为 `/core/api/resources/img/...`),不能直接传外部 URL +- `--range` 使用 A1 表示法指定锚点单元格(如 `A1`、`B3`),支持带工作表前缀(如 `Sheet1!A1`) +- `--width` / `--height` 为必填,单位像素,必须为正整数 +- `--offset-x` / `--offset-y` 表示相对锚点单元格左上角的偏移量(像素),默认 0,不能为负数 + +### 获取浮动图片详情 +``` +Usage: + dws sheet get-float-image [flags] +Example: + dws sheet get-float-image --node --sheet-id --float-image-id +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --float-image-id string 浮动图片 ID (必填) +``` + +获取单个浮动图片的详细信息,包括 ID、图片资源路径、锚点位置、尺寸和偏移量。 +`--float-image-id` 可通过 `list-float-images` 获取。 + +### 列出工作表所有浮动图片 +``` +Usage: + dws sheet list-float-images [flags] +Example: + dws sheet list-float-images --node --sheet-id +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) +``` + +列出指定工作表中所有浮动图片,返回 `floatImages` 数组和 `totalCount`。 + +### 更新浮动图片属性 +``` +Usage: + dws sheet update-float-image [flags] +Example: + # 移动浮动图片到新位置 + dws sheet update-float-image --node --sheet-id --float-image-id --range C5 + + # 调整尺寸 + dws sheet update-float-image --node --sheet-id --float-image-id --width 600 --height 400 + + # 替换图片(需先 media-upload 新图片获取 resourceUrl) + dws sheet update-float-image --node --sheet-id --float-image-id \ + --src "/core/api/resources/img/xxxx..." +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --float-image-id string 浮动图片 ID (必填) + --src string 新的图片资源路径,通过 media-upload 获取的 resourceUrl + --range string 新的锚点单元格,A1 表示法 + --width int 新的图片宽度,像素 + --height int 新的图片高度,像素 + --offset-x int 新的水平偏移量,像素 + --offset-y int 新的垂直偏移量,像素 +``` + +更新浮动图片的属性,`--src` / `--range` / `--width` / `--height` / `--offset-x` / `--offset-y` 至少传入一个。 +`--float-image-id` 可通过 `list-float-images` 获取。 + +### 删除浮动图片 +``` +Usage: + dws sheet delete-float-image [flags] +Example: + dws sheet delete-float-image --node --sheet-id --float-image-id +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --float-image-id string 浮动图片 ID (必填) +``` + +删除指定的浮动图片,操作不可恢复。`--float-image-id` 可通过 `list-float-images` 获取。 + +## 核心工作流 + +```bash +# ── 工作流 9: 上传附件到表格 ── + +# 1. 基本用法: 上传本地文件到表格 +dws sheet media-upload --node --file ./report.pdf -f json + +# 2. 自定义附件显示名称 (--name 指定上传后在表格中显示的名称) +dws sheet media-upload --node --file ./data.csv --name "销售数据.csv" -f json + +# 3. 指定 MIME 类型 (文件扩展名无法推断时) +dws sheet media-upload --node --file ./data.bin --name "导出数据.dat" --mime-type application/octet-stream -f json + +# 4. 完整流程: 创建表格 → 上传附件 +dws sheet create --name "项目资料" -f json +# 提取 nodeId 后: +dws sheet media-upload --node --file ./design.pdf -f json +dws sheet media-upload --node --file ./timeline.xlsx --name "项目时间线.xlsx" -f json + +# ── 工作流 10: 写入图片到表格单元格 ── + +# 1. 基本用法: 写入图片到指定单元格 +dws sheet write-image --node --sheet-id --range A1:A1 --file ./chart.png -f json + +# 2. 指定显示尺寸 +dws sheet write-image --node --sheet-id --range B2:B2 --file ./logo.png --width 200 --height 100 -f json + +# 3. 自定义图片名称 +dws sheet write-image --node --sheet-id --range C3:C3 --file ./photo.jpg --name "产品图.jpg" -f json + +# 4. 完整流程: 创建表格 → 写表头 → 写入图片 +dws sheet create --name "产品目录" -f json +# 提取 nodeId 后: +dws sheet range update --node --sheet-id Sheet1 --range "A1:B1" --values '[["产品名称","产品图片"]]' -f json +dws sheet range update --node --sheet-id Sheet1 --range "A2:A2" --values '[["MacBook Pro"]]' -f json +dws sheet write-image --node --sheet-id Sheet1 --range B2:B2 --file ./macbook.png --width 150 --height 100 -f json +``` + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `media-upload` | `resourceId`、`resourceUrl` | 附件已上传到表格;`resourceUrl` 可用于 `create-float-image` 的 `--src` | +| `write-image` | `resourceId` | 图片已写入指定单元格 | +| `create-float-image` | `floatImage`(含 `id`、`src`、`range`、`width`、`height`、`offsetX`、`offsetY`) | `id` 用于后续 get / update / delete 的 `--float-image-id` | +| `get-float-image` | `floatImage`(完整信息) | 查看单个浮动图片详情 | +| `list-float-images` | `floatImages` 数组、`totalCount` | 获取所有浮动图片的 `id`,用于后续操作 | +| `update-float-image` | `floatImage`(更新后的完整信息) | 确认更新结果 | +| `delete-float-image` | `message` | 确认删除完成 | +| `list` | 工作表的 `sheetId` | info / range read / range update / find 的 --sheet-id | + +## 注意事项 + +- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) +- `media-upload` 是两步自动完成的流程 (获取附件上传凭证 → OSS 上传),无需手动分步操作 +- `write-image` 是三步自动完成的流程 (获取附件上传凭证 → OSS 上传 → 写入图片到单元格),无需手动分步操作 +- ★ 向表格单元格中写入图片必须使用 `write-image`,禁止使用 `range update`。`range update` 底层调用的 `update_range` MCP 工具不支持图片类型参数,调用会失败 +- `write-image` 与 `media-upload` 的区别:`media-upload` 仅上传附件到表格获取 resourceId;`write-image` 在上传后还会将图片写入指定单元格 +- `create-float-image` 创建浮动图片前必须先通过 `media-upload` 上传图片获取 `resourceUrl`,再将其作为 `--src` 传入。`--src` 的格式为 `/core/api/resources/img/...`,不能直接传外部 URL +- `create-float-image` 的 `--range` 使用 A1 表示法指定锚点单元格(如 `A1`、`B3`),支持带工作表前缀(如 `Sheet1!A1`) +- `create-float-image` 的 `--width` / `--height` 为必填,单位像素,必须为正整数;`--offset-x` / `--offset-y` 可选,默认 0,不能为负数 +- `write-image`(单元格内嵌图片)vs `create-float-image`(浮动图片):`write-image` 将图片写入单元格内部,占据单元格内容;`create-float-image` 创建悬浮于单元格之上的浮动图片,不占用单元格内容,可自由调整位置和大小 +- ★ **浮动图片用 `create-float-image` 不用 `write-image`**:两者用途不同——`write-image` 写入单元格内部,`create-float-image` 创建悬浮于单元格之上的浮动图片;`--src` 必须来自 `media-upload` 的 `resourceUrl` +- `update-float-image` 的 `--src` / `--range` / `--width` / `--height` / `--offset-x` / `--offset-y` 至少必须提供一个 +- `list-float-images` 返回 `floatImages` 数组和 `totalCount`,每个元素包含 `id`(用于后续 get / update / delete) +- `delete-float-image` 操作不可恢复,删除后图片将从工作表中移除 diff --git a/skills/mono/references/products/sheet/sheet-range-operations.md b/skills/mono/references/products/sheet/sheet-range-operations.md new file mode 100644 index 00000000..36d92a03 --- /dev/null +++ b/skills/mono/references/products/sheet/sheet-range-operations.md @@ -0,0 +1,153 @@ +# 区域操作 + +## 使用场景 + +用户说"清空/清除区域/擦除内容/清除格式": +- 清除区域 → `range clear` +- 仅清除值 → `range clear --type content`(默认) +- 仅清除格式 → `range clear --type format` +- 全部清除 → `range clear --type all` +- 请勿用 `range update` 写入空字符串来模拟清空,`range clear` 更简洁且支持按类型清除 + +用户说"排序/给数据排序/按某列排序/升序/降序": +- 区域排序 → `range sort` +- **排序前必须先 `range read` 前 3-5 行**:读取排序范围的前几行(如范围是 A1:D100 则读 A1:D5),对比首行与后续行的模式来判断是否有表头: + - 首行全文本 + 后续行含数字/日期 → 有表头,加 `--has-header` + - 首行与后续行模式一致(都是数字或都是文本) → 无表头,不加 + - 首行值语义像列标题(如"姓名""金额""日期")且与后续行明显不同 → 有表头 + 禁止不读就排——表头误排入数据是不可撤销的破坏性操作 +- 请勿用 `range read` 读取数据后客户端排序再 `range update` 写回,`range sort` 是服务端原子操作 + +用户说"自动填充/填充序列/向下填充/拖拽填充/序列递增": +- 自动填充 → `range fill` +- 请勿用 `range read` 读取源数据后手动计算规律再 `range update` 写入,`range fill` 支持服务端智能填充 + +用户说"复制区域/把这块数据复制到/复制到另一个工作表": +- 复制区域 → `range copy-to` +- 跨工作表 → `range copy-to --target-sheet-id Sheet2` 或 `--target-range "Sheet2!A1"` +- 请勿用 `range read` + `range update` 读取再写入来模拟复制,`range copy-to` 是原子操作,保留公式引用调整 + +用户说"移动区域/把数据移到/剪切粘贴/移到另一个工作表": +- 移动区域 → `range move-to` +- 跨工作表 → `range move-to --target-sheet-id Sheet2` 或 `--target-range "Sheet2!A1"` +- 请勿用 `range read` + `range update` + `range clear` 读取-写入-清空来模拟移动,`range move-to` 是原子操作 + +## 命令详细参考 + +### 清除区域 +``` +Usage: + dws sheet range clear [flags] +Example: + dws sheet range clear --node --sheet-id --range "A1:B3" + dws sheet range clear --node --sheet-id --range "A1:B3" --type format + dws sheet range clear --node --sheet-id --range "A1:B3" --type all +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 清除范围,A1 表示法 (必填) + --type string 清除类型: content(仅值,默认) / format(仅格式) / all(全部) +``` + +### 区域排序 +``` +Usage: + dws sheet range sort [flags] +Example: + dws sheet range sort --node --sheet-id --range "A1:D10" \ + --sort-keys '[{"column":"A","ascending":true}]' + dws sheet range sort --node --sheet-id --range "A1:D10" \ + --sort-keys '[{"column":"A","ascending":true},{"column":"C","ascending":false}]' --has-header +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 排序范围,A1 表示法 (必填) + --sort-keys string 排序规则 JSON 数组 (必填) + --has-header 首行是否为表头(不参与排序) +``` + +`--sort-keys` 格式:`[{"column":"A","ascending":true}]`,`column` 使用字母列名(如 "A"、"B"、"AA")。多级排序按数组顺序优先级递减。 + +### 区域自动填充 +``` +Usage: + dws sheet range fill [flags] +Example: + dws sheet range fill --node --sheet-id \ + --source-range "A1:A5" --target-range "A6:A20" + dws sheet range fill --node --sheet-id \ + --source-range "A1:A5" --target-range "A6:A20" --fill-type copy +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --source-range string 源数据范围,A1 表示法 (必填) + --target-range string 目标填充范围,A1 表示法 (必填) + --fill-type string 填充类型: series(序列,默认) / copy(复制) / onlystyle(仅格式) / withoutstyle(仅值) +``` + +目标范围须与源范围在行或列维度对齐(不支持对角填充)。 + +### 复制区域 +``` +Usage: + dws sheet range copy-to [flags] +Example: + dws sheet range copy-to --node --sheet-id \ + --source-range "A1:C5" --target-range "D1" + dws sheet range copy-to --node --sheet-id \ + --source-range "A1:C5" --target-range "A1" --target-sheet-id "Sheet2" + dws sheet range copy-to --node --sheet-id \ + --source-range "A1:C5" --target-range "D1" --paste-type values +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 源工作表 ID 或名称 (必填) + --source-range string 源范围,A1 表示法 (必填) + --target-range string 目标位置,A1 表示法 (必填) + --target-sheet-id string 目标工作表 ID 或名称(可选,不传则复制到同一工作表) + --paste-type string 粘贴类型: values(仅值) / formulas(仅公式) / formats(仅格式) / all(全部,默认) +``` + +支持跨工作表复制,两种方式指定目标工作表: +- `--target-sheet-id "Sheet2"` 显式指定 +- `--target-range "Sheet2!A1"` 在目标范围中携带工作表前缀 + +源和目标范围不能重叠(同表时)。 + +### 移动区域 +``` +Usage: + dws sheet range move-to [flags] +Example: + dws sheet range move-to --node --sheet-id \ + --source-range "A1:C5" --target-range "D1" + dws sheet range move-to --node --sheet-id \ + --source-range "A1:C5" --target-range "A1" --target-sheet-id "Sheet2" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 源工作表 ID 或名称 (必填) + --source-range string 源范围,A1 表示法 (必填) + --target-range string 目标位置,A1 表示法 (必填) + --target-sheet-id string 目标工作表 ID 或名称(可选,不传则移动到同一工作表) +``` + +支持跨工作表移动,两种方式指定目标工作表: +- `--target-sheet-id "Sheet2"` 显式指定 +- `--target-range "Sheet2!A1"` 在目标范围中携带工作表前缀 + +源和目标范围不能重叠(同表时)。移动后源区域将被清空。 + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `list` | 工作表的 `sheetId` | range clear / range sort / range fill / range copy-to / range move-to 的 --sheet-id | + +## 注意事项 + +- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询真实的 `sheetId` / 工作表名称后再调用,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等);用户仅给出工作表名称时,也应通过 `list` 校验该名称是否存在,避免名称大小写或拼写不一致导致失败 +- ★ **清空区域用 `range clear` 不用 `range update`**:`range clear` 支持按类型(值/格式/全部)清除,比手动构造全空数组更简洁可靠 +- ★ **复制区域用 `range copy-to` 不用 `range read` + `range update`**:原子操作,保留公式引用自动调整,支持跨工作表 +- ★ **移动区域用 `range move-to` 不用 `range read` + `range update` + `range clear`**:原子操作,源区域自动清空,支持跨工作表 +- ★ **排序用 `range sort` 不用 `range read` + 客户端排序 + `range update`**:服务端原子操作,支持多级排序 +- ★ **排序前必须 `range read` 前几行判断表头**:读取排序范围前 3-5 行,对比首行与后续行的数据模式(类型、语义)来判断是否有表头。禁止不读就排,表头被排入数据不可撤销 +- ★ **填充用 `range fill` 不用 `range read` + 手动计算 + `range update`**:服务端智能填充,支持序列递增、公式扩展等 diff --git a/skills/mono/references/products/sheet/sheet-read-data.md b/skills/mono/references/products/sheet/sheet-read-data.md new file mode 100644 index 00000000..db0be1bd --- /dev/null +++ b/skills/mono/references/products/sheet/sheet-read-data.md @@ -0,0 +1,204 @@ +# 数据读取 + +## 使用场景 + +用户说"读数据/看表格内容": +- 快速查看纯值数据、批量处理、大表分批读 → `csv-get`(token 消耗低,防爆保护) +- 需要结构化信息(值+样式+数据验证+富文本+单元格级超链接)、查看公式或原始值 → `range read` +- 需要查看合并单元格 / 表头合并结构 → `sheet info`,读取返回的 `mergedRanges`;不要在 `csv-get` 或 `range read` 里找合并信息 + +## 命令选择 + +| 读取目的 | 推荐命令 | 说明 | +|---------|---------|------| +| 快速查看纯值、数据分析、大表分批读取 | `csv-get` | CSV 格式,token 消耗约为 JSON 的 1/3,内置 maxChars 防爆 | +| 查看数据验证配置(下拉/复选框) | `range read` | 返回 per-cell 结构,含 dataValidation | +| 查看单元格样式(背景色/字体/对齐等) | `range read` | 返回 per-cell 结构,含 cellStyles(仅显式设置的样式) | +| 查看单元格级超链接 | `range read` | 返回 per-cell 结构,含 hyperlink;富文本片段链接仍在 richText 内 | +| 查看公式文本 | `range read --value-render-option formula` | value 返回公式 | +| 获取原始值(数字/布尔而非格式化字符串) | `range read --value-render-option raw_value` | value 返回原始类型 | +| 查看合并单元格范围 | `sheet info` | 返回 `mergedRanges`,这是工作表结构信息,不属于单元格值读取 | + +## 命令详细参考 + +### 以 CSV 格式读取工作表数据(推荐) +``` +Usage: + dws sheet csv-get [flags] +Example: + dws sheet csv-get --node + dws sheet csv-get --node --sheet-id --range "A1:D10" + dws sheet csv-get --node --range "A1:Z500" --value-render-option raw_value + dws sheet csv-get --node --range "A1:D10" --max-chars 50000 +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (不传则默认第一个工作表) + --range string 读取范围,A1 表示法 (不传则读取全部非空数据) + --value-render-option string 取值模式: formatted_value(默认) | raw_value | formula + --max-chars int CSV 最大字符数 (默认 200000,超出截断) +``` + +**返回字段说明**: +- `csv` — CSV 文本,每逻辑行前加 `[row=N]` 前缀标注真实表格行号。行号一律从此前缀读取,禁止手算 +- `colIndices` — 列字母映射数组(如 `["A","B","C"]`)。定位列字母用 `colIndices[j]`,禁止手数逗号 +- `rowIndices` — 行号映射数组(如 `[1,2,3]`) +- `hasMore` — 是否因 maxChars 截断。为 true 时需要调整 `--range` 继续分页读取 + +`csv-get` 不返回合并单元格结构。若 CSV 中出现合并区域的非左上角单元格为空,不能据此判断该区域"无内容";需要先用 `dws sheet info --node --sheet-id --format json` 读取 `mergedRanges`,再结合左上角单元格理解合并区域语义。 + +**取值模式说明**: +| 模式 | 返回内容 | 适用场景 | +|------|---------|---------| +| `formatted_value` | 格式化展示值(如 ¥1,000.00、2025-06-01) | 只看数据 | +| `raw_value` | 原始值(如 1000、45808) | 数据处理、计算 | +| `formula` | 公式文本(如 =SUM(A1:A10)),无公式时回退原始值 | 查看/复制公式 | + +**大表分批读取**:当 `hasMore=true` 或数据量很大时,按行窗口分批: +- 先通过 `info` 获取 `lastNonEmptyRow` / `columnCount` 确定边界 +- 分批读取:`--range "A1:J500"`、`--range "A501:J1000"` …… +- 单次建议 ≤5000 单元格 + +### 读取工作表数据(per-cell 结构化信息) +``` +Usage: + dws sheet range read [flags] # 别名: dws sheet range get +Example: + dws sheet range read --node + dws sheet range read --node --sheet-id + dws sheet range read --node --sheet-id "Sheet1" --range "A1:D10" + dws sheet range read --node --range "Sheet1!A1:D10" + dws sheet range read --node --value-render-option raw_value + dws sheet range read --node --value-render-option formula + + # 使用 get 别名,与 read 等价 + dws sheet range get --node --sheet-id --range "A1:D10" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (不传则默认第一个工作表) + --range string 读取范围,A1 表示法 (如 A1:D10,不传则读取全部数据) + --value-render-option string 取值模式: formatted_value(默认) | raw_value | formula +``` + +**返回字段说明**: +- `cells` — 二维数组,第一维为行,第二维为列。每个元素为 per-cell 对象,字段如下: + +| 字段 | 类型 | 是否必有 | 说明 | +|------|------|---------|------| +| `value` | string / number / boolean / null | 始终存在 | 单元格值。`formatted_value` 模式为 string;`raw_value` 模式为原始类型(number/boolean/string/null);`formula` 模式为公式字符串或回退原始值 | +| `dataValidation` | object | 仅有数据验证时出现,无则省略 | 数据验证配置,见下表 | +| `hyperlink` | object | 仅有单元格级超链接时出现,无则省略 | 整格超链接,结构为 `{type, link, text?}`,见下表 | +| `richText` | object | 仅富文本单元格出现 | 富文本结构(含超链接、附件、图片、样式片段等),普通纯文本不含此字段 | +| `cellStyles` | object | 仅有显式设置的样式时出现;MCP 序列化层也可能返回全 null 空壳 | cell-level 样式,见下表。读取时只看非 null 字段;全 null 等同不存在 | + +`range read` / `range get` 不返回合并单元格结构。要看合并单元格,请先或另行调用 `dws sheet info --node --sheet-id --format json`,使用其中的 `mergedRanges`。 + +**dataValidation 结构**: + +| type | 字段 | 说明 | +|------|------|------| +| `dropdown` | `options: [{value: string, color?: string}]` | 下拉选项列表 | +| `dropdown` | `enableMultiSelect: boolean` | 是否允许多选 | +| `checkbox` | `checked: boolean` | 当前勾选状态 | + +**hyperlink 结构**: + +| type | 字段 | 说明 | +|------|------|------| +| `path` | `link` + 可选 `text` | 外部 URL 链接 | +| `sheet` | `link` + 可选 `text` | 工作表链接,`link` 为工作表 ID 或名称 | +| `range` | `link` + 可选 `text` | 单元格范围链接,`link` 为 A1 表示法,如 `Sheet1!A4` | + +**richText 结构**: + +`richText` 表示单元格内的富文本片段,常见结构为 `{type:"richText", texts:[...]}`。`texts` 数组内每个子项代表一个片段: + +| 子项 type | 常见字段 | 说明 | +|-----------|----------|------| +| `text` | `text` / `style` | 普通文本片段;`style` 是片段级样式 | +| `link` | `text` / `link` / `subType` / `style` | 富文本片段链接。`subType` 不存在时按 `path` 理解;`path` 表示外部 URL,`sheet` 表示工作表链接,`range` 表示单元格范围链接 | +| `attachment` | `text` / `resourceId` / `mimeType` / `size` | 附件片段 | +| `image` | `resourceId` / `resourceUrl` / `width` / `height` | 图片片段 | + +`richText.texts[].link.subType` 与 cell-level `hyperlink.type` 含义一致,但作用范围不同:`hyperlink` 是整个单元格可点击,richText `link` 只作用于该文本片段。读取到 `subType:"sheet"` 时,`link` 通常是真实工作表名称;读取到 `subType:"range"` 时,`link` 通常是 A1 范围(如 `Sheet2!A1:B20`)。不要把 richText 片段链接误当成整格 `hyperlink`。 + +**cellStyles 字段说明**(仅关注显式设置过的非 null 属性;未设置属性不存在或为 null,应忽略): + +| 字段 | 类型 | 说明 | +|------|------|------| +| `fontWeight` | string | `bold` / `normal` | +| `fontColor` | string | 字体颜色,`#RRGGBB` | +| `fontSize` | number | 字号 | +| `fontStyle` | string | `italic` / `normal` | +| `backgroundColor` | string | 背景色,`#RRGGBB` | +| `horizontalAlignment` | string | `left` / `center` / `right` / `general` | +| `verticalAlignment` | string | `top` / `middle` / `bottom` | +| `wordWrap` | string | `overflow` / `clip` / `autoWrap` | +| `numberFormat` | string | 数字格式代码,如 `@`、`#,##0.00`、`yyyy/m/d`;`@` 表示文本 | +| `textUnderline` | boolean | 下划线 | +| `textLineThrough` | boolean | 删除线 | + +**返回示例**: +```json +{ + "cells": [ + [ + {"value": "姓名", "cellStyles": {"fontWeight": "bold", "backgroundColor": "#FFF2CC"}}, + {"value": "状态", "cellStyles": {"fontWeight": "bold"}, "dataValidation": {"type": "dropdown", "options": [{"value": "进行中"}, {"value": "已完成", "color": "#52C41A"}], "enableMultiSelect": false}} + ], + [ + {"value": "张三"}, + {"value": "钉钉", "hyperlink": {"type": "path", "link": "https://dingtalk.com", "text": "钉钉"}} + ] + ], + "message": "Successfully retrieved cell data.", + "success": true +} +``` + +说明:第一行表头有 `cellStyles`(加粗 + 背景色),第二行第二格有单元格级 `hyperlink`。注意:MCP 平台序列化会将未设置的字段填充为 null(如 `"fontStyle": null`),读取时应忽略值为 null 的字段,仅关注非 null 的属性;如果 `cellStyles` 全字段都是 null,视同不存在。`richText` 字段同理——无富文本的普通单元格可能返回 `{"type": null, "texts": null}`,视同不存在。 + +**取值模式说明**: +| 模式 | value 返回内容 | 适用场景 | +|------|---------|---------| +| `formatted_value` | 格式化展示值(如 ¥1,000.00、2025-06-01) | 只看数据(默认) | +| `raw_value` | 原始值(如 1000、45808) | 数据处理、计算 | +| `formula` | 公式文本(如 =SUM(A1:A10)),无公式时回退原始值 | 查看/复制公式 | + +**超时处理建议**:读取大范围数据时若出现超时或响应过慢,请主动缩小 `--range` 查询范围,**建议单次读取的单元格数量控制在 5000 个以内**(例如 50 行 × 100 列、100 行 × 50 列)。对于大表可采用分页读取策略: +- 先通过 `info` 获取 `rowCount` / `lastNonEmptyRow` / `columnCount` 确定数据边界 +- 按行分批读取,如 `A1:J500`、`A501:J1000`、`A1001:J1500` …… +- 避免不传 `--range` 直接读取整个大工作表 + +## 核心工作流 + +```bash +# ── 工作流: 读取已有表格数据 ── + +# 1. 获取工作表列表 +dws sheet list --node --format json + +# 2. 查看工作表详情(行列数、最后非空位置、mergedRanges 等) +dws sheet info --node --sheet-id --format json + +# 3. 读取全部数据 +dws sheet range read --node --sheet-id --format json + +# 4. 读取指定区域 +dws sheet range read --node --sheet-id --range "A1:D10" --format json +``` + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `list` | 工作表的 `sheetId` | info / range read 的 --sheet-id | +| `info` | `rowCount` / `lastNonEmptyRow` / `columnCount` / `mergedRanges` | 确定数据范围、分页读取边界、识别合并单元格结构 | + +## 注意事项 + +- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询真实的 `sheetId` / 工作表名称后再调用,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等);用户仅给出工作表名称时,也应通过 `list` 校验该名称是否存在,避免名称大小写或拼写不一致导致失败 +- `range read` 不传 `--range` 时默认读取整个工作表的全部非空数据 +- `range read` 的 `--range` 支持 `Sheet1!A1:D10` 格式直接指定工作表(此时忽略 `--sheet-id`) +- ★ `csv-get` / `range read` / `range get` 不返回合并单元格结构;查看合并范围必须用 `sheet info` 的 `mergedRanges` +- `range read` 遇到超时或响应过慢时,应缩小 `--range` 查询范围,**单次读取的单元格数量建议控制在 5000 个以内**;数据量较大时通过 `info` 获取边界后分批读取,避免不传 `--range` 直接读取整个大工作表 +- ★ 当用户要求搜索/查找表格数据时,使用 `find` 命令,不要用 `range read` 读取全量数据后自行过滤——`find` 支持服务端搜索,效率更高、语义更准确 diff --git a/skills/mono/references/products/sheet/sheet-search-replace.md b/skills/mono/references/products/sheet/sheet-search-replace.md new file mode 100644 index 00000000..61a3db16 --- /dev/null +++ b/skills/mono/references/products/sheet/sheet-search-replace.md @@ -0,0 +1,118 @@ +# 搜索与替换 + +## 使用场景 + +用户说"搜索/查找/找单元格/搜内容/精确搜索/精确匹配/完全匹配/全字匹配": +- 搜索单元格 → `find` +- 精确匹配(只匹配完全等于的,不匹配包含的) → `find --match-entire-cell` +- 正则搜索 → `find --use-regexp` +- 搜索公式 → `find --match-formula` +- 不要用 `range read` 读取全量数据后在客户端过滤来替代 `find`,必须使用 `find` 命令的服务端搜索能力 + +用户说"替换/查找替换/全局替换/批量替换/把A替换成B/把所有的X改成Y": +- 查找替换 → `replace` +- 精确匹配后替换(只替换内容完全等于的单元格) → `replace --match-entire-cell` +- 正则替换 → `replace --use-regexp` +- 删除匹配内容 → `replace --replacement ""` +- 请勿用 `find` + `range update`、`range read` + `range update` 等组合来模拟替换,`replace` 是服务端原子操作,效率更高且返回替换计数 + +## 命令详细参考 + +### 在工作表中搜索单元格内容 +``` +Usage: + dws sheet find [flags] +Example: + # 基本搜索 + dws sheet find --node --sheet-id --find "销售额" + + # 在指定范围内搜索 + dws sheet find --node --sheet-id --find "合计" --range "A1:D100" + + # 正则表达式搜索(不区分大小写) + dws sheet find --node --sheet-id --find "^total" --use-regexp --match-case=false + + # 精确匹配整个单元格内容 + dws sheet find --node --sheet-id --find "完成" --match-entire-cell + + # 搜索公式文本 + dws sheet find --node --sheet-id --find "SUM" --match-formula +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --find string 搜索文本 (必填) + --range string 搜索范围,A1 表示法 (如 A1:D10) + --match-case 区分大小写 (默认 true) + --match-entire-cell 精确匹配整个单元格内容 + --use-regexp 启用正则表达式搜索 + --match-formula 搜索公式文本而非显示值 + --include-hidden 包含隐藏单元格 +``` + +### 全局查找替换 +``` +Usage: + dws sheet replace [flags] +Example: + dws sheet replace --node --sheet-id --find "旧文本" --replacement "新文本" + dws sheet replace --node --sheet-id --find "待处理" --replacement "已完成" --match-entire-cell + dws sheet replace --node --sheet-id --find "\\d{4}" --replacement "****" --use-regexp + dws sheet replace --node --sheet-id --find "旧" --replacement "新" --range "A1:D100" + dws sheet replace --node --sheet-id --find "临时" --replacement "" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --find string 查找文本 (必填) + --replacement string 替换文本 (必填,可为空字符串表示删除) + --range string 替换范围,A1 表示法 (如 A1:D100) + --match-case 区分大小写 (默认 false) + --match-entire-cell 完整单元格匹配 + --use-regexp 启用正则表达式匹配 + --include-hidden 包含隐藏行/列 +``` + +返回被替换的单元格数量。`--replacement` 可以为空字符串,表示删除匹配内容。 + +## 核心工作流 + +```bash +# ── 工作流: 搜索表格数据 ── + +# 1. 获取工作表列表 +dws sheet list --node --format json + +# 2. 基本搜索 — 在指定工作表中查找文本 +dws sheet find --node --sheet-id --find "销售额" --format json + +# 3. 在指定范围内搜索 +dws sheet find --node --sheet-id --find "合计" --range "A1:D100" --format json + +# 4. 正则搜索(不区分大小写) +dws sheet find --node --sheet-id --find "^total" --use-regexp --match-case=false --format json + +# 5. 精确匹配整个单元格 +dws sheet find --node --sheet-id --find "完成" --match-entire-cell --format json + +# 6. 搜索公式文本 +dws sheet find --node --sheet-id --find "SUM" --match-formula --format json +``` + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `list` | 工作表的 `sheetId` | find / replace 的 --sheet-id | +| `find` | `matchedCells` 中的 `a1Notation` | 定位目标单元格,用于 range read / range update | +| `replace` | `replaceCount` 被替换的单元格数量 | 确认替换结果 | + +## 注意事项 + +- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询真实的 `sheetId` / 工作表名称后再调用,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等);用户仅给出工作表名称时,也应通过 `list` 校验该名称是否存在,避免名称大小写或拼写不一致导致失败 +- ★ **搜索用 `find` 不用 `range read`**:`find` 是服务端搜索,禁止用 `range read` 全量读取后客户端过滤 +- ★ **替换用 `replace` 不用 `range update`**:`replace` 是服务端原子操作,返回替换计数 +- `find` 返回匹配单元格的地址(A1 表示法)和值,无匹配时返回空数组 +- `find` 的 `--match-entire-cell` 用于精确匹配:只返回单元格内容完全等于搜索文本的结果,不会匹配包含该文本的单元格(例如搜索"苹果"时,只匹配"苹果",不匹配"苹果手机""苹果汁"等)。用户说"精确搜索/完全匹配/只搜等于XX的"时必须使用此参数 +- `find` 的 `--match-case` 默认为 true(区分大小写),设为 false 可忽略大小写 +- `find` 的 `--use-regexp` 启用后,`--find` 参数作为正则表达式处理 +- `replace` 的 `--find` 不能为空字符串,`--replace` 可以为空字符串(表示删除匹配内容) +- `replace` 的 `--match-case` 默认为 false(不区分大小写),与 `find` 的默认行为不同 diff --git a/skills/mono/references/products/sheet/sheet-style-format.md b/skills/mono/references/products/sheet/sheet-style-format.md new file mode 100644 index 00000000..240e57fc --- /dev/null +++ b/skills/mono/references/products/sheet/sheet-style-format.md @@ -0,0 +1,274 @@ +# 单元格格式与合并 (style & format) + +## 三种样式设置方式 + +钉钉表格支持三种样式设置方式,适用不同场景: + +| 方式 | 命令 / 字段 | 适用场景 | 粒度 | +|------|------------|---------|------| +| **`set-style` / `batch-set-style`** | `dws sheet range set-style` | 批量刷整片区域的统一样式(表头加粗居中、数字格式等) | range 级别(2D 数组或全 range 统一值) | +| **`cellStyles`**(`range update` 内) | `--values` 中每个 cell 的 `cellStyles` 字段 | 写值同时附带样式,少量 cell 一步到位 | per-cell 级别 | +| **`style`**(richText 片段样式) | `--values` 中 richText 子项(`text`/`link`)的 `style` | 同一单元格内不同文字有不同字体样式 | 文本片段级别 | + +选择建议: +- 只设样式不改值 → `set-style` / `batch-set-style` +- 写值 + 样式一步到位(少量 cell) → `range update` + `cellStyles` +- 文本内部分段样式("重要"红色加粗,其余正常) → `range update` + `type:"richText"` 子项 `style` +- 大面积统一样式 → `set-style`(单值刷 range)或 `batch-set-style`(多 range 批量) + +注意:`set-style` / `batch-set-style` 和 `range update` 的 `cellStyles` 最终都作用于 cell-level 样式,效果相同。区别在于调用方式——前者是独立命令,后者嵌在写值调用中。`range read` 返回的 `cellStyles` 字段能读回所有显式设置过的 cell-level 样式,无论是通过哪种方式设置的。 + +`type:"text"` 顶层旧 `style` 字段不要作为新写法使用;整格样式用 `cellStyles`,分段样式才用 richText 子项 `style`。 + +## 使用场景 + +### 单元格格式 + +用户说"设置样式/改颜色/设背景色/加粗/居中/换行/字体颜色/字号": +- 仅设样式不改值 → `range set-style` +- 批量设置不同 range 的样式 → `range batch-set-style --batch ./styles.json`(内部顺序循环调 `update_range`) +- 写值同时附带样式 → `range update --values` 中使用 `cellStyles` 字段(参见 sheet-write-data.md) +- 请勿用 `range update --values` 写空/重写来模拟纯样式变更 + +用户说"设置数字格式/改成百分比/用人民币显示/按日期显示/文本格式/保留几位小数": +- 批量设置数字格式 → `range set-style --number-format <格式代码>`(如 `0%` / `"¥"#,##0.00` / `yyyy/m/d` / `@`) +- 写值时顺带设置数字格式 → `range update` 中 `cellStyles.numberFormat` + +用户说"合并单元格/合并/合并区域/按行合并/按列合并": +- 合并所有单元格 → `merge-cells`(默认 mergeAll) +- 按行合并 → `merge-cells --merge-type mergeRows` +- 按列合并 → `merge-cells --merge-type mergeColumns` + +用户说"取消合并/拆分单元格/还原合并": +- 取消合并单元格 → `unmerge-cells` + +## 命令详细参考 + +### 设置单元格样式 +``` +Usage: + dws sheet range set-style [flags] +Example: + # 给 A1:B3 打上黄底粗体居中 + dws sheet range set-style --node --sheet-id --range "A1:B3" \ + --bg-color "#FFF2CC" --font-weight bold --h-align center + + # 给 C1:C5 逐单元格设置不同背景色 + dws sheet range set-style --node --sheet-id --range "C1:C5" \ + --bg-colors-json '[["#FF0000"],["#00FF00"],["#0000FF"],["#FFFF00"],["#FF00FF"]]' + + # 整片 range 启用自动换行 + dws sheet range set-style --node --sheet-id --range "A1:E10" --word-wrap autoWrap +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 目标区域,如 A1:B3 (必填) + --bg-color string 背景色(#RRGGBB),一键刷整个 range;与 --bg-colors-json 二选一 + --bg-colors-json string 背景色二维 JSON 数组,维度需与 --range 一致 + --font-size int 字号,一键刷整个 range;与 --font-sizes-json 二选一 + --font-sizes-json string 字号二维 JSON 数组 + --h-align string 水平对齐:left/center/right/general + --h-aligns-json string 水平对齐二维 JSON 数组 + --v-align string 垂直对齐:top/middle/bottom + --v-aligns-json string 垂直对齐二维 JSON 数组 + --font-color string 字体颜色(#RRGGBB) + --font-colors-json string 字体颜色二维 JSON 数组 + --font-weight string 字体粗细:bold/normal + --font-weights-json string 字体粗细二维 JSON 数组 + --word-wrap string 换行方式:overflow/clip/autoWrap(整个 range 共用) + --number-format string 数字格式代码,如 General/@/#,##0/#,##0.00/0%/0.00%/yyyy/m/d/h:mm:ss +``` + +**特性说明**: +- 每个样式维度提供两种写法,二选一:`--xxx`(单值刷整个 range,CLI 本地展开为二维数组)vs `--xxx-json`(逐单元格指定,维度需与 `--range` 完全一致) +- 至少需传入一个样式参数。单次调用建议:行数 ≤ 1000,单元格总数 ≤ 5000 +- 枚举值按驼峰书写:`autoWrap`、`bold`、`normal`、`center` 等 + +### 批量设置单元格样式 +``` +Usage: + dws sheet range batch-set-style [flags] +Example: + dws sheet range batch-set-style --node --batch ./styles.json + dws sheet range batch-set-style --node --batch ./styles.json --continue-on-error +Flags: + --node string 表格文档 ID 或 URL (必填) + --batch string 批次配置 JSON 文件路径 (必填) + --continue-on-error 遇到失败时继续执行后续条目(默认遇错即停) +``` + +配置文件格式(JSON 数组,每个元素一条批次项): +```json +[ + { + "sheetId": "Sheet1", + "range": "A1:B3", + "bgColor": "#FFF2CC", + "fontSize": 12, + "hAlign": "center", + "vAlign": "middle", + "fontColor": "#333333", + "fontWeight": "bold", + "wordWrap": "autoWrap", + "numberFormat": "General" + }, + { + "sheetId": "Sheet1", + "range": "C1:C5", + "bgColorsJson": "[[\"#FF0000\"],[\"#00FF00\"],[\"#0000FF\"],[\"#FFFF00\"],[\"#FF00FF\"]]" + } +] +``` + +**特性说明**: +- CLI 侧顺序循环逐条调用 `update_range`(非服务端批量),运行时输出 `[N/M]` 进度 +- 每条记录执行与 `set-style` 一致的校验:至少一项样式字段 + rows ≤ 1000 + rows×cols ≤ 30000 + 枚举合法 +- 默认遇错即停(返回非 0),`--continue-on-error` 时所有条目跑完再返回首个错误 + +### 合并单元格 +``` +Usage: + dws sheet merge-cells [flags] +Example: + # 合并所有单元格(默认) + dws sheet merge-cells --node --sheet-id --range "A1:B3" + + # 按行合并 + dws sheet merge-cells --node --sheet-id --range "A1:C3" --merge-type mergeRows + + # 按列合并 + dws sheet merge-cells --node --sheet-id --range "A1:C3" --merge-type mergeColumns + + # 使用带工作表前缀的范围(忽略 --sheet-id) + dws sheet merge-cells --node --sheet-id --range "Sheet1!A1:B3" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 目标单元格区域地址,如 A1:B3 (必填) + --merge-type string 合并方式: mergeAll(默认)/mergeRows/mergeColumns +``` + +支持三种合并方式: +- `mergeAll`(默认):合并所有单元格,将选定区域内的所有单元格合并成一个 +- `mergeRows`:按行合并,在选定区域内将同一行相邻的单元格合并 +- `mergeColumns`:按列合并,在选定区域内将同一列相邻的单元格合并 + +注意:合并时只保留左上角单元格的值,其他单元格的值会被丢弃。 +`--range` 支持带工作表前缀的写法(如 `Sheet1!A1:B3`),此时将优先使用前缀解析出的工作表,忽略 `--sheet-id`。 +合并完成后,可通过 `dws sheet info --node --sheet-id --format json` 查看 `mergedRanges` 验证合并结构。 + +### 取消合并单元格 +``` +Usage: + dws sheet unmerge-cells [flags] +Example: + dws sheet unmerge-cells --node --sheet-id --range "A1:D5" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 取消合并的范围,A1 表示法 (必填) +``` + +取消指定范围内所有合并的单元格,恢复为独立单元格。 + +## number-format 格式 code + +适用范围:`number-format` 在 `range set-style` / `range batch-set-style` 中接受(CLI 对应 `--number-format`,batch 配置文件对应 `numberFormat`)。`range update` 没有 `--number-format` 参数,但可在写入值时通过每个 cell 的 `cellStyles.numberFormat` 设置同样的数字格式。 + +商品 ID、规格 ID、SKU、订单号、手机号、工号等数字形态标识符,使用文本格式 code:`@`。 + +常用格式: + +| 格式类型 | 推荐 code | 展示示例 | 适用场景 | +| --- | --- | --- | --- | +| 常规 | `General` | `1234` / `普通文本` | 普通文本/数字展示 | +| 文本 | `@` | `528545015680` | 商品 ID、规格 ID、SKU、订单号、手机号、工号 | +| 整数 | `0` | `1235` | 数量、计数 | +| 两位小数 | `0.00` | `1234.50` | 单价、评分 | +| 整数千分位 | `#,##0` | `1,235` | 数量、金额整数 | +| 千分位两位小数 | `#,##0.00` | `1,234.50` | 金额、单价 | +| 百分比 | `0%` / `0.00%` | `85%` / `85.00%` | 转化率、占比 | +| 日期 | `yyyy/m/d` | `2026/3/15` | 日期列 | +| 日期时间 | `yyyy/m/d h:mm` | `2026/3/15 14:30` | 日期时间列 | +| 时间 | `h:mm` / `h:mm:ss` | `14:30` / `14:30:05` | 时间列 | +| 科学计数法 | `0.00E+00` / `##0.0E+0` | `1.23E+05` | 科学数据 | +| 人民币 | `"¥"#,##0_);("¥"#,##0)` / `"¥"#,##0.00_);("¥"#,##0.00)` | `¥1,235` / `¥1,234.50` | 金额列 | +| 美元 | `$#,##0_);($#,##0)` / `$#,##0.00_);($#,##0.00)` | `$1,235` / `$1,234.50` | 金额列 | + +选择规则:没有特殊展示要求时,优先使用上面的常用格式。只有用户明确要求负数显示方式、中文日期、12 小时制、累计时长、分数或会计格式时,再选择下面的可选变体。 + +可选变体: + +| 用户要求 | 推荐 code | 推荐展示示例 | 可选 code(差异) | +| --- | --- | --- | --- | +| 负数用括号显示 | `#,##0 ;(#,##0)` | `(1,235)` | `#,##0.00;(#,##0.00)`:保留两位小数,如 `(1,234.50)` | +| 负数标红显示 | `#,##0 ;[red](#,##0)` | 红色 `(1,235)` | `#,##0.00;[red](#,##0.00)`:保留两位小数,如红色 `(1,234.50)` | +| 分数 | `# ?/?` | `1 1/2` | `# ??/??`:分母最多两位,如 `1 23/32` | +| 英文月份日期 | `d-mmm-yy` | `15-Mar-26` | `d-mmm`:省略年份,如 `15-Mar`;`mmm-yy`:只显示月年,如 `Mar-26` | +| 中文日期 | `yyyy"年"m"月"d"日"` | `2026年3月15日` | `yyyy"年"m"月"`:只显示年月,如 `2026年3月`;`m"月"d"日"`:只显示月日,如 `3月15日` | +| 12 小时制时间 | `h:mm AM/PM` | `2:30 PM` | `h:mm:ss AM/PM`:显示秒,如 `2:30:05 PM` | +| 中文上午/下午时间 | `上午/下午 h"时"mm"分"` | `下午 2时30分` | `上午/下午 h"时"mm"分"ss"秒"`:显示秒,如 `下午 2时30分05秒` | +| 分秒/累计时长 | `mm:ss` | `05:30` | `[h]:mm:ss`:累计小时,如 `27:05:30`;`mm:ss.0`:显示十分之一秒,如 `05:30.5` | +| 人民币负数标红 | `"¥"#,##0_);[red]("¥"#,##0)` | 红色 `(¥1,235)` | `"¥"#,##0.00_);[red]("¥"#,##0.00)`:保留两位小数,如红色 `(¥1,234.50)` | +| 美元负数标红 | `$#,##0_);[Red]($#,##0)` | 红色 `($1,235)` | `$#,##0.00_);[Red]($#,##0.00)`:保留两位小数,如红色 `($1,234.50)` | +| 会计数字 | `_(* #,##0_);_(* (#,##0);_(* "-"_);_(@_)` | `1,235`,零值显示 `-` | `_(* #,##0.00_);_(* (#,##0.00);_(* "-"??_);_(@_)`:保留两位小数,如 `1,234.50` | +| 人民币会计格式 | `_("¥"* #,##0_);_("¥"* (#,##0);_("¥"*"-"_);_(@_)` | `¥ 1,235`,零值显示 `¥ -` | `_("¥"* #,##0.00_);_("¥"*(#,##0.00);_("¥"* "-"??_);_(@_)`:保留两位小数,如 `¥ 1,234.50` | + +## 核心工作流 + +```bash +# ── 工作流 4: 写入数据并设置样式 ── + +# 1. 写入数据 +dws sheet range update --node --sheet-id --range "A1:C3" \ + --values '[["商品","单价","数量"],["苹果",5.5,100],["香蕉",3.2,200]]' --format json + +# 2. 设置数字格式(人民币) +dws sheet range set-style --node --sheet-id --range "B2:B3" \ + --number-format '"¥"#,##0.00' --format json + +# 3. 商品 ID / 规格 ID 按文本展示,避免科学计数法 +dws sheet range set-style --node --sheet-id --range "A2:A3" \ + --number-format "@" --format json + +# 4. 写入单元格级超链接 +dws sheet range update --node --sheet-id --range "D1" \ + --values '[[{"type":"text","text":"详情","hyperlink":{"type":"path","link":"https://dingtalk.com"}}]]' --format json +``` + +```bash +# ── 工作流 8: 合并单元格 ── + +# 1. 获取工作表列表 +dws sheet list --node --format json + +# 2. 合并所有单元格(默认 mergeAll) +dws sheet merge-cells --node --sheet-id --range "A1:B3" --format json + +# 3. 按行合并 +dws sheet merge-cells --node --sheet-id --range "A1:C3" --merge-type mergeRows --format json + +# 4. 按列合并 +dws sheet merge-cells --node --sheet-id --range "A1:C3" --merge-type mergeColumns --format json +``` + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `merge-cells` | `a1Notation` 实际被合并的范围、`mergeType` 生效的合并方式 | 确认合并结果 | +| `unmerge-cells` | `sheetId` 工作表 ID | 确认操作完成 | +| `list` | 工作表的 `sheetId` | info / range read / range update / find 的 --sheet-id | + +## 注意事项 + +- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) +- ★ `range update` / `range set-style` / `range batch-set-style` 单次调用上限(强制):行数 ≤ 1000,单元格总数(行×列)建议≤ 5000(服务端硬限 30000);超限请拆分多次调用。CLI 会在调用前做本地预校验,服务端超 30000 会直接报错 +- `range set-style` / `range batch-set-style` 的样式枚举按驼峰书写:`wordWrap` 取 `overflow`/`clip`/`autoWrap`,`fontWeight` 取 `bold`/`normal`,`hAlign` 取 `left`/`center`/`right`/`general`,`vAlign` 取 `top`/`middle`/`bottom`;背景色/字体颜色统一使用 `#RRGGBB` 格式 +- `range update` 支持通过 `cellStyles` 在写值时附带 per-cell 样式,适合少量单元格写值 + 样式一步到位的场景。批量设置整片区域的统一样式时,仍应使用 `set-style` / `batch-set-style` +- `merge-cells` 合并时只保留左上角单元格的值,其他单元格的值会被丢弃 +- `merge-cells` 的 `--merge-type` 不传时默认为 `mergeAll`(合并所有单元格) +- `merge-cells` 的 `--range` 支持带工作表前缀的写法(如 `Sheet1!A1:B3`),此时忽略 `--sheet-id` +- `merge-cells` 如果目标区域与其他合并单元格、锁定区域或表格区域存在交集,合并将失败 +- `unmerge-cells` 取消指定范围内所有合并单元格,使用 A1 表示法指定范围 +- 对已有表格做格式延续、插入列后修复表头、或写入前临时取消合并时,先记录 `sheet info` 返回的 `mergedRanges`,操作后按需用 `merge-cells` 恢复 diff --git a/skills/mono/references/products/sheet/sheet-workbook.md b/skills/mono/references/products/sheet/sheet-workbook.md new file mode 100644 index 00000000..8874d756 --- /dev/null +++ b/skills/mono/references/products/sheet/sheet-workbook.md @@ -0,0 +1,242 @@ +# 表格与工作表管理 + +## 使用场景 + +用户说"创建表格/新建电子表格": +- 创建表格文档 → `create` + +用户说"看工作表/有哪些工作表/表格结构": +- 列出工作表 → `list` +- 工作表详情 → `info` + +用户说"加工作表/新增Sheet": +- 新建工作表 → `new` + +用户说"修改工作表名称/重命名工作表/移动工作表位置/隐藏工作表/显示工作表/冻结行/冻结列/取消冻结/更新工作表属性": +- 更新工作表属性 → `update` +- 重命名工作表 → `update --name "新名称"` +- 移动工作表位置 → `update --index N` +- 隐藏工作表 → `update --hidden` +- 显示工作表 → `update --hidden=false` +- 冻结行列 → `update --frozen-row-count N --frozen-column-count M` +- 取消冻结 → `update --frozen-row-count 0 --frozen-column-count 0` + +用户说"复制工作表/拷贝工作表/克隆工作表/工作表副本": +- 复制工作表 → `copy` +- 复制并指定名称 → `copy --name "副本名称"` +- 复制并指定位置 → `copy --index N` + +用户说"删除工作表/移除工作表/删掉这个Sheet": +- 删除工作表 → `delete-sheet`(不可逆操作,执行前必须向用户确认) + +## 命令详细参考 + +### 创建钉钉表格文档 +``` +Usage: + dws sheet create [flags] +Example: + dws sheet create --name "销售数据" + dws sheet create --name "Q1 数据" --folder + dws sheet create --name "知识库表格" --workspace +Flags: + --name string 表格名称 (必填) + --folder string 目标文件夹 ID (dentryUuid 格式) 或 URL;禁止传入纯数字 dentryId + --workspace string 目标知识库 ID +``` + +> **ID 格式约束**:`--folder` 只接受 UUID 格式的 `fileId`(如 `ZgpG2NdyVXYOR2D5UGDok65MJMwvDqPk`)或 alidocs 文件夹 URL。`drive list` 返回中有 `dentryId`(纯数字,如 `218595998810`)和 `fileId`(UUID 格式)两个字段,**必须使用 `fileId`,禁止使用 `dentryId`**,传入纯数字会导致命令失败。 + +### 获取全部工作表列表 +``` +Usage: + dws sheet list [flags] +Example: + dws sheet list --node + dws sheet list --node "https://alidocs.dingtalk.com/i/nodes/" +Flags: + --node string 表格文档 ID 或 URL (必填) +``` + +### 获取指定工作表详情 +``` +Usage: + dws sheet info [flags] +Example: + dws sheet info --node + dws sheet info --node --sheet-id + dws sheet info --node --sheet-id "Sheet1" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (不传则返回第一个工作表) +``` + +返回字段中 `mergedRanges` 是当前工作表的合并单元格范围列表(A1 表示法,如 `["C7:D11"]`)。它属于工作表结构/布局元数据:读写单元格内容前,如需判断表头、分组标题、续写位置或避开合并冲突,应先看 `sheet info`,不要在 `range read` / `csv-get` 的单元格值里寻找合并信息。 + +### 新建工作表 +``` +Usage: + dws sheet new [flags] +Example: + dws sheet new --node --name "Sheet2" + dws sheet new --node --name "数据汇总" +Flags: + --node string 表格文档 ID (必填) + --name string 工作表名称 (必填) +``` + +### 更新工作表属性 +``` +Usage: + dws sheet update [flags] +Example: + # 改名 + 调整冻结 + dws sheet update --node --sheet-id --name "汇总表" --frozen-row-count 2 --frozen-column-count 1 + + # 隐藏工作表 + dws sheet update --node --sheet-id --hidden=true + + # 显示工作表 + dws sheet update --node --sheet-id --hidden=false + + # 移动工作表到第一个位置 + dws sheet update --node --sheet-id --index 0 + + # 取消冻结 + dws sheet update --node --sheet-id --frozen-row-count 0 --frozen-column-count 0 +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --name string 新名称,最长 100 字符,不能包含 / \ ? * [ ] : + --index int 新位置(从 0 开始) + --hidden --hidden=true 隐藏,--hidden=false 取消隐藏 + --frozen-row-count int 冻结行数,0 表示取消冻结 + --frozen-column-count int 冻结列数,0 表示取消冻结 +``` + +更新工作表名称、位置、隐藏状态、冻结行列。 +`--name` / `--index` / `--hidden` / `--frozen-row-count` / `--frozen-column-count` 至少提供一个;多个属性可同时传入,将在同一次请求中更新。 + +注意: +- 至少需要保留一个可见的工作表,不能将所有工作表都隐藏 +- 冻结行数/列数不能超过工作表的总行数/列数 + +### 复制工作表 +``` +Usage: + dws sheet copy [flags] +Example: + # 按默认位置复制 + dws sheet copy --node --sheet-id + + # 指定副本名称和位置 + dws sheet copy --node --sheet-id --name "销售副本" --index 2 + + # 只指定名称 + dws sheet copy --node --sheet-id --name "备份" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 源工作表 ID 或名称 (必填) + --name string 副本名称,最长 100 字符,不能包含 / \ ? * [ ] : (不传则系统自动生成) + --index int 副本位置(从 0 开始)(不传则放在源工作表之后) +``` + +复制指定工作表,在同一表格中创建一个副本。 +复制操作会将源工作表的所有内容(包括数据、格式、公式等)完整复制到新工作表中。 +传 `--index` 时,CLI 会先复制,再追加一次位置更新,把副本移动到目标索引。 +名称与已有工作表重复时系统会自动重命名。 + +### 删除工作表 +``` +Usage: + dws sheet delete-sheet [flags] +Example: + dws sheet delete-sheet --node --sheet-id +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 要删除的工作表 ID 或名称 (必填) +``` + +> **CAUTION:** 不可逆操作 — 执行前必须向用户确认。 + +删除指定的工作表及其所有数据。约束: +- 不能删除隐藏的工作表(需先通过 `sheet update --hidden false` 取消隐藏再删除) +- 不能删除最后一个可见工作表(至少保留一个可见工作表) + +## 核心工作流 + +```bash +# ── 工作流 1: 创建表格并写入数据 ── + +# 1. 创建表格文档 — 提取 nodeId +dws sheet create --name "销售数据" --format json + +# 2. 查看工作表列表 — 提取 sheetId +dws sheet list --node --format json + +# 3. 写入表头和数据 +dws sheet range update --node --sheet-id --range "A1:C1" \ + --values '[["姓名","部门","销售额"]]' --format json + +dws sheet range update --node --sheet-id --range "A2:C4" \ + --values '[["张三","销售部",50000],["李四","市场部",38000],["王五","销售部",62000]]' --format json + +# ── 工作流 2: 读取已有表格数据 ── + +# 1. 获取工作表列表 +dws sheet list --node --format json + +# 2. 查看工作表详情(行列数、最后非空位置等) +dws sheet info --node --sheet-id --format json + +# 3. 读取全部数据 +dws sheet range read --node --sheet-id --format json + +# 4. 读取指定区域 +dws sheet range read --node --sheet-id --range "A1:D10" --format json + +# ── 工作流 3: 多工作表管理 ── + +# 1. 新建工作表 +dws sheet new --node --name "汇总" --format json + +# 2. 在新工作表中写入汇总公式 +dws sheet range update --node --sheet-id --range "A1:B1" \ + --values '[["指标","数值"]]' --format json + +dws sheet range update --node --sheet-id --range "A2:B2" \ + --values '[["总销售额","=SUM(Sheet1!C2:C100)"]]' --format json +``` + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `create` | `nodeId` | list / info / new / range read / range update / find 的 --node | +| `list` | 工作表的 `sheetId` | info / range read / range update / find 的 --sheet-id | +| `new` | 新工作表的 `sheetId` | range read / range update / find 的 --sheet-id | +| `info` | `rowCount` / `lastNonEmptyRow` / `mergedRanges` | 确定数据范围、追加写入起始行、判断合并单元格结构 | + +## 注意事项 + +- ★ **`--sheet-id` 获取规范(强制)**:所有涉及 `--sheet-id` 参数的命令,除非用户主动提供了工作表 ID 或工作表名称,否则在 `sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询真实的 `sheetId` / 工作表名称后再调用,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等);用户仅给出工作表名称时,也应通过 `list` 校验该名称是否存在,避免名称大小写或拼写不一致导致失败 +- `mergedRanges` 中的范围表示一个整体语义区域。合并区域内非左上角单元格为空并不代表无内容,通常应以左上角单元格的值作为该合并区域的含义。 +- `create` 不传 `--folder` 和 `--workspace` 时,默认创建在"我的文档"根目录 +- `list` 返回所有工作表的 ID 和名称,是后续操作的必要前置步骤 +- `info` 不传 `--sheet-id` 时默认返回第一个工作表的详情 +- `new` 创建工作表时,如名称与已有工作表重复,系统会自动重命名 +- `update` 的 `--name`、`--index`、`--hidden`、`--frozen-row-count`、`--frozen-column-count` 至少必须提供一个 +- `update` 的 `--name` 最长 100 字符,不能包含 `/ \ ? * [ ] :` 等特殊字符 +- `update` 的 `--index` 为 0-based 非负整数,0 表示移动到最前面 +- `update` 的 `--hidden` 设为 true 时,至少需要保留一个可见的工作表,不能将所有工作表都隐藏 +- `update` 的 `--frozen-row-count` / `--frozen-column-count` 为非负整数,不能超过工作表的总行数/列数,设为 0 表示取消冻结 +- `update` 当同时提供多个属性时,所有属性将在同一次请求中更新 +- `copy` 复制操作会将源工作表的所有内容(包括数据、格式、公式等)完整复制到新工作表 +- `copy` 的 `--name` 可选,不传时系统自动生成名称(通常为"源名称 副本"或类似格式) +- `copy` 的 `--name` 最长 100 字符,不能包含 `/ \ ? * [ ] :` 等特殊字符 +- `copy` 当指定名称与已有工作表重复时,系统会自动重命名为合法值 +- `copy` 的 `--index` 可选,不传时副本将放置在源工作表之后的默认位置 +- `delete-sheet` 为不可逆操作,执行前必须向用户确认 +- `delete-sheet` 不能删除隐藏的工作表,需先通过 `update --hidden=false` 取消隐藏再删除 +- `delete-sheet` 不能删除最后一个可见工作表,至少保留一个可见工作表 +- ★ 关键区分: sheet(电子表格/单元格读写) vs aitable(AI多维表/结构化记录/字段定义) vs doc(文档编辑/阅读) diff --git a/skills/mono/references/products/sheet/sheet-write-data.md b/skills/mono/references/products/sheet/sheet-write-data.md new file mode 100644 index 00000000..1b92e2be --- /dev/null +++ b/skills/mono/references/products/sheet/sheet-write-data.md @@ -0,0 +1,415 @@ +# 数据写入 + +## 使用场景 + +用户说"写数据/填表/更新单元格/写入公式": +- 更新数据 → `range update` +- 【强制】`--sheet-id` 必填:即使是单工作表也不能省略,不要参照 `range read` 的默认行为;未知时先执行 `dws sheet list --node --format json` 获取 `sheetId`,禁止凭空臆测为 `Sheet1`、`sheet1`、`0`、`default` 等 +- 注意:如果用户的目的是替换文本、移动行列、追加空行空列、清空区域、排序、填充、复制区域或移动区域,请勿使用 `range update`,必须使用对应的专用命令(`replace`/`move-dimension`/`add-dimension`/`range clear`/`range sort`/`range fill`/`range copy-to`/`range move-to`) +- **批量纯值写入优先用 `csv-put`**:当写入场景同时满足以下条件时,必须优先使用 `csv-put` 而非 `range update`:(1) 写入的是纯值(不含公式、超链接、dataValidation、cellStyles、richText);(2) 数据量较大(超过 5 行或超过 20 个单元格);(3) 数据来源为表格/CSV 文本/结构化文本。`csv-put` 无需手动构造二维 JSON 数组,直接传 CSV 文本即可,更简洁高效且支持自动扩容 + +用户说"追加数据/添加行/在末尾加数据/新增记录": +- 追加数据 → `append` + +用户说"批量写入CSV/导入CSV/CSV写入表格/把CSV贴到表格里": +- 写入 CSV → `csv-put` +- 与 `range update` 的区别:`csv-put` 接受 CSV 文本直接写入,无需手动构造二维 JSON 数组;适合大批量纯值写入 +- 与 `append` 的区别:`csv-put` 写入指定位置(--start-cell),`append` 在末尾追加 + +**三种写入命令能力对比**: + +| 能力 | `range update` | `append` | `csv-put` | +|------|---------------|----------|-----------| +| 公式(`=` 开头) | 支持 | 不支持 | 不支持(当文本) | +| 单元格级超链接(`hyperlink`) | 支持 | 不支持 | 不支持 | +| 富文本(片段链接/附件/图片) | 支持 | 不支持 | 不支持 | +| richText 片段样式(bold/color) | 支持 | 不支持 | 不支持 | +| `cellStyles`(背景色/字号/对齐等 cell-level 样式) | 支持 | 不支持 | 不支持 | +| `{}` 跳过(保留原值) | 支持 | 不适用 | 不适用 | +| `dataValidation`(下拉/复选框) | 支持 | 不支持 | 不支持 | +| 原始值(纯数字/字符串) | 支持 | 支持 | 支持 | +| 自动定位末尾 | 不支持 | 支持 | 不支持 | +| 自动扩容行列 | 不支持 | 支持 | 支持 | + +## 命令详细参考 + +### 更新工作表指定区域内容 +``` +Usage: + dws sheet range update [flags] +Example: + # 写入文本 + dws sheet range update --node --sheet-id --range "A1:B2" \ + --values '[[{"type":"text","text":"姓名"},{"type":"text","text":"分数"}],[{"type":"text","text":"张三"},{"type":"text","text":"90"}]]' + + # 写入公式 + dws sheet range update --node --sheet-id --range "C2" \ + --values '[[{"type":"text","text":"=A2&B2"}]]' + + # 写入单元格级超链接 + dws sheet range update --node --sheet-id --range "A1" \ + --values '[[{"type":"text","text":"钉钉","hyperlink":{"type":"path","link":"https://dingtalk.com"}}]]' + + # 清理单元格级超链接,保留当前值 + dws sheet range update --node --sheet-id --range "A1" \ + --values '[[{"hyperlink":{"type":"none"}}]]' + + # 清空单个单元格(text 为空字符串) + dws sheet range update --node --sheet-id --range "A1" \ + --values '[[{"type":"text","text":""}]]' +Flags: + --node string 表格文档 ID (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 目标单元格区域地址,如 A1:B3 (必填) + --values string 单元格内容,二维 JSON 数组 (必填);每个元素必须是 object:{type:text,text:...}、{type:richText,texts:[...]}、{dataValidation:...}、{cellStyles:...}、{hyperlink:...} 或 {}(详见下文 values 参数格式说明) +``` + +**合并单元格注意(`range update`)**:这里说的是 `range update` 写入单元格对象这一路径,不是所有写入命令的统一行为。目标范围与已有合并区域冲突时,MCP 服务端会拦截并返回 `MERGED_CELLS_CONFLICT` 错误,错误消息中通常会列出具体冲突的合并区域地址。收到此错误时按以下流程处理: +1. 从错误消息中获取冲突的合并区域地址(如 `A1:B2, C3:D4`),或通过 `dws sheet info --node --sheet-id --format json` 查询完整的合并区域列表(`mergedRanges` 数组) +2. 用 `dws sheet unmerge-cells --range <冲突区域>` 取消这些合并 +3. 执行 `range update` 写入数据 +4. 如需保留原合并效果,用 `dws sheet merge-cells` 重新合并对应区域(注意合并后仅保留左上角单元格的值) + +续写或改写已有格式化表格时,先用 `sheet info` 读取 `mergedRanges`。若原数据块存在跨列标题行(如 `A1:G1`),新增同类标题行后也要用 `merge-cells` 复制相同合并模式;仅写入值或样式不会自动创建合并区域。 + +**单次调用建议**:行数 ≤ 1000,单元格总数(行×列)≤ 5000;超过时请拆分多次调用。 + +**何时该用 `csv-put` 替代**:如果你准备用 `range update` 写入纯值(不含公式、超链接、富文本对象),且数据量超过 5 行或 20 个单元格,应改用 `csv-put`——它接受 CSV 文本直接写入,无需手动拼装二维 JSON 数组,且支持自动扩容行列。仅在需要写入公式(`=SUM(...)`)、单元格级超链接、富文本对象或修改少量单元格时才使用 `range update`。 + +**范围职责**:`range update` 负责写入单元格内容(原始值/公式/富文本对象),并支持通过 `cellStyles` 附带 per-cell 样式。如需批量设置整片区域的样式(不写值),请使用 `dws sheet range set-style`。 + +### 在工作表末尾追加数据 +``` +Usage: + dws sheet append [flags] +Example: + dws sheet append --node --sheet-id --values '[["张三","销售部",50000]]' + dws sheet append --node --sheet-id \ + --values '[["李四","市场部",38000],["王五","销售部",62000]]' +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --values string 追加数据,二维 JSON 数组 (必填) +``` + +`--values` 为二维 JSON 数组,外层每个元素代表一行,内层每个元素代表一个单元格值。 +追加的数据列数应与工作表已有数据的列数保持一致。 + +### 将 CSV 数据写入指定位置 +``` +Usage: + dws sheet csv-put [flags] +Example: + dws sheet csv-put --node --sheet-id --start-cell A1 \ + --csv 'name,score\nAlice,95\nBob,87' + + dws sheet csv-put --node --sheet-id --start-cell B2 \ + --csv @data.csv --allow-overwrite + + cat data.csv | dws sheet csv-put --node --sheet-id \ + --start-cell A1 --csv - + + dws sheet csv-put --node --sheet-id --start-cell A1 \ + --csv @data.csv --dry-run +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --csv string CSV 文本、@文件路径 或 - 表示 stdin (必填) + --start-cell string 起始单元格,A1 表示法 (必填) + --allow-overwrite 允许覆盖已有数据 (默认 false) +``` + +将 RFC 4180 格式的 CSV 文本写入指定工作表的指定单元格位置。 +- **分隔符必须是英文逗号 `,`**(ASCII 0x2C),禁止使用中文逗号 `,`(U+FF0C)。中文逗号不会被识别为分隔符,会导致整行被写入同一个单元格。生成 CSV 内容时务必检查分隔符 +- 只写纯值,不支持公式/样式/批注。`=` 开头的内容当文本处理,不会被解析为公式 +- 数字/日期/百分数由表格引擎自动识别类型(如 `95` 存为数字,`2025-03-01` 存为日期) +- 自动扩容行列:CSV 数据超出当前工作表维度时自动追加行/列 +- 与 `range update` 不同,目标区域如含合并单元格,`csv-put` 会打散合并并写入纯值 +- 若需要保留原有合并结构,写入前先用 `sheet info` 记录 `mergedRanges`,写入后用 `merge-cells` 恢复对应区域 +- `--allow-overwrite` 默认 false,目标区域有数据时需显式传 `--allow-overwrite` 才能覆盖 +- `--csv` 支持三种输入:直接传文本、`@filepath` 从本地文件读取、`-` 从 stdin 管道读取 +- CSV 文本上限 2M 字符,单元格总数上限 30000 +- 特殊字符处理:CLI 会自动过滤 `\r`(Windows 换行符)和 BOM(UTF-8 文件头标记),Excel/Windows 导出的 CSV 可直接使用;如 CSV 数据中含零宽字符(U+200B 等)或 Bidi 控制符,CLI 会拒绝并报错 + +## values 参数格式说明 + +`range update` 只接受 `--values` 一个数据参数,为二维 JSON 数组,第一维为行,第二维为列。每个 cell 是以下之一: + +- `{}` 空对象:**跳过该单元格,保留原值不变**。只更新部分单元格时用 `{}` 占位,避免拆分多次调用 +- `{type:"text",...}` 或 `{type:"richText",...}` 对象 +- 任何 cell 可附加 `dataValidation` 字段,在写值的同时设置数据校验(下拉列表 / 复选框) +- 任何 cell 可附加 `cellStyles` 字段,在写值的同时设置 cell-level 样式(背景色 / 字体 / 对齐等) +- 任何 cell 可附加 `hyperlink` 字段设置单元格级超链接;`{"hyperlink":{"type":"none"}}` 表示清理单元格级超链接并保留当前值 + +### {}(跳过,保留原值) + +```json +{} +``` + +只更新范围内部分单元格时,用 `{}` 占位不需要修改的位置。示例:`--range "A1:C1" --values '[[{"type":"text","text":"新值"},{},{}]]'` 只更新 A1,B1 和 C1 保持不变。 + +### type=text(普通文本) + +```json +{ "type": "text", "text": "文本内容" } +{ "type": "text", "text": "重要", "cellStyles": { "fontWeight": "bold", "fontColor": "#FF0000" } } +``` + +- `text` 必须为字符串;`text=""` 表示**清空该 cell** +- `text` 以 `=` 开头识别为公式(如 `"=SUM(B2:B4)"`) +- 写数字 / 布尔请用字符串形式(如 `{"type":"text","text":"100"}` / `"true"`),服务端按内容自动识别 +- 字体样式(加粗/颜色/字号等)统一走 `cellStyles`,不支持 `style` 字段 + +### hyperlink 子结构(可选,与 type 同级,单元格级超链接) + +`hyperlink` 作用于整个单元格,适合“这个单元格整体可点击跳转”的场景。它和 richText 的片段级 `link` 不同。 + +> **hyperlink 三种语义**: +> - **不传 `hyperlink` 字段** → 保留原超链接(引擎自动 readback 回写) +> - **`hyperlink: {"type":"none"}`** → 显式清除单元格超链接 +> - **`hyperlink: {"type":"path"/"sheet"/"range", link, text?}`** → 写新超链接(覆盖) +> +> `{}` 跳过也会保留原超链接。 + +```json +{ "type": "text", "text": "钉钉", "hyperlink": { "type": "path", "link": "https://dingtalk.com" } } +{ "hyperlink": { "type": "sheet", "link": "Sheet2" } } +{ "hyperlink": { "type": "range", "link": "Sheet1!A4" } } +{ "hyperlink": { "type": "none" } } +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `type` | string | 必填,`path`(外部链接)/ `sheet`(工作表链接)/ `range`(单元格范围链接)/ `none`(显式清除) | +| `link` | string | type=path/sheet/range 时必填。`path` 为 URL;`sheet` 为工作表 ID 或名称;`range` 为 A1 表示法 | +| `text` | string | 可选显示文本。通常只传 cell 的 `text`,不用重复传 `hyperlink.text` | + +注意: +- 不传 `hyperlink` 字段同于 “保留原超链接”,无需先 read 再回传 +- Agent 调用统一使用 `hyperlink: {type:"none"}` 清除超链接;底层 REST 兼容 `hyperlink:null`,但 MCP schema / 网关可能过滤 null 字段,不要把 null 当默认写法 +- `hyperlink` 可以不带 `type/text` cell 单独出现,用于只设置或清理链接并保留原值 +- 不要把 `hyperlink` 和 `type:"richText"` 混用;整格链接用 `hyperlink`,片段链接用 richText 子项 `type:"link"` + +### type=richText(富文本:片段链接 / 附件 / 图片 / 多片段组合) + +```json +{ "type": "richText", "texts": [ ...子项数组... ] } +``` + +`texts` 子项 `type` 枚举与字段: + +| 子项 type | 必填字段 | 可选字段 | 说明 | +|-----------|---------|---------|------| +| `text` | `text`(字符串) | `style` | 普通文本片段 | +| `link` | `text` + `link`(都非空字符串) | `subType` / `style` | 富文本片段链接。`subType` 默认为 `path`;`path` 的 `link` 是 URL,`sheet` 的 `link` 是真实工作表名称,`range` 的 `link` 是 A1 表示法(如 `Sheet1!A1:B2`) | +| `attachment` | `text` + `resourceId` + `mimeType` | `size`(字节数) | 附件。`text` 是显示文件名,`resourceId` 通过 `dws sheet media-upload` 获取 | +| `image` | `resourceId` + `resourceUrl` | `text`(建议传 `""`) / `width` / `height` | 图片。两个 resource 字段都通过 `dws sheet media-upload` 获取;像素 | + +### style 子结构(仅 richText 子项的 `text` / `link` 类型支持) + +用于 richText 内部片段级样式,实现同一单元格内不同文字有不同样式(如部分文字红色加粗)。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `bold` | boolean | 加粗 | +| `italic` | boolean | 斜体 | +| `underline` | boolean | 下划线 | +| `strike` | boolean | 删除线 | +| `color` | string | 字体颜色,16 进制色值(如 `#FF0000`) | +| `size` | number | 字号,正整数 | + +**richText link 的 `subType`**: + +```json +{ "type": "link", "text": "钉钉", "link": "https://dingtalk.com", "subType": "path" } +{ "type": "link", "text": "工作表", "link": "Sheet2", "subType": "sheet" } +{ "type": "link", "text": "明细区域", "link": "Sheet2!A1:B20", "subType": "range" } +``` + +- 不传 `subType` 时按 `path` 处理,适合外部 URL +- `subType:"sheet"` / `"range"` 需要使用真实工作表名称或 A1 范围;未知时先 `dws sheet list --node --format json`,禁止猜 `Sheet1` +- 这只影响富文本片段链接;整格链接仍使用 cell-level `hyperlink` +- 写入后用 `range read` 读取时,`richText.texts[].subType` 会按同样语义返回;不要把 richText 片段链接和整格 `hyperlink` 混淆 + +注意:`type:"text"` 的顶层旧 `style` 字段只作为历史兼容存在,新请求不要使用;整个单元格的字体样式请用 `cellStyles`,同一 cell 内分段样式才用 richText 子项 `style`。 + +### dataValidation 子结构(可选,与 type 同级) + +任何 cell 可附加 `dataValidation` 字段,在写值的同时设置数据校验。支持两种类型: + +> **dataValidation 三种语义**: +> - **不传 `dataValidation` 字段** → 自动保留原 DV(无需 read 后回写) +> - **`dataValidation: {"type":"none"}`** → 显式清除该单元格 DV +> - **`dataValidation: {"type":"dropdown"/"checkbox", ...}`** → 写新 DV(覆盖原 DV) +> +> `{}` 跳过和不传 dataValidation 字段都会保留原 DV。 + +**dropdown(下拉列表)**: +```json +{ "type": "text", "text": "High", "dataValidation": { "type": "dropdown", "options": [{"value":"High","color":"#00ff00"},{"value":"Low","color":"#ff0000"}], "enableMultiSelect": false } } +``` +- `options`:必填,`[{value, color?}]` 数组 +- `enableMultiSelect`:可选,是否多选,默认 false + +**checkbox(复选框)**: +```json +{ "dataValidation": { "type": "checkbox", "checked": true } } +``` +- `checked`:可选,初始勾选状态,默认 false +- checkbox 通常不需要 type/text(保留原值),也可以和 `type:"text"` 共存 + +**翻译场景示例**(一次调用更新文本 + 翻译 dropdown 选项 + 跳过 checkbox): +```bash +dws sheet range update --node NODE_ID --sheet-id SHEET_ID --range "A1:C1" \ + --values '[[{"type":"text","text":"High","dataValidation":{"type":"dropdown","options":[{"value":"High"},{"value":"Medium"},{"value":"Low"}]}},{},{"type":"text","text":"Translated"}]]' +``` + +### cellStyles 子结构(可选,与 type 同级) + +任何 cell 可附加 `cellStyles` 字段,在写值的同时设置 cell-level 样式。与 `style`(内联文本样式)的区别见下方说明。 + +```json +{ "type": "text", "text": "重要", "cellStyles": { "fontWeight": "bold", "backgroundColor": "#FFF2CC" } } +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `fontWeight` | string | `bold` / `normal` | +| `fontColor` | string | 字体颜色,`#RRGGBB` | +| `fontSize` | number | 字号 | +| `fontStyle` | string | `italic` / `normal` | +| `backgroundColor` | string | 背景色,`#RRGGBB` | +| `horizontalAlignment` | string | `left` / `center` / `right` / `general` | +| `verticalAlignment` | string | `top` / `middle` / `bottom` | +| `wordWrap` | string | `overflow` / `clip` / `autoWrap` | +| `numberFormat` | string | 数字格式 code,如 `@`、`#,##0.00`、`yyyy/m/d`;格式 code 说明见 [「number-format 格式 code」](sheet-style-format.md#number-format-格式-code) | +| `textUnderline` | boolean | 下划线 | +| `textLineThrough` | boolean | 删除线 | + +所有字段均可选,只传需要设置的字段。也可以不传 `type`/`text`,仅用 `{cellStyles:{...}}` 对已有单元格追加样式(保留原值)。 + +选择 `numberFormat` 前,先阅读 [「number-format 格式 code」](sheet-style-format.md#number-format-格式-code),确认目标格式类型对应的 code。 + +长数字标识符请显式设置文本格式:商品 ID、规格 ID、SKU、订单号、手机号、工号等字段建议写成 `{"type":"text","text":"528545015680","cellStyles":{"numberFormat":"@"}}`。仅把值写成文本不一定能阻止常规格式展示;`@` 可以避免 11 位以上数字形态 ID 被显示成科学计数法。`range append` 不支持随行传 `cellStyles`,追加后请对返回的 `a1Notation` 或目标 ID 列执行 `range set-style --number-format "@"`。 + +**`cellStyles` vs `style` vs `set-style` 的区别**: + +| 方式 | 适用场景 | 写在哪里 | 作用范围 | +|------|---------|---------|---------| +| `style`(richText 片段样式) | 同一 cell 内不同文字有不同字体样式 | richText 子项(`text`/`link` 类型)的 `style` | 文本片段级别 | +| `cellStyles`(cell-level 样式) | 背景色、对齐、换行、数字格式等 | cell 的 `cellStyles` | 整个单元格 | +| `set-style` / `batch-set-style` | 批量设置整片区域的样式 | 单独命令,与 `range update` 分开调用 | 指定 range 内所有单元格 | + +典型用法: +- 写入少量单元格 + 样式 → 用 `range update` 的 `cellStyles`,一次调用搞定 +- 批量刷整片区域统一样式 → 用 `set-style`(如 "给 A1:Z1 表头加粗居中") +- 文本内部分段样式(如"重要"二字红色加粗,其余正常) → 用 `type:"richText"` + 子项 `style` + +### 混合示例(普通文字 + 带样式片段链接) + +```json +{ + "type": "richText", + "texts": [ + { "type": "text", "text": "请访问 " }, + { "type": "link", "text": "钉钉官网", "link": "https://dingtalk.com", "style": { "color": "#0080FF", "underline": true } } + ] +} +``` + +### 重要约束 + +- 不再支持 `{type:"number"}` / `{type:"boolean"}` / `{type:"null"}` —— MCP `complexValues` 仅接受 `text` / `richText` 两种 type,或 `{}` 跳过。数字 / 布尔走 `{type:"text","text":"<字符串形式>"}` +- 不支持直接传入原始值(字符串、数字、布尔、null、空字符串);`null` 不等同于 `{}`,`null` 会报错 +- 维度必须与 `--range` 范围完全一致,例如 `--range "A1:B3"` 需要 3 行 2 列的数组 +- 清理整格超链接使用 `{"hyperlink":{"type":"none"}}`;不要使用 `{"hyperlink":null}` 作为 agent 默认调用形态 +- 写图片到单元格建议直接用 `dws sheet write-image`(更简洁) +- 清空整片区域请用 `dws sheet range clear`;只清空单个 cell 可在 `--values` 中传 `{"type":"text","text":""}` + +## 核心工作流 + +```bash +# ── 工作流 1: 创建表格并写入数据 ── + +# 1. 创建表格文档 — 提取 nodeId +dws sheet create --name "销售数据" --format json + +# 2. 查看工作表列表 — 提取 sheetId +dws sheet list --node --format json + +# 3. 写入表头和数据 +dws sheet range update --node --sheet-id --range "A1:C1" \ + --values '[[{"type":"text","text":"姓名"},{"type":"text","text":"部门"},{"type":"text","text":"销售额"}]]' --format json + +dws sheet range update --node --sheet-id --range "A2:C4" \ + --values '[[{"type":"text","text":"张三"},{"type":"text","text":"销售部"},{"type":"text","text":"50000"}],[{"type":"text","text":"李四"},{"type":"text","text":"市场部"},{"type":"text","text":"38000"}],[{"type":"text","text":"王五"},{"type":"text","text":"销售部"},{"type":"text","text":"62000"}]]' --format json + +# ── 工作流 4: 写入数据并设置样式 ── + +# 1. 写入数据 +dws sheet range update --node --sheet-id --range "A1:C3" \ + --values '[[{"type":"text","text":"商品"},{"type":"text","text":"单价"},{"type":"text","text":"数量"}],[{"type":"text","text":"苹果"},{"type":"text","text":"5.5"},{"type":"text","text":"100"}],[{"type":"text","text":"香蕉"},{"type":"text","text":"3.2"},{"type":"text","text":"200"}]]' --format json + +# 2. 设置数字格式(人民币)——两种方式均可: +# 方式 A: 写值时通过 cellStyles 一步到位 +# 方式 B: 单独用 set-style 设置(适合只改格式不改值) +dws sheet range set-style --node --sheet-id --range "B2:B3" \ + --number-format '"¥"#,##0.00' --format json + +# 3. 长数字 ID 写值时同步设置文本格式,避免科学计数法 +dws sheet range update --node --sheet-id --range "D2:D3" \ + --values '[[{"type":"text","text":"528545015680","cellStyles":{"numberFormat":"@"}}],[{"type":"text","text":"528545015681","cellStyles":{"numberFormat":"@"}}]]' --format json + +# 4. 写入单元格级超链接 +dws sheet range update --node --sheet-id --range "D1" \ + --values '[[{"type":"text","text":"详情","hyperlink":{"type":"path","link":"https://dingtalk.com"}}]]' --format json + +# ── 工作流 5: 追加数据 ── + +# 1. 获取工作表列表 +dws sheet list --node --format json + +# 2. 查看工作表详情(确认列结构) +dws sheet info --node --sheet-id --format json + +# 3. 追加单行数据 +dws sheet append --node --sheet-id \ + --values '[["张三","销售部",50000]]' --format json + +# 4. 追加多行数据 +dws sheet append --node --sheet-id \ + --values '[["李四","市场部",38000],["王五","销售部",62000]]' --format json +``` + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `create` | `nodeId` | list / info / new / range update / append / csv-put 的 --node | +| `list` | 工作表的 `sheetId` | range update / append / csv-put 的 --sheet-id | +| `new` | 新工作表的 `sheetId` | range update / append / csv-put 的 --sheet-id | +| `info` | `rowCount` / `lastNonEmptyRow` / `mergedRanges` | 确定数据范围、追加写入起始行、识别合并单元格结构 | +| `append` | `a1Notation` 追加数据所在范围 | 确认追加位置 | +| `csv-put` | `a1Notation` 实际写入的单元格范围 | 确认写入位置和范围 | + +## 注意事项 + +- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询真实的 `sheetId` / 工作表名称后再调用,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等);用户仅给出工作表名称时,也应通过 `list` 校验该名称是否存在,避免名称大小写或拼写不一致导致失败 +- ★ **`range update` 维度校验(强制)**:调用 `range update` 写入 `--values` 时,必须严格校验二维 JSON 数组的行数与列数与 `--range` 指定的范围完全一致: + - 例如 `--range "A1:C3"` 表示 3 行 × 3 列,`--values` 必须是 `[[v1,v2,v3],[v4,v5,v6],[v7,v8,v9]]` 这样 3×3 的数组 + - `--range "A1"` 表示 1 行 × 1 列,`--values` 必须是 `[[v]]` + - 维度不足请按行 / 列补齐为同等大小;不需要修改的位置用 `{}` 跳过(保留原值),需要清空的位置用 `{"type":"text","text":""}`;禁止出现各行列数不一致或与 `--range` 不匹配的情况,否则调用会直接报错 + - 如需写整格超链接,把 `{"type":"text","text":"...","hyperlink":{"type":"path","link":"..."}}` 放进 `--values` 二维数组对应的单元格里;富文本片段链接才使用 richText 子项 `type:"link"` +- ★ **清空区域优先用 `range clear`(强制)**:需要清空整片区域时必须使用 `range clear`,禁止用 `range update` 模拟。仅在 `range update` 写入混合数据时个别 cell 需要清空,才在 `--values` 中用 `{"type":"text","text":""}` +- ★ **不再支持 `{type:"number"}` / `{type:"boolean"}` / `{type:"null"}`(强制)**:MCP `complexValues` 仅接受 `type:"text"` 与 `type:"richText"` 两种,CLI 会在本地直接拦截非法 type 并报错。写数字 / 布尔请用 `{"type":"text","text":"<字符串形式>"}`(服务端按内容自动识别),不要再用旧的 `value` 字段 +- **dataValidation 三语义**:不传字段=保留;`{type:"none"}`=清除;`{type:"dropdown"/"checkbox",...}`=覆盖。无需先 read 再回传,引擎自动保留原 DV +- **hyperlink 三语义**:不传字段=保留;`{type:"none"}`=清除;`{type:"path"/"sheet"/"range",...}`=覆盖。Agent 调用不要使用 `hyperlink:null` +- ★ **单次调用上限(强制)**:`range update` / `set-style` 行数 ≤ 1000,单元格总数建议 ≤ 5000(硬限 30000) +- ★ **大批量纯值写入用 `csv-put` 不用 `range update`**:当写入纯值(无公式、无超链接、无富文本对象)且数据量较大时(>5 行或 >20 单元格),必须使用 `csv-put`。`csv-put` 接受 CSV 文本直接写入,无需构造二维 JSON 数组,支持自动扩容,更简洁高效。仅在需要写入公式、单元格级超链接、富文本对象,或仅更新少量单元格时才使用 `range update` +- `range update` 必填 `--values`;单元格级超链接通过 cell 的 `hyperlink` 字段表达,附件 / 图片 / 带样式片段通过 `--values` 内的 richText 富格式表达,CLI 不再有 `--hyperlinks` 参数 +- `range update` 职责边界:`range update` 写入单元格内容(文本 / 公式 / 富文本对象),支持通过 `cellStyles` 附带 per-cell 样式(背景色 / 字号 / 对齐等)。但批量刷整片区域的统一样式时,应使用 `dws sheet range set-style`(如 "给表头加粗居中")或 `dws sheet range batch-set-style --batch `。两种方式各有适用场景:少量 cell 写值 + 样式一步到位用 `cellStyles`;大面积统一样式用 `set-style` +- `append` 自动定位到最后一行有数据的位置下方插入,无需手动计算行号 +- `append` 的 `--values` 二维数组中每行的列数必须一致,否则会报错。如果用户提供的数据中各行长度不同,必须先将短行用空字符串 `""` 补齐到与最长行相同的列数后再调用。追加的数据列数也应与工作表已有数据列数保持一致 +- `append` vs `range update`:追加新行用 `append`,修改已有单元格用 `range update` +- ★ **`append` / `csv-put` 不支持 `{}` skip、`dataValidation`、富文本、公式**:这些能力仅限 `range update`。`append` 和 `csv-put` 只接受原始值(字符串/数字/布尔),走的是不同的 MCP tool(`append_rows` / `set_range_from_csv`)。需要写入公式、超链接、下拉列表或跳过部分单元格时,必须使用 `range update` diff --git a/skills/mono/references/products/wiki.md b/skills/mono/references/products/wiki.md index d6f0693a..7c61c7b0 100644 --- a/skills/mono/references/products/wiki.md +++ b/skills/mono/references/products/wiki.md @@ -21,7 +21,6 @@ dws wiki member --help - 参数名不确定时 → 先 `--help`,再调用 - 报错 "unknown flag" 时 → `--help` 确认正确的 flag 名称 - 不确定某个功能是否存在时 → `dws wiki --help` 查看命令列表 -- `workspaceId` 是知识库空间 ID,只能用于 `wiki space/member --workspace`、`doc --workspace` 或 `doc search --workspace-ids`;不要把它传给 `doc list --folder`,也不要使用不存在的 `--space-id` ## 命令总览 @@ -38,6 +37,26 @@ Flags: --icon string 知识库图标标识 (选填) ``` +### 删除知识库 + +> **CAUTION:** 不可逆操作 — 执行前必须向用户确认。 + +``` +Usage: + dws wiki space delete [flags] +Example: + dws wiki space delete --workspace + dws wiki space delete --workspace "https://alidocs.dingtalk.com/i/spaces/xxx/overview" +Flags: + --workspace string 知识库 ID 或 URL (必填) +``` + +将指定知识库移入回收站。删除后知识库会进入回收站,可在回收站中恢复。 + +> **重要约束**: +> - 操作者必须具备知识库的 OWNER 角色。 +> - 删除操作不可逆(从回收站恢复除外),请确认后再执行。 + ### 查看知识库详情 ``` Usage: @@ -99,11 +118,29 @@ Flags: --role string 授予的角色 (必填,大小写敏感,必须全大写): MANAGER (管理者) / EDITOR (可编辑) / DOWNLOADER (可下载) / READER (可阅读) ``` -> **❗ 重要约束**: +> **重要约束**: > - 仅支持 USER 类型。 > - 角色枚举严格大写:MANAGER / EDITOR / DOWNLOADER / READER(OWNER 不可通过此接口添加,知识库创建者默认为所有者)。 > - 操作者需具备知识库的 OWNER 或 MANAGER 权限。 -> - 「我的文档」(myWikiSpace) 是个人空间,**不支持容器级成员管理**;后端会直接拒绝。如果你的目标只是把某篇文档分享给别人,请改用 `dws doc permission add` 在节点级别授权。 +> - 「我的文档」(myWikiSpace) 是个人空间,**不支持容器级成员管理**;后端会直接拒绝。如果你的目标只是把某篇文档分享给别人,请改用 `dws drive permission add` 在节点级别授权。 + +### 移除知识库成员 +``` +Usage: + dws wiki member remove [flags] +Example: + dws wiki member remove --workspace --users uid1 + dws wiki member remove --workspace --users uid1,uid2 +Flags: + --workspace string 目标知识库 ID 或 URL (必填) + --users strings 被移除的用户 userId 列表,逗号分隔 (必填,单次最多 30 个) +``` + +> **重要约束**: +> - OWNER 角色不可通过此接口移除。 +> - 操作者需具备知识库的 OWNER 或 MANAGER 权限。 +> - 移除后相关用户将无法访问该知识库下的内容(除非通过节点级权限另行授权)。 +> - 「我的文档」(myWikiSpace) 是个人空间,**不支持容器级成员管理**。 ### 修改知识库成员角色 ``` @@ -134,34 +171,154 @@ Flags: > 接口不支持游标分页,使用 `--limit` 一次性拉取。 +### 列出知识库节点 +``` +Usage: + dws wiki node list [flags] +Aliases: + list, ls +Example: + dws wiki node list --workspace --format json + dws wiki node list --workspace --folder --format json + dws wiki node list --workspace --limit 20 --cursor --format json +Flags: + --workspace string 知识库 ID (必填) + --folder string 父节点 nodeId (选填,不传则列出根目录) + --limit int 每页数量 (默认 50,最大 50) + --cursor string 分页游标 +``` + +### 在知识库中创建节点 +``` +Usage: + dws wiki node create [flags] +Example: + dws wiki node create --workspace --name "新文档" --format json + dws wiki node create --workspace --name "方案目录" --type folder --format json + dws wiki node create --workspace --name "数据表" --type asheet --folder --format json +Flags: + --workspace string 知识库 ID (必填) + --name string 节点名称 (必填) + --type string 节点类型: adoc / asheet / folder / axls (默认 adoc) + --folder string 父节点 nodeId (选填,不传则在根目录创建) +``` + +### 复制知识库节点 +``` +Usage: + dws wiki node copy [flags] +Example: + dws wiki node copy --workspace --node --format json + dws wiki node copy --workspace --node --folder --format json +Flags: + --workspace string 知识库 ID (必填) + --node string 源节点 ID (必填) + --folder string 目标文件夹 nodeId (选填) +``` + +### 移动知识库节点 +``` +Usage: + dws wiki node move [flags] +Example: + dws wiki node move --workspace --node --folder --format json + dws wiki node move --workspace --node --format json +Flags: + --workspace string 知识库 ID (必填) + --node string 源节点 ID (必填) + --folder string 目标文件夹 nodeId (选填) +``` + +### 删除知识库节点 + +> **CAUTION:** 不可逆操作 — 执行前必须向用户确认。 + +``` +Usage: + dws wiki node delete [flags] +Example: + dws wiki node delete --workspace --node + dws wiki node delete --workspace --node --yes +Flags: + --workspace string 知识库 ID (必填,用于权限校验) + --node string 节点 ID (必填) +``` + +将知识库中的节点移入回收站。权限要求: 对节点有"管理"权限。 + +### 在知识库中搜索节点 +``` +Usage: + dws wiki node search [flags] +Example: + dws wiki node search --workspace --query "方案" --format json + dws wiki node search --workspace --query "周报" --limit 10 --format json + dws wiki node search --workspace --query "设计" --extensions adoc,asheet --format json +Flags: + --workspace string 知识库 ID (必填) + --query string 搜索关键词 (必填) + --extensions string 按文件类型过滤,逗号分隔: adoc,asheet 等 (选填) + --limit int 每页数量 (选填) + --cursor string 分页游标 (选填) +``` + +在指定知识库空间内搜索节点。与 `drive search` 的区别: +- `wiki node search` — 限定在某个知识库空间内搜索(需要 `--workspace`) +- `drive search` — 全局搜索,聚合钉盘 + 文档空间结果 + +### 列出空间(支持钉盘空间类型) + +`wiki space list` 除了支持知识库类型(`orgWikiSpace` / `myWikiSpace`),还支持钉盘空间类型: + +``` +Usage: + dws wiki space list --type orgSpace --format json # 钉盘企业空间 + dws wiki space list --type mySpace --format json # 钉盘「我的文件」 + dws wiki space list --type orgWikiSpace --format json # 知识库(默认) + dws wiki space list --type myWikiSpace --format json # 我的文档 +Flags: + --type string 空间类型: + orgWikiSpace (默认) — 组织知识库 + myWikiSpace — 我的文档个人空间 + orgSpace — 钉盘企业空间 + mySpace — 钉盘「我的文件」 + --limit string 每页数量 1-50 (默认 20) + --cursor string 分页游标 (首页留空) +``` + +> 钉盘空间类型(`orgSpace` / `mySpace`)会自动路由到钉盘 MCP 服务,等同于原 `drive list-spaces`(已 deprecated)。 + ## 意图判断 - 用户说"创建知识库/新建知识库" → `space create` - 用户说"查看知识库/知识库详情" → `space get` - 用户说"我的知识库/知识库列表/有哪些知识库" → `space list` +- 用户说"列出钉盘空间/钉盘团队空间" → `space list --type orgSpace` - 用户说"搜索知识库/找知识库" → `space search` -- 用户说"我的文档/个人空间" → `space search --type myWikiSpace` 或 `space list --type myWikiSpace` -- 用户说"把知识库分享给某人/给某人加入知识库/邀请进知识库" → `member add`(需 `--workspace` + `--users` + `--role`) +- 用户说"我的文档/个人空间" → `space list --type myWikiSpace` +- 用户说"知识库下的文件/知识库里有哪些文档/浏览知识库内容" → `node list`(需 `--workspace`) +- 用户说"在知识库里搜文档/空间内搜索" → `node search`(需 `--workspace` + `--query`) +- 用户说"在知识库里创建文档/新建文件夹" → `node create`(需 `--workspace` + `--name`) +- 用户说"复制知识库里的文档" → `node copy`(需 `--workspace` + `--node`) +- 用户说"移动知识库里的文档" → `node move`(需 `--workspace` + `--node`) +- 用户说"删除知识库里的文档/节点" → `node delete`(需 `--workspace` + `--node`) +- 用户说"把知识库分享给某人/给某人加入知识库/邀请进知识库" → `member add`(需 `--workspace` + `--user` + `--role`) - 用户说"修改某人在知识库的权限/调整成员角色" → `member update` +- 用户说"移除知识库成员/把某人从知识库移除/删除知识库成员" → `member remove`(需 `--workspace` + `--users`) - 用户说"知识库有哪些成员/查看知识库成员" → `member list` +- 用户说"删除知识库/移除知识库/把知识库删了" → `space delete`(需 `--workspace`) -> ** 跨产品路由(重要)**:`dws wiki` 只管知识库容器(space/member),**不提供查看知识库文件/文档的能力**。以下意图必须走 `dws doc`,不要在 wiki 下尝试 `node`/`file`/`list` 等子命令: ->- 用户说"知识库下的文件/知识库里有哪些文档/浏览知识库内容" → 先用 `dws wiki space list` 或 `space search` 拿到 `workspaceId`,再走 **`dws doc list --workspace `** ->- 用户说"读某个知识库里的某篇文档" → 先 `dws wiki space list/search` 拿 `workspaceId`,再 `dws doc search --query "<文档名>" --workspace-ids --format json` 找 `nodeId`,最后 **`dws doc read --node --format json`** ->- 用户说"在知识库里搜文档" → 走 **`dws doc search --workspace-ids `** ->- 用户说"在知识库里创建文档" → 走 **`dws doc create --workspace `** +> **跨产品路由说明**:知识库节点的**内容操作**(读取/编辑/块级操作)仍由 `dws doc` 承担: +>- 用户说"读某个知识库里的某篇文档" → 先 `node list` 拿到 nodeId,再走 **`dws doc read --node `** +>- 用户说"搜文件"(不指定空间) → 走 **`dws drive search`**(全局聚合搜索) -> **禁止反模式**: ->- `dws doc list --space-id `:`doc list` 没有 `--space-id` ->- `dws doc list --folder `:`--folder` 只接受文件夹 `nodeId` / 文件夹 URL,不接受知识库 `workspaceId` ->- `doc get --node `:读取正文使用 `dws doc read --node --format json` ->- 多个知识库同名时,不要默认取第一个;用 `doc list --workspace` / `doc search --workspace-ids` / `doc read` 验证哪个空间包含目标文档或目标文件夹 - -关键区分: -- wiki(知识库空间级管理:创建/查询/列出/搜索/成员管理) vs doc(文档内容级操作:搜索/读写/编辑/节点级权限) -- wiki space(知识库容器) vs drive(钉盘文件存储/上传/下载) -- **wiki member**(容器级,授权整个知识库)vs **doc permission**(节点级,授权单篇文档) - - 「我的文档」**只能用** `doc permission`,不能用 `wiki member` +关键区分(两层模型): +- **wiki node**(空间管理层:节点的列出/创建/复制/移动/删除/搜索)vs **doc**(内容层:读写/编辑/块级/评论/导出)vs **drive**(存储层:文件上传/下载/搜索/权限,不关心格式) +- **wiki node search**(空间内搜索,需 `--workspace`)vs **drive search**(全局搜索,聚合钉盘+文档空间) +- **wiki node create**(在空间中创建空文件实体)vs **doc create**(创建文档并写入内容) +- **wiki member**(容器级,授权整个知识库)vs **doc permission / drive permission**(节点级,授权单篇文档) + - 「我的文档」**只能用** `doc permission` / `drive permission`,不能用 `wiki member` +- **wiki space list --type orgSpace/mySpace**(列出钉盘空间)vs **wiki space list**(默认列出知识库) ## 核心工作流 @@ -175,26 +332,66 @@ dws wiki space list --type myWikiSpace --format json # 搜索知识库 dws wiki space search --query "产品" --format json -# 搜索「我的文档」 -dws wiki space search --type myWikiSpace --format json - # 创建知识库 dws wiki space create --name "新项目文档" --desc "项目相关文档归档" --format json # 查看知识库详情 dws wiki space get --workspace --format json -# ── 工作流: 读取某个知识库里的指定文档 ── +# ── 工作流: 浏览知识库内容 ── + +# 1. 获取知识库 ID +dws wiki space list --format json -# 1. 找知识库空间,取 workspaceId -dws wiki space search --query "评测记录" --format json +# 2. 列出根目录节点 +dws wiki node list --workspace --format json -# 2. 在该知识库内搜索文档,取 nodeId -dws doc search --query "MinHash 学习笔记" --workspace-ids --format json +# 3. 进入子目录 +dws wiki node list --workspace --folder --format json -# 3. 读取正文 +# 4. 读取文档内容(跨到 doc) dws doc read --node --format json +# ── 工作流: 在知识库中创建文档 ── + +# 1. 创建文档节点 +dws wiki node create --workspace --name "新方案" --format json + +# 2. 创建文件夹 +dws wiki node create --workspace --name "方案归档" --type folder --format json + +# 3. 在指定文件夹下创建 +dws wiki node create --workspace --name "子文档" --folder --format json + +# ── 工作流: 在知识库中搜索 ── + +# 在指定知识库内搜索 +dws wiki node search --workspace --query "方案" --format json + +# 按文件类型过滤 +dws wiki node search --workspace --query "周报" --extensions adoc --format json + +# ── 工作流: 列出钉盘空间 ── + +# 列出钉盘企业空间 +dws wiki space list --type orgSpace --format json + +# 获取钉盘「我的文件」 +dws wiki space list --type mySpace --format json + +# ── 工作流: 复制/移动节点 ── + +# 复制节点到另一个文件夹 +dws wiki node copy --workspace --node --folder --format json + +# 移动节点到另一个文件夹 +dws wiki node move --workspace --node --folder --format json + +# ── 工作流: 删除知识库节点 ── + +# 删除节点(会要求确认) +dws wiki node delete --workspace --node + # ── 工作流: 给知识库加成员 ── # 1. 先确认知识库 ID(避免授权到「我的文档」) @@ -205,19 +402,38 @@ dws wiki member add --workspace --users --role EDITOR --format jso # 3. 查看当前成员 dws wiki member list --workspace --format json + +# ── 工作流: 移除知识库成员 ── + +# 1. 查看当前成员 +dws wiki member list --workspace --format json + +# 2. 移除成员 +dws wiki member remove --workspace --users --format json + +# ── 工作流: 删除知识库 ── + +# 1. 确认知识库信息 +dws wiki space get --workspace --format json + +# 2. 删除知识库 +dws wiki space delete --workspace --format json ``` ## 上下文传递表 | 操作 | 从返回中提取 | 用于 | |------|-------------|------| -| `space create` | `workspaceId` | space get 的 --workspace / member add 的 --workspace | -| `space list` | `workspaceId` | space get 的 --workspace / member add 的 --workspace | -| `space search` | `workspaceId` | space get 的 --workspace / member add 的 --workspace | +| `space create` | `workspaceId` | node list / member add 的 --workspace | +| `space list` | `workspaceId` | node list / member add 的 --workspace | +| `space search` | `workspaceId` | node list / member add 的 --workspace | | `space get` | `spaceUrl` | 分享给用户 | -| `member list` | `userId` | member update 的 --users | +| `node list` | `nodeId` | node copy/move/delete 的 --node / `dws doc read` 的 --node | +| `node search` | `nodeId` | node copy/move/delete 的 --node / `dws doc read` 的 --node | +| `node create` | `nodeId` | node copy/move/delete 的 --node / `dws doc read` 的 --node | +| `member list` | `userId` | member update 的 --users / member remove 的 --users | ## 相关产品 -- [doc](./doc.md) — 文档内容级操作(搜索/读写/编辑文档、知识库内文档管理) -- [drive](./drive.md) — 钉盘文件存储/上传/下载 +- [doc](./doc.md) — 内容层:文档读写/编辑/块级操作/评论/导出(仅对自研文档有意义) +- [drive](./drive.md) — 存储层:文件列出/搜索/上传/下载/复制/移动/重命名/删除/权限(不关心文件格式) diff --git a/skills/multi/dingtalk-aitable/references/aitable.md b/skills/multi/dingtalk-aitable/references/aitable.md index 37361df5..f49cfb0c 100644 --- a/skills/multi/dingtalk-aitable/references/aitable.md +++ b/skills/multi/dingtalk-aitable/references/aitable.md @@ -7,9 +7,15 @@ | 资源 | URI 格式 | |------|----------| | Base 文档 | `https://alidocs.dingtalk.com/i/nodes/{baseId}` | +| 指定数据表 | `https://alidocs.dingtalk.com/i/nodes/{baseId}?iframeQuery=sheetId%3D{tableId}` | +| 指定数据表+视图 | `https://alidocs.dingtalk.com/i/nodes/{baseId}?iframeQuery=sheetId%3D{tableId}%26viewId%3D{viewId}` | | 模板预览 | `https://docs.dingtalk.com/table/template/{templateId}` | -> **操作后请返回文档 URI**:每次执行 base list/search/create/get 操作后,从返回数据中提取 `baseId`,拼接为 `https://alidocs.dingtalk.com/i/nodes/{baseId}` 返回给用户。 +> **操作后请返回文档 URI**:返回链接时必须带上当前操作的数据表 tableId,让用户点击后直接看到目标数据表,而不是落在空白的默认表。 +> - 已知 tableId + viewId 时(view create 返回、view get 中提取):拼接 `https://alidocs.dingtalk.com/i/nodes/{baseId}?iframeQuery=sheetId%3D{tableId}%26viewId%3D{viewId}` +> - 已知 tableId 时(table create 返回、base get 中提取、record 操作所用的 tableId):拼接 `https://alidocs.dingtalk.com/i/nodes/{baseId}?iframeQuery=sheetId%3D{tableId}` +> - 仅有 baseId、无明确 tableId 时(如 base list/search):拼接 `https://alidocs.dingtalk.com/i/nodes/{baseId}` +> > 补充:如果 URL 不是来自 `aitable` 命令返回,而是用户直接贴的原始 `alidocs` URL,先按 [链接规范](../url-patterns.md#alidocs-url-类型探测流程) probe,确认是 `able` 后再按 AI 表格处理。 ## 命令索引表 @@ -19,10 +25,9 @@ | 命令 | 用途 | 必填参数 | 路由提醒 | |------|------|----------|----------| | `base list` | 列出最近访问的 Base | — | 仅返回最近访问过的,优先用 `base search` | -| `base search` | 搜索 Base;不传关键词时列出最近 Base | — | 可选 `--query`;不传时走 list_bases | +| `base search` | 按名称搜索 Base | `--query` | 关键词 ≥2 字符 | | `base get` | 获取 Base 信息(含 tables 列表) | `--base-id` | 用户给 URL 时提取末尾 ID | -| `base create` | 创建 Base | `--name` | 创建后直接用返回的 baseId | -| `base copy` | 复制 Base 到目标文件夹 | `--base-id` `--target-folder-id` | 目标必须是 `dws doc folder create/list` 返回的文档文件夹 `nodeId`;不要传钉盘数字 `dentryId`,也不要用手工新建 base/table 代替 | +| `base create` | 创建 Base | `--name` | 创建后直接用返回的 baseId;**默认新建的 base 自带一个空白「数据表」(含 3 行空记录)和一个空白仪表盘**,如需干净的空 base,传 `--template-id 1743` | | `base update` | 更新 Base 名称 | `--base-id` `--name` | — | | `base delete` | 删除 Base | `--base-id` | 不可逆 | @@ -31,8 +36,8 @@ | 命令 | 用途 | 必填参数 | 路由提醒 | |------|------|----------|----------| | `table get` | 获取表结构(字段+视图目录) | `--base-id` | 不传 `--table-ids` 返回全部表 | -| `table create` | 创建数据表 | `--base-id` `--name` | `--fields` 可选;不传时创建空字段表 | -| `table update` | 重命名表 | `--base-id` `--table-id` `--name` | — | +| `table create` | 创建数据表 | `--base-id` `--name` `--fields` | fields 为 JSON 数组,至少 1 个 | +| `table update` | 修改表名 / 备注 / 行命名规则 | `--base-id` `--table-id` + 三选一(`--name` / `--description` / `--record-name-key`) | `--record-name-key` 是固定枚举(如 task/project/event/customer/ji_lu 等),非字段 ID | | `table delete` | 删除表 | `--base-id` `--table-id` | 不可逆 | ### field (字段管理) → 详见 [aitable-field.md](./aitable/aitable-field.md)、[field-properties](./aitable/aitable-field-properties.md) @@ -44,6 +49,30 @@ | `field update` | 更新字段名/配置 | `--base-id` `--table-id` `--field-id` | 不可变更字段类型 | | `field delete` | 删除字段 | `--base-id` `--table-id` `--field-id` | 不可逆 | +#### 搜索字段选项 +``` +Usage: + dws aitable field search-options [flags] +Example: + dws aitable field search-options --base-id --table-id --field-id + dws aitable field search-options --base-id --table-id --field-id --keyword 已完成 + dws aitable field search-options --base-id --table-id --field-id --limit 100 +Flags: + --base-id string Base ID (必填) + --field-id string 目标字段 ID,必须是 singleSelect / multipleSelect 类型 (必填) + --keyword string 模糊搜索关键词,大小写不敏感、contains 匹配 option name;不传返回全部 + --limit int 返回的最大 option 数量,默认 3000(全量),最大 3000 + --table-id string Table ID (必填) +``` + +仅适用于 **singleSelect / multipleSelect** 字段。其他类型(text/number/date/...)调用会返回错误。 + +适用场景: +- options 较多,只想要含某关键词的子集(避免 `field get` 拉取整个字段配置带回所有 options)。 +- 写入 record 前预览选项 id ↔ name 的映射,确认要使用的选项确实存在。 + +> **写 record 时**:`record create / update` 对 singleSelect/multipleSelect 可直接传 option **name**,不需要用本命令。本命令主要用于 **filter** 写法(filters 优先用 option **id**)或选项较多需要精确定位时。 + ### record (记录管理) | 命令 | 用途 | 必读 reference | 路由提醒 | @@ -51,36 +80,66 @@ | `record query` | 查询/搜索记录 | [aitable-record-query.md](./aitable/aitable-record-query.md) | 先 `table get` 拿 fieldId;`--all` 自动翻页;filters 结构见 reference | | `record get` | 按 ID 取记录(`record query --record-ids` 的窄别名) | [aitable-record-query.md](./aitable/aitable-record-query.md) | 已知 recordId 时首选;必填 `--record-ids`(单次最多 100 条);未暴露 filters/sort/query/cursor/limit | | `record create` | 新增记录 | [aitable-record-create.md](./aitable/aitable-record-create.md) | cells key 必须是 fieldId 不是字段名;单次最多 100 条 | -| `record update` | 更新记录(每条独立 cells) | [aitable-record-update.md](./aitable/aitable-record-update.md) | 需先 query 拿 recordId;只传需改字段;`--records` 是 `[{recordId,cells},...]` 数组;同一组值批量更新也用此命令展开 records | +| `record update` | 更新记录(每条独立 cells) | [aitable-record-update.md](./aitable/aitable-record-update.md) | 需先 query 拿 recordId;只传需改字段;`--records` 是 `[{recordId,cells},...]` 数组 | +| `record batch-update` | 批量更新(同一 cells 应用到多条 recordId) | [aitable-record-update.md](./aitable/aitable-record-update.md)、[aitable-cell-value.md](./aitable/aitable-cell-value.md) | 适合"统一标记完成/统一改负责人"等共享 patch 场景;`--cells` 是 JSON object(key=fieldId,value 按字段类型见 cell-value.md),与 record update 的单条 cells 结构完全一致;必填 `--record-ids` `--cells`;单次最多 100 条 | | `record delete` | 删除记录 | [aitable-record-delete.md](./aitable/aitable-record-delete.md) | 不可逆,需先 query 确认 | +| `record history-list` | 查询单条记录的变更历史 | [aitable-record-history.md](./aitable/aitable-record-history.md) | 必填 `--record-id`;分页 `--offset --limit`,limit 范围 [1,50] 默认 20 | +| `record query-empty` | 查询完全没填用户字段的空行 | [aitable-record-query.md](./aitable/aitable-record-query.md) | 一页扫描 `--limit` [1,100] 默认 100;扫完前需用 `--cursor` 翻页(nextCursor 为空才表扫完) | +| `record share-url` | 批量获取记录分享链接 | [aitable-record-share.md](./aitable/aitable-record-share.md) | 必填 `--record-ids`(CSV,单次最多 20 条);可选 `--view-id` 带视图上下文 | +| `record upsert` | 批量创建或更新(按 recordId 是否存在自动拆分) | [aitable-record-upsert.md](./aitable/aitable-record-upsert.md) | --records 同 record update 格式;带 recordId 走 update,不带走 create;单次最多 100 | +| `record primary-doc-get` | 查询记录的主键文档 nodeId | [aitable-primary-doc.md](./aitable/aitable-primary-doc.md) | 返回的 nodeId 可直接用于 `dws doc read/update --node` | +| `record primary-doc-create` | 为记录创建主键文档(幂等) | [aitable-primary-doc.md](./aitable/aitable-primary-doc.md) | fieldId 必须是 primaryDoc 类型;已存在则返回已有 nodeId | ### view (视图管理) | 命令 | 用途 | 必填参数 | 路由提醒 | |------|------|----------|----------| -| `view get` | 获取视图配置 | `--base-id` `--table-id` | 不传 `--view-ids` 返回全部视图 | -| `view list` | 列出全部视图(`view get` 不传 `--view-ids` 的别名) | `--base-id` `--table-id` | 与 `view get` 完全等价;只需视图列表时优先 | -| `view create` | 创建视图 | `--base-id` `--table-id` `--view-type` | 类型: Grid/Kanban/Gantt/Calendar/Gallery/FormDesigner;可选 `--name` 指定视图名称(未传时自动生成)、`--config` 传初始配置 JSON | -| `view update` | 更新视图(**调整字段顺序的入口**) | `--base-id` `--table-id` `--view-id` | `visibleFieldIds` 重排字段顺序 | +| `view get` | 获取视图配置(不传子命令) | `--base-id` `--table-id` | 不传 `--view-ids` 返回全部视图 | +| `view get ` | 获取视图某个属性 | `--view-id` | 12 个:card/timebar/aggregate/filter/sort/group/visible-fields/field-widths(详见 [aitable-view-config.md](./aitable/aitable-view-config.md))+ lock/frozen-cols/row-height/fill-color-rule(详见 [aitable-view-extras.md](./aitable/aitable-view-extras.md)) | +| `view list` | 列出全部视图(`view get` 的别名) | `--base-id` `--table-id` | 与 `view get` 完全等价 | +| `view create` | 创建视图 | `--base-id` `--table-id` `--view-type` | 类型: Grid/Kanban/Gantt/Calendar/Gallery/FormDesigner;**Gantt 创建后必须 `view update timebar` 绑定日期字段** | +| `view update` | 整体更新视图 / 多属性合并更新 | `--base-id` `--table-id` `--view-id` | 可传 `--name --desc --config '{...}'`,**`--config` 路径继续保留** | +| `view update ` | 按属性局部更新(推荐)| `--view-id` + typed flag / `--json` | 12 个:card/timebar/aggregate/field-widths/visible-fields/filter/sort/group/name + frozen-cols/row-height/fill-color-rule | +| `view lock [--off]` | 锁定/解锁视图 | `--base-id` `--table-id` `--view-id` | 默认锁定;`--off` 解锁。详见 [aitable-view-extras.md](./aitable/aitable-view-extras.md) | +| `view duplicate` | 复制视图 | `--base-id` `--table-id` `--view-id` | 可选 `--new-name`;保留源视图全部配置。详见 [aitable-view-extras.md](./aitable/aitable-view-extras.md) | | `view delete` | 删除视图 | `--base-id` `--table-id` `--view-id` | 不可删最后一个/锁定视图 | -> **"移动字段/调整字段顺序"** 在 AI 表格里没有 `field reorder` 命令,必须通过 `view update --config '{"visibleFieldIds":[...]}'` 完成。 +> **优先用 `view get ` / `view update ` 子命令**:每个属性独立命令,typed flag 友好,agent 不必拼 JSON。**`view update --config '{...}'` 仍可用**,适合一次性多属性更新或脚本场景。 -> **view update --config 支持的 key 白名单**(传入其他 key 会报错): -> - `visibleFieldIds` — 视图可见字段列表及顺序(首列字段必须保留在第一位) -> - `filter` — 筛选规则**数组**(⚠️ 注意是数组 `[...]`,不是对象 `{...}`) -> - `sort` — 排序规则**数组** -> - `group` — 分组规则**数组** -> - `fieldWidths` — 列宽映射(仅 Grid 视图有效) -> -> **filter/sort/group 必须传数组格式**,不要和 `record query --filters`(对象格式)混淆。详见 [aitable-filter-sort.md](./aitable/aitable-filter-sort.md) § view update 章节。 -> CLI 会自动容错(对象→数组 wrap),但建议直接使用正确格式。 -> -> 不支持 `formInfo`、`requiredFields`、`conditionalRules` 等 FormDesigner 高级配置,这些 key 会被服务端忽略。 +> **属性按 attr 分类,决定该读哪份子文档**: +> - card / timebar / aggregate / filter / sort / group / visible-fields / field-widths → [aitable-view-config.md](./aitable/aitable-view-config.md) +> - lock / frozen-cols / row-height / fill-color-rule / duplicate → [aitable-view-extras.md](./aitable/aitable-view-extras.md) +> 后一类**不能**塞进 `view update --config '{...}'`,必须用各自专属子命令;如果错传 `flags` / `frozenColCount` / `cellHeight` / `conditionalFormats` 等 key 进 `--config`,CLI 会在 stderr 提示应改用的命令。 -### 表单视图 → 详见 [aitable-form.md](./aitable/aitable-form.md) +> **`view update --config` 支持的 9 个 key**: +> `visibleFieldIds` / `filter` / `sort` / `group` / `fieldWidths`(Grid) / `aggregate`(Grid) / `kanbanCard`(Kanban) / `ganttTimebar`(Gantt) / `galleryCard`(Gallery)。 +> filter/sort/group 必须传**数组**格式(与 `record query --filters` 的对象格式不同;CLI 会自动容错)。其他 key 会被服务端忽略并打 warning。 -悟空命令面不暴露 `form` 命令组;表单按 `viewType=FormDesigner` 的视图处理,创建/查看/更新/删除都使用 `view` 命令。 +### form (表单管理) → 详见 [aitable-form.md](./aitable/aitable-form.md) + +| 命令 | 用途 | 必填参数 | 路由提醒 | +|------|------|----------|----------| +| `form list` | 列出表单视图 | `--base-id` `--table-id` | 详情见 [aitable-form.md](./aitable/aitable-form.md) | +| `form get` | 按 viewId 取单个表单详情 | `--base-id` `--table-id` `--view-id` | — | +| `form create` | 创建表单视图 | `--base-id` `--table-id` `--name` | — | +| `form update` | 更新表单配置 | `--base-id` `--table-id` `--view-id` | title/name/description 至少一项 | +| `form delete` | 删除表单 | `--base-id` `--table-id` `--view-id` | 不可逆 | +| `form field list/update/hide` | 表单字段管理 | — | 详情见子文档 | +| `form questions create/delete` | 题目管理(=field create/delete) | — | 详情见子文档 | +| `form share get/update` | 表单分享配置 | — | 详情见子文档 | + +> **创建表单**有两种等价方式:`form create --name "..."`(推荐)或 `view create --view-type FormDesigner --name "..."`。 + +### workflow (自动化工作流) → 详见 [aitable-workflow.md](./aitable/aitable-workflow.md) + +| 命令 | 用途 | 必填参数 | 路由提醒 | +|------|------|----------|----------| +| `workflow list` | 列出 Base 下所有工作流 | `--base-id` | 支持 `--limit [1,100]` / `--offset >=0`;list 出参字段叫 `flowId` | +| `workflow get` | 获取单个工作流详情(含 flowSchema) | `--base-id` `--workflow-id` | `--workflow-id` 接受 list 里的 `flowId`(同值) | +| `workflow enable` | 启用工作流 | `--base-id` `--workflow-id` | 返回 `{enabled: true}` 是动作确认;要确认真启用看 list 的 `status` | +| `workflow disable` | 禁用工作流(高危) | `--base-id` `--workflow-id` `--yes` | 影响业务自动化,建议二次确认;status 变 STOP | + +> **当前不支持通过 CLI 新建/修改/删除工作流**,请去 AI 表格 Web 端(数据表页面 → 自动化)配置。 ### dashboard & chart → 详见 [aitable-dashboard-chart.md](./aitable/aitable-dashboard-chart.md) @@ -88,6 +147,7 @@ |------|------| | `dashboard get/create/update/delete` | 仪表盘管理 | | `dashboard config-example` | 查看仪表盘配置模板 | +| `dashboard arrange` | 自动重排仪表盘图表布局(智能填满网格,避免空缺) | | `chart get/create/update/delete` | 图表管理 | | `chart widgets-example` | 查看图表 widgets 配置模板 | @@ -111,15 +171,162 @@ |------|------|----------| | `template search` | 搜索模板 | `--query` | -## 评测执行硬约束 +### advperm (高级权限/自定义角色) → 详见 [aitable-advperm.md](./aitable/aitable-advperm.md) + +| 命令 | 用途 | 必填参数 | 路由提醒 | +|------|------|----------|----------| +| `advperm enable` | 开启 Base 高级权限总开关 | `--base-id` | 不开启时角色规则不生效 | +| `advperm disable` | 关闭 Base 高级权限总开关(高危) | `--base-id` `--yes` | 关闭后全员回退默认权限 | +| `advperm role-list` | 列出 Base 下所有角色 | `--base-id` | 同时返回自定义角色和系统角色;`roleType == "custom"` 是自定义,前缀 `system_` 是系统角色 | +| `advperm role-get` | 获取单角色完整配置 | `--base-id` `--role-id` | 含 subRoles 与字段/行级规则 | +| `advperm role-create` | 创建自定义角色 | `--base-id` `--name` | 可选 `--sub-roles` 同时指定子角色权限规则 | +| `advperm role-update` | 增量更新自定义角色(PATCH) | `--base-id` `--role-id` | 未传字段不变;`--sub-roles` 按 (targetId,targetType) 合并 | +| `advperm role-delete` | 删除自定义角色 | `--base-id` `--role-id` `--yes` | 不可逆;系统角色禁删;**调用者必须是该 AI 表格的管理员/Owner**,非管理员会得到 401 AUTH_ERROR | + +> **角色 CRUD 已全支持**:create/get/list/update/delete 都可走 CLI。 +> 所有写命令(enable/disable/role-create/role-update/role-delete)需要 Base 管理员权限;非管理员只能调 `role-list` / `role-get`(只读)。 +> "角色 ↔ 成员"绑定当前 CLI 不支持,仍需在 AI 表格 Web 端 → Base 设置 → 高级权限面板手动完成。 + +### section (文件夹与节点管理) + +> 用于在 Base 的导航树中组织 table / dashboard / 表单视图 / 文档等节点(类似文件夹)。 +> 操作前建议先用 `section list-nodes` 拿到 nodeId / sectionId 与父级关系。 + +#### 创建文件夹 +``` +Usage: + dws aitable section create [flags] +Example: + dws aitable section create --base-id --name 我的文件夹 + dws aitable section create --base-id --name 子文件夹 --parent-section-id --index 0 +Flags: + --base-id string Base ID (必填) + --name string 文件夹名称 (必填) + --parent-section-id string 父文件夹 ID;不传或空字符串表示创建在 Base 根目录下 + --index int 在父文件夹下的目标位置(0-based);不传则追加到末尾 +``` + +返回 `data.sectionId` 与 `data.name`。 + +#### 重命名文件夹 +``` +Usage: + dws aitable section rename [flags] +Example: + dws aitable section rename --base-id --section-id --new-name 新名称 +Flags: + --base-id string Base ID (必填) + --section-id string 目标文件夹 ID (必填) + --new-name string 新的文件夹名称 (必填) +``` + +#### 删除文件夹 +``` +Usage: + dws aitable section delete [flags] +Example: + dws aitable section delete --base-id --section-id +Flags: + --base-id string Base ID (必填) + --section-id string 目标文件夹 ID (必填) +``` + +> **注意**:删除不可逆;删除前可先用 `section list-empty` 确认是否为空文件夹。 + +#### 调整文件夹顺序 +``` +Usage: + dws aitable section reorder [flags] +Example: + dws aitable section reorder --base-id --section-id --target-index 0 +Flags: + --base-id string Base ID (必填) + --section-id string 目标文件夹 ID (必填) + --target-index int 目标位置(0-based)(必填) +``` -- 多轮任务必须执行到用户要求的最后一步;不要只回复"现在开始/下一步执行",也不要在创建 base/table/field 后提前结束。 -- 每个写操作后用 `base get`、`table get`、`field get`、`record query` 或对应 `view get/list` 读回验证真实 ID 与结果。 -- 字段批量 JSON 推荐 `fieldName`;CLI 兼容 `name`,但 skill 生成时不要主动使用 `name`。字段类型统一用小写/规范值,如 `text`、`number`、`singleSelect`、`attachment`。 -- 成员/负责人字段类型使用 `user`,不要生成 `member`。 -- 复制 AI 表格必须调用 `dws aitable base copy --base-id --target-folder-id --format json`。目标目录必须是 `dws doc folder create` 或 `dws doc list` 返回的文档文件夹 `nodeId`;不要传 `drive list` 返回的数字 `dentryId`,不要用新建 base/table 的手工方式代替 `base copy`。 -- 用户未指定目标文件夹时:先 `dws doc info --node --format json` 取 `workspaceId`,再 `dws doc folder create --workspace --name "AI表格副本" --format json` 创建目标文件夹,最后把返回的 `nodeId` 传给 `base copy`。 -- 导入 Excel/CSV 前先用 `find` 或 `ls` 确认真实文件路径;遇到中文文件名乱码或路径不匹配时,重新查找实际文件,不要停在解释阶段。 +> 在**当前父文件夹下**调整展示顺序。跨父级移动请用 `section move-node`。 + +#### 列出空文件夹 +``` +Usage: + dws aitable section list-empty [flags] +Example: + dws aitable section list-empty --base-id +Flags: + --base-id string Base ID (必填) +``` + +返回 `data.items: [{sectionId, name, parentSectionId}]` 与 `data.total`,用于清理或诊断导航树(parentSectionId 为空串表示在根目录下)。 + +#### 列出全部节点 +``` +Usage: + dws aitable section list-nodes [flags] +Example: + dws aitable section list-nodes --base-id +Flags: + --base-id string Base ID (必填) +``` + +返回 `data.items: [{nodeId, nodeType, parentSectionId, name?}]` 与 `data.total`,涵盖文件夹 / AI 表格 / 表单视图 / 仪表盘 / 文档 / 查询视图。 + +> **与其他命令的关联**:是 `section move-node` / `section reorder` 的前置定位命令——先用它拿到 nodeId 与 parentSectionId。 + +#### 移动节点 +``` +Usage: + dws aitable section move-node [flags] +Example: + dws aitable section move-node --base-id --node-id --new-parent-section-id + dws aitable section move-node --base-id --node-id --new-parent-section-id "" --target-index 0 +Flags: + --base-id string Base ID (必填) + --node-id string 要移动的节点 ID(文件夹/AI表格/表单视图/仪表盘/文档/查询视图)(必填) + --new-parent-section-id string 目标父文件夹 ID;空字符串表示移到 Base 根目录 (必填) + --target-index int Base 内节点的全局位置(0-based);不传则不调整 +``` + +> 服务端自动识别节点类型,无需区分文件夹与非文件夹。返回 `data.nodeId / newParentSectionId / nodeType`。 +> 对文件夹节点带 `--target-index` 时会先 move 再 reorder,中间失败会返回 `MOVE_OK_REORDER_FAILED`,可用 `section reorder` 重试。 + +## 复杂操作 + +### 仪表盘 / 图表(建议顺序) + +```bash +# 1) 先看配置模板(JSONC) +dws aitable dashboard config-example --format json +dws aitable chart widgets-example --format json + +# 2) 先拿 dashboard,再拿 chart 详情 +dws aitable dashboard get --base-id --dashboard-id --format json +dws aitable chart get --base-id --dashboard-id --chart-id --format json +``` + +要点: + +- `dashboard get` 返回的 `charts[].chartId` 可直接给 `chart get` 使用。 +- `dashboard share get` 可能返回 `404`(资源不存在或未开通),需按可重试错误处理,不要误判为参数拼错。 +- `chart share get` 可正常返回 `enabled/shareUrl`,用于分享状态判断。 + +### 导出数据(两阶段轮询) + +`export data` 常见为异步任务:首次调用可能只返回 `taskId`,需要继续轮询。 + +```bash +# 第一步:创建任务(按 scope 传必要参数) +dws aitable export data --base-id --scope table --table-id --format excel --timeout-ms 1000 + +# 第二步:拿 taskId 继续轮询,直到返回 downloadUrl +dws aitable export data --base-id --task-id --timeout-ms 3000 +``` + +参数约束 + +- `scope=all`:只需 `base-id` +- `scope=table`:必须 `table-id` +- `scope=view`:必须同时 `table-id + view-id` ## 意图判断 @@ -127,14 +334,14 @@ - 查看/查找/列表 → `base search`(优先)或 `base list`(仅浏览最近访问) - 详情 → `base get` - 创建 → `base create` -- 复制 → `base copy`,必须调用 `dws aitable base copy --base-id --target-folder-id --format json`;若无目标文件夹,先 `doc info --node ` 取 `workspaceId`,再 `doc folder create --workspace ` 创建文档文件夹作为目标。服务端返回 `Invalid target folder ID` 时,改用 `doc folder create` 新建目标文件夹后重试一次;不要手工重建副本。 - 修改 → `base update` - 删除 → `base delete` 用户说"数据表/子表/table": - 查看 → `table get` - 创建 → `table create` -- 重命名 → `table update` +- 重命名 / 改备注 / 改行命名规则 → `table update`(三选一:`--name` / `--description` / `--record-name-key`) +- 用户说"行命名规则/记录别名/卡片显示成 task/project/event 这种" → `table update --record-name-key <枚举键>`,**中文 → 枚举键**对照见 [aitable-record-name-key.md](./aitable/aitable-record-name-key.md) - 删除 → `table delete` 用户说"字段/列/column": @@ -145,19 +352,35 @@ 用户说"记录/行/数据/row": - 查看/搜索 → `record query`(读 [aitable-record-query.md](./aitable/aitable-record-query.md)) +- 找空行 / 没填东西的行 → `record query-empty`(读 [aitable-record-query.md](./aitable/aitable-record-query.md)) - 已知 recordId 反查字段值 → `record get`(按 ID 取专用,等价 `record query --record-ids`) - 添加/写入 → `record create`(读 [aitable-record-create.md](./aitable/aitable-record-create.md)) - 修改/更新(每条独立 cells) → `record update`(读 [aitable-record-update.md](./aitable/aitable-record-update.md)) -- **批量更新同一字段值**(统一标记/统一改值) → `record update --records '[{"recordId":"rec1","cells":{...}},{"recordId":"rec2","cells":{...}}]'` +- **批量更新同一字段值**(统一标记/统一改值) → `record batch-update --record-ids ... --cells '{...}'` - 删除 → `record delete` +- **查记录的字段变更历史 / 操作审计** → `record history-list`(读 [aitable-record-history.md](./aitable/aitable-record-history.md)) +- **取记录分享链接 / 把这行发给同事** → `record share-url`(读 [aitable-record-share.md](./aitable/aitable-record-share.md)) +- **不知道有没有 → 有就改、没有就建** → `record upsert`(读 [aitable-record-upsert.md](./aitable/aitable-record-upsert.md)) 用户说"视图/view": - 列出/查看全部视图 → `view list`(或 `view get` 不传 --view-ids,二者等价) - 看某个视图详情 → `view get --view-ids ` - 创建 → `view create` - 修改(含"调整字段顺序/隐藏字段") → `view update --config '{"visibleFieldIds":[...]}'` +- 修改某一项配置(filter/sort/group/card/timebar/aggregate 等)→ `view update `(读 [aitable-view-config.md](./aitable/aitable-view-config.md)) +- 锁定 / 冻结列 / 行高 / 数据高亮规则 / 复制视图 → 读 [aitable-view-extras.md](./aitable/aitable-view-extras.md) - 删除 → `view delete` +用户说"锁定视图/解锁视图/lock view" → `view lock` / `view lock --off`,详见 [aitable-view-extras.md](./aitable/aitable-view-extras.md) + +用户说"冻结列/冻结首列/frozen columns" → `view update frozen-cols --count N`,详见 [aitable-view-extras.md](./aitable/aitable-view-extras.md) + +用户说"行高/单元格高度/紧凑模式/cell height" → `view update row-height --cell-height N`(合法档位 32/56/88/128),详见 [aitable-view-extras.md](./aitable/aitable-view-extras.md) + +用户说"数据高亮/条件格式/单元格上色/fill color rule" → `view update fill-color-rule --json '[...]'`,详见 [aitable-view-extras.md](./aitable/aitable-view-extras.md) + +用户说"复制视图/duplicate view" → `view duplicate --view-id ... [--new-name ...]`,详见 [aitable-view-extras.md](./aitable/aitable-view-extras.md) + 用户说"筛选/过滤/filter" → 读 [aitable-filter-sort.md](./aitable/aitable-filter-sort.md) 用户说"统计/分析/聚合/TOP N/全量" → 读 [aitable-data-analysis-sop.md](./aitable/aitable-data-analysis-sop.md) @@ -166,16 +389,33 @@ 用户说"查找引用/lookup/filterUp/跨表" → 读 [aitable-formula-guide.md](./aitable/aitable-formula-guide.md)(§5.4 跨表引用) -用户说"表单/form/收集表/问卷/催办填写" → 读 [aitable-form.md](./aitable/aitable-form.md),使用 `view create --view-type FormDesigner` +用户说"表单/form/收集表/问卷/催办填写" → 读 [aitable-form.md](./aitable/aitable-form.md) + +用户说"自动化/工作流/流程/触发/automation/workflow" → 读 [aitable-workflow.md](./aitable/aitable-workflow.md) +- 看 Base 里有哪些流程 / 哪些在跑 → `workflow list`(看 `recordCount` / `runningCount`) +- 看某个流程具体配置(触发条件、动作步骤) → `workflow get` +- 启用流程 → `workflow enable` +- 临时停掉流程(调试 / 数据迁移)→ `workflow disable --yes` +- **新建 / 修改 / 删除流程**:当前不支持,引导用户到 AI 表格 Web 端 → 数据表 → 自动化 面板手动完成 用户说"仪表盘/图表/chart" → 读 [aitable-dashboard-chart.md](./aitable/aitable-dashboard-chart.md) +用户说"仪表盘排版乱了/图表对不齐/重新排布/自动布局/美化仪表盘" → `dashboard arrange`(读 [aitable-dashboard-chart.md](./aitable/aitable-dashboard-chart.md)) + 用户说"附件/上传文件" → 读 [aitable-attachment.md](./aitable/aitable-attachment.md) 用户说"导入/导出/import/export" → 读 [aitable-export-import.md](./aitable/aitable-export-import.md) 用户说"模板" → `template search` +用户说"高级权限/角色/权限控制/谁能看/谁能改" → 读 [aitable-advperm.md](./aitable/aitable-advperm.md) +- 开/关高级权限 → `advperm enable` / `advperm disable --yes` +- 看角色配置 → `advperm role-list` 或 `advperm role-get` +- 建角色(可同时指定子角色权限) → `advperm role-create --name ... --sub-roles '[...]'` +- 改角色名 / 改子角色权限(PATCH 语义,未传字段不变) → `advperm role-update --role-id ... [--name ...] [--sub-roles '[...]']` +- 删角色 → `advperm role-delete --yes` +- **角色 ↔ 成员绑定**:当前 CLI 不支持,仍需在 AI 表格 Web 端面板手动完成 + 命令报错/操作失败 → 读 [aitable-error-recovery.md](./aitable/aitable-error-recovery.md) **关键区分**: base=表格文件, table=数据表, field=列, record=行 @@ -190,7 +430,7 @@ dws aitable base search --query "项目" --format json dws aitable base get --base-id --format json # 3. 获取表结构 — 提取 fieldId -dws aitable table get --base-id --table-ids --format json +dws aitable table get --base-id --table-id --format json # 4. 查询记录 dws aitable record query --base-id --table-id --format json @@ -206,7 +446,8 @@ dws aitable record create --base-id --table-id \ |------|-------------|------| | `base list/search` | `baseId` | 所有后续命令的 --base-id,拼接文档 URI | | `base create` | `baseId` | 后续命令 + 文档 URI | -| `base get` | `tables[].tableId` | --table-id | +| `base get` | `tables[].tableId` | --table-id,拼接指定数据表 URI | +| `table create` | `tableId` | 后续命令 + 拼接指定数据表 URI | | `table get` | `fields[].fieldId` | record 操作的 cells key, field get/update/delete | | `record query` | `recordId` | record update/delete;按 ID 反查字段值用 `record get` | | `template search` | `templateId` | base create --template-id,拼接模板预览 URI | diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-advperm.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-advperm.md new file mode 100644 index 00000000..84d9dfe6 --- /dev/null +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-advperm.md @@ -0,0 +1,251 @@ +# advperm — 高级权限管理 + +控制 Base 的高级权限总开关,并管理自定义角色(增删改查 + 子角色权限规则)。 +适用场景:"如何控制谁能看/改 Base 数据"、"开启/关闭高级权限"、"新建/修改/删除角色"、"按字段或行配置权限"。 + +## 命令一览 + +| 命令 | 用途 | +|------|------| +| `advperm enable` | 开启 Base 高级权限总开关 | +| `advperm disable` | 关闭 Base 高级权限总开关(高危) | +| `advperm role-list` | 列出 Base 下全部角色 | +| `advperm role-get` | 获取单角色完整配置 | +| `advperm role-create` | 创建自定义角色 | +| `advperm role-update` | 增量更新自定义角色(PATCH 语义) | +| `advperm role-delete` | 删除自定义角色(不可逆) | + +> 所有子命令的 `--base-id` 必填,可用隐藏别名 `--base`。 + +## 命令详情 + +### advperm enable — 开启高级权限 + +```bash +dws aitable advperm enable --base-id BASE_ID --format json +``` + +返回 `{baseId, enabled: true}`。 + +只有开启后角色配置才会真正限制成员的可访问范围;关闭状态下角色配置仍可读但不生效。 + +### advperm disable — 关闭高级权限(高危) + +```bash +dws aitable advperm disable --base-id BASE_ID --yes --format json +``` + +返回 `{baseId, enabled: false}`。关闭后所有角色配置即刻失效,全员回退到默认权限。涉及多人协作或敏感数据务必和用户二次确认,建议先 `role-list` 留底。 + +### advperm role-list — 列出全部角色 + +```bash +dws aitable advperm role-list --base-id BASE_ID --format json +``` + +返回结构: + +```json +{ + "data": { + "enabled": true, + "defaultRole": { "mode": 0 }, + "roles": [ + { + "roleId": "10685308981", + "name": "可查看角色", + "roleType": "custom", + "system": false, + "subRoles": [ + { + "authLevel": "read", + "targetId": "HMEaRQ4", + "targetType": "sheet", + "config": { "actions": 268435455 }, + "display": { + "authLevelLabel": "仅查看", + "targetTypeLabel": "数据表", + "permissionScopeNote": "...", + "actionsLabels": ["新增视图", "删除视图", "修改视图"], + "actionsNote": "..." + } + } + ] + } + ] + } +} +``` + +关键字段: + +- `roleType`:`custom`(自定义) / `system_editor` / `system_reader` / `5000`(owner) / `4000`(manager)。 +- `system`:boolean,true 表示系统角色(不可删)。 +- `subRoles[].display.*`:服务端返回的人类可读标签,可直接拼接给用户阅读,无需自行映射枚举。 +- 不返回角色成员列表;如需"成员-角色"映射请去 AI 表格 Web 端。 +- 新建 Base 默认 `enabled=false`,开启后只有 `owner` / `manager` 两个 meta 角色;`system_editor` / `system_reader` 需要在 Web UI 给成员授权"可编辑/可查看"后才会被服务端自动生成。 + +`role-list` / `role-get` 不需要管理员权限,普通成员也可读。 + +### advperm role-get — 获取单角色配置 + +```bash +dws aitable advperm role-get --base-id BASE_ID --role-id ROLE_ID --format json +``` + +返回结构同 `role-list` 中单个 role 对象(含完整 `subRoles[].config` 字段/行级规则与 `display.*` 标签)。 + +### advperm role-create — 创建自定义角色 + +```bash +# 仅指定 name,子角色由服务端按默认(none)填充 +dws aitable advperm role-create --base-id BASE_ID --name "市场可读" --format json + +# 创建时即指定 sub-roles(推荐——避免再走一次 role-update) +dws aitable advperm role-create --base-id BASE_ID --name "市场可读" \ + --sub-roles '[{"targetId":"","targetType":"sheet","authLevel":"read"}]' --format json +``` + +| flag | 必填 | 说明 | +|------|:---:|------| +| `--name` | ✅ | 角色名称 | +| `--role-type` | | 角色类型字符串(留空由服务端决定默认值,如 `custom`) | +| `--flow-type` | | 流程类型字符串(按业务需要) | +| `--sub-roles` | | JSON 数组:`[{targetId, targetType, authLevel, appId?, config?}]`,详见下方"sub-roles 子字段"段 | + +返回新建角色的完整配置(同 `role-get` 出参格式,含自动生成的 default subRoles)。 +系统角色无法通过本命令创建。 + +### advperm role-update — 增量更新自定义角色(PATCH 语义) + +```bash +# 只改名 +dws aitable advperm role-update --base-id BASE_ID --role-id ROLE_ID --name "新名字" + +# 只改 sheet 子角色 authLevel,name 不传保持不变 +dws aitable advperm role-update --base-id BASE_ID --role-id ROLE_ID \ + --sub-roles '[{"targetId":"","targetType":"sheet","authLevel":"edit-own"}]' +``` + +| flag | 必填 | 说明 | +|------|:---:|------| +| `--role-id` | ✅ | 目标自定义角色 ID(数字 long 字符串) | +| `--name` | | 新角色名称;不传不修改 | +| `--role-type` / `--flow-type` | | 可选 | +| `--sub-roles` | | JSON 数组,**PATCH 合并语义**:按 `(targetId, targetType)` 合并到现有 subRoles,入参中的 sub 整体替换该 sub,**入参未提及的 sub 保留不变**(无需先调 `role-get` 自行 merge) | + +**系统角色禁止更新**(包括 owner / manager / system_editor / system_reader)。 + +### sub-roles 子字段 + +每个 sub-role 描述「角色对某个权限目标的访问粒度」: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `targetId` | string | 目标资源 ID(数据表 → `tableId`;仪表盘 → `dashboardId`;应用 → `appId`) | +| `targetType` | string | `sheet` / `dashboard` / `app` | +| `authLevel` | string | `manage` / `edit-own` / `edit-custom-field` / `edit-field-range` / `read` / `none` | +| `appId` | string(可选) | 仅 `targetType=app` 时使用 | +| `config` | object(可选) | 字段/行级细化规则;含 `actions`(位图)/ `rows` / `cells`。结构与 `role-get` 出参 `subRoles[].config` 对齐 | + +### advperm role-delete — 删除自定义角色(不可逆) + +```bash +dws aitable advperm role-delete --base-id BASE_ID --role-id ROLE_ID --yes --format json +``` + +要求同时满足: + +1. 该 Base 已开启高级权限(`role-list` 返回 `enabled=true`)。 +2. 当前 dws 登录用户是该 Base 的管理员/Owner。 +3. `--role-id` 是 `role-list` 返回的数字 long 字符串(如 `"10685308981"`),且对应角色 `system=false`。 + +不可逆,删前先 `role-get` 留底。 + +## 能力边界 + +| 能力 | 状态 | +|------|------| +| 开/关高级权限 | ✅ 需管理员 | +| 列出 / 读取角色 | ✅ 普通成员也可读 | +| 创建自定义角色 | ✅ 需管理员 | +| 增量修改角色(PATCH 语义,不清空未传字段) | ✅ 需管理员 | +| 删除自定义角色 | ✅ 需管理员 | +| 修改/删除系统角色 | ❌ 服务端禁止;只能在 AI 表格 Web 端操作 | +| 角色 ↔ 成员绑定 | ❌ CLI 暂不支持,需在 AI 表格 Web 端 → Base 设置 → 高级权限 → 角色管理面板手动完成 | + +## 错误码速查 + +| 场景 | code | type | message | +|------|------|------|---------| +| advperm 关闭时调用写接口(如 `role-delete` / `role-create` / `role-update`) | `ADVANCED_PERMISSION_DISABLED` | `USER_ERROR` | `Advanced permission is disabled for base , please enable it via setAdvancedPermission before managing roles` | +| 非管理员调用 `enable` / `disable` / `role-create` / `role-update` / `role-delete` | `401` | `AUTH_ERROR` | `the current user must be a manager (administrator) of this base to manage roles or advanced permission` | +| 删除/更新系统角色(`system=true`) | `600` | `USER_ERROR` | `Illegal argument` | +| 操作不存在的数字 roleId(get/update/delete) | `600` | `USER_ERROR` | `Illegal argument` | +| 传非数字 roleId(如 `owner` / `manager`) | `INVALID_PARAMS` | `INPUT_ERROR` | `roleId is required` | +| `role-create` 缺 `--name` | `INVALID_PARAMS` | `INPUT_ERROR` | `name is required` | +| `--sub-roles` JSON 不是数组 / 解析失败 | (CLI 层拦截) | — | `--sub-roles 解析失败 ...` / `--sub-roles 必须是 JSON 数组` | +| `--base-id` 无法解析 | `INVALID_BASE_ID` | `INPUT_ERROR` | `baseId cannot be resolved to docId` | + +> `600 / Illegal argument` 同时覆盖"操作系统角色"和"操作不存在 roleId"两种情况。拿到 `600` 时先 `role-list` 自查目标 roleId 是否存在、是否 `system=true`,再据此引导用户。 + +## 典型工作流 + +### 排查"成员看不到某些字段/记录" + +```bash +dws aitable advperm role-list --base-id BASE_ID --format json +# 若 enabled=false:高级权限未开,所有规则不生效,与用户确认是否需要 enable + +dws aitable advperm enable --base-id BASE_ID --format json +dws aitable advperm role-list --base-id BASE_ID --format json +# 看 roles[] 里有哪些自定义角色 + +dws aitable advperm role-get --base-id BASE_ID --role-id ROLE_ID --format json +# 检查 subRoles[].config 中的字段/行级权限规则 +``` + +### 新建一个"市场可读"角色 + +```bash +# 1. 确保高级权限已开 +dws aitable advperm enable --base-id BASE_ID --format json + +# 2. 拿目标 sheet 的 tableId +dws aitable table get --base-id BASE_ID --format json + +# 3. 创建角色 + 指定 sheet 子角色 authLevel=read +dws aitable advperm role-create --base-id BASE_ID --name "市场可读" \ + --sub-roles '[{"targetId":"","targetType":"sheet","authLevel":"read"}]' \ + --format json +# → 返回新角色完整配置,含 roleId,记下后续 patch / delete 使用 +``` + +### 升级角色权限(read → edit-own),保留其他配置 + +```bash +# 只传 sub-roles,name 等其他字段保持不变(PATCH 语义) +dws aitable advperm role-update --base-id BASE_ID --role-id ROLE_ID \ + --sub-roles '[{"targetId":"","targetType":"sheet","authLevel":"edit-own"}]' \ + --format json +``` + +### 改角色名(不影响权限规则) + +```bash +dws aitable advperm role-update --base-id BASE_ID --role-id ROLE_ID --name "新名字" +``` + +### 清理废弃角色 + +```bash +dws aitable advperm role-list --base-id BASE_ID --format json +dws aitable advperm role-delete --base-id BASE_ID --role-id ROLE_ID --yes --format json +``` + +### 关闭高级权限(恢复全员可见) + +```bash +dws aitable advperm role-list --base-id BASE_ID --format json > /tmp/roles-backup.json +dws aitable advperm disable --base-id BASE_ID --yes --format json +``` diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-attachment.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-attachment.md index 1648fc87..45e9c2ae 100644 --- a/skills/multi/dingtalk-aitable/references/aitable/aitable-attachment.md +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-attachment.md @@ -1,6 +1,8 @@ # attachment — 附件上传 > **STOP — 不要使用钉盘 (drive) 上传!** 钉盘 fileId 无法写入 attachment 字段。必须使用以下流程。 +> +> **STOP — 严禁在 record create/update 的 cells 里直接传图片 URL!** 直传 `{"url":"https://..."}` 会导致服务端同步下载图片,批量写入时触发 TIMEOUT_ERROR。正确做法:先 `attachment upload` 获取 `fileToken`,再用 `{"fileToken":"ft_xxx"}` 写入。 ## 准备附件上传 @@ -38,8 +40,8 @@ dws aitable record create --base-id --table-id \ dws aitable attachment upload --base-id --file-name report.pdf --size 204800 --format json # → 返回 uploadUrl、fileToken -# 2. PUT 上传(Content-Type 留空) -curl -X PUT "" -H "Content-Type:" --data-binary @report.pdf +# 2. PUT 上传(Content-Type 必须是文件的具体 MIME type) +curl -X PUT "" -H "Content-Type: application/pdf" --data-binary @report.pdf # 3. 写入记录 dws aitable record update --base-id --table-id \ diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-best-practices.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-best-practices.md index 2c9b2200..db63f634 100644 --- a/skills/multi/dingtalk-aitable/references/aitable/aitable-best-practices.md +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-best-practices.md @@ -4,35 +4,30 @@ | 字段类型 | 可写 | 正确方式 | |----------|------|----------| -| 文本/数字/日期/单选/多选/复选框/URL | 是 | `dws aitable record create` / `dws aitable record update` | -| 附件 | 是,但需先上传 | 先 `dws aitable attachment upload` 取 `uploadUrl/fileToken`,PUT 后把 `fileToken` 写入记录 | -| 创建人/修改人/创建时间/修改时间 | 否 | 系统字段,只读 | -| 公式/查找引用 | 否 | 由系统计算,只读 | -| AI 字段 | 否 | 由 AI 自动计算,只读 | +| 文本/数字/日期/单选/多选/复选框/URL | ✅ | record create/update | +| 附件 | ⚠️ | 必须先走 [attachment upload 流程](./aitable-attachment.md) | +| 创建人/修改人/创建时间/修改时间 | ❌ | 系统字段,只读 | +| 公式/查找引用 | ❌ | 只读,由系统计算 | +| AI 字段 | ❌ | 只读,由 AI 自动计算 | ## 2. 查询执行契约 -1. 优先用 `dws aitable record query --filters` 在服务端过滤,不要先拉全量再在上下文里手动筛选。 -2. 返回 `has_more=true` 时不能做全局结论,数据可能不完整。 -3. 查询前先用 `dws aitable table get --base-id --table-ids ` 获取真实 fieldId,不要猜字段 ID。 -4. 只需要部分字段时,用 `dws aitable record query --field-ids fld1,fld2` 降低响应体积。 -5. 已知 recordId 时,用 `dws aitable record get --record-ids rec1,rec2`,不要构造无意义 filters。 +1. **不要拉全量后在 context 里手动统计** — 优先用 `--filters` 在服务端过滤 +2. **has_more=true 时不能做全局结论** — 数据可能不完整 +3. **优先用 `--filters` 在服务端过滤** — 不要拉全量后在本地 jq/grep +4. **字段名必须来自 `table get` 真实返回** — 不要猜测 fieldId +5. **减少响应体积** — 用 `--field-ids` 仅返回需要的字段 ## 3. 任务选路 | 用户诉求 | 优先方案 | 不要误走 | |---------|----------|----------| -| 查看几条数据 | `dws aitable record query --base-id --table-id ` | 不要默认 `--all` | -| 全量拉取/统计 | `dws aitable record query --base-id --table-id --all` | 不要手动循环 cursor | -| 全量导出 | `dws aitable export data --base-id --scope all --format excel` | 不要 `--all` 拉全量再写文件 | -| 文件级导入 | `dws aitable import upload --base-id --file-name data.xlsx --file-size <字节数>` + `dws aitable import data --import-id ` | 不要手动解析 xlsx 再逐条写入 | -| 批量写入多条不同数据 | `dws aitable record create --base-id --table-id --records '[{"cells":{"":"值"}}]'` | 不要一次超过 100 条 | -| 批量给多条记录写同一组值 | `dws aitable record update --base-id --table-id --records '[{"recordId":"rec1","cells":{"":"值"}},{"recordId":"rec2","cells":{"":"值"}}]'` | 不要使用隐藏兼容命令 | -| 附件上传 | `dws aitable attachment upload --base-id --file-name report.pdf --size <字节数>` + PUT + `record create/update` | 不要用钉盘 drive 上传 | -| 调整字段顺序 | `dws aitable view update --base-id --table-id --view-id --config '{"visibleFieldIds":["fld1","fld2"]}'` | 没有 `field reorder` 命令 | -| 查看视图列表 | `dws aitable view list --base-id --table-id ` | 不需要用 `view get --view-ids` | -| 创建收集表/问卷 | `dws aitable view create --base-id --table-id --view-type FormDesigner --name "表单名"` | 不要使用隐藏兼容命令 | -| 仪表盘/图表 | 先 `dashboard config-example` / `chart widgets-example`,再 create/update | 不要猜 config 结构 | +| 查看几条数据 | `record query` | 不要用 `--all` | +| 全量拉取/统计 | `record query --all` | 不要手动循环 cursor | +| 全量导出为文件 | `export data` | 不要 `--all` 拉全量再写文件 | +| 批量写入 | `record create`(分批 100 条) | 不要一次传超过 100 条 | +| 附件/图片上传 | `attachment upload` 获取 fileToken → `record create/update` 用 fileToken 写入 | **严禁直接传图片 URL 到附件字段**(服务端同步下载会超时) | +| 文件级导入 | `import upload` + `import data` | 不要手动解析 xlsx 再逐条写入 | ## 4. 创建/修改后回读确认 @@ -40,34 +35,12 @@ | 写操作 | 建议回读命令 | 确认内容 | |--------|-------------|----------| -| `dws aitable base create` | `dws aitable base get --base-id ` | base 名称、tables 列表 | -| `dws aitable table create` | `dws aitable table get --base-id --table-ids ` | 表名、字段列表是否符合预期 | -| `dws aitable field create` | `dws aitable field get --base-id --table-id ` | 新字段是否出现在字段列表中 | -| `dws aitable record create/update` | `dws aitable record get --base-id --table-id --record-ids ` | 写入值是否正确 | -| `dws aitable view update` | `dws aitable view get --base-id --table-id --view-ids ` | `visibleFieldIds` 顺序是否正确 | -| `dws aitable view create/update` | `dws aitable view get --base-id --table-id --view-ids ` | 表单视图名称、描述和配置 | +| `table create` | `table get --table-ids <新tableId>` | 表名、字段列表是否符合预期 | +| `field create` | `table get --table-ids ` | 新字段是否出现在字段列表中 | +| `record create/update` | `record query --record-ids <新recordId>` | 写入值是否正确 | -## 5. 导入导出与异步任务 +## 5. AI 字段注意事项 -- `export data` 的 `--format` 是导出格式,不要在此命令上追加全局 `--format json`。 -- 创建导出任务: - ```bash - dws aitable export data --base-id --scope table --table-id \ - --format excel --timeout-ms 1000 - ``` -- 续等已有导出任务: - ```bash - dws aitable export data --base-id --task-id --timeout-ms 3000 - ``` -- 导入本地文件: - ```bash - dws aitable import upload --base-id --file-name data.xlsx --file-size <字节数> --format json - curl -X PUT "" -H "Content-Type:" --data-binary @data.xlsx - dws aitable import data --import-id --format json - ``` - -## 6. AI 字段注意事项 - -- AI 字段的 prompt 必须至少包含一个 `fieldRef` 引用,纯文本 prompt 会被后端拒绝。 -- 先创建/确认被引用字段的 fieldId,再在 prompt 中引用。 -- `outputType` 必须与字段类型一致,例如 `outputType=text` 配 `--type text`。 +- AI 字段的 prompt **必须至少包含一个 `fieldRef` 引用**,纯文本 prompt 会被后端拒绝 +- 先创建/确认被引用字段的 fieldId,再在 prompt 中引用 +- `outputType` 必须与字段类型一致(如 `outputType=text` 配 `--type text`) diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-cell-value.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-cell-value.md index 9f747bf9..bc3a8125 100644 --- a/skills/multi/dingtalk-aitable/references/aitable/aitable-cell-value.md +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-cell-value.md @@ -86,11 +86,16 @@ {"fldDateId": "2026-03-15T09:00+08:00"} ``` -**读取**:RFC3339 字符串 +**读取**:RFC3339 字符串(带时区) ```json {"fldDateId": "2026-03-15T09:00:00+08:00"} ``` +**过滤**(`record query --filters`):日期字段**只能用日期专用操作符** `date_eq` / `before` / `after` / `not_before` / `not_after` / `exist` / `un_exist`,比较值用日期字符串(如 `"2026-03-15"`)。 +- ❌ 通用 `eq` / `ne` / `gt` / `gte` / `lt` / `lte` / `contain` 对日期字段无效,会静默返回 0 条; +- ❌ 不支持区间 `date_between` 与相对 `from_now`(CLI 会直接拒绝),范围查询用 `not_before` + `not_after` 组合。 +- 详见 [aitable-filter-sort.md](./aitable-filter-sort.md) §日期字段过滤。 + --- ### currency(货币) @@ -236,15 +241,14 @@ ### attachment(附件) -**写入**:对象数组,支持 `fileToken` 或 `url` 形式 +**写入**:对象数组,**必须使用 `fileToken`** ```json {"fldAttachId": [{"fileToken": "ft_xxx"}]} -{"fldAttachId": [{"url": "https://example.com/file.pdf"}]} ``` -> ⚠️ **必须先通过 [attachment upload 流程](./aitable-attachment.md) 获取 `fileToken`**。 -> URL 形式是 best-effort 异步转存,不保证立即可用。 +> ⚠️ **必须先通过 [attachment upload 流程](./aitable-attachment.md) 上传文件获取 `fileToken`,再将 `fileToken` 写入 cells。** +> ❌ **严禁直接传 `{"url": "https://..."}` 形式写入附件/图片字段** — 服务端会同步下载图片,10 条记录即触发 TIMEOUT_ERROR 超时。 > 写入会**整体覆盖**原附件列表,不是追加。 **读取**:对象数组(含下载链接、文件名、大小) @@ -335,7 +339,7 @@ |------|----------| | cells key 用字段名称 `"课程名称"` | 用 fieldId `"fldXXX"` | | progress 写入 `75` | 写入 `0.75`(范围 0~1) | -| attachment 直接传文件路径 | 必须先 upload 获取 fileToken | +| attachment 直接传文件路径或图片 URL | 必须先 `attachment upload` 获取 fileToken,再用 fileToken 写入(直传 URL 会超时) | | user 字段传用户名字符串 | 传对象数组 `[{"userId":"...", "corpId":"..."}]` | | group 字段用 `openConversationId` | 用 `cid` | | singleSelect 传 option id 字符串 | 传 name 字符串或 `{"id":"...", "name":"..."}` 对象 | diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-dashboard-chart.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-dashboard-chart.md index ae9a3739..94d576e2 100644 --- a/skills/multi/dingtalk-aitable/references/aitable/aitable-dashboard-chart.md +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-dashboard-chart.md @@ -27,17 +27,18 @@ dws aitable chart get --base-id --dashboard-id --chart- | `dashboard update` | 更新仪表盘 | `--base-id` `--dashboard-id` + (`--config` 或 `--name`) | `--name` 仅改名;`--config` 更新完整配置 | | `dashboard delete` | 删除仪表盘 | `--base-id` `--dashboard-id` `--yes` | — | | `dashboard config-example` | 查看仪表盘配置模板 | 无 | 创建前先调此命令了解 config 结构 | +| `dashboard arrange` | 自动重排图表布局 | `--base-id` `--dashboard-id` | 把图表按行铺满网格,避免某行只占半幅、留下大片空白;返回 `{totalColumns, layout, alignedChartCount}` | ## chart 子命令 | 命令 | 用途 | 必填参数 | |------|------|----------| | `chart get` | 获取图表详情 | `--base-id` `--dashboard-id` `--chart-id` | -| `chart create` | 创建图表 | `--base-id` `--dashboard-id` `--config` `--layout` | +| `chart create` | 创建图表 | `--base-id` `--dashboard-id` `--config` | | `chart update` | 更新图表配置 | `--base-id` `--dashboard-id` `--chart-id` `--config` | | `chart delete` | 删除图表 | `--base-id` `--dashboard-id` `--chart-id` `--yes` | | `chart widgets-example` | 查看图表 widgets 配置模板 | 无 | ## 配置获取流程 -创建图表前,必须先调用 `chart widgets-example` 查看配置模板,了解每种图表类型需要的字段结构,然后根据实际 tableId 和 fieldId 填充配置;同时必须传 `--layout` 指定图表位置和尺寸,例如 `--layout '{"x":0,"y":0,"w":6,"h":4}'`。 +创建图表前,必须先调用 `chart widgets-example` 查看配置模板,了解每种图表类型需要的字段结构,然后根据实际 tableId 和 fieldId 填充配置。 diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-error-recovery.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-error-recovery.md index 947b8fcc..e62fb0a4 100644 --- a/skills/multi/dingtalk-aitable/references/aitable/aitable-error-recovery.md +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-error-recovery.md @@ -63,7 +63,7 @@ | 错误现象 / summary | 原因 | 恢复动作 | |-------------------|------|---------| -| 导出任务超时 | 数据量大,异步任务未完成 | 用 `export data --base-id --task-id ` 轮询直到完成 | +| 导出任务超时 | 数据量大,异步任务未完成 | 用 `export data --task-id ` 轮询直到完成 | | 导入文件格式错误 | 不支持的文件格式或文件损坏 | 确认文件为 .xlsx 格式且未加密 | ## 3. 重试策略 diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-export-import.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-export-import.md index 2fc9b469..fc755451 100644 --- a/skills/multi/dingtalk-aitable/references/aitable/aitable-export-import.md +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-export-import.md @@ -4,10 +4,10 @@ `export data` 为异步任务:首次调用可能只返回 `taskId`,需要继续轮询。 -> ⚠️ **`export data` 的 `--format` 是导出格式**:需要导出 xlsx/附件时写 `--format excel` / `excel_and_attachment`。不要在这个命令上追加全局 `--format json`。 +> ⚠️ **`--format` 冲突警告**:`export data` 的 `--format` 是**导出格式**(excel/attachment 等),不是全局输出格式。**此命令禁止追加全局 `--format json`**,否则会覆盖导出格式导致 `INVALID_EXPORT_FORMAT` 错误。输出默认就是 JSON,无需额外指定。 ```bash -# 第一步:创建任务(按 scope 传必要参数) +# 第一步:创建任务(按 scope 传必要参数)——注意:不要加 --format json! dws aitable export data --base-id --scope table --table-id --format excel --timeout-ms 1000 # 第二步:拿 taskId 继续轮询,直到返回 downloadUrl diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-field-properties.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-field-properties.md index 586d58cc..710d685e 100644 --- a/skills/multi/dingtalk-aitable/references/aitable/aitable-field-properties.md +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-field-properties.md @@ -10,7 +10,6 @@ - `field create --name --type --config` 中 config 单独传 JSON 字符串 - `field update --config` 只传 config 部分 - 不需要 config 的类型(如 text、checkbox、attachment)可省略 config 字段 -- 成员/负责人字段类型使用 `user`,不要使用 `member`;字段类型不要写 `Text`/`Number`,统一写规范值 `text`/`number` ## 2. 字段类型速查 @@ -235,6 +234,5 @@ AI 字段不使用 config,而使用独立的 `--ai-config` 参数。详见 [ai | options 更新时只传新增项 | 全量覆盖,旧选项全部丢失 | | formula 字段尝试写入值 | 只读字段,record create/update 会报错 | | linkedTableId 传表名而非 ID | 必须传 tableId(如 `tblXXX`),不接受表名 | -| 成员字段使用 `member` | 不支持;人员/负责人/成员字段统一使用 `user` | | progress 值写入 50 表示 50% | 实际应写入 0.5(range 0~1) | | rating 值超出 max | 写入会报错 | diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-filter-sort.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-filter-sort.md index 6728fc26..8fabf8b4 100644 --- a/skills/multi/dingtalk-aitable/references/aitable/aitable-filter-sort.md +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-filter-sort.md @@ -1,5 +1,7 @@ # filters & sort — 筛选排序语法参考 +> 视图(view)配置的 filter/sort/group **整体写入**请优先用 `view update filter` / `view update sort` / `view update group` 子命令,详见 [aitable-view-config.md](./aitable-view-config.md)。本文件聚焦于 `record query --filters` 与 view config filter 的语法和差异。 + ## filters 结构规范 ### 强制规则 @@ -49,11 +51,34 @@ CLI 同时兼容两种子条件写法(推荐格式 A): | `exist` / `un_exist` | 有值 / 为空 | `["fieldId"]`(无需第二项) | | `any_of` / `none_of` / `all_of` | 包含任一 / 不包含任一 / 全包含(多选字段) | `["fieldId", "optionName"]` | | `date_eq` / `before` / `after` | 日期等于 / 早于 / 晚于 | `["fieldId", "dateStr"]` | -| `not_before` / `not_after` | 不早于 / 不晚于 | `["fieldId", "dateStr"]` | -| `from_now` | 从现在起 N 天内 | `["fieldId", "天数"]` | -| `date_between` | 日期区间 | `["fieldId", "[startTs, endTs]"]` | +| `not_before` / `not_after` | 不早于(≥) / 不晚于(≤) | `["fieldId", "2026-05-22"]` | > **操作符拼写必须严格匹配上表**,CLI 会在调用前校验,错误拼写会被拒绝。 +> +> **没有 `date_between`(区间)操作符**,也**不支持 `from_now`**——date 字段不支持区间/相对过滤,传了会被 CLI 拒绝。范围查询用 `not_before` + `not_after` 组合,见下方专节。 + +### 日期字段过滤(date / 创建时间 / 修改时间) + +日期类字段的过滤规则与其它字段**不同**,是线上反馈最高频的踩坑点。**经集成测试实测**确认的规则: + +1. **只能用日期专用操作符**:`date_eq` / `before` / `after` / `not_before` / `not_after` / `exist` / `un_exist`(与前端筛选 UI 的「等于 / 早于 / 晚于 / 早于或等于 / 晚于或等于 / 不为空 / 为空」一一对应)。 +2. **比较值用日期字符串**,如 `"2026-05-22"`(也接受 RFC3339 / 毫秒时间戳,内部统一转成毫秒比较)。读取返回的是带时区 RFC3339(如 `"2026-05-22T00:00:00+08:00"`)。 +3. **通用操作符 `eq` / `ne` / `gt` / `gte` / `lt` / `lte` / `contain` 对 date 字段无效**——无论传 ISO 字符串还是毫秒时间戳,都会**静默返回 0 条**。这是后端 date 字段的比较规则,不是 bug,CLI 也无法在本地拦截(不知道字段类型),务必用对操作符。 +4. **没有区间操作符 `date_between`**,也**不支持 `from_now`(相对天数)**——均会静默返回 0 条,CLI 已直接拒绝。范围查询用 `not_before`(≥起点)+ `not_after`(≤终点)两个条件 `and` 组合。 + +| 需求 | 操作符 | 示例 operands | +|------|--------|--------------| +| 等于某天 | `date_eq` | `["fldDate", "2026-05-22"]` | +| 早于 / 晚于(不含当天) | `before` / `after` | `["fldDate", "2026-05-22"]` | +| 不早于(≥) / 不晚于(≤) | `not_before` / `not_after` | `["fldDate", "2026-05-22"]` | +| 有值 / 为空 | `exist` / `un_exist` | `["fldDate"]` | + +**日期区间查询(替代 between)**——查 `2026-05-01 ~ 2026-05-31`(含端点): + +```bash +dws aitable record query --base-id X --table-id Y \ + --filters '{"operator":"and","operands":[{"operator":"not_before","operands":["fldDate","2026-05-01"]},{"operator":"not_after","operands":["fldDate","2026-05-31"]}]}' +``` ### 常见错误拼写(CLI 会自动提示纠正) diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-form.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-form.md index 83528cd0..2b126d8f 100644 --- a/skills/multi/dingtalk-aitable/references/aitable/aitable-form.md +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-form.md @@ -1,37 +1,120 @@ -# 表单视图 — 使用 view(FormDesigner) +# form — 表单管理 -悟空命令面不暴露独立表单命令组。表单在 AI 表格里按 `viewType=FormDesigner` 的视图处理,所有生成命令都走 `view` 和 `field`。 +## 命令一览 -## 命令路线 +| 命令 | 用途 | +|------|------| +| `form list` | 列出数据表下所有表单视图 | +| `form get` | 按 viewId 取单个表单详情(list_form_views + viewIds 过滤) | +| `form create` | 创建表单视图(等价于 `view create --view-type FormDesigner`) | +| `form update` | 更新表单标题或描述 | +| `form delete` | 删除表单视图(不可逆) | +| `form field list` | 列出表单可见字段 | +| `form field update` | 更新字段必填/描述 | +| `form field hide` | 在表单中隐藏/显示字段(不影响底层数据表字段) | +| `form share get` | 获取分享配置 | +| `form share update` | 开启/关闭分享 | +| `form questions create` | 添加题目(等价于 `field create`,命令位置上的别名) | +| `form questions delete` | 删除题目(等价于 `field delete`,命令位置上的别名) | -| 诉求 | 使用命令 | 说明 | -|------|----------|------| -| 列出表单视图 | `dws aitable view list --base-id --table-id ` | 从返回视图中过滤 `viewType=FormDesigner` | -| 查看表单视图 | `dws aitable view get --base-id --table-id --view-ids ` | 按 viewId 获取 | -| 创建表单视图 | `dws aitable view create --base-id --table-id --view-type FormDesigner --name "表单名"` | 返回 `viewId` | -| 更新表单视图 | `dws aitable view update --base-id --table-id --view-id --name "新名称"` | 描述用 `--desc` JSON | -| 删除表单视图 | `dws aitable view delete --base-id --table-id --view-id --yes` | 不可逆 | -| 添加题目 | `dws aitable field create --base-id --table-id --fields '[...]'` | 题目本质是字段 | -| 删除题目 | `dws aitable field delete --base-id --table-id --field-id --yes` | 不可逆 | +## 建议操作顺序 -## 创建工作流 +```bash +# 1) 列出数据表下的表单视图 +dws aitable form list --base-id BASE_ID --table-id TABLE_ID --format json + +# 2) 查看单个表单详情 +dws aitable form get --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID --format json + +# 3) 查看表单字段配置 +dws aitable form field list --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID --format json + +# 4) 查看分享配置 +dws aitable form share get --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID --format json +``` + +## 要点 + +- **创建表单**有两种等价方式: + - `form create --name "表单名"`(推荐,语义清晰) + - `view create --view-type FormDesigner --name "表单名"`(底层一致) +- `form update` 支持 `--title` 与 `--name` 两个等价参数;至少需传一项 +- `form field update` 必须传 `--required` 或 `--field-description` 至少一项 +- `form field hide` 仅控制字段在表单中的可见性,不影响底层数据表字段 +- **题目管理**与字段管理本质相同(题目 = 表格字段): + - `form questions create` 与 `field create` 入参完全一致(`--fields` JSON 或 `--name --type`) + - `form questions delete` 与 `field delete` 入参完全一致(必传 `--field-id`) + - 设置必填要在 create 后用 `form field update --required true` 单独调一次 + +## form 子命令 + +| 命令 | 用途 | 必填参数 | 说明 | +|------|------|----------|------| +| `form list` | 列出表单视图 | `--base-id` `--table-id` | 返回 viewId/name/title/createdAt | +| `form get` | 按 viewId 取单个表单 | `--base-id` `--table-id` `--view-id` | 内部基于 list_form_views 过滤 | +| `form create` | 创建表单视图 | `--base-id` `--table-id` `--name` | viewType=FormDesigner | +| `form update` | 更新表单 | `--base-id` `--table-id` `--view-id` | `--title`/`--name`(等价)和 `--description` 至少传一项;同时传 title/name 时 title 优先 | +| `form delete` | 删除表单 | `--base-id` `--table-id` `--view-id` `--yes` | 不可逆 | + +## form field 子命令 + +| 命令 | 用途 | 必填参数 | 说明 | +|------|------|----------|------| +| `form field list` | 列出表单字段 | `--base-id` `--table-id` `--view-id` | 返回 fieldId/name/type/required/hidden/description(hidden=true 的字段不在此返回) | +| `form field update` | 更新表单字段 | `--base-id` `--table-id` `--view-id` `--field-id` | `--required` 或 `--field-description` 至少一项 | +| `form field hide` | 切换字段隐藏 | `--base-id` `--table-id` `--view-id` `--field-id` `--hidden` | `--hidden true` 隐藏 / `--hidden false` 显示 | + +## form questions 子命令 + +`form questions create/delete` 与 `field create/delete` 入参、行为完全一致,只是命令位置归属于 `form` 命令组,方便从表单视角操作题目。 + +| 命令 | 用途 | 必填参数 | 说明 | +|------|------|----------|------| +| `form questions create` | 添加题目 | `--base-id` `--table-id` + (`--fields` 或 `--name --type`) | 入参与 `field create` 完全一致 | +| `form questions delete` | 删除题目 | `--base-id` `--table-id` `--field-id` `--yes` | 入参与 `field delete` 完全一致;不可逆;批量需多次调用 | + +## form share 子命令 + +| 命令 | 用途 | 必填参数 | 说明 | +|------|------|----------|------| +| `form share get` | 获取分享配置 | `--base-id` `--table-id` `--view-id` | 返回 enabled/status/shareFormUuid | +| `form share update` | 开启/关闭分享 | `--base-id` `--table-id` `--view-id` `--enabled` | `--enabled true` 开启 / `--enabled false` 关闭。注意:UI 上"发布并分享"按钮是另一概念,本命令只切换内部 enabled 标志,开启后需在 UI 刷新页面才会看到分享面板 | + +## 完整工作流示例 + +> **占位符约定**: +> - `BASE_ID` 来自 `dws aitable base list` / `base search` 返回的 `data.bases[].baseId` +> - `TABLE_ID` 来自 `dws aitable base get --base-id BASE_ID` 返回的 `data.tables[].tableId` +> - `VIEW_ID` 来自步骤 1 `form create` 返回的 `data.viewId` +> - `FIELD_ID` 来自步骤 2 `form questions create` 返回的 `data.results[].fieldId` ```bash -# 1. 创建表单视图 -dws aitable view create --base-id BASE_ID --table-id TABLE_ID \ - --view-type FormDesigner --name "员工信息收集" --format json +# 1) 创建表单 → 取返回的 data.viewId 作为 VIEW_ID +dws aitable form create --base-id BASE_ID --table-id TABLE_ID --name "员工信息收集" --format json -# 2. 添加题目字段 -dws aitable field create --base-id BASE_ID --table-id TABLE_ID \ +# 2) 添加题目 → 取返回的 data.results[].fieldId 作为 FIELD_ID +dws aitable form questions create --base-id BASE_ID --table-id TABLE_ID \ --fields '[{"fieldName":"姓名","type":"text"},{"fieldName":"邮箱","type":"text"}]' --format json -# 3. 回读表单视图 -dws aitable view get --base-id BASE_ID --table-id TABLE_ID --view-ids VIEW_ID --format json +# 3) 配置表单标题与描述 +dws aitable form update --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID \ + --title "员工信息收集" --description "请填写您的基本信息" --format json + +# 4) 设置题目必填(FIELD_ID 来自步骤 2) +dws aitable form field update --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID \ + --field-id FIELD_ID --required true --format json + +# 5) 隐藏不需要的题目 +dws aitable form field hide --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID \ + --field-id FIELD_ID --hidden true --format json + +# 6) 开启分享(注意:开启后需 UI 刷新页面才会看到分享面板) +dws aitable form share update --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID \ + --enabled true --format json ``` -## 注意 +## 返回结构补充 -- 不要生成隐藏兼容的独立表单命令;它不属于悟空对齐的公开命令面。 -- `VIEW_ID` 来自 `view create` 返回。 -- `FIELD_ID` 来自 `field create` 或 `field get` 返回。 -- 字段必填、字段隐藏、分享开关等表单高级配置没有公开的悟空命令入口;不要用隐藏兼容命令替代。 +- `form list` 返回 `data.formViews[]`,**每条仅含** `viewId/name/title/createdAt`;`shareFormUuid` 不在此返回,请用 `form share get` 单独获取。 +- `form get` 返回结构与 `form list` 完全一致(`data.formViews[]`),仅含一条记录(与请求 viewId 一致)。Agent 提取时仍走 `data.formViews[0]`。 +- `form field list` 仅返回**未隐藏**的字段;`hidden=true` 的字段不在此返回,如需查看全部字段请用 `field get`。 diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-primary-doc.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-primary-doc.md new file mode 100644 index 00000000..eefda277 --- /dev/null +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-primary-doc.md @@ -0,0 +1,55 @@ +# 主键文档管理 + +## 适用场景 + +当需要为 AI 表格中的记录创建或查询关联的主键文档时使用。主键文档是 primaryDoc 类型字段对应的钉钉在线文档,可通过 `dws doc` 进行内容读写。 + +## 命令 + +### 查询主键文档 + +```bash +dws aitable record primary-doc-get --base-id BASE_ID --table-id TABLE_ID --record-id RECORD_ID +``` + +**参数:** +- `--base-id`(必填):Base ID +- `--table-id`(必填):Table ID +- `--record-id`(必填):Record ID + +**返回:** `data.nodeId` — 主键文档的 nodeId,可直接传给 `dws doc read/update` 的 `--node` 参数。若该记录尚未创建主键文档,`nodeId` 为 null。 + +### 创建主键文档 + +```bash +dws aitable record primary-doc-create --base-id BASE_ID --table-id TABLE_ID --field-id FIELD_ID --record-id RECORD_ID +``` + +**参数:** +- `--base-id`(必填):Base ID +- `--table-id`(必填):Table ID +- `--field-id`(必填):主键字段 ID,必须是 primaryDoc 类型(通过 `dws aitable table get` 查看字段类型) +- `--record-id`(必填):Record ID + +**返回:** `data.nodeId` — 创建或已存在的主键文档 nodeId。 + +**幂等性:** 若该记录已有主键文档,直接返回已有文档的 nodeId,不会重复创建。 + +## 注意事项 + +- `fieldId` 必须是 primaryDoc 类型,否则返回 `INVALID_FIELD_TYPE` 错误 +- 传入不存在的 `recordId` 会返回 `RECORD_NOT_FOUND` 错误 +- 创建后可通过 `dws doc update --node ` 写入文档内容,或 `dws doc read --node ` 读取 + +## 典型工作流 + +```bash +# 1. 查询表结构,拿到 primaryDoc 字段的 fieldId +dws aitable table get --base-id BASE_ID --table-ids TABLE_ID + +# 2. 为某条记录创建主键文档 +dws aitable record primary-doc-create --base-id BASE_ID --table-id TABLE_ID --field-id FIELD_ID --record-id RECORD_ID + +# 3. 拿到返回的 nodeId,用 dws doc 写入内容 +dws doc update --node --content "# 项目方案\n\n文档正文内容..." +``` diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-record-create.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-record-create.md index e64f1865..bf605899 100644 --- a/skills/multi/dingtalk-aitable/references/aitable/aitable-record-create.md +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-record-create.md @@ -27,12 +27,13 @@ Flags: | cells key 用字段名 | ❌ cells key 必须是 fieldId(如 `fldXXX`),不是字段名称(如 `"课程名称"`) | | 不先获取 fieldId | ❌ 必须先 `table get` 获取 fieldId,再写入记录 | | 单次超 100 条 | ❌ 单次最多 100 条,超过需分批 | +| 附件/图片字段直传 URL | ❌ 严禁 `{"url":"https://..."}` — 会触发 TIMEOUT_ERROR。必须先 `attachment upload` 获取 `fileToken`,再用 `{"fileToken":"ft_xxx"}` 写入。详见 [aitable-attachment.md](./aitable-attachment.md) | ## 正确流程 ```bash # 先获取 fieldId -dws aitable table get --base-id --table-ids --format json +dws aitable table get --base-id --table-id --format json # 从返回中提取 fieldId(如 fldABC123) # 再用 fieldId 写入记录 diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-record-history.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-record-history.md new file mode 100644 index 00000000..f02d3d87 --- /dev/null +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-record-history.md @@ -0,0 +1,97 @@ +# 行记录变更历史(record history-list) + +按 recordId 查询单条记录的全部变更历史,用于审计、回溯字段变更、定位操作人。 + +## 命令 + +``` +dws aitable record history-list \ + --base-id BASE_ID --table-id TABLE_ID --record-id REC_ID \ + [--offset N] [--limit M] +``` + +| flag | 说明 | +|------|------| +| `--base-id` | 所属 Base ID(必填,可用 `--base` 别名) | +| `--table-id` | 所属 Table ID(必填) | +| `--record-id` | 目标记录 ID(必填,单条;不支持批量) | +| `--offset` | 分页偏移量,默认 0 | +| `--limit` | 每页返回数量,范围 [1, 50],默认 20 | + +## 返回结构 + +```jsonc +{ + "data": { + "histories": [ + { + "type": "field_change", // 变更类型 + "action": "update", // 操作动作: create / update / delete + "newValue": "{\"...\":\"...\"}", // 变更后的值(JSON 字符串) + "oldValue": "{\"...\":\"...\"}", // 变更前的值(JSON 字符串) + "operateTime": 1733123456789, // 操作时间(毫秒级时间戳) + "typeChangedFields": "{...}", // 类型变更的字段信息(JSON 字符串) + "version": 7 // 版本号(单调递增) + } + ] + } +} +``` + +`newValue` / `oldValue` / `typeChangedFields` 是 JSON 字符串(不是 JSON 对象),需要二次 `JSON.parse` 才能拿到结构化值。 + +## 字段含义速查 + +| 字段 | 用途 | +|------|------| +| `type` | 高层分类:`record_create` / `field_change` / `record_delete` 等。先按 type 过滤大类。 | +| `action` | 三态:`create` / `update` / `delete`。比 type 粗,但便于按"动作"统计。 | +| `version` | 单调递增整数;同一 record 越新值越大。**用作"上一条 vs 这一条"的稳定排序键**。 | +| `operateTime` | 毫秒时间戳;可格式化成可读时间。多条同 version 的极端场景用 operateTime 兜底排序。 | + +## 典型用法 + +### 1. 看一条记录被改过几次 + +```bash +dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --format json \ + | jq '.data.histories[] | {version, action, operateTime}' +``` + +### 2. 翻页拉全量历史 + +```bash +# 第 1 页(最新 20 条) +dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --limit 50 --offset 0 + +# 第 2 页 +dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --limit 50 --offset 50 +``` + +`limit` 上限 50,需要更多请增加 `offset` 翻页。 + +### 3. 回溯某字段最近一次值 + +```bash +dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --limit 50 --format json \ + | jq '[.data.histories[] | select(.action == "update")][0].oldValue' +``` + +### 4. 找出删除事件(如果存在 delete history) + +```bash +dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --format json \ + | jq '.data.histories[] | select(.action == "delete") | {version, operateTime}' +``` + +## 注意事项 + +- 一次只能查一条 record;如需批量审计多条记录请循环调用。 +- 仅返回**字段值变更**与**记录生命周期事件**;视图、字段定义、表结构变更不在此 history 里。 +- 历史保留时长由 server 决定,过老的记录可能不再返回。 + +## 与其他 record 命令的关系 + +- 想看记录"现在长什么样" → `record query` / `record get` +- 想看记录"过去长什么样、什么时候改的" → `record history-list`(本命令) +- 想看"这张表整体改过什么" → 当前 CLI 不支持表级 history;只能逐 record 查 diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-record-name-key.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-record-name-key.md new file mode 100644 index 00000000..65a7fe89 --- /dev/null +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-record-name-key.md @@ -0,0 +1,47 @@ +# 行命名规则枚举键(recordNameKey)映射 + +`dws aitable table update --record-name-key <枚举键>` 用于设置数据表的"行命名规则"——卡片/详情页里"行"的展示别名。**取值是固定枚举,不是字段 ID**;传非法值服务端返回 `INVALID_RECORD_NAME_KEY`。 + +## 中文 → 枚举键(按 UI 下拉顺序) + +| 用户说 | --record-name-key | 用户说 | --record-name-key | +|---|---|---|---| +| 记录 | `ji_lu`(默认) | 项目 | `project` | +| 任务 | `task` | 事件 | `event` | +| 请求 | `request` | 活动 | `campaign` | +| 目标 | `objective` | 交付物 | `deliverable` | +| 资产 | `asset` | 客户 | `customer` | +| 订单 | `order` | 联系人 | `contact` | +| 物料/物品 | `item` | 问题 | `question` 或 `issue` | +| 工单 | `ticket` | 候选人 | `candidate` | +| 商机/机会 | `opportunity` | 会议 | `meeting` | +| 成员 | `member` | OKR | `okr` | + +## 其他常用键(按场景分组) + +- **业务流程**:`approval` / `application` / `case` / `decision` / `delivery` / `payment` / `purchase_order` / `quote` / `release` +- **HR / 财务**:`employee` / `expense` / `budget` / `invoice` +- **产品 / 研发**:`feature` / `feedback` / `idea` / `bug` / `requirement` / `risk` / `sprint` / `story` / `subtask` / `epic` +- **CRM**:`account` / `lead` / `prospect` / `deal` +- **运营 / 支持**:`note` / `report` / `topic` / `session` / `service` +- **资源 / 通用**:`file` / `document` / `product` / `team` / `user` / `vendor` / `key_result` / `metric` + +完整集合较大(共 273 个),服务端校验;以上未列出的合法键也可直接传(如 `goal` / `okr` / `pillar` / `phase` / `milestone` 等)。 + +## 使用示例 + +```bash +# 用户说"把这张表的行叫'任务'吧" → 传 task +dws aitable table update --base-id BASE --table-id TBL --record-name-key task + +# 用户说"换成项目" → 传 project +dws aitable table update --base-id BASE --table-id TBL --record-name-key project + +# 用户说"恢复成默认(记录)" → 传 ji_lu +dws aitable table update --base-id BASE --table-id TBL --record-name-key ji_lu +``` + +## 注意 + +- recordNameKey **不会在 `table get` 响应里回显**(`get_tables` DTO 设计上不暴露该字段);写入是否成功以 `table update` 的 set response 是否回填 `recordNameKey` 字段为准。 +- 中文别名是 server 内置 i18n,UI 显示用户对应的国际化文案,CLI 必须传英文枚举键。 diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-record-query.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-record-query.md index 8d04ab1b..20ed92da 100644 --- a/skills/multi/dingtalk-aitable/references/aitable/aitable-record-query.md +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-record-query.md @@ -72,3 +72,49 @@ dws aitable record query --base-id X --table-id Y --all --cursor "上次返回 - `--sort` 用 `"order":"desc"` → 必须用 `"direction":"desc"` - 不加 `--field-ids` 拉全字段 → 大表响应体积过大 - 全量拉取后在 context 里手动统计 → 应优先用 `--filters` 服务端过滤 + +## record query-empty — 找空行 + +`record query-empty` 是与 `record query` 平行的独立子命令,专门按表内顺序扫描出"完全没填用户字段"的空行。 + +```bash +dws aitable record query-empty --base-id BASE_ID --table-id TABLE_ID +``` + +| flag | 说明 | +|------|------| +| `--base-id` / `--base` | 必填 | +| `--table-id` | 必填 | +| `--limit` | 单次**扫描预算**(不是返回数);范围 [1, 100],默认 100 | +| `--cursor` | 分页游标。响应中 `nextCursor` 非空 → 用它翻页继续扫;nextCursor 为空(或不存在)→ 已扫完整表 | + +返回结构: + +```jsonc +{ "data": { "records": [...], "nextCursor": "..." } } +``` + +### 关键语义 + +1. **`--limit` 是扫描预算不是返回数**:可能扫了 100 条但全部非空,本页 `records: []`。 +2. **本页空 records ≠ 全表无空行**:必须看 `nextCursor`,nextCursor 还在就要继续翻。 +3. **空行定义**:除系统字段(recordId / 创建人 / 创建时间 / 修改人 / 修改时间)外,所有 cell 都是 null、空字符串、空集合或空 Map。一般是用户在 UI 上"插入空行"产生的。 + +### 典型用法 + +```bash +# 扫一页,看本页有没有空行 +dws aitable record query-empty --base-id BASE --table-id TBL + +# 翻页 +dws aitable record query-empty --base-id BASE --table-id TBL --cursor <上次的nextCursor> + +# 把整表扫完(手动循环 cursor) +NC="" +while : ; do + R=$(dws aitable record query-empty --base-id BASE --table-id TBL ${NC:+--cursor "$NC"} --format json) + echo "$R" | jq '.data.records[] | .recordId' + NC=$(echo "$R" | jq -r '.data.nextCursor // empty') + [ -z "$NC" ] && break +done +``` diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-record-share.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-record-share.md new file mode 100644 index 00000000..6e5eea51 --- /dev/null +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-record-share.md @@ -0,0 +1,54 @@ +# 行记录分享链接(record share-url) + +按 recordId 批量获取记录的分享链接,把某行单独发给同事查看。 + +## 命令 + +``` +dws aitable record share-url \ + --base-id BASE_ID --table-id TABLE_ID \ + --record-ids rec1,rec2,rec3 \ + [--view-id VIEW_ID] +``` + +| flag | 说明 | +|------|------| +| `--base-id` | 所属 Base ID(必填,可用 `--base` 别名) | +| `--table-id` | 所属 Table ID(必填) | +| `--record-ids` | 目标 Record ID 列表,CSV 逗号分隔,**单次最多 20 条**(必填) | +| `--view-id` | 视图 ID(可选)。带上后链接打开会落在该视图上下文里 | + +## 返回结构 + +```jsonc +{ + "data": { + "items": [ + { "recordId": "rec1", "shareUrl": "https://..." }, + { "recordId": "rec2", "shareUrl": "https://..." } + ] + } +} +``` + +`shareUrl` 为 null 表示该条获取失败(不影响其他条目)。 + +## 典型用法 + +```bash +# 一次拿一条记录的链接 +dws aitable record share-url --base-id BASE --table-id TBL --record-ids rec1 + +# 批量拿,配合 jq 过滤出 url +dws aitable record share-url --base-id BASE --table-id TBL --record-ids rec1,rec2,rec3 --format json \ + | jq '.data.items[] | {recordId, shareUrl}' + +# 带视图上下文(链接打开时落在指定视图) +dws aitable record share-url --base-id BASE --table-id TBL --record-ids rec1 --view-id viw_VIP +``` + +## 注意事项 + +- **单次最多 20 条**,超出请客户端拆批。 +- 该链接是分享链接(不是源文档链接),打开后看到的是该 record 的只读详情页。 +- 取消单条分享 / 关闭整表分享当前 CLI 不支持,需要在 AI 表格 Web 端操作。 diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-record-upsert.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-record-upsert.md new file mode 100644 index 00000000..1642282b --- /dev/null +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-record-upsert.md @@ -0,0 +1,89 @@ +# 行记录 Upsert(record upsert) + +按 `recordId` 是否存在,自动把入参拆分到 update 链路或 create 链路:批次混合"已存在改 + 新出现建"时用,省掉客户端按 ID 分批的逻辑。 + +## 命令 + +``` +dws aitable record upsert \ + --base-id BASE_ID --table-id TABLE_ID \ + --records '[{"recordId":"<可选>","cells":{...}}, ...]' +``` + +| flag | 说明 | +|------|------| +| `--base-id` | 必填(可用 `--base` 别名) | +| `--table-id` | 必填 | +| `--records` | 待 upsert 的记录 JSON 数组,**单次最多 100 条**(必填)| +| `--records-file` | 从文件读入(命令行 JSON 太长时用),与 `--records` 互斥优先级更高 | + +## --records 结构 + +每项 JSON: + +```jsonc +{ + "recordId": "rec1", // 可选;带 → update,缺省 → create + "cells": { // 必填;key 是 fieldId,value 按字段类型 + "fldTitleId": "新标题", + "fldNumberId": 42 + } +} +``` + +`cells` 写入格式与 `record create` / `record update` **完全一致**(key 必须是 fieldId 不是字段名;按字段类型见 [aitable-cell-value.md](./aitable-cell-value.md))。 + +## 返回结构 + +```jsonc +{ + "data": { + "createdRecordIds": ["recX", "recY"], // 不带 recordId 的项产出 + "updatedRecordIds": ["recA", "recB"] // 带 recordId 的项产出 + } +} +``` + +`createdRecordIds` 顺序对应入参里**不带 recordId**的项(按出现顺序汇总),同理 `updatedRecordIds` 对应**带 recordId**的项。 + +## 典型用法 + +```bash +# 1) 全部新建:所有项都不带 recordId +dws aitable record upsert --base-id BASE --table-id TBL --records '[ + {"cells":{"fldTitleId":"任务1","fldStatusId":"待办"}}, + {"cells":{"fldTitleId":"任务2","fldStatusId":"待办"}} +]' + +# 2) 全部更新:所有项都带 recordId +dws aitable record upsert --base-id BASE --table-id TBL --records '[ + {"recordId":"rec1","cells":{"fldStatusId":"已完成"}}, + {"recordId":"rec2","cells":{"fldStatusId":"已完成"}} +]' + +# 3) 混合:第 1 条更新(带 recordId),第 2 条创建(不带) +dws aitable record upsert --base-id BASE --table-id TBL --records '[ + {"recordId":"rec1","cells":{"fldStatusId":"已完成"}}, + {"cells":{"fldTitleId":"新增任务","fldStatusId":"待办"}} +]' + +# 4) 长 JSON 用文件 +dws aitable record upsert --base-id BASE --table-id TBL --records-file ./batch.json +``` + +## 与 record create / record update 的关系 + +| 场景 | 命令 | +|------|------| +| 确定全是新增 | `record create` | +| 确定全是更新(每条独立 cells) | `record update` | +| 确定全是更新(共享同一 cells) | `record batch-update` | +| **不确定有没有,按 recordId 自动分流** | `record upsert`(本命令) | + +`record upsert` 的 `--records` 入参格式与 `record update` 完全相同,唯一差别是 `recordId` 字段在 upsert 里是可选的。如果批次确定全是更新或全是新建,用专用命令更清晰;批次混合时(典型场景:定时同步外部数据,源里既有已存在的也有新出现的),用 upsert。 + +## 注意事项 + +- **单次最多 100 条**(创建 + 更新合计),超出请客户端拆批。 +- `cells` 的 key 必须是 fieldId 不是字段名(先用 `record query` 或 `field get` 拿 fieldId)。 +- 只读字段(formula / lookup / 系统字段)不能写入 — upsert 链路与 update 链路同样限制。 diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-view-config.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-view-config.md new file mode 100644 index 00000000..6b22ec03 --- /dev/null +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-view-config.md @@ -0,0 +1,203 @@ +# 视图配置(view get/update ) + +按属性局部读/写视图配置。每个属性独立子命令,typed flag 友好,agent 不必拼 JSON。 +向后兼容:`view update --config '{...}'` 一次多属性入口仍可用。 + +## viewType × 支持矩阵 + +| viewType | card | timebar | aggregate | filter / sort / group | visible-fields | field-widths | name | +|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| Grid | | | ✅ | ✅ | ✅ | ✅ | ✅ | +| Kanban | ✅ | | | ✅ | ✅ | | ✅ | +| Gallery | ✅ | | | ✅ | ✅ | | ✅ | +| Gantt | | ✅ | | ✅ | ✅ | | ✅ | +| Calendar | | | | ✅ | ✅ | | ✅ | +| FormDesigner | (走 `form` 系列命令) | | | | | | | + +> card 在 Kanban 走 `kanbanCard`,在 Gallery 走 `galleryCard`,CLI 自动按 viewType dispatch(preflight 1 次 `get_views`)。timebar 仅 Gantt 支持;Calendar 服务端未暴露任何 timebar 配置。 + +> **Gantt 视图必须两步创建**:`view create --view-type Gantt` 只创建空壳(`ganttTimebar: {}`),**必须**紧跟 `view update timebar --start-field <日期字段ID>` 绑定时间轴字段,否则视图打开是空白。`create_view` 的 `--config` 中传入 `ganttTimebar` 会被服务端忽略。 + +## 读取:view get + +所有 `view get ` 共用 `--base-id` / `--table-id` / `--view-id`,输出是该属性子块的 JSON(不存在时输出 `{}`)。viewType 不匹配会报错并指明应该选哪种视图。 + +```bash +dws aitable view get card --view-id VIEW_ID --format json # Kanban / Gallery +dws aitable view get timebar --view-id VIEW_ID --format json # Gantt +dws aitable view get aggregate --view-id VIEW_ID --format json # Grid +dws aitable view get filter --view-id VIEW_ID --format json # 所有 +dws aitable view get sort --view-id VIEW_ID --format json +dws aitable view get group --view-id VIEW_ID --format json +dws aitable view get visible-fields --view-id VIEW_ID --format json +dws aitable view get field-widths --view-id VIEW_ID --format json # Grid +``` + +## 写入:view update + +所有 `view update ` 共用 `--base-id` / `--table-id` / `--view-id`。 +**typed flag + `--json` 可混用**;冲突时 typed flag 优先并 stderr 提示。 +card / timebar / aggregate 三类写入有 viewType 校验(preflight 1 次 get_views)。 + +### view update card(Kanban / Gallery) + +服务端按 viewType 分发到 `kanbanCard` 或 `galleryCard`。typed flag 共享。 + +| flag | 类型 | 说明 | +|------|------|------| +| `--cover-field-id` | string | 封面字段 ID(Kanban / Gallery 通用),与 `--no-cover` 互斥 | +| `--no-cover` | bool | 清除封面(等价 `coverFieldId="NONE"`) | +| `--cover-resize-mode` | string | `cover` / `contain` / `stretch` | +| `--hidden-field-title` | bool | 隐藏字段名标题(仅 Kanban 生效) | +| `--cover-mode` | string | `none` / `auto` / `custom`(仅 Gallery 生效) | +| `--display-field-name` | bool | 是否显示字段名(仅 Gallery 生效) | +| `--json` | JSON | 完整 card 子块对象 | + +```bash +dws aitable view update card --view-id KANBAN_ID --cover-field-id fldAttachment --cover-resize-mode contain +dws aitable view update card --view-id KANBAN_ID --no-cover +dws aitable view update card --view-id GALLERY_ID --cover-mode auto +dws aitable view update card --view-id GALLERY_ID --json '{"coverMode":"custom","coverFieldId":"fldX","displayFieldName":true}' +``` + +### view update timebar(仅 Gantt) + +| flag | 类型 | 说明 | +|------|------|------| +| `--start-field` | string (date fieldId) | 开始日期字段 | +| `--end-field` | string (date fieldId) | 结束日期字段 | +| `--display-field-id` | string | 时间条上显示的标题字段 | +| `--timeline-scale` | string | `year` / `quarter` / `month` / `weeks` | +| `--color-configs` | JSON 数组 | 颜色配置数组(结构由下游协议定义;清空传 `[]`) | +| `--official-holiday` | bool | 是否标注法定节假日 | +| `--json` | JSON | 完整 ganttTimebar 子块 | + +```bash +dws aitable view update timebar --view-id GANTT_ID --start-field fldStart --end-field fldEnd --timeline-scale month +dws aitable view update timebar --view-id GANTT_ID --official-holiday=true +``` + +### view update aggregate(仅 Grid) + +值是 `map[fieldId]→AggregateAction string`;传 null 清除某个字段聚合。 + +| flag | 类型 | 说明 | +|------|------|------| +| `--field-id` | string | 配合 `--action` 设置**单字段**聚合 | +| `--action` | string | `SUM`/`AVG`/`MAX`/`MIN`/`MEDIAN`/`RANGE`/`TOTAL`/`DISTINCT`/`EXIST`/`UN_EXIST`/`CHECKED`/`EARLIEST_DATE` 等(按字段类型可用) | +| `--clear-field-id` | string (CSV) | 一/多个字段 ID,清除其聚合 | +| `--json` | JSON | 完整 aggregate map | + +```bash +dws aitable view update aggregate --view-id GRID_ID --field-id fldX --action SUM +dws aitable view update aggregate --view-id GRID_ID --clear-field-id fldA,fldB +dws aitable view update aggregate --view-id GRID_ID --json '{"fldX":"AVG","fldY":null}' +``` + +### view update field-widths(仅 Grid) + +| flag | 类型 | +|------|------| +| `--field-id` + `--width` | string + int(单字段) | +| `--json` | `{fldId: width, ...}` | + +```bash +dws aitable view update field-widths --view-id GRID_ID --field-id fldX --width 200 +dws aitable view update field-widths --view-id GRID_ID --json '{"fldA":120,"fldB":200}' +``` + +### view update visible-fields(通用) + +整组替换可见字段列表与顺序。首列字段(primaryDoc)必须保留在数组第一位。 + +> ⚠️ 注意:服务端**只接受 reorder,不接受真"隐藏字段"**——如果传入的列表比当前 columns 短,缺失的字段不会被隐藏。需要真正隐藏字段请到 AI 表格 Web UI。 + +| flag | 类型 | +|------|------| +| `--field-ids` | string (CSV) | +| `--json` | string 数组 JSON(与 `--field-ids` 同传时 `--json` 优先) | + +```bash +dws aitable view update visible-fields --view-id VIEW_ID --field-ids fldPrimary,fldA,fldB +dws aitable view update visible-fields --view-id VIEW_ID --json '["fldPrimary","fldA","fldB"]' +``` + +### view update filter / sort / group(通用,纯 --json) + +```bash +dws aitable view update filter --view-id VIEW_ID --json '[{"operator":"and","operands":[{"operator":"eq","operands":["fldX","value"]}]}]' +dws aitable view update sort --view-id VIEW_ID --json '[{"fieldId":"fldX","direction":"asc"}]' +dws aitable view update group --view-id VIEW_ID --json '[{"fieldId":"fldX","direction":"asc"}]' +``` + +> filter/sort/group 入参格式与 `record query --filters`(对象格式)**不同**:view config 这边外层必须是数组。传对象 CLI 会自动 wrap,建议直接用数组。详见 [aitable-filter-sort.md](./aitable-filter-sort.md)。 + +### view update name(重命名) + +```bash +dws aitable view update name --view-id VIEW_ID --name "新视图名" +``` + +等价于 `dws aitable view update --view-id VIEW_ID --name "新视图名"`,无 `config` 参数。 + +## 服务端字段速查(与 dws CLI 关系) + +| dws 子命令 | 服务端 `update_view.config` 子键 | 服务端 Java 模型 | +|---|---|---| +| `view update card`(Kanban) | `kanbanCard` | `KanbanCardUpdateInput` | +| `view update card`(Gallery) | `galleryCard` | `GalleryCardUpdateInput` | +| `view update timebar` | `ganttTimebar` | `GanttTimebarUpdateInput` | +| `view update aggregate` | `aggregate` | `Map` | +| `view update visible-fields` | `visibleFieldIds` | `List` | +| `view update filter / sort / group` | `filter` / `sort` / `group` | `List` | +| `view update field-widths` | `fieldWidths` | `Map` | +| `view update name` | (不在 config 内)`newViewName` 顶层 | — | + +## 典型工作流 + +### 排查"Kanban 卡片为啥不显示封面" + +```bash +dws aitable view get card --view-id KANBAN_ID --format json +# → 看 coverFieldId 是不是 "NONE" 或缺失;不是再看 coverResizeMode 是不是 contain 导致裁掉 +``` + +### 创建可用的 Gantt 视图(必须两步) + +```bash +# 第 1 步:创建 Gantt 视图 +dws aitable view create --base-id BASE_ID --table-id TABLE_ID \ + --view-type Gantt --name "项目甘特图" -f json +# → 记录返回的 viewId + +# 第 2 步(必须):绑定日期字段,否则视图为空 +dws aitable view update timebar --base-id BASE_ID --table-id TABLE_ID \ + --view-id VIEW_ID --start-field fldDateStart +# 可选:加结束日期、标题字段、时间尺度 +# --end-field fldDateEnd --display-field-id fldName --timeline-scale month +``` + +### 把 Gantt 时间轴改成季度尺度并加节假日 + +```bash +dws aitable view update timebar --view-id GANTT_ID \ + --timeline-scale quarter --official-holiday=true +``` + +### 用 dws 脚本批量替换 Kanban 封面字段 + +```bash +for v in viw1 viw2 viw3; do + dws aitable view update card --view-id $v --cover-field-id fldNewCover --cover-resize-mode cover --format json | jq .status +done +``` + +### 一次性多属性更新(仍走 legacy --config) + +```bash +dws aitable view update --view-id VIEW_ID --config '{ + "visibleFieldIds":["fldPrimary","fldA","fldB"], + "filter":[{"operator":"and","operands":[]}], + "kanbanCard":{"coverFieldId":"fldImg","coverResizeMode":"contain"} +}' +``` diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-view-extras.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-view-extras.md new file mode 100644 index 00000000..23db8059 --- /dev/null +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-view-extras.md @@ -0,0 +1,198 @@ +# 视图扩展操作(lock / frozen-cols / row-height / fill-color-rule / duplicate) + +本文档讲 5 项视图操作命令: + +- 锁定 / 解锁视图:`view lock` / `view get lock` +- 冻结列:`view update frozen-cols` / `view get frozen-cols` +- 行高:`view update row-height` / `view get row-height` +- 数据高亮规则(条件填色):`view update fill-color-rule` / `view get fill-color-rule` +- 复制视图:`view duplicate` + +> **与 [aitable-view-config.md](./aitable-view-config.md) 的分工**: +> - `aitable-view-config.md` 讲 `view get/update ` 中 8 个属性:filter / sort / group / visible-fields / field-widths / aggregate / card / timebar。 +> - 本文档讲上面 5 项额外能力(包括 attr 形式的 frozen-cols / row-height / fill-color-rule,以及顶层独立的 lock / duplicate)。 +> 这 5 项**不能**通过 `view update --config '{...}'` 写入,必须用各自专属子命令。 + +## 命令矩阵 + +| 子命令 | 用途 | 必填参数 | 适用 viewType | +|---|---|---|---| +| `view lock [--off]` | 锁定(默认)/ 解锁视图 | `--base-id --table-id --view-id` | 全部 | +| `view get lock` | 读取锁定状态 | `--base-id --table-id --view-id` | 全部 | +| `view update frozen-cols --count N` | 冻结左侧 N 列(0 取消) | `--base-id --table-id --view-id --count` | Grid | +| `view get frozen-cols` | 读取冻结列数 | `--base-id --table-id --view-id` | Grid | +| `view update row-height --cell-height N` | 设置单元格高度(像素) | `--base-id --table-id --view-id --cell-height` | Grid | +| `view get row-height` | 读取单元格高度 | `--base-id --table-id --view-id` | Grid | +| `view update fill-color-rule --json '[...]'` | 全量覆盖条件填色规则 | `--base-id --table-id --view-id --json` | Grid | +| `view get fill-color-rule` | 读取条件填色规则 | `--base-id --table-id --view-id` | 全部(其他视图返回 `[]`) | +| `view duplicate [--new-name X]` | 复制视图 | `--base-id --table-id --view-id` | 全部 | + +## 视图锁定 / 解锁 + +```bash +# 锁定(默认) +dws aitable view lock --view-id VIEW_ID + +# 解锁 +dws aitable view lock --view-id VIEW_ID --off + +# 查询当前是否锁定 +dws aitable view get lock --view-id VIEW_ID --format json +# → {"data": {"baseId": ..., "tableId": ..., "viewId": ..., "locked": true|false}} +``` + +锁定的视图禁止他人修改其配置(filter/sort/group/字段顺序等),但记录读写不受影响。锁定状态可重复 set,幂等。 + +## 冻结列(仅 Grid) + +```bash +# 冻结从首列起 1 列 +dws aitable view update frozen-cols --view-id VIEW_ID --count 1 + +# 取消冻结 +dws aitable view update frozen-cols --view-id VIEW_ID --count 0 + +# 查询当前冻结列数 +dws aitable view get frozen-cols --view-id VIEW_ID --format json +# → {"data": {..., "count": 1}} count 为 null 表示视图未显式设置 +``` + +`--count` 必须 ≥ 0;负数会被拒绝。 + +## 行高(仅 Grid) + +⚠️ **`--cell-height` 只接受 4 档枚举:32 / 56 / 88 / 128**(与前端 CELL_HEIGHTS 约定一致),其他值会被拒绝。默认值为 32。 + +```bash +# 设置行高 — 推荐档位 32 / 56 / 88 / 128 +dws aitable view update row-height --view-id VIEW_ID --cell-height 56 + +# 查询当前行高 +dws aitable view get row-height --view-id VIEW_ID --format json +# → {"data": {..., "cellHeight": 56}} cellHeight 为 null 表示视图未显式设置(前端按 32 渲染) +``` + +## 数据高亮规则(条件填色,仅 Grid) + +`view update fill-color-rule` **整组覆盖**,传 `--json '[]'` 清空所有规则。 + +### 规则结构 + +每条规则 JSON 结构: + +```jsonc +{ + "type": "cell" | "row" | "column" | "preRow", + "formatFieldId": "fldX", // 命中规则后被高亮的字段(cell/column 类型有意义) + "format": { "color": "firstLine5" }, // ⚠️ 必须用 FORMAT_COLORS 代号,不接受 hex + "filters": [ // 当前固定 1 条 + { + "fieldId": "fldX", // ⚠️ 不是 operands[0] + "symbol": "GT", // ⚠️ 不是 operator;大写枚举 + "value": 100 // 部分 symbol(EXIST/UN_EXIST)不需要 value + } + ] +} +``` + +### color 合法值(FORMAT_COLORS) + +`firstLine1` ~ `firstLine11`(共 11 档色码,对应前端调色盘)。**不接受 `#FF0000` 这种 hex**。 + +### filter.symbol 合法值 + +| 类别 | symbol | +|---|---| +| 数值/通用比较 | `GT` / `LT` / `GTE` / `LTE` / `EQ` / `NE` | +| 文本 | `CONTAIN` / `EXCLUSIVE` | +| 存在性(无 value) | `EXIST` / `UN_EXIST` | +| 多选 / 集合 | `ALL_OF` / `ANY_OF` / `NONE_OF` | +| 日期 | `BEFORE` / `AFTER` / `NOT_BEFORE` / `NOT_AFTER` / `DATE_EQ` / `FROM_NOW` / `DATE_BETWEEN` | + +> **与 `record query --filters` / `view update filter` 的格式不同**:那两处用 `{operator, operands}` 结构;这里是 `{fieldId, symbol, value}`。不要混用。 + +### 典型用法 + +```bash +# 1) 给金额字段 > 100 的单元格上 firstLine5 色 +dws aitable view update fill-color-rule --view-id GRID_ID --json '[ + { + "type":"cell", + "formatFieldId":"fldAmount", + "format":{"color":"firstLine5"}, + "filters":[{"fieldId":"fldAmount","symbol":"GT","value":100}] + } +]' + +# 2) 清空所有规则 +dws aitable view update fill-color-rule --view-id GRID_ID --json '[]' + +# 3) 查询当前规则 +dws aitable view get fill-color-rule --view-id GRID_ID --format json +# → {"data": [...]} 数组 +``` + +> **写入后请用 `view get fill-color-rule` 二次确认实际生效**,以读到的 `data` 数组为准。 + +## 复制视图 + +```bash +# 显式命名 +dws aitable view duplicate --view-id VIEW_ID --new-name "副本视图" + +# 系统自动命名(一般是 "原视图名 (副本)") +dws aitable view duplicate --view-id VIEW_ID --format json +# → {"data": {..., "viewId": "<新视图ID>", "sourceViewId": "<原视图ID>", "viewName": "..."}} +``` + +复制会保留源视图的 filter / sort / group / visible-fields / card / timebar 等全部配置;新视图的 viewId 与源视图独立。 + +## 这些字段不能用 `view update --config '{...}'` 写 + +下列字段必须用对应的专属子命令;如果错塞进 `view update --config`,CLI 会在 stderr 提示对应子命令并拒绝把字段当 view config 处理: + +| 错误用法 | 应改用 | +|---|---| +| `--config '{"flags":1}'` | `view lock` / `view lock --off` | +| `--config '{"frozenColCount":2}'` | `view update frozen-cols --count N` | +| `--config '{"cellHeight":56}'` | `view update row-height --cell-height N` | +| `--config '{"rowHeightLevel":"tall"}'` | `view update row-height --cell-height N`(合法档位 32/56/88/128) | +| `--config '{"conditionalFormats":[...]}'` | `view update fill-color-rule --json '[...]'` | + +## 典型工作流 + +### 配置一个"金额超阈值红色高亮"的 Grid 视图 + +```bash +BASE=baseXXX; TABLE=tblYYY; VIEW=viwGridZZ; FLD=fldAmount + +# 1) 关键字段冻结,避免横向滚动看不到 +dws aitable view update frozen-cols --base-id $BASE --table-id $TABLE --view-id $VIEW --count 1 + +# 2) 加大行高让数据更易读 +dws aitable view update row-height --base-id $BASE --table-id $TABLE --view-id $VIEW --cell-height 56 + +# 3) 金额 > 100 的单元格上色 +dws aitable view update fill-color-rule --base-id $BASE --table-id $TABLE --view-id $VIEW --json "[ + {\"type\":\"cell\",\"formatFieldId\":\"$FLD\",\"format\":{\"color\":\"firstLine5\"}, + \"filters\":[{\"fieldId\":\"$FLD\",\"symbol\":\"GT\",\"value\":100}]} +]" + +# 4) 锁定视图,防止他人改坏 +dws aitable view lock --base-id $BASE --table-id $TABLE --view-id $VIEW +``` + +### 复制一个"金牌客户"视图给销售团队 + +```bash +dws aitable view duplicate --view-id viw_VIP_template --new-name "金牌客户-华东区" +# 取返回里 data.viewId 进一步定制 +``` + +### 排查"我设置了高亮规则为啥没生效" + +```bash +# 看实际生效的 conditionalFormats +dws aitable view get fill-color-rule --view-id VIEW_ID --format json +# → 如果是 [] 说明上次写入失败;常见原因:color 用了 hex(必须 firstLineN)/ filter 用了 operator(必须 symbol) +``` diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-workflow.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-workflow.md new file mode 100644 index 00000000..a223155a --- /dev/null +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-workflow.md @@ -0,0 +1,180 @@ +# workflow — 自动化工作流管理 + +启停 / 查看 / 列出 Base 下的自动化工作流(在 AI 表格 Web 端配置的 "当 X 时自动 Y" 流程)。 +适用场景:用户问 "停掉这个流程"、"看下都有哪些自动化流程"、"流程 X 的配置是什么"。 + +## 命令一览 + +| 命令 | 用途 | +|------|------| +| `workflow list` | 列出 Base 下所有工作流(含状态/创建人/最后修改时间),支持分页 | +| `workflow get` | 获取单个工作流详情(含 flowSchema 完整节点定义) | +| `workflow enable` | 启用指定工作流(按配置的触发条件自动执行) | +| `workflow disable` | 禁用指定工作流(高危,建议 `--yes` 二次确认) | + +> 所有子命令的 `--base-id` 必填(可用隐藏别名 `--base`)。 +> 当前**不支持通过 CLI 新建工作流**,请在 AI 表格 Web 端配置好后用 `workflow list` 拿到 ID 再启停。 + +## 命令详情 + +### workflow list — 列出工作流 + +```bash +dws aitable workflow list --base-id BASE_ID --format json +dws aitable workflow list --base-id BASE_ID --limit 50 --offset 100 +``` + +| flag | 说明 | +|------|------| +| `--base-id` | 必填 | +| `--limit` | 可选,分页大小 `[1, 100]`,不传走服务端默认 20 | +| `--offset` | 可选,分页偏移量 `>= 0`,不传走服务端默认 0 | + +返回结构: + +```json +{ + "data": { + "list": [ + { + "flowId": "G-FLOW-XXXXXX", // ★ 注意字段名是 flowId + "name": "流程1", + "description": "当创建记录时,就更新记录", + "status": "RUNNING", // RUNNING / STOP + "creatorStaffId": "281493", + "lastModifier": { "name": "李普阳", "staffId": "281493" }, + "gmtModified": 1780318540000, + "versionId": "G-FLOW-VER-XXXXXX", + "icons": ["..."], // 触发器+动作的图标 + "isSubFlow": false, + "opPermissions": { "canEdit": true } + } + ], + "recordCount": 1, // Base 下总数 + "runningCount": 1 // RUNNING 状态的数量 + } +} +``` + +**注意**: +- 标识字段服务端在 `list` 里叫 **`flowId`**,但在 `enable` / `disable` 出参里叫 **`workflowId`**。CLI `--workflow-id` 传任一即可(同值)。 +- `status` 是字符串枚举:`RUNNING`(启用中)/ `STOP`(已禁用),**不是** boolean。 +- `runningCount` 是当前 Base 下 status=RUNNING 的工作流数,方便快速判断「有几个流程在跑」。 + +### workflow get — 获取单个工作流详情 + +```bash +dws aitable workflow get --base-id BASE_ID --workflow-id WORKFLOW_ID --format json +``` + +| flag | 说明 | +|------|------| +| `--base-id` | 必填 | +| `--workflow-id` | 必填,对应 list 出参里的 `flowId` | + +返回完整工作流配置: + +```json +{ + "data": { + "name": "流程1", + "namespace": "...", + "status": "RUNNING", + "versionId": "G-FLOW-VER-XXXXXX", + "versionNo": 14, + "versionStatus": "...", + "accessor": {...}, // 访问者信息 + "corpId": "...", + "flowAttribute": {...}, // 流程顶层属性 + "flowSchema": {...}, // ★ 流程节点定义(触发器/动作/分支等) + "gmtCreate": 1780317804000, + "gmtModified": 1780318540000 + } +} +``` + +`flowSchema` 是完整的节点 DAG,结构因流程而异(条件触发器 vs 定时触发器、单分支 vs 多分支等)。agent 应按需读取关心字段,不要试图建静态 schema。 + +### workflow enable — 启用工作流 + +```bash +dws aitable workflow enable --base-id BASE_ID --workflow-id WORKFLOW_ID --format json +``` + +返回 `{workflowId, enabled: true}` —— **`enabled: true` 是动作确认,不是当前状态查询**。要确认真启用了,必须再 `workflow list` 看 `status` 是否变成 `"RUNNING"` 或 `runningCount` 是否加 1。 + +### workflow disable — 禁用工作流(高危) + +```bash +dws aitable workflow disable --base-id BASE_ID --workflow-id WORKFLOW_ID --yes --format json +``` + +返回 `{workflowId, disabled: true}` —— 同样是动作确认。禁用后该工作流不再自动触发。 + +**风险**:直接影响业务自动化(如停掉「记录创建后自动发通知」会让通知断流)。建议: +- 操作前先 `workflow get` 留底当前配置 +- 脚本场景显式传 `--yes`;交互场景让用户在 prompt 中再次确认 + +## 能力边界 + +| 能力 | 状态 | +|------|------| +| 列出工作流 | ✅ | +| 看工作流详情(含 flowSchema) | ✅ | +| 启用/禁用 | ✅ | +| 新建工作流 | ❌ 当前不支持,请去 AI 表格 Web 端 → 数据表 → 自动化 创建 | +| 修改工作流配置 | ❌ 同上,需 Web UI 编辑 | +| 删除工作流 | ❌ 同上 | +| 查看运行历史/执行日志 | ❌ 暂未开放 | +| 手动触发/单次运行 | ❌ 暂未开放 | + +## 错误码速查 + +| 场景 | code | type | 备注 | +|------|------|------|------| +| `workflow-id` 不存在调 get | `GET_WORKFLOW_ERROR` | `SYSTEM_ERROR` | message 可能为 null,先 `workflow list` 核对 ID | +| `workflow-id` 不存在调 enable | `ENABLE_WORKFLOW_ERROR` | `SYSTEM_ERROR` | message 含 "场域中不存在该 namespace" | +| `workflow-id` 不存在调 disable | `DISABLE_WORKFLOW_ERROR` | `SYSTEM_ERROR` | 同上 | +| `--limit` < 1 或 > 100 | (CLI 层拦截) | — | `--limit 必须在 [1, 100] 范围内,got N` | +| `--offset` < 0 | (CLI 层拦截) | — | `--offset 必须 >= 0,got N` | + +> 拿到 `*_WORKFLOW_ERROR / SYSTEM_ERROR` 时,先 `workflow list` 自查目标 ID 是否还存在、是否在当前 Base 下。 + +## 典型工作流 + +### 看看 Base 里有哪些自动化在跑 + +```bash +dws aitable workflow list --base-id BASE_ID --format json | jq '.data | {total: .recordCount, running: .runningCount, items: .list | map({name, status, flowId})}' +``` + +### 临时停掉某个流程做调试 + +```bash +# 1. 留底当前状态 +dws aitable workflow get --base-id BASE_ID --workflow-id WORKFLOW_ID --format json > /tmp/wf-backup.json + +# 2. 禁用 +dws aitable workflow disable --base-id BASE_ID --workflow-id WORKFLOW_ID --yes --format json + +# 3. 调试做完后重启 +dws aitable workflow enable --base-id BASE_ID --workflow-id WORKFLOW_ID --format json + +# 4. 确认 status=RUNNING +dws aitable workflow list --base-id BASE_ID --format json | jq '.data.list[] | select(.flowId == "WORKFLOW_ID") | .status' +``` + +### 批量关掉某个 Base 下所有 workflow(调试 / 迁移前清场) + +```bash +for WF in $(dws aitable workflow list --base-id BASE_ID --limit 100 --format json | jq -r '.data.list[] | select(.status == "RUNNING") | .flowId'); do + dws aitable workflow disable --base-id BASE_ID --workflow-id "$WF" --yes --format json | jq .status +done +``` + +## 注意事项 + +- `--workflow-id` 接受的就是 `list` 返回里的 `flowId`(同值,CLI 屏蔽了服务端字段名差异)。 +- enable / disable 出参里的 `enabled` / `disabled` 是 **动作确认 flag**,不是当前状态字段。要确认真生效请走 `workflow list` 查 `status`。 +- `workflow get` 的 `flowSchema` 结构随触发器/动作类型变化,不要假设固定字段。 +- 新建/修改/删除工作流目前必须在 AI 表格 Web 端(数据表页面 → 自动化)完成。 diff --git a/skills/multi/dingtalk-doc/references/doc.md b/skills/multi/dingtalk-doc/references/doc.md index dd67165c..218915d8 100644 --- a/skills/multi/dingtalk-doc/references/doc.md +++ b/skills/multi/dingtalk-doc/references/doc.md @@ -35,13 +35,13 @@ dws doc --help # 查看具体命令的完整参数说明 -dws doc list --help +dws doc read --help dws doc create --help dws doc block insert --help # 查看子命令组下的所有命令 dws doc block --help -dws doc permission --help +dws doc media --help ``` 规则: @@ -54,12 +54,10 @@ dws doc permission --help > 命令名 → 单文件,按需加载子文档。复杂任务请优先看下方 §场景索引。 -### 检索 / 阅读 / 元信息 +### 阅读 / 元信息 | 命令 | 用途 | 必填参数 | 详见 | |------|------|----------|------| -| `doc search` | 关键字搜索 / 最近访问 | — | [`doc/doc-search.md`](./doc/doc-search.md) | -| `doc list` | 文件夹遍历 | — | [`doc/doc-list.md`](./doc/doc-list.md) | | `doc info` | 文档元信息(含 contentType / extension) | `--node` | [`doc/doc-info.md`](./doc/doc-info.md) | | `doc read` | 读取正文(markdown 或 jsonml) | `--node` | [`doc/doc-read.md`](./doc/doc-read.md) | @@ -71,23 +69,31 @@ dws doc permission --help | `doc update` | 整篇 / 段落级更新(markdown / jsonml) | `--node` `--mode` | [`doc/doc-update.md`](./doc/doc-update.md) | | `doc block list/insert/update/delete` | 块级精细编辑(含 JSONML 节点操作) | `--node` (+ `--block-id`) | [`doc/doc-block.md`](./doc/doc-block.md) | -### 附件 / 评论 / 权限 / 导出 +### 附件 / 评论 / 导出 | 命令 | 用途 | 必填参数 | 详见 | |------|------|----------|------| | `doc media insert/download` | 附件 / 图片插入与下载 | `--node` `--file` 或 `--resource-id` | [`doc/doc-media.md`](./doc/doc-media.md) | | `doc comment list/create/reply/create-inline` | 文档评论与划词评论 | `--node` (+ ...) | [`doc/doc-comment.md`](./doc/doc-comment.md) | -| `doc permission add/update/list` | 节点级授权 | `--node` `--user` `--role` | [`doc/doc-permission.md`](./doc/doc-permission.md) | | `doc export` / `doc export get` | 在线文档导出 docx | `--node` `--output` | [`doc/doc-export.md`](./doc/doc-export.md) | -### 文件操作 +### 文件操作(已迁移,以下命令 deprecated) -| 命令 | 用途 | 必填参数 | 详见 | -|------|------|----------|------| -| `doc upload` | 上传文件到文档空间/知识库 | `--file` | [`doc/doc-file-ops.md`](./doc/doc-file-ops.md) | -| `doc download` | 下载已有文件(非 ALIDOC) | `--node` `--output` | [`doc/doc-file-ops.md`](./doc/doc-file-ops.md) | -| `doc copy` / `doc move` / `doc rename` / `doc delete` | 复制/移动/重命名/删除 | `--node` (+ ...) | [`doc/doc-file-ops.md`](./doc/doc-file-ops.md) | -| `doc folder create` | 创建文件夹 | `--name` | [`doc/doc-file-ops.md`](./doc/doc-file-ops.md) | +> **迁移提示**:文件管理命令已按产品领域架构重新归属,旧命令在过渡期内仍可使用,运行时会输出 deprecated 警告。 + +| 旧命令 | 推荐命令 | 详见 | +|--------|----------|------| +| `doc upload` | `dws drive upload` | [`drive.md`](./drive.md) | +| `doc download` | `dws drive download` | [`drive.md`](./drive.md) | +| `doc copy` | `dws drive copy` | [`drive.md`](./drive.md) | +| `doc move` | `dws drive move` | [`drive.md`](./drive.md) | +| `doc rename` | `dws drive rename` | [`drive.md`](./drive.md) | +| `doc delete` | `dws drive delete` | [`drive.md`](./drive.md) | +| `doc folder create` | `dws drive folder create` | [`drive.md`](./drive.md) | +| `doc file create` | `dws wiki node create --type ` | [`wiki.md`](./wiki.md) | +| `doc permission *` | `dws drive permission *` | [`drive.md`](./drive.md) | +| `doc list` | `dws drive list --workspace` / `dws wiki node list` | [`drive.md`](./drive.md) / [`wiki.md`](./wiki.md) | +| `doc search` | `dws drive search` / `dws wiki node search` | [`drive.md`](./drive.md) / [`wiki.md`](./wiki.md) | ### 排版规范 / JSONML 参考 @@ -105,7 +111,7 @@ dws doc permission --help | 任务场景 | 一次性读取 | 主命令 | |---------|-----------|--------| -| 定位 nodeId / URL 解析 / 目录遍历 | [`doc-search.md`](./doc/doc-search.md) + [`doc-list.md`](./doc/doc-list.md) + [`doc-info.md`](./doc/doc-info.md) | search | +| 定位 nodeId / URL 解析 | [`doc-info.md`](./doc/doc-info.md)(搜索请用 `dws drive search` / `dws wiki node search`;遍历请用 `dws drive list` / `dws wiki node list`) | `drive search` / `wiki node search` | | 阅读已有文档 | [`doc-info.md`](./doc/doc-info.md) + [`doc-read.md`](./doc/doc-read.md) | read | | 创建新文档 | [`doc-create.md`](./doc/doc-create.md) + [`doc-update.md`](./doc/doc-update.md)(写入管道)+ [`style/doc-create-workflow.md`](./doc/style/doc-create-workflow.md) + [`style/doc-style-guideline.md`](./doc/style/doc-style-guideline.md) | create | | 创建文档且包含图片/截图/图文并茂 | [`doc-create.md`](./doc/doc-create.md) + [`doc-media.md`](./doc/doc-media.md) + [`style/doc-create-workflow.md`](./doc/style/doc-create-workflow.md) + [`style/doc-style-guideline.md`](./doc/style/doc-style-guideline.md) | create → media insert | @@ -114,16 +120,17 @@ dws doc permission --help | 插入富 block(callout / 分栏 / 表格) | [`doc-block.md`](./doc/doc-block.md) + [`format/doc-jsonml-cookbook.md`](./doc/format/doc-jsonml-cookbook.md) + [`style/doc-style-guideline.md`](./doc/style/doc-style-guideline.md) | block insert | | 上传图片 / 附件 | [`doc-media.md`](./doc/doc-media.md) | media insert | | 评论 / 划词评论(含 @人) | [`doc-comment.md`](./doc/doc-comment.md)(+ `dws contact user search` 取 mention 用 userId) | comment create | -| 文档分享 / 节点级权限 | [`doc-permission.md`](./doc/doc-permission.md) | permission add | +| 文档分享 / 节点级权限 | [`drive.md`](./drive.md)(已迁移:`dws drive permission add/update/list/remove`) | `drive permission` | | 导出 PDF / DOCX | [`doc-info.md`](./doc/doc-info.md) + [`doc-export.md`](./doc/doc-export.md) | export | -| 文件下载 / 上传 / 移动 / 重命名 / 复制 | [`doc-file-ops.md`](./doc/doc-file-ops.md) + [`doc-info.md`](./doc/doc-info.md)(download 前判 contentType) | upload / download / move / copy / rename | +| 文件下载 / 上传 / 移动 / 重命名 / 复制 | [`drive.md`](./drive.md)(已迁移:`dws drive upload/download/copy/move/rename/delete`) | `drive *` | ## 意图判断 用户说"找文档/搜文档/最近文档": -- 搜索 → `search` -- 浏览 → `list` +- 全局搜索 → `dws drive search --query "<关键词>"`(聚合钉盘+文档空间) +- 空间内搜索 → `dws wiki node search --workspace --keyword "<关键词>"` +- 遍历文件夹 → `dws drive list --workspace ` 或 `dws wiki node list --workspace ` 用户说"看文档/读内容/文档内容": @@ -139,49 +146,47 @@ dws doc permission --help > 严禁把「创建表格」路由到 `doc create`: > -> - 用户说"创建表格/新建表格/建个电子表格/在线表格" → 走 [`dws sheet create`](../../dingtalk-sheet/references/sheet.md#创建钉钉表格文档)(axls 在线电子表格) -> - 用户说"创建多维表格/新建 AI 表格/建 base/数据库表" → 走 [`dws aitable base create`](../../dingtalk-aitable/references/aitable.md#创建-ai-表格)(able 多维表格) +> - 用户说"创建表格/新建表格/建个电子表格/在线表格" → 走 [`dws sheet create`](./sheet.md#创建钉钉表格文档)(axls 在线电子表格) +> - 用户说"创建多维表格/新建 AI 表格/建 base/数据库表" → 走 [`dws aitable base create`](./aitable.md#创建-ai-表格)(able 多维表格) 用户说"建文件夹/新建目录": -- 创建 → `folder create` +- 创建 → `dws drive folder create` 用户说"上传文件/传文件/上传到文档/上传到知识库": -- 上传 → `upload`(需本地文件路径) -- 上传并转换 → `upload --convert` +- 上传 → `dws drive upload`(需本地文件路径) 用户说"下载/导出/下载到本地/导出文档/导出为Word/导出为docx/把文档导出来": - **必须先判断目标文件类型**,再决定走 `export` 还是 `download`: - - 在线文档 (alidocs/adoc) → **`export`**(格式转换后导出为 docx) - - 已有文件(PDF、图片、附件、视频等非在线文档) → **`download`**(直接下载原始文件) + - 在线文档 (alidocs/adoc) → **`doc export`**(导出是内容层操作,仅对 adoc 有意义) + - 已有文件(PDF、图片、附件、视频等非在线文档) → **`dws drive download`** - 判断方法: - 1. 如果用户明确说了"导出文档"、"导出为Word/docx" → 直接走 `export` - 2. 如果用户明确说了"下载PDF/图片/附件" → 直接走 `download` - 3. 不确定时,先用 `info --node ` 查询节点信息,根据返回的 `contentType` 字段判断: - - `contentType` 为 `ALIDOC` → 走 `export` - - `contentType` 为 `DOCUMENT`/`IMAGE`/`VIDEO` 等 → 走 `download` + 1. 如果用户明确说了"导出文档"、"导出为Word/docx" → 直接走 `doc export` + 2. 如果用户明确说了"下载PDF/图片/附件" → 直接走 `drive download` + 3. 不确定时,先用 `drive info --node ` 查询节点信息,根据返回的 `contentType` 字段判断: + - `contentType` 为 `ALIDOC` → 走 `doc export` + - `contentType` 为 `DOCUMENT`/`IMAGE`/`VIDEO` 等 → 走 `drive download` > **严禁将"导出文档"直接路由到 `download`**。`download` 只能下载已有文件(原样下载),`export` 是将在线文档格式转换后导出为 docx,两者完全不同。 用户说"复制文档/拷贝文件/复制到": -- 复制 → `copy`(需文档 ID 或 URL + 目标位置) +- 复制 → `dws drive copy` 用户说"移动文档/搬到/移到/转移文件": -- 移动 → `move`(需文档 ID 或 URL + 目标位置) +- 移动 → `dws drive move` 用户说"重命名/rename/改名/改文档名/修改文档名称/修改文档标题/把这个文档叫做...": -- 重命名 → `doc rename`(需文档 ID 或 URL + 新名称) -- 只要意图是修改文档在列表和链接中展示的名称,统一路由到 `dws doc rename --node --name "新名称"`;不要走 `drive`、`doc update` 或重新 `doc create`。 +- 重命名 → `dws drive rename --node --name "新名称"` - 只有用户明确说"正文里的标题/章节标题/段落标题/H1 标题"时,才走 `block update`。 用户说"删除文档/删掉这个文件/移到回收站/丢掉这篇文档": -- 删除节点 → `delete`(危险操作,需确认;需文档 ID 或 URL) +- 删除节点 → `dws drive delete` 用户说"插入附件/上传附件到文档/往文档里加文件/加附件": @@ -205,16 +210,17 @@ dws doc permission --help 用户说"给某人开权限/分享给某人/授权某文档/把这篇文档给 xxx 看": -- 新增权限 → `permission add`(需 `--node` + `--user` + `--role`) -- 修改权限 → `permission update` -- 查看谁有权限 → `permission list` +- 新增权限 → `dws drive permission add` +- 修改权限 → `dws drive permission update` +- 查看谁有权限 → `dws drive permission list` +- 移除权限 → `dws drive permission remove` > **关键区分**: > -> - "把**某篇文档**授权给某人" → `doc permission add`(节点级,包括「我的文档」下的文档都支持) +> - "把**某篇文档**授权给某人" → `drive permission add`(节点级,包括「我的文档」下的文档都支持) > - "把**某个知识库**整体授权给某人" → `wiki member add`(容器级,但**「我的文档」个人空间不支持**) -> 补充:如果用户直接粘贴的是原始 `alidocs` URL,先按 [链接规范](./url-patterns.md#alidocs-url-类型探测流程) probe;只有 probe 确认是 `adoc` / `file` / `folder` 后,才继续按下列意图执行。 +> 补充:如果用户直接粘贴的是原始 `alidocs` URL,先按 [链接规范](../url-patterns.md#alidocs-url-类型探测流程) probe;只有 probe 确认是 `adoc` / `file` / `folder` 后,才继续按下列意图执行。 **用户直接粘贴文档 URL(无其他指令)**: @@ -226,7 +232,7 @@ dws doc permission --help - "帮我看看这个文档" → `read` - "这个文档的信息" → `info` - "往这个文档追加内容" → `update --mode append` -- "把这个文档标题改成 X" / "这个文档改名为 X" → `rename` +- "把这个文档标题改成 X" / "这个文档改名为 X" → `dws drive rename` - "把正文里的一级标题/章节标题改成 X" → `block update` 关键区分: doc(文档编辑/阅读) vs aitable(数据表格操作) vs drive(钉盘文件管理) @@ -236,14 +242,13 @@ dws doc permission --help > 步骤性指引。"读哪些文件"组合参见上方 §场景索引;命令详细参数参见对应子文件。 ```bash -# ── 工作流 1: 浏览并阅读文档 ── -dws doc list --format json # 1. 浏览根目录 -dws doc list --folder --format json # 2. 进入子目录 -dws doc info --node --format json # 3. 元信息(含 contentType) -dws doc read --node --format json # 4. 读 markdown 正文 +# ── 工作流 1: 定位并阅读文档 ── +dws drive search --query "<关键词>" --format json # 1. 搜索定位 nodeId(或 wiki node search) +dws doc info --node --format json # 2. 元信息(含 contentType) +dws doc read --node --format json # 3. 读 markdown 正文 # ── 工作流 2: 创建文档(含分片自动写入)── -dws doc folder create --name "项目资料" --format json # 1. (可选) 文件夹 +dws drive folder create --name "项目资料" --format json # 1. (可选) 创建文件夹 dws doc create --name "项目周报" --content-file /tmp/x.md \ --folder --format json # 2. 创建 + 写入 dws doc read --node --format json # 3. 回读校验(必须) @@ -263,14 +268,14 @@ dws doc update --node --content-file /tmp/doc.json \ dws doc read --node --content-format jsonml # 3. 回读 # 担心被并发覆盖时,可加 --revision 触发并发检查(详见 doc-update.md) -# ── 工作流 5: 上传 vs 插入附件 ── -dws doc upload --file ./report.pdf --folder # 上传作为独立文件 -dws doc media insert --node --file ./report.pdf # 上传并作为附件块插入正文 +# ── 工作流 5: 上传独立文件 vs 插入附件到文档正文 ── +dws drive upload --file ./report.pdf --folder # 上传作为独立文件(存储层) +dws doc media insert --node --file ./report.pdf # 上传并作为附件块插入正文(内容层) # ── 工作流 6: 下载 vs 导出(先 info 判 contentType)── dws doc info --node --format json # 必须先查 contentType -dws doc export --node --output ~/downloads/ # contentType=ALIDOC 走 export -dws doc download --node --output ~/downloads/ # contentType≠ALIDOC 走 download +dws doc export --node --output ~/downloads/ # contentType=ALIDOC 走 export(内容层) +dws drive download --node --output ~/downloads/ # contentType≠ALIDOC 走 drive download(存储层) # ── 工作流 7: 评论 + 划词(@人 需 userId)── dws contact user search --query "张三" --format json # 取 userId @@ -279,38 +284,32 @@ dws doc block list --node --format json # 划词需先 dws doc comment create-inline --node --block-id \ --start 0 --end 10 --content "建议调整" --selected-text "原文" --format json -# ── 工作流 8: 权限授予(节点级)── +# ── 工作流 8: 权限授予(节点级,已迁移到 drive)── dws contact user search --query "张三" --format json # 取 userId -dws doc permission add --node --user , --role EDITOR --format json -dws doc permission list --node --format json # 校验 - -# ── 工作流 9: 文件操作(复制/移动/重命名/删除)── -# 第一步:获取 nodeId(三种方式按场景选一,命中即停,不要冗余调用) -# 方式 A(优先):用户直接提供 URL / nodeId → 直接传 --node,跳过 search/list -# 方式 B:按关键字找:dws doc search --query "项目周报" --format json -# 方式 C:按文件夹遍历:dws doc list --folder --format json -# 第二步:执行(--node 支持 ID 或完整 URL;--folder 支持文件夹 nodeId 或 alidocs 文件夹 URL) -dws doc copy --node --folder --format json # 异步任务 -dws doc move --node --folder --format json -dws doc rename --node --name "新名称" --format json -# 删除是危险操作:必须先向用户展示「即将删除「文档名」到回收站」 → 用户确认 → 才加 --yes -dws doc delete --node --yes --format json +dws drive permission add --node --user , --role EDITOR --format json +dws drive permission list --node --format json # 校验 + +# ── 工作流 9: 文件操作(已迁移到 drive)── +# 第一步:获取 nodeId +# 方式 A(优先):用户直接提供 URL / nodeId → 直接传 --node +# 方式 B:按关键字找:dws drive search --query "项目周报" --format json +# 方式 C:按文件夹遍历:dws drive list --workspace --format json +# 第二步:执行 +dws drive copy --node --folder --format json +dws drive move --node --folder --format json +dws drive rename --node --name "新名称" --format json +dws drive delete --node --yes --format json ``` ## 上下文传递表 | 操作 | 从返回中提取 | 用于 | |------|-------------|------| -| `list` | `nodes[].nodeId` | read / info / update / copy / move / rename / block 操作的 --node | -| `list` | folder 类型的 `nodeId` | list 的 --folder, create / copy / move 的 --folder | -| `search` | 文档 `nodeId` / URL / `createTime` / `creatorUid` | read / info / update / copy / move / rename 的 --node;创建时间与创建者信息 | +| `drive search` / `wiki node search` | 文档 `nodeId` / URL | doc read / info / update 等所有 `--node` 入参 | +| `drive list` / `wiki node list` | `nodes[].nodeId` | doc read / info / update / block 操作的 --node | | `create` | `nodeId` | update / block 操作的 --node | -| `folder create` | `nodeId` | create / list / upload / copy / move 的 --folder | | `block list` | `blockId` | block insert 的 --ref-block, block update/delete 的 --block-id | | `read --content-format jsonml` | `revision` | update --content-format jsonml 的 --revision(可选,并发检查时使用) | -| `upload` | `nodeId` / URL | 上传后文件的访问链接 | -| `download` | 本地文件路径 | 下载后的文件保存位置 | -| `copy` | `nodeId` / URL (异步,不保证返回) | 复制是异步任务,若任务未完成则不会返回新文档 ID;如需获取可稍后通过 list 查询目标文件夹 | | `media insert` | `resourceId` | 附件已插入文档,可通过 block list 查看附件块 | | `media download` | 附件下载链接 `downloadUrl` | 下载文档中的附件资源 | | `block list` | attachment 块的 `resourceId` | media download 的 --resource-id | @@ -319,11 +318,11 @@ dws doc delete --node --yes --format json | `comment create-inline` | `commentKey` | comment reply 的 --comment-key | | `block list` | `blocks[].element.id` | comment create-inline 的 --block-id | | `block list` | `blocks[].element.paragraph.text` | 计算 create-inline 的 --start / --end 偏移量 | -| `contact user search` | `userId` | comment create/reply/create-inline 的 --mention;permission add/update 的 --user | +| `contact user search` | `userId` | comment create/reply/create-inline 的 --mention;drive permission 的 --user | ## 相关产品 -- [wiki](../../dingtalk-wiki/references/wiki.md) — 知识库空间级管理(创建/查询/列出/搜索知识库),doc 中的文档存储在 wiki 知识库中 -- [aitable](../../dingtalk-aitable/references/aitable.md) — 结构化数据表格(行列/字段/记录),不是富文本文档 -- [drive](../../dingtalk-drive/references/drive.md) — 钉盘文件存储/上传/下载,不是文档内容编辑 -- [report](../../dingtalk-report/references/report.md) — 钉钉日志系统(日报/周报模版),不是在线文档 +- [wiki](./wiki.md) — 知识库空间级管理(创建/查询/列出/搜索知识库),doc 中的文档存储在 wiki 知识库中 +- [aitable](./aitable.md) — 结构化数据表格(行列/字段/记录),不是富文本文档 +- [drive](./drive.md) — 钉盘文件存储/上传/下载,不是文档内容编辑 +- [report](./report.md) — 钉钉日志系统(日报/周报模版),不是在线文档 diff --git a/skills/multi/dingtalk-doc/references/doc/doc-block.md b/skills/multi/dingtalk-doc/references/doc/doc-block.md index 439cd021..04e97ac2 100644 --- a/skills/multi/dingtalk-doc/references/doc/doc-block.md +++ b/skills/multi/dingtalk-doc/references/doc/doc-block.md @@ -2,13 +2,13 @@ > **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件: > 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流 -> 2. [`./style/doc-update-workflow.md`](./style/doc-update-workflow.md) — 改写流程(编辑形态优先级、JSONML normalize/validator 行为) +> 2. [`./style/doc-update-workflow.md`](./style/doc-update-workflow.md) — 改写流程(编辑形态优先级、JSONML validator 行为) > 3. [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md) — JSONML 范例(含 callout / 分栏 / 表格 / 标题等节点的完整命令) > 4. [`./format/doc-jsonml-schema.md`](./format/doc-jsonml-schema.md) — JSONML 节点结构字段定义 > > **同任务常配合**:[`doc-update.md`](./doc-update.md)(整篇 overwrite / 末尾追加纯文本)/ [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md)(JSONML 复制范例) -> **改写已有文档优先 JSONML**:保真度最高、callout / 分栏 / 表格 / @人 / 附件 / 颜色 / 嵌套都能 1:1 round-trip;写入端默认 normalize + validate。详见 [`./style/doc-update-workflow.md` §1.3 编辑形态优先级](./style/doc-update-workflow.md)。 +> **改写已有文档优先 JSONML**:保真度最高、callout / 分栏 / 表格 / @人 / 附件 / 颜色 / 嵌套都能 1:1 round-trip;写入端有 validator 兜底。详见 [`./style/doc-update-workflow.md` §1.3 编辑形态优先级](./style/doc-update-workflow.md)。 --- @@ -88,8 +88,7 @@ Flags: --level int 标题级别 1-6 (配合 --heading,默认 1) --element string 块元素 JSON (高级);content-format=jsonml 时为 JSONML 数组字符串 --content-format string 输入格式: 默认为 element,可选 jsonml - --fix-jsonml 启用全部 JSONML 修复(含 JSON 语法修复 + 结构修复),推荐 agent 调用时使用 - --no-fix-jsonml 关闭全部 JSONML 修复(跳过 JSON 语法修复和结构修复),用于排查原始错误 + --fix-jsonml 启用 JSON 语法修复(括号/逗号补全),推荐 agent 调用时使用 --index int 参照位置索引 (从 0 开始) --where string 插入方向: before / after (默认 after) --ref-block string 参照块 ID (优先级高于 --index) @@ -116,8 +115,7 @@ Flags: --level int 标题级别 1-6 (配合 --heading,默认 1) --element string 块元素 JSON (高级);content-format=jsonml 时为 JSONML 数组字符串 --content-format string 输入格式: 默认为 element,可选 jsonml - --fix-jsonml 启用全部 JSONML 修复(含 JSON 语法修复 + 结构修复),推荐 agent 调用时使用 - --no-fix-jsonml 关闭全部 JSONML 修复(跳过 JSON 语法修复和结构修复),用于排查原始错误 + --fix-jsonml 启用 JSON 语法修复(括号/逗号补全),推荐 agent 调用时使用 ``` > 使用 `--content-format jsonml` 时,`element` 中的 `uuid` **必须**等于 `--block-id`,否则报错。 @@ -169,17 +167,16 @@ dws doc block update --node DOC_ID --block-id UUID --content-format jsonml \ dws doc block delete --node DOC_ID --block-id UUID ``` -> 关于自动修复 / 严格校验:默认会自动注入 uuid、把裸字符串包成 span/leaf;如要禁用,用 `--no-fix-jsonml`。如需同时启用 JSON 语法修复(修复 LLM 遗漏的括号/逗号),用 `--fix-jsonml`。文本结构定义见 [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md)。 +> 关于校验:CLI 不做结构修复,裸字符串、缺 uuid 等错误会被 validator 直接抦下。如需同时启用 JSON 语法修复(修复 LLM 遗漏的括号/逗号),用 `--fix-jsonml`。文本结构定义见 [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md)。 ## 关键说明 - **块类型**:paragraph、heading、blockquote、callout、columns、orderedList、unorderedList、table、sheet、attachment、slot。 - **快捷 vs --element**:`block insert` 优先使用 `--text` 或 `--heading` 快捷方式;复杂块类型(table、callout、columns 等)使用 `--element` JSON 或 `--content-format jsonml`。 - **简单内容追加**:建议用 [`./doc-update.md`](./doc-update.md) `--mode append`,不必走 block insert。 -- **JSONML normalize + validator**(写入端默认行为): - - 缺 `uuid` 的 block 会自动注入;裸字符串自动包成 `["span",{"data-type":"text"},["span",{"data-type":"leaf"},"..."]]`;每条修复以 `[FIX]` 行输出。 - - 结构错误会被 validator 拦下并返回带 path 的错误(如 `$[2][2]: paragraph child must be span wrapper, got raw string.`)。 - - `--no-fix-jsonml` 关闭全部修复(normalize + JSON repair);`--fix-jsonml` 开启全部修复(含 JSON 语法修复),推荐 agent 调用。 +- **JSONML validator**(写入端默认行为): + - 裸字符串、缺 uuid 等结构错误会被 validator 抦下并返回带 path 的错误(如 `$[2][2]: paragraph child must be span wrapper, got raw string.`)。 + - `--fix-jsonml` 开启 JSON 语法修复,推荐 agent 调用。 - **图片插入**:插入图片走 [`./doc-media.md`](./doc-media.md) `media insert`(作为附件块),不走 block insert。 - **分割线**:用 [`./doc-update.md`](./doc-update.md) `--content "---" --mode append`,不走 block insert。 @@ -265,7 +262,7 @@ dws doc block insert --node --content-format jsonml --parent-block `dws doc create` 只能创建在线文字文档(adoc),**不要**用它承接所有「新建 xxx」请求。收到「创建/新建」类需求时,必须先按文件类型分流: > -> - 用户说「创建表格 / 新建表格 / 建个电子表格 / 在线表格 / 销售数据表」等 → 走 [`dws sheet create`](../../../dingtalk-sheet/references/sheet.md#创建钉钉表格文档)(钉钉在线电子表格 `axls`),**不要**走 `doc create` -> - 用户说「创建多维表格 / 新建 AI 表格 / 建个 base / 数据库表」等 → 走 [`dws aitable base create`](../../../dingtalk-aitable/references/aitable.md#创建-ai-表格)(多维表格 `able`),**不要**走 `doc create` +> - 用户说「创建表格 / 新建表格 / 建个电子表格 / 在线表格 / 销售数据表」等 → 走 [`dws sheet create`](../sheet.md#创建钉钉表格文档)(钉钉在线电子表格 `axls`),**不要**走 `doc create` +> - 用户说「创建多维表格 / 新建 AI 表格 / 建个 base / 数据库表」等 → 走 [`dws aitable base create`](../aitable.md#创建-ai-表格)(多维表格 `able`),**不要**走 `doc create` > - 用户说「创建文档 / 新建文档 / 写篇文档 / 会议纪要 / 周报 / 方案」等文字型内容 → 才走 `dws doc create` > > 一句话口诀:表格 → sheet/aitable;文档 → doc。 @@ -35,13 +35,12 @@ Flags: --content string 文档初始内容(短文本字面量);传 - 表示从 stdin 读取 --content-file string 从文件读取文档内容(UTF-8)。推荐长/多行/表格内容使用 --content-format string 内容格式: 默认为 markdown,可选 jsonml - --fix-jsonml 启用全部 JSONML 修复(含 JSON 语法修复 + 结构修复),推荐 agent 调用时使用 - --no-fix-jsonml 关闭全部 JSONML 修复(跳过 JSON 语法修复和结构修复),用于排查原始错误 + --fix-jsonml 启用 JSON 语法修复(括号/逗号补全),推荐 agent 调用时使用 ``` ## 关键说明 -- **`--name` 是 H1**:正文从 `##` 开始;正文内不要再写 `#` 一级标题(除非确需且已说明动机)。若正文首行仍是与 `--name` 相同的一级标题,CLI 会自动移除并在 stderr 提示(仅精确匹配会被移除,其他一级标题不受影响)。 +- **`--name` 是 H1**:正文从 `##` 开始;正文内不要再写 `#` 一级标题(除非确需且已说明动机)。 - 不传 `--folder` 和 `--workspace` 时,默认创建在「我的文档」根目录。 - `--folder` 仅接受文档文件夹 `nodeId` / `dentryUuid` / alidocs 文件夹 URL;**禁止**传入 drive `dentryId`、`parentId`、`spaceId` 这类纯数字 ID。 - 输入方式选择见 [`./doc-update.md` §内容写入管道](./doc-update.md#内容写入管道createupdate-共用)(与 update 共用)。短文本字面量可 `--content`,多行/表格/特殊字符必须 `--content-file` 或 `--content -`。 diff --git a/skills/multi/dingtalk-doc/references/doc/doc-export.md b/skills/multi/dingtalk-doc/references/doc/doc-export.md index 45e31cba..0abbeedf 100644 --- a/skills/multi/dingtalk-doc/references/doc/doc-export.md +++ b/skills/multi/dingtalk-doc/references/doc/doc-export.md @@ -5,9 +5,9 @@ > **路由前置判断**:用户说「下载/导出」时**必须**先用 [`./doc-info.md`](./doc-info.md) `info --node --format json` 查 `contentType`: > - `contentType` 为 `ALIDOC`(在线文档)→ **必须用 `export`**,禁止用 `download` -> - `contentType` 为 `DOCUMENT`/`IMAGE`/`VIDEO` 等(已有文件)→ 用 [`./doc-file-ops.md`](./doc-file-ops.md) `download` +> - `contentType` 为 `DOCUMENT`/`IMAGE`/`VIDEO` 等(已有文件)→ 用 `dws drive download`(详见 [`../drive.md`](../drive.md)) > -> `download` 只能下载**已有文件**(原样下载),`export` 是将**在线文档格式转换**后导出为 docx,两者完全不同。 +> `drive download` 只能下载**已有文件**(原样下载),`export` 是将**在线文档格式转换**后导出为 docx,两者完全不同。 --- @@ -77,4 +77,4 @@ dws doc export get --job-id --format json - [`../doc.md` §意图判断](../doc.md#意图判断)(如何路由到本命令) - [`./doc-info.md`](./doc-info.md)(前置:判断 contentType=ALIDOC 才走 export) -- [`./doc-file-ops.md`](./doc-file-ops.md)(非 ALIDOC 文件用 download) +- [`../drive.md`](../drive.md)(非 ALIDOC 文件用 `dws drive download`) diff --git a/skills/multi/dingtalk-doc/references/doc/doc-info.md b/skills/multi/dingtalk-doc/references/doc/doc-info.md index 856ddab6..49b7a682 100644 --- a/skills/multi/dingtalk-doc/references/doc/doc-info.md +++ b/skills/multi/dingtalk-doc/references/doc/doc-info.md @@ -2,9 +2,9 @@ > **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件: > 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流 -> 2. [`../url-patterns.md`](../url-patterns.md) — 仅当用户原始 `alidocs` URL 需要 probe 时 +> 2. [`../../url-patterns.md`](../../url-patterns.md) — 仅当用户原始 `alidocs` URL 需要 probe 时 > -> **同任务常配合**:[`doc-search.md`](./doc-search.md) / [`doc-list.md`](./doc-list.md)(先拿到 nodeId)/ [`doc-read.md`](./doc-read.md)(确认是 ALIDOC 后读正文) +> **同任务常配合**:`dws drive search` / `dws wiki node search`(先定位 nodeId)/ [`doc-read.md`](./doc-read.md)(确认是 ALIDOC 后读正文) ## 命令格式 @@ -37,7 +37,7 @@ Flags: 当用户输入包含钉钉文档 URL 时,**必须先识别并提取 DOC_ID**,再判断意图。 -补充:如果这是用户直接提供的原始 `alidocs` URL,必须先按 [链接规范](../url-patterns.md#alidocs-url-类型探测流程) probe 一次确认真实类型,再判断是否继续走 `doc`。 +补充:如果这是用户直接提供的原始 `alidocs` URL,必须先按 [链接规范](../../url-patterns.md#alidocs-url-类型探测流程) probe 一次确认真实类型,再判断是否继续走 `doc`。 ### 支持的 URL 格式 @@ -51,7 +51,7 @@ Flags: 1. 匹配 URL 中 `alidocs.dingtalk.com` 域名 2. 取 URL path 的最后一段作为 DOC_ID(去掉 query string 和 fragment) 3. 提取出的 DOC_ID 可直接用于所有 `--node` 参数,也可将完整 URL 传给 `--node`(CLI 会自动解析) -4. 对用户直接提供的原始 `alidocs` URL,先按 [链接规范](../url-patterns.md#alidocs-url-类型探测流程) 执行 probe;只有 probe 确认是 `adoc` / `file` / `folder` 时,才继续走 `doc` +4. 对用户直接提供的原始 `alidocs` URL,先按 [链接规范](../../url-patterns.md#alidocs-url-类型探测流程) 执行 probe;只有 probe 确认是 `adoc` / `file` / `folder` 时,才继续走 `doc` ## ID 边界与参数映射 @@ -59,7 +59,7 @@ Flags: - `dentryUuid` 是 `alidocs` URL `/i/nodes/{dentryUuid}` 的最后一段,在 `doc` 场景中等价于可传入 CLI 的 `nodeId`;不要把它改写成数字 ID。 - `dentryId` 通常是纯数字,**不是** `doc` 的 `nodeId`,也不是 `doc --folder` 的目标文件夹 ID;不要把数字 `dentryId` 当作 `--node`、`--folder` 或 `--parent-id` 使用。 - `parentId` / `--parent-id` 不是 `doc` 命令参数;`doc` 里目标父文件夹统一使用 `--folder `,目标知识库使用 `--workspace `。 -- 如果上下文只有数字 `dentryId`,但用户要读、改、移动、复制、重命名文档,先通过 `doc search` / `doc list` / 用户提供的 `alidocs` URL 获取 `nodeId` / `dentryUuid`,不要用数字 `dentryId` 重试为父目录参数。 +- 如果上下文只有数字 `dentryId`,但用户要读、改、移动、复制、重命名文档,先通过 `dws drive search` / `dws drive list` / 用户提供的 `alidocs` URL 获取 `nodeId` / `dentryUuid`,不要用数字 `dentryId` 重试为父目录参数。 ## 处理流程 @@ -78,8 +78,8 @@ Flags: | 方式 | 触发条件 | 操作 | |------|----------|------| | **A** | 用户**直接提供文档 URL 或 nodeId** | **直接传给 `--node`**,无需额外查询;优先使用此方式 | -| **B** | 用户给出关键字 / 文档名 | `dws doc search --query "<关键字>" --format json` 从返回的 `nodes[].nodeId` 提取 | -| **C** | 用户指向某个文件夹下的文档 | `dws doc list --folder --format json` 从返回中提取 | +| **B** | 用户给出关键字 / 文档名 | `dws drive search --query "<关键字>" --format json` 或 `dws wiki node search --workspace --keyword "<关键字>"` 从返回中提取 nodeId | +| **C** | 用户指向某个文件夹下的文档 | `dws drive list --workspace --format json` 或 `dws wiki node list --workspace ` 从返回中提取 | > **关键节省**:方式 A 命中时,禁止再调 search/list "确认一下" —— 用户提供的 URL/nodeId 本身就是权威输入。同理,`--folder` 也支持 alidocs 文件夹 URL 直传,不要先 search 把 URL 解析成纯数字 ID 再传。 @@ -104,13 +104,13 @@ dws doc read --node "https://alidocs.dingtalk.com/document/preview?cid=749936706 `--folder` 参数同样支持 alidocs 文件夹 URL 或文档文件夹 nodeId。 -不要把纯数字 `dentryId` 当成这里的 ID。需要父文件夹时,使用文件夹的 `nodeId` / `dentryUuid` / URL 传给 `--folder`;不能改用 `--parent-id`。如果上一步只拿到了 drive/chat 链路里的纯数字 `dentryId`、`spaceId` 或 `parent-id`,说明还没有拿到 doc 文件夹,应该省略 `--folder` 使用默认文档根目录,或先通过 `dws doc list/search` 找到文档文件夹 nodeId。 +不要把纯数字 `dentryId` 当成这里的 ID。需要父文件夹时,使用文件夹的 `nodeId` / `dentryUuid` / URL 传给 `--folder`;不能改用 `--parent-id`。如果上一步只拿到了 drive/chat 链路里的纯数字 `dentryId`、`spaceId` 或 `parent-id`,说明还没有拿到 doc 文件夹,应该省略 `--folder` 使用默认文档根目录,或先通过 `dws drive search` / `dws drive list` 找到文档文件夹 nodeId。 ## 上下文传递 | 从返回中提取 | 用于 | |-------------|------| -| `contentType` + `extension` | 选择 [`./doc-read.md`](./doc-read.md) / `dws sheet ...` / `dws aitable ...` / [`./doc-file-ops.md`](./doc-file-ops.md) 的下载/导出路径 | +| `contentType` + `extension` | 选择 [`./doc-read.md`](./doc-read.md) / `dws sheet ...` / `dws aitable ...` / `dws drive download`(非 ALIDOC 走存储层下载) | | `nodeId` / `docUrl` | 后续所有 `--node` 入参 | ## 常用模板 @@ -145,6 +145,6 @@ dws doc info --node # 错误 ## 参考 - [`../doc.md` §意图判断](../doc.md#意图判断)(如何路由到本命令) -- [`./doc-search.md`](./doc-search.md) / [`./doc-list.md`](./doc-list.md)(前置:拿 nodeId 的入口) +- `dws drive search` / `dws wiki node search`(前置:定位 nodeId 的搜索入口,详见 [`../drive.md`](../drive.md) / [`../wiki.md`](../wiki.md)) - [`./doc-read.md`](./doc-read.md)(contentType=ALIDOC + extension=adoc 的后续命令) -- [`../url-patterns.md`](../url-patterns.md)(用户原始 alidocs URL 的 probe 流程) +- [`../../url-patterns.md`](../../url-patterns.md)(用户原始 alidocs URL 的 probe 流程) diff --git a/skills/multi/dingtalk-doc/references/doc/doc-media.md b/skills/multi/dingtalk-doc/references/doc/doc-media.md index 8aed95b2..3be8600b 100644 --- a/skills/multi/dingtalk-doc/references/doc/doc-media.md +++ b/skills/multi/dingtalk-doc/references/doc/doc-media.md @@ -4,7 +4,7 @@ > 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流 > ⚠️ **图片插入硬规则**: -> - 图片来源如果是钉盘/文档空间中的文件,**必须先下载到本地**(`dws doc download --node <图片nodeId> --output /tmp/xxx.png`),再执行 `media insert` +> - 图片来源如果是钉盘/文档空间中的文件,**必须先下载到本地**(`dws drive download --node <图片nodeId> --output /tmp/xxx.png`),再执行 `media insert` > - **禁止**把钉盘/文档节点 URL(如 `alidocs.dingtalk.com/i/nodes/...`)写进 Markdown `![](...)` 图片语法——这些是页面链接,不是可渲染的图片资源 > - 创建文档时需要图文并茂:先用 `doc create` 写入纯文本骨架,再对每张图片执行 `media insert`,最后用 `doc block list` 验证附件块存在 @@ -36,7 +36,7 @@ Flags: ### 关键说明 - `--mime-type` 可选,不指定时根据扩展名自动推断;支持常见文件类型(PDF、Office、图片、视频、压缩包等)。 -- 与 [`./doc-file-ops.md`](./doc-file-ops.md) `doc upload` 的区别:`upload` 将文件上传到文档空间/知识库作为**独立文件**;`media insert` 将文件作为**附件块插入到文档正文中**。 +- 与 `dws drive upload` 的区别:`drive upload` 将文件上传到文档空间/知识库作为**独立文件**;`media insert` 将文件作为**附件块插入到文档正文中**。 --- @@ -94,5 +94,5 @@ dws doc media download --node --resource-id - [`../doc.md` §意图判断](../doc.md#意图判断)(如何路由到本命令族) - [`./doc-block.md`](./doc-block.md)(block list 取 attachment 的 resourceId) -- [`./doc-file-ops.md`](./doc-file-ops.md)(独立文件上传:`doc upload`) +- [`../drive.md`](../drive.md)(独立文件上传:`dws drive upload`) - [`./style/doc-style-guideline.md` §4.9 附件与图片](./style/doc-style-guideline.md)(图示与附件使用规范) diff --git a/skills/multi/dingtalk-doc/references/doc/doc-update.md b/skills/multi/dingtalk-doc/references/doc/doc-update.md index 720b7c83..34d1097b 100644 --- a/skills/multi/dingtalk-doc/references/doc/doc-update.md +++ b/skills/multi/dingtalk-doc/references/doc/doc-update.md @@ -27,8 +27,7 @@ Flags: --mode string 更新模式: overwrite=覆盖, append=追加 (必填) --content-format string 内容格式: 默认为 markdown,可选 jsonml --revision int 文档版本号(仅 --content-format jsonml 时生效,可选);传入后服务端做并发检查,版本不一致时返回 VersionConflict。不传则直接覆盖,不做并发检查 - --fix-jsonml 启用全部 JSONML 修复(含 JSON 语法修复 + 结构修复),推荐 agent 调用时使用 - --no-fix-jsonml 关闭全部 JSONML 修复(跳过 JSON 语法修复和结构修复),用于排查原始错误 + --fix-jsonml 启用 JSON 语法修复(括号/逗号补全),推荐 agent 调用时使用 --index int 插入位置(从 0 开始),仅在 mode=append 时生效。指定将内容插入到文档第几个 block 之前。不传时追加到末尾。block 的 index 可通过 doc block list 获取。插入成功后,该位置及之后所有 block 的 index 会依次 +1 ``` @@ -52,7 +51,7 @@ Flags: ]} ``` -> 默认模式下,CLI 会自动给缺 uuid 的 block 注入 uuid、把 `["p", {}, "hello"]` 之类的裸字符串自动包裹成上述 span/leaf 形式(每条修复都会以 `[FIX]` 行输出)。如需严格按原样发送,加 `--no-fix-jsonml`,结构错误会被 validator 直接拦下。如果输入来自 LLM 生成且可能有 JSON 语法错误(缺括号/逗号),加 `--fix-jsonml` 启用全部修复。 +> CLI 不做结构修复——裸字符串、缺 uuid 等错误会被 validator 直接抦下。body 必须以 `["root", ...]` 为根节点,缺少会报错。如果输入来自 LLM 生成且可能有 JSON 语法错误(缺括号/逗号),加 `--fix-jsonml` 启用 JSON 语法修复。 **典型流程**(无损读改写): @@ -171,7 +170,7 @@ dws doc update --node --content-file --mode overwrite --content-f dws doc update --node --content-file --mode append --content-format markdown ``` -> **注意**:分块 append 存在静默失败风险(部分片段返回 success 但实际未写入),执行前**必须**向用户发出截断风险提示并等待确认。完整规范见 [`04-document.md` «分块 append 截断风险提示»](../04-document.md)。 +> **注意**:分块 append 存在静默失败风险(部分片段返回 success 但实际未写入),执行前**必须**向用户发出截断风险提示并等待确认。完整规范见 [`../../best_practices/04-document.md` «分块 append 截断风险提示»](../../best_practices/04-document.md)。 ### stdin 变体 diff --git a/skills/multi/dingtalk-doc/references/doc/format/doc-jsonml-cookbook.md b/skills/multi/dingtalk-doc/references/doc/format/doc-jsonml-cookbook.md index e0883c9f..b0847ca3 100644 --- a/skills/multi/dingtalk-doc/references/doc/format/doc-jsonml-cookbook.md +++ b/skills/multi/dingtalk-doc/references/doc/format/doc-jsonml-cookbook.md @@ -52,7 +52,7 @@ - 表格用 `table → tr → tc`(无 th/td),表头底色用 `tc` 的 `"fill"` 属性 - 关键数据着色用 leaf 的 `"color"`(绿=好 / 红=风险) - 状态标记用 leaf 的 `"highlight"`(黄=待确认、绿=完成、红=阻塞) -- uuid 可全部省略——CLI normalize 自动补充 +- uuid 必须显式提供——CLI 不再自动补充 ## ⚠️ JSONML 结构严格约束(生成时必须遵守) @@ -96,16 +96,16 @@ ## 核心规则 1. **每个 block 节点应有 uuid**:`["tag", {"uuid": "唯一ID"}, ...children]` - - insert 时可以自行生成任意唯一字符串,后端会自动分配正式 uuid + - insert 时必须提供 uuid(可自行生成任意唯一字符串,后端会自动分配正式 uuid) - update 时 uuid **必须**与 `--block-id` 一致 - - normalize **仅在 attrs 槽完全缺失**(如 `["p", "text"]`)时才补 uuid;如果你写了 `["p", {}, ...]`,normalize 会尊重这个空 attrs 不再补 uuid(这一行为保证 `doc read → doc update` 不污染原文档) + - uuid 必须显式提供,不再自动补充 2. **文本必须用 span + leaf 三层结构**,不要直接写裸字符串 - ✅ `["p", {"uuid": "x"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "hello"]]]` - - ❌ `["p", {"uuid": "x"}, "hello"]` — validator 会报错;默认模式下 CLI 会自动包成 ✅ 的形式并打印 `[FIX]` 行 + - ❌ `["p", {"uuid": "x"}, "hello"]` — validator 会报错,请手动包成 ✅ 的形式 - ⚠️ `["p", {"uuid": "x"}, ["text", {}, "hello"]]` — `text` 是历史 inline tag,validator 不会报错,但建议改写为 ✅ 形式以与 `dws doc read --content-format jsonml` 的输出保持一致 3. **attrs 对象必须存在**(即使为空):`["p", {}, ...]` 不能省略 `{}` -> **自动修复 vs 严格模式**:CLI 默认 normalize 会把 ❌ 的裸字符串自动包成 ✅;如要 1:1 透传原始 JSONML(例如复现服务端报错),用 `--no-fix-jsonml`,此时 validator 会以 `JSONPath + Suggestion` 形式逐条报错。如果输入来自 LLM 且可能有 JSON 语法错误(缺括号/逗号),用 `--fix-jsonml` 启用全部修复。 +> **严格模式(缺省)**:CLI 不做结构修复,裸字符串等错误会被 validator 以 `JSONPath + Suggestion` 形式逐条报错。如果输入来自 LLM 且可能有 JSON 语法错误(缺括号/逗号),用 `--fix-jsonml` 启用 JSON 语法修复。 ## 段落 (p) @@ -118,6 +118,11 @@ dws doc block insert --node --content-format jsonml \ dws doc block insert --node --content-format jsonml \ --element '["p", {"uuid": "new2"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf", "bold": true}, "加粗"], ["span", {"data-type": "leaf"}, "普通"], ["span", {"data-type": "leaf", "italic": true}, "斜体"]]]' +# 多行文本(每行一个 p,同一 p 内的多个 span 不会换行) +dws doc block insert --node --content-format jsonml \ + --element '["p", {"uuid": "line1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf", "bold": true}, "第一行标题"]]]' \ + --element '["p", {"uuid": "line2"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "第二行正文内容"]]]' + # 带链接(link 是与 text 并列的子节点) dws doc block insert --node --content-format jsonml \ --element '["p", {"uuid": "new3"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "请访问"]], ["a", {"href": "https://example.com"}, "链接文字"]]' @@ -126,11 +131,73 @@ dws doc block insert --node --content-format jsonml \ **leaf 支持的格式属性**: - `bold: true` — 加粗 - `italic: true` — 斜体 -- `underline: true` — 下划线 +- `underline: {"value": "single"}` — 下划线(value: `single`/`dash`/`wave`/`double`/`none`,可选 `color`) - `strike: true` — 删除线 -- `color: "#ff0000"` — 文字颜色 +- `dstrike: true` — 双删除线 +- `color: "#ff0000"` — 文字颜色(`#rrggbb` 格式) - `highlight: "#ffff00"` — 高亮背景色 -- `sz: 14` / `szUnit: "pt"` — 字号 +- `sz: 14` / `szUnit: "pt"` — 字号(szUnit 默认 `"px"`,推荐显式写 `"pt"`) + `fonts: {"ascii": "Arial", "eastAsia": "SimHei"}` — 字体(四分区:ascii/hAnsi/cs/eastAsia,值必须使用 font-family 名称,见下方字体表) +- `vertAlign: "superscript"` — 上标(`"subscript"` 下标,`"baseline"` 基线) +- `spacing: 2` — 字间距(单位 pt) + +**字体名称映射**(`fonts` 字段必须使用 font-family 值,不能写中文名): + +| 用户说法 | font-family 值 | 用户说法 | font-family 值 | +|---------|---------------|---------|---------------| +| 宋体 | `SimSun` | 黑体 | `SimHei` | +| 微软雅黑 | `Microsoft YaHei` | 微软雅黑UI | `Microsoft YaHei UI` | +| 仿宋 | `FangSong` | 仿宋_GB2312 | `FangSong_GB2312` | +| 楷体 | `KaiTi` | 楷体_GB2312 | `KaiTi_GB2312` | +| 等线 | `DengXian` | 新宋体 | `NSimSun` | +| 宋体-简 | `SimSun SC` | 宋体-繁 | `SimSun TC` | +| 黑体-简 | `Heiti SC` | 黑体-繁 | `Heiti TC` | +| 华文宋体 | `STSong` | 华文黑体 | `STHeiti` | +| 华文楷体 | `STKaiti` | 华文仿宋 | `STFangsong` | +| 华文中宋 | `STZhongsong` | 华文行楷 | `STXingkai` | +| 华文隶书 | `STLiti` | 华文新魏 | `STXinwei` | +| 华文细黑 | `STXihei` | 华文琥珀 | `STHupo` | +| 苹方-简 | `PingFang SC` | 苹方-繁 | `PingFang TC` | +| 苹方-港 | `PingFang HK` | 冬青黑-简 | `Hiragino Sans GB` | +| 兰亭黑-简 | `Lantinghei SC` | 兰亭黑-繁 | `Lantinghei TC` | +| 凌慧体-简 | `LingWai SC` | 幼圆 | `YouYuan` | +| 思源黑体 | `Source Han Sans CN` | 思源宋体 | `Source Han Serif CN` | +| 思源等宽 | `Source Han Mono SC` | 思源黑体Regular | `Source Han Sans CN Regular` | +| 阿里普惠体2.0 | `"Alibaba PuHuiTi 2.0"` | 阿里普惠体3.0 | `"Alibaba PuHuiTi 3.0"` | +| 钉钉进步体 | `DingTalk JinBuTi` | Adobe仿宋 | `Adobe 仿宋 Std` | +| 方正小标宋_GBK | `FZXiaoBiaoSong-B05` | 方正小标宋简体 | `FZXiaoBiaoSong-B05S` | +| 方正黑体 | `FZHei-B01S` | 方正楷体 | `FZKai-Z03S` | +| 方正仿宋 | `FZFangSong-Z02S` | 方正仿宋_GBK | `FZFangSong-Z02` | +| PMingLiU | `PMingLiU` | — | — | + +**英文字体**(font-family 值即为字体名): +`Arial` ・ `Calibri` ・ `Cambria` ・ `Centaur` ・ `Comfortaa` ・ `Comic Sans MS` ・ `Courier New` ・ `Franklin Gothic` ・ `Garamond` ・ `Georgia` ・ `Helvetica` ・ `Impact` ・ `Lora` ・ `Lucida Sans` ・ `Merriweather` ・ `Montserrat` ・ `Nunito` ・ `Oswald` ・ `Playfair Display` ・ `Roboto` ・ `Spectral` ・ `Times New Roman` ・ `Trebuchet MS` ・ `Verdana` + +> **规则**:优先从上表匹配;用户指定的字体不在列表时,使用该字体在操作系统中的真实 font-family 名称(如"更纱黑体" → `Sarasa Gothic SC`)。 + +**leaf 组合示例**: + +```json +["span", {"data-type": "leaf", "bold": true, "color": "#C62828", "sz": 16, "szUnit": "pt"}, "红色加粗大字"] +["span", {"data-type": "leaf", "strike": true, "color": "#9E9E9E"}, "已废弃内容"] +["span", {"data-type": "leaf", "vertAlign": "superscript"}, "[1]"] +["span", {"data-type": "leaf", "fonts": {"ascii": "Courier New", "eastAsia": "DengXian"}}, "等宽字体"] +``` + +**段落级排版属性**(写在 p/h1-h6 的 attrs 上): +- `jc: "center"` — 对齐(`left`/`center`/`right`/`both`/`justify`) +- `spacing: {"line": 1.5, "lineRule": "auto"}` — 行距(lineRule=auto 时 line 为倍数:1=单倍、1.5=1.5倍、2=双倍) +- `spacing: {"before": 12, "after": 8}` — 段前/段后间距(单位 pt) +- `ind: {"firstLine": 32}` — 首行缩进(≈ 2 中文字符) +- `ind: {"left": 96}` — 左缩进 + +**段落排版示例**: + +```json +["p", {"uuid": "p1", "jc": "center"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "居中段落"]]] +["p", {"uuid": "p2", "spacing": {"line": 1.5, "lineRule": "auto"}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "1.5倍行距"]]] +["p", {"uuid": "p3", "spacing": {"line": 2, "lineRule": "auto", "before": 12, "after": 8}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "双倍行距+段前后间距"]]] +``` ## 标题 (h1-h6) @@ -155,9 +222,11 @@ dws doc block update --node --block-id --content-format json dws doc block insert --node --content-format jsonml \ --element '["p", {"uuid": "li1", "list": {"listId": "mylist1", "level": 0, "isOrdered": false}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "无序列表第一项"]]]' -# 有序列表项 +# 有序列表项(仅第一项设 start,后续项不设,系统自动递增) dws doc block insert --node --content-format jsonml \ --element '["p", {"uuid": "li2", "list": {"listId": "mylist2", "level": 0, "isOrdered": true, "start": 1}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "有序列表第一项"]]]' +dws doc block insert --node --content-format jsonml \ + --element '["p", {"uuid": "li2b", "list": {"listId": "mylist2", "level": 0, "isOrdered": true}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "有序列表第二项"]]]' # 缩进子项(level: 1) dws doc block insert --node --content-format jsonml \ @@ -220,8 +289,10 @@ dws doc block insert --node --content-format jsonml \ ## 表格 (table) +> colsWidth 单位为 **pt**(页宽约 650pt)。如配合 `tblW: {"type": "pct"}` 则为百分比权重。 + ```bash -# 2行2列表格 +# 2行2列表格(各列 200pt) dws doc block insert --node --content-format jsonml \ --element '["table", {"uuid": "tb1", "colsWidth": [200, 200]}, ["tr", {"uuid": "tr1"}, ["tc", {"uuid": "tc1", "colSpan": 1, "rowSpan": 1}, ["p", {"uuid": "tcp1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "标题A"]]]], ["tc", {"uuid": "tc2", "colSpan": 1, "rowSpan": 1}, ["p", {"uuid": "tcp2"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "标题B"]]]]], ["tr", {"uuid": "tr2"}, ["tc", {"uuid": "tc3", "colSpan": 1, "rowSpan": 1}, ["p", {"uuid": "tcp3"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "数据1"]]]], ["tc", {"uuid": "tc4", "colSpan": 1, "rowSpan": 1}, ["p", {"uuid": "tcp4"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "数据2"]]]]]]]' ``` @@ -239,14 +310,19 @@ dws doc block insert --node --content-format jsonml \ ## 分栏布局 (columns) -分栏复用 table tag,通过 `sr: true` 区分。 +分栏复用 table tag,通过 `sr: true` 区分。分栏的 `tc` 可设置 `fill`(背景色)和 `border`(边框)属性提升视觉效果。 ```bash -# 两栏布局 +# 两栏布局(带背景色) dws doc block insert --node --content-format jsonml \ - --element '["table", {"uuid": "col1", "sr": true, "colsWidth": [300, 300]}, ["tr", {"uuid": "coltr"}, ["tc", {"uuid": "coltc1"}, ["p", {"uuid": "colp1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "左栏内容"]]]], ["tc", {"uuid": "coltc2"}, ["p", {"uuid": "colp2"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "右栏内容"]]]]]]' + --element '["table", {"uuid": "col1", "sr": true, "colsWidth": [300, 300]}, ["tr", {"uuid": "coltr"}, ["tc", {"uuid": "coltc1", "fill": "#EEF6FF", "vAlign": "top"}, ["p", {"uuid": "colp1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "左栏内容"]]]], ["tc", {"uuid": "coltc2", "fill": "#FFF3E0", "vAlign": "top"}, ["p", {"uuid": "colp2"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "右栏内容"]]]]]]' ``` +**分栏视觉属性**: +- `fill` — 单元格背景色(推荐淡色,如 `#EEF6FF` / `#FFF8E1` / `#F3E5F5`) +- `border` — 边框配置(可选) +- 分栏建议始终设置 `fill` 背景色,纯白底分栏视觉上与普通段落无异,读者无法感知分栏结构 + ## 嵌入块 (embed) 通用文件/iframe 嵌入。`embed` 是 void 块,仅含 attrs,无子节点。 @@ -374,7 +450,7 @@ dws doc block update --node --block-id --content-format json | 错误写法 | 问题 | 正确写法 | |---------|------|---------| -| `["p", {}, "文字"]` | 裸字符串。validator 会报 `段落子节点不能是裸字符串`;默认 normalize 会自动包成右侧形式(打印 `[FIX]` 行) | `["p", {}, ["span", {"data-type":"text"}, ["span", {"data-type":"leaf"}, "文字"]]]` | +| `["p", {}, "文字"]` | 裸字符串。validator 会报 `段落子节点不能是裸字符串`,请手动包成右侧形式 | `["p", {}, ["span", {"data-type":"text"}, ["span", {"data-type":"leaf"}, "文字"]]]` | | `["p", {}, ["text", {}, "文字"]]` | `text` 是历史 inline tag,validator 不报错但服务端实际渲染的 canonical 形式是 span/leaf;为与 `doc read` 输出一致,建议改写 | 同上,用 span + data-type | | `["callout", {}, ...]` | 不存在 callout tag | `["container", {"subType": "colorBlocks", ...}, ...]` | | `["list", {}, ...]` | 不存在 list tag | `["p", {"list": {...}}, ...]` | diff --git a/skills/multi/dingtalk-doc/references/doc/format/doc-jsonml-schema.md b/skills/multi/dingtalk-doc/references/doc/format/doc-jsonml-schema.md index ae162665..69375599 100644 --- a/skills/multi/dingtalk-doc/references/doc/format/doc-jsonml-schema.md +++ b/skills/multi/dingtalk-doc/references/doc/format/doc-jsonml-schema.md @@ -30,28 +30,23 @@ JSONML 是文档内容树的序列化格式: - 第二个元素为文档级属性对象(如 `sectPr` 页面设置),可选 - 后续元素为块级节点(每个 block 节点应带 `uuid`) -全量覆写(overwrite)时,CLI 接受三种 body 形态: +全量覆写(overwrite)时,CLI 要求 body 必须以 `["root", ...]` 为根节点: 1. `["root", {sectPr}, ...blocks]` — 服务端 canonical 形式,`doc read` 输出 -2. `[blocks, ...]` — 纯块数组(缺少 root,validator 会 warn 但不阻断) -3. 单个 `[tag, ...]` — 当作单 block,validator warn +2. `["root", {}, ...blocks]` — 无页面设置时用空 attrs -## CLI 行为概览(validator + normalize) +## CLI 行为概览(validator) -写入端(`doc create/update`、`block insert/update`)默认会走 **normalize → validate** 两步: +写入端(`doc create/update`、`block insert/update`)走 **validate** 一步,不做结构修复: -| 行为 | 缺省 | `--fix-jsonml` | `--no-fix-jsonml` | -|------|------|----------------|-------------------| -| JSON 语法修复(括号/逗号补全) | ✗ | ✓(打印 `[FIX]`) | ✗ | -| 解包单 block 为 body | ✓ | ✓ | ✗ | -| **attrs 槽完全缺失**时补 `attrs` + `uuid` | ✓ | ✓ | ✗ | -| 裸字符串 → `span/text + span/leaf` 自动包裹 | ✓(打印 `[FIX]`) | ✓(打印 `[FIX]`) | ✗ | -| validator 阻断(HasErrors → 拒绝发送) | ✓ | ✓ | ✓ | -| validator 警告(warnings → 仅 stderr) | ✓ | ✓ | ✓ | +| 行为 | 缺省 | `--fix-jsonml` | +|------|------|----------------| +| JSON 语法修复(括号/逗号补全) | ✗ | ✓(打印 `[FIX]`) | +| validator 阻断(HasErrors → 拒绝发送) | ✓ | ✓ | +| validator 警告(warnings → 仅 stderr) | ✓ | ✓ | +| root 校验(仅 doc create/update) | ✓ | ✓ | -> **uuid 注入的边界**:仅当 attrs 槽**完全缺失**(如 `["p", "text"]`)才补 `attrs` + `uuid`。当生产者已显式给出 `attrs`(哪怕是 `{}` 或不含 uuid),normalize 一律不再补 uuid —— 视为生产者明确意图。这避免 `doc read → doc update` 回灌时污染原文档中未修改的节点(真实文档中常见 `["h1", {}, ...]` 形态)。 - -三态设计:缺省 = 结构修复 ON + JSON 语法修复 OFF;`--fix-jsonml` 全开(含 JSON repair,推荐 agent 调用);`--no-fix-jsonml` 全关(用于排查原始错误)。校验始终执行,不可跳过。 +> `doc create/update` 要求 body 必须以 `["root", {attrs?}, ...blocks]` 为根节点。缺少 root 会报错而非自动包装。`doc block insert/update` 不要求 root。 报错格式(面向 agent): @@ -89,12 +84,12 @@ Suggestion: ["span",{"data-type":"text"},["span",{"data-type":"leaf"}," Marks 表的属性集对 canonical 的 leaf 和 legacy 的 text 都适用;差别仅在承载位置(leaf 的 attrs vs text 的 attrs)。 @@ -133,9 +139,9 @@ Suggestion: ["span",{"data-type":"text"},["span",{"data-type":"leaf"},"= 2KB 时必须写入 UTF-8 临时 `.md` 文件 | | 格式 | 按 §JSONML 起稿判定 决定起稿路径:命中 JSONML 起稿条件时**直接用 JSONML 构造**(跳过 markdown);未命中时用 Markdown 起稿,创建后按 [doc-update-workflow.md](./doc-update-workflow.md) 精修 | @@ -75,13 +75,171 @@ --- +## 设计规划(JSONML 起稿前必做) + +判定走 JSONML 路径后,**禁止立即动手写 JSONML**。先完成以下两阶段规划,各自产出一个持久文件作为后续阶段的锚点。 + +### 设计原则(全程生效) + +1. **结构即信息** — 标题层级、表格 vs 分栏 vs 列表、callout 位置都应编码内容逻辑。问自己:“去掉这个结构元素,读者会丢失信息或体验变差吗?”— 丢失信息则必保留;不丢失信息但能提升可读性或美观度(如分割线分隔章节、分栏对比排版)也应保留;既不携带信息也不提升体验的装饰元素才删除。 +2. **视觉层级引导阅读** — 每一屏必须让读者瞥一眼就能回答:“这块最重要的是什么?”字号/粗体/颜色形成明确梯度:标题 > 重点数据 > 正文 > 辅助信息。 +3. **克制产生质感** — 遵循 60-30-10 配色比例:60% 中性底色(白/浅灰)、30% 辅助色、10% 强调色。多色系共存时需保持**同等饱和度**并各有语义角色(如淡蓝=信息、淡黄=提示、淡红=风险),同一色系内深浅变化自由。callout 不超过 2 个。 +4. **设计先于执行** — 从规划阶段起每个结构块的样式就已确定,执行时(无论直接 JSONML 还是脚手架精修)只是落地已有设计,不是边写边想。 +5. **同类同色、一色多阶** — 同类信息必须使用相同色系;单一色系按元素角色展开为深/中/浅/极浅四级(标题文字用深色、强调用中色、高亮/表头用浅色、背景用极浅色),不要全篇只用一个 hex 值。 + +### Phase 1:RFC — 需求理解与设计方向 + +**目标**:明确“做什么”和“为什么这样做”,形成方向性锚点。 + +#### 1.1 需求提取(全量列出用户显式要求) + +通读用户 prompt,抽取两类要求并编为清单: + +**内容要求**: +- 标题、字数、章节划分 +- 数据来源、受众 +- 语气/风格(如“大气”“专业”“轻松”) + +**样式要求**(每一条都必须在最终输出中体现,不得遗漏): +- 字体:映射为 font-family 名称(参照 cookbook 字体映射表),区分“全文字体”和“特定元素字体” +- 字号、行距、对齐、颜色 +- 强调手段(加粗、高亮、配色…) +- 约束(如“每部分不省略”) + +> 用户没有明确指定的维度(如未指定行距、未指定表格样式)由 Phase 2 补充设计决策。 + +#### 1.2 内容-表现适配(每个章节的内容适合用什么元素) + +对每个章节回答:“这个内容的核心是什么类型的信息?”→ 选择最佳元素: + +**块级结构元素**: + +| 信息类型 | 首选元素 | 不适合 | +|----------|---------|--------| +| 多个同类实体对比(≥ 4 项或 ≥ 3 维度) | 彩色表头表格 | 纯文本段落 | +| 少量实体对比(2-3 项× 少量维度) | 分栏(每栏一个实体,可设边框/背景色) | 大宽表格 | +| 时间序列/流程(行程/步骤) | 有序列表 + 粗体时间标签 | 无序列表 | +| 单个结论/推荐/重要提示 | callout(“花大胆”的地方) | 普通段落 | +| 描述性文字(背景/说明) | 正文段落 + 关键词粗体 | 表格 | +| 分类列举(特色/亮点) | 无序列表 | 表格(数据不够多列时) | +| 数值强调(评分/价格/统计) | 加粗 + 着色 | 跳过不强调 | +| 引用原文(用户评价/网友点评/官方说明) | 引用块 | 普通段落 | +| 任务/待办清单 | checklist(`- [ ]`) | 普通列表 | +| 章节分隔/主题转换 | 分割线(`hr`) | 空行 | +| 板块内子区域分隔(同一单元格/容器内多个逻辑段) | hr 内部分隔(在 tc 或 container 内部使用) | 空行或留白 | +| 结构化元信息(人/时间/地点/属性清单) | 键值对表格(窄标签列 ~15-20% + 宽内容列) | 多行段落 | +| 分类标签/状态标记 | 标签元素(tag) | 纯文本标记 | + +**行内强调元素**: +- **emoji** — 用于 callout 前缀、状态标记、H2/H3 标题前;不在普通段落和列表项中滥用 +- **加粗/高亮/着色** — 强调关键数据和结论 +- **highlight 色带** — 在标题 span 上设 `"highlight": "#浅色"` 形成轻量色条标记,比 callout 更轻,适合区分多个并列板块的主题色 +- **灰色辅助文字** — 用浅灰色(如 `#979A9B`)标记示例/说明/占位文字,与正文形成明确的主次层级 +- **图片** — 实景照片、截图、示意图能显著提升理解时使用 + +**分栏选型补充**:分栏栏数无上限,但推荐 2-4 栏(≥5 栏会比较拥挤),可设置边框和背景色。**建议设置 `fill` 背景色**(纯白底分栏视觉上与普通段落无异,读者不易感知分栏结构)。适合场景: +- 2-3 个同类实体并排展示(每栏一个实体,含标题 + 描述 + 关键数据) +- 轻量对比:“优点 vs 缺点”、“方案 A vs B”、“Day 1 vs Day 2” +- 网格布局:多次插入同栏数分栏可形成卡片网格(如 2×3、3×2),适合 4-9 个结构相同、内容等长的卡片式实体。**约束**:每个格子内容必须结构一致且长度相近,否则高低不齐会很丑;实体数不能整除栏数时不使用 + +例如:“5 个实体多维度对比” → **彩色表头表格**;“两组信息并排” → **分栏**;“6 个结构相同的卡片” → **2×3 分栏网格**。 + +#### 1.3 起稿策略选择 + +根据文档复杂度和上方适配结果,确定起稿路径:直接 JSONML / Markdown 脚手架 + JSONML 精修 / 直接 JSONML 分段构造。具体条件和流程见下方「起稿」节的策略表。 + +#### 落盘 + +将以上内容写入 `-rfc.md`,包括: +- 需求摘要(内容要求 + 样式要求) +- 每章展现策略 + 选择理由 +- 起稿策略 + +--- + +### Phase 2:Spec — 精确设计参数 + +**目标**:将 RFC 的方向决策转化为可直接执行的参数。 + +#### 2.1 视觉体系设计(确定全局设计变量) + +在以下四个维度做出明确选择,每个选择都必须能解释为什么适合这篇文档: + +**色彩**(选定主色后展开为色阶): + +用户指定颜色时(如"蓝色""绿色"),不要全篇只用一个 hex 值。将其展开为 4 级色阶,按元素角色分配: + +| 角色 | 用途 | 色阶要求 | +|------|------|----------| +| 深色 | 标题文字、重点数据 `color` | 白底上高对比可读 | +| 中色 | 正文强调、链接 `color` | 辨识度高但不抢标题 | +| 浅色 | highlight 色带、表头 fill | 底色柔和,上方深色文字可读 | +| 极浅 | container bgcolor、大面积背景 | 接近白色,仅提供区域感 | + +**规则**: +- 深→浅的层级关系不可颠倒(不能用极浅色做标题文字、不能用深色做背景) +- 同类板块/同类标题必须使用完全相同的色阶组合,通过色彩的重复形成视觉韵律 +- 不同类别可用不同色系区分(如:任务=蓝系、风险=红系、成果=绿系) +- 遵循 60-30-10 配色比例:60% 中性底色、30% 辅助色、10% 强调色;用户指定的颜色值优先 + +**字体梯度**(形成明确层级): +- 主标题(h2):字体 / 字号 / 粗体 / 颜色 +- 正文:字体 / 字号 / 行距 +- 强调文字:粗体 + 颜色或高亮 +- 辅助信息(注释/来源):字号偏小 / 灰色 + +**表格风格**(有表格时): +- 表头:底色 + 文字色 + 是否粗体 +- 单元格:默认对齐 / 字号 + +**视觉重心**(克制原则落地): +全篇选 1-2 处给予最强视觉处理(配色 callout / 彩色表头 / 分栏对比),其余元素保持朴素。不要处处强调 — 处处强调等于没有强调。 + +callout 可通过 `"showstk": true, "sticker": "图标名"` 配置顶部贴纸图标(如“灯泡”“火”“钉子”),增强语义标识。设置了 sticker 后,高亮块内首个段落不要再以 emoji 开头,避免紧邻的位置出现两个图标。 + +#### 落盘与回验 + +1. 将以上内容写入 `-design.md`,包括: + - 逐章元素映射(用什么标签、什么属性) + - 色阶具体 hex 值 + - 字体梯度参数 + - 表格风格细节 + +2. **回验**:Read `-rfc.md`,逐条确认: + - [ ] RFC 中每条需求在 Spec 中有对应实现 + - [ ] 展现策略在 Spec 中有具体参数支撑 + - [ ] 配色遵循 60-30-10 比例、callout ≤ 2 个、多色系饱和度一致且有语义角色 + +回验通过后进入下一节开始构造 JSONML。 + +--- + ## JSONML 起稿(命中判定时使用) -当判定为 JSONML 起稿时,在本地临时 JSON 文件中直接构造完整的 JSONML 文档树。路径:`/tmp/.json`。 +根据 RFC 中确定的起稿策略执行: + +| 策略 | 条件 | 执行流程 | +|------|------|----------| +| **直接 JSONML** | 短文档(≤ 15 块级节点)且结构简单 | 在 `/tmp/.json` 手写完整 JSONML 树 → `doc create --content-format jsonml` | +| **Markdown 脚手架 + JSONML 精修** | 长文档且结构较线性 | ① Markdown 建立内容骨架 → `doc create --content-format markdown` ② `doc read --node --content-format jsonml --output /tmp/.json` 拉回 JSONML(已含 uuid) ③ **Read `-rfc.md` + `-design.md`** 回顾规划 ④ 逐章对照执行结构变换 + 叠加样式 | +| **直接 JSONML 分段构造** | 长文档且含大量富结构 | 按章节分段构造 JSONML,每段写完校验通过后再写下一段,最后拼接 | + +> **脚手架策略警示**:Markdown 无法表达分栏/callout/色彩表头,拉回的 JSONML 只有纯文本骨架。精修阶段不是“在现有结构上加色”,而是“参照 RFC/Spec 重组结构”。 > **MUST READ**:动手写 JSONML 前,必须先用 Read 工具读取 [doc-jsonml-cookbook.md](../format/doc-jsonml-cookbook.md) — 其中 §决策型文档骨架范例 有可直接复制修改的完整模板。 > 节点类型和属性的权威定义见 [doc-jsonml-schema.md](../format/doc-jsonml-schema.md)。 +### ⚠️ JSONML 降级约束 + +**禁止因一次校验失败就放弃 JSONML 降级为 Markdown。** 当用户需求已触发 JSONML 起稿判定时,JSONML 是实现其样式要求的首选路径。失败时的处理策略: + +1. **校验报错** → 读取错误信息,定位具体节点,修复后重试 +2. **JSON 语法错误** → 检查括号匹配、逗号、引号,修复后重试 +3. **反复失败(≥3 次)** → 尝试简化结构(减少嵌套、拆分复杂节点)再试 +4. **仍然失败** → 退化为「Markdown 脚手架 + JSONML 精修」路径(流程同上方策略表),并告知用户当前状况 + +> “由于 JSONML 结构复杂且容易出错,改用 Markdown” — 这不是合法降级理由。必须先充分重试,且降级后仍需通过精修补回样式。 + ### ⚠️ JSONML 结构严格约束(生成时必须遵守) 每个节点是一个 JSON 数组:`[tagName, attributes?, ...children]` @@ -99,6 +257,10 @@ | 多余逗号 | `["p", {},]` | JSON 解析失败 | | 缺少逗号 | `["p", {} ["span"]]` | JSON 解析失败 | | 引号不匹配 | `["p", {"uuid": "abc}]` | JSON 解析失败 | +| 有序列表每项都设 `start:1` | `{"start":1}` 在每项重复 | 所有项编号重置为 1(显示为 a/a/a) | +| 列表 `level` 从 1 开始 | `"level": 1` 作为顶级 | 顶级列表项多一层缩进;建议:顶级 `"level": 0`,子级 `"level": 1` | +| 用 `fontFamily` 设字体 | `"fontFamily": "Arial"` | 校验报错;正确写法:`"fonts": {"ascii": "Arial", "eastAsia": "..."}` | +| 用多个 span “换行” | 同一 `p` 内放两个 `span` | 不会产生换行;每个换行必须是独立的 `p` 节点 | ### 基础结构 @@ -114,14 +276,20 @@ - 根节点固定 `"root"`(不是 `"body"`) - `--name` 已是 H1,JSONML 从 `h2` 开始 - 表格结构是 `table → tr → tc`(无 `th`/`td`) -- uuid 可省略(CLI normalize 自动补) +- 分栏是 `table` + `"sr": true`,`tc` 建议设 `fill` 背景色 +- 有序列表:仅第一项设 `"start": 1`,后续项不设 `start`(系统自动递增) +- 列表 `level` 建议从 0 开始:顶级项 `"level": 0`,子项 `"level": 1`,以此类推 +- uuid 必须显式提供(CLI 不自动生成) +- **每行内容对应一个 `p` 节点** — 同一 `p` 内的多个 `span` 不会换行,只会横向拼接;需要换行时必须拆分为多个 `p` ### 视觉设计要点 构造时主动使用这些属性实现视觉效果: - **文字着色**:leaf 上 `"color": "#hex"`、`"highlight": "#hex"` +- **highlight 色带**:标题 leaf 上 `"highlight": "#浅色"` 可形成色条效果(比 callout 更轻量的板块标记) - **字号**:leaf 上 `"sz": 14, "szUnit": "pt"` - **callout**:`["container", {"subType": "colorBlocks", "metadata": {"bgcolor": "#E8F5E9", "border": "left"}}, ...blocks]` +- **callout + sticker**:`["container", {"subType": "colorBlocks", "metadata": {"bgcolor": "#FEF3F3", "showstk": true, "sticker": "火"}}, ...blocks]` - **表格单元格底色**:tc 上 `"fill": "#hex"` ### 写入 @@ -147,12 +315,12 @@ dws doc read --node --content-format jsonml --output /tmp/-readba - 只使用用户已提供或对话中已确认的正文素材。 - 如果正文素材不足,先补齐文档目标、受众、章节和缺口;不要在本文中临时扩展跨产品采集流程。 - **先按 [doc-style-guideline.md §2.0 类型判断决策表](./doc-style-guideline.md) 确定文档类型,再用对应类型的骨架样板(§2.1 决策型 / §2.2 执行型 / §2.3 说明型 / §2.4 知识沉淀型)**。不要套通用三段式。 -- **`--name` 已是 H1,正文从 `##` 开始**;正文内不要再写 `#` 一级标题(除非确实需要正文内再造一级 H1 并说明动机)。与 `--name` 相同的首行一级标题会被 CLI 自动移除(stderr 有提示),但不要依赖这个兜底。 +- **`--name` 已是 H1,正文从 `##` 开始**;正文内不要再写 `#` 一级标题(除非确实需要正文内再造一级 H1 并说明动机)。 - 摘要、bullet、引用块、callout 等元素的使用边界以 style-guideline §3-§7 为准。 - 同类信息保持一致:风险、状态、行动项各用一种元素 + 一种视觉语义(style-guideline §1.2 / §5)。 - 临时文件必须保留真实换行,不能把换行写成字面量 `\n`。 - Markdown 草稿阶段**不要**写 callout / 分栏 / 附件——这些留到「创建后的精修」用 `doc block insert` 操作(style-guideline §1.3)。 -- **图片素材闭环(硬规则)**:正文需求含图片/截图/图文并茂时,**禁止**在 Markdown 中写 `![](...)` 图片语法(包括真实存在的 alidocs URL)。正确做法:Markdown 只写文本骨架和图片占位说明(如 `📌 此处插入:xxx 产品截图`),创建文档后逐个执行 `dws doc media insert --node --file <本地图片路径>` 插入,最后用 `dws doc block list --node ` 验证图片块存在。图片来源如果是钉盘文件,必须先 `dws doc download --node <图片nodeId> --output /tmp/xxx.png` 下载到本地再 insert。 +- **图片素材闭环(硬规则)**:正文需求含图片/截图/图文并茂时,**禁止**在 Markdown 中写 `![](...)` 图片语法(包括真实存在的 alidocs URL)。正确做法:Markdown 只写文本骨架和图片占位说明(如 `📌 此处插入:xxx 产品截图`),创建文档后逐个执行 `dws doc media insert --node --file <本地图片路径>` 插入,最后用 `dws doc block list --node ` 验证图片块存在。图片来源如果是钉盘文件,必须先 `dws drive download --node <图片nodeId> --output /tmp/xxx.png` 下载到本地再 insert。 ## 创建写入 @@ -230,7 +398,7 @@ dws doc update --node --content-file /tmp/-resume.md --mode appen 精修常见入口(**按 [doc-update-workflow.md §1.3](./doc-update-workflow.md) 优先级排序:JSONML 首选**): -- 单 block JSONML 精修(首选):`doc block list --node --content-format jsonml --block-id ` 取子树 → `doc block update --node --block-id --content-format jsonml --element '[...]'` 写回(uuid 必须 == --block-id;写入端默认 normalize + validate,详见 [doc-update-workflow.md §4.4](./doc-update-workflow.md)) +- 单 block JSONML 精修(首选):`doc block list --node --content-format jsonml --block-id ` 取子树 → `doc block update --node --block-id --content-format jsonml --element '[...]'` 写回(uuid 必须 == --block-id;写入端默认执行 schema validate,详见 [doc-update-workflow.md §4.4](./doc-update-workflow.md)) - 整篇 JSONML 无损:`doc update --content-format jsonml --mode overwrite`(默认直接覆盖,适合一次改多处或改 root sectPr;担心并发覆盖时加 `--revision ` 触发并发检查) - 插入附件 / 图片:`doc media insert`(无 JSONML 形态,直接走 element) - element JSON 次选:`doc block insert` / `doc block update` 不带 `--content-format jsonml` 时按老接口 JSON 解析;仅在 JSONML 不支持某字段时使用 diff --git a/skills/multi/dingtalk-doc/references/doc/style/doc-style-guideline.md b/skills/multi/dingtalk-doc/references/doc/style/doc-style-guideline.md index 1e25e87a..1e00cad5 100644 --- a/skills/multi/dingtalk-doc/references/doc/style/doc-style-guideline.md +++ b/skills/multi/dingtalk-doc/references/doc/style/doc-style-guideline.md @@ -56,7 +56,7 @@ 1. **读者**:谁打开这篇文档?读完要做什么动作(操作 / 选择 / 查参数 / 看推理)? 2. **唯一记忆点**:读者关掉文档后,最想让他记住的一句话是什么?这句话决定开头摘要 / callout 该写什么。 -3. **形态**:按 §2.0 推荐格式列 + [doc-create-workflow.md §JSONML 起稿判定](./doc-create-workflow.md#jsonml-起稿判定) 决定路径。命中判定条件(决策型 / 含对比的知识沉淀型 / 用户意图关键词)→ **直接 JSONML 起稿**;未命中 → markdown 起稿 + 创建后精修。 +3. **形态**:按 §2.0 推荐格式列 + [doc-create-workflow.md §JSONML 起稿判定](./doc-create-workflow.md#jsonml-起稿判定) 决定路径。命中判定条件(决策型 / 含对比的知识沉淀型 / 用户意图关键词)→ **直接 JSONML 起稿**(但必须先完成 [doc-create-workflow.md §设计规划](./doc-create-workflow.md#设计规划jsonml-起稿前必做) 的 4 步规划);未命中 → markdown 起稿 + 创建后精修。 写完自检时回看这三个答案:开头有没有兑现「记忆点」、形态有没有兑现「推荐格式」。两条任一不兑现,按 §8 自检表对应行动。 diff --git a/skills/multi/dingtalk-doc/references/doc/style/doc-update-workflow.md b/skills/multi/dingtalk-doc/references/doc/style/doc-update-workflow.md index 16299af5..45d1cb88 100644 --- a/skills/multi/dingtalk-doc/references/doc/style/doc-update-workflow.md +++ b/skills/multi/dingtalk-doc/references/doc/style/doc-update-workflow.md @@ -46,7 +46,7 @@ JSONML 模式下这些元素的节点结构见 [doc-jsonml-schema.md](../format/ | 优先级 | 形态 | 适用 | |--------|------|------| -| ① 首选 | `--content-format jsonml` | 保真度最高;callout / 分栏 / 表格 / @人 / 附件 / 颜色 / 嵌套结构都能 1:1 round-trip;写入端有 normalize + validator 兜底(§4.4) | +| ① 首选 | `--content-format jsonml` | 保真度最高;callout / 分栏 / 表格 / @人 / 附件 / 颜色 / 嵌套结构都能 1:1 round-trip;写入端有 validator 兜底(§4.4) | | ② 次选 | `--content-format element`(JSON,老接口) | JSONML 不支持某个块字段时;或快速插入 callout / 分栏不想构造 JSONML 时;不保真改写正文 | | ③ 兜底 | markdown(不带 `--content-format` 即默认)| 纯文本追加、整篇重排骨架;callout / 分栏 / 颜色 / 部分属性会被 markdown 还原过程丢失 | @@ -106,7 +106,7 @@ JSONML 模式下这些元素的节点结构见 [doc-jsonml-schema.md](../format/ ## 四、改写路径详细 -> **首选 JSONML(§4.4)**——保真度最高且 normalize/validator 兜底;本节其余路径(markdown / element)仅在 §1.3 列出的"次选 / 兜底"场景下使用。 +> **首选 JSONML(§4.4)**——保真度最高且 validator 兜底;本节其余路径(markdown / element)仅在 §1.3 列出的"次选 / 兜底"场景下使用。 ### 4.1 段落级 overwrite(markdown 兜底路径) @@ -161,7 +161,7 @@ dws doc block insert --node --ref-block --where after --cont ### 4.4 JSONML 无损改写(**首选路径**) -> 改写已有文档**默认走本节**——保真度最高,callout / 分栏 / 表格 / @人 / 附件 / 颜色 / 嵌套都能 1:1 round-trip;写入端有 normalize + validator 兜底。其他路径(§4.1/4.2/4.3/4.5 markdown)仅在 §1.3 列出的"次选 / 兜底"场景下使用。 +> 改写已有文档**默认走本节**——保真度最高,callout / 分栏 / 表格 / @人 / 附件 / 颜色 / 嵌套都能 1:1 round-trip;写入端有 validator 兜底。其他路径(§4.1/4.2/4.3/4.5 markdown)仅在 §1.3 列出的"次选 / 兜底"场景下使用。 两条子路径: @@ -201,16 +201,15 @@ dws doc update --node --content-file /tmp/doc_modified.json \ > **并发安全模式(担心被并发覆盖时使用)**:如果担心多 agent 同时改这篇文档,可以把第 1 步 read 返回的 `revision` 通过 `--revision ` 透传给第 4 步:服务端会做并发检查,版本不一致返回 `VersionConflict`,此时回到第 1 步重读重写即可。普通单 agent 改写场景默认不传 `--revision`。 -#### JSONML 写入端的 normalize 与 validator +#### JSONML 写入端的 validator -写入命令(`doc create/update` + `doc block insert/update`)默认按 **normalize → validate** 两步处理 JSONML: +写入命令(`doc create/update` + `doc block insert/update`)走 **validate** 一步,不做结构修复: -| 行为 | 缺省 | `--fix-jsonml` | `--no-fix-jsonml` | -|------|------|----------------|-------------------| -| JSON 语法修复(括号/逗号补全) | ✗ | ✓(打印 `[FIX]`) | ✗ | -| 注入缺失的 block `uuid` | ✓ | ✓ | ✗ | -| 裸字符串 → 包成 `["span",{"data-type":"text"},["span",{"data-type":"leaf"},"..."]]` | ✓(stderr 打印 `[FIX]`) | ✓ | ✗ | -| validator 阻断(HasErrors → 拒发) | ✓ | ✓ | ✓ | +| 行为 | 缺省 | `--fix-jsonml` | +|------|------|----------------| +| JSON 语法修复(括号/逗号补全) | ✗ | ✓(打印 `[FIX]`) | +| validator 阻断(HasErrors → 拒发) | ✓ | ✓ | +| root 校验(仅 doc create/update) | ✓ | ✓ | 报错格式(agent 友好): @@ -219,11 +218,11 @@ $[2][2]: paragraph child must be span wrapper, got raw string. Suggestion: ["span",{"data-type":"text"},["span",{"data-type":"leaf"},""]] ``` -三态设计: +设计要点: -- 不传:结构修复 ON + JSON 语法修复 OFF + 校验 ON(推荐人工调用) -- `--fix-jsonml`:全部修复 ON(含 JSON 语法修复,推荐 agent 调用) -- `--no-fix-jsonml`:全部修复 OFF,校验仍 ON;用于排查原始错误 +- 缺省为严格模式:不做结构修复,裸字符串、缺 uuid 等错误会被 validator 抦下。 +- `doc create/update` 要求 body 必须以 `["root", ...]` 为根节点,缺少会报错。`doc block insert/update` 不要求 root。 +- `--fix-jsonml`:启用 JSON 语法修复(修复 LLM 遗漏的括号/逗号),推荐 agent 调用。 **何时不走本节、改用 markdown**:纯文本追加章节(§4.2)、整篇按全新骨架重写(§4.5,且无富结构需要保留时)、只在乎"加一段文字"且确认目标段落无 callout / 分栏 / 颜色 / @人 / 附件。其余场景默认本节。 @@ -259,7 +258,7 @@ dws doc update --node --content-file /tmp/-full.md --mode overwri 当一次性追加内容 **超过 200KB** 时,必须拆分为多片 `--mode append`,并在执行第一片**之前**向用户发出截断风险提示等待确认。 -完整规范(提示话术模板、触发条件、失败处理)见 [04-document.md «分块 append 截断风险提示»](../../04-document.md)。 +完整规范(提示话术模板、触发条件、失败处理)见 [04-document.md «分块 append 截断风险提示»](../../../best_practices/04-document.md)。 update 场景下的额外约束: diff --git a/skills/multi/dingtalk-drive/references/drive.md b/skills/multi/dingtalk-drive/references/drive.md index 30145280..6304f2c1 100644 --- a/skills/multi/dingtalk-drive/references/drive.md +++ b/skills/multi/dingtalk-drive/references/drive.md @@ -10,6 +10,7 @@ dws drive --help # 查看具体命令的完整参数说明 dws drive list --help +dws drive search --help dws drive upload --help dws drive download --help ``` @@ -30,7 +31,7 @@ Example: dws drive list --limit 20 dws drive list --limit 20 --folder --order-by name --order asc Flags: - --limit int 每页返回数量,默认 20,最大 100 (可选) + --limit int 每页返回数量,默认 20,最大 50 (可选) --cursor string 分页游标,首次不传 (可选) --order string 排序方向: asc|desc,默认 desc (可选) --order-by string 排序字段: createTime|modifyTime|name (可选) @@ -65,6 +66,43 @@ spaceType 筛选规则: - `spaceType` — 空间类型(如 `orgSpace`) - `nextToken` — 若不为空,表示还有更多空间可查询(仅企业空间) +### 搜索钉盘文件/文件夹/空间 + +按关键词在钉盘中搜索文件、文件夹或团队空间。不同于 `list`(需要明确的 spaceId/parentId 逐层遍历),`search` 用于不知道具体位置、只记得名称/关键词的场景。 + +``` +Usage: + dws drive search [flags] +Example: + dws drive search --query "季度汇报" + dws drive search --query "合同" --target file --extensions pdf,docx + dws drive search --query "项目" --target space + dws drive search --query "方案" --created-from 1700000000000 --created-to 1710000000000 + dws drive search --query "周报" --creator-uids 012345 + dws drive search --query "报告" --limit 30 --cursor +Flags: + --query string 搜索关键词 (必填) + --target string 搜索目标: all(默认) | file | space (可选) + --file-types strings 按文件内容类型过滤,逗号分隔: alidoc,document,image,video,audio,archive (仅 target=file/all 生效) + --extensions strings 按文件扩展名过滤,不含点号,逗号分隔 (如 pdf,docx,adoc) + --creator-uids strings 按创建者用户 ID 过滤,逗号分隔 + --created-from int 创建时间起始 (毫秒时间戳,含) + --created-to int 创建时间截止 (毫秒时间戳,含) + --modified-from int 修改时间起始 (毫秒时间戳,含) + --modified-to int 修改时间截止 (毫秒时间戳,含) + --limit int 每页返回数量(默认 10,最大 30) + --cursor string 分页游标,从上次返回的 nextCursor 获取 (可选) +``` + +搜索目标 (`--target`) 选择规则: +- `all`(默认):同时搜文件与空间,返回混合结果 — 不确定目标是文件还是空间时使用 +- `file`:只搜文件 / 文件夹,支持 `--file-types` / `--extensions` 过滤 — 明确是找文件时使用 +- `space`:只搜团队空间 — 明确知道空间名、需快速定位空间 spaceId/rootFolderId 时使用 + +返回结果中 `type` 字段区分:`SPACE`(空间)、`FILE`(普通文件)、`FOLDER`(文件夹)、`ALIDOC`(钉钉在线文档)。 + +> **提示**:结果按相关性排序,首页未命中时优先调整关键词 / 补充 `--file-types`/`--extensions` 缩小范围 / 加上时间范围,而非反复翻页。 + ### 获取文件元数据信息 ``` @@ -85,8 +123,8 @@ Flags: | extension | 文件类型 | 操作 | 命令 | |-----------|---------|------|------| | adoc | 在线文档 | 在线获取 Markdown 内容 | `dws doc read --node ` | -| axls | 在线表格 | 在线读取表格数据 | `dws sheet list` → `dws sheet range read` | -| able | 多维表格 | 在线查询记录 | `dws aitable table list` → `dws aitable record query` | +| axls | 在线表格 | 在线读取表格数据 | `dws sheet get-all-sheets` → `dws sheet get-range` | +| able | 多维表格 | 在线查询记录 | `dws aitable get-tables` → `dws aitable query-records` | | 其他(pdf/docx/txt/png 等) | 普通文件 | **不支持在线分析**,需用户主动下载后本地查看 | `dws drive download` | ### 下载文件到本地 @@ -161,18 +199,27 @@ Flags: 用户说"我的文件/钉盘/网盘/云盘" → `list` 用户说"钉盘空间/团队文件/有哪些空间/空间列表/团队文件列表" → `list-spaces` +用户说"搜索钉盘文件/钉盘里找个文件/查找某个钉盘文件/钉盘中搜索" → `search` 用户说"文件详情/文件信息" → `info` 用户说"下载文件" → `download` -用户说"新建文件夹/创建目录" → `mkdir` +用户说"新建文件夹/创建目录" → `mkdir`(钉盘空间)/ `folder create`(文档空间) 用户说"上传文件/传文件到钉盘" → `upload`(必须使用此命令,自动完成三步流程) -用户说"复制文件/移动文件/搬到/移到" → 使用 `dws doc copy`/`dws doc move`(详见下方「复制/移动钉盘文件」工作流) +用户说"复制文件/移动文件/搬到/移到" → `copy` / `move` +用户说"重命名/改名" → `rename` 用户说"删除文件/删除文件夹/移到回收站" → `delete`(危险操作,需确认) +用户说"给文档授权/分享权限" → `permission add` + +关键区分: drive(文件管理) vs doc(文档内容读写) vs wiki(空间管理) + +**drive search vs wiki node search**: 用户提到"钉盘/网盘/我的文件里搜" → `drive search`;提到"知识库/文档空间/workspace 里搜" → `wiki node search`;未明确目标时优先问明。 -关键区分: drive(钉盘文件管理) vs doc(文档内容读写) +**drive upload**: 文件上传统一走 `drive upload`。上传到知识库/文档空间时加 `--workspace` 参数。 -**drive upload vs doc upload**: 用户提到"钉盘/网盘/我的文件"→ `drive upload`;提到"知识库/文档空间/workspace"→ `doc upload`;未明确目标时默认 `drive upload` +**drive permission vs wiki member**: "给某篇文档/文件授权" → `drive permission add`(节点级);"给某个知识库整体加成员" → `wiki member add`(空间级) -**钉盘文件复制/移动**: drive 本身没有 copy/move 命令,需使用 `dws doc copy`/`dws doc move` 实现(详见下方工作流) +**创建在线文档/表格/脑图**: drive 不支持创建文件,需走 `wiki node create --type `(创建空节点)或 `doc create`(创建并写入内容)。 + +**导出文档/导出为Word**: 导出是内容层操作,走 `doc export`,不属于 drive。 ## 核心工作流 @@ -201,57 +248,85 @@ dws drive upload --file ./报告.pdf --folder --format json dws drive delete --node --yes --format json ``` -## 复制/移动钉盘文件 +## 文档空间管理命令 + +> 以下命令操作的是**文档空间**(知识库 / 我的文档),底层路由到 doc MCP server。 +> 与钉盘命令(list / mkdir / upload 等)的区别:钉盘命令操作钉盘空间(spaceId 纯数字),文档空间命令操作知识库/我的文档(workspaceId 加密 string)。 + +### 复制/移动/重命名文件 + +``` +Usage: + dws drive copy --node [--folder ] [--workspace ] + dws drive move --node [--folder ] [--workspace ] + dws drive rename --node --name "新名称" +Flags: + --node string 文档/文件 ID 或 URL (必填) + --folder string 目标文件夹 nodeId + --workspace string 目标知识库 ID + --name string 新名称 (仅 rename 必填) +``` + +> **字段选择**:`drive list` 返回中有 `dentryId`(数字格式)和 `fileId`(UUID 格式),**必须使用 `fileId`(UUID 格式)**作为 `--node` 和 `--folder` 参数值。 -钉盘本身没有 copy/move 命令,需使用 `dws doc copy`/`dws doc move` 实现。 +### 创建文件夹(文档空间) -> **注意:字段选择**:`drive list` 返回中有 `dentryId`(数字格式)和 `fileId`(UUID 格式)两个字段,**必须使用 `fileId`(UUID 格式,如 `ZgpG2NdyVXYOR2D5UGDok65MJMwvDqPk`)**作为 `--node` 和 `--folder` 的参数值。**禁止使用 `dentryId`(数字格式,如 `220335325118`),传入数字格式会导致命令失败。** +``` +Usage: + dws drive folder create --name "文件夹名" +Flags: + --name string 名称 (必填) + --folder string 父文件夹 nodeId + --workspace string 目标知识库 ID +``` -> **注意**:钉盘场域下,仅支持将文件复制/移动到文件夹下,不支持文档下嵌套文档。 +### 权限管理(文档节点级) + +> 仅适用于文档空间节点,不适用于钉盘文件。 + +``` +Usage: + dws drive permission add --node --users uid1,uid2 --role READER + dws drive permission update --node --users uid1 --role EDITOR + dws drive permission list --node + dws drive permission remove --node --users uid1 +Flags: + --node string 目标节点 ID 或 URL (必填) + --users string 用户 userId 列表,逗号分隔 + --role string 角色: MANAGER / EDITOR / DOWNLOADER / READER + --limit int 返回成员数上限 (仅 list,默认 30,最大 200) + --filter-role string 按角色过滤 (仅 list) +``` + +> **注意**:`drive export` 不存在。导出仅对自研文档 (adoc) 有意义,属于内容层操作,应使用 `doc export`。 ### 目标位置参数规则 | 目标位置 | 参数传递方式 | 前置步骤 | |---------|-----------|---------| | 未指定目标(默认) | `--folder ` | 先 `dws drive list-spaces --space-type mySpace` 获取「我的文件」的 `rootFolderId` | -| 知识库空间根目录 | `--workspace ` | 无需额外步骤,直接传入 workspaceId | +| 知识库空间根目录 | `--workspace ` | 无需额外步骤 | | 钉盘 space 根目录 | `--folder ` | 先 `dws drive list-spaces` 获取目标 space 的 `rootFolderId` | -| 钉盘 space 下的子文件夹 | `--folder ` | 先 `dws drive list --space-id ` 逐层浏览,获取目标文件夹的 `fileId`(dentryUuid 格式) | +| 钉盘 space 下的子文件夹 | `--folder ` | 先 `dws drive list --space-id ` 逐层浏览 | ### 工作流示例 ```bash -# ── 场景 默认: 用户未指定目标位置 → 复制/移动到「我的文件」根目录 ── -# 1. 获取源文件 dentryUuid +# ── 场景 默认: 复制/移动到「我的文件」根目录 ── dws drive list --space-id --format json -# 2. 获取「我的文件」个人空间的 rootFolderId dws drive list-spaces --space-type mySpace --format json -# 3. 用「我的文件」的 rootFolderId 作为 --folder -dws doc copy --node <源文件dentryUuid> --folder <我的文件rootFolderId> --format json +dws drive copy --node <源文件dentryUuid> --folder <我的文件rootFolderId> --format json -# ── 场景 A: 复制钉盘文件到知识库空间根目录 ── -# 1. 获取源文件 dentryUuid -dws drive list --space-id --format json -# 2. 直接传 workspaceId 即可 -dws doc copy --node <源文件dentryUuid> --workspace --format json +# ── 场景 A: 复制到知识库空间根目录 ── +dws drive copy --node <源文件dentryUuid> --workspace --format json -# ── 场景 B: 移动钉盘文件到另一个钉盘 space 根目录 ── -# 1. 获取源文件 dentryUuid -dws drive list --space-id --format json -# 2. 获取目标 space 的 rootFolderId +# ── 场景 B: 移动到另一个钉盘 space 根目录 ── dws drive list-spaces --format json -# 3. 用 rootFolderId 作为 --folder(不需要传 --workspace) -dws doc move --node <源文件dentryUuid> --folder <目标space的rootFolderId> --format json +dws drive move --node <源文件dentryUuid> --folder <目标space的rootFolderId> --format json -# ── 场景 C: 复制钉盘文件到钉盘 space 下的子文件夹 ── -# 1. 获取源文件 dentryUuid -dws drive list --space-id --format json -# 2. 浏览目标 space 找到目标文件夹的 fileId(dentryUuid 格式) +# ── 场景 C: 复制到钉盘子文件夹 ── dws drive list --space-id --format json -# 若目标在更深层级,继续用 --folder 逐层浏览 -dws drive list --space-id --folder <父文件夹dentryUuid> --format json -# 3. 用目标文件夹的 fileId 作为 --folder -dws doc copy --node <源文件dentryUuid> --folder <目标文件夹fileId> --format json +dws drive copy --node <源文件dentryUuid> --folder <目标文件夹fileId> --format json ``` ## 上下文传递表 @@ -259,10 +334,13 @@ dws doc copy --node <源文件dentryUuid> --folder <目标文件夹fileId> --for | 操作 | 从返回中提取 | 用于 | | ------------- | ---------------------------- | -------------------------------------------------------- | -| `list` | **`fileId`**(UUID 格式,注意:不是 `dentryId`) | info / download / mkdir / delete / list 的 --node 或 --folder;`doc copy/move` 的 --node 或 --folder | +| `list` | **`fileId`**(UUID 格式,注意:不是 `dentryId`) | info / download / mkdir / delete / list 的 --node 或 --folder;`drive copy/move` 的 --node 或 --folder | | `list` | `spaceId` | info / download / mkdir / commit 的 --space-id | -| `list-spaces` | `rootFolderId` | `doc copy/move` 的 --folder(复制/移动到钉盘 space 根目录时) | +| `list-spaces` | `rootFolderId` | `drive copy/move` 的 --folder(复制/移动到钉盘 space 根目录时) | | `list-spaces` | `spaceId` | list / info / download / mkdir / upload 的 --space-id | +| `search` | **`fileId`**(文件/文件夹结果) | info / download / delete 的 --node;list 的 --folder | +| `search` | `spaceId` / `rootFolderId`(空间结果) | list 的 --space-id;`drive copy/move` 的 --folder | +| `search` | `nextCursor` | search 的 --cursor(翻页) | | `mkdir` | `fileId`(UUID 格式) | list 的 --folder | > **重要**:`drive list` 返回结果中同时包含 `dentryId` 和 `fileId` 两个字段。所有需要传 `--node` 的命令(info / download / delete)必须使用 `fileId`(即 dentryUuid),**不要使用** `dentryId`。 @@ -273,6 +351,7 @@ dws doc copy --node <源文件dentryUuid> --folder <目标文件夹fileId> --for - 不传 `--space-id` 时默认使用「我的文件」空间 - 不传 `--folder` 时默认操作空间根目录 - `--folder` 只能使用父文件夹的 `dentryUuid`。不要把 `drive info` 返回的数字型 `dentryId` 当作父目录;`dentryId` 只用于 `chat message send --dentry-id` +- **`--limit` 最大值为 50**,禁止传入超过 50 的值(如 `--limit 100`)。用户要求超过 50 条时,应使用 `--limit 50` 配合 `--cursor` 分页查询,不要直接传大于 50 的值 - `--order-by` 支持: `createTime`、`modifyTime`、`name` - **上传文件必须使用 `dws drive upload` 命令**,禁止使用 `upload-info` + `curl` + `commit` 三步手动流程 - `--file-name` 必须包含扩展名(如 `report.pdf`) diff --git a/skills/multi/dingtalk-sheet/references/sheet.md b/skills/multi/dingtalk-sheet/references/sheet.md index edc0bdb3..82022692 100644 --- a/skills/multi/dingtalk-sheet/references/sheet.md +++ b/skills/multi/dingtalk-sheet/references/sheet.md @@ -1,1994 +1,92 @@ # 电子表格 (sheet) 命令参考 -## 适用范围(重要) +> **渐进式文档**:本文件为路由层(索引 + 意图判断 + 全局约束),各命令的详细参数、示例和注意事项在 [sheet/](./sheet/) 目录下按需加载。 -`sheet` 产品线仅支持钉钉在线电子表格(`contentType=ALIDOC`、`extension=axls`),不支持上传的 `xlsx` / `xls` / `xlsm` / `csv` 等本地表格文件。 +## 适用范围 + +`sheet` 仅支持钉钉在线电子表格(`contentType=ALIDOC`、`extension=axls`)。 | 文件类型 | 处理方式 | |---------|---------| -| 在线电子表格(`axls`) | 走 `sheet` 全部命令(读/写/筛选/合并/导出等服务端原子操作) | -| `xlsx` / `xls` / `xlsm` / `csv` 等本地表格文件 | 必须用 `dws doc download --node --output <路径>` 先下载到本地再解析处理,禁止调用任何 `sheet` 子命令(sheet 底层 MCP 工具仅认 `axls`,传入 xlsx 节点会直接报错) | -| 想把在线表格导出为 xlsx | 用 `dws sheet export` ——输入是 `axls`,输出是 xlsx(axls → xlsx 的格式转换) | - -> 用户直接粘贴原始 `alidocs` URL 时必须先 probe:先执行 `dws doc info --node --format json`,按 [链接规范](../url-patterns.md#alidocs-url-类型探测流程) 校验 `contentType` 和 `extension`: -> - 仅当 `contentType=ALIDOC` 且 `extension=axls` 时,才继续走 `sheet` -> - 如果是 `xlsx` / `xls` / `xlsm` / `csv`,立即转向 `dws doc download`,并告知用户"这是本地表格文件,已为你下载到本地处理" - -## URL 识别与 NODE_ID 提取 - -当用户输入包含钉钉文档 URL 时,必须先识别并提取 NODE_ID,再判断意图。 - -硬性规则:对用户直接给出的原始 `alidocs` URL,不允许直接走 `sheet` 命令,必须先执行: - -```bash -dws doc info --node "" --format json -``` +| 在线电子表格(`axls`) | 走 `sheet` 全部命令 | +| `xlsx` / `xls` / `xlsm` / `csv` | `dws drive download --node --output <路径>` 下载到本地处理,禁止调用任何 `sheet` 子命令 | +| 本地 xlsx 导入为在线表格 | `dws drive upload --file <路径> --convert`(上传并转换为在线电子表格,转换后可用 `sheet` 命令操作) | +| 在线表格导出为 xlsx | `dws sheet export`(axls → xlsx 格式转换) | -根据返回路由: +用户贴原始 `alidocs` URL 时必须先 probe:`dws doc info --node --format json`,按 [链接规范](../url-patterns.md#alidocs-url-类型探测流程) 校验: - `contentType=ALIDOC` + `extension=axls` → 继续走 `sheet` -- `contentType≠ALIDOC` + `extension=xlsx` / `xls` / `xlsm` / `csv` → 转向 `dws doc download --node --output <路径>`,禁止调用任何 sheet 子命令 -- 其他类型 → 按 [链接规范](../url-patterns.md#alidocs-url-类型探测流程) 路由 - -补充:如果这是用户直接提供的原始 `alidocs` URL,先按 [链接规范](../url-patterns.md#alidocs-url-类型探测流程) probe 一次确认真实类型,再判断是否继续走 `sheet`。 - -### 支持的 URL 格式 - -| 格式 | 示例 | NODE_ID 提取方式 | -|------|------|----------------| -| `alidocs.dingtalk.com/i/nodes/{id}` | `https://alidocs.dingtalk.com/i/nodes/9E05BDRVQePjzLkZt2p2vE7kV63zgkYA` | 取 URL 路径最后一段 | -| `alidocs.dingtalk.com/i/nodes/{id}?queryParams` | `https://alidocs.dingtalk.com/i/nodes/abc123?doc_type=wiki_doc` | 忽略 query 参数,取路径最后一段 | -| `alidocs.dingtalk.com/spreadsheetv2/{key}/...` | `https://alidocs.dingtalk.com/spreadsheetv2/vKJngl50tJN1v3a3/...?dentryKey=vKJngl50tJN1v3a3&type=s` | **不要提取 path segment**,将完整 URL 直接传给 `--node` 参数,由 MCP 服务端解析 | - -### 提取规则 - -1. 匹配 URL 中 `alidocs.dingtalk.com` 域名 -2. **判断 URL 路径格式**: - - 路径包含 `/i/nodes/` → 取 URL path 的最后一段作为 NODE_ID(去掉 query string 和 fragment) - - 路径包含 `/spreadsheetv2/` → **不要提取 path segment**,必须将完整 URL 原样传给 `--node` 参数(因为 path 中的短 ID 不是合法的 nodeId,MCP 服务端会自行解析完整 URL) -3. 对于 `/i/nodes/` 格式,提取出的 NODE_ID 可直接用于所有 `--node` 参数,也可将完整 URL 传给 `--node`(CLI 会自动解析) -4. 对用户直接提供的原始 `alidocs` URL,先按 [链接规范](../url-patterns.md#alidocs-url-类型探测流程) probe;只有 probe 确认 `contentType=ALIDOC` 且 `extension=axls` 时,才继续留在 `sheet`;如果 `extension=xlsx` / `xls` / `xlsm` / `csv`,必须转向 `dws doc download`,不能走任何 sheet 命令 - -## 查询命令帮助 - -当你不确定某个命令的具体参数、格式或可选项时,**优先执行 `--help` 查询**,不要猜测参数名或凭记忆编造。 - -```bash -# 查看 sheet 下所有子命令 -dws sheet --help - -# 查看具体命令的完整参数说明 -dws sheet range update --help -dws sheet filter-view create --help -dws sheet insert-dimension --help - -# 查看子命令组下的所有命令 -dws sheet range --help -dws sheet filter-view --help -``` - -规则: -- 参数名不确定时 → 先 `--help`,再调用 -- 报错 "unknown flag" 时 → `--help` 确认正确的 flag 名称 -- 不确定某个功能是否存在时 → `dws sheet --help` 查看命令列表 - -## 命令速查表 - -| 命令 | 用途 | -|------|------| -| `sheet create` | 创建钉钉表格文档 | -| `sheet list` | 获取全部工作表列表 | -| `sheet info` | 获取指定工作表详情 | -| `sheet new` | 新建工作表 | -| `sheet update` | 更新工作表属性(标题/位置/隐藏/冻结) | -| `sheet copy` | 复制工作表 | -| `sheet range read` | 读取工作表数据(别名: range get) | -| `sheet range update` | 更新指定区域内容(值/公式/超链接) | -| `sheet range set-style` | 设置单元格样式 | -| `sheet range batch-set-style` | 按配置文件批量设置样式 | -| `sheet find` | 搜索单元格内容 | -| `sheet append` | 在末尾追加数据行 | -| `sheet csv-put` | 将 CSV 数据写入指定位置(纯值,自动扩容) | -| `sheet replace` | 全局查找替换文本 | -| `sheet merge-cells` | 合并单元格 | -| `sheet unmerge-cells` | 取消合并单元格 | -| `sheet set-dropdown` | 设置下拉列表 | -| `sheet get-dropdown` | 获取下拉列表配置 | -| `sheet delete-dropdown` | 删除下拉列表 | -| `sheet insert-dimension` | 在指定位置插入行或列 | -| `sheet delete-dimension` | 删除指定位置的行或列 | -| `sheet update-dimension` | 更新行/列属性(显隐/行高/列宽) | -| `sheet move-dimension` | 移动行或列到指定位置 | -| `sheet add-dimension` | 在末尾追加空行或空列 | -| `sheet media-upload` | 上传附件到表格 | -| `sheet write-image` | 上传图片并写入单元格 | -| `sheet create-float-image` | 创建浮动图片 | -| `sheet get-float-image` | 获取浮动图片详情 | -| `sheet list-float-images` | 列出工作表所有浮动图片 | -| `sheet update-float-image` | 更新浮动图片属性 | -| `sheet delete-float-image` | 删除浮动图片 | -| `sheet export` | 导出表格为 xlsx | -| `sheet filter get` | 获取全局筛选信息 | -| `sheet filter create` | 创建全局筛选 | -| `sheet filter delete` | 删除全局筛选 | -| `sheet filter update` | 批量更新筛选条件 | -| `sheet filter clear-criteria` | 清除单列筛选条件 | -| `sheet filter sort` | 筛选排序 | -| `sheet filter-view list` | 获取所有筛选视图 | -| `sheet filter-view create` | 创建筛选视图 | -| `sheet filter-view update` | 更新筛选视图属性 | -| `sheet filter-view delete` | 删除筛选视图 | -| `sheet filter-view update-criteria` | 更新筛选视图列条件 | -| `sheet filter-view delete-criteria` | 删除筛选视图列条件 | -| `sheet filter-view info` | 获取单个筛选视图详情 | -| `sheet filter-view list-criteria` | 列出筛选视图所有列条件 | -| `sheet filter-view get-criteria` | 获取单列筛选条件详情 | - -> 不确定参数?对任意命令执行 `dws sheet <命令> --help` 查看完整用法。 - -## 意图判断 - -### 表格与工作表管理 - -用户说"创建表格/新建电子表格": -- 创建表格文档 → `create` - -用户说"看工作表/有哪些工作表/表格结构": -- 列出工作表 → `list` -- 工作表详情 → `info` - -用户说"加工作表/新增Sheet": -- 新建工作表 → `new` - -用户说"修改工作表名称/重命名工作表/移动工作表位置/隐藏工作表/显示工作表/冻结行/冻结列/取消冻结/更新工作表属性": -- 更新工作表属性 → `update` -- 重命名工作表 → `update --title "新名称"` -- 移动工作表位置 → `update --index N` -- 隐藏工作表 → `update --hidden` -- 显示工作表 → `update --hidden=false` -- 冻结行列 → `update --frozen-row-count N --frozen-column-count M` -- 取消冻结 → `update --frozen-row-count 0 --frozen-column-count 0` - -用户说"复制工作表/拷贝工作表/克隆工作表/工作表副本": -- 复制工作表 → `copy` -- 复制并指定名称 → `copy --title "副本名称"` -- 复制并指定位置 → `copy --index N` - -### 数据读写 - -用户说"读数据/看表格内容": -- 读取数据 → `range read` - -用户说"写数据/填表/更新单元格/写入公式": -- 更新数据 → `range update` -- 【强制】`--sheet-id` 必填:即使是单工作表也不能省略,不要参照 `range read` 的默认行为;未知时先执行 `dws sheet list --node --format json` 获取 `sheetId`,禁止凭空臆测为 `Sheet1`、`sheet1`、`0`、`default` 等 -- 注意:如果用户的目的是替换文本、移动行列或追加空行空列,请勿使用 `range update`,必须使用对应的专用命令(`replace`/`move-dimension`/`add-dimension`) -- **批量纯值写入优先用 `csv-put`**:当写入场景同时满足以下条件时,必须优先使用 `csv-put` 而非 `range update`:(1) 写入的是纯值(不含公式、超链接);(2) 数据量较大(超过 5 行或超过 20 个单元格);(3) 数据来源为表格/CSV 文本/结构化文本。`csv-put` 无需手动构造二维 JSON 数组,直接传 CSV 文本即可,更简洁高效且支持自动扩容 - -用户说"追加数据/添加行/在末尾加数据/新增记录": -- 追加数据 → `append` - -用户说"批量写入CSV/导入CSV/CSV写入表格/把CSV贴到表格里": -- 写入 CSV → `csv-put` -- 与 `range update` 的区别:`csv-put` 接受 CSV 文本直接写入,无需手动构造二维 JSON 数组;适合大批量纯值写入 -- 与 `append` 的区别:`csv-put` 写入指定位置(--start-cell),`append` 在末尾追加 - -用户说"搜索/查找/找单元格/搜内容/精确搜索/精确匹配/完全匹配/全字匹配": -- 搜索单元格 → `find` -- 精确匹配(只匹配完全等于的,不匹配包含的) → `find --match-entire-cell` -- 正则搜索 → `find --use-regexp` -- 搜索公式 → `find --match-formula` -- 不要用 `range read` 读取全量数据后在客户端过滤来替代 `find`,必须使用 `find` 命令的服务端搜索能力 - -用户说"替换/查找替换/全局替换/批量替换/把A替换成B/把所有的X改成Y": -- 查找替换 → `replace` -- 精确匹配后替换(只替换内容完全等于的单元格) → `replace --match-entire-cell` -- 正则替换 → `replace --use-regexp` -- 删除匹配内容 → `replace --replacement ""` -- 请勿用 `find` + `range update`、`range read` + `range update` 等组合来模拟替换,`replace` 是服务端原子操作,效率更高且返回替换计数 - -### 行列操作 - -用户说"插入行/插入列/在某行前插入/在某列前插入": -- 插入行或列 → `insert-dimension` -- 在末尾追加 → `append`(insert-dimension 不支持末尾追加) - -用户说"删除行/删除列/删掉第几行/删掉某列/移除行/移除列": -- 删除行或列 → `delete-dimension` -- 仅清空内容但保留行/列 → `range update --values` 写入空字符串 `""` - -用户说"隐藏行/隐藏列/显示行/显示列/设置行高/设置列宽/调整行高/调整列宽/行列属性": -- 隐藏/显示行或列 → `update-dimension --hidden` / `--hidden=false` -- 设置行高/列宽 → `update-dimension --pixel-size` -- 同时修改尺寸与显隐 → `update-dimension --pixel-size --hidden` - -用户说"移动行/移动列/调整行顺序/调整列顺序/行列拖拽/把第N行移到第M行": -- 移动行或列 → `move-dimension` -- 请勿用 `range read` + `range update` 读取再重写来模拟移动,`move-dimension` 是原子操作,能保留格式和合并状态 - -用户说"追加空行/追加空列/增加行数/增加列数/扩展表格/在末尾加空行": -- 追加空行/空列 → `add-dimension` -- 注意与 `append`(追加数据行)区分:`add-dimension` 追加的是空行/空列,`append` 追加的是带数据的行 -- 请勿用 `range update` 写空数据来模拟追加,`add-dimension` 直接扩展表格维度 - -### 单元格格式 - -用户说"设置样式/改颜色/设背景色/加粗/居中/换行/字体颜色/字号": -- 设置单元格样式 → `range set-style` -- 批量设置不同 range 的样式 → `range batch-set-style --batch ./styles.json`(内部顺序循环调 `update_range`) -- 请勿用 `range update --values` 写空/重写来模拟样式变更;也请勿把样式变更混在 `range update` 里、再故意清空 `--values` - -用户说"设置数字格式/改成百分比/用人民币显示/按日期显示/文本格式/保留几位小数": -- 设置数字格式 → `range set-style --number-format <格式代码>`(如 `0%` / `¥#,##0.00` / `yyyy/m/d` / `@`) -- 请勿用 `range update` 传递数字格式,`range update` 仅负责写入值与超链接;数字格式属于单元格样式,统一走 `range set-style` - -用户说"合并单元格/合并/合并区域/按行合并/按列合并": -- 合并所有单元格 → `merge-cells`(默认 mergeAll) -- 按行合并 → `merge-cells --merge-type mergeRows` -- 按列合并 → `merge-cells --merge-type mergeColumns` - -用户说"取消合并/拆分单元格/还原合并": -- 取消合并单元格 → `unmerge-cells` - -### 下拉列表 - -用户说"设置下拉列表/下拉选项/下拉菜单/添加下拉/配置下拉": -- 设置下拉列表 → `set-dropdown` -- 设置多选下拉 → `set-dropdown --multi-select` - -用户说"查看下拉列表/获取下拉配置/下拉列表有哪些选项": -- 获取下拉列表配置 → `get-dropdown` - -用户说"删除下拉列表/移除下拉/取消下拉/清除下拉": -- 删除下拉列表 → `delete-dropdown` - -### 媒体上传 - -用户说"上传附件/传文件到表格/上传文件到表格/上传到表格": -- 上传附件 → `media-upload`(需表格 ID 或 URL + 本地文件路径) -- 用户指定了上传后的名称 → `media-upload --name "自定义名称"` -- `media-upload` 的 `--name` 参数用于指定附件在表格中显示的名称(不改变本地文件名);不传时默认使用本地文件名 - -用户说"写入图片/插入图片/加图片/放图片到单元格/嵌入图片到表格": -- 写入图片 → `write-image`(需表格 ID + 工作表 ID + 单元格范围 + 本地图片路径) -- 禁止使用 `range update` 写入图片,因为 `update_range` 的 MCP 工具不支持图片类型参数,调用必定失败。必须使用 `write-image` 命令 -- 用户指定了图片尺寸 → `write-image --width N --height M` - -### 浮动图片 - -用户说"浮动图片/悬浮图片/在表格上放一张图/加个浮动的图": -- 创建浮动图片 → 先 `media-upload` 上传图片获取 `resourceUrl`,再 `create-float-image` -- 浮动图片悬浮于单元格之上,不占用单元格内容,与 `write-image`(写入单元格内部的图片)不同 - -用户说"查看浮动图片/有哪些浮动图片/浮动图片列表": -- 列出所有浮动图片 → `list-float-images` -- 查看某个浮动图片详情 → `get-float-image` - -用户说"移动浮动图片/调整浮动图片大小/修改浮动图片/更新浮动图片": -- 更新浮动图片属性 → `update-float-image`(可更新锚点位置、尺寸、偏移量、图片资源路径) - -用户说"删除浮动图片/移除浮动图片": -- 删除浮动图片 → `delete-float-image` - -关键区分:`write-image`(单元格内嵌图片,占据单元格内容)vs `create-float-image`(浮动图片,悬浮于单元格之上,不占内容) - -### 筛选视图 - -用户说"筛选/过滤/只看某些值/只显示满足条件的行/筛选数据/创建筛选/删除筛选/设置筛选条件/清除筛选/排序": -- 查看当前筛选 → `filter get` -- 创建筛选 → `filter create` -- 删除筛选 → `filter delete` -- 批量设置多列条件 → `filter update` -- 清除某一列条件 → `filter clear-criteria` -- 按列排序 → `filter sort` -- **区分全局筛选与筛选视图**:如果用户说"筛选视图"则走 `filter-view` 系列;如果只说"筛选/过滤/只看"则默认走全局 `filter` 系列 -- **禁止替代方案**:当用户要求"筛选/只看/仅保留某些行"时,必须通过 `filter create` / `filter update` 创建真实的筛选器。禁止用"删除不符合条件的行"或"新建工作表只放符合条件的行"来代替——这些做法会让原数据丢失或不可恢复 - -用户说"筛选视图/查看筛选视图/有哪些筛选视图/筛选视图列表": -- 获取所有筛选视图 → `filter-view list` - -用户说"筛选视图详情/查看某个筛选视图/筛选视图信息/筛选视图配置": -- 获取单个筛选视图详情 → `filter-view info` - -用户说"创建筛选视图/新建筛选视图/添加筛选视图": -- 创建筛选视图 → `filter-view create` - -用户说"更新筛选视图/修改筛选视图/改筛选视图名称/改筛选视图范围": -- 更新筛选视图属性 → `filter-view update` - -用户说"删除筛选视图/移除筛选视图": -- 删除筛选视图 → `filter-view delete` - -用户说"设置筛选条件/添加筛选条件/配置筛选视图条件/按值筛选/按条件筛选/按颜色筛选": -- 设置筛选视图列条件 → `filter-view update-criteria` - -用户说"查看筛选条件/有哪些筛选条件/筛选视图设了什么条件/列出筛选条件": -- 列出所有列条件 → `filter-view list-criteria` -- 查看某一列的条件 → `filter-view get-criteria --column N` - -用户说"清除筛选条件/移除筛选条件/取消筛选条件": -- 清除筛选视图列条件 → `filter-view delete-criteria` -- 注意与 `filter-view delete`(删除整个筛选视图)区分:`delete-criteria` 仅清除指定列的条件,不删除筛选视图本身 - -### 导出 - -用户说"导出/下载xlsx/存为Excel/存成表格文件/把表格变成xlsx/导出表格/下载表格/导出为 excel": -- 导出表格 → `export`(单命令一站式,内部自动完成提交、轮询、可选下载) -- 仅需传 `--node`,可选 `--output` 指定本地文件/目录(不传则返回 downloadUrl) -- 需要落盘到本地 → `dws sheet export --node --output `,命令自动下载 xlsx -- 禁止用 `range read` 全量读取后自行拼接 xlsx 来模拟导出,必须使用 `export` 命令(服务端原子导出,保留格式/合并/公式等属性) -- 禁止在 AI Agent 侧实现轮询或重试,CLI 内部已按渐进式退避策略完成(最多 30 次约 5 分钟) - -### URL 粘贴场景 +- `extension=xlsx` / `xls` / `xlsm` / `csv` → 转 `dws drive download`,告知用户"这是本地表格文件,已为你下载到本地处理" + +## URL → NODE_ID + +| URL 格式 | 提取方式 | +|----------|---------| +| `.../i/nodes/{id}` 或 `.../i/nodes/{id}?query` | 取路径末段作 NODE_ID(忽略 query) | +| `.../spreadsheetv2/{key}/...` | **完整 URL 原样传 `--node`**,禁止提取 path segment | + +参数不确定时先 `dws sheet <命令> --help`。 + +## Reference 索引 + +| Reference | 描述 | +|-----------|------| +| [sheet-workbook](./sheet/sheet-workbook.md) | 管理表格文档与工作表。当用户说"创建表格"、"有哪些工作表"、"新建/重命名/隐藏/冻结/复制/删除工作表"时使用。命令:`create`/`list`/`info`/`new`/`update`/`copy`/`delete-sheet` | +| [sheet-read-data](./sheet/sheet-read-data.md) | 读取工作表数据。当用户说"读数据"、"看表格内容"、"查看数据"时使用。推荐 `csv-get`(CSV 格式、token 低、防爆保护);需 value + dataValidation / hyperlink / richText / cellStyles 等 per-cell 元数据时用 `range read`。大范围数据建议分页读取(单次 ≤5000 单元格)。命令:`csv-get`/`range read` | +| [sheet-write-data](./sheet/sheet-write-data.md) | 写入数据到工作表。当用户说"写数据"、"填表"、"更新单元格"、"写公式"、"超链接"、"写值同时设样式/数据验证"、"追加数据"、"导入CSV"时使用。大批量纯值(>5行或>20单元格)必须用 `csv-put` 而非 `range update`。命令:`range update`/`append`/`csv-put` | +| [sheet-search-replace](./sheet/sheet-search-replace.md) | 搜索和替换文本。当用户说"搜索"、"查找"、"替换"、"把A改成B"时使用。禁止用 `range read` 全量读取后客户端过滤代替 `find`,禁止用 `range update` 模拟 `replace`。命令:`find`/`replace` | +| [sheet-range-operations](./sheet/sheet-range-operations.md) | 区域结构性操作。当用户说"清空"、"排序"、"自动填充"、"复制区域到"、"移动数据到"时使用。均为服务端原子操作,禁止 `range read`+`range update` 组合模拟。排序前必须先读前几行判断表头。命令:`range clear`/`range sort`/`range fill`/`range copy-to`/`range move-to` | +| [sheet-dimension-operations](./sheet/sheet-dimension-operations.md) | 行列增删移动与属性设置。当用户说"插入行/列"、"删除行/列"、"隐藏/显示行列"、"设行高/列宽"、"移动行/列"、"追加空行/空列"时使用。命令:`insert-dimension`/`delete-dimension`/`update-dimension`/`move-dimension`/`add-dimension` | +| [sheet-style-format](./sheet/sheet-style-format.md) | 单元格样式与合并。当用户说"设样式"、"改颜色/字体/对齐"、"数字格式(百分比/货币/日期)"、"合并/取消合并"时使用。纯样式/批量样式走 `set-style`;写值同时设置少量 cell 样式可用 `range update` 的 `cellStyles`。命令:`range set-style`/`range batch-set-style`/`merge-cells`/`unmerge-cells` | +| [sheet-dropdown](./sheet/sheet-dropdown.md) | 下拉列表管理。当用户说"设置下拉"、"下拉选项"、"删除下拉"时使用。命令:`set-dropdown`/`get-dropdown`/`delete-dropdown` | +| [sheet-media-image](./sheet/sheet-media-image.md) | 附件上传与图片。当用户说"上传附件"、"写入图片到单元格"、"浮动图片"时使用。单元格图片用 `write-image`(禁止 `range update`);浮动图片需先 `media-upload` 再 `create-float-image`。命令:`media-upload`/`write-image`/`create-float-image`/`get-float-image`/`list-float-images`/`update-float-image`/`delete-float-image` | +| [sheet-filter](./sheet/sheet-filter.md) | 全局筛选。当用户说"筛选"、"过滤"、"只看某些行"(未说"筛选视图")时使用。禁止用"删除不符合条件的行"代替筛选。命令:`filter get`/`create`/`delete`/`update`/`clear-criteria`/`sort` | +| [sheet-filter-view](./sheet/sheet-filter-view.md) | 筛选视图(个人化,不影响协作者)。当用户明确说"筛选视图"时使用,与全局筛选相互独立。命令:`filter-view list`/`create`/`update`/`delete`/`info`/`update-criteria`/`delete-criteria`/`list-criteria`/`get-criteria` | +| [sheet-conditional-format](./sheet/sheet-conditional-format.md) | 条件格式规则。触发词:标红/标黄/高亮/突出/标记/数据条/色阶/颜色随数据变 → **强制**走条件格式,禁止 `range set-style` 静态样式替代。命令:`cond-format list`/`create`/`update`/`delete` | +| [sheet-export](./sheet/sheet-export.md) | 导出表格为 xlsx。当用户说"导出"、"下载xlsx"、"存为Excel"时使用。单命令一站式,CLI 内部自动轮询,禁止 Agent 侧重试。命令:`export` | + +## 全局硬约束 + +1. **`--sheet-id` 禁止臆测**:未知时必须 `dws sheet list --node --format json` 查询,禁止编造 `Sheet1`/`sheet1`/`0`/`default` +2. **合并单元格是结构信息**:`dws sheet info --node --sheet-id --format json` 返回 `mergedRanges`(如 `["C7:D11"]`);不要在 `range read` / `csv-get` 里寻找合并信息 +3. **`range update` 维度校验**:`--values` 行列数必须与 `--range` 完全一致;只接 `--values` 一个数据参数,cell `type` 仅支持 `text` / `richText`;整格超链接通过 cell-level `hyperlink` 表达,富文本片段链接才使用 `richText.texts[].type="link"` +4. **dataValidation 三语义**:不传 `dataValidation` 字段=保留原 DV;`dataValidation:{type:"none"}`=显式清除;`dataValidation:{type:"dropdown"/"checkbox",...}`=覆盖。`{}` 跳过亦保留原 DV +5. **hyperlink 三语义**:不传 `hyperlink` 字段=保留原整格超链接;`hyperlink:{type:"none"}`=显式清除;`hyperlink:{type:"path"/"sheet"/"range",link,...}`=覆盖。Agent 调用不要用 `hyperlink:null`,避免网关/Schema 过滤 null 字段 +6. **样式写法**:cell-level 样式用 `cellStyles` 或 `range set-style`;richText 片段级样式才用子项 `style`。不要在 `type:"text"` 顶层使用旧 `style` 字段 +7. **用专用命令不用组合模拟**:搜索→`find`、替换→`replace`、清空→`range clear`、排序→`range sort`、填充→`range fill`、复制区域→`range copy-to`、移动区域→`range move-to`、移动行列→`move-dimension` +8. **大批量纯值用 `csv-put`**(>5 行或 >20 单元格),不用 `range update` +9. **单元格图片用 `write-image`**(`range update` 不支持图片参数) +10. **`export` 禁止自行轮询**(CLI 内部已完成渐进式退避,最多 30 次约 5 分钟) +11. **单次调用上限**:`range update` / `set-style` 行数 ≤ 1000,单元格总数建议 ≤ 5000(硬限 30000) +12. **关键区分**:sheet(电子表格/单元格读写)vs aitable(AI多维表/结构化记录)vs doc(文档) + +## URL 粘贴场景 用户直接粘贴表格 URL(无其他指令): -- 先 probe:`dws doc info --node --format json` 校验 `contentType` 和 `extension` -- `extension=axls` → `list`(列出工作表)+ `range read`(读取第一个工作表数据) -- `extension=xlsx` / `xls` / `xlsm` / `csv` → 转 `dws doc download --node --output ./`,告知用户"这是本地表格文件,已为你下载到本地",然后基于本地文件继续后续处理 +- 先 probe:`dws doc info --node --format json` +- `extension=axls` → `list` + `range read`(读取第一个工作表数据) +- `extension=xlsx`/`xls`/`xlsm`/`csv` → 转 `dws drive download --node --output ./` 用户粘贴 URL + 附加指令: -- 已 probe 为 `axls` 时: - - "帮我看看这个表格有什么数据" → `range read` - - "这个表格有哪些工作表" → `list` - - "往这个表格写入数据" → `range update` - - "帮我找一下表格里的XXX" → `find` -- probe 为 xlsx/xls/xlsm/csv 时:无论用户说"读数据/查看/分析",先走 `dws doc download` 下载到本地,由用户或后续步骤对本地 xlsx 进行解析,严禁调用 `sheet list` / `range read` 等命令 - -关键区分: sheet(电子表格/单元格读写) vs aitable(AI多维表/结构化记录) vs doc(文档编辑/阅读) - -## 关键注意事项 - -以下是最容易出错的规则,**必须严格遵守**: - -- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) -- ★ **`range update` 维度校验(强制)**:`--values` / `--hyperlinks` 的行列数必须与 `--range` 完全一致。例如 `--range "A1:C3"` → `--values` 必须是 3×3 数组 -- ★ **`range update` 清空规范(强制)**:清空单元格用空字符串 `""`,禁止用 `null`(全 null 会被跳过无效果) -- ★ **单次调用上限(强制)**:`range update` / `set-style` 行数 ≤ 1000,单元格总数建议 ≤ 5000(硬限 30000) -- ★ **大批量纯值写入用 `csv-put` 不用 `range update`**:当写入纯值(无公式/超链接)且数据量较大时(>5 行或 >20 单元格),必须使用 `csv-put`。`csv-put` 接受 CSV 文本直接写入,无需构造二维 JSON 数组,支持自动扩容,更简洁高效。仅在需要写入公式、超链接、或仅更新少量单元格时才使用 `range update` -- ★ **搜索用 `find` 不用 `range read`**:`find` 是服务端搜索,禁止用 `range read` 全量读取后客户端过滤 -- ★ **替换用 `replace` 不用 `range update`**:`replace` 是服务端原子操作,返回替换计数 -- ★ **移动用 `move-dimension` 不用 `range update`**:原子操作,保留格式和合并状态 -- ★ **单元格图片用 `write-image` 不用 `range update`**:`update_range` MCP 不支持图片参数,调用必失败 -- ★ **浮动图片用 `create-float-image` 不用 `write-image`**:两者用途不同——`write-image` 写入单元格内部,`create-float-image` 创建悬浮于单元格之上的浮动图片;`--src` 必须来自 `media-upload` 的 `resourceUrl` -- ★ **`export` 禁止自行轮询/重试**:CLI 内部已完成渐进式退避轮询(最多 30 次约 5 分钟),失败时直接告知用户稍后再试 -- ★ **关键区分**:sheet(在线电子表格/单元格读写) vs aitable(AI多维表/结构化记录/字段定义) vs doc(文档编辑/阅读) - -> 完整注意事项请参见本文档末尾「注意事项(完整版)」章节。 - -## 命令详细参考 - -> 以下为各命令的完整 Usage、Flags 和示例。参数不确定时也可直接执行 `dws sheet <命令> --help` 在线查看。 - -### 创建钉钉表格文档 -``` -Usage: - dws sheet create [flags] -Example: - dws sheet create --name "销售数据" - dws sheet create --name "Q1 数据" --folder - dws sheet create --name "知识库表格" --workspace -Flags: - --name string 表格名称 (必填) - --folder string 目标文件夹 ID (dentryUuid 格式) 或 URL;禁止传入纯数字 dentryId - --workspace string 目标知识库 ID -``` - -> **ID 格式约束**:`--folder` 只接受 UUID 格式的 `fileId`(如 `ZgpG2NdyVXYOR2D5UGDok65MJMwvDqPk`)或 alidocs 文件夹 URL。`drive list` 返回中有 `dentryId`(纯数字,如 `218595998810`)和 `fileId`(UUID 格式)两个字段,**必须使用 `fileId`,禁止使用 `dentryId`**,传入纯数字会导致命令失败。 - -### 获取全部工作表列表 -``` -Usage: - dws sheet list [flags] -Example: - dws sheet list --node - dws sheet list --node "https://alidocs.dingtalk.com/i/nodes/" -Flags: - --node string 表格文档 ID 或 URL (必填) -``` - -### 获取指定工作表详情 -``` -Usage: - dws sheet info [flags] -Example: - dws sheet info --node - dws sheet info --node --sheet-id - dws sheet info --node --sheet-id "Sheet1" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (不传则返回第一个工作表) -``` - -### 新建工作表 -``` -Usage: - dws sheet new [flags] -Example: - dws sheet new --node --name "Sheet2" - dws sheet new --node --name "数据汇总" -Flags: - --node string 表格文档 ID (必填) - --name string 工作表名称 (必填) -``` - -### 更新工作表属性 -``` -Usage: - dws sheet update [flags] -Example: - # 改名 + 调整冻结 - dws sheet update --node --sheet-id --title "汇总表" --frozen-row-count 2 --frozen-column-count 1 - - # 隐藏工作表 - dws sheet update --node --sheet-id --hidden=true - - # 显示工作表 - dws sheet update --node --sheet-id --hidden=false - - # 移动工作表到第一个位置 - dws sheet update --node --sheet-id --index 0 - - # 取消冻结 - dws sheet update --node --sheet-id --frozen-row-count 0 --frozen-column-count 0 -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --title string 新标题,最长 100 字符,不能包含 / \ ? * [ ] : - --index int 新位置(从 0 开始) - --hidden --hidden=true 隐藏,--hidden=false 取消隐藏 - --frozen-row-count int 冻结行数,0 表示取消冻结 - --frozen-column-count int 冻结列数,0 表示取消冻结 -``` - -更新工作表标题、位置、隐藏状态、冻结行列。 -`--title` / `--index` / `--hidden` / `--frozen-row-count` / `--frozen-column-count` 至少提供一个;多个属性可同时传入,将在同一次请求中更新。 - -注意: -- 至少需要保留一个可见的工作表,不能将所有工作表都隐藏 -- 冻结行数/列数不能超过工作表的总行数/列数 - -### 复制工作表 -``` -Usage: - dws sheet copy [flags] -Example: - # 按默认位置复制 - dws sheet copy --node --sheet-id - - # 指定副本名称和位置 - dws sheet copy --node --sheet-id --title "销售副本" --index 2 - - # 只指定名称 - dws sheet copy --node --sheet-id --title "备份" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 源工作表 ID 或名称 (必填) - --title string 副本名称,最长 100 字符,不能包含 / \ ? * [ ] : (不传则系统自动生成) - --index int 副本位置(从 0 开始)(不传则放在源工作表之后) -``` - -复制指定工作表,在同一表格中创建一个副本。 -复制操作会将源工作表的所有内容(包括数据、格式、公式等)完整复制到新工作表中。 -传 `--index` 时,CLI 会先复制,再追加一次位置更新,把副本移动到目标索引。 -名称与已有工作表重复时系统会自动重命名。 - -### 读取工作表数据 -``` -Usage: - dws sheet range read [flags] # 别名: dws sheet range get -Example: - dws sheet range read --node - dws sheet range read --node --sheet-id - dws sheet range read --node --sheet-id "Sheet1" --range "A1:D10" - dws sheet range read --node --range "Sheet1!A1:D10" - - # 使用 get 别名,与 read 等价 - dws sheet range get --node --sheet-id --range "A1:D10" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (不传则默认第一个工作表) - --range string 读取范围,A1 表示法 (如 A1:D10,不传则读取全部数据) -``` - -**超时处理建议**:读取大范围数据时若出现超时或响应过慢,请主动缩小 `--range` 查询范围,**建议单次读取的单元格数量控制在 5000 个以内**(例如 50 行 × 100 列、100 行 × 50 列)。对于大表可采用分页读取策略: -- 先通过 `info` 获取 `rowCount` / `lastNonEmptyRow` / `columnCount` 确定数据边界 -- 按行分批读取,如 `A1:J500`、`A501:J1000`、`A1001:J1500` …… -- 避免不传 `--range` 直接读取整个大工作表 - -### 更新工作表指定区域内容 -``` -Usage: - dws sheet range update [flags] -Example: - # 写入值 - dws sheet range update --node --sheet-id --range "A1:B2" \ - --values '[["姓名","分数"],["张三",90]]' - - # 写入公式 - dws sheet range update --node --sheet-id --range "C2" \ - --values '[["=A2&B2"]]' - - # 写入超链接 - dws sheet range update --node --sheet-id --range "A1" \ - --hyperlinks '[[{"type":"path","link":"https://dingtalk.com","text":"钉钉"}]]' - - # 清空区域(使用空字符串 "") - dws sheet range update --node --sheet-id --range "A1:B3" \ - --values '[["",""],["",""],["",""]]' -Flags: - --node string 表格文档 ID (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 目标单元格区域地址,如 A1:B3 (必填) - --values string 单元格值,二维 JSON 数组 (与 --hyperlinks 至少传一项) - --hyperlinks string 超链接,二维 JSON 数组 (与 --values 至少传一项) -``` - -**单次调用建议**:行数 ≤ 1000,单元格总数(行×列)≤ 5000;超过时请拆分多次调用。 - -**何时该用 `csv-put` 替代**:如果你准备用 `range update` 写入纯值(不含公式和超链接),且数据量超过 5 行或 20 个单元格,应改用 `csv-put`——它接受 CSV 文本直接写入,无需手动拼装二维 JSON 数组,且支持自动扩容行列。仅在需要写入公式(`=SUM(...)`)、超链接(`--hyperlinks`)、或修改少量单元格时才使用 `range update`。 - -**范围职责**:`range update` 仅负责写入单元格的值与超链接,不接受任何样式参数。如需设置数字格式(百分比 / 货币 / 日期 / 文本等)请使用 `dws sheet range set-style --number-format <格式代码>`,可与其他样式参数同时传入。 - -### 设置单元格样式 -``` -Usage: - dws sheet range set-style [flags] -Example: - # 给 A1:B3 打上黄底粗体居中 - dws sheet range set-style --node --sheet-id --range "A1:B3" \ - --bg-color "#FFF2CC" --font-weight bold --h-align center - - # 给 C1:C5 逐单元格设置不同背景色 - dws sheet range set-style --node --sheet-id --range "C1:C5" \ - --bg-colors-json '[["#FF0000"],["#00FF00"],["#0000FF"],["#FFFF00"],["#FF00FF"]]' - - # 整片 range 启用自动换行 - dws sheet range set-style --node --sheet-id --range "A1:E10" --word-wrap autoWrap -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 目标区域,如 A1:B3 (必填) - --bg-color string 背景色(#RRGGBB),一键刷整个 range;与 --bg-colors-json 二选一 - --bg-colors-json string 背景色二维 JSON 数组,维度需与 --range 一致 - --font-size int 字号,一键刷整个 range;与 --font-sizes-json 二选一 - --font-sizes-json string 字号二维 JSON 数组 - --h-align string 水平对齐:left/center/right/general - --h-aligns-json string 水平对齐二维 JSON 数组 - --v-align string 垂直对齐:top/middle/bottom - --v-aligns-json string 垂直对齐二维 JSON 数组 - --font-color string 字体颜色(#RRGGBB) - --font-colors-json string 字体颜色二维 JSON 数组 - --font-weight string 字体粗细:bold/normal - --font-weights-json string 字体粗细二维 JSON 数组 - --word-wrap string 换行方式:overflow/clip/autoWrap(整个 range 共用) - --number-format string 数字格式,如 General/@/#,##0/0%/yyyy/m/d -``` - -**特性说明**: -- 每个样式维度提供两种写法,二选一:`--xxx`(单值刷整个 range,CLI 本地展开为二维数组)vs `--xxx-json`(逐单元格指定,维度需与 `--range` 完全一致) -- 至少需传入一个样式参数。单次调用建议:行数 ≤ 1000,单元格总数 ≤ 5000 -- 枚举值按驼峰书写:`autoWrap`、`bold`、`normal`、`center` 等 - -### 批量设置单元格样式 -``` -Usage: - dws sheet range batch-set-style [flags] -Example: - dws sheet range batch-set-style --node --batch ./styles.json - dws sheet range batch-set-style --node --batch ./styles.json --continue-on-error -Flags: - --node string 表格文档 ID 或 URL (必填) - --batch string 批次配置 JSON 文件路径 (必填) - --continue-on-error 遇到失败时继续执行后续条目(默认遇错即停) -``` - -配置文件格式(JSON 数组,每个元素一条批次项): -```json -[ - { - "sheetId": "Sheet1", - "range": "A1:B3", - "bgColor": "#FFF2CC", - "fontSize": 12, - "hAlign": "center", - "vAlign": "middle", - "fontColor": "#333333", - "fontWeight": "bold", - "wordWrap": "autoWrap", - "numberFormat": "General" - }, - { - "sheetId": "Sheet1", - "range": "C1:C5", - "bgColorsJson": "[[\"#FF0000\"],[\"#00FF00\"],[\"#0000FF\"],[\"#FFFF00\"],[\"#FF00FF\"]]" - } -] -``` - -**特性说明**: -- CLI 侧顺序循环逐条调用 `update_range`(非服务端批量),运行时输出 `[N/M]` 进度 -- 每条记录执行与 `set-style` 一致的校验:至少一项样式字段 + rows ≤ 1000 + rows×cols ≤ 30000 + 枚举合法 -- 默认遇错即停(返回非 0),`--continue-on-error` 时所有条目跑完再返回首个错误 - -### 在工作表中搜索单元格内容 -``` -Usage: - dws sheet find [flags] -Example: - # 基本搜索 - dws sheet find --node --sheet-id --find "销售额" - - # 在指定范围内搜索 - dws sheet find --node --sheet-id --find "合计" --range "A1:D100" - - # 正则表达式搜索(不区分大小写) - dws sheet find --node --sheet-id --find "^total" --use-regexp --match-case=false - - # 精确匹配整个单元格内容 - dws sheet find --node --sheet-id --find "完成" --match-entire-cell - - # 搜索公式文本 - dws sheet find --node --sheet-id --find "SUM" --match-formula -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --find string 搜索文本 (必填) - --range string 搜索范围,A1 表示法 (如 A1:D10) - --match-case 区分大小写 (默认 true) - --match-entire-cell 精确匹配整个单元格内容 - --use-regexp 启用正则表达式搜索 - --match-formula 搜索公式文本而非显示值 - --include-hidden 包含隐藏单元格 -``` - -### 在工作表末尾追加数据 -``` -Usage: - dws sheet append [flags] -Example: - dws sheet append --node --sheet-id --values '[["张三","销售部",50000]]' - dws sheet append --node --sheet-id "Sheet1" \ - --values '[["李四","市场部",38000],["王五","销售部",62000]]' -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --values string 追加数据,二维 JSON 数组 (必填) -``` - -`--values` 为二维 JSON 数组,外层每个元素代表一行,内层每个元素代表一个单元格值。 -追加的数据列数应与工作表已有数据的列数保持一致。 - -### 将 CSV 数据写入指定位置 -``` -Usage: - dws sheet csv-put [flags] -Example: - dws sheet csv-put --node --sheet-id --start-cell A1 \ - --csv 'name,score\nAlice,95\nBob,87' - - dws sheet csv-put --node --sheet-id --start-cell B2 \ - --csv @data.csv --allow-overwrite - - cat data.csv | dws sheet csv-put --node --sheet-id \ - --start-cell A1 --csv - - - dws sheet csv-put --node --sheet-id --start-cell A1 \ - --csv @data.csv --dry-run -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --csv string CSV 文本、@文件路径 或 - 表示 stdin (必填) - --start-cell string 起始单元格,A1 表示法 (必填) - --allow-overwrite 允许覆盖已有数据 (默认 false) -``` - -将 RFC 4180 格式的 CSV 文本写入指定工作表的指定单元格位置。 -- 只写纯值,不支持公式/样式/批注。`=` 开头的内容当文本处理,不会被解析为公式 -- 数字/日期/百分数由表格引擎自动识别类型(如 `95` 存为数字,`2025-03-01` 存为日期) -- 自动扩容行列:CSV 数据超出当前工作表维度时自动追加行/列 -- 目标区域如含合并单元格,合并将被打散,值正常写入 -- `--allow-overwrite` 默认 false,目标区域有数据时需显式传 `--allow-overwrite` 才能覆盖 -- `--csv` 支持三种输入:直接传文本、`@filepath` 从本地文件读取、`-` 从 stdin 管道读取 -- CSV 文本上限 2M 字符,单元格总数上限 30000 -- 特殊字符处理:CLI 会自动过滤 `\r`(Windows 换行符)和 BOM(UTF-8 文件头标记),Excel/Windows 导出的 CSV 可直接使用;如 CSV 数据中含零宽字符(U+200B 等)或 Bidi 控制符,CLI 会拒绝并报错 - -### 在指定位置插入行或列 -``` -Usage: - dws sheet insert-dimension [flags] -Example: - # 在第 3 行之前插入 2 行 - dws sheet insert-dimension --node --sheet-id --dimension ROWS --position "3" --length 2 - - # 在 A 列之前插入 1 列 - dws sheet insert-dimension --node --sheet-id --dimension COLUMNS --position "A" --length 1 - - # 使用工作表前缀(忽略 --sheet-id) - dws sheet insert-dimension --node --sheet-id --dimension ROWS --position "Sheet1!3" --length 5 - - # 在 AB 列之前插入 3 列 - dws sheet insert-dimension --node --sheet-id --dimension COLUMNS --position "AB" --length 3 -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --dimension string 插入维度: ROWS 或 COLUMNS (必填) - --position string 插入位置,A1 表示法 (必填)。ROWS 时为行号如 "3";COLUMNS 时为列字母如 "A" - --length string 插入数量,正整数 (必填),最大 5000 -``` - -在钉钉表格指定工作表的指定位置之前插入若干空行或空列。 -`--dimension ROWS` 时,`--position` 为 1-based 行号字符串;`--dimension COLUMNS` 时,`--position` 为列字母。 -支持在 `--position` 中携带工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id`。 -若需要在末尾追加行/列,请使用 `append` 命令。 - -### 删除指定位置的行或列 - -> **CAUTION:** 不可逆操作 — 执行前必须向用户确认。 - -``` -Usage: - dws sheet delete-dimension [flags] -Example: - # 从第 3 行开始删除 2 行 - dws sheet delete-dimension --node --sheet-id --dimension ROWS --position "3" --length 2 - - # 从 A 列开始删除 1 列 - dws sheet delete-dimension --node --sheet-id --dimension COLUMNS --position "A" --length 1 - - # 使用工作表前缀(忽略 --sheet-id) - dws sheet delete-dimension --node --sheet-id --dimension ROWS --position "Sheet1!3" --length 5 - - # 从 AB 列开始删除 3 列 - dws sheet delete-dimension --node --sheet-id --dimension COLUMNS --position "AB" --length 3 -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --dimension string 删除维度: ROWS 或 COLUMNS (必填) - --position string 删除起始位置,A1 表示法 (必填)。ROWS 时为行号如 "3";COLUMNS 时为列字母如 "A" - --length string 删除数量,正整数 (必填),最大 5000 -``` - -在钉钉表格指定工作表中,从指定位置起删除若干连续的行或列。 -`--dimension ROWS` 时,`--position` 为 1-based 行号字符串;`--dimension COLUMNS` 时,`--position` 为列字母。 -支持在 `--position` 中携带工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id`。 -删除后后续的行/列会向前移动填补空位;若需要仅清空内容但保留行/列占位,请使用 `range update` 将目标区域写入空字符串 `""`。 - -### 更新指定范围行/列属性 -``` -Usage: - dws sheet update-dimension [flags] -Example: - # 隐藏第 3~4 行 - dws sheet update-dimension --node --sheet-id --dimension ROWS --start-index "3" --length 2 --hidden - - # 显示 A~B 列 - dws sheet update-dimension --node --sheet-id --dimension COLUMNS --start-index "A" --length 2 --hidden=false - - # 设置第 1~5 行行高为 40px - dws sheet update-dimension --node --sheet-id --dimension ROWS --start-index "1" --length 5 --pixel-size 40 - - # 设置 C 列列宽为 200px 并隐藏 - dws sheet update-dimension --node --sheet-id --dimension COLUMNS --start-index "C" --length 1 --pixel-size 200 --hidden - - # 使用工作表前缀(忽略 --sheet-id) - dws sheet update-dimension --node --sheet-id --dimension ROWS --start-index "Sheet1!3" --length 2 --hidden -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --dimension string 更新维度: ROWS 或 COLUMNS (必填) - --start-index string 起始位置,A1 表示法 (必填)。ROWS 时为行号如 "3";COLUMNS 时为列字母如 "A" - --length string 更新数量,正整数 (必填),最大 5000 - --hidden 是否隐藏 (true=隐藏, false=显示),与 --pixel-size 至少填其一 - --pixel-size int 行高或列宽(像素),ROWS 时为行高,COLUMNS 时为列宽,与 --hidden 至少填其一 -``` - -批量更新钉钉表格指定工作表中连续多行/多列的属性,支持设置显隐状态(hidden)与行高/列宽(pixelSize)。 -`--dimension ROWS` 时,`--start-index` 为 1-based 行号字符串;`--dimension COLUMNS` 时,`--start-index` 为列字母。 -支持在 `--start-index` 中携带工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id`。 -`--hidden` 与 `--pixel-size` 至少必须提供一个。当同时提供时,将先应用尺寸再应用显隐,任一失败整体失败。 -`--pixel-size` 单位为像素,`dimension=ROWS` 时表示行高、`dimension=COLUMNS` 时表示列宽。 - -### 合并单元格 -``` -Usage: - dws sheet merge-cells [flags] -Example: - # 合并所有单元格(默认) - dws sheet merge-cells --node --sheet-id --range "A1:B3" - - # 按行合并 - dws sheet merge-cells --node --sheet-id --range "A1:C3" --merge-type mergeRows - - # 按列合并 - dws sheet merge-cells --node --sheet-id --range "A1:C3" --merge-type mergeColumns - - # 使用带工作表前缀的范围(忽略 --sheet-id) - dws sheet merge-cells --node --sheet-id --range "Sheet1!A1:B3" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 目标单元格区域地址,如 A1:B3 (必填) - --merge-type string 合并方式: mergeAll(默认)/mergeRows/mergeColumns -``` - -支持三种合并方式: -- `mergeAll`(默认):合并所有单元格,将选定区域内的所有单元格合并成一个 -- `mergeRows`:按行合并,在选定区域内将同一行相邻的单元格合并 -- `mergeColumns`:按列合并,在选定区域内将同一列相邻的单元格合并 - -注意:合并时只保留左上角单元格的值,其他单元格的值会被丢弃。 -`--range` 支持带工作表前缀的写法(如 `Sheet1!A1:B3`),此时将优先使用前缀解析出的工作表,忽略 `--sheet-id`。 - -### 上传附件到表格 -``` -Usage: - dws sheet media-upload [flags] -Example: - dws sheet media-upload --node --file ./report.pdf - dws sheet media-upload --node --file ./data.bin --name "数据文件.dat" --mime-type application/octet-stream -Flags: - --node string 目标表格文档的标识,支持传入 URL 或 ID (必填) - --file string 本地文件路径 (必填) - --name string 附件显示名称 (默认使用文件名) - --mime-type string 文件 MIME 类型 (默认根据扩展名推断) -``` - -### 上传图片并写入表格单元格 -``` -Usage: - dws sheet write-image [flags] -Example: - dws sheet write-image --node --sheet-id --range A1:A1 --file ./chart.png - dws sheet write-image --node --sheet-id --range B2:B2 --file ./logo.png --width 200 --height 100 -Flags: - --node string 目标表格文档的标识,支持传入 URL 或 ID (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 目标单元格区域地址,如 A1:A1 (必填) - --file string 本地图片文件路径 (必填) - --name string 图片显示名称 (默认使用文件名) - --mime-type string 文件 MIME 类型 (默认根据扩展名推断) - --width int 图片显示宽度 (可选) - --height int 图片显示高度 (可选) -``` - -### 创建浮动图片 -``` -Usage: - dws sheet create-float-image [flags] -Example: - # 先上传图片获取 resourceUrl - dws sheet media-upload --node --file ./chart.png - # 输出: resourceUrl: /core/api/resources/img/xxxx... - - # 再创建浮动图片 - dws sheet create-float-image --node --sheet-id \ - --src "/core/api/resources/img/xxxx..." --range A1 --width 400 --height 300 - - # 带偏移量 - dws sheet create-float-image --node --sheet-id \ - --src "/core/api/resources/img/xxxx..." --range B2 --width 200 --height 150 --offset-x 10 --offset-y 20 -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --src string 图片资源路径,通过 media-upload 获取的 resourceUrl (必填) - --range string 锚点单元格,A1 表示法,如 A1、B3 (必填) - --width int 图片宽度,像素,正整数 (必填) - --height int 图片高度,像素,正整数 (必填) - --offset-x int 水平偏移量,像素 (默认 0) - --offset-y int 垂直偏移量,像素 (默认 0) -``` - -浮动图片悬浮于单元格之上,不占用单元格内容,可自由定位和调整大小。 -- `--src` 必须是 `media-upload` 返回的 `resourceUrl`(格式为 `/core/api/resources/img/...`),不能直接传外部 URL -- `--range` 使用 A1 表示法指定锚点单元格(如 `A1`、`B3`),支持带工作表前缀(如 `Sheet1!A1`) -- `--width` / `--height` 为必填,单位像素,必须为正整数 -- `--offset-x` / `--offset-y` 表示相对锚点单元格左上角的偏移量(像素),默认 0,不能为负数 - -### 获取浮动图片详情 -``` -Usage: - dws sheet get-float-image [flags] -Example: - dws sheet get-float-image --node --sheet-id --float-image-id -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --float-image-id string 浮动图片 ID (必填) -``` - -获取单个浮动图片的详细信息,包括 ID、图片资源路径、锚点位置、尺寸和偏移量。 -`--float-image-id` 可通过 `list-float-images` 获取。 - -### 列出工作表所有浮动图片 -``` -Usage: - dws sheet list-float-images [flags] -Example: - dws sheet list-float-images --node --sheet-id -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) -``` - -列出指定工作表中所有浮动图片,返回 `floatImages` 数组和 `totalCount`。 - -### 更新浮动图片属性 -``` -Usage: - dws sheet update-float-image [flags] -Example: - # 移动浮动图片到新位置 - dws sheet update-float-image --node --sheet-id --float-image-id --range C5 - - # 调整尺寸 - dws sheet update-float-image --node --sheet-id --float-image-id --width 600 --height 400 - - # 替换图片(需先 media-upload 新图片获取 resourceUrl) - dws sheet update-float-image --node --sheet-id --float-image-id \ - --src "/core/api/resources/img/xxxx..." -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --float-image-id string 浮动图片 ID (必填) - --src string 新的图片资源路径,通过 media-upload 获取的 resourceUrl - --range string 新的锚点单元格,A1 表示法 - --width int 新的图片宽度,像素 - --height int 新的图片高度,像素 - --offset-x int 新的水平偏移量,像素 - --offset-y int 新的垂直偏移量,像素 -``` - -更新浮动图片的属性,`--src` / `--range` / `--width` / `--height` / `--offset-x` / `--offset-y` 至少传入一个。 -`--float-image-id` 可通过 `list-float-images` 获取。 - -### 删除浮动图片 -``` -Usage: - dws sheet delete-float-image [flags] -Example: - dws sheet delete-float-image --node --sheet-id --float-image-id -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --float-image-id string 浮动图片 ID (必填) -``` - -删除指定的浮动图片,操作不可恢复。`--float-image-id` 可通过 `list-float-images` 获取。 - -### 全局查找替换 -``` -Usage: - dws sheet replace [flags] -Example: - dws sheet replace --node --sheet-id --find "旧文本" --replacement "新文本" - dws sheet replace --node --sheet-id --find "待处理" --replacement "已完成" --match-entire-cell - dws sheet replace --node --sheet-id --find "\\d{4}" --replacement "****" --use-regexp - dws sheet replace --node --sheet-id --find "旧" --replacement "新" --range "A1:D100" - dws sheet replace --node --sheet-id --find "临时" --replacement "" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --find string 查找文本 (必填) - --replacement string 替换文本 (必填,可为空字符串表示删除) - --range string 替换范围,A1 表示法 (如 A1:D100) - --match-case 区分大小写 (默认 false) - --match-entire-cell 完整单元格匹配 - --use-regexp 启用正则表达式匹配 - --include-hidden 包含隐藏行/列 -``` - -返回被替换的单元格数量。`--replacement` 可以为空字符串,表示删除匹配内容。 - -### 移动行或列 -``` -Usage: - dws sheet move-dimension [flags] -Example: - # 将第 2 行移动到第 5 行的位置(索引从 0 开始) - dws sheet move-dimension --node --sheet-id \ - --dimension ROWS --start-index 1 --end-index 1 --destination-index 4 - - # 将第 2~4 行移动到第 1 行之前 - dws sheet move-dimension --node --sheet-id \ - --dimension ROWS --start-index 1 --end-index 3 --destination-index 0 - - # 将 B~C 列移动到 E 列的位置 - dws sheet move-dimension --node --sheet-id \ - --dimension COLUMNS --start-index 1 --end-index 2 --destination-index 4 -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --dimension string 维度类型: ROWS 或 COLUMNS (必填) - --start-index int 源起始索引,0-based (必填) - --end-index int 源结束索引,0-based,包含 (必填) - --destination-index int 目标位置索引,0-based (必填) -``` +- probe 为 `axls` → 按 Reference 索引路由到对应命令 +- probe 为 xlsx/csv → 先 `dws drive download` 下载到本地,严禁调用 sheet 命令 -索引均为 0-based(第 1 行/列的索引为 0)。destination-index 不能在 [start-index, end-index] 范围内。 - -**destination-index 计算规则:** -destination-index 是目标位置的 0-based 索引,即移动到第 n 行/列则传 n-1: -- 通用公式:`destination-index = 目标行号(1-based) - 1` -- 例如:将第 2 行移到第 5 行位置 → `destination-index = 5 - 1 = 4`,即 `start-index=1, end-index=1, destination-index=4` -- 例如:将第 4 行移到第 1 行(最前面)→ `destination-index = 1 - 1 = 0`,即 `start-index=3, end-index=3, destination-index=0` - -### 追加空行或空列 -``` -Usage: - dws sheet add-dimension [flags] -Example: - dws sheet add-dimension --node --sheet-id --dimension ROWS --length 5 - dws sheet add-dimension --node --sheet-id --dimension COLUMNS --length 3 -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --dimension string 维度类型: ROWS 或 COLUMNS (必填) - --length int 追加数量,正整数,最多 5000 (必填) -``` - -在工作表末尾追加指定数量的空行或空列。 - -### 取消合并单元格 -``` -Usage: - dws sheet unmerge-cells [flags] -Example: - dws sheet unmerge-cells --node --sheet-id --range "A1:D5" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 取消合并的范围,A1 表示法 (必填) -``` - -取消指定范围内所有合并的单元格,恢复为独立单元格。 - -### 设置下拉列表 -``` -Usage: - dws sheet set-dropdown [flags] -Example: - # 设置单选下拉列表 - dws sheet set-dropdown --node --sheet-id --range "A2:A100" \ - --options '[{"value":"选项1"},{"value":"选项2"},{"value":"选项3"}]' - - # 设置带颜色的多选下拉列表 - dws sheet set-dropdown --node --sheet-id --range "B2:B50" \ - --options '[{"value":"高","color":"#ff0000"},{"value":"中","color":"#ffaa00"},{"value":"低","color":"#00ff00"}]' \ - --multi-select -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 目标单元格范围,A1 表示法,如 A2:A100 (必填) - --options string 下拉选项 JSON 数组 (必填),如 '[{"value":"选项1","color":"#ff0000"}]' - --multi-select 是否允许多选(默认单选) -``` - -在指定单元格范围内设置下拉列表。设置后用户可从预定义选项中选择值。 -- **用途**:为单元格配置下拉列表,支持自定义选项颜色和多选。 -- **场景**:规范数据输入,如状态选择(完成/进行中/待处理)、优先级(高/中/低)等。 -- **注意**:选项值不能包含英文逗号;如果目标范围已存在下拉列表,会被新配置覆盖。 - -### 获取下拉列表配置 -``` -Usage: - dws sheet get-dropdown [flags] -Example: - dws sheet get-dropdown --node --sheet-id --range "A2:A100" - dws sheet get-dropdown --node --sheet-id --range "A1" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 查询范围,A1 表示法,如 A1:A100 (必填) -``` - -查询指定范围内的下拉列表配置信息,包括选项值、颜色和是否多选。 -- **用途**:查看单元格已设置的下拉列表选项和配置。 -- **场景**:在修改下拉列表前先查询现有配置;确认下拉列表是否设置成功。 -- **返回**:`dataValidations` 数组,相同选项的单元格聚合为一组,每组包含 `conditionValues`(选项值)、`ranges`(覆盖范围)、`options`(含 `enableMultiSelect` 和 `colorValueMap`)。范围内无下拉列表时 `hasDropdown` 为 false。 - -### 删除下拉列表 -``` -Usage: - dws sheet delete-dropdown [flags] -Example: - dws sheet delete-dropdown --node --sheet-id --range "A2:A100" - dws sheet delete-dropdown --node --sheet-id --range "B1:D10" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 要删除下拉列表的范围,A1 表示法 (必填) -``` - -删除指定范围内的下拉列表配置,单元格恢复为普通文本格式。 -- **用途**:移除不再需要的下拉列表约束。 -- **注意**:已填写的单元格值不会被清除;目标范围不存在下拉列表时操作仍返回成功。 - -### 获取筛选信息 -``` -Usage: - dws sheet filter get [flags] -Example: - dws sheet filter get --node --sheet-id - dws sheet filter get --node "https://alidocs.dingtalk.com/i/nodes/" --sheet-id "Sheet1" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) -``` - -获取指定工作表的全局筛选信息,返回筛选范围和各列的筛选条件详情。 -- **用途**:查看当前工作表上是否存在全局筛选及其配置。 -- **场景**:在修改或删除筛选前,先读取当前筛选配置;创建筛选前先确认是否已存在(每个工作表只能有一个筛选)。 -- **区分**:全局筛选(filter)影响所有协作者看到的数据展示;筛选视图(filter-view)是个人化的。 -- **返回**:`range`(筛选范围,A1 表示法)和 `columnFilterCriteria`(各列条件,key 为列偏移量)。如果未设置筛选,返回筛选信息为空。 - -### 创建筛选 -``` -Usage: - dws sheet filter create [flags] -Example: - # 创建筛选框架(不设条件) - dws sheet filter create --node --sheet-id --range "A1:E100" - - # 创建筛选并同时设置条件(按值筛选) - dws sheet filter create --node --sheet-id --range "A1:E100" --criteria '[{"column":1,"filterType":"values","visibleValues":["北京","上海"]}]' - - # 创建筛选并设置条件筛选 - dws sheet filter create --node --sheet-id --range "A1:E100" --criteria '[{"column":2,"filterType":"condition","conditions":[{"operator":"greater","value":"100"}]}]' -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 筛选范围,A1 表示法,须包含表头行 (必填) - --criteria string 筛选条件 JSON 数组 (可选) -``` - -在工作表中创建全局筛选。 -- **用途**:为工作表建立筛选器,使数据可按条件过滤展示。 -- **约束**:每个工作表只能有一个全局筛选,已存在时会报错。应先 `filter get` 确认不存在后再创建。 -- **range 规范**:必须包含表头行(如 `A1:E100`),不能只包含数据行。 -- **criteria 格式**:JSON 数组,每个元素含 `column`(列偏移量,从 0 开始)和筛选条件字段。不传则仅创建空筛选框架,后续可通过 `filter update` 设置条件。 - -### 删除筛选 - -> **CAUTION:** 不可逆操作 — 执行前必须向用户确认。 - -``` -Usage: - dws sheet filter delete [flags] -Example: - dws sheet filter delete --node --sheet-id --yes -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) -``` - -删除工作表的全局筛选。 -- **用途**:移除筛选器,所有被隐藏的行将重新显示。 -- **不可逆**:删除后所有筛选条件丢失,需重新创建。 -- **前置**:工作表没有筛选时调用会报错,应先 `filter get` 确认存在。 - -### 批量更新筛选条件 -``` -Usage: - dws sheet filter update [flags] -Example: - # 同时设置多列的筛选条件 - dws sheet filter update --node --sheet-id --criteria '[{"column":0,"filterType":"values","visibleValues":["已完成","进行中"]},{"column":2,"filterType":"condition","conditions":[{"operator":"greater","value":"50"}]}]' - - # 按颜色筛选 - dws sheet filter update --node --sheet-id --criteria '[{"column":1,"filterType":"color","backgroundColor":"#FF0000"}]' -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --criteria string 筛选条件 JSON 数组 (必填) -``` - -批量更新筛选条件,可同时设置多列的筛选条件。 -- **用途**:一次性设置或替换多列的筛选条件。 -- **前置**:工作表必须已创建筛选(通过 `filter create`)。 -- **覆盖式**:指定列的条件会被替换,未指定的列保持不变。如只想修改某一列,建议先 `filter get` 读取现有配置。 -- **criteria 格式**:JSON 数组,支持三种 `filterType`: - - `values`:按值筛选,指定 `visibleValues` 数组 - - `condition`:按条件筛选,指定 `conditions` 数组(最多 2 个)和可选的 `conditionOperator`(`and`/`or`) - - `color`:按颜色筛选,指定 `backgroundColor` 或 `fontColor`(二选一) - -### 清除单列筛选条件 -``` -Usage: - dws sheet filter clear-criteria [flags] -Example: - # 清除第 2 列(B 列)的筛选条件 - dws sheet filter clear-criteria --node --sheet-id --column 1 - - # 清除第 1 列(A 列)的筛选条件 - dws sheet filter clear-criteria --node --sheet-id --column 0 -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --column number 列偏移量,从 0 开始 (必填) -``` - -清除筛选中某一列的筛选条件。 -- **用途**:移除某列的筛选条件,该列不再参与筛选计算。 -- **区分**:仅清除指定列的条件,不删除整个筛选。如需删除整个筛选,使用 `filter delete`。 -- **幂等**:指定列没有设置筛选条件时调用不会报错。 - -### 筛选排序 -``` -Usage: - dws sheet filter sort [flags] -Example: - # 按第 1 列(A 列)升序排序 - dws sheet filter sort --node --sheet-id --column 0 --ascending - - # 按第 3 列(C 列)降序排序 - dws sheet filter sort --node --sheet-id --column 2 --ascending=false -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --column number 排序列偏移量,从 0 开始 (必填) - --ascending 是否升序,默认 true (可选) -``` - -对筛选范围内的数据按指定列排序。 -- **用途**:对数据行按某一列的值进行升序或降序排列。 -- **前置**:工作表必须已创建筛选(通过 `filter create`)。 -- **注意**:排序会实际改变工作表中数据行的物理顺序,不可撤销。 -- **column**:列偏移量从 0 开始,相对于筛选范围首列。 - -### 获取所有筛选视图 -``` -Usage: - dws sheet filter-view list [flags] -Example: - dws sheet filter-view list --node --sheet-id - dws sheet filter-view list --node "https://alidocs.dingtalk.com/i/nodes/" --sheet-id "Sheet1" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) -``` - -获取指定工作表的所有筛选视图列表,返回每个筛选视图的 ID、名称和范围信息。 -- **用途**:查看当前工作表上已创建的所有筛选视图,获取视图 ID、名称和范围。 -- **场景**:在对筛选视图进行 update / delete / update-criteria 等操作前,先用 list 获取可用的 filterViewId。 -- **区分**:筛选视图(filter-view)是个人化的数据过滤方式,与全局筛选不同。每个用户可以创建自己的筛选视图,互不影响原始数据。如果没有筛选视图,返回空列表。 - -### 创建筛选视图 -``` -Usage: - dws sheet filter-view create [flags] -Example: - # 创建不带筛选条件的筛选视图 - dws sheet filter-view create --node --sheet-id --name "我的视图" --range "A1:E10" - - # 创建带按值筛选条件的筛选视图 - dws sheet filter-view create --node --sheet-id --name "销售筛选" --range "A1:E10" \ - --criteria '[{"column":0,"filterType":"values","visibleValues":["销售部"]}]' - - # 创建带按条件筛选的筛选视图(大于等于 200000) - dws sheet filter-view create --node --sheet-id --name "高预算" --range "A1:C10" \ - --criteria '[{"column":1,"filterType":"condition","conditions":[{"operator":"greater-equal","value":"200000"}]}]' -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --name string 筛选视图名称 (必填) - --range string 筛选视图范围,A1 表示法,如 A1:E10 (必填) - --criteria string 筛选条件,JSON 数组 (可选) -``` - -在指定工作表中创建一个筛选视图。 -- **用途**:为指定数据区域创建一个可命名的个人化筛选视图,可选同时设置筛选条件。 -- **场景**:用户需要针对某个数据区域建立固定的筛选视角(如"高绩效员工""研发部数据"),方便反复查看。 -- **区分**:与全局筛选不同,筛选视图是个人化的,不影响其他用户看到的数据。如果只需创建视图不设条件,后续可通过 `update-criteria` 单独设置;如果要一步到位,可通过 `--criteria` 在创建时直接设置。 -`--criteria` 为 JSON 数组,每个元素包含 `column`(列偏移量,从 0 开始)和筛选条件字段。支持三种筛选类型: -- `values`:按值筛选,通过 `visibleValues` 指定允许显示的值列表 -- `condition`:按条件筛选,通过 `conditions` 指定条件列表(最多 2 个),每个条件包含 `operator` 和 `value`。支持的操作符(kebab-case):`equal`、`not-equal`、`contains`、`not-contains`、`starts-with`、`not-starts-with`、`ends-with`、`not-ends-with`、`greater`、`greater-equal`、`less`、`less-equal`。多条件之间通过 `conditionOperator` 指定逻辑关系:`and`(且,默认)或 `or`(或) -- `color`:按颜色筛选,通过 `backgroundColor` 或 `fontColor` 指定颜色值(十六进制,如 `#FF0000`),二选一 - -### 更新筛选视图属性 -``` -Usage: - dws sheet filter-view update [flags] -Example: - # 更新筛选视图名称 - dws sheet filter-view update --node --sheet-id --filter-view-id --name "新名称" - - # 更新筛选视图范围 - dws sheet filter-view update --node --sheet-id --filter-view-id --range "A1:F20" - - # 更新筛选条件 - dws sheet filter-view update --node --sheet-id --filter-view-id \ - --criteria '[{"column":1,"filterType":"condition","conditions":[{"operator":"greater","value":"100"}]}]' -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --filter-view-id string 筛选视图 ID (必填) - --name string 筛选视图新名称 - --range string 筛选视图新范围,A1 表示法 - --criteria string 筛选条件,JSON 数组 -``` - -更新筛选视图的名称、范围和/或筛选条件,`--name`、`--range`、`--criteria` 至少传入一个。 -- **用途**:修改已有筛选视图的名称、数据范围或筛选条件。 -- **场景**:数据区域扩展后需要扩大筛选视图范围,或重命名视图,或通过 `--criteria` 一次性批量更新多列筛选条件。 -- **区分**:`update` 可同时修改名称、范围和条件,适合批量更新;`update-criteria` 只能设置单列条件,适合精确控制某一列的筛选逻辑。`--criteria` 指定列的条件会被替换,未指定的列保持不变。 - -`--criteria` 为 JSON 数组,格式与 `filter-view create` 的 `--criteria` 相同,支持的筛选类型和操作符参见「创建筛选视图」说明。 - -### 删除筛选视图 - -> **CAUTION:** 不可逆操作 — 执行前必须向用户确认。 - -``` -Usage: - dws sheet filter-view delete [flags] -Example: - dws sheet filter-view delete --node --sheet-id --filter-view-id -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --filter-view-id string 筛选视图 ID (必填) -``` - -删除指定的筛选视图。 -- **用途**:永久删除一个不再需要的筛选视图及其所有筛选条件。 -- **场景**:筛选视图已过时或不再需要时,清理无用的视图。 -- **区分**:`delete` 删除整个筛选视图(包括所有列的条件),操作不可恢复;`delete-criteria` 只删除某一列的筛选条件,视图本身保留。此操作不影响全局筛选或其他筛选视图,也不影响原始数据。 - -### 更新筛选视图列条件 -``` -Usage: - dws sheet filter-view update-criteria [flags] -Example: - # 按值筛选:只显示"销售部"和"市场部" - dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ - --column 0 --filter-criteria '{"filterType":"values","visibleValues":["销售部","市场部"]}' - - # 按条件筛选:大于 100 - dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ - --column 2 --filter-criteria '{"filterType":"condition","conditions":[{"operator":"greater","value":"100"}]}' - - # 按条件筛选:大于等于 200000 - dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ - --column 1 --filter-criteria '{"filterType":"condition","conditions":[{"operator":"greater-equal","value":"200000"}]}' - - # 按条件筛选:小于 100 - dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ - --column 1 --filter-criteria '{"filterType":"condition","conditions":[{"operator":"less","value":"100"}]}' - - # 多条件筛选:大于等于 60 且 小于等于 90 - dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ - --column 2 --filter-criteria '{"filterType":"condition","conditionOperator":"and","conditions":[{"operator":"greater-equal","value":"60"},{"operator":"less-equal","value":"90"}]}' - - # 按颜色筛选:背景色为红色 - dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ - --column 1 --filter-criteria '{"filterType":"color","backgroundColor":"#FF0000"}' -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --filter-view-id string 筛选视图 ID (必填) - --column int 列偏移量,从 0 开始 (必填) - --filter-criteria string 筛选条件,JSON 对象 (必填) -``` - -更新筛选视图中某一列的筛选条件。 -- **用途**:为筛选视图的指定列创建或更新筛选条件,控制该列哪些数据行可见。 -- **场景**:只显示某些特定值的行(如"只看研发部")→ `filterType: values`;按数值条件筛选(如"绩效 ≥ 85")→ `filterType: condition` + `operator: greater-equal`;按文本条件筛选(如"名称包含关键字")→ `filterType: condition` + `operator: contains`。 -- **区分**:`update-criteria` 精确控制单列条件,适合逐列设置不同的筛选逻辑;`filter-view update --criteria` 可以批量更新多列条件;`delete-criteria` 是 `update-criteria` 的逆操作,删除指定列的条件。 - -`--column` 为列偏移量(从 0 开始),相对于筛选视图范围首列。 -例如筛选视图范围为 `B1:E10`,则 `--column 0` 代表 B 列,`--column 1` 代表 C 列。 - -`--filter-criteria` 为 JSON 对象,支持三种筛选类型: -- `values`:按值筛选,通过 `visibleValues` 指定允许显示的值列表 -- `condition`:按条件筛选,通过 `conditions` 指定条件列表(最多 2 个),每个条件包含 `operator` 和 `value`。支持的操作符:`equal`、`not-equal`、`contains`、`not-contains`、`starts-with`、`not-starts-with`、`ends-with`、`not-ends-with`、`greater`、`greater-equal`、`less`、`less-equal`。多条件之间通过 `conditionOperator` 指定逻辑关系:`and`(且,默认)或 `or`(或) -- `color`:按颜色筛选,通过 `backgroundColor` 或 `fontColor` 指定颜色值(十六进制,如 `#FF0000`),二选一 - -### 删除筛选视图列条件 -``` -Usage: - dws sheet filter-view delete-criteria [flags] -Example: - # 删除第 1 列(A 列)的筛选条件 - dws sheet filter-view delete-criteria --node --sheet-id --filter-view-id --column 0 - - # 删除第 3 列(C 列)的筛选条件 - dws sheet filter-view delete-criteria --node --sheet-id --filter-view-id --column 2 -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --filter-view-id string 筛选视图 ID (必填) - --column int 列偏移量,从 0 开始 (必填) -``` - -清除筛选视图中指定列的筛选条件。 -- **用途**:移除筛选视图中指定列的筛选条件,使该列不再参与过滤。 -- **场景**:之前通过 `update-criteria` 设置了某列的筛选条件,现在需要取消该列的筛选以显示全部数据。 -- **区分**:`delete-criteria` 只清除指定列的条件,筛选视图本身和其他列的条件保持不变;`delete` 会删除整个筛选视图。如果指定列没有设置筛选条件,调用此命令不会报错(幂等操作)。 - -### 获取单个筛选视图详情 -``` -Usage: - dws sheet filter-view info [flags] -Example: - # 查看指定筛选视图的详情 - dws sheet filter-view info --node --sheet-id --filter-view-id -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --filter-view-id string 筛选视图 ID (必填) -``` - -获取指定筛选视图的完整信息,包括 ID、名称、范围和筛选条件。 -- **用途**:查看某个筛选视图的当前配置,包括已设置的所有筛选条件详情。 -- **场景**:在修改或删除筛选视图前,先确认其当前状态;或在 `update-criteria` 后验证条件是否生效。 -- **区分**:`info` 返回单个视图的完整信息(含 criteria);`list` 返回所有视图的列表概要。`info` 需要指定 `--filter-view-id`,ID 可通过 `list` 获取。 -- **实现**:内部调用 `get_filter_views` 获取全部列表后按 ID 过滤。 - -### 列出筛选视图所有列条件 -``` -Usage: - dws sheet filter-view list-criteria [flags] -Example: - # 列出筛选视图的所有条件 - dws sheet filter-view list-criteria --node --sheet-id --filter-view-id -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --filter-view-id string 筛选视图 ID (必填) -``` - -列出指定筛选视图中已设置的所有列筛选条件。 -- **用途**:查看某个筛选视图当前设置了哪些列的筛选条件,包括每列的条件类型和具体规则。 -- **场景**:在管理筛选条件(修改/删除特定列条件)前,先了解当前视图有哪些条件;或排查筛选结果不符合预期时检查条件配置。 -- **区分**:`list-criteria` 返回所有列的条件(按列偏移量为 key 的对象);`get-criteria` 只返回指定列的条件。如果没有设置任何条件,返回空对象 `{}`。 -- **实现**:内部调用 `get_filter_views` 获取视图详情后提取 `criteria` 字段。 - -### 获取单列筛选条件 -``` -Usage: - dws sheet filter-view get-criteria [flags] -Example: - # 查看第 1 列(偏移量 0)的筛选条件 - dws sheet filter-view get-criteria --node --sheet-id --filter-view-id --column 0 - - # 查看第 3 列(偏移量 2)的筛选条件 - dws sheet filter-view get-criteria --node --sheet-id --filter-view-id --column 2 -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --filter-view-id string 筛选视图 ID (必填) - --column int 列偏移量,从 0 开始 (必填) -``` - -获取指定筛选视图中某一列的筛选条件详情。 -- **用途**:查看某个筛选视图中指定列当前设置的筛选条件,包括条件类型、运算符和比较值。 -- **场景**:在修改某列条件前,先查看其当前配置;或验证 `update-criteria` 后该列条件是否正确。 -- **区分**:`get-criteria` 只返回指定列的条件;`list-criteria` 返回所有列的条件。`--column` 为列偏移量(从 0 开始),相对于筛选视图范围首列。 -- **实现**:内部调用 `get_filter_views` 获取视图详情后按列偏移量过滤 `criteria` 中的对应条件。 - -### 导出表格为 xlsx(异步任务一站式) -``` -Usage: - dws sheet export [flags] # 一站式:提交 → 轮询 → 可选下载 -Example: - # 仅导出,返回 downloadUrl(链接有时效性,请尽快下载) - dws sheet export --node - dws sheet export --node "https://alidocs.dingtalk.com/i/nodes/" - - # 导出并自动下载为本地文件 - dws sheet export --node --output ./report.xlsx - - # --output 为目录时,自动按下载链接中的文件名保存 - dws sheet export --node --output ./ - -Flags: - --node string 表格文档 ID 或 URL (必填) - --output string 本地保存路径(可选,支持文件路径或目录) -``` - -将钉钉在线电子表格导出为 Office xlsx 格式。**单命令一站式**:命令内部自动完成「提交任务 → 渐进式退避轮询 → (可选)下载文件」全流程,AI Agent 无需自行拆分步骤或实现轮询。 - -**内部流程**: -1. 调 `submit_export_job` 获取 `jobId` -2. 按渐进式退避策略轮询 `query_export_job` 直至任务终态或超时 -3. 任务成功后取得 `downloadUrl`;若指定了 `--output`,自动 HTTP GET 下载 xlsx 到本地文件 - -**内置轮询策略(CLI 内实现,无需关心)**: -- 第 1~5 次:每次间隔 2 秒 -- 第 6~10 次:每次间隔 5 秒 -- 第 11~20 次:每次间隔 10 秒 -- 第 21~30 次:每次间隔 15 秒 -- **硬上限:最多轮询 30 次(约 5 分钟)**,超时后命令返回错误 - -**命令返回**: -- `--output` 未指定:进度日志 + 末尾输出 `jobId` 和 `downloadUrl`(链接有时效性,请尽快下载) -- `--output` 指定为文件路径:下载到该路径并输出 `导出完成: ` -- `--output` 指定为已存在目录:自动从 `downloadUrl` 推断文件名并保存到该目录下 - -**失败处理(命令内部已处理,Agent 仅需转述)**: -- MCP 返回 `FAILED`:命令立即返回错误并附带失败原因,**禁止自动重试 `dws sheet export`**,告知用户稍后再试 -- 轮询 30 次仍 `PROCESSING`:命令返回超时错误,告知用户稍后再试 - -**限制**:仅支持钉钉在线电子表格(alxs)→ xlsx。导出钉钉文字文档请使用 `doc` 产品对应的导出工具。 - -## 核心工作流 - -```bash -# ── 工作流 1: 创建表格并写入数据 ── - -# 1. 创建表格文档 — 提取 nodeId -dws sheet create --name "销售数据" --format json - -# 2. 查看工作表列表 — 提取 sheetId -dws sheet list --node --format json - -# 3. 写入表头和数据 -dws sheet range update --node --sheet-id --range "A1:C1" \ - --values '[["姓名","部门","销售额"]]' --format json - -dws sheet range update --node --sheet-id --range "A2:C4" \ - --values '[["张三","销售部",50000],["李四","市场部",38000],["王五","销售部",62000]]' --format json - -# ── 工作流 2: 读取已有表格数据 ── - -# 1. 获取工作表列表 -dws sheet list --node --format json - -# 2. 查看工作表详情(行列数、最后非空位置等) -dws sheet info --node --sheet-id --format json - -# 3. 读取全部数据 -dws sheet range read --node --sheet-id --format json - -# 4. 读取指定区域 -dws sheet range read --node --sheet-id --range "A1:D10" --format json - -# ── 工作流 3: 多工作表管理 ── - -# 1. 新建工作表 -dws sheet new --node --name "汇总" --format json - -# 2. 在新工作表中写入汇总公式 -dws sheet range update --node --sheet-id --range "A1:B1" \ - --values '[["指标","数值"]]' --format json - -dws sheet range update --node --sheet-id --range "A2:B2" \ - --values '[["总销售额","=SUM(Sheet1!C2:C100)"]]' --format json - -# ── 工作流 4: 写入数据并设置样式 ── - -# 1. 写入数据 -dws sheet range update --node --sheet-id --range "A1:C3" \ - --values '[["商品","单价","数量"],["苹果",5.5,100],["香蕉",3.2,200]]' --format json - -# 2. 设置数字格式(人民币)——请走 set-style,不要放到 range update -dws sheet range set-style --node --sheet-id --range "B2:B3" \ - --number-format "¥#,##0.00" --format json - -# 3. 写入超链接 -dws sheet range update --node --sheet-id --range "D1" \ - --hyperlinks '[[{"type":"path","link":"https://dingtalk.com","text":"详情"}]]' --format json - -# ── 工作流 5: 追加数据 ── - -# 1. 获取工作表列表 -dws sheet list --node --format json - -# 2. 查看工作表详情(确认列结构) -dws sheet info --node --sheet-id --format json - -# 3. 追加单行数据 -dws sheet append --node --sheet-id \ - --values '[["张三","销售部",50000]]' --format json - -# 4. 追加多行数据 -dws sheet append --node --sheet-id \ - --values '[["李四","市场部",38000],["王五","销售部",62000]]' --format json -``` - -```bash -# ── 工作流 6: 插入行或列 ── - -# 1. 获取工作表列表 -dws sheet list --node --format json - -# 2. 在第 3 行之前插入 2 行 -dws sheet insert-dimension --node --sheet-id \ - --dimension ROWS --position "3" --length 2 --format json - -# 3. 在 A 列之前插入 1 列 -dws sheet insert-dimension --node --sheet-id \ - --dimension COLUMNS --position "A" --length 1 --format json - -# 4. 使用工作表前缀指定位置 -dws sheet insert-dimension --node --sheet-id \ - --dimension ROWS --position "Sheet1!5" --length 3 --format json -``` - -```bash -# ── 工作流 6b: 删除行或列 ── - -# 1. 获取工作表列表 -dws sheet list --node --format json - -# 2. 从第 3 行开始删除 2 行 -dws sheet delete-dimension --node --sheet-id \ - --dimension ROWS --position "3" --length 2 --format json - -# 3. 从 A 列开始删除 1 列 -dws sheet delete-dimension --node --sheet-id \ - --dimension COLUMNS --position "A" --length 1 --format json - -# 4. 使用工作表前缀指定位置 -dws sheet delete-dimension --node --sheet-id \ - --dimension ROWS --position "Sheet1!5" --length 3 --format json -``` - -```bash -# ── 工作流 6c: 更新行/列属性(显隐、行高/列宽) ── - -# 1. 获取工作表列表 -dws sheet list --node --format json - -# 2. 隐藏第 3~4 行 -dws sheet update-dimension --node --sheet-id \ - --dimension ROWS --start-index "3" --length 2 --hidden --format json - -# 3. 显示 A~B 列 -dws sheet update-dimension --node --sheet-id \ - --dimension COLUMNS --start-index "A" --length 2 --hidden=false --format json - -# 4. 设置第 1~5 行行高为 40px -dws sheet update-dimension --node --sheet-id \ - --dimension ROWS --start-index "1" --length 5 --pixel-size 40 --format json - -# 5. 设置 C 列列宽为 200px 并隐藏 -dws sheet update-dimension --node --sheet-id \ - --dimension COLUMNS --start-index "C" --length 1 --pixel-size 200 --hidden --format json -``` - -```bash -# ── 工作流 7: 搜索表格数据 ── - -# 1. 获取工作表列表 -dws sheet list --node --format json - -# 2. 基本搜索 — 在指定工作表中查找文本 -dws sheet find --node --sheet-id --find "销售额" --format json - -# 3. 在指定范围内搜索 -dws sheet find --node --sheet-id --find "合计" --range "A1:D100" --format json - -# 4. 正则搜索(不区分大小写) -dws sheet find --node --sheet-id --find "^total" --use-regexp --match-case=false --format json - -# 5. 精确匹配整个单元格 -dws sheet find --node --sheet-id --find "完成" --match-entire-cell --format json - -# 6. 搜索公式文本 -dws sheet find --node --sheet-id --find "SUM" --match-formula --format json -``` - -```bash -# ── 工作流 8: 合并单元格 ── - -# 1. 获取工作表列表 -dws sheet list --node --format json - -# 2. 合并所有单元格(默认 mergeAll) -dws sheet merge-cells --node --sheet-id --range "A1:B3" --format json - -# 3. 按行合并 -dws sheet merge-cells --node --sheet-id --range "A1:C3" --merge-type mergeRows --format json - -# 4. 按列合并 -dws sheet merge-cells --node --sheet-id --range "A1:C3" --merge-type mergeColumns --format json -``` - -```bash -# ── 工作流 9: 上传附件到表格 ── - -# 1. 基本用法: 上传本地文件到表格 -dws sheet media-upload --node --file ./report.pdf -f json - -# 2. 自定义附件显示名称 (--name 指定上传后在表格中显示的名称) -dws sheet media-upload --node --file ./data.csv --name "销售数据.csv" -f json - -# 3. 指定 MIME 类型 (文件扩展名无法推断时) -dws sheet media-upload --node --file ./data.bin --name "导出数据.dat" --mime-type application/octet-stream -f json - -# 4. 完整流程: 创建表格 → 上传附件 -dws sheet create --name "项目资料" -f json -# 提取 nodeId 后: -dws sheet media-upload --node --file ./design.pdf -f json -dws sheet media-upload --node --file ./timeline.xlsx --name "项目时间线.xlsx" -f json - -# ── 工作流 10: 写入图片到表格单元格 ── - -# 1. 基本用法: 写入图片到指定单元格 -dws sheet write-image --node --sheet-id --range A1:A1 --file ./chart.png -f json - -# 2. 指定显示尺寸 -dws sheet write-image --node --sheet-id --range B2:B2 --file ./logo.png --width 200 --height 100 -f json - -# 3. 自定义图片名称 -dws sheet write-image --node --sheet-id --range C3:C3 --file ./photo.jpg --name "产品图.jpg" -f json - -# 4. 完整流程: 创建表格 → 写表头 → 写入图片 -dws sheet create --name "产品目录" -f json -# 提取 nodeId 后: -dws sheet range update --node --sheet-id Sheet1 --range "A1:B1" --values '[["产品名称","产品图片"]]' -f json -dws sheet range update --node --sheet-id Sheet1 --range "A2:A2" --values '[["MacBook Pro"]]' -f json -dws sheet write-image --node --sheet-id Sheet1 --range B2:B2 --file ./macbook.png --width 150 --height 100 -f json -``` - -```bash -# ── 工作流 11: 筛选视图管理 ── - -# 1. 获取工作表列表 -dws sheet list --node -f json - -# 2. 查看已有筛选视图 -dws sheet filter-view list --node --sheet-id -f json - -# 3. 创建筛选视图(不带条件) -dws sheet filter-view create --node --sheet-id \ - --name "我的筛选" --range "A1:E100" -f json - -# 4. 为筛选视图设置列条件(按值筛选) -dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ - --column 0 --filter-criteria '{"filterType":"values","visibleValues":["销售部","市场部"]}' -f json - -# 5. 为筛选视图设置列条件(按条件筛选) -dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ - --column 2 --filter-criteria '{"filterType":"condition","conditions":[{"operator":"greater","value":"100"}]}' -f json - -# 6. 更新筛选视图名称和范围 -dws sheet filter-view update --node --sheet-id --filter-view-id \ - --name "销售数据筛选" --range "A1:F200" -f json - -# 7. 清除某列的筛选条件 -dws sheet filter-view delete-criteria --node --sheet-id --filter-view-id \ - --column 0 -f json - -# 8. 删除筛选视图 -dws sheet filter-view delete --node --sheet-id --filter-view-id -f json -``` +## 导入本地表格 +用户说"导入Excel/把xlsx转为在线表格/上传表格并在线编辑"时: ```bash -# ── 工作流 11b: 创建带条件的筛选视图(一步完成) ── +# 上传并转换为在线电子表格(转换后返回 nodeId,可用 sheet 命令操作) +dws drive upload --file ./data.xlsx --convert -# 创建筛选视图时直接指定筛选条件 -dws sheet filter-view create --node --sheet-id \ - --name "高销售额视图" --range "A1:E100" \ - --criteria '[{"column":0,"filterType":"values","visibleValues":["销售部"]},{"column":2,"filterType":"condition","conditions":[{"operator":"greater","value":"50000"}]}]' \ - -f json -``` - -```bash -# ── 工作流 12: 导出表格为 xlsx(单命令一站式)── - -# 场景 A:仅获取下载链接(命令内部自动完成提交+轮询,最终返回 downloadUrl) -dws sheet export --node --format json -# 传入 URL 也可: -# dws sheet export --node "https://alidocs.dingtalk.com/i/nodes/" --format json +# 指定上传到某个文件夹 +dws drive upload --file ./data.xlsx --folder --convert -# 场景 B:导出并自动下载为本地文件 -dws sheet export --node --output ./report.xlsx - -# 场景 C:下载到目录,自动按链接推断文件名 -dws sheet export --node --output ./ - -# 禁止在 Agent 侧实现任何轮询或重试,CLI 内部已按 2s/5s/10s/15s 渐进式退避自动完成(最多 30 次)。 -# 若命令返回失败或超时,直接告知用户稍后再试,不要自动重调 dws sheet export。 +# 指定上传到知识库 +dws drive upload --file ./data.xlsx --workspace --convert ``` +- `--convert` 是关键参数,不加则仅上传为附件,不会转换为在线电子表格 +- 转换后的文档为 `axls` 格式,可用 `sheet` 全部命令操作 +- 支持 `.xlsx` / `.xls` / `.csv` 等格式 -## 上下文传递表 - -| 操作 | 从返回中提取 | 用于 | -|------|-------------|------| -| `create` | `nodeId` | list / info / new / range read / range update / find 的 --node | -| `list` | 工作表的 `sheetId` | info / range read / range update / find 的 --sheet-id | -| `new` | 新工作表的 `sheetId` | range read / range update / find 的 --sheet-id | -| `info` | `rowCount` / `lastNonEmptyRow` | 确定数据范围、追加写入起始行 | -| `find` | `matchedCells` 中的 `a1Notation` | 定位目标单元格,用于 range read / range update | -| `append` | `a1Notation` 追加数据所在范围 | 确认追加位置 | -| `csv-put` | `a1Notation` 实际写入的单元格范围 | 确认写入位置和范围 | -| `insert-dimension` | `a1Notation` 新插入区域范围 | 确认插入位置和范围 | -| `delete-dimension` | `a1Notation` 被删除区域范围 | 确认删除位置和范围 | -| `update-dimension` | `a1Notation` 被更新区域范围、`hidden` 生效的显隐状态、`pixelSize` 生效的尺寸 | 确认更新结果 | -| `merge-cells` | `a1Notation` 实际被合并的范围、`mergeType` 生效的合并方式 | 确认合并结果 | -| `media-upload` | `resourceId`、`resourceUrl` | 附件已上传到表格;`resourceUrl` 可用于 `create-float-image` 的 `--src` | -| `write-image` | `resourceId` | 图片已写入指定单元格 | -| `create-float-image` | `floatImage`(含 `id`、`src`、`range`、`width`、`height`、`offsetX`、`offsetY`) | `id` 用于后续 get / update / delete 的 `--float-image-id` | -| `get-float-image` | `floatImage`(完整信息) | 查看单个浮动图片详情 | -| `list-float-images` | `floatImages` 数组、`totalCount` | 获取所有浮动图片的 `id`,用于后续操作 | -| `update-float-image` | `floatImage`(更新后的完整信息) | 确认更新结果 | -| `delete-float-image` | `message` | 确认删除完成 | -| `replace` | `replaceCount` 被替换的单元格数量 | 确认替换结果 | -| `move-dimension` | `sheetId` 工作表 ID | 确认操作完成 | -| `add-dimension` | `sheetId` 工作表 ID | 确认操作完成 | -| `unmerge-cells` | `sheetId` 工作表 ID | 确认操作完成 | -| `set-dropdown` | `range` 实际设置范围、`optionCount` 选项数量、`enableMultiSelect` 是否多选 | 确认下拉列表设置成功 | -| `get-dropdown` | `hasDropdown` 是否存在下拉、`dataValidations` 下拉配置列表(含 `conditionValues`、`ranges`、`options`) | 查看已有下拉配置 | -| `delete-dropdown` | `range` 实际删除范围 | 确认下拉列表删除完成 | -| `filter-view list` | `filterViews` 筛选视图列表(含 `id`、`name`、`range`) | 获取 filterViewId 用于 info / update / delete / update-criteria / delete-criteria / list-criteria / get-criteria | -| `filter-view info` | `id`、`name`、`range`、`criteria` | 查看单个视图完整配置,确认条件是否生效 | -| `filter-view create` | `id` 筛选视图 ID、`name`、`range` | 用于后续 update / delete / update-criteria / delete-criteria 的 --filter-view-id | -| `filter-view update` | `id`、`name`、`range`、`criteria` | 确认更新结果 | -| `filter-view delete` | `id` 被删除的筛选视图 ID | 确认删除完成 | -| `filter-view update-criteria` | `id` 筛选视图 ID | 确认条件设置完成 | -| `filter-view delete-criteria` | `id` 筛选视图 ID | 确认条件清除完成 | -| `filter-view list-criteria` | 所有列条件(按列偏移量为 key 的对象) | 了解当前视图已设置哪些列的条件 | -| `filter-view get-criteria` | 指定列的条件详情(`filterType`、`conditions` 等) | 查看某列的具体筛选规则 | -| `export` | `downloadUrl`(未指定 --output)/ `导出完成: `(指定 --output) | 直接下发给用户或告知文件已保存到本地。命令内部已完成轮询,不要再调用其他 export 相关命令 | - -## nodeId 多格式说明 - -所有 `--node` 参数同时支持文档 ID、文档 URL、表格分享链接,系统自动识别。详细格式和提取规则请参见前文「URL 识别与 NODE_ID 提取」章节。 - -> ** 禁止使用 `dentryId`**:`drive list` 返回结果中同时包含 `dentryId`(纯数字,如 `218595998810`)和 `fileId`(UUID 格式,如 `ZgpG2NdyVXYOR2D5UGDok65MJMwvDqPk`)两个字段。sheet 的 `--node` 和 `--folder` 参数**只能使用 `fileId`(UUID 格式)**,不能使用纯数字的 `dentryId`,传入纯数字会导致命令失败。 - -## values 参数格式说明 - -`--values` 为二维 JSON 数组,第一维为行,第二维为列: -- 字符串值: `"文本"` -- 数字值: `100` 或 `3.14` -- 公式: `"=SUM(B2:B4)"`(以 `=` 开头的字符串自动识别为公式) -- 清空单元格: 统一使用空字符串 `""`(不要用 `null` 取代,null 不会保留原值且全 null 会被视为无效调用跳过) - -维度必须与 `--range` 范围一致,例如 `--range "A1:B3"` 需要 3 行 2 列的数组。 - -## hyperlinks 参数格式说明 - -`--hyperlinks` 为二维 JSON 数组,每个元素为对象或 null: -- `type`: 链接类型,可选 `path`(外部链接)、`sheet`(工作表跳转)、`range`(单元格跳转) -- `link`: 链接地址 -- `text`: 显示文本 - -与 `--values` 共存时,hyperlinks 优先级更高。 - -## number-format 常用值 - -适用范围:`number-format` 仅在 `range set-style` / `range batch-set-style` 中接受(CLI 对应 `--number-format`,batch 配置文件对应 `numberFormat`);`range update` 不接受该参数。 - -| 格式代码 | 说明 | 示例 | -|----------|------|------| -| `General` | 常规 | 1234.5 | -| `@` | 文本 | 001234 | -| `#,##0` | 整数千分位 | 1,235 | -| `#,##0.00` | 两位小数 | 1,234.50 | -| `0%` | 百分比 | 85% | -| `yyyy/m/d` | 日期 | 2026/3/15 | -| `hh:mm:ss` | 时间 | 14:30:00 | -| `¥#,##0` | 人民币 | ¥1,235 | - -## 注意事项(完整版) - -> 标 ★ 的条目已在前文「关键注意事项」中列出,此处为完整说明。 +## nodeId 说明 -- ★ `--sheet-id` 获取规范(强制):所有涉及 `--sheet-id` 参数的命令(`info` / `new` / `range read` / `range update` / `find` / `append` / `insert-dimension` / `delete-dimension` / `update-dimension` / `move-dimension` / `add-dimension` / `merge-cells` / `unmerge-cells` / `replace` / `write-image` / `set-dropdown` / `get-dropdown` / `delete-dropdown` / `filter-view *` 等),除非用户主动提供了工作表 ID 或工作表名称,否则在 `sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询真实的 `sheetId` / 工作表名称后再调用,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等);用户仅给出工作表名称时,也应通过 `list` 校验该名称是否存在,避免名称大小写或拼写不一致导致失败 -- ★ `range update` 维度校验(强制):调用 `range update` 写入 `--values` 或 `--hyperlinks` 时,必须严格校验二维 JSON 数组的行数与列数与 `--range` 指定的范围完全一致: - - 例如 `--range "A1:C3"` 表示 3 行 × 3 列,`--values` 必须是 `[[v1,v2,v3],[v4,v5,v6],[v7,v8,v9]]` 这样 3×3 的数组 - - `--range "A1"` 表示 1 行 × 1 列,`--values` 必须是 `[[v]]` - - 行数不足需要用空字符串补齐,列数不足需要补齐到每行相同长度;禁止出现各行列数不一致或与 `--range` 不匹配的情况,否则调用会直接报错 - - 同时传入 `--values` 和 `--hyperlinks` 时,两个二维数组的行列数都必须与 `--range` 严格一致 -- ★ `range update` 清空单元格规范(强制):如需清空单元格内容,统一使用空字符串 `""`。禁止使用 `null`:`null` 不会保留单元格原值,也不存在"选择性保留"场景;且若 `--values` 全部为 `null`,整体调用会被视为无效而跳过,无任何写入效果 -- `create` 不传 `--folder` 和 `--workspace` 时,默认创建在"我的文档"根目录 -- `list` 返回所有工作表的 ID 和名称,是后续操作的必要前置步骤 -- `info` 不传 `--sheet-id` 时默认返回第一个工作表的详情 -- `range read` 不传 `--range` 时默认读取整个工作表的全部非空数据 -- `range read` 的 `--range` 支持 `Sheet1!A1:D10` 格式直接指定工作表(此时忽略 `--sheet-id`) -- `range read` 遇到超时或响应过慢时,应缩小 `--range` 查询范围,**单次读取的单元格数量建议控制在 5000 个以内**;数据量较大时通过 `info` 获取边界后分批读取,避免不传 `--range` 直接读取整个大工作表 -- `range update` 的 `--values` 和 `--hyperlinks` 至少传入一项 -- `range update` 职责边界:`range update` 仅写入单元格的值与超链接,不接受任何样式参数(包括但不限于数字格式 / 背景色 / 字体 / 对齐方式等)。如需设置数字格式,请使用 `dws sheet range set-style --number-format <格式代码>`;批量场景走 `dws sheet range batch-set-style --batch `(配置文件中使用 `numberFormat` 字段)。不要在同一次 `range update` 调用里同时完成写值与格式设置 -- ★ `range update` / `range set-style` / `range batch-set-style` 单次调用上限(强制):行数 ≤ 1000,单元格总数(行×列)建议≤ 5000(底层硬限 30000);超限请拆分多次调用。CLI 会在调用前做本地预校验,底层超 30000 会直接报错 -- `range set-style` / `range batch-set-style` 的样式枚举按驼峰书写:`wordWrap` 取 `overflow`/`clip`/`autoWrap`,`fontWeight` 取 `bold`/`normal`,`hAlign` 取 `left`/`center`/`right`/`general`,`vAlign` 取 `top`/`middle`/`bottom`;背景色/字体颜色统一使用 `#RRGGBB` 格式 -- `new` 创建工作表时,如名称与已有工作表重复,系统会自动重命名 -- `find` 返回匹配单元格的地址(A1 表示法)和值,无匹配时返回空数组 -- `find` 的 `--match-entire-cell` 用于精确匹配:只返回单元格内容完全等于搜索文本的结果,不会匹配包含该文本的单元格(例如搜索"苹果"时,只匹配"苹果",不匹配"苹果手机""苹果汁"等)。用户说"精确搜索/完全匹配/只搜等于XX的"时必须使用此参数 -- `find` 的 `--match-case` 默认为 true(区分大小写),设为 false 可忽略大小写 -- `find` 的 `--use-regexp` 启用后,`--find` 参数作为正则表达式处理 -- ★ 当用户要求搜索/查找表格数据时,使用 `find` 命令,不要用 `range read` 读取全量数据后自行过滤——`find` 支持服务端搜索,效率更高、语义更准确 -- `append` 自动定位到最后一行有数据的位置下方插入,无需手动计算行号 -- `append` 的 `--values` 二维数组中每行的列数必须一致,否则会报错。如果用户提供的数据中各行长度不同,必须先将短行用空字符串 `""` 补齐到与最长行相同的列数后再调用。追加的数据列数也应与工作表已有数据列数保持一致 -- `append` vs `range update`:追加新行用 `append`,修改已有单元格用 `range update` -- `insert-dimension` 在指定位置之前插入空行或空列,不写入数据;如需在末尾追加行/列,使用 `append` -- `insert-dimension` 的 `--dimension` 只接受 `ROWS` 或 `COLUMNS` -- `insert-dimension` 的 `--position` 支持工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id` -- `insert-dimension` 的 `--length` 最大为 5000 -- `delete-dimension` 从指定位置起删除若干连续的行或列,删除后后续行/列向前移动填补空位 -- `delete-dimension` 的 `--dimension` 只接受 `ROWS` 或 `COLUMNS` -- `delete-dimension` 的 `--position` 支持工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id` -- `delete-dimension` 的 `--length` 最大为 5000 -- `delete-dimension` 若需仅清空内容但保留行/列占位,请使用 `range update` 将目标区域写入空字符串 `""`(参见《range update 清空单元格规范》) -- `update-dimension` 批量更新连续行/列的显隐状态与行高/列宽 -- `update-dimension` 的 `--dimension` 只接受 `ROWS` 或 `COLUMNS` -- `update-dimension` 的 `--start-index` 支持工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id` -- `update-dimension` 的 `--length` 最大为 5000 -- `update-dimension` 的 `--hidden` 与 `--pixel-size` 至少必须提供一个 -- `update-dimension` 的 `--pixel-size` 单位为像素,`dimension=ROWS` 时表示行高、`dimension=COLUMNS` 时表示列宽 -- `update-dimension` 当同时提供 `--hidden` 与 `--pixel-size` 时,将先应用尺寸再应用显隐,任一失败整体失败 -- `merge-cells` 合并时只保留左上角单元格的值,其他单元格的值会被丢弃 -- `merge-cells` 的 `--merge-type` 不传时默认为 `mergeAll`(合并所有单元格) -- `merge-cells` 的 `--range` 支持带工作表前缀的写法(如 `Sheet1!A1:B3`),此时忽略 `--sheet-id` -- `merge-cells` 如果目标区域与其他合并单元格、锁定区域或表格区域存在交集,合并将失败 -- `media-upload` 是两步自动完成的流程 (获取附件上传凭证 → OSS 上传),无需手动分步操作 -- `write-image` 是三步自动完成的流程 (获取附件上传凭证 → OSS 上传 → 写入图片到单元格),无需手动分步操作 -- ★ 向表格单元格中写入图片必须使用 `write-image`,禁止使用 `range update`。`range update` 底层调用的 `update_range` MCP 工具不支持图片类型参数,调用会失败 -- `write-image` 与 `media-upload` 的区别:`media-upload` 仅上传附件到表格获取 resourceId;`write-image` 在上传后还会将图片写入指定单元格 -- `create-float-image` 创建浮动图片前必须先通过 `media-upload` 上传图片获取 `resourceUrl`,再将其作为 `--src` 传入。`--src` 的格式为 `/core/api/resources/img/...`,不能直接传外部 URL -- `create-float-image` 的 `--range` 使用 A1 表示法指定锚点单元格(如 `A1`、`B3`),支持带工作表前缀(如 `Sheet1!A1`) -- `create-float-image` 的 `--width` / `--height` 为必填,单位像素,必须为正整数;`--offset-x` / `--offset-y` 可选,默认 0,不能为负数 -- `write-image`(单元格内嵌图片)vs `create-float-image`(浮动图片):`write-image` 将图片写入单元格内部,占据单元格内容;`create-float-image` 创建悬浮于单元格之上的浮动图片,不占用单元格内容,可自由调整位置和大小 -- `update-float-image` 的 `--src` / `--range` / `--width` / `--height` / `--offset-x` / `--offset-y` 至少必须提供一个 -- `list-float-images` 返回 `floatImages` 数组和 `totalCount`,每个元素包含 `id`(用于后续 get / update / delete) -- `delete-float-image` 操作不可恢复,删除后图片将从工作表中移除 -- `replace` 的 `--find` 不能为空字符串,`--replace` 可以为空字符串(表示删除匹配内容) -- `replace` 的 `--match-case` 默认为 false(不区分大小写),与 `find` 的默认行为不同 -- ★ `replace` vs `range update`:需要批量替换文本时,必须使用 `replace` 命令,禁止用 `range update` 手动重写单元格来实现替换效果。`replace` 支持服务端全局替换,效率更高且会返回替换计数 -- ★ `move-dimension` vs `range update`:需要移动行或列时,必须使用 `move-dimension` 命令,禁止用 `range update` 读取数据后手动重写来模拟移动效果。`move-dimension` 是原子操作,能保留单元格的格式、合并状态等属性 -- `move-dimension` 的索引均为 0-based(第 1 行/列的索引为 0),`endIndex` 包含在移动范围内 -- `move-dimension` 的 `--destination-index` 不能在 [start-index, end-index] 范围内 -- `move-dimension` 的移动跨度(end-index - start-index + 1)不超过 5000 -- `move-dimension` 的 `--destination-index` 是目标位置的 0-based 索引,即移动到第 n 行/列则传 `n - 1`(通用公式:`destination-index = 目标行号(1-based) - 1`) -- `add-dimension` vs `range update`:需要在末尾追加空行/空列时,必须使用 `add-dimension` 命令,禁止用 `range update` 写空数据来模拟追加效果 -- `add-dimension` 追加的是空行/空列,与 `append`(追加带数据的行)不同 -- `add-dimension` 的 `--length` 必须为正整数(>= 1),行列均不超过 5000 -- `unmerge-cells` 取消指定范围内所有合并单元格,使用 A1 表示法指定范围 -- `set-dropdown` 在指定范围内设置下拉列表,`--options` 为 JSON 数组,每个元素包含 `value`(必填)和 `color`(可选,`#RRGGBB` 格式)。选项值不能包含英文逗号。`--multi-select` 启用多选模式。如果目标范围已存在下拉列表,会被新配置覆盖 -- `get-dropdown` 查询指定范围内的下拉列表配置,返回 `dataValidations` 数组,相同选项的单元格聚合为一组。无下拉列表时 `hasDropdown` 为 false -- `delete-dropdown` 删除指定范围内的下拉列表配置,单元格恢复为普通文本格式。已填写的值不会被清除。目标范围不存在下拉列表时操作仍返回成功 -- ★ **全局筛选(filter)与筛选视图(filter-view)的区别**:全局筛选影响所有协作者看到的数据展示,每个工作表最多一个;筛选视图是个人化的,互不影响。用户只说"筛选"时默认走 `filter` 系列 -- `filter get` 获取工作表的全局筛选信息,返回 `range`(筛选范围)和 `columnFilterCriteria`(各列条件)。无筛选时返回空 -- `filter create` 创建全局筛选时 `--range` 必须包含表头行(如 `A1:E100`),不能只包含数据行。每个工作表只能有一个筛选,已存在时报错 -- `filter create` 的 `--criteria` 可选,不传则仅创建空筛选框架,后续通过 `filter update` 设置条件 -- `filter delete` 删除后所有筛选条件丢失且所有被隐藏行重新显示,不可恢复 -- `filter delete` 工作表没有筛选时调用会报错,应先 `filter get` 确认存在 -- `filter update` 是覆盖式:指定列的条件会被替换,未指定的列保持不变。如只想修改某一列,建议先 `filter get` 读取现有配置再 patch -- `filter update` 前置:工作表必须已创建筛选 -- `filter clear-criteria` 仅清除指定列的条件,不删除整个筛选。指定列无条件时不报错(幂等) -- `filter sort` 会实际改变数据行的物理顺序,不可撤销。前置:工作表必须已创建筛选 -- ★ **筛选操作规范**(参照飞书 core-operations): - - 当用户要求"筛选/只看/仅保留 X"时,**必须**通过 `filter create` / `filter update` 创建真实的筛选器。**禁止**用"删除不符合条件的行"或"新建工作表只放符合条件的行"来代替 - - 创建/更新筛选后**必须** `filter get` 回读验证配置正确 - - 更新已有筛选前先 `filter get` 读取当前配置,确认目标存在且了解现有条件后再操作 - - 筛选条件的列索引(`column`)必须与实际数据列精确对应,不要凭猜测填写 - - 筛选不支持正则表达式,传入正则会当成普通文本处理 -- `filter-view list` 获取指定工作表的所有筛选视图列表,返回的 `id` 可用于后续 info / update / delete / update-criteria / delete-criteria / list-criteria / get-criteria 的 `--filter-view-id` -- `filter-view info` 获取单个筛选视图的完整信息(含 criteria),内部复用 `get_filter_views` MCP 按 ID 过滤 -- `filter-view list-criteria` 列出指定筛选视图已设置的所有列条件,返回按列偏移量为 key 的对象;无条件时返回空对象 `{}` -- `filter-view get-criteria` 获取指定列的条件详情,`--column` 为列偏移量(从 0 开始);该列无条件时返回错误提示 -- `filter-view create` 创建筛选视图时 `--range` 应包含表头行。`--criteria` 可选,不传则创建后无筛选条件,后续可通过 `filter-view update-criteria` 设置 -- `filter-view update` 的 `--name`、`--range`、`--criteria` 至少需要传入一个,未指定的字段保持不变 -- `filter-view update` 的 `--criteria` 中指定列的条件会被替换,未指定的列保持不变 -- `filter-view delete` 删除后该视图及其所有筛选条件将被永久移除,不可恢复 -- `filter-view delete` 不影响全局筛选或其他筛选视图 -- `filter-view update-criteria` 的 `--column` 为列偏移量(从 0 开始),相对于筛选视图范围首列。例如筛选视图范围为 `B1:E10`,则 `--column 0` 代表 B 列 -- `filter-view update-criteria` 设置条件后立即在该筛选视图中生效,仅影响当前视图,不影响全局筛选或其他筛选视图 -- `filter-view update-criteria` 的 `--filter-criteria` 中 `conditions` 最多 2 个条件,多条件之间通过 `conditionOperator` 指定逻辑关系(`and` 或 `or`) -- `filter-view delete-criteria` 仅清除指定列的条件,不会删除整个筛选视图。如需删除整个筛选视图,请使用 `filter-view delete` -- `filter-view delete-criteria` 如果指定列没有设置筛选条件,调用不会报错 -- 筛选视图相关操作需要"可阅读"权限(list / info / list-criteria / get-criteria)或"可编辑"权限(create / update / delete / update-criteria / delete-criteria),不支持跨组织操作 -- ★ `export` 仅支持钉钉在线电子表格(alxs)→ xlsx;传入钉钉文字文档会报 `invalidRequest.document.typeIllegal` -- ★ `export` 为单命令一站式,CLI 内部已自动完成「提交 → 渐进式退避轮询 → 可选下载」,**Agent 不得在外部实现轮询或重试**;命令返回成功后不再调用其他 export 相关命令 -- `export` 内置轮询策略:1~5 次间隔 2s、6~10 次间隔 5s、11~20 次间隔 10s、21~30 次间隔 15s,硬上限 30 次(约 5 分钟);超时后命令返回错误,告知用户稍后再试即可 -- ★ `export` 命令返回失败或超时时,**禁止自动重调 `dws sheet export`**;直接告知用户导出失败并建议稍后再试 -- `export` 未指定 `--output` 时,返回的 `downloadUrl` 具有时效性,获取后请尽快下载;若用户需要本地文件,优先直接传 `--output` 让 CLI 代为下载 -- `export` 的 `--output` 可为文件路径或已存在目录;为目录时自动从 `downloadUrl` 推断文件名,为文件路径时直接按该路径保存 -- 用户要求"导出表格/下载 xlsx"时,必须使用 `export` 单命令,禁止用 `range read` 读全量数据后自行拼 xlsx 模拟导出(服务端导出会保留格式/合并/公式等完整属性) -- `update` 的 `--title`、`--index`、`--hidden`、`--frozen-row-count`、`--frozen-column-count` 至少必须提供一个 -- `update` 的 `--title` 最长 100 字符,不能包含 `/ \ ? * [ ] :` 等特殊字符 -- `update` 的 `--index` 为 0-based 非负整数,0 表示移动到最前面 -- `update` 的 `--hidden` 设为 true 时,至少需要保留一个可见的工作表,不能将所有工作表都隐藏 -- `update` 的 `--frozen-row-count` / `--frozen-column-count` 为非负整数,不能超过工作表的总行数/列数,设为 0 表示取消冻结 -- `update` 当同时提供多个属性时,所有属性将在同一次请求中更新 -- `copy` 复制操作会将源工作表的所有内容(包括数据、格式、公式等)完整复制到新工作表 -- `copy` 的 `--title` 可选,不传时系统自动生成名称(通常为"源名称 副本"或类似格式) -- `copy` 的 `--title` 最长 100 字符,不能包含 `/ \ ? * [ ] :` 等特殊字符 -- `copy` 当指定名称与已有工作表重复时,系统会自动重命名为合法值 -- `copy` 的 `--index` 可选,不传时副本将放置在源工作表之后的默认位置 -- ★ 关键区分: sheet(电子表格/单元格读写) vs aitable(AI多维表/结构化记录/字段定义) vs doc(文档编辑/阅读) -- sheet 产品线仅支持 `axls`(在线电子表格,`contentType=ALIDOC`),不支持 `xlsx` / `xls` / `xlsm` / `csv` 等本地表格文件 -- 遇到未知 `alidocs` URL 时,必须先 probe(`dws doc info --node --format json`)确认 `contentType` 和 `extension`,才能决定是否走 sheet -- 当节点 `extension=xlsx` / `xls` / `xlsm` / `csv`(`contentType≠ALIDOC`)时,必须用 `dws doc download --node --output <路径>` 先下载到本地再处理,禁止调用任何 sheet 子命令(sheet 底层 MCP 工具只识别 axls,调用 xlsx 节点必失败) -- 要把在线表格导出为 xlsx 文件——走 `dws sheet export`(axls → xlsx 的格式转换);要读已有的 xlsx 文件——走 `dws doc download` 后在本地解析,两者方向相反 +`--node` 同时支持文档 ID、URL、分享链接。`drive list` 返回中必须用 `fileId`(UUID 格式),禁止用 `dentryId`(纯数字)。 diff --git a/skills/multi/dingtalk-sheet/references/sheet/sheet-conditional-format.md b/skills/multi/dingtalk-sheet/references/sheet/sheet-conditional-format.md new file mode 100644 index 00000000..b4b7532a --- /dev/null +++ b/skills/multi/dingtalk-sheet/references/sheet/sheet-conditional-format.md @@ -0,0 +1,271 @@ +# 条件格式 (conditional format) + +## 使用场景 + +### 条件格式 + +#### 强制走条件格式的触发词(硬约束) + +当用户出现以下口语指令时,**强制**走 `cond-format create/update/delete`,**禁止**用 `range set-style` 写静态背景色/字体色代替: + +- **颜色动作**:"标红 / 标黄 / 标绿 / 上色 / 染色 / 涂色" +- **视觉强调**:"高亮 / 突出 / 标记 / 标注 / 区分" +- **条件触发**:"重复的标出来 / 异常的圈出来 / 过期的染红 / 大于 X 的标黄 / 不达标的标红" +- **联动语义**:"颜色随数据变 / 联动 / 自动更新 / 改了数据颜色也跟着变" +- **数值可视化**:"数据条 / 色阶 / 渐变色 / 进度条样式" + +**判断标准**:交付后 `cond-format list` 必须能返回该规则;否则视为违规。 + +> 如果用 `range set-style` 写静态背景色,源数据变化时颜色不会跟着变。典型反例:用户要求"过期单元格标红"时用静态填充——日期变化后颜色不再准确。 + +**大数据量优势**:当数据量 > 1000 行时,条件格式是首选——它由服务端自身渲染,不需要逐行调用 `range set-style`,性能远优于静态样式写入。 + +#### 意图判断 + +用户说"条件格式/条件样式/自动变色/满足条件时高亮/按条件设样式/条件格式规则": +- 查看已有条件格式 → `cond-format list` +- 查看指定规则详情 → `cond-format list --rule-id RULE_ID` + +用户说"创建条件格式/设置条件格式/大于某值时标红/包含某文本时高亮/数据条/图标集/色阶/重复值高亮": +- 创建条件格式规则 → `cond-format create` +- `--condition` 为 JSON 对象,key 为条件类型,value 为条件参数 +- `--cell-style` 为命中时的样式(适用于数值/文本/空值/错误/重复/公式/排名/平均值/标准差类型) +- `--data-bar-style` 为数据条样式(仅数据条类型时使用) +- 色阶和图标集类型不需要 `--cell-style`(样式内置在条件定义中) + +用户说"修改条件格式/更新条件格式规则/改条件/改样式/改范围": +- 更新条件格式规则 → `cond-format update` +- 未传入的字段保持原有值不变 +- 传入 `--condition` 会替换原有条件类型 + +用户说"删除条件格式/移除条件格式/取消条件格式": +- 删除条件格式规则 → `cond-format delete --yes` +- 删除不可恢复,执行前必须向用户确认,同意后才加 `--yes` 执行 +- 规则已不存在时操作仍返回成功 + +#### 常见配置错误(必须注意) + +- **创建后必须验证**:条件格式创建后必须调用 `cond-format list` 验证规则是否生效。如果验证发现规则未生效或配置不正确,应立即修复并重试 +- **范围要精确**:条件格式的应用范围必须精确覆盖用户指定的列/行,不要遗漏也不要过度扩大 +- **`backgroundColor` vs `fontColor` 的中文语义**:用户中文语境下的"标红/高亮/染色/标记"指**单元格背景色**,用 `backgroundColor`;"文字红/字体红/把字变红"才用 `fontColor`。默认无说明时选 `backgroundColor` +- **日期/空值比较必须防空**:用户说"过期的标红"时,公式必须排除空单元格,否则空白格也会被误判为过期而全表标红。正确公式:`=AND(E1<>"", E1<=TODAY())`;错误公式:`=E1<=TODAY()`(空值会被当作 0 判为过期) +- **公式引用方式**:自定义公式条件中的单元格引用需要根据实际场景选择相对/绝对引用(如 `=E1<=TODAY()` 使用相对引用使公式随行变化,而非 `=$E$1<=TODAY()` 只比较一个格) +- **创建前必须确认列对应**:仅读表头不够——如果表头语义含糊,formula 里引用的列字母可能张冠李戴。建议先读 3-5 行数据样本(如 `range read --range "A1:Z5"`)确认列名对应的实际值和数据类型 + +#### 辅助列+条件格式两步走(高频致命错误防护) + +**用户明确要求"辅助列+条件格式"两步走时,禁止用 `formulaCondition` 绕过**: + +当用户说以下任意一种表达时,必须按两步走(先建辅助列 → 再基于辅助列做条件格式),**禁止**直接用一个 `formulaCondition` 公式一步完成: +- "**增加辅助列**,再/然后标记……" +- "**先计算/判断** XX **是否** YY,**再**标记……" +- "**新建一列**放结果,再用结果染色" +- 明确要求用"辅助列"、"辅助字段"、"判断列"、"标记列" + +**正确做法(两步走)**: +```bash +# Step 1: 用 range update 在新列写判断公式(形成"是/否"辅助列) +dws sheet range update --node NODE_ID --sheet-id SHEET_ID --range "H2:H100" \ + --values '[["=IF(A2>B2,\"是\",\"否\")"],...]' + +# Step 2: 基于辅助列值做条件格式(用 formulaCondition 引用辅助列) +dws sheet cond-format create --node NODE_ID --sheet-id SHEET_ID \ + --ranges '["A2:H100"]' \ + --condition '{"formulaCondition":{"formula":"=$H2=\"是\""}}' \ + --cell-style '{"backgroundColor":"#FFECEC"}' +``` + +**错误做法(一步走绕过辅助列)**: +```bash +# 虽然逻辑等价,但产物里缺辅助列 → 用户打开表格看不到"是/否"列 +dws sheet cond-format create --node NODE_ID --sheet-id SHEET_ID \ + --ranges '["A2:H100"]' \ + --condition '{"formulaCondition":{"formula":"=$A2>$B2"}}' \ + --cell-style '{"backgroundColor":"#FFECEC"}' +``` + +**为什么禁止一步走**:用户明确要求辅助列是有**业务意图**的——让人肉眼能在表里看到判断结果列;条件格式只是视觉辅助。一步 `formulaCondition` 虽然效果对了,但用户打开表格看不到辅助列,被视为"操作不完整"。 + +> `formulaCondition` 单独使用的场景是:用户**没有**明确要求辅助列、只要"标红符合条件的行"时。 + +#### 典型工作流 + +``` +1. 先读取现有条件格式了解当前配置 + dws sheet cond-format list --node NODE_ID --sheet-id SHEET_ID + +2. 创建/更新/删除条件格式规则 + dws sheet cond-format create --node NODE_ID --sheet-id SHEET_ID --ranges '["A1:E100"]' \ + --condition '{"numberCondition":{"operator":"greater","value1":"80"}}' \ + --cell-style '{"backgroundColor":"#FFCDD2","fontColor":"#B71C1C","bold":true}' + +3. 再次读取验证结果是否生效 + dws sheet cond-format list --node NODE_ID --sheet-id SHEET_ID +``` + +#### 条件类型参考 + +条件类型(`--condition` JSON 的 key,每次只能选一种): + +| 条件类型 | 说明 | 参数 | +|---------|------|------| +| `numberCondition` | 数值比较 | operator: equal/not-equal/greater/greater-equal/less/less-equal/between/not-between + value1 + value2 | +| `textCondition` | 文本匹配 | operator: contains/not-contains/starts-with/ends-with + value | +| `emptyCondition` | 空值判断 | operator: is-empty/is-not-empty | +| `errorCondition` | 错误值 | operator: error/no-error | +| `duplicateCondition` | 重复/唯一值 | operator: duplicate/unique | +| `formulaCondition` | 自定义公式 | formula: "=A1>100" | +| `rankCondition` | 排名 | value + isPercent + isBottom | +| `averageCondition` | 高于/低于平均值 | isAbove + andEqual | +| `stdevCondition` | 标准差 | value + isAbove + andEqual | +| `dataBarCondition` | 数据条 | minPoint + maxPoint(每个含 type + value) | +| `iconSetCondition` | 图标集 | iconSet(数组)+ showIconOnly | +| `colorScaleCondition` | 色阶 | criterias(数组,每项含 type + value + color) | + +## 命令详细参考 + +### 获取条件格式规则 +``` +Usage: + dws sheet cond-format list [flags] +Example: + # 获取所有条件格式规则 + dws sheet cond-format list --node --sheet-id + + # 获取单个规则的详情 + dws sheet cond-format list --node --sheet-id --rule-id +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --rule-id string 条件格式规则 ID (可选,不传则返回全部) +``` + +- **用途**:查看指定工作表中已有的条件格式规则,或获取单个规则的详情。 +- **场景**:创建/更新/删除条件格式前后验证规则状态;获取 ruleId 供后续 update/delete 使用。 +- **返回**:rules 数组,每条规则包含 id、type、ranges、条件参数、cellStyle/dataBarStyle 等。 + +### 创建条件格式规则 +``` +Usage: + dws sheet cond-format create [flags] +Example: + # 数值条件:大于 80 时标红加粗 + dws sheet cond-format create --node --sheet-id \ + --ranges '["A1:A100"]' \ + --condition '{"numberCondition":{"operator":"greater","value1":"80"}}' \ + --cell-style '{"backgroundColor":"#FFCDD2","fontColor":"#B71C1C","bold":true}' + + # 文本条件:包含"延期"时加删除线 + dws sheet cond-format create --node --sheet-id \ + --ranges '["B1:B50"]' \ + --condition '{"textCondition":{"operator":"contains","value":"延期"}}' \ + --cell-style '{"backgroundColor":"#FFF3E0","strikethrough":true}' + + # 数据条 + dws sheet cond-format create --node --sheet-id \ + --ranges '["C1:C20"]' \ + --condition '{"dataBarCondition":{"minPoint":{"type":"auto"},"maxPoint":{"type":"auto"}}}' \ + --data-bar-style '{"fill":["#4CAF50","#F44336"],"isGradient":true}' + + # 色阶(三色) + dws sheet cond-format create --node --sheet-id \ + --ranges '["D1:D50"]' \ + --condition '{"colorScaleCondition":{"criterias":[{"type":"maxmin","color":"#F44336"},{"type":"percentile","value":"50","color":"#FFEB3B"},{"type":"maxmin","color":"#4CAF50"}]}}' + + # 重复值高亮 + dws sheet cond-format create --node --sheet-id \ + --ranges '["E1:E100"]' \ + --condition '{"duplicateCondition":{"operator":"duplicate"}}' \ + --cell-style '{"backgroundColor":"#FCE4EC"}' +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --ranges string 应用范围 JSON 数组 (必填),如 '["A1:E10"]' + --condition string 条件类型及参数 JSON 对象 (必填) + --cell-style string 单元格样式 JSON 对象 (可选) + --data-bar-style string 数据条样式 JSON 对象 (可选,仅数据条类型) +``` + +- **用途**:在指定工作表中创建一条条件格式规则。 +- **注意事项**: + - 创建后**必须**用 `cond-format list` 验证规则是否生效 + - 中文"标红/高亮/染色"默认指 `backgroundColor`,"字体红"才是 `fontColor` + - 日期/空值公式必须防空:`=AND(E1<>"", E1<=TODAY())` 而非 `=E1<=TODAY()` + - 公式中用相对引用使公式随行变化 + - 创建前建议先 `range read` 读 3-5 行数据确认列对应关系 +- **条件类型**(`--condition` JSON 的 key,每次只能选一种):numberCondition / textCondition / emptyCondition / errorCondition / duplicateCondition / formulaCondition / rankCondition / averageCondition / stdevCondition / dataBarCondition / iconSetCondition / colorScaleCondition + +### 更新条件格式规则 +``` +Usage: + dws sheet cond-format update [flags] +Example: + # 修改条件(改为大于 90) + dws sheet cond-format update --node --sheet-id --rule-id \ + --condition '{"numberCondition":{"operator":"greater","value1":"90"}}' + + # 修改样式 + dws sheet cond-format update --node --sheet-id --rule-id \ + --cell-style '{"backgroundColor":"#C8E6C9","fontColor":"#1B5E20"}' + + # 修改应用范围 + dws sheet cond-format update --node --sheet-id --rule-id \ + --ranges '["A1:F200"]' +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --rule-id string 条件格式规则 ID (必填) + --ranges string 应用范围 JSON 数组 (可选) + --condition string 条件类型及参数 JSON 对象 (可选,传入后替换原有条件) + --cell-style string 单元格样式 JSON 对象 (可选) + --data-bar-style string 数据条样式 JSON 对象 (可选,仅数据条类型) +``` + +- **用途**:更新已有条件格式规则的部分或全部配置。 +- **场景**:修改阈值、切换条件类型、调整样式、扩大应用范围。 +- **注意**:未传入的字段保持原有值不变;`--ranges`/`--condition`/`--cell-style`/`--data-bar-style` 至少传入一个。 +- **ruleId 获取**:通过 `cond-format list` 获取。 + +### 删除条件格式规则 + +> **CAUTION:** 不可逆操作 — 执行前必须向用户确认。 + +``` +Usage: + dws sheet cond-format delete [flags] +Example: + # 删除条件格式规则(必须加 --yes 确认) + dws sheet cond-format delete --node --sheet-id --rule-id --yes +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --rule-id string 条件格式规则 ID (必填) +``` + +- **用途**:删除指定条件格式规则。 +- **幂等性**:规则已不存在时操作仍返回成功。 +- **ruleId 获取**:通过 `cond-format list` 获取。 + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `cond-format list` | `rules` 数组(含 `id`、`type`、`ranges`、条件参数、`cellStyle`/`dataBarStyle`) | 获取 ruleId 供 update / delete 使用;验证规则是否生效 | +| `cond-format create` | 新创建规则的 `id` | 用于后续 update / delete 的 --rule-id | +| `cond-format update` | 更新后的规则信息 | 确认更新结果 | +| `cond-format delete` | 操作结果 | 确认删除完成 | +| `list` | 工作表的 `sheetId` | cond-format list / create / update / delete 的 --sheet-id | + +## 注意事项 + +- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) +- ★ **强制走条件格式的触发词**:用户说"标红/标黄/高亮/突出/标记/数据条/色阶/颜色随数据变"时,强制走 `cond-format create/update/delete`,禁止用 `range set-style` 写静态背景色代替 +- **创建后必须验证**:条件格式创建后必须调用 `cond-format list` 验证规则是否生效 +- **范围要精确**:条件格式的应用范围必须精确覆盖用户指定的列/行,不要遗漏也不要过度扩大 +- **`backgroundColor` vs `fontColor` 的中文语义**:用户中文语境下的"标红/高亮/染色/标记"指**单元格背景色**,用 `backgroundColor`;"文字红/字体红/把字变红"才用 `fontColor`。默认无说明时选 `backgroundColor` +- **日期/空值比较必须防空**:用户说"过期的标红"时,公式必须排除空单元格。正确公式:`=AND(E1<>"", E1<=TODAY())`;错误公式:`=E1<=TODAY()`(空值会被当作 0 判为过期) +- **公式引用方式**:自定义公式条件中的单元格引用需要根据实际场景选择相对/绝对引用(如 `=E1<=TODAY()` 使用相对引用使公式随行变化) +- **创建前必须确认列对应**:仅读表头不够——如果表头语义含糊,formula 里引用的列字母可能张冠李戴。建议先读 3-5 行数据样本(如 `range read --range "A1:Z5"`)确认列名对应的实际值和数据类型 +- **辅助列+条件格式两步走**:用户明确要求"辅助列"时,必须按两步走(先建辅助列 → 再基于辅助列做条件格式),禁止直接用 `formulaCondition` 一步绕过 +- **大数据量优势**:当数据量 > 1000 行时,条件格式是首选——它由服务端自身渲染,不需要逐行调用 `range set-style`,性能远优于静态样式写入 +- **判断标准**:交付后 `cond-format list` 必须能返回该规则;否则视为违规 diff --git a/skills/multi/dingtalk-sheet/references/sheet/sheet-dimension-operations.md b/skills/multi/dingtalk-sheet/references/sheet/sheet-dimension-operations.md new file mode 100644 index 00000000..589c157e --- /dev/null +++ b/skills/multi/dingtalk-sheet/references/sheet/sheet-dimension-operations.md @@ -0,0 +1,275 @@ +# 行列操作 (dimension operations) + +## 使用场景 + +### 行列操作 + +用户说"插入行/插入列/在某行前插入/在某列前插入": +- 插入行或列 → `insert-dimension` +- 在末尾追加 → `append`(insert-dimension 不支持末尾追加) + +用户说"删除行/删除列/删掉第几行/删掉某列/移除行/移除列": +- 删除行或列 → `delete-dimension` +- 仅清空内容但保留行/列 → `range clear`(默认清除值保留格式) + +用户说"隐藏行/隐藏列/显示行/显示列/设置行高/设置列宽/调整行高/调整列宽/行列属性": +- 隐藏/显示行或列 → `update-dimension --hidden` / `--hidden=false` +- 设置行高/列宽 → `update-dimension --pixel-size` +- 同时修改尺寸与显隐 → `update-dimension --pixel-size --hidden` + +用户说"移动行/移动列/调整行顺序/调整列顺序/行列拖拽/把第N行移到第M行": +- 移动行或列 → `move-dimension` +- 请勿用 `range read` + `range update` 读取再重写来模拟移动,`move-dimension` 是原子操作,能保留格式和合并状态 + +用户说"追加空行/追加空列/增加行数/增加列数/扩展表格/在末尾加空行": +- 追加空行/空列 → `add-dimension` +- 注意与 `append`(追加数据行)区分:`add-dimension` 追加的是空行/空列,`append` 追加的是带数据的行 +- 请勿用 `range update` 写空数据来模拟追加,`add-dimension` 直接扩展表格维度 + +**结构预检**:插入、删除、移动行列前,必须先执行 `dws sheet info --node --sheet-id --format json` 查看 `mergedRanges`。合并区域跨过操作位置时,行列变更可能导致表头/分组标题断裂、空白或错位;需要先向用户说明影响,必要时取消合并后操作,再按原模式重新 `merge-cells`。 + +## 命令详细参考 + +### 在指定位置插入行或列 +``` +Usage: + dws sheet insert-dimension [flags] +Example: + # 在第 3 行之前插入 2 行 + dws sheet insert-dimension --node --sheet-id --dimension ROWS --position "3" --length 2 + + # 在 A 列之前插入 1 列 + dws sheet insert-dimension --node --sheet-id --dimension COLUMNS --position "A" --length 1 + + # 使用工作表前缀(忽略 --sheet-id) + dws sheet insert-dimension --node --sheet-id --dimension ROWS --position "Sheet1!3" --length 5 + + # 在 AB 列之前插入 3 列 + dws sheet insert-dimension --node --sheet-id --dimension COLUMNS --position "AB" --length 3 +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --dimension string 插入维度: ROWS 或 COLUMNS (必填) + --position string 插入位置,A1 表示法 (必填)。ROWS 时为行号如 "3";COLUMNS 时为列字母如 "A" + --length string 插入数量,正整数 (必填),最大 5000 +``` + +在钉钉表格指定工作表的指定位置之前插入若干空行或空列。 +`--dimension ROWS` 时,`--position` 为 1-based 行号字符串;`--dimension COLUMNS` 时,`--position` 为列字母。 +支持在 `--position` 中携带工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id`。 +若需要在末尾追加行/列,请使用 `append` 命令。 + +### 删除指定位置的行或列 + +> **CAUTION:** 不可逆操作 — 执行前必须向用户确认。 + +``` +Usage: + dws sheet delete-dimension [flags] +Example: + # 从第 3 行开始删除 2 行 + dws sheet delete-dimension --node --sheet-id --dimension ROWS --position "3" --length 2 + + # 从 A 列开始删除 1 列 + dws sheet delete-dimension --node --sheet-id --dimension COLUMNS --position "A" --length 1 + + # 使用工作表前缀(忽略 --sheet-id) + dws sheet delete-dimension --node --sheet-id --dimension ROWS --position "Sheet1!3" --length 5 + + # 从 AB 列开始删除 3 列 + dws sheet delete-dimension --node --sheet-id --dimension COLUMNS --position "AB" --length 3 +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --dimension string 删除维度: ROWS 或 COLUMNS (必填) + --position string 删除起始位置,A1 表示法 (必填)。ROWS 时为行号如 "3";COLUMNS 时为列字母如 "A" + --length string 删除数量,正整数 (必填),最大 5000 +``` + +在钉钉表格指定工作表中,从指定位置起删除若干连续的行或列。 +`--dimension ROWS` 时,`--position` 为 1-based 行号字符串;`--dimension COLUMNS` 时,`--position` 为列字母。 +支持在 `--position` 中携带工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id`。 +删除后后续的行/列会向前移动填补空位;若需要仅清空内容但保留行/列占位,请使用 `range clear`。 + +### 更新指定范围行/列属性 +``` +Usage: + dws sheet update-dimension [flags] +Example: + # 隐藏第 3~4 行 + dws sheet update-dimension --node --sheet-id --dimension ROWS --start-index "3" --length 2 --hidden + + # 显示 A~B 列 + dws sheet update-dimension --node --sheet-id --dimension COLUMNS --start-index "A" --length 2 --hidden=false + + # 设置第 1~5 行行高为 40px + dws sheet update-dimension --node --sheet-id --dimension ROWS --start-index "1" --length 5 --pixel-size 40 + + # 设置 C 列列宽为 200px 并隐藏 + dws sheet update-dimension --node --sheet-id --dimension COLUMNS --start-index "C" --length 1 --pixel-size 200 --hidden + + # 使用工作表前缀(忽略 --sheet-id) + dws sheet update-dimension --node --sheet-id --dimension ROWS --start-index "Sheet1!3" --length 2 --hidden +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --dimension string 更新维度: ROWS 或 COLUMNS (必填) + --start-index string 起始位置,A1 表示法 (必填)。ROWS 时为行号如 "3";COLUMNS 时为列字母如 "A" + --length string 更新数量,正整数 (必填),最大 5000 + --hidden 是否隐藏 (true=隐藏, false=显示),与 --pixel-size 至少填其一 + --pixel-size int 行高或列宽(像素),ROWS 时为行高,COLUMNS 时为列宽,与 --hidden 至少填其一 +``` + +批量更新钉钉表格指定工作表中连续多行/多列的属性,支持设置显隐状态(hidden)与行高/列宽(pixelSize)。 +`--dimension ROWS` 时,`--start-index` 为 1-based 行号字符串;`--dimension COLUMNS` 时,`--start-index` 为列字母。 +支持在 `--start-index` 中携带工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id`。 +`--hidden` 与 `--pixel-size` 至少必须提供一个。当同时提供时,将先应用尺寸再应用显隐,任一失败整体失败。 +`--pixel-size` 单位为像素,`dimension=ROWS` 时表示行高、`dimension=COLUMNS` 时表示列宽。 + +### 移动行或列 +``` +Usage: + dws sheet move-dimension [flags] +Example: + # 将第 2 行移动到第 5 行的位置 + dws sheet move-dimension --node --sheet-id \ + --dimension ROWS --start-index "2" --end-index "2" --destination-index "5" + + # 将第 2~4 行(共 3 行)移动到第 1 行的位置(最前面) + dws sheet move-dimension --node --sheet-id \ + --dimension ROWS --start-index "2" --end-index "4" --destination-index "1" + + # 将 B~C 列(共 2 列)移动到 D 列的位置 + dws sheet move-dimension --node --sheet-id \ + --dimension COLUMNS --start-index "B" --end-index "C" --destination-index "D" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --dimension string 维度类型: ROWS 或 COLUMNS (必填) + --start-index string 源起始位置,A1 表示法 (必填) + --end-index string 源结束位置,A1 表示法 (必填) + --destination-index string 目标位置,A1 表示法 (必填) +``` + +startIndex、endIndex 和 destinationIndex 均使用 A1 表示法:`--dimension ROWS` 时为 1-based 行号(如 "2"),`--dimension COLUMNS` 时为列字母(如 "B")。 +源行/列将移动到 destinationIndex 所指的位置。destinationIndex 不能落在源范围 [startIndex, endIndex] 内。 + +**合并单元格注意**:如果源范围或目标位置涉及合并单元格,操作会报错中断。移动前先通过 `dws sheet info --node --sheet-id --format json` 查询 `mergedRanges`,必要时先用 `unmerge-cells` 取消合并再移动,移动后再用 `merge-cells` 恢复需要保留的合并区域。 + +### 追加空行或空列 +``` +Usage: + dws sheet add-dimension [flags] +Example: + dws sheet add-dimension --node --sheet-id --dimension ROWS --length 5 + dws sheet add-dimension --node --sheet-id --dimension COLUMNS --length 3 +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --dimension string 维度类型: ROWS 或 COLUMNS (必填) + --length int 追加数量,正整数,最多 5000 (必填) +``` + +在工作表末尾追加指定数量的空行或空列。 + +## 核心工作流 + +```bash +# ── 工作流 6: 插入行或列 ── + +# 1. 获取工作表列表 +dws sheet list --node --format json + +# 2. 在第 3 行之前插入 2 行 +dws sheet insert-dimension --node --sheet-id \ + --dimension ROWS --position "3" --length 2 --format json + +# 3. 在 A 列之前插入 1 列 +dws sheet insert-dimension --node --sheet-id \ + --dimension COLUMNS --position "A" --length 1 --format json + +# 4. 使用工作表前缀指定位置 +dws sheet insert-dimension --node --sheet-id \ + --dimension ROWS --position "Sheet1!5" --length 3 --format json +``` + +```bash +# ── 工作流 6b: 删除行或列 ── + +# 1. 获取工作表列表 +dws sheet list --node --format json + +# 2. 从第 3 行开始删除 2 行 +dws sheet delete-dimension --node --sheet-id \ + --dimension ROWS --position "3" --length 2 --format json + +# 3. 从 A 列开始删除 1 列 +dws sheet delete-dimension --node --sheet-id \ + --dimension COLUMNS --position "A" --length 1 --format json + +# 4. 使用工作表前缀指定位置 +dws sheet delete-dimension --node --sheet-id \ + --dimension ROWS --position "Sheet1!5" --length 3 --format json +``` + +```bash +# ── 工作流 6c: 更新行/列属性(显隐、行高/列宽) ── + +# 1. 获取工作表列表 +dws sheet list --node --format json + +# 2. 隐藏第 3~4 行 +dws sheet update-dimension --node --sheet-id \ + --dimension ROWS --start-index "3" --length 2 --hidden --format json + +# 3. 显示 A~B 列 +dws sheet update-dimension --node --sheet-id \ + --dimension COLUMNS --start-index "A" --length 2 --hidden=false --format json + +# 4. 设置第 1~5 行行高为 40px +dws sheet update-dimension --node --sheet-id \ + --dimension ROWS --start-index "1" --length 5 --pixel-size 40 --format json + +# 5. 设置 C 列列宽为 200px 并隐藏 +dws sheet update-dimension --node --sheet-id \ + --dimension COLUMNS --start-index "C" --length 1 --pixel-size 200 --hidden --format json +``` + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `insert-dimension` | `a1Notation` 新插入区域范围 | 确认插入位置和范围 | +| `delete-dimension` | `a1Notation` 被删除区域范围 | 确认删除位置和范围 | +| `update-dimension` | `a1Notation` 被更新区域范围、`hidden` 生效的显隐状态、`pixelSize` 生效的尺寸 | 确认更新结果 | +| `move-dimension` | `sheetId` 工作表 ID | 确认操作完成 | +| `add-dimension` | `sheetId` 工作表 ID | 确认操作完成 | +| `list` | 工作表的 `sheetId` | info / range read / range update / find 的 --sheet-id | + +## 注意事项 + +- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) +- `sheet info` 的 `mergedRanges` 是行列结构操作的重要预检信息。插入列时尤其要检查多行表头合并区,原有合并区域通常不会自动扩展到新列,必要时需重新设置合并区域 +- `insert-dimension` 在指定位置之前插入空行或空列,不写入数据;如需在末尾追加行/列,使用 `append` +- `insert-dimension` 的 `--dimension` 只接受 `ROWS` 或 `COLUMNS` +- `insert-dimension` 的 `--position` 支持工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id` +- `insert-dimension` 的 `--length` 最大为 5000 +- `delete-dimension` 从指定位置起删除若干连续的行或列,删除后后续行/列向前移动填补空位 +- `delete-dimension` 的 `--dimension` 只接受 `ROWS` 或 `COLUMNS` +- `delete-dimension` 的 `--position` 支持工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id` +- `delete-dimension` 的 `--length` 最大为 5000 +- `delete-dimension` 若需仅清空内容但保留行/列占位,请使用 `range clear`(默认清除值保留格式,比手动构造空数组更简洁) +- `update-dimension` 批量更新连续行/列的显隐状态与行高/列宽 +- `update-dimension` 的 `--dimension` 只接受 `ROWS` 或 `COLUMNS` +- `update-dimension` 的 `--start-index` 支持工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id` +- `update-dimension` 的 `--length` 最大为 5000 +- `update-dimension` 的 `--hidden` 与 `--pixel-size` 至少必须提供一个 +- `update-dimension` 的 `--pixel-size` 单位为像素,`dimension=ROWS` 时表示行高、`dimension=COLUMNS` 时表示列宽 +- `update-dimension` 当同时提供 `--hidden` 与 `--pixel-size` 时,将先应用尺寸再应用显隐,任一失败整体失败 +- ★ `move-dimension` vs `range update`:需要移动行或列时,必须使用 `move-dimension` 命令,禁止用 `range update` 读取数据后手动重写来模拟移动效果。`move-dimension` 是原子操作,能保留单元格的格式、合并状态等属性 +- `move-dimension` 的 `--start-index`、`--end-index` 和 `--destination-index` 均使用 A1 表示法(ROWS 时为 1-based 行号,COLUMNS 时为列字母) +- `move-dimension` 的 `--destination-index` 不能落在源范围 [startIndex, endIndex] 内 +- `move-dimension` 的源范围 [startIndex, endIndex] 最大跨度为 5000 +- `add-dimension` vs `range update`:需要在末尾追加空行/空列时,必须使用 `add-dimension` 命令,禁止用 `range update` 写空数据来模拟追加效果 +- `add-dimension` 追加的是空行/空列,与 `append`(追加带数据的行)不同 +- `add-dimension` 的 `--length` 必须为正整数(>= 1),行列均不超过 5000 diff --git a/skills/multi/dingtalk-sheet/references/sheet/sheet-dropdown.md b/skills/multi/dingtalk-sheet/references/sheet/sheet-dropdown.md new file mode 100644 index 00000000..0b135aa4 --- /dev/null +++ b/skills/multi/dingtalk-sheet/references/sheet/sheet-dropdown.md @@ -0,0 +1,94 @@ +# 下拉列表 (dropdown) + +## 使用场景 + +### 下拉列表 + +用户说"设置下拉列表/下拉选项/下拉菜单/添加下拉/配置下拉": +- 设置下拉列表 → `set-dropdown` +- 设置多选下拉 → `set-dropdown --multi-select` + +用户说"查看下拉列表/获取下拉配置/下拉列表有哪些选项": +- 获取下拉列表配置 → `get-dropdown` + +用户说"删除下拉列表/移除下拉/取消下拉/清除下拉": +- 删除下拉列表 → `delete-dropdown` + +## 命令详细参考 + +### 设置下拉列表 +``` +Usage: + dws sheet set-dropdown [flags] +Example: + # 设置单选下拉列表 + dws sheet set-dropdown --node --sheet-id --range "A2:A100" \ + --options '[{"value":"选项1"},{"value":"选项2"},{"value":"选项3"}]' + + # 设置带颜色的多选下拉列表 + dws sheet set-dropdown --node --sheet-id --range "B2:B50" \ + --options '[{"value":"高","color":"#ff0000"},{"value":"中","color":"#ffaa00"},{"value":"低","color":"#00ff00"}]' \ + --multi-select +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 目标单元格范围,A1 表示法,如 A2:A100 (必填) + --options string 下拉选项 JSON 数组 (必填),如 '[{"value":"选项1","color":"#ff0000"}]' + --multi-select 是否允许多选(默认单选) +``` + +在指定单元格范围内设置下拉列表。设置后用户可从预定义选项中选择值。 +- **用途**:为单元格配置下拉列表,支持自定义选项颜色和多选。 +- **场景**:规范数据输入,如状态选择(完成/进行中/待处理)、优先级(高/中/低)等。 +- **注意**:选项值不能包含英文逗号;如果目标范围已存在下拉列表,会被新配置覆盖。 + +### 获取下拉列表配置 +``` +Usage: + dws sheet get-dropdown [flags] +Example: + dws sheet get-dropdown --node --sheet-id --range "A2:A100" + dws sheet get-dropdown --node --sheet-id --range "A1" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 查询范围,A1 表示法,如 A1:A100 (必填) +``` + +查询指定范围内的下拉列表配置信息,包括选项值、颜色和是否多选。 +- **用途**:查看单元格已设置的下拉列表选项和配置。 +- **场景**:在修改下拉列表前先查询现有配置;确认下拉列表是否设置成功。 +- **返回**:`dataValidations` 数组,相同选项的单元格聚合为一组,每组包含 `conditionValues`(选项值)、`ranges`(覆盖范围)、`options`(含 `enableMultiSelect` 和 `colorValueMap`)。范围内无下拉列表时 `hasDropdown` 为 false。 + +### 删除下拉列表 +``` +Usage: + dws sheet delete-dropdown [flags] +Example: + dws sheet delete-dropdown --node --sheet-id --range "A2:A100" + dws sheet delete-dropdown --node --sheet-id --range "B1:D10" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 要删除下拉列表的范围,A1 表示法 (必填) +``` + +删除指定范围内的下拉列表配置,单元格恢复为普通文本格式。 +- **用途**:移除不再需要的下拉列表约束。 +- **注意**:已填写的单元格值不会被清除;目标范围不存在下拉列表时操作仍返回成功。 + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `set-dropdown` | `range` 实际设置范围、`optionCount` 选项数量、`enableMultiSelect` 是否多选 | 确认下拉列表设置成功 | +| `get-dropdown` | `hasDropdown` 是否存在下拉、`dataValidations` 下拉配置列表(含 `conditionValues`、`ranges`、`options`) | 查看已有下拉配置 | +| `delete-dropdown` | `range` 实际删除范围 | 确认下拉列表删除完成 | +| `list` | 工作表的 `sheetId` | info / range read / range update / find 的 --sheet-id | + +## 注意事项 + +- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) +- `set-dropdown` 在指定范围内设置下拉列表,`--options` 为 JSON 数组,每个元素包含 `value`(必填)和 `color`(可选,`#RRGGBB` 格式)。选项值不能包含英文逗号。`--multi-select` 启用多选模式。如果目标范围已存在下拉列表,会被新配置覆盖 +- `get-dropdown` 查询指定范围内的下拉列表配置,返回 `dataValidations` 数组,相同选项的单元格聚合为一组。无下拉列表时 `hasDropdown` 为 false +- `delete-dropdown` 删除指定范围内的下拉列表配置,单元格恢复为普通文本格式。已填写的值不会被清除。目标范围不存在下拉列表时操作仍返回成功 diff --git a/skills/multi/dingtalk-sheet/references/sheet/sheet-export.md b/skills/multi/dingtalk-sheet/references/sheet/sheet-export.md new file mode 100644 index 00000000..359b8c16 --- /dev/null +++ b/skills/multi/dingtalk-sheet/references/sheet/sheet-export.md @@ -0,0 +1,95 @@ +# 导出 (export) + +## 使用场景 + +### 导出 + +用户说"导出/下载xlsx/存为Excel/存成表格文件/把表格变成xlsx/导出表格/下载表格/导出为 excel": +- 导出表格 → `export`(单命令一站式,内部自动完成提交、轮询、可选下载) +- 仅需传 `--node`,可选 `--output` 指定本地文件/目录(不传则返回 downloadUrl) +- 需要落盘到本地 → `dws sheet export --node --output `,命令自动下载 xlsx +- 禁止用 `range read` 全量读取后自行拼接 xlsx 来模拟导出,必须使用 `export` 命令(服务端原子导出,保留格式/合并/公式等属性) +- 禁止在 AI Agent 侧实现轮询或重试,CLI 内部已按渐进式退避策略完成(最多 30 次约 5 分钟) + +## 命令详细参考 + +### 导出表格为 xlsx(异步任务一站式) +``` +Usage: + dws sheet export [flags] # 一站式:提交 → 轮询 → 可选下载 +Example: + # 仅导出,返回 downloadUrl(链接有时效性,请尽快下载) + dws sheet export --node + dws sheet export --node "https://alidocs.dingtalk.com/i/nodes/" + + # 导出并自动下载为本地文件 + dws sheet export --node --output ./report.xlsx + + # --output 为目录时,自动按下载链接中的文件名保存 + dws sheet export --node --output ./ + +Flags: + --node string 表格文档 ID 或 URL (必填) + --output string 本地保存路径(可选,支持文件路径或目录) +``` + +将钉钉在线电子表格导出为 Office xlsx 格式。**单命令一站式**:命令内部自动完成「提交任务 → 渐进式退避轮询 → (可选)下载文件」全流程,AI Agent 无需自行拆分步骤或实现轮询。 + +**内部流程**: +1. 调 `submit_export_job` 获取 `jobId` +2. 按渐进式退避策略轮询 `query_export_job` 直至任务终态或超时 +3. 任务成功后取得 `downloadUrl`;若指定了 `--output`,自动 HTTP GET 下载 xlsx 到本地文件 + +**内置轮询策略(CLI 内实现,无需关心)**: +- 第 1~5 次:每次间隔 2 秒 +- 第 6~10 次:每次间隔 5 秒 +- 第 11~20 次:每次间隔 10 秒 +- 第 21~30 次:每次间隔 15 秒 +- **硬上限:最多轮询 30 次(约 5 分钟)**,超时后命令返回错误 + +**命令返回**: +- `--output` 未指定:进度日志 + 末尾输出 `jobId` 和 `downloadUrl`(链接有时效性,请尽快下载) +- `--output` 指定为文件路径:下载到该路径并输出 `导出完成: ` +- `--output` 指定为已存在目录:自动从 `downloadUrl` 推断文件名并保存到该目录下 + +**失败处理(命令内部已处理,Agent 仅需转述)**: +- MCP 返回 `FAILED`:命令立即返回错误并附带失败原因,**禁止自动重试 `dws sheet export`**,告知用户稍后再试 +- 轮询 30 次仍 `PROCESSING`:命令返回超时错误,告知用户稍后再试 + +**限制**:仅支持钉钉在线电子表格(alxs)→ xlsx。导出钉钉文字文档请使用 `doc` 产品对应的导出工具。 + +## 核心工作流 + +```bash +# ── 工作流 12: 导出表格为 xlsx(单命令一站式)── + +# 场景 A:仅获取下载链接(命令内部自动完成提交+轮询,最终返回 downloadUrl) +dws sheet export --node --format json +# 传入 URL 也可: +# dws sheet export --node "https://alidocs.dingtalk.com/i/nodes/" --format json + +# 场景 B:导出并自动下载为本地文件 +dws sheet export --node --output ./report.xlsx + +# 场景 C:下载到目录,自动按链接推断文件名 +dws sheet export --node --output ./ + +# 禁止在 Agent 侧实现任何轮询或重试,CLI 内部已按 2s/5s/10s/15s 渐进式退避自动完成(最多 30 次)。 +# 若命令返回失败或超时,直接告知用户稍后再试,不要自动重调 dws sheet export。 +``` + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `export` | `downloadUrl`(未指定 --output)/ `导出完成: `(指定 --output) | 直接下发给用户或告知文件已保存到本地。命令内部已完成轮询,不要再调用其他 export 相关命令 | + +## 注意事项 + +- ★ `export` 仅支持钉钉在线电子表格(alxs)→ xlsx;传入钉钉文字文档会报 `invalidRequest.document.typeIllegal` +- ★ `export` 为单命令一站式,CLI 内部已自动完成「提交 → 渐进式退避轮询 → 可选下载」,**Agent 不得在外部实现轮询或重试**;命令返回成功后不再调用其他 export 相关命令 +- `export` 内置轮询策略:1~5 次间隔 2s、6~10 次间隔 5s、11~20 次间隔 10s、21~30 次间隔 15s,硬上限 30 次(约 5 分钟);超时后命令返回错误,告知用户稍后再试即可 +- ★ `export` 命令返回失败或超时时,**禁止自动重调 `dws sheet export`**;直接告知用户导出失败并建议稍后再试 +- `export` 未指定 `--output` 时,返回的 `downloadUrl` 具有时效性,获取后请尽快下载;若用户需要本地文件,优先直接传 `--output` 让 CLI 代为下载 +- `export` 的 `--output` 可为文件路径或已存在目录;为目录时自动从 `downloadUrl` 推断文件名,为文件路径时直接按该路径保存 +- 用户要求"导出表格/下载 xlsx"时,必须使用 `export` 单命令,禁止用 `range read` 读全量数据后自行拼 xlsx 模拟导出(服务端导出会保留格式/合并/公式等完整属性) diff --git a/skills/multi/dingtalk-sheet/references/sheet/sheet-filter-view.md b/skills/multi/dingtalk-sheet/references/sheet/sheet-filter-view.md new file mode 100644 index 00000000..631a6c6f --- /dev/null +++ b/skills/multi/dingtalk-sheet/references/sheet/sheet-filter-view.md @@ -0,0 +1,344 @@ +# 筛选视图 (filter-view) + +## 使用场景 + +### 筛选视图 + +用户说"筛选视图/查看筛选视图/有哪些筛选视图/筛选视图列表": +- 获取所有筛选视图 → `filter-view list` + +用户说"筛选视图详情/查看某个筛选视图/筛选视图信息/筛选视图配置": +- 获取单个筛选视图详情 → `filter-view info` + +用户说"创建筛选视图/新建筛选视图/添加筛选视图": +- 创建筛选视图 → `filter-view create` + +用户说"更新筛选视图/修改筛选视图/改筛选视图名称/改筛选视图范围": +- 更新筛选视图属性 → `filter-view update` + +用户说"删除筛选视图/移除筛选视图": +- 删除筛选视图 → `filter-view delete` + +用户说"设置筛选条件/添加筛选条件/配置筛选视图条件/按值筛选/按条件筛选/按颜色筛选": +- 设置筛选视图列条件 → `filter-view update-criteria` + +用户说"查看筛选条件/有哪些筛选条件/筛选视图设了什么条件/列出筛选条件": +- 列出所有列条件 → `filter-view list-criteria` +- 查看某一列的条件 → `filter-view get-criteria --column N` + +用户说"清除筛选条件/移除筛选条件/取消筛选条件": +- 清除筛选视图列条件 → `filter-view delete-criteria` +- 注意与 `filter-view delete`(删除整个筛选视图)区分:`delete-criteria` 仅清除指定列的条件,不删除筛选视图本身 + +## 命令详细参考 + +### 获取所有筛选视图 +``` +Usage: + dws sheet filter-view list [flags] +Example: + dws sheet filter-view list --node --sheet-id + dws sheet filter-view list --node "https://alidocs.dingtalk.com/i/nodes/" --sheet-id "Sheet1" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) +``` + +获取指定工作表的所有筛选视图列表,返回每个筛选视图的 ID、名称和范围信息。 +- **用途**:查看当前工作表上已创建的所有筛选视图,获取视图 ID、名称和范围。 +- **场景**:在对筛选视图进行 update / delete / update-criteria 等操作前,先用 list 获取可用的 filterViewId。 +- **区分**:筛选视图(filter-view)是个人化的数据过滤方式,与全局筛选不同。每个用户可以创建自己的筛选视图,互不影响原始数据。如果没有筛选视图,返回空列表。 + +### 创建筛选视图 +``` +Usage: + dws sheet filter-view create [flags] +Example: + # 创建不带筛选条件的筛选视图 + dws sheet filter-view create --node --sheet-id --name "我的视图" --range "A1:E10" + + # 创建带按值筛选条件的筛选视图 + dws sheet filter-view create --node --sheet-id --name "销售筛选" --range "A1:E10" \ + --criteria '[{"column":0,"filterType":"values","visibleValues":["销售部"]}]' + + # 创建带按条件筛选的筛选视图(大于等于 200000) + dws sheet filter-view create --node --sheet-id --name "高预算" --range "A1:C10" \ + --criteria '[{"column":1,"filterType":"condition","conditions":[{"operator":"greater-equal","value":"200000"}]}]' +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --name string 筛选视图名称 (必填) + --range string 筛选视图范围,A1 表示法,如 A1:E10 (必填) + --criteria string 筛选条件,JSON 数组 (可选) +``` + +在指定工作表中创建一个筛选视图。 +- **用途**:为指定数据区域创建一个可命名的个人化筛选视图,可选同时设置筛选条件。 +- **场景**:用户需要针对某个数据区域建立固定的筛选视角(如"高绩效员工""研发部数据"),方便反复查看。 +- **区分**:与全局筛选不同,筛选视图是个人化的,不影响其他用户看到的数据。如果只需创建视图不设条件,后续可通过 `update-criteria` 单独设置;如果要一步到位,可通过 `--criteria` 在创建时直接设置。 +`--criteria` 为 JSON 数组,每个元素包含 `column`(列偏移量,从 0 开始)和筛选条件字段。支持三种筛选类型: +- `values`:按值筛选,通过 `visibleValues` 指定允许显示的值列表 +- `condition`:按条件筛选,通过 `conditions` 指定条件列表(最多 2 个),每个条件包含 `operator` 和 `value`。支持的操作符(kebab-case):`equal`、`not-equal`、`contains`、`not-contains`、`starts-with`、`not-starts-with`、`ends-with`、`not-ends-with`、`greater`、`greater-equal`、`less`、`less-equal`。多条件之间通过 `conditionOperator` 指定逻辑关系:`and`(且,默认)或 `or`(或) +- `color`:按颜色筛选,通过 `backgroundColor` 或 `fontColor` 指定颜色值(十六进制,如 `#FF0000`),二选一 + +### 更新筛选视图属性 +``` +Usage: + dws sheet filter-view update [flags] +Example: + # 更新筛选视图名称 + dws sheet filter-view update --node --sheet-id --filter-view-id --name "新名称" + + # 更新筛选视图范围 + dws sheet filter-view update --node --sheet-id --filter-view-id --range "A1:F20" + + # 更新筛选条件 + dws sheet filter-view update --node --sheet-id --filter-view-id \ + --criteria '[{"column":1,"filterType":"condition","conditions":[{"operator":"greater","value":"100"}]}]' +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --filter-view-id string 筛选视图 ID (必填) + --name string 筛选视图新名称 + --range string 筛选视图新范围,A1 表示法 + --criteria string 筛选条件,JSON 数组 +``` + +更新筛选视图的名称、范围和/或筛选条件,`--name`、`--range`、`--criteria` 至少传入一个。 +- **用途**:修改已有筛选视图的名称、数据范围或筛选条件。 +- **场景**:数据区域扩展后需要扩大筛选视图范围,或重命名视图,或通过 `--criteria` 一次性批量更新多列筛选条件。 +- **区分**:`update` 可同时修改名称、范围和条件,适合批量更新;`update-criteria` 只能设置单列条件,适合精确控制某一列的筛选逻辑。`--criteria` 指定列的条件会被替换,未指定的列保持不变。 + +`--criteria` 为 JSON 数组,格式与 `filter-view create` 的 `--criteria` 相同,支持的筛选类型和操作符参见「创建筛选视图」说明。 + +### 删除筛选视图 + +> **CAUTION:** 不可逆操作 — 执行前必须向用户确认。 + +``` +Usage: + dws sheet filter-view delete [flags] +Example: + dws sheet filter-view delete --node --sheet-id --filter-view-id +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --filter-view-id string 筛选视图 ID (必填) +``` + +删除指定的筛选视图。 +- **用途**:永久删除一个不再需要的筛选视图及其所有筛选条件。 +- **场景**:筛选视图已过时或不再需要时,清理无用的视图。 +- **区分**:`delete` 删除整个筛选视图(包括所有列的条件),操作不可恢复;`delete-criteria` 只删除某一列的筛选条件,视图本身保留。此操作不影响全局筛选或其他筛选视图,也不影响原始数据。 + +### 更新筛选视图列条件 +``` +Usage: + dws sheet filter-view update-criteria [flags] +Example: + # 按值筛选:只显示"销售部"和"市场部" + dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ + --column 0 --filter-criteria '{"filterType":"values","visibleValues":["销售部","市场部"]}' + + # 按条件筛选:大于 100 + dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ + --column 2 --filter-criteria '{"filterType":"condition","conditions":[{"operator":"greater","value":"100"}]}' + + # 按条件筛选:大于等于 200000 + dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ + --column 1 --filter-criteria '{"filterType":"condition","conditions":[{"operator":"greater-equal","value":"200000"}]}' + + # 按条件筛选:小于 100 + dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ + --column 1 --filter-criteria '{"filterType":"condition","conditions":[{"operator":"less","value":"100"}]}' + + # 多条件筛选:大于等于 60 且 小于等于 90 + dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ + --column 2 --filter-criteria '{"filterType":"condition","conditionOperator":"and","conditions":[{"operator":"greater-equal","value":"60"},{"operator":"less-equal","value":"90"}]}' + + # 按颜色筛选:背景色为红色 + dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ + --column 1 --filter-criteria '{"filterType":"color","backgroundColor":"#FF0000"}' +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --filter-view-id string 筛选视图 ID (必填) + --column int 列偏移量,从 0 开始 (必填) + --filter-criteria string 筛选条件,JSON 对象 (必填) +``` + +更新筛选视图中某一列的筛选条件。 +- **用途**:为筛选视图的指定列创建或更新筛选条件,控制该列哪些数据行可见。 +- **场景**:只显示某些特定值的行(如"只看研发部")→ `filterType: values`;按数值条件筛选(如"绩效 ≥ 85")→ `filterType: condition` + `operator: greater-equal`;按文本条件筛选(如"名称包含关键字")→ `filterType: condition` + `operator: contains`。 +- **区分**:`update-criteria` 精确控制单列条件,适合逐列设置不同的筛选逻辑;`filter-view update --criteria` 可以批量更新多列条件;`delete-criteria` 是 `update-criteria` 的逆操作,删除指定列的条件。 + +`--column` 为列偏移量(从 0 开始),相对于筛选视图范围首列。 +例如筛选视图范围为 `B1:E10`,则 `--column 0` 代表 B 列,`--column 1` 代表 C 列。 + +`--filter-criteria` 为 JSON 对象,支持三种筛选类型: +- `values`:按值筛选,通过 `visibleValues` 指定允许显示的值列表 +- `condition`:按条件筛选,通过 `conditions` 指定条件列表(最多 2 个),每个条件包含 `operator` 和 `value`。支持的操作符:`equal`、`not-equal`、`contains`、`not-contains`、`starts-with`、`not-starts-with`、`ends-with`、`not-ends-with`、`greater`、`greater-equal`、`less`、`less-equal`。多条件之间通过 `conditionOperator` 指定逻辑关系:`and`(且,默认)或 `or`(或) +- `color`:按颜色筛选,通过 `backgroundColor` 或 `fontColor` 指定颜色值(十六进制,如 `#FF0000`),二选一 + +### 删除筛选视图列条件 +``` +Usage: + dws sheet filter-view delete-criteria [flags] +Example: + # 删除第 1 列(A 列)的筛选条件 + dws sheet filter-view delete-criteria --node --sheet-id --filter-view-id --column 0 + + # 删除第 3 列(C 列)的筛选条件 + dws sheet filter-view delete-criteria --node --sheet-id --filter-view-id --column 2 +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --filter-view-id string 筛选视图 ID (必填) + --column int 列偏移量,从 0 开始 (必填) +``` + +清除筛选视图中指定列的筛选条件。 +- **用途**:移除筛选视图中指定列的筛选条件,使该列不再参与过滤。 +- **场景**:之前通过 `update-criteria` 设置了某列的筛选条件,现在需要取消该列的筛选以显示全部数据。 +- **区分**:`delete-criteria` 只清除指定列的条件,筛选视图本身和其他列的条件保持不变;`delete` 会删除整个筛选视图。如果指定列没有设置筛选条件,调用此命令不会报错(幂等操作)。 + +### 获取单个筛选视图详情 +``` +Usage: + dws sheet filter-view info [flags] +Example: + # 查看指定筛选视图的详情 + dws sheet filter-view info --node --sheet-id --filter-view-id +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --filter-view-id string 筛选视图 ID (必填) +``` + +获取指定筛选视图的完整信息,包括 ID、名称、范围和筛选条件。 +- **用途**:查看某个筛选视图的当前配置,包括已设置的所有筛选条件详情。 +- **场景**:在修改或删除筛选视图前,先确认其当前状态;或在 `update-criteria` 后验证条件是否生效。 +- **区分**:`info` 返回单个视图的完整信息(含 criteria);`list` 返回所有视图的列表概要。`info` 需要指定 `--filter-view-id`,ID 可通过 `list` 获取。 +- **实现**:内部调用 `get_filter_views` 获取全部列表后按 ID 过滤。 + +### 列出筛选视图所有列条件 +``` +Usage: + dws sheet filter-view list-criteria [flags] +Example: + # 列出筛选视图的所有条件 + dws sheet filter-view list-criteria --node --sheet-id --filter-view-id +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --filter-view-id string 筛选视图 ID (必填) +``` + +列出指定筛选视图中已设置的所有列筛选条件。 +- **用途**:查看某个筛选视图当前设置了哪些列的筛选条件,包括每列的条件类型和具体规则。 +- **场景**:在管理筛选条件(修改/删除特定列条件)前,先了解当前视图有哪些条件;或排查筛选结果不符合预期时检查条件配置。 +- **区分**:`list-criteria` 返回所有列的条件(按列偏移量为 key 的对象);`get-criteria` 只返回指定列的条件。如果没有设置任何条件,返回空对象 `{}`。 +- **实现**:内部调用 `get_filter_views` 获取视图详情后提取 `criteria` 字段。 + +### 获取单列筛选条件 +``` +Usage: + dws sheet filter-view get-criteria [flags] +Example: + # 查看第 1 列(偏移量 0)的筛选条件 + dws sheet filter-view get-criteria --node --sheet-id --filter-view-id --column 0 + + # 查看第 3 列(偏移量 2)的筛选条件 + dws sheet filter-view get-criteria --node --sheet-id --filter-view-id --column 2 +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --filter-view-id string 筛选视图 ID (必填) + --column int 列偏移量,从 0 开始 (必填) +``` + +获取指定筛选视图中某一列的筛选条件详情。 +- **用途**:查看某个筛选视图中指定列当前设置的筛选条件,包括条件类型、运算符和比较值。 +- **场景**:在修改某列条件前,先查看其当前配置;或验证 `update-criteria` 后该列条件是否正确。 +- **区分**:`get-criteria` 只返回指定列的条件;`list-criteria` 返回所有列的条件。`--column` 为列偏移量(从 0 开始),相对于筛选视图范围首列。 +- **实现**:内部调用 `get_filter_views` 获取视图详情后按列偏移量过滤 `criteria` 中的对应条件。 + +## 核心工作流 + +```bash +# ── 工作流 11: 筛选视图管理 ── + +# 1. 获取工作表列表 +dws sheet list --node -f json + +# 2. 查看已有筛选视图 +dws sheet filter-view list --node --sheet-id -f json + +# 3. 创建筛选视图(不带条件) +dws sheet filter-view create --node --sheet-id \ + --name "我的筛选" --range "A1:E100" -f json + +# 4. 为筛选视图设置列条件(按值筛选) +dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ + --column 0 --filter-criteria '{"filterType":"values","visibleValues":["销售部","市场部"]}' -f json + +# 5. 为筛选视图设置列条件(按条件筛选) +dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ + --column 2 --filter-criteria '{"filterType":"condition","conditions":[{"operator":"greater","value":"100"}]}' -f json + +# 6. 更新筛选视图名称和范围 +dws sheet filter-view update --node --sheet-id --filter-view-id \ + --name "销售数据筛选" --range "A1:F200" -f json + +# 7. 清除某列的筛选条件 +dws sheet filter-view delete-criteria --node --sheet-id --filter-view-id \ + --column 0 -f json + +# 8. 删除筛选视图 +dws sheet filter-view delete --node --sheet-id --filter-view-id -f json +``` + +```bash +# ── 工作流 11b: 创建带条件的筛选视图(一步完成) ── + +# 创建筛选视图时直接指定筛选条件 +dws sheet filter-view create --node --sheet-id \ + --name "高销售额视图" --range "A1:E100" \ + --criteria '[{"column":0,"filterType":"values","visibleValues":["销售部"]},{"column":2,"filterType":"condition","conditions":[{"operator":"greater","value":"50000"}]}]' \ + -f json +``` + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `filter-view list` | `filterViews` 筛选视图列表(含 `id`、`name`、`range`) | 获取 filterViewId 用于 info / update / delete / update-criteria / delete-criteria / list-criteria / get-criteria | +| `filter-view info` | `id`、`name`、`range`、`criteria` | 查看单个视图完整配置,确认条件是否生效 | +| `filter-view create` | `id` 筛选视图 ID、`name`、`range` | 用于后续 update / delete / update-criteria / delete-criteria 的 --filter-view-id | +| `filter-view update` | `id`、`name`、`range`、`criteria` | 确认更新结果 | +| `filter-view delete` | `id` 被删除的筛选视图 ID | 确认删除完成 | +| `filter-view update-criteria` | `id` 筛选视图 ID | 确认条件设置完成 | +| `filter-view delete-criteria` | `id` 筛选视图 ID | 确认条件清除完成 | +| `filter-view list-criteria` | 所有列条件(按列偏移量为 key 的对象) | 了解当前视图已设置哪些列的条件 | +| `filter-view get-criteria` | 指定列的条件详情(`filterType`、`conditions` 等) | 查看某列的具体筛选规则 | +| `list` | 工作表的 `sheetId` | info / range read / range update / find 的 --sheet-id | + +## 注意事项 + +- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) +- ★ **全局筛选(filter)与筛选视图(filter-view)的区别**:全局筛选影响所有协作者看到的数据展示,每个工作表最多一个;筛选视图是个人化的,互不影响。用户只说"筛选"时默认走 `filter` 系列 +- `filter-view list` 获取指定工作表的所有筛选视图列表,返回的 `id` 可用于后续 info / update / delete / update-criteria / delete-criteria / list-criteria / get-criteria 的 `--filter-view-id` +- `filter-view info` 获取单个筛选视图的完整信息(含 criteria),内部复用 `get_filter_views` MCP 按 ID 过滤 +- `filter-view list-criteria` 列出指定筛选视图已设置的所有列条件,返回按列偏移量为 key 的对象;无条件时返回空对象 `{}` +- `filter-view get-criteria` 获取指定列的条件详情,`--column` 为列偏移量(从 0 开始);该列无条件时返回错误提示 +- `filter-view create` 创建筛选视图时 `--range` 应包含表头行。`--criteria` 可选,不传则创建后无筛选条件,后续可通过 `filter-view update-criteria` 设置 +- `filter-view update` 的 `--name`、`--range`、`--criteria` 至少需要传入一个,未指定的字段保持不变 +- `filter-view update` 的 `--criteria` 中指定列的条件会被替换,未指定的列保持不变 +- `filter-view delete` 删除后该视图及其所有筛选条件将被永久移除,不可恢复 +- `filter-view delete` 不影响全局筛选或其他筛选视图 +- `filter-view update-criteria` 的 `--column` 为列偏移量(从 0 开始),相对于筛选视图范围首列。例如筛选视图范围为 `B1:E10`,则 `--column 0` 代表 B 列 +- `filter-view update-criteria` 设置条件后立即在该筛选视图中生效,仅影响当前视图,不影响全局筛选或其他筛选视图 +- `filter-view update-criteria` 的 `--filter-criteria` 中 `conditions` 最多 2 个条件,多条件之间通过 `conditionOperator` 指定逻辑关系(`and` 或 `or`) +- `filter-view delete-criteria` 仅清除指定列的条件,不会删除整个筛选视图。如需删除整个筛选视图,请使用 `filter-view delete` +- `filter-view delete-criteria` 如果指定列没有设置筛选条件,调用不会报错 +- 筛选视图相关操作需要"可阅读"权限(list / info / list-criteria / get-criteria)或"可编辑"权限(create / update / delete / update-criteria / delete-criteria),不支持跨组织操作 diff --git a/skills/multi/dingtalk-sheet/references/sheet/sheet-filter.md b/skills/multi/dingtalk-sheet/references/sheet/sheet-filter.md new file mode 100644 index 00000000..0e65607b --- /dev/null +++ b/skills/multi/dingtalk-sheet/references/sheet/sheet-filter.md @@ -0,0 +1,181 @@ +# 全局筛选 (filter) + +## 使用场景 + +### 筛选视图 + +用户说"筛选/过滤/只看某些值/只显示满足条件的行/筛选数据/创建筛选/删除筛选/设置筛选条件/清除筛选/排序": +- 查看当前筛选 → `filter get` +- 创建筛选 → `filter create` +- 删除筛选 → `filter delete` +- 批量设置多列条件 → `filter update` +- 清除某一列条件 → `filter clear-criteria` +- 按列排序 → `filter sort` +- **区分全局筛选与筛选视图**:如果用户说"筛选视图"则走 `filter-view` 系列;如果只说"筛选/过滤/只看"则默认走全局 `filter` 系列 +- **禁止替代方案**:当用户要求"筛选/只看/仅保留某些行"时,必须通过 `filter create` / `filter update` 创建真实的筛选器。禁止用"删除不符合条件的行"或"新建工作表只放符合条件的行"来代替——这些做法会让原数据丢失或不可恢复 + +## 命令详细参考 + +### 获取筛选信息 +``` +Usage: + dws sheet filter get [flags] +Example: + dws sheet filter get --node --sheet-id + dws sheet filter get --node "https://alidocs.dingtalk.com/i/nodes/" --sheet-id "Sheet1" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) +``` + +获取指定工作表的全局筛选信息,返回筛选范围和各列的筛选条件详情。 +- **用途**:查看当前工作表上是否存在全局筛选及其配置。 +- **场景**:在修改或删除筛选前,先读取当前筛选配置;创建筛选前先确认是否已存在(每个工作表只能有一个筛选)。 +- **区分**:全局筛选(filter)影响所有协作者看到的数据展示;筛选视图(filter-view)是个人化的。 +- **返回**:`range`(筛选范围,A1 表示法)和 `columnFilterCriteria`(各列条件,key 为列偏移量)。如果未设置筛选,返回筛选信息为空。 + +### 创建筛选 +``` +Usage: + dws sheet filter create [flags] +Example: + # 创建筛选框架(不设条件) + dws sheet filter create --node --sheet-id --range "A1:E100" + + # 创建筛选并同时设置条件(按值筛选) + dws sheet filter create --node --sheet-id --range "A1:E100" --criteria '[{"column":1,"filterType":"values","visibleValues":["北京","上海"]}]' + + # 创建筛选并设置条件筛选 + dws sheet filter create --node --sheet-id --range "A1:E100" --criteria '[{"column":2,"filterType":"condition","conditions":[{"operator":"greater","value":"100"}]}]' +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 筛选范围,A1 表示法,须包含表头行 (必填) + --criteria string 筛选条件 JSON 数组 (可选) +``` + +在工作表中创建全局筛选。 +- **用途**:为工作表建立筛选器,使数据可按条件过滤展示。 +- **约束**:每个工作表只能有一个全局筛选,已存在时会报错。应先 `filter get` 确认不存在后再创建。 +- **range 规范**:必须包含表头行(如 `A1:E100`),不能只包含数据行。 +- **criteria 格式**:JSON 数组,每个元素含 `column`(列偏移量,从 0 开始)和筛选条件字段。不传则仅创建空筛选框架,后续可通过 `filter update` 设置条件。 + +### 删除筛选 + +> **CAUTION:** 不可逆操作 — 执行前必须向用户确认。 + +``` +Usage: + dws sheet filter delete [flags] +Example: + dws sheet filter delete --node --sheet-id --yes +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) +``` + +删除工作表的全局筛选。 +- **用途**:移除筛选器,所有被隐藏的行将重新显示。 +- **不可逆**:删除后所有筛选条件丢失,需重新创建。 +- **前置**:工作表没有筛选时调用会报错,应先 `filter get` 确认存在。 + +### 批量更新筛选条件 +``` +Usage: + dws sheet filter update [flags] +Example: + # 同时设置多列的筛选条件 + dws sheet filter update --node --sheet-id --criteria '[{"column":0,"filterType":"values","visibleValues":["已完成","进行中"]},{"column":2,"filterType":"condition","conditions":[{"operator":"greater","value":"50"}]}]' + + # 按颜色筛选 + dws sheet filter update --node --sheet-id --criteria '[{"column":1,"filterType":"color","backgroundColor":"#FF0000"}]' +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --criteria string 筛选条件 JSON 数组 (必填) +``` + +批量更新筛选条件,可同时设置多列的筛选条件。 +- **用途**:一次性设置或替换多列的筛选条件。 +- **前置**:工作表必须已创建筛选(通过 `filter create`)。 +- **覆盖式**:指定列的条件会被替换,未指定的列保持不变。如只想修改某一列,建议先 `filter get` 读取现有配置。 +- **criteria 格式**:JSON 数组,支持三种 `filterType`: + - `values`:按值筛选,指定 `visibleValues` 数组 + - `condition`:按条件筛选,指定 `conditions` 数组(最多 2 个)和可选的 `conditionOperator`(`and`/`or`) + - `color`:按颜色筛选,指定 `backgroundColor` 或 `fontColor`(二选一) + +### 清除单列筛选条件 +``` +Usage: + dws sheet filter clear-criteria [flags] +Example: + # 清除第 2 列(B 列)的筛选条件 + dws sheet filter clear-criteria --node --sheet-id --column 1 + + # 清除第 1 列(A 列)的筛选条件 + dws sheet filter clear-criteria --node --sheet-id --column 0 +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --column number 列偏移量,从 0 开始 (必填) +``` + +清除筛选中某一列的筛选条件。 +- **用途**:移除某列的筛选条件,该列不再参与筛选计算。 +- **区分**:仅清除指定列的条件,不删除整个筛选。如需删除整个筛选,使用 `filter delete`。 +- **幂等**:指定列没有设置筛选条件时调用不会报错。 + +### 筛选排序 +``` +Usage: + dws sheet filter sort [flags] +Example: + # 按第 1 列(A 列)升序排序 + dws sheet filter sort --node --sheet-id --column 0 --ascending + + # 按第 3 列(C 列)降序排序 + dws sheet filter sort --node --sheet-id --column 2 --ascending=false +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --column number 排序列偏移量,从 0 开始 (必填) + --ascending 是否升序,默认 true (可选) +``` + +对筛选范围内的数据按指定列排序。 +- **用途**:对数据行按某一列的值进行升序或降序排列。 +- **前置**:工作表必须已创建筛选(通过 `filter create`)。 +- **注意**:排序会实际改变工作表中数据行的物理顺序,不可撤销。 +- **column**:列偏移量从 0 开始,相对于筛选范围首列。 + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `filter get` | `range`(筛选范围)、`columnFilterCriteria`(各列条件) | 查看当前筛选配置,确认筛选是否存在 | +| `filter create` | 筛选创建成功的确认 | 确认筛选已建立,后续可通过 `filter update` 设置条件 | +| `filter delete` | 删除成功的确认 | 确认筛选已删除 | +| `filter update` | 更新成功的确认 | 确认条件已设置 | +| `filter clear-criteria` | 清除成功的确认 | 确认指定列的条件已清除 | +| `filter sort` | 排序成功的确认 | 确认排序已完成 | +| `list` | 工作表的 `sheetId` | info / range read / range update / find 的 --sheet-id | + +## 注意事项 + +- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) +- ★ **全局筛选(filter)与筛选视图(filter-view)的区别**:全局筛选影响所有协作者看到的数据展示,每个工作表最多一个;筛选视图是个人化的,互不影响。用户只说"筛选"时默认走 `filter` 系列 +- `filter get` 获取工作表的全局筛选信息,返回 `range`(筛选范围)和 `columnFilterCriteria`(各列条件)。无筛选时返回空 +- `filter create` 创建全局筛选时 `--range` 必须包含表头行(如 `A1:E100`),不能只包含数据行。每个工作表只能有一个筛选,已存在时报错 +- `filter create` 的 `--criteria` 可选,不传则仅创建空筛选框架,后续通过 `filter update` 设置条件 +- `filter delete` 删除后所有筛选条件丢失且所有被隐藏行重新显示,不可恢复 +- `filter delete` 工作表没有筛选时调用会报错,应先 `filter get` 确认存在 +- `filter update` 是覆盖式:指定列的条件会被替换,未指定的列保持不变。如只想修改某一列,建议先 `filter get` 读取现有配置再 patch +- `filter update` 前置:工作表必须已创建筛选 +- `filter clear-criteria` 仅清除指定列的条件,不删除整个筛选。指定列无条件时不报错(幂等) +- `filter sort` 会实际改变数据行的物理顺序,不可撤销。前置:工作表必须已创建筛选 +- ★ **筛选操作规范**(参照飞书 core-operations): + - 当用户要求"筛选/只看/仅保留 X"时,**必须**通过 `filter create` / `filter update` 创建真实的筛选器。**禁止**用"删除不符合条件的行"或"新建工作表只放符合条件的行"来代替 + - 创建/更新筛选后**必须** `filter get` 回读验证配置正确 + - 更新已有筛选前先 `filter get` 读取当前配置,确认目标存在且了解现有条件后再操作 + - 筛选条件的列索引(`column`)必须与实际数据列精确对应,不要凭猜测填写 + - 筛选不支持正则表达式,传入正则会当成普通文本处理 diff --git a/skills/multi/dingtalk-sheet/references/sheet/sheet-media-image.md b/skills/multi/dingtalk-sheet/references/sheet/sheet-media-image.md new file mode 100644 index 00000000..40cf3640 --- /dev/null +++ b/skills/multi/dingtalk-sheet/references/sheet/sheet-media-image.md @@ -0,0 +1,239 @@ +# 媒体上传与图片 (media & image) + +## 使用场景 + +### 媒体上传 + +用户说"上传附件/传文件到表格/上传文件到表格/上传到表格": +- 上传附件 → `media-upload`(需表格 ID 或 URL + 本地文件路径) +- 用户指定了上传后的名称 → `media-upload --name "自定义名称"` +- `media-upload` 的 `--name` 参数用于指定附件在表格中显示的名称(不改变本地文件名);不传时默认使用本地文件名 + +用户说"写入图片/插入图片/加图片/放图片到单元格/嵌入图片到表格": +- 写入图片 → `write-image`(需表格 ID + 工作表 ID + 单元格范围 + 本地图片路径) +- 禁止使用 `range update` 写入图片,因为 `update_range` 的 MCP 工具不支持图片类型参数,调用必定失败。必须使用 `write-image` 命令 +- 用户指定了图片尺寸 → `write-image --width N --height M` + +### 浮动图片 + +用户说"浮动图片/悬浮图片/在表格上放一张图/加个浮动的图": +- 创建浮动图片 → 先 `media-upload` 上传图片获取 `resourceUrl`,再 `create-float-image` +- 浮动图片悬浮于单元格之上,不占用单元格内容,与 `write-image`(写入单元格内部的图片)不同 + +用户说"查看浮动图片/有哪些浮动图片/浮动图片列表": +- 列出所有浮动图片 → `list-float-images` +- 查看某个浮动图片详情 → `get-float-image` + +用户说"移动浮动图片/调整浮动图片大小/修改浮动图片/更新浮动图片": +- 更新浮动图片属性 → `update-float-image`(可更新锚点位置、尺寸、偏移量、图片资源路径) + +用户说"删除浮动图片/移除浮动图片": +- 删除浮动图片 → `delete-float-image` + +关键区分:`write-image`(单元格内嵌图片,占据单元格内容)vs `create-float-image`(浮动图片,悬浮于单元格之上,不占内容) + +## 命令详细参考 + +### 上传附件到表格 +``` +Usage: + dws sheet media-upload [flags] +Example: + dws sheet media-upload --node --file ./report.pdf + dws sheet media-upload --node --file ./data.bin --name "数据文件.dat" --mime-type application/octet-stream +Flags: + --node string 目标表格文档的标识,支持传入 URL 或 ID (必填) + --file string 本地文件路径 (必填) + --name string 附件显示名称 (默认使用文件名) + --mime-type string 文件 MIME 类型 (默认根据扩展名推断) +``` + +### 上传图片并写入表格单元格 +``` +Usage: + dws sheet write-image [flags] +Example: + dws sheet write-image --node --sheet-id --range A1:A1 --file ./chart.png + dws sheet write-image --node --sheet-id --range B2:B2 --file ./logo.png --width 200 --height 100 +Flags: + --node string 目标表格文档的标识,支持传入 URL 或 ID (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 目标单元格区域地址,如 A1:A1 (必填) + --file string 本地图片文件路径 (必填) + --name string 图片显示名称 (默认使用文件名) + --mime-type string 文件 MIME 类型 (默认根据扩展名推断) + --width int 图片显示宽度 (可选) + --height int 图片显示高度 (可选) +``` + +### 创建浮动图片 +``` +Usage: + dws sheet create-float-image [flags] +Example: + # 先上传图片获取 resourceUrl + dws sheet media-upload --node --file ./chart.png + # 输出: resourceUrl: /core/api/resources/img/xxxx... + + # 再创建浮动图片 + dws sheet create-float-image --node --sheet-id \ + --src "/core/api/resources/img/xxxx..." --range A1 --width 400 --height 300 + + # 带偏移量 + dws sheet create-float-image --node --sheet-id \ + --src "/core/api/resources/img/xxxx..." --range B2 --width 200 --height 150 --offset-x 10 --offset-y 20 +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --src string 图片资源路径,通过 media-upload 获取的 resourceUrl (必填) + --range string 锚点单元格,A1 表示法,如 A1、B3 (必填) + --width int 图片宽度,像素,正整数 (必填) + --height int 图片高度,像素,正整数 (必填) + --offset-x int 水平偏移量,像素 (默认 0) + --offset-y int 垂直偏移量,像素 (默认 0) +``` + +浮动图片悬浮于单元格之上,不占用单元格内容,可自由定位和调整大小。 +- `--src` 必须是 `media-upload` 返回的 `resourceUrl`(格式为 `/core/api/resources/img/...`),不能直接传外部 URL +- `--range` 使用 A1 表示法指定锚点单元格(如 `A1`、`B3`),支持带工作表前缀(如 `Sheet1!A1`) +- `--width` / `--height` 为必填,单位像素,必须为正整数 +- `--offset-x` / `--offset-y` 表示相对锚点单元格左上角的偏移量(像素),默认 0,不能为负数 + +### 获取浮动图片详情 +``` +Usage: + dws sheet get-float-image [flags] +Example: + dws sheet get-float-image --node --sheet-id --float-image-id +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --float-image-id string 浮动图片 ID (必填) +``` + +获取单个浮动图片的详细信息,包括 ID、图片资源路径、锚点位置、尺寸和偏移量。 +`--float-image-id` 可通过 `list-float-images` 获取。 + +### 列出工作表所有浮动图片 +``` +Usage: + dws sheet list-float-images [flags] +Example: + dws sheet list-float-images --node --sheet-id +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) +``` + +列出指定工作表中所有浮动图片,返回 `floatImages` 数组和 `totalCount`。 + +### 更新浮动图片属性 +``` +Usage: + dws sheet update-float-image [flags] +Example: + # 移动浮动图片到新位置 + dws sheet update-float-image --node --sheet-id --float-image-id --range C5 + + # 调整尺寸 + dws sheet update-float-image --node --sheet-id --float-image-id --width 600 --height 400 + + # 替换图片(需先 media-upload 新图片获取 resourceUrl) + dws sheet update-float-image --node --sheet-id --float-image-id \ + --src "/core/api/resources/img/xxxx..." +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --float-image-id string 浮动图片 ID (必填) + --src string 新的图片资源路径,通过 media-upload 获取的 resourceUrl + --range string 新的锚点单元格,A1 表示法 + --width int 新的图片宽度,像素 + --height int 新的图片高度,像素 + --offset-x int 新的水平偏移量,像素 + --offset-y int 新的垂直偏移量,像素 +``` + +更新浮动图片的属性,`--src` / `--range` / `--width` / `--height` / `--offset-x` / `--offset-y` 至少传入一个。 +`--float-image-id` 可通过 `list-float-images` 获取。 + +### 删除浮动图片 +``` +Usage: + dws sheet delete-float-image [flags] +Example: + dws sheet delete-float-image --node --sheet-id --float-image-id +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --float-image-id string 浮动图片 ID (必填) +``` + +删除指定的浮动图片,操作不可恢复。`--float-image-id` 可通过 `list-float-images` 获取。 + +## 核心工作流 + +```bash +# ── 工作流 9: 上传附件到表格 ── + +# 1. 基本用法: 上传本地文件到表格 +dws sheet media-upload --node --file ./report.pdf -f json + +# 2. 自定义附件显示名称 (--name 指定上传后在表格中显示的名称) +dws sheet media-upload --node --file ./data.csv --name "销售数据.csv" -f json + +# 3. 指定 MIME 类型 (文件扩展名无法推断时) +dws sheet media-upload --node --file ./data.bin --name "导出数据.dat" --mime-type application/octet-stream -f json + +# 4. 完整流程: 创建表格 → 上传附件 +dws sheet create --name "项目资料" -f json +# 提取 nodeId 后: +dws sheet media-upload --node --file ./design.pdf -f json +dws sheet media-upload --node --file ./timeline.xlsx --name "项目时间线.xlsx" -f json + +# ── 工作流 10: 写入图片到表格单元格 ── + +# 1. 基本用法: 写入图片到指定单元格 +dws sheet write-image --node --sheet-id --range A1:A1 --file ./chart.png -f json + +# 2. 指定显示尺寸 +dws sheet write-image --node --sheet-id --range B2:B2 --file ./logo.png --width 200 --height 100 -f json + +# 3. 自定义图片名称 +dws sheet write-image --node --sheet-id --range C3:C3 --file ./photo.jpg --name "产品图.jpg" -f json + +# 4. 完整流程: 创建表格 → 写表头 → 写入图片 +dws sheet create --name "产品目录" -f json +# 提取 nodeId 后: +dws sheet range update --node --sheet-id Sheet1 --range "A1:B1" --values '[["产品名称","产品图片"]]' -f json +dws sheet range update --node --sheet-id Sheet1 --range "A2:A2" --values '[["MacBook Pro"]]' -f json +dws sheet write-image --node --sheet-id Sheet1 --range B2:B2 --file ./macbook.png --width 150 --height 100 -f json +``` + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `media-upload` | `resourceId`、`resourceUrl` | 附件已上传到表格;`resourceUrl` 可用于 `create-float-image` 的 `--src` | +| `write-image` | `resourceId` | 图片已写入指定单元格 | +| `create-float-image` | `floatImage`(含 `id`、`src`、`range`、`width`、`height`、`offsetX`、`offsetY`) | `id` 用于后续 get / update / delete 的 `--float-image-id` | +| `get-float-image` | `floatImage`(完整信息) | 查看单个浮动图片详情 | +| `list-float-images` | `floatImages` 数组、`totalCount` | 获取所有浮动图片的 `id`,用于后续操作 | +| `update-float-image` | `floatImage`(更新后的完整信息) | 确认更新结果 | +| `delete-float-image` | `message` | 确认删除完成 | +| `list` | 工作表的 `sheetId` | info / range read / range update / find 的 --sheet-id | + +## 注意事项 + +- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) +- `media-upload` 是两步自动完成的流程 (获取附件上传凭证 → OSS 上传),无需手动分步操作 +- `write-image` 是三步自动完成的流程 (获取附件上传凭证 → OSS 上传 → 写入图片到单元格),无需手动分步操作 +- ★ 向表格单元格中写入图片必须使用 `write-image`,禁止使用 `range update`。`range update` 底层调用的 `update_range` MCP 工具不支持图片类型参数,调用会失败 +- `write-image` 与 `media-upload` 的区别:`media-upload` 仅上传附件到表格获取 resourceId;`write-image` 在上传后还会将图片写入指定单元格 +- `create-float-image` 创建浮动图片前必须先通过 `media-upload` 上传图片获取 `resourceUrl`,再将其作为 `--src` 传入。`--src` 的格式为 `/core/api/resources/img/...`,不能直接传外部 URL +- `create-float-image` 的 `--range` 使用 A1 表示法指定锚点单元格(如 `A1`、`B3`),支持带工作表前缀(如 `Sheet1!A1`) +- `create-float-image` 的 `--width` / `--height` 为必填,单位像素,必须为正整数;`--offset-x` / `--offset-y` 可选,默认 0,不能为负数 +- `write-image`(单元格内嵌图片)vs `create-float-image`(浮动图片):`write-image` 将图片写入单元格内部,占据单元格内容;`create-float-image` 创建悬浮于单元格之上的浮动图片,不占用单元格内容,可自由调整位置和大小 +- ★ **浮动图片用 `create-float-image` 不用 `write-image`**:两者用途不同——`write-image` 写入单元格内部,`create-float-image` 创建悬浮于单元格之上的浮动图片;`--src` 必须来自 `media-upload` 的 `resourceUrl` +- `update-float-image` 的 `--src` / `--range` / `--width` / `--height` / `--offset-x` / `--offset-y` 至少必须提供一个 +- `list-float-images` 返回 `floatImages` 数组和 `totalCount`,每个元素包含 `id`(用于后续 get / update / delete) +- `delete-float-image` 操作不可恢复,删除后图片将从工作表中移除 diff --git a/skills/multi/dingtalk-sheet/references/sheet/sheet-range-operations.md b/skills/multi/dingtalk-sheet/references/sheet/sheet-range-operations.md new file mode 100644 index 00000000..36d92a03 --- /dev/null +++ b/skills/multi/dingtalk-sheet/references/sheet/sheet-range-operations.md @@ -0,0 +1,153 @@ +# 区域操作 + +## 使用场景 + +用户说"清空/清除区域/擦除内容/清除格式": +- 清除区域 → `range clear` +- 仅清除值 → `range clear --type content`(默认) +- 仅清除格式 → `range clear --type format` +- 全部清除 → `range clear --type all` +- 请勿用 `range update` 写入空字符串来模拟清空,`range clear` 更简洁且支持按类型清除 + +用户说"排序/给数据排序/按某列排序/升序/降序": +- 区域排序 → `range sort` +- **排序前必须先 `range read` 前 3-5 行**:读取排序范围的前几行(如范围是 A1:D100 则读 A1:D5),对比首行与后续行的模式来判断是否有表头: + - 首行全文本 + 后续行含数字/日期 → 有表头,加 `--has-header` + - 首行与后续行模式一致(都是数字或都是文本) → 无表头,不加 + - 首行值语义像列标题(如"姓名""金额""日期")且与后续行明显不同 → 有表头 + 禁止不读就排——表头误排入数据是不可撤销的破坏性操作 +- 请勿用 `range read` 读取数据后客户端排序再 `range update` 写回,`range sort` 是服务端原子操作 + +用户说"自动填充/填充序列/向下填充/拖拽填充/序列递增": +- 自动填充 → `range fill` +- 请勿用 `range read` 读取源数据后手动计算规律再 `range update` 写入,`range fill` 支持服务端智能填充 + +用户说"复制区域/把这块数据复制到/复制到另一个工作表": +- 复制区域 → `range copy-to` +- 跨工作表 → `range copy-to --target-sheet-id Sheet2` 或 `--target-range "Sheet2!A1"` +- 请勿用 `range read` + `range update` 读取再写入来模拟复制,`range copy-to` 是原子操作,保留公式引用调整 + +用户说"移动区域/把数据移到/剪切粘贴/移到另一个工作表": +- 移动区域 → `range move-to` +- 跨工作表 → `range move-to --target-sheet-id Sheet2` 或 `--target-range "Sheet2!A1"` +- 请勿用 `range read` + `range update` + `range clear` 读取-写入-清空来模拟移动,`range move-to` 是原子操作 + +## 命令详细参考 + +### 清除区域 +``` +Usage: + dws sheet range clear [flags] +Example: + dws sheet range clear --node --sheet-id --range "A1:B3" + dws sheet range clear --node --sheet-id --range "A1:B3" --type format + dws sheet range clear --node --sheet-id --range "A1:B3" --type all +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 清除范围,A1 表示法 (必填) + --type string 清除类型: content(仅值,默认) / format(仅格式) / all(全部) +``` + +### 区域排序 +``` +Usage: + dws sheet range sort [flags] +Example: + dws sheet range sort --node --sheet-id --range "A1:D10" \ + --sort-keys '[{"column":"A","ascending":true}]' + dws sheet range sort --node --sheet-id --range "A1:D10" \ + --sort-keys '[{"column":"A","ascending":true},{"column":"C","ascending":false}]' --has-header +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 排序范围,A1 表示法 (必填) + --sort-keys string 排序规则 JSON 数组 (必填) + --has-header 首行是否为表头(不参与排序) +``` + +`--sort-keys` 格式:`[{"column":"A","ascending":true}]`,`column` 使用字母列名(如 "A"、"B"、"AA")。多级排序按数组顺序优先级递减。 + +### 区域自动填充 +``` +Usage: + dws sheet range fill [flags] +Example: + dws sheet range fill --node --sheet-id \ + --source-range "A1:A5" --target-range "A6:A20" + dws sheet range fill --node --sheet-id \ + --source-range "A1:A5" --target-range "A6:A20" --fill-type copy +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --source-range string 源数据范围,A1 表示法 (必填) + --target-range string 目标填充范围,A1 表示法 (必填) + --fill-type string 填充类型: series(序列,默认) / copy(复制) / onlystyle(仅格式) / withoutstyle(仅值) +``` + +目标范围须与源范围在行或列维度对齐(不支持对角填充)。 + +### 复制区域 +``` +Usage: + dws sheet range copy-to [flags] +Example: + dws sheet range copy-to --node --sheet-id \ + --source-range "A1:C5" --target-range "D1" + dws sheet range copy-to --node --sheet-id \ + --source-range "A1:C5" --target-range "A1" --target-sheet-id "Sheet2" + dws sheet range copy-to --node --sheet-id \ + --source-range "A1:C5" --target-range "D1" --paste-type values +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 源工作表 ID 或名称 (必填) + --source-range string 源范围,A1 表示法 (必填) + --target-range string 目标位置,A1 表示法 (必填) + --target-sheet-id string 目标工作表 ID 或名称(可选,不传则复制到同一工作表) + --paste-type string 粘贴类型: values(仅值) / formulas(仅公式) / formats(仅格式) / all(全部,默认) +``` + +支持跨工作表复制,两种方式指定目标工作表: +- `--target-sheet-id "Sheet2"` 显式指定 +- `--target-range "Sheet2!A1"` 在目标范围中携带工作表前缀 + +源和目标范围不能重叠(同表时)。 + +### 移动区域 +``` +Usage: + dws sheet range move-to [flags] +Example: + dws sheet range move-to --node --sheet-id \ + --source-range "A1:C5" --target-range "D1" + dws sheet range move-to --node --sheet-id \ + --source-range "A1:C5" --target-range "A1" --target-sheet-id "Sheet2" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 源工作表 ID 或名称 (必填) + --source-range string 源范围,A1 表示法 (必填) + --target-range string 目标位置,A1 表示法 (必填) + --target-sheet-id string 目标工作表 ID 或名称(可选,不传则移动到同一工作表) +``` + +支持跨工作表移动,两种方式指定目标工作表: +- `--target-sheet-id "Sheet2"` 显式指定 +- `--target-range "Sheet2!A1"` 在目标范围中携带工作表前缀 + +源和目标范围不能重叠(同表时)。移动后源区域将被清空。 + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `list` | 工作表的 `sheetId` | range clear / range sort / range fill / range copy-to / range move-to 的 --sheet-id | + +## 注意事项 + +- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询真实的 `sheetId` / 工作表名称后再调用,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等);用户仅给出工作表名称时,也应通过 `list` 校验该名称是否存在,避免名称大小写或拼写不一致导致失败 +- ★ **清空区域用 `range clear` 不用 `range update`**:`range clear` 支持按类型(值/格式/全部)清除,比手动构造全空数组更简洁可靠 +- ★ **复制区域用 `range copy-to` 不用 `range read` + `range update`**:原子操作,保留公式引用自动调整,支持跨工作表 +- ★ **移动区域用 `range move-to` 不用 `range read` + `range update` + `range clear`**:原子操作,源区域自动清空,支持跨工作表 +- ★ **排序用 `range sort` 不用 `range read` + 客户端排序 + `range update`**:服务端原子操作,支持多级排序 +- ★ **排序前必须 `range read` 前几行判断表头**:读取排序范围前 3-5 行,对比首行与后续行的数据模式(类型、语义)来判断是否有表头。禁止不读就排,表头被排入数据不可撤销 +- ★ **填充用 `range fill` 不用 `range read` + 手动计算 + `range update`**:服务端智能填充,支持序列递增、公式扩展等 diff --git a/skills/multi/dingtalk-sheet/references/sheet/sheet-read-data.md b/skills/multi/dingtalk-sheet/references/sheet/sheet-read-data.md new file mode 100644 index 00000000..db0be1bd --- /dev/null +++ b/skills/multi/dingtalk-sheet/references/sheet/sheet-read-data.md @@ -0,0 +1,204 @@ +# 数据读取 + +## 使用场景 + +用户说"读数据/看表格内容": +- 快速查看纯值数据、批量处理、大表分批读 → `csv-get`(token 消耗低,防爆保护) +- 需要结构化信息(值+样式+数据验证+富文本+单元格级超链接)、查看公式或原始值 → `range read` +- 需要查看合并单元格 / 表头合并结构 → `sheet info`,读取返回的 `mergedRanges`;不要在 `csv-get` 或 `range read` 里找合并信息 + +## 命令选择 + +| 读取目的 | 推荐命令 | 说明 | +|---------|---------|------| +| 快速查看纯值、数据分析、大表分批读取 | `csv-get` | CSV 格式,token 消耗约为 JSON 的 1/3,内置 maxChars 防爆 | +| 查看数据验证配置(下拉/复选框) | `range read` | 返回 per-cell 结构,含 dataValidation | +| 查看单元格样式(背景色/字体/对齐等) | `range read` | 返回 per-cell 结构,含 cellStyles(仅显式设置的样式) | +| 查看单元格级超链接 | `range read` | 返回 per-cell 结构,含 hyperlink;富文本片段链接仍在 richText 内 | +| 查看公式文本 | `range read --value-render-option formula` | value 返回公式 | +| 获取原始值(数字/布尔而非格式化字符串) | `range read --value-render-option raw_value` | value 返回原始类型 | +| 查看合并单元格范围 | `sheet info` | 返回 `mergedRanges`,这是工作表结构信息,不属于单元格值读取 | + +## 命令详细参考 + +### 以 CSV 格式读取工作表数据(推荐) +``` +Usage: + dws sheet csv-get [flags] +Example: + dws sheet csv-get --node + dws sheet csv-get --node --sheet-id --range "A1:D10" + dws sheet csv-get --node --range "A1:Z500" --value-render-option raw_value + dws sheet csv-get --node --range "A1:D10" --max-chars 50000 +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (不传则默认第一个工作表) + --range string 读取范围,A1 表示法 (不传则读取全部非空数据) + --value-render-option string 取值模式: formatted_value(默认) | raw_value | formula + --max-chars int CSV 最大字符数 (默认 200000,超出截断) +``` + +**返回字段说明**: +- `csv` — CSV 文本,每逻辑行前加 `[row=N]` 前缀标注真实表格行号。行号一律从此前缀读取,禁止手算 +- `colIndices` — 列字母映射数组(如 `["A","B","C"]`)。定位列字母用 `colIndices[j]`,禁止手数逗号 +- `rowIndices` — 行号映射数组(如 `[1,2,3]`) +- `hasMore` — 是否因 maxChars 截断。为 true 时需要调整 `--range` 继续分页读取 + +`csv-get` 不返回合并单元格结构。若 CSV 中出现合并区域的非左上角单元格为空,不能据此判断该区域"无内容";需要先用 `dws sheet info --node --sheet-id --format json` 读取 `mergedRanges`,再结合左上角单元格理解合并区域语义。 + +**取值模式说明**: +| 模式 | 返回内容 | 适用场景 | +|------|---------|---------| +| `formatted_value` | 格式化展示值(如 ¥1,000.00、2025-06-01) | 只看数据 | +| `raw_value` | 原始值(如 1000、45808) | 数据处理、计算 | +| `formula` | 公式文本(如 =SUM(A1:A10)),无公式时回退原始值 | 查看/复制公式 | + +**大表分批读取**:当 `hasMore=true` 或数据量很大时,按行窗口分批: +- 先通过 `info` 获取 `lastNonEmptyRow` / `columnCount` 确定边界 +- 分批读取:`--range "A1:J500"`、`--range "A501:J1000"` …… +- 单次建议 ≤5000 单元格 + +### 读取工作表数据(per-cell 结构化信息) +``` +Usage: + dws sheet range read [flags] # 别名: dws sheet range get +Example: + dws sheet range read --node + dws sheet range read --node --sheet-id + dws sheet range read --node --sheet-id "Sheet1" --range "A1:D10" + dws sheet range read --node --range "Sheet1!A1:D10" + dws sheet range read --node --value-render-option raw_value + dws sheet range read --node --value-render-option formula + + # 使用 get 别名,与 read 等价 + dws sheet range get --node --sheet-id --range "A1:D10" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (不传则默认第一个工作表) + --range string 读取范围,A1 表示法 (如 A1:D10,不传则读取全部数据) + --value-render-option string 取值模式: formatted_value(默认) | raw_value | formula +``` + +**返回字段说明**: +- `cells` — 二维数组,第一维为行,第二维为列。每个元素为 per-cell 对象,字段如下: + +| 字段 | 类型 | 是否必有 | 说明 | +|------|------|---------|------| +| `value` | string / number / boolean / null | 始终存在 | 单元格值。`formatted_value` 模式为 string;`raw_value` 模式为原始类型(number/boolean/string/null);`formula` 模式为公式字符串或回退原始值 | +| `dataValidation` | object | 仅有数据验证时出现,无则省略 | 数据验证配置,见下表 | +| `hyperlink` | object | 仅有单元格级超链接时出现,无则省略 | 整格超链接,结构为 `{type, link, text?}`,见下表 | +| `richText` | object | 仅富文本单元格出现 | 富文本结构(含超链接、附件、图片、样式片段等),普通纯文本不含此字段 | +| `cellStyles` | object | 仅有显式设置的样式时出现;MCP 序列化层也可能返回全 null 空壳 | cell-level 样式,见下表。读取时只看非 null 字段;全 null 等同不存在 | + +`range read` / `range get` 不返回合并单元格结构。要看合并单元格,请先或另行调用 `dws sheet info --node --sheet-id --format json`,使用其中的 `mergedRanges`。 + +**dataValidation 结构**: + +| type | 字段 | 说明 | +|------|------|------| +| `dropdown` | `options: [{value: string, color?: string}]` | 下拉选项列表 | +| `dropdown` | `enableMultiSelect: boolean` | 是否允许多选 | +| `checkbox` | `checked: boolean` | 当前勾选状态 | + +**hyperlink 结构**: + +| type | 字段 | 说明 | +|------|------|------| +| `path` | `link` + 可选 `text` | 外部 URL 链接 | +| `sheet` | `link` + 可选 `text` | 工作表链接,`link` 为工作表 ID 或名称 | +| `range` | `link` + 可选 `text` | 单元格范围链接,`link` 为 A1 表示法,如 `Sheet1!A4` | + +**richText 结构**: + +`richText` 表示单元格内的富文本片段,常见结构为 `{type:"richText", texts:[...]}`。`texts` 数组内每个子项代表一个片段: + +| 子项 type | 常见字段 | 说明 | +|-----------|----------|------| +| `text` | `text` / `style` | 普通文本片段;`style` 是片段级样式 | +| `link` | `text` / `link` / `subType` / `style` | 富文本片段链接。`subType` 不存在时按 `path` 理解;`path` 表示外部 URL,`sheet` 表示工作表链接,`range` 表示单元格范围链接 | +| `attachment` | `text` / `resourceId` / `mimeType` / `size` | 附件片段 | +| `image` | `resourceId` / `resourceUrl` / `width` / `height` | 图片片段 | + +`richText.texts[].link.subType` 与 cell-level `hyperlink.type` 含义一致,但作用范围不同:`hyperlink` 是整个单元格可点击,richText `link` 只作用于该文本片段。读取到 `subType:"sheet"` 时,`link` 通常是真实工作表名称;读取到 `subType:"range"` 时,`link` 通常是 A1 范围(如 `Sheet2!A1:B20`)。不要把 richText 片段链接误当成整格 `hyperlink`。 + +**cellStyles 字段说明**(仅关注显式设置过的非 null 属性;未设置属性不存在或为 null,应忽略): + +| 字段 | 类型 | 说明 | +|------|------|------| +| `fontWeight` | string | `bold` / `normal` | +| `fontColor` | string | 字体颜色,`#RRGGBB` | +| `fontSize` | number | 字号 | +| `fontStyle` | string | `italic` / `normal` | +| `backgroundColor` | string | 背景色,`#RRGGBB` | +| `horizontalAlignment` | string | `left` / `center` / `right` / `general` | +| `verticalAlignment` | string | `top` / `middle` / `bottom` | +| `wordWrap` | string | `overflow` / `clip` / `autoWrap` | +| `numberFormat` | string | 数字格式代码,如 `@`、`#,##0.00`、`yyyy/m/d`;`@` 表示文本 | +| `textUnderline` | boolean | 下划线 | +| `textLineThrough` | boolean | 删除线 | + +**返回示例**: +```json +{ + "cells": [ + [ + {"value": "姓名", "cellStyles": {"fontWeight": "bold", "backgroundColor": "#FFF2CC"}}, + {"value": "状态", "cellStyles": {"fontWeight": "bold"}, "dataValidation": {"type": "dropdown", "options": [{"value": "进行中"}, {"value": "已完成", "color": "#52C41A"}], "enableMultiSelect": false}} + ], + [ + {"value": "张三"}, + {"value": "钉钉", "hyperlink": {"type": "path", "link": "https://dingtalk.com", "text": "钉钉"}} + ] + ], + "message": "Successfully retrieved cell data.", + "success": true +} +``` + +说明:第一行表头有 `cellStyles`(加粗 + 背景色),第二行第二格有单元格级 `hyperlink`。注意:MCP 平台序列化会将未设置的字段填充为 null(如 `"fontStyle": null`),读取时应忽略值为 null 的字段,仅关注非 null 的属性;如果 `cellStyles` 全字段都是 null,视同不存在。`richText` 字段同理——无富文本的普通单元格可能返回 `{"type": null, "texts": null}`,视同不存在。 + +**取值模式说明**: +| 模式 | value 返回内容 | 适用场景 | +|------|---------|---------| +| `formatted_value` | 格式化展示值(如 ¥1,000.00、2025-06-01) | 只看数据(默认) | +| `raw_value` | 原始值(如 1000、45808) | 数据处理、计算 | +| `formula` | 公式文本(如 =SUM(A1:A10)),无公式时回退原始值 | 查看/复制公式 | + +**超时处理建议**:读取大范围数据时若出现超时或响应过慢,请主动缩小 `--range` 查询范围,**建议单次读取的单元格数量控制在 5000 个以内**(例如 50 行 × 100 列、100 行 × 50 列)。对于大表可采用分页读取策略: +- 先通过 `info` 获取 `rowCount` / `lastNonEmptyRow` / `columnCount` 确定数据边界 +- 按行分批读取,如 `A1:J500`、`A501:J1000`、`A1001:J1500` …… +- 避免不传 `--range` 直接读取整个大工作表 + +## 核心工作流 + +```bash +# ── 工作流: 读取已有表格数据 ── + +# 1. 获取工作表列表 +dws sheet list --node --format json + +# 2. 查看工作表详情(行列数、最后非空位置、mergedRanges 等) +dws sheet info --node --sheet-id --format json + +# 3. 读取全部数据 +dws sheet range read --node --sheet-id --format json + +# 4. 读取指定区域 +dws sheet range read --node --sheet-id --range "A1:D10" --format json +``` + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `list` | 工作表的 `sheetId` | info / range read 的 --sheet-id | +| `info` | `rowCount` / `lastNonEmptyRow` / `columnCount` / `mergedRanges` | 确定数据范围、分页读取边界、识别合并单元格结构 | + +## 注意事项 + +- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询真实的 `sheetId` / 工作表名称后再调用,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等);用户仅给出工作表名称时,也应通过 `list` 校验该名称是否存在,避免名称大小写或拼写不一致导致失败 +- `range read` 不传 `--range` 时默认读取整个工作表的全部非空数据 +- `range read` 的 `--range` 支持 `Sheet1!A1:D10` 格式直接指定工作表(此时忽略 `--sheet-id`) +- ★ `csv-get` / `range read` / `range get` 不返回合并单元格结构;查看合并范围必须用 `sheet info` 的 `mergedRanges` +- `range read` 遇到超时或响应过慢时,应缩小 `--range` 查询范围,**单次读取的单元格数量建议控制在 5000 个以内**;数据量较大时通过 `info` 获取边界后分批读取,避免不传 `--range` 直接读取整个大工作表 +- ★ 当用户要求搜索/查找表格数据时,使用 `find` 命令,不要用 `range read` 读取全量数据后自行过滤——`find` 支持服务端搜索,效率更高、语义更准确 diff --git a/skills/multi/dingtalk-sheet/references/sheet/sheet-search-replace.md b/skills/multi/dingtalk-sheet/references/sheet/sheet-search-replace.md new file mode 100644 index 00000000..61a3db16 --- /dev/null +++ b/skills/multi/dingtalk-sheet/references/sheet/sheet-search-replace.md @@ -0,0 +1,118 @@ +# 搜索与替换 + +## 使用场景 + +用户说"搜索/查找/找单元格/搜内容/精确搜索/精确匹配/完全匹配/全字匹配": +- 搜索单元格 → `find` +- 精确匹配(只匹配完全等于的,不匹配包含的) → `find --match-entire-cell` +- 正则搜索 → `find --use-regexp` +- 搜索公式 → `find --match-formula` +- 不要用 `range read` 读取全量数据后在客户端过滤来替代 `find`,必须使用 `find` 命令的服务端搜索能力 + +用户说"替换/查找替换/全局替换/批量替换/把A替换成B/把所有的X改成Y": +- 查找替换 → `replace` +- 精确匹配后替换(只替换内容完全等于的单元格) → `replace --match-entire-cell` +- 正则替换 → `replace --use-regexp` +- 删除匹配内容 → `replace --replacement ""` +- 请勿用 `find` + `range update`、`range read` + `range update` 等组合来模拟替换,`replace` 是服务端原子操作,效率更高且返回替换计数 + +## 命令详细参考 + +### 在工作表中搜索单元格内容 +``` +Usage: + dws sheet find [flags] +Example: + # 基本搜索 + dws sheet find --node --sheet-id --find "销售额" + + # 在指定范围内搜索 + dws sheet find --node --sheet-id --find "合计" --range "A1:D100" + + # 正则表达式搜索(不区分大小写) + dws sheet find --node --sheet-id --find "^total" --use-regexp --match-case=false + + # 精确匹配整个单元格内容 + dws sheet find --node --sheet-id --find "完成" --match-entire-cell + + # 搜索公式文本 + dws sheet find --node --sheet-id --find "SUM" --match-formula +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --find string 搜索文本 (必填) + --range string 搜索范围,A1 表示法 (如 A1:D10) + --match-case 区分大小写 (默认 true) + --match-entire-cell 精确匹配整个单元格内容 + --use-regexp 启用正则表达式搜索 + --match-formula 搜索公式文本而非显示值 + --include-hidden 包含隐藏单元格 +``` + +### 全局查找替换 +``` +Usage: + dws sheet replace [flags] +Example: + dws sheet replace --node --sheet-id --find "旧文本" --replacement "新文本" + dws sheet replace --node --sheet-id --find "待处理" --replacement "已完成" --match-entire-cell + dws sheet replace --node --sheet-id --find "\\d{4}" --replacement "****" --use-regexp + dws sheet replace --node --sheet-id --find "旧" --replacement "新" --range "A1:D100" + dws sheet replace --node --sheet-id --find "临时" --replacement "" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --find string 查找文本 (必填) + --replacement string 替换文本 (必填,可为空字符串表示删除) + --range string 替换范围,A1 表示法 (如 A1:D100) + --match-case 区分大小写 (默认 false) + --match-entire-cell 完整单元格匹配 + --use-regexp 启用正则表达式匹配 + --include-hidden 包含隐藏行/列 +``` + +返回被替换的单元格数量。`--replacement` 可以为空字符串,表示删除匹配内容。 + +## 核心工作流 + +```bash +# ── 工作流: 搜索表格数据 ── + +# 1. 获取工作表列表 +dws sheet list --node --format json + +# 2. 基本搜索 — 在指定工作表中查找文本 +dws sheet find --node --sheet-id --find "销售额" --format json + +# 3. 在指定范围内搜索 +dws sheet find --node --sheet-id --find "合计" --range "A1:D100" --format json + +# 4. 正则搜索(不区分大小写) +dws sheet find --node --sheet-id --find "^total" --use-regexp --match-case=false --format json + +# 5. 精确匹配整个单元格 +dws sheet find --node --sheet-id --find "完成" --match-entire-cell --format json + +# 6. 搜索公式文本 +dws sheet find --node --sheet-id --find "SUM" --match-formula --format json +``` + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `list` | 工作表的 `sheetId` | find / replace 的 --sheet-id | +| `find` | `matchedCells` 中的 `a1Notation` | 定位目标单元格,用于 range read / range update | +| `replace` | `replaceCount` 被替换的单元格数量 | 确认替换结果 | + +## 注意事项 + +- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询真实的 `sheetId` / 工作表名称后再调用,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等);用户仅给出工作表名称时,也应通过 `list` 校验该名称是否存在,避免名称大小写或拼写不一致导致失败 +- ★ **搜索用 `find` 不用 `range read`**:`find` 是服务端搜索,禁止用 `range read` 全量读取后客户端过滤 +- ★ **替换用 `replace` 不用 `range update`**:`replace` 是服务端原子操作,返回替换计数 +- `find` 返回匹配单元格的地址(A1 表示法)和值,无匹配时返回空数组 +- `find` 的 `--match-entire-cell` 用于精确匹配:只返回单元格内容完全等于搜索文本的结果,不会匹配包含该文本的单元格(例如搜索"苹果"时,只匹配"苹果",不匹配"苹果手机""苹果汁"等)。用户说"精确搜索/完全匹配/只搜等于XX的"时必须使用此参数 +- `find` 的 `--match-case` 默认为 true(区分大小写),设为 false 可忽略大小写 +- `find` 的 `--use-regexp` 启用后,`--find` 参数作为正则表达式处理 +- `replace` 的 `--find` 不能为空字符串,`--replace` 可以为空字符串(表示删除匹配内容) +- `replace` 的 `--match-case` 默认为 false(不区分大小写),与 `find` 的默认行为不同 diff --git a/skills/multi/dingtalk-sheet/references/sheet/sheet-style-format.md b/skills/multi/dingtalk-sheet/references/sheet/sheet-style-format.md new file mode 100644 index 00000000..240e57fc --- /dev/null +++ b/skills/multi/dingtalk-sheet/references/sheet/sheet-style-format.md @@ -0,0 +1,274 @@ +# 单元格格式与合并 (style & format) + +## 三种样式设置方式 + +钉钉表格支持三种样式设置方式,适用不同场景: + +| 方式 | 命令 / 字段 | 适用场景 | 粒度 | +|------|------------|---------|------| +| **`set-style` / `batch-set-style`** | `dws sheet range set-style` | 批量刷整片区域的统一样式(表头加粗居中、数字格式等) | range 级别(2D 数组或全 range 统一值) | +| **`cellStyles`**(`range update` 内) | `--values` 中每个 cell 的 `cellStyles` 字段 | 写值同时附带样式,少量 cell 一步到位 | per-cell 级别 | +| **`style`**(richText 片段样式) | `--values` 中 richText 子项(`text`/`link`)的 `style` | 同一单元格内不同文字有不同字体样式 | 文本片段级别 | + +选择建议: +- 只设样式不改值 → `set-style` / `batch-set-style` +- 写值 + 样式一步到位(少量 cell) → `range update` + `cellStyles` +- 文本内部分段样式("重要"红色加粗,其余正常) → `range update` + `type:"richText"` 子项 `style` +- 大面积统一样式 → `set-style`(单值刷 range)或 `batch-set-style`(多 range 批量) + +注意:`set-style` / `batch-set-style` 和 `range update` 的 `cellStyles` 最终都作用于 cell-level 样式,效果相同。区别在于调用方式——前者是独立命令,后者嵌在写值调用中。`range read` 返回的 `cellStyles` 字段能读回所有显式设置过的 cell-level 样式,无论是通过哪种方式设置的。 + +`type:"text"` 顶层旧 `style` 字段不要作为新写法使用;整格样式用 `cellStyles`,分段样式才用 richText 子项 `style`。 + +## 使用场景 + +### 单元格格式 + +用户说"设置样式/改颜色/设背景色/加粗/居中/换行/字体颜色/字号": +- 仅设样式不改值 → `range set-style` +- 批量设置不同 range 的样式 → `range batch-set-style --batch ./styles.json`(内部顺序循环调 `update_range`) +- 写值同时附带样式 → `range update --values` 中使用 `cellStyles` 字段(参见 sheet-write-data.md) +- 请勿用 `range update --values` 写空/重写来模拟纯样式变更 + +用户说"设置数字格式/改成百分比/用人民币显示/按日期显示/文本格式/保留几位小数": +- 批量设置数字格式 → `range set-style --number-format <格式代码>`(如 `0%` / `"¥"#,##0.00` / `yyyy/m/d` / `@`) +- 写值时顺带设置数字格式 → `range update` 中 `cellStyles.numberFormat` + +用户说"合并单元格/合并/合并区域/按行合并/按列合并": +- 合并所有单元格 → `merge-cells`(默认 mergeAll) +- 按行合并 → `merge-cells --merge-type mergeRows` +- 按列合并 → `merge-cells --merge-type mergeColumns` + +用户说"取消合并/拆分单元格/还原合并": +- 取消合并单元格 → `unmerge-cells` + +## 命令详细参考 + +### 设置单元格样式 +``` +Usage: + dws sheet range set-style [flags] +Example: + # 给 A1:B3 打上黄底粗体居中 + dws sheet range set-style --node --sheet-id --range "A1:B3" \ + --bg-color "#FFF2CC" --font-weight bold --h-align center + + # 给 C1:C5 逐单元格设置不同背景色 + dws sheet range set-style --node --sheet-id --range "C1:C5" \ + --bg-colors-json '[["#FF0000"],["#00FF00"],["#0000FF"],["#FFFF00"],["#FF00FF"]]' + + # 整片 range 启用自动换行 + dws sheet range set-style --node --sheet-id --range "A1:E10" --word-wrap autoWrap +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 目标区域,如 A1:B3 (必填) + --bg-color string 背景色(#RRGGBB),一键刷整个 range;与 --bg-colors-json 二选一 + --bg-colors-json string 背景色二维 JSON 数组,维度需与 --range 一致 + --font-size int 字号,一键刷整个 range;与 --font-sizes-json 二选一 + --font-sizes-json string 字号二维 JSON 数组 + --h-align string 水平对齐:left/center/right/general + --h-aligns-json string 水平对齐二维 JSON 数组 + --v-align string 垂直对齐:top/middle/bottom + --v-aligns-json string 垂直对齐二维 JSON 数组 + --font-color string 字体颜色(#RRGGBB) + --font-colors-json string 字体颜色二维 JSON 数组 + --font-weight string 字体粗细:bold/normal + --font-weights-json string 字体粗细二维 JSON 数组 + --word-wrap string 换行方式:overflow/clip/autoWrap(整个 range 共用) + --number-format string 数字格式代码,如 General/@/#,##0/#,##0.00/0%/0.00%/yyyy/m/d/h:mm:ss +``` + +**特性说明**: +- 每个样式维度提供两种写法,二选一:`--xxx`(单值刷整个 range,CLI 本地展开为二维数组)vs `--xxx-json`(逐单元格指定,维度需与 `--range` 完全一致) +- 至少需传入一个样式参数。单次调用建议:行数 ≤ 1000,单元格总数 ≤ 5000 +- 枚举值按驼峰书写:`autoWrap`、`bold`、`normal`、`center` 等 + +### 批量设置单元格样式 +``` +Usage: + dws sheet range batch-set-style [flags] +Example: + dws sheet range batch-set-style --node --batch ./styles.json + dws sheet range batch-set-style --node --batch ./styles.json --continue-on-error +Flags: + --node string 表格文档 ID 或 URL (必填) + --batch string 批次配置 JSON 文件路径 (必填) + --continue-on-error 遇到失败时继续执行后续条目(默认遇错即停) +``` + +配置文件格式(JSON 数组,每个元素一条批次项): +```json +[ + { + "sheetId": "Sheet1", + "range": "A1:B3", + "bgColor": "#FFF2CC", + "fontSize": 12, + "hAlign": "center", + "vAlign": "middle", + "fontColor": "#333333", + "fontWeight": "bold", + "wordWrap": "autoWrap", + "numberFormat": "General" + }, + { + "sheetId": "Sheet1", + "range": "C1:C5", + "bgColorsJson": "[[\"#FF0000\"],[\"#00FF00\"],[\"#0000FF\"],[\"#FFFF00\"],[\"#FF00FF\"]]" + } +] +``` + +**特性说明**: +- CLI 侧顺序循环逐条调用 `update_range`(非服务端批量),运行时输出 `[N/M]` 进度 +- 每条记录执行与 `set-style` 一致的校验:至少一项样式字段 + rows ≤ 1000 + rows×cols ≤ 30000 + 枚举合法 +- 默认遇错即停(返回非 0),`--continue-on-error` 时所有条目跑完再返回首个错误 + +### 合并单元格 +``` +Usage: + dws sheet merge-cells [flags] +Example: + # 合并所有单元格(默认) + dws sheet merge-cells --node --sheet-id --range "A1:B3" + + # 按行合并 + dws sheet merge-cells --node --sheet-id --range "A1:C3" --merge-type mergeRows + + # 按列合并 + dws sheet merge-cells --node --sheet-id --range "A1:C3" --merge-type mergeColumns + + # 使用带工作表前缀的范围(忽略 --sheet-id) + dws sheet merge-cells --node --sheet-id --range "Sheet1!A1:B3" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 目标单元格区域地址,如 A1:B3 (必填) + --merge-type string 合并方式: mergeAll(默认)/mergeRows/mergeColumns +``` + +支持三种合并方式: +- `mergeAll`(默认):合并所有单元格,将选定区域内的所有单元格合并成一个 +- `mergeRows`:按行合并,在选定区域内将同一行相邻的单元格合并 +- `mergeColumns`:按列合并,在选定区域内将同一列相邻的单元格合并 + +注意:合并时只保留左上角单元格的值,其他单元格的值会被丢弃。 +`--range` 支持带工作表前缀的写法(如 `Sheet1!A1:B3`),此时将优先使用前缀解析出的工作表,忽略 `--sheet-id`。 +合并完成后,可通过 `dws sheet info --node --sheet-id --format json` 查看 `mergedRanges` 验证合并结构。 + +### 取消合并单元格 +``` +Usage: + dws sheet unmerge-cells [flags] +Example: + dws sheet unmerge-cells --node --sheet-id --range "A1:D5" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 取消合并的范围,A1 表示法 (必填) +``` + +取消指定范围内所有合并的单元格,恢复为独立单元格。 + +## number-format 格式 code + +适用范围:`number-format` 在 `range set-style` / `range batch-set-style` 中接受(CLI 对应 `--number-format`,batch 配置文件对应 `numberFormat`)。`range update` 没有 `--number-format` 参数,但可在写入值时通过每个 cell 的 `cellStyles.numberFormat` 设置同样的数字格式。 + +商品 ID、规格 ID、SKU、订单号、手机号、工号等数字形态标识符,使用文本格式 code:`@`。 + +常用格式: + +| 格式类型 | 推荐 code | 展示示例 | 适用场景 | +| --- | --- | --- | --- | +| 常规 | `General` | `1234` / `普通文本` | 普通文本/数字展示 | +| 文本 | `@` | `528545015680` | 商品 ID、规格 ID、SKU、订单号、手机号、工号 | +| 整数 | `0` | `1235` | 数量、计数 | +| 两位小数 | `0.00` | `1234.50` | 单价、评分 | +| 整数千分位 | `#,##0` | `1,235` | 数量、金额整数 | +| 千分位两位小数 | `#,##0.00` | `1,234.50` | 金额、单价 | +| 百分比 | `0%` / `0.00%` | `85%` / `85.00%` | 转化率、占比 | +| 日期 | `yyyy/m/d` | `2026/3/15` | 日期列 | +| 日期时间 | `yyyy/m/d h:mm` | `2026/3/15 14:30` | 日期时间列 | +| 时间 | `h:mm` / `h:mm:ss` | `14:30` / `14:30:05` | 时间列 | +| 科学计数法 | `0.00E+00` / `##0.0E+0` | `1.23E+05` | 科学数据 | +| 人民币 | `"¥"#,##0_);("¥"#,##0)` / `"¥"#,##0.00_);("¥"#,##0.00)` | `¥1,235` / `¥1,234.50` | 金额列 | +| 美元 | `$#,##0_);($#,##0)` / `$#,##0.00_);($#,##0.00)` | `$1,235` / `$1,234.50` | 金额列 | + +选择规则:没有特殊展示要求时,优先使用上面的常用格式。只有用户明确要求负数显示方式、中文日期、12 小时制、累计时长、分数或会计格式时,再选择下面的可选变体。 + +可选变体: + +| 用户要求 | 推荐 code | 推荐展示示例 | 可选 code(差异) | +| --- | --- | --- | --- | +| 负数用括号显示 | `#,##0 ;(#,##0)` | `(1,235)` | `#,##0.00;(#,##0.00)`:保留两位小数,如 `(1,234.50)` | +| 负数标红显示 | `#,##0 ;[red](#,##0)` | 红色 `(1,235)` | `#,##0.00;[red](#,##0.00)`:保留两位小数,如红色 `(1,234.50)` | +| 分数 | `# ?/?` | `1 1/2` | `# ??/??`:分母最多两位,如 `1 23/32` | +| 英文月份日期 | `d-mmm-yy` | `15-Mar-26` | `d-mmm`:省略年份,如 `15-Mar`;`mmm-yy`:只显示月年,如 `Mar-26` | +| 中文日期 | `yyyy"年"m"月"d"日"` | `2026年3月15日` | `yyyy"年"m"月"`:只显示年月,如 `2026年3月`;`m"月"d"日"`:只显示月日,如 `3月15日` | +| 12 小时制时间 | `h:mm AM/PM` | `2:30 PM` | `h:mm:ss AM/PM`:显示秒,如 `2:30:05 PM` | +| 中文上午/下午时间 | `上午/下午 h"时"mm"分"` | `下午 2时30分` | `上午/下午 h"时"mm"分"ss"秒"`:显示秒,如 `下午 2时30分05秒` | +| 分秒/累计时长 | `mm:ss` | `05:30` | `[h]:mm:ss`:累计小时,如 `27:05:30`;`mm:ss.0`:显示十分之一秒,如 `05:30.5` | +| 人民币负数标红 | `"¥"#,##0_);[red]("¥"#,##0)` | 红色 `(¥1,235)` | `"¥"#,##0.00_);[red]("¥"#,##0.00)`:保留两位小数,如红色 `(¥1,234.50)` | +| 美元负数标红 | `$#,##0_);[Red]($#,##0)` | 红色 `($1,235)` | `$#,##0.00_);[Red]($#,##0.00)`:保留两位小数,如红色 `($1,234.50)` | +| 会计数字 | `_(* #,##0_);_(* (#,##0);_(* "-"_);_(@_)` | `1,235`,零值显示 `-` | `_(* #,##0.00_);_(* (#,##0.00);_(* "-"??_);_(@_)`:保留两位小数,如 `1,234.50` | +| 人民币会计格式 | `_("¥"* #,##0_);_("¥"* (#,##0);_("¥"*"-"_);_(@_)` | `¥ 1,235`,零值显示 `¥ -` | `_("¥"* #,##0.00_);_("¥"*(#,##0.00);_("¥"* "-"??_);_(@_)`:保留两位小数,如 `¥ 1,234.50` | + +## 核心工作流 + +```bash +# ── 工作流 4: 写入数据并设置样式 ── + +# 1. 写入数据 +dws sheet range update --node --sheet-id --range "A1:C3" \ + --values '[["商品","单价","数量"],["苹果",5.5,100],["香蕉",3.2,200]]' --format json + +# 2. 设置数字格式(人民币) +dws sheet range set-style --node --sheet-id --range "B2:B3" \ + --number-format '"¥"#,##0.00' --format json + +# 3. 商品 ID / 规格 ID 按文本展示,避免科学计数法 +dws sheet range set-style --node --sheet-id --range "A2:A3" \ + --number-format "@" --format json + +# 4. 写入单元格级超链接 +dws sheet range update --node --sheet-id --range "D1" \ + --values '[[{"type":"text","text":"详情","hyperlink":{"type":"path","link":"https://dingtalk.com"}}]]' --format json +``` + +```bash +# ── 工作流 8: 合并单元格 ── + +# 1. 获取工作表列表 +dws sheet list --node --format json + +# 2. 合并所有单元格(默认 mergeAll) +dws sheet merge-cells --node --sheet-id --range "A1:B3" --format json + +# 3. 按行合并 +dws sheet merge-cells --node --sheet-id --range "A1:C3" --merge-type mergeRows --format json + +# 4. 按列合并 +dws sheet merge-cells --node --sheet-id --range "A1:C3" --merge-type mergeColumns --format json +``` + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `merge-cells` | `a1Notation` 实际被合并的范围、`mergeType` 生效的合并方式 | 确认合并结果 | +| `unmerge-cells` | `sheetId` 工作表 ID | 确认操作完成 | +| `list` | 工作表的 `sheetId` | info / range read / range update / find 的 --sheet-id | + +## 注意事项 + +- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) +- ★ `range update` / `range set-style` / `range batch-set-style` 单次调用上限(强制):行数 ≤ 1000,单元格总数(行×列)建议≤ 5000(服务端硬限 30000);超限请拆分多次调用。CLI 会在调用前做本地预校验,服务端超 30000 会直接报错 +- `range set-style` / `range batch-set-style` 的样式枚举按驼峰书写:`wordWrap` 取 `overflow`/`clip`/`autoWrap`,`fontWeight` 取 `bold`/`normal`,`hAlign` 取 `left`/`center`/`right`/`general`,`vAlign` 取 `top`/`middle`/`bottom`;背景色/字体颜色统一使用 `#RRGGBB` 格式 +- `range update` 支持通过 `cellStyles` 在写值时附带 per-cell 样式,适合少量单元格写值 + 样式一步到位的场景。批量设置整片区域的统一样式时,仍应使用 `set-style` / `batch-set-style` +- `merge-cells` 合并时只保留左上角单元格的值,其他单元格的值会被丢弃 +- `merge-cells` 的 `--merge-type` 不传时默认为 `mergeAll`(合并所有单元格) +- `merge-cells` 的 `--range` 支持带工作表前缀的写法(如 `Sheet1!A1:B3`),此时忽略 `--sheet-id` +- `merge-cells` 如果目标区域与其他合并单元格、锁定区域或表格区域存在交集,合并将失败 +- `unmerge-cells` 取消指定范围内所有合并单元格,使用 A1 表示法指定范围 +- 对已有表格做格式延续、插入列后修复表头、或写入前临时取消合并时,先记录 `sheet info` 返回的 `mergedRanges`,操作后按需用 `merge-cells` 恢复 diff --git a/skills/multi/dingtalk-sheet/references/sheet/sheet-workbook.md b/skills/multi/dingtalk-sheet/references/sheet/sheet-workbook.md new file mode 100644 index 00000000..8874d756 --- /dev/null +++ b/skills/multi/dingtalk-sheet/references/sheet/sheet-workbook.md @@ -0,0 +1,242 @@ +# 表格与工作表管理 + +## 使用场景 + +用户说"创建表格/新建电子表格": +- 创建表格文档 → `create` + +用户说"看工作表/有哪些工作表/表格结构": +- 列出工作表 → `list` +- 工作表详情 → `info` + +用户说"加工作表/新增Sheet": +- 新建工作表 → `new` + +用户说"修改工作表名称/重命名工作表/移动工作表位置/隐藏工作表/显示工作表/冻结行/冻结列/取消冻结/更新工作表属性": +- 更新工作表属性 → `update` +- 重命名工作表 → `update --name "新名称"` +- 移动工作表位置 → `update --index N` +- 隐藏工作表 → `update --hidden` +- 显示工作表 → `update --hidden=false` +- 冻结行列 → `update --frozen-row-count N --frozen-column-count M` +- 取消冻结 → `update --frozen-row-count 0 --frozen-column-count 0` + +用户说"复制工作表/拷贝工作表/克隆工作表/工作表副本": +- 复制工作表 → `copy` +- 复制并指定名称 → `copy --name "副本名称"` +- 复制并指定位置 → `copy --index N` + +用户说"删除工作表/移除工作表/删掉这个Sheet": +- 删除工作表 → `delete-sheet`(不可逆操作,执行前必须向用户确认) + +## 命令详细参考 + +### 创建钉钉表格文档 +``` +Usage: + dws sheet create [flags] +Example: + dws sheet create --name "销售数据" + dws sheet create --name "Q1 数据" --folder + dws sheet create --name "知识库表格" --workspace +Flags: + --name string 表格名称 (必填) + --folder string 目标文件夹 ID (dentryUuid 格式) 或 URL;禁止传入纯数字 dentryId + --workspace string 目标知识库 ID +``` + +> **ID 格式约束**:`--folder` 只接受 UUID 格式的 `fileId`(如 `ZgpG2NdyVXYOR2D5UGDok65MJMwvDqPk`)或 alidocs 文件夹 URL。`drive list` 返回中有 `dentryId`(纯数字,如 `218595998810`)和 `fileId`(UUID 格式)两个字段,**必须使用 `fileId`,禁止使用 `dentryId`**,传入纯数字会导致命令失败。 + +### 获取全部工作表列表 +``` +Usage: + dws sheet list [flags] +Example: + dws sheet list --node + dws sheet list --node "https://alidocs.dingtalk.com/i/nodes/" +Flags: + --node string 表格文档 ID 或 URL (必填) +``` + +### 获取指定工作表详情 +``` +Usage: + dws sheet info [flags] +Example: + dws sheet info --node + dws sheet info --node --sheet-id + dws sheet info --node --sheet-id "Sheet1" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (不传则返回第一个工作表) +``` + +返回字段中 `mergedRanges` 是当前工作表的合并单元格范围列表(A1 表示法,如 `["C7:D11"]`)。它属于工作表结构/布局元数据:读写单元格内容前,如需判断表头、分组标题、续写位置或避开合并冲突,应先看 `sheet info`,不要在 `range read` / `csv-get` 的单元格值里寻找合并信息。 + +### 新建工作表 +``` +Usage: + dws sheet new [flags] +Example: + dws sheet new --node --name "Sheet2" + dws sheet new --node --name "数据汇总" +Flags: + --node string 表格文档 ID (必填) + --name string 工作表名称 (必填) +``` + +### 更新工作表属性 +``` +Usage: + dws sheet update [flags] +Example: + # 改名 + 调整冻结 + dws sheet update --node --sheet-id --name "汇总表" --frozen-row-count 2 --frozen-column-count 1 + + # 隐藏工作表 + dws sheet update --node --sheet-id --hidden=true + + # 显示工作表 + dws sheet update --node --sheet-id --hidden=false + + # 移动工作表到第一个位置 + dws sheet update --node --sheet-id --index 0 + + # 取消冻结 + dws sheet update --node --sheet-id --frozen-row-count 0 --frozen-column-count 0 +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --name string 新名称,最长 100 字符,不能包含 / \ ? * [ ] : + --index int 新位置(从 0 开始) + --hidden --hidden=true 隐藏,--hidden=false 取消隐藏 + --frozen-row-count int 冻结行数,0 表示取消冻结 + --frozen-column-count int 冻结列数,0 表示取消冻结 +``` + +更新工作表名称、位置、隐藏状态、冻结行列。 +`--name` / `--index` / `--hidden` / `--frozen-row-count` / `--frozen-column-count` 至少提供一个;多个属性可同时传入,将在同一次请求中更新。 + +注意: +- 至少需要保留一个可见的工作表,不能将所有工作表都隐藏 +- 冻结行数/列数不能超过工作表的总行数/列数 + +### 复制工作表 +``` +Usage: + dws sheet copy [flags] +Example: + # 按默认位置复制 + dws sheet copy --node --sheet-id + + # 指定副本名称和位置 + dws sheet copy --node --sheet-id --name "销售副本" --index 2 + + # 只指定名称 + dws sheet copy --node --sheet-id --name "备份" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 源工作表 ID 或名称 (必填) + --name string 副本名称,最长 100 字符,不能包含 / \ ? * [ ] : (不传则系统自动生成) + --index int 副本位置(从 0 开始)(不传则放在源工作表之后) +``` + +复制指定工作表,在同一表格中创建一个副本。 +复制操作会将源工作表的所有内容(包括数据、格式、公式等)完整复制到新工作表中。 +传 `--index` 时,CLI 会先复制,再追加一次位置更新,把副本移动到目标索引。 +名称与已有工作表重复时系统会自动重命名。 + +### 删除工作表 +``` +Usage: + dws sheet delete-sheet [flags] +Example: + dws sheet delete-sheet --node --sheet-id +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 要删除的工作表 ID 或名称 (必填) +``` + +> **CAUTION:** 不可逆操作 — 执行前必须向用户确认。 + +删除指定的工作表及其所有数据。约束: +- 不能删除隐藏的工作表(需先通过 `sheet update --hidden false` 取消隐藏再删除) +- 不能删除最后一个可见工作表(至少保留一个可见工作表) + +## 核心工作流 + +```bash +# ── 工作流 1: 创建表格并写入数据 ── + +# 1. 创建表格文档 — 提取 nodeId +dws sheet create --name "销售数据" --format json + +# 2. 查看工作表列表 — 提取 sheetId +dws sheet list --node --format json + +# 3. 写入表头和数据 +dws sheet range update --node --sheet-id --range "A1:C1" \ + --values '[["姓名","部门","销售额"]]' --format json + +dws sheet range update --node --sheet-id --range "A2:C4" \ + --values '[["张三","销售部",50000],["李四","市场部",38000],["王五","销售部",62000]]' --format json + +# ── 工作流 2: 读取已有表格数据 ── + +# 1. 获取工作表列表 +dws sheet list --node --format json + +# 2. 查看工作表详情(行列数、最后非空位置等) +dws sheet info --node --sheet-id --format json + +# 3. 读取全部数据 +dws sheet range read --node --sheet-id --format json + +# 4. 读取指定区域 +dws sheet range read --node --sheet-id --range "A1:D10" --format json + +# ── 工作流 3: 多工作表管理 ── + +# 1. 新建工作表 +dws sheet new --node --name "汇总" --format json + +# 2. 在新工作表中写入汇总公式 +dws sheet range update --node --sheet-id --range "A1:B1" \ + --values '[["指标","数值"]]' --format json + +dws sheet range update --node --sheet-id --range "A2:B2" \ + --values '[["总销售额","=SUM(Sheet1!C2:C100)"]]' --format json +``` + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `create` | `nodeId` | list / info / new / range read / range update / find 的 --node | +| `list` | 工作表的 `sheetId` | info / range read / range update / find 的 --sheet-id | +| `new` | 新工作表的 `sheetId` | range read / range update / find 的 --sheet-id | +| `info` | `rowCount` / `lastNonEmptyRow` / `mergedRanges` | 确定数据范围、追加写入起始行、判断合并单元格结构 | + +## 注意事项 + +- ★ **`--sheet-id` 获取规范(强制)**:所有涉及 `--sheet-id` 参数的命令,除非用户主动提供了工作表 ID 或工作表名称,否则在 `sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询真实的 `sheetId` / 工作表名称后再调用,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等);用户仅给出工作表名称时,也应通过 `list` 校验该名称是否存在,避免名称大小写或拼写不一致导致失败 +- `mergedRanges` 中的范围表示一个整体语义区域。合并区域内非左上角单元格为空并不代表无内容,通常应以左上角单元格的值作为该合并区域的含义。 +- `create` 不传 `--folder` 和 `--workspace` 时,默认创建在"我的文档"根目录 +- `list` 返回所有工作表的 ID 和名称,是后续操作的必要前置步骤 +- `info` 不传 `--sheet-id` 时默认返回第一个工作表的详情 +- `new` 创建工作表时,如名称与已有工作表重复,系统会自动重命名 +- `update` 的 `--name`、`--index`、`--hidden`、`--frozen-row-count`、`--frozen-column-count` 至少必须提供一个 +- `update` 的 `--name` 最长 100 字符,不能包含 `/ \ ? * [ ] :` 等特殊字符 +- `update` 的 `--index` 为 0-based 非负整数,0 表示移动到最前面 +- `update` 的 `--hidden` 设为 true 时,至少需要保留一个可见的工作表,不能将所有工作表都隐藏 +- `update` 的 `--frozen-row-count` / `--frozen-column-count` 为非负整数,不能超过工作表的总行数/列数,设为 0 表示取消冻结 +- `update` 当同时提供多个属性时,所有属性将在同一次请求中更新 +- `copy` 复制操作会将源工作表的所有内容(包括数据、格式、公式等)完整复制到新工作表 +- `copy` 的 `--name` 可选,不传时系统自动生成名称(通常为"源名称 副本"或类似格式) +- `copy` 的 `--name` 最长 100 字符,不能包含 `/ \ ? * [ ] :` 等特殊字符 +- `copy` 当指定名称与已有工作表重复时,系统会自动重命名为合法值 +- `copy` 的 `--index` 可选,不传时副本将放置在源工作表之后的默认位置 +- `delete-sheet` 为不可逆操作,执行前必须向用户确认 +- `delete-sheet` 不能删除隐藏的工作表,需先通过 `update --hidden=false` 取消隐藏再删除 +- `delete-sheet` 不能删除最后一个可见工作表,至少保留一个可见工作表 +- ★ 关键区分: sheet(电子表格/单元格读写) vs aitable(AI多维表/结构化记录/字段定义) vs doc(文档编辑/阅读) diff --git a/skills/multi/dingtalk-sheet/references/sheet/sheet-write-data.md b/skills/multi/dingtalk-sheet/references/sheet/sheet-write-data.md new file mode 100644 index 00000000..1b92e2be --- /dev/null +++ b/skills/multi/dingtalk-sheet/references/sheet/sheet-write-data.md @@ -0,0 +1,415 @@ +# 数据写入 + +## 使用场景 + +用户说"写数据/填表/更新单元格/写入公式": +- 更新数据 → `range update` +- 【强制】`--sheet-id` 必填:即使是单工作表也不能省略,不要参照 `range read` 的默认行为;未知时先执行 `dws sheet list --node --format json` 获取 `sheetId`,禁止凭空臆测为 `Sheet1`、`sheet1`、`0`、`default` 等 +- 注意:如果用户的目的是替换文本、移动行列、追加空行空列、清空区域、排序、填充、复制区域或移动区域,请勿使用 `range update`,必须使用对应的专用命令(`replace`/`move-dimension`/`add-dimension`/`range clear`/`range sort`/`range fill`/`range copy-to`/`range move-to`) +- **批量纯值写入优先用 `csv-put`**:当写入场景同时满足以下条件时,必须优先使用 `csv-put` 而非 `range update`:(1) 写入的是纯值(不含公式、超链接、dataValidation、cellStyles、richText);(2) 数据量较大(超过 5 行或超过 20 个单元格);(3) 数据来源为表格/CSV 文本/结构化文本。`csv-put` 无需手动构造二维 JSON 数组,直接传 CSV 文本即可,更简洁高效且支持自动扩容 + +用户说"追加数据/添加行/在末尾加数据/新增记录": +- 追加数据 → `append` + +用户说"批量写入CSV/导入CSV/CSV写入表格/把CSV贴到表格里": +- 写入 CSV → `csv-put` +- 与 `range update` 的区别:`csv-put` 接受 CSV 文本直接写入,无需手动构造二维 JSON 数组;适合大批量纯值写入 +- 与 `append` 的区别:`csv-put` 写入指定位置(--start-cell),`append` 在末尾追加 + +**三种写入命令能力对比**: + +| 能力 | `range update` | `append` | `csv-put` | +|------|---------------|----------|-----------| +| 公式(`=` 开头) | 支持 | 不支持 | 不支持(当文本) | +| 单元格级超链接(`hyperlink`) | 支持 | 不支持 | 不支持 | +| 富文本(片段链接/附件/图片) | 支持 | 不支持 | 不支持 | +| richText 片段样式(bold/color) | 支持 | 不支持 | 不支持 | +| `cellStyles`(背景色/字号/对齐等 cell-level 样式) | 支持 | 不支持 | 不支持 | +| `{}` 跳过(保留原值) | 支持 | 不适用 | 不适用 | +| `dataValidation`(下拉/复选框) | 支持 | 不支持 | 不支持 | +| 原始值(纯数字/字符串) | 支持 | 支持 | 支持 | +| 自动定位末尾 | 不支持 | 支持 | 不支持 | +| 自动扩容行列 | 不支持 | 支持 | 支持 | + +## 命令详细参考 + +### 更新工作表指定区域内容 +``` +Usage: + dws sheet range update [flags] +Example: + # 写入文本 + dws sheet range update --node --sheet-id --range "A1:B2" \ + --values '[[{"type":"text","text":"姓名"},{"type":"text","text":"分数"}],[{"type":"text","text":"张三"},{"type":"text","text":"90"}]]' + + # 写入公式 + dws sheet range update --node --sheet-id --range "C2" \ + --values '[[{"type":"text","text":"=A2&B2"}]]' + + # 写入单元格级超链接 + dws sheet range update --node --sheet-id --range "A1" \ + --values '[[{"type":"text","text":"钉钉","hyperlink":{"type":"path","link":"https://dingtalk.com"}}]]' + + # 清理单元格级超链接,保留当前值 + dws sheet range update --node --sheet-id --range "A1" \ + --values '[[{"hyperlink":{"type":"none"}}]]' + + # 清空单个单元格(text 为空字符串) + dws sheet range update --node --sheet-id --range "A1" \ + --values '[[{"type":"text","text":""}]]' +Flags: + --node string 表格文档 ID (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 目标单元格区域地址,如 A1:B3 (必填) + --values string 单元格内容,二维 JSON 数组 (必填);每个元素必须是 object:{type:text,text:...}、{type:richText,texts:[...]}、{dataValidation:...}、{cellStyles:...}、{hyperlink:...} 或 {}(详见下文 values 参数格式说明) +``` + +**合并单元格注意(`range update`)**:这里说的是 `range update` 写入单元格对象这一路径,不是所有写入命令的统一行为。目标范围与已有合并区域冲突时,MCP 服务端会拦截并返回 `MERGED_CELLS_CONFLICT` 错误,错误消息中通常会列出具体冲突的合并区域地址。收到此错误时按以下流程处理: +1. 从错误消息中获取冲突的合并区域地址(如 `A1:B2, C3:D4`),或通过 `dws sheet info --node --sheet-id --format json` 查询完整的合并区域列表(`mergedRanges` 数组) +2. 用 `dws sheet unmerge-cells --range <冲突区域>` 取消这些合并 +3. 执行 `range update` 写入数据 +4. 如需保留原合并效果,用 `dws sheet merge-cells` 重新合并对应区域(注意合并后仅保留左上角单元格的值) + +续写或改写已有格式化表格时,先用 `sheet info` 读取 `mergedRanges`。若原数据块存在跨列标题行(如 `A1:G1`),新增同类标题行后也要用 `merge-cells` 复制相同合并模式;仅写入值或样式不会自动创建合并区域。 + +**单次调用建议**:行数 ≤ 1000,单元格总数(行×列)≤ 5000;超过时请拆分多次调用。 + +**何时该用 `csv-put` 替代**:如果你准备用 `range update` 写入纯值(不含公式、超链接、富文本对象),且数据量超过 5 行或 20 个单元格,应改用 `csv-put`——它接受 CSV 文本直接写入,无需手动拼装二维 JSON 数组,且支持自动扩容行列。仅在需要写入公式(`=SUM(...)`)、单元格级超链接、富文本对象或修改少量单元格时才使用 `range update`。 + +**范围职责**:`range update` 负责写入单元格内容(原始值/公式/富文本对象),并支持通过 `cellStyles` 附带 per-cell 样式。如需批量设置整片区域的样式(不写值),请使用 `dws sheet range set-style`。 + +### 在工作表末尾追加数据 +``` +Usage: + dws sheet append [flags] +Example: + dws sheet append --node --sheet-id --values '[["张三","销售部",50000]]' + dws sheet append --node --sheet-id \ + --values '[["李四","市场部",38000],["王五","销售部",62000]]' +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --values string 追加数据,二维 JSON 数组 (必填) +``` + +`--values` 为二维 JSON 数组,外层每个元素代表一行,内层每个元素代表一个单元格值。 +追加的数据列数应与工作表已有数据的列数保持一致。 + +### 将 CSV 数据写入指定位置 +``` +Usage: + dws sheet csv-put [flags] +Example: + dws sheet csv-put --node --sheet-id --start-cell A1 \ + --csv 'name,score\nAlice,95\nBob,87' + + dws sheet csv-put --node --sheet-id --start-cell B2 \ + --csv @data.csv --allow-overwrite + + cat data.csv | dws sheet csv-put --node --sheet-id \ + --start-cell A1 --csv - + + dws sheet csv-put --node --sheet-id --start-cell A1 \ + --csv @data.csv --dry-run +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --csv string CSV 文本、@文件路径 或 - 表示 stdin (必填) + --start-cell string 起始单元格,A1 表示法 (必填) + --allow-overwrite 允许覆盖已有数据 (默认 false) +``` + +将 RFC 4180 格式的 CSV 文本写入指定工作表的指定单元格位置。 +- **分隔符必须是英文逗号 `,`**(ASCII 0x2C),禁止使用中文逗号 `,`(U+FF0C)。中文逗号不会被识别为分隔符,会导致整行被写入同一个单元格。生成 CSV 内容时务必检查分隔符 +- 只写纯值,不支持公式/样式/批注。`=` 开头的内容当文本处理,不会被解析为公式 +- 数字/日期/百分数由表格引擎自动识别类型(如 `95` 存为数字,`2025-03-01` 存为日期) +- 自动扩容行列:CSV 数据超出当前工作表维度时自动追加行/列 +- 与 `range update` 不同,目标区域如含合并单元格,`csv-put` 会打散合并并写入纯值 +- 若需要保留原有合并结构,写入前先用 `sheet info` 记录 `mergedRanges`,写入后用 `merge-cells` 恢复对应区域 +- `--allow-overwrite` 默认 false,目标区域有数据时需显式传 `--allow-overwrite` 才能覆盖 +- `--csv` 支持三种输入:直接传文本、`@filepath` 从本地文件读取、`-` 从 stdin 管道读取 +- CSV 文本上限 2M 字符,单元格总数上限 30000 +- 特殊字符处理:CLI 会自动过滤 `\r`(Windows 换行符)和 BOM(UTF-8 文件头标记),Excel/Windows 导出的 CSV 可直接使用;如 CSV 数据中含零宽字符(U+200B 等)或 Bidi 控制符,CLI 会拒绝并报错 + +## values 参数格式说明 + +`range update` 只接受 `--values` 一个数据参数,为二维 JSON 数组,第一维为行,第二维为列。每个 cell 是以下之一: + +- `{}` 空对象:**跳过该单元格,保留原值不变**。只更新部分单元格时用 `{}` 占位,避免拆分多次调用 +- `{type:"text",...}` 或 `{type:"richText",...}` 对象 +- 任何 cell 可附加 `dataValidation` 字段,在写值的同时设置数据校验(下拉列表 / 复选框) +- 任何 cell 可附加 `cellStyles` 字段,在写值的同时设置 cell-level 样式(背景色 / 字体 / 对齐等) +- 任何 cell 可附加 `hyperlink` 字段设置单元格级超链接;`{"hyperlink":{"type":"none"}}` 表示清理单元格级超链接并保留当前值 + +### {}(跳过,保留原值) + +```json +{} +``` + +只更新范围内部分单元格时,用 `{}` 占位不需要修改的位置。示例:`--range "A1:C1" --values '[[{"type":"text","text":"新值"},{},{}]]'` 只更新 A1,B1 和 C1 保持不变。 + +### type=text(普通文本) + +```json +{ "type": "text", "text": "文本内容" } +{ "type": "text", "text": "重要", "cellStyles": { "fontWeight": "bold", "fontColor": "#FF0000" } } +``` + +- `text` 必须为字符串;`text=""` 表示**清空该 cell** +- `text` 以 `=` 开头识别为公式(如 `"=SUM(B2:B4)"`) +- 写数字 / 布尔请用字符串形式(如 `{"type":"text","text":"100"}` / `"true"`),服务端按内容自动识别 +- 字体样式(加粗/颜色/字号等)统一走 `cellStyles`,不支持 `style` 字段 + +### hyperlink 子结构(可选,与 type 同级,单元格级超链接) + +`hyperlink` 作用于整个单元格,适合“这个单元格整体可点击跳转”的场景。它和 richText 的片段级 `link` 不同。 + +> **hyperlink 三种语义**: +> - **不传 `hyperlink` 字段** → 保留原超链接(引擎自动 readback 回写) +> - **`hyperlink: {"type":"none"}`** → 显式清除单元格超链接 +> - **`hyperlink: {"type":"path"/"sheet"/"range", link, text?}`** → 写新超链接(覆盖) +> +> `{}` 跳过也会保留原超链接。 + +```json +{ "type": "text", "text": "钉钉", "hyperlink": { "type": "path", "link": "https://dingtalk.com" } } +{ "hyperlink": { "type": "sheet", "link": "Sheet2" } } +{ "hyperlink": { "type": "range", "link": "Sheet1!A4" } } +{ "hyperlink": { "type": "none" } } +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `type` | string | 必填,`path`(外部链接)/ `sheet`(工作表链接)/ `range`(单元格范围链接)/ `none`(显式清除) | +| `link` | string | type=path/sheet/range 时必填。`path` 为 URL;`sheet` 为工作表 ID 或名称;`range` 为 A1 表示法 | +| `text` | string | 可选显示文本。通常只传 cell 的 `text`,不用重复传 `hyperlink.text` | + +注意: +- 不传 `hyperlink` 字段同于 “保留原超链接”,无需先 read 再回传 +- Agent 调用统一使用 `hyperlink: {type:"none"}` 清除超链接;底层 REST 兼容 `hyperlink:null`,但 MCP schema / 网关可能过滤 null 字段,不要把 null 当默认写法 +- `hyperlink` 可以不带 `type/text` cell 单独出现,用于只设置或清理链接并保留原值 +- 不要把 `hyperlink` 和 `type:"richText"` 混用;整格链接用 `hyperlink`,片段链接用 richText 子项 `type:"link"` + +### type=richText(富文本:片段链接 / 附件 / 图片 / 多片段组合) + +```json +{ "type": "richText", "texts": [ ...子项数组... ] } +``` + +`texts` 子项 `type` 枚举与字段: + +| 子项 type | 必填字段 | 可选字段 | 说明 | +|-----------|---------|---------|------| +| `text` | `text`(字符串) | `style` | 普通文本片段 | +| `link` | `text` + `link`(都非空字符串) | `subType` / `style` | 富文本片段链接。`subType` 默认为 `path`;`path` 的 `link` 是 URL,`sheet` 的 `link` 是真实工作表名称,`range` 的 `link` 是 A1 表示法(如 `Sheet1!A1:B2`) | +| `attachment` | `text` + `resourceId` + `mimeType` | `size`(字节数) | 附件。`text` 是显示文件名,`resourceId` 通过 `dws sheet media-upload` 获取 | +| `image` | `resourceId` + `resourceUrl` | `text`(建议传 `""`) / `width` / `height` | 图片。两个 resource 字段都通过 `dws sheet media-upload` 获取;像素 | + +### style 子结构(仅 richText 子项的 `text` / `link` 类型支持) + +用于 richText 内部片段级样式,实现同一单元格内不同文字有不同样式(如部分文字红色加粗)。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `bold` | boolean | 加粗 | +| `italic` | boolean | 斜体 | +| `underline` | boolean | 下划线 | +| `strike` | boolean | 删除线 | +| `color` | string | 字体颜色,16 进制色值(如 `#FF0000`) | +| `size` | number | 字号,正整数 | + +**richText link 的 `subType`**: + +```json +{ "type": "link", "text": "钉钉", "link": "https://dingtalk.com", "subType": "path" } +{ "type": "link", "text": "工作表", "link": "Sheet2", "subType": "sheet" } +{ "type": "link", "text": "明细区域", "link": "Sheet2!A1:B20", "subType": "range" } +``` + +- 不传 `subType` 时按 `path` 处理,适合外部 URL +- `subType:"sheet"` / `"range"` 需要使用真实工作表名称或 A1 范围;未知时先 `dws sheet list --node --format json`,禁止猜 `Sheet1` +- 这只影响富文本片段链接;整格链接仍使用 cell-level `hyperlink` +- 写入后用 `range read` 读取时,`richText.texts[].subType` 会按同样语义返回;不要把 richText 片段链接和整格 `hyperlink` 混淆 + +注意:`type:"text"` 的顶层旧 `style` 字段只作为历史兼容存在,新请求不要使用;整个单元格的字体样式请用 `cellStyles`,同一 cell 内分段样式才用 richText 子项 `style`。 + +### dataValidation 子结构(可选,与 type 同级) + +任何 cell 可附加 `dataValidation` 字段,在写值的同时设置数据校验。支持两种类型: + +> **dataValidation 三种语义**: +> - **不传 `dataValidation` 字段** → 自动保留原 DV(无需 read 后回写) +> - **`dataValidation: {"type":"none"}`** → 显式清除该单元格 DV +> - **`dataValidation: {"type":"dropdown"/"checkbox", ...}`** → 写新 DV(覆盖原 DV) +> +> `{}` 跳过和不传 dataValidation 字段都会保留原 DV。 + +**dropdown(下拉列表)**: +```json +{ "type": "text", "text": "High", "dataValidation": { "type": "dropdown", "options": [{"value":"High","color":"#00ff00"},{"value":"Low","color":"#ff0000"}], "enableMultiSelect": false } } +``` +- `options`:必填,`[{value, color?}]` 数组 +- `enableMultiSelect`:可选,是否多选,默认 false + +**checkbox(复选框)**: +```json +{ "dataValidation": { "type": "checkbox", "checked": true } } +``` +- `checked`:可选,初始勾选状态,默认 false +- checkbox 通常不需要 type/text(保留原值),也可以和 `type:"text"` 共存 + +**翻译场景示例**(一次调用更新文本 + 翻译 dropdown 选项 + 跳过 checkbox): +```bash +dws sheet range update --node NODE_ID --sheet-id SHEET_ID --range "A1:C1" \ + --values '[[{"type":"text","text":"High","dataValidation":{"type":"dropdown","options":[{"value":"High"},{"value":"Medium"},{"value":"Low"}]}},{},{"type":"text","text":"Translated"}]]' +``` + +### cellStyles 子结构(可选,与 type 同级) + +任何 cell 可附加 `cellStyles` 字段,在写值的同时设置 cell-level 样式。与 `style`(内联文本样式)的区别见下方说明。 + +```json +{ "type": "text", "text": "重要", "cellStyles": { "fontWeight": "bold", "backgroundColor": "#FFF2CC" } } +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `fontWeight` | string | `bold` / `normal` | +| `fontColor` | string | 字体颜色,`#RRGGBB` | +| `fontSize` | number | 字号 | +| `fontStyle` | string | `italic` / `normal` | +| `backgroundColor` | string | 背景色,`#RRGGBB` | +| `horizontalAlignment` | string | `left` / `center` / `right` / `general` | +| `verticalAlignment` | string | `top` / `middle` / `bottom` | +| `wordWrap` | string | `overflow` / `clip` / `autoWrap` | +| `numberFormat` | string | 数字格式 code,如 `@`、`#,##0.00`、`yyyy/m/d`;格式 code 说明见 [「number-format 格式 code」](sheet-style-format.md#number-format-格式-code) | +| `textUnderline` | boolean | 下划线 | +| `textLineThrough` | boolean | 删除线 | + +所有字段均可选,只传需要设置的字段。也可以不传 `type`/`text`,仅用 `{cellStyles:{...}}` 对已有单元格追加样式(保留原值)。 + +选择 `numberFormat` 前,先阅读 [「number-format 格式 code」](sheet-style-format.md#number-format-格式-code),确认目标格式类型对应的 code。 + +长数字标识符请显式设置文本格式:商品 ID、规格 ID、SKU、订单号、手机号、工号等字段建议写成 `{"type":"text","text":"528545015680","cellStyles":{"numberFormat":"@"}}`。仅把值写成文本不一定能阻止常规格式展示;`@` 可以避免 11 位以上数字形态 ID 被显示成科学计数法。`range append` 不支持随行传 `cellStyles`,追加后请对返回的 `a1Notation` 或目标 ID 列执行 `range set-style --number-format "@"`。 + +**`cellStyles` vs `style` vs `set-style` 的区别**: + +| 方式 | 适用场景 | 写在哪里 | 作用范围 | +|------|---------|---------|---------| +| `style`(richText 片段样式) | 同一 cell 内不同文字有不同字体样式 | richText 子项(`text`/`link` 类型)的 `style` | 文本片段级别 | +| `cellStyles`(cell-level 样式) | 背景色、对齐、换行、数字格式等 | cell 的 `cellStyles` | 整个单元格 | +| `set-style` / `batch-set-style` | 批量设置整片区域的样式 | 单独命令,与 `range update` 分开调用 | 指定 range 内所有单元格 | + +典型用法: +- 写入少量单元格 + 样式 → 用 `range update` 的 `cellStyles`,一次调用搞定 +- 批量刷整片区域统一样式 → 用 `set-style`(如 "给 A1:Z1 表头加粗居中") +- 文本内部分段样式(如"重要"二字红色加粗,其余正常) → 用 `type:"richText"` + 子项 `style` + +### 混合示例(普通文字 + 带样式片段链接) + +```json +{ + "type": "richText", + "texts": [ + { "type": "text", "text": "请访问 " }, + { "type": "link", "text": "钉钉官网", "link": "https://dingtalk.com", "style": { "color": "#0080FF", "underline": true } } + ] +} +``` + +### 重要约束 + +- 不再支持 `{type:"number"}` / `{type:"boolean"}` / `{type:"null"}` —— MCP `complexValues` 仅接受 `text` / `richText` 两种 type,或 `{}` 跳过。数字 / 布尔走 `{type:"text","text":"<字符串形式>"}` +- 不支持直接传入原始值(字符串、数字、布尔、null、空字符串);`null` 不等同于 `{}`,`null` 会报错 +- 维度必须与 `--range` 范围完全一致,例如 `--range "A1:B3"` 需要 3 行 2 列的数组 +- 清理整格超链接使用 `{"hyperlink":{"type":"none"}}`;不要使用 `{"hyperlink":null}` 作为 agent 默认调用形态 +- 写图片到单元格建议直接用 `dws sheet write-image`(更简洁) +- 清空整片区域请用 `dws sheet range clear`;只清空单个 cell 可在 `--values` 中传 `{"type":"text","text":""}` + +## 核心工作流 + +```bash +# ── 工作流 1: 创建表格并写入数据 ── + +# 1. 创建表格文档 — 提取 nodeId +dws sheet create --name "销售数据" --format json + +# 2. 查看工作表列表 — 提取 sheetId +dws sheet list --node --format json + +# 3. 写入表头和数据 +dws sheet range update --node --sheet-id --range "A1:C1" \ + --values '[[{"type":"text","text":"姓名"},{"type":"text","text":"部门"},{"type":"text","text":"销售额"}]]' --format json + +dws sheet range update --node --sheet-id --range "A2:C4" \ + --values '[[{"type":"text","text":"张三"},{"type":"text","text":"销售部"},{"type":"text","text":"50000"}],[{"type":"text","text":"李四"},{"type":"text","text":"市场部"},{"type":"text","text":"38000"}],[{"type":"text","text":"王五"},{"type":"text","text":"销售部"},{"type":"text","text":"62000"}]]' --format json + +# ── 工作流 4: 写入数据并设置样式 ── + +# 1. 写入数据 +dws sheet range update --node --sheet-id --range "A1:C3" \ + --values '[[{"type":"text","text":"商品"},{"type":"text","text":"单价"},{"type":"text","text":"数量"}],[{"type":"text","text":"苹果"},{"type":"text","text":"5.5"},{"type":"text","text":"100"}],[{"type":"text","text":"香蕉"},{"type":"text","text":"3.2"},{"type":"text","text":"200"}]]' --format json + +# 2. 设置数字格式(人民币)——两种方式均可: +# 方式 A: 写值时通过 cellStyles 一步到位 +# 方式 B: 单独用 set-style 设置(适合只改格式不改值) +dws sheet range set-style --node --sheet-id --range "B2:B3" \ + --number-format '"¥"#,##0.00' --format json + +# 3. 长数字 ID 写值时同步设置文本格式,避免科学计数法 +dws sheet range update --node --sheet-id --range "D2:D3" \ + --values '[[{"type":"text","text":"528545015680","cellStyles":{"numberFormat":"@"}}],[{"type":"text","text":"528545015681","cellStyles":{"numberFormat":"@"}}]]' --format json + +# 4. 写入单元格级超链接 +dws sheet range update --node --sheet-id --range "D1" \ + --values '[[{"type":"text","text":"详情","hyperlink":{"type":"path","link":"https://dingtalk.com"}}]]' --format json + +# ── 工作流 5: 追加数据 ── + +# 1. 获取工作表列表 +dws sheet list --node --format json + +# 2. 查看工作表详情(确认列结构) +dws sheet info --node --sheet-id --format json + +# 3. 追加单行数据 +dws sheet append --node --sheet-id \ + --values '[["张三","销售部",50000]]' --format json + +# 4. 追加多行数据 +dws sheet append --node --sheet-id \ + --values '[["李四","市场部",38000],["王五","销售部",62000]]' --format json +``` + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `create` | `nodeId` | list / info / new / range update / append / csv-put 的 --node | +| `list` | 工作表的 `sheetId` | range update / append / csv-put 的 --sheet-id | +| `new` | 新工作表的 `sheetId` | range update / append / csv-put 的 --sheet-id | +| `info` | `rowCount` / `lastNonEmptyRow` / `mergedRanges` | 确定数据范围、追加写入起始行、识别合并单元格结构 | +| `append` | `a1Notation` 追加数据所在范围 | 确认追加位置 | +| `csv-put` | `a1Notation` 实际写入的单元格范围 | 确认写入位置和范围 | + +## 注意事项 + +- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询真实的 `sheetId` / 工作表名称后再调用,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等);用户仅给出工作表名称时,也应通过 `list` 校验该名称是否存在,避免名称大小写或拼写不一致导致失败 +- ★ **`range update` 维度校验(强制)**:调用 `range update` 写入 `--values` 时,必须严格校验二维 JSON 数组的行数与列数与 `--range` 指定的范围完全一致: + - 例如 `--range "A1:C3"` 表示 3 行 × 3 列,`--values` 必须是 `[[v1,v2,v3],[v4,v5,v6],[v7,v8,v9]]` 这样 3×3 的数组 + - `--range "A1"` 表示 1 行 × 1 列,`--values` 必须是 `[[v]]` + - 维度不足请按行 / 列补齐为同等大小;不需要修改的位置用 `{}` 跳过(保留原值),需要清空的位置用 `{"type":"text","text":""}`;禁止出现各行列数不一致或与 `--range` 不匹配的情况,否则调用会直接报错 + - 如需写整格超链接,把 `{"type":"text","text":"...","hyperlink":{"type":"path","link":"..."}}` 放进 `--values` 二维数组对应的单元格里;富文本片段链接才使用 richText 子项 `type:"link"` +- ★ **清空区域优先用 `range clear`(强制)**:需要清空整片区域时必须使用 `range clear`,禁止用 `range update` 模拟。仅在 `range update` 写入混合数据时个别 cell 需要清空,才在 `--values` 中用 `{"type":"text","text":""}` +- ★ **不再支持 `{type:"number"}` / `{type:"boolean"}` / `{type:"null"}`(强制)**:MCP `complexValues` 仅接受 `type:"text"` 与 `type:"richText"` 两种,CLI 会在本地直接拦截非法 type 并报错。写数字 / 布尔请用 `{"type":"text","text":"<字符串形式>"}`(服务端按内容自动识别),不要再用旧的 `value` 字段 +- **dataValidation 三语义**:不传字段=保留;`{type:"none"}`=清除;`{type:"dropdown"/"checkbox",...}`=覆盖。无需先 read 再回传,引擎自动保留原 DV +- **hyperlink 三语义**:不传字段=保留;`{type:"none"}`=清除;`{type:"path"/"sheet"/"range",...}`=覆盖。Agent 调用不要使用 `hyperlink:null` +- ★ **单次调用上限(强制)**:`range update` / `set-style` 行数 ≤ 1000,单元格总数建议 ≤ 5000(硬限 30000) +- ★ **大批量纯值写入用 `csv-put` 不用 `range update`**:当写入纯值(无公式、无超链接、无富文本对象)且数据量较大时(>5 行或 >20 单元格),必须使用 `csv-put`。`csv-put` 接受 CSV 文本直接写入,无需构造二维 JSON 数组,支持自动扩容,更简洁高效。仅在需要写入公式、单元格级超链接、富文本对象,或仅更新少量单元格时才使用 `range update` +- `range update` 必填 `--values`;单元格级超链接通过 cell 的 `hyperlink` 字段表达,附件 / 图片 / 带样式片段通过 `--values` 内的 richText 富格式表达,CLI 不再有 `--hyperlinks` 参数 +- `range update` 职责边界:`range update` 写入单元格内容(文本 / 公式 / 富文本对象),支持通过 `cellStyles` 附带 per-cell 样式(背景色 / 字号 / 对齐等)。但批量刷整片区域的统一样式时,应使用 `dws sheet range set-style`(如 "给表头加粗居中")或 `dws sheet range batch-set-style --batch `。两种方式各有适用场景:少量 cell 写值 + 样式一步到位用 `cellStyles`;大面积统一样式用 `set-style` +- `append` 自动定位到最后一行有数据的位置下方插入,无需手动计算行号 +- `append` 的 `--values` 二维数组中每行的列数必须一致,否则会报错。如果用户提供的数据中各行长度不同,必须先将短行用空字符串 `""` 补齐到与最长行相同的列数后再调用。追加的数据列数也应与工作表已有数据列数保持一致 +- `append` vs `range update`:追加新行用 `append`,修改已有单元格用 `range update` +- ★ **`append` / `csv-put` 不支持 `{}` skip、`dataValidation`、富文本、公式**:这些能力仅限 `range update`。`append` 和 `csv-put` 只接受原始值(字符串/数字/布尔),走的是不同的 MCP tool(`append_rows` / `set_range_from_csv`)。需要写入公式、超链接、下拉列表或跳过部分单元格时,必须使用 `range update` diff --git a/skills/multi/dingtalk-wiki/references/wiki.md b/skills/multi/dingtalk-wiki/references/wiki.md index d6f0693a..7c61c7b0 100644 --- a/skills/multi/dingtalk-wiki/references/wiki.md +++ b/skills/multi/dingtalk-wiki/references/wiki.md @@ -21,7 +21,6 @@ dws wiki member --help - 参数名不确定时 → 先 `--help`,再调用 - 报错 "unknown flag" 时 → `--help` 确认正确的 flag 名称 - 不确定某个功能是否存在时 → `dws wiki --help` 查看命令列表 -- `workspaceId` 是知识库空间 ID,只能用于 `wiki space/member --workspace`、`doc --workspace` 或 `doc search --workspace-ids`;不要把它传给 `doc list --folder`,也不要使用不存在的 `--space-id` ## 命令总览 @@ -38,6 +37,26 @@ Flags: --icon string 知识库图标标识 (选填) ``` +### 删除知识库 + +> **CAUTION:** 不可逆操作 — 执行前必须向用户确认。 + +``` +Usage: + dws wiki space delete [flags] +Example: + dws wiki space delete --workspace + dws wiki space delete --workspace "https://alidocs.dingtalk.com/i/spaces/xxx/overview" +Flags: + --workspace string 知识库 ID 或 URL (必填) +``` + +将指定知识库移入回收站。删除后知识库会进入回收站,可在回收站中恢复。 + +> **重要约束**: +> - 操作者必须具备知识库的 OWNER 角色。 +> - 删除操作不可逆(从回收站恢复除外),请确认后再执行。 + ### 查看知识库详情 ``` Usage: @@ -99,11 +118,29 @@ Flags: --role string 授予的角色 (必填,大小写敏感,必须全大写): MANAGER (管理者) / EDITOR (可编辑) / DOWNLOADER (可下载) / READER (可阅读) ``` -> **❗ 重要约束**: +> **重要约束**: > - 仅支持 USER 类型。 > - 角色枚举严格大写:MANAGER / EDITOR / DOWNLOADER / READER(OWNER 不可通过此接口添加,知识库创建者默认为所有者)。 > - 操作者需具备知识库的 OWNER 或 MANAGER 权限。 -> - 「我的文档」(myWikiSpace) 是个人空间,**不支持容器级成员管理**;后端会直接拒绝。如果你的目标只是把某篇文档分享给别人,请改用 `dws doc permission add` 在节点级别授权。 +> - 「我的文档」(myWikiSpace) 是个人空间,**不支持容器级成员管理**;后端会直接拒绝。如果你的目标只是把某篇文档分享给别人,请改用 `dws drive permission add` 在节点级别授权。 + +### 移除知识库成员 +``` +Usage: + dws wiki member remove [flags] +Example: + dws wiki member remove --workspace --users uid1 + dws wiki member remove --workspace --users uid1,uid2 +Flags: + --workspace string 目标知识库 ID 或 URL (必填) + --users strings 被移除的用户 userId 列表,逗号分隔 (必填,单次最多 30 个) +``` + +> **重要约束**: +> - OWNER 角色不可通过此接口移除。 +> - 操作者需具备知识库的 OWNER 或 MANAGER 权限。 +> - 移除后相关用户将无法访问该知识库下的内容(除非通过节点级权限另行授权)。 +> - 「我的文档」(myWikiSpace) 是个人空间,**不支持容器级成员管理**。 ### 修改知识库成员角色 ``` @@ -134,34 +171,154 @@ Flags: > 接口不支持游标分页,使用 `--limit` 一次性拉取。 +### 列出知识库节点 +``` +Usage: + dws wiki node list [flags] +Aliases: + list, ls +Example: + dws wiki node list --workspace --format json + dws wiki node list --workspace --folder --format json + dws wiki node list --workspace --limit 20 --cursor --format json +Flags: + --workspace string 知识库 ID (必填) + --folder string 父节点 nodeId (选填,不传则列出根目录) + --limit int 每页数量 (默认 50,最大 50) + --cursor string 分页游标 +``` + +### 在知识库中创建节点 +``` +Usage: + dws wiki node create [flags] +Example: + dws wiki node create --workspace --name "新文档" --format json + dws wiki node create --workspace --name "方案目录" --type folder --format json + dws wiki node create --workspace --name "数据表" --type asheet --folder --format json +Flags: + --workspace string 知识库 ID (必填) + --name string 节点名称 (必填) + --type string 节点类型: adoc / asheet / folder / axls (默认 adoc) + --folder string 父节点 nodeId (选填,不传则在根目录创建) +``` + +### 复制知识库节点 +``` +Usage: + dws wiki node copy [flags] +Example: + dws wiki node copy --workspace --node --format json + dws wiki node copy --workspace --node --folder --format json +Flags: + --workspace string 知识库 ID (必填) + --node string 源节点 ID (必填) + --folder string 目标文件夹 nodeId (选填) +``` + +### 移动知识库节点 +``` +Usage: + dws wiki node move [flags] +Example: + dws wiki node move --workspace --node --folder --format json + dws wiki node move --workspace --node --format json +Flags: + --workspace string 知识库 ID (必填) + --node string 源节点 ID (必填) + --folder string 目标文件夹 nodeId (选填) +``` + +### 删除知识库节点 + +> **CAUTION:** 不可逆操作 — 执行前必须向用户确认。 + +``` +Usage: + dws wiki node delete [flags] +Example: + dws wiki node delete --workspace --node + dws wiki node delete --workspace --node --yes +Flags: + --workspace string 知识库 ID (必填,用于权限校验) + --node string 节点 ID (必填) +``` + +将知识库中的节点移入回收站。权限要求: 对节点有"管理"权限。 + +### 在知识库中搜索节点 +``` +Usage: + dws wiki node search [flags] +Example: + dws wiki node search --workspace --query "方案" --format json + dws wiki node search --workspace --query "周报" --limit 10 --format json + dws wiki node search --workspace --query "设计" --extensions adoc,asheet --format json +Flags: + --workspace string 知识库 ID (必填) + --query string 搜索关键词 (必填) + --extensions string 按文件类型过滤,逗号分隔: adoc,asheet 等 (选填) + --limit int 每页数量 (选填) + --cursor string 分页游标 (选填) +``` + +在指定知识库空间内搜索节点。与 `drive search` 的区别: +- `wiki node search` — 限定在某个知识库空间内搜索(需要 `--workspace`) +- `drive search` — 全局搜索,聚合钉盘 + 文档空间结果 + +### 列出空间(支持钉盘空间类型) + +`wiki space list` 除了支持知识库类型(`orgWikiSpace` / `myWikiSpace`),还支持钉盘空间类型: + +``` +Usage: + dws wiki space list --type orgSpace --format json # 钉盘企业空间 + dws wiki space list --type mySpace --format json # 钉盘「我的文件」 + dws wiki space list --type orgWikiSpace --format json # 知识库(默认) + dws wiki space list --type myWikiSpace --format json # 我的文档 +Flags: + --type string 空间类型: + orgWikiSpace (默认) — 组织知识库 + myWikiSpace — 我的文档个人空间 + orgSpace — 钉盘企业空间 + mySpace — 钉盘「我的文件」 + --limit string 每页数量 1-50 (默认 20) + --cursor string 分页游标 (首页留空) +``` + +> 钉盘空间类型(`orgSpace` / `mySpace`)会自动路由到钉盘 MCP 服务,等同于原 `drive list-spaces`(已 deprecated)。 + ## 意图判断 - 用户说"创建知识库/新建知识库" → `space create` - 用户说"查看知识库/知识库详情" → `space get` - 用户说"我的知识库/知识库列表/有哪些知识库" → `space list` +- 用户说"列出钉盘空间/钉盘团队空间" → `space list --type orgSpace` - 用户说"搜索知识库/找知识库" → `space search` -- 用户说"我的文档/个人空间" → `space search --type myWikiSpace` 或 `space list --type myWikiSpace` -- 用户说"把知识库分享给某人/给某人加入知识库/邀请进知识库" → `member add`(需 `--workspace` + `--users` + `--role`) +- 用户说"我的文档/个人空间" → `space list --type myWikiSpace` +- 用户说"知识库下的文件/知识库里有哪些文档/浏览知识库内容" → `node list`(需 `--workspace`) +- 用户说"在知识库里搜文档/空间内搜索" → `node search`(需 `--workspace` + `--query`) +- 用户说"在知识库里创建文档/新建文件夹" → `node create`(需 `--workspace` + `--name`) +- 用户说"复制知识库里的文档" → `node copy`(需 `--workspace` + `--node`) +- 用户说"移动知识库里的文档" → `node move`(需 `--workspace` + `--node`) +- 用户说"删除知识库里的文档/节点" → `node delete`(需 `--workspace` + `--node`) +- 用户说"把知识库分享给某人/给某人加入知识库/邀请进知识库" → `member add`(需 `--workspace` + `--user` + `--role`) - 用户说"修改某人在知识库的权限/调整成员角色" → `member update` +- 用户说"移除知识库成员/把某人从知识库移除/删除知识库成员" → `member remove`(需 `--workspace` + `--users`) - 用户说"知识库有哪些成员/查看知识库成员" → `member list` +- 用户说"删除知识库/移除知识库/把知识库删了" → `space delete`(需 `--workspace`) -> ** 跨产品路由(重要)**:`dws wiki` 只管知识库容器(space/member),**不提供查看知识库文件/文档的能力**。以下意图必须走 `dws doc`,不要在 wiki 下尝试 `node`/`file`/`list` 等子命令: ->- 用户说"知识库下的文件/知识库里有哪些文档/浏览知识库内容" → 先用 `dws wiki space list` 或 `space search` 拿到 `workspaceId`,再走 **`dws doc list --workspace `** ->- 用户说"读某个知识库里的某篇文档" → 先 `dws wiki space list/search` 拿 `workspaceId`,再 `dws doc search --query "<文档名>" --workspace-ids --format json` 找 `nodeId`,最后 **`dws doc read --node --format json`** ->- 用户说"在知识库里搜文档" → 走 **`dws doc search --workspace-ids `** ->- 用户说"在知识库里创建文档" → 走 **`dws doc create --workspace `** +> **跨产品路由说明**:知识库节点的**内容操作**(读取/编辑/块级操作)仍由 `dws doc` 承担: +>- 用户说"读某个知识库里的某篇文档" → 先 `node list` 拿到 nodeId,再走 **`dws doc read --node `** +>- 用户说"搜文件"(不指定空间) → 走 **`dws drive search`**(全局聚合搜索) -> **禁止反模式**: ->- `dws doc list --space-id `:`doc list` 没有 `--space-id` ->- `dws doc list --folder `:`--folder` 只接受文件夹 `nodeId` / 文件夹 URL,不接受知识库 `workspaceId` ->- `doc get --node `:读取正文使用 `dws doc read --node --format json` ->- 多个知识库同名时,不要默认取第一个;用 `doc list --workspace` / `doc search --workspace-ids` / `doc read` 验证哪个空间包含目标文档或目标文件夹 - -关键区分: -- wiki(知识库空间级管理:创建/查询/列出/搜索/成员管理) vs doc(文档内容级操作:搜索/读写/编辑/节点级权限) -- wiki space(知识库容器) vs drive(钉盘文件存储/上传/下载) -- **wiki member**(容器级,授权整个知识库)vs **doc permission**(节点级,授权单篇文档) - - 「我的文档」**只能用** `doc permission`,不能用 `wiki member` +关键区分(两层模型): +- **wiki node**(空间管理层:节点的列出/创建/复制/移动/删除/搜索)vs **doc**(内容层:读写/编辑/块级/评论/导出)vs **drive**(存储层:文件上传/下载/搜索/权限,不关心格式) +- **wiki node search**(空间内搜索,需 `--workspace`)vs **drive search**(全局搜索,聚合钉盘+文档空间) +- **wiki node create**(在空间中创建空文件实体)vs **doc create**(创建文档并写入内容) +- **wiki member**(容器级,授权整个知识库)vs **doc permission / drive permission**(节点级,授权单篇文档) + - 「我的文档」**只能用** `doc permission` / `drive permission`,不能用 `wiki member` +- **wiki space list --type orgSpace/mySpace**(列出钉盘空间)vs **wiki space list**(默认列出知识库) ## 核心工作流 @@ -175,26 +332,66 @@ dws wiki space list --type myWikiSpace --format json # 搜索知识库 dws wiki space search --query "产品" --format json -# 搜索「我的文档」 -dws wiki space search --type myWikiSpace --format json - # 创建知识库 dws wiki space create --name "新项目文档" --desc "项目相关文档归档" --format json # 查看知识库详情 dws wiki space get --workspace --format json -# ── 工作流: 读取某个知识库里的指定文档 ── +# ── 工作流: 浏览知识库内容 ── + +# 1. 获取知识库 ID +dws wiki space list --format json -# 1. 找知识库空间,取 workspaceId -dws wiki space search --query "评测记录" --format json +# 2. 列出根目录节点 +dws wiki node list --workspace --format json -# 2. 在该知识库内搜索文档,取 nodeId -dws doc search --query "MinHash 学习笔记" --workspace-ids --format json +# 3. 进入子目录 +dws wiki node list --workspace --folder --format json -# 3. 读取正文 +# 4. 读取文档内容(跨到 doc) dws doc read --node --format json +# ── 工作流: 在知识库中创建文档 ── + +# 1. 创建文档节点 +dws wiki node create --workspace --name "新方案" --format json + +# 2. 创建文件夹 +dws wiki node create --workspace --name "方案归档" --type folder --format json + +# 3. 在指定文件夹下创建 +dws wiki node create --workspace --name "子文档" --folder --format json + +# ── 工作流: 在知识库中搜索 ── + +# 在指定知识库内搜索 +dws wiki node search --workspace --query "方案" --format json + +# 按文件类型过滤 +dws wiki node search --workspace --query "周报" --extensions adoc --format json + +# ── 工作流: 列出钉盘空间 ── + +# 列出钉盘企业空间 +dws wiki space list --type orgSpace --format json + +# 获取钉盘「我的文件」 +dws wiki space list --type mySpace --format json + +# ── 工作流: 复制/移动节点 ── + +# 复制节点到另一个文件夹 +dws wiki node copy --workspace --node --folder --format json + +# 移动节点到另一个文件夹 +dws wiki node move --workspace --node --folder --format json + +# ── 工作流: 删除知识库节点 ── + +# 删除节点(会要求确认) +dws wiki node delete --workspace --node + # ── 工作流: 给知识库加成员 ── # 1. 先确认知识库 ID(避免授权到「我的文档」) @@ -205,19 +402,38 @@ dws wiki member add --workspace --users --role EDITOR --format jso # 3. 查看当前成员 dws wiki member list --workspace --format json + +# ── 工作流: 移除知识库成员 ── + +# 1. 查看当前成员 +dws wiki member list --workspace --format json + +# 2. 移除成员 +dws wiki member remove --workspace --users --format json + +# ── 工作流: 删除知识库 ── + +# 1. 确认知识库信息 +dws wiki space get --workspace --format json + +# 2. 删除知识库 +dws wiki space delete --workspace --format json ``` ## 上下文传递表 | 操作 | 从返回中提取 | 用于 | |------|-------------|------| -| `space create` | `workspaceId` | space get 的 --workspace / member add 的 --workspace | -| `space list` | `workspaceId` | space get 的 --workspace / member add 的 --workspace | -| `space search` | `workspaceId` | space get 的 --workspace / member add 的 --workspace | +| `space create` | `workspaceId` | node list / member add 的 --workspace | +| `space list` | `workspaceId` | node list / member add 的 --workspace | +| `space search` | `workspaceId` | node list / member add 的 --workspace | | `space get` | `spaceUrl` | 分享给用户 | -| `member list` | `userId` | member update 的 --users | +| `node list` | `nodeId` | node copy/move/delete 的 --node / `dws doc read` 的 --node | +| `node search` | `nodeId` | node copy/move/delete 的 --node / `dws doc read` 的 --node | +| `node create` | `nodeId` | node copy/move/delete 的 --node / `dws doc read` 的 --node | +| `member list` | `userId` | member update 的 --users / member remove 的 --users | ## 相关产品 -- [doc](./doc.md) — 文档内容级操作(搜索/读写/编辑文档、知识库内文档管理) -- [drive](./drive.md) — 钉盘文件存储/上传/下载 +- [doc](./doc.md) — 内容层:文档读写/编辑/块级操作/评论/导出(仅对自研文档有意义) +- [drive](./drive.md) — 存储层:文件列出/搜索/上传/下载/复制/移动/重命名/删除/权限(不关心文件格式) diff --git a/test/cli_compat/aitable_test.go b/test/cli_compat/aitable_test.go index f2a9f6e4..b40fe7e9 100644 --- a/test/cli_compat/aitable_test.go +++ b/test/cli_compat/aitable_test.go @@ -442,6 +442,7 @@ func TestAitableExportData_should_call_export_data_with_wukong_payload(t *testin "--table-id", "T1", "--format", "excel", "--timeout-ms", "1000", + "--dry-run", }) if err := root.Execute(); err != nil { t.Fatalf("unexpected error: %v", err) @@ -454,11 +455,18 @@ func TestAitableExportData_should_call_export_data_with_wukong_payload(t *testin DryRun bool `json:"dry_run"` } `json:"invocation"` } + var flatInv struct { + Tool string `json:"tool"` + Params map[string]any `json:"params"` + DryRun bool `json:"dry_run"` + } if err := json.Unmarshal(out.Bytes(), &inv); err != nil { t.Fatalf("json.Unmarshal() error = %v\noutput:\n%s", err, out.String()) } - if inv.Invocation.Tool != "" && !inv.Invocation.DryRun { + if inv.Invocation.Tool != "" { cap.record(inv.Invocation.Tool, inv.Invocation.Params, "") + } else if err := json.Unmarshal(out.Bytes(), &flatInv); err == nil && flatInv.Tool != "" { + cap.record(flatInv.Tool, flatInv.Params, "") } assertToolName(t, cap, "export_data") @@ -509,6 +517,109 @@ func TestAitableViewUpdate_should_pass_description(t *testing.T) { assertToolArg(t, cap, "viewDescription", map[string]any{"content": []any{}}) } +func TestAitableRecordHistoryList_should_call_query_record_history(t *testing.T) { + cap := setupTestDeps(t, "aitable") + root := buildRoot() + if err := execCmd(t, root, []string{"aitable", "record", "history-list"}, map[string]string{ + "base-id": "B1", + "table-id": "T1", + "record-id": "R1", + "limit": "30", + "offset": "10", + }); err != nil { + t.Fatalf("unexpected error: %v", err) + } + assertToolName(t, cap, "query_record_history") + assertToolArg(t, cap, "baseId", "B1") + assertToolArg(t, cap, "tableId", "T1") + assertToolArg(t, cap, "recordId", "R1") + assertToolArg(t, cap, "limit", 30) + assertToolArg(t, cap, "offset", 10) +} + +func TestAitableRecordUpsert_should_call_record_upsert(t *testing.T) { + cap := setupTestDeps(t, "aitable") + root := buildRoot() + if err := execCmd(t, root, []string{"aitable", "record", "upsert"}, map[string]string{ + "base-id": "B1", + "table-id": "T1", + "records": `[{"recordId":"R1","cells":{"fld":"updated"}},{"cells":{"fld":"new"}}]`, + }); err != nil { + t.Fatalf("unexpected error: %v", err) + } + assertToolName(t, cap, "record_upsert") + assertToolArg(t, cap, "baseId", "B1") + assertToolArg(t, cap, "tableId", "T1") +} + +func TestAitableViewLock_should_call_lock_or_unlock_view(t *testing.T) { + cap := setupTestDeps(t, "aitable") + root := buildRoot() + if err := execCmdWithArgs(t, root, []string{"aitable", "view", "lock"}, map[string]string{ + "base-id": "B1", + "table-id": "T1", + "view-id": "V1", + }, []string{"--off"}); err != nil { + t.Fatalf("unexpected error: %v", err) + } + assertToolName(t, cap, "lock_or_unlock_view") + assertToolArg(t, cap, "action", "unlock") +} + +func TestAitableWorkflowList_should_call_list_workflows(t *testing.T) { + cap := setupTestDeps(t, "aitable") + root := buildRoot() + if err := execCmd(t, root, []string{"aitable", "workflow", "list"}, map[string]string{ + "base-id": "B1", + "limit": "50", + "offset": "100", + }); err != nil { + t.Fatalf("unexpected error: %v", err) + } + assertToolName(t, cap, "list_workflows") + assertToolArg(t, cap, "baseId", "B1") + assertToolArg(t, cap, "limit", 50) + assertToolArg(t, cap, "offset", 100) +} + +func TestAitableAdvpermRoleCreate_should_call_create_role(t *testing.T) { + cap := setupTestDeps(t, "aitable") + root := buildRoot() + if err := execCmd(t, root, []string{"aitable", "advperm", "role-create"}, map[string]string{ + "base-id": "B1", + "name": "市场可读", + "sub-roles": `[{"targetId":"T1","targetType":"sheet","authLevel":"read"}]`, + }); err != nil { + t.Fatalf("unexpected error: %v", err) + } + assertToolName(t, cap, "create_role") + assertToolArg(t, cap, "baseId", "B1") + assertToolArg(t, cap, "name", "市场可读") + assertToolArg(t, cap, "subRoles", []any{map[string]any{ + "targetId": "T1", + "targetType": "sheet", + "authLevel": "read", + }}) +} + +func TestAitableSectionMoveNode_should_call_move_nsheet_node(t *testing.T) { + cap := setupTestDeps(t, "aitable") + root := buildRoot() + if err := execCmd(t, root, []string{"aitable", "section", "move-node"}, map[string]string{ + "base-id": "B1", + "node-id": "NODE_001", + "new-parent-section-id": "SEC_001", + "target-index": "0", + }); err != nil { + t.Fatalf("unexpected error: %v", err) + } + assertToolName(t, cap, "move_nsheet_node") + assertToolArg(t, cap, "baseId", "B1") + assertToolArg(t, cap, "nodeId", "NODE_001") + assertToolArg(t, cap, "newParentSectionId", "SEC_001") + assertToolArg(t, cap, "targetIndex", 0) +} + func TestAitableSurface_should_not_expose_open_source_only_commands(t *testing.T) { root := buildRoot() for _, path := range [][]string{ diff --git a/test/cli_compat/helpers_test.go b/test/cli_compat/helpers_test.go index 1433dd08..e782b704 100644 --- a/test/cli_compat/helpers_test.go +++ b/test/cli_compat/helpers_test.go @@ -135,6 +135,12 @@ func execCmdWithArgs(t *testing.T, root *cobra.Command, path []string, flags map cap := getCapture(t) cliArgs := []string{"-f", "json"} + if cap != nil { + cliArgs = append(cliArgs, "--mock") + if !cap.dryRun && !cap.preview { + cliArgs = append(cliArgs, "--dry-run") + } + } cliArgs = append(cliArgs, path...) // Add dry-run if capture says so @@ -179,10 +185,10 @@ func execCmdWithArgs(t *testing.T, root *cobra.Command, path []string, flags map } if cap != nil { if jsonErr := json.Unmarshal(out.Bytes(), &inv); jsonErr == nil && inv.Invocation.Tool != "" { - if !inv.Invocation.DryRun || cap.preview { + if !inv.Invocation.DryRun || cap.preview || !cap.dryRun { cap.record(inv.Invocation.Tool, inv.Invocation.Params, "") } - // For dry-run: don't record (matches old behavior: assertCallCount == 0) + // Explicit dry-run captures still do not record, matching old assertCallCount == 0 behavior. } else if jsonErr := json.Unmarshal(out.Bytes(), &flatInv); jsonErr == nil && flatInv.Tool != "" { dryRunPreview := flatInv.DryRun || cap.dryRun || cap.preview if !dryRunPreview || cap.preview {