From 4c43108bdf70bb3047fd7574cac2264f0987fc8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= <47820304+PeterGuy326@users.noreply.github.com> Date: Thu, 9 Jul 2026 21:28:57 +0800 Subject: [PATCH] sync wukong hardcoded command additions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sync wukong hardcoded command additions Co-authored-by: 修雨 <47820304+PeterGuy326@users.noreply.github.com> --- README.md | 6 +- README_zh.md | 6 +- auto-test/cli_to_mcp/README.md | 16 + .../testcases/agoal/test_01_agoal_commands.py | 193 ++++++ .../test_29_group_notice_and_new_commands.py | 163 +++++ auto-test/cli_to_mcp/testcases/conftest.py | 8 + .../testcases/doc/test_12_import.py | 54 ++ .../mail/test_11_mail_new_commands.py | 143 +++++ ...t_12_mail_allowlist_blocklist_autoreply.py | 133 ++++ .../sheet/test_36_group_dimension.py | 80 +++ .../testcases/test_utils/__init__.py | 58 ++ internal/app/skill_setup.go | 4 +- internal/helpers/agoal.go | 603 ++++++++++++++++++ internal/helpers/chat.go | 271 +++++++- internal/helpers/doc.go | 292 ++++++++- internal/helpers/mail.go | 396 ++++++++++-- internal/helpers/register_products.go | 1 + internal/helpers/sheet.go | 2 + internal/helpers/sheet_batch.go | 27 + internal/helpers/sheet_dimension.go | 79 ++- internal/helpers/wukong_sync_commands_test.go | 182 ++++++ internal/syncdata/endpoints.go | 1 + internal/syncdata/routing.go | 1 + scripts/install.sh | 2 +- skills/mono/SKILL.md | 1 + skills/mono/references/products/agoal.md | 161 +++++ skills/mono/references/products/chat.md | 134 ++++ skills/mono/references/products/doc.md | 42 ++ .../references/products/doc/doc-import.md | 53 ++ skills/mono/references/products/mail.md | 160 +++++ skills/mono/references/products/sheet.md | 46 +- .../products/sheet/sheet-batch-operations.md | 19 + .../sheet/sheet-dimension-operations.md | 44 ++ skills/multi/dingtalk-agoal/SKILL.md | 43 ++ .../multi/dingtalk-agoal/references/agoal.md | 77 +++ skills/multi/dingtalk-aisearch/SKILL.md | 2 +- skills/multi/dingtalk-aitable/SKILL.md | 2 +- skills/multi/dingtalk-attendance/SKILL.md | 2 +- skills/multi/dingtalk-calendar/SKILL.md | 2 +- skills/multi/dingtalk-chat/SKILL.md | 2 +- skills/multi/dingtalk-chat/references/chat.md | 134 ++++ skills/multi/dingtalk-contact/SKILL.md | 2 +- skills/multi/dingtalk-devdoc/SKILL.md | 2 +- skills/multi/dingtalk-ding/SKILL.md | 2 +- skills/multi/dingtalk-doc/SKILL.md | 2 +- skills/multi/dingtalk-doc/references/doc.md | 42 ++ .../dingtalk-doc/references/doc/doc-import.md | 53 ++ skills/multi/dingtalk-drive/SKILL.md | 2 +- skills/multi/dingtalk-live/SKILL.md | 2 +- skills/multi/dingtalk-mail/SKILL.md | 2 +- skills/multi/dingtalk-mail/references/mail.md | 160 +++++ skills/multi/dingtalk-minutes/SKILL.md | 2 +- skills/multi/dingtalk-oa/SKILL.md | 2 +- skills/multi/dingtalk-report/SKILL.md | 2 +- skills/multi/dingtalk-sheet/SKILL.md | 2 +- .../multi/dingtalk-sheet/references/sheet.md | 46 +- .../sheet/sheet-batch-operations.md | 19 + .../sheet/sheet-dimension-operations.md | 44 ++ skills/multi/dingtalk-skill/SKILL.md | 2 +- skills/multi/dingtalk-todo/SKILL.md | 2 +- skills/multi/dingtalk-wiki/SKILL.md | 2 +- 61 files changed, 3954 insertions(+), 83 deletions(-) create mode 100644 auto-test/cli_to_mcp/README.md create mode 100644 auto-test/cli_to_mcp/testcases/agoal/test_01_agoal_commands.py create mode 100644 auto-test/cli_to_mcp/testcases/chat/test_29_group_notice_and_new_commands.py create mode 100644 auto-test/cli_to_mcp/testcases/conftest.py create mode 100644 auto-test/cli_to_mcp/testcases/doc/test_12_import.py create mode 100644 auto-test/cli_to_mcp/testcases/mail/test_11_mail_new_commands.py create mode 100644 auto-test/cli_to_mcp/testcases/mail/test_12_mail_allowlist_blocklist_autoreply.py create mode 100644 auto-test/cli_to_mcp/testcases/sheet/test_36_group_dimension.py create mode 100644 auto-test/cli_to_mcp/testcases/test_utils/__init__.py create mode 100644 internal/helpers/agoal.go create mode 100644 internal/helpers/wukong_sync_commands_test.go create mode 100644 skills/mono/references/products/agoal.md create mode 100644 skills/mono/references/products/doc/doc-import.md create mode 100644 skills/multi/dingtalk-agoal/SKILL.md create mode 100644 skills/multi/dingtalk-agoal/references/agoal.md create mode 100644 skills/multi/dingtalk-doc/references/doc/doc-import.md diff --git a/README.md b/README.md index f644f2d0..14ac8ebe 100644 --- a/README.md +++ b/README.md @@ -71,9 +71,9 @@ The installer ships skills in one of two layouts. CLI commands (`dws aitable ... | Mode | What gets installed | Best for | |------|----------------------|----------| | **mono** (stable, default) | One `dws` skill covering all products | Cross-product workflows; single entry point | -| **multi** 🧪 **EXPERIMENTAL** | 18 per-product skills (`dingtalk-aitable`, `dingtalk-calendar`, `dingtalk-chat`, ...) | Single-product tasks; smaller context per call | +| **multi** 🧪 **EXPERIMENTAL** | 22 per-product skills (`dingtalk-aitable`, `dingtalk-calendar`, `dingtalk-chat`, ...) | Single-product tasks; smaller context per call | -> 🧪 **`multi` is currently EXPERIMENTAL / preview.** 18 product-scoped skills all pass the dispatch verifier, but interface, naming and cross-skill references may change in future releases. For production / shared environments, prefer `mono`. File issues if you hit problems. +> 🧪 **`multi` is currently EXPERIMENTAL / preview.** 22 product-scoped skills all pass the dispatch verifier, but interface, naming and cross-skill references may change in future releases. For production / shared environments, prefer `mono`. File issues if you hit problems. How to pick: @@ -331,7 +331,7 @@ dws aitable record query --base-id BASE_ID --table-id TABLE_ID --limit 10 The repo ships a complete Agent Skill system under `skills/`, now organized into two layouts: - `skills/mono/` — single-skill layout (one `SKILL.md` + `references/products/`), recommended default. -- `skills/multi/` — per-product skills (`dingtalk-aitable/`, `dingtalk-calendar/`, `dingtalk-chat/`, ... 20 products in total), each with its own `SKILL.md`. 🧪 **EXPERIMENTAL / preview — see banner in each multi `SKILL.md` for caveats.** +- `skills/multi/` — per-product skills (`dingtalk-aitable/`, `dingtalk-calendar/`, `dingtalk-chat/`, ... 22 products in total), each with its own `SKILL.md`. 🧪 **EXPERIMENTAL / preview — see banner in each multi `SKILL.md` for caveats.** After installing, AI tools like Claude Code / Cursor can operate DingTalk directly through natural language: diff --git a/README_zh.md b/README_zh.md index 72f1ea22..ae33ec58 100644 --- a/README_zh.md +++ b/README_zh.md @@ -71,9 +71,9 @@ irm https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-workspace-cli/ma | 模式 | 安装内容 | 适合场景 | |------|----------|----------| | **mono**(稳定,默认) | 一个 `dws` skill,覆盖全部产品 | 跨产品组合操作;单一入口召唤 | -| **multi** 🧪 **试验版 / Preview** | 20 个独立产品 skill(`dingtalk-aitable` / `dingtalk-calendar` / `dingtalk-chat` ...) | 单产品任务;每次召唤上下文更小 | +| **multi** 🧪 **试验版 / Preview** | 22 个独立产品 skill(`dingtalk-aitable` / `dingtalk-calendar` / `dingtalk-chat` ...) | 单产品任务;每次召唤上下文更小 | -> 🧪 **multi 模式当前为 EXPERIMENTAL(试验版 / Preview)**。20 个独立 skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整。生产 / 共享环境建议优先用 `mono`。问题请提 issue 反馈。 +> 🧪 **multi 模式当前为 EXPERIMENTAL(试验版 / Preview)**。22 个独立 skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整。生产 / 共享环境建议优先用 `mono`。问题请提 issue 反馈。 怎么选: @@ -328,7 +328,7 @@ dws aitable record query --base-id BASE_ID --table-id TABLE_ID --limit 10 仓库内置完整的 Agent Skill 体系(`skills/` 目录),目前重组为两套布局: - `skills/mono/` — 单 skill 布局(一个 `SKILL.md` + `references/products/`),默认推荐。 -- `skills/multi/` — 每个产品一个独立 skill(`dingtalk-aitable/` / `dingtalk-calendar/` / `dingtalk-chat/` ... 共 18 个),每个 skill 自带 `SKILL.md`。🧪 **试验版 / Preview — 各 multi `SKILL.md` 头部有详细注意事项。** +- `skills/multi/` — 每个产品一个独立 skill(`dingtalk-aitable/` / `dingtalk-calendar/` / `dingtalk-chat/` ... 共 22 个),每个 skill 自带 `SKILL.md`。🧪 **试验版 / Preview — 各 multi `SKILL.md` 头部有详细注意事项。** 安装之后,Claude Code / Cursor 等 AI 工具就能通过自然语言直接操作钉钉: diff --git a/auto-test/cli_to_mcp/README.md b/auto-test/cli_to_mcp/README.md new file mode 100644 index 00000000..b2bdd695 --- /dev/null +++ b/auto-test/cli_to_mcp/README.md @@ -0,0 +1,16 @@ +# cli_to_mcp smoke tests + +This directory contains lightweight command-to-tool contract tests for hardcoded +DWS commands synced from `dws-wukong`. + +The tests do not call live DingTalk APIs. They exercise command help, validation, +and `--dry-run` output so command paths and MCP argument mappings stay stable. + +Run with an already built binary: + +```bash +DWS_BIN=/path/to/dws pytest auto-test/cli_to_mcp/testcases +``` + +If `DWS_BIN` is not set, the runner falls back to `go run ./cmd` from the repo +root. diff --git a/auto-test/cli_to_mcp/testcases/agoal/test_01_agoal_commands.py b/auto-test/cli_to_mcp/testcases/agoal/test_01_agoal_commands.py new file mode 100644 index 00000000..bc40a452 --- /dev/null +++ b/auto-test/cli_to_mcp/testcases/agoal/test_01_agoal_commands.py @@ -0,0 +1,193 @@ +from test_utils import combined_output, dry_run_args + + +def assert_ok(result): + output = combined_output(result) + assert result.returncode == 0, output + return output + + +def test_agoal_strategy_and_contract_cli_to_mcp(dws): + output = assert_ok( + dws.run_raw( + "agoal", + "strategy", + "list", + "--scope-type", + "PERSONAL", + "--scope-id", + "user123", + "--request-id", + "req-1", + "--dry-run", + ) + ) + assert "list_strategy_decodings" in output + assert dry_run_args(output) == { + "scopeType": "PERSONAL", + "openId": "user123", + "requestId": "req-1", + } + + output = assert_ok( + dws.run_raw( + "agoal", + "strategy", + "update", + "--profile-id", + "profile123", + "--content", + '[{"id":"e1","title":{"title":"new"}}]', + "--dry-run", + ) + ) + assert "update_strategy_decoding" in output + assert dry_run_args(output) == { + "profileId": "profile123", + "content": [{"id": "e1", "title": {"title": "new"}}], + } + + output = assert_ok(dws.run_raw("agoal", "contract", "fields", "--dry-run")) + assert "list_op_contract_fields" in output + assert dry_run_args(output) == {} + + output = assert_ok( + dws.run_raw( + "agoal", + "contract", + "update", + "--contract-id", + "contract123", + "--dimensions", + '[{"id":"dim1","title":"metric"}]', + "--audit-config", + '{"needAudit":true}', + "--objective-template", + '{"id":"tpl1"}', + "--dry-run", + ) + ) + assert "update_op_contract" in output + assert dry_run_args(output) == { + "contractId": "contract123", + "dimensions": [{"id": "dim1", "title": "metric"}], + "auditConfig": '{"needAudit":true}', + "objectiveTemplate": '{"id":"tpl1"}', + } + + +def test_agoal_scorecard_user_report_template_cli_to_mcp(dws): + output = assert_ok( + dws.run_raw( + "agoal", + "scorecard", + "detail", + "--selected-time", + "2026-01-01T00:00:00+08:00", + "--dept-id", + "dept123", + "--dry-run", + ) + ) + assert "get_score_card_detail" in output + args = dry_run_args(output) + assert args["deptId"] == "dept123" + assert args["selectedTime"] == 1767196800000 + + output = assert_ok( + dws.run_raw( + "agoal", + "scorecard", + "update", + "--dept-id", + "dept123", + "--selected-time", + "2026-01-01", + "--id", + "sc123", + "--tracking-period-type", + "MONTHLY", + "--content", + '[{"id":"dim1","items":[]}]', + "--dry-run", + ) + ) + assert "update_score_card" in output + args = dry_run_args(output) + assert args["selectedTime"] == 1767196800000 + assert args["content"] == [{"id": "dim1", "items": []}] + + output = assert_ok( + dws.run_raw( + "agoal", + "user", + "objectives", + "--user-id", + "user123", + "--rule-id", + "rule123", + "--period-ids", + "p1,p2", + "--dry-run", + ) + ) + assert "list_user_objectives" in output + assert dry_run_args(output) == { + "dingUserId": "user123", + "objectiveRuleId": "rule123", + "periodIds": ["p1", "p2"], + } + + output = assert_ok( + dws.run_raw( + "agoal", + "report", + "submit-detail", + "--template-id", + "tpl123", + "--submit-state", + "LATE", + "--query-date", + "2026-06-18T00:00:00+08:00", + "--page", + "1", + "--page-size", + "20", + "--keyword", + "alice", + "--dry-run", + ) + ) + assert "get_submit_detail" in output + assert dry_run_args(output) == { + "templateId": "tpl123", + "submitState": "LATE", + "queryDate": "2026-06-18", + "page": 1, + "pageSize": 20, + "keyword": "alice", + } + + output = assert_ok( + dws.run_raw( + "agoal", + "obj-template", + "create-or-update", + "--title", + "tpl", + "--dimensions", + '[{"title":"dim"}]', + "--objective-weight", + "--dimension-weight", + "--compute-by-weight", + "--dry-run", + ) + ) + assert "create_or_update_obj_template" in output + assert dry_run_args(output) == { + "title": "tpl", + "dimensions": '[{"title":"dim"}]', + "objectiveWeight": True, + "dimensionWeight": True, + "computeByWeight": True, + } diff --git a/auto-test/cli_to_mcp/testcases/chat/test_29_group_notice_and_new_commands.py b/auto-test/cli_to_mcp/testcases/chat/test_29_group_notice_and_new_commands.py new file mode 100644 index 00000000..2fda8c65 --- /dev/null +++ b/auto-test/cli_to_mcp/testcases/chat/test_29_group_notice_and_new_commands.py @@ -0,0 +1,163 @@ +from test_utils import combined_output, dry_run_args + + +def assert_ok(result): + output = combined_output(result) + assert result.returncode == 0, output + return output + + +def test_group_notice_cli_to_mcp(dws): + output = assert_ok( + dws.run_raw( + "chat", + "group", + "notice", + "create", + "--group", + "cid123", + "--content", + "maintenance tonight", + "--sticky", + "--send-ding", + "--dry-run", + ) + ) + assert "create_group_notice" in output + assert dry_run_args(output) == { + "openConversationId": "cid123", + "content": "maintenance tonight", + "sticky": True, + "sendDing": True, + } + + output = assert_ok( + dws.run_raw( + "chat", + "group", + "notice", + "edit", + "--group", + "cid123", + "--notice-id", + "notice123", + "--content", + "updated", + "--dry-run", + ) + ) + assert "edit_group_notice" in output + assert dry_run_args(output) == { + "openConversationId": "cid123", + "dataId": "notice123", + "content": "updated", + } + + output = assert_ok( + dws.run_raw( + "chat", + "group", + "notice", + "get", + "--group", + "cid123", + "--notice-id", + "notice123", + "--dry-run", + ) + ) + assert "get_group_notice" in output + assert dry_run_args(output) == { + "openConversationId": "cid123", + "dataId": "notice123", + } + + output = assert_ok( + dws.run_raw( + "chat", + "group", + "notice", + "list", + "--group", + "cid123", + "--limit", + "20", + "--cursor", + "next", + "--scheduled", + "--dry-run", + ) + ) + assert "list_group_notices" in output + assert dry_run_args(output) == { + "openConversationId": "cid123", + "limit": 20, + "cursor": "next", + "scheduled": True, + } + + +def test_chat_misc_new_commands_cli_to_mcp(dws): + output = assert_ok( + dws.run_raw( + "chat", + "group", + "share-invite", + "--source", + "sourceCid", + "--target", + "targetCid", + "--expires-seconds", + "3600", + "--uuid", + "uuid-1", + "--dry-run", + ) + ) + assert "share_group_invite_url" in output + assert dry_run_args(output) == { + "sourceOpenConversationId": "sourceCid", + "targetOpenConversationId": "targetCid", + "expiresSeconds": 3600, + "uuid": "uuid-1", + } + + output = assert_ok( + dws.run_raw("chat", "text", "translate", "--query", "hello", "--to", "zh_CN", "--dry-run") + ) + assert "translate" in output + assert dry_run_args(output) == {"query": "hello", "to": "zh_CN"} + + output = assert_ok( + dws.run_raw( + "chat", + "category", + "create-smart", + "--name", + "priority", + "--keywords", + "alpha,beta", + "--members", + "uid1,uid2", + "--dry-run", + ) + ) + assert "create_smart_conv_category" in output + assert dry_run_args(output) == { + "title": "priority", + "keywords": ["alpha", "beta"], + "memberOpenDingTalkIds": ["uid1", "uid2"], + } + + output = assert_ok( + dws.run_raw( + "chat", + "message", + "list-emotion-replies", + "--msg-ids", + "msg1,msg2", + "--dry-run", + ) + ) + assert "list_message_emotion_replies" in output + assert dry_run_args(output) == {"openMessageIds": ["msg1", "msg2"]} diff --git a/auto-test/cli_to_mcp/testcases/conftest.py b/auto-test/cli_to_mcp/testcases/conftest.py new file mode 100644 index 00000000..c1ce4468 --- /dev/null +++ b/auto-test/cli_to_mcp/testcases/conftest.py @@ -0,0 +1,8 @@ +import pytest + +from test_utils import DWSRunner + + +@pytest.fixture(scope="session") +def dws(): + return DWSRunner() diff --git a/auto-test/cli_to_mcp/testcases/doc/test_12_import.py b/auto-test/cli_to_mcp/testcases/doc/test_12_import.py new file mode 100644 index 00000000..80143d0f --- /dev/null +++ b/auto-test/cli_to_mcp/testcases/doc/test_12_import.py @@ -0,0 +1,54 @@ +from test_utils import combined_output + + +def assert_ok(result): + output = combined_output(result) + assert result.returncode == 0, output + return output + + +def test_doc_import_help_and_validation(dws, tmp_path): + output = assert_ok(dws.run_raw("doc", "import", "--help")) + assert "dws doc import" in output + assert "--file string" in output + assert "--workspace string" in output + assert "--name string" in output + + result = dws.run_raw("doc", "import", "--file", str(tmp_path / "missing.md"), "--dry-run") + output = combined_output(result) + assert result.returncode != 0 + assert "cannot read file" in output + + bad = tmp_path / "bad.exe" + bad.write_text("bad", encoding="utf-8") + result = dws.run_raw("doc", "import", "--file", str(bad), "--dry-run") + output = combined_output(result) + assert result.returncode != 0 + assert "unsupported file format" in output + + +def test_doc_import_dry_run(dws, tmp_path): + source = tmp_path / "sample.md" + source.write_text("# Sample\n\nhello\n", encoding="utf-8") + + output = assert_ok( + dws.run_raw( + "doc", + "import", + "--file", + str(source), + "--name", + "Imported Sample", + "--workspace", + "workspace123", + "--dry-run", + ) + ) + assert "Imported Sample" in output + assert "sample.md" in output + assert "md" in output + + +def test_doc_import_get_dry_run(dws): + output = assert_ok(dws.run_raw("doc", "import", "get", "--task-id", "task123", "--dry-run")) + assert "task123" in output diff --git a/auto-test/cli_to_mcp/testcases/mail/test_11_mail_new_commands.py b/auto-test/cli_to_mcp/testcases/mail/test_11_mail_new_commands.py new file mode 100644 index 00000000..1a6bd5d0 --- /dev/null +++ b/auto-test/cli_to_mcp/testcases/mail/test_11_mail_new_commands.py @@ -0,0 +1,143 @@ +import os + +from test_utils import combined_output, dry_run_args + + +def mail_email() -> str: + return os.environ.get("DINGTALK_MAIL_EMAIL", "user@example.com") + + +def assert_ok(result): + output = combined_output(result) + assert result.returncode == 0, output + return output + + +def assert_fails(result, expected: str): + output = combined_output(result) + assert result.returncode != 0, output + assert expected in output + + +def test_mailbox_profile_cli_to_mcp(dws): + output = assert_ok(dws.run_raw("mail", "mailbox", "profile", "--help")) + assert "dws mail mailbox profile" in output + assert "--email string" in output + + assert_fails(dws.run_raw("mail", "mailbox", "profile"), "email") + + output = assert_ok( + dws.run_raw("mail", "mailbox", "profile", "--email", mail_email(), "--dry-run") + ) + assert "get_mailbox_profile" in output + assert dry_run_args(output) == {"email": mail_email()} + + +def test_message_batch_get_cli_to_mcp(dws): + output = assert_ok(dws.run_raw("mail", "message", "batch-get", "--help")) + assert "dws mail message batch-get" in output + assert "--email string" in output + assert "--ids string" in output + + assert_fails( + dws.run_raw("mail", "message", "batch-get", "--email", mail_email()), + "ids", + ) + + too_many_ids = ",".join(f"msg_{i:02d}" for i in range(21)) + assert_fails( + dws.run_raw( + "mail", + "message", + "batch-get", + "--email", + mail_email(), + "--ids", + too_many_ids, + "--dry-run", + ), + "20", + ) + + output = assert_ok( + dws.run_raw( + "mail", + "message", + "batch-get", + "--email", + mail_email(), + "--ids", + "msg_001,msg_002", + "--dry-run", + ) + ) + assert "get_email_by_message_id" in output + assert "msg_001" in output + assert "msg_002" in output + + +def test_sent_message_recall_cli_to_mcp(dws): + output = assert_ok(dws.run_raw("mail", "sent-message", "recall", "--help")) + assert "dws mail sent-message recall" in output + assert "--subject string" in output + assert "--yes" in output + + assert_fails( + dws.run_raw( + "mail", + "sent-message", + "recall", + "--email", + mail_email(), + "--id", + "msg_001", + "--subject", + "subject", + ), + "--yes", + ) + + output = assert_ok( + dws.run_raw( + "mail", + "sent-message", + "recall", + "--email", + mail_email(), + "--id", + "msg_001", + "--subject", + "subject", + "--yes", + "--dry-run", + ) + ) + assert "recall_sent_message" in output + assert dry_run_args(output) == { + "email": mail_email(), + "id": "msg_001", + "subject": "subject", + } + + +def test_sent_message_recall_detail_cli_to_mcp(dws): + output = assert_ok(dws.run_raw("mail", "sent-message", "recall-detail", "--help")) + assert "dws mail sent-message recall-detail" in output + assert "--email string" in output + assert "--id string" in output + assert "FINISHED" in output + + output = assert_ok( + dws.run_raw( + "mail", + "sent-message", + "recall-detail", + "--email", + mail_email(), + "--id", + "task_001", + "--dry-run", + ) + ) + assert "get_recall_detail" in output + assert dry_run_args(output) == {"email": mail_email(), "id": "task_001"} diff --git a/auto-test/cli_to_mcp/testcases/mail/test_12_mail_allowlist_blocklist_autoreply.py b/auto-test/cli_to_mcp/testcases/mail/test_12_mail_allowlist_blocklist_autoreply.py new file mode 100644 index 00000000..2046693c --- /dev/null +++ b/auto-test/cli_to_mcp/testcases/mail/test_12_mail_allowlist_blocklist_autoreply.py @@ -0,0 +1,133 @@ +import os + +from test_utils import combined_output, dry_run_args + + +def mail_email() -> str: + return os.environ.get("DINGTALK_MAIL_EMAIL", "user@example.com") + + +def assert_ok(result): + output = combined_output(result) + assert result.returncode == 0, output + return output + + +def test_auto_reply_update_cli_to_mcp(dws): + output = assert_ok(dws.run_raw("mail", "auto-reply", "update", "--help")) + for flag in ("--email string", "--enabled string", "--start string", "--end string", "--scope string", "--content string"): + assert flag in output + + output = assert_ok( + dws.run_raw( + "mail", + "auto-reply", + "update", + "--email", + mail_email(), + "--enabled", + "true", + "--start", + "2026/07/01 09:00:00 +0800", + "--end", + "2026/07/07 18:00:00 +0800", + "--scope", + "all", + "--content", + "out of office", + "--dry-run", + ) + ) + assert "update_auto_reply" in output + assert dry_run_args(output) == { + "email": mail_email(), + "enabled": True, + "startTime": "2026/07/01 09:00:00 +0800", + "endTime": "2026/07/07 18:00:00 +0800", + "scope": "all", + "content": "out of office", + } + + +def test_allow_list_cli_to_mcp(dws): + output = assert_ok(dws.run_raw("mail", "allow-list", "list", "--email", mail_email(), "--dry-run")) + assert "list_mailbox_allowlist" in output + assert dry_run_args(output) == {"email": mail_email()} + + output = assert_ok( + dws.run_raw( + "mail", + "allow-list", + "add", + "--email", + mail_email(), + "--entries", + "partner@example.com,@example.org", + "--dry-run", + ) + ) + assert "add_mailbox_allowlist" in output + assert dry_run_args(output) == { + "email": mail_email(), + "entries": ["partner@example.com", "@example.org"], + } + + output = assert_ok( + dws.run_raw( + "mail", + "allow-list", + "remove", + "--email", + mail_email(), + "--entries", + "partner@example.com", + "--dry-run", + ) + ) + assert "remove_mailbox_allowlist" in output + assert dry_run_args(output) == { + "email": mail_email(), + "entries": ["partner@example.com"], + } + + +def test_block_list_cli_to_mcp(dws): + output = assert_ok(dws.run_raw("mail", "block-list", "list", "--email", mail_email(), "--dry-run")) + assert "list_mailbox_blocklist" in output + assert dry_run_args(output) == {"email": mail_email()} + + output = assert_ok( + dws.run_raw( + "mail", + "block-list", + "add", + "--email", + mail_email(), + "--entries", + "spam@example.com,@junk.example", + "--dry-run", + ) + ) + assert "add_mailbox_blocklist" in output + assert dry_run_args(output) == { + "email": mail_email(), + "entries": ["spam@example.com", "@junk.example"], + } + + output = assert_ok( + dws.run_raw( + "mail", + "block-list", + "remove", + "--email", + mail_email(), + "--entries", + "spam@example.com", + "--dry-run", + ) + ) + assert "remove_mailbox_blocklist" in output + assert dry_run_args(output) == { + "email": mail_email(), + "entries": ["spam@example.com"], + } diff --git a/auto-test/cli_to_mcp/testcases/sheet/test_36_group_dimension.py b/auto-test/cli_to_mcp/testcases/sheet/test_36_group_dimension.py new file mode 100644 index 00000000..b2737490 --- /dev/null +++ b/auto-test/cli_to_mcp/testcases/sheet/test_36_group_dimension.py @@ -0,0 +1,80 @@ +from test_utils import combined_output, dry_run_args + + +def assert_ok(result): + output = combined_output(result) + assert result.returncode == 0, output + return output + + +def test_group_dimension_cli_to_mcp(dws): + output = assert_ok(dws.run_raw("sheet", "group-dimension", "--help")) + assert "dws sheet group-dimension" in output + assert "--group-state string" in output + + output = assert_ok( + dws.run_raw( + "sheet", + "group-dimension", + "--node", + "node123", + "--sheet-id", + "Sheet1", + "--range", + "3:7", + "--group-state", + "fold", + "--dry-run", + ) + ) + assert "group_dimension" in output + assert dry_run_args(output) == { + "nodeId": "node123", + "sheetId": "Sheet1", + "range": "3:7", + "groupState": "fold", + } + + +def test_ungroup_dimension_cli_to_mcp(dws): + output = assert_ok(dws.run_raw("sheet", "ungroup-dimension", "--help")) + assert "dws sheet ungroup-dimension" in output + + output = assert_ok( + dws.run_raw( + "sheet", + "ungroup-dimension", + "--node", + "node123", + "--sheet-id", + "Sheet1", + "--range", + "C:F", + "--dry-run", + ) + ) + assert "ungroup_dimension" in output + assert dry_run_args(output) == { + "nodeId": "node123", + "sheetId": "Sheet1", + "range": "C:F", + } + + +def test_group_dimension_rejects_invalid_state(dws): + result = dws.run_raw( + "sheet", + "group-dimension", + "--node", + "node123", + "--sheet-id", + "Sheet1", + "--range", + "3:7", + "--group-state", + "invalid", + "--dry-run", + ) + output = combined_output(result) + assert result.returncode != 0 + assert "group-state" in output diff --git a/auto-test/cli_to_mcp/testcases/test_utils/__init__.py b/auto-test/cli_to_mcp/testcases/test_utils/__init__.py new file mode 100644 index 00000000..377beca2 --- /dev/null +++ b/auto-test/cli_to_mcp/testcases/test_utils/__init__.py @@ -0,0 +1,58 @@ +import json +import os +import re +import shlex +import subprocess +from pathlib import Path + + +def repo_root(start_file: str) -> Path: + current = Path(start_file).resolve() + for parent in [current, *current.parents]: + if (parent / "go.mod").exists(): + return parent + raise RuntimeError(f"cannot locate repo root from {start_file}") + + +def resolve_dws_cmd(start_file: str) -> list[str]: + root = repo_root(start_file) + if env_bin := os.environ.get("DWS_BIN"): + return shlex.split(env_bin) + + for rel in ("dws", "build/dws", "bin/dws", "dingtalk-workspace-cli"): + candidate = root / rel + if candidate.exists() and os.access(candidate, os.X_OK): + return [str(candidate)] + + return ["go", "run", "./cmd"] + + +def combined_output(result: subprocess.CompletedProcess) -> str: + return (result.stdout or "") + (result.stderr or "") + + +def dry_run_args(output: str) -> dict: + match = re.search(r"Arguments:\s*(\{.*\})", output, re.S) + assert match, f"dry-run output does not contain Arguments JSON: {output}" + return json.loads(match.group(1)) + + +class DWSRunner: + def __init__(self): + self.root = repo_root(__file__) + self.cmd = resolve_dws_cmd(__file__) + + def run_raw(self, *args: str, timeout: int = 45) -> subprocess.CompletedProcess: + return subprocess.run( + [*self.cmd, *args], + cwd=self.root, + text=True, + capture_output=True, + timeout=timeout, + ) + + def run(self, *args: str, timeout: int = 45): + result = self.run_raw(*args, timeout=timeout) + output = combined_output(result) + assert result.returncode == 0, output + return json.loads(result.stdout) diff --git a/internal/app/skill_setup.go b/internal/app/skill_setup.go index fc9f230a..77cde3d9 100644 --- a/internal/app/skill_setup.go +++ b/internal/app/skill_setup.go @@ -64,7 +64,7 @@ skill 源默认取二进制内嵌的版本(升级二进制即升级 skill) dws skill setup --mode mono --yes # 非交互装 mono dws skill setup --mode multi --target claude # multi 全装到 ~/.claude/skills/ dws skill setup --mode multi -s aitable -s calendar # 只装 aitable + calendar - dws skill setup --mode multi -x live -x devdoc # 装其余 18 个,剔除 2 个 + dws skill setup --mode multi -x live -x devdoc # 装其余 20 个,剔除 2 个 dws skill setup --source /path/to/repo # 显式指定 skill 源`, DisableAutoGenTag: true, RunE: runSkillSetup, @@ -506,7 +506,7 @@ func confirmSkillSetup(out io.Writer, mode, src string, dests []string, multiSki if mode == skillSetupModeMulti { fmt.Fprintln(out, "\n🧪 ─────────────────────────────────────────────────────────────") fmt.Fprintln(out, " multi 模式当前为 EXPERIMENTAL(试验版 / Preview)") - fmt.Fprintln(out, " · 20 个 dingtalk-* 子 skill 跑过 verifier,可用但未达 stable") + fmt.Fprintln(out, " · 22 个 dingtalk-* 子 skill 跑过 verifier,可用但未达 stable") fmt.Fprintln(out, " · 跨 skill 引用、bundle 命名、目录布局后续可能调整") fmt.Fprintln(out, " · 不建议在生产 / 共享环境直接落地;问题请提 issue 反馈") fmt.Fprintln(out, " 稳定版请用 --mode mono") diff --git a/internal/helpers/agoal.go b/internal/helpers/agoal.go new file mode 100644 index 00000000..e3171f8a --- /dev/null +++ b/internal/helpers/agoal.go @@ -0,0 +1,603 @@ +package helpers + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/spf13/cobra" +) + +// ────────────────────────────────────────────────────────── +// dws agoal — Agoal 管理 +// ────────────────────────────────────────────────────────── + +func newAgoalCommand() *cobra.Command { + root := &cobra.Command{ + Use: "agoal", + Short: "Agoal 管理", + Long: `管理钉钉 Agoal:战略解码、经营合约、计分卡、用户目标、周月报。 + +命令结构: + dws agoal strategy list 获取战略解码列表 + dws agoal strategy detail 获取战略解码详情 + dws agoal strategy update 更新战略解码 + dws agoal contract list 获取经营合约列表 + dws agoal contract fields 获取经营合约字段列表 + dws agoal contract detail 获取经营合约详情 + dws agoal contract update 更新经营合约 + dws agoal scorecard detail 获取计分卡详情 + dws agoal scorecard entity-detail 获取计分卡实体详情 + dws agoal scorecard update 更新计分卡 + dws agoal user rules 获取用户规则 + dws agoal user objectives 查询用户目标列表 + dws agoal report list-statistics 获取周月报数据跟催列表 + dws agoal report submit-detail 获取周月报规则提交详情 + dws agoal obj-template list 获取目标模板列表 + dws agoal obj-template create-or-update 新增或更新目标模板`, + RunE: groupRunE, + } + + // ── strategy: 战略解码管理 ────────────────────────────────── + + strategyCmd := &cobra.Command{Use: "strategy", Short: "战略解码管理", RunE: groupRunE} + + strategyListCmd := &cobra.Command{ + Use: "list", + Short: "获取战略解码列表", + Long: `按部门或个人维度查询战略解码列表。 +scopeType 支持: + DEPT — 按部门查询 + PERSONAL — 按个人查询 + +--scope-id 为对应维度的钉钉部门 id 或用户 id。`, + Example: ` dws agoal strategy list --scope-type PERSONAL --scope-id USER_ID + dws agoal strategy list --scope-type DEPT --scope-id DEPT_ID`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "scope-type", "scope-id"); err != nil { + return err + } + toolArgs := map[string]any{ + "scopeType": mustGetFlag(cmd, "scope-type"), + "openId": mustGetFlag(cmd, "scope-id"), + } + if v, _ := cmd.Flags().GetString("request-id"); v != "" { + toolArgs["requestId"] = v + } + return callMCPTool("list_strategy_decodings", toolArgs) + }, + } + strategyListCmd.Flags().String("scope-type", "", "解码范围类型: DEPT/PERSONAL (必填)") + strategyListCmd.Flags().String("scope-id", "", "scope-type 对应的钉钉部门 id 或用户 id (必填)") + strategyListCmd.Flags().String("request-id", "", "requestId (可选)") + + strategyDetailCmd := &cobra.Command{ + Use: "detail", + Short: "获取战略解码详情", + Long: `根据战略解码 id (profileId) 获取战略解码的详细信息。`, + Example: ` dws agoal strategy detail --profile-id PROFILE_ID`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "profile-id"); err != nil { + return err + } + toolArgs := map[string]any{ + "profileId": mustGetFlag(cmd, "profile-id"), + } + if v, _ := cmd.Flags().GetString("request-id"); v != "" { + toolArgs["requestId"] = v + } + return callMCPTool("get_strategy_decoding_detail", toolArgs) + }, + } + strategyDetailCmd.Flags().String("profile-id", "", "战略解码 id (必填)") + strategyDetailCmd.Flags().String("request-id", "", "requestId (可选)") + + strategyUpdateCmd := &cobra.Command{ + Use: "update", + Short: "更新战略解码", + Long: `谨慎操作,基于查询接口返回的老数据进行修改,本接口是覆盖逻辑,会根据战略解码id进行对应的修改。 +--content 为 JSON 数组,每个实体对象包含: + id — 实体 id + title — 标题对象 {"title":"标题文本"} + linkEntityId — 所属的实体 ID(查询接口中此字段有值一定要传进来) + entityType — 类型: OGSM_OBJECTIVE/目的、OGSM_GOAL/目标、OGSM_STRATEGY/策略、OGSM_MEASUREMENT/衡量标准、OGSM_TACTICS/行动方案 + status — 状态: NORMAL/正常、PRE_PUBLISH_THEN_UPDATE/预发布更新、PRE_PUBLISH_THEN_CREATE/预发布新增、PRE_PUBLISH_THEN_DELETE/预发布删除 + supporters — 承接人数组 [{type, dingId, staffId}],type: USER/个人、DEPARTMENT/部门,staffId 类型为 USER 时必填 + indicators — 关键指标 id 字符串数组,如 ["id1","id2"] + linkSources — 资源关联数组 [{id, linkType, linkId, source, objectId, keyResultId}] + linkType: project/项目、task/任务、campaign/战役空间、product/产品空间、doc/文档、standard/其它 + source: teambition + executors — 人员 dingId 字符串数组,如 ["dingId1","dingId2"] + teams — 部门 dingId 字符串数组,如 ["dingId1","dingId2"]`, + Example: ` dws agoal strategy update --profile-id PROFILE_ID --content '[{"id":"entity1","title":{"title":"新目标"},"entityType":"OGSM_OBJECTIVE","status":"NORMAL","executors":["dingId1"],"teams":["deptDingId1"]}]'`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "profile-id", "content"); err != nil { + return err + } + var contentArr []any + if err := json.Unmarshal([]byte(mustGetFlag(cmd, "content")), &contentArr); err != nil { + return fmt.Errorf("--content must be a valid JSON array: %w", err) + } + toolArgs := map[string]any{ + "profileId": mustGetFlag(cmd, "profile-id"), + "content": contentArr, + } + if v, _ := cmd.Flags().GetString("request-id"); v != "" { + toolArgs["requestId"] = v + } + return callMCPTool("update_strategy_decoding", toolArgs) + }, + } + strategyUpdateCmd.Flags().String("profile-id", "", "战略解码 id (必填)") + strategyUpdateCmd.Flags().String("content", "", "实体列表 JSON 数组 (必填)") + strategyUpdateCmd.Flags().String("request-id", "", "requestId (可选)") + + strategyCmd.AddCommand(strategyListCmd, strategyDetailCmd, strategyUpdateCmd) + + // ── contract: 经营合约管理 ────────────────────────────────── + + contractCmd := &cobra.Command{Use: "contract", Short: "经营合约管理", RunE: groupRunE} + + contractListCmd := &cobra.Command{ + Use: "list", + Short: "获取经营合约列表", + Long: `按部门或个人维度查询经营合约列表。 +scopeType 支持: + DEPT — 按部门查询 + PERSONAL — 按个人查询 + +--scope-id 为通讯录里的部门 id 或用户 id。`, + Example: ` dws agoal contract list --scope-type PERSONAL --scope-id USER_ID + dws agoal contract list --scope-type DEPT --scope-id DEPT_ID`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "scope-type", "scope-id"); err != nil { + return err + } + toolArgs := map[string]any{ + "scopeType": mustGetFlag(cmd, "scope-type"), + "openId": mustGetFlag(cmd, "scope-id"), + } + if v, _ := cmd.Flags().GetString("request-id"); v != "" { + toolArgs["requestId"] = v + } + return callMCPTool("list_op_contracts", toolArgs) + }, + } + contractListCmd.Flags().String("scope-type", "", "合约范围类型: DEPT/PERSONAL (必填)") + contractListCmd.Flags().String("scope-id", "", "scope-type 对应的钉钉部门 id 或用户 id (必填)") + contractListCmd.Flags().String("request-id", "", "requestId (可选)") + + contractFieldsCmd := &cobra.Command{ + Use: "fields", + Short: "获取经营合约字段列表", + Long: `获取指定组织下经营合约的字段配置信息。`, + Example: ` dws agoal contract fields`, + RunE: func(cmd *cobra.Command, args []string) error { + toolArgs := map[string]any{} + if v, _ := cmd.Flags().GetString("request-id"); v != "" { + toolArgs["requestId"] = v + } + return callMCPTool("list_op_contract_fields", toolArgs) + }, + } + contractFieldsCmd.Flags().String("request-id", "", "requestId (可选)") + + contractDetailCmd := &cobra.Command{ + Use: "detail", + Short: "获取经营合约详情", + Long: `根据经营合约 id 获取经营合约的详细信息。`, + Example: ` dws agoal contract detail --contract-id CONTRACT_ID`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "contract-id"); err != nil { + return err + } + toolArgs := map[string]any{ + "contractId": mustGetFlag(cmd, "contract-id"), + } + if v, _ := cmd.Flags().GetString("request-id"); v != "" { + toolArgs["requestId"] = v + } + return callMCPTool("get_op_contract_detail", toolArgs) + }, + } + contractDetailCmd.Flags().String("contract-id", "", "经营合约 id (必填)") + contractDetailCmd.Flags().String("request-id", "", "requestId (可选)") + + contractUpdateCmd := &cobra.Command{ + Use: "update", + Short: "更新经营合约", + Long: `谨慎操作,一定要基于查询接口返回的老数据进行修改,然后给本接口使用,因为本接口是覆盖逻辑,会使用传入的数据去覆盖已存在的数据。 +--dimensions 为 JSON 数组,每个维度对象包含: + id — 维度 id + title — 维度名称 + description — 维度描述 + weight — 维度权重 + objectives — 目标列表 + dimensionConfig — 维度配置 + children — 子维度列表 + +可选参数: + --audit-config — 审批配置 JSON,如 {"needAudit":true,"processTemplateId":"TPL_ID"} + --objective-template — 合约模板 JSON,如 {"id":"TPL_ID","title":"模板名称"}`, + Example: ` dws agoal contract update --contract-id CONTRACT_ID \ + --dimensions '[{"id":"dim1","title":"业绩","weight":60,"objectives":[{"id":"obj1"}]}]'`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "contract-id", "dimensions"); err != nil { + return err + } + var dimensionsArr []any + if err := json.Unmarshal([]byte(mustGetFlag(cmd, "dimensions")), &dimensionsArr); err != nil { + return fmt.Errorf("--dimensions must be a valid JSON array: %w", err) + } + toolArgs := map[string]any{ + "contractId": mustGetFlag(cmd, "contract-id"), + "dimensions": dimensionsArr, + } + if v, _ := cmd.Flags().GetString("request-id"); v != "" { + toolArgs["requestId"] = v + } + if v, _ := cmd.Flags().GetString("audit-config"); v != "" { + toolArgs["auditConfig"] = v + } + if v, _ := cmd.Flags().GetString("objective-template"); v != "" { + toolArgs["objectiveTemplate"] = v + } + return callMCPTool("update_op_contract", toolArgs) + }, + } + contractUpdateCmd.Flags().String("contract-id", "", "经营合约 id (必填)") + contractUpdateCmd.Flags().String("request-id", "", "requestId (可选)") + contractUpdateCmd.Flags().String("audit-config", "", "审批配置 JSON (可选)") + contractUpdateCmd.Flags().String("objective-template", "", "合约模板 JSON (可选)") + contractUpdateCmd.Flags().String("dimensions", "", "维度内容列表 JSON 数组 (必填)") + + contractCmd.AddCommand(contractListCmd, contractFieldsCmd, contractDetailCmd, contractUpdateCmd) + + // ── scorecard: 计分卡管理 ─────────────────────────────────── + + scorecardCmd := &cobra.Command{Use: "scorecard", Short: "计分卡管理", RunE: groupRunE} + + scorecardDetailCmd := &cobra.Command{ + Use: "detail", + Short: "获取计分卡详情", + Long: `根据部门 id 和时间获取计分卡详情。 +--selected-time 接受 ISO-8601 字符串,传入对应周期起始时刻: + 2026年 → "2026-01-01T00:00:00+08:00" + 2026年5月 → "2026-05-01T00:00:00+08:00"`, + Example: ` dws agoal scorecard detail --selected-time "2026-01-01T00:00:00+08:00" --dept-id DEPT_ID`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "selected-time", "dept-id"); err != nil { + return err + } + selectedTimeMs, err := parseISO8601ToMillis(mustGetFlag(cmd, "selected-time")) + if err != nil { + return fmt.Errorf("--selected-time: %w", err) + } + toolArgs := map[string]any{ + "selectedTime": selectedTimeMs, + "deptId": mustGetFlag(cmd, "dept-id"), + } + if v, _ := cmd.Flags().GetString("request-id"); v != "" { + toolArgs["requestId"] = v + } + return callMCPTool("get_score_card_detail", toolArgs) + }, + } + scorecardDetailCmd.Flags().String("selected-time", "", "ISO-8601 时间字符串,如 \"2026-01-01T00:00:00+08:00\" (必填)") + scorecardDetailCmd.Flags().String("dept-id", "", "部门 id (必填)") + scorecardDetailCmd.Flags().String("request-id", "", "requestId (可选)") + + scorecardEntityDetailCmd := &cobra.Command{ + Use: "entity-detail", + Short: "获取计分卡实体详情", + Long: `根据计分卡 id 和实体 id 获取计分卡实体的详细信息。`, + Example: ` dws agoal scorecard entity-detail --sc-id SC_ID --entity-id ENTITY_ID`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "sc-id", "entity-id"); err != nil { + return err + } + toolArgs := map[string]any{ + "scId": mustGetFlag(cmd, "sc-id"), + "entityId": mustGetFlag(cmd, "entity-id"), + } + if v, _ := cmd.Flags().GetString("request-id"); v != "" { + toolArgs["requestId"] = v + } + return callMCPTool("get_score_card_entity_detail", toolArgs) + }, + } + scorecardEntityDetailCmd.Flags().String("sc-id", "", "计分卡 id (必填)") + scorecardEntityDetailCmd.Flags().String("entity-id", "", "计分卡实体 id (必填)") + scorecardEntityDetailCmd.Flags().String("request-id", "", "requestId (可选)") + + scorecardUpdateCmd := &cobra.Command{ + Use: "update", + Short: "更新计分卡", + Long: `更新指定计分卡的内容。 +--content 为 JSON 数组,每个维度对象包含: + id — 维度 id + title — 维度名称 + items — 指标或关键事项列表,每项包含: + id — 实体 id + title — 名称 + reference — 参考信息 + start — 起始值 + target — 目标值 + executors — 负责人列表,每项包含 openId`, + Example: ` dws agoal scorecard update --dept-id DEPT_ID --selected-time "2025-01-01T00:00:00+08:00" --id SC_ID --tracking-period-type MONTHLY --content '[{"id":"dim1","title":"业绩","items":[{"id":"item1","title":"收入","target":"100"}]}]'`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "dept-id", "selected-time", "id", "tracking-period-type", "content"); err != nil { + return err + } + selectedTimeMs, err := parseISO8601ToMillis(mustGetFlag(cmd, "selected-time")) + if err != nil { + return fmt.Errorf("--selected-time: %w", err) + } + var contentArr []any + if err := json.Unmarshal([]byte(mustGetFlag(cmd, "content")), &contentArr); err != nil { + return fmt.Errorf("--content must be a valid JSON array: %w", err) + } + toolArgs := map[string]any{ + "deptId": mustGetFlag(cmd, "dept-id"), + "selectedTime": selectedTimeMs, + "id": mustGetFlag(cmd, "id"), + "trackingPeriodType": mustGetFlag(cmd, "tracking-period-type"), + "content": contentArr, + } + if v, _ := cmd.Flags().GetString("request-id"); v != "" { + toolArgs["requestId"] = v + } + return callMCPTool("update_score_card", toolArgs) + }, + } + scorecardUpdateCmd.Flags().String("dept-id", "", "部门 id (必填)") + scorecardUpdateCmd.Flags().String("selected-time", "", "ISO-8601 时间字符串,如 \"2026-01-01T00:00:00+08:00\" (必填)") + scorecardUpdateCmd.Flags().String("id", "", "计分卡 id (必填)") + scorecardUpdateCmd.Flags().String("tracking-period-type", "", "跟踪周期类型: MONTHLY/月度追踪、QUARTERLY/季度追踪 (必填)") + scorecardUpdateCmd.Flags().String("content", "", "内容 JSON 数组 (必填)") + scorecardUpdateCmd.Flags().String("request-id", "", "requestId (可选)") + + scorecardCmd.AddCommand(scorecardDetailCmd, scorecardEntityDetailCmd, scorecardUpdateCmd) + + // ── user: 用户目标管理 ────────────────────────────────────── + + userCmd := &cobra.Command{Use: "user", Short: "用户目标管理", RunE: groupRunE} + + userRulesCmd := &cobra.Command{ + Use: "rules", + Short: "获取用户的规则周期列表", + Long: `获取用户的规则周期列表。不传 dingUserId 时默认取操作人自己的规则。`, + Example: ` dws agoal user rules + dws agoal user rules --user-id USER_ID`, + RunE: func(cmd *cobra.Command, args []string) error { + toolArgs := map[string]any{} + if v, _ := cmd.Flags().GetString("user-id"); v != "" { + toolArgs["dingUserId"] = v + } + if v, _ := cmd.Flags().GetString("request-id"); v != "" { + toolArgs["requestId"] = v + } + return callMCPTool("get_user_rules", toolArgs) + }, + } + userRulesCmd.Flags().String("user-id", "", "要查询的人员钉钉 id (可选,默认取操作人)") + userRulesCmd.Flags().String("request-id", "", "requestId (可选)") + + userObjectivesCmd := &cobra.Command{ + Use: "objectives", + Short: "查询用户目标列表", + Long: `根据用户、规则 id 和周期列表查询用户的目标。`, + Example: ` dws agoal user objectives --user-id USER_ID --rule-id RULE_ID --period-ids "period1,period2"`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "user-id", "rule-id", "period-ids"); err != nil { + return err + } + toolArgs := map[string]any{ + "dingUserId": mustGetFlag(cmd, "user-id"), + "objectiveRuleId": mustGetFlag(cmd, "rule-id"), + "periodIds": parseCSVValues(mustGetFlag(cmd, "period-ids")), + } + if v, _ := cmd.Flags().GetString("request-id"); v != "" { + toolArgs["requestId"] = v + } + return callMCPTool("list_user_objectives", toolArgs) + }, + } + userObjectivesCmd.Flags().String("user-id", "", "要查询的人员钉钉 id (必填)") + userObjectivesCmd.Flags().String("rule-id", "", "规则 id (必填)") + userObjectivesCmd.Flags().String("period-ids", "", "周期 id 列表,逗号分隔 (必填)") + userObjectivesCmd.Flags().String("request-id", "", "requestId (可选)") + + userCmd.AddCommand(userRulesCmd, userObjectivesCmd) + + // ── report: 周月报管理 ────────────────────────────────────── + + reportCmd := &cobra.Command{Use: "report", Short: "周月报管理", RunE: groupRunE} + + reportListStatisticsCmd := &cobra.Command{ + Use: "list-statistics", + Short: "获取周月报数据跟催列表", + Long: `获取周月报规则的人员提交情况列表,包含按时提交、迟交、未提交的人员数量统计。`, + Example: ` dws agoal report list-statistics + dws agoal report list-statistics --keyword "周报规则"`, + RunE: func(cmd *cobra.Command, args []string) error { + toolArgs := map[string]any{} + if v, _ := cmd.Flags().GetString("request-id"); v != "" { + toolArgs["requestId"] = v + } + if v, _ := cmd.Flags().GetString("keyword"); v != "" { + toolArgs["keyword"] = v + } + return callMCPTool("list_report_statistics", toolArgs) + }, + } + reportListStatisticsCmd.Flags().String("request-id", "", "requestId (可选)") + reportListStatisticsCmd.Flags().String("keyword", "", "搜索规则名称 (可选)") + + reportSubmitDetailCmd := &cobra.Command{ + Use: "submit-detail", + Short: "获取周月报规则提交详情", + Long: `获取周月报规则提交详情,包含按时提交、迟交、未提交中的具体人员以及提交的时间、周报id等。 +--submit-state 支持: + ON_TIME — 按时提交 + LATE — 迟交 + NOT_SUBMITTED — 未提交 +--query-date 接受 ISO-8601 字符串,如 "2026-06-18T00:00:00+08:00"`, + Example: ` dws agoal report submit-detail --template-id TPL_ID --submit-state ON_TIME + dws agoal report submit-detail --template-id TPL_ID --submit-state LATE --query-date "2026-06-18T00:00:00+08:00" --page 1 --page-size 20`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "template-id", "submit-state"); err != nil { + return err + } + toolArgs := map[string]any{ + "templateId": mustGetFlag(cmd, "template-id"), + "submitState": mustGetFlag(cmd, "submit-state"), + } + if v, _ := cmd.Flags().GetString("request-id"); v != "" { + toolArgs["requestId"] = v + } + if v, _ := cmd.Flags().GetString("query-date"); v != "" { + queryDateMs, err := parseISO8601ToMillis(v) + if err != nil { + return fmt.Errorf("--query-date: %w", err) + } + toolArgs["queryDate"] = time.UnixMilli(queryDateMs).In(shanghaiLocation()).Format("2006-01-02") + } + if v, _ := cmd.Flags().GetInt("page"); v != 0 { + toolArgs["page"] = v + } + if v, _ := cmd.Flags().GetInt("page-size"); v != 0 { + toolArgs["pageSize"] = v + } + if v, _ := cmd.Flags().GetString("keyword"); v != "" { + toolArgs["keyword"] = v + } + return callMCPTool("get_submit_detail", toolArgs) + }, + } + reportSubmitDetailCmd.Flags().String("template-id", "", "规则模板 id (必填)") + reportSubmitDetailCmd.Flags().String("submit-state", "", "提交状态: ON_TIME(按时提交)/LATE(迟交)/NOT_SUBMITTED(未提交) (必填)") + reportSubmitDetailCmd.Flags().String("request-id", "", "requestId (可选)") + reportSubmitDetailCmd.Flags().String("query-date", "", "查询日期,ISO-8601 格式(如 \"2026-06-18T00:00:00+08:00\"),默认为当天 (可选)") + reportSubmitDetailCmd.Flags().Int("page", 0, "分页参数,默认为 1 (可选)") + reportSubmitDetailCmd.Flags().Int("page-size", 0, "分页参数,默认为 10 (可选)") + reportSubmitDetailCmd.Flags().String("keyword", "", "搜索员工名称 (可选)") + + reportCmd.AddCommand(reportListStatisticsCmd, reportSubmitDetailCmd) + + // ── template: 目标模板管理 ────────────────────────────────── + + objTemplateCmd := &cobra.Command{Use: "obj-template", Short: "目标模板管理", RunE: groupRunE} + + objTemplateListCmd := &cobra.Command{ + Use: "list", + Short: "获取目标模板列表", + Long: `获取目标模板列表,支持关键词搜索和分页。`, + Example: ` dws agoal obj-template list + dws agoal obj-template list --keyword "业绩" --page 1 --page-size 10`, + RunE: func(cmd *cobra.Command, args []string) error { + toolArgs := map[string]any{} + if v, _ := cmd.Flags().GetString("request-id"); v != "" { + toolArgs["requestId"] = v + } + if v, _ := cmd.Flags().GetInt("page"); v != 0 { + toolArgs["page"] = v + } + if v, _ := cmd.Flags().GetInt("page-size"); v != 0 { + toolArgs["pageSize"] = v + } + if v, _ := cmd.Flags().GetString("keyword"); v != "" { + toolArgs["keyword"] = v + } + return callMCPTool("list_obj_template", toolArgs) + }, + } + objTemplateListCmd.Flags().String("request-id", "", "requestId (可选)") + objTemplateListCmd.Flags().Int("page", 0, "页码,默认 1 (可选)") + objTemplateListCmd.Flags().Int("page-size", 0, "每页数量,默认 10 (可选)") + objTemplateListCmd.Flags().String("keyword", "", "搜索关键词 (可选)") + + objTemplateCreateOrUpdateCmd := &cobra.Command{ + Use: "create-or-update", + Short: "新增或更新目标模板", + Long: `新增或更新目标模板。覆盖逻辑,更新时一定要基于查询接口返回的老数据进行修改,新增时建议先参考已存在的模板数据。 +--dimensions 为必填参数,JSON 字符串,包含目标维度、维度配置、目标内容。 +--objective-weight 启用目标权重 +--dimension-weight 启用维度权重 +--compute-by-weight 维度是否参与计算`, + Example: ` dws agoal obj-template create-or-update --title "业绩模板" --dimensions '[{"title":"维度1","weight":100}]' + dws agoal obj-template create-or-update --template-id TPL_ID --dimensions '[...基于老数据修改...]'`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "dimensions"); err != nil { + return err + } + toolArgs := map[string]any{} + if v, _ := cmd.Flags().GetString("request-id"); v != "" { + toolArgs["requestId"] = v + } + if v, _ := cmd.Flags().GetString("template-id"); v != "" { + toolArgs["templateId"] = v + } + if v, _ := cmd.Flags().GetString("title"); v != "" { + toolArgs["title"] = v + } + if cmd.Flags().Changed("objective-weight") { + v, _ := cmd.Flags().GetBool("objective-weight") + toolArgs["objectiveWeight"] = v + } + if cmd.Flags().Changed("dimension-weight") { + v, _ := cmd.Flags().GetBool("dimension-weight") + toolArgs["dimensionWeight"] = v + } + if cmd.Flags().Changed("compute-by-weight") { + v, _ := cmd.Flags().GetBool("compute-by-weight") + toolArgs["computeByWeight"] = v + } + toolArgs["dimensions"] = mustGetFlag(cmd, "dimensions") + return callMCPTool("create_or_update_obj_template", toolArgs) + }, + } + objTemplateCreateOrUpdateCmd.Flags().String("request-id", "", "requestId (可选)") + objTemplateCreateOrUpdateCmd.Flags().String("template-id", "", "模板 id (更新时必填)") + objTemplateCreateOrUpdateCmd.Flags().String("title", "", "模板标题 (新增时必填)") + objTemplateCreateOrUpdateCmd.Flags().Bool("objective-weight", false, "是否启用目标权重") + objTemplateCreateOrUpdateCmd.Flags().Bool("dimension-weight", false, "是否启用维度权重") + objTemplateCreateOrUpdateCmd.Flags().Bool("compute-by-weight", false, "维度是否参与计算") + objTemplateCreateOrUpdateCmd.Flags().String("dimensions", "", "模板关联的维度 JSON 字符串 (必填,更新时基于老数据修改,新增时建议参考已有模板)") + + objTemplateCmd.AddCommand(objTemplateListCmd, objTemplateCreateOrUpdateCmd) + + root.AddCommand(strategyCmd, contractCmd, scorecardCmd, userCmd, reportCmd, objTemplateCmd) + + return root +} + +// parseISO8601ToMillis 将 ISO-8601 时间字符串解析为毫秒时间戳。 +// 支持格式:RFC3339(含时区)、无时区(默认 Asia/Shanghai)、仅日期。 +func parseISO8601ToMillis(value string) (int64, error) { + if t, err := time.Parse(time.RFC3339, value); err == nil { + return t.UnixMilli(), nil + } + + shanghaiLoc := shanghaiLocation() + + if t, err := time.ParseInLocation("2006-01-02T15:04:05", value, shanghaiLoc); err == nil { + return t.UnixMilli(), nil + } + if t, err := time.ParseInLocation("2006-01-02 15:04:05", value, shanghaiLoc); err == nil { + return t.UnixMilli(), nil + } + if t, err := time.ParseInLocation("2006-01-02", value, shanghaiLoc); err == nil { + return t.UnixMilli(), nil + } + + return 0, fmt.Errorf("invalid ISO-8601 time format %q, expected e.g. \"2026-01-01T00:00:00+08:00\"", value) +} + +func shanghaiLocation() *time.Location { + loc, err := time.LoadLocation("Asia/Shanghai") + if err != nil { + return time.FixedZone("CST", 8*3600) + } + return loc +} diff --git a/internal/helpers/chat.go b/internal/helpers/chat.go index 30cfbbfd..b98f08b2 100644 --- a/internal/helpers/chat.go +++ b/internal/helpers/chat.go @@ -4741,12 +4741,277 @@ status 可选值: chatGroupMembersListByIdsCmd.Flags().String("users", "", "成员 openDingTalkId 列表,逗号分隔 (必填)") _ = chatGroupMembersListByIdsCmd.MarkFlagRequired("users") - chatGroupCmd.AddCommand(chatGroupBotsCmd, chatGroupDismissCmd, chatGroupSetHistoryCmd, chatGroupListMyGroupsCmd, chatGroupUpdateNickCmd, chatGroupUpdateAliasCmd, chatGroupListAllCmd, chatGroupListJoinValidationsCmd, chatGroupAuditJoinValidationCmd) + // ── group notice: 群公告管理 ──────────────────────────────── + chatGroupNoticeCmd := &cobra.Command{Use: "notice", Short: "群公告管理", RunE: groupRunE} + + chatGroupNoticeCreateCmd := &cobra.Command{ + Use: "create", + Short: "发布群公告", + Long: `在指定群聊中发布群公告,正文为 Markdown 格式。 +支持标题、加粗、斜体、删除线、行内代码、链接、代码块、有序/无序/任务列表、表格、引用、分割线、图片、段落、换行。 +定时发布:传 --run-at 指定执行时间点,ISO-8601 格式(建议带时区偏移,不带时按北京时区处理)。`, + Example: ` dws chat group notice create --group --content "今晚 22 点系统维护,请提前保存工作内容" + dws chat group notice create --group --content "# 重要通知\n请大家查收" --sticky --send-ding + dws chat group notice create --group --content "明早九点例会" --run-at "2026-07-03T09:00:00+08:00" + # 查询群 ID: dws chat search --query "群名"`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "group", "content"); err != nil { + return err + } + toolArgs := map[string]any{ + "openConversationId": mustGetFlag(cmd, "group"), + "content": mustGetFlag(cmd, "content"), + } + if v, _ := cmd.Flags().GetBool("sticky"); v { + toolArgs["sticky"] = true + } + if v, _ := cmd.Flags().GetBool("send-ding"); v { + toolArgs["sendDing"] = true + } + if v, _ := cmd.Flags().GetString("run-at"); v != "" { + toolArgs["scheduled"] = true + toolArgs["runAtText"] = v + } + return callMCPToolOnServer("im", "create_group_notice", toolArgs) + }, + } + chatGroupNoticeCreateCmd.Flags().String("group", "", "群聊 openConversationId (必填)") + _ = chatGroupNoticeCreateCmd.MarkFlagRequired("group") + chatGroupNoticeCreateCmd.Flags().String("content", "", "公告正文,Markdown 格式 (必填)") + _ = chatGroupNoticeCreateCmd.MarkFlagRequired("content") + chatGroupNoticeCreateCmd.Flags().Bool("sticky", false, "是否吊顶置顶(默认 false)") + chatGroupNoticeCreateCmd.Flags().Bool("send-ding", false, "是否发 DING 提醒(默认 false)") + chatGroupNoticeCreateCmd.Flags().String("run-at", "", "定时发布时间 ISO-8601(如 2026-07-03T09:00:00+08:00,传入则定时发布)") + + chatGroupNoticeEditCmd := &cobra.Command{ + Use: "edit", + Short: "修改群公告", + Long: `修改指定群聊中的群公告,正文为 Markdown 格式,会整体替换原公告内容。`, + Example: ` dws chat group notice edit --group --notice-id --content "更新后的公告内容" + dws chat group notice edit --group --notice-id --content "更新后的公告内容" --sticky --send-ding + # 查询公告 ID: dws chat group notice list --group `, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "group", "notice-id", "content"); err != nil { + return err + } + toolArgs := map[string]any{ + "openConversationId": mustGetFlag(cmd, "group"), + "dataId": mustGetFlag(cmd, "notice-id"), + "content": mustGetFlag(cmd, "content"), + } + if v, _ := cmd.Flags().GetBool("sticky"); v { + toolArgs["sticky"] = true + } + if v, _ := cmd.Flags().GetBool("send-ding"); v { + toolArgs["sendDing"] = true + } + return callMCPToolOnServer("im", "edit_group_notice", toolArgs) + }, + } + chatGroupNoticeEditCmd.Flags().String("group", "", "群聊 openConversationId (必填)") + _ = chatGroupNoticeEditCmd.MarkFlagRequired("group") + chatGroupNoticeEditCmd.Flags().String("notice-id", "", "群公告 dataId (必填)") + _ = chatGroupNoticeEditCmd.MarkFlagRequired("notice-id") + chatGroupNoticeEditCmd.Flags().String("content", "", "公告新正文,Markdown 格式 (必填)") + _ = chatGroupNoticeEditCmd.MarkFlagRequired("content") + chatGroupNoticeEditCmd.Flags().Bool("sticky", false, "是否吊顶置顶(不传按 false 处理)") + chatGroupNoticeEditCmd.Flags().Bool("send-ding", false, "是否发 DING 提醒(默认 false)") + + chatGroupNoticeGetCmd := &cobra.Command{ + Use: "get", + Short: "查看群公告详情", + Long: `查看指定群公告的详情,包含正文摘要、吊顶状态、发布者、已读人数、点赞/评论数等信息。`, + Example: ` dws chat group notice get --group --notice-id + # 查询公告 ID: dws chat group notice list --group `, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "group", "notice-id"); err != nil { + return err + } + return callMCPToolOnServer("im", "get_group_notice", map[string]any{ + "openConversationId": mustGetFlag(cmd, "group"), + "dataId": mustGetFlag(cmd, "notice-id"), + }) + }, + } + chatGroupNoticeGetCmd.Flags().String("group", "", "群聊 openConversationId (必填)") + _ = chatGroupNoticeGetCmd.MarkFlagRequired("group") + chatGroupNoticeGetCmd.Flags().String("notice-id", "", "群公告 dataId (必填)") + _ = chatGroupNoticeGetCmd.MarkFlagRequired("notice-id") + + chatGroupNoticeListCmd := &cobra.Command{ + Use: "list", + Short: "查看群公告列表", + Long: `分页查看指定群聊的群公告列表。默认查询已发布公告,传 --scheduled 查询定时公告列表。 +支持游标分页,hasMore=true 时用返回的 nextPageCursor 作为下次 --cursor。`, + Example: ` dws chat group notice list --group + dws chat group notice list --group --limit 20 --cursor + dws chat group notice list --group --scheduled + # 查询群 ID: dws chat search --query "群名"`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "group"); err != nil { + return err + } + toolArgs := map[string]any{ + "openConversationId": mustGetFlag(cmd, "group"), + } + if v, _ := cmd.Flags().GetInt("limit"); v > 0 { + toolArgs["limit"] = v + } + if v, _ := cmd.Flags().GetString("cursor"); v != "" { + toolArgs["cursor"] = v + } + if v, _ := cmd.Flags().GetBool("scheduled"); v { + toolArgs["scheduled"] = true + } + return callMCPToolOnServer("im", "list_group_notices", toolArgs) + }, + } + chatGroupNoticeListCmd.Flags().String("group", "", "群聊 openConversationId (必填)") + _ = chatGroupNoticeListCmd.MarkFlagRequired("group") + chatGroupNoticeListCmd.Flags().Int("limit", 10, "每页返回数量(默认 10,最大 100)") + chatGroupNoticeListCmd.Flags().String("cursor", "", "分页游标(首次不传,翻页传返回的 nextPageCursor)") + chatGroupNoticeListCmd.Flags().Bool("scheduled", false, "是否查询定时公告列表(默认 false,查询已发布公告)") + chatGroupNoticeCmd.AddCommand(chatGroupNoticeCreateCmd, chatGroupNoticeEditCmd, chatGroupNoticeGetCmd, chatGroupNoticeListCmd) + + chatGroupShareInviteCmd := &cobra.Command{ + Use: "share-invite", + Short: "分享群聊链接到会话", + Long: `将指定群的邀请链接分享到另一个会话或单聊用户。--target 和 --receiver 二选一:--target 指定目标会话,--receiver 指定单聊用户。`, + Example: ` dws chat group share-invite --source <被分享群openConversationId> --target <目标会话openConversationId> + dws chat group share-invite --source <被分享群openConversationId> --receiver <接收者openDingTalkId> + dws chat group share-invite --source --target --expires-seconds 86400 + # 查询群 ID: dws chat search --query "群名"`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "source"); err != nil { + return err + } + target, _ := cmd.Flags().GetString("target") + receiver, _ := cmd.Flags().GetString("receiver") + if target == "" && receiver == "" { + return fmt.Errorf("--target or --receiver is required") + } + if target != "" && receiver != "" { + return fmt.Errorf("--target and --receiver are mutually exclusive") + } + toolArgs := map[string]any{ + "sourceOpenConversationId": mustGetFlag(cmd, "source"), + } + if target != "" { + toolArgs["targetOpenConversationId"] = target + } + if receiver != "" { + toolArgs["receiverOpenDingTalkId"] = receiver + } + if v, _ := cmd.Flags().GetInt64("expires-seconds"); v > 0 || cmd.Flags().Changed("expires-seconds") { + toolArgs["expiresSeconds"] = v + } + if v, _ := cmd.Flags().GetString("uuid"); v != "" { + toolArgs["uuid"] = v + } + return callMCPToolOnServer("im", "share_group_invite_url", toolArgs) + }, + } + chatGroupShareInviteCmd.Flags().String("source", "", "被分享群的 openConversationId (必填)") + _ = chatGroupShareInviteCmd.MarkFlagRequired("source") + chatGroupShareInviteCmd.Flags().String("target", "", "接收分享消息的会话 openConversationId(与 --receiver 二选一)") + chatGroupShareInviteCmd.Flags().String("receiver", "", "接收分享消息的单聊用户 openDingTalkId(与 --target 二选一)") + chatGroupShareInviteCmd.Flags().Int64("expires-seconds", 0, "链接有效期(秒),0 表示永久有效,不传使用服务端默认值") + chatGroupShareInviteCmd.Flags().String("uuid", "", "消息幂等键(可选)") + + chatCategoryCreateSmartCmd := &cobra.Command{ + Use: "create-smart", + Short: "创建智能会话分组", + Long: `创建智能会话分组,可指定群名称关键词和群内成员 openDingTalkId 作为匹配规则。`, + Example: ` dws chat category create-smart --name "工作群" + dws chat category create-smart --name "项目组" --keywords "项目,开发" + dws chat category create-smart --name "团队群" --members openDingTalkId1,openDingTalkId2 + dws chat category create-smart --name "重点群" --keywords "重点" --members openDingTalkId1`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "name"); err != nil { + return err + } + toolArgs := map[string]any{ + "title": mustGetFlag(cmd, "name"), + } + if v, _ := cmd.Flags().GetString("keywords"); v != "" { + toolArgs["keywords"] = parseCSVValues(v) + } + if v, _ := cmd.Flags().GetString("members"); v != "" { + toolArgs["memberOpenDingTalkIds"] = parseCSVValues(v) + } + return callMCPToolOnServer("im", "create_smart_conv_category", toolArgs) + }, + } + chatCategoryCreateSmartCmd.Flags().String("name", "", "分组名称 (必填)") + _ = chatCategoryCreateSmartCmd.MarkFlagRequired("name") + chatCategoryCreateSmartCmd.Flags().String("keywords", "", "群名称关键词列表,逗号分隔(可选)") + chatCategoryCreateSmartCmd.Flags().String("members", "", "群内成员 openDingTalkId 列表,逗号分隔(可选)") + + chatMessageListEmotionRepliesCmd := &cobra.Command{ + Use: "list-emotion-replies", + Short: "批量拉取消息的表情回复和文字回复", + Example: ` dws chat message list-emotion-replies --msg-ids msgId1,msgId2,msgId3 + # 消息 ID 可通过 dws chat message list 获取`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "msg-ids"); err != nil { + return err + } + msgIds := parseCSVValues(mustGetFlag(cmd, "msg-ids")) + return callMCPToolOnServer("im", "list_message_emotion_replies", map[string]any{ + "openMessageIds": msgIds, + }) + }, + } + chatMessageListEmotionRepliesCmd.Flags().String("msg-ids", "", "消息 ID 列表,逗号分隔 (必填)") + _ = chatMessageListEmotionRepliesCmd.MarkFlagRequired("msg-ids") + + supportedTranslateLanguages := map[string]bool{ + "en_US": true, "zh_CN": true, "zh_TW": true, "zh_HK": true, + "ja_JP": true, "ko_KR": true, "vi_VN": true, "th_TH": true, + "id_ID": true, "ms_MY": true, "es_419": true, "fr_FR": true, + "pt_BR": true, "tr_TR": true, "ru_RU": true, "de_DE": true, + "hi_IN": true, "hu_HU": true, "pl_PL": true, "sv_SE": true, + "fi_FI": true, "cs_CZ": true, "ar_SA": true, "tl_PH": true, + "he_IL": true, "nl_NL": true, "lo_LA": true, "it_IT": true, + } + chatTextCmd := &cobra.Command{Use: "text", Short: "文本内容处理", RunE: groupRunE} + chatTextTranslateCmd := &cobra.Command{ + Use: "translate", + Short: "翻译文本内容", + Long: `将指定文本翻译成目标语言。 +支持的目标语言代码: en_US, zh_CN, zh_TW, zh_HK, ja_JP, ko_KR, vi_VN, th_TH, +id_ID, ms_MY, es_419, fr_FR, pt_BR, tr_TR, ru_RU, de_DE, hi_IN, hu_HU, +pl_PL, sv_SE, fi_FI, cs_CZ, ar_SA, tl_PH, he_IL, nl_NL, lo_LA, it_IT`, + Example: ` dws chat text translate --query "你好世界" --to en_US + dws chat text translate --query "Hello World" --to zh_CN + dws chat text translate --query "Bonjour" --to ja_JP`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "query", "to"); err != nil { + return err + } + toLang := mustGetFlag(cmd, "to") + if !supportedTranslateLanguages[toLang] { + return fmt.Errorf("unsupported target language: %s", toLang) + } + return callMCPToolOnServer("im", "translate", map[string]any{ + "query": mustGetFlag(cmd, "query"), + "to": toLang, + }) + }, + } + chatTextTranslateCmd.Flags().String("query", "", "待翻译的文本内容 (必填)") + _ = chatTextTranslateCmd.MarkFlagRequired("query") + chatTextTranslateCmd.Flags().String("to", "en_US", "目标语言代码 (必填,默认 en_US)") + _ = chatTextTranslateCmd.MarkFlagRequired("to") + chatTextCmd.AddCommand(chatTextTranslateCmd) + + chatGroupCmd.AddCommand(chatGroupBotsCmd, chatGroupDismissCmd, chatGroupSetHistoryCmd, chatGroupListMyGroupsCmd, chatGroupUpdateNickCmd, chatGroupUpdateAliasCmd, chatGroupListAllCmd, chatGroupListJoinValidationsCmd, chatGroupAuditJoinValidationCmd, chatGroupNoticeCmd, chatGroupShareInviteCmd) chatGroupMembersCmd.AddCommand(chatGroupMembersRemoveBotCmd, chatGroupMembersListByIdsCmd) chatBotCmd.AddCommand(chatBotFindCmd) - chatMessageCmd.AddCommand(chatMessageListDirectCmd, chatMessageSearchCommonCmd, chatMessageCombineForwardCmd, chatMessageForwardTopicCmd, chatMessageSetPinCmd, chatMessageUnsetPinCmd, chatMessageListPinCmd, chatMessageSetTopMsgCmd, chatMessageUnsetTopMsgCmd) + chatCategoryCmd.AddCommand(chatCategoryCreateSmartCmd) + chatMessageCmd.AddCommand(chatMessageListDirectCmd, chatMessageSearchCommonCmd, chatMessageCombineForwardCmd, chatMessageForwardTopicCmd, chatMessageSetPinCmd, chatMessageUnsetPinCmd, chatMessageListPinCmd, chatMessageSetTopMsgCmd, chatMessageUnsetTopMsgCmd, chatMessageListEmotionRepliesCmd) - root.AddCommand(chatChmodCmd, chatDataAuthCmd, chatGroupCmd, chatSearchCmd, chatSearchCommonCmd, chatMessageCmd, chatFileCmd, newChatMediaGroup(), chatBotCmd, chatMessageListTopConversationsCmd, chatConversationInfoCmd, chatCategoryCmd, chatGroupRoleCmd, chatMuteCmd, chatSetTopCmd, chatGroupMuteCmd, chatGroupMuteMemberCmd, chatHideCmd, chatMuteAtAllCmd, chatMuteRedEnvelopeCmd, chatMarkUnreadCmd, chatClearRedPointCmd, chatClearAllRedPointCmd, chatListAllConversationsCmd, chatClearMessagesCmd, chatMarkReadCmd) + root.AddCommand(chatChmodCmd, chatDataAuthCmd, chatGroupCmd, chatSearchCmd, chatSearchCommonCmd, chatMessageCmd, chatFileCmd, newChatMediaGroup(), chatBotCmd, chatMessageListTopConversationsCmd, chatConversationInfoCmd, chatCategoryCmd, chatGroupRoleCmd, chatMuteCmd, chatSetTopCmd, chatGroupMuteCmd, chatGroupMuteMemberCmd, chatHideCmd, chatMuteAtAllCmd, chatMuteRedEnvelopeCmd, chatMarkUnreadCmd, chatClearRedPointCmd, chatClearAllRedPointCmd, chatListAllConversationsCmd, chatClearMessagesCmd, chatMarkReadCmd, chatTextCmd) // hint: dws chat send → dws chat message send root.AddCommand(hintSubCmd("send", "use: dws chat message send")) diff --git a/internal/helpers/doc.go b/internal/helpers/doc.go index 3f2c8cfc..f2c9a0cc 100644 --- a/internal/helpers/doc.go +++ b/internal/helpers/doc.go @@ -787,7 +787,7 @@ func newDocCommand() *cobra.Command { root := &cobra.Command{ Use: "doc", Short: "钉钉文档管理", - Long: `管理钉钉文档:浏览、读写、块级编辑、导出、模板管理。 + Long: `管理钉钉文档:浏览、读写、块级编辑、导出、导入、模板管理。 命令结构: dws doc info 获取文档元信息 @@ -798,6 +798,8 @@ func newDocCommand() *cobra.Command { dws doc comment [list|create|delete] 文档评论管理 dws doc export 导出在线文档 (支持 docx / markdown / pdf,自动完成提交→轮询→下载) dws doc export get 查询导出任务结果 (手动兜底) + dws doc import 导入本地文件为在线文档 (支持 docx / xlsx / md 等) + dws doc import get 查询导入任务结果 (手动兜底) dws doc template list 获取文档模板列表 dws doc template search 搜索文档模板 dws doc template apply 应用文档模板创建新文档 @@ -2446,6 +2448,233 @@ CLI 内部自动完成全部流程: exportCmd.AddCommand(exportGetCmd) + // ── import: 文件导入为在线文档(一体化:上传→转换→轮询)────────────── + importCmd := &cobra.Command{ + Use: "import", + Short: "导入本地文件为在线文档 (支持 docx / xlsx / md 等)", + Long: `将本地文件导入为钉钉在线文档。 + +支持的文件格式 (按扩展名): + docx, doc → 文字文档 + xlsx, xls → 电子表格 + md, txt → 文字文档 + xmind, mark → 脑图 + +文件大小限制: 20MB + +CLI 内部自动完成全部流程: + 1. 创建导入会话(获取 OSS 上传凭证) + 2. 上传文件到 OSS + 3. 确认导入(触发格式转换) + 4. 渐进式退避轮询等待完成(最多约 5 分钟) + +如果轮询超时仍未完成,会输出 taskId 供后续手动查询: + dws doc import get --task-id `, + Example: ` # 导入 Word 文档 + dws doc import --file ./report.docx + + # 导入到指定文件夹 + dws doc import --file ./notes.md --folder + + # 导入到知识库根目录 + dws doc import --file ./data.xlsx --workspace + + # 自定义导入后的文档名称 + dws doc import --file ./draft.md --name "项目周报"`, + RunE: func(cmd *cobra.Command, args []string) error { + filePath := mustGetFlag(cmd, "file") + if filePath == "" && len(args) > 0 { + filePath = args[0] + } + if filePath == "" { + return fmt.Errorf("flag --file is required (or pass file path as argument)") + } + + fi, err := os.Stat(filePath) + if err != nil { + return fmt.Errorf("cannot read file %s: %w", filePath, err) + } + if fi.IsDir() { + return fmt.Errorf("%s is a directory, not a file", filePath) + } + + const maxFileSize = 20 * 1024 * 1024 + fileSize := fi.Size() + if fileSize > maxFileSize { + return fmt.Errorf("file size %d bytes exceeds 20MB limit", fileSize) + } + if fileSize == 0 { + return fmt.Errorf("file is empty: %s", filePath) + } + + ext := strings.TrimPrefix(strings.ToLower(filepath.Ext(filePath)), ".") + supportedFormats := map[string]bool{ + "docx": true, "doc": true, "xlsx": true, "xls": true, + "md": true, "txt": true, "xmind": true, "mark": true, + } + if !supportedFormats[ext] { + return fmt.Errorf("unsupported file format %q, supported: docx, doc, xlsx, xls, md, txt, xmind, mark", ext) + } + + fileName := filepath.Base(filePath) + name, _ := cmd.Flags().GetString("name") + if name == "" { + name = strings.TrimSuffix(fileName, filepath.Ext(fileName)) + } + folder := flagOrFallback(cmd, "folder", "folder-id") + workspace := flagOrFallback(cmd, "workspace", "workspace-id") + + if deps.Caller.DryRun() { + deps.Out.PrintKeyValue("操作", "导入本地文件为在线文档") + deps.Out.PrintKeyValue("文件", filePath) + deps.Out.PrintKeyValue("名称", name) + deps.Out.PrintKeyValue("格式", ext) + deps.Out.PrintKeyValue("大小", fmt.Sprintf("%d bytes", fileSize)) + return nil + } + + ctx := context.Background() + + deps.Out.PrintInfo("[1/4] 创建导入会话...") + sessionArgs := map[string]any{ + "fileName": name, + "suffix": ext, + "fileSize": fileSize, + } + if folder != "" { + sessionArgs["targetFolderId"] = folder + } + if workspace != "" { + sessionArgs["workspaceId"] = workspace + } + + sessionText, err := callMCPToolReturnText(ctx, "create_import_session", sessionArgs) + if err != nil { + return fmt.Errorf("创建导入会话失败: %w", err) + } + + var sessionResult map[string]any + if err := json.Unmarshal([]byte(sessionText), &sessionResult); err != nil { + return fmt.Errorf("解析导入会话响应失败: %w", err) + } + sessionID, _ := sessionResult["sessionId"].(string) + uploadURL, _ := sessionResult["uploadUrl"].(string) + if sessionID == "" || uploadURL == "" { + deps.Out.PrintRaw(sessionText) + return fmt.Errorf("创建导入会话成功但缺少 sessionId 或 uploadUrl") + } + deps.Out.PrintInfo(fmt.Sprintf(" 会话已创建,sessionId: %s", sessionID)) + + deps.Out.PrintInfo("[2/4] 上传文件...") + if err := httpPutFile(ctx, uploadURL, nil, filePath, fileSize); err != nil { + return fmt.Errorf("文件上传失败 (sessionId=%s): %w", sessionID, err) + } + deps.Out.PrintInfo(" 文件上传完成") + + deps.Out.PrintInfo("[3/4] 确认导入,启动格式转换...") + confirmText, err := callMCPToolReturnText(ctx, "confirm_import", map[string]any{ + "sessionId": sessionID, + }) + if err != nil { + return fmt.Errorf("确认导入失败 (sessionId=%s): %w", sessionID, err) + } + + var confirmResult map[string]any + if err := json.Unmarshal([]byte(confirmText), &confirmResult); err != nil { + return fmt.Errorf("解析确认导入响应失败: %w", err) + } + taskID, _ := confirmResult["taskId"].(string) + if taskID == "" { + deps.Out.PrintRaw(confirmText) + return fmt.Errorf("确认导入成功但未返回 taskId") + } + deps.Out.PrintInfo(fmt.Sprintf(" 转换任务已提交,taskId: %s", taskID)) + + deps.Out.PrintInfo("[4/4] 等待格式转换完成...") + importResult, err := pollDocImportTask(ctx, taskID) + if err != nil { + return err + } + + documentURL, _ := importResult["documentUrl"].(string) + documentName, _ := importResult["documentName"].(string) + documentType, _ := importResult["documentType"].(string) + + deps.Out.PrintInfo(fmt.Sprintf("导入完成: %s", documentURL)) + deps.Out.PrintJSON(map[string]any{ + "success": true, + "taskId": taskID, + "documentUrl": documentURL, + "documentName": documentName, + "documentType": documentType, + }) + return nil + }, + } + importCmd.Flags().String("file", "", "本地文件路径 (必填)") + importCmd.Flags().String("folder", "", "目标文件夹 ID 或 URL (可选,与 --workspace 至少传一个)") + importCmd.Flags().String("workspace", "", "目标知识库 ID 或 URL (可选,与 --folder 至少传一个)") + importCmd.Flags().StringP("name", "n", "", "导入后文档名称 (可选,默认取文件名)") + importCmd.Flags().String("folder-id", "", "") + _ = importCmd.Flags().MarkHidden("folder-id") + importCmd.Flags().String("workspace-id", "", "") + _ = importCmd.Flags().MarkHidden("workspace-id") + + importGetCmd := &cobra.Command{ + Use: "get", + Short: "查询导入任务结果(手动兜底)", + Long: `根据 taskId 查询文档导入任务的执行结果。 +通常不需要手动调用,dws doc import 会自动完成轮询。 +仅在导入命令超时或中断后,用于手动查询任务状态。 + +任务状态: + processing 转换中 + completed 导入成功,返回 documentUrl + failed 导入失败`, + Example: ` dws doc import get --task-id `, + RunE: func(cmd *cobra.Command, _ []string) error { + taskID := mustGetFlag(cmd, "task-id") + if taskID == "" { + return fmt.Errorf("flag --task-id is required") + } + + if deps.Caller.DryRun() { + deps.Out.PrintKeyValue("操作", "查询导入任务结果") + deps.Out.PrintKeyValue("任务ID", taskID) + return nil + } + + ctx := context.Background() + text, err := callMCPToolReturnText(ctx, "query_import_task", map[string]any{"taskId": taskID}) + if err != nil { + return err + } + + var result map[string]any + if err := json.Unmarshal([]byte(text), &result); err != nil { + deps.Out.PrintRaw(text) + return nil + } + + status, _ := result["status"].(string) + message, _ := result["message"].(string) + + switch strings.ToLower(status) { + case "completed", "processing": + deps.Out.PrintJSON(result) + return nil + default: + deps.Out.PrintJSON(result) + if message != "" { + return fmt.Errorf("导入任务失败 (status=%s): %s", status, message) + } + return fmt.Errorf("导入任务失败 (status=%s)", status) + } + }, + } + importGetCmd.Flags().String("task-id", "", "导入任务 ID (必填)") + importCmd.AddCommand(importGetCmd) + // ── doc version 子命令组 ── versionCmd := &cobra.Command{ Use: "version", @@ -2658,7 +2887,7 @@ CLI 内部自动完成全部流程: // will skip flags that already exist. for _, cmd := range []*cobra.Command{ searchCmd, listCmd, createCmd, updateCmd, uploadCmd, downloadCmd, - copyCmd, moveCmd, renameCmd, deleteCmd, exportCmd, + copyCmd, moveCmd, renameCmd, deleteCmd, exportCmd, importCmd, } { RegisterCrossProductAliases(cmd) } @@ -2700,7 +2929,7 @@ CLI 内部自动完成全部流程: folderCmd.Hidden = true permissionCmd.Hidden = true - root.AddCommand(searchCmd, listCmd, infoCmd, readCmd, createCmd, updateCmd, uploadCmd, downloadCmd, copyCmd, moveCmd, renameCmd, deleteCmd, fileCmd, folderCmd, blockCmd, commentCmd, mediaCmd, permissionCmd, exportCmd, versionCmd, templateCmd) + root.AddCommand(searchCmd, listCmd, infoCmd, readCmd, createCmd, updateCmd, uploadCmd, downloadCmd, copyCmd, moveCmd, renameCmd, deleteCmd, fileCmd, folderCmd, blockCmd, commentCmd, mediaCmd, permissionCmd, exportCmd, importCmd, versionCmd, templateCmd) return root } @@ -2947,6 +3176,63 @@ func pollDocExportJob(ctx context.Context, jobID string) (downloadURL string, er return "", fmt.Errorf("导出任务超时:已轮询 %d 次仍在处理中 (jobId=%s),请稍后使用 dws doc export get --job-id %s 手动查询", maxPolls, jobID, jobID) } +// pollDocImportTask polls the import task status with progressive backoff. +func pollDocImportTask(ctx context.Context, taskID string) (map[string]any, error) { + const maxPolls = 30 + + pollInterval := func(attempt int) time.Duration { + switch { + case attempt <= 5: + return 2 * time.Second + case attempt <= 10: + return 5 * time.Second + case attempt <= 20: + return 10 * time.Second + default: + return 15 * time.Second + } + } + + for attempt := 1; attempt <= maxPolls; attempt++ { + interval := pollInterval(attempt) + deps.Out.PrintInfo(fmt.Sprintf(" 第 %d/%d 次查询,等待 %v ...", attempt, maxPolls, interval)) + + select { + case <-ctx.Done(): + return nil, fmt.Errorf("导入轮询被取消 (taskId=%s): %w", taskID, ctx.Err()) + case <-time.After(interval): + } + + text, queryErr := callMCPToolReturnText(ctx, "query_import_task", map[string]any{"taskId": taskID}) + if queryErr != nil { + return nil, fmt.Errorf("查询导入任务失败 (taskId=%s): %w", taskID, queryErr) + } + + var result map[string]any + if parseErr := json.Unmarshal([]byte(text), &result); parseErr != nil { + return nil, fmt.Errorf("解析查询结果失败 (taskId=%s): %w", taskID, parseErr) + } + + status, _ := result["status"].(string) + switch strings.ToLower(status) { + case "completed": + return result, nil + case "processing": + continue + case "failed": + message, _ := result["message"].(string) + if message != "" { + return nil, fmt.Errorf("导入任务失败 (taskId=%s): %s", taskID, message) + } + return nil, fmt.Errorf("导入任务失败 (taskId=%s)", taskID) + default: + continue + } + } + + return nil, fmt.Errorf("导入任务超时:已轮询 %d 次仍在处理中 (taskId=%s),请稍后使用 dws doc import get --task-id %s 手动查询", maxPolls, taskID, taskID) +} + // stripDuplicateTitle removes the leading H1 heading from markdown content // when it matches the document name (set via --name). This prevents the title // from appearing twice: once as document metadata and once in the body. diff --git a/internal/helpers/mail.go b/internal/helpers/mail.go index f826d0f9..736f2121 100644 --- a/internal/helpers/mail.go +++ b/internal/helpers/mail.go @@ -114,7 +114,38 @@ func newMailCommand() *cobra.Command { }, } - mailboxCmd.AddCommand(mailboxListCmd) + mailboxProfileCmd := &cobra.Command{ + Use: "profile", + Short: "获取用户邮箱信息", + Long: `根据邮箱地址获取用户的邮箱详细信息,包含容量、别名等。 + +返回字段: + email 邮箱地址 + emailAliases 邮件地址别名列表 + name 用户名 + nickname 用户昵称 + displayName 用户显示名 + mboxSize 邮箱容量(字节) + mboxSizeUsed 已使用的邮箱容量(字节) + createdTime 创建时间 + modifiedTime 修改时间 + +错误说明: + domain.notFound 该用户的邮箱不是由钉钉邮箱托管,无法完成操作`, + Example: ` dws mail mailbox profile --email user@company.com`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "email"); err != nil { + return err + } + return callMCPTool("get_mailbox_profile", map[string]any{ + "email": mustGetFlag(cmd, "email"), + }) + }, + } + + mailboxProfileCmd.Flags().String("email", "", "用户的邮箱地址 (必填)") + + mailboxCmd.AddCommand(mailboxListCmd, mailboxProfileCmd) messageCmd := &cobra.Command{Use: "message", Short: "邮件管理", RunE: groupRunE} @@ -1138,6 +1169,64 @@ internetMessageId 来源:message send / draft send / message reply / message r }, } + messageBatchGetCmd := &cobra.Command{ + Use: "batch-get", + Short: "批量获取邮件详情", + Long: `根据邮件 ID 列表批量获取邮件完整内容,包含正文。 + +限制说明: + - 单次最多 20 个邮件 ID + - CLI 会逐个调用 get_email_by_message_id 并返回聚合结果 + +错误说明: + domain.notFound 该用户的邮箱不是由钉钉邮箱托管,无法完成操作`, + Example: ` dws mail message batch-get --email user@company.com --ids , + dws mail message batch-get --email user@company.com --ids ,,`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "email", "ids"); err != nil { + return err + } + email := mustGetFlag(cmd, "email") + ids := parseRecipients(mustGetFlag(cmd, "ids")) + if len(ids) == 0 { + return fmt.Errorf("--ids 至少需要 1 个邮件 ID") + } + if len(ids) > 20 { + return fmt.Errorf("--ids 单次最多支持 20 个邮件 ID,当前: %d", len(ids)) + } + if deps.Caller.DryRun() { + fmt.Printf("[DRY-RUN] Preview only, not executed:\n") + fmt.Printf("Operation: batch-get (loop get_email_by_message_id x%d)\n", len(ids)) + fmt.Printf("Arguments:\n") + fmt.Printf(" email: %s\n", email) + fmt.Printf(" ids: %v\n", ids) + return nil + } + items := make([]map[string]any, 0, len(ids)) + for _, id := range ids { + text, err := callMCPToolReturnText(context.Background(), "get_email_by_message_id", map[string]any{ + "email": email, + "messageId": id, + }) + if err != nil { + return fmt.Errorf("获取邮件详情失败 id=%s: %w", id, err) + } + var payload any + if err := json.Unmarshal([]byte(text), &payload); err != nil { + payload = text + } + items = append(items, map[string]any{ + "id": id, + "result": payload, + }) + } + return deps.Out.PrintJSON(map[string]any{ + "email": email, + "messages": items, + }) + }, + } + draftCreateCmd := &cobra.Command{ Use: "create", Short: "创建草稿", @@ -1407,12 +1496,88 @@ internetMessageId 来源:message send / draft send / message reply / message r messageBatchModifyCmd.Flags().String("action", "", "操作类型: markRead/markUnread/addTags/removeTags (必填)") messageBatchModifyCmd.Flags().String("tags", "", "标签 ID 列表,逗号分隔 (action 为 addTags/removeTags 时必填)") + messageBatchGetCmd.Flags().String("email", "", "邮件所属邮箱地址 (必填)") + messageBatchGetCmd.Flags().String("ids", "", "要获取的邮件 ID 列表,逗号分隔,最多 20 个 (必填)") + messageVerifyCmd.Flags().String("email", "", "邮件所属邮箱地址 (必填)") messageVerifyCmd.Flags().String("internet-message-id", "", "邮件的 internetMessageId (必填),取自发送类命令返回值") messageCmd.AddCommand(messageListCmd, messageSearchCmd, messageGetCmd, messageSendCmd, messageReplyCmd, messageReplyAllCmd, messageForwardCmd, - messageBatchMoveCmd, messageBatchDeleteCmd, messageBatchModifyCmd, messageVerifyCmd) + messageBatchMoveCmd, messageBatchDeleteCmd, messageBatchModifyCmd, messageBatchGetCmd, messageVerifyCmd) + + sentMessageCmd := &cobra.Command{Use: "sent-message", Short: "已发送邮件管理", RunE: groupRunE} + + sentMessageRecallCmd := &cobra.Command{ + Use: "recall", + Short: "[危险] 撤回已发送的邮件", + Long: `撤回已发送的邮件。仅支持撤回同组织内未读邮件。 + +返回字段: + id 撤回任务 ID(可用于 recall-detail 查询进度) + success 接口调用是否成功 + errorCode 错误码(仅失败时存在) + errorMsg 错误信息(仅失败时存在) + +错误说明: + domain.notFound 该用户的邮箱不是由钉钉邮箱托管,无法完成操作`, + Example: ` dws mail sent-message recall --email user@company.com --id --subject "邮件主题" --yes`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "email", "id", "subject"); err != nil { + return err + } + yes, _ := cmd.Flags().GetBool("yes") + if !yes && !deps.Caller.DryRun() { + return fmt.Errorf("此操作为危险操作,需要传入 --yes 确认执行") + } + return callMCPTool("recall_sent_message", map[string]any{ + "email": mustGetFlag(cmd, "email"), + "id": mustGetFlag(cmd, "id"), + "subject": mustGetFlag(cmd, "subject"), + }) + }, + } + + sentMessageRecallDetailCmd := &cobra.Command{ + Use: "recall-detail", + Short: "查询邮件撤回进度", + Long: `根据撤回任务 ID 查询邮件撤回的详细进度。 + +撤回任务 ID 来源:sent-message recall 命令返回值中的 id 字段。 + +返回字段: + id 撤回任务 ID + status 任务状态: UNINITED/SUBMITTED/RUNNING/FINISHED/CANCELED/FAILED + createdTime 创建时间 + updatedTime 更新时间 + totalCount 总邮件数 + succeededCount 成功撤回数 + failedCount 撤回失败数 + details 每封邮件的撤回结果 + +错误说明: + domain.notFound 该用户的邮箱不是由钉钉邮箱托管,无法完成操作`, + Example: ` dws mail sent-message recall-detail --email user@company.com --id `, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "email", "id"); err != nil { + return err + } + return callMCPTool("get_recall_detail", map[string]any{ + "email": mustGetFlag(cmd, "email"), + "id": mustGetFlag(cmd, "id"), + }) + }, + } + + sentMessageRecallCmd.Flags().String("email", "", "发件人邮箱地址 (必填)") + sentMessageRecallCmd.Flags().String("id", "", "要撤回的邮件 ID (必填)") + sentMessageRecallCmd.Flags().String("subject", "", "邮件主题 (必填)") + sentMessageRecallCmd.Flags().Bool("yes", false, "跳过确认提示,直接执行") + + sentMessageRecallDetailCmd.Flags().String("email", "", "用户的邮箱地址 (必填)") + sentMessageRecallDetailCmd.Flags().String("id", "", "撤回任务 ID (必填),由 recall 命令返回") + + sentMessageCmd.AddCommand(sentMessageRecallCmd, sentMessageRecallDetailCmd) draftCreateCmd.Flags().String("from", "", "发件人邮箱 (必填)") draftCreateCmd.Flags().String("sender", "", "--from 的别名") @@ -1913,52 +2078,55 @@ user 对象字段: }, } - // TODO: auto-reply update 暂时注释,待服务端修复后恢复 - // autoReplyUpdateCmd := &cobra.Command{ - // Use: "update", - // Short: "更新/设置用户的自动回复配置", - // Long: `更新或设置用户的邮件自动回复配置。所有参数均为必填。 - // - // 建议工作流:先通过 auto-reply get 获取当前配置,再传入需要修改的字段值。 - // - // 时间格式示例:2026/06/25 16:30:00 +0800 - // - // 参数说明(全部必填): - // --enabled 是否启用自动回复 (true/false) - // --startTime 自动回复开始时间 - // --endTime 自动回复结束时间 - // --scope 回复范围: "contact"(仅联系人) 或 "all"(所有人) - // --content 自动回复内容 - // - // 错误说明: - // domain.notFound 该用户的邮箱不是由钉钉邮箱托管,无法完成操作`, - // Example: ` dws mail auto-reply update --email user@company.com --enabled true --startTime "2026/07/01 09:00:00 +0800" --endTime "2026/07/07 18:00:00 +0800" --scope all --content "出差中,请稍后联系" - // dws mail auto-reply update --email user@company.com --enabled false --startTime "" --endTime "" --scope all --content ""`, - // RunE: func(cmd *cobra.Command, args []string) error { - // if err := validateRequiredFlags(cmd, "email", "enabled", "startTime", "endTime", "scope", "content"); err != nil { - // return err - // } - // toolArgs := map[string]any{ - // "email": mustGetFlag(cmd, "email"), - // "enabled": mustGetFlag(cmd, "enabled") == "true", - // "startTime": mustGetFlag(cmd, "startTime"), - // "endTime": mustGetFlag(cmd, "endTime"), - // "scope": mustGetFlag(cmd, "scope"), - // "content": mustGetFlag(cmd, "content"), - // } - // return callMCPTool("update_auto_reply", toolArgs) - // }, - // } + autoReplyUpdateCmd := &cobra.Command{ + Use: "update", + Short: "更新/设置用户的自动回复配置", + Long: `更新或设置用户的邮件自动回复配置。所有参数均为必填。 + +建议工作流:先通过 auto-reply get 获取当前配置,再传入需要修改的字段值。 + +时间格式示例:2026/06/25 16:00:00 +0800 + +参数说明(全部必填): + --enabled 是否启用自动回复 (true/false) + --start 自动回复开始时间 + --end 自动回复结束时间 + --scope 回复范围: "contact"(仅联系人) 或 "all"(所有人) + --content 自动回复内容 + +错误说明: + domain.notFound 该用户的邮箱不是由钉钉邮箱托管,无法完成操作`, + Example: ` dws mail auto-reply update --email user@company.com --enabled true --start "2026/07/01 09:00:00 +0800" --end "2026/07/07 18:00:00 +0800" --scope all --content "出差中,请稍后联系" + dws mail auto-reply update --email user@company.com --enabled false --start "2026/07/01 09:00:00 +0800" --end "2026/07/07 18:00:00 +0800" --scope all --content "已关闭自动回复"`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "email", "enabled", "start", "end", "scope", "content"); err != nil { + return err + } + enabledRaw := strings.ToLower(strings.TrimSpace(mustGetFlag(cmd, "enabled"))) + if enabledRaw != "true" && enabledRaw != "false" { + return fmt.Errorf("--enabled 必须为 true 或 false") + } + toolArgs := map[string]any{ + "email": mustGetFlag(cmd, "email"), + "enabled": enabledRaw == "true", + "startTime": mustGetFlag(cmd, "start"), + "endTime": mustGetFlag(cmd, "end"), + "scope": mustGetFlag(cmd, "scope"), + "content": mustGetFlag(cmd, "content"), + } + return callMCPTool("update_auto_reply", toolArgs) + }, + } autoReplyGetCmd.Flags().String("email", "", "用户的邮箱地址 (必填)") - // autoReplyUpdateCmd.Flags().String("email", "", "用户的邮箱地址 (必填)") - // autoReplyUpdateCmd.Flags().String("enabled", "", "是否启用自动回复: true/false (必填)") - // autoReplyUpdateCmd.Flags().String("startTime", "", "自动回复开始时间 (必填),格式: 2026/06/25 16:30:00 +0800") - // autoReplyUpdateCmd.Flags().String("endTime", "", "自动回复结束时间 (必填),格式: 2026/06/25 16:30:00 +0800") - // autoReplyUpdateCmd.Flags().String("scope", "", "回复范围: contact(仅联系人)/all(所有人) (必填)") - // autoReplyUpdateCmd.Flags().String("content", "", "自动回复内容 (必填)") + autoReplyUpdateCmd.Flags().String("email", "", "用户的邮箱地址 (必填)") + autoReplyUpdateCmd.Flags().String("enabled", "", "是否启用自动回复: true/false (必填)") + autoReplyUpdateCmd.Flags().String("start", "", "自动回复开始时间,格式: YYYY/MM/DD HH:MM:SS +ZZZZ (必填)") + autoReplyUpdateCmd.Flags().String("end", "", "自动回复结束时间,格式: YYYY/MM/DD HH:MM:SS +ZZZZ (必填)") + autoReplyUpdateCmd.Flags().String("scope", "", "回复范围: contact(仅联系人)/all(所有人) (必填)") + autoReplyUpdateCmd.Flags().String("content", "", "自动回复内容 (必填)") - autoReplyCmd.AddCommand(autoReplyGetCmd) // , autoReplyUpdateCmd) + autoReplyCmd.AddCommand(autoReplyGetCmd, autoReplyUpdateCmd) // ── rule 收信规则 ──────────────────────────────────── ruleCmd := &cobra.Command{Use: "rule", Short: "收信规则管理", RunE: groupRunE} @@ -2145,7 +2313,143 @@ object 与 operation 合法组合: ruleCmd.AddCommand(ruleListCmd, ruleCreateCmd, ruleUpdateCmd, ruleDeleteCmd, ruleAdjustCmd) - root.AddCommand(mailboxCmd, messageCmd, draftCmd, threadCmd, folderCmd, tagCmd, userCmd, attachmentCmd, templateCmd, contactCmd, autoReplyCmd, ruleCmd) + // ── allow-list 个人收信白名单 ──────────────────────────────── + allowListCmd := &cobra.Command{Use: "allow-list", Short: "个人收信白名单管理", RunE: groupRunE} + + allowListListCmd := &cobra.Command{ + Use: "list", + Short: "列出个人收信白名单", + Long: `列出当前用户的个人收信白名单地址列表。 + +返回字段: + entries 白名单地址列表 + success 接口调用是否成功 + errorCode 错误码(仅失败时存在) + errorMsg 错误信息(仅失败时存在)`, + Example: ` dws mail allow-list list --email user@company.com`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "email"); err != nil { + return err + } + return callMCPTool("list_mailbox_allowlist", map[string]any{ + "email": mustGetFlag(cmd, "email"), + }) + }, + } + allowListAddCmd := &cobra.Command{ + Use: "add", + Short: "添加个人收信白名单", + Long: `向个人收信白名单中添加邮件地址或域名。 + +条目格式: + - 邮件地址:123@domain.com + - 域名:@domain.com(域名前需加 @ 符号)`, + Example: ` dws mail allow-list add --email user@company.com --entries a@b.com,@spam.com`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "email", "entries"); err != nil { + return err + } + return callMCPTool("add_mailbox_allowlist", map[string]any{ + "email": mustGetFlag(cmd, "email"), + "entries": parseRecipients(mustGetFlag(cmd, "entries")), + }) + }, + } + allowListRemoveCmd := &cobra.Command{ + Use: "remove", + Short: "移除个人收信白名单", + Long: `从个人收信白名单中移除邮件地址或域名。 + +条目格式: + - 邮件地址:123@domain.com + - 域名:@domain.com(域名前需加 @ 符号)`, + Example: ` dws mail allow-list remove --email user@company.com --entries a@b.com,@spam.com`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "email", "entries"); err != nil { + return err + } + return callMCPTool("remove_mailbox_allowlist", map[string]any{ + "email": mustGetFlag(cmd, "email"), + "entries": parseRecipients(mustGetFlag(cmd, "entries")), + }) + }, + } + allowListListCmd.Flags().String("email", "", "用户的邮箱地址 (必填)") + allowListAddCmd.Flags().String("email", "", "用户的邮箱地址 (必填)") + allowListAddCmd.Flags().String("entries", "", "逗号分隔的地址列表,支持邮件地址(如123@domain.com)或域名(如@domain.com)") + allowListRemoveCmd.Flags().String("email", "", "用户的邮箱地址 (必填)") + allowListRemoveCmd.Flags().String("entries", "", "逗号分隔的地址列表,支持邮件地址(如123@domain.com)或域名(如@domain.com)") + allowListCmd.AddCommand(allowListListCmd, allowListAddCmd, allowListRemoveCmd) + + // ── block-list 个人收信黑名单 ──────────────────────────────── + blockListCmd := &cobra.Command{Use: "block-list", Short: "个人收信黑名单管理", RunE: groupRunE} + + blockListListCmd := &cobra.Command{ + Use: "list", + Short: "列出个人收信黑名单", + Long: `列出当前用户的个人收信黑名单地址列表。 + +返回字段: + entries 黑名单地址列表 + success 接口调用是否成功 + errorCode 错误码(仅失败时存在) + errorMsg 错误信息(仅失败时存在)`, + Example: ` dws mail block-list list --email user@company.com`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "email"); err != nil { + return err + } + return callMCPTool("list_mailbox_blocklist", map[string]any{ + "email": mustGetFlag(cmd, "email"), + }) + }, + } + blockListAddCmd := &cobra.Command{ + Use: "add", + Short: "添加个人收信黑名单", + Long: `向个人收信黑名单中添加邮件地址或域名。 + +条目格式: + - 邮件地址:123@domain.com + - 域名:@domain.com(域名前需加 @ 符号)`, + Example: ` dws mail block-list add --email user@company.com --entries spam@bad.com,@junk.com`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "email", "entries"); err != nil { + return err + } + return callMCPTool("add_mailbox_blocklist", map[string]any{ + "email": mustGetFlag(cmd, "email"), + "entries": parseRecipients(mustGetFlag(cmd, "entries")), + }) + }, + } + blockListRemoveCmd := &cobra.Command{ + Use: "remove", + Short: "移除个人收信黑名单", + Long: `从个人收信黑名单中移除邮件地址或域名。 + +条目格式: + - 邮件地址:123@domain.com + - 域名:@domain.com(域名前需加 @ 符号)`, + Example: ` dws mail block-list remove --email user@company.com --entries spam@bad.com,@junk.com`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "email", "entries"); err != nil { + return err + } + return callMCPTool("remove_mailbox_blocklist", map[string]any{ + "email": mustGetFlag(cmd, "email"), + "entries": parseRecipients(mustGetFlag(cmd, "entries")), + }) + }, + } + blockListListCmd.Flags().String("email", "", "用户的邮箱地址 (必填)") + blockListAddCmd.Flags().String("email", "", "用户的邮箱地址 (必填)") + blockListAddCmd.Flags().String("entries", "", "逗号分隔的地址列表,支持邮件地址(如123@domain.com)或域名(如@domain.com)") + blockListRemoveCmd.Flags().String("email", "", "用户的邮箱地址 (必填)") + blockListRemoveCmd.Flags().String("entries", "", "逗号分隔的地址列表,支持邮件地址(如123@domain.com)或域名(如@domain.com)") + blockListCmd.AddCommand(blockListListCmd, blockListAddCmd, blockListRemoveCmd) + + root.AddCommand(mailboxCmd, messageCmd, sentMessageCmd, draftCmd, threadCmd, folderCmd, tagCmd, userCmd, attachmentCmd, templateCmd, contactCmd, autoReplyCmd, ruleCmd, allowListCmd, blockListCmd) return root } diff --git a/internal/helpers/register_products.go b/internal/helpers/register_products.go index 2cf0cd13..bacd9c48 100644 --- a/internal/helpers/register_products.go +++ b/internal/helpers/register_products.go @@ -23,6 +23,7 @@ func init() { name string fn func() *cobra.Command }{ + {"agoal", newAgoalCommand}, {"aisearch", newAisearchCommand}, {"aitable", newAitableCommand}, {"attendance", newAttendanceCommand}, diff --git a/internal/helpers/sheet.go b/internal/helpers/sheet.go index a33faa25..eceb5615 100644 --- a/internal/helpers/sheet.go +++ b/internal/helpers/sheet.go @@ -48,6 +48,8 @@ func newSheetCommand() *cobra.Command { dws sheet insert-dimension 在指定位置插入行或列 dws sheet delete-dimension 删除指定位置的行或列 dws sheet update-dimension 更新指定范围行/列属性(显隐、行高/列宽) + dws sheet group-dimension 对指定连续行/列创建分组 + dws sheet ungroup-dimension 取消指定连续行/列分组 dws sheet media-upload 上传附件到表格 dws sheet write-image 上传图片并写入表格单元格 dws sheet replace 全局查找替换文本 diff --git a/internal/helpers/sheet_batch.go b/internal/helpers/sheet_batch.go index b75b5a2a..674a2b48 100644 --- a/internal/helpers/sheet_batch.go +++ b/internal/helpers/sheet_batch.go @@ -39,6 +39,8 @@ var batchOpDispatch = map[string]batchOpMapping{ "csv-put": {"set_range_from_csv", BuildCsvPutArgs}, "delete-float-image": {"delete_float_image", BuildDeleteFloatImageArgs}, "update-dimension": {"update_dimension", BuildUpdateDimensionArgs}, + "group-dimension": {"group_dimension", BuildGroupDimensionArgs}, + "ungroup-dimension": {"ungroup_dimension", BuildUngroupDimensionArgs}, } // translateBatchOp translates a batch operation from CLI format to MCP format. @@ -269,6 +271,27 @@ func BuildUpdateDimensionArgs(input map[string]any) map[string]any { return args } +// BuildGroupDimensionArgs converts CLI flags to MCP params for group_dimension. +func BuildGroupDimensionArgs(input map[string]any) map[string]any { + groupState := batchStr(input, "group-state", "groupState") + if groupState == "" { + groupState = "expand" + } + return map[string]any{ + "sheetId": batchStr(input, "sheet-id"), + "range": batchStr(input, "range"), + "groupState": groupState, + } +} + +// BuildUngroupDimensionArgs converts CLI flags to MCP params for ungroup_dimension. +func BuildUngroupDimensionArgs(input map[string]any) map[string]any { + return map[string]any{ + "sheetId": batchStr(input, "sheet-id"), + "range": batchStr(input, "range"), + } +} + // resolveCsvContent resolves @filepath and - stdin to CSV text, matching standalone csv-put behavior. func resolveCsvContent(csvVal string) string { switch { @@ -307,8 +330,12 @@ CLI 层自动翻译为 MCP toolName + 参数名,无需记忆 MCP 参数名。 支持的 CLI 命令名: range clear / range update / merge-cells / unmerge-cells / update-dimension range fill / range copy-to / add-dimension / delete-dimension / move-dimension + group-dimension / ungroup-dimension set-dropdown / delete-dropdown / csv-put / delete-float-image +注意:batch-update 中 group-dimension 适合默认展开分组;需要 --group-state fold 时请使用独立 +dws sheet group-dimension 命令。 + --operations 是 JSON 数组,每项包含: toolName CLI 命令名(如 "range clear", "range update") input 该命令的入参(不含 --node),键用 flag 名去掉 -- diff --git a/internal/helpers/sheet_dimension.go b/internal/helpers/sheet_dimension.go index 0348e8db..69b41125 100644 --- a/internal/helpers/sheet_dimension.go +++ b/internal/helpers/sheet_dimension.go @@ -375,6 +375,83 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 updateDimensionCmd.Flags().Bool("hidden", false, "是否隐藏 (true=隐藏, false=显示)") updateDimensionCmd.Flags().Int("pixel-size", 0, "行高或列宽(像素),ROWS 时为行高,COLUMNS 时为列宽") + groupDimensionCmd := &cobra.Command{ + Use: "group-dimension", + Short: "对指定连续行/列创建分组", + Long: `对钉钉表格指定工作表中的连续整行或整列创建分组。 + +--range 使用整行/整列范围: + 行分组:3:7 或 3 + 列分组:C:F 或 C + +支持在 --range 中携带工作表前缀(如 "Sheet1!3:7" / "Sheet1!C:F"),此时将忽略 --sheet-id。 +创建后可通过 sheet info --include groups 回读 rowGroups / columnGroups。 + +--group-state 支持 expand / fold,默认 expand。`, + Example: ` # 分组第 3~7 行 + dws sheet group-dimension --node NODE_ID --sheet-id SHEET_ID --range "3:7" + + # 分组并折叠 C~F 列 + dws sheet group-dimension --node NODE_ID --sheet-id SHEET_ID --range "C:F" --group-state fold + + # 使用工作表前缀(忽略 --sheet-id) + dws sheet group-dimension --node NODE_ID --sheet-id SHEET_ID --range "Sheet1!3:7"`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "node", "sheet-id", "range"); err != nil { + return err + } + groupState, _ := cmd.Flags().GetString("group-state") + switch groupState { + case "", "expand": + groupState = "expand" + case "fold": + default: + return fmt.Errorf("--group-state 必须为 expand 或 fold,当前值: %s", groupState) + } + return callMCPTool("group_dimension", map[string]any{ + "nodeId": mustGetFlag(cmd, "node"), + "sheetId": mustGetFlag(cmd, "sheet-id"), + "range": mustGetFlag(cmd, "range"), + "groupState": groupState, + }) + }, + } + groupDimensionCmd.Flags().String("node", "", "表格文档 ID 或 URL (必填)") + groupDimensionCmd.Flags().String("sheet-id", "", "工作表 ID 或名称 (必填)") + groupDimensionCmd.Flags().String("range", "", `整行/整列范围 (必填),如 "3:7" 或 "C:F"`) + groupDimensionCmd.Flags().String("group-state", "expand", "创建后的分组状态: expand 或 fold") + + ungroupDimensionCmd := &cobra.Command{ + Use: "ungroup-dimension", + Short: "取消指定连续行/列分组", + Long: `取消钉钉表格指定工作表中的连续整行或整列分组。 + +--range 使用整行/整列范围: + 行分组:3:7 或 3 + 列分组:C:F 或 C + +支持在 --range 中携带工作表前缀(如 "Sheet1!3:7" / "Sheet1!C:F"),此时将忽略 --sheet-id。 +取消后可通过 sheet info --include groups 回读 rowGroups / columnGroups。`, + Example: ` # 取消第 3~7 行分组 + dws sheet ungroup-dimension --node NODE_ID --sheet-id SHEET_ID --range "3:7" + + # 取消 C~F 列分组 + dws sheet ungroup-dimension --node NODE_ID --sheet-id SHEET_ID --range "C:F"`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "node", "sheet-id", "range"); err != nil { + return err + } + return callMCPTool("ungroup_dimension", map[string]any{ + "nodeId": mustGetFlag(cmd, "node"), + "sheetId": mustGetFlag(cmd, "sheet-id"), + "range": mustGetFlag(cmd, "range"), + }) + }, + } + ungroupDimensionCmd.Flags().String("node", "", "表格文档 ID 或 URL (必填)") + ungroupDimensionCmd.Flags().String("sheet-id", "", "工作表 ID 或名称 (必填)") + ungroupDimensionCmd.Flags().String("range", "", `整行/整列范围 (必填),如 "3:7" 或 "C:F"`) + // ── dropdown ────────────────────────────────────────────────── setDropdownCmd := &cobra.Command{ Use: "set-dropdown", @@ -499,7 +576,7 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 return []*cobra.Command{ insertDimensionCmd, moveDimensionCmd, addDimensionCmd, mergeCellsCmd, unmergeRangeCmd, - deleteDimensionCmd, updateDimensionCmd, + deleteDimensionCmd, updateDimensionCmd, groupDimensionCmd, ungroupDimensionCmd, setDropdownCmd, getDropdownCmd, deleteDropdownCmd, } } diff --git a/internal/helpers/wukong_sync_commands_test.go b/internal/helpers/wukong_sync_commands_test.go new file mode 100644 index 00000000..ccb6b448 --- /dev/null +++ b/internal/helpers/wukong_sync_commands_test.go @@ -0,0 +1,182 @@ +// 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 ( + "reflect" + "testing" + + "github.com/spf13/cobra" +) + +func requireWukongSyncCommand(t *testing.T, root *cobra.Command, path ...string) *cobra.Command { + t.Helper() + cmd, remaining, err := root.Find(path) + if err != nil { + t.Fatalf("%s: command not found: %v", path, err) + } + if len(remaining) != 0 { + t.Fatalf("%s: unresolved path suffix %v (resolved to %q)", path, remaining, cmd.CommandPath()) + } + return cmd +} + +func requireWukongSyncFlags(t *testing.T, cmd *cobra.Command, names ...string) { + t.Helper() + for _, name := range names { + if cmd.Flags().Lookup(name) == nil { + t.Fatalf("%s: missing flag --%s", cmd.CommandPath(), name) + } + } +} + +func TestWukongSyncMailCommands(t *testing.T) { + root := newMailCommand() + cases := []struct { + path []string + flags []string + }{ + {[]string{"mailbox", "profile"}, []string{"email"}}, + {[]string{"message", "batch-get"}, []string{"email", "ids"}}, + {[]string{"sent-message", "recall"}, []string{"email", "id", "subject", "yes"}}, + {[]string{"sent-message", "recall-detail"}, []string{"email", "id"}}, + {[]string{"auto-reply", "update"}, []string{"email", "enabled", "start", "end", "scope", "content"}}, + {[]string{"allow-list", "list"}, []string{"email"}}, + {[]string{"allow-list", "add"}, []string{"email", "entries"}}, + {[]string{"allow-list", "remove"}, []string{"email", "entries"}}, + {[]string{"block-list", "list"}, []string{"email"}}, + {[]string{"block-list", "add"}, []string{"email", "entries"}}, + {[]string{"block-list", "remove"}, []string{"email", "entries"}}, + } + for _, tc := range cases { + cmd := requireWukongSyncCommand(t, root, tc.path...) + requireWukongSyncFlags(t, cmd, tc.flags...) + } +} + +func TestWukongSyncChatCommands(t *testing.T) { + root := newChatCommand() + cases := []struct { + path []string + flags []string + }{ + {[]string{"group", "notice", "create"}, []string{"group", "content", "sticky", "send-ding", "run-at"}}, + {[]string{"group", "notice", "edit"}, []string{"group", "notice-id", "content", "sticky", "send-ding"}}, + {[]string{"group", "notice", "get"}, []string{"group", "notice-id"}}, + {[]string{"group", "notice", "list"}, []string{"group", "limit", "cursor", "scheduled"}}, + {[]string{"group", "share-invite"}, []string{"source", "target", "receiver", "expires-seconds", "uuid"}}, + {[]string{"text", "translate"}, []string{"query", "to"}}, + {[]string{"category", "create-smart"}, []string{"name", "keywords", "members"}}, + {[]string{"message", "list-emotion-replies"}, []string{"msg-ids"}}, + } + for _, tc := range cases { + cmd := requireWukongSyncCommand(t, root, tc.path...) + requireWukongSyncFlags(t, cmd, tc.flags...) + } +} + +func TestWukongSyncDocCommands(t *testing.T) { + root := newDocCommand() + + importCmd := requireWukongSyncCommand(t, root, "import") + requireWukongSyncFlags(t, importCmd, "file", "folder", "workspace", "name", "folder-id", "workspace-id") + + importGetCmd := requireWukongSyncCommand(t, root, "import", "get") + requireWukongSyncFlags(t, importGetCmd, "task-id") +} + +func TestWukongSyncSheetCommands(t *testing.T) { + root := newSheetCommand() + + groupCmd := requireWukongSyncCommand(t, root, "group-dimension") + requireWukongSyncFlags(t, groupCmd, "node", "sheet-id", "range", "group-state") + + ungroupCmd := requireWukongSyncCommand(t, root, "ungroup-dimension") + requireWukongSyncFlags(t, ungroupCmd, "node", "sheet-id", "range") +} + +func TestWukongSyncSheetBatchDimensionGroupMapping(t *testing.T) { + group, err := translateBatchOp(map[string]any{ + "toolName": "group-dimension", + "input": map[string]any{ + "sheet-id": "Sheet1", + "range": "3:7", + "group-state": "fold", + }, + }) + if err != nil { + t.Fatalf("group-dimension mapping returned error: %v", err) + } + wantGroup := map[string]any{ + "toolName": "group_dimension", + "input": map[string]any{ + "sheetId": "Sheet1", + "range": "3:7", + "groupState": "fold", + }, + } + if !reflect.DeepEqual(group, wantGroup) { + t.Fatalf("group-dimension mapping mismatch:\n got: %#v\nwant: %#v", group, wantGroup) + } + + ungroup, err := translateBatchOp(map[string]any{ + "toolName": "ungroup-dimension", + "input": map[string]any{ + "sheet-id": "Sheet1", + "range": "C:F", + }, + }) + if err != nil { + t.Fatalf("ungroup-dimension mapping returned error: %v", err) + } + wantUngroup := map[string]any{ + "toolName": "ungroup_dimension", + "input": map[string]any{ + "sheetId": "Sheet1", + "range": "C:F", + }, + } + if !reflect.DeepEqual(ungroup, wantUngroup) { + t.Fatalf("ungroup-dimension mapping mismatch:\n got: %#v\nwant: %#v", ungroup, wantUngroup) + } +} + +func TestWukongSyncAgoalCommands(t *testing.T) { + root := newAgoalCommand() + cases := []struct { + path []string + flags []string + }{ + {[]string{"strategy", "list"}, []string{"scope-type", "scope-id", "request-id"}}, + {[]string{"strategy", "detail"}, []string{"profile-id", "request-id"}}, + {[]string{"strategy", "update"}, []string{"profile-id", "content", "request-id"}}, + {[]string{"contract", "list"}, []string{"scope-type", "scope-id", "request-id"}}, + {[]string{"contract", "fields"}, []string{"request-id"}}, + {[]string{"contract", "detail"}, []string{"contract-id", "request-id"}}, + {[]string{"contract", "update"}, []string{"contract-id", "dimensions", "audit-config", "objective-template", "request-id"}}, + {[]string{"scorecard", "detail"}, []string{"selected-time", "dept-id", "request-id"}}, + {[]string{"scorecard", "entity-detail"}, []string{"sc-id", "entity-id", "request-id"}}, + {[]string{"scorecard", "update"}, []string{"dept-id", "selected-time", "id", "tracking-period-type", "content", "request-id"}}, + {[]string{"user", "rules"}, []string{"user-id", "request-id"}}, + {[]string{"user", "objectives"}, []string{"user-id", "rule-id", "period-ids", "request-id"}}, + {[]string{"report", "list-statistics"}, []string{"keyword", "request-id"}}, + {[]string{"report", "submit-detail"}, []string{"template-id", "submit-state", "query-date", "page", "page-size", "keyword", "request-id"}}, + {[]string{"obj-template", "list"}, []string{"keyword", "page", "page-size", "request-id"}}, + {[]string{"obj-template", "create-or-update"}, []string{"template-id", "title", "objective-weight", "dimension-weight", "compute-by-weight", "dimensions", "request-id"}}, + } + for _, tc := range cases { + cmd := requireWukongSyncCommand(t, root, tc.path...) + requireWukongSyncFlags(t, cmd, tc.flags...) + } +} diff --git a/internal/syncdata/endpoints.go b/internal/syncdata/endpoints.go index ae4f5783..ac896af3 100644 --- a/internal/syncdata/endpoints.go +++ b/internal/syncdata/endpoints.go @@ -5,6 +5,7 @@ package syncdata func StaticServers() []ServerInfo { return []ServerInfo{ + {ID: "agoal", Name: "Agoal", Endpoint: "https://mcp-gw.dingtalk.com/server/1db49ea94ffe74c25a7079a68b4df6629c79130fb9095d1b499f507015d847a9", Prefixes: []string{"agoal"}}, {ID: "aisearch", Name: "AI 搜问", Endpoint: "https://mcp-gw.dingtalk.com/server/ai-search", Prefixes: []string{"aisearch", "enterprise"}}, {ID: "aitable", Name: "AI 多维表", Endpoint: "https://mcp-gw.dingtalk.com/server/5f0d121611f14e878f7d42c3e32bf6c4a790d433066adae38c062a657c397047", Prefixes: []string{"table", "record", "field", "base", "attachment", "view", "dashboard", "chart", "export", "import"}}, {ID: "aitable-helper", Name: "AI 多维表(辅助)", Endpoint: "https://mcp-gw.dingtalk.com/server/bb2984ee6b10c1560b4fe943ca620f646bed31f215c551a53abf040b52591a95", Prefixes: []string{"form", "share_form"}}, diff --git a/internal/syncdata/routing.go b/internal/syncdata/routing.go index 6922170e..4f4492ab 100644 --- a/internal/syncdata/routing.go +++ b/internal/syncdata/routing.go @@ -5,6 +5,7 @@ package syncdata func CmdToProduct() map[string]string { return map[string]string{ + "agoal": "agoal", "aisearch": "aisearch", "aitable": "aitable", "attendance": "attendance", diff --git a/scripts/install.sh b/scripts/install.sh index c981d3e9..d48260ac 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -260,7 +260,7 @@ resolve_skill_mode() { print_multi_mode_notice() { say "" say "🧪 Skill mode: multi (EXPERIMENTAL / preview) — automatic skill install skipped." - say " ⚠ multi is not yet stable. 20 product-scoped skills pass dispatch verifier," + say " ⚠ multi is not yet stable. 22 product-scoped skills pass dispatch verifier," say " but interface, naming and cross-skill references may change in future releases." say " For production / shared environments, use mono mode (--mode mono)." say "" diff --git a/skills/mono/SKILL.md b/skills/mono/SKILL.md index 3d8d4832..10331733 100644 --- a/skills/mono/SKILL.md +++ b/skills/mono/SKILL.md @@ -50,6 +50,7 @@ cli_version: ">=1.0.15" ## 意图判断决策树 用户提到"AI应用/创建应用/生成系统/做工具/管理后台/低代码" → `aiapp` +用户提到"目标管理/Agoal/战略解码/经营合约/计分卡/目标模板/周月报提交统计" → `agoal` 用户提到"找人/搜人/谁负责 XX/某事项的负责人/某项目的人/团队成员/上级/下级/按工号找人/按手机号找人" → `aisearch` 用户提到"表格/多维表/AI表格/记录/数据/视图/图表/仪表盘" → `aitable` 用户提到"考勤/打卡/排班" → `attendance` diff --git a/skills/mono/references/products/agoal.md b/skills/mono/references/products/agoal.md new file mode 100644 index 00000000..527351ea --- /dev/null +++ b/skills/mono/references/products/agoal.md @@ -0,0 +1,161 @@ +# Agoal(目标管理) + +## 产品说明 + +Agoal 是钉钉目标管理工具,支持战略解码、经营合约、计分卡、用户目标、目标模板、周月报六大模块,帮助组织将战略目标从顶层分解到个人并持续跟踪。 + +**CLI 前缀**: `dws agoal` + +## 命令总览 + +### strategy (战略解码管理) + +| 命令 | 用途 | 必填参数 | 备注 | +|------|------|----------|------| +| `strategy list` | 获取战略解码列表 | `--scope-type` `--scope-id` | scopeType: DEPT/PERSONAL;scope-id 为 scope-type 对应的部门 id 或用户 id | +| `strategy detail` | 获取战略解码详情 | `--profile-id` | 根据战略解码 id 查询 | +| `strategy update` | 更新战略解码 | `--profile-id` `--content` | 覆盖逻辑,必须基于查询返回的老数据修改后再传入;`--content` 为 JSON 数组 | + +`strategy update` 是覆盖式更新:一定要先 `strategy detail` 获取完整数据,在原数据基础上修改后再传入。 + +### contract (经营合约管理) + +| 命令 | 用途 | 必填参数 | 备注 | +|------|------|----------|------| +| `contract list` | 获取经营合约列表 | `--scope-type` `--scope-id` | scopeType: DEPT/PERSONAL;scope-id 为 scope-type 对应的部门 id 或用户 id | +| `contract fields` | 获取经营合约字段列表 | - | 获取组织下经营合约的字段配置 | +| `contract detail` | 获取经营合约详情 | `--contract-id` | 根据合约 id 查询 | +| `contract update` | 更新经营合约 | `--contract-id` `--dimensions` | 覆盖逻辑;可选 `--audit-config`、`--objective-template` | + +`contract update` 同样是覆盖式更新:必须基于 `contract detail` 返回的数据修改后再传入。 + +### scorecard (计分卡管理) + +| 命令 | 用途 | 必填参数 | 备注 | +|------|------|----------|------| +| `scorecard detail` | 获取计分卡详情 | `--selected-time` `--dept-id` | selectedTime 为 ISO-8601 字符串,如 `"2026-01-01T00:00:00+08:00"` | +| `scorecard entity-detail` | 获取计分卡实体详情 | `--sc-id` `--entity-id` | 根据计分卡 id 和实体 id 查询 | +| `scorecard update` | 更新计分卡 | `--dept-id` `--selected-time` `--id` `--tracking-period-type` `--content` | trackingPeriodType: MONTHLY/QUARTERLY | + +### user (用户目标管理) + +| 命令 | 用途 | 必填参数 | 备注 | +|------|------|----------|------| +| `user rules` | 获取用户规则周期列表 | - | 可选 `--user-id`,不传则默认取操作人自己 | +| `user objectives` | 查询用户目标列表 | `--user-id` `--rule-id` `--period-ids` | `--period-ids` 为逗号分隔的周期 id 列表 | + +### report (周月报管理) + +| 命令 | 用途 | 必填参数 | 备注 | +|------|------|----------|------| +| `report list-statistics` | 获取周月报数据跟催列表 | - | 返回各规则的人员提交情况统计;可选 `--keyword` | +| `report submit-detail` | 获取周月报规则提交详情 | `--template-id` `--submit-state` | submitState: ON_TIME/LATE/NOT_SUBMITTED;可选 `--query-date`、`--page`、`--page-size`、`--keyword` | + +### obj-template (目标模板管理) + +| 命令 | 用途 | 必填参数 | 备注 | +|------|------|----------|------| +| `obj-template list` | 获取目标模板列表 | - | 可选 `--keyword`、`--page`、`--page-size` | +| `obj-template create-or-update` | 新增或更新目标模板 | `--dimensions` | 覆盖逻辑;新增时 `--title` 必填;更新时 `--template-id` 必填 | + +## 意图判断 + +用户说"战略解码/战略目标/OGSM": +- 查看/列表 → `strategy list` +- 详情 → `strategy detail` +- 修改/更新 → `strategy update`(先查后改) + +用户说"经营合约/合约/KPI合约": +- 查看/列表 → `contract list` +- 字段配置 → `contract fields` +- 详情 → `contract detail` +- 修改/更新 → `contract update`(先查后改) + +用户说"计分卡/scorecard/绩效看板": +- 查看详情 → `scorecard detail` +- 实体详情 → `scorecard entity-detail` +- 修改/更新 → `scorecard update` + +用户说"目标/OKR/我的目标/个人目标": +- 规则周期 → `user rules` +- 目标列表 → `user objectives` + +用户说"目标模板/模板管理": +- 查看模板列表 → `obj-template list` +- 新增模板 → `obj-template create-or-update --title "模板名称"` +- 更新模板 → `obj-template create-or-update --template-id TPL_ID` + +用户说"周月报/周报统计/提交情况/跟催/迟交/未提交": +- 查看提交统计列表 → `report list-statistics` +- 查看某规则的提交详情 → `report submit-detail` + +## 核心工作流 + +```bash +# 查看战略解码列表与详情 +dws agoal strategy list --scope-type DEPT --scope-id DEPT_ID --format json +dws agoal strategy detail --profile-id PROFILE_ID --format json + +# 更新战略解码:必须基于 detail 返回内容修改后传入 +dws agoal strategy update --profile-id PROFILE_ID \ + --content '[{"id":"entity1","title":{"title":"新目标"},"entityType":"OGSM_OBJECTIVE","status":"NORMAL","executors":["dingId1"]}]' \ + --format json + +# 查看经营合约列表、字段与详情 +dws agoal contract list --scope-type PERSONAL --scope-id USER_ID --format json +dws agoal contract fields --format json +dws agoal contract detail --contract-id CONTRACT_ID --format json + +# 更新经营合约:必须基于 detail 返回内容修改后传入 +dws agoal contract update --contract-id CONTRACT_ID \ + --dimensions '[{"id":"dim1","title":"维度名称","objectives":[]}]' \ + --format json + +# 查看计分卡 +dws agoal scorecard detail --selected-time "2026-01-01T00:00:00+08:00" --dept-id DEPT_ID --format json +dws agoal scorecard entity-detail --sc-id SC_ID --entity-id ENTITY_ID --format json + +# 更新计分卡 +dws agoal scorecard update --dept-id DEPT_ID --selected-time "2026-01-01T00:00:00+08:00" \ + --id SC_ID --tracking-period-type MONTHLY \ + --content '[{"id":"dim1","title":"业绩","items":[{"id":"item1","title":"收入","target":"100"}]}]' \ + --format json + +# 查询用户目标 +dws agoal user rules --user-id USER_ID --format json +dws agoal user objectives --user-id USER_ID --rule-id RULE_ID --period-ids "period1,period2" --format json + +# 周月报提交统计与详情 +dws agoal report list-statistics --format json +dws agoal report list-statistics --keyword "周报规则" --format json +dws agoal report submit-detail --template-id TPL_ID --submit-state ON_TIME --format json +dws agoal report submit-detail --template-id TPL_ID --submit-state LATE --query-date "2026-06-18T00:00:00+08:00" --page 1 --page-size 20 --format json + +# 目标模板 +dws agoal obj-template list --format json +dws agoal obj-template list --keyword "业绩" --format json +dws agoal obj-template create-or-update --title "业绩模板" --objective-weight --dimension-weight --dimensions '[...]' --format json +dws agoal obj-template create-or-update --template-id TPL_ID --title "业绩模板" --dimensions '[...]' --format json +``` + +## 上下文传递表 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `strategy list` | `profileId` | `strategy detail` / `strategy update` 的 `--profile-id` | +| `strategy detail` | 完整实体数据 | `strategy update` 的 `--content`(基于此修改) | +| `contract list` | `contractId` | `contract detail` / `contract update` 的 `--contract-id` | +| `contract detail` | 完整维度数据 | `contract update` 的 `--dimensions`(基于此修改) | +| `scorecard detail` | `scId`、`entityId` | `scorecard entity-detail` / `scorecard update` 的 `--id` | +| `user rules` | `ruleId`、`periodIds` | `user objectives` 的 `--rule-id` `--period-ids` | +| `report list-statistics` | `templateId` | `report submit-detail` 的 `--template-id` | +| `obj-template list` | `templateId` | `obj-template create-or-update` 的 `--template-id`(更新时) | + +## 注意事项 + +- 所有 update / create-or-update 命令都是覆盖逻辑:必须先用对应 detail/list 查询完整数据,在原数据基础上修改后再传入。 +- 所有命令支持可选参数 `--request-id`。 +- `--scope-type` 仅支持 `DEPT` 和 `PERSONAL`。 +- `--selected-time` 接受 ISO-8601 字符串,如 `"2026-01-01T00:00:00+08:00"`。 +- `--period-ids` 为逗号分隔字符串,如 `"period1,period2"`。 +- `report submit-detail` 的 `--query-date` 接受 ISO-8601 字符串;不传则默认当天。 diff --git a/skills/mono/references/products/chat.md b/skills/mono/references/products/chat.md index dfdf1f3b..20eb7ce6 100644 --- a/skills/mono/references/products/chat.md +++ b/skills/mono/references/products/chat.md @@ -139,6 +139,26 @@ Flags: --expires-seconds int64 链接有效期(秒),0 表示永久有效,不传使用服务端默认值 ``` +#### 分享群聊链接到会话 — 将指定群的邀请链接分享到另一个会话或单聊用户 + +`--target` 和 `--receiver` 二选一:`--target` 指定目标会话,`--receiver` 指定单聊用户。 + +``` +Usage: + dws chat group share-invite [flags] +Example: + dws chat group share-invite --source <被分享群openConversationId> --target <目标会话openConversationId> + dws chat group share-invite --source <被分享群openConversationId> --receiver <接收者openDingTalkId> + dws chat group share-invite --source --target --expires-seconds 86400 + # 查询群 ID: dws chat search --query "群名" +Flags: + --source string 被分享群的 openConversationId (必填) + --target string 接收分享消息的会话 openConversationId(与 --receiver 二选一) + --receiver string 接收分享消息的单聊用户 openDingTalkId(与 --target 二选一) + --expires-seconds int64 链接有效期(秒),0 表示永久有效,不传使用服务端默认值 + --uuid string 消息幂等键(可选) +``` + #### 退出群聊 — 当前用户退出指定群聊 ``` Usage: @@ -272,6 +292,65 @@ Flags: - 不传 --role 时返回群主 + 管理员的所有群 ``` +### group notice (群公告管理) + +#### 发布群公告 — 在指定群聊中发布群公告,支持 Markdown、定时发布 + +``` +Usage: + dws chat group notice create [flags] +Example: + dws chat group notice create --group --content "今晚 22 点系统维护,请提前保存工作内容" + dws chat group notice create --group --content "# 重要通知\n请大家查收" --sticky --send-ding + dws chat group notice create --group --content "明早九点例会" --run-at "2026-07-03T09:00:00+08:00" +Flags: + --group string 群聊 openConversationId (必填) + --content string 公告正文,Markdown 格式 (必填) + --sticky 是否吊顶置顶(默认 false) + --send-ding 是否发 DING 提醒(默认 false) + --run-at string 定时发布时间 ISO-8601(传入则定时发布) +``` + +#### 修改群公告 — 整体替换指定群公告的内容 +``` +Usage: + dws chat group notice edit [flags] +Example: + dws chat group notice edit --group --notice-id --content "更新后的公告内容" +Flags: + --group string 群聊 openConversationId (必填) + --notice-id string 群公告 dataId (必填) + --content string 公告新正文,Markdown 格式 (必填) + --sticky 是否吊顶置顶 + --send-ding 是否发 DING 提醒 +``` + +#### 查看群公告详情 +``` +Usage: + dws chat group notice get [flags] +Example: + dws chat group notice get --group --notice-id +Flags: + --group string 群聊 openConversationId (必填) + --notice-id string 群公告 dataId (必填) +``` + +#### 查看群公告列表 +``` +Usage: + dws chat group notice list [flags] +Example: + dws chat group notice list --group + dws chat group notice list --group --limit 20 --cursor + dws chat group notice list --group --scheduled +Flags: + --group string 群聊 openConversationId (必填) + --limit int 每页返回数量(默认 10,最大 100) + --cursor string 分页游标 + --scheduled 是否查询定时公告列表 +``` + ### group-role (群身份管理) #### 查看群身份列表 — 拉取指定群聊的自定义群身份列表 @@ -994,6 +1073,20 @@ Flags: - 如果已有合适的表情,无需创建新的 ``` +#### 批量拉取消息的表情回复和文字回复 + +根据消息 ID 列表批量查询消息的表情回复和文字回复信息。 + +``` +Usage: + dws chat message list-emotion-replies [flags] +Example: + dws chat message list-emotion-replies --msg-ids msgId1,msgId2,msgId3 + # 消息 ID 可通过 dws chat message list 获取 +Flags: + --msg-ids string 消息 ID 列表,逗号分隔 (必填) +``` + ### list-top-conversations (置顶会话) #### 拉取置顶会话列表 @@ -1309,6 +1402,21 @@ Flags: --title string 分组名称 (必填) ``` +#### 创建智能会话分组 — 可指定群名称关键词和群内成员作为匹配规则 +``` +Usage: + dws chat category create-smart [flags] +Example: + dws chat category create-smart --name "工作群" + dws chat category create-smart --name "项目组" --keywords "项目,开发" + dws chat category create-smart --name "团队群" --members openDingTalkId1,openDingTalkId2 + dws chat category create-smart --name "重点群" --keywords "重点" --members openDingTalkId1 +Flags: + --name string 分组名称 (必填) + --keywords string 群名称关键词列表,逗号分隔(可选) + --members string 群内成员 openDingTalkId 列表,逗号分隔(可选) +``` + #### 删除会话分组 ``` Usage: @@ -1358,6 +1466,23 @@ Flags: --category-ids string 目标分组 ID 列表,逗号分隔 (必填) ``` +### text (文本内容处理) + +#### 翻译文本内容 +``` +Usage: + dws chat text translate [flags] +Example: + dws chat text translate --query "你好世界" --to en_US + dws chat text translate --query "Hello World" --to zh_CN + dws chat text translate --query "Bonjour" --to ja_JP +Flags: + --query string 待翻译的文本内容 (必填) + --to string 目标语言代码 (必填) +``` + +支持的目标语言代码:`en_US`, `zh_CN`, `zh_TW`, `zh_HK`, `ja_JP`, `ko_KR`, `vi_VN`, `th_TH`, `id_ID`, `ms_MY`, `es_419`, `fr_FR`, `pt_BR`, `tr_TR`, `ru_RU`, `de_DE`, `hi_IN`, `hu_HU`, `pl_PL`, `sv_SE`, `fi_FI`, `cs_CZ`, `ar_SA`, `tl_PH`, `he_IL`, `nl_NL`, `lo_LA`, `it_IT`。 + ### mute (会话免打扰) #### 会话消息免打扰 — 开启或关闭会话消息免打扰(支持单聊和群聊) @@ -1679,6 +1804,7 @@ Flags: 用户说"查看会话分组/自定义分组" → `chat category list` 用户说"某个分组下的会话/分组会话列表" → `chat category list-conversations` 用户说"新建会话分组/创建分组" → `chat category create` +用户说"创建智能分组/新建智能会话分组" → `chat category create-smart` 用户说"删除会话分组" → `chat category delete` 用户说"重命名分组/改分组名" → `chat category rename` 用户说"把会话加入分组/会话归类到分组" → `chat category add-conv` @@ -1693,7 +1819,10 @@ Flags: 用户说"查询某人的群身份/某人在群里有什么身份" → `chat group-role query-user` 用户说"转让群主/换群主/群主转让" → `chat group transfer-owner` 用户说"群邀请链接/入群链接/加群链接" → `chat group invite-url` +用户说"分享群链接/把群分享给某人/群链接发到某群" → `chat group share-invite` +用户说"群公告/发布公告/修改公告/查看公告/定时公告" → `chat group notice create/edit/get/list` 用户说"批量查消息/按ID查消息/根据消息ID查" → `chat message list-by-ids` +用户说"批量查消息回复/表情回复/文字回复/消息回应列表" → `chat message list-emotion-replies` 用户说"emoji回应/表情回应/给消息加表情" → `chat message add-emoji` 用户说"取消emoji回应/移除表情回应" → `chat message remove-emoji` 用户说"文字表情回应/添加文字表情" → `chat message add-text-emotion` @@ -1732,6 +1861,7 @@ Flags: 用户说"全员禁言/群禁言/解除禁言" → `chat group-mute` 用户说"禁言某人/指定成员禁言/解除某人禁言" → `chat group-mute-member` 用户说"设管理员/取消管理员/设置群管理员" → `chat group set-admin` +用户说"翻译文本/把这段话翻译成英文/翻译成中文" → `chat text translate` 关键区分: - `chat search` — 搜**群/会话名**返回 `openConversationId`,**不**搜消息内容;要搜消息内容请用 `chat message search-advanced`(首选)/ `chat message search` / `list-by-sender` / `list-all`,**勿混淆** @@ -1756,10 +1886,12 @@ Flags: - `chat message search-advanced` — 多维度搜索消息(支持关键词、发送者、@我、@指定人、多个会话等维度组合,与 `search` 的区别:`search` 仅支持关键词且必填,`search-advanced` 所有参数均可选) - `chat message list-by-ids` — 根据消息 ID 批量查询消息(最多 50 条) - `chat message add-emoji` / `remove-emoji` — 对消息添加/移除 emoji 表情回应 +- `chat message list-emotion-replies` — 批量拉取消息的表情回复和文字回复 - `chat message add-text-emotion` / `remove-text-emotion` — 对消息添加/移除文字表情回应 - `chat message create-text-emotion` — 创建文字表情模板,返回 emotionId 供 add-text-emotion 使用 - `chat category list` — 获取用户自定义会话分组列表 - `chat category list-conversations` — 拉取指定分组下的会话列表 +- `chat category create-smart` — 创建智能会话分组(可指定群名称关键词和成员作为匹配规则) - `chat mute` — 开启/关闭会话消息免打扰(默认开启,--off 关闭) - `chat hide` — 在会话列表中隐藏会话(支持单聊/群聊,收到新消息时重新出现) - `chat mute-at-all` — 关闭/开启 @所有人消息提醒(默认关闭,--off 恢复) @@ -1773,6 +1905,8 @@ Flags: - `chat file upload` — 已下线;不要调用 `chat/upload_conversation_file_by_url`,本地文件发送改用 `chat message send --msg-type file --file-path` - `chat group transfer-owner` — 转让群主 - `chat group invite-url` — 获取群邀请链接 +- `chat group share-invite` — 分享群聊链接到会话 +- `chat group notice create/edit/get/list` — 群公告发布、修改、详情、列表 - `chat message reply` — 引用回复消息(在群聊中引用某条消息并回复文字) - `chat message forward` — 转发单条消息(将一条消息从源会话转发到目标会话) - `chat set-top` — 设置/取消会话置顶(默认置顶,--off 取消) diff --git a/skills/mono/references/products/doc.md b/skills/mono/references/products/doc.md index fe799e71..6e91e6e7 100644 --- a/skills/mono/references/products/doc.md +++ b/skills/mono/references/products/doc.md @@ -131,6 +131,27 @@ Flags: --convert 是否转换为钉钉在线文档 ``` +### 导入本地文件为在线文档 + +详见 [doc/doc-import.md](./doc/doc-import.md)。 + +``` +Usage: + dws doc import [flags] + dws doc import get [flags] +Example: + dws doc import --file ./report.docx + dws doc import --file ./notes.md --folder + dws doc import --file ./data.xlsx --workspace + dws doc import get --task-id +Flags: + --file string 本地文件路径 (必填) + --folder string 目标文件夹 ID 或 URL + --workspace string 目标知识库 ID 或 URL + --name string 导入后文档名称 (默认取文件名) + --task-id string 导入任务 ID (import get 必填) +``` + ### 下载文件到本地(已迁移到 drive) > **弃用提示**:`dws doc download` 已迁移到 `dws drive download`(执行 `doc download` 会打印弃用警告)。下载已有文件(PDF/图片/附件等非在线文档)改用: @@ -618,6 +639,12 @@ Flags: - 上传 → `upload`(需本地文件路径) - 上传并转换 → `upload --convert` +用户说"导入文件/导入为在线文档/导入 Word/导入 Excel/导入 xmind/导入 Markdown/把本地文件转在线文档": +- 导入并转换为在线文档 → `doc import --file <本地路径>` +- 支持 docx/doc/xlsx/xls/md/txt/xmind/mark,文件大小不超过 20MB +- 如果用户指定知识库或文件夹,补充 `--workspace` 或 `--folder` +- 不要把本地文件内容先读出来再 `doc create/update`;应直接使用 `doc import` + 用户说"下载/导出/下载到本地/导出文档/导出为Word/导出为docx/把文档导出来": - **必须先判断目标文件类型**,再决定走 `doc export` 还是 `drive download`: - 在线文档(alidocs/adoc)→ **`doc export`**(内容级命令,格式转换后导出为 docx,未迁移) @@ -768,6 +795,20 @@ dws drive download --node --output ~/downloads/ # 如果是在线文档 (ALIDOC),用 doc export: dws doc export --node --output ~/downloads/ +# ── 工作流 5a: 导入本地文件为在线文档 ── + +# 导入到默认位置 +dws doc import --file ./report.docx --format json + +# 导入到指定文件夹 +dws doc import --file ./notes.md --folder --format json + +# 导入到知识库 +dws doc import --file ./data.xlsx --workspace --format json + +# 如果导入命令超时或中断,可用 import get 手动查询任务状态: +# dws doc import get --task-id + # ── 工作流 6: 上传附件并插入文档 ── # media insert 自动完成三步流程: @@ -882,6 +923,7 @@ dws doc export --node --output ./exported.docx | `drive list`(原 `doc list`,已弃用) | `nodes[].nodeId` / folder 类型 `nodeId` | read / info / update / block 的 --node;folder 用作 `--folder` | | `drive search`(原 `doc search`,已弃用) | 文档 `nodeId` / URL / `createTime` / `creatorUid` | read / info / update 的 --node;创建时间与创建者信息 | | `create` | `nodeId` | update / block 操作的 --node | +| `import` | `nodeId` / `documentUrl` / `documentName` / `documentType`;中断时提取 `taskId` | 后续 read / info / sheet 操作;中断后用 `doc import get --task-id` | | `drive mkdir`(原 `doc folder create`,已弃用) | `nodeId` | create 的 --folder | | `block list` | `blockId` | block insert 的 --ref-block, block update/delete 的 --block-id | | `comment list` | `commentList[].commentKey` | comment reply 的 --comment-key | diff --git a/skills/mono/references/products/doc/doc-import.md b/skills/mono/references/products/doc/doc-import.md new file mode 100644 index 00000000..bd6291de --- /dev/null +++ b/skills/mono/references/products/doc/doc-import.md @@ -0,0 +1,53 @@ +# 本地文件导入为在线文档 (doc import) + +## 使用场景 + +用户说"导入 Word/Excel/Markdown/xmind 到钉钉文档"、"把本地文件转成在线文档"、"导入到知识库/文件夹"时,使用 `dws doc import`。 + +不要先读取文件内容再调用 `doc create` 或 `doc update`。`doc import` 会按文件格式走导入任务,保留更完整的原始结构。 + +## 命令 + +```bash +dws doc import --file ./report.docx --format json +dws doc import --file ./notes.md --folder --format json +dws doc import --file ./data.xlsx --workspace --format json +dws doc import --file ./draft.md --name "项目周报" --format json +``` + +```bash +dws doc import get --task-id --format json +``` + +## 参数 + +| 参数 | 说明 | +|------|------| +| `--file` | 本地文件路径,必填 | +| `--folder` | 目标文件夹 ID 或 URL,可选 | +| `--workspace` | 目标知识库 ID 或 URL,可选 | +| `--name` / `-n` | 导入后的文档名称;不传时使用文件名 | +| `--task-id` | `import get` 查询导入任务时必填 | + +支持格式:docx、doc、xlsx、xls、md、txt、xmind、mark。文件大小上限 20MB。 + +## 工作流 + +1. 确认本地文件存在且格式受支持。 +2. 如果用户指定目标知识库或文件夹,传 `--workspace` 或 `--folder`;未指定时导入到默认位置。 +3. 执行 `dws doc import --file ... --format json`。 +4. 正常情况下 CLI 会自动提交、上传并轮询导入任务。 +5. 如果命令超时或中断,从输出中提取 `taskId`,再执行 `dws doc import get --task-id --format json`。 + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `doc import` | `nodeId` / `documentUrl` / `documentName` / `documentType` | 后续 `doc read` / `doc info` / `sheet` 操作 | +| `doc import` 中断 | `taskId` | `doc import get --task-id` 查询任务状态 | + +## 注意事项 + +- `--folder` 和 `--workspace` 都是目标位置参数;用户明确指定文件夹时优先使用 `--folder`。 +- 导入 Excel 后通常得到在线表格,后续数据读取和编辑走 `dws sheet ...`。 +- 导入 Markdown 或 Word 后通常得到在线文档,后续内容读取和编辑走 `dws doc ...`。 diff --git a/skills/mono/references/products/mail.md b/skills/mono/references/products/mail.md index 14159224..bea9ebdb 100644 --- a/skills/mono/references/products/mail.md +++ b/skills/mono/references/products/mail.md @@ -5,6 +5,7 @@ | 命令 | 功能简述 | |------|----------| | `dws mail mailbox list` | 查询**当前用户自己**的可用邮箱列表 | +| `dws mail mailbox profile` | 获取用户邮箱详细信息(容量、别名等) | | `dws mail message list` | 列出指定文件夹中的邮件(默认收件箱) | | `dws mail message search` | 搜索邮件(KQL 语法,按主题/发件人/日期等) | | `dws mail message get` | 查看邮件完整内容(含正文) | @@ -15,7 +16,10 @@ | `dws mail message batch-move` | 批量移动邮件到指定文件夹 | | `dws mail message batch-delete` | 批量删除邮件 | | `dws mail message batch-update` | 批量修改邮件状态(标记已读/未读/添加标签/移除标签) | +| `dws mail message batch-get` | 批量获取邮件详情(最多 20 封) | | `dws mail message verify` | 根据 internetMessageId 查询邮件发送状态 | +| `dws mail sent-message recall` | 撤回已发送的邮件(仅支持同组织内未读邮件) | +| `dws mail sent-message recall-detail` | 查询邮件撤回进度 | | `dws mail draft create` | 创建草稿(保留在草稿箱,不发送) | | `dws mail draft update` | 更新草稿内容(保留在草稿箱,不发送) | | `dws mail draft send` | 发送草稿箱中已有的草稿 | @@ -46,6 +50,13 @@ | `dws mail contact update` | 更新个人邮件联系人信息 | | `dws mail contact batch-delete` | 批量删除个人邮件联系人 | | `dws mail auto-reply get` | 获取用户的自动回复配置 | +| `dws mail auto-reply update` | 更新/设置用户的自动回复配置 | +| `dws mail allow-list list` | 列出个人收信白名单 | +| `dws mail allow-list add` | 添加个人收信白名单 | +| `dws mail allow-list remove` | 移除个人收信白名单 | +| `dws mail block-list list` | 列出个人收信黑名单 | +| `dws mail block-list add` | 添加个人收信黑名单 | +| `dws mail block-list remove` | 移除个人收信黑名单 | | `dws mail rule list` | 列出个人收信规则 | | `dws mail rule create` | 创建个人收信规则 | | `dws mail rule update` | 更新个人收信规则 | @@ -93,6 +104,27 @@ Example: |------|------|------| | `emailAccounts` | `List[]` | 邮箱列表,每条包含 `email`(邮箱地址)、`orgName`(所属企业,个人邮箱为 null)、`type`(`ORG`=企业邮箱 / `PERSONAL`=个人邮箱) | +### 获取用户邮箱信息 + +``` +Usage: + dws mail mailbox profile [flags] +Example: + dws mail mailbox profile --email user@company.com +Flags: + --email string 用户的邮箱地址 (必填) +``` + +**返回字段:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `email` | string | 邮箱地址 | +| `emailAliases` | List[] | 邮件地址别名列表 | +| `name` / `nickname` / `displayName` | string | 用户名、昵称、显示名 | +| `mboxSize` / `mboxSizeUsed` | int64 | 邮箱容量与已用容量(字节) | +| `createdTime` / `modifiedTime` | string | 创建时间与更新时间 | + ### 查找他人邮箱地址(通讯录查人) > **这不是 `mailbox list`。** 当需要获取**某人**的邮箱地址时,必须走以下三路并发查询,取最先返回有效邮箱的结果。禁止臆测邮箱地址。 @@ -218,6 +250,21 @@ Flags: > **注意:** 即使邮件确有附件,`message get` 返回的 `message` 对象也**不含** `attachments`/`isRead`/`tags` 字段(实测 keys 仅上表所列这些)。要取附件请另用 `dws mail attachment list`。 +### 批量获取邮件详情 + +``` +Usage: + dws mail message batch-get [flags] +Example: + dws mail message batch-get --email user@company.com --ids , + dws mail message batch-get --email user@company.com --ids ,, +Flags: + --email string 邮件所属邮箱地址 (必填) + --ids string 要获取的邮件 ID 列表,逗号分隔,最多 20 个 (必填) +``` + +单次最多获取 20 封邮件。CLI 会逐个调用 `get_email_by_message_id`,并以 `messages` 数组聚合返回。 + ### 发送邮件 ``` Usage: @@ -935,6 +982,53 @@ Flags: | `failed` | 发送失败 | | `unknown` | 未知状态 | +### [危险] 撤回已发送的邮件 + +``` +Usage: + dws mail sent-message recall [flags] +Example: + dws mail sent-message recall --email user@company.com --id --subject "邮件主题" --yes +Flags: + --email string 发件人邮箱地址 (必填) + --id string 要撤回的邮件 ID (必填) + --subject string 邮件主题 (必填) + --yes 跳过确认提示,直接执行 (可选) +``` + +> CAUTION: 此命令会撤回已发送邮件。仅支持撤回同组织内未读邮件,执行前必须确认目标邮件、主题和影响范围。 + +**返回字段:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | string | 撤回任务 ID,可用于 `recall-detail` 查询进度 | +| `success` | boolean | 接口调用是否成功 | +| `errorCode` / `errorMsg` | string | 错误码和错误信息(仅失败时存在) | + +### 查询邮件撤回进度 + +``` +Usage: + dws mail sent-message recall-detail [flags] +Example: + dws mail sent-message recall-detail --email user@company.com --id +Flags: + --email string 用户的邮箱地址 (必填) + --id string 撤回任务 ID (必填),由 recall 命令返回 +``` + +根据撤回任务 ID 查询邮件撤回进度。任务状态包括 `UNINITED` / `SUBMITTED` / `RUNNING` / `FINISHED` / `CANCELED` / `FAILED`。 + +**返回字段:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | string | 撤回任务 ID | +| `status` | string | 任务状态 | +| `totalCount` / `succeededCount` / `failedCount` | int | 总数、成功数、失败数 | +| `details` | List[] | 每封邮件的撤回结果 | + ### 创建草稿 ``` Usage: @@ -1220,6 +1314,61 @@ Flags: | `scope` | string | 回复范围: "contact"(仅联系人) 或 "all"(所有人) | | `content` | string | 自动回复内容 | +### 更新自动回复配置 + +更新或设置用户的邮件自动回复配置。建议先通过 `auto-reply get` 获取当前配置,再传入需要修改的字段值。 + +``` +Usage: + dws mail auto-reply update [flags] +Example: + dws mail auto-reply update --email user@company.com --enabled true \ + --start "2026/07/01 09:00:00 +0800" --end "2026/07/07 18:00:00 +0800" \ + --scope all --content "出差中,请稍后联系" + dws mail auto-reply update --email user@company.com --enabled false \ + --start "2026/07/01 09:00:00 +0800" --end "2026/07/07 18:00:00 +0800" \ + --scope all --content "已关闭自动回复" +Flags: + --email string 用户的邮箱地址 (必填) + --enabled string 是否启用自动回复: true/false (必填) + --start string 自动回复开始时间,格式: YYYY/MM/DD HH:MM:SS +ZZZZ (必填) + --end string 自动回复结束时间,格式: YYYY/MM/DD HH:MM:SS +ZZZZ (必填) + --scope string 回复范围: contact(仅联系人)/all(所有人) (必填) + --content string 自动回复内容 (必填) +``` + +### 个人收信白名单管理 + +``` +Usage: + dws mail allow-list list [flags] + dws mail allow-list add [flags] + dws mail allow-list remove [flags] +Examples: + dws mail allow-list list --email user@company.com + dws mail allow-list add --email user@company.com --entries a@b.com,@example.com + dws mail allow-list remove --email user@company.com --entries a@b.com,@example.com +Flags: + --email string 用户的邮箱地址 (必填) + --entries string 逗号分隔的地址列表,支持邮件地址或 @domain.com 域名(add/remove 必填) +``` + +### 个人收信黑名单管理 + +``` +Usage: + dws mail block-list list [flags] + dws mail block-list add [flags] + dws mail block-list remove [flags] +Examples: + dws mail block-list list --email user@company.com + dws mail block-list add --email user@company.com --entries spam@bad.com,@junk.com + dws mail block-list remove --email user@company.com --entries spam@bad.com,@junk.com +Flags: + --email string 用户的邮箱地址 (必填) + --entries string 逗号分隔的地址列表,支持邮件地址或 @domain.com 域名(add/remove 必填) +``` + ### 收信规则管理 #### 列出收信规则 @@ -1473,9 +1622,11 @@ Flags: ## 意图判断 用户说"我的邮箱/邮箱地址" → `mailbox list`(**仅限查询自己的邮箱,不能查他人**) +用户说"邮箱详情/邮箱容量/邮箱别名/邮箱 profile" → `mailbox profile --email <邮箱>` 用户说"获取/查找/得到 某人的邮箱地址" → **不是 `mailbox list`**,走三路并发查询流程(见「查找他人邮箱地址」章节) 用户说"找邮件/搜邮件/查邮件" → `message search` 用户说"看邮件/打开邮件/邮件内容" → 先 `message search` 获取 messageId,再 `message get` +用户说"批量查看邮件详情/批量打开这些邮件/按多个邮件ID取详情" → `message batch-get --ids `(单次最多 20 个) 用户说"发邮件/写邮件" → 先 `mailbox list` 获取发件地址,再 `message send` 用户说“给(某人名字)发邮件” / “查询某人发给我的邮件” / “查询发给某人的邮件” / 任何涉及按人名查找邮箱的场景 → **第一步**:并发同时发起以下三路查询,取最先返回有效邮箱的结果;若三路均无有效邮箱,ask_human 请用户提供,禁止臆测: @@ -1502,6 +1653,11 @@ Flags: 用户说"删除会话/把会话放入已删除/批量删除会话" → 单条用 `thread trash`,多条用 `thread batch-trash`;传入的是会话 ID,不是邮件 ID 用户说"批量标记邮件已读/未读/给邮件加标签/移除邮件标签" → `message batch-update --action markRead/markUnread/addTags/removeTags`(操作对象是邮件 ID;会话级操作用 `thread update/batch-update`) 用户说"邮件发出去了吗/查邮件发送状态/确认邮件是否发送成功/邮件投递结果" → 用发送类命令返回的 `internetMessageId`,调用 `message verify` 查询 `sendStatus` +用户说"撤回邮件/撤回已发送邮件" → `sent-message recall --email <邮箱> --id --subject "<主题>" --yes`(危险操作,先确认) +用户说"查询邮件撤回进度/撤回任务状态" → `sent-message recall-detail --email <邮箱> --id ` +用户说"设置/更新/关闭自动回复" → `auto-reply update`(所有字段必填,先 `auto-reply get` 获取当前配置) +用户说"收信白名单/添加白名单/移除白名单" → `allow-list list/add/remove` +用户说"收信黑名单/添加黑名单/移除黑名单" → `block-list list/add/remove` 用户说"查看会话/获取会话/看这封邮件的会话" → 先 `message search` 或 `message get` 获取邮件中的 `conversationId`,再 `thread get` 用户说"搜索/查找/联系 邮箱用户/联系人/某人的邮箱地址" → `user search`(搜索通讯录人员,不是搜邮件内容) 用户说"发送草稿/把草稿发出去/发这封草稿" → 先 `message search --query "folderId:5"` 找到草稿 messageId,再 `draft send` @@ -1618,7 +1774,9 @@ dws mail thread get --email user@company.com --id --select mess | 操作 | 从返回中提取 | 用于 | |------|-------------|------| | `mailbox list` | 邮箱地址 | message search/get/send/thread get 的 --email/--from | +| `mailbox profile` | `emailAliases` / `mboxSize` / `mboxSizeUsed` | 展示邮箱别名与容量信息 | | `message search` | `messageId` | message get 的 --id | +| `message search` | `messageId` | message batch-get 的 --ids | | `message search` | `conversationId` | thread get 的 --id | | `message search` | `messageId` | attachment list 的 --id | | `attachment list` | `attachments[].id` / `attachments[].name` | attachment download 的 --attachment-id / --name | @@ -1633,6 +1791,8 @@ dws mail thread get --email user@company.com --id --select mess | `aisearch person` → `contact user get` / `contact user search` / `mail user search` | 用户邮箱 (orgAuthEmail / email) | message send 的 --to/--cc(三路并发,取先到结果) | | `user search` | 用户邮箱 (email) | message send 的 --to/--cc | | `message send` / `draft send` / `message reply` / `message reply-all` / `message forward` | `internetMessageId` | `message verify` 的 --internet-message-id | +| `sent-message recall` | `id` | sent-message recall-detail 的 --id | +| `auto-reply get` | `enabled` / `startTime` / `endTime` / `scope` / `content` | auto-reply update 的当前配置基线 | ## 注意事项 diff --git a/skills/mono/references/products/sheet.md b/skills/mono/references/products/sheet.md index 44c8fb55..c4731a22 100644 --- a/skills/mono/references/products/sheet.md +++ b/skills/mono/references/products/sheet.md @@ -108,6 +108,8 @@ dws sheet filter-view --help | `sheet update-dimension` | 更新行/列属性(显隐/行高/列宽) | | `sheet move-dimension` | 移动行或列到指定位置 | | `sheet add-dimension` | 在末尾追加空行或空列 | +| `sheet group-dimension` | 创建行/列分组 | +| `sheet ungroup-dimension` | 取消行/列分组 | | `sheet media-upload` | 上传附件到表格 | | `sheet write-image` | 上传图片并写入单元格 | | `sheet create-float-image` | 创建浮动图片 | @@ -156,7 +158,7 @@ dws sheet filter-view --help | 读取数据 | [sheet/sheet-read-data.md](./sheet/sheet-read-data.md) | range read / csv-get | | 区域操作 | [sheet/sheet-range-operations.md](./sheet/sheet-range-operations.md) | range clear / sort / fill / copy-to / move-to | | 批量操作 | [sheet/sheet-batch-operations.md](./sheet/sheet-batch-operations.md) | range batch-clear / batch-update | -| 行列操作 | [sheet/sheet-dimension-operations.md](./sheet/sheet-dimension-operations.md) | insert / delete / update / move / add-dimension | +| 行列操作 | [sheet/sheet-dimension-operations.md](./sheet/sheet-dimension-operations.md) | insert / delete / update / move / add-dimension / group-dimension / ungroup-dimension | | 样式与合并 | [sheet/sheet-style-format.md](./sheet/sheet-style-format.md) | range set-style / batch-set-style / merge-cells / unmerge-cells | | 条件格式 | [sheet/sheet-conditional-format.md](./sheet/sheet-conditional-format.md) | cond-format list / create / update / delete | | 浮动图表 | [sheet/sheet-chart.md](./sheet/sheet-chart.md) | chart list / create / update / delete | @@ -254,6 +256,11 @@ dws sheet filter-view --help - 注意与 `append`(追加数据行)区分:`add-dimension` 追加的是空行/空列,`append` 追加的是带数据的行 - 请勿用 `range update` 写空数据来模拟追加,`add-dimension` 直接扩展表格维度 +用户说"创建行分组/创建列分组/折叠几行/折叠几列/取消行列分组": +- 创建分组 → `group-dimension --range <整行或整列范围>` +- 取消分组 → `ungroup-dimension --range <整行或整列范围>` +- `--range` 只能传整行或整列范围,如 `3:7`、`C:F`、`Sheet1!3:7`,不要传 `A1:C5` + ### 单元格格式 用户说"设置样式/改颜色/设背景色/加粗/居中/换行/字体颜色/字号": @@ -1099,6 +1106,38 @@ Flags: 在工作表末尾追加指定数量的空行或空列。 +### 创建行或列分组 +``` +Usage: + dws sheet group-dimension [flags] +Example: + dws sheet group-dimension --node --sheet-id --range "3:7" + dws sheet group-dimension --node --sheet-id --range "C:F" --group-state fold + dws sheet group-dimension --node --sheet-id --range "Sheet1!3:7" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 整行/整列范围,如 "3:7" 或 "C:F" (必填) + --group-state string 创建后的分组状态: expand 或 fold,默认 expand +``` + +在工作表中为连续行或连续列创建分组。`--range` 必须是整行或整列范围,不支持普通单元格矩形范围。 + +### 取消行或列分组 +``` +Usage: + dws sheet ungroup-dimension [flags] +Example: + dws sheet ungroup-dimension --node --sheet-id --range "3:7" + dws sheet ungroup-dimension --node --sheet-id --range "C:F" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 整行/整列范围,如 "3:7" 或 "C:F" (必填) +``` + +取消指定连续行或连续列的分组。 + ### 取消合并单元格 ``` Usage: @@ -1903,6 +1942,8 @@ dws sheet export --node --output ./ | `replace` | `replaceCount` 被替换的单元格数量 | 确认替换结果 | | `move-dimension` | `sheetId` 工作表 ID | 确认操作完成 | | `add-dimension` | `sheetId` 工作表 ID | 确认操作完成 | +| `group-dimension` | `range` / `level` / `groupState` | 确认行列分组层级和展开状态 | +| `ungroup-dimension` | `range` / `level` | 确认行列分组已取消 | | `unmerge-cells` | `sheetId` 工作表 ID | 确认操作完成 | | `set-dropdown` | `range` 实际设置范围、`optionCount` 选项数量、`enableMultiSelect` 是否多选 | 确认下拉列表设置成功 | | `get-dropdown` | `hasDropdown` 是否存在下拉、`dataValidations` 下拉配置列表(含 `conditionValues`、`ranges`、`options`) | 查看已有下拉配置 | @@ -2032,6 +2073,9 @@ dws sheet export --node --output ./ - `add-dimension` vs `range update`:需要在末尾追加空行/空列时,必须使用 `add-dimension` 命令,禁止用 `range update` 写空数据来模拟追加效果 - `add-dimension` 追加的是空行/空列,与 `append`(追加带数据的行)不同 - `add-dimension` 的 `--length` 必须为正整数(>= 1),行列均不超过 5000 +- `group-dimension` / `ungroup-dimension` 的 `--range` 只接受整行或整列范围,如 `3:7`、`C:F`、`Sheet1!3:7`,不支持 `A1:C5` +- `group-dimension` 的 `--group-state` 只接受 `expand` 或 `fold`,默认 `expand` +- `batch-update` 支持 `group-dimension` / `ungroup-dimension`;需要创建后立即折叠时,优先使用独立 `group-dimension --group-state fold` - `unmerge-cells` 取消指定范围内所有合并单元格,使用 A1 表示法指定范围 - `set-dropdown` 在指定范围内设置下拉列表,`--options` 为 JSON 数组,每个元素包含 `value`(必填)和 `color`(可选,`#RRGGBB` 格式)。选项值不能包含英文逗号。`--multi-select` 启用多选模式。如果目标范围已存在下拉列表,会被新配置覆盖 - `get-dropdown` 查询指定范围内的下拉列表配置,返回 `dataValidations` 数组,相同选项的单元格聚合为一组。无下拉列表时 `hasDropdown` 为 false diff --git a/skills/mono/references/products/sheet/sheet-batch-operations.md b/skills/mono/references/products/sheet/sheet-batch-operations.md index ce79f719..d354af92 100644 --- a/skills/mono/references/products/sheet/sheet-batch-operations.md +++ b/skills/mono/references/products/sheet/sheet-batch-operations.md @@ -26,6 +26,7 @@ - 需要对**多个**不同区域执行 `merge-cells` / `unmerge-cells` 时(如按分组合并多列相同内容) - 需要对**多个**不同区域执行 `update-dimension` 时(如统一调整多列列宽或多行行高,含 hidden + pixelSize) - 需要先插入行列再写入数据时(`add-dimension` + `range update` / `csv-put`) +- 需要批量创建或取消行列分组时(`group-dimension` / `ungroup-dimension`) - 需要对多个区域执行不同写入操作时(多次 `range update` + `range clear` 等组合) 当同一工具需要对多个区域重复调用时,**推荐**改用 `batch-update` 合并为单次请求——`batch-update` 是原子提交(要么全成功要么整批回滚);逐个调用非原子,中途失败会留下半成品。 @@ -80,6 +81,7 @@ Flags: toolName 用 CLI 命令名,input 的键用 CLI flag 名去掉 --: range clear / range update / merge-cells / unmerge-cells / update-dimension range fill / range copy-to / add-dimension / delete-dimension / move-dimension + group-dimension / ungroup-dimension set-dropdown / delete-dropdown / csv-put / delete-float-image Notes: - 默认严格事务模式:任一子操作失败 → 整批回滚到初始状态 @@ -177,6 +179,8 @@ Cause: The requested resource was not found by the identifier 'NonExistentSheet' | `add-dimension` | [sheet-dimension-operations](./sheet-dimension-operations.md) | | `delete-dimension` | [sheet-dimension-operations](./sheet-dimension-operations.md) | | `move-dimension` | [sheet-dimension-operations](./sheet-dimension-operations.md) | +| `group-dimension` | [sheet-dimension-operations](./sheet-dimension-operations.md) | +| `ungroup-dimension` | [sheet-dimension-operations](./sheet-dimension-operations.md) | | `set-dropdown` | [sheet-dropdown](./sheet-dropdown.md) | | `delete-dropdown` | [sheet-dropdown](./sheet-dropdown.md) | | `csv-put` | [sheet-write-data](./sheet-write-data.md) | @@ -230,6 +234,19 @@ Cause: The requested resource was not found by the identifier 'NonExistentSheet' ] ``` +### 批量创建行列分组 + +```jsonc +[ + {"toolName":"group-dimension", + "input":{"sheet-id":"...","range":"3:7","group-state":"expand"}}, + {"toolName":"group-dimension", + "input":{"sheet-id":"...","range":"C:F"}}, + {"toolName":"ungroup-dimension", + "input":{"sheet-id":"...","range":"10:12"}} +] +``` + ## 上下文传递 | 操作 | 从返回中提取 | 用于 | @@ -244,3 +261,5 @@ Cause: The requested resource was not found by the identifier 'NonExistentSheet' - ★ **需要组合多个不同写操作(清除+写入等)时,用 `batch-update`**:原子事务,任一操作失败则整批回滚,避免留下半成品 - ★ **批次完成后必须回读校验**:用 `range read` 或 `csv-get` 抽样回读受影响区域,至少校验 3-5 个代表性单元格(首 / 中 / 末),与预期值对照 - ★ **`batch-update` 不支持嵌套**:`--operations` 中的 `toolName` 必须是原子操作,不可再嵌套 `batch-update` +- `batch-update` 支持 `group-dimension` / `ungroup-dimension`;`input.range` 只接受整行或整列范围,如 `3:7`、`C:F` +- 需要创建后立即折叠分组时,优先使用独立 `dws sheet group-dimension --group-state fold` diff --git a/skills/mono/references/products/sheet/sheet-dimension-operations.md b/skills/mono/references/products/sheet/sheet-dimension-operations.md index 589c157e..afc2f5ac 100644 --- a/skills/mono/references/products/sheet/sheet-dimension-operations.md +++ b/skills/mono/references/products/sheet/sheet-dimension-operations.md @@ -26,6 +26,12 @@ - 注意与 `append`(追加数据行)区分:`add-dimension` 追加的是空行/空列,`append` 追加的是带数据的行 - 请勿用 `range update` 写空数据来模拟追加,`add-dimension` 直接扩展表格维度 +用户说"创建行分组/创建列分组/折叠几行/折叠几列/展开行列分组/取消行列分组": +- 创建分组 → `group-dimension --range <整行或整列范围>` +- 创建并折叠 → `group-dimension --group-state fold` +- 取消分组 → `ungroup-dimension --range <整行或整列范围>` +- `--range` 只能传整行或整列范围,如 `3:7`、`C:F`、`Sheet1!3:7` + **结构预检**:插入、删除、移动行列前,必须先执行 `dws sheet info --node --sheet-id --format json` 查看 `mergedRanges`。合并区域跨过操作位置时,行列变更可能导致表头/分组标题断裂、空白或错位;需要先向用户说明影响,必要时取消合并后操作,再按原模式重新 `merge-cells`。 ## 命令详细参考 @@ -172,6 +178,38 @@ Flags: 在工作表末尾追加指定数量的空行或空列。 +### 创建行或列分组 +``` +Usage: + dws sheet group-dimension [flags] +Example: + dws sheet group-dimension --node --sheet-id --range "3:7" + dws sheet group-dimension --node --sheet-id --range "C:F" --group-state fold + dws sheet group-dimension --node --sheet-id --range "Sheet1!3:7" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 整行/整列范围,如 "3:7" 或 "C:F" (必填) + --group-state string 创建后的分组状态: expand 或 fold,默认 expand +``` + +为连续行或连续列创建分组。`--range` 必须是整行或整列范围,不支持 `A1:C5` 这类普通单元格矩形范围。 + +### 取消行或列分组 +``` +Usage: + dws sheet ungroup-dimension [flags] +Example: + dws sheet ungroup-dimension --node --sheet-id --range "3:7" + dws sheet ungroup-dimension --node --sheet-id --range "C:F" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 整行/整列范围,如 "3:7" 或 "C:F" (必填) +``` + +取消指定连续行或连续列的分组。 + ## 核心工作流 ```bash @@ -244,6 +282,8 @@ dws sheet update-dimension --node --sheet-id \ | `update-dimension` | `a1Notation` 被更新区域范围、`hidden` 生效的显隐状态、`pixelSize` 生效的尺寸 | 确认更新结果 | | `move-dimension` | `sheetId` 工作表 ID | 确认操作完成 | | `add-dimension` | `sheetId` 工作表 ID | 确认操作完成 | +| `group-dimension` | `range` / `level` / `groupState` | 确认行列分组层级和展开状态 | +| `ungroup-dimension` | `range` / `level` | 确认行列分组已取消 | | `list` | 工作表的 `sheetId` | info / range read / range update / find 的 --sheet-id | ## 注意事项 @@ -273,3 +313,7 @@ dws sheet update-dimension --node --sheet-id \ - `add-dimension` vs `range update`:需要在末尾追加空行/空列时,必须使用 `add-dimension` 命令,禁止用 `range update` 写空数据来模拟追加效果 - `add-dimension` 追加的是空行/空列,与 `append`(追加带数据的行)不同 - `add-dimension` 的 `--length` 必须为正整数(>= 1),行列均不超过 5000 +- `group-dimension` / `ungroup-dimension` 的 `--range` 只接受整行或整列范围,如 `3:7`、`C:F`、`Sheet1!3:7`,不支持 `A1:C5` +- `group-dimension` 的 `--group-state` 只接受 `expand` 或 `fold`,默认 `expand` +- 分组操作返回 `level` 表示当前分组层级;不要把它误认为固定深度参数 +- `batch-update` 支持 `group-dimension` / `ungroup-dimension`;需要创建后立即折叠时,优先使用独立 `group-dimension --group-state fold` diff --git a/skills/multi/dingtalk-agoal/SKILL.md b/skills/multi/dingtalk-agoal/SKILL.md new file mode 100644 index 00000000..25fbf694 --- /dev/null +++ b/skills/multi/dingtalk-agoal/SKILL.md @@ -0,0 +1,43 @@ +--- +name: dingtalk-agoal +description: 钉钉 Agoal 目标管理。Use when 用户说 目标管理/Agoal/战略解码/经营合约/计分卡/OKR/目标模板/周月报提交统计/跟催。Distinct from dingtalk-todo(待办任务)、dingtalk-report(日志)。命令前缀:dws agoal。 +cli_version: ">=0.2.14" +metadata: + category: product + stability: experimental + requires: + bins: + - dws +--- + +# 钉钉 Agoal Skill + +> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。22 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 + +> **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH. + + + +> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws --help` 或 `--dry-run` 验证。 + +> 命令参考:[agoal.md](references/agoal.md)。 + +## 意图表 + +| 用户说 | 命令 | +|--------|------| +| "查战略解码 / OGSM" | `dws agoal strategy list --scope-type --scope-id ` | +| "看战略解码详情" | `dws agoal strategy detail --profile-id ` | +| "更新战略解码" | 先 detail,基于完整数据修改后 `dws agoal strategy update ...` | +| "查经营合约 / KPI合约" | `dws agoal contract list/detail/fields` | +| "更新经营合约" | 先 detail,基于完整 dimensions 修改后 `dws agoal contract update ...` | +| "查计分卡" | `dws agoal scorecard detail --selected-time --dept-id ` | +| "我的目标 / 个人目标" | `dws agoal user rules` → `dws agoal user objectives` | +| "周月报提交统计 / 跟催 / 迟交 / 未提交" | `dws agoal report list-statistics` → `dws agoal report submit-detail` | +| "目标模板" | `dws agoal obj-template list/create-or-update` | + +## 写操作硬约束 + +- `strategy update` / `contract update` / `scorecard update` / `obj-template create-or-update` 都是覆盖式写入。 +- 必须先读取现有详情或模板列表,在原数据基础上修改;禁止只传局部字段覆盖。 +- 执行前必须向用户展示目标对象、修改字段和最终命令参数摘要,并等待确认。 diff --git a/skills/multi/dingtalk-agoal/references/agoal.md b/skills/multi/dingtalk-agoal/references/agoal.md new file mode 100644 index 00000000..1f1ad332 --- /dev/null +++ b/skills/multi/dingtalk-agoal/references/agoal.md @@ -0,0 +1,77 @@ +# Agoal(目标管理) + +## 产品说明 + +Agoal 是钉钉目标管理工具,支持战略解码、经营合约、计分卡、用户目标、目标模板、周月报六大模块,帮助组织将战略目标从顶层分解到个人并持续跟踪。 + +**CLI 前缀**: `dws agoal` + +## 命令总览 + +### strategy (战略解码管理) + +| 命令 | 用途 | 必填参数 | 备注 | +|------|------|----------|------| +| `strategy list` | 获取战略解码列表 | `--scope-type` `--scope-id` | scopeType: DEPT/PERSONAL;scope-id 为 scope-type 对应的部门 id 或用户 id | +| `strategy detail` | 获取战略解码详情 | `--profile-id` | 根据战略解码 id 查询 | +| `strategy update` | 更新战略解码 | `--profile-id` `--content` | 覆盖逻辑,必须基于查询返回的老数据修改后再传入;`--content` 为 JSON 数组 | + +### contract (经营合约管理) + +| 命令 | 用途 | 必填参数 | 备注 | +|------|------|----------|------| +| `contract list` | 获取经营合约列表 | `--scope-type` `--scope-id` | scopeType: DEPT/PERSONAL;scope-id 为 scope-type 对应的部门 id 或用户 id | +| `contract fields` | 获取经营合约字段列表 | - | 获取组织下经营合约的字段配置 | +| `contract detail` | 获取经营合约详情 | `--contract-id` | 根据合约 id 查询 | +| `contract update` | 更新经营合约 | `--contract-id` `--dimensions` | 覆盖逻辑;可选 `--audit-config`、`--objective-template` | + +### scorecard (计分卡管理) + +| 命令 | 用途 | 必填参数 | 备注 | +|------|------|----------|------| +| `scorecard detail` | 获取计分卡详情 | `--selected-time` `--dept-id` | selectedTime 为 ISO-8601 字符串 | +| `scorecard entity-detail` | 获取计分卡实体详情 | `--sc-id` `--entity-id` | 根据计分卡 id 和实体 id 查询 | +| `scorecard update` | 更新计分卡 | `--dept-id` `--selected-time` `--id` `--tracking-period-type` `--content` | trackingPeriodType: MONTHLY/QUARTERLY | + +### user / report / obj-template + +| 命令 | 用途 | 必填参数 | +|------|------|----------| +| `user rules` | 获取用户规则周期列表 | - | +| `user objectives` | 查询用户目标列表 | `--user-id` `--rule-id` `--period-ids` | +| `report list-statistics` | 获取周月报数据跟催列表 | - | +| `report submit-detail` | 获取周月报规则提交详情 | `--template-id` `--submit-state` | +| `obj-template list` | 获取目标模板列表 | - | +| `obj-template create-or-update` | 新增或更新目标模板 | `--dimensions` | + +## 意图判断 + +- "战略解码/战略目标/OGSM" → `strategy list/detail/update` +- "经营合约/合约/KPI合约" → `contract list/fields/detail/update` +- "计分卡/scorecard/绩效看板" → `scorecard detail/entity-detail/update` +- "目标/OKR/我的目标/个人目标" → `user rules/objectives` +- "目标模板/模板管理" → `obj-template list/create-or-update` +- "周月报/周报统计/提交情况/跟催/迟交/未提交" → `report list-statistics/submit-detail` + +## 常用命令 + +```bash +dws agoal strategy list --scope-type DEPT --scope-id DEPT_ID --format json +dws agoal strategy detail --profile-id PROFILE_ID --format json +dws agoal contract list --scope-type PERSONAL --scope-id USER_ID --format json +dws agoal contract fields --format json +dws agoal contract detail --contract-id CONTRACT_ID --format json +dws agoal scorecard detail --selected-time "2026-01-01T00:00:00+08:00" --dept-id DEPT_ID --format json +dws agoal user rules --user-id USER_ID --format json +dws agoal user objectives --user-id USER_ID --rule-id RULE_ID --period-ids "period1,period2" --format json +dws agoal report list-statistics --format json +dws agoal report submit-detail --template-id TPL_ID --submit-state ON_TIME --format json +dws agoal obj-template list --keyword "业绩" --format json +``` + +## 安全规则 + +- 所有 update / create-or-update 命令都是覆盖逻辑:必须先用对应 detail/list 查询完整数据,在原数据基础上修改后再传入。 +- 写入类命令执行前必须展示变更摘要并等待用户确认。 +- `--selected-time` / `--query-date` 接受 ISO-8601 字符串。 +- `--period-ids` 为逗号分隔字符串。 diff --git a/skills/multi/dingtalk-aisearch/SKILL.md b/skills/multi/dingtalk-aisearch/SKILL.md index 298a9c69..65a43d72 100644 --- a/skills/multi/dingtalk-aisearch/SKILL.md +++ b/skills/multi/dingtalk-aisearch/SKILL.md @@ -12,7 +12,7 @@ metadata: # 钉钉 AI 搜问(搜人)Skill -> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。20 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 +> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。22 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 > **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH. diff --git a/skills/multi/dingtalk-aitable/SKILL.md b/skills/multi/dingtalk-aitable/SKILL.md index 687d149f..1dac92c3 100644 --- a/skills/multi/dingtalk-aitable/SKILL.md +++ b/skills/multi/dingtalk-aitable/SKILL.md @@ -12,7 +12,7 @@ metadata: # 钉钉 AI 表格 Skill -> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。20 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 +> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。22 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 > **PREREQUISITE:** Read the root `dws` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH. diff --git a/skills/multi/dingtalk-attendance/SKILL.md b/skills/multi/dingtalk-attendance/SKILL.md index 6c461421..556c9f18 100644 --- a/skills/multi/dingtalk-attendance/SKILL.md +++ b/skills/multi/dingtalk-attendance/SKILL.md @@ -12,7 +12,7 @@ metadata: # 钉钉考勤 Skill -> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。20 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 +> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。22 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 > **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH. diff --git a/skills/multi/dingtalk-calendar/SKILL.md b/skills/multi/dingtalk-calendar/SKILL.md index 1348e3dd..18980c3f 100644 --- a/skills/multi/dingtalk-calendar/SKILL.md +++ b/skills/multi/dingtalk-calendar/SKILL.md @@ -12,7 +12,7 @@ metadata: # 钉钉日历 Skill -> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。20 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 +> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。22 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 > **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH. diff --git a/skills/multi/dingtalk-chat/SKILL.md b/skills/multi/dingtalk-chat/SKILL.md index d0ad4621..5c746d91 100644 --- a/skills/multi/dingtalk-chat/SKILL.md +++ b/skills/multi/dingtalk-chat/SKILL.md @@ -12,7 +12,7 @@ metadata: # 钉钉群聊 / 消息 Skill -> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。20 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 +> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。22 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 > **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH. diff --git a/skills/multi/dingtalk-chat/references/chat.md b/skills/multi/dingtalk-chat/references/chat.md index 2595b4e8..cf38a771 100644 --- a/skills/multi/dingtalk-chat/references/chat.md +++ b/skills/multi/dingtalk-chat/references/chat.md @@ -140,6 +140,26 @@ Flags: --expires-seconds int64 链接有效期(秒),0 表示永久有效,不传使用服务端默认值 ``` +#### 分享群聊链接到会话 — 将指定群的邀请链接分享到另一个会话或单聊用户 + +`--target` 和 `--receiver` 二选一:`--target` 指定目标会话,`--receiver` 指定单聊用户。 + +``` +Usage: + dws chat group share-invite [flags] +Example: + dws chat group share-invite --source <被分享群openConversationId> --target <目标会话openConversationId> + dws chat group share-invite --source <被分享群openConversationId> --receiver <接收者openDingTalkId> + dws chat group share-invite --source --target --expires-seconds 86400 + # 查询群 ID: dws chat search --query "群名" +Flags: + --source string 被分享群的 openConversationId (必填) + --target string 接收分享消息的会话 openConversationId(与 --receiver 二选一) + --receiver string 接收分享消息的单聊用户 openDingTalkId(与 --target 二选一) + --expires-seconds int64 链接有效期(秒),0 表示永久有效,不传使用服务端默认值 + --uuid string 消息幂等键(可选) +``` + #### 退出群聊 — 当前用户退出指定群聊 ``` Usage: @@ -274,6 +294,65 @@ Flags: - 不传 --role 时返回群主 + 管理员的所有群 ``` +### group notice (群公告管理) + +#### 发布群公告 — 在指定群聊中发布群公告,支持 Markdown、定时发布 + +``` +Usage: + dws chat group notice create [flags] +Example: + dws chat group notice create --group --content "今晚 22 点系统维护,请提前保存工作内容" + dws chat group notice create --group --content "# 重要通知\n请大家查收" --sticky --send-ding + dws chat group notice create --group --content "明早九点例会" --run-at "2026-07-03T09:00:00+08:00" +Flags: + --group string 群聊 openConversationId (必填) + --content string 公告正文,Markdown 格式 (必填) + --sticky 是否吊顶置顶(默认 false) + --send-ding 是否发 DING 提醒(默认 false) + --run-at string 定时发布时间 ISO-8601(传入则定时发布) +``` + +#### 修改群公告 — 整体替换指定群公告的内容 +``` +Usage: + dws chat group notice edit [flags] +Example: + dws chat group notice edit --group --notice-id --content "更新后的公告内容" +Flags: + --group string 群聊 openConversationId (必填) + --notice-id string 群公告 dataId (必填) + --content string 公告新正文,Markdown 格式 (必填) + --sticky 是否吊顶置顶 + --send-ding 是否发 DING 提醒 +``` + +#### 查看群公告详情 +``` +Usage: + dws chat group notice get [flags] +Example: + dws chat group notice get --group --notice-id +Flags: + --group string 群聊 openConversationId (必填) + --notice-id string 群公告 dataId (必填) +``` + +#### 查看群公告列表 +``` +Usage: + dws chat group notice list [flags] +Example: + dws chat group notice list --group + dws chat group notice list --group --limit 20 --cursor + dws chat group notice list --group --scheduled +Flags: + --group string 群聊 openConversationId (必填) + --limit int 每页返回数量(默认 10,最大 100) + --cursor string 分页游标 + --scheduled 是否查询定时公告列表 +``` + ### group-role (群身份管理) #### 查看群身份列表 — 拉取指定群聊的自定义群身份列表 @@ -959,6 +1038,20 @@ Flags: - 如果已有合适的表情,无需创建新的 ``` +#### 批量拉取消息的表情回复和文字回复 + +根据消息 ID 列表批量查询消息的表情回复和文字回复信息。 + +``` +Usage: + dws chat message list-emotion-replies [flags] +Example: + dws chat message list-emotion-replies --msg-ids msgId1,msgId2,msgId3 + # 消息 ID 可通过 dws chat message list 获取 +Flags: + --msg-ids string 消息 ID 列表,逗号分隔 (必填) +``` + ### list-top-conversations (置顶会话) #### 拉取置顶会话列表 @@ -1274,6 +1367,21 @@ Flags: --title string 分组名称 (必填) ``` +#### 创建智能会话分组 — 可指定群名称关键词和群内成员作为匹配规则 +``` +Usage: + dws chat category create-smart [flags] +Example: + dws chat category create-smart --name "工作群" + dws chat category create-smart --name "项目组" --keywords "项目,开发" + dws chat category create-smart --name "团队群" --members openDingTalkId1,openDingTalkId2 + dws chat category create-smart --name "重点群" --keywords "重点" --members openDingTalkId1 +Flags: + --name string 分组名称 (必填) + --keywords string 群名称关键词列表,逗号分隔(可选) + --members string 群内成员 openDingTalkId 列表,逗号分隔(可选) +``` + #### 删除会话分组 ``` Usage: @@ -1323,6 +1431,23 @@ Flags: --category-ids string 目标分组 ID 列表,逗号分隔 (必填) ``` +### text (文本内容处理) + +#### 翻译文本内容 +``` +Usage: + dws chat text translate [flags] +Example: + dws chat text translate --query "你好世界" --to en_US + dws chat text translate --query "Hello World" --to zh_CN + dws chat text translate --query "Bonjour" --to ja_JP +Flags: + --query string 待翻译的文本内容 (必填) + --to string 目标语言代码 (必填) +``` + +支持的目标语言代码:`en_US`, `zh_CN`, `zh_TW`, `zh_HK`, `ja_JP`, `ko_KR`, `vi_VN`, `th_TH`, `id_ID`, `ms_MY`, `es_419`, `fr_FR`, `pt_BR`, `tr_TR`, `ru_RU`, `de_DE`, `hi_IN`, `hu_HU`, `pl_PL`, `sv_SE`, `fi_FI`, `cs_CZ`, `ar_SA`, `tl_PH`, `he_IL`, `nl_NL`, `lo_LA`, `it_IT`。 + ### mute (会话免打扰) #### 会话消息免打扰 — 开启或关闭会话消息免打扰(支持单聊和群聊) @@ -1644,6 +1769,7 @@ Flags: 用户说"查看会话分组/自定义分组" → `chat category list` 用户说"某个分组下的会话/分组会话列表" → `chat category list-conversations` 用户说"新建会话分组/创建分组" → `chat category create` +用户说"创建智能分组/新建智能会话分组" → `chat category create-smart` 用户说"删除会话分组" → `chat category delete` 用户说"重命名分组/改分组名" → `chat category rename` 用户说"把会话加入分组/会话归类到分组" → `chat category add-conv` @@ -1658,7 +1784,10 @@ Flags: 用户说"查询某人的群身份/某人在群里有什么身份" → `chat group-role query-user` 用户说"转让群主/换群主/群主转让" → `chat group transfer-owner` 用户说"群邀请链接/入群链接/加群链接" → `chat group invite-url` +用户说"分享群链接/把群分享给某人/群链接发到某群" → `chat group share-invite` +用户说"群公告/发布公告/修改公告/查看公告/定时公告" → `chat group notice create/edit/get/list` 用户说"批量查消息/按ID查消息/根据消息ID查" → `chat message list-by-ids` +用户说"批量查消息回复/表情回复/文字回复/消息回应列表" → `chat message list-emotion-replies` 用户说"emoji回应/表情回应/给消息加表情" → `chat message add-emoji` 用户说"取消emoji回应/移除表情回应" → `chat message remove-emoji` 用户说"文字表情回应/添加文字表情" → `chat message add-text-emotion` @@ -1697,6 +1826,7 @@ Flags: 用户说"全员禁言/群禁言/解除禁言" → `chat group-mute` 用户说"禁言某人/指定成员禁言/解除某人禁言" → `chat group-mute-member` 用户说"设管理员/取消管理员/设置群管理员" → `chat group set-admin` +用户说"翻译文本/把这段话翻译成英文/翻译成中文" → `chat text translate` 关键区分: - `chat search` — 搜**群/会话名**返回 `openConversationId`,**不**搜消息内容;要搜消息内容请用 `chat message search-advanced`(首选)/ `chat message search` / `list-by-sender` / `list-all`,**勿混淆** @@ -1721,13 +1851,17 @@ Flags: - `chat message search-advanced` — 多维度搜索消息(支持关键词、发送者、@我、@指定人、多个会话等维度组合,与 `search` 的区别:`search` 仅支持关键词且必填,`search-advanced` 所有参数均可选) - `chat message list-by-ids` — 根据消息 ID 批量查询消息(最多 50 条) - `chat message add-emoji` / `remove-emoji` — 对消息添加/移除 emoji 表情回应 +- `chat message list-emotion-replies` — 批量拉取消息的表情回复和文字回复 - `chat message add-text-emotion` / `remove-text-emotion` — 对消息添加/移除文字表情回应 - `chat message create-text-emotion` — 创建文字表情模板,返回 emotionId 供 add-text-emotion 使用 - `chat category list` — 获取用户自定义会话分组列表 - `chat category list-conversations` — 拉取指定分组下的会话列表 +- `chat category create-smart` — 创建智能会话分组(可指定群名称关键词和成员作为匹配规则) - `chat mute` — 开启/关闭会话消息免打扰(默认开启,--off 关闭) - `chat group transfer-owner` — 转让群主 - `chat group invite-url` — 获取群邀请链接 +- `chat group share-invite` — 分享群聊链接到会话 +- `chat group notice create/edit/get/list` — 群公告发布、修改、详情、列表 - `chat message reply` — 引用回复消息(在群聊中引用某条消息并回复文字) - `chat message forward` — 转发单条消息(将一条消息从源会话转发到目标会话) - `chat set-top` — 设置/取消会话置顶(默认置顶,--off 取消) diff --git a/skills/multi/dingtalk-contact/SKILL.md b/skills/multi/dingtalk-contact/SKILL.md index 2008668d..40c0bd0d 100644 --- a/skills/multi/dingtalk-contact/SKILL.md +++ b/skills/multi/dingtalk-contact/SKILL.md @@ -12,7 +12,7 @@ metadata: # 钉钉通讯录 Skill -> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。20 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 +> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。22 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 > **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH. diff --git a/skills/multi/dingtalk-devdoc/SKILL.md b/skills/multi/dingtalk-devdoc/SKILL.md index 39d5a0ee..c380da8b 100644 --- a/skills/multi/dingtalk-devdoc/SKILL.md +++ b/skills/multi/dingtalk-devdoc/SKILL.md @@ -12,7 +12,7 @@ metadata: # 钉钉开放平台文档 Skill -> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。20 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 +> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。22 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 > **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH. diff --git a/skills/multi/dingtalk-ding/SKILL.md b/skills/multi/dingtalk-ding/SKILL.md index 153e6b72..94fb1e08 100644 --- a/skills/multi/dingtalk-ding/SKILL.md +++ b/skills/multi/dingtalk-ding/SKILL.md @@ -12,7 +12,7 @@ metadata: # 钉钉 DING 紧急消息 Skill -> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。20 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 +> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。22 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 > **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH. diff --git a/skills/multi/dingtalk-doc/SKILL.md b/skills/multi/dingtalk-doc/SKILL.md index 3bba1780..be6be9cf 100644 --- a/skills/multi/dingtalk-doc/SKILL.md +++ b/skills/multi/dingtalk-doc/SKILL.md @@ -12,7 +12,7 @@ metadata: # 钉钉文档 Skill -> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。20 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 +> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。22 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 > **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH. diff --git a/skills/multi/dingtalk-doc/references/doc.md b/skills/multi/dingtalk-doc/references/doc.md index 73e3ce70..a817834b 100644 --- a/skills/multi/dingtalk-doc/references/doc.md +++ b/skills/multi/dingtalk-doc/references/doc.md @@ -131,6 +131,27 @@ Flags: --convert 是否转换为钉钉在线文档 ``` +### 导入本地文件为在线文档 + +详见 [doc/doc-import.md](./doc/doc-import.md)。 + +``` +Usage: + dws doc import [flags] + dws doc import get [flags] +Example: + dws doc import --file ./report.docx + dws doc import --file ./notes.md --folder + dws doc import --file ./data.xlsx --workspace + dws doc import get --task-id +Flags: + --file string 本地文件路径 (必填) + --folder string 目标文件夹 ID 或 URL + --workspace string 目标知识库 ID 或 URL + --name string 导入后文档名称 (默认取文件名) + --task-id string 导入任务 ID (import get 必填) +``` + ### 下载文件到本地(已迁移到 drive) > **弃用提示**:`dws doc download` 已迁移到 `dws drive download`(执行 `doc download` 会打印弃用警告)。下载已有文件(PDF/图片/附件等非在线文档)改用: @@ -618,6 +639,12 @@ Flags: - 上传 → `upload`(需本地文件路径) - 上传并转换 → `upload --convert` +用户说"导入文件/导入为在线文档/导入 Word/导入 Excel/导入 xmind/导入 Markdown/把本地文件转在线文档": +- 导入并转换为在线文档 → `doc import --file <本地路径>` +- 支持 docx/doc/xlsx/xls/md/txt/xmind/mark,文件大小不超过 20MB +- 如果用户指定知识库或文件夹,补充 `--workspace` 或 `--folder` +- 不要把本地文件内容先读出来再 `doc create/update`;应直接使用 `doc import` + 用户说"下载/导出/下载到本地/导出文档/导出为Word/导出为docx/把文档导出来": - **必须先判断目标文件类型**,再决定走 `doc export` 还是 `drive download`: - 在线文档(alidocs/adoc)→ **`doc export`**(内容级命令,格式转换后导出为 docx,未迁移) @@ -768,6 +795,20 @@ dws drive download --node --output ~/downloads/ # 如果是在线文档 (ALIDOC),用 doc export: dws doc export --node --output ~/downloads/ +# ── 工作流 5a: 导入本地文件为在线文档 ── + +# 导入到默认位置 +dws doc import --file ./report.docx --format json + +# 导入到指定文件夹 +dws doc import --file ./notes.md --folder --format json + +# 导入到知识库 +dws doc import --file ./data.xlsx --workspace --format json + +# 如果导入命令超时或中断,可用 import get 手动查询任务状态: +# dws doc import get --task-id + # ── 工作流 6: 上传附件并插入文档 ── # media insert 自动完成三步流程: @@ -882,6 +923,7 @@ dws doc export --node --output ./exported.docx | `drive list`(原 `doc list`,已弃用) | `nodes[].nodeId` / folder 类型 `nodeId` | read / info / update / block 的 --node;folder 用作 `--folder` | | `drive search`(原 `doc search`,已弃用) | 文档 `nodeId` / URL / `createTime` / `creatorUid` | read / info / update 的 --node;创建时间与创建者信息 | | `create` | `nodeId` | update / block 操作的 --node | +| `import` | `nodeId` / `documentUrl` / `documentName` / `documentType`;中断时提取 `taskId` | 后续 read / info / sheet 操作;中断后用 `doc import get --task-id` | | `drive mkdir`(原 `doc folder create`,已弃用) | `nodeId` | create 的 --folder | | `block list` | `blockId` | block insert 的 --ref-block, block update/delete 的 --block-id | | `comment list` | `commentList[].commentKey` | comment reply 的 --comment-key | diff --git a/skills/multi/dingtalk-doc/references/doc/doc-import.md b/skills/multi/dingtalk-doc/references/doc/doc-import.md new file mode 100644 index 00000000..bd6291de --- /dev/null +++ b/skills/multi/dingtalk-doc/references/doc/doc-import.md @@ -0,0 +1,53 @@ +# 本地文件导入为在线文档 (doc import) + +## 使用场景 + +用户说"导入 Word/Excel/Markdown/xmind 到钉钉文档"、"把本地文件转成在线文档"、"导入到知识库/文件夹"时,使用 `dws doc import`。 + +不要先读取文件内容再调用 `doc create` 或 `doc update`。`doc import` 会按文件格式走导入任务,保留更完整的原始结构。 + +## 命令 + +```bash +dws doc import --file ./report.docx --format json +dws doc import --file ./notes.md --folder --format json +dws doc import --file ./data.xlsx --workspace --format json +dws doc import --file ./draft.md --name "项目周报" --format json +``` + +```bash +dws doc import get --task-id --format json +``` + +## 参数 + +| 参数 | 说明 | +|------|------| +| `--file` | 本地文件路径,必填 | +| `--folder` | 目标文件夹 ID 或 URL,可选 | +| `--workspace` | 目标知识库 ID 或 URL,可选 | +| `--name` / `-n` | 导入后的文档名称;不传时使用文件名 | +| `--task-id` | `import get` 查询导入任务时必填 | + +支持格式:docx、doc、xlsx、xls、md、txt、xmind、mark。文件大小上限 20MB。 + +## 工作流 + +1. 确认本地文件存在且格式受支持。 +2. 如果用户指定目标知识库或文件夹,传 `--workspace` 或 `--folder`;未指定时导入到默认位置。 +3. 执行 `dws doc import --file ... --format json`。 +4. 正常情况下 CLI 会自动提交、上传并轮询导入任务。 +5. 如果命令超时或中断,从输出中提取 `taskId`,再执行 `dws doc import get --task-id --format json`。 + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `doc import` | `nodeId` / `documentUrl` / `documentName` / `documentType` | 后续 `doc read` / `doc info` / `sheet` 操作 | +| `doc import` 中断 | `taskId` | `doc import get --task-id` 查询任务状态 | + +## 注意事项 + +- `--folder` 和 `--workspace` 都是目标位置参数;用户明确指定文件夹时优先使用 `--folder`。 +- 导入 Excel 后通常得到在线表格,后续数据读取和编辑走 `dws sheet ...`。 +- 导入 Markdown 或 Word 后通常得到在线文档,后续内容读取和编辑走 `dws doc ...`。 diff --git a/skills/multi/dingtalk-drive/SKILL.md b/skills/multi/dingtalk-drive/SKILL.md index e5812381..4e0f348c 100644 --- a/skills/multi/dingtalk-drive/SKILL.md +++ b/skills/multi/dingtalk-drive/SKILL.md @@ -12,7 +12,7 @@ metadata: # 钉盘 Skill -> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。20 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 +> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。22 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 > **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH. diff --git a/skills/multi/dingtalk-live/SKILL.md b/skills/multi/dingtalk-live/SKILL.md index 422608eb..04536408 100644 --- a/skills/multi/dingtalk-live/SKILL.md +++ b/skills/multi/dingtalk-live/SKILL.md @@ -12,7 +12,7 @@ metadata: # 钉钉直播 Skill -> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。20 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 +> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。22 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 > **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH. diff --git a/skills/multi/dingtalk-mail/SKILL.md b/skills/multi/dingtalk-mail/SKILL.md index 2478996b..c7fd05c8 100644 --- a/skills/multi/dingtalk-mail/SKILL.md +++ b/skills/multi/dingtalk-mail/SKILL.md @@ -12,7 +12,7 @@ metadata: # 钉钉邮箱 Skill -> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。20 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 +> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。22 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 > **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH. diff --git a/skills/multi/dingtalk-mail/references/mail.md b/skills/multi/dingtalk-mail/references/mail.md index 14159224..bea9ebdb 100644 --- a/skills/multi/dingtalk-mail/references/mail.md +++ b/skills/multi/dingtalk-mail/references/mail.md @@ -5,6 +5,7 @@ | 命令 | 功能简述 | |------|----------| | `dws mail mailbox list` | 查询**当前用户自己**的可用邮箱列表 | +| `dws mail mailbox profile` | 获取用户邮箱详细信息(容量、别名等) | | `dws mail message list` | 列出指定文件夹中的邮件(默认收件箱) | | `dws mail message search` | 搜索邮件(KQL 语法,按主题/发件人/日期等) | | `dws mail message get` | 查看邮件完整内容(含正文) | @@ -15,7 +16,10 @@ | `dws mail message batch-move` | 批量移动邮件到指定文件夹 | | `dws mail message batch-delete` | 批量删除邮件 | | `dws mail message batch-update` | 批量修改邮件状态(标记已读/未读/添加标签/移除标签) | +| `dws mail message batch-get` | 批量获取邮件详情(最多 20 封) | | `dws mail message verify` | 根据 internetMessageId 查询邮件发送状态 | +| `dws mail sent-message recall` | 撤回已发送的邮件(仅支持同组织内未读邮件) | +| `dws mail sent-message recall-detail` | 查询邮件撤回进度 | | `dws mail draft create` | 创建草稿(保留在草稿箱,不发送) | | `dws mail draft update` | 更新草稿内容(保留在草稿箱,不发送) | | `dws mail draft send` | 发送草稿箱中已有的草稿 | @@ -46,6 +50,13 @@ | `dws mail contact update` | 更新个人邮件联系人信息 | | `dws mail contact batch-delete` | 批量删除个人邮件联系人 | | `dws mail auto-reply get` | 获取用户的自动回复配置 | +| `dws mail auto-reply update` | 更新/设置用户的自动回复配置 | +| `dws mail allow-list list` | 列出个人收信白名单 | +| `dws mail allow-list add` | 添加个人收信白名单 | +| `dws mail allow-list remove` | 移除个人收信白名单 | +| `dws mail block-list list` | 列出个人收信黑名单 | +| `dws mail block-list add` | 添加个人收信黑名单 | +| `dws mail block-list remove` | 移除个人收信黑名单 | | `dws mail rule list` | 列出个人收信规则 | | `dws mail rule create` | 创建个人收信规则 | | `dws mail rule update` | 更新个人收信规则 | @@ -93,6 +104,27 @@ Example: |------|------|------| | `emailAccounts` | `List[]` | 邮箱列表,每条包含 `email`(邮箱地址)、`orgName`(所属企业,个人邮箱为 null)、`type`(`ORG`=企业邮箱 / `PERSONAL`=个人邮箱) | +### 获取用户邮箱信息 + +``` +Usage: + dws mail mailbox profile [flags] +Example: + dws mail mailbox profile --email user@company.com +Flags: + --email string 用户的邮箱地址 (必填) +``` + +**返回字段:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `email` | string | 邮箱地址 | +| `emailAliases` | List[] | 邮件地址别名列表 | +| `name` / `nickname` / `displayName` | string | 用户名、昵称、显示名 | +| `mboxSize` / `mboxSizeUsed` | int64 | 邮箱容量与已用容量(字节) | +| `createdTime` / `modifiedTime` | string | 创建时间与更新时间 | + ### 查找他人邮箱地址(通讯录查人) > **这不是 `mailbox list`。** 当需要获取**某人**的邮箱地址时,必须走以下三路并发查询,取最先返回有效邮箱的结果。禁止臆测邮箱地址。 @@ -218,6 +250,21 @@ Flags: > **注意:** 即使邮件确有附件,`message get` 返回的 `message` 对象也**不含** `attachments`/`isRead`/`tags` 字段(实测 keys 仅上表所列这些)。要取附件请另用 `dws mail attachment list`。 +### 批量获取邮件详情 + +``` +Usage: + dws mail message batch-get [flags] +Example: + dws mail message batch-get --email user@company.com --ids , + dws mail message batch-get --email user@company.com --ids ,, +Flags: + --email string 邮件所属邮箱地址 (必填) + --ids string 要获取的邮件 ID 列表,逗号分隔,最多 20 个 (必填) +``` + +单次最多获取 20 封邮件。CLI 会逐个调用 `get_email_by_message_id`,并以 `messages` 数组聚合返回。 + ### 发送邮件 ``` Usage: @@ -935,6 +982,53 @@ Flags: | `failed` | 发送失败 | | `unknown` | 未知状态 | +### [危险] 撤回已发送的邮件 + +``` +Usage: + dws mail sent-message recall [flags] +Example: + dws mail sent-message recall --email user@company.com --id --subject "邮件主题" --yes +Flags: + --email string 发件人邮箱地址 (必填) + --id string 要撤回的邮件 ID (必填) + --subject string 邮件主题 (必填) + --yes 跳过确认提示,直接执行 (可选) +``` + +> CAUTION: 此命令会撤回已发送邮件。仅支持撤回同组织内未读邮件,执行前必须确认目标邮件、主题和影响范围。 + +**返回字段:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | string | 撤回任务 ID,可用于 `recall-detail` 查询进度 | +| `success` | boolean | 接口调用是否成功 | +| `errorCode` / `errorMsg` | string | 错误码和错误信息(仅失败时存在) | + +### 查询邮件撤回进度 + +``` +Usage: + dws mail sent-message recall-detail [flags] +Example: + dws mail sent-message recall-detail --email user@company.com --id +Flags: + --email string 用户的邮箱地址 (必填) + --id string 撤回任务 ID (必填),由 recall 命令返回 +``` + +根据撤回任务 ID 查询邮件撤回进度。任务状态包括 `UNINITED` / `SUBMITTED` / `RUNNING` / `FINISHED` / `CANCELED` / `FAILED`。 + +**返回字段:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | string | 撤回任务 ID | +| `status` | string | 任务状态 | +| `totalCount` / `succeededCount` / `failedCount` | int | 总数、成功数、失败数 | +| `details` | List[] | 每封邮件的撤回结果 | + ### 创建草稿 ``` Usage: @@ -1220,6 +1314,61 @@ Flags: | `scope` | string | 回复范围: "contact"(仅联系人) 或 "all"(所有人) | | `content` | string | 自动回复内容 | +### 更新自动回复配置 + +更新或设置用户的邮件自动回复配置。建议先通过 `auto-reply get` 获取当前配置,再传入需要修改的字段值。 + +``` +Usage: + dws mail auto-reply update [flags] +Example: + dws mail auto-reply update --email user@company.com --enabled true \ + --start "2026/07/01 09:00:00 +0800" --end "2026/07/07 18:00:00 +0800" \ + --scope all --content "出差中,请稍后联系" + dws mail auto-reply update --email user@company.com --enabled false \ + --start "2026/07/01 09:00:00 +0800" --end "2026/07/07 18:00:00 +0800" \ + --scope all --content "已关闭自动回复" +Flags: + --email string 用户的邮箱地址 (必填) + --enabled string 是否启用自动回复: true/false (必填) + --start string 自动回复开始时间,格式: YYYY/MM/DD HH:MM:SS +ZZZZ (必填) + --end string 自动回复结束时间,格式: YYYY/MM/DD HH:MM:SS +ZZZZ (必填) + --scope string 回复范围: contact(仅联系人)/all(所有人) (必填) + --content string 自动回复内容 (必填) +``` + +### 个人收信白名单管理 + +``` +Usage: + dws mail allow-list list [flags] + dws mail allow-list add [flags] + dws mail allow-list remove [flags] +Examples: + dws mail allow-list list --email user@company.com + dws mail allow-list add --email user@company.com --entries a@b.com,@example.com + dws mail allow-list remove --email user@company.com --entries a@b.com,@example.com +Flags: + --email string 用户的邮箱地址 (必填) + --entries string 逗号分隔的地址列表,支持邮件地址或 @domain.com 域名(add/remove 必填) +``` + +### 个人收信黑名单管理 + +``` +Usage: + dws mail block-list list [flags] + dws mail block-list add [flags] + dws mail block-list remove [flags] +Examples: + dws mail block-list list --email user@company.com + dws mail block-list add --email user@company.com --entries spam@bad.com,@junk.com + dws mail block-list remove --email user@company.com --entries spam@bad.com,@junk.com +Flags: + --email string 用户的邮箱地址 (必填) + --entries string 逗号分隔的地址列表,支持邮件地址或 @domain.com 域名(add/remove 必填) +``` + ### 收信规则管理 #### 列出收信规则 @@ -1473,9 +1622,11 @@ Flags: ## 意图判断 用户说"我的邮箱/邮箱地址" → `mailbox list`(**仅限查询自己的邮箱,不能查他人**) +用户说"邮箱详情/邮箱容量/邮箱别名/邮箱 profile" → `mailbox profile --email <邮箱>` 用户说"获取/查找/得到 某人的邮箱地址" → **不是 `mailbox list`**,走三路并发查询流程(见「查找他人邮箱地址」章节) 用户说"找邮件/搜邮件/查邮件" → `message search` 用户说"看邮件/打开邮件/邮件内容" → 先 `message search` 获取 messageId,再 `message get` +用户说"批量查看邮件详情/批量打开这些邮件/按多个邮件ID取详情" → `message batch-get --ids `(单次最多 20 个) 用户说"发邮件/写邮件" → 先 `mailbox list` 获取发件地址,再 `message send` 用户说“给(某人名字)发邮件” / “查询某人发给我的邮件” / “查询发给某人的邮件” / 任何涉及按人名查找邮箱的场景 → **第一步**:并发同时发起以下三路查询,取最先返回有效邮箱的结果;若三路均无有效邮箱,ask_human 请用户提供,禁止臆测: @@ -1502,6 +1653,11 @@ Flags: 用户说"删除会话/把会话放入已删除/批量删除会话" → 单条用 `thread trash`,多条用 `thread batch-trash`;传入的是会话 ID,不是邮件 ID 用户说"批量标记邮件已读/未读/给邮件加标签/移除邮件标签" → `message batch-update --action markRead/markUnread/addTags/removeTags`(操作对象是邮件 ID;会话级操作用 `thread update/batch-update`) 用户说"邮件发出去了吗/查邮件发送状态/确认邮件是否发送成功/邮件投递结果" → 用发送类命令返回的 `internetMessageId`,调用 `message verify` 查询 `sendStatus` +用户说"撤回邮件/撤回已发送邮件" → `sent-message recall --email <邮箱> --id --subject "<主题>" --yes`(危险操作,先确认) +用户说"查询邮件撤回进度/撤回任务状态" → `sent-message recall-detail --email <邮箱> --id ` +用户说"设置/更新/关闭自动回复" → `auto-reply update`(所有字段必填,先 `auto-reply get` 获取当前配置) +用户说"收信白名单/添加白名单/移除白名单" → `allow-list list/add/remove` +用户说"收信黑名单/添加黑名单/移除黑名单" → `block-list list/add/remove` 用户说"查看会话/获取会话/看这封邮件的会话" → 先 `message search` 或 `message get` 获取邮件中的 `conversationId`,再 `thread get` 用户说"搜索/查找/联系 邮箱用户/联系人/某人的邮箱地址" → `user search`(搜索通讯录人员,不是搜邮件内容) 用户说"发送草稿/把草稿发出去/发这封草稿" → 先 `message search --query "folderId:5"` 找到草稿 messageId,再 `draft send` @@ -1618,7 +1774,9 @@ dws mail thread get --email user@company.com --id --select mess | 操作 | 从返回中提取 | 用于 | |------|-------------|------| | `mailbox list` | 邮箱地址 | message search/get/send/thread get 的 --email/--from | +| `mailbox profile` | `emailAliases` / `mboxSize` / `mboxSizeUsed` | 展示邮箱别名与容量信息 | | `message search` | `messageId` | message get 的 --id | +| `message search` | `messageId` | message batch-get 的 --ids | | `message search` | `conversationId` | thread get 的 --id | | `message search` | `messageId` | attachment list 的 --id | | `attachment list` | `attachments[].id` / `attachments[].name` | attachment download 的 --attachment-id / --name | @@ -1633,6 +1791,8 @@ dws mail thread get --email user@company.com --id --select mess | `aisearch person` → `contact user get` / `contact user search` / `mail user search` | 用户邮箱 (orgAuthEmail / email) | message send 的 --to/--cc(三路并发,取先到结果) | | `user search` | 用户邮箱 (email) | message send 的 --to/--cc | | `message send` / `draft send` / `message reply` / `message reply-all` / `message forward` | `internetMessageId` | `message verify` 的 --internet-message-id | +| `sent-message recall` | `id` | sent-message recall-detail 的 --id | +| `auto-reply get` | `enabled` / `startTime` / `endTime` / `scope` / `content` | auto-reply update 的当前配置基线 | ## 注意事项 diff --git a/skills/multi/dingtalk-minutes/SKILL.md b/skills/multi/dingtalk-minutes/SKILL.md index 4401c096..9848f86d 100644 --- a/skills/multi/dingtalk-minutes/SKILL.md +++ b/skills/multi/dingtalk-minutes/SKILL.md @@ -12,7 +12,7 @@ metadata: # 钉钉 AI 听记 Skill -> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。20 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 +> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。22 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 > **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH. diff --git a/skills/multi/dingtalk-oa/SKILL.md b/skills/multi/dingtalk-oa/SKILL.md index cfa7280e..56e52e93 100644 --- a/skills/multi/dingtalk-oa/SKILL.md +++ b/skills/multi/dingtalk-oa/SKILL.md @@ -12,7 +12,7 @@ metadata: # 钉钉 OA 审批 Skill -> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。20 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 +> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。22 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 > **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH. diff --git a/skills/multi/dingtalk-report/SKILL.md b/skills/multi/dingtalk-report/SKILL.md index c71cd0a9..ee3400fd 100644 --- a/skills/multi/dingtalk-report/SKILL.md +++ b/skills/multi/dingtalk-report/SKILL.md @@ -12,7 +12,7 @@ metadata: # 钉钉日志 Skill -> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。20 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 +> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。22 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 > **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH. diff --git a/skills/multi/dingtalk-sheet/SKILL.md b/skills/multi/dingtalk-sheet/SKILL.md index 2aad7a62..48010a9a 100644 --- a/skills/multi/dingtalk-sheet/SKILL.md +++ b/skills/multi/dingtalk-sheet/SKILL.md @@ -12,7 +12,7 @@ metadata: # 钉钉电子表格 Skill -> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。20 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 +> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。22 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 > **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH. diff --git a/skills/multi/dingtalk-sheet/references/sheet.md b/skills/multi/dingtalk-sheet/references/sheet.md index 44c8fb55..c4731a22 100644 --- a/skills/multi/dingtalk-sheet/references/sheet.md +++ b/skills/multi/dingtalk-sheet/references/sheet.md @@ -108,6 +108,8 @@ dws sheet filter-view --help | `sheet update-dimension` | 更新行/列属性(显隐/行高/列宽) | | `sheet move-dimension` | 移动行或列到指定位置 | | `sheet add-dimension` | 在末尾追加空行或空列 | +| `sheet group-dimension` | 创建行/列分组 | +| `sheet ungroup-dimension` | 取消行/列分组 | | `sheet media-upload` | 上传附件到表格 | | `sheet write-image` | 上传图片并写入单元格 | | `sheet create-float-image` | 创建浮动图片 | @@ -156,7 +158,7 @@ dws sheet filter-view --help | 读取数据 | [sheet/sheet-read-data.md](./sheet/sheet-read-data.md) | range read / csv-get | | 区域操作 | [sheet/sheet-range-operations.md](./sheet/sheet-range-operations.md) | range clear / sort / fill / copy-to / move-to | | 批量操作 | [sheet/sheet-batch-operations.md](./sheet/sheet-batch-operations.md) | range batch-clear / batch-update | -| 行列操作 | [sheet/sheet-dimension-operations.md](./sheet/sheet-dimension-operations.md) | insert / delete / update / move / add-dimension | +| 行列操作 | [sheet/sheet-dimension-operations.md](./sheet/sheet-dimension-operations.md) | insert / delete / update / move / add-dimension / group-dimension / ungroup-dimension | | 样式与合并 | [sheet/sheet-style-format.md](./sheet/sheet-style-format.md) | range set-style / batch-set-style / merge-cells / unmerge-cells | | 条件格式 | [sheet/sheet-conditional-format.md](./sheet/sheet-conditional-format.md) | cond-format list / create / update / delete | | 浮动图表 | [sheet/sheet-chart.md](./sheet/sheet-chart.md) | chart list / create / update / delete | @@ -254,6 +256,11 @@ dws sheet filter-view --help - 注意与 `append`(追加数据行)区分:`add-dimension` 追加的是空行/空列,`append` 追加的是带数据的行 - 请勿用 `range update` 写空数据来模拟追加,`add-dimension` 直接扩展表格维度 +用户说"创建行分组/创建列分组/折叠几行/折叠几列/取消行列分组": +- 创建分组 → `group-dimension --range <整行或整列范围>` +- 取消分组 → `ungroup-dimension --range <整行或整列范围>` +- `--range` 只能传整行或整列范围,如 `3:7`、`C:F`、`Sheet1!3:7`,不要传 `A1:C5` + ### 单元格格式 用户说"设置样式/改颜色/设背景色/加粗/居中/换行/字体颜色/字号": @@ -1099,6 +1106,38 @@ Flags: 在工作表末尾追加指定数量的空行或空列。 +### 创建行或列分组 +``` +Usage: + dws sheet group-dimension [flags] +Example: + dws sheet group-dimension --node --sheet-id --range "3:7" + dws sheet group-dimension --node --sheet-id --range "C:F" --group-state fold + dws sheet group-dimension --node --sheet-id --range "Sheet1!3:7" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 整行/整列范围,如 "3:7" 或 "C:F" (必填) + --group-state string 创建后的分组状态: expand 或 fold,默认 expand +``` + +在工作表中为连续行或连续列创建分组。`--range` 必须是整行或整列范围,不支持普通单元格矩形范围。 + +### 取消行或列分组 +``` +Usage: + dws sheet ungroup-dimension [flags] +Example: + dws sheet ungroup-dimension --node --sheet-id --range "3:7" + dws sheet ungroup-dimension --node --sheet-id --range "C:F" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 整行/整列范围,如 "3:7" 或 "C:F" (必填) +``` + +取消指定连续行或连续列的分组。 + ### 取消合并单元格 ``` Usage: @@ -1903,6 +1942,8 @@ dws sheet export --node --output ./ | `replace` | `replaceCount` 被替换的单元格数量 | 确认替换结果 | | `move-dimension` | `sheetId` 工作表 ID | 确认操作完成 | | `add-dimension` | `sheetId` 工作表 ID | 确认操作完成 | +| `group-dimension` | `range` / `level` / `groupState` | 确认行列分组层级和展开状态 | +| `ungroup-dimension` | `range` / `level` | 确认行列分组已取消 | | `unmerge-cells` | `sheetId` 工作表 ID | 确认操作完成 | | `set-dropdown` | `range` 实际设置范围、`optionCount` 选项数量、`enableMultiSelect` 是否多选 | 确认下拉列表设置成功 | | `get-dropdown` | `hasDropdown` 是否存在下拉、`dataValidations` 下拉配置列表(含 `conditionValues`、`ranges`、`options`) | 查看已有下拉配置 | @@ -2032,6 +2073,9 @@ dws sheet export --node --output ./ - `add-dimension` vs `range update`:需要在末尾追加空行/空列时,必须使用 `add-dimension` 命令,禁止用 `range update` 写空数据来模拟追加效果 - `add-dimension` 追加的是空行/空列,与 `append`(追加带数据的行)不同 - `add-dimension` 的 `--length` 必须为正整数(>= 1),行列均不超过 5000 +- `group-dimension` / `ungroup-dimension` 的 `--range` 只接受整行或整列范围,如 `3:7`、`C:F`、`Sheet1!3:7`,不支持 `A1:C5` +- `group-dimension` 的 `--group-state` 只接受 `expand` 或 `fold`,默认 `expand` +- `batch-update` 支持 `group-dimension` / `ungroup-dimension`;需要创建后立即折叠时,优先使用独立 `group-dimension --group-state fold` - `unmerge-cells` 取消指定范围内所有合并单元格,使用 A1 表示法指定范围 - `set-dropdown` 在指定范围内设置下拉列表,`--options` 为 JSON 数组,每个元素包含 `value`(必填)和 `color`(可选,`#RRGGBB` 格式)。选项值不能包含英文逗号。`--multi-select` 启用多选模式。如果目标范围已存在下拉列表,会被新配置覆盖 - `get-dropdown` 查询指定范围内的下拉列表配置,返回 `dataValidations` 数组,相同选项的单元格聚合为一组。无下拉列表时 `hasDropdown` 为 false diff --git a/skills/multi/dingtalk-sheet/references/sheet/sheet-batch-operations.md b/skills/multi/dingtalk-sheet/references/sheet/sheet-batch-operations.md index ce79f719..d354af92 100644 --- a/skills/multi/dingtalk-sheet/references/sheet/sheet-batch-operations.md +++ b/skills/multi/dingtalk-sheet/references/sheet/sheet-batch-operations.md @@ -26,6 +26,7 @@ - 需要对**多个**不同区域执行 `merge-cells` / `unmerge-cells` 时(如按分组合并多列相同内容) - 需要对**多个**不同区域执行 `update-dimension` 时(如统一调整多列列宽或多行行高,含 hidden + pixelSize) - 需要先插入行列再写入数据时(`add-dimension` + `range update` / `csv-put`) +- 需要批量创建或取消行列分组时(`group-dimension` / `ungroup-dimension`) - 需要对多个区域执行不同写入操作时(多次 `range update` + `range clear` 等组合) 当同一工具需要对多个区域重复调用时,**推荐**改用 `batch-update` 合并为单次请求——`batch-update` 是原子提交(要么全成功要么整批回滚);逐个调用非原子,中途失败会留下半成品。 @@ -80,6 +81,7 @@ Flags: toolName 用 CLI 命令名,input 的键用 CLI flag 名去掉 --: range clear / range update / merge-cells / unmerge-cells / update-dimension range fill / range copy-to / add-dimension / delete-dimension / move-dimension + group-dimension / ungroup-dimension set-dropdown / delete-dropdown / csv-put / delete-float-image Notes: - 默认严格事务模式:任一子操作失败 → 整批回滚到初始状态 @@ -177,6 +179,8 @@ Cause: The requested resource was not found by the identifier 'NonExistentSheet' | `add-dimension` | [sheet-dimension-operations](./sheet-dimension-operations.md) | | `delete-dimension` | [sheet-dimension-operations](./sheet-dimension-operations.md) | | `move-dimension` | [sheet-dimension-operations](./sheet-dimension-operations.md) | +| `group-dimension` | [sheet-dimension-operations](./sheet-dimension-operations.md) | +| `ungroup-dimension` | [sheet-dimension-operations](./sheet-dimension-operations.md) | | `set-dropdown` | [sheet-dropdown](./sheet-dropdown.md) | | `delete-dropdown` | [sheet-dropdown](./sheet-dropdown.md) | | `csv-put` | [sheet-write-data](./sheet-write-data.md) | @@ -230,6 +234,19 @@ Cause: The requested resource was not found by the identifier 'NonExistentSheet' ] ``` +### 批量创建行列分组 + +```jsonc +[ + {"toolName":"group-dimension", + "input":{"sheet-id":"...","range":"3:7","group-state":"expand"}}, + {"toolName":"group-dimension", + "input":{"sheet-id":"...","range":"C:F"}}, + {"toolName":"ungroup-dimension", + "input":{"sheet-id":"...","range":"10:12"}} +] +``` + ## 上下文传递 | 操作 | 从返回中提取 | 用于 | @@ -244,3 +261,5 @@ Cause: The requested resource was not found by the identifier 'NonExistentSheet' - ★ **需要组合多个不同写操作(清除+写入等)时,用 `batch-update`**:原子事务,任一操作失败则整批回滚,避免留下半成品 - ★ **批次完成后必须回读校验**:用 `range read` 或 `csv-get` 抽样回读受影响区域,至少校验 3-5 个代表性单元格(首 / 中 / 末),与预期值对照 - ★ **`batch-update` 不支持嵌套**:`--operations` 中的 `toolName` 必须是原子操作,不可再嵌套 `batch-update` +- `batch-update` 支持 `group-dimension` / `ungroup-dimension`;`input.range` 只接受整行或整列范围,如 `3:7`、`C:F` +- 需要创建后立即折叠分组时,优先使用独立 `dws sheet group-dimension --group-state fold` diff --git a/skills/multi/dingtalk-sheet/references/sheet/sheet-dimension-operations.md b/skills/multi/dingtalk-sheet/references/sheet/sheet-dimension-operations.md index 589c157e..afc2f5ac 100644 --- a/skills/multi/dingtalk-sheet/references/sheet/sheet-dimension-operations.md +++ b/skills/multi/dingtalk-sheet/references/sheet/sheet-dimension-operations.md @@ -26,6 +26,12 @@ - 注意与 `append`(追加数据行)区分:`add-dimension` 追加的是空行/空列,`append` 追加的是带数据的行 - 请勿用 `range update` 写空数据来模拟追加,`add-dimension` 直接扩展表格维度 +用户说"创建行分组/创建列分组/折叠几行/折叠几列/展开行列分组/取消行列分组": +- 创建分组 → `group-dimension --range <整行或整列范围>` +- 创建并折叠 → `group-dimension --group-state fold` +- 取消分组 → `ungroup-dimension --range <整行或整列范围>` +- `--range` 只能传整行或整列范围,如 `3:7`、`C:F`、`Sheet1!3:7` + **结构预检**:插入、删除、移动行列前,必须先执行 `dws sheet info --node --sheet-id --format json` 查看 `mergedRanges`。合并区域跨过操作位置时,行列变更可能导致表头/分组标题断裂、空白或错位;需要先向用户说明影响,必要时取消合并后操作,再按原模式重新 `merge-cells`。 ## 命令详细参考 @@ -172,6 +178,38 @@ Flags: 在工作表末尾追加指定数量的空行或空列。 +### 创建行或列分组 +``` +Usage: + dws sheet group-dimension [flags] +Example: + dws sheet group-dimension --node --sheet-id --range "3:7" + dws sheet group-dimension --node --sheet-id --range "C:F" --group-state fold + dws sheet group-dimension --node --sheet-id --range "Sheet1!3:7" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 整行/整列范围,如 "3:7" 或 "C:F" (必填) + --group-state string 创建后的分组状态: expand 或 fold,默认 expand +``` + +为连续行或连续列创建分组。`--range` 必须是整行或整列范围,不支持 `A1:C5` 这类普通单元格矩形范围。 + +### 取消行或列分组 +``` +Usage: + dws sheet ungroup-dimension [flags] +Example: + dws sheet ungroup-dimension --node --sheet-id --range "3:7" + dws sheet ungroup-dimension --node --sheet-id --range "C:F" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 整行/整列范围,如 "3:7" 或 "C:F" (必填) +``` + +取消指定连续行或连续列的分组。 + ## 核心工作流 ```bash @@ -244,6 +282,8 @@ dws sheet update-dimension --node --sheet-id \ | `update-dimension` | `a1Notation` 被更新区域范围、`hidden` 生效的显隐状态、`pixelSize` 生效的尺寸 | 确认更新结果 | | `move-dimension` | `sheetId` 工作表 ID | 确认操作完成 | | `add-dimension` | `sheetId` 工作表 ID | 确认操作完成 | +| `group-dimension` | `range` / `level` / `groupState` | 确认行列分组层级和展开状态 | +| `ungroup-dimension` | `range` / `level` | 确认行列分组已取消 | | `list` | 工作表的 `sheetId` | info / range read / range update / find 的 --sheet-id | ## 注意事项 @@ -273,3 +313,7 @@ dws sheet update-dimension --node --sheet-id \ - `add-dimension` vs `range update`:需要在末尾追加空行/空列时,必须使用 `add-dimension` 命令,禁止用 `range update` 写空数据来模拟追加效果 - `add-dimension` 追加的是空行/空列,与 `append`(追加带数据的行)不同 - `add-dimension` 的 `--length` 必须为正整数(>= 1),行列均不超过 5000 +- `group-dimension` / `ungroup-dimension` 的 `--range` 只接受整行或整列范围,如 `3:7`、`C:F`、`Sheet1!3:7`,不支持 `A1:C5` +- `group-dimension` 的 `--group-state` 只接受 `expand` 或 `fold`,默认 `expand` +- 分组操作返回 `level` 表示当前分组层级;不要把它误认为固定深度参数 +- `batch-update` 支持 `group-dimension` / `ungroup-dimension`;需要创建后立即折叠时,优先使用独立 `group-dimension --group-state fold` diff --git a/skills/multi/dingtalk-skill/SKILL.md b/skills/multi/dingtalk-skill/SKILL.md index 12fba005..048bc07d 100644 --- a/skills/multi/dingtalk-skill/SKILL.md +++ b/skills/multi/dingtalk-skill/SKILL.md @@ -12,7 +12,7 @@ metadata: # 悟空技能管理 Skill -> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。20 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 +> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。22 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 > **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH. diff --git a/skills/multi/dingtalk-todo/SKILL.md b/skills/multi/dingtalk-todo/SKILL.md index 9e850bfd..7417aeca 100644 --- a/skills/multi/dingtalk-todo/SKILL.md +++ b/skills/multi/dingtalk-todo/SKILL.md @@ -12,7 +12,7 @@ metadata: # 钉钉待办 Skill -> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。20 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 +> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。22 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 > **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH. diff --git a/skills/multi/dingtalk-wiki/SKILL.md b/skills/multi/dingtalk-wiki/SKILL.md index 7722500c..3a36097b 100644 --- a/skills/multi/dingtalk-wiki/SKILL.md +++ b/skills/multi/dingtalk-wiki/SKILL.md @@ -12,7 +12,7 @@ metadata: # 钉钉知识库 Skill -> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。20 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 +> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。22 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。 > **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH.