Compare commits

..
Author SHA1 Message Date
chichuan 20d27f6db9 Merge latest main into codex/fix-macos-auth-test-scope
Resolve the macOS race budget conflict in favour of the focused scope.

c1f96241 on main extended the whole-package macOS race step from 10m to
12m and pinned that budget in the workflow contract. This branch removes
the whole-package run instead: ./internal/app is already covered by the
Ubuntu "race: app" shard, and macOS only needs the natively-gated tests.
With the focused scope the job drops from 10m47s to ~3m, so the 12m
budget is no longer needed and the two step timeouts (6m + 5m) fit inside
the 15m job budget with headroom.

The contract assertion c1f96241 added is superseded rather than dropped:
pinning both focused commands locks the per-step timeouts, and the
existing checks still block a whole-package regression and require
./internal/app to appear exactly once.
2026-08-04 20:14:58 +08:00
github-actions[bot] ca2b8adcb2 Merge pull request #853 from DingTalk-Real-AI/codex/sync-wukong-oa-approval
feat(oa): add approval form workflow commands
2026-08-04 20:05:58 +08:00
chichuan a3c7009f9b Merge latest main into codex/fix-macos-auth-test-scope 2026-08-04 18:17:33 +08:00
chichuan 2d3f820f91 ci: keep the darwin-gated upgrade self-heal test reachable on macOS
Narrowing the macOS internal/app -run pattern orphaned
TestValidateNewBinary_RecoversFromUnsignedDarwin: it is the only
runtime.GOOS != "darwin" gated test in the package, the Ubuntu race shard
skips it on Linux, and the platform coverage gate only runs
^(TestAllShortcuts|TestCrossPlatformCoverage). No CI job selected it any
more, so it could never run or fail again.

- Add the self-heal test back to the macOS -run pattern.
- Add the (CrossPlatformCoverage)? group the Windows pattern already has,
  which also recovers TestCrossPlatformCoverageAuthMigrateKeychainRemainingBranches.
- Attribute vacuous runs: the two skip paths now name the branch that went
  unverified, and DWS_REQUIRE_AMFI_SELF_HEAL=1 escalates such a run to a
  hard failure on a host that does enforce amfid. GitHub's hosted macOS
  runners do not reproduce the amfid kill, so this test has been silently
  skipping there all along.
- Restore step-timeout headroom: 10m + 5m exactly equalled the 15m job
  budget, leaving none for setup. The keychain/auth step drops to 6m
  (measured 2m43s).
- Add a contract test that couples the macOS -run pattern to the set of
  darwin-gated tests in internal/app, so the next narrowing fails loudly
  instead of silently orphaning one.
