Compare commits

...
Author SHA1 Message Date
chichuan f48a707e04 Merge latest main into codex/sync-wukong-wiki-feed 2026-08-04 15:30:25 +08:00
chichuan 7cb0de1f29 test(wiki): register feed command in the interface baseline
Add wiki.feed and wiki.feed.list to the CLI interface baseline so the new
command enters the backwards-compatibility contract and a later change
cannot silently drop it. The wiki root entry gains feed in its command
list; the leaf records the reviewed flags including the hidden
cross-product aliases.

Only the wiki nodes are merged. `make update-interface-baseline` would
also fold in 90 unrelated nodes that main has accumulated for chat,
aitable, doc, drive, and sheet; catching those up belongs in a separate
maintenance change, not in this feature PR.
2026-08-04 15:28:50 +08:00
chichuan f8d1fb84c0 Merge branch 'main' into codex/sync-wukong-wiki-feed 2026-08-04 14:42:10 +08:00
chichuan d3692e7b6e docs(wiki): document knowledge base feed query
Mirror the dws-wukong Skill updates for `dws wiki feed list` across both
Skill layouts: command reference, intent routing, the feed workflow, and
the nextToken context-passing row. Also refresh the wiki capability
summaries so the feed query is discoverable from the product tables.
2026-08-04 11:42:27 +08:00
chichuan e2e855d12a feat(wiki): add knowledge base feed query command
Port the knowledge base activity feed capability from the internal
dws-wukong branch feat/pull_knowledge_base_dynamic-wiki (merged there as
58fd118c) to the open-source CLI as `dws wiki feed list`.

The command forwards to the native wiki MCP tool list_workspace_feeds,
mapping --workspace/--limit/--cursor/--exclude-file onto
workspaceId/maxResults/nextToken/excludeFile. Cross-product hidden
aliases come from RegisterCrossProductAliases rather than hand-written
flags, so --workspace-id/--page-token/--next-token/--page-size all
resolve.

Register the reviewed Schema inputs (MCP contract pinned from the live
wiki tools/list payload, CommandRegistry entry, safety and selection
hints, parameter bindings, surface completeness count) and regenerate the
Catalog and Agent metadata projections.
2026-08-04 11:42:15 +08:00
github-actions[bot] e40f5bc537 Merge pull request #854 from DingTalk-Real-AI/codex/fix-chat-download-url
fix(chat): restore download-media JSON contract
2026-08-04 10:50:25 +08:00
修雨 3210232876 Merge latest main into codex/fix-chat-download-url 2026-08-04 10:22:26 +08:00
github-actions[bot] 162a2eb0a7 chore: update formula for v1.0.56 [skip ci] 2026-08-04 02:16:23 +00:00
chichuan d3f62193e7 Merge pull request #859 from DingTalk-Real-AI/codex/changelog-v1.0.56
docs: seal v1.0.56 changelog
2026-08-04 10:05:35 +08:00
修雨 1a11c687ed Merge remote-tracking branch 'origin/main' into codex/fix-chat-download-url 2026-08-04 10:04:55 +08:00
修雨 dfed4ba37d Merge main into codex/fix-chat-download-url 2026-08-04 10:04:07 +08:00
chichuan f26df04679 docs: seal v1.0.56 changelog 2026-08-04 10:00:32 +08:00
github-actions[bot] d02b03436d chore: update beta formula for v1.0.56-beta.4 [skip ci] 2026-08-04 01:55:53 +00:00
chichuan bc7b96ba5f Merge pull request #858 from DingTalk-Real-AI/codex/changelog-v1.0.56-beta.4
docs: seal v1.0.56-beta.4 changelog
2026-08-04 09:46:33 +08:00
chichuan 9539c887f6 docs: seal v1.0.56-beta.4 changelog 2026-08-04 09:35:44 +08:00
github-actions[bot] 6607f44724 Merge pull request #852 from AlwaysLee/feat/center-protocol-transfer
feat: multipart download engine with checkpoint resume and credential refresh
2026-08-04 09:29:17 +08:00
半圭 837a96fe3d fix: show friendly message on Ctrl+C instead of internal error JSON
When user interrupts multipart download with Ctrl+C, display a helpful
message indicating checkpoint is saved and download can be resumed,
instead of returning an internal error with context.Canceled.
2026-08-04 08:58:17 +08:00
半圭 fdcd44f9e3 fix: handle Ctrl+C (SIGINT) gracefully during drive download
- Replace context.Background() with cmd.Context() in download and
  download-version commands so SIGINT propagates to download goroutines
- Enables graceful interruption of multipart downloads via Ctrl+C
2026-08-03 23:21:59 +08:00
半圭 34c0c86a59 feat: center protocol upload/download refactoring with multipart download
- Add multipart download engine (drive_transfer.go) with Range probe,
  resume support, and credential auto-refresh on 401/403
- Add --part-size, --parallel, --no-resume flags to drive download and
  download-version commands
- Replace httpGetFile with driveTransferDownload for chunked parallel
  downloads in download and download-version commands
- Replace uploadToDrive credential parsing with driveUploadPut
  (transparent header pass-through, retry on 401/403)
- Add typed httpStatusError for non-2xx HTTP responses in doc.go
- Add comprehensive unit tests (32 cases) for drive_transfer
- Update drive reference documentation with multipart download behavior
- Add E2E test for multipart download (auto-test/, gitignored)