2026-08-04 17:59:59 +08:00
chichuan f5a1b64d7a test(oa): include approval cases in native coverage 2026-08-04 17:36:22 +08:00
chichuan c1f96241af ci: extend macOS race test budget 2026-08-04 17:23:04 +08:00
chichuan eb571e6e73 fix(oa): remove unreachable mode checks 2026-08-04 16:42:57 +08:00
chichuan 4d5a47ac93 Merge latest main into codex/sync-wukong-oa-approval 2026-08-04 16:16:21 +08:00
chichuan 6108f51c9d fix(oa): align approval mode schema contracts 2026-08-04 16:08:51 +08:00
github-actions[bot] 6376f294da Merge pull request #862 from DingTalk-Real-AI/codex/sync-wukong-wiki-feed
feat(wiki): add knowledge base feed query command
2026-08-04 07:48:31 +00:00
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
chichuan 31e3f6bcbc Merge remote-tracking branch 'origin/main' into codex/sync-wukong-oa-approval
# Conflicts:
#	internal/cli/schema_agent_metadata/index.json
#	internal/cli/schema_agent_metadata_audit.json
#	internal/cli/schema_catalog/catalog.json
2026-08-04 11:41:38 +08:00
chichuan 678f108adf docs(oa): complete approval workflow references 2026-08-04 11:39:55 +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
Raph a240ad2b81 ci: focus macOS auth race tests 2026-08-03 23:09:58 +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
chichuan f39a3f5417 Merge branch 'main' into codex/sync-wukong-oa-approval 2026-08-03 21:18:39 +08:00
chichuan e806e761ad test(oa): cover approval request branches 2026-08-03 19:48:26 +08:00
chichuan a6696fe9e9 fix(schema): map OA request wrappers 2026-08-03 19:26:22 +08:00
chichuan 06af21c7a1 fix(oa): complete approval instance options 2026-08-03 19:09:51 +08:00
chichuan eba2b692ec feat(oa): add approval form workflow commands 2026-08-03 19:02:39 +08:00
51 changed files with 13178 additions and 143 deletions
+5 -2
View File
@@ -693,8 +693,11 @@ jobs:
with:
go-version-file: go.mod
- name: Test macOS auth and Keychain paths with Race Detection
run: go test -v -race -count=1 -timeout=10m ./internal/keychain ./internal/auth ./internal/app
- name: Test macOS auth and Keychain packages with Race Detection
run: go test -v -race -count=1 -timeout=6m ./internal/keychain ./internal/auth
- name: Test macOS auth migration, Keychain diagnostics, and upgrade self-heal with Race Detection
run: go test -v -race -count=1 -timeout=5m ./internal/app -run '^(TestValidateNewBinary_RecoversFromUnsignedDarwin|Test(CrossPlatformCoverage)?Auth(MigrateKeychain|StatusDiagnosticReportsCiphertextKeyMismatch))'
test-windows:
name: Test (Windows)
+32
View File
@@ -6,6 +6,38 @@ 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
+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.3"
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.3/dws-darwin-arm64.tar.gz"
sha256 "5d35bb3fca7883a4ee51e1561aefd6b954b104313359a7c05b30a333ea41e749"
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.3/dws-darwin-amd64.tar.gz"
sha256 "5a94069a3ab2c811d915639bbfd9ff17035b77501d5f9070c0b540ffe525a41b"
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.3/dws-linux-arm64.tar.gz"
sha256 "e9cbc1c647f3ea703e1b93264c0e7d92871cfabe26fef26fbe17b9dff4b80c0f"
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.3/dws-linux-amd64.tar.gz"
sha256 "7c475841ea871d204f9f6efc7d6e5378cf19db4541aeaa8a27d1387fa6f2a1f1"
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.3/dws-skills.zip"
sha256 "b29b35345613bc9260ae37578eb982472591c9dc548b02176b5b9ba0bdc431f2"
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
+6 -3
View File
@@ -40,7 +40,7 @@ Every command inherits these flags (documented here once, not repeated per comma
- [`dws doc` — DingTalk Doc](#dws-doc) · 21 commands
- [`dws drive` — DingTalk Drive](#dws-drive) · 6 commands
- [`dws minutes` — AI Minutes](#dws-minutes) · 19 commands
- [`dws oa` — OA Approval](#dws-oa) · 9 commands
- [`dws oa` — OA Approval](#dws-oa) · 12 commands
- [`dws report` — Reports](#dws-report) · 7 commands
- [`dws todo` — Todo Tasks](#dws-todo) · 6 commands
@@ -277,14 +277,17 @@ _AI meeting notes: listing, summary, todos, transcription, recording control, mi
## `dws oa` — OA Approval
_OA approval workflows: list, approve, reject, revoke, records._
_OA approval workflows: inspect forms, forecast routes, create instances, approve, reject, revoke, and audit records._
**9 commands**
**12 commands**
| Command | Description | When to use |
|---|---|---|
| `dws oa approval approve` | Approve a pending approval process instance (task) as the current user. | When the agent acts on a pending approval the user has delegated it to handle. |
| `dws oa approval create-instance` | Create a real approval process instance from validated form values or a complete request payload. | After the agent has inspected the form Schema, forecast the route, resolved any selectable approvers, and obtained explicit user confirmation. |
| `dws oa approval detail` | Retrieve full details of an approval process instance, including form fields, attachments, and state. | When the agent needs to read the content of an approval ticket before deciding on it or summarizing it. |
| `dws oa approval form-schema` | Retrieve the form Schema for an approval template by processCode. | Before collecting or validating values for a new approval instance. |
| `dws oa approval forecast-process` | Forecast the approval route for a template and its proposed form values. | Before creating an instance, especially when the route contains user-selectable approver or notifier nodes. |
| `dws oa approval list-forms` | List approval process templates (forms) the current user is allowed to initiate. | When the agent needs to pick the right approval form before submitting a new request. |
| `dws oa approval list-initiated` | List approval process instances the current user has initiated. | When the agent reviews the status of approvals the user submitted. |
| `dws oa approval list-pending` | List approval process instances currently awaiting action from the current user. | When the agent surfaces "needs your approval" items in the user's inbox. |
@@ -0,0 +1,76 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package app
import (
"strings"
"testing"
)
func TestOAApprovalDualModeConstraintsReachEmbeddedSchema(t *testing.T) {
tools := embeddedSchemaAllToolsForHelpFlagTest(t, NewRootCommand())
tests := []struct {
canonical string
optional []string
requireTogether []string
mutuallyExclusive []string
}{
{
canonical: "oa.forecast_process",
optional: []string{"request", "process-code", "dept-id", "form-values"},
requireTogether: []string{"process-code", "dept-id", "form-values"},
mutuallyExclusive: []string{"process-code", "dept-id", "form-values"},
},
{
canonical: "oa.start_process_instance",
optional: []string{"request", "process-code", "dept-id", "form-values", "originator-user-id", "approvers", "approvers-action-type", "cc-list", "cc-position"},
requireTogether: []string{"process-code", "form-values"},
mutuallyExclusive: []string{"process-code", "dept-id", "form-values", "originator-user-id", "approvers", "approvers-action-type", "cc-list", "cc-position"},
},
}
for _, test := range tests {
t.Run(test.canonical, func(t *testing.T) {
tool := tools[test.canonical]
parameters := schemaContractMap(tool["parameters"])
for _, name := range test.optional {
if got := parameters[name]["required"]; got != false {
t.Errorf("--%s required = %#v, want false for dual-mode command", name, got)
}
}
assertSchemaContractConstraintGroup(t, tool, "require_one_of", []string{"request", "process-code"})
assertSchemaContractConstraintGroup(t, tool, "require_together", test.requireTogether)
for _, name := range test.mutuallyExclusive {
assertSchemaContractConstraintGroup(t, tool, "mutually_exclusive", []string{"request", name})
}
constraints, _ := tool["constraints"].(map[string]any)
groups, _ := constraints["mutually_exclusive"].([]any)
if len(groups) != len(test.mutuallyExclusive) {
t.Errorf("mutually_exclusive group count = %d, want %d: %#v", len(groups), len(test.mutuallyExclusive), groups)
}
for _, rawGroup := range groups {
group, _ := rawGroup.([]any)
if len(group) != 2 {
t.Errorf("mutually_exclusive contains an over-broad group: %#v", group)
}
}
hasRequestOnlyExample := false
for _, example := range schemaContractStringSlice(tool["examples"]) {
if strings.Contains(example, " --request ") && !strings.Contains(example, " --process-code ") && !strings.Contains(example, " --form-values ") {
hasRequestOnlyExample = true
}
}
if !hasRequestOnlyExample {
t.Errorf("examples do not contain a request-only invocation: %#v", tool["examples"])
}
})
}
create := tools["oa.start_process_instance"]
if got := schemaContractString(create["confirmation"]); got != "user_required" {
t.Errorf("create-instance confirmation = %q, want user_required", got)
}
}
+19 -3
View File
@@ -543,6 +543,13 @@ func TestIsLikelyAMFIKill(t *testing.T) {
// validateNewBinary recovers via repairDarwinBinary (ad-hoc codesign) and
// successfully re-executes the binary. We use go itself as a stand-in for the
// new dws binary — it's a real signed Mach-O we can strip and re-sign.
//
// GitHub's hosted macOS runners do not reproduce the amfid kill, so in CI this
// test reports a skip that names the unverified path rather than implying the
// self-heal was exercised. Set DWS_REQUIRE_AMFI_SELF_HEAL=1 on a host that does
// enforce amfid to turn such a vacuous run into a hard failure.
const requireAMFISelfHealEnv = "DWS_REQUIRE_AMFI_SELF_HEAL"
func TestValidateNewBinary_RecoversFromUnsignedDarwin(t *testing.T) {
if runtime.GOOS != "darwin" {
@@ -573,15 +580,24 @@ func TestValidateNewBinary_RecoversFromUnsignedDarwin(t *testing.T) {
t.Fatalf("strip signature: %v\n%s", err, out)
}
// Sanity: confirm direct exec is killed.
// Sanity: confirm direct exec is killed. When it is not, repairDarwinBinary
// never runs and the rest of this test proves nothing — say so.
if _, err := tryExecVersion(bin); err == nil {
t.Skip("unsigned binary executed without amfid kill — likely Intel Mac or SIP disabled")
const unverified = "amfid did not kill the unsigned binary, so repairDarwinBinary was NOT exercised"
if os.Getenv(requireAMFISelfHealEnv) == "1" {
t.Fatalf("%s (%s=1)", unverified, requireAMFISelfHealEnv)
}
t.Skipf("%s — host does not enforce amfid (Intel Mac, SIP disabled, or hosted runner)", unverified)
}
// validateNewBinary should self-heal and succeed.
if err := validateNewBinary(bin, "dev"); err != nil {
if strings.Contains(err.Error(), "signal: killed") {
t.Skipf("host security policy still rejects the ad-hoc signed test binary: %v", err)
const unverified = "host security policy still rejects the ad-hoc signed binary, so the self-heal outcome was NOT verified"
if os.Getenv(requireAMFISelfHealEnv) == "1" {
t.Fatalf("%s (%s=1): %v", unverified, requireAMFISelfHealEnv, err)
}
t.Skipf("%s: %v", unverified, err)
}
t.Fatalf("validateNewBinary did not recover: %v", err)
}
@@ -1,17 +1,17 @@
{
"version": 1,
"source_hash": "sha256:b513a679eb51b64afa3f31364b45c29bc4590f0568908650f426ac8f4b041b2b",
"surface_hash": "sha256:41044fe1b6723564c40d684381ac6d6f23c4dfd58f9769350f02f223f02fa894",
"source_hash": "sha256:9a6bc544f78424a85d7465cbb8ce20ee80c954de7ec3f7657eb42b05679d663d",
"surface_hash": "sha256:beeddac7cd934e409e47e5b4552dbc188840ad46205d8d48340569a97c51b59b",
"coverage": {
"surface_products": 26,
"products_with_metadata": 26,
"surface_tools": 846,
"tools_with_metadata": 846,
"tools_with_agent_summary": 846,
"tools_with_use_when": 846,
"tools_with_avoid_when": 846,
"tools_with_examples": 846,
"tools_with_interface_mode": 846,
"surface_tools": 850,
"tools_with_metadata": 850,
"tools_with_agent_summary": 850,
"tools_with_use_when": 850,
"tools_with_avoid_when": 850,
"tools_with_examples": 850,
"tools_with_interface_mode": 850,
"unmatched_skill_tools": 122,
"unreviewed_skill_tools": 11
},
+801
View File
@@ -2077,6 +2077,290 @@
"已知 processInstanceId 与待办 taskId,用户明确要求同意该审批任务时"
]
},
"oa approval create-instance": {
"agent_summary": "发起新的审批实例",
"agent_summary_source": "dws-agent-selection/oa",
"availability": "available",
"avoid_when": [
"只需预测流程时使用 forecast-process;用户尚未确认或字段未按 Schema 核对时不要发起"
],
"confirmation": "user_required",
"effect": "write",
"effect_source": "agent-hint",
"examples": [
"dws oa approval create-instance --process-code \u003cprocessCode\u003e --form-values '{\"事由\":\"测试\"}'",
"dws oa approval create-instance --request '{\"processCode\":\"PROC-xxx\",\"deptId\":-1,\"formComponentValues\":[{\"name\":\"事由\",\"value\":\"测试\"}],\"targetSelectActioners\":[{\"actionerKey\":\"manual-node\",\"actionerStaffIds\":[\"user-id\"]}]}'"
],
"field_provenance": {
"agent_summary": {
"value": "发起新的审批实例",
"source": "internal/cli/schema_hints/selection/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 start_process_instance;示例覆盖简单与高级请求契约,真实执行仍须在用户确认后显式添加 --yes。",
"candidates": [
{
"value": "发起新的审批实例",
"source": "internal/cli/schema_hints/selection/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 start_process_instance;示例覆盖简单与高级请求契约,真实执行仍须在用户确认后显式添加 --yes。"
}
]
},
"availability": {
"value": "available",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 start_process_instance;命令在调用前强制显式 --yes,未提供时直接拒绝,不进入交互确认。",
"candidates": [
{
"value": "available",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 start_process_instance;命令在调用前强制显式 --yes,未提供时直接拒绝,不进入交互确认。"
},
{
"value": "available",
"source": "internal/cli/schema_mcp_metadata.json#tools.oa.start_process_instance.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"avoid_when": {
"value": [
"只需预测流程时使用 forecast-process;用户尚未确认或字段未按 Schema 核对时不要发起"
],
"source": "internal/cli/schema_hints/selection/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 start_process_instance;示例覆盖简单与高级请求契约,真实执行仍须在用户确认后显式添加 --yes。",
"candidates": [
{
"value": [
"只需预测流程时使用 forecast-process;用户尚未确认或字段未按 Schema 核对时不要发起"
],
"source": "internal/cli/schema_hints/selection/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 start_process_instance;示例覆盖简单与高级请求契约,真实执行仍须在用户确认后显式添加 --yes。"
}
]
},
"confirmation": {
"value": "user_required",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 start_process_instance;命令在调用前强制显式 --yes,未提供时直接拒绝,不进入交互确认。",
"candidates": [
{
"value": "user_required",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 start_process_instance;命令在调用前强制显式 --yes,未提供时直接拒绝,不进入交互确认。"
},
{
"value": "not_required",
"source": "risk-default",
"precedence": "inference_or_default",
"selected": false
}
]
},
"effect": {
"value": "write",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 start_process_instance;命令在调用前强制显式 --yes,未提供时直接拒绝,不进入交互确认。",
"candidates": [
{
"value": "write",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 start_process_instance;命令在调用前强制显式 --yes,未提供时直接拒绝,不进入交互确认。"
},
{
"value": "write",
"source": "command-verb",
"precedence": "inference_or_default",
"selected": false
}
]
},
"examples": {
"value": [
"dws oa approval create-instance --process-code \u003cprocessCode\u003e --form-values '{\"事由\":\"测试\"}'",
"dws oa approval create-instance --request '{\"processCode\":\"PROC-xxx\",\"deptId\":-1,\"formComponentValues\":[{\"name\":\"事由\",\"value\":\"测试\"}],\"targetSelectActioners\":[{\"actionerKey\":\"manual-node\",\"actionerStaffIds\":[\"user-id\"]}]}'"
],
"source": "internal/cli/schema_hints/selection/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 start_process_instance;示例覆盖简单与高级请求契约,真实执行仍须在用户确认后显式添加 --yes。",
"candidates": [
{
"value": [
"dws oa approval create-instance --process-code \u003cprocessCode\u003e --form-values '{\"事由\":\"测试\"}'",
"dws oa approval create-instance --request '{\"processCode\":\"PROC-xxx\",\"deptId\":-1,\"formComponentValues\":[{\"name\":\"事由\",\"value\":\"测试\"}],\"targetSelectActioners\":[{\"actionerKey\":\"manual-node\",\"actionerStaffIds\":[\"user-id\"]}]}'"
],
"source": "internal/cli/schema_hints/selection/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 start_process_instance;示例覆盖简单与高级请求契约,真实执行仍须在用户确认后显式添加 --yes。"
}
]
},
"idempotency": {
"value": "non_idempotent",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 start_process_instance;命令在调用前强制显式 --yes,未提供时直接拒绝,不进入交互确认。",
"candidates": [
{
"value": "non_idempotent",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 start_process_instance;命令在调用前强制显式 --yes,未提供时直接拒绝,不进入交互确认。"
},
{
"value": "unknown",
"source": "effect-default",
"precedence": "inference_or_default",
"selected": false
}
]
},
"interface_mode": {
"value": "mcp",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 start_process_instance;命令在调用前强制显式 --yes,未提供时直接拒绝,不进入交互确认。",
"candidates": [
{
"value": "mcp",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 start_process_instance;命令在调用前强制显式 --yes,未提供时直接拒绝,不进入交互确认。"
},
{
"value": "mcp",
"source": "internal/cli/schema_mcp_metadata.json#tools.oa.start_process_instance.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"interface_ref": {
"value": "oa.start_process_instance",
"source": "internal/cli/schema_mcp_metadata.json#tools.oa.start_process_instance.interface_ref",
"precedence": "mcp_fallback",
"resolution": "highest_precedence",
"candidates": [
{
"value": "oa.start_process_instance",
"source": "internal/cli/schema_mcp_metadata.json#tools.oa.start_process_instance.interface_ref",
"precedence": "mcp_fallback",
"selected": true
}
]
},
"reviewed": {
"value": true,
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 start_process_instance;命令在调用前强制显式 --yes,未提供时直接拒绝,不进入交互确认。",
"candidates": [
{
"value": true,
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 start_process_instance;命令在调用前强制显式 --yes,未提供时直接拒绝,不进入交互确认。"
},
{
"value": true,
"source": "internal/cli/schema_hints/selection/oa.json",
"precedence": "reviewed_explicit",
"selected": false,
"review_reason": "公开 OA MCP tools/list 已验证 start_process_instance;示例覆盖简单与高级请求契约,真实执行仍须在用户确认后显式添加 --yes。"
}
]
},
"risk": {
"value": "high",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 start_process_instance;命令在调用前强制显式 --yes,未提供时直接拒绝,不进入交互确认。",
"candidates": [
{
"value": "high",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 start_process_instance;命令在调用前强制显式 --yes,未提供时直接拒绝,不进入交互确认。"
},
{
"value": "medium",
"source": "effect-default",
"precedence": "inference_or_default",
"selected": false
}
]
},
"use_when": {
"value": [
"用户确认要发起审批,且已查询表单 Schema、核对字段及审批路径后使用"
],
"source": "internal/cli/schema_hints/selection/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 start_process_instance;示例覆盖简单与高级请求契约,真实执行仍须在用户确认后显式添加 --yes。",
"candidates": [
{
"value": [
"用户确认要发起审批,且已查询表单 Schema、核对字段及审批路径后使用"
],
"source": "internal/cli/schema_hints/selection/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 start_process_instance;示例覆盖简单与高级请求契约,真实执行仍须在用户确认后显式添加 --yes。"
}
]
}
},
"idempotency": "non_idempotent",
"interface_mode": "mcp",
"interface_ref": {
"product_id": "oa",
"rpc_name": "start_process_instance"
},
"reviewed": true,
"risk": "high",
"source_refs": [
"CommandRegistry:canonical_path=oa.start_process_instance",
"cobra-help:dws oa approval create-instance",
"internal/cli/schema_hints/metadata/oa.json",
"internal/cli/schema_hints/selection/oa.json",
"internal/cli/schema_mcp_metadata.json#tools.oa.start_process_instance",
"live-mcp-tools-list:oa.start_process_instance",
"skills/mono/references/products/oa.md"
],
"use_when": [
"用户确认要发起审批,且已查询表单 Schema、核对字段及审批路径后使用"
]
},
"oa approval detail": {
"agent_summary": "获取指定审批实例的详情信息",
"agent_summary_source": "dws-agent-selection/oa",
@@ -2343,6 +2627,523 @@
"已知 processInstanceId,需要查看表单内容与当前状态详情时"
]
},
"oa approval forecast-process": {
"agent_summary": "预测审批流程与自选审批节点",
"agent_summary_source": "dws-agent-selection/oa",
"availability": "available",
"avoid_when": [
"需要真正创建审批单时改用 create-instance;未获得字段定义时先用 form-schema"
],
"confirmation": "not_required",
"effect": "read",
"effect_source": "agent-hint",
"examples": [
"dws oa approval forecast-process --process-code \u003cprocessCode\u003e --dept-id -1 --form-values '{\"金额\":\"100\"}'",
"dws oa approval forecast-process --request '{\"processCode\":\"PROC-xxx\",\"deptId\":-1,\"formComponentValues\":[[{\"name\":\"金额\",\"value\":\"100\"}]]}'"
],
"field_provenance": {
"agent_summary": {
"value": "预测审批流程与自选审批节点",
"source": "internal/cli/schema_hints/selection/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 forecast_process;示例覆盖 Cobra 简单模式与完整请求高级模式。",
"candidates": [
{
"value": "预测审批流程与自选审批节点",
"source": "internal/cli/schema_hints/selection/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 forecast_process;示例覆盖 Cobra 简单模式与完整请求高级模式。"
}
]
},
"availability": {
"value": "available",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 forecast_process,只预测流程而不创建审批实例。",
"candidates": [
{
"value": "available",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 forecast_process,只预测流程而不创建审批实例。"
},
{
"value": "available",
"source": "internal/cli/schema_mcp_metadata.json#tools.oa.forecast_process.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"avoid_when": {
"value": [
"需要真正创建审批单时改用 create-instance;未获得字段定义时先用 form-schema"
],
"source": "internal/cli/schema_hints/selection/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 forecast_process;示例覆盖 Cobra 简单模式与完整请求高级模式。",
"candidates": [
{
"value": [
"需要真正创建审批单时改用 create-instance;未获得字段定义时先用 form-schema"
],
"source": "internal/cli/schema_hints/selection/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 forecast_process;示例覆盖 Cobra 简单模式与完整请求高级模式。"
}
]
},
"confirmation": {
"value": "not_required",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 forecast_process,只预测流程而不创建审批实例。",
"candidates": [
{
"value": "not_required",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 forecast_process,只预测流程而不创建审批实例。"
}
]
},
"effect": {
"value": "read",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 forecast_process,只预测流程而不创建审批实例。",
"candidates": [
{
"value": "read",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 forecast_process,只预测流程而不创建审批实例。"
}
]
},
"examples": {
"value": [
"dws oa approval forecast-process --process-code \u003cprocessCode\u003e --dept-id -1 --form-values '{\"金额\":\"100\"}'",
"dws oa approval forecast-process --request '{\"processCode\":\"PROC-xxx\",\"deptId\":-1,\"formComponentValues\":[[{\"name\":\"金额\",\"value\":\"100\"}]]}'"
],
"source": "internal/cli/schema_hints/selection/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 forecast_process;示例覆盖 Cobra 简单模式与完整请求高级模式。",
"candidates": [
{
"value": [
"dws oa approval forecast-process --process-code \u003cprocessCode\u003e --dept-id -1 --form-values '{\"金额\":\"100\"}'",
"dws oa approval forecast-process --request '{\"processCode\":\"PROC-xxx\",\"deptId\":-1,\"formComponentValues\":[[{\"name\":\"金额\",\"value\":\"100\"}]]}'"
],
"source": "internal/cli/schema_hints/selection/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 forecast_process;示例覆盖 Cobra 简单模式与完整请求高级模式。"
}
]
},
"idempotency": {
"value": "idempotent",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 forecast_process,只预测流程而不创建审批实例。",
"candidates": [
{
"value": "idempotent",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 forecast_process,只预测流程而不创建审批实例。"
}
]
},
"interface_mode": {
"value": "mcp",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 forecast_process,只预测流程而不创建审批实例。",
"candidates": [
{
"value": "mcp",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 forecast_process,只预测流程而不创建审批实例。"
},
{
"value": "mcp",
"source": "internal/cli/schema_mcp_metadata.json#tools.oa.forecast_process.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"interface_ref": {
"value": "oa.forecast_process",
"source": "internal/cli/schema_mcp_metadata.json#tools.oa.forecast_process.interface_ref",
"precedence": "mcp_fallback",
"resolution": "highest_precedence",
"candidates": [
{
"value": "oa.forecast_process",
"source": "internal/cli/schema_mcp_metadata.json#tools.oa.forecast_process.interface_ref",
"precedence": "mcp_fallback",
"selected": true
}
]
},
"reviewed": {
"value": true,
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 forecast_process,只预测流程而不创建审批实例。",
"candidates": [
{
"value": true,
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 forecast_process,只预测流程而不创建审批实例。"
},
{
"value": true,
"source": "internal/cli/schema_hints/selection/oa.json",
"precedence": "reviewed_explicit",
"selected": false,
"review_reason": "公开 OA MCP tools/list 已验证 forecast_process;示例覆盖 Cobra 简单模式与完整请求高级模式。"
}
]
},
"risk": {
"value": "low",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 forecast_process,只预测流程而不创建审批实例。",
"candidates": [
{
"value": "low",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 forecast_process,只预测流程而不创建审批实例。"
}
]
},
"use_when": {
"value": [
"已知道 processCode 且已根据表单 Schema 组装字段,需要在发起前确认审批路径或自选节点时"
],
"source": "internal/cli/schema_hints/selection/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 forecast_process;示例覆盖 Cobra 简单模式与完整请求高级模式。",
"candidates": [
{
"value": [
"已知道 processCode 且已根据表单 Schema 组装字段,需要在发起前确认审批路径或自选节点时"
],
"source": "internal/cli/schema_hints/selection/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 forecast_process;示例覆盖 Cobra 简单模式与完整请求高级模式。"
}
]
}
},
"idempotency": "idempotent",
"interface_mode": "mcp",
"interface_ref": {
"product_id": "oa",
"rpc_name": "forecast_process"
},
"reviewed": true,
"risk": "low",
"source_refs": [
"CommandRegistry:canonical_path=oa.forecast_process",
"cobra-help:dws oa approval forecast-process",
"internal/cli/schema_hints/metadata/oa.json",
"internal/cli/schema_hints/selection/oa.json",
"internal/cli/schema_mcp_metadata.json#tools.oa.forecast_process",
"live-mcp-tools-list:oa.forecast_process",
"skills/mono/references/products/oa.md"
],
"use_when": [
"已知道 processCode 且已根据表单 Schema 组装字段,需要在发起前确认审批路径或自选节点时"
]
},
"oa approval form-schema": {
"agent_summary": "查询审批模板的表单 Schema",
"agent_summary_source": "dws-agent-selection/oa",
"availability": "available",
"avoid_when": [
"只需列出可用模板时使用 list-forms;不要把返回的 Schema 当作可直接提交的实例请求"
],
"confirmation": "not_required",
"effect": "read",
"effect_source": "agent-hint",
"examples": [
"dws oa approval form-schema --process-code \u003cprocessCode\u003e"
],
"field_provenance": {
"agent_summary": {
"value": "查询审批模板的表单 Schema",
"source": "internal/cli/schema_hints/selection/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 get_process_schema,按 GitHub OA 命令树手写选型。",
"candidates": [
{
"value": "查询审批模板的表单 Schema",
"source": "internal/cli/schema_hints/selection/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 get_process_schema,按 GitHub OA 命令树手写选型。"
}
]
},
"availability": {
"value": "available",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 get_process_schema,可按 processCode 只读查询表单结构。",
"candidates": [
{
"value": "available",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 get_process_schema,可按 processCode 只读查询表单结构。"
},
{
"value": "available",
"source": "internal/cli/schema_mcp_metadata.json#tools.oa.get_process_schema.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"avoid_when": {
"value": [
"只需列出可用模板时使用 list-forms;不要把返回的 Schema 当作可直接提交的实例请求"
],
"source": "internal/cli/schema_hints/selection/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 get_process_schema,按 GitHub OA 命令树手写选型。",
"candidates": [
{
"value": [
"只需列出可用模板时使用 list-forms;不要把返回的 Schema 当作可直接提交的实例请求"
],
"source": "internal/cli/schema_hints/selection/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 get_process_schema,按 GitHub OA 命令树手写选型。"
}
]
},
"confirmation": {
"value": "not_required",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 get_process_schema,可按 processCode 只读查询表单结构。",
"candidates": [
{
"value": "not_required",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 get_process_schema,可按 processCode 只读查询表单结构。"
}
]
},
"effect": {
"value": "read",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 get_process_schema,可按 processCode 只读查询表单结构。",
"candidates": [
{
"value": "read",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 get_process_schema,可按 processCode 只读查询表单结构。"
}
]
},
"examples": {
"value": [
"dws oa approval form-schema --process-code \u003cprocessCode\u003e"
],
"source": "internal/cli/schema_hints/selection/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 get_process_schema,按 GitHub OA 命令树手写选型。",
"candidates": [
{
"value": [
"dws oa approval form-schema --process-code \u003cprocessCode\u003e"
],
"source": "internal/cli/schema_hints/selection/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 get_process_schema,按 GitHub OA 命令树手写选型。"
}
]
},
"idempotency": {
"value": "idempotent",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 get_process_schema,可按 processCode 只读查询表单结构。",
"candidates": [
{
"value": "idempotent",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 get_process_schema,可按 processCode 只读查询表单结构。"
}
]
},
"interface_mode": {
"value": "mcp",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 get_process_schema,可按 processCode 只读查询表单结构。",
"candidates": [
{
"value": "mcp",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 get_process_schema,可按 processCode 只读查询表单结构。"
},
{
"value": "mcp",
"source": "internal/cli/schema_mcp_metadata.json#tools.oa.get_process_schema.interface_ref",
"precedence": "mcp_fallback",
"selected": false
}
]
},
"interface_ref": {
"value": "oa.get_process_schema",
"source": "internal/cli/schema_mcp_metadata.json#tools.oa.get_process_schema.interface_ref",
"precedence": "mcp_fallback",
"resolution": "highest_precedence",
"candidates": [
{
"value": "oa.get_process_schema",
"source": "internal/cli/schema_mcp_metadata.json#tools.oa.get_process_schema.interface_ref",
"precedence": "mcp_fallback",
"selected": true
}
]
},
"reviewed": {
"value": true,
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 get_process_schema,可按 processCode 只读查询表单结构。",
"candidates": [
{
"value": true,
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 get_process_schema,可按 processCode 只读查询表单结构。"
},
{
"value": true,
"source": "internal/cli/schema_hints/selection/oa.json",
"precedence": "reviewed_explicit",
"selected": false,
"review_reason": "公开 OA MCP tools/list 已验证 get_process_schema,按 GitHub OA 命令树手写选型。"
}
]
},
"risk": {
"value": "low",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 get_process_schema,可按 processCode 只读查询表单结构。",
"candidates": [
{
"value": "low",
"source": "internal/cli/schema_hints/metadata/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 get_process_schema,可按 processCode 只读查询表单结构。"
}
]
},
"use_when": {
"value": [
"已从 list-forms 或 search-forms 获得 processCode,需要读取字段、选项和必填规则后再填写审批时"
],
"source": "internal/cli/schema_hints/selection/oa.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "公开 OA MCP tools/list 已验证 get_process_schema,按 GitHub OA 命令树手写选型。",
"candidates": [
{
"value": [
"已从 list-forms 或 search-forms 获得 processCode,需要读取字段、选项和必填规则后再填写审批时"
],
"source": "internal/cli/schema_hints/selection/oa.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "公开 OA MCP tools/list 已验证 get_process_schema,按 GitHub OA 命令树手写选型。"
}
]
}
},
"idempotency": "idempotent",
"interface_mode": "mcp",
"interface_ref": {
"product_id": "oa",
"rpc_name": "get_process_schema"
},
"reviewed": true,
"risk": "low",
"source_refs": [
"CommandRegistry:canonical_path=oa.get_process_schema",
"cobra-help:dws oa approval form-schema",
"internal/cli/schema_hints/metadata/oa.json",
"internal/cli/schema_hints/selection/oa.json",
"internal/cli/schema_mcp_metadata.json#tools.oa.get_process_schema",
"live-mcp-tools-list:oa.get_process_schema",
"skills/mono/references/products/oa.md"
],
"use_when": [
"已从 list-forms 或 search-forms 获得 processCode,需要读取字段、选项和必填规则后再填写审批时"
]
},
"oa approval list-cc": {
"agent_summary": "获取抄送用户的列表",
"agent_summary_source": "dws-agent-selection/oa",
@@ -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",
+100 -44
View File
@@ -1,20 +1,20 @@
{
"version": 1,
"source_hash": "sha256:b513a679eb51b64afa3f31364b45c29bc4590f0568908650f426ac8f4b041b2b",
"surface_hash": "sha256:41044fe1b6723564c40d684381ac6d6f23c4dfd58f9769350f02f223f02fa894",
"source_files": 160,
"source_hash": "sha256:9a6bc544f78424a85d7465cbb8ce20ee80c954de7ec3f7657eb42b05679d663d",
"surface_hash": "sha256:beeddac7cd934e409e47e5b4552dbc188840ad46205d8d48340569a97c51b59b",
"source_files": 162,
"hint_files": 54,
"hint_products": 43,
"hint_tools": 1844,
"hint_tools": 1852,
"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": 423,
"surface_tools": 418,
"eligible_summaries": 417,
"applied_summaries": 0,
"preserved_summaries": 413,
"preserved_summaries": 417,
"rejected_tools": [
"chat.unread_message_conversation_list"
],
@@ -29,13 +29,13 @@
"coverage": {
"surface_products": 26,
"products_with_metadata": 26,
"surface_tools": 846,
"tools_with_metadata": 846,
"tools_with_agent_summary": 846,
"tools_with_use_when": 846,
"tools_with_avoid_when": 846,
"tools_with_examples": 846,
"tools_with_interface_mode": 846,
"surface_tools": 850,
"tools_with_metadata": 850,
"tools_with_agent_summary": 850,
"tools_with_use_when": 850,
"tools_with_avoid_when": 850,
"tools_with_examples": 850,
"tools_with_interface_mode": 850,
"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",
@@ -3671,8 +3671,8 @@
"line": 114,
"candidates": [
"oa approval approve",
"oa approval detail",
"oa approval list-cc"
"oa approval create-instance",
"oa approval detail"
],
"review": {
"status": "stale",
@@ -3685,8 +3685,8 @@
"line": 115,
"candidates": [
"oa approval approve",
"oa approval detail",
"oa approval list-cc"
"oa approval create-instance",
"oa approval detail"
],
"review": {
"status": "stale",
@@ -3696,11 +3696,11 @@
{
"tool_path": "oa approval ding-info",
"source": "skills/mono/references/products/oa.md",
"line": 128,
"line": 485,
"candidates": [
"oa approval approve",
"oa approval detail",
"oa approval list-cc"
"oa approval create-instance",
"oa approval detail"
],
"review": {
"status": "stale",
@@ -3710,11 +3710,11 @@
{
"tool_path": "oa approval ding-info",
"source": "skills/mono/references/products/oa.md",
"line": 149,
"line": 506,
"candidates": [
"oa approval approve",
"oa approval detail",
"oa approval list-cc"
"oa approval create-instance",
"oa approval detail"
],
"review": {
"status": "stale",
@@ -3724,11 +3724,11 @@
{
"tool_path": "oa approval revert-activities",
"source": "skills/mono/references/products/oa.md",
"line": 164,
"line": 521,
"candidates": [
"oa approval approve",
"oa approval detail",
"oa approval list-cc"
"oa approval create-instance",
"oa approval detail"
],
"review": {
"status": "stale",
@@ -3738,11 +3738,11 @@
{
"tool_path": "oa approval append-task",
"source": "skills/mono/references/products/oa.md",
"line": 276,
"line": 633,
"candidates": [
"oa approval approve",
"oa approval detail",
"oa approval list-cc"
"oa approval create-instance",
"oa approval detail"
],
"review": {
"status": "stale",
@@ -3752,11 +3752,11 @@
{
"tool_path": "oa approval append-task",
"source": "skills/mono/references/products/oa.md",
"line": 277,
"line": 634,
"candidates": [
"oa approval approve",
"oa approval detail",
"oa approval list-cc"
"oa approval create-instance",
"oa approval detail"
],
"review": {
"status": "stale",
@@ -3766,11 +3766,11 @@
{
"tool_path": "oa approval revert-task",
"source": "skills/mono/references/products/oa.md",
"line": 297,
"line": 654,
"candidates": [
"oa approval approve",
"oa approval detail",
"oa approval list-cc"
"oa approval create-instance",
"oa approval detail"
],
"review": {
"status": "stale",
@@ -3780,11 +3780,67 @@
{
"tool_path": "oa approval revert-task",
"source": "skills/mono/references/products/oa.md",
"line": 299,
"line": 656,
"candidates": [
"oa approval approve",
"oa approval detail",
"oa approval list-cc"
"oa approval create-instance",
"oa approval detail"
],
"review": {
"status": "stale",
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "oa approval search-forms",
"source": "skills/mono/references/products/oa.md",
"line": 676,
"candidates": [
"oa approval approve",
"oa approval create-instance",
"oa approval detail"
],
"review": {
"status": "stale",
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "oa approval search-forms",
"source": "skills/mono/references/products/oa.md",
"line": 682,
"candidates": [
"oa approval approve",
"oa approval create-instance",
"oa approval detail"
],
"review": {
"status": "stale",
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "oa approval search-forms",
"source": "skills/mono/references/products/oa.md",
"line": 724,
"candidates": [
"oa approval approve",
"oa approval create-instance",
"oa approval detail"
],
"review": {
"status": "stale",
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "oa approval search-forms",
"source": "skills/mono/references/products/oa.md",
"line": 754,
"candidates": [
"oa approval approve",
"oa approval create-instance",
"oa approval detail"
],
"review": {
"status": "stale",
@@ -3839,8 +3895,8 @@
"line": 102,
"candidates": [
"oa approval approve",
"oa approval detail",
"oa approval list-cc"
"oa approval create-instance",
"oa approval detail"
],
"review": {
"status": "group",
+139 -13
View File
@@ -1,17 +1,17 @@
{
"version": 1,
"surface_hash": "sha256:41044fe1b6723564c40d684381ac6d6f23c4dfd58f9769350f02f223f02fa894",
"source_hash": "sha256:1b4f0e6e6fe49115137ff81717462374b2fdd05ad4ca1bfb1b2dd8272aa6bad8",
"surface_hash": "sha256:beeddac7cd934e409e47e5b4552dbc188840ad46205d8d48340569a97c51b59b",
"source_hash": "sha256:6fe48c19b11a2d917fad3d59c96b40bf1b7620ed4334d96cef6544dbe3742e30",
"catalog": {
"agent_metadata": {
"products_with_metadata": 26,
"source": "embedded-skill-metadata",
"source_hash": "sha256:b513a679eb51b64afa3f31364b45c29bc4590f0568908650f426ac8f4b041b2b",
"surface_hash": "sha256:41044fe1b6723564c40d684381ac6d6f23c4dfd58f9769350f02f223f02fa894",
"source_hash": "sha256:9a6bc544f78424a85d7465cbb8ce20ee80c954de7ec3f7657eb42b05679d663d",
"surface_hash": "sha256:beeddac7cd934e409e47e5b4552dbc188840ad46205d8d48340569a97c51b59b",
"surface_products": 26,
"surface_tools": 846,
"tools_with_agent_summary": 846,
"tools_with_metadata": 846,
"surface_tools": 850,
"tools_with_agent_summary": 850,
"tools_with_metadata": 850,
"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": 423,
"version": 1
},
"kind": "schema",
@@ -21117,7 +21117,7 @@
"id": "oa",
"name": "OA 审批 / 同意 / 拒绝 / 撤销",
"runtime": true,
"tool_count": 22,
"tool_count": 25,
"tools": [
{
"agent_metadata_source": "embedded-skill-metadata",
@@ -21183,6 +21183,37 @@
"已知 processInstanceId,需要为审批实例添加评论文本时"
]
},
{
"agent_metadata_source": "embedded-skill-metadata",
"agent_summary": "预测审批流程与自选审批节点",
"agent_summary_source": "dws-agent-selection/oa",
"availability": "available",
"avoid_when": [
"需要真正创建审批单时改用 create-instance;未获得字段定义时先用 form-schema"
],
"canonical_path": "oa.forecast_process",
"cli_name": "forecast-process",
"cli_path": "oa approval forecast-process",
"confirmation": "not_required",
"description": "根据表单值预测审批流程与自选节点",
"effect": "read",
"group": "approval",
"idempotency": "idempotent",
"interface_mode": "mcp",
"interface_ref": {
"product_id": "oa",
"rpc_name": "forecast_process"
},
"metadata_source": "embedded-mcp-metadata",
"name": "forecast_process",
"primary_cli_path": "oa approval forecast-process",
"reviewed": true,
"risk": "low",
"title": "根据表单值预测审批流程与自选节点",
"use_when": [
"已知道 processCode 且已根据表单 Schema 组装字段,需要在发起前确认审批路径或自选节点时"
]
},
{
"agent_metadata_source": "embedded-skill-metadata",
"agent_summary": "获取员工已处理任务列表",
@@ -21311,6 +21342,37 @@
"已知 processInstanceId,需要查看谁做了什么审批操作及结果时"
]
},
{
"agent_metadata_source": "embedded-skill-metadata",
"agent_summary": "查询审批模板的表单 Schema",
"agent_summary_source": "dws-agent-selection/oa",
"availability": "available",
"avoid_when": [
"只需列出可用模板时使用 list-forms;不要把返回的 Schema 当作可直接提交的实例请求"
],
"canonical_path": "oa.get_process_schema",
"cli_name": "form-schema",
"cli_path": "oa approval form-schema",
"confirmation": "not_required",
"description": "查询审批模板的表单 Schema",
"effect": "read",
"group": "approval",
"idempotency": "idempotent",
"interface_mode": "mcp",
"interface_ref": {
"product_id": "oa",
"rpc_name": "get_process_schema"
},
"metadata_source": "embedded-mcp-metadata",
"name": "get_process_schema",
"primary_cli_path": "oa approval form-schema",
"reviewed": true,
"risk": "low",
"title": "查询审批模板的表单 Schema",
"use_when": [
"已从 list-forms 或 search-forms 获得 processCode,需要读取字段、选项和必填规则后再填写审批时"
]
},
{
"agent_metadata_source": "embedded-skill-metadata",
"agent_summary": "获取已提交实例列表",
@@ -21779,6 +21841,37 @@
"use_when": [
"当你已知想找的审批大致名称(如「报销」「请假」)、想快速定位对应表单及其 processCode 时使用,比 +list-forms 全量列举更高效;传入关键字,返回名称或 processCode 匹配的表单,供后续 +list-initiated 按模板查询。"
]
},
{
"agent_metadata_source": "embedded-skill-metadata",
"agent_summary": "发起新的审批实例",
"agent_summary_source": "dws-agent-selection/oa",
"availability": "available",
"avoid_when": [
"只需预测流程时使用 forecast-process;用户尚未确认或字段未按 Schema 核对时不要发起"
],
"canonical_path": "oa.start_process_instance",
"cli_name": "create-instance",
"cli_path": "oa approval create-instance",
"confirmation": "user_required",
"description": "发起审批实例(需要 --yes 确认)",
"effect": "write",
"group": "approval",
"idempotency": "non_idempotent",
"interface_mode": "mcp",
"interface_ref": {
"product_id": "oa",
"rpc_name": "start_process_instance"
},
"metadata_source": "embedded-mcp-metadata",
"name": "start_process_instance",
"primary_cli_path": "oa approval create-instance",
"reviewed": true,
"risk": "high",
"title": "发起审批实例(需要 --yes 确认)",
"use_when": [
"用户确认要发起审批,且已查询表单 Schema、核对字段及审批路径后使用"
]
}
],
"use_when": [
@@ -26107,7 +26200,7 @@
"avoid_when": [
"需要编辑在线文档正文时使用 doc;只管理钉盘普通文件时使用 drive"
],
"description": "知识库 / 空间管理 / 节点管理 / 成员管理",
"description": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
"field_provenance": {
"agent_summary": {
"candidates": [
@@ -26161,9 +26254,9 @@
}
},
"id": "wiki",
"name": "知识库 / 空间管理 / 节点管理 / 成员管理",
"name": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
"runtime": true,
"tool_count": 16,
"tool_count": 17,
"tools": [
{
"agent_metadata_source": "embedded-skill-metadata",
@@ -26450,6 +26543,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": "将知识库中的节点复制到指定位置",
@@ -26671,6 +26797,6 @@
}
],
"source": "embedded-command-catalog",
"tool_count": 846
"tool_count": 850
}
}
+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": {
File diff suppressed because it is too large Load Diff
+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": [
@@ -61,6 +61,18 @@
"canonical_path": "oa.list_pending_tasks",
"cli_path": "oa approval tasks"
},
{
"canonical_path": "oa.get_process_schema",
"cli_path": "oa approval form-schema"
},
{
"canonical_path": "oa.forecast_process",
"cli_path": "oa approval forecast-process"
},
{
"canonical_path": "oa.start_process_instance",
"cli_path": "oa approval create-instance"
},
{
"canonical_path": "oa.shortcut_list_pending",
"cli_path": "oa +list-pending"
@@ -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"
@@ -91,6 +91,21 @@
"reviewed": true,
"runtime_gate": "confirm_delete"
},
"oa.get_process_schema": {
"effect": "read", "risk": "low", "confirmation": "not_required", "idempotency": "idempotent",
"interface_mode": "mcp", "availability": "available", "reviewed": true, "runtime_gate": "none",
"review_reason": "公开 OA MCP tools/list 已验证 get_process_schema,可按 processCode 只读查询表单结构。"
},
"oa.forecast_process": {
"effect": "read", "risk": "low", "confirmation": "not_required", "idempotency": "idempotent",
"interface_mode": "mcp", "availability": "available", "reviewed": true, "runtime_gate": "none",
"review_reason": "公开 OA MCP tools/list 已验证 forecast_process,只预测流程而不创建审批实例。"
},
"oa.start_process_instance": {
"effect": "write", "risk": "high", "confirmation": "user_required", "idempotency": "non_idempotent",
"interface_mode": "mcp", "availability": "available", "reviewed": true, "runtime_gate": "typed_yes",
"review_reason": "公开 OA MCP tools/list 已验证 start_process_instance;命令在调用前强制显式 --yes,未提供时直接拒绝,不进入交互确认。"
},
"oa.shortcut_list_pending": {
"effect": "read",
"risk": "low",
@@ -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": 846,
"source_tools": 850,
"matched_tools": 71
},
"tools": {
@@ -32,6 +32,39 @@
"live-dws-schema:oa.approve_processInstance"
]
},
"oa.get_process_schema": {
"agent_summary": "查询审批模板的表单 Schema",
"use_when": ["已从 list-forms 或 search-forms 获得 processCode,需要读取字段、选项和必填规则后再填写审批时"],
"avoid_when": ["只需列出可用模板时使用 list-forms;不要把返回的 Schema 当作可直接提交的实例请求"],
"examples": ["dws oa approval form-schema --process-code <processCode>"],
"reviewed": true,
"review_reason": "公开 OA MCP tools/list 已验证 get_process_schema,按 GitHub OA 命令树手写选型。",
"source_refs": ["CommandRegistry:canonical_path=oa.get_process_schema", "cobra-help:dws oa approval form-schema", "live-mcp-tools-list:oa.get_process_schema"]
},
"oa.forecast_process": {
"agent_summary": "预测审批流程与自选审批节点",
"use_when": ["已知道 processCode 且已根据表单 Schema 组装字段,需要在发起前确认审批路径或自选节点时"],
"avoid_when": ["需要真正创建审批单时改用 create-instance;未获得字段定义时先用 form-schema"],
"examples": [
"dws oa approval forecast-process --process-code <processCode> --dept-id -1 --form-values '{\"金额\":\"100\"}'",
"dws oa approval forecast-process --request '{\"processCode\":\"PROC-xxx\",\"deptId\":-1,\"formComponentValues\":[[{\"name\":\"金额\",\"value\":\"100\"}]]}'"
],
"reviewed": true,
"review_reason": "公开 OA MCP tools/list 已验证 forecast_process;示例覆盖 Cobra 简单模式与完整请求高级模式。",
"source_refs": ["CommandRegistry:canonical_path=oa.forecast_process", "cobra-help:dws oa approval forecast-process", "live-mcp-tools-list:oa.forecast_process"]
},
"oa.start_process_instance": {
"agent_summary": "发起新的审批实例",
"use_when": ["用户确认要发起审批,且已查询表单 Schema、核对字段及审批路径后使用"],
"avoid_when": ["只需预测流程时使用 forecast-process;用户尚未确认或字段未按 Schema 核对时不要发起"],
"examples": [
"dws oa approval create-instance --process-code <processCode> --form-values '{\"事由\":\"测试\"}'",
"dws oa approval create-instance --request '{\"processCode\":\"PROC-xxx\",\"deptId\":-1,\"formComponentValues\":[{\"name\":\"事由\",\"value\":\"测试\"}],\"targetSelectActioners\":[{\"actionerKey\":\"manual-node\",\"actionerStaffIds\":[\"user-id\"]}]}'"
],
"reviewed": true,
"review_reason": "公开 OA MCP tools/list 已验证 start_process_instance;示例覆盖简单与高级请求契约,真实执行仍须在用户确认后显式添加 --yes。",
"source_refs": ["CommandRegistry:canonical_path=oa.start_process_instance", "cobra-help:dws oa approval create-instance", "live-mcp-tools-list:oa.start_process_instance"]
},
"oa.dingflow_comments": {
"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": [
+51
View File
@@ -17,6 +17,30 @@
"unmatched_tools": 85
},
"tools": {
"oa.forecast_process": {
"title": "forecast_process",
"description": "审批流程预测",
"parameters": {
"ProcessForecastPopRequest": {"type": "object", "description": "ProcessForecastPopRequest", "required": false}
},
"interface_ref": {"product_id": "oa", "rpc_name": "forecast_process"}
},
"oa.get_process_schema": {
"title": "get_process_schema",
"description": "根据表单的processCode,获取该表单的 schema 信息",
"parameters": {
"processCode": {"type": "string", "description": "需要查询schema信息的表单processCode", "required": true}
},
"interface_ref": {"product_id": "oa", "rpc_name": "get_process_schema"}
},
"oa.start_process_instance": {
"title": "审批发起表单实例",
"description": "根据用户输入审批表单内容、流程、抄送人等信息发起审批",
"parameters": {
"ProcessInstanceCreationPopRequest": {"type": "object", "description": "ProcessInstanceCreationPopRequest", "required": false}
},
"interface_ref": {"product_id": "oa", "rpc_name": "start_process_instance"}
},
"aisearch.enterprise_person_search": {
"title": "enterprise_person_search",
"description": "企业内找人搜索工具,支持按姓名、部门、职位、技能、工作职责等多维度搜索企业内人员。\\n\\n**适用场景示例**:\\n- 找一下负责智能化的人\\n- 帮我找产品经理\\n- 技术部有哪些人\\n- 张三的领导是谁\\n- 李四手下有哪些人\\n- 帮我找下王芳\\n- 谁在做大模型相关的工作\\n- 公司有哪些架构师\\n- 市场部的同事有谁\\n- 有没有会Java的同事\\n- 研发部的负责人是谁\\n- 帮我找一下跟智能客服有关的人\\n- 做设计的同事有谁\\n- 谁是技术总监\\n- 张三在哪个部门\\n\\n",
@@ -10666,6 +10690,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 无法立即返回,请稍后查看目标文件夹确认结果。",
+31 -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:157aaf77922525f0cc010ce25432282c27c87f9b8ce2ef877a43724f2bc6d15b",
"reason": "Reviewed v3 baseline after combining the OA full-request wrapper bindings and exclusions with wiki.list_workspace_feeds workspaceId/maxResults/nextToken/excludeFile bindings.",
"reviewed": true
},
"removals": {
@@ -955,6 +955,9 @@
"limit": "pageSize",
"page": "pageNumber"
},
"oa.forecast_process": {
"request": "ProcessForecastPopRequest"
},
"oa.list_initiated_instances": {
"cursor": "nextToken",
"end": "endTime",
@@ -983,6 +986,9 @@
"oa.revoke_processInstance": {
"instance-id": "processInstanceId"
},
"oa.start_process_instance": {
"request": "ProcessInstanceCreationPopRequest"
},
"report.get_received_report_list": {
"end": "endTime",
"start": "startTime"
@@ -1324,6 +1330,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 +1802,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.",
@@ -1965,6 +1983,17 @@
"sheet.write_image --file": "local upload input used to obtain resourceId/resourceUrl",
"sheet.write_image --mime-type": "local upload metadata",
"sheet.write_image --name": "local upload metadata",
"oa.forecast_process --dept-id": "Conditional request wrapper: --dept-id is encoded inside ProcessForecastPopRequest together with the other simple-mode flags, so it has no independent top-level MCP property.",
"oa.forecast_process --form-values": "Conditional request wrapper: --form-values is transformed into formComponentValues inside ProcessForecastPopRequest, so it has no independent top-level MCP property.",
"oa.forecast_process --process-code": "Conditional request wrapper: --process-code is encoded inside ProcessForecastPopRequest together with the other simple-mode flags, so it has no independent top-level MCP property.",
"oa.start_process_instance --approvers": "Conditional request wrapper: --approvers is transformed into an approvers array inside ProcessInstanceCreationPopRequest, so it has no independent top-level MCP property.",
"oa.start_process_instance --approvers-action-type": "Conditional request wrapper: --approvers-action-type only configures the generated approvers array inside ProcessInstanceCreationPopRequest, so it has no independent top-level MCP property.",
"oa.start_process_instance --cc-list": "Conditional request wrapper: --cc-list is encoded inside ProcessInstanceCreationPopRequest only when supplied, so it has no independent top-level MCP property.",
"oa.start_process_instance --cc-position": "Conditional request wrapper: --cc-position only configures the generated ccList inside ProcessInstanceCreationPopRequest, so it has no independent top-level MCP property.",
"oa.start_process_instance --dept-id": "Conditional request wrapper: --dept-id is encoded inside ProcessInstanceCreationPopRequest together with the other simple-mode flags, so it has no independent top-level MCP property.",
"oa.start_process_instance --form-values": "Conditional request wrapper: --form-values is transformed into formComponentValues inside ProcessInstanceCreationPopRequest, so it has no independent top-level MCP property.",
"oa.start_process_instance --originator-user-id": "Conditional request wrapper: --originator-user-id is encoded inside ProcessInstanceCreationPopRequest only when supplied, so it has no independent top-level MCP property.",
"oa.start_process_instance --process-code": "Conditional request wrapper: --process-code is encoded inside ProcessInstanceCreationPopRequest together with the other simple-mode flags, so it has no independent top-level MCP property.",
"todo.add_todo_attachment --file-path": "local upload input used to construct attachmentList",
"todo.get_user_todos_in_current_org --query-all": "Local route selector: the default path calls get_user_todos_in_current_org, while --query-all switches to get_user_todos; it is not a property of the pinned default RPC.",
"todo.list_todo_attachment --task-id": "Reviewed unpinned adapter: --task-id is nested under todoAttachmentListRequest at runtime, while the immutable pinned MCP snapshot has no interface_ref for todo.list_todo_attachment.",
+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
+169 -1
View File
@@ -1,20 +1,54 @@
package helpers
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"strconv"
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
"github.com/spf13/cobra"
)
func decodeOARequest(raw string) (map[string]any, error) {
dec := json.NewDecoder(bytes.NewBufferString(raw))
dec.UseNumber()
var request map[string]any
if err := dec.Decode(&request); err != nil || request == nil {
if err != nil {
return nil, err
}
return nil, fmt.Errorf("JSON 请求不能为 null")
}
if err := dec.Decode(new(any)); !errors.Is(err, io.EOF) {
return nil, fmt.Errorf("JSON 请求包含多余内容")
}
return request, nil
}
func oaFormValues(raw string) ([]map[string]string, error) {
var values map[string]string
if err := json.Unmarshal([]byte(raw), &values); err != nil {
return nil, err
}
result := make([]map[string]string, 0, len(values))
for name, value := range values {
result = append(result, map[string]string{"name": name, "value": value})
}
return result, nil
}
// ──────────────────────────────────────────────────────────
// dws oa — OA 审批
// MCP tools(tools/list): list_pending_approvals, get_processInstance_detail,
// approve_processInstance, reject_processInstance, revoke_processInstance,
// get_processInstance_records, list_initiated_instances, list_pending_tasks,
// list_user_visible_process, append_task, search_form, oa_ding_user, revert_task,
// get_inst_revert_activities
// get_inst_revert_activities, get_process_schema, forecast_process,
// start_process_instance
// ──────────────────────────────────────────────────────────
func newOaCommand() *cobra.Command {
@@ -463,6 +497,97 @@ func newOaCommand() *cobra.Command {
},
}
approvalFormSchemaCmd := &cobra.Command{
Use: "form-schema", Short: "查询审批模板的表单 Schema",
Example: "dws oa approval form-schema --process-code <processCode>",
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "process-code"); err != nil {
return err
}
return callMCPTool("get_process_schema", map[string]any{"processCode": mustGetFlag(cmd, "process-code")})
},
}
approvalForecastCmd := &cobra.Command{
Use: "forecast-process", Short: "根据表单值预测审批流程与自选节点",
Example: "dws oa approval forecast-process --process-code <processCode> --dept-id -1 --form-values '{\"金额\":\"100\"}'",
RunE: func(cmd *cobra.Command, args []string) error {
if raw, _ := cmd.Flags().GetString("request"); raw != "" {
request, err := decodeOARequest(raw)
if err != nil {
return fmt.Errorf("--request JSON 解析失败: %w", err)
}
return callMCPTool("forecast_process", map[string]any{"ProcessForecastPopRequest": request})
}
if err := validateRequiredFlags(cmd, "process-code", "dept-id", "form-values"); err != nil {
return err
}
deptID, err := strconv.ParseInt(mustGetFlag(cmd, "dept-id"), 10, 64)
if err != nil {
return fmt.Errorf("--dept-id 必须为整数: %w", err)
}
values, err := oaFormValues(mustGetFlag(cmd, "form-values"))
if err != nil {
return fmt.Errorf("--form-values JSON 解析失败: %w", err)
}
return callMCPTool("forecast_process", map[string]any{"ProcessForecastPopRequest": map[string]any{"processCode": mustGetFlag(cmd, "process-code"), "deptId": deptID, "formComponentValues": [][]map[string]string{values}}})
},
}
approvalCreateCmd := &cobra.Command{
Use: "create-instance", Short: "发起审批实例(需要 --yes 确认)",
Example: "dws oa approval create-instance --process-code <processCode> --form-values '{\"事由\":\"测试\"}' --yes",
RunE: func(cmd *cobra.Command, args []string) error {
if !commandDryRun(cmd) {
yes, _ := cmd.Flags().GetBool("yes")
if !yes {
return fmt.Errorf("发起审批实例会创建真实业务数据;请先核对参数,然后添加 --yes 确认执行")
}
}
var request map[string]any
if raw, _ := cmd.Flags().GetString("request"); raw != "" {
var err error
request, err = decodeOARequest(raw)
if err != nil {
return fmt.Errorf("--request JSON 解析失败: %w", err)
}
} else {
if err := validateRequiredFlags(cmd, "process-code", "form-values"); err != nil {
return err
}
values, err := oaFormValues(mustGetFlag(cmd, "form-values"))
if err != nil {
return fmt.Errorf("--form-values JSON 解析失败: %w", err)
}
request = map[string]any{"processCode": mustGetFlag(cmd, "process-code"), "formComponentValues": values}
if dept, _ := cmd.Flags().GetString("dept-id"); dept != "" {
value, err := strconv.ParseInt(dept, 10, 64)
if err != nil {
return fmt.Errorf("--dept-id 必须为整数: %w", err)
}
request["deptId"] = value
}
if userID, _ := cmd.Flags().GetString("originator-user-id"); userID != "" {
request["originatorUserId"] = userID
}
if rawApprovers, _ := cmd.Flags().GetString("approvers"); rawApprovers != "" {
action, _ := cmd.Flags().GetString("approvers-action-type")
if action != "AND" && action != "OR" && action != "NONE" {
return fmt.Errorf("--approvers-action-type 必须为 AND、OR 或 NONE")
}
request["approvers"] = []map[string]any{{"actionType": action, "userIds": strings.Split(rawApprovers, ",")}}
}
if rawCC, _ := cmd.Flags().GetString("cc-list"); rawCC != "" {
position, _ := cmd.Flags().GetString("cc-position")
if position != "START" && position != "FINISH" && position != "START_FINISH" {
return fmt.Errorf("--cc-position 必须为 START、FINISH 或 START_FINISH")
}
request["ccList"] = strings.Split(rawCC, ",")
request["ccPosition"] = position
}
}
return callMCPTool("start_process_instance", map[string]any{"ProcessInstanceCreationPopRequest": request})
},
}
approvalListPendingCmd.Flags().String("start", "", "开始时间 ISO-8601 (如 2026-03-10T00:00:00+08:00) (必填)")
approvalListPendingCmd.Flags().String("end", "", "结束时间 ISO-8601 (如 2026-03-10T23:59:59+08:00) (必填)")
approvalListPendingCmd.Flags().String("page", "", "分页页码 (可选)")
@@ -533,6 +658,46 @@ func newOaCommand() *cobra.Command {
approvalRevertTaskCmd.Flags().String("target-activity-id", "", "退回到的节点 ID(退回发起人固定传 sid-startevent)(必填)")
approvalRevertTaskCmd.Flags().String("action", "", "退回方式:REVERT_FOR_APPROVAL(退回到审批人)/ REVERT_FOR_RESUBMIT(退回到发起人)(必填)")
approvalRevertTaskCmd.Flags().String("remark", "", "退回说明 (可选)")
approvalFormSchemaCmd.Flags().String("process-code", "", "审批模板 processCode (必填)")
approvalForecastCmd.Flags().String("process-code", "", "审批模板 processCode(简单模式使用;与 --request 互斥)")
approvalForecastCmd.Flags().String("dept-id", "", "发起人部门 ID(简单模式使用;与 --request 互斥)")
approvalForecastCmd.Flags().String("form-values", "", "表单值 JSON(简单模式使用;与 --request 互斥)")
approvalForecastCmd.Flags().String("request", "", "完整请求 JSON(高级模式;与简单模式参数互斥)")
approvalForecastCmd.MarkFlagsOneRequired("request", "process-code")
approvalForecastCmd.MarkFlagsRequiredTogether("process-code", "dept-id", "form-values")
forecastMutuallyExclusive := make([][]string, 0, 3)
for _, name := range []string{"process-code", "dept-id", "form-values"} {
approvalForecastCmd.MarkFlagsMutuallyExclusive("request", name)
forecastMutuallyExclusive = append(forecastMutuallyExclusive, []string{"request", name})
}
cli.AnnotateRuntimeConstraints(approvalForecastCmd, cli.RuntimeSchemaConstraints{
MutuallyExclusive: forecastMutuallyExclusive,
RequireOneOf: [][]string{{"request", "process-code"}},
RequireTogether: [][]string{{"process-code", "dept-id", "form-values"}},
})
approvalCreateCmd.Flags().String("process-code", "", "审批模板 processCode(简单模式使用;与 --request 互斥)")
approvalCreateCmd.Flags().String("dept-id", "-1", "发起人部门 ID")
approvalCreateCmd.Flags().String("form-values", "", "表单值 JSON(简单模式使用;与 --request 互斥)")
approvalCreateCmd.Flags().String("request", "", "完整请求 JSON(高级模式;与简单模式参数互斥)")
approvalCreateCmd.Flags().String("originator-user-id", "", "审批发起人 userId")
approvalCreateCmd.Flags().String("approvers", "", "审批人 userId 列表,多个用逗号分隔")
approvalCreateCmd.Flags().String("approvers-action-type", "OR", "审批类型:AND、OR 或 NONE")
approvalCreateCmd.Flags().String("cc-list", "", "抄送人 userId 列表,多个用逗号分隔")
approvalCreateCmd.Flags().String("cc-position", "START", "抄送时点:START、FINISH 或 START_FINISH")
approvalCreateCmd.MarkFlagsOneRequired("request", "process-code")
approvalCreateCmd.MarkFlagsRequiredTogether("process-code", "form-values")
createSimpleFlags := []string{"process-code", "dept-id", "form-values", "originator-user-id", "approvers", "approvers-action-type", "cc-list", "cc-position"}
createMutuallyExclusive := make([][]string, 0, len(createSimpleFlags))
for _, name := range createSimpleFlags {
approvalCreateCmd.MarkFlagsMutuallyExclusive("request", name)
createMutuallyExclusive = append(createMutuallyExclusive, []string{"request", name})
}
cli.AnnotateRuntimeConstraints(approvalCreateCmd, cli.RuntimeSchemaConstraints{
MutuallyExclusive: createMutuallyExclusive,
RequireOneOf: [][]string{{"request", "process-code"}},
RequireTogether: [][]string{{"process-code", "form-values"}},
})
approvalCmd.AddCommand(
approvalListPendingCmd,
@@ -555,6 +720,9 @@ func newOaCommand() *cobra.Command {
approvalAppendTaskCmd,
approvalRevertActivitiesCmd,
approvalRevertTaskCmd,
approvalFormSchemaCmd,
approvalForecastCmd,
approvalCreateCmd,
)
root.AddCommand(approvalCmd)
+169 -1
View File
@@ -1,6 +1,34 @@
package helpers
import "testing"
import (
"io"
"os"
"strings"
"testing"
)
func executeOACommand(t *testing.T, caller *scriptedToolCaller, args ...string) error {
t.Helper()
previous := deps
previousArgs := os.Args
os.Args = []string{"dws", "oa"}
InitDeps(caller)
deps.Out.w = io.Discard
deps.Out.errW = io.Discard
t.Cleanup(func() {
deps = previous
os.Args = previousArgs
})
cmd := newOaCommand()
cmd.PersistentFlags().Bool("yes", false, "跳过确认")
cmd.SilenceErrors = true
cmd.SilenceUsage = true
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetArgs(args)
return cmd.Execute()
}
func TestCrossPlatformCoverageOARemainingTimeAndRevertBranches(t *testing.T) {
installScriptedCaller(t, &scriptedToolCaller{dry: true})
@@ -31,3 +59,143 @@ func TestCrossPlatformCoverageOARemainingTimeAndRevertBranches(t *testing.T) {
t.Fatalf("revert task: %v", err)
}
}
func TestCrossPlatformCoverageOAApprovalCreateInstanceMapsInternalSimpleOptions(t *testing.T) {
caller := &scriptedToolCaller{}
err := executeOACommand(t, caller,
"approval", "create-instance",
"--process-code", "PROC",
"--form-values", `{"事由":"测试"}`,
"--originator-user-id", "originator",
"--approvers", "approver-1,approver-2",
"--approvers-action-type", "AND",
"--cc-list", "cc-1,cc-2",
"--cc-position", "FINISH",
"--yes",
)
if err != nil {
t.Fatalf("create instance: %v", err)
}
if caller.server != "oa" || caller.tool != "start_process_instance" {
t.Fatalf("called %s/%s, want oa/start_process_instance", caller.server, caller.tool)
}
request, ok := caller.args["ProcessInstanceCreationPopRequest"].(map[string]any)
if !ok {
t.Fatalf("request payload = %#v", caller.args)
}
if got := request["originatorUserId"]; got != "originator" {
t.Fatalf("originatorUserId = %#v", got)
}
approvers, ok := request["approvers"].([]map[string]any)
if !ok || len(approvers) != 1 || approvers[0]["actionType"] != "AND" {
t.Fatalf("approvers = %#v", request["approvers"])
}
if got := approvers[0]["userIds"]; len(got.([]string)) != 2 || got.([]string)[0] != "approver-1" || got.([]string)[1] != "approver-2" {
t.Fatalf("approver userIds = %#v", got)
}
if got := request["ccList"]; len(got.([]string)) != 2 || got.([]string)[0] != "cc-1" || got.([]string)[1] != "cc-2" {
t.Fatalf("ccList = %#v", got)
}
if got := request["ccPosition"]; got != "FINISH" {
t.Fatalf("ccPosition = %#v", got)
}
}
func TestCrossPlatformCoverageOAApprovalCreateInstanceRejectsMixedRequestModes(t *testing.T) {
caller := &scriptedToolCaller{}
err := executeOACommand(t, caller,
"approval", "create-instance",
"--request", `{"processCode":"PROC"}`,
"--process-code", "PROC",
"--yes",
)
if err == nil {
t.Fatal("mixed request modes returned nil")
}
if caller.calls != 0 {
t.Fatalf("unexpected MCP call count: %d", caller.calls)
}
}
func TestCrossPlatformCoverageOAApprovalCreateInstanceRequiresExplicitYes(t *testing.T) {
caller := &scriptedToolCaller{}
err := executeOACommand(t, caller,
"approval", "create-instance",
"--request", `{"processCode":"PROC"}`,
)
if err == nil || !strings.Contains(err.Error(), "--yes") {
t.Fatalf("create instance without --yes error = %v, want explicit --yes requirement", err)
}
if caller.calls != 0 {
t.Fatalf("create instance without --yes made %d MCP calls", caller.calls)
}
}
func TestCrossPlatformCoverageOAApprovalNewCommandValidationAndRequestModes(t *testing.T) {
validCases := []struct {
name string
args []string
tool string
}{
{
name: "form schema",
args: []string{"approval", "form-schema", "--process-code", "PROC"},
tool: "get_process_schema",
},
{
name: "forecast simple mode",
args: []string{"approval", "forecast-process", "--process-code", "PROC", "--dept-id", "-1", "--form-values", `{"金额":"100"}`},
tool: "forecast_process",
},
{
name: "forecast request mode",
args: []string{"approval", "forecast-process", "--request", `{"processCode":"PROC"}`},
tool: "forecast_process",
},
{
name: "create request mode",
args: []string{"approval", "create-instance", "--request", `{"processCode":"PROC"}`, "--yes"},
tool: "start_process_instance",
},
}
for _, tc := range validCases {
t.Run(tc.name, func(t *testing.T) {
caller := &scriptedToolCaller{}
if err := executeOACommand(t, caller, tc.args...); err != nil {
t.Fatalf("execute %v: %v", tc.args, err)
}
if caller.tool != tc.tool || caller.calls != 1 {
t.Fatalf("called tool=%q calls=%d, want %q once", caller.tool, caller.calls, tc.tool)
}
})
}
invalidCases := [][]string{
{"approval", "form-schema"},
{"approval", "forecast-process"},
{"approval", "forecast-process", "--request", `{"processCode":"PROC"}`, "--process-code", "PROC"},
{"approval", "forecast-process", "--request", "{"},
{"approval", "forecast-process", "--request", "null"},
{"approval", "forecast-process", "--request", "{} {}"},
{"approval", "forecast-process", "--process-code", "PROC", "--dept-id", "bad", "--form-values", `{"金额":"100"}`},
{"approval", "forecast-process", "--process-code", "PROC", "--dept-id", "-1", "--form-values", "["},
{"approval", "create-instance", "--process-code", "PROC", "--form-values", `{}`},
{"approval", "create-instance", "--yes"},
{"approval", "create-instance", "--request", "{", "--yes"},
{"approval", "create-instance", "--request", "null", "--yes"},
{"approval", "create-instance", "--request", "{} {}", "--yes"},
{"approval", "create-instance", "--process-code", "PROC", "--form-values", "[", "--yes"},
{"approval", "create-instance", "--process-code", "PROC", "--form-values", `{}`, "--dept-id", "bad", "--yes"},
{"approval", "create-instance", "--process-code", "PROC", "--form-values", `{}`, "--approvers", "u", "--approvers-action-type", "bad", "--yes"},
{"approval", "create-instance", "--process-code", "PROC", "--form-values", `{}`, "--cc-list", "u", "--cc-position", "bad", "--yes"},
}
for _, args := range invalidCases {
caller := &scriptedToolCaller{}
if err := executeOACommand(t, caller, args...); err == nil {
t.Fatalf("invalid args %v returned nil", args)
}
if caller.calls != 0 {
t.Fatalf("invalid args %v made %d MCP calls", args, caller.calls)
}
}
}
@@ -23,10 +23,16 @@ type scriptedToolCaller struct {
format string
dry bool
calls int
server string
tool string
args map[string]any
}
func (c *scriptedToolCaller) CallTool(context.Context, string, string, map[string]any) (*edition.ToolResult, error) {
func (c *scriptedToolCaller) CallTool(_ context.Context, serverID, toolName string, args map[string]any) (*edition.ToolResult, error) {
c.calls++
c.server = serverID
c.tool = toolName
c.args = args
if len(c.steps) == 0 {
return &edition.ToolResult{}, nil
}
+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) |
## 意图判断决策树
+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`。
> - 下载凭证过期会自动刷新并继续下载,已完成的部分不会重下;单个分片失败会自动重试,无需手动处理。
### 创建文件夹
```
+406
View File
@@ -117,6 +117,363 @@ Flags:
--query string 关键字,匹配 processCode 或表单名称 (必填)
```
### 按模板 processCode 查询表单 Schema 信息
> **说明:** 根据已知的 processCode 精确查询表单的完整 Schema,包括表单名称、状态、创建者、创建/修改时间以及表单组件 JSON(content 字段)。
```
Usage:
dws oa approval form-schema [flags]
Example:
dws oa approval form-schema --process-code PROC-594AE140-6AA5-4BA4-AF0C-9E6F66DB1E0B
Flags:
--process-code string 表单模板 processCode (必填)
```
返回值字段:
- `result.processName` — 表单名称
- `result.processCode` — 表单 processCode
- `result.processStatus` — 表单状态(如 `PUBLISHED`)
- `result.creator` — 创建者 userId
- `result.gmtCreate` / `result.gmtModified` — 创建/修改时间(毫秒时间戳)
- `result.processIconUrl` — 表单图标 URL
- `result.processDescription` — 表单描述
- `result.content` — 表单组件 JSON 字符串,包含表单项(items)和标题等配置
### 流程预测
```
Usage:
dws oa approval forecast-process [flags]
Example:
# 简单预测
dws oa approval forecast-process --process-code PROC-xxx --dept-id -1 --form-values '{"单行输入框":"测试内容"}'
# 指定部门预测
dws oa approval forecast-process --process-code PROC-xxx --dept-id 12345 --form-values '{"金额":"5000"}'
# 高级用法:传入完整 JSON
dws oa approval forecast-process --request '{"processCode":"PROC-xxx","deptId":-1,"formComponentValues":[[{"name":"单行输入框","value":"测试"}]]}'
Flags:
--process-code string 审批模板 processCode(简单模式必填)
--form-values string 表单值 JSON,格式 '{"控件名称":"值"}'(简单模式必填)
--dept-id string 发起人所在部门 ID,根部门填 -1(简单模式必填)
--request string 完整请求体 JSON(高级模式,与简单模式互斥)
```
> **注意:** forecast 接口的 `formComponentValues` 比 create-instance 多一层数组包裹(`[[{...}]]`),CLI 简单模式已自动处理,高级模式需自行包裹。`processCode`、`deptId`、`formComponentValues` 三个字段均为必填,`userId` 由系统从登录态自动填充。
#### 流程预测的作用
在 `create-instance` 之前调用 `forecast-process`,可以根据已填写的表单值预测审批流程走向,核心价值有两个:
1. **展示流程路径** — 告诉用户这个审批会经过哪些节点(审批人、抄送人、条件分支),让用户在提交前就知道流程走向。
2. **识别自选审批人节点** — 返回中 `targetSelect: true` 的节点需要用户手动选择审批人/抄送人,Agent 应提示用户选人,并将结果传入 `create-instance` 的 `targetSelectActioners`。
#### 返回值关键字段
| 字段 | 含义 |
|------|------|
| `result.forecastSuccess` | 预测是否成功 |
| `result.staticWorkflow` | 是否为静态流程(无条件分支) |
| `result.workflowForecastNodes` | 流程节点路径,每个节点包含 `activityId` 和 `outIds`(下一跳) |
| `result.workflowActivityRuleVOs` | **重点**:每个节点的详细规则,包含节点类型、审批人、是否自选等 |
#### `workflowActivityRuleVOs` 节点字段解读
| 字段 | 含义 |
|------|------|
| `activityId` | 节点 ID |
| `workflowActor.actorKey` | 自选节点的规则 key,即 `targetSelectActioners` 中 `actionerKey` 的值 |
| `activityName` | 节点名称(如"审批人"、"抄送人") |
| `activityType` | 节点类型:`target_approval`(已指定审批人)、`target_select`(需自选)、`target_notifier`(抄送) |
| `targetSelect` | **`true` 表示需要用户自选审批人/抄送人** |
| `activityActioners` | 已确定的处理人列表(含 `emplId`、`name`) |
| `workflowActor.actorType` | 角色类型:`approver`(审批人)、`notifier`(抄送人) |
| `workflowActor.approvalMethod` | 多人审批方式:`ONE_BY_ONE`(依次审批) |
| `workflowActor.actorSelectionType` | 选人范围:`allStaff`(全员可选)等 |
| `prevActivityId` | 上一节点 ID |
#### Agent 处理流程
```
1. 调用 forecast-process,传入 processCode + form-values
2. 遍历 workflowActivityRuleVOs:
a. 向用户展示每个节点的名称、类型、已指定处理人
b. 若 targetSelect == true:
- 提示用户"节点「{activityName}」需要您自选{actorType}人"
- 使用 dws aisearch person --keyword "<姓名>" --dimension name --format json 帮用户查找并选人
- 记录 activityId 和用户选择的 userIds
3. 将自选结果组装为 targetSelectActioners,传入 create-instance 高级模式 --request
```
#### 自选节点 → `targetSelectActioners` 组装示例
假设 forecast 返回两个自选节点:
```json
{
"targetSelectActioners": [
{
"actionerKey": "manual_33ff_89cb_da91_e3aa",
"actionerStaffIds": ["userId_选人A"]
},
{
"actionerKey": "manual_a29e_9633_f8b7_7291",
"actionerStaffIds": ["userId_选人B"]
}
]
}
```
此字段通过 `create-instance --request` 的高级模式传入。`actionerKey` 来自 forecast 返回的 `workflowActor.actorKey`。
### 发起审批实例
#### 执行摘要
- **如果用户未明确给出 `processCode`,必须固定走 `search-forms` → `form-schema` → 收集表单值 → `forecast-process` → 自选节点选人 → `create-instance`**,不要跳过 `form-schema` 直接拼请求。
- **如果用户明确给出 `processCode`,固定走 `form-schema` → 收集表单值 → `forecast-process` → 自选节点选人 → `create-instance`**,不要跳过 `form-schema` 直接拼请求。
- **`form-schema` 返回的 `content` 不是创建 payload 的原样模板。** 它主要用于识别控件 `label`(即 name)、`id`、控件类型(componentName)和选项值范围;真正的 `formComponentValues` 中 `value` 结构以本文的控件值格式表为准。
- **`forecast-process` 返回的自选节点必须在发起前让用户选人。** 若 `workflowActivityRuleVOs` 中有 `targetSelect: true` 的节点,必须提示用户选择处理人,并将结果通过 `targetSelectActioners` 传入 `create-instance`。
- **所有人员类参数使用 userId。** 若用户给的是姓名,先用 `dws aisearch person --keyword "<姓名>" --dimension name --format json` 解析成 userId。**严禁把姓名直接写进** `approvers`、`ccList`、`directAppointedApprovers`、`targetSelectActioners` 或表单人员控件。
- **创建实例前一次性汇总确认。** `create-instance` 是写操作,执行前一次性展示模板、表单值、流程预测结果和审批人/抄送人供用户确认。
#### 严禁行为
- **严禁跳过 `form-schema`。** 未拿到表单 Schema 前,不得调用 `create-instance`。
- **严禁复用旧的 Schema 结果。** 每次发起实例前都必须重新调用 `form-schema`,模板可能已被修改。
- **严禁在存在不支持必填控件时强行发起。** 若 `form-schema` 返回的必填控件中有不支持类型(如附件等),直接告知用户不支持通过 CLI 发起。
- **严禁把 `form-schema` 返回的 `content` 当成可直接提交的 payload 模板。**
- **严禁把姓名直接写进 `approvers`、`ccList`、`directAppointedApprovers`、`targetSelectActioners` 或表单人员控件。** 必须先通过 `dws aisearch person --keyword "<姓名>" --dimension name --format json` 转成 userId。
- **严禁在未得到用户确认前直接执行真实提单。**
- **严禁猜测控件名称或选项值。** 必须从 `form-schema` 返回中提取。
- **严禁跳过 `forecast-process` 中的自选节点选人。** 若预测返回 `targetSelect: true` 的节点,必须让用户选人后再发起。
#### 最小判断表
| 你手上有什么 | 下一步 |
|---|---|
| 只有口语需求,比如"帮我发起请假审批" | 先 `search-forms --query 请假` |
| 已拿到 `processCode` | 直接 `form-schema --process-code <code>` |
| 已拿到 Schema | 向用户展示控件列表,收集表单值 |
| 已收集表单值 | `forecast-process` 预测流程走向 |
| 预测返回有 `targetSelect: true` 节点 | 让用户为自选节点选人(`dws aisearch person --keyword "<姓名>" --dimension name --format json` 解析姓名) |
| 预测完成,自选节点已选人 | 汇总确认后 `create-instance --yes` |
| 用户明确说"不走模板流程,直接指定审批人" | 使用 `directAppointedApprovers`(高级模式) |
#### 工作流
```
1. search-forms --query <关键词> → 拿到 processCode(若已有则跳过)
2. form-schema --process-code <code> → 拿到控件列表、类型、选项值
3. 检查 Schema 中是否有不支持的必填控件 → 若有则直接告知用户不支持发起
4. 收集表单值 → 向用户展示控件列表,收集用户填写的表单值
5. forecast-process → 根据表单值预测流程走向,识别自选节点
6. 自选节点选人 → 若预测返回 targetSelect=true 的节点,让用户选人(用 dws aisearch person --keyword "<姓名>" --dimension name --format json 解析姓名)
7. 汇总确认后 create-instance --yes → 展示完整信息(表单值 + 流程路径 + 审批人),用户确认后执行发起
```
> **IMPORTANT:每次发起实例前都必须重新调用 `form-schema` 查询模板。** 即使用户之前查询过同一个 processCode,模板可能已被修改(控件增减、选项变更、必填属性调整等),不得复用旧的 Schema 结果。
#### 交互优化原则
> **核心目标:流程清晰,步骤有序,避免重复询问。**
1. **先查 Schema 再收集表单值(步骤 2→4):** `form-schema` 后向用户展示需要填写的控件列表,然后一次性收集全部表单值。不要在未拿到 Schema 前就问用户填什么。
2. **流程预测后再选自选审批人(步骤 5→6):** `forecast-process` 返回流程路径和自选节点后:
- 先向用户展示完整的流程路径(经过哪些节点、各节点处理人)
- 对 `targetSelect: true` 的节点,提示用户"节点「{activityName}」需要您自选{actorType}人"
- 用 `dws aisearch person --keyword "<姓名>" --dimension name --format json` 帮用户查找并选人
- 若有多个自选节点,一次性收集所有自选节点的选人结果
3. **单次汇总确认(步骤 7):** 发起前一次性展示完整信息供用户确认:
- 审批模板名称
- 表单各控件值
- 流程预测结果(审批路径)
- 各节点审批人/抄送人(含自选节点选人结果)
**反例(禁止):**
- 未查 Schema 就直接问用户填什么表单值
- 流程预测后逐个节点分别询问选人,而非一次性收集
- 用户确认前直接执行发起
```
Usage:
dws oa approval create-instance [flags]
Example:
# 简单发起(Agent 在汇总确认后需加 --yes)
dws oa approval create-instance --process-code PROC-xxx --form-values '{"单行输入框":"测试内容"}' --yes
# 指定审批人(OR=或签,AND=会签,NONE=单人)
dws oa approval create-instance --process-code PROC-xxx --form-values '{"单行输入框":"测试"}' --approvers "userId1,userId2" --approvers-action-type OR --yes
# 指定抄送人
dws oa approval create-instance --process-code PROC-xxx --form-values '{"单行输入框":"测试"}' --cc-list "userId1" --cc-position START --yes
# 高级用法:传入完整 JSON(支持 directAppointedApprovers、targetSelectActioners 等全部字段)
dws oa approval create-instance --request '{"processCode":"PROC-xxx","deptId":-1,"formComponentValues":[{"name":"单行输入框","value":"测试"}]}' --yes
Flags:
--process-code string 审批模板 processCode(简单模式必填)
--form-values string 表单值 JSON,格式 '{"控件名称":"值"}'(简单模式必填)
--dept-id string 发起人所在部门 ID,根部门填 -1(可选,默认 -1)
--originator-user-id string 审批发起人 userId(可选,MCP 工具可从登录态获取)
--approvers string 审批人 userId 列表,多个用逗号分隔(可选)
--approvers-action-type string 审批类型:AND(会签)、OR(或签)、NONE(单人)(可选,默认 OR)
--cc-list string 抄送人 userId 列表,多个用逗号分隔(可选)
--cc-position string 抄送时间点:START/FINISH/START_FINISH(可选,默认 START)
--request string 完整请求体 JSON(高级模式,与简单模式互斥)
--yes 显式确认并发起审批;未提供时命令直接拒绝,不进入交互确认(Agent 必须先汇总并获得用户确认)
```
#### 两种模式
- **简单模式:** 通过 `--process-code` + `--form-values` + 可选 flags 发起,适合大多数场景
- **高级模式:** 通过 `--request` 传入完整 JSON 请求体,支持 `directAppointedApprovers`、`targetSelectActioners` 等复杂字段
#### 组装 form-values
`form-values` 是简单模式下的核心入参;传入时必须是一个 JSON 对象字符串,key 为控件 label,value 为该控件的提交值。组装原则:
- 先用 `form-schema` 识别有哪些控件、每个控件的 `label`(name)、`componentName`(type)、选项值范围以及明细子控件结构。
- **`form-schema` 返回的 `content` 不是可直接提交的原样模板。** 它提供控件定义,`value` 结构须按下方控件值格式表组装。
- 提交时必须保证每个控件的 `name`(即 label)与 Schema 中的 `props.label` **完全一致**。
- 如果用户提供的是人员信息,先用 `dws aisearch person --keyword "<姓名>" --dimension name --format json` 转成 userId 后再写入对应控件。
- 单选/多选控件提交的是选项文本(option value),该值从 `form-schema` 返回的选项定义中取得。
- `InnerContactField`、`DepartmentField`、`TableField`、`DDDateRangeField`、`DDAttachment` 等控件的 `value` 结构各不相同,必须按下方格式表单独组装,不要套用文本控件的写法。
- `TextNote`(文字说明)不收集数据,**不要**出现在 `formComponentValues` 中。
#### 表单控件值格式速查
> **重要:** `formComponentValues` 中每条记录的 `name` 必须与审批模板中控件的 `label`(即 `form-schema` 返回的 `content.items[].props.label`)**完全一致**。`value` 为字符串类型,最大 65535 字符。
>
> **详细参考:** 每种控件的完整属性、约束和示例见 [oa-form-components.md](oa/oa-form-components.md)。组装前**必须先阅读该文档**。
| 控件类型 | componentName | value 格式 | 示例 | 备注 |
|---------|---------------|-----------|------|-------------------------------------------------------|
| 单行输入框 | `TextField` | 纯文本 | `"测试内容"` | |
| 多行输入框 | `TextareaField` | 纯文本 | `"第一行\n第二行"` | |
| 数字输入框 | `NumberField` | 数字字符串 | `"100"` | |
| 单选框 | `DDSelectField` | 选项文本 | `"同意"` | 必须与模板 options 中的 value 完全匹配 |
| 多选框 | `DDMultiSelectField` | JSON 数组字符串 | `'["选项A","选项B"]'` | 每个选项须与模板 options 匹配; |
| 日期控件 | `DDDateField` | `yyyy-MM-dd` | `"2026-07-27"` | |
| 时间区间 | `DDDateRangeField` | JSON 数组字符串 | `'["2026-07-27","2026-07-30"]'` | label 为数组 `["开始","结束"]`,用开始时间 label 作 name |
| 金额控件 | `MoneyField` | 数字字符串 | `"1500.50"` | 自动显示大写金额 |
| 电话控件 | `PhoneField` | 手机号字符串 | `"13800138000"` | |
| 联系人控件 | `InnerContactField` | userId | `"user123"` | 多人时传 JSON 数组 `'["user1","user2"]'`;choice="0"单选/"1"多 |
| 部门控件 | `DepartmentField` | 部门 ID | `"12345"` | 多部门传 JSON 数组;multiple=true 时支持多选 |
| 省市区控件 | `AddressField` | JSON 数组字符串 | `'["浙江省","杭州市","西湖区"]'` | 三级联动;needDetail=true 时末尾加详细地址 |
| 图片控件 | `DDPhotoField` | URL 数组转义字符串 | `"[\"http://example.com/img1.jpg\"]"` | 支持 URL 直接提交;**不支持本地文件上传** |
| 附件控件 | `DDAttachment` | JSON 数组转义字符串 | `"[{\"spaceId\":\"xxx\",\"fileName\":\"a.pdf\",\"fileSize\":\"333\",\"fileType\":\"pdf\",\"fileId\":\"xxx\"}]"` | **当前不支持通过 CLI 提交**,需钉盘上传接口获取 fileId 等字段 |
| 评分控件 | `StarRatingField` | 数字字符串 | `"4"` | limit 控制最大星数(默认 5) |
| 关联审批单 | `RelateField` | 审批实例 ID | `"q-xxx"` | 须为当前组织下已存在的实例 |
| 明细控件 | `TableField` | JSON 数组字符串 | `'[{"子控件名":"值1"},{"子控件名":"值2"}]'` | 不可嵌套 TableField;不可含 DDMultiSelectField/DDPhotoField;最大 100 行 |
| 身份证控件 | `IdCardField` | 身份证号 | `"330102199001011234"` | 内置格式校验 |
| 文字说明 | `TextNote` | — | — | **不收集数据**,不会出现在 formComponentValues 中 |
#### API 不支持的控件
以下控件**不支持**通过创建实例 API 提交:
- `TextNote`(文字说明)— 纯展示,不收集数据
- `CalculateField`(计算公式)— 由系统自动计算
- `SeqNumberField`(流水号)— 由系统自动生成
- `OcrTextField` / `OcrIdCardField`(OCR 识别)— 需要客户端交互
- **`DDAttachment`(附件控件)— 当前不支持通过 CLI 提交**,value 需要 spaceId、fileName、fileSize、fileType、fileId 字段,须通过钉盘上传附件接口获取
- **套件类控件(暂不支持)** — `InvoiceField`(发票)、`RecipientAccountField`(收款账户)等业务套件控件当前暂不支持通过 CLI 发起,包含这些控件的审批模板请直接在钉钉客户端操作
> **部分支持的控件:** `DDPhotoField`(图片控件)**支持通过 URL 直接提交**(见上方速查表),仅不支持本地文件上传(CLI 未封装钉盘 CDN 上传流程)。若用户只有本地文件,需告知在钉钉客户端补充。
如果目标审批模板包含上述控件,不要硬拼 `form-values`;应告知用户这些字段无需填写或需要在钉钉客户端补充。
> **必填不支持控件判断规则:** 检查 `form-schema` 返回的控件列表,若存在上述不支持控件且其 `props.required` 为 `true`(必填项),则**直接告知用户该审批模板不支持通过 CLI 发起**,请在钉钉客户端操作。只有不支持控件为非必填时,才可跳过该控件继续发起。
#### 高级模式请求体字段(`--request` JSON 完整结构)
| 字段 | 类型 | 必填 | 说明 |
|-----|------|------|------|
| `processCode` | String | 是 | 审批模板唯一码 |
| `originatorUserId` | String | 是 | 发起人 userId(MCP 工具可从登录态自动获取) |
| `deptId` | Long | 否 | 发起人部门 ID,根部门填 -1;approvers 已传时可不填 |
| `formComponentValues` | Array | 是 | 表单控件值列表,最大 150 条 |
| `approvers` | Array | 否 | 指定审批人列表(覆盖模板流程),最大 20 条 |
| `approvers[].actionType` | String | 否 | `AND`(会签)/ `OR`(或签)/ `NONE`(单人) |
| `approvers[].userIds` | Array | 否 | 审批人 userId 列表 |
| `ccList` | Array | 否 | 抄送人 userId 列表,最大 50 |
| `ccPosition` | String | 否 | `START` / `FINISH` / `START_FINISH` |
| `directAppointedApprovers` | Array | 否 | 指定审批人组(覆盖模板流程),结构见下方 |
| `targetSelectActioners` | Array | 否 | 自选审批人(模板中有自选节点时必填),最大 20 条 |
#### 节点参数组装
> **详细参考:** 流程节点类型、审批模式、条件分支和 10 种审批人选择规则的完整说明见 [oa-process-nodes.md](oa/oa-process-nodes.md)。
**directAppointedApprovers(指定审批人覆盖模板流程):**
当用户明确说"不走模板默认流程"或"直接指定 XX 审批"时使用。
```json
[
{
"staffIds": ["userId1", "userId2"],
"taskActionType": "NONE",
"staffId": ""
}
]
```
- `staffIds`:审批人 userId 列表(必须通过 `dws aisearch person --keyword "<姓名>" --dimension name --format json` 获取,严禁填姓名)
- `taskActionType`:`NONE`(单人审批)/ `AND`(会签)/ `OR`(或签)
**targetSelectActioners(模板有自选审批节点时使用):**
当 `form-schema` 返回的模板流程中存在自选审批节点(`target_select` 类型)时必填。
```json
[
{
"actionerKey": "manual_nodeId_xxxx_yyyy",
"actionerStaffIds": ["userId1"]
}
]
```
- `actionerKey`:自选节点的规则 key,可通过获取审批单流程节点信息接口获取 `actorKey`
- `actionerStaffIds`:操作人 userId 列表
**审批类型(approvers actionType)说明:**
| 值 | 含义 | 说明 |
|----|------|------|
| `AND` | 会签 | 所有审批人都必须审批通过 |
| `OR` | 或签 | 任一审批人审批即可 |
| `NONE` | 单人审批 | 只有一个审批人 |
**抄送时间点(ccPosition)说明:**
| 值 | 含义 |
|----|------|
| `START` | 审批发起时抄送 |
| `FINISH` | 审批完成时抄送 |
| `START_FINISH` | 发起和完成时都抄送 |
#### 表单控件约束
- 单个表单最多 200 个控件
- 控件 label(name)和 placeholder 最大 50 字符
- `DDSelectField` / `DDMultiSelectField` 的选项 value 必须与模板中配置的选项文本完全一致
- `TableField`(明细)内不可嵌套 `TableField`,不可包含 `DDMultiSelectField` 和 `DDPhotoField`
- `TextNote`(文字说明)不收集数据,无需在 `formComponentValues` 中传入
- `InnerContactField` 的 userId 应为当前组织下在职成员
- `DepartmentField` 应传入当前组织下存在的部门 ID
- `RelateField` 传入的审批实例 ID 应为当前组织下已存在的实例
#### 返回结果
创建成功后,返回的 `result` 字段即为新审批实例的 `processInstanceId`。建议向用户展示:
```
审批已创建成功:
- 审批模板: <processName>(来自 form-schema)
- 审批实例 ID: <processInstanceId>(来自 create-instance 返回的 result)
```
后续可用该 processInstanceId 执行 `detail`、`tasks`、`records`、`revoke` 等操作。
### 获取审批任务的被催办人 userId
> **催办必须两步串联:** ① `ding-info` 获取被催办人 `userId` → ② `ding message send` 发送催办消息。禁止跳过第一步直接猜测 userId。
@@ -316,6 +673,18 @@ Flags:
用户说"审批记录/操作历史" → `approval records`
用户说"我发起的审批" → `approval list-initiated`(需 --process-code,可从 list-forms / search-forms / detail 获取)
用户说"有哪些审批表单/可见表单" → `approval list-forms`
用户说"搜索审批表单/查找xx审批表单/有没有xx表单" → `approval search-forms`(需 --query)
用户说"查表单schema/查表单结构/表单模板信息/查表单组件/查表单定义/表单有哪些字段/表单的字段信息" → `approval form-schema`(需 --process-code,可从 list-forms / search-forms / detail 获取)
用户说"预测审批流程/流程预测/审批走向/这个审批走哪些人/审批流程预览" → `approval forecast-process`(需 --process-code、--dept-id、--form-values)
- 在 `form-schema` 之后、`create-instance` 之前调用
- 返回的 `workflowActivityRuleVOs` 中 `targetSelect: true` 的节点需要用户自选审批人
- 自选结果组装为 `targetSelectActioners` 传入 `create-instance`
用户说"发起审批/提交审批/帮我发起XX审批/新建审批单/提一个XX审批/帮我提XX申请" → 五步流程:① `search-forms --query XX` 获取 processCode → ② `form-schema --process-code <code>` 获取表单字段定义 → ③ 阅读 [oa-form-components.md](oa/oa-form-components.md) 和 [oa-process-nodes.md](oa/oa-process-nodes.md) 后组装表单值 → ④ `forecast-process` 预测流程走向并识别自选节点 → ⑤ 若有自选节点让用户选人,确认后 `create-instance --yes` 发起
- 如果用户已知 processCode,可跳过第①步
- `--form-values` 的 key 必须与 `form-schema` 返回的控件 label 一致
- `forecast-process` 返回自选节点时必须让用户选人,不得跳过
- 执行前**必须向用户确认**表单内容、流程预测结果、审批人和抄送人
- 示例:"帮我发起一个AI审批单" → ① `search-forms --query AI` → ② `form-schema --process-code <code>` → ③ 组装表单值 → ④ `forecast-process` → ⑤ 向用户确认流程走向和自选审批人后 `create-instance --yes`
用户说"我有哪些待审的任务" → `approval tasks`
用户说"我发起的审批单" -> `approval list-submitted`
用户说"我审批/处理过的审批单" -> `approval list-executed`
@@ -351,6 +720,12 @@ dws oa approval records --instance-id <processInstanceId> --format json
# 7. 获取可见审批表单(得到 processCode)
dws oa approval list-forms --cursor 0 --limit 100 --format json
# 7b. 按关键字模糊搜索表单(快速定位 processCode)
dws oa approval search-forms --query AI --format json
# 7c. 按 processCode 查询表单 Schema(获取表单结构、组件定义)
dws oa approval form-schema --process-code <code> --format json
# 8. 查看自己发起的审批列表(--process-code 来自 list-forms / search-forms / detail)
dws oa approval list-initiated --process-code <code> \
--start "2026-03-10T00:00:00+08:00" --end "2026-03-10T23:59:59+08:00" \
@@ -373,6 +748,22 @@ dws oa approval oa-comments --instance-id <processInstanceId> --content "同意
# 14. 对审批实例进行抄送(processInstanceId 来自 list-pending 或 detail)
dws oa approval oa-cc-noticer --instance-id <processInstanceId> --users "68674200835816" --format json
dws oa approval oa-cc-noticer --instance-id <processInstanceId> --users "userId1,userId2" --format json
# 18. 发起审批(完整流程:搜表单 → 查 Schema → 收集表单值 → 流程预测 → 自选节点选人 → 发起)
# 18a. 模糊搜索表单获取 processCode
dws oa approval search-forms --query AI --format json
# 18b. 查询表单 Schema 获取字段定义
dws oa approval form-schema --process-code <code> --format json
# 18c. 收集表单值(向用户展示控件列表,用户填写后组装 form-values)
# 18d. 流程预测(根据表单值预测审批走向,识别自选审批人节点;processCode/deptId/formValues 必填,userId 由登录态自动填充)
dws oa approval forecast-process --process-code <code> --dept-id -1 --form-values '{"单行输入框":"测试内容"}' --format json
# 18e. 若 forecast 返回 targetSelect=true 的节点,用 dws aisearch person --keyword "<姓名>" --dimension name --format json 帮用户选人
# 18f. 发起审批实例(form-values 的 key 须与 Schema 中控件 label 一致)
dws oa approval create-instance --process-code <code> --form-values '{"单行输入框":"测试内容"}' --yes --format json
# 18g. 发起并指定审批人和抄送人
dws oa approval create-instance --process-code <code> --form-values '{"单行输入框":"测试"}' --approvers "userId1,userId2" --approvers-action-type OR --cc-list "userId3" --cc-position START --yes --format json
# 18h. 发起并使用 forecast 自选审批人结果(高级模式)
dws oa approval create-instance --request '{"processCode":"PROC-xxx","deptId":-1,"formComponentValues":[{"name":"单行输入框","value":"测试"}],"targetSelectActioners":[{"actionerKey":"manual_33ff_89cb_da91_e3aa","actionerStaffIds":["userId_选人A"]}]}' --yes --format json
```
## 上下文传递表
@@ -384,6 +775,11 @@ dws oa approval oa-cc-noticer --instance-id <processInstanceId> --users "userId1
| `detail` | `processCode` | list-initiated 的 --process-code |
| `list-forms` | `processCode` | list-initiated 的 --process-code |
| `search-forms` | `processCode` | list-initiated 的 --process-code |
| `form-schema` | `processCode`, `processName`, `content` | 查看表单结构定义;`content` 字段包含表单组件 JSON,可解析获取字段列表;**控件 label 作为 create-instance --form-values 的 key** |
| `search-forms` → `form-schema` | `processCode` → 表单字段定义 | forecast-process / create-instance 的 --process-code 和 --form-values 填写依据 |
| `forecast-process` | `workflowActivityRuleVOs`(`activityId`, `targetSelect`, `activityActioners`, `workflowActor`) | ① 向用户展示流程走向和各节点处理人;② `targetSelect: true` 的节点需用户自选审批人,`workflowActor.actorKey` 作为 `targetSelectActioners` 的 `actionerKey` 传入 create-instance |
| `search-forms` → `form-schema` → `forecast-process` | `processCode` → 字段定义 → 流程走向 + 自选节点 | create-instance 的完整上下文:表单值 + 流程路径 + targetSelectActioners |
| `create-instance` | `result`(processInstanceId) | detail / tasks / records / revoke 等的 --instance-id,可跟踪已发起的审批 |
## 注意事项
@@ -395,6 +791,16 @@ dws oa approval oa-cc-noticer --instance-id <processInstanceId> --users "userId1
- `list-initiated` 的 `--process-code` 可从 `list-forms`、`search-forms` 或 `detail` 返回中提取。当 `list-forms` 返回 `processCodeList` 为空(`totalCount -1`)时,用 `search-forms --query <表单名>`(如 `--query 报销`)按名称精准拿 `processCode` 更稳
- `list-initiated` 的 `--start` / `--end` 区间有后端上限(约 120 天)。超过上限会返回误导性的 `business_error: 时间戳无效`(实为区间过长,不是时间格式问题)。跨度大时请拆成多段短区间分别查询
- `form-schema` 的 `--process-code` 可从 `list-forms`、`search-forms` 或 `detail` 返回中提取;返回的 `content` 字段为 JSON 字符串,需解析后查看表单组件(items)定义。
- `create-instance` 发起前**必须先阅读** [oa-form-components.md](oa/oa-form-components.md)(控件值格式)和 [oa-process-nodes.md](oa/oa-process-nodes.md)(流程节点规则),再调用 `form-schema` 获取表单字段定义,确保 `--form-values` 中的 key 与控件 label 完全一致。
- `create-instance` 发起前**应先调用 `forecast-process`** 预测流程走向,识别自选审批人节点(`targetSelect: true`),让用户选人后再提交。
- `create-instance` 的 `--form-values` 接受 JSON 格式 `'{"控件名称":"值"}'`,代码会自动转为 `[{"name":"控件名称","value":"值"}]`。
- `create-instance` 简单模式适合常见场景;如需 `directAppointedApprovers`(指定审批人覆盖模板流程)或 `targetSelectActioners`(自选审批节点)等高级字段,使用 `--request` 传完整 JSON。`--request` 与简单模式 flags 互斥。
- `create-instance` 会创建真实审批数据;Agent 只有在用户确认模板、表单值、流程路径和人员后才能传入 `--yes`。
- `create-instance` 返回的 processInstanceId 可用于 `detail`、`tasks`、`records`、`revoke` 等后续操作。
- `forecast-process` 的 `processCode`、`deptId`、`formComponentValues` 三个字段均为必填(`userId` 由系统自动填充);`formComponentValues` 比 `create-instance` 多一层数组包裹(`[[{...}]]`),CLI 简单模式已自动处理。
- `forecast-process` 返回 `workflowActivityRuleVOs` 中 `targetSelect: true` 的节点,其 `workflowActor.actorKey` 必须作为 `targetSelectActioners` 的 `actionerKey` 传入 `create-instance`。
## 自动化脚本
| 脚本 | 场景 | 用法 |
@@ -0,0 +1,346 @@
# OA 审批表单控件参考
本文档详细描述钉钉 OA 审批中每种表单控件(componentName)在**发起审批实例**时 `formComponentValues` 的 `value` 格式、约束和注意事项。
> **核心原则:** `formComponentValues[].name` 必须与审批模板中控件的 `props.label` **完全一致**,`value` 为字符串类型(最大 65535 字符)。
---
## 通用约束
| 约束 | 说明 |
|------|------|
| 单表单最大控件数 | 200 |
| label / placeholder 最大长度 | 50 字符 |
| value 最大长度 | 65535 字符 |
| ID / bizAlias 唯一性 | 同一表单内不可重复 |
| TextNote | 不收集数据,不出现在 formComponentValues 中 |
---
## 基础控件
### TextField(单行输入框)
| 属性 | 说明 |
|------|------|
| `componentName` | `TextField` |
| value 格式 | 纯文本字符串 |
| 示例 | `"测试内容"` |
| 约束 | 无特殊约束 |
```json
{ "name": "单行输入框", "value": "测试内容" }
```
### TextareaField(多行输入框)
| 属性 | 说明 |
|------|------|
| `componentName` | `TextareaField` |
| value 格式 | 纯文本字符串,支持换行 |
| 示例 | `"第一行\n第二行"` |
| 约束 | 无 `ratio` 属性 |
```json
{ "name": "多行输入框", "value": "第一行\n第二行\n第三行" }
```
### NumberField(数字输入框)
| 属性 | 说明 |
|------|------|
| `componentName` | `NumberField` |
| value 格式 | 数字字符串 |
| 示例 | `"100"` |
| 约束 | 适合数量、天数等纯数字场景 |
```json
{ "name": "加班天数", "value": "3" }
```
### DDSelectField(单选框)
| 属性 | 说明 |
|------|------|
| `componentName` | `DDSelectField` |
| value 格式 | 选项文本字符串 |
| 示例 | `"同意"` |
| 约束 | **必须与模板 `options[].value` 完全匹配**,不可自行编造选项 |
模板中的选项结构(从 `form-schema` 获取):
```json
"options": [
{ "key": "option_0", "value": "同意" },
{ "key": "option_1", "value": "不同意" }
]
```
提交时传选项的 `value` 文本:
```json
{ "name": "审批意见", "value": "同意" }
```
### DDMultiSelectField(多选框)
| 属性 | 说明 |
|------|------|
| `componentName` | `DDMultiSelectField` |
| value 格式 | JSON 数组字符串,每个元素为选项文本 |
| 示例 | `'["选项A","选项B"]'` |
| 约束 | 每个选项须与模板 `options[].value` 匹配;|
```json
{ "name": "兴趣爱好", "value": "[\"阅读\",\"运动\"]" }
```
### DDDateField(日期控件)
| 属性 | 说明 |
|------|------|
| `componentName` | `DDDateField` |
| value 格式 | `yyyy-MM-dd` 格式字符串 |
| 示例 | `"2026-07-27"` |
| 约束 | 格式固定,不可传其他日期格式 |
```json
{ "name": "请假日期", "value": "2026-07-27" }
```
### DDDateRangeField(时间区间控件)
| 属性 | 说明 |
|------|------|
| `componentName` | `DDDateRangeField` |
| value 格式 | JSON 数组字符串 `[开始日期, 结束日期]` |
| 示例 | `'["2026-07-27","2026-07-30"]'` |
| 约束 | `props.label` 为数组 `["开始时间","结束时间"]`;提交时 `name` 使用**开始时间的 label** |
模板中的 label 结构(从 `form-schema` 获取):
```json
"props": { "label": ["开始时间", "结束时间"] }
```
提交时用**开始时间 label** 作为 name:
```json
{ "name": "开始时间", "value": "[\"2026-07-27\",\"2026-07-30\"]" }
```
### PhoneField(电话控件)
| 属性 | 说明 |
|------|------|
| `componentName` | `PhoneField` |
| value 格式 | 手机号字符串 |
| 示例 | `"13800138000"` |
| 约束 | `mode: "phone"` 为手机号 |
```json
{ "name": "联系电话", "value": "13800138000" }
```
### IdCardField(身份证控件)
| 属性 | 说明 |
|------|------|
| `componentName` | `IdCardField` |
| value 格式 | 身份证号字符串 |
| 示例 | `"330102199001011234"` |
| 约束 | 内置格式校验,须传合法身份证号 |
```json
{ "name": "身份证号", "value": "330102199001011234" }
```
### TextNote(文字说明)
| 属性 | 说明 |
|------|------|
| `componentName` | `TextNote` |
| value 格式 | — |
| 约束 | **不收集数据**,不出现在 formComponentValues 中 |
> 遇到 TextNote 控件时直接跳过,不要尝试为它填写值。
---
## 增强控件
### MoneyField(金额控件)
| 属性 | 说明 |
|------|------|
| `componentName` | `MoneyField` |
| value 格式 | 数字字符串 |
| 示例 | `"1500.50"` |
| 约束 | 系统自动显示大写金额(`notUpper: "0"` 时显示) |
```json
{ "name": "报销金额", "value": "1500.50" }
```
### InnerContactField(联系人控件)
| 属性 | 说明 |
|------|----------------------------------------------------|
| `componentName` | `InnerContactField` |
| value 格式 | userId 字符串,多人时为 JSON 数组字符串 |
| 示例(单选) | `"user123"` |
| 示例(多选) | `'["userId1","userId2"]'` |
| 约束 | `choice: "0"` 单选 / `"1"` 多选;userId 须为**当前组织下在职成员** |
```json
{ "name": "项目负责人", "value": "[\"userId1\",\"userId2\"]" }
```
> **严禁直接写姓名。** 必须先通过 `dws aisearch person --keyword "<姓名>" --dimension name --format json` 查询获取 userId;多结果时须让用户消歧确认。
### DepartmentField(部门控件)
| 属性 | 说明 |
|------|------|
| `componentName` | `DepartmentField` |
| value 格式 | 部门 ID 字符串,多部门时为 JSON 数组字符串 |
| 示例(单选) | `"12345"` |
| 示例(多选) | `'["12345","67890"]'` |
| 约束 | `multiple: boolean` 控制单选/多选;部门 ID 须为**当前组织下存在的部门** |
```json
{ "name": "所属部门", "value": "12345" }
```
### AddressField(省市区控件)
| 属性 | 说明 |
|------|------|
| `componentName` | `AddressField` |
| value 格式 | JSON 数组字符串 `["省","市","区"]` |
| 示例 | `'["浙江省","杭州市","西湖区"]'` |
| 约束 | 三级联动选择器;`needDetail: true` 时末尾追加详细地址文本 |
```json
{ "name": "办公地点", "value": "[\"浙江省\",\"杭州市\",\"西湖区\"]" }
```
### DDPhotoField(图片控件)
> **支持通过图片 URL 提交,不支持本地文件上传。** 如果用户已有图片 URL(如公网可访问的图片链接),可直接填入 value 提交。CLI 尚未封装本地文件上传到钉盘 CDN 的流程,若用户只有本地文件而非 URL,需告知用户在钉钉客户端补充。
| 属性 | 说明 |
|------|------|
| `componentName` | `DDPhotoField` |
| value 格式 | URL 数组转义字符串,即使只有一个 URL 也需数组形式 |
| 示例 | `"[\"http://example.com/img1.jpg\",\"http://example.com/img2.jpg\"]"` |
| 约束 | 支持 URL 直接提交;**不支持本地文件上传**(CLI 未封装钉盘上传流程); |
```json
{ "name": "图片", "value": "[\"http://example.com/photo.jpg\"]" }
```
### DDAttachment(附件控件)
> **[注意] 当前暂不支持通过 CLI 提交附件控件。** 附件控件的 value 需要包含 spaceId、fileName、fileSize、fileType 和 fileId 字段,这些字段需要通过调用钉盘的上传附件接口获取,CLI 尚未封装此流程。包含附件控件的审批模板请在钉钉客户端操作。
| 属性 | 说明 |
|------|------|
| `componentName` | `DDAttachment` |
| value 格式 | JSON 数组转义字符串,每个元素包含 spaceId、fileName、fileSize、fileType、fileId |
| 示例(参考) | `"[{\"spaceId\":\"163xxx\",\"fileName\":\"2644.JPG\",\"fileSize\":\"333\",\"fileType\":\"jpg\",\"fileId\":\"643xxx\"}]"` |
| 约束 | **当前不支持通过 CLI 提交**;各字段需通过钉盘上传附件接口获取 |
### StarRatingField(评分控件)
| 属性 | 说明 |
|------|------|
| `componentName` | `StarRatingField` |
| value 格式 | 数字字符串 |
| 示例 | `"4"` |
| 约束 | `limit` 控制最大星数(默认 5) |
```json
{ "name": "满意度评分", "value": "4" }
```
### RelateField(关联审批单)
| 属性 | 说明 |
|------|------|
| `componentName` | `RelateField` |
| value 格式 | 审批实例 ID 字符串 |
| 示例 | `"q-ZZ1sQaTIuYFpKI9aNC1g"` |
| 约束 | 须为**当前组织下已存在的审批实例 ID** |
```json
{ "name": "关联审批单", "value": "q-ZZ1sQaTIuYFpKI9aNC1g" }
```
### SignatureField(签名控件)
| 属性 | 说明 |
|------|------|
| `componentName` | `SignatureField` |
| value 格式 | 签名图片 mediaId |
| 约束 | 需要客户端交互签名,通常不支持 API 直接提交 |
---
## 复合控件
### TableField(明细控件)
| 属性 | 说明 |
|------|------|
| `componentName` | `TableField` |
| value 格式 | JSON 数组字符串,每个元素为一行数据的键值对 |
| 示例 | `'[{"商品名":"笔记本","数量":"2"},{"商品名":"钢笔","数量":"1"}]'` |
| 约束 | **不可嵌套 TableField**;**不可包含 DDMultiSelectField 和 DDPhotoField**;最大 100 行;总长度不超过 65535 字符 |
模板结构(从 `form-schema` 获取):
```json
{
"componentName": "TableField",
"props": { "label": "采购明细" },
"children": [
{ "componentName": "TextField", "props": { "label": "商品名", "id": "TextField_XXX" } },
{ "componentName": "NumberField", "props": { "label": "数量", "id": "NumberField_YYY" } }
]
}
```
提交时每行用子控件 label 作 key:
```json
{
"name": "采购明细",
"value": "[{\"商品名\":\"笔记本\",\"数量\":\"2\"},{\"商品名\":\"钢笔\",\"数量\":\"1\"}]"
}
```
---
## API 不支持的控件
以下控件**不支持**通过创建实例 API 提交,遇到时应告知用户需在钉钉客户端补充:
| 控件 | componentName | 原因 |
|------|---------------|------|
| 文字说明 | `TextNote` | 纯展示,不收集数据 |
| 计算公式 | `CalculateField` | 由系统自动计算,不可手动填写 |
| 流水号 | `SeqNumberField` | 由系统自动生成 |
| OCR 文本识别 | `OcrTextField` | 需要客户端 OCR 交互 |
| OCR 身份证识别 | `OcrIdCardField` | 需要客户端 OCR 交互 |
| 附件控件 | `DDAttachment` | value 需要 spaceId、fileName、fileSize、fileType、fileId,须通过钉盘上传接口获取,CLI 尚未封装 |
> **部分支持的控件:** `DDPhotoField`(图片控件)**支持通过 URL 直接提交**,但不支持本地文件上传(CLI 未封装钉盘 CDN 上传流程)。若用户只有本地文件,需告知在钉钉客户端补充。详见本文 [DDPhotoField](#ddphotofield图片控件) 章节。
> **套件类控件(暂不支持)** — `InvoiceField`(发票)、`RecipientAccountField`(收款账户)等业务套件控件当前暂不支持通过 CLI 发起,包含这些控件的审批模板请直接在钉钉客户端操作。
---
## 组装优先级
1. **每次发起前都重新调用 `form-schema`**,不得复用旧结果(模板可能已被修改)
2. 先读 `form-schema` 返回的 `content`,识别所有控件的 `label`、`componentName`、`options`、`props.required`
3. **检查是否存在不支持控件且为必填项(`props.required: true`)**,若有则直接告知用户该模板不支持通过 CLI 发起,请在钉钉客户端操作
4. 按本文档中每种控件的 value 格式组装 `formComponentValues`
5. **不要把 `form-schema` 的 `content` 当成可直接提交的模板**
6. 遇到 API 不支持的控件(非必填),跳过并告知用户
@@ -0,0 +1,374 @@
# OA 审批流程节点与审批人规则参考
本文档描述钉钉 OA 审批的流程节点类型、审批模式、条件分支和审批人选择规则,用于理解审批模板结构和正确填写 `create-instance` 的节点参数。
---
## 流程结构概览
审批流程是一个嵌套树结构:
- **根节点**:发起人节点(`type: "start"`,`nodeId: "sid-startevent"`),固定不可删除
- **后续节点**:通过 `childNode` 链接形成链式结构
- **分支节点**:条件分支(`route` + `condition`)或并行分支(`parallel`)
- 当没有后续节点时,`childNode` 字段**必须省略**(不可设为 `null`)
---
## 7 种节点类型
### 1. 发起人节点(start)
| 属性 | 值 |
|------|-----|
| `type` | `start` |
| `nodeId` | `sid-startevent`(固定) |
| `properties` | `{}`(空对象) |
唯一、不可删除。是流程的起点。
### 2. 审批人节点(approver)
核心决策节点,有审批/拒绝权限。
| 属性 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `actionerRules` | Array | 是 | 审批人选择规则,至少一条 |
| `activateType` | String | 是 | 多人审批模式(见下方) |
| `approvalType` | String | 是 | 固定 `"MANUAL"` |
| `agreeAll` | Boolean | 是 | `true` 全部通过 / `false` 任一通过 |
| `noneActionerAction` | String | 否 | 如 `"admin"`(找不到审批人时转管理员) |
支持全部 10 种 actionerRules 类型。
### 3. 办理人节点(handler)
执行工作,无审批决策权。
| 属性 | 类型 | 必填 |
|------|------|------|
| `actionerRules` | Array | 是 |
| `activateType` | String | 是 |
支持 9 种 actionerRules(不支持 `target_matrix_approval`)。
### 4. 抄送人节点(notifier)
仅接收通知,无决策权。
| 属性 | 类型 | 必填 |
|------|------|------|
| `actionerRules` | Array | 是 |
支持多条 actionerRules 组合在一个节点中,实现同时抄送多类人员。
### 5. 条件分支(route + condition)
条件路由节点,包含多个条件分支。
**route 节点:**
- `type: "route"`
- `conditionNodes[]`:分支数组,按优先级排序,**默认分支必须在最后**
- `properties: {}`
**condition 节点(conditionNodes 的每个元素):**
- `type: "condition"`
- `isdefault: true`:标记默认分支
- `properties.conditions`:二维条件数组
- 外层数组:多个条件组,**OR 关系**
- 内层数组:多个条件对象,**AND 关系**
- 默认分支:`[[]]`(一个空组)
### 6. 并行分支(parallel)
多个分支同时执行,全部完成后才继续。
| 属性 | 说明 |
|------|------|
| `branches[]` | 分支数组 |
| `branches[].name` | 分支名称 |
| `branches[].childNode` | 该分支的第一个节点 |
### 7. 付款人节点(payer)
财务付款节点。
| 属性 | 说明 |
|------|------|
| `actionerRules` | 审批人规则 |
| `paymentConfig.amountField` | 金额控件 ID |
| `paymentConfig.accountField` | 收款账户控件 ID |
---
## 多人审批模式
| 模式 | `activateType` | `agreeAll` | 说明 |
|------|---------------|-----------|------|
| 会签 | `"ALL"` | `true` | 所有审批人都必须审批通过 |
| 或签 | `"ALL"` | `false` | 任一审批人审批即可 |
| 依次审批 | `"ONE_BY_ONE"` | `true` | 按顺序逐级审批 |
---
## 10 种审批人选择规则(actionerRules)
### 1. 指定成员(target_approval)
明确指定具体人员。
```json
{
"type": "target_approval",
"approvals": [
{ "userName": "张三", "workNo": "manager123" }
],
"isEmpty": false
}
```
- `workNo` 必须通过 `dws aisearch person --keyword "<工号>" --dimension jobNumber --format json` 获取,**严禁编造**
- 在 `create-instance` 中对应 `directAppointedApprovers` 的 `staffIds`
### 2. 直属主管(target_formula / reportLineManager)
按汇报线找到直属主管。
```json
{
"type": "target_formula",
"subType": "reportLineManager",
"formula": "ReportLineManager(corpId,originator,1)",
"isEmpty": false
}
```
- `formula` 中最后的数字 N 表示第 N 级主管
- **重要区分:** 用户说"直属主管/直属领导/汇报线主管"才用此规则;用户说"主管审批/leader审批"(模糊)时默认用 `target_management`(部门主管)
### 3. 发起人自己(target_originator)
发起人自行审批。
```json
{
"type": "target_originator",
"isEmpty": false
}
```
最简单的规则,只有 `type` 和 `isEmpty`。
### 4. 部门主管(target_management)
从发起人所在部门层级找主管。
```json
{
"type": "target_management",
"level": 1,
"autoUp": true,
"isEmpty": false
}
```
- `level: 1`:直接部门主管
- `autoUp: true`:找不到时向上级部门搜索
- **这是"主管审批/leader审批"模糊场景的默认选择**
### 5. 表单部门主管(target_formula / managerOfDept)
根据表单中部门控件选择的主管。
```json
{
"type": "target_formula",
"subType": "managerOfDept",
"formula": "ManagerOfDept(corpId,$('DepartmentField_XXX'),1)",
"isEmpty": false
}
```
- `formula` 中引用表单中的 `DepartmentField` 控件 ID
### 6. 发起人自选(target_select)
发起人在提单时自行选择审批人。
```json
{
"type": "target_select",
"select": ["allStaff"],
"range": {},
"key": "manual_nodeId_xxxx_yyyy",
"multi": 1,
"isEmpty": false
}
```
- `select: ["allStaff"]`:可选全组织人员
- `multi: 1`:单选
- `key`:格式 `manual_{nodeId}_{hex}_{hex}`
- 在 `create-instance` 中对应 `targetSelectActioners` 的 `actionerKey`
### 7. 角色标签主管(target_managers_labels)
按角色标签找多级主管。
```json
{
"type": "target_managers_labels",
"labelNames": ["项目经理"],
"labels": ["labelId123"],
"levels": [1],
"isEmpty": false
}
```
- `labels` 中的 ID 必须通过 `dws contact label get --names "<角色名>" --format json` 获取;已知角色名时直接查询,否则先 `dws contact label list --format json` 获取全部角色列表后匹配
### 8. 表单联系人(target_formcomponent_approval)
从表单中的联系人控件读取审批人。
```json
{
"type": "target_formcomponent_approval",
"paramKey": "InnerContactField_XXX",
"label": "项目负责人",
"isEmpty": false
}
```
- `paramKey` 指向表单中的 `InnerContactField` 控件 ID
- 该控件中填写的人即为审批人
### 9. 角色标签(target_label)
按角色标签找人(如"财务"、"HR")。
```json
{
"type": "target_label",
"labelNames": "财务",
"labels": "459272424",
"isEmpty": false
}
```
- `labels`:角色标签 ID(字符串),必须通过 `dws contact label get --names "<角色名>" --format json` 获取;未知角色名时先 `dws contact label list --format json`
- `labelNames`:角色显示名称
- **严禁编造 label ID**
### 10. 审批矩阵(target_matrix_approval)
按审批矩阵规则确定审批人。
```json
{
"type": "target_matrix_approval",
"matrixId": "xxx",
"roleColumnId": "yyy",
"expression": {
"subFilters": [...],
"operator": "AND"
}
}
```
- 仅适用于审批人节点
- 目前尚在完善中
---
## 条件分支详解
### 条件类型
| `type` | 依据 | 关键字段 |
|--------|------|---------|
| `dingtalk_actioner_dept_condition` | 发起人部门/人员/角色 | `paramKey: "dingtalk_origin_dept"`, `conds[]` |
| `dingtalk_actioner_dept_component_condition` | 表单部门控件 | `paramKey: 控件ID`, `conds[]` |
| `dingtalk_actioner_range_condition` | 数值/金额/时长范围 | `lowerBound`(>=) / `lowerBoundNotEqual`(>) / `upperBoundEqual`(<=) / `upperBound`(<) / `boundEqual`(=) |
| `dingtalk_actioner_value_condition` | 单选匹配 | `paramKey: 控件ID`, `paramValues[]`(选项 key) |
| `dingtalk_multi_value_condition` | 多选匹配 | `paramKey: 控件ID`, `paramValues[]`, `matchType`(1=精确/2=全选/3=任一) |
| `dingtalk_actioner_cascade_component_condition` | 级联控件 | `paramValues[]`, `displayValues[]` |
| `dingtalk_actioner_boolean_condition` | 布尔值 | `boundEqual: true/false` |
| `dingtalk_rule_template` | 节假日判断 | `template`, `outVars` |
| `dingtalk_formula` | 公式 | `formula`, `formulaDisplay` |
| `dingtalk_biz_var_condition` | 业务变量 | `dsKey`, `conds[]` |
| `dingtalk_table_condition` | 明细内字段 | `parentFieldId`, `componentName`, `paramValue` |
### 范围条件操作符
| 字段 | 含义 |
|------|------|
| `lowerBound` | >= (大于等于) |
| `lowerBoundNotEqual` | > (大于) |
| `upperBoundEqual` | <= (小于等于) |
| `upperBound` | < (小于) |
| `boundEqual` | = (等于) |
### 默认分支
- `isdefault: true`
- `conditions: [[]]`(一个空的条件组)
- **必须放在 `conditionNodes[]` 的最后**
---
## create-instance 中的节点参数映射
### directAppointedApprovers(指定审批人覆盖模板流程)
当需要**不使用模板默认流程、直接指定审批人**时使用。
```json
{
"directAppointedApprovers": [
{
"staffIds": ["userId1", "userId2"],
"taskActionType": "NONE",
"staffId": ""
}
]
}
```
| 字段 | 说明 |
|------|------|
| `staffIds` | 审批人 userId 列表(通过 `dws aisearch person --keyword "<姓名>" --dimension name --format json` 获取;多结果须消歧) |
| `taskActionType` | `NONE`(单人)/ `AND`(会签)/ `OR`(或签) |
| `staffId` | 留空字符串 |
### targetSelectActioners(自选审批人)
当模板流程中存在**自选审批节点**(`target_select` 类型)时必填。
```json
{
"targetSelectActioners": [
{
"actionerKey": "manual_nodeId_xxxx_yyyy",
"actionerStaffIds": ["userId1"]
}
]
}
```
| 字段 | 说明 |
|------|------|
| `actionerKey` | 自选节点的规则 key,从审批流程节点信息接口获取 `actorKey` |
| `actionerStaffIds` | 操作人 userId 列表 |
---
## 组装优先级
1. 先用 `forecast-process` 获取模板的流程节点结构(`workflowActivityRuleVOs`)
2. 根据节点中的 `activityType` 和 `targetSelect` 判断是否需要传入 `directAppointedApprovers` 或 `targetSelectActioners`
3. 如果预测返回 `targetSelect: true` 的自选节点,`targetSelectActioners` 必填
4. 如果用户要求覆盖默认流程,使用 `directAppointedApprovers`
5. **所有 userId 必须通过 `dws aisearch person --keyword "<姓名>" --dimension name --format json` 获取,严禁填姓名;多结果须消歧**
> **交互优化:** 若用户在 `forecast-process` 前已指定审批人/抄送人姓名,`forecast-process` 返回自选节点后应自动映射,仅对未覆盖的自选节点追问,不要重复询问。详见 [oa.md](../oa.md) 交互优化原则。
+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 "<姓名>"`)|
## 相关产品
@@ -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` 分片下载参数,行为与最新版下载一致。
#### 回滚文件到指定历史版本
+402
View File
@@ -120,6 +120,363 @@ Flags:
--query string 关键字,匹配 processCode 或表单名称 (必填)
```
### 按模板 processCode 查询表单 Schema 信息
> **说明:** 根据已知的 processCode 精确查询表单的完整 Schema,包括表单名称、状态、创建者、创建/修改时间以及表单组件 JSON(content 字段)。
```
Usage:
dws oa approval form-schema [flags]
Example:
dws oa approval form-schema --process-code PROC-594AE140-6AA5-4BA4-AF0C-9E6F66DB1E0B
Flags:
--process-code string 表单模板 processCode (必填)
```
返回值字段:
- `result.processName` — 表单名称
- `result.processCode` — 表单 processCode
- `result.processStatus` — 表单状态(如 `PUBLISHED`)
- `result.creator` — 创建者 userId
- `result.gmtCreate` / `result.gmtModified` — 创建/修改时间(毫秒时间戳)
- `result.processIconUrl` — 表单图标 URL
- `result.processDescription` — 表单描述
- `result.content` — 表单组件 JSON 字符串,包含表单项(items)和标题等配置
### 流程预测
```
Usage:
dws oa approval forecast-process [flags]
Example:
# 简单预测
dws oa approval forecast-process --process-code PROC-xxx --dept-id -1 --form-values '{"单行输入框":"测试内容"}'
# 指定部门预测
dws oa approval forecast-process --process-code PROC-xxx --dept-id 12345 --form-values '{"金额":"5000"}'
# 高级用法:传入完整 JSON
dws oa approval forecast-process --request '{"processCode":"PROC-xxx","deptId":-1,"formComponentValues":[[{"name":"单行输入框","value":"测试"}]]}'
Flags:
--process-code string 审批模板 processCode(简单模式必填)
--form-values string 表单值 JSON,格式 '{"控件名称":"值"}'(简单模式必填)
--dept-id string 发起人所在部门 ID,根部门填 -1(简单模式必填)
--request string 完整请求体 JSON(高级模式,与简单模式互斥)
```
> **注意:** forecast 接口的 `formComponentValues` 比 create-instance 多一层数组包裹(`[[{...}]]`),CLI 简单模式已自动处理,高级模式需自行包裹。`processCode`、`deptId`、`formComponentValues` 三个字段均为必填,`userId` 由系统从登录态自动填充。
#### 流程预测的作用
在 `create-instance` 之前调用 `forecast-process`,可以根据已填写的表单值预测审批流程走向,核心价值有两个:
1. **展示流程路径** — 告诉用户这个审批会经过哪些节点(审批人、抄送人、条件分支),让用户在提交前就知道流程走向。
2. **识别自选审批人节点** — 返回中 `targetSelect: true` 的节点需要用户手动选择审批人/抄送人,Agent 应提示用户选人,并将结果传入 `create-instance` 的 `targetSelectActioners`。
#### 返回值关键字段
| 字段 | 含义 |
|------|------|
| `result.forecastSuccess` | 预测是否成功 |
| `result.staticWorkflow` | 是否为静态流程(无条件分支) |
| `result.workflowForecastNodes` | 流程节点路径,每个节点包含 `activityId` 和 `outIds`(下一跳) |
| `result.workflowActivityRuleVOs` | **重点**:每个节点的详细规则,包含节点类型、审批人、是否自选等 |
#### `workflowActivityRuleVOs` 节点字段解读
| 字段 | 含义 |
|------|------|
| `activityId` | 节点 ID |
| `workflowActor.actorKey` | 自选节点的规则 key,即 `targetSelectActioners` 中 `actionerKey` 的值 |
| `activityName` | 节点名称(如"审批人"、"抄送人") |
| `activityType` | 节点类型:`target_approval`(已指定审批人)、`target_select`(需自选)、`target_notifier`(抄送) |
| `targetSelect` | **`true` 表示需要用户自选审批人/抄送人** |
| `activityActioners` | 已确定的处理人列表(含 `emplId`、`name`) |
| `workflowActor.actorType` | 角色类型:`approver`(审批人)、`notifier`(抄送人) |
| `workflowActor.approvalMethod` | 多人审批方式:`ONE_BY_ONE`(依次审批) |
| `workflowActor.actorSelectionType` | 选人范围:`allStaff`(全员可选)等 |
| `prevActivityId` | 上一节点 ID |
#### Agent 处理流程
```
1. 调用 forecast-process,传入 processCode + form-values
2. 遍历 workflowActivityRuleVOs:
a. 向用户展示每个节点的名称、类型、已指定处理人
b. 若 targetSelect == true:
- 提示用户"节点「{activityName}」需要您自选{actorType}人"
- 使用 dws aisearch person --keyword "<姓名>" --dimension name --format json 帮用户查找并选人
- 记录 activityId 和用户选择的 userIds
3. 将自选结果组装为 targetSelectActioners,传入 create-instance 高级模式 --request
```
#### 自选节点 → `targetSelectActioners` 组装示例
假设 forecast 返回两个自选节点:
```json
{
"targetSelectActioners": [
{
"actionerKey": "manual_33ff_89cb_da91_e3aa",
"actionerStaffIds": ["userId_选人A"]
},
{
"actionerKey": "manual_a29e_9633_f8b7_7291",
"actionerStaffIds": ["userId_选人B"]
}
]
}
```
此字段通过 `create-instance --request` 的高级模式传入。`actionerKey` 来自 forecast 返回的 `workflowActor.actorKey`。
### 发起审批实例
#### 执行摘要
- **如果用户未明确给出 `processCode`,必须固定走 `search-forms` → `form-schema` → 收集表单值 → `forecast-process` → 自选节点选人 → `create-instance`**,不要跳过 `form-schema` 直接拼请求。
- **如果用户明确给出 `processCode`,固定走 `form-schema` → 收集表单值 → `forecast-process` → 自选节点选人 → `create-instance`**,不要跳过 `form-schema` 直接拼请求。
- **`form-schema` 返回的 `content` 不是创建 payload 的原样模板。** 它主要用于识别控件 `label`(即 name)、`id`、控件类型(componentName)和选项值范围;真正的 `formComponentValues` 中 `value` 结构以本文的控件值格式表为准。
- **`forecast-process` 返回的自选节点必须在发起前让用户选人。** 若 `workflowActivityRuleVOs` 中有 `targetSelect: true` 的节点,必须提示用户选择处理人,并将结果通过 `targetSelectActioners` 传入 `create-instance`。
- **所有人员类参数使用 userId。** 若用户给的是姓名,先用 `dws aisearch person --keyword "<姓名>" --dimension name --format json` 解析成 userId。**严禁把姓名直接写进** `approvers`、`ccList`、`directAppointedApprovers`、`targetSelectActioners` 或表单人员控件。
- **创建实例前一次性汇总确认。** `create-instance` 是写操作,执行前一次性展示模板、表单值、流程预测结果和审批人/抄送人供用户确认。
#### 严禁行为
- **严禁跳过 `form-schema`。** 未拿到表单 Schema 前,不得调用 `create-instance`。
- **严禁复用旧的 Schema 结果。** 每次发起实例前都必须重新调用 `form-schema`,模板可能已被修改。
- **严禁在存在不支持必填控件时强行发起。** 若 `form-schema` 返回的必填控件中有不支持类型(如附件等),直接告知用户不支持通过 CLI 发起。
- **严禁把 `form-schema` 返回的 `content` 当成可直接提交的 payload 模板。**
- **严禁把姓名直接写进 `approvers`、`ccList`、`directAppointedApprovers`、`targetSelectActioners` 或表单人员控件。** 必须先通过 `dws aisearch person --keyword "<姓名>" --dimension name --format json` 转成 userId。
- **严禁在未得到用户确认前直接执行真实提单。**
- **严禁猜测控件名称或选项值。** 必须从 `form-schema` 返回中提取。
- **严禁跳过 `forecast-process` 中的自选节点选人。** 若预测返回 `targetSelect: true` 的节点,必须让用户选人后再发起。
#### 最小判断表
| 你手上有什么 | 下一步 |
|---|---|
| 只有口语需求,比如"帮我发起请假审批" | 先 `search-forms --query 请假` |
| 已拿到 `processCode` | 直接 `form-schema --process-code <code>` |
| 已拿到 Schema | 向用户展示控件列表,收集表单值 |
| 已收集表单值 | `forecast-process` 预测流程走向 |
| 预测返回有 `targetSelect: true` 节点 | 让用户为自选节点选人(`dws aisearch person --keyword "<姓名>" --dimension name --format json` 解析姓名) |
| 预测完成,自选节点已选人 | 汇总确认后 `create-instance --yes` |
| 用户明确说"不走模板流程,直接指定审批人" | 使用 `directAppointedApprovers`(高级模式) |
#### 工作流
```
1. search-forms --query <关键词> → 拿到 processCode(若已有则跳过)
2. form-schema --process-code <code> → 拿到控件列表、类型、选项值
3. 检查 Schema 中是否有不支持的必填控件 → 若有则直接告知用户不支持发起
4. 收集表单值 → 向用户展示控件列表,收集用户填写的表单值
5. forecast-process → 根据表单值预测流程走向,识别自选节点
6. 自选节点选人 → 若预测返回 targetSelect=true 的节点,让用户选人(用 dws aisearch person --keyword "<姓名>" --dimension name --format json 解析姓名)
7. 汇总确认后 create-instance --yes → 展示完整信息(表单值 + 流程路径 + 审批人),用户确认后执行发起
```
> **IMPORTANT:每次发起实例前都必须重新调用 `form-schema` 查询模板。** 即使用户之前查询过同一个 processCode,模板可能已被修改(控件增减、选项变更、必填属性调整等),不得复用旧的 Schema 结果。
#### 交互优化原则
> **核心目标:流程清晰,步骤有序,避免重复询问。**
1. **先查 Schema 再收集表单值(步骤 2→4):** `form-schema` 后向用户展示需要填写的控件列表,然后一次性收集全部表单值。不要在未拿到 Schema 前就问用户填什么。
2. **流程预测后再选自选审批人(步骤 5→6):** `forecast-process` 返回流程路径和自选节点后:
- 先向用户展示完整的流程路径(经过哪些节点、各节点处理人)
- 对 `targetSelect: true` 的节点,提示用户"节点「{activityName}」需要您自选{actorType}人"
- 用 `dws aisearch person --keyword "<姓名>" --dimension name --format json` 帮用户查找并选人
- 若有多个自选节点,一次性收集所有自选节点的选人结果
3. **单次汇总确认(步骤 7):** 发起前一次性展示完整信息供用户确认:
- 审批模板名称
- 表单各控件值
- 流程预测结果(审批路径)
- 各节点审批人/抄送人(含自选节点选人结果)
**反例(禁止):**
- 未查 Schema 就直接问用户填什么表单值
- 流程预测后逐个节点分别询问选人,而非一次性收集
- 用户确认前直接执行发起
```
Usage:
dws oa approval create-instance [flags]
Example:
# 简单发起(Agent 在汇总确认后需加 --yes)
dws oa approval create-instance --process-code PROC-xxx --form-values '{"单行输入框":"测试内容"}' --yes
# 指定审批人(OR=或签,AND=会签,NONE=单人)
dws oa approval create-instance --process-code PROC-xxx --form-values '{"单行输入框":"测试"}' --approvers "userId1,userId2" --approvers-action-type OR --yes
# 指定抄送人
dws oa approval create-instance --process-code PROC-xxx --form-values '{"单行输入框":"测试"}' --cc-list "userId1" --cc-position START --yes
# 高级用法:传入完整 JSON(支持 directAppointedApprovers、targetSelectActioners 等全部字段)
dws oa approval create-instance --request '{"processCode":"PROC-xxx","deptId":-1,"formComponentValues":[{"name":"单行输入框","value":"测试"}]}' --yes
Flags:
--process-code string 审批模板 processCode(简单模式必填)
--form-values string 表单值 JSON,格式 '{"控件名称":"值"}'(简单模式必填)
--dept-id string 发起人所在部门 ID,根部门填 -1(可选,默认 -1)
--originator-user-id string 审批发起人 userId(可选,MCP 工具可从登录态获取)
--approvers string 审批人 userId 列表,多个用逗号分隔(可选)
--approvers-action-type string 审批类型:AND(会签)、OR(或签)、NONE(单人)(可选,默认 OR)
--cc-list string 抄送人 userId 列表,多个用逗号分隔(可选)
--cc-position string 抄送时间点:START/FINISH/START_FINISH(可选,默认 START)
--request string 完整请求体 JSON(高级模式,与简单模式互斥)
--yes 显式确认并发起审批;未提供时命令直接拒绝,不进入交互确认(Agent 必须先汇总并获得用户确认)
```
#### 两种模式
- **简单模式:** 通过 `--process-code` + `--form-values` + 可选 flags 发起,适合大多数场景
- **高级模式:** 通过 `--request` 传入完整 JSON 请求体,支持 `directAppointedApprovers`、`targetSelectActioners` 等复杂字段
#### 组装 form-values
`form-values` 是简单模式下的核心入参;传入时必须是一个 JSON 对象字符串,key 为控件 label,value 为该控件的提交值。组装原则:
- 先用 `form-schema` 识别有哪些控件、每个控件的 `label`(name)、`componentName`(type)、选项值范围以及明细子控件结构。
- **`form-schema` 返回的 `content` 不是可直接提交的原样模板。** 它提供控件定义,`value` 结构须按下方控件值格式表组装。
- 提交时必须保证每个控件的 `name`(即 label)与 Schema 中的 `props.label` **完全一致**。
- 如果用户提供的是人员信息,先用 `dws aisearch person --keyword "<姓名>" --dimension name --format json` 转成 userId 后再写入对应控件。
- 单选/多选控件提交的是选项文本(option value),该值从 `form-schema` 返回的选项定义中取得。
- `InnerContactField`、`DepartmentField`、`TableField`、`DDDateRangeField`、`DDAttachment` 等控件的 `value` 结构各不相同,必须按下方格式表单独组装,不要套用文本控件的写法。
- `TextNote`(文字说明)不收集数据,**不要**出现在 `formComponentValues` 中。
#### 表单控件值格式速查
> **重要:** `formComponentValues` 中每条记录的 `name` 必须与审批模板中控件的 `label`(即 `form-schema` 返回的 `content.items[].props.label`)**完全一致**。`value` 为字符串类型,最大 65535 字符。
>
> **详细参考:** 每种控件的完整属性、约束和示例见 [oa-form-components.md](oa/oa-form-components.md)。组装前**必须先阅读该文档**。
| 控件类型 | componentName | value 格式 | 示例 | 备注 |
|---------|---------------|-----------|------|-------------------------------------------------------|
| 单行输入框 | `TextField` | 纯文本 | `"测试内容"` | |
| 多行输入框 | `TextareaField` | 纯文本 | `"第一行\n第二行"` | |
| 数字输入框 | `NumberField` | 数字字符串 | `"100"` | |
| 单选框 | `DDSelectField` | 选项文本 | `"同意"` | 必须与模板 options 中的 value 完全匹配 |
| 多选框 | `DDMultiSelectField` | JSON 数组字符串 | `'["选项A","选项B"]'` | 每个选项须与模板 options 匹配; |
| 日期控件 | `DDDateField` | `yyyy-MM-dd` | `"2026-07-27"` | |
| 时间区间 | `DDDateRangeField` | JSON 数组字符串 | `'["2026-07-27","2026-07-30"]'` | label 为数组 `["开始","结束"]`,用开始时间 label 作 name |
| 金额控件 | `MoneyField` | 数字字符串 | `"1500.50"` | 自动显示大写金额 |
| 电话控件 | `PhoneField` | 手机号字符串 | `"13800138000"` | |
| 联系人控件 | `InnerContactField` | userId | `"user123"` | 多人时传 JSON 数组 `'["user1","user2"]'`;choice="0"单选/"1"多 |
| 部门控件 | `DepartmentField` | 部门 ID | `"12345"` | 多部门传 JSON 数组;multiple=true 时支持多选 |
| 省市区控件 | `AddressField` | JSON 数组字符串 | `'["浙江省","杭州市","西湖区"]'` | 三级联动;needDetail=true 时末尾加详细地址 |
| 图片控件 | `DDPhotoField` | URL 数组转义字符串 | `"[\"http://example.com/img1.jpg\"]"` | 支持 URL 直接提交;**不支持本地文件上传** |
| 附件控件 | `DDAttachment` | JSON 数组转义字符串 | `"[{\"spaceId\":\"xxx\",\"fileName\":\"a.pdf\",\"fileSize\":\"333\",\"fileType\":\"pdf\",\"fileId\":\"xxx\"}]"` | **当前不支持通过 CLI 提交**,需钉盘上传接口获取 fileId 等字段 |
| 评分控件 | `StarRatingField` | 数字字符串 | `"4"` | limit 控制最大星数(默认 5) |
| 关联审批单 | `RelateField` | 审批实例 ID | `"q-xxx"` | 须为当前组织下已存在的实例 |
| 明细控件 | `TableField` | JSON 数组字符串 | `'[{"子控件名":"值1"},{"子控件名":"值2"}]'` | 不可嵌套 TableField;不可含 DDMultiSelectField/DDPhotoField;最大 100 行 |
| 身份证控件 | `IdCardField` | 身份证号 | `"330102199001011234"` | 内置格式校验 |
| 文字说明 | `TextNote` | — | — | **不收集数据**,不会出现在 formComponentValues 中 |
#### API 不支持的控件
以下控件**不支持**通过创建实例 API 提交:
- `TextNote`(文字说明)— 纯展示,不收集数据
- `CalculateField`(计算公式)— 由系统自动计算
- `SeqNumberField`(流水号)— 由系统自动生成
- `OcrTextField` / `OcrIdCardField`(OCR 识别)— 需要客户端交互
- **`DDAttachment`(附件控件)— 当前不支持通过 CLI 提交**,value 需要 spaceId、fileName、fileSize、fileType、fileId 字段,须通过钉盘上传附件接口获取
- **套件类控件(暂不支持)** — `InvoiceField`(发票)、`RecipientAccountField`(收款账户)等业务套件控件当前暂不支持通过 CLI 发起,包含这些控件的审批模板请直接在钉钉客户端操作
> **部分支持的控件:** `DDPhotoField`(图片控件)**支持通过 URL 直接提交**(见上方速查表),仅不支持本地文件上传(CLI 未封装钉盘 CDN 上传流程)。若用户只有本地文件,需告知在钉钉客户端补充。
如果目标审批模板包含上述控件,不要硬拼 `form-values`;应告知用户这些字段无需填写或需要在钉钉客户端补充。
> **必填不支持控件判断规则:** 检查 `form-schema` 返回的控件列表,若存在上述不支持控件且其 `props.required` 为 `true`(必填项),则**直接告知用户该审批模板不支持通过 CLI 发起**,请在钉钉客户端操作。只有不支持控件为非必填时,才可跳过该控件继续发起。
#### 高级模式请求体字段(`--request` JSON 完整结构)
| 字段 | 类型 | 必填 | 说明 |
|-----|------|------|------|
| `processCode` | String | 是 | 审批模板唯一码 |
| `originatorUserId` | String | 是 | 发起人 userId(MCP 工具可从登录态自动获取) |
| `deptId` | Long | 否 | 发起人部门 ID,根部门填 -1;approvers 已传时可不填 |
| `formComponentValues` | Array | 是 | 表单控件值列表,最大 150 条 |
| `approvers` | Array | 否 | 指定审批人列表(覆盖模板流程),最大 20 条 |
| `approvers[].actionType` | String | 否 | `AND`(会签)/ `OR`(或签)/ `NONE`(单人) |
| `approvers[].userIds` | Array | 否 | 审批人 userId 列表 |
| `ccList` | Array | 否 | 抄送人 userId 列表,最大 50 |
| `ccPosition` | String | 否 | `START` / `FINISH` / `START_FINISH` |
| `directAppointedApprovers` | Array | 否 | 指定审批人组(覆盖模板流程),结构见下方 |
| `targetSelectActioners` | Array | 否 | 自选审批人(模板中有自选节点时必填),最大 20 条 |
#### 节点参数组装
> **详细参考:** 流程节点类型、审批模式、条件分支和 10 种审批人选择规则的完整说明见 [oa-process-nodes.md](oa/oa-process-nodes.md)。
**directAppointedApprovers(指定审批人覆盖模板流程):**
当用户明确说"不走模板默认流程"或"直接指定 XX 审批"时使用。
```json
[
{
"staffIds": ["userId1", "userId2"],
"taskActionType": "NONE",
"staffId": ""
}
]
```
- `staffIds`:审批人 userId 列表(必须通过 `dws aisearch person --keyword "<姓名>" --dimension name --format json` 获取,严禁填姓名)
- `taskActionType`:`NONE`(单人审批)/ `AND`(会签)/ `OR`(或签)
**targetSelectActioners(模板有自选审批节点时使用):**
当 `form-schema` 返回的模板流程中存在自选审批节点(`target_select` 类型)时必填。
```json
[
{
"actionerKey": "manual_nodeId_xxxx_yyyy",
"actionerStaffIds": ["userId1"]
}
]
```
- `actionerKey`:自选节点的规则 key,可通过获取审批单流程节点信息接口获取 `actorKey`
- `actionerStaffIds`:操作人 userId 列表
**审批类型(approvers actionType)说明:**
| 值 | 含义 | 说明 |
|----|------|------|
| `AND` | 会签 | 所有审批人都必须审批通过 |
| `OR` | 或签 | 任一审批人审批即可 |
| `NONE` | 单人审批 | 只有一个审批人 |
**抄送时间点(ccPosition)说明:**
| 值 | 含义 |
|----|------|
| `START` | 审批发起时抄送 |
| `FINISH` | 审批完成时抄送 |
| `START_FINISH` | 发起和完成时都抄送 |
#### 表单控件约束
- 单个表单最多 200 个控件
- 控件 label(name)和 placeholder 最大 50 字符
- `DDSelectField` / `DDMultiSelectField` 的选项 value 必须与模板中配置的选项文本完全一致
- `TableField`(明细)内不可嵌套 `TableField`,不可包含 `DDMultiSelectField` 和 `DDPhotoField`
- `TextNote`(文字说明)不收集数据,无需在 `formComponentValues` 中传入
- `InnerContactField` 的 userId 应为当前组织下在职成员
- `DepartmentField` 应传入当前组织下存在的部门 ID
- `RelateField` 传入的审批实例 ID 应为当前组织下已存在的实例
#### 返回结果
创建成功后,返回的 `result` 字段即为新审批实例的 `processInstanceId`。建议向用户展示:
```
审批已创建成功:
- 审批模板: <processName>(来自 form-schema)
- 审批实例 ID: <processInstanceId>(来自 create-instance 返回的 result)
```
后续可用该 processInstanceId 执行 `detail`、`tasks`、`records`、`revoke` 等操作。
### 获取审批任务的被催办人 userId
> **催办必须两步串联:** ① `ding-info` 获取被催办人 `userId` → ② `ding message send` 发送催办消息。禁止跳过第一步直接猜测 userId。
@@ -320,6 +677,17 @@ Flags:
用户说"我发起的审批" → `approval list-initiated`(需 --process-code,可从 list-forms 或 detail 获取)
用户说"有哪些审批表单/可见表单" → `approval list-forms`
用户说"搜索审批表单/查找xx审批表单/有没有xx表单" → `approval search-forms`(需 --query)
用户说"查表单schema/查表单结构/表单模板信息/查表单组件/查表单定义/表单有哪些字段/表单的字段信息" → `approval form-schema`(需 --process-code,可从 list-forms / search-forms / detail 获取)
用户说"预测审批流程/流程预测/审批走向/这个审批走哪些人/审批流程预览" → `approval forecast-process`(需 --process-code、--dept-id、--form-values)
- 在 `form-schema` 之后、`create-instance` 之前调用
- 返回的 `workflowActivityRuleVOs` 中 `targetSelect: true` 的节点需要用户自选审批人
- 自选结果组装为 `targetSelectActioners` 传入 `create-instance`
用户说"发起审批/提交审批/帮我发起XX审批/新建审批单/提一个XX审批/帮我提XX申请" → 五步流程:① `search-forms --query XX` 获取 processCode → ② `form-schema --process-code <code>` 获取表单字段定义 → ③ 阅读 [oa-form-components.md](oa/oa-form-components.md) 和 [oa-process-nodes.md](oa/oa-process-nodes.md) 后组装表单值 → ④ `forecast-process` 预测流程走向并识别自选节点 → ⑤ 若有自选节点让用户选人,确认后 `create-instance --yes` 发起
- 如果用户已知 processCode,可跳过第①步
- `--form-values` 的 key 必须与 `form-schema` 返回的控件 label 一致
- `forecast-process` 返回自选节点时必须让用户选人,不得跳过
- 执行前**必须向用户确认**表单内容、流程预测结果、审批人和抄送人
- 示例:"帮我发起一个AI审批单" → ① `search-forms --query AI` → ② `form-schema --process-code <code>` → ③ 组装表单值 → ④ `forecast-process` → ⑤ 向用户确认流程走向和自选审批人后 `create-instance --yes`
用户说"催办审批/DING 一下审批人/提醒审批/催一下审批/催批/提醒审批人" → 先 `approval ding-info`(拿到被催办人 `userId`),再 `ding message send`(将 userId 作为 `--users` 传入;`--robot-code` 优先走 `$DINGTALK_DING_ROBOT_CODE` 或向用户确认;`--content` 由 agent 根据审批上下文撰写)
- **禁止跳过 ding-info:** 不得自行猜测或编造 userId,必须先调用 `ding-info` 获取
- **机器人编码获取顺序:** ① `$DINGTALK_DING_ROBOT_CODE` 环境变量 → ② 用户显式提供 → ③ 询问用户
@@ -378,6 +746,9 @@ dws oa approval list-forms --cursor 0 --limit 100 --format json
# 7b. 按关键字模糊搜索表单(快速定位 processCode)
dws oa approval search-forms --query AI --format json
# 7c. 按 processCode 查询表单 Schema(获取表单结构、组件定义)
dws oa approval form-schema --process-code <code> --format json
# 8. 查看自己发起的审批列表(--process-code 来自 list-forms 或 detail)
dws oa approval list-initiated --process-code <code> \
--start "2026-03-10T00:00:00+08:00" --end "2026-03-10T23:59:59+08:00" \
@@ -420,6 +791,22 @@ dws oa approval revert-activities --task-id <taskId> --format json
dws oa approval revert-task --instance-id <processInstanceId> --task-id <taskId> --target-activity-id sid-startevent --action REVERT_FOR_RESUBMIT --remark "补充说明后重提" --format json
# 17c. 退回到某个审批节点重新审批(targetActivityId 和 action 从 revert-activities 返回中获取)
dws oa approval revert-task --instance-id <processInstanceId> --task-id <taskId> --target-activity-id <activityId> --action REVERT_FOR_APPROVAL --remark "重新审批" --format json
# 18. 发起审批(完整流程:搜表单 → 查 Schema → 收集表单值 → 流程预测 → 自选节点选人 → 发起)
# 18a. 模糊搜索表单获取 processCode
dws oa approval search-forms --query AI --format json
# 18b. 查询表单 Schema 获取字段定义
dws oa approval form-schema --process-code <code> --format json
# 18c. 收集表单值(向用户展示控件列表,用户填写后组装 form-values)
# 18d. 流程预测(根据表单值预测审批走向,识别自选审批人节点;processCode/deptId/formValues 必填,userId 由登录态自动填充)
dws oa approval forecast-process --process-code <code> --dept-id -1 --form-values '{"单行输入框":"测试内容"}' --format json
# 18e. 若 forecast 返回 targetSelect=true 的节点,用 dws aisearch person --keyword "<姓名>" --dimension name --format json 帮用户选人
# 18f. 发起审批实例(form-values 的 key 须与 Schema 中控件 label 一致)
dws oa approval create-instance --process-code <code> --form-values '{"单行输入框":"测试内容"}' --yes --format json
# 18g. 发起并指定审批人和抄送人
dws oa approval create-instance --process-code <code> --form-values '{"单行输入框":"测试"}' --approvers "userId1,userId2" --approvers-action-type OR --cc-list "userId3" --cc-position START --yes --format json
# 18h. 发起并使用 forecast 自选审批人结果(高级模式)
dws oa approval create-instance --request '{"processCode":"PROC-xxx","deptId":-1,"formComponentValues":[{"name":"单行输入框","value":"测试"}],"targetSelectActioners":[{"actionerKey":"manual_33ff_89cb_da91_e3aa","actionerStaffIds":["userId_选人A"]}]}' --yes --format json
```
## 上下文传递表
@@ -431,6 +818,11 @@ dws oa approval revert-task --instance-id <processInstanceId> --task-id <taskId>
| `detail` | `processCode` | list-initiated 的 --process-code |
| `list-forms` | `processCode` | list-initiated 的 --process-code |
| `search-forms` | `processCode` | list-initiated 的 --process-code |
| `form-schema` | `processCode`, `processName`, `content` | 查看表单结构定义;`content` 字段包含表单组件 JSON,可解析获取字段列表;**控件 label 作为 create-instance --form-values 的 key** |
| `search-forms` → `form-schema` | `processCode` → 表单字段定义 | forecast-process / create-instance 的 --process-code 和 --form-values 填写依据 |
| `forecast-process` | `workflowActivityRuleVOs`(`activityId`, `targetSelect`, `activityActioners`, `workflowActor`) | ① 向用户展示流程走向和各节点处理人;② `targetSelect: true` 的节点需用户自选审批人,`workflowActor.actorKey` 作为 `targetSelectActioners` 的 `actionerKey` 传入 create-instance |
| `search-forms` → `form-schema` → `forecast-process` | `processCode` → 字段定义 → 流程走向 + 自选节点 | create-instance 的完整上下文:表单值 + 流程路径 + targetSelectActioners |
| `create-instance` | `result`(processInstanceId) | detail / tasks / records / revoke 等的 --instance-id,可跟踪已发起的审批 |
| `ding-info` | `userId` | ding message send 的 --users(多个逗号拼接);**robotCode 优先走 `$DINGTALK_DING_ROBOT_CODE` 环境变量,content 由 agent 根据审批上下文撰写;返回空时报错并停止** |
| `revert-activities` | `activityId`, `revertAction`, `activityName` | revert-task 的 --target-activity-id 和 --action;**返回空时必须告知用户"无可回退节点"** |
@@ -459,6 +851,16 @@ dws oa approval revert-task --instance-id <processInstanceId> --task-id <taskId>
- `ding-info` 返回空或报错时,必须明确告知用户"无法获取该任务的被催办人信息"并停止
- DING 默认发应用内提醒(无成本);如需短信/电话提醒可加 `--type sms` 或 `--type call`(有成本,建议向用户确认)
- `form-schema` 的 `--process-code` 可从 `list-forms`、`search-forms` 或 `detail` 返回中提取;返回的 `content` 字段为 JSON 字符串,需解析后查看表单组件(items)定义。
- `create-instance` 发起前**必须先阅读** [oa-form-components.md](oa/oa-form-components.md)(控件值格式)和 [oa-process-nodes.md](oa/oa-process-nodes.md)(流程节点规则),再调用 `form-schema` 获取表单字段定义,确保 `--form-values` 中的 key 与控件 label 完全一致。
- `create-instance` 发起前**应先调用 `forecast-process`** 预测流程走向,识别自选审批人节点(`targetSelect: true`),让用户选人后再提交。
- `create-instance` 的 `--form-values` 接受 JSON 格式 `'{"控件名称":"值"}'`,代码会自动转为 `[{"name":"控件名称","value":"值"}]`。
- `create-instance` 简单模式适合常见场景;如需 `directAppointedApprovers`(指定审批人覆盖模板流程)或 `targetSelectActioners`(自选审批节点)等高级字段,使用 `--request` 传完整 JSON。`--request` 与简单模式 flags 互斥。
- `create-instance` 会创建真实审批数据;Agent 只有在用户确认模板、表单值、流程路径和人员后才能传入 `--yes`。
- `create-instance` 返回的 processInstanceId 可用于 `detail`、`tasks`、`records`、`revoke` 等后续操作。
- `forecast-process` 的 `processCode`、`deptId`、`formComponentValues` 三个字段均为必填(`userId` 由系统自动填充);`formComponentValues` 比 `create-instance` 多一层数组包裹(`[[{...}]]`),CLI 简单模式已自动处理。
- `forecast-process` 返回 `workflowActivityRuleVOs` 中 `targetSelect: true` 的节点,其 `workflowActor.actorKey` 必须作为 `targetSelectActioners` 的 `actionerKey` 传入 `create-instance`。
## 自动化脚本
| 脚本 | 场景 | 用法 |
@@ -0,0 +1,346 @@
# OA 审批表单控件参考
本文档详细描述钉钉 OA 审批中每种表单控件(componentName)在**发起审批实例**时 `formComponentValues` 的 `value` 格式、约束和注意事项。
> **核心原则:** `formComponentValues[].name` 必须与审批模板中控件的 `props.label` **完全一致**,`value` 为字符串类型(最大 65535 字符)。
---
## 通用约束
| 约束 | 说明 |
|------|------|
| 单表单最大控件数 | 200 |
| label / placeholder 最大长度 | 50 字符 |
| value 最大长度 | 65535 字符 |
| ID / bizAlias 唯一性 | 同一表单内不可重复 |
| TextNote | 不收集数据,不出现在 formComponentValues 中 |
---
## 基础控件
### TextField(单行输入框)
| 属性 | 说明 |
|------|------|
| `componentName` | `TextField` |
| value 格式 | 纯文本字符串 |
| 示例 | `"测试内容"` |
| 约束 | 无特殊约束 |
```json
{ "name": "单行输入框", "value": "测试内容" }
```
### TextareaField(多行输入框)
| 属性 | 说明 |
|------|------|
| `componentName` | `TextareaField` |
| value 格式 | 纯文本字符串,支持换行 |
| 示例 | `"第一行\n第二行"` |
| 约束 | 无 `ratio` 属性 |
```json
{ "name": "多行输入框", "value": "第一行\n第二行\n第三行" }
```
### NumberField(数字输入框)
| 属性 | 说明 |
|------|------|
| `componentName` | `NumberField` |
| value 格式 | 数字字符串 |
| 示例 | `"100"` |
| 约束 | 适合数量、天数等纯数字场景 |
```json
{ "name": "加班天数", "value": "3" }
```
### DDSelectField(单选框)
| 属性 | 说明 |
|------|------|
| `componentName` | `DDSelectField` |
| value 格式 | 选项文本字符串 |
| 示例 | `"同意"` |
| 约束 | **必须与模板 `options[].value` 完全匹配**,不可自行编造选项 |
模板中的选项结构(从 `form-schema` 获取):
```json
"options": [
{ "key": "option_0", "value": "同意" },
{ "key": "option_1", "value": "不同意" }
]
```
提交时传选项的 `value` 文本:
```json
{ "name": "审批意见", "value": "同意" }
```
### DDMultiSelectField(多选框)
| 属性 | 说明 |
|------|------|
| `componentName` | `DDMultiSelectField` |
| value 格式 | JSON 数组字符串,每个元素为选项文本 |
| 示例 | `'["选项A","选项B"]'` |
| 约束 | 每个选项须与模板 `options[].value` 匹配; |
```json
{ "name": "兴趣爱好", "value": "[\"阅读\",\"运动\"]" }
```
### DDDateField(日期控件)
| 属性 | 说明 |
|------|------|
| `componentName` | `DDDateField` |
| value 格式 | `yyyy-MM-dd` 格式字符串 |
| 示例 | `"2026-07-27"` |
| 约束 | 格式固定,不可传其他日期格式 |
```json
{ "name": "请假日期", "value": "2026-07-27" }
```
### DDDateRangeField(时间区间控件)
| 属性 | 说明 |
|------|------|
| `componentName` | `DDDateRangeField` |
| value 格式 | JSON 数组字符串 `[开始日期, 结束日期]` |
| 示例 | `'["2026-07-27","2026-07-30"]'` |
| 约束 | `props.label` 为数组 `["开始时间","结束时间"]`;提交时 `name` 使用**开始时间的 label** |
模板中的 label 结构(从 `form-schema` 获取):
```json
"props": { "label": ["开始时间", "结束时间"] }
```
提交时用**开始时间 label** 作为 name:
```json
{ "name": "开始时间", "value": "[\"2026-07-27\",\"2026-07-30\"]" }
```
### PhoneField(电话控件)
| 属性 | 说明 |
|------|------|
| `componentName` | `PhoneField` |
| value 格式 | 手机号字符串 |
| 示例 | `"13800138000"` |
| 约束 | `mode: "phone"` 为手机号 |
```json
{ "name": "联系电话", "value": "13800138000" }
```
### IdCardField(身份证控件)
| 属性 | 说明 |
|------|------|
| `componentName` | `IdCardField` |
| value 格式 | 身份证号字符串 |
| 示例 | `"330102199001011234"` |
| 约束 | 内置格式校验,须传合法身份证号 |
```json
{ "name": "身份证号", "value": "330102199001011234" }
```
### TextNote(文字说明)
| 属性 | 说明 |
|------|------|
| `componentName` | `TextNote` |
| value 格式 | — |
| 约束 | **不收集数据**,不出现在 formComponentValues 中 |
> 遇到 TextNote 控件时直接跳过,不要尝试为它填写值。
---
## 增强控件
### MoneyField(金额控件)
| 属性 | 说明 |
|------|------|
| `componentName` | `MoneyField` |
| value 格式 | 数字字符串 |
| 示例 | `"1500.50"` |
| 约束 | 系统自动显示大写金额(`notUpper: "0"` 时显示) |
```json
{ "name": "报销金额", "value": "1500.50" }
```
### InnerContactField(联系人控件)
| 属性 | 说明 |
|------|----------------------------------------------------|
| `componentName` | `InnerContactField` |
| value 格式 | userId 字符串,多人时为 JSON 数组字符串 |
| 示例(单选) | `"user123"` |
| 示例(多选) | `'["userId1","userId2"]'` |
| 约束 | `choice: "0"` 单选 / `"1"` 多选;userId 须为**当前组织下在职成员** |
```json
{ "name": "项目负责人", "value": "[\"userId1\",\"userId2\"]" }
```
> **严禁直接写姓名。** 必须先通过 `dws aisearch person --keyword "<姓名>" --dimension name --format json` 查询获取 userId;多结果时须让用户消歧确认。
### DepartmentField(部门控件)
| 属性 | 说明 |
|------|------|
| `componentName` | `DepartmentField` |
| value 格式 | 部门 ID 字符串,多部门时为 JSON 数组字符串 |
| 示例(单选) | `"12345"` |
| 示例(多选) | `'["12345","67890"]'` |
| 约束 | `multiple: boolean` 控制单选/多选;部门 ID 须为**当前组织下存在的部门** |
```json
{ "name": "所属部门", "value": "12345" }
```
### AddressField(省市区控件)
| 属性 | 说明 |
|------|------|
| `componentName` | `AddressField` |
| value 格式 | JSON 数组字符串 `["省","市","区"]` |
| 示例 | `'["浙江省","杭州市","西湖区"]'` |
| 约束 | 三级联动选择器;`needDetail: true` 时末尾追加详细地址文本 |
```json
{ "name": "办公地点", "value": "[\"浙江省\",\"杭州市\",\"西湖区\"]" }
```
### DDPhotoField(图片控件)
> **支持通过图片 URL 提交,不支持本地文件上传。** 如果用户已有图片 URL(如公网可访问的图片链接),可直接填入 value 提交。CLI 尚未封装本地文件上传到钉盘 CDN 的流程,若用户只有本地文件而非 URL,需告知用户在钉钉客户端补充。
| 属性 | 说明 |
|------|------|
| `componentName` | `DDPhotoField` |
| value 格式 | URL 数组转义字符串,即使只有一个 URL 也需数组形式 |
| 示例 | `"[\"http://example.com/img1.jpg\",\"http://example.com/img2.jpg\"]"` |
| 约束 | 支持 URL 直接提交;**不支持本地文件上传**(CLI 未封装钉盘上传流程); |
```json
{ "name": "图片", "value": "[\"http://example.com/photo.jpg\"]" }
```
### DDAttachment(附件控件)
> **[注意] 当前暂不支持通过 CLI 提交附件控件。** 附件控件的 value 需要包含 spaceId、fileName、fileSize、fileType 和 fileId 字段,这些字段需要通过调用钉盘的上传附件接口获取,CLI 尚未封装此流程。包含附件控件的审批模板请在钉钉客户端操作。
| 属性 | 说明 |
|------|------|
| `componentName` | `DDAttachment` |
| value 格式 | JSON 数组转义字符串,每个元素包含 spaceId、fileName、fileSize、fileType、fileId |
| 示例(参考) | `"[{\"spaceId\":\"163xxx\",\"fileName\":\"2644.JPG\",\"fileSize\":\"333\",\"fileType\":\"jpg\",\"fileId\":\"643xxx\"}]"` |
| 约束 | **当前不支持通过 CLI 提交**;各字段需通过钉盘上传附件接口获取 |
### StarRatingField(评分控件)
| 属性 | 说明 |
|------|------|
| `componentName` | `StarRatingField` |
| value 格式 | 数字字符串 |
| 示例 | `"4"` |
| 约束 | `limit` 控制最大星数(默认 5) |
```json
{ "name": "满意度评分", "value": "4" }
```
### RelateField(关联审批单)
| 属性 | 说明 |
|------|------|
| `componentName` | `RelateField` |
| value 格式 | 审批实例 ID 字符串 |
| 示例 | `"q-ZZ1sQaTIuYFpKI9aNC1g"` |
| 约束 | 须为**当前组织下已存在的审批实例 ID** |
```json
{ "name": "关联审批单", "value": "q-ZZ1sQaTIuYFpKI9aNC1g" }
```
### SignatureField(签名控件)
| 属性 | 说明 |
|------|------|
| `componentName` | `SignatureField` |
| value 格式 | 签名图片 mediaId |
| 约束 | 需要客户端交互签名,通常不支持 API 直接提交 |
---
## 复合控件
### TableField(明细控件)
| 属性 | 说明 |
|------|------|
| `componentName` | `TableField` |
| value 格式 | JSON 数组字符串,每个元素为一行数据的键值对 |
| 示例 | `'[{"商品名":"笔记本","数量":"2"},{"商品名":"钢笔","数量":"1"}]'` |
| 约束 | **不可嵌套 TableField**;**不可包含 DDMultiSelectField 和 DDPhotoField**;最大 100 行;总长度不超过 65535 字符 |
模板结构(从 `form-schema` 获取):
```json
{
"componentName": "TableField",
"props": { "label": "采购明细" },
"children": [
{ "componentName": "TextField", "props": { "label": "商品名", "id": "TextField_XXX" } },
{ "componentName": "NumberField", "props": { "label": "数量", "id": "NumberField_YYY" } }
]
}
```
提交时每行用子控件 label 作 key:
```json
{
"name": "采购明细",
"value": "[{\"商品名\":\"笔记本\",\"数量\":\"2\"},{\"商品名\":\"钢笔\",\"数量\":\"1\"}]"
}
```
---
## API 不支持的控件
以下控件**不支持**通过创建实例 API 提交,遇到时应告知用户需在钉钉客户端补充:
| 控件 | componentName | 原因 |
|------|---------------|------|
| 文字说明 | `TextNote` | 纯展示,不收集数据 |
| 计算公式 | `CalculateField` | 由系统自动计算,不可手动填写 |
| 流水号 | `SeqNumberField` | 由系统自动生成 |
| OCR 文本识别 | `OcrTextField` | 需要客户端 OCR 交互 |
| OCR 身份证识别 | `OcrIdCardField` | 需要客户端 OCR 交互 |
| 附件控件 | `DDAttachment` | value 需要 spaceId、fileName、fileSize、fileType、fileId,须通过钉盘上传接口获取,CLI 尚未封装 |
> **部分支持的控件:** `DDPhotoField`(图片控件)**支持通过 URL 直接提交**,但不支持本地文件上传(CLI 未封装钉盘 CDN 上传流程)。若用户只有本地文件,需告知在钉钉客户端补充。详见本文 [DDPhotoField](#ddphotofield图片控件) 章节。
> **套件类控件(暂不支持)** — `InvoiceField`(发票)、`RecipientAccountField`(收款账户)等业务套件控件当前暂不支持通过 CLI 发起,包含这些控件的审批模板请直接在钉钉客户端操作。
---
## 组装优先级
1. **每次发起前都重新调用 `form-schema`**,不得复用旧结果(模板可能已被修改)
2. 先读 `form-schema` 返回的 `content`,识别所有控件的 `label`、`componentName`、`options`、`props.required`
3. **检查是否存在不支持控件且为必填项(`props.required: true`)**,若有则直接告知用户该模板不支持通过 CLI 发起,请在钉钉客户端操作
4. 按本文档中每种控件的 value 格式组装 `formComponentValues`
5. **不要把 `form-schema` 的 `content` 当成可直接提交的模板**
6. 遇到 API 不支持的控件(非必填),跳过并告知用户
@@ -0,0 +1,374 @@
# OA 审批流程节点与审批人规则参考
本文档描述钉钉 OA 审批的流程节点类型、审批模式、条件分支和审批人选择规则,用于理解审批模板结构和正确填写 `create-instance` 的节点参数。
---
## 流程结构概览
审批流程是一个嵌套树结构:
- **根节点**:发起人节点(`type: "start"`,`nodeId: "sid-startevent"`),固定不可删除
- **后续节点**:通过 `childNode` 链接形成链式结构
- **分支节点**:条件分支(`route` + `condition`)或并行分支(`parallel`)
- 当没有后续节点时,`childNode` 字段**必须省略**(不可设为 `null`)
---
## 7 种节点类型
### 1. 发起人节点(start)
| 属性 | 值 |
|------|-----|
| `type` | `start` |
| `nodeId` | `sid-startevent`(固定) |
| `properties` | `{}`(空对象) |
唯一、不可删除。是流程的起点。
### 2. 审批人节点(approver)
核心决策节点,有审批/拒绝权限。
| 属性 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `actionerRules` | Array | 是 | 审批人选择规则,至少一条 |
| `activateType` | String | 是 | 多人审批模式(见下方) |
| `approvalType` | String | 是 | 固定 `"MANUAL"` |
| `agreeAll` | Boolean | 是 | `true` 全部通过 / `false` 任一通过 |
| `noneActionerAction` | String | 否 | 如 `"admin"`(找不到审批人时转管理员) |
支持全部 10 种 actionerRules 类型。
### 3. 办理人节点(handler)
执行工作,无审批决策权。
| 属性 | 类型 | 必填 |
|------|------|------|
| `actionerRules` | Array | 是 |
| `activateType` | String | 是 |
支持 9 种 actionerRules(不支持 `target_matrix_approval`)。
### 4. 抄送人节点(notifier)
仅接收通知,无决策权。
| 属性 | 类型 | 必填 |
|------|------|------|
| `actionerRules` | Array | 是 |
支持多条 actionerRules 组合在一个节点中,实现同时抄送多类人员。
### 5. 条件分支(route + condition)
条件路由节点,包含多个条件分支。
**route 节点:**
- `type: "route"`
- `conditionNodes[]`:分支数组,按优先级排序,**默认分支必须在最后**
- `properties: {}`
**condition 节点(conditionNodes 的每个元素):**
- `type: "condition"`
- `isdefault: true`:标记默认分支
- `properties.conditions`:二维条件数组
- 外层数组:多个条件组,**OR 关系**
- 内层数组:多个条件对象,**AND 关系**
- 默认分支:`[[]]`(一个空组)
### 6. 并行分支(parallel)
多个分支同时执行,全部完成后才继续。
| 属性 | 说明 |
|------|------|
| `branches[]` | 分支数组 |
| `branches[].name` | 分支名称 |
| `branches[].childNode` | 该分支的第一个节点 |
### 7. 付款人节点(payer)
财务付款节点。
| 属性 | 说明 |
|------|------|
| `actionerRules` | 审批人规则 |
| `paymentConfig.amountField` | 金额控件 ID |
| `paymentConfig.accountField` | 收款账户控件 ID |
---
## 多人审批模式
| 模式 | `activateType` | `agreeAll` | 说明 |
|------|---------------|-----------|------|
| 会签 | `"ALL"` | `true` | 所有审批人都必须审批通过 |
| 或签 | `"ALL"` | `false` | 任一审批人审批即可 |
| 依次审批 | `"ONE_BY_ONE"` | `true` | 按顺序逐级审批 |
---
## 10 种审批人选择规则(actionerRules)
### 1. 指定成员(target_approval)
明确指定具体人员。
```json
{
"type": "target_approval",
"approvals": [
{ "userName": "张三", "workNo": "manager123" }
],
"isEmpty": false
}
```
- `workNo` 必须通过 `dws aisearch person --keyword "<工号>" --dimension jobNumber --format json` 获取,**严禁编造**
- 在 `create-instance` 中对应 `directAppointedApprovers` 的 `staffIds`
### 2. 直属主管(target_formula / reportLineManager)
按汇报线找到直属主管。
```json
{
"type": "target_formula",
"subType": "reportLineManager",
"formula": "ReportLineManager(corpId,originator,1)",
"isEmpty": false
}
```
- `formula` 中最后的数字 N 表示第 N 级主管
- **重要区分:** 用户说"直属主管/直属领导/汇报线主管"才用此规则;用户说"主管审批/leader审批"(模糊)时默认用 `target_management`(部门主管)
### 3. 发起人自己(target_originator)
发起人自行审批。
```json
{
"type": "target_originator",
"isEmpty": false
}
```
最简单的规则,只有 `type` 和 `isEmpty`。
### 4. 部门主管(target_management)
从发起人所在部门层级找主管。
```json
{
"type": "target_management",
"level": 1,
"autoUp": true,
"isEmpty": false
}
```
- `level: 1`:直接部门主管
- `autoUp: true`:找不到时向上级部门搜索
- **这是"主管审批/leader审批"模糊场景的默认选择**
### 5. 表单部门主管(target_formula / managerOfDept)
根据表单中部门控件选择的主管。
```json
{
"type": "target_formula",
"subType": "managerOfDept",
"formula": "ManagerOfDept(corpId,$('DepartmentField_XXX'),1)",
"isEmpty": false
}
```
- `formula` 中引用表单中的 `DepartmentField` 控件 ID
### 6. 发起人自选(target_select)
发起人在提单时自行选择审批人。
```json
{
"type": "target_select",
"select": ["allStaff"],
"range": {},
"key": "manual_nodeId_xxxx_yyyy",
"multi": 1,
"isEmpty": false
}
```
- `select: ["allStaff"]`:可选全组织人员
- `multi: 1`:单选
- `key`:格式 `manual_{nodeId}_{hex}_{hex}`
- 在 `create-instance` 中对应 `targetSelectActioners` 的 `actionerKey`
### 7. 角色标签主管(target_managers_labels)
按角色标签找多级主管。
```json
{
"type": "target_managers_labels",
"labelNames": ["项目经理"],
"labels": ["labelId123"],
"levels": [1],
"isEmpty": false
}
```
- `labels` 中的 ID 必须通过 `dws contact label get --names "<角色名>" --format json` 获取;已知角色名时直接查询,否则先 `dws contact label list --format json` 获取全部角色列表后匹配
### 8. 表单联系人(target_formcomponent_approval)
从表单中的联系人控件读取审批人。
```json
{
"type": "target_formcomponent_approval",
"paramKey": "InnerContactField_XXX",
"label": "项目负责人",
"isEmpty": false
}
```
- `paramKey` 指向表单中的 `InnerContactField` 控件 ID
- 该控件中填写的人即为审批人
### 9. 角色标签(target_label)
按角色标签找人(如"财务"、"HR")。
```json
{
"type": "target_label",
"labelNames": "财务",
"labels": "459272424",
"isEmpty": false
}
```
- `labels`:角色标签 ID(字符串),必须通过 `dws contact label get --names "<角色名>" --format json` 获取;未知角色名时先 `dws contact label list --format json`
- `labelNames`:角色显示名称
- **严禁编造 label ID**
### 10. 审批矩阵(target_matrix_approval)
按审批矩阵规则确定审批人。
```json
{
"type": "target_matrix_approval",
"matrixId": "xxx",
"roleColumnId": "yyy",
"expression": {
"subFilters": [...],
"operator": "AND"
}
}
```
- 仅适用于审批人节点
- 目前尚在完善中
---
## 条件分支详解
### 条件类型
| `type` | 依据 | 关键字段 |
|--------|------|---------|
| `dingtalk_actioner_dept_condition` | 发起人部门/人员/角色 | `paramKey: "dingtalk_origin_dept"`, `conds[]` |
| `dingtalk_actioner_dept_component_condition` | 表单部门控件 | `paramKey: 控件ID`, `conds[]` |
| `dingtalk_actioner_range_condition` | 数值/金额/时长范围 | `lowerBound`(>=) / `lowerBoundNotEqual`(>) / `upperBoundEqual`(<=) / `upperBound`(<) / `boundEqual`(=) |
| `dingtalk_actioner_value_condition` | 单选匹配 | `paramKey: 控件ID`, `paramValues[]`(选项 key) |
| `dingtalk_multi_value_condition` | 多选匹配 | `paramKey: 控件ID`, `paramValues[]`, `matchType`(1=精确/2=全选/3=任一) |
| `dingtalk_actioner_cascade_component_condition` | 级联控件 | `paramValues[]`, `displayValues[]` |
| `dingtalk_actioner_boolean_condition` | 布尔值 | `boundEqual: true/false` |
| `dingtalk_rule_template` | 节假日判断 | `template`, `outVars` |
| `dingtalk_formula` | 公式 | `formula`, `formulaDisplay` |
| `dingtalk_biz_var_condition` | 业务变量 | `dsKey`, `conds[]` |
| `dingtalk_table_condition` | 明细内字段 | `parentFieldId`, `componentName`, `paramValue` |
### 范围条件操作符
| 字段 | 含义 |
|------|------|
| `lowerBound` | >= (大于等于) |
| `lowerBoundNotEqual` | > (大于) |
| `upperBoundEqual` | <= (小于等于) |
| `upperBound` | < (小于) |
| `boundEqual` | = (等于) |
### 默认分支
- `isdefault: true`
- `conditions: [[]]`(一个空的条件组)
- **必须放在 `conditionNodes[]` 的最后**
---
## create-instance 中的节点参数映射
### directAppointedApprovers(指定审批人覆盖模板流程)
当需要**不使用模板默认流程、直接指定审批人**时使用。
```json
{
"directAppointedApprovers": [
{
"staffIds": ["userId1", "userId2"],
"taskActionType": "NONE",
"staffId": ""
}
]
}
```
| 字段 | 说明 |
|------|------|
| `staffIds` | 审批人 userId 列表(通过 `dws aisearch person --keyword "<姓名>" --dimension name --format json` 获取;多结果须消歧) |
| `taskActionType` | `NONE`(单人)/ `AND`(会签)/ `OR`(或签) |
| `staffId` | 留空字符串 |
### targetSelectActioners(自选审批人)
当模板流程中存在**自选审批节点**(`target_select` 类型)时必填。
```json
{
"targetSelectActioners": [
{
"actionerKey": "manual_nodeId_xxxx_yyyy",
"actionerStaffIds": ["userId1"]
}
]
}
```
| 字段 | 说明 |
|------|------|
| `actionerKey` | 自选节点的规则 key,从审批流程节点信息接口获取 `actorKey` |
| `actionerStaffIds` | 操作人 userId 列表 |
---
## 组装优先级
1. 先用 `forecast-process` 获取模板的流程节点结构(`workflowActivityRuleVOs`)
2. 根据节点中的 `activityType` 和 `targetSelect` 判断是否需要传入 `directAppointedApprovers` 或 `targetSelectActioners`
3. 如果预测返回 `targetSelect: true` 的自选节点,`targetSelectActioners` 必填
4. 如果用户要求覆盖默认流程,使用 `directAppointedApprovers`
5. **所有 userId 必须通过 `dws aisearch person --keyword "<姓名>" --dimension name --format json` 获取,严禁填姓名;多结果须消歧**
> **交互优化:** 若用户在 `forecast-process` 前已指定审批人/抄送人姓名,`forecast-process` 返回自选节点后应自动映射,仅对未覆盖的自选节点追问,不要重复询问。详见 [oa.md](../oa.md) 交互优化原则。
+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()
+26
View File
@@ -537,6 +537,32 @@ func TestChangelogPRFastPathWorkflowContract(t *testing.T) {
t.Error("full race shards must retain enough package-level time for internal/app")
}
darwinStart := strings.Index(admission, "\n test-darwin:\n")
darwinEnd := strings.Index(admission, "\n test-windows:\n")
if darwinStart < 0 || darwinEnd <= darwinStart {
t.Fatal("Code Admission workflow missing macOS test job boundaries")
}
darwinJob := admission[darwinStart:darwinEnd]
// The macOS job no longer runs ./internal/app as a whole package, so it does
// not need the package-level race budget the Ubuntu shard gets — the Ubuntu
// "race: app" shard already covers everything except the natively-gated
// tests. Pinning the two focused commands replaces that budget assertion:
// it locks the per-step timeouts and blocks a whole-package regression.
for _, want := range []string{
`go test -v -race -count=1 -timeout=6m ./internal/keychain ./internal/auth`,
`go test -v -race -count=1 -timeout=5m ./internal/app -run '^(TestValidateNewBinary_RecoversFromUnsignedDarwin|Test(CrossPlatformCoverage)?Auth(MigrateKeychain|StatusDiagnosticReportsCiphertextKeyMismatch))'`,
} {
if !strings.Contains(darwinJob, want) {
t.Errorf("macOS native test job missing focused auth contract %q", want)
}
}
if strings.Contains(darwinJob, "./internal/keychain ./internal/auth ./internal/app") {
t.Error("macOS native test job must not repeat the complete internal/app race shard")
}
if count := strings.Count(darwinJob, "./internal/app"); count != 1 {
t.Errorf("macOS native test job internal/app invocation count = %d, want 1 focused invocation", count)
}
coverageStart := strings.Index(admission, "\n coverage:\n")
coverageEnd := strings.Index(admission, "\n policy:\n")
if coverageStart < 0 || coverageEnd <= coverageStart {
+141
View File
@@ -0,0 +1,141 @@
package scripts_test
import (
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
)
// TestMacOSNativeJobKeepsDarwinGatedAppTestsReachable couples the macOS test
// job's -run pattern to the set of darwin-gated tests in internal/app.
//
// The Ubuntu race shard runs the whole package but skips anything gated on
// runtime.GOOS != "darwin", and the platform coverage gate only runs
// ^(TestAllShortcuts|TestCrossPlatformCoverage). That makes the macOS job the
// only place a darwin-gated internal/app test can execute, so narrowing its
// -run pattern can silently orphan one — which is exactly what happened in
// #857 before review caught it.
func TestMacOSNativeJobKeepsDarwinGatedAppTestsReachable(t *testing.T) {
root, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatalf("Abs(repo root) error = %v", err)
}
pattern := macOSAppRunPattern(t, root)
matcher, err := regexp.Compile(pattern)
if err != nil {
t.Fatalf("Compile(macOS -run pattern %q) error = %v", pattern, err)
}
gated := darwinGatedTestNames(t, filepath.Join(root, "internal", "app"))
if len(gated) == 0 {
t.Fatal("found no darwin-gated tests in internal/app; the scanner is broken or the gate style changed")
}
for _, name := range gated {
if !matcher.MatchString(name) {
t.Errorf(
"darwin-gated test %s is unreachable in CI: the Ubuntu shard skips it on Linux and the macOS -run pattern %q does not select it",
name,
pattern,
)
}
}
}
// macOSAppRunPattern extracts the -run pattern the macOS job applies to
// ./internal/app from the CI workflow.
func macOSAppRunPattern(t *testing.T, root string) string {
t.Helper()
data, err := os.ReadFile(filepath.Join(root, ".github", "workflows", "ci.yml"))
if err != nil {
t.Fatalf("ReadFile(ci.yml) error = %v", err)
}
workflow := string(data)
start := strings.Index(workflow, "\n test-darwin:\n")
end := strings.Index(workflow, "\n test-windows:\n")
if start < 0 || end <= start {
t.Fatal("ci.yml missing macOS test job boundaries")
}
matches := regexp.MustCompile(`\./internal/app -run '([^']+)'`).FindAllStringSubmatch(workflow[start:end], -1)
if len(matches) != 1 {
t.Fatalf("macOS test job ./internal/app -run invocation count = %d, want exactly 1", len(matches))
}
return matches[0][1]
}
// darwinGatedTestNames returns every Test function in dir whose body gates on
// runtime.GOOS != "darwin".
func darwinGatedTestNames(t *testing.T, dir string) []string {
t.Helper()
entries, err := filepath.Glob(filepath.Join(dir, "*_test.go"))
if err != nil {
t.Fatalf("Glob(%s) error = %v", dir, err)
}
var names []string
fset := token.NewFileSet()
for _, entry := range entries {
file, parseErr := parser.ParseFile(fset, entry, nil, parser.SkipObjectResolution)
if parseErr != nil {
t.Fatalf("ParseFile(%s) error = %v", entry, parseErr)
}
for _, decl := range file.Decls {
fn, ok := decl.(*ast.FuncDecl)
if !ok || fn.Body == nil || !strings.HasPrefix(fn.Name.Name, "Test") {
continue
}
if gatesOnNonDarwin(fn.Body) {
names = append(names, fn.Name.Name)
}
}
}
return names
}
// gatesOnNonDarwin reports whether body contains a `runtime.GOOS != "darwin"`
// comparison, the idiom this repo uses to skip a test off macOS.
func gatesOnNonDarwin(body *ast.BlockStmt) bool {
found := false
ast.Inspect(body, func(node ast.Node) bool {
if found {
return false
}
binary, ok := node.(*ast.BinaryExpr)
if !ok || binary.Op != token.NEQ {
return true
}
if !isRuntimeGOOS(binary.X) {
return true
}
literal, ok := binary.Y.(*ast.BasicLit)
if !ok || literal.Kind != token.STRING {
return true
}
if literal.Value == `"darwin"` {
found = true
return false
}
return true
})
return found
}
// isRuntimeGOOS reports whether expr is the selector runtime.GOOS.
func isRuntimeGOOS(expr ast.Expr) bool {
selector, ok := expr.(*ast.SelectorExpr)
if !ok || selector.Sel.Name != "GOOS" {
return false
}
ident, ok := selector.X.(*ast.Ident)
return ok && ident.Name == "runtime"
}