CR: 28984991
2026-08-03 23:21:59 +08:00
修雨 b1f4a5d62a test(chat): add download-media CLI integration coverage 2026-08-03 21:50:17 +08:00
修雨 bf33ab622f fix(chat): restore download media JSON result 2026-08-03 21:50:17 +08:00
github-actions[bot] b2cbca2762 chore: update beta formula for v1.0.56-beta.3 [skip ci] 2026-08-03 12:55:19 +00:00
chichuan 9ce95db08e Merge pull request #855 from DingTalk-Real-AI/codex/changelog-v1.0.56-beta.3
docs: seal v1.0.56-beta.3 changelog
2026-08-03 20:39:44 +08:00
chichuan e99c20a0a1 docs: seal v1.0.56-beta.3 changelog 2026-08-03 20:34:17 +08:00
github-actions[bot] 96bfae079a Merge pull request #846 from wxianfeng/fix/event-unix-socket-tmpdir
fix(event): use secure Unix bus runtime directory
2026-08-03 16:04:41 +08:00
wxianfeng bb18cdba3b Merge upstream/main into fix/event-unix-socket-tmpdir 2026-08-03 15:45:38 +08:00
wxianfeng 015a1f85ca fix(event): satisfy platform coverage gate 2026-08-03 15:42:17 +08:00
github-actions[bot] 8854e0d1d4 Merge pull request #851 from abucraft/codex/aitable-workflow-docs
feat: add aitable workflow edit example command
2026-08-03 07:01:27 +00:00
镜玄 22862508b8 feat: add aitable workflow edit example command 2026-08-03 14:47:59 +08:00
wxianfeng 5459bcc524 fix(event): secure Unix bus runtime directory 2026-08-03 11:40:01 +08:00
wxianfeng 029c665029 fix(event): place Unix bus sockets in temp dir 2026-07-31 18:01:34 +08:00
51 changed files with 7819 additions and 146 deletions
+47
View File
@@ -6,6 +6,53 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th
## [Unreleased]
### Fixed
- **Chat media download JSON compatibility** — `dws chat message download-media --format json` once again returns a clean `{success, downloadUrl, output}` result after the file is saved, preserving the temporary URL and resolved local path without progress text corrupting JSON stdout.
## [1.0.56] - 2026-08-04
This stable release promotes the fully delivered `v1.0.56-beta.4` baseline.
It includes PR #852's resilient multipart Drive download implementation,
together with the v1.0.56 beta-line command, Schema, Skill, and runtime
improvements already validated through the prerelease channel.
### Added
- **Resilient multipart Drive downloads** (#852) — `drive download` and
`drive download-version` support parallel chunk transfer, Range probing,
fingerprint-validated checkpoint resume, automatic 401/403 credential
refresh, and graceful interruption with checkpoint preservation.
## [1.0.56-beta.4] - 2026-08-04
This beta adds PR #852 on top of v1.0.56-beta.3. It makes Drive downloads
resilient for large files through parallel transfer, validated resumable
checkpoints, and automatic credential refresh.
### Added
- **Multipart Drive downloads** (#852) — adds `--part-size`, `--parallel`, and
`--no-resume` to `drive download` and `drive download-version`. Files above
the part-size threshold use a Range probe and parallel chunks, resume from a
fingerprint-validated checkpoint, refresh credentials on 401/403, and keep
the checkpoint when Ctrl+C interrupts a transfer.
## [1.0.56-beta.3] - 2026-08-03
This beta adds PRs #846 and #851 on top of v1.0.56-beta.2. It adds a
service-provided Aitable workflow-editing reference command and makes local
event-bus IPC reliable on shared filesystems by placing Unix sockets in a
validated private runtime directory.
### Added
- **Aitable workflow editing reference** (#851) — adds `dws aitable workflow edit-example`, a parameter-free read command that returns the service-provided workflow editing documentation and `workflow-dsl/v1` examples through `aitable/edit_workflow_example`.
### Fixed
- **Event bus sockets on shared filesystems** (#846) — Unix event buses now place their local IPC socket in a private per-user runtime directory (`XDG_RUNTIME_DIR` when available, otherwise a `0700` per-UID directory under the system temporary directory) while retaining locks, metadata, logs, and subscription state in the configured Workdir. Listener and dial paths validate directory ownership and permissions before use. This prevents `dws event consume` from failing with `bind: errno 524` when `~/.dws` is hosted on NFS, CSI, FUSE, or another filesystem that does not support Unix Domain Sockets without exposing the socket directly in a shared `/tmp` root. When `XDG_RUNTIME_DIR` is unavailable, the per-UID directory name is deterministic: ownership validation prevents endpoint hijacking, but another local user can pre-create the directory to deny service; multi-user deployments should provide a private `XDG_RUNTIME_DIR`.
## [1.0.56-beta.2] - 2026-07-30
This beta adds PRs #831 and #835 on top of v1.0.56-beta.1. It separates
+11 -11
View File
@@ -1,33 +1,33 @@
class DingtalkWorkspaceCliBeta < Formula
desc "Automate DingTalk workspace tasks from the terminal (beta channel)"
homepage "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli"
version "1.0.56-beta.2"
version "1.0.56-beta.4"
license "Apache-2.0"
keg_only "it is the beta channel and conflicts with dingtalk-workspace-cli"
on_macos do
if Hardware::CPU.arm?
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.2/dws-darwin-arm64.tar.gz"
sha256 "19b52b5427dbf24acfeb1da16a93e6edfd0443389a778abbbfd381ff8f656139"
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.4/dws-darwin-arm64.tar.gz"
sha256 "f1f9b6394137edbd0b08d632aab34e92a0f3f81d80107a47de1bec9b384f0515"
else
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.2/dws-darwin-amd64.tar.gz"
sha256 "621da52d04f391234d160a0522d70c1fb2e36da59102ef201a9a3a11b706b3e8"
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.4/dws-darwin-amd64.tar.gz"
sha256 "cd3c64d20723c420e2490405d0bf8eecfd7e2b8fc352f63f23de5847a1d38f55"
end
end
on_linux do
if Hardware::CPU.arm?
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.2/dws-linux-arm64.tar.gz"
sha256 "4ba956463f4b583c1727f58026a6bc1fb23d27537083e3bafd541a05ab15b48b"
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.4/dws-linux-arm64.tar.gz"
sha256 "910918d88074534e680a2e320d3cb364ad092e96b9c422f9e75d11c9c0815dd8"
else
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.2/dws-linux-amd64.tar.gz"
sha256 "a8156ec5b89355faf8c08a65d9c088f89a7b411a418fb4b488f3d472efc79670"
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.4/dws-linux-amd64.tar.gz"
sha256 "172fe0d84443be953d0c6f2c2433540e4b972fbe7776cff1417ec9c73723552b"
end
end
resource "skills" do
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.2/dws-skills.zip"
sha256 "92c71fdeade88b3b76a00cb74ebd3223cc4110c7ee151d36d782537613129967"
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.4/dws-skills.zip"
sha256 "a3457befe858cbf3fe85848428b630bfd3a5f626256ed6b49415267948915152"
end
def install
+11 -11
View File
@@ -1,33 +1,33 @@
class DingtalkWorkspaceCli < Formula
desc "Automate DingTalk workspace tasks from the terminal"
homepage "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli"
version "1.0.55"
version "1.0.56"
license "Apache-2.0"
on_macos do
if Hardware::CPU.arm?
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.55/dws-darwin-arm64.tar.gz"
sha256 "dd753bbd051e5dd007cf433b8aa211c4a221dd73dfcb0b3783fa924d09f12351"
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56/dws-darwin-arm64.tar.gz"
sha256 "5c6003fe484aa36cc00820a574186652467b9d075f19c159cf807e57590256ba"
else
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.55/dws-darwin-amd64.tar.gz"
sha256 "f465eb7ac38a8a84eac4eb821fd15424bfc6f6245a60fa695ba97a639970dd77"
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56/dws-darwin-amd64.tar.gz"
sha256 "969b005a10682c2a1a828fa112165b5b0cd8ceeed8d22110ef7f39402cc36804"
end
end
on_linux do
if Hardware::CPU.arm?
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.55/dws-linux-arm64.tar.gz"
sha256 "5961be0fd551ec8e69b6fff2b1609f73486f7e6c3ffe8eb4bb99fa1ed691b401"
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56/dws-linux-arm64.tar.gz"
sha256 "530c5ea7ddc7de320d9c2471fbd33752a723d00c9665f49321c7580e8392c756"
else
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.55/dws-linux-amd64.tar.gz"
sha256 "051ba404a5f6a8fb15def0e0f5d9d273cf9d63f881df2fffe159f2c4ea3366e7"
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56/dws-linux-amd64.tar.gz"
sha256 "675fa42727ac9a549c6710b82e1980cd0f795363d71d5116a4e69771b7c5470e"
end
end
resource "skills" do
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.55/dws-skills.zip"
sha256 "bd35f674f184001f5a03c7b5fa6029ebcda54f0054e15cd608b5b5e213ce2d05"
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56/dws-skills.zip"
sha256 "3d57794e4660a089209ce3962571d16ca0d46141e973c9993257a301cce0e097"
end
def install
@@ -37075,6 +37075,265 @@
"用户明确要求停止某自动化工作流时"
]
},
"aitable workflow edit-example": {
"agent_summary": "获取 AI 表格工作流编辑文档与 workflow-dsl/v1 示例。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"实际创建工作流用 workflow create;修改已有工作流用 workflow update;查询已发布定义用 workflow get"
],
"confirmation": "not_required",
"effect": "read",
"effect_source": "agent-hint",
"examples": [
"dws aitable workflow edit-example"
],
"field_provenance": {
"agent_summary": {
"value": "获取 AI 表格工作流编辑文档与 workflow-dsl/v1 示例。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
"candidates": [
{
"value": "获取 AI 表格工作流编辑文档与 workflow-dsl/v1 示例。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。"
}
]
},
"availability": {
"value": "available",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"candidates": [
{
"value": "available",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry."
}
]
},
"avoid_when": {
"value": [
"实际创建工作流用 workflow create;修改已有工作流用 workflow update;查询已发布定义用 workflow get"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
"candidates": [
{
"value": [
"实际创建工作流用 workflow create;修改已有工作流用 workflow update;查询已发布定义用 workflow get"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。"
}
]
},
"confirmation": {
"value": "not_required",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"candidates": [
{
"value": "not_required",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry."
}
]
},
"effect": {
"value": "read",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"candidates": [
{
"value": "read",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry."
}
]
},
"examples": {
"value": [
"dws aitable workflow edit-example"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
"candidates": [
{
"value": [
"dws aitable workflow edit-example"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。"
}
]
},
"idempotency": {
"value": "idempotent",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"candidates": [
{
"value": "idempotent",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry."
}
]
},
"interface_mode": {
"value": "composite",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"candidates": [
{
"value": "composite",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry."
}
]
},
"interface_reason": {
"value": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"candidates": [
{
"value": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry."
}
]
},
"interface_ref": {
"value": null,
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "interface_disposition_matrix",
"review_reason": "final interface mode composite forbids a direct MCP interface_ref",
"candidates": [
{
"value": null,
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "final interface mode composite forbids a direct MCP interface_ref"
}
]
},
"reviewed": {
"value": true,
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"candidates": [
{
"value": true,
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry."
},
{
"value": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"selected": false,
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。"
}
]
},
"risk": {
"value": "low",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"candidates": [
{
"value": "low",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry."
}
]
},
"use_when": {
"value": [
"创建或更新工作流前,需要确认最新 workflow-dsl/v1 结构、节点写法或完整示例时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
"candidates": [
{
"value": [
"创建或更新工作流前,需要确认最新 workflow-dsl/v1 结构、节点写法或完整示例时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。"
}
]
}
},
"idempotency": "idempotent",
"interface_mode": "composite",
"interface_reason": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
"reviewed": true,
"risk": "low",
"source_refs": [
"cobra-help:dws aitable workflow edit-example --help",
"internal/cli/schema_command_registry.json#aitable.workflow_edit_example",
"internal/cli/schema_hints/metadata/aitable.json",
"internal/cli/schema_hints/selection/aitable.json",
"mcp-contract:aitable/edit_workflow_example",
"skills/mono/references/products/aitable/aitable-workflow.md"
],
"use_when": [
"创建或更新工作流前,需要确认最新 workflow-dsl/v1 结构、节点写法或完整示例时"
]
},
"aitable workflow enable": {
"agent_summary": "启用工作流。",
"agent_summary_source": "dws-agent-selection/aitable",
@@ -1,17 +1,17 @@
{
"version": 1,
"source_hash": "sha256:670ca810a83bf2aa6f387a18c3af746393d5ea9de24570cbcebbcf994eb7b613",
"surface_hash": "sha256:60eee8e2f37d6d9d60689efce85082798eb9ad38b7ba7c0b471c3de676a85a16",
"source_hash": "sha256:1f76aa71b00f7b807b863ced6e98aca76dc4bda65107ea2a6c67eb4ec629db43",
"surface_hash": "sha256:89bc72d0a5ff03df634d733dc8b71c3010572524542538552c33c657ae012562",
"coverage": {
"surface_products": 26,
"products_with_metadata": 26,
"surface_tools": 845,
"tools_with_metadata": 845,
"tools_with_agent_summary": 845,
"tools_with_use_when": 845,
"tools_with_avoid_when": 845,
"tools_with_examples": 845,
"tools_with_interface_mode": 845,
"surface_tools": 847,
"tools_with_metadata": 847,
"tools_with_agent_summary": 847,
"tools_with_use_when": 847,
"tools_with_avoid_when": 847,
"tools_with_examples": 847,
"tools_with_interface_mode": 847,
"unmatched_skill_tools": 122,
"unreviewed_skill_tools": 11
},
@@ -259,6 +259,265 @@
"当你只记得知识库名称的部分关键词、想快速按名称定位某个知识库时使用;输入关键词返回匹配的知识库列表,比逐页 +space-list 更快找到目标 workspaceId。"
]
},
"wiki feed list": {
"agent_summary": "查询知识库的活动动态:谁在什么时间更新/上传/评论了哪些文档",
"agent_summary_source": "dws-agent-selection/wiki",
"availability": "available",
"avoid_when": [
"要看知识库当前有哪些节点用 node list;库内按关键词找文档用 node search",
"要读某篇文档正文用 doc read;跨库全局找文件用 drive search"
],
"confirmation": "not_required",
"effect": "read",
"effect_source": "command-verb",
"examples": [
"dws wiki feed list --workspace \u003cworkspaceId\u003e --format json",
"dws wiki feed list --workspace \u003cworkspaceId\u003e --limit 10 --format json"
],
"field_provenance": {
"agent_summary": {
"value": "查询知识库的活动动态:谁在什么时间更新/上传/评论了哪些文档",
"source": "internal/cli/schema_hints/selection/wiki.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:依据 wiki MCP 实时 tools/list 的 list_workspace_feeds description/inputSchema 与 Cobra Long 手写选型文案,并与 node list / node search / doc read / drive search 划清路由边界;不改变命令身份、参数契约或接口绑定。",
"candidates": [
{
"value": "查询知识库的活动动态:谁在什么时间更新/上传/评论了哪些文档",
"source": "internal/cli/schema_hints/selection/wiki.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工审阅:依据 wiki MCP 实时 tools/list 的 list_workspace_feeds description/inputSchema 与 Cobra Long 手写选型文案,并与 node list / node search / doc read / drive search 划清路由边界;不改变命令身份、参数契约或接口绑定。"
}
]
},
"availability": {
"value": "available",
"source": "internal/cli/schema_hints/metadata/wiki.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Live wiki MCP tools/list exposes list_workspace_feeds as a read-only knowledge base activity query. The reviewed CLI leaf forwards workspaceId/maxResults/nextToken/excludeFile to the native wiki server and adds no side effects, so it keeps the same read-only posture as the sibling wiki list commands.",
"candidates": [
{
"value": "available",
"source": "internal/cli/schema_hints/metadata/wiki.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Live wiki MCP tools/list exposes list_workspace_feeds as a read-only knowledge base activity query. The reviewed CLI leaf forwards workspaceId/maxResults/nextToken/excludeFile to the native wiki server and adds no side effects, so it keeps the same read-only posture as the sibling wiki list commands."
},
{
"value": "available",
"source": "internal/cli/schema_mcp_metadata.json#tools.wiki.list_workspace_feeds.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"avoid_when": {
"value": [
"要看知识库当前有哪些节点用 node list;库内按关键词找文档用 node search",
"要读某篇文档正文用 doc read;跨库全局找文件用 drive search"
],
"source": "internal/cli/schema_hints/selection/wiki.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:依据 wiki MCP 实时 tools/list 的 list_workspace_feeds description/inputSchema 与 Cobra Long 手写选型文案,并与 node list / node search / doc read / drive search 划清路由边界;不改变命令身份、参数契约或接口绑定。",
"candidates": [
{
"value": [
"要看知识库当前有哪些节点用 node list;库内按关键词找文档用 node search",
"要读某篇文档正文用 doc read;跨库全局找文件用 drive search"
],
"source": "internal/cli/schema_hints/selection/wiki.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工审阅:依据 wiki MCP 实时 tools/list 的 list_workspace_feeds description/inputSchema 与 Cobra Long 手写选型文案,并与 node list / node search / doc read / drive search 划清路由边界;不改变命令身份、参数契约或接口绑定。"
}
]
},
"confirmation": {
"value": "not_required",
"source": "risk-default",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "not_required",
"source": "risk-default",
"precedence": "inference_or_default",
"selected": true
}
]
},
"effect": {
"value": "read",
"source": "command-verb",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "read",
"source": "command-verb",
"precedence": "inference_or_default",
"selected": true
}
]
},
"examples": {
"value": [
"dws wiki feed list --workspace \u003cworkspaceId\u003e --format json",
"dws wiki feed list --workspace \u003cworkspaceId\u003e --limit 10 --format json"
],
"source": "internal/cli/schema_hints/selection/wiki.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:依据 wiki MCP 实时 tools/list 的 list_workspace_feeds description/inputSchema 与 Cobra Long 手写选型文案,并与 node list / node search / doc read / drive search 划清路由边界;不改变命令身份、参数契约或接口绑定。",
"candidates": [
{
"value": [
"dws wiki feed list --workspace \u003cworkspaceId\u003e --format json",
"dws wiki feed list --workspace \u003cworkspaceId\u003e --limit 10 --format json"
],
"source": "internal/cli/schema_hints/selection/wiki.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工审阅:依据 wiki MCP 实时 tools/list 的 list_workspace_feeds description/inputSchema 与 Cobra Long 手写选型文案,并与 node list / node search / doc read / drive search 划清路由边界;不改变命令身份、参数契约或接口绑定。"
}
]
},
"idempotency": {
"value": "idempotent",
"source": "effect-default",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "idempotent",
"source": "effect-default",
"precedence": "inference_or_default",
"selected": true
}
]
},
"interface_mode": {
"value": "mcp",
"source": "internal/cli/schema_hints/metadata/wiki.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Live wiki MCP tools/list exposes list_workspace_feeds as a read-only knowledge base activity query. The reviewed CLI leaf forwards workspaceId/maxResults/nextToken/excludeFile to the native wiki server and adds no side effects, so it keeps the same read-only posture as the sibling wiki list commands.",
"candidates": [
{
"value": "mcp",
"source": "internal/cli/schema_hints/metadata/wiki.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Live wiki MCP tools/list exposes list_workspace_feeds as a read-only knowledge base activity query. The reviewed CLI leaf forwards workspaceId/maxResults/nextToken/excludeFile to the native wiki server and adds no side effects, so it keeps the same read-only posture as the sibling wiki list commands."
},
{
"value": "mcp",
"source": "internal/cli/schema_mcp_metadata.json#tools.wiki.list_workspace_feeds.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"interface_ref": {
"value": "wiki.list_workspace_feeds",
"source": "internal/cli/schema_mcp_metadata.json#tools.wiki.list_workspace_feeds.interface_ref",
"precedence": "mcp_fallback",
"resolution": "highest_precedence",
"candidates": [
{
"value": "wiki.list_workspace_feeds",
"source": "internal/cli/schema_mcp_metadata.json#tools.wiki.list_workspace_feeds.interface_ref",
"precedence": "mcp_fallback",
"selected": true
}
]
},
"reviewed": {
"value": true,
"source": "internal/cli/schema_hints/metadata/wiki.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Live wiki MCP tools/list exposes list_workspace_feeds as a read-only knowledge base activity query. The reviewed CLI leaf forwards workspaceId/maxResults/nextToken/excludeFile to the native wiki server and adds no side effects, so it keeps the same read-only posture as the sibling wiki list commands.",
"candidates": [
{
"value": true,
"source": "internal/cli/schema_hints/metadata/wiki.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Live wiki MCP tools/list exposes list_workspace_feeds as a read-only knowledge base activity query. The reviewed CLI leaf forwards workspaceId/maxResults/nextToken/excludeFile to the native wiki server and adds no side effects, so it keeps the same read-only posture as the sibling wiki list commands."
},
{
"value": true,
"source": "internal/cli/schema_hints/selection/wiki.json",
"precedence": "reviewed_explicit",
"selected": false,
"review_reason": "人工审阅:依据 wiki MCP 实时 tools/list 的 list_workspace_feeds description/inputSchema 与 Cobra Long 手写选型文案,并与 node list / node search / doc read / drive search 划清路由边界;不改变命令身份、参数契约或接口绑定。"
}
]
},
"risk": {
"value": "low",
"source": "effect-default",
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"candidates": [
{
"value": "low",
"source": "effect-default",
"precedence": "inference_or_default",
"selected": true
}
]
},
"use_when": {
"value": [
"用户问某个知识库最近有什么更新、谁改了什么、有哪些评论等协作动态时",
"需要巡检知识库变更并按时间线汇总成动态摘要时"
],
"source": "internal/cli/schema_hints/selection/wiki.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:依据 wiki MCP 实时 tools/list 的 list_workspace_feeds description/inputSchema 与 Cobra Long 手写选型文案,并与 node list / node search / doc read / drive search 划清路由边界;不改变命令身份、参数契约或接口绑定。",
"candidates": [
{
"value": [
"用户问某个知识库最近有什么更新、谁改了什么、有哪些评论等协作动态时",
"需要巡检知识库变更并按时间线汇总成动态摘要时"
],
"source": "internal/cli/schema_hints/selection/wiki.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工审阅:依据 wiki MCP 实时 tools/list 的 list_workspace_feeds description/inputSchema 与 Cobra Long 手写选型文案,并与 node list / node search / doc read / drive search 划清路由边界;不改变命令身份、参数契约或接口绑定。"
}
]
}
},
"idempotency": "idempotent",
"interface_mode": "mcp",
"interface_ref": {
"product_id": "wiki",
"rpc_name": "list_workspace_feeds"
},
"reviewed": true,
"risk": "low",
"source_refs": [
"CommandRegistry:canonical_path=wiki.list_workspace_feeds",
"Skill:skills/mono/references/products/wiki.md",
"cobra-help:dws wiki feed list",
"internal/cli/schema_hints/metadata/wiki.json",
"internal/cli/schema_hints/selection/wiki.json",
"internal/cli/schema_mcp_metadata.json#tools.wiki.list_workspace_feeds",
"live-mcp-tools-list:wiki.list_workspace_feeds",
"skills/mono/references/products/wiki.md"
],
"use_when": [
"用户问某个知识库最近有什么更新、谁改了什么、有哪些评论等协作动态时",
"需要巡检知识库变更并按时间线汇总成动态摘要时"
]
},
"wiki member add": {
"agent_summary": "为指定知识库添加一个或多个成员,并授予指定角色",
"agent_summary_source": "dws-agent-selection/wiki",
+16 -16
View File
@@ -1,20 +1,20 @@
{
"version": 1,
"source_hash": "sha256:670ca810a83bf2aa6f387a18c3af746393d5ea9de24570cbcebbcf994eb7b613",
"surface_hash": "sha256:60eee8e2f37d6d9d60689efce85082798eb9ad38b7ba7c0b471c3de676a85a16",
"source_hash": "sha256:1f76aa71b00f7b807b863ced6e98aca76dc4bda65107ea2a6c67eb4ec629db43",
"surface_hash": "sha256:89bc72d0a5ff03df634d733dc8b71c3010572524542538552c33c657ae012562",
"source_files": 160,
"hint_files": 54,
"hint_products": 43,
"hint_tools": 1842,
"hint_tools": 1846,
"interface_metadata": {
"source": "mcp-tools-list+cli-registry",
"revision": "4574f7022c32cf4c033e9b7b4156e2fec815fed8",
"source_hash": "sha256:17251f74a4f76142457cc0e87251ecb86c1e3bda0cdf2edc02f351589716ecbc",
"source_tools": 419,
"surface_tools": 414,
"eligible_summaries": 413,
"source_tools": 420,
"surface_tools": 415,
"eligible_summaries": 414,
"applied_summaries": 0,
"preserved_summaries": 413,
"preserved_summaries": 414,
"rejected_tools": [
"chat.unread_message_conversation_list"
],
@@ -29,13 +29,13 @@
"coverage": {
"surface_products": 26,
"products_with_metadata": 26,
"surface_tools": 845,
"tools_with_metadata": 845,
"tools_with_agent_summary": 845,
"tools_with_use_when": 845,
"tools_with_avoid_when": 845,
"tools_with_examples": 845,
"tools_with_interface_mode": 845,
"surface_tools": 847,
"tools_with_metadata": 847,
"tools_with_agent_summary": 847,
"tools_with_use_when": 847,
"tools_with_avoid_when": 847,
"tools_with_examples": 847,
"tools_with_interface_mode": 847,
"unmatched_skill_tools": 122,
"unreviewed_skill_tools": 11
},
@@ -155,8 +155,8 @@
"tool_path": "wiki",
"candidates": [
"wiki +space-search",
"wiki member add",
"wiki member list"
"wiki feed list",
"wiki member add"
],
"review": {
"status": "group",
+73 -13
View File
@@ -1,17 +1,17 @@
{
"version": 1,
"surface_hash": "sha256:60eee8e2f37d6d9d60689efce85082798eb9ad38b7ba7c0b471c3de676a85a16",
"source_hash": "sha256:862734fb976a4c9c78d010b7391c74822f37805f5acc650551f4ac60f701c13a",
"surface_hash": "sha256:89bc72d0a5ff03df634d733dc8b71c3010572524542538552c33c657ae012562",
"source_hash": "sha256:ccdbef2cd88ade9fce277b4dfc9ef433724cf54f9ce6676b77169e99c9944f45",
"catalog": {
"agent_metadata": {
"products_with_metadata": 26,
"source": "embedded-skill-metadata",
"source_hash": "sha256:670ca810a83bf2aa6f387a18c3af746393d5ea9de24570cbcebbcf994eb7b613",
"surface_hash": "sha256:60eee8e2f37d6d9d60689efce85082798eb9ad38b7ba7c0b471c3de676a85a16",
"source_hash": "sha256:1f76aa71b00f7b807b863ced6e98aca76dc4bda65107ea2a6c67eb4ec629db43",
"surface_hash": "sha256:89bc72d0a5ff03df634d733dc8b71c3010572524542538552c33c657ae012562",
"surface_products": 26,
"surface_tools": 845,
"tools_with_agent_summary": 845,
"tools_with_metadata": 845,
"surface_tools": 847,
"tools_with_agent_summary": 847,
"tools_with_metadata": 847,
"unmatched_skill_tools": 122,
"version": 1
},
@@ -33,7 +33,7 @@
"source": "mcp-tools-list+cli-registry",
"source_hash": "sha256:17251f74a4f76142457cc0e87251ecb86c1e3bda0cdf2edc02f351589716ecbc",
"source_revision": "4574f7022c32cf4c033e9b7b4156e2fec815fed8",
"tool_count": 419,
"tool_count": 420,
"version": 1
},
"kind": "schema",
@@ -288,7 +288,7 @@
"id": "aitable",
"name": "AI 表格操作",
"runtime": true,
"tool_count": 145,
"tool_count": 146,
"tools": [
{
"agent_metadata_source": "embedded-skill-metadata",
@@ -4449,6 +4449,33 @@
"用户明确要求停止某自动化工作流时"
]
},
{
"agent_metadata_source": "embedded-skill-metadata",
"agent_summary": "获取 AI 表格工作流编辑文档与 workflow-dsl/v1 示例。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"实际创建工作流用 workflow create;修改已有工作流用 workflow update;查询已发布定义用 workflow get"
],
"canonical_path": "aitable.workflow_edit_example",
"cli_name": "edit-example",
"cli_path": "aitable workflow edit-example",
"confirmation": "not_required",
"description": "返回服务端提供的 AI 表格工作流编辑文档与示例。\n可作为 workflow create / workflow update 的 workflow-dsl/v1 结构参考;此命令不需要 Base ID 或其他参数。",
"effect": "read",
"group": "workflow",
"idempotency": "idempotent",
"interface_mode": "composite",
"interface_reason": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
"name": "workflow_edit_example",
"primary_cli_path": "aitable workflow edit-example",
"reviewed": true,
"risk": "low",
"title": "获取工作流编辑文档与示例",
"use_when": [
"创建或更新工作流前,需要确认最新 workflow-dsl/v1 结构、节点写法或完整示例时"
]
},
{
"agent_metadata_source": "embedded-skill-metadata",
"agent_summary": "启用工作流。",
@@ -26080,7 +26107,7 @@
"avoid_when": [
"需要编辑在线文档正文时使用 doc;只管理钉盘普通文件时使用 drive"
],
"description": "知识库 / 空间管理 / 节点管理 / 成员管理",
"description": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
"field_provenance": {
"agent_summary": {
"candidates": [
@@ -26134,9 +26161,9 @@
}
},
"id": "wiki",
"name": "知识库 / 空间管理 / 节点管理 / 成员管理",
"name": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
"runtime": true,
"tool_count": 16,
"tool_count": 17,
"tools": [
{
"agent_metadata_source": "embedded-skill-metadata",
@@ -26423,6 +26450,39 @@
"需要 workspaceId / rootFolderId 作为后续 node/drive 操作前置时"
]
},
{
"agent_metadata_source": "embedded-skill-metadata",
"agent_summary": "查询知识库的活动动态:谁在什么时间更新/上传/评论了哪些文档",
"agent_summary_source": "dws-agent-selection/wiki",
"availability": "available",
"avoid_when": [
"要看知识库当前有哪些节点用 node list;库内按关键词找文档用 node search",
"要读某篇文档正文用 doc read;跨库全局找文件用 drive search"
],
"canonical_path": "wiki.list_workspace_feeds",
"cli_name": "list",
"cli_path": "wiki feed list",
"confirmation": "not_required",
"description": "查询指定知识库的动态列表,返回动态类型、时间、内容摘要等信息。\n\n通过 --workspace 指定知识库,支持传入知识库 ID 或知识库 URL。\n支持分页,通过 --cursor 传入上次返回的 nextToken 获取下一页。\n\n权限要求:调用者需具备知识库的成员权限,非成员会被拒绝访问。",
"effect": "read",
"group": "feed",
"idempotency": "idempotent",
"interface_mode": "mcp",
"interface_ref": {
"product_id": "wiki",
"rpc_name": "list_workspace_feeds"
},
"metadata_source": "embedded-mcp-metadata",
"name": "list_workspace_feeds",
"primary_cli_path": "wiki feed list",
"reviewed": true,
"risk": "low",
"title": "查询知识库动态列表",
"use_when": [
"用户问某个知识库最近有什么更新、谁改了什么、有哪些评论等协作动态时",
"需要巡检知识库变更并按时间线汇总成动态摘要时"
]
},
{
"agent_metadata_source": "embedded-skill-metadata",
"agent_summary": "将知识库中的节点复制到指定位置",
@@ -26644,6 +26704,6 @@
}
],
"source": "embedded-command-catalog",
"tool_count": 845
"tool_count": 847
}
}
@@ -97777,6 +97777,326 @@
"用户明确要求停止某自动化工作流时"
]
},
"aitable.workflow_edit_example": {
"agent_metadata_source": "embedded-skill-metadata",
"agent_source_refs": [
"cobra-help:dws aitable workflow edit-example --help",
"internal/cli/schema_command_registry.json#aitable.workflow_edit_example",
"internal/cli/schema_hints/metadata/aitable.json",
"internal/cli/schema_hints/selection/aitable.json",
"mcp-contract:aitable/edit_workflow_example",
"skills/mono/references/products/aitable/aitable-workflow.md"
],
"agent_summary": "获取 AI 表格工作流编辑文档与 workflow-dsl/v1 示例。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"实际创建工作流用 workflow create;修改已有工作流用 workflow update;查询已发布定义用 workflow get"
],
"canonical_path": "aitable.workflow_edit_example",
"cli_name": "edit-example",
"cli_path": "aitable workflow edit-example",
"confirmation": "not_required",
"description": "返回服务端提供的 AI 表格工作流编辑文档与示例。\n可作为 workflow create / workflow update 的 workflow-dsl/v1 结构参考;此命令不需要 Base ID 或其他参数。",
"display": "AI 表格操作",
"effect": "read",
"effect_source": "agent-hint",
"examples": [
"dws aitable workflow edit-example"
],
"field_provenance": {
"agent_summary": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "获取 AI 表格工作流编辑文档与 workflow-dsl/v1 示例。"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "获取 AI 表格工作流编辑文档与 workflow-dsl/v1 示例。"
},
"availability": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"selected": true,
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": "available"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": "available"
},
"avoid_when": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"实际创建工作流用 workflow create;修改已有工作流用 workflow update;查询已发布定义用 workflow get"
]
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"实际创建工作流用 workflow create;修改已有工作流用 workflow update;查询已发布定义用 workflow get"
]
},
"canonical_path": {
"candidates": [
{
"precedence": "command_registry",
"selected": true,
"source": "reviewed_command_registry",
"source_ref": "aitable workflow edit-example",
"value": "aitable.workflow_edit_example"
}
],
"precedence": "command_registry",
"resolution": "registry_identity",
"source": "reviewed_command_registry",
"source_ref": "aitable workflow edit-example",
"value": "aitable.workflow_edit_example"
},
"confirmation": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"selected": true,
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": "not_required"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": "not_required"
},
"description": {
"candidates": [
{
"precedence": "cobra_help",
"selected": true,
"source": "cobra_help",
"value": "返回服务端提供的 AI 表格工作流编辑文档与示例。\n可作为 workflow create / workflow update 的 workflow-dsl/v1 结构参考;此命令不需要 Base ID 或其他参数。"
}
],
"precedence": "cobra_help",
"resolution": "highest_precedence",
"source": "cobra_help",
"value": "返回服务端提供的 AI 表格工作流编辑文档与示例。\n可作为 workflow create / workflow update 的 workflow-dsl/v1 结构参考;此命令不需要 Base ID 或其他参数。"
},
"effect": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"selected": true,
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": "read"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": "read"
},
"examples": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"dws aitable workflow edit-example"
]
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"dws aitable workflow edit-example"
]
},
"idempotency": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"selected": true,
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": "idempotent"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": "idempotent"
},
"interface_mode": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"selected": true,
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": "composite"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": "composite"
},
"interface_reason": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"selected": true,
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command."
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command."
},
"interface_ref": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "final interface mode composite forbids a direct MCP interface_ref",
"selected": true,
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": null
}
],
"precedence": "reviewed_explicit",
"resolution": "interface_disposition_matrix",
"review_reason": "final interface mode composite forbids a direct MCP interface_ref",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": null
},
"reviewed": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"selected": true,
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": true
},
{
"precedence": "reviewed_explicit",
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
"selected": false,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": true
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": true
},
"risk": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"selected": true,
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": "low"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"source": "internal/cli/schema_hints/metadata/aitable.json",
"value": "low"
},
"title": {
"candidates": [
{
"precedence": "cobra_help",
"selected": true,
"source": "cobra_help",
"value": "获取工作流编辑文档与示例"
}
],
"precedence": "cobra_help",
"resolution": "highest_precedence",
"source": "cobra_help",
"value": "获取工作流编辑文档与示例"
},
"use_when": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"创建或更新工作流前,需要确认最新 workflow-dsl/v1 结构、节点写法或完整示例时"
]
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"创建或更新工作流前,需要确认最新 workflow-dsl/v1 结构、节点写法或完整示例时"
]
}
},
"group": "workflow",
"has_parameters": false,
"idempotency": "idempotent",
"interface_mode": "composite",
"interface_reason": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
"is_alias": false,
"name": "workflow_edit_example",
"parameter_count": 0,
"parameters": {},
"path": "aitable.workflow_edit_example",
"primary_cli_path": "aitable workflow edit-example",
"product_id": "aitable",
"reviewed": true,
"risk": "low",
"source": "reviewed_command_registry",
"title": "获取工作流编辑文档与示例",
"use_when": [
"创建或更新工作流前,需要确认最新 workflow-dsl/v1 结构、节点写法或完整示例时"
]
},
"aitable.workflow_enable": {
"agent_metadata_source": "embedded-skill-metadata",
"agent_source_refs": [
+576 -2
View File
@@ -5312,8 +5312,99 @@
"is_alias": false,
"metadata_source": "embedded-mcp-metadata",
"name": "download_file",
"parameter_count": 3,
"parameter_count": 6,
"parameters": {
"no-resume": {
"description": "关闭断点续传 (可选)",
"field_provenance": {
"description": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_usage",
"value": "关闭断点续传 (可选)"
},
{
"precedence": "default",
"selected": false,
"source": "default",
"value": ""
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_usage",
"value": "关闭断点续传 (可选)"
},
"property": {
"candidates": [
{
"precedence": "reviewed_mapping_exclusion",
"review_reason": "CLI-only transfer-layer flag controlling download resume behaviour; not an MCP interface parameter.",
"selected": true,
"source": "reviewed_mapping_exclusion",
"value": ""
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "noResume"
}
],
"precedence": "reviewed_mapping_exclusion",
"resolution": "highest_precedence",
"review_reason": "CLI-only transfer-layer flag controlling download resume behaviour; not an MCP interface parameter.",
"source": "reviewed_mapping_exclusion",
"value": ""
},
"required": {
"candidates": [
{
"precedence": "default",
"selected": true,
"source": "default",
"value": false
}
],
"precedence": "default",
"resolution": "fallback",
"source": "default",
"value": false
},
"required_when": {
"candidates": [
{
"precedence": "default",
"selected": true,
"source": "default",
"value": ""
}
],
"precedence": "default",
"resolution": "highest_precedence",
"source": "default",
"value": ""
},
"type": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_flag_type",
"value": "boolean"
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_flag_type",
"value": "boolean"
}
},
"required": false,
"type": "boolean"
},
"node": {
"description": "文件 ID (dentryUuid) (必填)",
"field_provenance": {
@@ -5520,6 +5611,202 @@
"required": true,
"type": "string"
},
"parallel": {
"default": "4",
"description": "分片下载并发数,范围 1-8 (可选)",
"field_provenance": {
"description": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_usage",
"value": "分片下载并发数,范围 1-8 (可选)"
},
{
"precedence": "default",
"selected": false,
"source": "default",
"value": ""
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_usage",
"value": "分片下载并发数,范围 1-8 (可选)"
},
"property": {
"candidates": [
{
"precedence": "reviewed_mapping_exclusion",
"review_reason": "CLI-only transfer-layer flag controlling parallel chunk downloads; not an MCP interface parameter.",
"selected": true,
"source": "reviewed_mapping_exclusion",
"value": ""
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "parallel"
}
],
"precedence": "reviewed_mapping_exclusion",
"resolution": "highest_precedence",
"review_reason": "CLI-only transfer-layer flag controlling parallel chunk downloads; not an MCP interface parameter.",
"source": "reviewed_mapping_exclusion",
"value": ""
},
"required": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_nonzero_default",
"value": false
},
{
"precedence": "default",
"selected": false,
"source": "default",
"value": false
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_nonzero_default",
"value": false
},
"required_when": {
"candidates": [
{
"precedence": "default",
"selected": true,
"source": "default",
"value": ""
}
],
"precedence": "default",
"resolution": "highest_precedence",
"source": "default",
"value": ""
},
"type": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_flag_type",
"value": "integer"
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_flag_type",
"value": "integer"
}
},
"required": false,
"type": "integer"
},
"part-size": {
"default": "16MB",
"description": "分片下载的分片大小,如 8MB/16MB/1GB,范围 1MB-1GB (可选)",
"field_provenance": {
"description": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_usage",
"value": "分片下载的分片大小,如 8MB/16MB/1GB,范围 1MB-1GB (可选)"
},
{
"precedence": "default",
"selected": false,
"source": "default",
"value": ""
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_usage",
"value": "分片下载的分片大小,如 8MB/16MB/1GB,范围 1MB-1GB (可选)"
},
"property": {
"candidates": [
{
"precedence": "reviewed_mapping_exclusion",
"review_reason": "CLI-only transfer-layer flag controlling download chunk size; not an MCP interface parameter.",
"selected": true,
"source": "reviewed_mapping_exclusion",
"value": ""
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "partSize"
}
],
"precedence": "reviewed_mapping_exclusion",
"resolution": "highest_precedence",
"review_reason": "CLI-only transfer-layer flag controlling download chunk size; not an MCP interface parameter.",
"source": "reviewed_mapping_exclusion",
"value": ""
},
"required": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_nonzero_default",
"value": false
},
{
"precedence": "default",
"selected": false,
"source": "default",
"value": false
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_nonzero_default",
"value": false
},
"required_when": {
"candidates": [
{
"precedence": "default",
"selected": true,
"source": "default",
"value": ""
}
],
"precedence": "default",
"resolution": "highest_precedence",
"source": "default",
"value": ""
},
"type": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_flag_type",
"value": "string"
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_flag_type",
"value": "string"
}
},
"required": false,
"type": "string"
},
"space-id": {
"description": "文件所属空间 ID (可选)",
"field_provenance": {
@@ -5937,8 +6224,99 @@
"interface_reason": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
"is_alias": false,
"name": "download_file_version",
"parameter_count": 3,
"parameter_count": 6,
"parameters": {
"no-resume": {
"description": "关闭断点续传 (可选)",
"field_provenance": {
"description": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_usage",
"value": "关闭断点续传 (可选)"
},
{
"precedence": "default",
"selected": false,
"source": "default",
"value": ""
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_usage",
"value": "关闭断点续传 (可选)"
},
"property": {
"candidates": [
{
"precedence": "reviewed_mapping_exclusion",
"review_reason": "Reviewed unpinned adapter: drive.download_file_version has no singular pinned interface_ref; --no-resume is a CLI wrapper input and does not publish a direct interface property.",
"selected": true,
"source": "reviewed_mapping_exclusion",
"value": ""
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "noResume"
}
],
"precedence": "reviewed_mapping_exclusion",
"resolution": "highest_precedence",
"review_reason": "Reviewed unpinned adapter: drive.download_file_version has no singular pinned interface_ref; --no-resume is a CLI wrapper input and does not publish a direct interface property.",
"source": "reviewed_mapping_exclusion",
"value": ""
},
"required": {
"candidates": [
{
"precedence": "default",
"selected": true,
"source": "default",
"value": false
}
],
"precedence": "default",
"resolution": "fallback",
"source": "default",
"value": false
},
"required_when": {
"candidates": [
{
"precedence": "default",
"selected": true,
"source": "default",
"value": ""
}
],
"precedence": "default",
"resolution": "highest_precedence",
"source": "default",
"value": ""
},
"type": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_flag_type",
"value": "boolean"
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_flag_type",
"value": "boolean"
}
},
"required": false,
"type": "boolean"
},
"node": {
"description": "文件 ID (dentryUuid) 或 URL (必填)",
"field_provenance": {
@@ -6133,6 +6511,202 @@
"required": true,
"type": "string"
},
"parallel": {
"default": "4",
"description": "分片下载并发数,范围 1-8 (可选)",
"field_provenance": {
"description": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_usage",
"value": "分片下载并发数,范围 1-8 (可选)"
},
{
"precedence": "default",
"selected": false,
"source": "default",
"value": ""
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_usage",
"value": "分片下载并发数,范围 1-8 (可选)"
},
"property": {
"candidates": [
{
"precedence": "reviewed_mapping_exclusion",
"review_reason": "Reviewed unpinned adapter: drive.download_file_version has no singular pinned interface_ref; --parallel is a CLI wrapper input and does not publish a direct interface property.",
"selected": true,
"source": "reviewed_mapping_exclusion",
"value": ""
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "parallel"
}
],
"precedence": "reviewed_mapping_exclusion",
"resolution": "highest_precedence",
"review_reason": "Reviewed unpinned adapter: drive.download_file_version has no singular pinned interface_ref; --parallel is a CLI wrapper input and does not publish a direct interface property.",
"source": "reviewed_mapping_exclusion",
"value": ""
},
"required": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_nonzero_default",
"value": false
},
{
"precedence": "default",
"selected": false,
"source": "default",
"value": false
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_nonzero_default",
"value": false
},
"required_when": {
"candidates": [
{
"precedence": "default",
"selected": true,
"source": "default",
"value": ""
}
],
"precedence": "default",
"resolution": "highest_precedence",
"source": "default",
"value": ""
},
"type": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_flag_type",
"value": "integer"
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_flag_type",
"value": "integer"
}
},
"required": false,
"type": "integer"
},
"part-size": {
"default": "16MB",
"description": "分片下载的分片大小,如 8MB/16MB/1GB,范围 1MB-1GB (可选)",
"field_provenance": {
"description": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_usage",
"value": "分片下载的分片大小,如 8MB/16MB/1GB,范围 1MB-1GB (可选)"
},
{
"precedence": "default",
"selected": false,
"source": "default",
"value": ""
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_usage",
"value": "分片下载的分片大小,如 8MB/16MB/1GB,范围 1MB-1GB (可选)"
},
"property": {
"candidates": [
{
"precedence": "reviewed_mapping_exclusion",
"review_reason": "Reviewed unpinned adapter: drive.download_file_version has no singular pinned interface_ref; --part-size is a CLI wrapper input and does not publish a direct interface property.",
"selected": true,
"source": "reviewed_mapping_exclusion",
"value": ""
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "partSize"
}
],
"precedence": "reviewed_mapping_exclusion",
"resolution": "highest_precedence",
"review_reason": "Reviewed unpinned adapter: drive.download_file_version has no singular pinned interface_ref; --part-size is a CLI wrapper input and does not publish a direct interface property.",
"source": "reviewed_mapping_exclusion",
"value": ""
},
"required": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_nonzero_default",
"value": false
},
{
"precedence": "default",
"selected": false,
"source": "default",
"value": false
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_nonzero_default",
"value": false
},
"required_when": {
"candidates": [
{
"precedence": "default",
"selected": true,
"source": "default",
"value": ""
}
],
"precedence": "default",
"resolution": "highest_precedence",
"source": "default",
"value": ""
},
"type": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_flag_type",
"value": "string"
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_flag_type",
"value": "string"
}
},
"required": false,
"type": "string"
},
"version": {
"description": "历史版本号 (必填,正整数,从 drive list --versions 获取)",
"field_provenance": {
+803 -16
View File
@@ -28,7 +28,7 @@
"cli_path": "wiki member add",
"confirmation": "not_required",
"description": "为指定知识库添加一个或多个成员,并授予指定角色。\n\n通过 --users 传入逗号分隔的 userId 列表,多个用户将被授予同一角色。\n\n支持的角色 (--role)(必须大写):\n MANAGER 管理员,可读写、管理成员\n EDITOR 编辑者,可查看、编辑、上传内容\n DOWNLOADER 查看下载者,可查看并下载内容\n READER 仅可查看者,仅可查看,不可下载\n\n注意:\n- OWNER 角色不可通过此接口添加,知识库创建者默认为所有者。\n- 操作者需具备知识库的 OWNER 或 MANAGER 权限。\n- 单次请求最多 30 个成员,超出请分批调用。\n\n支持通过 --workspace 传入知识库 ID 或知识库 URL,系统自动识别。\n用户 uid 可通过「钉钉通讯录」相关命令检索,如:\n dws contact user search --keyword \"姓名\"",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
"effect": "write",
"effect_source": "command-verb",
"examples": [
@@ -732,7 +732,7 @@
"cli_path": "wiki node create",
"confirmation": "not_required",
"description": "在指定知识库中创建文档、文件夹或其他类型的节点。\n\n通过 --type 指定节点类型(服务端支持以下值,asheet 不被支持):\n adoc 在线文档 (默认)\n axls 在线电子表格\n able 多维表\n appt 在线演示\n adraw 白板/画板\n amind 脑图\n folder 文件夹\n\n通过 --folder 指定父节点,不传则创建在知识库根目录。",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
"effect": "write",
"effect_source": "command-verb",
"examples": [
@@ -1501,7 +1501,7 @@
"cli_path": "wiki space create",
"confirmation": "not_required",
"description": "创建一个新的钉钉文档知识库(WikiSpace)。\n\n创建成功后返回新知识库的 workspaceId,可用于后续在该知识库下创建文档或遍历文件。\n操作受权限控制,仅当调用者具备在当前组织内创建知识库的权限时可成功创建。",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
"effect": "write",
"effect_source": "command-verb",
"examples": [
@@ -2149,7 +2149,7 @@
"cli_path": "wiki node delete",
"confirmation": "user_required",
"description": "将知识库中的节点移入回收站。\n\n注意: 这是一个危险操作。执行前需要确认,或传入 --yes 跳过确认。\n删除后节点会进入回收站,有保留期限可恢复。\n\n权限要求: 对节点有\"管理\"权限。",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
"effect": "destructive",
"effect_source": "agent-hint",
"examples": [
@@ -2737,7 +2737,7 @@
"cli_path": "wiki space delete",
"confirmation": "user_required",
"description": "将指定知识库移入回收站。\n\n删除后知识库会进入回收站,可在回收站中恢复(有保留期限)。\n支持传入知识库 ID 或知识库 URL,系统自动识别。\n知识库 URL 格式:https://alidocs.dingtalk.com/i/spaces/{workspaceId}/overview\n\n注意:\n- 操作者必须具备知识库的 OWNER 角色。\n- 这是一个危险操作,执行前请确认。",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
"effect": "destructive",
"effect_source": "agent-hint",
"examples": [
@@ -3219,7 +3219,7 @@
"cli_path": "wiki space get",
"confirmation": "not_required",
"description": "获取指定知识库的详细信息,包括名称、描述、创建者、创建时间、成员数量等。\n\n支持传入知识库 ID 或知识库 URL,系统自动识别。\n知识库 URL 格式:https://alidocs.dingtalk.com/i/spaces/{workspaceId}/overview",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
"effect": "read",
"effect_source": "command-verb",
"examples": [
@@ -3676,7 +3676,7 @@
"cli_path": "wiki member list",
"confirmation": "not_required",
"description": "查询指定知识库的成员列表,返回每位成员的 userId、姓名、角色等信息。\n\n注意:底层不支持游标分页,--limit 仅控制单次返回的最大条数(最大 200)。\n若结果被截断(出参 truncated=true),可通过 --filter-role 收窄查询范围;\nORG 类型授权不会出现在查询结果中。",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
"effect": "read",
"effect_source": "command-verb",
"examples": [
@@ -4379,7 +4379,7 @@
"cli_path": "wiki node list",
"confirmation": "not_required",
"description": "列出指定知识库下的直接子节点(文档、文件夹、表格等)。\n\n通过 --folder 指定父节点可列出子目录内容;不传 --folder 则列出知识库根目录。\n支持分页,通过 --cursor 传入上次返回的 pageToken 获取下一页。",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
"effect": "read",
"effect_source": "command-verb",
"examples": [
@@ -5163,7 +5163,7 @@
"cli_path": "wiki space list",
"confirmation": "not_required",
"description": "获取当前用户有权访问的空间列表。统一管理两种空间类型。\n\n通过 --type 参数控制返回范围:\n orgWikiSpace — 组织知识库列表(默认,支持分页)\n myWikiSpace — 当前用户的「我的文档」个人空间(固定 1 条)\n orgSpace — 钉盘企业空间(团队文件)列表\n mySpace — 钉盘「我的文件」个人空间",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
"effect": "read",
"effect_source": "command-verb",
"examples": [
@@ -5796,6 +5796,793 @@
"需要 workspaceId / rootFolderId 作为后续 node/drive 操作前置时"
]
},
"wiki.list_workspace_feeds": {
"agent_metadata_source": "embedded-skill-metadata",
"agent_source_refs": [
"CommandRegistry:canonical_path=wiki.list_workspace_feeds",
"Skill:skills/mono/references/products/wiki.md",
"cobra-help:dws wiki feed list",
"internal/cli/schema_hints/metadata/wiki.json",
"internal/cli/schema_hints/selection/wiki.json",
"internal/cli/schema_mcp_metadata.json#tools.wiki.list_workspace_feeds",
"live-mcp-tools-list:wiki.list_workspace_feeds",
"skills/mono/references/products/wiki.md"
],
"agent_summary": "查询知识库的活动动态:谁在什么时间更新/上传/评论了哪些文档",
"agent_summary_source": "dws-agent-selection/wiki",
"availability": "available",
"avoid_when": [
"要看知识库当前有哪些节点用 node list;库内按关键词找文档用 node search",
"要读某篇文档正文用 doc read;跨库全局找文件用 drive search"
],
"canonical_path": "wiki.list_workspace_feeds",
"cli_name": "list",
"cli_path": "wiki feed list",
"confirmation": "not_required",
"description": "查询指定知识库的动态列表,返回动态类型、时间、内容摘要等信息。\n\n通过 --workspace 指定知识库,支持传入知识库 ID 或知识库 URL。\n支持分页,通过 --cursor 传入上次返回的 nextToken 获取下一页。\n\n权限要求:调用者需具备知识库的成员权限,非成员会被拒绝访问。",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
"effect": "read",
"effect_source": "command-verb",
"examples": [
"dws wiki feed list --workspace \u003cworkspaceId\u003e --format json",
"dws wiki feed list --workspace \u003cworkspaceId\u003e --limit 10 --format json"
],
"field_provenance": {
"agent_summary": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "人工审阅:依据 wiki MCP 实时 tools/list 的 list_workspace_feeds description/inputSchema 与 Cobra Long 手写选型文案,并与 node list / node search / doc read / drive search 划清路由边界;不改变命令身份、参数契约或接口绑定。",
"selected": true,
"source": "internal/cli/schema_hints/selection/wiki.json",
"value": "查询知识库的活动动态:谁在什么时间更新/上传/评论了哪些文档"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:依据 wiki MCP 实时 tools/list 的 list_workspace_feeds description/inputSchema 与 Cobra Long 手写选型文案,并与 node list / node search / doc read / drive search 划清路由边界;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/wiki.json",
"value": "查询知识库的活动动态:谁在什么时间更新/上传/评论了哪些文档"
},
"availability": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "Live wiki MCP tools/list exposes list_workspace_feeds as a read-only knowledge base activity query. The reviewed CLI leaf forwards workspaceId/maxResults/nextToken/excludeFile to the native wiki server and adds no side effects, so it keeps the same read-only posture as the sibling wiki list commands.",
"selected": true,
"source": "internal/cli/schema_hints/metadata/wiki.json",
"value": "available"
},
{
"precedence": "mcp_fallback",
"selected": false,
"source": "internal/cli/schema_mcp_metadata.json#tools.wiki.list_workspace_feeds.interface_ref",
"value": "available"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Live wiki MCP tools/list exposes list_workspace_feeds as a read-only knowledge base activity query. The reviewed CLI leaf forwards workspaceId/maxResults/nextToken/excludeFile to the native wiki server and adds no side effects, so it keeps the same read-only posture as the sibling wiki list commands.",
"source": "internal/cli/schema_hints/metadata/wiki.json",
"value": "available"
},
"avoid_when": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "人工审阅:依据 wiki MCP 实时 tools/list 的 list_workspace_feeds description/inputSchema 与 Cobra Long 手写选型文案,并与 node list / node search / doc read / drive search 划清路由边界;不改变命令身份、参数契约或接口绑定。",
"selected": true,
"source": "internal/cli/schema_hints/selection/wiki.json",
"value": [
"要看知识库当前有哪些节点用 node list;库内按关键词找文档用 node search",
"要读某篇文档正文用 doc read;跨库全局找文件用 drive search"
]
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:依据 wiki MCP 实时 tools/list 的 list_workspace_feeds description/inputSchema 与 Cobra Long 手写选型文案,并与 node list / node search / doc read / drive search 划清路由边界;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/wiki.json",
"value": [
"要看知识库当前有哪些节点用 node list;库内按关键词找文档用 node search",
"要读某篇文档正文用 doc read;跨库全局找文件用 drive search"
]
},
"canonical_path": {
"candidates": [
{
"precedence": "command_registry",
"selected": true,
"source": "reviewed_command_registry",
"source_ref": "wiki feed list",
"value": "wiki.list_workspace_feeds"
}
],
"precedence": "command_registry",
"resolution": "registry_identity",
"source": "reviewed_command_registry",
"source_ref": "wiki feed list",
"value": "wiki.list_workspace_feeds"
},
"confirmation": {
"candidates": [
{
"precedence": "inference_or_default",
"selected": true,
"source": "risk-default",
"value": "not_required"
}
],
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"source": "risk-default",
"value": "not_required"
},
"description": {
"candidates": [
{
"precedence": "cobra_help",
"selected": true,
"source": "cobra_help",
"value": "查询指定知识库的动态列表,返回动态类型、时间、内容摘要等信息。\n\n通过 --workspace 指定知识库,支持传入知识库 ID 或知识库 URL。\n支持分页,通过 --cursor 传入上次返回的 nextToken 获取下一页。\n\n权限要求:调用者需具备知识库的成员权限,非成员会被拒绝访问。"
},
{
"precedence": "mcp_metadata",
"selected": false,
"source": "mcp_metadata",
"value": "功能概述:\r\n查询指定钉钉文档知识库的活动动态,包括谁在什么时间对哪些文档进行了更新、上传、评论等操作。通过传入 workspaceId(知识库 ID 或知识库 URL)定位目标知识库,返回动态列表,每条包含动态类型(type)、发生时间(time)和内容摘要(content)。支持游标分页(nextToken),可选排除文件相关动态(excludeFile)。\r\n\r\n适用场景:\r\n– 需要了解知识库最近有哪些文档被更新或上传了新文件。\r\n– 需要追踪知识库内的协作活动,如谁评论了哪篇文档。\r\n– 定期巡检知识库变更情况,生成动态摘要报告。\r\n\r\n注意事项:\r\n– 权限要求:操作者需具备知识库的成员权限,非成员会被拒绝访问。\r\n– 通过 workspaceId 定位知识库,支持知识库 ID(纯字符串)或知识库 URL(如 https://alidocs.dingtalk.com/i/spaces/{workspaceId}/overview),系统自动提取。\r\n– 每页默认返回 20 条动态,通过 maxResults 调整,最大 50 条。首页不传 nextToken,翻页时传入上次返回的 nextToken 值。\r\n– excludeFile 设为 true 可过滤掉文件相关的动态,默认 false 即返回全部动态类型。"
}
],
"precedence": "cobra_help",
"resolution": "highest_precedence",
"source": "cobra_help",
"value": "查询指定知识库的动态列表,返回动态类型、时间、内容摘要等信息。\n\n通过 --workspace 指定知识库,支持传入知识库 ID 或知识库 URL。\n支持分页,通过 --cursor 传入上次返回的 nextToken 获取下一页。\n\n权限要求:调用者需具备知识库的成员权限,非成员会被拒绝访问。"
},
"effect": {
"candidates": [
{
"precedence": "inference_or_default",
"selected": true,
"source": "command-verb",
"value": "read"
}
],
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"source": "command-verb",
"value": "read"
},
"examples": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "人工审阅:依据 wiki MCP 实时 tools/list 的 list_workspace_feeds description/inputSchema 与 Cobra Long 手写选型文案,并与 node list / node search / doc read / drive search 划清路由边界;不改变命令身份、参数契约或接口绑定。",
"selected": true,
"source": "internal/cli/schema_hints/selection/wiki.json",
"value": [
"dws wiki feed list --workspace \u003cworkspaceId\u003e --format json",
"dws wiki feed list --workspace \u003cworkspaceId\u003e --limit 10 --format json"
]
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:依据 wiki MCP 实时 tools/list 的 list_workspace_feeds description/inputSchema 与 Cobra Long 手写选型文案,并与 node list / node search / doc read / drive search 划清路由边界;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/wiki.json",
"value": [
"dws wiki feed list --workspace \u003cworkspaceId\u003e --format json",
"dws wiki feed list --workspace \u003cworkspaceId\u003e --limit 10 --format json"
]
},
"idempotency": {
"candidates": [
{
"precedence": "inference_or_default",
"selected": true,
"source": "effect-default",
"value": "idempotent"
}
],
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"source": "effect-default",
"value": "idempotent"
},
"interface_mode": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "Live wiki MCP tools/list exposes list_workspace_feeds as a read-only knowledge base activity query. The reviewed CLI leaf forwards workspaceId/maxResults/nextToken/excludeFile to the native wiki server and adds no side effects, so it keeps the same read-only posture as the sibling wiki list commands.",
"selected": true,
"source": "internal/cli/schema_hints/metadata/wiki.json",
"value": "mcp"
},
{
"precedence": "mcp_fallback",
"selected": false,
"source": "internal/cli/schema_mcp_metadata.json#tools.wiki.list_workspace_feeds.interface_ref",
"value": "mcp"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Live wiki MCP tools/list exposes list_workspace_feeds as a read-only knowledge base activity query. The reviewed CLI leaf forwards workspaceId/maxResults/nextToken/excludeFile to the native wiki server and adds no side effects, so it keeps the same read-only posture as the sibling wiki list commands.",
"source": "internal/cli/schema_hints/metadata/wiki.json",
"value": "mcp"
},
"interface_ref": {
"candidates": [
{
"precedence": "mcp_fallback",
"selected": true,
"source": "internal/cli/schema_mcp_metadata.json#tools.wiki.list_workspace_feeds.interface_ref",
"value": {
"product_id": "wiki",
"rpc_name": "list_workspace_feeds"
}
}
],
"precedence": "mcp_fallback",
"resolution": "highest_precedence",
"source": "internal/cli/schema_mcp_metadata.json#tools.wiki.list_workspace_feeds.interface_ref",
"value": {
"product_id": "wiki",
"rpc_name": "list_workspace_feeds"
}
},
"metadata_source": {
"candidates": [
{
"precedence": "derived_resolution",
"selected": true,
"source": "metadata_source_resolution",
"value": "embedded-mcp-metadata"
}
],
"precedence": "derived_resolution",
"resolution": "highest_precedence",
"source": "metadata_source_resolution",
"value": "embedded-mcp-metadata"
},
"reviewed": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "Live wiki MCP tools/list exposes list_workspace_feeds as a read-only knowledge base activity query. The reviewed CLI leaf forwards workspaceId/maxResults/nextToken/excludeFile to the native wiki server and adds no side effects, so it keeps the same read-only posture as the sibling wiki list commands.",
"selected": true,
"source": "internal/cli/schema_hints/metadata/wiki.json",
"value": true
},
{
"precedence": "reviewed_explicit",
"review_reason": "人工审阅:依据 wiki MCP 实时 tools/list 的 list_workspace_feeds description/inputSchema 与 Cobra Long 手写选型文案,并与 node list / node search / doc read / drive search 划清路由边界;不改变命令身份、参数契约或接口绑定。",
"selected": false,
"source": "internal/cli/schema_hints/selection/wiki.json",
"value": true
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Live wiki MCP tools/list exposes list_workspace_feeds as a read-only knowledge base activity query. The reviewed CLI leaf forwards workspaceId/maxResults/nextToken/excludeFile to the native wiki server and adds no side effects, so it keeps the same read-only posture as the sibling wiki list commands.",
"source": "internal/cli/schema_hints/metadata/wiki.json",
"value": true
},
"risk": {
"candidates": [
{
"precedence": "inference_or_default",
"selected": true,
"source": "effect-default",
"value": "low"
}
],
"precedence": "inference_or_default",
"resolution": "highest_precedence",
"source": "effect-default",
"value": "low"
},
"title": {
"candidates": [
{
"precedence": "cobra_help",
"selected": true,
"source": "cobra_help",
"value": "查询知识库动态列表"
},
{
"precedence": "mcp_metadata",
"selected": false,
"source": "mcp_metadata",
"value": "list_workspace_feeds"
}
],
"precedence": "cobra_help",
"resolution": "highest_precedence",
"source": "cobra_help",
"value": "查询知识库动态列表"
},
"use_when": {
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "人工审阅:依据 wiki MCP 实时 tools/list 的 list_workspace_feeds description/inputSchema 与 Cobra Long 手写选型文案,并与 node list / node search / doc read / drive search 划清路由边界;不改变命令身份、参数契约或接口绑定。",
"selected": true,
"source": "internal/cli/schema_hints/selection/wiki.json",
"value": [
"用户问某个知识库最近有什么更新、谁改了什么、有哪些评论等协作动态时",
"需要巡检知识库变更并按时间线汇总成动态摘要时"
]
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:依据 wiki MCP 实时 tools/list 的 list_workspace_feeds description/inputSchema 与 Cobra Long 手写选型文案,并与 node list / node search / doc read / drive search 划清路由边界;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/wiki.json",
"value": [
"用户问某个知识库最近有什么更新、谁改了什么、有哪些评论等协作动态时",
"需要巡检知识库变更并按时间线汇总成动态摘要时"
]
}
},
"group": "feed",
"has_parameters": true,
"idempotency": "idempotent",
"interface_mode": "mcp",
"interface_ref": {
"product_id": "wiki",
"rpc_name": "list_workspace_feeds"
},
"is_alias": false,
"metadata_source": "embedded-mcp-metadata",
"name": "list_workspace_feeds",
"parameter_count": 4,
"parameters": {
"cursor": {
"description": "分页游标 (首页留空)",
"field_provenance": {
"description": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_usage",
"value": "分页游标 (首页留空)"
},
{
"precedence": "mcp_metadata",
"selected": false,
"source": "mcp_metadata",
"value": "分页游标,首次查询不传,后续翻页时传入上次返回的 nextToken。"
},
{
"precedence": "default",
"selected": false,
"source": "default",
"value": ""
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_usage",
"value": "分页游标 (首页留空)"
},
"property": {
"candidates": [
{
"precedence": "versioned_binding",
"selected": true,
"source": "versioned_parameter_binding",
"value": "nextToken"
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "cursor"
}
],
"precedence": "versioned_binding",
"resolution": "highest_precedence",
"source": "versioned_parameter_binding",
"value": "nextToken"
},
"required": {
"candidates": [
{
"precedence": "default",
"selected": true,
"source": "default",
"value": false
}
],
"precedence": "default",
"resolution": "fallback",
"source": "default",
"value": false
},
"required_when": {
"candidates": [
{
"precedence": "default",
"selected": true,
"source": "default",
"value": ""
}
],
"precedence": "default",
"resolution": "highest_precedence",
"source": "default",
"value": ""
},
"type": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_flag_type",
"value": "string"
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_flag_type",
"value": "string"
}
},
"interface_description": "分页游标,首次查询不传,后续翻页时传入上次返回的 nextToken。",
"property": "nextToken",
"required": false,
"type": "string"
},
"exclude-file": {
"description": "是否排除文件相关的动态 (默认 false)",
"field_provenance": {
"description": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_usage",
"value": "是否排除文件相关的动态 (默认 false)"
},
{
"precedence": "mcp_metadata",
"selected": false,
"source": "mcp_metadata",
"value": "是否排除文件相关的动态,选填,默认 false。"
},
{
"precedence": "default",
"selected": false,
"source": "default",
"value": ""
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_usage",
"value": "是否排除文件相关的动态 (默认 false)"
},
"property": {
"candidates": [
{
"precedence": "versioned_binding",
"selected": true,
"source": "versioned_parameter_binding",
"value": "excludeFile"
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "excludeFile"
}
],
"precedence": "versioned_binding",
"resolution": "highest_precedence",
"source": "versioned_parameter_binding",
"value": "excludeFile"
},
"required": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_nonzero_default",
"value": false
},
{
"precedence": "default",
"selected": false,
"source": "default",
"value": false
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_nonzero_default",
"value": false
},
"required_when": {
"candidates": [
{
"precedence": "default",
"selected": true,
"source": "default",
"value": ""
}
],
"precedence": "default",
"resolution": "highest_precedence",
"source": "default",
"value": ""
},
"type": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_flag_type",
"value": "boolean"
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_flag_type",
"value": "boolean"
}
},
"interface_description": "是否排除文件相关的动态,选填,默认 false。",
"property": "excludeFile",
"required": false,
"type": "boolean"
},
"limit": {
"description": "每页数量 (默认 20,最大 50)",
"field_provenance": {
"description": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_usage",
"value": "每页数量 (默认 20,最大 50)"
},
{
"precedence": "mcp_metadata",
"selected": false,
"source": "mcp_metadata",
"value": "期望返回的最大动态条数,默认 20,最大 50。"
},
{
"precedence": "default",
"selected": false,
"source": "default",
"value": ""
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_usage",
"value": "每页数量 (默认 20,最大 50)"
},
"interface_type": {
"candidates": [
{
"precedence": "mcp_metadata",
"selected": true,
"source": "mcp_metadata",
"value": "number"
},
{
"precedence": "fallback",
"selected": false,
"source": "cobra_flag_type",
"value": "integer"
}
],
"precedence": "mcp_metadata",
"resolution": "highest_precedence",
"source": "mcp_metadata",
"value": "number"
},
"property": {
"candidates": [
{
"precedence": "versioned_binding",
"selected": true,
"source": "versioned_parameter_binding",
"value": "maxResults"
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "limit"
}
],
"precedence": "versioned_binding",
"resolution": "highest_precedence",
"source": "versioned_parameter_binding",
"value": "maxResults"
},
"required": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_nonzero_default",
"value": false
},
{
"precedence": "default",
"selected": false,
"source": "default",
"value": false
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_nonzero_default",
"value": false
},
"required_when": {
"candidates": [
{
"precedence": "default",
"selected": true,
"source": "default",
"value": ""
}
],
"precedence": "default",
"resolution": "highest_precedence",
"source": "default",
"value": ""
},
"type": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_flag_type",
"value": "integer"
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_flag_type",
"value": "integer"
}
},
"interface_description": "期望返回的最大动态条数,默认 20,最大 50。",
"interface_type": "number",
"property": "maxResults",
"required": false,
"type": "integer"
},
"workspace": {
"description": "知识库 ID 或 URL (必填)",
"field_provenance": {
"description": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_usage",
"value": "知识库 ID 或 URL (必填)"
},
{
"precedence": "mcp_metadata",
"selected": false,
"source": "mcp_metadata",
"value": "目标知识库的标识,支持两种格式:1) 知识库 ID(纯字符串);2) 知识库 URL,如 https://alidocs.dingtalk.com/i/spaces/{workspaceId}/overview,系统自动提取其中的 workspaceId。"
},
{
"precedence": "default",
"selected": false,
"source": "default",
"value": ""
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_usage",
"value": "知识库 ID 或 URL (必填)"
},
"property": {
"candidates": [
{
"precedence": "versioned_binding",
"selected": true,
"source": "versioned_parameter_binding",
"value": "workspaceId"
},
{
"precedence": "inference",
"selected": false,
"source": "flag_name_inference",
"value": "workspace"
}
],
"precedence": "versioned_binding",
"resolution": "highest_precedence",
"source": "versioned_parameter_binding",
"value": "workspaceId"
},
"required": {
"candidates": [
{
"precedence": "mcp_metadata",
"selected": true,
"source": "mcp_metadata",
"value": true
},
{
"precedence": "inference",
"selected": false,
"source": "usage_required_inference",
"value": true
},
{
"precedence": "default",
"selected": false,
"source": "default",
"value": false
}
],
"precedence": "mcp_metadata",
"resolution": "highest_precedence",
"source": "mcp_metadata",
"value": true
},
"required_when": {
"candidates": [
{
"precedence": "default",
"selected": true,
"source": "default",
"value": ""
}
],
"precedence": "default",
"resolution": "highest_precedence",
"source": "default",
"value": ""
},
"type": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_flag_type",
"value": "string"
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_flag_type",
"value": "string"
}
},
"interface_description": "目标知识库的标识,支持两种格式:1) 知识库 ID(纯字符串);2) 知识库 URL,如 https://alidocs.dingtalk.com/i/spaces/{workspaceId}/overview,系统自动提取其中的 workspaceId。",
"property": "workspaceId",
"required": true,
"type": "string"
}
},
"path": "wiki.list_workspace_feeds",
"primary_cli_path": "wiki feed list",
"product_id": "wiki",
"reviewed": true,
"risk": "low",
"source": "reviewed_command_registry",
"title": "查询知识库动态列表",
"use_when": [
"用户问某个知识库最近有什么更新、谁改了什么、有哪些评论等协作动态时",
"需要巡检知识库变更并按时间线汇总成动态摘要时"
]
},
"wiki.node_copy": {
"agent_metadata_source": "embedded-skill-metadata",
"agent_source_refs": [
@@ -5822,7 +6609,7 @@
"cli_path": "wiki node copy",
"confirmation": "not_required",
"description": "将知识库中的节点复制到指定位置。\n\n通过 --node 指定源节点,通过 --folder 指定目标文件夹。\n不传 --folder 时复制到 --workspace 指定知识库的根目录。",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
"effect": "write",
"effect_source": "command-verb",
"examples": [
@@ -6488,7 +7275,7 @@
"cli_path": "wiki node move",
"confirmation": "not_required",
"description": "将知识库中的节点移动到指定位置。\n\n通过 --node 指定源节点,通过 --folder 指定目标文件夹。\n不传 --folder 时移动到 --workspace 指定知识库的根目录。\n\n注意:跨知识库移动需要同时具备源和目标的相应权限。",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
"effect": "write",
"effect_source": "command-verb",
"examples": [
@@ -7153,7 +7940,7 @@
"cli_path": "wiki node search",
"confirmation": "not_required",
"description": "在指定知识库内搜索文档/文件夹/表格等节点。\n\n通过 --workspace 限定搜索范围到某个知识库,通过 --query 指定搜索关键词。\n支持按文件扩展名过滤(--extensions),如 adoc、asheet、pdf 等。",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
"effect": "read",
"effect_source": "command-verb",
"examples": [
@@ -8051,7 +8838,7 @@
"cli_path": "wiki member remove",
"confirmation": "not_required",
"description": "从指定知识库中移除一个或多个成员(仅支持 USER 类型)。\n\n移除后相关用户将无法访问该知识库下的内容(除非通过节点级权限另行授权)。\n\n注意:\n- OWNER 角色不可通过此接口移除。\n- 操作者需具备知识库的 OWNER 或 MANAGER 权限。\n- 单次请求最多 30 个成员,超出请分批调用。",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
"effect": "write",
"effect_source": "agent-hint",
"examples": [
@@ -8671,7 +9458,7 @@
]
},
"description": "根据关键词搜索当前用户有权限访问的知识库列表,匹配知识库名称和描述。",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
"effect": "read",
"effect_source": "command-verb",
"examples": [
@@ -9370,7 +10157,7 @@
"cli_path": "wiki +space-search",
"confirmation": "not_required",
"description": "当你只记得知识库名称的部分关键词、想快速按名称定位某个知识库时使用;输入关键词返回匹配的知识库列表,比逐页 +space-list 更快找到目标 workspaceId。",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
"effect": "read",
"effect_source": "agent-hint",
"examples": [
@@ -9880,7 +10667,7 @@
"cli_path": "wiki member update",
"confirmation": "not_required",
"description": "更新指定知识库已有成员的角色。\n\n支持的角色 (--role)(必须大写):\n MANAGER 管理员\n EDITOR 编辑者\n DOWNLOADER 查看下载者\n READER 仅可查看者\n\n注意:\n- OWNER 角色不可通过此接口变更。\n- 同一成员在同一知识库只能拥有一个角色,变更后旧角色自动替换。\n- 操作者需具备知识库的 OWNER 或 MANAGER 权限。\n\n仅可更新已存在成员关系的成员,新增成员请使用 dws wiki member add。",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
"effect": "write",
"effect_source": "command-verb",
"examples": [
@@ -452,6 +452,10 @@
"canonical_path": "aitable.workflow_disable",
"cli_path": "aitable workflow disable"
},
{
"canonical_path": "aitable.workflow_edit_example",
"cli_path": "aitable workflow edit-example"
},
{
"canonical_path": "aitable.workflow_enable",
"cli_path": "aitable workflow enable"
@@ -1,6 +1,10 @@
{
"id": "wiki",
"tools": [
{
"canonical_path": "wiki.list_workspace_feeds",
"cli_path": "wiki feed list"
},
{
"canonical_path": "wiki.add_member",
"cli_path": "wiki member add"
@@ -999,6 +999,19 @@
"reviewed": true,
"runtime_gate": "confirm_delete"
},
"aitable.workflow_edit_example": {
"effect": "read",
"risk": "low",
"confirmation": "not_required",
"idempotency": "idempotent",
"interface_mode": "composite",
"availability": "available",
"interface_reason": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
"reviewed": true,
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
"cli_path": "aitable workflow edit-example",
"runtime_gate": "none"
},
"aitable.workflow_enable": {
"interface_ref": {
"product_id": "aitable-helper",
@@ -74,6 +74,12 @@
"interface_reason": "The CLI command routes by --type between wiki/list_wikiSpaces and drive/list_spaces, so the reviewed executable wrapper has no single direct MCP interface.",
"reviewed": true
},
"wiki.list_workspace_feeds": {
"interface_mode": "mcp",
"availability": "available",
"reviewed": true,
"review_reason": "Live wiki MCP tools/list exposes list_workspace_feeds as a read-only knowledge base activity query. The reviewed CLI leaf forwards workspaceId/maxResults/nextToken/excludeFile to the native wiki server and adds no side effects, so it keeps the same read-only posture as the sibling wiki list commands."
},
"wiki.node_copy": {
"interface_ref": {
"product_id": "doc",
@@ -7,7 +7,7 @@
"channel": "open-source"
},
"coverage": {
"source_tools": 845,
"source_tools": 847,
"matched_tools": 71
},
"tools": {
@@ -2250,6 +2250,26 @@
"dws-schema-live:none (helper/composite; Skill+Cobra)"
]
},
"aitable.workflow_edit_example": {
"agent_summary": "获取 AI 表格工作流编辑文档与 workflow-dsl/v1 示例。",
"use_when": [
"创建或更新工作流前,需要确认最新 workflow-dsl/v1 结构、节点写法或完整示例时"
],
"avoid_when": [
"实际创建工作流用 workflow create;修改已有工作流用 workflow update;查询已发布定义用 workflow get"
],
"examples": [
"dws aitable workflow edit-example"
],
"reviewed": true,
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
"source_refs": [
"internal/cli/schema_command_registry.json#aitable.workflow_edit_example",
"cobra-help:dws aitable workflow edit-example --help",
"skills/mono/references/products/aitable/aitable-workflow.md",
"mcp-contract:aitable/edit_workflow_example"
]
},
"aitable.workflow_enable": {
"agent_summary": "启用工作流。",
"use_when": [
@@ -220,6 +220,29 @@
"live-mcp:dws schema wiki.list_wikiSpaces"
]
},
"wiki.list_workspace_feeds": {
"agent_summary": "查询知识库的活动动态:谁在什么时间更新/上传/评论了哪些文档",
"use_when": [
"用户问某个知识库最近有什么更新、谁改了什么、有哪些评论等协作动态时",
"需要巡检知识库变更并按时间线汇总成动态摘要时"
],
"avoid_when": [
"要看知识库当前有哪些节点用 node list;库内按关键词找文档用 node search",
"要读某篇文档正文用 doc read;跨库全局找文件用 drive search"
],
"examples": [
"dws wiki feed list --workspace <workspaceId> --format json",
"dws wiki feed list --workspace <workspaceId> --limit 10 --format json"
],
"reviewed": true,
"review_reason": "人工审阅:依据 wiki MCP 实时 tools/list 的 list_workspace_feeds description/inputSchema 与 Cobra Long 手写选型文案,并与 node list / node search / doc read / drive search 划清路由边界;不改变命令身份、参数契约或接口绑定。",
"source_refs": [
"CommandRegistry:canonical_path=wiki.list_workspace_feeds",
"cobra-help:dws wiki feed list",
"Skill:skills/mono/references/products/wiki.md",
"live-mcp-tools-list:wiki.list_workspace_feeds"
]
},
"wiki.node_copy": {
"agent_summary": "将知识库中的节点复制到指定位置",
"use_when": [
+27
View File
@@ -10666,6 +10666,33 @@
"rpc_name": "list_wikiSpaces"
}
},
"wiki.list_workspace_feeds": {
"title": "list_workspace_feeds",
"description": "功能概述:\r\n查询指定钉钉文档知识库的活动动态,包括谁在什么时间对哪些文档进行了更新、上传、评论等操作。通过传入 workspaceId(知识库 ID 或知识库 URL)定位目标知识库,返回动态列表,每条包含动态类型(type)、发生时间(time)和内容摘要(content)。支持游标分页(nextToken),可选排除文件相关动态(excludeFile)。\r\n\r\n适用场景:\r\n– 需要了解知识库最近有哪些文档被更新或上传了新文件。\r\n– 需要追踪知识库内的协作活动,如谁评论了哪篇文档。\r\n– 定期巡检知识库变更情况,生成动态摘要报告。\r\n\r\n注意事项:\r\n– 权限要求:操作者需具备知识库的成员权限,非成员会被拒绝访问。\r\n– 通过 workspaceId 定位知识库,支持知识库 ID(纯字符串)或知识库 URL(如 https://alidocs.dingtalk.com/i/spaces/{workspaceId}/overview),系统自动提取。\r\n– 每页默认返回 20 条动态,通过 maxResults 调整,最大 50 条。首页不传 nextToken,翻页时传入上次返回的 nextToken 值。\r\n– excludeFile 设为 true 可过滤掉文件相关的动态,默认 false 即返回全部动态类型。",
"parameters": {
"excludeFile": {
"type": "boolean",
"description": "是否排除文件相关的动态,选填,默认 false。"
},
"maxResults": {
"type": "number",
"description": "期望返回的最大动态条数,默认 20,最大 50。"
},
"nextToken": {
"type": "string",
"description": "分页游标,首次查询不传,后续翻页时传入上次返回的 nextToken。"
},
"workspaceId": {
"type": "string",
"description": "目标知识库的标识,支持两种格式:1) 知识库 ID(纯字符串);2) 知识库 URL,如 https://alidocs.dingtalk.com/i/spaces/{workspaceId}/overview,系统自动提取其中的 workspaceId。",
"required": true
}
},
"interface_ref": {
"product_id": "wiki",
"rpc_name": "list_workspace_feeds"
}
},
"wiki.node_copy": {
"title": "将指定节点复制到目标文件夹",
"description": "将指定节点复制到目标文件夹。\r\n\r\n支持的节点类型:知识库节点(文档、文件夹)、钉盘文件/文件夹。\r\nnodeId 支持文档 URL 或 dentryUuid(32 位字母数字字符串)。\r\ntargetFolderId 为目标文件夹的 dentryUuid;workspaceId 为目标知识库标识,不传 targetFolderId 时复制到该知识库根目录,如果不传 targetFolderId 和 workspaceId,默认到当前用户所在组织的「我的文档」下\r\n\r\n权限要求:\r\n- 对源节点有可查看下载权限\r\n- 对目标文件夹有写入权限\r\n\r\n注意:复制操作底层可能异步执行,异步时操作已提交但新节点 ID 无法立即返回,请稍后查看目标文件夹确认结果。",
+14 -2
View File
@@ -2,8 +2,8 @@
"version": 3,
"baseline": {
"manifest": "schema-parameter-bindings-v3",
"sha256": "sha256:ff0f76145dd27da5429434a348344c9cdf74c44eaf6f4ea5411d29d904a9066f",
"reason": "Reviewed v3 baseline after binding sheet.info --include to the include property while porting the wukong sheet info expansion flag.",
"sha256": "sha256:43147fbb7881204bc9cf9662543c7d49d7aefdfa82a2f9e684923547e0b8a0bd",
"reason": "Reviewed v3 baseline after binding wiki.list_workspace_feeds --workspace/--limit/--cursor/--exclude-file to the workspaceId/maxResults/nextToken/excludeFile properties while porting the wukong knowledge base feed query command.",
"reviewed": true
},
"removals": {
@@ -1324,6 +1324,12 @@
"limit": "pageSize",
"workspace": "workspaceId"
},
"wiki.list_workspace_feeds": {
"cursor": "nextToken",
"exclude-file": "excludeFile",
"limit": "maxResults",
"workspace": "workspaceId"
},
"wiki.node_copy": {
"folder": "targetFolderId",
"node": "nodeId",
@@ -1790,9 +1796,15 @@
"drive.apply_permission --reason": "Reviewed unpinned adapter: drive.apply_permission has no singular pinned interface_ref; --reason is a CLI wrapper input and does not publish a direct interface property.",
"drive.apply_permission --role": "Reviewed unpinned adapter: drive.apply_permission has no singular pinned interface_ref; --role is a CLI wrapper input and does not publish a direct interface property.",
"drive.apply_permission --users": "Reviewed unpinned adapter: drive.apply_permission has no singular pinned interface_ref; --users is a CLI wrapper input and does not publish a direct interface property.",
"drive.download_file --no-resume": "CLI-only transfer-layer flag controlling download resume behaviour; not an MCP interface parameter.",
"drive.download_file --output": "local output path",
"drive.download_file --parallel": "CLI-only transfer-layer flag controlling parallel chunk downloads; not an MCP interface parameter.",
"drive.download_file --part-size": "CLI-only transfer-layer flag controlling download chunk size; not an MCP interface parameter.",
"drive.download_file_version --no-resume": "Reviewed unpinned adapter: drive.download_file_version has no singular pinned interface_ref; --no-resume is a CLI wrapper input and does not publish a direct interface property.",
"drive.download_file_version --node": "Reviewed unpinned adapter: drive.download_file_version has no singular pinned interface_ref; --node is a CLI wrapper input and does not publish a direct interface property.",
"drive.download_file_version --output": "Reviewed unpinned adapter: drive.download_file_version has no singular pinned interface_ref; --output is a CLI wrapper input and does not publish a direct interface property.",
"drive.download_file_version --parallel": "Reviewed unpinned adapter: drive.download_file_version has no singular pinned interface_ref; --parallel is a CLI wrapper input and does not publish a direct interface property.",
"drive.download_file_version --part-size": "Reviewed unpinned adapter: drive.download_file_version has no singular pinned interface_ref; --part-size is a CLI wrapper input and does not publish a direct interface property.",
"drive.download_file_version --version": "Reviewed unpinned adapter: drive.download_file_version has no singular pinned interface_ref; --version is a CLI wrapper input and does not publish a direct interface property.",
"drive.get_cover --node": "Reviewed unpinned adapter: drive.get_cover has no singular pinned interface_ref; --node is a CLI wrapper input and does not publish a direct interface property.",
"drive.get_star_list --content-types": "Reviewed unpinned adapter: drive.get_star_list has no singular pinned interface_ref; --content-types is a CLI wrapper input and does not publish a direct interface property.",
+3 -1
View File
@@ -25,7 +25,9 @@ import (
)
// MetaFileName is the on-disk name of the bus metadata file. It lives
// alongside bus.lock and bus.sock inside the bus working directory.
// alongside bus.lock inside the bus working directory. Unix bus sockets live
// in a private per-user runtime directory so shared config filesystems do not
// need socket support.
const MetaFileName = "bus.meta"
// Meta is the JSON document written once at bus startup. Its primary
+3 -3
View File
@@ -30,9 +30,9 @@ import (
type SpawnFunc func(SpawnConfig) (pid int, err error)
// DiscoverConfig describes one discover attempt. WorkDir holds bus.lock and
// usually (on Unix) bus.sock — see dwsevent.IPCEndpoint for the short-path
// fallback when WorkDir is too deep; the caller must mkdir it with
// pkg/config.DirPerm beforehand.
// persistent bus metadata; Unix sockets live in a private per-user runtime
// directory so WorkDir may reside on a shared filesystem without socket
// support. The caller must mkdir WorkDir with pkg/config.DirPerm beforehand.
type DiscoverConfig struct {
WorkDir string
IPCEndpoint string
+2 -2
View File
@@ -69,8 +69,8 @@ type BusEntry struct {
// IPCEndpoint returns the IPC endpoint for this entry. Delegates to
// dwsevent.IPCEndpoint so status/stop dial exactly where consume and the
// bus daemon bound (including the short-path fallback when WorkDir is too
// deep for sun_path).
// bus daemon bound (a private per-user runtime path on Unix and a named pipe
// on Windows).
func (e BusEntry) IPCEndpoint() string {
hash := e.ClientIDHash
if e.IdentityHash != "" {
+36 -14
View File
@@ -17,8 +17,16 @@ import (
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
)
const eventRuntimeDirPrefix = "dws-event-"
func currentUserID() string {
return strconv.Itoa(os.Geteuid())
}
// MaxUnixSocketPath returns the longest Unix socket path accepted by
// bind/connect on this OS (Go rejects longer names with EINVAL before
// the syscall). sockaddr_un.sun_path is 104 bytes on darwin and the
@@ -35,17 +43,19 @@ func maxUnixSocketPath(goos string) int {
}
// IPCEndpoint returns the bus IPC endpoint for one identity: a Named Pipe
// name on Windows, otherwise bus.sock inside workDir.
// name on Windows, otherwise a deterministic Unix socket under a private
// per-user runtime directory.
//
// The canonical Unix location is <workDir>/bus.sock, but workDir derives
// from the config dir, which can be arbitrarily deep (e.g. dwssb sandboxes
// use ~/.dwssb/sandboxes/<name>/config/...). When the canonical path would
// exceed the OS sun_path limit, the socket falls back to a short
// deterministic path under os.TempDir keyed by a hash of workDir, so every
// process (consume parent, forked _bus child, status/stop tooling) that
// derives the endpoint from the same workDir agrees on the location.
// bus.lock / bus.meta / bus.log always stay in workDir — only the socket
// moves.
// Unix sockets must live on a local filesystem that supports bind(2).
// Config directories may reside on NFS, CSI, FUSE, or other shared mounts
// that reject Unix socket creation with ENOTSUPP. On Unix, the endpoint uses
// XDG_RUNTIME_DIR when it is absolute and short enough; otherwise it falls
// back to a per-UID directory under os.TempDir. The transport creates and
// validates that directory as owner-only before listening or dialing. The
// socket name is keyed by a hash of workDir so every process (consume parent,
// forked _bus child, status/stop tooling) that derives the endpoint from the
// same workDir agrees on the location. bus.lock / bus.meta / bus.log always
// stay in workDir.
//
// This is the single source of truth for endpoint derivation; the cobra
// layer and busctl must not re-implement the shape.
@@ -60,9 +70,21 @@ func ipcEndpointForOS(goos, workDir, editionName string, sourceKind SourceKind,
if goos == "windows" {
return `\\.\pipe\dws-event-` + editionName + "-" + string(sourceKind) + "-" + identityHash
}
sock := filepath.Join(workDir, "bus.sock")
if len(sock) <= maxUnixSocketPath(goos) {
return sock
return unixSocketEndpoint(goos, workDir, strings.TrimSpace(os.Getenv("XDG_RUNTIME_DIR")), os.TempDir())
}
func unixSocketEndpoint(goos, workDir, runtimeDir, tempDir string) string {
socketName := "dws-evt-" + IdentityHash(workDir) + ".sock"
userDirName := eventRuntimeDirPrefix + currentUserID()
if filepath.IsAbs(runtimeDir) {
candidate := filepath.Join(runtimeDir, userDirName, socketName)
if len(candidate) <= maxUnixSocketPath(goos) {
return candidate
}
}
return filepath.Join(os.TempDir(), "dws-evt-"+IdentityHash(workDir)+".sock")
fallback := filepath.Join(tempDir, userDirName, socketName)
if len(fallback) <= maxUnixSocketPath(goos) {
return fallback
}
return filepath.Join("/tmp", userDirName, socketName)
}
+19 -1
View File
@@ -14,6 +14,7 @@
package event
import (
"os"
"path/filepath"
"strings"
"testing"
@@ -33,10 +34,27 @@ func TestCrossPlatformCoverageEndpointPortableCoverageEdges(t *testing.T) {
if got := ipcEndpointForOS("windows", "ignored", "open", "", "hash"); got != `\\.\pipe\dws-event-open-app_stream-hash` {
t.Fatalf("Windows endpoint = %q", got)
}
if got := ipcEndpointForOS("darwin", "short", "open", SourceKindPersonalStream, "hash"); got != filepath.Join("short", "bus.sock") {
runtimeRoot := filepath.VolumeName(os.TempDir()) + string(filepath.Separator)
runtimeDir := filepath.Join(runtimeRoot, "dws-xdg-runtime")
t.Setenv("XDG_RUNTIME_DIR", runtimeDir)
workDir := "portable-xdg-workdir"
wantXDG := filepath.Join(runtimeDir, eventRuntimeDirPrefix+currentUserID(), "dws-evt-"+IdentityHash(workDir)+".sock")
if got := ipcEndpointForOS("linux", workDir, "open", SourceKindPersonalStream, "hash"); got != wantXDG {
t.Fatalf("XDG Unix endpoint = %q, want %q", got, wantXDG)
}
t.Setenv("XDG_RUNTIME_DIR", "")
if got := ipcEndpointForOS("darwin", "short", "open", SourceKindPersonalStream, "hash"); got != filepath.Join(os.TempDir(), eventRuntimeDirPrefix+currentUserID(), "dws-evt-"+IdentityHash("short")+".sock") {
t.Fatalf("short Unix endpoint = %q", got)
}
if got := ipcEndpointForOS("darwin", strings.Repeat("x", 200), "open", SourceKindAppStream, "hash"); !strings.Contains(got, "dws-evt-") {
t.Fatalf("long Unix endpoint = %q", got)
}
longTempDir := filepath.Join(string(filepath.Separator), strings.Repeat("long-temp-root", 20))
wantShortFallback := filepath.Join("/tmp", eventRuntimeDirPrefix+currentUserID(), "dws-evt-"+IdentityHash(workDir)+".sock")
if got := unixSocketEndpoint("darwin", workDir, "", longTempDir); got != wantShortFallback {
t.Fatalf("overlong temp endpoint = %q, want %q", got, wantShortFallback)
}
}
+57 -5
View File
@@ -23,6 +23,7 @@ import (
)
func TestEndpointPlatformVariants(t *testing.T) {
t.Setenv("XDG_RUNTIME_DIR", "")
if maxUnixSocketPath("linux") != 107 || maxUnixSocketPath("darwin") != 103 {
t.Fatal("Unix socket limits changed")
}
@@ -31,7 +32,7 @@ func TestEndpointPlatformVariants(t *testing.T) {
t.Fatalf("Windows pipe = %q", pipe)
}
short := ipcEndpointForOS("darwin", "/tmp/events", "open", SourceKindPersonalStream, "hash")
if short != filepath.Join("/tmp/events", "bus.sock") {
if short != filepath.Join(os.TempDir(), eventRuntimeDirPrefix+currentUserID(), "dws-evt-"+IdentityHash("/tmp/events")+".sock") {
t.Fatalf("short Unix endpoint = %q", short)
}
long := ipcEndpointForOS("darwin", "/"+strings.Repeat("deep/", 40), "open", SourceKindAppStream, "hash")
@@ -40,16 +41,40 @@ func TestEndpointPlatformVariants(t *testing.T) {
}
}
func TestIPCEndpointShortWorkDirUsesCanonicalPath(t *testing.T) {
workDir := "/tmp/dws/events/open/app_stream/aabbccdd00112233"
func TestIPCEndpointUsesXDGUserRuntimeDir(t *testing.T) {
tempRoot, err := filepath.EvalSymlinks("/tmp")
if err != nil {
t.Fatalf("EvalSymlinks: %v", err)
}
runtimeDir, err := os.MkdirTemp(tempRoot, "dws-xdg-")
if err != nil {
t.Fatalf("MkdirTemp: %v", err)
}
t.Cleanup(func() { _ = os.RemoveAll(runtimeDir) })
t.Setenv("XDG_RUNTIME_DIR", runtimeDir)
workDir := "/shared/events/open/app_stream/aabbccdd00112233"
got := IPCEndpoint(workDir, "open", SourceKindAppStream, "aabbccdd00112233")
want := filepath.Join(workDir, "bus.sock")
want := filepath.Join(runtimeDir, eventRuntimeDirPrefix+currentUserID(), "dws-evt-"+IdentityHash(workDir)+".sock")
if got != want {
t.Fatalf("IPCEndpoint = %q, want %q", got, want)
}
}
func TestIPCEndpointLongWorkDirFallsBackUnderTempDir(t *testing.T) {
func TestIPCEndpointWithoutXDGUsesPerUserLocalTempDir(t *testing.T) {
t.Setenv("XDG_RUNTIME_DIR", "")
workDir := "/tmp/dws/events/open/app_stream/aabbccdd00112233"
got := IPCEndpoint(workDir, "open", SourceKindAppStream, "aabbccdd00112233")
want := filepath.Join(os.TempDir(), eventRuntimeDirPrefix+currentUserID(), "dws-evt-"+IdentityHash(workDir)+".sock")
if got != want {
t.Fatalf("IPCEndpoint = %q, want %q", got, want)
}
if strings.HasPrefix(got, workDir) {
t.Fatalf("IPCEndpoint = %q, want endpoint outside workDir", got)
}
}
func TestIPCEndpointLongWorkDirUsesLocalTempDir(t *testing.T) {
t.Setenv("XDG_RUNTIME_DIR", "")
// Mirrors the dwssb sandbox layout that produced a 111-byte socket
// path — over macOS's 103-byte usable sun_path budget.
workDir := "/Users/zhengyubai/.dwssb/sandboxes/event-subscribe/config/events/open/personal_stream/3928ce0fb4860a52"
@@ -66,6 +91,7 @@ func TestIPCEndpointLongWorkDirFallsBackUnderTempDir(t *testing.T) {
}
func TestIPCEndpointFallbackIsDeterministicPerWorkDir(t *testing.T) {
t.Setenv("XDG_RUNTIME_DIR", "")
long := strings.Repeat("x", 120)
a := IPCEndpoint("/base/"+long+"/one", "open", SourceKindPersonalStream, "hash")
b := IPCEndpoint("/base/"+long+"/one", "open", SourceKindPersonalStream, "hash")
@@ -77,3 +103,29 @@ func TestIPCEndpointFallbackIsDeterministicPerWorkDir(t *testing.T) {
t.Fatalf("different workDirs collided on endpoint %q", a)
}
}
func TestIPCEndpointLongXDGPathFallsBackToTempDir(t *testing.T) {
t.Setenv("XDG_RUNTIME_DIR", "/"+strings.Repeat("runtime/", 30))
workDir := "/shared/events/open/personal_stream/aabbccdd00112233"
got := ipcEndpointForOS("linux", workDir, "open", SourceKindPersonalStream, "hash")
wantPrefix := filepath.Join(os.TempDir(), eventRuntimeDirPrefix+currentUserID()) + string(filepath.Separator)
if !strings.HasPrefix(got, wantPrefix) {
t.Fatalf("IPCEndpoint = %q, want fallback under %q", got, wantPrefix)
}
if len(got) > maxUnixSocketPath("linux") {
t.Fatalf("fallback path still too long: %d > %d (%q)", len(got), maxUnixSocketPath("linux"), got)
}
}
func TestIPCEndpointLongTempDirUsesShortSystemFallback(t *testing.T) {
workDir := "/shared/events/open/personal_stream/aabbccdd00112233"
longTempDir := "/" + strings.Repeat("long-temp-root/", 20)
got := unixSocketEndpoint("linux", workDir, "", longTempDir)
want := filepath.Join("/tmp", eventRuntimeDirPrefix+currentUserID(), "dws-evt-"+IdentityHash(workDir)+".sock")
if got != want {
t.Fatalf("IPCEndpoint = %q, want short fallback %q", got, want)
}
if len(got) > maxUnixSocketPath("linux") {
t.Fatalf("short fallback path too long: %d > %d (%q)", len(got), maxUnixSocketPath("linux"), got)
}
}
@@ -73,6 +73,83 @@ func TestCrossPlatformCoverageUnixListenErrorCoverage(t *testing.T) {
}
}
func TestCrossPlatformCoverageUnixSocketDirectoryErrorCoverage(t *testing.T) {
oldLstat, oldRuntimeStat, oldMkdir := lstatSocketPath, statSocketRuntimeRoot, mkdirSocketDir
t.Cleanup(func() {
lstatSocketPath, statSocketRuntimeRoot, mkdirSocketDir = oldLstat, oldRuntimeStat, oldMkdir
})
wantErr := errors.New("synthetic socket directory failure")
if err := ensureSocketDir("relative/bus.sock", true); err == nil || !strings.Contains(err.Error(), "must be absolute") {
t.Fatalf("relative socket path error = %v", err)
}
root := shortSecureTempDir(t)
missingRootPath := filepath.Join(root, "missing-root", "dws-event-test", "bus.sock")
if err := ensureSocketDir(missingRootPath, false); err == nil || !errors.Is(err, os.ErrNotExist) {
t.Fatalf("missing runtime root error = %v", err)
}
rootInfo, err := oldLstat(root)
if err != nil {
t.Fatalf("lstat secure root: %v", err)
}
lstatSocketPath = func(path string) (os.FileInfo, error) {
if path == "/tmp" {
return fileInfoWithMode{FileInfo: rootInfo, mode: os.ModeSymlink | 0o777}, nil
}
return oldLstat(path)
}
statSocketRuntimeRoot = func(path string) (os.FileInfo, error) {
if path == "/tmp" {
return nil, wantErr
}
return oldRuntimeStat(path)
}
if err := ensureSocketDir("/tmp/dws-event-coverage/bus.sock", false); !errors.Is(err, wantErr) {
t.Fatalf("runtime root resolution error = %v", err)
}
lstatSocketPath, statSocketRuntimeRoot = oldLstat, oldRuntimeStat
rootFile := filepath.Join(root, "runtime-root-file")
if err := os.WriteFile(rootFile, []byte("not a directory"), 0o600); err != nil {
t.Fatalf("write runtime root file: %v", err)
}
if err := ensureSocketDir(filepath.Join(rootFile, "dws-event-test", "bus.sock"), false); err == nil || !strings.Contains(err.Error(), "runtime root is not a directory") {
t.Fatalf("non-directory runtime root error = %v", err)
}
mkdirSocketDir = func(string, os.FileMode) error { return wantErr }
if err := ensureSocketDir(filepath.Join(root, "mkdir-failure", "bus.sock"), true); !errors.Is(err, wantErr) {
t.Fatalf("socket directory creation error = %v", err)
}
mkdirSocketDir = oldMkdir
if err := ensureSocketDir(filepath.Join(root, "missing-socket-dir", "bus.sock"), false); err == nil || !errors.Is(err, os.ErrNotExist) {
t.Fatalf("missing socket directory error = %v", err)
}
withoutOwner := fileInfoWithoutOwner{FileInfo: rootInfo}
if err := validateSocketRuntimeRoot(root, withoutOwner, uint32(os.Geteuid())); err == nil || !strings.Contains(err.Error(), "owner") {
t.Fatalf("runtime root owner error = %v", err)
}
if err := validatePrivateSocketDir(root, withoutOwner, uint32(os.Geteuid())); err == nil || !strings.Contains(err.Error(), "owner") {
t.Fatalf("socket directory owner error = %v", err)
}
}
type fileInfoWithMode struct {
os.FileInfo
mode os.FileMode
}
func (f fileInfoWithMode) Mode() os.FileMode { return f.mode }
func (f fileInfoWithMode) IsDir() bool { return f.mode.IsDir() }
type fileInfoWithoutOwner struct{ os.FileInfo }
func (fileInfoWithoutOwner) Sys() any { return struct{}{} }
type stubNetListener struct {
close func() error
}
+98 -5
View File
@@ -16,9 +16,12 @@
package transport
import (
"errors"
"fmt"
"net"
"os"
"path/filepath"
"syscall"
dwsevent "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/config"
@@ -30,10 +33,13 @@ type unixListener struct {
}
var (
statSocket = os.Stat
removeSocket = os.Remove
listenUnix = net.Listen
chmodSocket = os.Chmod
statSocket = os.Stat
removeSocket = os.Remove
listenUnix = net.Listen
chmodSocket = os.Chmod
lstatSocketPath = os.Lstat
statSocketRuntimeRoot = os.Stat
mkdirSocketDir = os.Mkdir
)
func (u *unixListener) Accept() (net.Conn, error) { return u.l.Accept() }
@@ -56,11 +62,95 @@ func checkSocketPath(path string) error {
return nil
}
// ensureSocketDir makes the socket's immediate parent an owner-only
// directory and rejects unsafe pre-existing paths. The parent of that
// directory must itself either be private to the effective user (for
// XDG_RUNTIME_DIR and macOS temporary roots) or sticky (for Linux /tmp), so
// another user cannot rename the private directory out from under us.
func ensureSocketDir(path string, create bool) error {
if !filepath.IsAbs(path) {
return fmt.Errorf("transport: unix socket path must be absolute: %s", path)
}
dir := filepath.Dir(path)
root := filepath.Dir(dir)
rootInfo, err := lstatSocketPath(root)
if err != nil {
return fmt.Errorf("transport: inspect socket runtime root %s: %w", root, err)
}
// macOS exposes the system /tmp as a root-owned symlink to /private/tmp.
// Follow only that well-known alias, then apply the same ownership/sticky
// validation to its target. Arbitrary runtime-root symlinks remain rejected.
if rootInfo.Mode()&os.ModeSymlink != 0 && filepath.Clean(root) == "/tmp" {
rootInfo, err = statSocketRuntimeRoot(root)
if err != nil {
return fmt.Errorf("transport: resolve socket runtime root %s: %w", root, err)
}
}
if err := validateSocketRuntimeRoot(root, rootInfo, uint32(os.Geteuid())); err != nil {
return err
}
if create {
if err := mkdirSocketDir(dir, config.DirPerm); err != nil && !errors.Is(err, os.ErrExist) {
return fmt.Errorf("transport: create socket directory %s: %w", dir, err)
}
}
dirInfo, err := lstatSocketPath(dir)
if err != nil {
return fmt.Errorf("transport: inspect socket directory %s: %w", dir, err)
}
return validatePrivateSocketDir(dir, dirInfo, uint32(os.Geteuid()))
}
func validateSocketRuntimeRoot(path string, info os.FileInfo, effectiveUID uint32) error {
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("transport: socket runtime root is not a directory: %s", path)
}
owner, err := fileOwnerUID(info)
if err != nil {
return fmt.Errorf("transport: inspect socket runtime root owner %s: %w", path, err)
}
privateOwnerRoot := owner == effectiveUID && info.Mode().Perm()&0o022 == 0
stickyRoot := info.Mode()&os.ModeSticky != 0
if !privateOwnerRoot && !stickyRoot {
return fmt.Errorf("transport: socket runtime root is neither private nor sticky: %s", path)
}
return nil
}
func validatePrivateSocketDir(path string, info os.FileInfo, effectiveUID uint32) error {
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("transport: socket directory is not a directory: %s", path)
}
owner, err := fileOwnerUID(info)
if err != nil {
return fmt.Errorf("transport: inspect socket directory owner %s: %w", path, err)
}
if owner != effectiveUID {
return fmt.Errorf("transport: socket directory %s is owned by uid %d, want %d", path, owner, effectiveUID)
}
if perm := info.Mode().Perm(); perm != config.DirPerm {
return fmt.Errorf("transport: socket directory %s has permissions %04o, want %04o", path, perm, config.DirPerm)
}
return nil
}
func fileOwnerUID(info os.FileInfo) (uint32, error) {
stat, ok := info.Sys().(*syscall.Stat_t)
if !ok {
return 0, errors.New("stat result does not expose an owner uid")
}
return stat.Uid, nil
}
func listen(path string) (Listener, error) {
if err := checkSocketPath(path); err != nil {
return nil, err
}
// Stale socket cleanup. Caller holds bus.lock so this is race-safe.
if err := ensureSocketDir(path, true); err != nil {
return nil, err
}
// The private per-user parent excludes other users. The caller's bus.lock
// serializes stale-socket cleanup for processes using the same WorkDir.
if _, err := statSocket(path); err == nil {
if err := removeSocket(path); err != nil {
return nil, fmt.Errorf("transport: remove stale socket %s: %w", path, err)
@@ -82,5 +172,8 @@ func dial(path string) (net.Conn, error) {
if err := checkSocketPath(path); err != nil {
return nil, err
}
if err := ensureSocketDir(path, false); err != nil {
return nil, err
}
return net.Dial("unix", path)
}
+138 -5
View File
@@ -21,12 +21,32 @@ import (
"net"
"os"
"path/filepath"
"strings"
"sync"
"testing"
dwsevent "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event"
)
func shortSecureTempDir(t *testing.T) string {
t.Helper()
tempRoot, err := filepath.EvalSymlinks("/tmp")
if err != nil {
t.Fatalf("EvalSymlinks: %v", err)
}
dir, err := os.MkdirTemp(tempRoot, "dws-et-")
if err != nil {
t.Fatalf("MkdirTemp: %v", err)
}
if err := os.Chmod(dir, 0o700); err != nil {
t.Fatalf("chmod temp dir: %v", err)
}
t.Cleanup(func() { _ = os.RemoveAll(dir) })
return dir
}
func TestListen_DialRoundtrip(t *testing.T) {
path := filepath.Join(t.TempDir(), "bus.sock")
path := filepath.Join(shortSecureTempDir(t), "bus.sock")
l, err := Listen(path)
if err != nil {
t.Fatalf("Listen: %v", err)
@@ -87,7 +107,7 @@ func TestListen_DialRoundtrip(t *testing.T) {
}
func TestListen_StaleSocketCleanup(t *testing.T) {
path := filepath.Join(t.TempDir(), "bus.sock")
path := filepath.Join(shortSecureTempDir(t), "bus.sock")
// Pre-create a stale file at path (not a valid socket).
if err := os.WriteFile(path, []byte("stale"), 0o600); err != nil {
t.Fatalf("pre-create: %v", err)
@@ -99,8 +119,121 @@ func TestListen_StaleSocketCleanup(t *testing.T) {
defer l.Close()
}
func TestCrossPlatformCoverageListenCreatesPrivateSocketDirectory(t *testing.T) {
dir := filepath.Join(shortSecureTempDir(t), "dws-event-test")
path := filepath.Join(dir, "bus.sock")
l, err := Listen(path)
if err != nil {
t.Fatalf("Listen: %v", err)
}
defer l.Close()
st, err := os.Stat(dir)
if err != nil {
t.Fatalf("stat socket directory: %v", err)
}
if mode := st.Mode().Perm(); mode != 0o700 {
t.Fatalf("socket directory mode = %04o, want 0700", mode)
}
}
func TestCrossPlatformCoverageListenRejectsWorldAccessibleSocketDirectory(t *testing.T) {
dir := filepath.Join(shortSecureTempDir(t), "dws-event-test")
if err := os.Mkdir(dir, 0o700); err != nil {
t.Fatalf("mkdir: %v", err)
}
if err := os.Chmod(dir, 0o777); err != nil {
t.Fatalf("chmod: %v", err)
}
if _, err := Listen(filepath.Join(dir, "bus.sock")); err == nil || !strings.Contains(err.Error(), "want 0700") {
t.Fatalf("Listen error = %v, want 0700 directory rejection", err)
}
}
func TestCrossPlatformCoverageDialRejectsWorldAccessibleSocketDirectory(t *testing.T) {
dir := filepath.Join(shortSecureTempDir(t), "dws-event-test")
if err := os.Mkdir(dir, 0o700); err != nil {
t.Fatalf("mkdir: %v", err)
}
if err := os.Chmod(dir, 0o777); err != nil {
t.Fatalf("chmod: %v", err)
}
if _, err := Dial(filepath.Join(dir, "bus.sock")); err == nil || !strings.Contains(err.Error(), "want 0700") {
t.Fatalf("Dial error = %v, want 0700 directory rejection", err)
}
}
func TestCrossPlatformCoverageListenRejectsSymlinkSocketDirectory(t *testing.T) {
root := shortSecureTempDir(t)
target := filepath.Join(root, "target")
if err := os.Mkdir(target, 0o700); err != nil {
t.Fatalf("mkdir target: %v", err)
}
link := filepath.Join(root, "dws-event-test")
if err := os.Symlink(target, link); err != nil {
t.Fatalf("symlink: %v", err)
}
if _, err := Listen(filepath.Join(link, "bus.sock")); err == nil || !strings.Contains(err.Error(), "not a directory") {
t.Fatalf("Listen error = %v, want symlink directory rejection", err)
}
}
func TestCrossPlatformCoverageValidatePrivateSocketDirRejectsDifferentOwner(t *testing.T) {
dir := shortSecureTempDir(t)
st, err := os.Lstat(dir)
if err != nil {
t.Fatalf("lstat: %v", err)
}
otherUID := uint32(os.Geteuid() + 1)
if err := validatePrivateSocketDir(dir, st, otherUID); err == nil || !strings.Contains(err.Error(), "is owned by uid") {
t.Fatalf("validatePrivateSocketDir error = %v, want owner mismatch", err)
}
}
func TestCrossPlatformCoverageListenRejectsUntrustedRuntimeRoot(t *testing.T) {
root := filepath.Join(shortSecureTempDir(t), "untrusted")
if err := os.Mkdir(root, 0o700); err != nil {
t.Fatalf("mkdir root: %v", err)
}
if err := os.Chmod(root, 0o777); err != nil {
t.Fatalf("chmod root: %v", err)
}
dir := filepath.Join(root, "dws-event-test")
if err := os.Mkdir(dir, 0o700); err != nil {
t.Fatalf("mkdir socket dir: %v", err)
}
if _, err := Listen(filepath.Join(dir, "bus.sock")); err == nil || !strings.Contains(err.Error(), "neither private nor sticky") {
t.Fatalf("Listen error = %v, want untrusted runtime root rejection", err)
}
}
func TestCrossPlatformCoverageListenSharedWorkDirUsesLocalSecureRuntimeEndpoint(t *testing.T) {
root := shortSecureTempDir(t)
runtimeDir := filepath.Join(root, "runtime")
if err := os.Mkdir(runtimeDir, 0o700); err != nil {
t.Fatalf("mkdir runtime: %v", err)
}
t.Setenv("XDG_RUNTIME_DIR", runtimeDir)
sharedWorkDir := filepath.Join(root, "simulated-nfs", "events", "open", "personal_stream", "identity")
endpoint := dwsevent.IPCEndpoint(sharedWorkDir, "open", dwsevent.SourceKindPersonalStream, "identity")
if strings.HasPrefix(endpoint, sharedWorkDir) {
t.Fatalf("endpoint = %q, want socket outside shared WorkDir %q", endpoint, sharedWorkDir)
}
l, err := Listen(endpoint)
if err != nil {
t.Fatalf("Listen on local runtime endpoint: %v", err)
}
defer l.Close()
if mode, err := os.Stat(filepath.Dir(endpoint)); err != nil {
t.Fatalf("stat runtime socket directory: %v", err)
} else if mode.Mode().Perm() != 0o700 {
t.Fatalf("runtime socket directory mode = %04o, want 0700", mode.Mode().Perm())
}
}
func TestListen_CloseUnlinksSocket(t *testing.T) {
path := filepath.Join(t.TempDir(), "bus.sock")
path := filepath.Join(shortSecureTempDir(t), "bus.sock")
l, err := Listen(path)
if err != nil {
t.Fatalf("Listen: %v", err)
@@ -114,7 +247,7 @@ func TestListen_CloseUnlinksSocket(t *testing.T) {
}
func TestDial_NoServerReturnsError(t *testing.T) {
path := filepath.Join(t.TempDir(), "nonexistent.sock")
path := filepath.Join(shortSecureTempDir(t), "nonexistent.sock")
if _, err := Dial(path); err == nil {
t.Fatal("Dial to nonexistent socket should error")
}
@@ -124,7 +257,7 @@ func TestDial_NoServerReturnsError(t *testing.T) {
// surfaces as io.EOF to the server's Reader — the EOF signal is what bus
// uses to unregister dead consumers (plan invariant #5).
func TestReader_HandlesPeerCloseEOF(t *testing.T) {
path := filepath.Join(t.TempDir(), "bus.sock")
path := filepath.Join(shortSecureTempDir(t), "bus.sock")
l, err := Listen(path)
if err != nil {
t.Fatalf("Listen: %v", err)
+13 -1
View File
@@ -853,6 +853,7 @@ func newAitableCommand() *cobra.Command {
dws aitable form [list|delete|update] 表单管理
dws aitable form field [list|update|hide] 表单字段管理
dws aitable form share [get|update|notify] 表单分享管理
dws aitable workflow [edit-example|create|update|enable|disable|get|list] 自动化工作流管理
dws aitable dashboard [get|create|update|delete|config-example] 仪表盘管理
dws aitable chart [get|create|update|delete|widgets-example] 图表管理
dws aitable export data 数据导出
@@ -3260,6 +3261,17 @@ valid=false 仍表示 DSL 校验或发布未通过,必须读取 issues 修正
},
}
workflowEditExampleCmd := &cobra.Command{
Use: "edit-example",
Short: "获取工作流编辑文档与示例",
Long: `返回服务端提供的 AI 表格工作流编辑文档与示例。
可作为 workflow create / workflow update 的 workflow-dsl/v1 结构参考;此命令不需要 Base ID 或其他参数。`,
Example: ` dws aitable workflow edit-example`,
RunE: func(cmd *cobra.Command, args []string) error {
return callAitableTool("edit_workflow_example", map[string]any{})
},
}
workflowUpdateCmd := &cobra.Command{
Use: "update",
Short: "更新并发布已有自动化工作流",
@@ -4856,7 +4868,7 @@ parentSectionId 为空串表示该节点在 Base 根目录下。
workflowListCmd.Flags().Int("limit", 0, "分页大小 [1, 100],不传走服务端默认 20")
workflowListCmd.Flags().Int("offset", 0, "分页偏移量,>= 0,不传走服务端默认 0")
workflowCmd.AddCommand(
workflowCreateCmd, workflowUpdateCmd,
workflowEditExampleCmd, workflowCreateCmd, workflowUpdateCmd,
workflowEnableCmd, workflowDisableCmd,
workflowGetCmd, workflowListCmd,
)
@@ -94,6 +94,23 @@ func TestAitableWorkflowCreateMapsDSLWithoutRetry(t *testing.T) {
}
}
func TestAitableWorkflowEditExampleMapsEmptyArguments(t *testing.T) {
caller, err := runAitableWorkflowCommand(t, nil, "edit-example")
if err != nil {
t.Fatalf("workflow edit-example returned error: %v", err)
}
if len(caller.calls) != 1 {
t.Fatalf("tool call count = %d, want 1", len(caller.calls))
}
call := caller.calls[0]
if call.productID != "aitable" || call.toolName != "edit_workflow_example" {
t.Fatalf("tool call = %s/%s, want aitable/edit_workflow_example", call.productID, call.toolName)
}
if len(call.args) != 0 {
t.Fatalf("tool args = %#v, want empty arguments", call.args)
}
}
func TestAitableWorkflowUpdateReadsDSLFile(t *testing.T) {
path := t.TempDir() + "/workflow.json"
if err := os.WriteFile(path, []byte(`{"version":"workflow-dsl/v1","name":"updated"}`), 0o600); err != nil {
+14 -2
View File
@@ -3434,6 +3434,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
conversationID := mustGetFlag(cmd, "open-conversation-id")
messageID := mustGetFlag(cmd, "message-id")
outputPath := mustGetFlag(cmd, "output")
jsonMode := deps.Caller.Format() == "json"
switch resourceType {
case "mediaId":
@@ -3455,7 +3456,9 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
ctx := context.Background()
// Step 1: 获取下载 URL
deps.Out.PrintInfo("[1/2] 获取资源下载链接...")
if !jsonMode {
deps.Out.PrintInfo("[1/2] 获取资源下载链接...")
}
text, err := callMCPToolReturnTextOnServer(ctx, "im", "get_resource_download_url", map[string]any{
"resourceType": resourceType,
"resourceId": resourceID,
@@ -3488,11 +3491,20 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
}
// Step 2: HTTP GET 下载文件
deps.Out.PrintInfo(fmt.Sprintf("[2/2] 下载资源到 %s ...", outputPath))
if !jsonMode {
deps.Out.PrintInfo(fmt.Sprintf("[2/2] 下载资源到 %s ...", outputPath))
}
if err := httpGetFile(ctx, resourceURL, dlHeaders, outputPath); err != nil {
return err
}
if jsonMode {
return deps.Out.PrintJSONUnescaped(map[string]any{
"success": true,
"downloadUrl": resourceURL,
"output": outputPath,
})
}
deps.Out.PrintInfo(fmt.Sprintf("下载完成: %s", outputPath))
return nil
},
@@ -0,0 +1,95 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package helpers
import (
"bytes"
"context"
"encoding/json"
"io"
"os"
"path/filepath"
"strings"
"testing"
)
func TestChatDownloadMediaJSONPreservesLegacyResult(t *testing.T) {
previousDeps, previousArgs := deps, os.Args
previousHTTPGetFile := httpGetFile
os.Args = []string{"dws", "chat"}
t.Cleanup(func() {
deps = previousDeps
os.Args = previousArgs
httpGetFile = previousHTTPGetFile
})
const downloadURL = "https://download.example.test/photo.jpg?token=one&part=two"
caller := &scriptedToolCaller{
format: "json",
steps: []scriptedToolStep{{
text: `{"resourceUrl":"` + downloadURL + `"}`,
}},
}
InitDeps(caller)
var stdout bytes.Buffer
deps.Out = &Formatter{w: &stdout, errW: io.Discard}
outputDir := t.TempDir()
wantOutput := filepath.Join(outputDir, "photo.jpg")
httpGetFile = func(_ context.Context, gotURL string, _ map[string]string, gotOutput string) error {
if gotURL != downloadURL {
t.Fatalf("download URL = %q, want %q", gotURL, downloadURL)
}
if gotOutput != wantOutput {
t.Fatalf("download output = %q, want %q", gotOutput, wantOutput)
}
return nil
}
root := newChatCommand()
installExampleGlobalFlags(root)
root.SilenceErrors = true
root.SilenceUsage = true
root.SetOut(io.Discard)
root.SetErr(io.Discard)
root.SetArgs([]string{
"message", "download-media",
"--type=mediaId",
"--resource-id=resource",
"--message-id=message",
"--open-conversation-id=conversation",
"--output=" + outputDir,
})
if err := root.ExecuteContext(context.Background()); err != nil {
t.Fatal(err)
}
var got struct {
Success bool `json:"success"`
DownloadURL string `json:"downloadUrl"`
Output string `json:"output"`
}
if err := json.Unmarshal(stdout.Bytes(), &got); err != nil {
t.Fatalf("stdout is not JSON: %v\n%s", err, stdout.String())
}
if !got.Success || got.DownloadURL != downloadURL || got.Output != wantOutput {
t.Fatalf("result = %#v, want success=true downloadUrl=%q output=%q", got, downloadURL, wantOutput)
}
if strings.Contains(stdout.String(), "[INFO]") {
t.Fatalf("JSON stdout contains progress text: %s", stdout.String())
}
if !strings.Contains(stdout.String(), "?token=one&part=two") {
t.Fatalf("downloadUrl was escaped or changed: %s", stdout.String())
}
}
+4 -2
View File
@@ -276,7 +276,8 @@ func defaultHTTPPutFile(ctx context.Context, url string, headers map[string]stri
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("OSS upload failed: HTTP %d: %s", resp.StatusCode, string(body))
// typed httpStatusError 供上层按 401/403 分支重取凭证
return fmt.Errorf("OSS upload failed: %w", &httpStatusError{StatusCode: resp.StatusCode, Body: string(body)})
}
return nil
@@ -434,7 +435,8 @@ func defaultHTTPGetFile(ctx context.Context, url string, headers map[string]stri
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
// typed httpStatusError 供上层按 401/403 分支重取凭证
return &httpStatusError{StatusCode: resp.StatusCode, Body: string(body)}
}
outFile, err := docCreateDestination(destPath)
+136 -14
View File
@@ -476,7 +476,8 @@ func newDriveCommand() *cobra.Command {
--output 指定本地保存路径,可以是文件路径或目录。
如果指定目录,文件名从下载 URL 中自动推断。`,
Example: ` dws drive download --node <dentryUuid> --output ./report.pdf
dws drive download --node <dentryUuid> --output ~/downloads/`,
dws drive download --node <dentryUuid> --output ~/downloads/
dws drive download --node <dentryUuid> --output ./big.zip --part-size 32MB --parallel 8`,
RunE: func(cmd *cobra.Command, args []string) error {
fileID := flagOrFallback(cmd, "node", "file-id")
if fileID == "" {
@@ -492,6 +493,15 @@ func newDriveCommand() *cobra.Command {
argsMap["spaceId"] = v
}
// fail-fast:分片下载参数校验
dlOpts, err := driveDownloadOptionsFromFlags(cmd)
if err != nil {
return err
}
dlOpts.logf = func(format string, a ...any) {
deps.Out.PrintInfo(fmt.Sprintf(format, a...))
}
if deps.Caller.DryRun() {
deps.Out.PrintKeyValue("操作", "下载钉盘文件")
deps.Out.PrintKeyValue("文件ID", fileID)
@@ -499,7 +509,7 @@ func newDriveCommand() *cobra.Command {
return nil
}
ctx := context.Background()
ctx := cmd.Context()
// Step 1: 获取下载 URL 和签名请求头
deps.Out.PrintInfo("[1/2] 获取下载链接...")
@@ -508,7 +518,7 @@ func newDriveCommand() *cobra.Command {
return err
}
resourceURL, dlHeaders, err := parseDownloadInfo(text)
resourceURL, dlHeaders, err := parseDriveDownloadInfo(text)
if err != nil {
return err
}
@@ -523,9 +533,34 @@ func newDriveCommand() *cobra.Command {
outputPath = filepath.Join(outputPath, filename)
}
// Step 2: HTTP GET 下载文件
// Step 2: 分片下载(自动分派 + 401/403 凭证刷新重试)
deps.Out.PrintInfo(fmt.Sprintf("[2/2] 下载文件到 %s ...", outputPath))
if err := httpGetFile(ctx, resourceURL, dlHeaders, outputPath); err != nil {
dlOpts.knownSize = parseDownloadFileSize(text)
dlOpts.nodeID = fileID
dlOpts.version = parseDownloadFileVersion(text)
fetchCred := func(fctx context.Context) (string, map[string]string, int, error) {
t, ferr := callMCPToolReturnText(fctx, "download_file", argsMap)
if ferr != nil {
return "", nil, 0, ferr
}
u, h, perr := parseDriveDownloadInfo(t)
if perr != nil {
return "", nil, 0, perr
}
return u, h, parseDownloadFileVersion(t), nil
}
if err := driveTransferDownload(ctx, fetchCred, resourceURL, dlHeaders, outputPath, dlOpts); err != nil {
if errors.Is(err, context.Canceled) || ctx.Err() == context.Canceled {
partSize, _ := cmd.Flags().GetString("part-size")
noResume, _ := cmd.Flags().GetBool("no-resume")
if partSize != "" && !noResume {
fmt.Fprintf(cmd.ErrOrStderr(), "\n[INFO] 下载中断,已保存断点(可重新执行相同命令续传)\n")
} else {
fmt.Fprintf(cmd.ErrOrStderr(), "\n[INFO] 下载中断\n")
}
cmd.SilenceErrors = true
return err
}
return err
}
@@ -564,6 +599,15 @@ func newDriveCommand() *cobra.Command {
return fmt.Errorf("flag --output is required")
}
// fail-fast:分片下载参数校验
dlOpts, err := driveDownloadOptionsFromFlags(cmd)
if err != nil {
return err
}
dlOpts.logf = func(format string, a ...any) {
deps.Out.PrintInfo(fmt.Sprintf(format, a...))
}
if deps.Caller.DryRun() {
deps.Out.PrintKeyValue("操作", "下载文件历史版本")
deps.Out.PrintKeyValue("节点ID", fileID)
@@ -572,16 +616,17 @@ func newDriveCommand() *cobra.Command {
return nil
}
ctx := context.Background()
ctx := cmd.Context()
deps.Out.PrintInfo("[1/2] 获取历史版本下载链接...")
text, err := callMCPToolReturnTextOnServer(ctx, "drive", "download_file_version", map[string]any{
dlArgsMap := map[string]any{
"nodeId": fileID,
"version": versionNum,
})
}
text, err := callMCPToolReturnTextOnServer(ctx, "drive", "download_file_version", dlArgsMap)
if err != nil {
return err
}
resourceURL, dlHeaders, err := parseDownloadInfo(text)
resourceURL, dlHeaders, err := parseDriveDownloadInfo(text)
if err != nil {
return err
}
@@ -593,7 +638,32 @@ func newDriveCommand() *cobra.Command {
outputPath = filepath.Join(outputPath, filename)
}
deps.Out.PrintInfo(fmt.Sprintf("[2/2] 下载文件到 %s ...", outputPath))
if err := httpGetFile(ctx, resourceURL, dlHeaders, outputPath); err != nil {
dlOpts.knownSize = parseDownloadFileSize(text)
dlOpts.nodeID = fileID
dlOpts.version = versionNum
fetchCred := func(fctx context.Context) (string, map[string]string, int, error) {
t, ferr := callMCPToolReturnTextOnServer(fctx, "drive", "download_file_version", dlArgsMap)
if ferr != nil {
return "", nil, 0, ferr
}
u, h, perr := parseDriveDownloadInfo(t)
if perr != nil {
return "", nil, 0, perr
}
return u, h, parseDownloadFileVersion(t), nil
}
if err := driveTransferDownload(ctx, fetchCred, resourceURL, dlHeaders, outputPath, dlOpts); err != nil {
if errors.Is(err, context.Canceled) || ctx.Err() == context.Canceled {
partSize, _ := cmd.Flags().GetString("part-size")
noResume, _ := cmd.Flags().GetBool("no-resume")
if partSize != "" && !noResume {
fmt.Fprintf(cmd.ErrOrStderr(), "\n[INFO] 下载中断,已保存断点(可重新执行相同命令续传)\n")
} else {
fmt.Fprintf(cmd.ErrOrStderr(), "\n[INFO] 下载中断\n")
}
cmd.SilenceErrors = true
return err
}
return err
}
deps.Out.PrintInfo(fmt.Sprintf("下载完成: %s", outputPath))
@@ -709,10 +779,16 @@ func newDriveCommand() *cobra.Command {
driveDownloadCmd.Flags().String("node", "", "文件 ID (dentryUuid) (必填)")
driveDownloadCmd.Flags().String("space-id", "", "文件所属空间 ID (可选)")
driveDownloadCmd.Flags().String("output", "", "本地保存路径 (文件路径或目录,必填)")
driveDownloadCmd.Flags().String("part-size", "16MB", "分片下载的分片大小,如 8MB/16MB/1GB,范围 1MB-1GB (可选)")
driveDownloadCmd.Flags().Int("parallel", 4, "分片下载并发数,范围 1-8 (可选)")
driveDownloadCmd.Flags().Bool("no-resume", false, "关闭断点续传 (可选)")
driveDownloadVersionCmd.Flags().String("node", "", "文件 ID (dentryUuid) 或 URL (必填)")
driveDownloadVersionCmd.Flags().Int("version", 0, "历史版本号 (必填,正整数,从 drive list --versions 获取)")
driveDownloadVersionCmd.Flags().String("output", "", "本地保存路径 (文件路径或目录,必填)")
driveDownloadVersionCmd.Flags().String("part-size", "16MB", "分片下载的分片大小,如 8MB/16MB/1GB,范围 1MB-1GB (可选)")
driveDownloadVersionCmd.Flags().Int("parallel", 4, "分片下载并发数,范围 1-8 (可选)")
driveDownloadVersionCmd.Flags().Bool("no-resume", false, "关闭断点续传 (可选)")
for _, alias := range []string{"url", "id", "node-id", "doc-id", "file-id"} {
driveDownloadVersionCmd.Flags().String(alias, "", "")
_ = driveDownloadVersionCmd.Flags().MarkHidden(alias)
@@ -2006,13 +2082,13 @@ func uploadToDrive(ctx context.Context, filePath, fileName string, fileSize int6
if err != nil {
return err
}
resourceURL, uploadID, headers, err := parseDriveUploadInfo(text)
// HTTP PUT 上传文件(OSS 与中心协议同路径;headers 透传,401/403 重取凭证重试一次)
uploadID, err := driveUploadPut(ctx, text, func(rctx context.Context) (string, error) {
return callMCPToolReturnTextOnServer(rctx, "drive", "get_upload_info", step1Args)
}, filePath, fileSize)
if err != nil {
return err
}
if err := httpPutFile(ctx, resourceURL, headers, filePath, fileSize); err != nil {
return err
}
commitArgs := map[string]any{
"fileName": fileName,
@@ -2219,3 +2295,49 @@ func isPermissionCLIError(err error) bool {
var patErr *PATError
return errors.As(err, &patErr)
}
// parseDriveDownloadInfo 从 drive 的 download_file 返回里取下载 URL 与请求头。
// drive 返回形如 {"result":{"downloadUrl":"https://..."}};OSS 预签名 URL 自带签名参数、
// 无额外请求头;中心协议(httpToCenterWithToken)返回的 headers 含 dentry-token,
// 需原样透传。对历史字段名(resourceUrl / resourceUrls[].url)做 fallback。
func parseDriveDownloadInfo(text string) (string, map[string]string, error) {
var data map[string]any
if err := json.Unmarshal([]byte(text), &data); err != nil {
return "", nil, fmt.Errorf("解析 download_file 返回失败: %w", err)
}
if result, ok := data["result"].(map[string]any); ok {
data = result
}
headers := make(map[string]string)
if h, ok := data["headers"].(map[string]any); ok {
for k, v := range h {
if s, ok := v.(string); ok {
headers[k] = s
}
}
}
dlURL, _ := data["downloadUrl"].(string)
if dlURL == "" {
dlURL, _ = data["resourceUrl"].(string)
}
if dlURL == "" {
if arr, ok := data["resourceUrls"].([]any); ok && len(arr) > 0 {
if first, ok := arr[0].(map[string]any); ok {
dlURL, _ = first["url"].(string)
if h, ok := first["headers"].(map[string]any); ok {
for k, v := range h {
if s, ok := v.(string); ok {
headers[k] = s
}
}
}
}
}
}
if dlURL == "" {
return "", nil, fmt.Errorf("download_file 未返回下载链接(downloadUrl 为空)")
}
return dlURL, headers, nil
}
+847
View File
@@ -0,0 +1,847 @@
package helpers
// ──────────────────────────────────────────────────────────
// drive 传输增强:中心协议(token 化凭证)+ 分片下载(Range)
//
// 下载:文件大小 ≥ 2×part-size 时自动分片并发下载(对齐 aws s3 cp /
// ossutil 惯例),断点续传默认开启(<dest>.dwspart 临时文件 + checkpoint
// 元信息),服务端不支持 Range 时自动回退整流;401/403(凭证过期)自动
// 重新调用 MCP 取新凭证后续传。
// 上传:中心协议(uploadType=httpToCenterWithToken)与 OSS 走同一 PUT
// 路径(URL 服务端拼好、headers 透传、客户端零追加),401/403 重取凭证
// 重试一次;服务端超限错误补充可读提示。
// ──────────────────────────────────────────────────────────
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/spf13/cobra"
)
const (
driveDownloadDefaultPartSize = 16 * 1024 * 1024 // --part-size 默认 16MB
driveDownloadMinPartSize = 1 * 1024 * 1024
driveDownloadMaxPartSize = 1024 * 1024 * 1024
driveDownloadDefaultParallel = 4
driveDownloadMaxParallel = 8
// 单分片常规失败指数退避重试次数(不含 401/403 凭证刷新)。
driveDownloadPartRetries = 3
// 单分片 401/403 凭证刷新重试上限(防止无效凭证死循环)。
driveDownloadPartAuthRetries = 2
drivePartFileSuffix = ".dwspart"
drivePartMetaSuffix = ".dwspart.meta"
driveCheckpointVersion = 1
uploadTypeCenterToken = "httpToCenterWithToken"
driveTransferBodyErrCap = 2048 // 错误响应 body 截断长度
)
// driveRangeClient 分片下载/探测专用 HTTP 客户端。
// 整流路径仍走可注入的 httpGetFile,保持既有测试注入点不变。
var driveRangeClient = &http.Client{Timeout: 10 * time.Minute}
// errCredentialRefreshVersionUnknown 凭证刷新后无法验证文件版本一致性(双方之一
// version=0),激进策略要求清空已完成分片从头下载。
var errCredentialRefreshVersionUnknown = errors.New("凭证刷新后无法验证文件版本一致性,需从头下载")
// Testable OS operation hooks (package-level for coverage injection).
var (
driveJsonMarshal = json.Marshal
driveOsRename = os.Rename
driveFileTruncate = (*os.File).Truncate
driveFileSync = (*os.File).Sync
driveFileStat = (*os.File).Stat
)
// ──────────────────────────────────────────────────────────
// HTTP 状态错误
// ──────────────────────────────────────────────────────────
// httpStatusError 表示非 2xx 的 HTTP 响应,供上层按状态码分支(401/403 重取凭证等)。
type httpStatusError struct {
StatusCode int
Body string
}
func (e *httpStatusError) Error() string {
return fmt.Sprintf("HTTP %d: %s", e.StatusCode, e.Body)
}
// isAuthStatusError 判断错误是否为 401/403(凭证过期/无效)。
// 兼容 typed httpStatusError 与文本形态(测试注入或历史包装的 "HTTP 401/403" 错误)。
func isAuthStatusError(err error) bool {
if err == nil {
return false
}
var se *httpStatusError
if errors.As(err, &se) {
return se.StatusCode == http.StatusUnauthorized || se.StatusCode == http.StatusForbidden
}
msg := err.Error()
return strings.Contains(msg, "HTTP 401") || strings.Contains(msg, "HTTP 403")
}
// ──────────────────────────────────────────────────────────
// 参数解析
// ──────────────────────────────────────────────────────────
// parsePartSize 解析 --part-size 的人类可读值(16MB、512KB、1GB;纯数字按字节)。
func parsePartSize(s string) (int64, error) {
v := strings.TrimSpace(strings.ToUpper(s))
if v == "" {
return 0, fmt.Errorf("--part-size 不能为空(示例: 16MB、512KB、1GB)")
}
unit := int64(1)
switch {
case strings.HasSuffix(v, "GB"):
unit, v = 1<<30, strings.TrimSuffix(v, "GB")
case strings.HasSuffix(v, "MB"):
unit, v = 1<<20, strings.TrimSuffix(v, "MB")
case strings.HasSuffix(v, "KB"):
unit, v = 1<<10, strings.TrimSuffix(v, "KB")
case strings.HasSuffix(v, "G"):
unit, v = 1<<30, strings.TrimSuffix(v, "G")
case strings.HasSuffix(v, "M"):
unit, v = 1<<20, strings.TrimSuffix(v, "M")
case strings.HasSuffix(v, "K"):
unit, v = 1<<10, strings.TrimSuffix(v, "K")
case strings.HasSuffix(v, "B"):
v = strings.TrimSuffix(v, "B")
}
n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64)
if err != nil || n <= 0 {
return 0, fmt.Errorf("--part-size 格式非法: %q(示例: 16MB、512KB、1GB)", s)
}
size := n * unit
if size < driveDownloadMinPartSize || size > driveDownloadMaxPartSize {
return 0, fmt.Errorf("--part-size 取值范围 %s - %s,当前值: %s",
formatByteSize(driveDownloadMinPartSize), formatByteSize(driveDownloadMaxPartSize), s)
}
return size, nil
}
func formatByteSize(n int64) string {
switch {
case n >= 1<<30 && n%(1<<30) == 0:
return fmt.Sprintf("%dGB", n/(1<<30))
case n >= 1<<20 && n%(1<<20) == 0:
return fmt.Sprintf("%dMB", n/(1<<20))
case n >= 1<<10 && n%(1<<10) == 0:
return fmt.Sprintf("%dKB", n/(1<<10))
default:
return fmt.Sprintf("%dB", n)
}
}
// driveDownloadOptions 分片下载选项(由 drive download 的 flag 解析而来)。
type driveDownloadOptions struct {
partSize int64
parallel int
resume bool
knownSize int64 // MCP 返回的 fileSize;未知(0)或小于阈值时直接整流
nodeID string // 节点唯一标识(dentryUuid),用于生成 checkpoint 指纹
version int // 文件版本号;0 表示最新版
logf func(format string, args ...any)
}
// driveDownloadOptionsFromFlags 解析并校验 --part-size / --parallel / --no-resume。
func driveDownloadOptionsFromFlags(cmd *cobra.Command) (driveDownloadOptions, error) {
raw, _ := cmd.Flags().GetString("part-size")
partSize, err := parsePartSize(raw)
if err != nil {
return driveDownloadOptions{}, err
}
parallel, _ := cmd.Flags().GetInt("parallel")
if parallel < 1 || parallel > driveDownloadMaxParallel {
return driveDownloadOptions{}, fmt.Errorf("--parallel 取值范围 1-%d,当前值: %d", driveDownloadMaxParallel, parallel)
}
noResume, _ := cmd.Flags().GetBool("no-resume")
return driveDownloadOptions{partSize: partSize, parallel: parallel, resume: !noResume}, nil
}
// parseDownloadFileSize 从 download_file 返回中提取 fileSize(缺失/非法返回 0)。
func parseDownloadFileSize(text string) int64 {
var data map[string]any
if json.Unmarshal([]byte(text), &data) != nil {
return 0
}
if r, ok := data["result"].(map[string]any); ok {
data = r
}
switch v := data["fileSize"].(type) {
case float64:
return int64(v)
case string:
n, _ := strconv.ParseInt(v, 10, 64)
return n
}
return 0
}
// parseDownloadFileVersion 从 MCP download_file 响应中提取文件当前版本号。
// 返回 0 表示未获取到(兼容旧版 MCP 不返回 version 的场景)。
func parseDownloadFileVersion(text string) int {
var data map[string]any
if json.Unmarshal([]byte(text), &data) != nil {
return 0
}
if r, ok := data["result"].(map[string]any); ok {
data = r
}
switch v := data["version"].(type) {
case float64:
return int(v)
case string:
n, _ := strconv.Atoi(v)
return n
}
return 0
}
// ──────────────────────────────────────────────────────────
// 凭证状态(分片过程共享,401/403 时 single-flight 刷新)
// ──────────────────────────────────────────────────────────
// driveCredentialFetcher 重新调用 MCP 获取下载 URL + headers(含 dentry-token)。
type driveCredentialFetcher func(ctx context.Context) (url string, headers map[string]string, version int, err error)
type driveCredentialState struct {
mu sync.Mutex
fetch driveCredentialFetcher
url string
headers map[string]string
gen int
initialVersion int // 首次获取的文件版本号;0 表示未知(兼容旧 MCP)
}
func (cs *driveCredentialState) current() (string, map[string]string, int) {
cs.mu.Lock()
defer cs.mu.Unlock()
return cs.url, cs.headers, cs.gen
}
// refresh 重取凭证。仅当调用方持有的 generation 仍是最新时才真正重取
// (其他并发分片已刷新过则直接复用新凭证,避免重复 MCP 调用)。
func (cs *driveCredentialState) refresh(ctx context.Context, gen int) error {
cs.mu.Lock()
defer cs.mu.Unlock()
if cs.gen > gen {
return nil // 已被其他分片刷新
}
if cs.fetch == nil {
return fmt.Errorf("下载凭证已过期且无法自动刷新")
}
url, headers, version, err := cs.fetch(ctx)
if err != nil {
return err
}
// 版本校验
if cs.initialVersion > 0 && version > 0 {
if version != cs.initialVersion {
// 双方版本已知且不同 → 文件已被覆盖,终止下载防止数据不一致
return fmt.Errorf("下载凭证刷新后文件版本已变更(%d → %d),终止下载以防数据不一致", cs.initialVersion, version)
}
// 版本一致,正常继续
} else {
// 激进策略:版本不可验证(至少一方为 0),更新凭证但返回特殊错误
// 让上层清空已完成分片从头下载
cs.url, cs.headers = url, headers
cs.gen++
return errCredentialRefreshVersionUnknown
}
cs.url, cs.headers = url, headers
cs.gen++
return nil
}
// ──────────────────────────────────────────────────────────
// 下载入口:整流 / 分片自动分派
// ──────────────────────────────────────────────────────────
// driveTransferDownload 下载入口:按文件大小自动选择整流或分片下载。
// - 文件大小未知(MCP 未返回 fileSize)或 < 2×partSize → 直接整流
// (复用可注入的 httpGetFile,保持存量行为与测试注入边界不变,
// 401/403 时重取凭证重试一次);
// - 已知大小 ≥ 2×partSize → 首请求以 Range: bytes=0-0 探测:206 且解析出
// 总长 → 分片下载;服务端返回 200(不支持 Range)→ 直接消费该响应整流落盘。
func driveTransferDownload(ctx context.Context, fetch driveCredentialFetcher, rawURL string, headers map[string]string, destPath string, opts driveDownloadOptions) error {
if opts.partSize <= 0 {
opts.partSize = driveDownloadDefaultPartSize
}
if opts.parallel <= 0 {
opts.parallel = driveDownloadDefaultParallel
}
threshold := 2 * opts.partSize
if opts.knownSize < threshold {
// 含 knownSize==0(大小未知):不发起额外探测请求,保持存量整流行为
return downloadSingleWithAuthRetry(ctx, fetch, rawURL, headers, destPath)
}
creds := &driveCredentialState{fetch: fetch, url: rawURL, headers: headers, initialVersion: opts.version}
totalSize, fullResp, err := probeRangeSupport(ctx, creds)
if err != nil {
return err
}
if fullResp != nil {
// 服务端不支持 Range(忽略探测请求头返回 200 全量流):直接消费落盘
defer fullResp.Body.Close()
return writeStreamToFile(fullResp.Body, destPath)
}
curURL, curHeaders, _ := creds.current()
if totalSize <= 0 || totalSize < threshold {
// 总长未知(Content-Range 异常)或小于阈值:整流下载
return downloadSingleWithAuthRetry(ctx, func(fctx context.Context) (string, map[string]string, int, error) {
if fetch == nil {
return "", nil, 0, fmt.Errorf("下载凭证已过期且无法自动刷新")
}
return fetch(fctx)
}, curURL, curHeaders, destPath)
}
return downloadRangedParts(ctx, creds, destPath, totalSize, opts)
}
// downloadSingleWithAuthRetry 整流下载;401/403(凭证过期)时重新调用 MCP
// 获取新 URL+token 重试一次,仍失败走既有错误路径。
func downloadSingleWithAuthRetry(ctx context.Context, fetch driveCredentialFetcher, urlStr string, headers map[string]string, destPath string) error {
err := httpGetFile(ctx, urlStr, headers, destPath)
if err == nil || !isAuthStatusError(err) || fetch == nil {
return err
}
newURL, newHeaders, _, ferr := fetch(ctx)
if ferr != nil {
return err
}
return httpGetFile(ctx, newURL, newHeaders, destPath)
}
// probeRangeSupport 发送 Range: bytes=0-0 探测请求验证 206/Content-Range。
// 返回 (totalSize, nil, nil) 表示支持 Range 且已知总长;
// 返回 (0, resp, nil) 表示服务端忽略 Range 返回 200 全量响应(调用方直接消费);
// 401/403 时刷新凭证重试一次。
func probeRangeSupport(ctx context.Context, creds *driveCredentialState) (int64, *http.Response, error) {
for attempt := 0; ; attempt++ {
urlStr, headers, gen := creds.current()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, urlStr, nil)
if err != nil {
return 0, nil, err
}
for k, v := range headers {
req.Header.Set(k, v)
}
req.Header.Set("Range", "bytes=0-0")
resp, err := driveRangeClient.Do(req)
if err != nil {
return 0, nil, err
}
switch {
case resp.StatusCode == http.StatusPartialContent:
total, perr := parseContentRangeTotal(resp.Header.Get("Content-Range"))
_, _ = io.Copy(io.Discard, resp.Body)
resp.Body.Close()
if perr != nil {
return 0, nil, nil // Content-Range 异常:总长未知,回退整流
}
return total, nil, nil
case resp.StatusCode == http.StatusOK:
return 0, resp, nil
case resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden:
body, _ := io.ReadAll(io.LimitReader(resp.Body, driveTransferBodyErrCap))
resp.Body.Close()
if attempt > 0 {
return 0, nil, fmt.Errorf("下载凭证刷新后仍鉴权失败")
}
if rerr := creds.refresh(ctx, gen); rerr != nil && !errors.Is(rerr, errCredentialRefreshVersionUnknown) {
return 0, nil, fmt.Errorf("重新获取下载凭证失败: %w (原错误: %v)",
rerr, &httpStatusError{StatusCode: resp.StatusCode, Body: string(body)})
}
// errCredentialRefreshVersionUnknown 在探测阶段无需处理(尚无已完成分片),
// 凭证已更新,循环继续用新凭证重试探测。
default:
body, _ := io.ReadAll(io.LimitReader(resp.Body, driveTransferBodyErrCap))
resp.Body.Close()
return 0, nil, &httpStatusError{StatusCode: resp.StatusCode, Body: string(body)}
}
}
}
// parseContentRangeTotal 从 "bytes 0-0/12345" 解析总长;"*" 视为未知。
func parseContentRangeTotal(cr string) (int64, error) {
idx := strings.LastIndex(cr, "/")
if idx < 0 || idx == len(cr)-1 {
return 0, fmt.Errorf("非法 Content-Range: %q", cr)
}
totalStr := cr[idx+1:]
if totalStr == "*" {
return 0, fmt.Errorf("Content-Range 总长未知: %q", cr)
}
total, err := strconv.ParseInt(totalStr, 10, 64)
if err != nil || total <= 0 {
return 0, fmt.Errorf("非法 Content-Range 总长: %q", cr)
}
return total, nil
}
// parseContentRange 解析 "bytes <start>-<end>/<total>" 格式的 Content-Range 头。
// 返回值 start、end 为字节偏移(闭区间),total 为文件总长("*" 视为 -1)。
func parseContentRange(header string) (start, end, total int64, err error) {
if header == "" {
return 0, 0, 0, fmt.Errorf("Content-Range 为空")
}
// 去掉 "bytes " 前缀
const prefix = "bytes "
if !strings.HasPrefix(header, prefix) {
return 0, 0, 0, fmt.Errorf("非法 Content-Range 前缀: %q", header)
}
rest := header[len(prefix):] // e.g. "0-1048575/104857600"
// 按 "/" 分割范围与总长
slashIdx := strings.LastIndex(rest, "/")
if slashIdx < 0 || slashIdx == len(rest)-1 {
return 0, 0, 0, fmt.Errorf("非法 Content-Range 格式: %q", header)
}
rangePart := rest[:slashIdx] // "0-1048575"
totalPart := rest[slashIdx+1:] // "104857600" 或 "*"
// 解析 total
if totalPart == "*" {
total = -1
} else {
total, err = strconv.ParseInt(totalPart, 10, 64)
if err != nil || total <= 0 {
return 0, 0, 0, fmt.Errorf("非法 Content-Range 总长: %q", header)
}
}
// 按 "-" 分割 start 和 end
dashIdx := strings.Index(rangePart, "-")
if dashIdx < 0 {
return 0, 0, 0, fmt.Errorf("非法 Content-Range 区间: %q", header)
}
start, err = strconv.ParseInt(rangePart[:dashIdx], 10, 64)
if err != nil || start < 0 {
return 0, 0, 0, fmt.Errorf("非法 Content-Range start: %q", header)
}
end, err = strconv.ParseInt(rangePart[dashIdx+1:], 10, 64)
if err != nil || end < start {
return 0, 0, 0, fmt.Errorf("非法 Content-Range end: %q", header)
}
return start, end, total, nil
}
func writeStreamToFile(r io.Reader, destPath string) error {
out, err := os.Create(destPath)
if err != nil {
return err
}
defer out.Close()
if _, err := io.Copy(out, r); err != nil {
return err
}
return nil
}
// ──────────────────────────────────────────────────────────
// 分片切分与 checkpoint
// ──────────────────────────────────────────────────────────
type driveDownloadPart struct {
index int
offset int64
length int64
}
// splitDownloadParts 按 partSize 等长切片,末片为余量。
func splitDownloadParts(totalSize, partSize int64) []driveDownloadPart {
if totalSize <= 0 || partSize <= 0 {
return nil
}
count := int((totalSize + partSize - 1) / partSize)
parts := make([]driveDownloadPart, 0, count)
for i := 0; i < count; i++ {
offset := int64(i) * partSize
length := partSize
if offset+length > totalSize {
length = totalSize - offset
}
parts = append(parts, driveDownloadPart{index: i, offset: offset, length: length})
}
return parts
}
// driveDownloadCheckpoint 断点续传元信息,随每个分片完成原子落盘。
type driveDownloadCheckpoint struct {
Version int `json:"version"`
Fingerprint string `json:"fingerprint"`
TotalSize int64 `json:"totalSize"`
PartSize int64 `json:"partSize"`
Completed []bool `json:"completed"`
}
// driveDownloadFingerprint 基于节点 ID + 版本号 + 文件总长 + 资源 URL 计算指纹。
// version>0 时只取 URL path(重签名不影响 checkpoint 复用);version==0(最新版)时
// 取完整 path+query——中心协议相同 path 可能对应不同实际版本,query 中的签名/token
// 标识了具体资源快照,防止错误复用旧 checkpoint。
// resourceURL 为空时不影响其他字段的指纹计算(安全降级)。
func driveDownloadFingerprint(nodeID string, version int, totalSize int64, resourceURL string) string {
urlComponent := ""
if resourceURL != "" {
if u, err := url.Parse(resourceURL); err == nil && u != nil {
if version == 0 {
// 最新版:含 query 以区分不同签名(不同实际版本)
urlComponent = u.RequestURI()
} else {
// 指定版本:只取 path,重签名不应废弃 checkpoint
urlComponent = u.Path
}
}
}
sum := sha256.Sum256([]byte(fmt.Sprintf("%s|%d|%d|%s", nodeID, version, totalSize, urlComponent)))
return hex.EncodeToString(sum[:])
}
// loadDriveDownloadCheckpoint 读取并校验 checkpoint;任一字段不匹配返回 nil(从头下载)。
func loadDriveDownloadCheckpoint(metaPath, fingerprint string, totalSize, partSize int64, partCount int) *driveDownloadCheckpoint {
data, err := os.ReadFile(metaPath)
if err != nil {
return nil
}
var cp driveDownloadCheckpoint
if err := json.Unmarshal(data, &cp); err != nil {
return nil
}
if cp.Version != driveCheckpointVersion || cp.Fingerprint != fingerprint ||
cp.TotalSize != totalSize || cp.PartSize != partSize || len(cp.Completed) != partCount {
return nil
}
return &cp
}
// save 原子写入(临时文件 + rename),避免中断产生半截 checkpoint。
func (cp *driveDownloadCheckpoint) save(metaPath string) error {
data, err := driveJsonMarshal(cp)
if err != nil {
return err
}
tmp := metaPath + ".tmp"
if err := os.WriteFile(tmp, data, 0o644); err != nil {
return err
}
return driveOsRename(tmp, metaPath)
}
// ──────────────────────────────────────────────────────────
// 分片下载引擎
// ──────────────────────────────────────────────────────────
// downloadRangedParts 并发分片下载到 <dest>.dwspart,全部完成后校验总长并
// 原子重命名为 destPath、清理 checkpoint;中途失败保留分片产物供断点续传。
func downloadRangedParts(ctx context.Context, creds *driveCredentialState, destPath string, totalSize int64, opts driveDownloadOptions) error {
parts := splitDownloadParts(totalSize, opts.partSize)
partPath := destPath + drivePartFileSuffix
metaPath := destPath + drivePartMetaSuffix
// 取首次凭证 URL 用于指纹计算,防止同大小文件覆盖后 checkpoint 错误复用
initialURL, _, _ := creds.current()
fingerprint := driveDownloadFingerprint(opts.nodeID, opts.version, totalSize, initialURL)
var cp *driveDownloadCheckpoint
if opts.resume {
cp = loadDriveDownloadCheckpoint(metaPath, fingerprint, totalSize, opts.partSize, len(parts))
// 分片数据文件缺失或长度不符时 checkpoint 作废,从头下载
if cp != nil {
if fi, err := os.Stat(partPath); err != nil || fi.Size() != totalSize {
cp = nil
}
}
} else {
// --no-resume:清理历史断点产物,从头下载且不写 checkpoint
_ = os.Remove(partPath)
_ = os.Remove(metaPath)
}
if cp == nil {
cp = &driveDownloadCheckpoint{
Version: driveCheckpointVersion,
Fingerprint: fingerprint,
TotalSize: totalSize,
PartSize: opts.partSize,
Completed: make([]bool, len(parts)),
}
}
f, err := os.OpenFile(partPath, os.O_RDWR|os.O_CREATE, 0o644)
if err != nil {
return fmt.Errorf("创建分片临时文件失败: %w", err)
}
if err := driveFileTruncate(f, totalSize); err != nil {
f.Close()
return fmt.Errorf("预分配分片临时文件失败: %w", err)
}
remaining := 0
for _, p := range parts {
if !cp.Completed[p.index] {
remaining++
}
}
if opts.logf != nil {
if remaining < len(parts) {
opts.logf("断点续传: 共 %d 分片(%s/片,并发 %d),已完成 %d,续传 %d",
len(parts), formatByteSize(opts.partSize), opts.parallel, len(parts)-remaining, remaining)
} else {
opts.logf("分片下载: 共 %d 分片(%s/片,并发 %d)", len(parts), formatByteSize(opts.partSize), opts.parallel)
}
}
runCtx, cancel := context.WithCancel(ctx)
defer cancel()
var (
mu sync.Mutex // 保护 cp 与 checkpoint 落盘
wg sync.WaitGroup
errOnce sync.Once
firstErr error
)
fail := func(err error) {
errOnce.Do(func() {
firstErr = err
cancel()
})
}
jobs := make(chan driveDownloadPart)
workers := opts.parallel
if workers > remaining {
workers = remaining
}
if workers < 1 {
workers = 1
}
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for part := range jobs {
if runCtx.Err() != nil {
return
}
if err := downloadOnePart(runCtx, creds, f, part, totalSize); err != nil {
fail(fmt.Errorf("分片 %d/%d 下载失败: %w", part.index+1, len(parts), err))
return
}
mu.Lock()
cp.Completed[part.index] = true
var saveErr error
if opts.resume {
saveErr = cp.save(metaPath)
}
mu.Unlock()
if saveErr != nil {
fail(fmt.Errorf("写入下载断点信息失败: %w", saveErr))
return
}
}
}()
}
dispatch:
for _, part := range parts {
if cp.Completed[part.index] {
continue
}
select {
case jobs <- part:
case <-runCtx.Done():
break dispatch
}
}
close(jobs)
wg.Wait()
if firstErr != nil {
f.Close()
if errors.Is(firstErr, errCredentialRefreshVersionUnknown) {
// 激进策略:版本不可验证,清空 checkpoint 和分片文件防止错误续传;
// 用户重跑时将自然从头下载。
_ = os.Remove(metaPath)
_ = os.Remove(partPath)
}
return firstErr // 其他错误保留 .dwspart 与 checkpoint,重跑同一命令自动续传
}
if err := driveFileSync(f); err != nil {
f.Close()
return err
}
fi, statErr := driveFileStat(f)
f.Close()
if statErr != nil {
return statErr
}
if fi.Size() != totalSize {
return fmt.Errorf("下载完成但文件长度不符: got %d, want %d", fi.Size(), totalSize)
}
if err := driveOsRename(partPath, destPath); err != nil {
return fmt.Errorf("重命名下载文件失败: %w", err)
}
_ = os.Remove(metaPath)
return nil
}
// downloadOnePart 下载单个分片:常规失败指数退避重试 driveDownloadPartRetries 次;
// 401/403 触发凭证 single-flight 刷新(不计入常规重试,上限 driveDownloadPartAuthRetries),
// 刷新后用新凭证续传,不重下其他已完成分片。
func downloadOnePart(ctx context.Context, creds *driveCredentialState, f *os.File, part driveDownloadPart, totalSize int64) error {
attempt := 0
authRetries := 0
backoff := 500 * time.Millisecond
for {
urlStr, headers, gen := creds.current()
err := fetchRangeInto(ctx, urlStr, headers, f, part, totalSize)
if err == nil {
return nil
}
if ctx.Err() != nil {
return err
}
if isAuthStatusError(err) && authRetries < driveDownloadPartAuthRetries {
authRetries++
if rerr := creds.refresh(ctx, gen); rerr != nil {
return fmt.Errorf("重新获取下载凭证失败: %w (原错误: %v)", rerr, err)
}
continue // 凭证刷新不计常规重试、不退避
}
attempt++
if attempt > driveDownloadPartRetries {
return err
}
select {
case <-time.After(backoff):
case <-ctx.Done():
return err
}
backoff *= 2
}
}
// fetchRangeInto 拉取 [offset, offset+length) 区间并写入文件对应偏移。
func fetchRangeInto(ctx context.Context, urlStr string, headers map[string]string, f *os.File, part driveDownloadPart, expectedTotal int64) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, urlStr, nil)
if err != nil {
return err
}
for k, v := range headers {
req.Header.Set(k, v)
}
req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", part.offset, part.offset+part.length-1))
resp, err := driveRangeClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusPartialContent {
body, _ := io.ReadAll(io.LimitReader(resp.Body, driveTransferBodyErrCap))
return &httpStatusError{StatusCode: resp.StatusCode, Body: string(body)}
}
// 校验 Content-Range 响应区间与请求分片一致,防止代理/服务端返回错位数据
cr := resp.Header.Get("Content-Range")
if cr == "" {
return fmt.Errorf("分片响应缺少 Content-Range 头,无法验证数据偏移一致性")
}
{
crStart, crEnd, crTotal, crErr := parseContentRange(cr)
if crErr != nil {
return fmt.Errorf("Content-Range 解析失败: %w", crErr)
}
wantStart := part.offset
wantEnd := part.offset + part.length - 1
if crStart != wantStart || crEnd != wantEnd {
return fmt.Errorf("Content-Range 区间不匹配: 响应 %d-%d, 期望 %d-%d",
crStart, crEnd, wantStart, wantEnd)
}
if crTotal > 0 && expectedTotal > 0 && crTotal != expectedTotal {
return fmt.Errorf("Content-Range 总长不匹配: 响应 %d, 期望 %d", crTotal, expectedTotal)
}
}
n, err := io.Copy(io.NewOffsetWriter(f, part.offset), io.LimitReader(resp.Body, part.length))
if err != nil {
return err
}
if n != part.length {
return fmt.Errorf("分片长度不符: got %d, want %d", n, part.length)
}
return nil
}
// ──────────────────────────────────────────────────────────
// 上传:中心协议识别 + 401/403 重试 + 超限可读提示
// ──────────────────────────────────────────────────────────
// parseDriveUploadType 提取 get_upload_info 返回中的 uploadType(可选字段)。
// "httpToCenterWithToken" 表示中心协议;存量 OSS 返回无该字段,返回空串。
func parseDriveUploadType(text string) string {
var data map[string]any
if json.Unmarshal([]byte(text), &data) != nil {
return ""
}
if r, ok := data["result"].(map[string]any); ok {
data = r
}
t, _ := data["uploadType"].(string)
return t
}
// decorateUploadSizeError 为服务端上传超限错误补充可读提示。
// 不做本地文件大小上限校验(上限为服务端动态权益值,本地硬编码会漂移)。
func decorateUploadSizeError(err error, uploadType string) error {
var se *httpStatusError
if !errors.As(err, &se) {
return err
}
if se.StatusCode == http.StatusRequestEntityTooLarge ||
(uploadType == uploadTypeCenterToken && likelySizeLimitBody(se.Body)) {
return fmt.Errorf("%w\n提示: 文件大小可能超出空间容量或上传上限,请确认文件大小、清理空间或联系管理员调整容量后重试", err)
}
return err
}
func likelySizeLimitBody(body string) bool {
b := strings.ToLower(body)
if strings.Contains(b, "超限") || strings.Contains(b, "超出") || strings.Contains(b, "容量") {
return true
}
return strings.Contains(b, "size") && (strings.Contains(b, "limit") || strings.Contains(b, "exceed") || strings.Contains(b, "over"))
}
// driveUploadPut 解析上传凭证并执行 HTTP PUT;401/403(token 过期)时通过 refetch
// 重新获取凭证重试一次。返回最终生效凭证的 uploadId(凭证刷新后以新值为准)。
// 中心协议(uploadType=httpToCenterWithToken)与 OSS 走同一路径:resourceUrl 为
// 服务端拼好的完整 PUT URL(客户端零追加),headers(含 dentry-token)原样透传。
func driveUploadPut(ctx context.Context, credText string, refetch func(context.Context) (string, error), filePath string, fileSize int64) (string, error) {
resourceURL, uploadID, headers, err := parseDriveUploadInfo(credText)
if err != nil {
return "", err
}
uploadType := parseDriveUploadType(credText)
putErr := httpPutFile(ctx, resourceURL, headers, filePath, fileSize)
if putErr != nil && isAuthStatusError(putErr) && refetch != nil {
text2, rerr := refetch(ctx)
if rerr == nil {
if url2, id2, headers2, perr := parseDriveUploadInfo(text2); perr == nil {
uploadID = id2
uploadType = parseDriveUploadType(text2)
putErr = httpPutFile(ctx, url2, headers2, filePath, fileSize)
}
}
}
if putErr != nil {
return "", decorateUploadSizeError(putErr, uploadType)
}
return uploadID, nil
}
File diff suppressed because it is too large Load Diff
+56 -2
View File
@@ -143,8 +143,8 @@ func proxySubCmd(use, targetProduct, targetPath string, flagRenames map[string]s
func newWikiCommand() *cobra.Command {
root := &cobra.Command{
Use: "wiki",
Short: "知识库 / 空间管理 / 节点管理 / 成员管理",
Long: `管理钉钉文档知识库:空间管理(创建/查看/列出/搜索/删除)、节点管理(列出/创建/复制/移动/删除)、成员管理(添加/更新/列出/移除)。`,
Short: "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
Long: `管理钉钉文档知识库:空间管理(创建/查看/列出/搜索/删除)、节点管理(列出/创建/复制/移动/删除)、成员管理(添加/更新/列出/移除)、动态查询(知识库活动动态)。`,
RunE: groupRunE,
}
@@ -779,6 +779,60 @@ ORG 类型授权不会出现在查询结果中。`,
root.AddCommand(nodeCmd)
// ── feed (知识库动态查询) ─────────────────────────────────
feedCmd := &cobra.Command{
Use: "feed",
Short: "知识库动态查询",
Long: `查询知识库的动态:谁在什么时间更新/上传/评论了哪些文档。`,
RunE: groupRunE,
}
feedListCmd := &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "查询知识库动态列表",
Long: `查询指定知识库的动态列表,返回动态类型、时间、内容摘要等信息。
通过 --workspace 指定知识库,支持传入知识库 ID 或知识库 URL。
支持分页,通过 --cursor 传入上次返回的 nextToken 获取下一页。
权限要求:调用者需具备知识库的成员权限,非成员会被拒绝访问。`,
Example: ` dws wiki feed list --workspace <workspaceId>
dws wiki feed list --workspace <workspaceId> --limit 10
dws wiki feed list --workspace <workspaceId> --cursor <nextToken>`,
RunE: func(cmd *cobra.Command, args []string) error {
workspaceID, err := mustFlagOrFallback(cmd, "workspace", "workspace-id")
if err != nil {
return err
}
toolArgs := map[string]any{
"workspaceId": workspaceID,
}
if v, _ := cmd.Flags().GetInt("limit"); v > 0 {
toolArgs["maxResults"] = v
}
if v := flagOrFallback(cmd, "cursor", "page-token"); v != "" {
toolArgs["nextToken"] = v
}
if cmd.Flags().Changed("exclude-file") {
v, _ := cmd.Flags().GetBool("exclude-file")
toolArgs["excludeFile"] = v
}
return callMCPTool("list_workspace_feeds", toolArgs)
},
}
feedListCmd.Flags().String("workspace", "", "知识库 ID 或 URL (必填)")
feedListCmd.Flags().Int("limit", 0, "每页数量 (默认 20,最大 50)")
feedListCmd.Flags().String("cursor", "", "分页游标 (首页留空)")
feedListCmd.Flags().Bool("exclude-file", false, "是否排除文件相关的动态 (默认 false)")
feedListCmd.Flags().String("workspace-id", "", "")
_ = feedListCmd.Flags().MarkHidden("workspace-id")
RegisterCrossProductAliases(feedListCmd)
feedCmd.AddCommand(feedListCmd)
root.AddCommand(feedCmd)
// ── [PROXY] wiki create/get/list/search → wiki space create/get/list/search ──
// Agent 常省略 "space" 直接输入 dws wiki list,透明转发到 wiki space 对应命令
root.AddCommand(
@@ -132,6 +132,9 @@ func TestCrossPlatformCoverageWikiRoutingAndValidationEdges(t *testing.T) {
{"space", "list", "--type", "orgSpace", "--limit", "3", "--cursor", "next"},
{"space", "list", "--type", "mySpace", "--limit", "not-a-number"},
{"space", "search", "--type", "myWikiSpace"},
{"feed", "list", "--workspace", "space"},
{"feed", "list", "--workspace", "space", "--limit", "3", "--cursor", "next", "--exclude-file"},
{"feed", "list", "--workspace-id", "space", "--page-token", "next"},
} {
if err := executeWikiEdge(t, args...); err != nil {
t.Fatalf("Execute(%v): %v", args, err)
@@ -142,6 +145,7 @@ func TestCrossPlatformCoverageWikiRoutingAndValidationEdges(t *testing.T) {
{"node", "create", "--workspace", "space", "--name", "name", "--folder", "123"},
{"node", "copy", "--workspace", "space", "--node", "node", "--folder", "123"},
{"node", "move", "--workspace", "space", "--node", "node", "--folder", "123"},
{"feed", "list"},
} {
if err := executeWikiEdge(t, args...); err == nil {
t.Fatalf("Execute(%v) returned nil", args)
+1 -1
View File
@@ -93,7 +93,7 @@ cli_version: ">=1.0.15"
| `mail` | 邮箱:邮箱地址查询/邮件搜索(KQL)/邮件详情/发送邮件 | [mail.md](./references/products/mail.md) |
| `sheet` | 在线电子表格(axls):工作表 CRUD/区域读写/CSV 批量写入/行列增删/合并/查找替换/筛选视图/全局筛选/排序/下拉列表/条件格式/浮动图片/浮动图表/模板/导出 xlsx(单命令一站式) | [sheet.md](./references/products/sheet.md) |
| `todo` | 待办:创建(含优先级/截止时间/循环)/查询/修改/标记完成/删除 | [todo.md](./references/products/todo.md) |
| `wiki` | 知识库:空间创建/详情/列表/搜索 + 成员管理 | [wiki.md](./references/products/wiki.md) |
| `wiki` | 知识库:空间创建/详情/列表/搜索 + 成员管理 + 知识库动态查询 | [wiki.md](./references/products/wiki.md) |
| `event` | 个人 IM 事件:监听消息接收、指定发送人、已读、撤回、表情回应,NDJSON 输出(实时驱动 Agent)| [event.md](./references/products/event.md) |
## 意图判断决策树
@@ -105,6 +105,7 @@
| 命令 | 用途 | 必填参数 | 路由提醒 |
|------|------|----------|----------|
| `workflow edit-example` | 获取编辑文档与 DSL 示例 | 无 | create/update 前优先调用,内容由服务端提供 |
| `workflow create` | 创建并发布工作流 | `--base-id` `--dsl` | `--dsl` 为完整 workflow-dsl/v1;非幂等,不自动重试 |
| `workflow update` | 更新并发布工作流 | `--base-id` `--workflow-id` `--dsl` | 全量替换,先 get 留底;检查 `data.valid/issues` |
| `workflow list` | 列出 Base 下所有工作流 | `--base-id` | 支持 `--limit [1,100]` / `--offset >=0`;list 出参字段叫 `flowId` |
@@ -7,6 +7,7 @@
| 命令 | 用途 |
|------|------|
| `workflow edit-example` | 获取工作流编辑文档与 workflow-dsl/v1 示例 |
| `workflow create` | 创建并发布自动化工作流 |
| `workflow update` | 更新并发布已有自动化工作流 |
| `workflow list` | 列出 Base 下所有工作流(含状态/创建人/最后修改时间),支持分页 |
@@ -14,11 +15,11 @@
| `workflow enable` | 启用指定工作流(按配置的触发条件自动执行) |
| `workflow disable` | 禁用指定工作流(高危,建议 `--yes` 二次确认) |
> 所有子命令的 `--base-id` 必填(可用隐藏别名 `--base`)。
> `workflow edit-example` 无参数;其他子命令的 `--base-id` 必填(可用隐藏别名 `--base`)。
## DSL 入参格式与最小 Demo
`workflow create/update` 的 `--dsl` 接收完整的 `workflow-dsl/v1` JSON object,不是局部 patch。支持内联 JSON、`@文件路径` 或 `-` 从 stdin 读取。
先运行 `workflow edit-example` 获取服务端提供的最新编辑文档和示例。`workflow create/update` 的 `--dsl` 接收完整的 `workflow-dsl/v1` JSON object,不是局部 patch。支持内联 JSON、`@文件路径` 或 `-` 从 stdin 读取。
复杂工作流应先用 `table get` / `field get` / `view list` 确认真实 `sheetId`、`fieldId`、`viewId`,并检查所有 `next`、`loopEntry`、branch `to` 和 ref。下面是一个不依赖数据表字段的最小定时消息工作流:
@@ -53,6 +54,14 @@ create 和 update 都必须同时满足 `status=success`、`data.valid=true`、`
## 命令详情
### workflow edit-example — 获取编辑文档与示例
```bash
dws aitable workflow edit-example --format json
```
该命令无业务参数,调用 `aitable/edit_workflow_example` 返回服务端提供的工作流编辑文档和示例。创建或更新复杂工作流前优先调用它,避免依赖可能过期的本地 DSL 结构。
### workflow create — 创建并发布工作流
```bash
+9
View File
@@ -202,14 +202,23 @@ Usage:
Example:
dws drive download --node <dentryUuid> --output ./report.pdf
dws drive download --node <dentryUuid> --output ~/downloads/
dws drive download --node <dentryUuid> --output ./big.zip --part-size 32MB --parallel 8
Flags:
--node string 文件 ID (dentryUuid) (必填)
--output string 本地保存路径 (必填),可以是文件路径或目录;如果指定目录,文件名从下载 URL 中自动推断
--space-id string 文件所属空间 ID (可选)
--part-size string 分片下载的分片大小,支持 KB/MB/GB 单位,范围 1MB-1GB (默认 16MB)
--parallel int 分片下载并发数,范围 1-8 (默认 4)
--no-resume 关闭断点续传,忽略历史下载进度从头下载 (默认开启续传)
```
> **注意**:`--output` 是必填参数,不传会报错。
> **大文件分片下载**:
> - 大文件自动分片并发下载,小文件整流下载,行为对用户透明,无需任何额外操作。
> - 断点续传默认开启:下载中断后重跑同一命令会自动跳过已完成部分继续下载(`<目标文件>.dwspart` 为临时进度文件,下载完成后自动清理);不需要续传时加 `--no-resume`。
> - 下载凭证过期会自动刷新并继续下载,已完成的部分不会重下;单个分片失败会自动重试,无需手动处理。
### 创建文件夹
```
+41
View File
@@ -181,6 +181,30 @@ Flags:
> ⚠️ **返回字段限制**:`member list` 每条只返回 `name` / `role` / `type` 三个字段,**不含 userId**(服务端不返回)。因此**无法**从 `member list` 拿到 userId 再去串联 `member update` / `member remove`。要对某人改角色 / 移除,需另行拿到其 userId(例如用 `dws contact user search --query "<姓名>"` 按姓名反查)。
### 查询知识库动态
```
Usage:
dws wiki feed list [flags]
Aliases:
list, ls
Example:
dws wiki feed list --workspace <workspaceId> --format json
dws wiki feed list --workspace <workspaceId> --limit 10 --format json
dws wiki feed list --workspace <workspaceId> --exclude-file --format json
dws wiki feed list --workspace <workspaceId> --limit 10 --cursor <nextToken> --format json
Flags:
--workspace string 知识库 ID 或 URL (必填)
--limit int 每页数量 (默认 20,最大 50)
--cursor string 分页游标 (首页留空)
--exclude-file 是否排除文件相关的动态 (默认 false)
```
查询指定知识库的动态,返回谁在什么时间进行了更新、上传、评论等操作。
支持传入知识库 ID 或知识库 URL,系统自动识别。
支持分页,通过 `--cursor` 传入上次返回的 nextToken 获取下一页;出参 `hasMore` 指示是否还有下一页。
> **权限要求**:调用者需具备知识库的成员权限,非成员会被拒绝访问。
### 列出知识库节点
```
Usage:
@@ -296,6 +320,8 @@ Flags:
- 用户说"修改某人在知识库的权限/调整成员角色" → `member update`
- 用户说"移除知识库成员/把某人从知识库移除/删除知识库成员" → `member remove`(需 `--workspace` + `--users`)
- 用户说"知识库有哪些成员/查看知识库成员" → `member list`
- 用户说"知识库动态/最近有什么更新/谁改了什么/知识库活动" → `feed list`(需 `--workspace`)
- 用户说"知识库最近的评论/更新记录/操作日志" → `feed list`(需 `--workspace`)
- 用户说"删除知识库/移除知识库/把知识库删了" → `space delete`(需 `--workspace`)
> **跨产品路由说明**:知识库节点的**内容操作**(读取/编辑/块级操作)仍由 `dws doc` 承担:
@@ -382,6 +408,20 @@ dws wiki node move --workspace <workspaceId> --node <nodeId> --folder <targetFol
# 删除节点(会要求确认)
dws wiki node delete --workspace <workspaceId> --node <nodeId>
# ── 工作流: 查询知识库动态 ──
# 1. 获取知识库 ID
dws wiki space list --format json
# 2. 查询知识库动态
dws wiki feed list --workspace <workspaceId> --format json
# 3. 排除文件动态,只看文档操作
dws wiki feed list --workspace <workspaceId> --exclude-file --format json
# 4. 翻页(cursor 取上一页返回的 nextToken)
dws wiki feed list --workspace <workspaceId> --cursor <nextToken> --format json
# ── 工作流: 给知识库加成员 ──
# 1. 先确认知识库 ID(避免授权到「我的文档」)
@@ -424,6 +464,7 @@ dws wiki space delete --workspace <workspaceId> --format json
| `node list` | `nodeId` | node copy/move/delete 的 --node / `dws doc read` 的 --node |
| `node search` | `nodeId` | node copy/move/delete 的 --node / `dws doc read` 的 --node |
| `node create` | `nodeId` | node copy/move/delete 的 --node / `dws doc read` 的 --node |
| `feed list` | `nextToken` | feed list 的 --cursor(翻页,`hasMore` 为 true 时继续)|
| `member list` | `name` / `role` / `type`(**不含 userId**)| 仅用于查看成员名单;**无法**从这里取 userId 去串联 member update/remove,需另行按姓名反查 userId(如 `dws contact user search --query "<姓名>"`)|
## 相关产品
@@ -134,6 +134,7 @@ Flags:
| 命令 | 用途 | 必填参数 | 路由提醒 |
|------|------|----------|----------|
| `workflow edit-example` | 获取编辑文档与 DSL 示例 | 无 | create/update 前优先调用,内容由服务端提供 |
| `workflow create` | 创建并发布自动化工作流 | `--base-id` `--dsl` | 按子文档 Demo 组装 DSL;必须检查返回的 `data.valid` / `issues`;create 不自动重试 |
| `workflow update` | 更新并发布已有自动化工作流 | `--base-id` `--workflow-id` `--dsl` | 先 get 留底;提交完整目标 DSL;必须检查 `data.valid` / `issues` |
| `workflow list` | 列出 Base 下所有工作流 | `--base-id` | 支持 `--limit [1,100]` / `--offset >=0`;list 出参字段叫 `flowId` |
@@ -7,6 +7,7 @@
| 命令 | 用途 |
|------|------|
| `workflow edit-example` | 获取工作流编辑文档与 workflow-dsl/v1 示例 |
| `workflow create` | 创建并发布自动化工作流 |
| `workflow update` | 更新并发布已有自动化工作流 |
| `workflow list` | 列出 Base 下所有工作流(含状态/创建人/最后修改时间),支持分页 |
@@ -14,15 +15,15 @@
| `workflow enable` | 启用指定工作流(按配置的触发条件自动执行) |
| `workflow disable` | 禁用指定工作流(高危,建议 `--yes` 二次确认) |
> 所有子命令的 `--base-id` 必填(可用隐藏别名 `--base`)。
> `workflow edit-example` 无参数;其他子命令的 `--base-id` 必填(可用隐藏别名 `--base`)。
## DSL 入参格式与最小 Demo
`workflow create/update` 的 `--dsl` 接收钉钉 AI 表格 `workflow-dsl/v1` JSON object。当前同步范围只包含 create/update,没有新增 DSL 文档子命令;其他 Agent 可以直接使用下面的最小 Demo 理解调用格式。
先运行 `workflow edit-example` 获取服务端提供的最新编辑文档和示例。`workflow create/update` 的 `--dsl` 接收钉钉 AI 表格 `workflow-dsl/v1` JSON object。
复杂工作流还应注意:
1. 如果 Agent 运行环境直接提供 AI 表格 MCP 的 `get_workflow_dsl_docs`,可用它获取最新 DSL Guide、Schema 和示例。
1. 使用 `workflow edit-example` 获取最新 DSL Guide、结构和示例。
2. 涉及数据表、字段或视图的节点,先用 `table get` / `field get` / `view list` 确认真实 `sheetId`、`fieldId`、`viewId`。
3. create 和 update 都提交完整的 workflow-dsl/v1 JSON object,并检查所有 `next`、`loopEntry`、branch `to` 和 ref。
@@ -82,6 +83,14 @@ create 和 update 都必须同时满足 `status=success`、`data.valid=true`、`
## 命令详情
### workflow edit-example — 获取编辑文档与示例
```bash
dws aitable workflow edit-example --format json
```
该命令无业务参数,调用 `aitable/edit_workflow_example` 返回服务端提供的工作流编辑文档和示例。创建或更新复杂工作流前优先调用它,避免依赖可能过期的本地 DSL 结构。
### workflow create — 创建并发布工作流
```bash
@@ -170,14 +170,23 @@ Usage:
Example:
dws drive download --node <dentryUuid> --output ./report.pdf
dws drive download --node <dentryUuid> --output ~/downloads/
dws drive download --node <dentryUuid> --output ./big.zip --part-size 32MB --parallel 8
Flags:
--node string 文件 ID (dentryUuid) (必填)
--output string 本地保存路径 (文件路径或目录,不传则保存到当前目录);如果指定目录,文件名从下载 URL 中自动推断 (可选)
--space-id string 文件所属空间 ID (可选)
--part-size string 分片下载的分片大小,支持 KB/MB/GB 单位,范围 1MB-1GB (默认 16MB)
--parallel int 分片下载并发数,范围 1-8 (默认 4)
--no-resume 关闭断点续传,忽略历史下载进度从头下载 (默认开启续传)
```
> **提示**:`--output` 为可选参数,不传则保存到当前目录,文件名从下载 URL 中自动推断。
> **大文件分片下载**:
> - 大文件自动分片并发下载,小文件整流下载,行为对用户透明,无需任何额外操作。
> - 断点续传默认开启:下载中断后重跑同一命令会自动跳过已完成部分继续下载(`<目标文件>.dwspart` 为临时进度文件,下载完成后自动清理);不需要续传时加 `--no-resume`。
> - 下载凭证过期会自动刷新并继续下载,已完成的部分不会重下;单个分片失败会自动重试,无需手动处理。
### 创建文件夹
```
@@ -526,6 +535,7 @@ Flags:
> **两步下载流程**:先调用 MCP 工具获取历史版本下载 URL 和签名头,再 HTTP GET 下载文件内容到本地。
> `--output` 指定目录时,优先从文件信息中获取原始文件名,获取不到时从下载 URL 推断。
> 历史版本下载同样支持 `--part-size` / `--parallel` / `--no-resume` 分片下载参数,行为与最新版下载一致。
#### 回滚文件到指定历史版本
+2 -1
View File
@@ -1,6 +1,6 @@
---
name: dingtalk-wiki
description: 钉钉知识库与空间管理。Use when 用户说 知识库/wiki/创建知识库/搜索知识库空间/我的文档/团队空间/空间成员/空间内节点创建/列出/搜索/复制/移动/删除。知识库节点复制移动走本 skill,普通钉盘文件复制移动走 dingtalk-drive;空间内单文档内容读写先用本 skill 定位再切到 dingtalk-doc。命令前缀:dws wiki。
description: 钉钉知识库与空间管理。Use when 用户说 知识库/wiki/创建知识库/搜索知识库空间/我的文档/团队空间/空间成员/空间内节点创建/列出/搜索/复制/移动/删除/知识库动态。知识库节点复制移动走本 skill,普通钉盘文件复制移动走 dingtalk-drive;空间内单文档内容读写先用本 skill 定位再切到 dingtalk-doc。命令前缀:dws wiki。
metadata:
cli_version: ">=0.2.14"
category: product
@@ -39,6 +39,7 @@ metadata:
| "列出知识库里的文件/节点" | `dws wiki node list --workspace <WS_ID>` |
| "在知识库里搜" | `dws wiki node search --workspace <WS_ID> --query "<关键词>"` |
| "在知识库里创建文档节点" | `dws wiki node create --workspace <WS_ID> --type adoc --name "<名称>"` |
| "知识库动态 / 最近有什么更新 / 谁改了什么" | `dws wiki feed list --workspace <WS_ID>` |
## 标准 SOP(必遵流程)
@@ -171,6 +171,30 @@ Flags:
> 接口不支持游标分页,使用 `--limit` 一次性拉取。
### 查询知识库动态
```
Usage:
dws wiki feed list [flags]
Aliases:
list, ls
Example:
dws wiki feed list --workspace <workspaceId> --format json
dws wiki feed list --workspace <workspaceId> --limit 10 --format json
dws wiki feed list --workspace <workspaceId> --exclude-file --format json
dws wiki feed list --workspace <workspaceId> --limit 10 --cursor <nextToken> --format json
Flags:
--workspace string 知识库 ID 或 URL (必填)
--limit int 每页数量 (默认 20,最大 50)
--cursor string 分页游标 (首页留空)
--exclude-file 是否排除文件相关的动态 (默认 false)
```
查询指定知识库的动态,返回谁在什么时间进行了更新、上传、评论等操作。
支持传入知识库 ID 或知识库 URL,系统自动识别。
支持分页,通过 `--cursor` 传入上次返回的 nextToken 获取下一页;出参 `hasMore` 指示是否还有下一页。
> **权限要求**:调用者需具备知识库的成员权限,非成员会被拒绝访问。
### 列出知识库节点
```
Usage:
@@ -306,6 +330,8 @@ Flags:
- 用户说"修改某人在知识库的权限/调整成员角色" → `member update`
- 用户说"移除知识库成员/把某人从知识库移除/删除知识库成员" → `member remove`(需 `--workspace` + `--users`)
- 用户说"知识库有哪些成员/查看知识库成员" → `member list`
- 用户说"知识库动态/最近有什么更新/谁改了什么/知识库活动" → `feed list`(需 `--workspace`)
- 用户说"知识库最近的评论/更新记录/操作日志" → `feed list`(需 `--workspace`)
- 用户说"删除知识库/移除知识库/把知识库删了" → `space delete`(需 `--workspace`)
> **跨产品路由说明**:知识库节点的**内容操作**(读取/编辑/块级操作)仍由 `dws doc` 承担:
@@ -392,6 +418,20 @@ dws wiki node move --workspace <workspaceId> --node <nodeId> --folder <targetFol
# 删除节点(会要求确认)
dws wiki node delete --workspace <workspaceId> --node <nodeId>
# ── 工作流: 查询知识库动态 ──
# 1. 获取知识库 ID
dws wiki space list --format json
# 2. 查询知识库动态
dws wiki feed list --workspace <workspaceId> --format json
# 3. 排除文件动态,只看文档操作
dws wiki feed list --workspace <workspaceId> --exclude-file --format json
# 4. 翻页(cursor 取上一页返回的 nextToken)
dws wiki feed list --workspace <workspaceId> --cursor <nextToken> --format json
# ── 工作流: 给知识库加成员 ──
# 1. 先确认知识库 ID(避免授权到「我的文档」)
@@ -431,6 +471,7 @@ dws wiki space delete --workspace <workspaceId> --format json
| `node list` | `nodeId` | node copy/move/delete 的 --node / `dws doc read` 的 --node |
| `node search` | `nodeId` | node copy/move/delete 的 --node / `dws doc read` 的 --node |
| `node create` | `nodeId` | node copy/move/delete 的 --node / `dws doc read` 的 --node |
| `feed list` | `nextToken` | feed list 的 --cursor(翻页,`hasMore` 为 true 时继续)|
| `member list` | `userId` | member update 的 --users / member remove 的 --users |
## 相关产品
+13 -1
View File
@@ -5681,7 +5681,7 @@
[wiki]
runnable: true
hidden: false
commands: +space-search, member, node, space
commands: +space-search, feed, member, node, space
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[wiki.+space-search]
@@ -5689,6 +5689,18 @@
hidden: false
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:string|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[wiki.feed]
runnable: true
hidden: false
commands: list
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[wiki.feed.list]
runnable: true
hidden: false
aliases: ls
flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --exclude-file:bool|required=false|hidden=false|no-opt="true"|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --next-token:string|required=false|hidden=true|no-opt=""|scope=local, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --page-size:string|required=false|hidden=true|no-opt=""|scope=local, --page-token:string|required=false|hidden=true|no-opt=""|scope=local, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --workspace:string|required=false|hidden=false|no-opt=""|scope=local, --workspace-id:string|required=false|hidden=true|no-opt=""|scope=local, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited
[wiki.member]
runnable: true
hidden: false
+187
View File
@@ -163,6 +163,193 @@ func TestMockMCPSmoke_CLIRoutesSerializedArgumentsAndPrintsJSON(t *testing.T) {
}
}
func TestMockMCPSmoke_ChatDownloadMediaJSONCLI(t *testing.T) {
type mediaRequest struct {
method string
path string
query string
header string
}
mediaPayload := []byte("synthetic media payload")
var mediaRequestsMu sync.Mutex
var mediaRequests []mediaRequest
mediaServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mediaRequestsMu.Lock()
mediaRequests = append(mediaRequests, mediaRequest{
method: r.Method,
path: r.URL.Path,
query: r.URL.RawQuery,
header: r.Header.Get("X-Download-Fixture"),
})
mediaRequestsMu.Unlock()
w.WriteHeader(http.StatusOK)
_, _ = w.Write(mediaPayload)
}))
defer mediaServer.Close()
downloadURL := mediaServer.URL + "/photo.jpg?fixture=one&part=two"
downloadInfo, err := json.Marshal(map[string]any{
"resourceUrl": downloadURL,
"headers": map[string]string{
"X-Download-Fixture": "synthetic",
},
})
if err != nil {
t.Fatal(err)
}
var requestsMu sync.Mutex
var requests []recordedToolCall
mcpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
call := recordedToolCall{
path: r.URL.Path,
method: r.Method,
authorization: r.Header.Get("Authorization"),
}
var envelope struct {
JSONRPC string `json:"jsonrpc"`
ID int `json:"id"`
Method string `json:"method"`
Params struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments"`
} `json:"params"`
}
if err := json.NewDecoder(r.Body).Decode(&envelope); err != nil {
call.err = err
} else {
call.jsonrpc = envelope.JSONRPC
call.tool = envelope.Params.Name
call.arguments = envelope.Params.Arguments
if envelope.Method != "tools/call" {
call.err = fmt.Errorf("JSON-RPC method = %q, want tools/call", envelope.Method)
}
}
requestsMu.Lock()
requests = append(requests, call)
requestsMu.Unlock()
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"jsonrpc": "2.0",
"id": envelope.ID,
"result": map[string]any{
"content": []map[string]any{{
"type": "text",
"text": string(downloadInfo),
}},
},
})
}))
defer mcpServer.Close()
outputDir := t.TempDir()
env := isolatedCLIEnv(t, map[string]string{
"DINGTALK_IM_MCP_URL": mcpServer.URL + "/mcp/im",
})
args := []string{
"--token", "ci-smoke-token",
"--format", "json",
"chat", "message", "download-media",
"--type", "mediaId",
"--resource-id", "resource-001",
"--message-id", "message-001",
"--open-conversation-id", "conversation-001",
"--output", outputDir,
}
stdout, stderr, err := runCLI(t, env, args...)
if err != nil {
t.Fatalf("dws chat message download-media failed: %v\nstdout:\n%s\nstderr:\n%s", err, stdout, stderr)
}
requestsMu.Lock()
recorded := append([]recordedToolCall(nil), requests...)
requestsMu.Unlock()
if len(recorded) != 1 {
t.Fatalf("local fake MCP server received %d requests, want exactly one tools/call: %#v", len(recorded), recorded)
}
call := recorded[0]
if call.err != nil {
t.Fatal(call.err)
}
if call.path != "/mcp/im" || call.method != http.MethodPost || call.jsonrpc != "2.0" {
t.Fatalf("MCP request = path %q method %q jsonrpc %q", call.path, call.method, call.jsonrpc)
}
if call.authorization != "Bearer ci-smoke-token" {
t.Fatalf("Authorization = %q, want synthetic smoke token", call.authorization)
}
if call.tool != "get_resource_download_url" {
t.Fatalf("tool = %q, want get_resource_download_url", call.tool)
}
wantArgs := map[string]any{
"resourceType": "mediaId",
"resourceId": "resource-001",
"openMessageId": "message-001",
"openConversationId": "conversation-001",
}
if !reflect.DeepEqual(call.arguments, wantArgs) {
t.Fatalf("arguments = %#v, want %#v", call.arguments, wantArgs)
}
mediaRequestsMu.Lock()
recordedMedia := append([]mediaRequest(nil), mediaRequests...)
mediaRequestsMu.Unlock()
if len(recordedMedia) != 1 {
t.Fatalf("media server received %d requests, want exactly one: %#v", len(recordedMedia), recordedMedia)
}
gotMedia := recordedMedia[0]
if gotMedia.method != http.MethodGet || gotMedia.path != "/photo.jpg" {
t.Fatalf("media request = method %q path %q", gotMedia.method, gotMedia.path)
}
if gotMedia.query != "fixture=one&part=two" {
t.Fatalf("media query = %q, want fixture=one&part=two", gotMedia.query)
}
if gotMedia.header != "synthetic" {
t.Fatalf("media header = %q, want synthetic", gotMedia.header)
}
wantOutput := filepath.Join(outputDir, "photo.jpg")
gotPayload, err := os.ReadFile(wantOutput)
if err != nil {
t.Fatalf("read downloaded file: %v", err)
}
if !bytes.Equal(gotPayload, mediaPayload) {
t.Fatalf("downloaded payload = %q, want %q", gotPayload, mediaPayload)
}
var result map[string]any
if err := json.Unmarshal([]byte(stdout), &result); err != nil {
t.Fatalf("CLI returned non-JSON stdout: %v\nstdout:\n%s\nstderr:\n%s", err, stdout, stderr)
}
if len(result) != 3 || result["success"] != true || result["downloadUrl"] != downloadURL || result["output"] != wantOutput {
t.Fatalf("CLI result = %#v, want exact success/downloadUrl/output contract", result)
}
if strings.Contains(stdout, "[INFO]") {
t.Fatalf("JSON stdout contains progress text: %s", stdout)
}
if !strings.Contains(stdout, "?fixture=one&part=two") {
t.Fatalf("downloadUrl was escaped or changed: %s", stdout)
}
publicResult := map[string]any{
"success": true,
"downloadUrl": "http://127.0.0.1:<fixture-port>/photo.jpg?fixture=one&part=two",
"output": "./downloads/photo.jpg",
}
var publicOutput bytes.Buffer
publicEncoder := json.NewEncoder(&publicOutput)
publicEncoder.SetEscapeHTML(false)
publicEncoder.SetIndent("", " ")
if err := publicEncoder.Encode(publicResult); err != nil {
t.Fatal(err)
}
t.Log("CLI (public-safe): dws chat message download-media --type mediaId --resource-id resource-001 --message-id message-001 --open-conversation-id conversation-001 --output ./downloads/ --format json")
t.Logf("stdout (public-safe):\n%s", strings.TrimSpace(publicOutput.String()))
t.Logf("downloaded file: photo.jpg (%d bytes, content verified)", len(gotPayload))
}
func isolatedCLIEnv(t *testing.T, extra map[string]string) []string {
t.Helper()