Compare commits
49
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20d27f6db9 | ||
|
|
ca2b8adcb2 | ||
|
|
a3c7009f9b | ||
|
|
2d3f820f91 | ||
|
|
f5a1b64d7a | ||
|
|
c1f96241af | ||
|
|
eb571e6e73 | ||
|
|
4d5a47ac93 | ||
|
|
6108f51c9d | ||
|
|
6376f294da | ||
|
|
f48a707e04 | ||
|
|
7cb0de1f29 | ||
|
|
f8d1fb84c0 | ||
|
|
d3692e7b6e | ||
|
|
e2e855d12a | ||
|
|
31e3f6bcbc | ||
|
|
678f108adf | ||
|
|
e40f5bc537 | ||
|
|
3210232876 | ||
|
|
162a2eb0a7 | ||
|
|
d3f62193e7 | ||
|
|
1a11c687ed | ||
|
|
dfed4ba37d | ||
|
|
f26df04679 | ||
|
|
d02b03436d | ||
|
|
bc7b96ba5f | ||
|
|
9539c887f6 | ||
|
|
6607f44724 | ||
|
|
837a96fe3d | ||
|
|
fdcd44f9e3 | ||
|
|
34c0c86a59 | ||
|
|
a240ad2b81 | ||
|
|
b1f4a5d62a | ||
|
|
bf33ab622f | ||
|
|
f39a3f5417 | ||
|
|
b2cbca2762 | ||
|
|
9ce95db08e | ||
|
|
e99c20a0a1 | ||
|
|
e806e761ad | ||
|
|
a6696fe9e9 | ||
|
|
06af21c7a1 | ||
|
|
eba2b692ec | ||
|
|
96bfae079a | ||
|
|
bb18cdba3b | ||
|
|
015a1f85ca | ||
|
|
8854e0d1d4 | ||
|
|
22862508b8 | ||
|
|
5459bcc524 | ||
|
|
029c665029 |
@@ -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)
|
||||
|
||||
@@ -6,6 +6,53 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Chat media download JSON compatibility** — `dws chat message download-media --format json` once again returns a clean `{success, downloadUrl, output}` result after the file is saved, preserving the temporary URL and resolved local path without progress text corrupting JSON stdout.
|
||||
|
||||
## [1.0.56] - 2026-08-04
|
||||
|
||||
This stable release promotes the fully delivered `v1.0.56-beta.4` baseline.
|
||||
It includes PR #852's resilient multipart Drive download implementation,
|
||||
together with the v1.0.56 beta-line command, Schema, Skill, and runtime
|
||||
improvements already validated through the prerelease channel.
|
||||
|
||||
### Added
|
||||
|
||||
- **Resilient multipart Drive downloads** (#852) — `drive download` and
|
||||
`drive download-version` support parallel chunk transfer, Range probing,
|
||||
fingerprint-validated checkpoint resume, automatic 401/403 credential
|
||||
refresh, and graceful interruption with checkpoint preservation.
|
||||
|
||||
## [1.0.56-beta.4] - 2026-08-04
|
||||
|
||||
This beta adds PR #852 on top of v1.0.56-beta.3. It makes Drive downloads
|
||||
resilient for large files through parallel transfer, validated resumable
|
||||
checkpoints, and automatic credential refresh.
|
||||
|
||||
### Added
|
||||
|
||||
- **Multipart Drive downloads** (#852) — adds `--part-size`, `--parallel`, and
|
||||
`--no-resume` to `drive download` and `drive download-version`. Files above
|
||||
the part-size threshold use a Range probe and parallel chunks, resume from a
|
||||
fingerprint-validated checkpoint, refresh credentials on 401/403, and keep
|
||||
the checkpoint when Ctrl+C interrupts a transfer.
|
||||
|
||||
## [1.0.56-beta.3] - 2026-08-03
|
||||
|
||||
This beta adds PRs #846 and #851 on top of v1.0.56-beta.2. It adds a
|
||||
service-provided Aitable workflow-editing reference command and makes local
|
||||
event-bus IPC reliable on shared filesystems by placing Unix sockets in a
|
||||
validated private runtime directory.
|
||||
|
||||
### Added
|
||||
|
||||
- **Aitable workflow editing reference** (#851) — adds `dws aitable workflow edit-example`, a parameter-free read command that returns the service-provided workflow editing documentation and `workflow-dsl/v1` examples through `aitable/edit_workflow_example`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Event bus sockets on shared filesystems** (#846) — Unix event buses now place their local IPC socket in a private per-user runtime directory (`XDG_RUNTIME_DIR` when available, otherwise a `0700` per-UID directory under the system temporary directory) while retaining locks, metadata, logs, and subscription state in the configured Workdir. Listener and dial paths validate directory ownership and permissions before use. This prevents `dws event consume` from failing with `bind: errno 524` when `~/.dws` is hosted on NFS, CSI, FUSE, or another filesystem that does not support Unix Domain Sockets without exposing the socket directly in a shared `/tmp` root. When `XDG_RUNTIME_DIR` is unavailable, the per-UID directory name is deterministic: ownership validation prevents endpoint hijacking, but another local user can pre-create the directory to deny service; multi-user deployments should provide a private `XDG_RUNTIME_DIR`.
|
||||
|
||||
## [1.0.56-beta.2] - 2026-07-30
|
||||
|
||||
This beta adds PRs #831 and #835 on top of v1.0.56-beta.1. It separates
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
class DingtalkWorkspaceCliBeta < Formula
|
||||
desc "Automate DingTalk workspace tasks from the terminal (beta channel)"
|
||||
homepage "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli"
|
||||
version "1.0.56-beta.2"
|
||||
version "1.0.56-beta.4"
|
||||
license "Apache-2.0"
|
||||
keg_only "it is the beta channel and conflicts with dingtalk-workspace-cli"
|
||||
|
||||
on_macos do
|
||||
if Hardware::CPU.arm?
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.2/dws-darwin-arm64.tar.gz"
|
||||
sha256 "19b52b5427dbf24acfeb1da16a93e6edfd0443389a778abbbfd381ff8f656139"
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.4/dws-darwin-arm64.tar.gz"
|
||||
sha256 "f1f9b6394137edbd0b08d632aab34e92a0f3f81d80107a47de1bec9b384f0515"
|
||||
else
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.2/dws-darwin-amd64.tar.gz"
|
||||
sha256 "621da52d04f391234d160a0522d70c1fb2e36da59102ef201a9a3a11b706b3e8"
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.4/dws-darwin-amd64.tar.gz"
|
||||
sha256 "cd3c64d20723c420e2490405d0bf8eecfd7e2b8fc352f63f23de5847a1d38f55"
|
||||
end
|
||||
end
|
||||
|
||||
on_linux do
|
||||
if Hardware::CPU.arm?
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.2/dws-linux-arm64.tar.gz"
|
||||
sha256 "4ba956463f4b583c1727f58026a6bc1fb23d27537083e3bafd541a05ab15b48b"
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.4/dws-linux-arm64.tar.gz"
|
||||
sha256 "910918d88074534e680a2e320d3cb364ad092e96b9c422f9e75d11c9c0815dd8"
|
||||
else
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.2/dws-linux-amd64.tar.gz"
|
||||
sha256 "a8156ec5b89355faf8c08a65d9c088f89a7b411a418fb4b488f3d472efc79670"
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.4/dws-linux-amd64.tar.gz"
|
||||
sha256 "172fe0d84443be953d0c6f2c2433540e4b972fbe7776cff1417ec9c73723552b"
|
||||
end
|
||||
end
|
||||
|
||||
resource "skills" do
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.2/dws-skills.zip"
|
||||
sha256 "92c71fdeade88b3b76a00cb74ebd3223cc4110c7ee151d36d782537613129967"
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.56-beta.4/dws-skills.zip"
|
||||
sha256 "a3457befe858cbf3fe85848428b630bfd3a5f626256ed6b49415267948915152"
|
||||
end
|
||||
|
||||
def install
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -37075,6 +37075,265 @@
|
||||
"用户明确要求停止某自动化工作流时"
|
||||
]
|
||||
},
|
||||
"aitable workflow edit-example": {
|
||||
"agent_summary": "获取 AI 表格工作流编辑文档与 workflow-dsl/v1 示例。",
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"实际创建工作流用 workflow create;修改已有工作流用 workflow update;查询已发布定义用 workflow get"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "read",
|
||||
"effect_source": "agent-hint",
|
||||
"examples": [
|
||||
"dws aitable workflow edit-example"
|
||||
],
|
||||
"field_provenance": {
|
||||
"agent_summary": {
|
||||
"value": "获取 AI 表格工作流编辑文档与 workflow-dsl/v1 示例。",
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "获取 AI 表格工作流编辑文档与 workflow-dsl/v1 示例。",
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"availability": {
|
||||
"value": "available",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "available",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry."
|
||||
}
|
||||
]
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"实际创建工作流用 workflow create;修改已有工作流用 workflow update;查询已发布定义用 workflow get"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"实际创建工作流用 workflow create;修改已有工作流用 workflow update;查询已发布定义用 workflow get"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"confirmation": {
|
||||
"value": "not_required",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "not_required",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry."
|
||||
}
|
||||
]
|
||||
},
|
||||
"effect": {
|
||||
"value": "read",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "read",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry."
|
||||
}
|
||||
]
|
||||
},
|
||||
"examples": {
|
||||
"value": [
|
||||
"dws aitable workflow edit-example"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"dws aitable workflow edit-example"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"idempotency": {
|
||||
"value": "idempotent",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "idempotent",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry."
|
||||
}
|
||||
]
|
||||
},
|
||||
"interface_mode": {
|
||||
"value": "composite",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "composite",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry."
|
||||
}
|
||||
]
|
||||
},
|
||||
"interface_reason": {
|
||||
"value": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry."
|
||||
}
|
||||
]
|
||||
},
|
||||
"interface_ref": {
|
||||
"value": null,
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "interface_disposition_matrix",
|
||||
"review_reason": "final interface mode composite forbids a direct MCP interface_ref",
|
||||
"candidates": [
|
||||
{
|
||||
"value": null,
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "final interface mode composite forbids a direct MCP interface_ref"
|
||||
}
|
||||
]
|
||||
},
|
||||
"reviewed": {
|
||||
"value": true,
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": true,
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry."
|
||||
},
|
||||
{
|
||||
"value": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": false,
|
||||
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"risk": {
|
||||
"value": "low",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "low",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry."
|
||||
}
|
||||
]
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"创建或更新工作流前,需要确认最新 workflow-dsl/v1 结构、节点写法或完整示例时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"创建或更新工作流前,需要确认最新 workflow-dsl/v1 结构、节点写法或完整示例时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"idempotency": "idempotent",
|
||||
"interface_mode": "composite",
|
||||
"interface_reason": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
|
||||
"reviewed": true,
|
||||
"risk": "low",
|
||||
"source_refs": [
|
||||
"cobra-help:dws aitable workflow edit-example --help",
|
||||
"internal/cli/schema_command_registry.json#aitable.workflow_edit_example",
|
||||
"internal/cli/schema_hints/metadata/aitable.json",
|
||||
"internal/cli/schema_hints/selection/aitable.json",
|
||||
"mcp-contract:aitable/edit_workflow_example",
|
||||
"skills/mono/references/products/aitable/aitable-workflow.md"
|
||||
],
|
||||
"use_when": [
|
||||
"创建或更新工作流前,需要确认最新 workflow-dsl/v1 结构、节点写法或完整示例时"
|
||||
]
|
||||
},
|
||||
"aitable workflow enable": {
|
||||
"agent_summary": "启用工作流。",
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"version": 1,
|
||||
"source_hash": "sha256:670ca810a83bf2aa6f387a18c3af746393d5ea9de24570cbcebbcf994eb7b613",
|
||||
"surface_hash": "sha256:60eee8e2f37d6d9d60689efce85082798eb9ad38b7ba7c0b471c3de676a85a16",
|
||||
"source_hash": "sha256:9a6bc544f78424a85d7465cbb8ce20ee80c954de7ec3f7657eb42b05679d663d",
|
||||
"surface_hash": "sha256:beeddac7cd934e409e47e5b4552dbc188840ad46205d8d48340569a97c51b59b",
|
||||
"coverage": {
|
||||
"surface_products": 26,
|
||||
"products_with_metadata": 26,
|
||||
"surface_tools": 845,
|
||||
"tools_with_metadata": 845,
|
||||
"tools_with_agent_summary": 845,
|
||||
"tools_with_use_when": 845,
|
||||
"tools_with_avoid_when": 845,
|
||||
"tools_with_examples": 845,
|
||||
"tools_with_interface_mode": 845,
|
||||
"surface_tools": 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
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
{
|
||||
"version": 1,
|
||||
"source_hash": "sha256:670ca810a83bf2aa6f387a18c3af746393d5ea9de24570cbcebbcf994eb7b613",
|
||||
"surface_hash": "sha256:60eee8e2f37d6d9d60689efce85082798eb9ad38b7ba7c0b471c3de676a85a16",
|
||||
"source_files": 160,
|
||||
"source_hash": "sha256:9a6bc544f78424a85d7465cbb8ce20ee80c954de7ec3f7657eb42b05679d663d",
|
||||
"surface_hash": "sha256:beeddac7cd934e409e47e5b4552dbc188840ad46205d8d48340569a97c51b59b",
|
||||
"source_files": 162,
|
||||
"hint_files": 54,
|
||||
"hint_products": 43,
|
||||
"hint_tools": 1842,
|
||||
"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": 845,
|
||||
"tools_with_metadata": 845,
|
||||
"tools_with_agent_summary": 845,
|
||||
"tools_with_use_when": 845,
|
||||
"tools_with_avoid_when": 845,
|
||||
"tools_with_examples": 845,
|
||||
"tools_with_interface_mode": 845,
|
||||
"surface_tools": 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",
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"version": 1,
|
||||
"surface_hash": "sha256:60eee8e2f37d6d9d60689efce85082798eb9ad38b7ba7c0b471c3de676a85a16",
|
||||
"source_hash": "sha256:862734fb976a4c9c78d010b7391c74822f37805f5acc650551f4ac60f701c13a",
|
||||
"surface_hash": "sha256:beeddac7cd934e409e47e5b4552dbc188840ad46205d8d48340569a97c51b59b",
|
||||
"source_hash": "sha256:6fe48c19b11a2d917fad3d59c96b40bf1b7620ed4334d96cef6544dbe3742e30",
|
||||
"catalog": {
|
||||
"agent_metadata": {
|
||||
"products_with_metadata": 26,
|
||||
"source": "embedded-skill-metadata",
|
||||
"source_hash": "sha256:670ca810a83bf2aa6f387a18c3af746393d5ea9de24570cbcebbcf994eb7b613",
|
||||
"surface_hash": "sha256:60eee8e2f37d6d9d60689efce85082798eb9ad38b7ba7c0b471c3de676a85a16",
|
||||
"source_hash": "sha256:9a6bc544f78424a85d7465cbb8ce20ee80c954de7ec3f7657eb42b05679d663d",
|
||||
"surface_hash": "sha256:beeddac7cd934e409e47e5b4552dbc188840ad46205d8d48340569a97c51b59b",
|
||||
"surface_products": 26,
|
||||
"surface_tools": 845,
|
||||
"tools_with_agent_summary": 845,
|
||||
"tools_with_metadata": 845,
|
||||
"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",
|
||||
@@ -288,7 +288,7 @@
|
||||
"id": "aitable",
|
||||
"name": "AI 表格操作",
|
||||
"runtime": true,
|
||||
"tool_count": 145,
|
||||
"tool_count": 146,
|
||||
"tools": [
|
||||
{
|
||||
"agent_metadata_source": "embedded-skill-metadata",
|
||||
@@ -4449,6 +4449,33 @@
|
||||
"用户明确要求停止某自动化工作流时"
|
||||
]
|
||||
},
|
||||
{
|
||||
"agent_metadata_source": "embedded-skill-metadata",
|
||||
"agent_summary": "获取 AI 表格工作流编辑文档与 workflow-dsl/v1 示例。",
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"实际创建工作流用 workflow create;修改已有工作流用 workflow update;查询已发布定义用 workflow get"
|
||||
],
|
||||
"canonical_path": "aitable.workflow_edit_example",
|
||||
"cli_name": "edit-example",
|
||||
"cli_path": "aitable workflow edit-example",
|
||||
"confirmation": "not_required",
|
||||
"description": "返回服务端提供的 AI 表格工作流编辑文档与示例。\n可作为 workflow create / workflow update 的 workflow-dsl/v1 结构参考;此命令不需要 Base ID 或其他参数。",
|
||||
"effect": "read",
|
||||
"group": "workflow",
|
||||
"idempotency": "idempotent",
|
||||
"interface_mode": "composite",
|
||||
"interface_reason": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
|
||||
"name": "workflow_edit_example",
|
||||
"primary_cli_path": "aitable workflow edit-example",
|
||||
"reviewed": true,
|
||||
"risk": "low",
|
||||
"title": "获取工作流编辑文档与示例",
|
||||
"use_when": [
|
||||
"创建或更新工作流前,需要确认最新 workflow-dsl/v1 结构、节点写法或完整示例时"
|
||||
]
|
||||
},
|
||||
{
|
||||
"agent_metadata_source": "embedded-skill-metadata",
|
||||
"agent_summary": "启用工作流。",
|
||||
@@ -21090,7 +21117,7 @@
|
||||
"id": "oa",
|
||||
"name": "OA 审批 / 同意 / 拒绝 / 撤销",
|
||||
"runtime": true,
|
||||
"tool_count": 22,
|
||||
"tool_count": 25,
|
||||
"tools": [
|
||||
{
|
||||
"agent_metadata_source": "embedded-skill-metadata",
|
||||
@@ -21156,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": "获取员工已处理任务列表",
|
||||
@@ -21284,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": "获取已提交实例列表",
|
||||
@@ -21752,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": [
|
||||
@@ -26080,7 +26200,7 @@
|
||||
"avoid_when": [
|
||||
"需要编辑在线文档正文时使用 doc;只管理钉盘普通文件时使用 drive"
|
||||
],
|
||||
"description": "知识库 / 空间管理 / 节点管理 / 成员管理",
|
||||
"description": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
|
||||
"field_provenance": {
|
||||
"agent_summary": {
|
||||
"candidates": [
|
||||
@@ -26134,9 +26254,9 @@
|
||||
}
|
||||
},
|
||||
"id": "wiki",
|
||||
"name": "知识库 / 空间管理 / 节点管理 / 成员管理",
|
||||
"name": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
|
||||
"runtime": true,
|
||||
"tool_count": 16,
|
||||
"tool_count": 17,
|
||||
"tools": [
|
||||
{
|
||||
"agent_metadata_source": "embedded-skill-metadata",
|
||||
@@ -26423,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": "将知识库中的节点复制到指定位置",
|
||||
@@ -26644,6 +26797,6 @@
|
||||
}
|
||||
],
|
||||
"source": "embedded-command-catalog",
|
||||
"tool_count": 845
|
||||
"tool_count": 850
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97777,6 +97777,326 @@
|
||||
"用户明确要求停止某自动化工作流时"
|
||||
]
|
||||
},
|
||||
"aitable.workflow_edit_example": {
|
||||
"agent_metadata_source": "embedded-skill-metadata",
|
||||
"agent_source_refs": [
|
||||
"cobra-help:dws aitable workflow edit-example --help",
|
||||
"internal/cli/schema_command_registry.json#aitable.workflow_edit_example",
|
||||
"internal/cli/schema_hints/metadata/aitable.json",
|
||||
"internal/cli/schema_hints/selection/aitable.json",
|
||||
"mcp-contract:aitable/edit_workflow_example",
|
||||
"skills/mono/references/products/aitable/aitable-workflow.md"
|
||||
],
|
||||
"agent_summary": "获取 AI 表格工作流编辑文档与 workflow-dsl/v1 示例。",
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"实际创建工作流用 workflow create;修改已有工作流用 workflow update;查询已发布定义用 workflow get"
|
||||
],
|
||||
"canonical_path": "aitable.workflow_edit_example",
|
||||
"cli_name": "edit-example",
|
||||
"cli_path": "aitable workflow edit-example",
|
||||
"confirmation": "not_required",
|
||||
"description": "返回服务端提供的 AI 表格工作流编辑文档与示例。\n可作为 workflow create / workflow update 的 workflow-dsl/v1 结构参考;此命令不需要 Base ID 或其他参数。",
|
||||
"display": "AI 表格操作",
|
||||
"effect": "read",
|
||||
"effect_source": "agent-hint",
|
||||
"examples": [
|
||||
"dws aitable workflow edit-example"
|
||||
],
|
||||
"field_provenance": {
|
||||
"agent_summary": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": "获取 AI 表格工作流编辑文档与 workflow-dsl/v1 示例。"
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": "获取 AI 表格工作流编辑文档与 workflow-dsl/v1 示例。"
|
||||
},
|
||||
"availability": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"value": "available"
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"value": "available"
|
||||
},
|
||||
"avoid_when": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"实际创建工作流用 workflow create;修改已有工作流用 workflow update;查询已发布定义用 workflow get"
|
||||
]
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"实际创建工作流用 workflow create;修改已有工作流用 workflow update;查询已发布定义用 workflow get"
|
||||
]
|
||||
},
|
||||
"canonical_path": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "command_registry",
|
||||
"selected": true,
|
||||
"source": "reviewed_command_registry",
|
||||
"source_ref": "aitable workflow edit-example",
|
||||
"value": "aitable.workflow_edit_example"
|
||||
}
|
||||
],
|
||||
"precedence": "command_registry",
|
||||
"resolution": "registry_identity",
|
||||
"source": "reviewed_command_registry",
|
||||
"source_ref": "aitable workflow edit-example",
|
||||
"value": "aitable.workflow_edit_example"
|
||||
},
|
||||
"confirmation": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"value": "not_required"
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"value": "not_required"
|
||||
},
|
||||
"description": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_help",
|
||||
"selected": true,
|
||||
"source": "cobra_help",
|
||||
"value": "返回服务端提供的 AI 表格工作流编辑文档与示例。\n可作为 workflow create / workflow update 的 workflow-dsl/v1 结构参考;此命令不需要 Base ID 或其他参数。"
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_help",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_help",
|
||||
"value": "返回服务端提供的 AI 表格工作流编辑文档与示例。\n可作为 workflow create / workflow update 的 workflow-dsl/v1 结构参考;此命令不需要 Base ID 或其他参数。"
|
||||
},
|
||||
"effect": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"value": "read"
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"value": "read"
|
||||
},
|
||||
"examples": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"dws aitable workflow edit-example"
|
||||
]
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"dws aitable workflow edit-example"
|
||||
]
|
||||
},
|
||||
"idempotency": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"value": "idempotent"
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"value": "idempotent"
|
||||
},
|
||||
"interface_mode": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"value": "composite"
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"value": "composite"
|
||||
},
|
||||
"interface_reason": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"value": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command."
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"value": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command."
|
||||
},
|
||||
"interface_ref": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "final interface mode composite forbids a direct MCP interface_ref",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"value": null
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "interface_disposition_matrix",
|
||||
"review_reason": "final interface mode composite forbids a direct MCP interface_ref",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"value": null
|
||||
},
|
||||
"reviewed": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
|
||||
"selected": false,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": true
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"value": true
|
||||
},
|
||||
"risk": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"value": "low"
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"value": "low"
|
||||
},
|
||||
"title": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_help",
|
||||
"selected": true,
|
||||
"source": "cobra_help",
|
||||
"value": "获取工作流编辑文档与示例"
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_help",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_help",
|
||||
"value": "获取工作流编辑文档与示例"
|
||||
},
|
||||
"use_when": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"创建或更新工作流前,需要确认最新 workflow-dsl/v1 结构、节点写法或完整示例时"
|
||||
]
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"value": [
|
||||
"创建或更新工作流前,需要确认最新 workflow-dsl/v1 结构、节点写法或完整示例时"
|
||||
]
|
||||
}
|
||||
},
|
||||
"group": "workflow",
|
||||
"has_parameters": false,
|
||||
"idempotency": "idempotent",
|
||||
"interface_mode": "composite",
|
||||
"interface_reason": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
|
||||
"is_alias": false,
|
||||
"name": "workflow_edit_example",
|
||||
"parameter_count": 0,
|
||||
"parameters": {},
|
||||
"path": "aitable.workflow_edit_example",
|
||||
"primary_cli_path": "aitable workflow edit-example",
|
||||
"product_id": "aitable",
|
||||
"reviewed": true,
|
||||
"risk": "low",
|
||||
"source": "reviewed_command_registry",
|
||||
"title": "获取工作流编辑文档与示例",
|
||||
"use_when": [
|
||||
"创建或更新工作流前,需要确认最新 workflow-dsl/v1 结构、节点写法或完整示例时"
|
||||
]
|
||||
},
|
||||
"aitable.workflow_enable": {
|
||||
"agent_metadata_source": "embedded-skill-metadata",
|
||||
"agent_source_refs": [
|
||||
|
||||
@@ -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
@@ -28,7 +28,7 @@
|
||||
"cli_path": "wiki member add",
|
||||
"confirmation": "not_required",
|
||||
"description": "为指定知识库添加一个或多个成员,并授予指定角色。\n\n通过 --users 传入逗号分隔的 userId 列表,多个用户将被授予同一角色。\n\n支持的角色 (--role)(必须大写):\n MANAGER 管理员,可读写、管理成员\n EDITOR 编辑者,可查看、编辑、上传内容\n DOWNLOADER 查看下载者,可查看并下载内容\n READER 仅可查看者,仅可查看,不可下载\n\n注意:\n- OWNER 角色不可通过此接口添加,知识库创建者默认为所有者。\n- 操作者需具备知识库的 OWNER 或 MANAGER 权限。\n- 单次请求最多 30 个成员,超出请分批调用。\n\n支持通过 --workspace 传入知识库 ID 或知识库 URL,系统自动识别。\n用户 uid 可通过「钉钉通讯录」相关命令检索,如:\n dws contact user search --keyword \"姓名\"",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
|
||||
"effect": "write",
|
||||
"effect_source": "command-verb",
|
||||
"examples": [
|
||||
@@ -732,7 +732,7 @@
|
||||
"cli_path": "wiki node create",
|
||||
"confirmation": "not_required",
|
||||
"description": "在指定知识库中创建文档、文件夹或其他类型的节点。\n\n通过 --type 指定节点类型(服务端支持以下值,asheet 不被支持):\n adoc 在线文档 (默认)\n axls 在线电子表格\n able 多维表\n appt 在线演示\n adraw 白板/画板\n amind 脑图\n folder 文件夹\n\n通过 --folder 指定父节点,不传则创建在知识库根目录。",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
|
||||
"effect": "write",
|
||||
"effect_source": "command-verb",
|
||||
"examples": [
|
||||
@@ -1501,7 +1501,7 @@
|
||||
"cli_path": "wiki space create",
|
||||
"confirmation": "not_required",
|
||||
"description": "创建一个新的钉钉文档知识库(WikiSpace)。\n\n创建成功后返回新知识库的 workspaceId,可用于后续在该知识库下创建文档或遍历文件。\n操作受权限控制,仅当调用者具备在当前组织内创建知识库的权限时可成功创建。",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
|
||||
"effect": "write",
|
||||
"effect_source": "command-verb",
|
||||
"examples": [
|
||||
@@ -2149,7 +2149,7 @@
|
||||
"cli_path": "wiki node delete",
|
||||
"confirmation": "user_required",
|
||||
"description": "将知识库中的节点移入回收站。\n\n注意: 这是一个危险操作。执行前需要确认,或传入 --yes 跳过确认。\n删除后节点会进入回收站,有保留期限可恢复。\n\n权限要求: 对节点有\"管理\"权限。",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
|
||||
"effect": "destructive",
|
||||
"effect_source": "agent-hint",
|
||||
"examples": [
|
||||
@@ -2737,7 +2737,7 @@
|
||||
"cli_path": "wiki space delete",
|
||||
"confirmation": "user_required",
|
||||
"description": "将指定知识库移入回收站。\n\n删除后知识库会进入回收站,可在回收站中恢复(有保留期限)。\n支持传入知识库 ID 或知识库 URL,系统自动识别。\n知识库 URL 格式:https://alidocs.dingtalk.com/i/spaces/{workspaceId}/overview\n\n注意:\n- 操作者必须具备知识库的 OWNER 角色。\n- 这是一个危险操作,执行前请确认。",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
|
||||
"effect": "destructive",
|
||||
"effect_source": "agent-hint",
|
||||
"examples": [
|
||||
@@ -3219,7 +3219,7 @@
|
||||
"cli_path": "wiki space get",
|
||||
"confirmation": "not_required",
|
||||
"description": "获取指定知识库的详细信息,包括名称、描述、创建者、创建时间、成员数量等。\n\n支持传入知识库 ID 或知识库 URL,系统自动识别。\n知识库 URL 格式:https://alidocs.dingtalk.com/i/spaces/{workspaceId}/overview",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
|
||||
"effect": "read",
|
||||
"effect_source": "command-verb",
|
||||
"examples": [
|
||||
@@ -3676,7 +3676,7 @@
|
||||
"cli_path": "wiki member list",
|
||||
"confirmation": "not_required",
|
||||
"description": "查询指定知识库的成员列表,返回每位成员的 userId、姓名、角色等信息。\n\n注意:底层不支持游标分页,--limit 仅控制单次返回的最大条数(最大 200)。\n若结果被截断(出参 truncated=true),可通过 --filter-role 收窄查询范围;\nORG 类型授权不会出现在查询结果中。",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
|
||||
"effect": "read",
|
||||
"effect_source": "command-verb",
|
||||
"examples": [
|
||||
@@ -4379,7 +4379,7 @@
|
||||
"cli_path": "wiki node list",
|
||||
"confirmation": "not_required",
|
||||
"description": "列出指定知识库下的直接子节点(文档、文件夹、表格等)。\n\n通过 --folder 指定父节点可列出子目录内容;不传 --folder 则列出知识库根目录。\n支持分页,通过 --cursor 传入上次返回的 pageToken 获取下一页。",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
|
||||
"effect": "read",
|
||||
"effect_source": "command-verb",
|
||||
"examples": [
|
||||
@@ -5163,7 +5163,7 @@
|
||||
"cli_path": "wiki space list",
|
||||
"confirmation": "not_required",
|
||||
"description": "获取当前用户有权访问的空间列表。统一管理两种空间类型。\n\n通过 --type 参数控制返回范围:\n orgWikiSpace — 组织知识库列表(默认,支持分页)\n myWikiSpace — 当前用户的「我的文档」个人空间(固定 1 条)\n orgSpace — 钉盘企业空间(团队文件)列表\n mySpace — 钉盘「我的文件」个人空间",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
|
||||
"effect": "read",
|
||||
"effect_source": "command-verb",
|
||||
"examples": [
|
||||
@@ -5796,6 +5796,793 @@
|
||||
"需要 workspaceId / rootFolderId 作为后续 node/drive 操作前置时"
|
||||
]
|
||||
},
|
||||
"wiki.list_workspace_feeds": {
|
||||
"agent_metadata_source": "embedded-skill-metadata",
|
||||
"agent_source_refs": [
|
||||
"CommandRegistry:canonical_path=wiki.list_workspace_feeds",
|
||||
"Skill:skills/mono/references/products/wiki.md",
|
||||
"cobra-help:dws wiki feed list",
|
||||
"internal/cli/schema_hints/metadata/wiki.json",
|
||||
"internal/cli/schema_hints/selection/wiki.json",
|
||||
"internal/cli/schema_mcp_metadata.json#tools.wiki.list_workspace_feeds",
|
||||
"live-mcp-tools-list:wiki.list_workspace_feeds",
|
||||
"skills/mono/references/products/wiki.md"
|
||||
],
|
||||
"agent_summary": "查询知识库的活动动态:谁在什么时间更新/上传/评论了哪些文档",
|
||||
"agent_summary_source": "dws-agent-selection/wiki",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"要看知识库当前有哪些节点用 node list;库内按关键词找文档用 node search",
|
||||
"要读某篇文档正文用 doc read;跨库全局找文件用 drive search"
|
||||
],
|
||||
"canonical_path": "wiki.list_workspace_feeds",
|
||||
"cli_name": "list",
|
||||
"cli_path": "wiki feed list",
|
||||
"confirmation": "not_required",
|
||||
"description": "查询指定知识库的动态列表,返回动态类型、时间、内容摘要等信息。\n\n通过 --workspace 指定知识库,支持传入知识库 ID 或知识库 URL。\n支持分页,通过 --cursor 传入上次返回的 nextToken 获取下一页。\n\n权限要求:调用者需具备知识库的成员权限,非成员会被拒绝访问。",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
|
||||
"effect": "read",
|
||||
"effect_source": "command-verb",
|
||||
"examples": [
|
||||
"dws wiki feed list --workspace \u003cworkspaceId\u003e --format json",
|
||||
"dws wiki feed list --workspace \u003cworkspaceId\u003e --limit 10 --format json"
|
||||
],
|
||||
"field_provenance": {
|
||||
"agent_summary": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工审阅:依据 wiki MCP 实时 tools/list 的 list_workspace_feeds description/inputSchema 与 Cobra Long 手写选型文案,并与 node list / node search / doc read / drive search 划清路由边界;不改变命令身份、参数契约或接口绑定。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/wiki.json",
|
||||
"value": "查询知识库的活动动态:谁在什么时间更新/上传/评论了哪些文档"
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:依据 wiki MCP 实时 tools/list 的 list_workspace_feeds description/inputSchema 与 Cobra Long 手写选型文案,并与 node list / node search / doc read / drive search 划清路由边界;不改变命令身份、参数契约或接口绑定。",
|
||||
"source": "internal/cli/schema_hints/selection/wiki.json",
|
||||
"value": "查询知识库的活动动态:谁在什么时间更新/上传/评论了哪些文档"
|
||||
},
|
||||
"availability": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "Live wiki MCP tools/list exposes list_workspace_feeds as a read-only knowledge base activity query. The reviewed CLI leaf forwards workspaceId/maxResults/nextToken/excludeFile to the native wiki server and adds no side effects, so it keeps the same read-only posture as the sibling wiki list commands.",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/metadata/wiki.json",
|
||||
"value": "available"
|
||||
},
|
||||
{
|
||||
"precedence": "mcp_fallback",
|
||||
"selected": false,
|
||||
"source": "internal/cli/schema_mcp_metadata.json#tools.wiki.list_workspace_feeds.interface_ref",
|
||||
"value": "available"
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Live wiki MCP tools/list exposes list_workspace_feeds as a read-only knowledge base activity query. The reviewed CLI leaf forwards workspaceId/maxResults/nextToken/excludeFile to the native wiki server and adds no side effects, so it keeps the same read-only posture as the sibling wiki list commands.",
|
||||
"source": "internal/cli/schema_hints/metadata/wiki.json",
|
||||
"value": "available"
|
||||
},
|
||||
"avoid_when": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工审阅:依据 wiki MCP 实时 tools/list 的 list_workspace_feeds description/inputSchema 与 Cobra Long 手写选型文案,并与 node list / node search / doc read / drive search 划清路由边界;不改变命令身份、参数契约或接口绑定。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/wiki.json",
|
||||
"value": [
|
||||
"要看知识库当前有哪些节点用 node list;库内按关键词找文档用 node search",
|
||||
"要读某篇文档正文用 doc read;跨库全局找文件用 drive search"
|
||||
]
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:依据 wiki MCP 实时 tools/list 的 list_workspace_feeds description/inputSchema 与 Cobra Long 手写选型文案,并与 node list / node search / doc read / drive search 划清路由边界;不改变命令身份、参数契约或接口绑定。",
|
||||
"source": "internal/cli/schema_hints/selection/wiki.json",
|
||||
"value": [
|
||||
"要看知识库当前有哪些节点用 node list;库内按关键词找文档用 node search",
|
||||
"要读某篇文档正文用 doc read;跨库全局找文件用 drive search"
|
||||
]
|
||||
},
|
||||
"canonical_path": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "command_registry",
|
||||
"selected": true,
|
||||
"source": "reviewed_command_registry",
|
||||
"source_ref": "wiki feed list",
|
||||
"value": "wiki.list_workspace_feeds"
|
||||
}
|
||||
],
|
||||
"precedence": "command_registry",
|
||||
"resolution": "registry_identity",
|
||||
"source": "reviewed_command_registry",
|
||||
"source_ref": "wiki feed list",
|
||||
"value": "wiki.list_workspace_feeds"
|
||||
},
|
||||
"confirmation": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "inference_or_default",
|
||||
"selected": true,
|
||||
"source": "risk-default",
|
||||
"value": "not_required"
|
||||
}
|
||||
],
|
||||
"precedence": "inference_or_default",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "risk-default",
|
||||
"value": "not_required"
|
||||
},
|
||||
"description": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_help",
|
||||
"selected": true,
|
||||
"source": "cobra_help",
|
||||
"value": "查询指定知识库的动态列表,返回动态类型、时间、内容摘要等信息。\n\n通过 --workspace 指定知识库,支持传入知识库 ID 或知识库 URL。\n支持分页,通过 --cursor 传入上次返回的 nextToken 获取下一页。\n\n权限要求:调用者需具备知识库的成员权限,非成员会被拒绝访问。"
|
||||
},
|
||||
{
|
||||
"precedence": "mcp_metadata",
|
||||
"selected": false,
|
||||
"source": "mcp_metadata",
|
||||
"value": "功能概述:\r\n查询指定钉钉文档知识库的活动动态,包括谁在什么时间对哪些文档进行了更新、上传、评论等操作。通过传入 workspaceId(知识库 ID 或知识库 URL)定位目标知识库,返回动态列表,每条包含动态类型(type)、发生时间(time)和内容摘要(content)。支持游标分页(nextToken),可选排除文件相关动态(excludeFile)。\r\n\r\n适用场景:\r\n– 需要了解知识库最近有哪些文档被更新或上传了新文件。\r\n– 需要追踪知识库内的协作活动,如谁评论了哪篇文档。\r\n– 定期巡检知识库变更情况,生成动态摘要报告。\r\n\r\n注意事项:\r\n– 权限要求:操作者需具备知识库的成员权限,非成员会被拒绝访问。\r\n– 通过 workspaceId 定位知识库,支持知识库 ID(纯字符串)或知识库 URL(如 https://alidocs.dingtalk.com/i/spaces/{workspaceId}/overview),系统自动提取。\r\n– 每页默认返回 20 条动态,通过 maxResults 调整,最大 50 条。首页不传 nextToken,翻页时传入上次返回的 nextToken 值。\r\n– excludeFile 设为 true 可过滤掉文件相关的动态,默认 false 即返回全部动态类型。"
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_help",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_help",
|
||||
"value": "查询指定知识库的动态列表,返回动态类型、时间、内容摘要等信息。\n\n通过 --workspace 指定知识库,支持传入知识库 ID 或知识库 URL。\n支持分页,通过 --cursor 传入上次返回的 nextToken 获取下一页。\n\n权限要求:调用者需具备知识库的成员权限,非成员会被拒绝访问。"
|
||||
},
|
||||
"effect": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "inference_or_default",
|
||||
"selected": true,
|
||||
"source": "command-verb",
|
||||
"value": "read"
|
||||
}
|
||||
],
|
||||
"precedence": "inference_or_default",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "command-verb",
|
||||
"value": "read"
|
||||
},
|
||||
"examples": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工审阅:依据 wiki MCP 实时 tools/list 的 list_workspace_feeds description/inputSchema 与 Cobra Long 手写选型文案,并与 node list / node search / doc read / drive search 划清路由边界;不改变命令身份、参数契约或接口绑定。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/wiki.json",
|
||||
"value": [
|
||||
"dws wiki feed list --workspace \u003cworkspaceId\u003e --format json",
|
||||
"dws wiki feed list --workspace \u003cworkspaceId\u003e --limit 10 --format json"
|
||||
]
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:依据 wiki MCP 实时 tools/list 的 list_workspace_feeds description/inputSchema 与 Cobra Long 手写选型文案,并与 node list / node search / doc read / drive search 划清路由边界;不改变命令身份、参数契约或接口绑定。",
|
||||
"source": "internal/cli/schema_hints/selection/wiki.json",
|
||||
"value": [
|
||||
"dws wiki feed list --workspace \u003cworkspaceId\u003e --format json",
|
||||
"dws wiki feed list --workspace \u003cworkspaceId\u003e --limit 10 --format json"
|
||||
]
|
||||
},
|
||||
"idempotency": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "inference_or_default",
|
||||
"selected": true,
|
||||
"source": "effect-default",
|
||||
"value": "idempotent"
|
||||
}
|
||||
],
|
||||
"precedence": "inference_or_default",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "effect-default",
|
||||
"value": "idempotent"
|
||||
},
|
||||
"interface_mode": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "Live wiki MCP tools/list exposes list_workspace_feeds as a read-only knowledge base activity query. The reviewed CLI leaf forwards workspaceId/maxResults/nextToken/excludeFile to the native wiki server and adds no side effects, so it keeps the same read-only posture as the sibling wiki list commands.",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/metadata/wiki.json",
|
||||
"value": "mcp"
|
||||
},
|
||||
{
|
||||
"precedence": "mcp_fallback",
|
||||
"selected": false,
|
||||
"source": "internal/cli/schema_mcp_metadata.json#tools.wiki.list_workspace_feeds.interface_ref",
|
||||
"value": "mcp"
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Live wiki MCP tools/list exposes list_workspace_feeds as a read-only knowledge base activity query. The reviewed CLI leaf forwards workspaceId/maxResults/nextToken/excludeFile to the native wiki server and adds no side effects, so it keeps the same read-only posture as the sibling wiki list commands.",
|
||||
"source": "internal/cli/schema_hints/metadata/wiki.json",
|
||||
"value": "mcp"
|
||||
},
|
||||
"interface_ref": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "mcp_fallback",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_mcp_metadata.json#tools.wiki.list_workspace_feeds.interface_ref",
|
||||
"value": {
|
||||
"product_id": "wiki",
|
||||
"rpc_name": "list_workspace_feeds"
|
||||
}
|
||||
}
|
||||
],
|
||||
"precedence": "mcp_fallback",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "internal/cli/schema_mcp_metadata.json#tools.wiki.list_workspace_feeds.interface_ref",
|
||||
"value": {
|
||||
"product_id": "wiki",
|
||||
"rpc_name": "list_workspace_feeds"
|
||||
}
|
||||
},
|
||||
"metadata_source": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "derived_resolution",
|
||||
"selected": true,
|
||||
"source": "metadata_source_resolution",
|
||||
"value": "embedded-mcp-metadata"
|
||||
}
|
||||
],
|
||||
"precedence": "derived_resolution",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "metadata_source_resolution",
|
||||
"value": "embedded-mcp-metadata"
|
||||
},
|
||||
"reviewed": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "Live wiki MCP tools/list exposes list_workspace_feeds as a read-only knowledge base activity query. The reviewed CLI leaf forwards workspaceId/maxResults/nextToken/excludeFile to the native wiki server and adds no side effects, so it keeps the same read-only posture as the sibling wiki list commands.",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/metadata/wiki.json",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工审阅:依据 wiki MCP 实时 tools/list 的 list_workspace_feeds description/inputSchema 与 Cobra Long 手写选型文案,并与 node list / node search / doc read / drive search 划清路由边界;不改变命令身份、参数契约或接口绑定。",
|
||||
"selected": false,
|
||||
"source": "internal/cli/schema_hints/selection/wiki.json",
|
||||
"value": true
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Live wiki MCP tools/list exposes list_workspace_feeds as a read-only knowledge base activity query. The reviewed CLI leaf forwards workspaceId/maxResults/nextToken/excludeFile to the native wiki server and adds no side effects, so it keeps the same read-only posture as the sibling wiki list commands.",
|
||||
"source": "internal/cli/schema_hints/metadata/wiki.json",
|
||||
"value": true
|
||||
},
|
||||
"risk": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "inference_or_default",
|
||||
"selected": true,
|
||||
"source": "effect-default",
|
||||
"value": "low"
|
||||
}
|
||||
],
|
||||
"precedence": "inference_or_default",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "effect-default",
|
||||
"value": "low"
|
||||
},
|
||||
"title": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_help",
|
||||
"selected": true,
|
||||
"source": "cobra_help",
|
||||
"value": "查询知识库动态列表"
|
||||
},
|
||||
{
|
||||
"precedence": "mcp_metadata",
|
||||
"selected": false,
|
||||
"source": "mcp_metadata",
|
||||
"value": "list_workspace_feeds"
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_help",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_help",
|
||||
"value": "查询知识库动态列表"
|
||||
},
|
||||
"use_when": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工审阅:依据 wiki MCP 实时 tools/list 的 list_workspace_feeds description/inputSchema 与 Cobra Long 手写选型文案,并与 node list / node search / doc read / drive search 划清路由边界;不改变命令身份、参数契约或接口绑定。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/wiki.json",
|
||||
"value": [
|
||||
"用户问某个知识库最近有什么更新、谁改了什么、有哪些评论等协作动态时",
|
||||
"需要巡检知识库变更并按时间线汇总成动态摘要时"
|
||||
]
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:依据 wiki MCP 实时 tools/list 的 list_workspace_feeds description/inputSchema 与 Cobra Long 手写选型文案,并与 node list / node search / doc read / drive search 划清路由边界;不改变命令身份、参数契约或接口绑定。",
|
||||
"source": "internal/cli/schema_hints/selection/wiki.json",
|
||||
"value": [
|
||||
"用户问某个知识库最近有什么更新、谁改了什么、有哪些评论等协作动态时",
|
||||
"需要巡检知识库变更并按时间线汇总成动态摘要时"
|
||||
]
|
||||
}
|
||||
},
|
||||
"group": "feed",
|
||||
"has_parameters": true,
|
||||
"idempotency": "idempotent",
|
||||
"interface_mode": "mcp",
|
||||
"interface_ref": {
|
||||
"product_id": "wiki",
|
||||
"rpc_name": "list_workspace_feeds"
|
||||
},
|
||||
"is_alias": false,
|
||||
"metadata_source": "embedded-mcp-metadata",
|
||||
"name": "list_workspace_feeds",
|
||||
"parameter_count": 4,
|
||||
"parameters": {
|
||||
"cursor": {
|
||||
"description": "分页游标 (首页留空)",
|
||||
"field_provenance": {
|
||||
"description": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_usage",
|
||||
"value": "分页游标 (首页留空)"
|
||||
},
|
||||
{
|
||||
"precedence": "mcp_metadata",
|
||||
"selected": false,
|
||||
"source": "mcp_metadata",
|
||||
"value": "分页游标,首次查询不传,后续翻页时传入上次返回的 nextToken。"
|
||||
},
|
||||
{
|
||||
"precedence": "default",
|
||||
"selected": false,
|
||||
"source": "default",
|
||||
"value": ""
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_usage",
|
||||
"value": "分页游标 (首页留空)"
|
||||
},
|
||||
"property": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "versioned_binding",
|
||||
"selected": true,
|
||||
"source": "versioned_parameter_binding",
|
||||
"value": "nextToken"
|
||||
},
|
||||
{
|
||||
"precedence": "inference",
|
||||
"selected": false,
|
||||
"source": "flag_name_inference",
|
||||
"value": "cursor"
|
||||
}
|
||||
],
|
||||
"precedence": "versioned_binding",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "versioned_parameter_binding",
|
||||
"value": "nextToken"
|
||||
},
|
||||
"required": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "default",
|
||||
"selected": true,
|
||||
"source": "default",
|
||||
"value": false
|
||||
}
|
||||
],
|
||||
"precedence": "default",
|
||||
"resolution": "fallback",
|
||||
"source": "default",
|
||||
"value": false
|
||||
},
|
||||
"required_when": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "default",
|
||||
"selected": true,
|
||||
"source": "default",
|
||||
"value": ""
|
||||
}
|
||||
],
|
||||
"precedence": "default",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "default",
|
||||
"value": ""
|
||||
},
|
||||
"type": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_flag_type",
|
||||
"value": "string"
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_flag_type",
|
||||
"value": "string"
|
||||
}
|
||||
},
|
||||
"interface_description": "分页游标,首次查询不传,后续翻页时传入上次返回的 nextToken。",
|
||||
"property": "nextToken",
|
||||
"required": false,
|
||||
"type": "string"
|
||||
},
|
||||
"exclude-file": {
|
||||
"description": "是否排除文件相关的动态 (默认 false)",
|
||||
"field_provenance": {
|
||||
"description": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_usage",
|
||||
"value": "是否排除文件相关的动态 (默认 false)"
|
||||
},
|
||||
{
|
||||
"precedence": "mcp_metadata",
|
||||
"selected": false,
|
||||
"source": "mcp_metadata",
|
||||
"value": "是否排除文件相关的动态,选填,默认 false。"
|
||||
},
|
||||
{
|
||||
"precedence": "default",
|
||||
"selected": false,
|
||||
"source": "default",
|
||||
"value": ""
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_usage",
|
||||
"value": "是否排除文件相关的动态 (默认 false)"
|
||||
},
|
||||
"property": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "versioned_binding",
|
||||
"selected": true,
|
||||
"source": "versioned_parameter_binding",
|
||||
"value": "excludeFile"
|
||||
},
|
||||
{
|
||||
"precedence": "inference",
|
||||
"selected": false,
|
||||
"source": "flag_name_inference",
|
||||
"value": "excludeFile"
|
||||
}
|
||||
],
|
||||
"precedence": "versioned_binding",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "versioned_parameter_binding",
|
||||
"value": "excludeFile"
|
||||
},
|
||||
"required": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_nonzero_default",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"precedence": "default",
|
||||
"selected": false,
|
||||
"source": "default",
|
||||
"value": false
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_nonzero_default",
|
||||
"value": false
|
||||
},
|
||||
"required_when": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "default",
|
||||
"selected": true,
|
||||
"source": "default",
|
||||
"value": ""
|
||||
}
|
||||
],
|
||||
"precedence": "default",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "default",
|
||||
"value": ""
|
||||
},
|
||||
"type": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_flag_type",
|
||||
"value": "boolean"
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_flag_type",
|
||||
"value": "boolean"
|
||||
}
|
||||
},
|
||||
"interface_description": "是否排除文件相关的动态,选填,默认 false。",
|
||||
"property": "excludeFile",
|
||||
"required": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"limit": {
|
||||
"description": "每页数量 (默认 20,最大 50)",
|
||||
"field_provenance": {
|
||||
"description": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_usage",
|
||||
"value": "每页数量 (默认 20,最大 50)"
|
||||
},
|
||||
{
|
||||
"precedence": "mcp_metadata",
|
||||
"selected": false,
|
||||
"source": "mcp_metadata",
|
||||
"value": "期望返回的最大动态条数,默认 20,最大 50。"
|
||||
},
|
||||
{
|
||||
"precedence": "default",
|
||||
"selected": false,
|
||||
"source": "default",
|
||||
"value": ""
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_usage",
|
||||
"value": "每页数量 (默认 20,最大 50)"
|
||||
},
|
||||
"interface_type": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "mcp_metadata",
|
||||
"selected": true,
|
||||
"source": "mcp_metadata",
|
||||
"value": "number"
|
||||
},
|
||||
{
|
||||
"precedence": "fallback",
|
||||
"selected": false,
|
||||
"source": "cobra_flag_type",
|
||||
"value": "integer"
|
||||
}
|
||||
],
|
||||
"precedence": "mcp_metadata",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "mcp_metadata",
|
||||
"value": "number"
|
||||
},
|
||||
"property": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "versioned_binding",
|
||||
"selected": true,
|
||||
"source": "versioned_parameter_binding",
|
||||
"value": "maxResults"
|
||||
},
|
||||
{
|
||||
"precedence": "inference",
|
||||
"selected": false,
|
||||
"source": "flag_name_inference",
|
||||
"value": "limit"
|
||||
}
|
||||
],
|
||||
"precedence": "versioned_binding",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "versioned_parameter_binding",
|
||||
"value": "maxResults"
|
||||
},
|
||||
"required": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_nonzero_default",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"precedence": "default",
|
||||
"selected": false,
|
||||
"source": "default",
|
||||
"value": false
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_nonzero_default",
|
||||
"value": false
|
||||
},
|
||||
"required_when": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "default",
|
||||
"selected": true,
|
||||
"source": "default",
|
||||
"value": ""
|
||||
}
|
||||
],
|
||||
"precedence": "default",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "default",
|
||||
"value": ""
|
||||
},
|
||||
"type": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_flag_type",
|
||||
"value": "integer"
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_flag_type",
|
||||
"value": "integer"
|
||||
}
|
||||
},
|
||||
"interface_description": "期望返回的最大动态条数,默认 20,最大 50。",
|
||||
"interface_type": "number",
|
||||
"property": "maxResults",
|
||||
"required": false,
|
||||
"type": "integer"
|
||||
},
|
||||
"workspace": {
|
||||
"description": "知识库 ID 或 URL (必填)",
|
||||
"field_provenance": {
|
||||
"description": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_usage",
|
||||
"value": "知识库 ID 或 URL (必填)"
|
||||
},
|
||||
{
|
||||
"precedence": "mcp_metadata",
|
||||
"selected": false,
|
||||
"source": "mcp_metadata",
|
||||
"value": "目标知识库的标识,支持两种格式:1) 知识库 ID(纯字符串);2) 知识库 URL,如 https://alidocs.dingtalk.com/i/spaces/{workspaceId}/overview,系统自动提取其中的 workspaceId。"
|
||||
},
|
||||
{
|
||||
"precedence": "default",
|
||||
"selected": false,
|
||||
"source": "default",
|
||||
"value": ""
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_usage",
|
||||
"value": "知识库 ID 或 URL (必填)"
|
||||
},
|
||||
"property": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "versioned_binding",
|
||||
"selected": true,
|
||||
"source": "versioned_parameter_binding",
|
||||
"value": "workspaceId"
|
||||
},
|
||||
{
|
||||
"precedence": "inference",
|
||||
"selected": false,
|
||||
"source": "flag_name_inference",
|
||||
"value": "workspace"
|
||||
}
|
||||
],
|
||||
"precedence": "versioned_binding",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "versioned_parameter_binding",
|
||||
"value": "workspaceId"
|
||||
},
|
||||
"required": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "mcp_metadata",
|
||||
"selected": true,
|
||||
"source": "mcp_metadata",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"precedence": "inference",
|
||||
"selected": false,
|
||||
"source": "usage_required_inference",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"precedence": "default",
|
||||
"selected": false,
|
||||
"source": "default",
|
||||
"value": false
|
||||
}
|
||||
],
|
||||
"precedence": "mcp_metadata",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "mcp_metadata",
|
||||
"value": true
|
||||
},
|
||||
"required_when": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "default",
|
||||
"selected": true,
|
||||
"source": "default",
|
||||
"value": ""
|
||||
}
|
||||
],
|
||||
"precedence": "default",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "default",
|
||||
"value": ""
|
||||
},
|
||||
"type": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_flag_type",
|
||||
"value": "string"
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_flag_type",
|
||||
"value": "string"
|
||||
}
|
||||
},
|
||||
"interface_description": "目标知识库的标识,支持两种格式:1) 知识库 ID(纯字符串);2) 知识库 URL,如 https://alidocs.dingtalk.com/i/spaces/{workspaceId}/overview,系统自动提取其中的 workspaceId。",
|
||||
"property": "workspaceId",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"path": "wiki.list_workspace_feeds",
|
||||
"primary_cli_path": "wiki feed list",
|
||||
"product_id": "wiki",
|
||||
"reviewed": true,
|
||||
"risk": "low",
|
||||
"source": "reviewed_command_registry",
|
||||
"title": "查询知识库动态列表",
|
||||
"use_when": [
|
||||
"用户问某个知识库最近有什么更新、谁改了什么、有哪些评论等协作动态时",
|
||||
"需要巡检知识库变更并按时间线汇总成动态摘要时"
|
||||
]
|
||||
},
|
||||
"wiki.node_copy": {
|
||||
"agent_metadata_source": "embedded-skill-metadata",
|
||||
"agent_source_refs": [
|
||||
@@ -5822,7 +6609,7 @@
|
||||
"cli_path": "wiki node copy",
|
||||
"confirmation": "not_required",
|
||||
"description": "将知识库中的节点复制到指定位置。\n\n通过 --node 指定源节点,通过 --folder 指定目标文件夹。\n不传 --folder 时复制到 --workspace 指定知识库的根目录。",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
|
||||
"effect": "write",
|
||||
"effect_source": "command-verb",
|
||||
"examples": [
|
||||
@@ -6488,7 +7275,7 @@
|
||||
"cli_path": "wiki node move",
|
||||
"confirmation": "not_required",
|
||||
"description": "将知识库中的节点移动到指定位置。\n\n通过 --node 指定源节点,通过 --folder 指定目标文件夹。\n不传 --folder 时移动到 --workspace 指定知识库的根目录。\n\n注意:跨知识库移动需要同时具备源和目标的相应权限。",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
|
||||
"effect": "write",
|
||||
"effect_source": "command-verb",
|
||||
"examples": [
|
||||
@@ -7153,7 +7940,7 @@
|
||||
"cli_path": "wiki node search",
|
||||
"confirmation": "not_required",
|
||||
"description": "在指定知识库内搜索文档/文件夹/表格等节点。\n\n通过 --workspace 限定搜索范围到某个知识库,通过 --query 指定搜索关键词。\n支持按文件扩展名过滤(--extensions),如 adoc、asheet、pdf 等。",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
|
||||
"effect": "read",
|
||||
"effect_source": "command-verb",
|
||||
"examples": [
|
||||
@@ -8051,7 +8838,7 @@
|
||||
"cli_path": "wiki member remove",
|
||||
"confirmation": "not_required",
|
||||
"description": "从指定知识库中移除一个或多个成员(仅支持 USER 类型)。\n\n移除后相关用户将无法访问该知识库下的内容(除非通过节点级权限另行授权)。\n\n注意:\n- OWNER 角色不可通过此接口移除。\n- 操作者需具备知识库的 OWNER 或 MANAGER 权限。\n- 单次请求最多 30 个成员,超出请分批调用。",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
|
||||
"effect": "write",
|
||||
"effect_source": "agent-hint",
|
||||
"examples": [
|
||||
@@ -8671,7 +9458,7 @@
|
||||
]
|
||||
},
|
||||
"description": "根据关键词搜索当前用户有权限访问的知识库列表,匹配知识库名称和描述。",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
|
||||
"effect": "read",
|
||||
"effect_source": "command-verb",
|
||||
"examples": [
|
||||
@@ -9370,7 +10157,7 @@
|
||||
"cli_path": "wiki +space-search",
|
||||
"confirmation": "not_required",
|
||||
"description": "当你只记得知识库名称的部分关键词、想快速按名称定位某个知识库时使用;输入关键词返回匹配的知识库列表,比逐页 +space-list 更快找到目标 workspaceId。",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
|
||||
"effect": "read",
|
||||
"effect_source": "agent-hint",
|
||||
"examples": [
|
||||
@@ -9880,7 +10667,7 @@
|
||||
"cli_path": "wiki member update",
|
||||
"confirmation": "not_required",
|
||||
"description": "更新指定知识库已有成员的角色。\n\n支持的角色 (--role)(必须大写):\n MANAGER 管理员\n EDITOR 编辑者\n DOWNLOADER 查看下载者\n READER 仅可查看者\n\n注意:\n- OWNER 角色不可通过此接口变更。\n- 同一成员在同一知识库只能拥有一个角色,变更后旧角色自动替换。\n- 操作者需具备知识库的 OWNER 或 MANAGER 权限。\n\n仅可更新已存在成员关系的成员,新增成员请使用 dws wiki member add。",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理",
|
||||
"display": "知识库 / 空间管理 / 节点管理 / 成员管理 / 动态查询",
|
||||
"effect": "write",
|
||||
"effect_source": "command-verb",
|
||||
"examples": [
|
||||
|
||||
@@ -452,6 +452,10 @@
|
||||
"canonical_path": "aitable.workflow_disable",
|
||||
"cli_path": "aitable workflow disable"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.workflow_edit_example",
|
||||
"cli_path": "aitable workflow edit-example"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.workflow_enable",
|
||||
"cli_path": "aitable workflow enable"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -999,6 +999,19 @@
|
||||
"reviewed": true,
|
||||
"runtime_gate": "confirm_delete"
|
||||
},
|
||||
"aitable.workflow_edit_example": {
|
||||
"effect": "read",
|
||||
"risk": "low",
|
||||
"confirmation": "not_required",
|
||||
"idempotency": "idempotent",
|
||||
"interface_mode": "composite",
|
||||
"availability": "available",
|
||||
"interface_reason": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
|
||||
"reviewed": true,
|
||||
"review_reason": "The command sends empty arguments to aitable/edit_workflow_example and returns the service-provided workflow editing documentation and examples. The operation is read-only and safe to retry.",
|
||||
"cli_path": "aitable workflow edit-example",
|
||||
"runtime_gate": "none"
|
||||
},
|
||||
"aitable.workflow_enable": {
|
||||
"interface_ref": {
|
||||
"product_id": "aitable-helper",
|
||||
|
||||
@@ -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": 845,
|
||||
"source_tools": 850,
|
||||
"matched_tools": 71
|
||||
},
|
||||
"tools": {
|
||||
|
||||
@@ -2250,6 +2250,26 @@
|
||||
"dws-schema-live:none (helper/composite; Skill+Cobra)"
|
||||
]
|
||||
},
|
||||
"aitable.workflow_edit_example": {
|
||||
"agent_summary": "获取 AI 表格工作流编辑文档与 workflow-dsl/v1 示例。",
|
||||
"use_when": [
|
||||
"创建或更新工作流前,需要确认最新 workflow-dsl/v1 结构、节点写法或完整示例时"
|
||||
],
|
||||
"avoid_when": [
|
||||
"实际创建工作流用 workflow create;修改已有工作流用 workflow update;查询已发布定义用 workflow get"
|
||||
],
|
||||
"examples": [
|
||||
"dws aitable workflow edit-example"
|
||||
],
|
||||
"reviewed": true,
|
||||
"review_reason": "依据新增 Cobra leaf 和用户提供的 aitable/edit_workflow_example 空参数 MCP 契约审阅选型语义,将该命令限定为 create/update 前的只读文档入口。",
|
||||
"source_refs": [
|
||||
"internal/cli/schema_command_registry.json#aitable.workflow_edit_example",
|
||||
"cobra-help:dws aitable workflow edit-example --help",
|
||||
"skills/mono/references/products/aitable/aitable-workflow.md",
|
||||
"mcp-contract:aitable/edit_workflow_example"
|
||||
]
|
||||
},
|
||||
"aitable.workflow_enable": {
|
||||
"agent_summary": "启用工作流。",
|
||||
"use_when": [
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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 无法立即返回,请稍后查看目标文件夹确认结果。",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -25,7 +25,9 @@ import (
|
||||
)
|
||||
|
||||
// MetaFileName is the on-disk name of the bus metadata file. It lives
|
||||
// alongside bus.lock and bus.sock inside the bus working directory.
|
||||
// alongside bus.lock inside the bus working directory. Unix bus sockets live
|
||||
// in a private per-user runtime directory so shared config filesystems do not
|
||||
// need socket support.
|
||||
const MetaFileName = "bus.meta"
|
||||
|
||||
// Meta is the JSON document written once at bus startup. Its primary
|
||||
|
||||
@@ -30,9 +30,9 @@ import (
|
||||
type SpawnFunc func(SpawnConfig) (pid int, err error)
|
||||
|
||||
// DiscoverConfig describes one discover attempt. WorkDir holds bus.lock and
|
||||
// usually (on Unix) bus.sock — see dwsevent.IPCEndpoint for the short-path
|
||||
// fallback when WorkDir is too deep; the caller must mkdir it with
|
||||
// pkg/config.DirPerm beforehand.
|
||||
// persistent bus metadata; Unix sockets live in a private per-user runtime
|
||||
// directory so WorkDir may reside on a shared filesystem without socket
|
||||
// support. The caller must mkdir WorkDir with pkg/config.DirPerm beforehand.
|
||||
type DiscoverConfig struct {
|
||||
WorkDir string
|
||||
IPCEndpoint string
|
||||
|
||||
@@ -69,8 +69,8 @@ type BusEntry struct {
|
||||
|
||||
// IPCEndpoint returns the IPC endpoint for this entry. Delegates to
|
||||
// dwsevent.IPCEndpoint so status/stop dial exactly where consume and the
|
||||
// bus daemon bound (including the short-path fallback when WorkDir is too
|
||||
// deep for sun_path).
|
||||
// bus daemon bound (a private per-user runtime path on Unix and a named pipe
|
||||
// on Windows).
|
||||
func (e BusEntry) IPCEndpoint() string {
|
||||
hash := e.ClientIDHash
|
||||
if e.IdentityHash != "" {
|
||||
|
||||
+36
-14
@@ -17,8 +17,16 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const eventRuntimeDirPrefix = "dws-event-"
|
||||
|
||||
func currentUserID() string {
|
||||
return strconv.Itoa(os.Geteuid())
|
||||
}
|
||||
|
||||
// MaxUnixSocketPath returns the longest Unix socket path accepted by
|
||||
// bind/connect on this OS (Go rejects longer names with EINVAL before
|
||||
// the syscall). sockaddr_un.sun_path is 104 bytes on darwin and the
|
||||
@@ -35,17 +43,19 @@ func maxUnixSocketPath(goos string) int {
|
||||
}
|
||||
|
||||
// IPCEndpoint returns the bus IPC endpoint for one identity: a Named Pipe
|
||||
// name on Windows, otherwise bus.sock inside workDir.
|
||||
// name on Windows, otherwise a deterministic Unix socket under a private
|
||||
// per-user runtime directory.
|
||||
//
|
||||
// The canonical Unix location is <workDir>/bus.sock, but workDir derives
|
||||
// from the config dir, which can be arbitrarily deep (e.g. dwssb sandboxes
|
||||
// use ~/.dwssb/sandboxes/<name>/config/...). When the canonical path would
|
||||
// exceed the OS sun_path limit, the socket falls back to a short
|
||||
// deterministic path under os.TempDir keyed by a hash of workDir, so every
|
||||
// process (consume parent, forked _bus child, status/stop tooling) that
|
||||
// derives the endpoint from the same workDir agrees on the location.
|
||||
// bus.lock / bus.meta / bus.log always stay in workDir — only the socket
|
||||
// moves.
|
||||
// Unix sockets must live on a local filesystem that supports bind(2).
|
||||
// Config directories may reside on NFS, CSI, FUSE, or other shared mounts
|
||||
// that reject Unix socket creation with ENOTSUPP. On Unix, the endpoint uses
|
||||
// XDG_RUNTIME_DIR when it is absolute and short enough; otherwise it falls
|
||||
// back to a per-UID directory under os.TempDir. The transport creates and
|
||||
// validates that directory as owner-only before listening or dialing. The
|
||||
// socket name is keyed by a hash of workDir so every process (consume parent,
|
||||
// forked _bus child, status/stop tooling) that derives the endpoint from the
|
||||
// same workDir agrees on the location. bus.lock / bus.meta / bus.log always
|
||||
// stay in workDir.
|
||||
//
|
||||
// This is the single source of truth for endpoint derivation; the cobra
|
||||
// layer and busctl must not re-implement the shape.
|
||||
@@ -60,9 +70,21 @@ func ipcEndpointForOS(goos, workDir, editionName string, sourceKind SourceKind,
|
||||
if goos == "windows" {
|
||||
return `\\.\pipe\dws-event-` + editionName + "-" + string(sourceKind) + "-" + identityHash
|
||||
}
|
||||
sock := filepath.Join(workDir, "bus.sock")
|
||||
if len(sock) <= maxUnixSocketPath(goos) {
|
||||
return sock
|
||||
return unixSocketEndpoint(goos, workDir, strings.TrimSpace(os.Getenv("XDG_RUNTIME_DIR")), os.TempDir())
|
||||
}
|
||||
|
||||
func unixSocketEndpoint(goos, workDir, runtimeDir, tempDir string) string {
|
||||
socketName := "dws-evt-" + IdentityHash(workDir) + ".sock"
|
||||
userDirName := eventRuntimeDirPrefix + currentUserID()
|
||||
if filepath.IsAbs(runtimeDir) {
|
||||
candidate := filepath.Join(runtimeDir, userDirName, socketName)
|
||||
if len(candidate) <= maxUnixSocketPath(goos) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
return filepath.Join(os.TempDir(), "dws-evt-"+IdentityHash(workDir)+".sock")
|
||||
fallback := filepath.Join(tempDir, userDirName, socketName)
|
||||
if len(fallback) <= maxUnixSocketPath(goos) {
|
||||
return fallback
|
||||
}
|
||||
return filepath.Join("/tmp", userDirName, socketName)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -33,10 +34,27 @@ func TestCrossPlatformCoverageEndpointPortableCoverageEdges(t *testing.T) {
|
||||
if got := ipcEndpointForOS("windows", "ignored", "open", "", "hash"); got != `\\.\pipe\dws-event-open-app_stream-hash` {
|
||||
t.Fatalf("Windows endpoint = %q", got)
|
||||
}
|
||||
if got := ipcEndpointForOS("darwin", "short", "open", SourceKindPersonalStream, "hash"); got != filepath.Join("short", "bus.sock") {
|
||||
|
||||
runtimeRoot := filepath.VolumeName(os.TempDir()) + string(filepath.Separator)
|
||||
runtimeDir := filepath.Join(runtimeRoot, "dws-xdg-runtime")
|
||||
t.Setenv("XDG_RUNTIME_DIR", runtimeDir)
|
||||
workDir := "portable-xdg-workdir"
|
||||
wantXDG := filepath.Join(runtimeDir, eventRuntimeDirPrefix+currentUserID(), "dws-evt-"+IdentityHash(workDir)+".sock")
|
||||
if got := ipcEndpointForOS("linux", workDir, "open", SourceKindPersonalStream, "hash"); got != wantXDG {
|
||||
t.Fatalf("XDG Unix endpoint = %q, want %q", got, wantXDG)
|
||||
}
|
||||
|
||||
t.Setenv("XDG_RUNTIME_DIR", "")
|
||||
if got := ipcEndpointForOS("darwin", "short", "open", SourceKindPersonalStream, "hash"); got != filepath.Join(os.TempDir(), eventRuntimeDirPrefix+currentUserID(), "dws-evt-"+IdentityHash("short")+".sock") {
|
||||
t.Fatalf("short Unix endpoint = %q", got)
|
||||
}
|
||||
if got := ipcEndpointForOS("darwin", strings.Repeat("x", 200), "open", SourceKindAppStream, "hash"); !strings.Contains(got, "dws-evt-") {
|
||||
t.Fatalf("long Unix endpoint = %q", got)
|
||||
}
|
||||
|
||||
longTempDir := filepath.Join(string(filepath.Separator), strings.Repeat("long-temp-root", 20))
|
||||
wantShortFallback := filepath.Join("/tmp", eventRuntimeDirPrefix+currentUserID(), "dws-evt-"+IdentityHash(workDir)+".sock")
|
||||
if got := unixSocketEndpoint("darwin", workDir, "", longTempDir); got != wantShortFallback {
|
||||
t.Fatalf("overlong temp endpoint = %q, want %q", got, wantShortFallback)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
)
|
||||
|
||||
func TestEndpointPlatformVariants(t *testing.T) {
|
||||
t.Setenv("XDG_RUNTIME_DIR", "")
|
||||
if maxUnixSocketPath("linux") != 107 || maxUnixSocketPath("darwin") != 103 {
|
||||
t.Fatal("Unix socket limits changed")
|
||||
}
|
||||
@@ -31,7 +32,7 @@ func TestEndpointPlatformVariants(t *testing.T) {
|
||||
t.Fatalf("Windows pipe = %q", pipe)
|
||||
}
|
||||
short := ipcEndpointForOS("darwin", "/tmp/events", "open", SourceKindPersonalStream, "hash")
|
||||
if short != filepath.Join("/tmp/events", "bus.sock") {
|
||||
if short != filepath.Join(os.TempDir(), eventRuntimeDirPrefix+currentUserID(), "dws-evt-"+IdentityHash("/tmp/events")+".sock") {
|
||||
t.Fatalf("short Unix endpoint = %q", short)
|
||||
}
|
||||
long := ipcEndpointForOS("darwin", "/"+strings.Repeat("deep/", 40), "open", SourceKindAppStream, "hash")
|
||||
@@ -40,16 +41,40 @@ func TestEndpointPlatformVariants(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPCEndpointShortWorkDirUsesCanonicalPath(t *testing.T) {
|
||||
workDir := "/tmp/dws/events/open/app_stream/aabbccdd00112233"
|
||||
func TestIPCEndpointUsesXDGUserRuntimeDir(t *testing.T) {
|
||||
tempRoot, err := filepath.EvalSymlinks("/tmp")
|
||||
if err != nil {
|
||||
t.Fatalf("EvalSymlinks: %v", err)
|
||||
}
|
||||
runtimeDir, err := os.MkdirTemp(tempRoot, "dws-xdg-")
|
||||
if err != nil {
|
||||
t.Fatalf("MkdirTemp: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = os.RemoveAll(runtimeDir) })
|
||||
t.Setenv("XDG_RUNTIME_DIR", runtimeDir)
|
||||
workDir := "/shared/events/open/app_stream/aabbccdd00112233"
|
||||
got := IPCEndpoint(workDir, "open", SourceKindAppStream, "aabbccdd00112233")
|
||||
want := filepath.Join(workDir, "bus.sock")
|
||||
want := filepath.Join(runtimeDir, eventRuntimeDirPrefix+currentUserID(), "dws-evt-"+IdentityHash(workDir)+".sock")
|
||||
if got != want {
|
||||
t.Fatalf("IPCEndpoint = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPCEndpointLongWorkDirFallsBackUnderTempDir(t *testing.T) {
|
||||
func TestIPCEndpointWithoutXDGUsesPerUserLocalTempDir(t *testing.T) {
|
||||
t.Setenv("XDG_RUNTIME_DIR", "")
|
||||
workDir := "/tmp/dws/events/open/app_stream/aabbccdd00112233"
|
||||
got := IPCEndpoint(workDir, "open", SourceKindAppStream, "aabbccdd00112233")
|
||||
want := filepath.Join(os.TempDir(), eventRuntimeDirPrefix+currentUserID(), "dws-evt-"+IdentityHash(workDir)+".sock")
|
||||
if got != want {
|
||||
t.Fatalf("IPCEndpoint = %q, want %q", got, want)
|
||||
}
|
||||
if strings.HasPrefix(got, workDir) {
|
||||
t.Fatalf("IPCEndpoint = %q, want endpoint outside workDir", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPCEndpointLongWorkDirUsesLocalTempDir(t *testing.T) {
|
||||
t.Setenv("XDG_RUNTIME_DIR", "")
|
||||
// Mirrors the dwssb sandbox layout that produced a 111-byte socket
|
||||
// path — over macOS's 103-byte usable sun_path budget.
|
||||
workDir := "/Users/zhengyubai/.dwssb/sandboxes/event-subscribe/config/events/open/personal_stream/3928ce0fb4860a52"
|
||||
@@ -66,6 +91,7 @@ func TestIPCEndpointLongWorkDirFallsBackUnderTempDir(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIPCEndpointFallbackIsDeterministicPerWorkDir(t *testing.T) {
|
||||
t.Setenv("XDG_RUNTIME_DIR", "")
|
||||
long := strings.Repeat("x", 120)
|
||||
a := IPCEndpoint("/base/"+long+"/one", "open", SourceKindPersonalStream, "hash")
|
||||
b := IPCEndpoint("/base/"+long+"/one", "open", SourceKindPersonalStream, "hash")
|
||||
@@ -77,3 +103,29 @@ func TestIPCEndpointFallbackIsDeterministicPerWorkDir(t *testing.T) {
|
||||
t.Fatalf("different workDirs collided on endpoint %q", a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPCEndpointLongXDGPathFallsBackToTempDir(t *testing.T) {
|
||||
t.Setenv("XDG_RUNTIME_DIR", "/"+strings.Repeat("runtime/", 30))
|
||||
workDir := "/shared/events/open/personal_stream/aabbccdd00112233"
|
||||
got := ipcEndpointForOS("linux", workDir, "open", SourceKindPersonalStream, "hash")
|
||||
wantPrefix := filepath.Join(os.TempDir(), eventRuntimeDirPrefix+currentUserID()) + string(filepath.Separator)
|
||||
if !strings.HasPrefix(got, wantPrefix) {
|
||||
t.Fatalf("IPCEndpoint = %q, want fallback under %q", got, wantPrefix)
|
||||
}
|
||||
if len(got) > maxUnixSocketPath("linux") {
|
||||
t.Fatalf("fallback path still too long: %d > %d (%q)", len(got), maxUnixSocketPath("linux"), got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPCEndpointLongTempDirUsesShortSystemFallback(t *testing.T) {
|
||||
workDir := "/shared/events/open/personal_stream/aabbccdd00112233"
|
||||
longTempDir := "/" + strings.Repeat("long-temp-root/", 20)
|
||||
got := unixSocketEndpoint("linux", workDir, "", longTempDir)
|
||||
want := filepath.Join("/tmp", eventRuntimeDirPrefix+currentUserID(), "dws-evt-"+IdentityHash(workDir)+".sock")
|
||||
if got != want {
|
||||
t.Fatalf("IPCEndpoint = %q, want short fallback %q", got, want)
|
||||
}
|
||||
if len(got) > maxUnixSocketPath("linux") {
|
||||
t.Fatalf("short fallback path too long: %d > %d (%q)", len(got), maxUnixSocketPath("linux"), got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,83 @@ func TestCrossPlatformCoverageUnixListenErrorCoverage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageUnixSocketDirectoryErrorCoverage(t *testing.T) {
|
||||
oldLstat, oldRuntimeStat, oldMkdir := lstatSocketPath, statSocketRuntimeRoot, mkdirSocketDir
|
||||
t.Cleanup(func() {
|
||||
lstatSocketPath, statSocketRuntimeRoot, mkdirSocketDir = oldLstat, oldRuntimeStat, oldMkdir
|
||||
})
|
||||
|
||||
wantErr := errors.New("synthetic socket directory failure")
|
||||
if err := ensureSocketDir("relative/bus.sock", true); err == nil || !strings.Contains(err.Error(), "must be absolute") {
|
||||
t.Fatalf("relative socket path error = %v", err)
|
||||
}
|
||||
|
||||
root := shortSecureTempDir(t)
|
||||
missingRootPath := filepath.Join(root, "missing-root", "dws-event-test", "bus.sock")
|
||||
if err := ensureSocketDir(missingRootPath, false); err == nil || !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("missing runtime root error = %v", err)
|
||||
}
|
||||
|
||||
rootInfo, err := oldLstat(root)
|
||||
if err != nil {
|
||||
t.Fatalf("lstat secure root: %v", err)
|
||||
}
|
||||
lstatSocketPath = func(path string) (os.FileInfo, error) {
|
||||
if path == "/tmp" {
|
||||
return fileInfoWithMode{FileInfo: rootInfo, mode: os.ModeSymlink | 0o777}, nil
|
||||
}
|
||||
return oldLstat(path)
|
||||
}
|
||||
statSocketRuntimeRoot = func(path string) (os.FileInfo, error) {
|
||||
if path == "/tmp" {
|
||||
return nil, wantErr
|
||||
}
|
||||
return oldRuntimeStat(path)
|
||||
}
|
||||
if err := ensureSocketDir("/tmp/dws-event-coverage/bus.sock", false); !errors.Is(err, wantErr) {
|
||||
t.Fatalf("runtime root resolution error = %v", err)
|
||||
}
|
||||
lstatSocketPath, statSocketRuntimeRoot = oldLstat, oldRuntimeStat
|
||||
|
||||
rootFile := filepath.Join(root, "runtime-root-file")
|
||||
if err := os.WriteFile(rootFile, []byte("not a directory"), 0o600); err != nil {
|
||||
t.Fatalf("write runtime root file: %v", err)
|
||||
}
|
||||
if err := ensureSocketDir(filepath.Join(rootFile, "dws-event-test", "bus.sock"), false); err == nil || !strings.Contains(err.Error(), "runtime root is not a directory") {
|
||||
t.Fatalf("non-directory runtime root error = %v", err)
|
||||
}
|
||||
|
||||
mkdirSocketDir = func(string, os.FileMode) error { return wantErr }
|
||||
if err := ensureSocketDir(filepath.Join(root, "mkdir-failure", "bus.sock"), true); !errors.Is(err, wantErr) {
|
||||
t.Fatalf("socket directory creation error = %v", err)
|
||||
}
|
||||
mkdirSocketDir = oldMkdir
|
||||
|
||||
if err := ensureSocketDir(filepath.Join(root, "missing-socket-dir", "bus.sock"), false); err == nil || !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("missing socket directory error = %v", err)
|
||||
}
|
||||
|
||||
withoutOwner := fileInfoWithoutOwner{FileInfo: rootInfo}
|
||||
if err := validateSocketRuntimeRoot(root, withoutOwner, uint32(os.Geteuid())); err == nil || !strings.Contains(err.Error(), "owner") {
|
||||
t.Fatalf("runtime root owner error = %v", err)
|
||||
}
|
||||
if err := validatePrivateSocketDir(root, withoutOwner, uint32(os.Geteuid())); err == nil || !strings.Contains(err.Error(), "owner") {
|
||||
t.Fatalf("socket directory owner error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type fileInfoWithMode struct {
|
||||
os.FileInfo
|
||||
mode os.FileMode
|
||||
}
|
||||
|
||||
func (f fileInfoWithMode) Mode() os.FileMode { return f.mode }
|
||||
func (f fileInfoWithMode) IsDir() bool { return f.mode.IsDir() }
|
||||
|
||||
type fileInfoWithoutOwner struct{ os.FileInfo }
|
||||
|
||||
func (fileInfoWithoutOwner) Sys() any { return struct{}{} }
|
||||
|
||||
type stubNetListener struct {
|
||||
close func() error
|
||||
}
|
||||
|
||||
@@ -16,9 +16,12 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
dwsevent "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/config"
|
||||
@@ -30,10 +33,13 @@ type unixListener struct {
|
||||
}
|
||||
|
||||
var (
|
||||
statSocket = os.Stat
|
||||
removeSocket = os.Remove
|
||||
listenUnix = net.Listen
|
||||
chmodSocket = os.Chmod
|
||||
statSocket = os.Stat
|
||||
removeSocket = os.Remove
|
||||
listenUnix = net.Listen
|
||||
chmodSocket = os.Chmod
|
||||
lstatSocketPath = os.Lstat
|
||||
statSocketRuntimeRoot = os.Stat
|
||||
mkdirSocketDir = os.Mkdir
|
||||
)
|
||||
|
||||
func (u *unixListener) Accept() (net.Conn, error) { return u.l.Accept() }
|
||||
@@ -56,11 +62,95 @@ func checkSocketPath(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureSocketDir makes the socket's immediate parent an owner-only
|
||||
// directory and rejects unsafe pre-existing paths. The parent of that
|
||||
// directory must itself either be private to the effective user (for
|
||||
// XDG_RUNTIME_DIR and macOS temporary roots) or sticky (for Linux /tmp), so
|
||||
// another user cannot rename the private directory out from under us.
|
||||
func ensureSocketDir(path string, create bool) error {
|
||||
if !filepath.IsAbs(path) {
|
||||
return fmt.Errorf("transport: unix socket path must be absolute: %s", path)
|
||||
}
|
||||
dir := filepath.Dir(path)
|
||||
root := filepath.Dir(dir)
|
||||
rootInfo, err := lstatSocketPath(root)
|
||||
if err != nil {
|
||||
return fmt.Errorf("transport: inspect socket runtime root %s: %w", root, err)
|
||||
}
|
||||
// macOS exposes the system /tmp as a root-owned symlink to /private/tmp.
|
||||
// Follow only that well-known alias, then apply the same ownership/sticky
|
||||
// validation to its target. Arbitrary runtime-root symlinks remain rejected.
|
||||
if rootInfo.Mode()&os.ModeSymlink != 0 && filepath.Clean(root) == "/tmp" {
|
||||
rootInfo, err = statSocketRuntimeRoot(root)
|
||||
if err != nil {
|
||||
return fmt.Errorf("transport: resolve socket runtime root %s: %w", root, err)
|
||||
}
|
||||
}
|
||||
if err := validateSocketRuntimeRoot(root, rootInfo, uint32(os.Geteuid())); err != nil {
|
||||
return err
|
||||
}
|
||||
if create {
|
||||
if err := mkdirSocketDir(dir, config.DirPerm); err != nil && !errors.Is(err, os.ErrExist) {
|
||||
return fmt.Errorf("transport: create socket directory %s: %w", dir, err)
|
||||
}
|
||||
}
|
||||
dirInfo, err := lstatSocketPath(dir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("transport: inspect socket directory %s: %w", dir, err)
|
||||
}
|
||||
return validatePrivateSocketDir(dir, dirInfo, uint32(os.Geteuid()))
|
||||
}
|
||||
|
||||
func validateSocketRuntimeRoot(path string, info os.FileInfo, effectiveUID uint32) error {
|
||||
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
|
||||
return fmt.Errorf("transport: socket runtime root is not a directory: %s", path)
|
||||
}
|
||||
owner, err := fileOwnerUID(info)
|
||||
if err != nil {
|
||||
return fmt.Errorf("transport: inspect socket runtime root owner %s: %w", path, err)
|
||||
}
|
||||
privateOwnerRoot := owner == effectiveUID && info.Mode().Perm()&0o022 == 0
|
||||
stickyRoot := info.Mode()&os.ModeSticky != 0
|
||||
if !privateOwnerRoot && !stickyRoot {
|
||||
return fmt.Errorf("transport: socket runtime root is neither private nor sticky: %s", path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePrivateSocketDir(path string, info os.FileInfo, effectiveUID uint32) error {
|
||||
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
|
||||
return fmt.Errorf("transport: socket directory is not a directory: %s", path)
|
||||
}
|
||||
owner, err := fileOwnerUID(info)
|
||||
if err != nil {
|
||||
return fmt.Errorf("transport: inspect socket directory owner %s: %w", path, err)
|
||||
}
|
||||
if owner != effectiveUID {
|
||||
return fmt.Errorf("transport: socket directory %s is owned by uid %d, want %d", path, owner, effectiveUID)
|
||||
}
|
||||
if perm := info.Mode().Perm(); perm != config.DirPerm {
|
||||
return fmt.Errorf("transport: socket directory %s has permissions %04o, want %04o", path, perm, config.DirPerm)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fileOwnerUID(info os.FileInfo) (uint32, error) {
|
||||
stat, ok := info.Sys().(*syscall.Stat_t)
|
||||
if !ok {
|
||||
return 0, errors.New("stat result does not expose an owner uid")
|
||||
}
|
||||
return stat.Uid, nil
|
||||
}
|
||||
|
||||
func listen(path string) (Listener, error) {
|
||||
if err := checkSocketPath(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Stale socket cleanup. Caller holds bus.lock so this is race-safe.
|
||||
if err := ensureSocketDir(path, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The private per-user parent excludes other users. The caller's bus.lock
|
||||
// serializes stale-socket cleanup for processes using the same WorkDir.
|
||||
if _, err := statSocket(path); err == nil {
|
||||
if err := removeSocket(path); err != nil {
|
||||
return nil, fmt.Errorf("transport: remove stale socket %s: %w", path, err)
|
||||
@@ -82,5 +172,8 @@ func dial(path string) (net.Conn, error) {
|
||||
if err := checkSocketPath(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ensureSocketDir(path, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return net.Dial("unix", path)
|
||||
}
|
||||
|
||||
@@ -21,12 +21,32 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
dwsevent "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event"
|
||||
)
|
||||
|
||||
func shortSecureTempDir(t *testing.T) string {
|
||||
t.Helper()
|
||||
tempRoot, err := filepath.EvalSymlinks("/tmp")
|
||||
if err != nil {
|
||||
t.Fatalf("EvalSymlinks: %v", err)
|
||||
}
|
||||
dir, err := os.MkdirTemp(tempRoot, "dws-et-")
|
||||
if err != nil {
|
||||
t.Fatalf("MkdirTemp: %v", err)
|
||||
}
|
||||
if err := os.Chmod(dir, 0o700); err != nil {
|
||||
t.Fatalf("chmod temp dir: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = os.RemoveAll(dir) })
|
||||
return dir
|
||||
}
|
||||
|
||||
func TestListen_DialRoundtrip(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "bus.sock")
|
||||
path := filepath.Join(shortSecureTempDir(t), "bus.sock")
|
||||
l, err := Listen(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Listen: %v", err)
|
||||
@@ -87,7 +107,7 @@ func TestListen_DialRoundtrip(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestListen_StaleSocketCleanup(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "bus.sock")
|
||||
path := filepath.Join(shortSecureTempDir(t), "bus.sock")
|
||||
// Pre-create a stale file at path (not a valid socket).
|
||||
if err := os.WriteFile(path, []byte("stale"), 0o600); err != nil {
|
||||
t.Fatalf("pre-create: %v", err)
|
||||
@@ -99,8 +119,121 @@ func TestListen_StaleSocketCleanup(t *testing.T) {
|
||||
defer l.Close()
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageListenCreatesPrivateSocketDirectory(t *testing.T) {
|
||||
dir := filepath.Join(shortSecureTempDir(t), "dws-event-test")
|
||||
path := filepath.Join(dir, "bus.sock")
|
||||
l, err := Listen(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Listen: %v", err)
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
st, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("stat socket directory: %v", err)
|
||||
}
|
||||
if mode := st.Mode().Perm(); mode != 0o700 {
|
||||
t.Fatalf("socket directory mode = %04o, want 0700", mode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageListenRejectsWorldAccessibleSocketDirectory(t *testing.T) {
|
||||
dir := filepath.Join(shortSecureTempDir(t), "dws-event-test")
|
||||
if err := os.Mkdir(dir, 0o700); err != nil {
|
||||
t.Fatalf("mkdir: %v", err)
|
||||
}
|
||||
if err := os.Chmod(dir, 0o777); err != nil {
|
||||
t.Fatalf("chmod: %v", err)
|
||||
}
|
||||
if _, err := Listen(filepath.Join(dir, "bus.sock")); err == nil || !strings.Contains(err.Error(), "want 0700") {
|
||||
t.Fatalf("Listen error = %v, want 0700 directory rejection", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageDialRejectsWorldAccessibleSocketDirectory(t *testing.T) {
|
||||
dir := filepath.Join(shortSecureTempDir(t), "dws-event-test")
|
||||
if err := os.Mkdir(dir, 0o700); err != nil {
|
||||
t.Fatalf("mkdir: %v", err)
|
||||
}
|
||||
if err := os.Chmod(dir, 0o777); err != nil {
|
||||
t.Fatalf("chmod: %v", err)
|
||||
}
|
||||
if _, err := Dial(filepath.Join(dir, "bus.sock")); err == nil || !strings.Contains(err.Error(), "want 0700") {
|
||||
t.Fatalf("Dial error = %v, want 0700 directory rejection", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageListenRejectsSymlinkSocketDirectory(t *testing.T) {
|
||||
root := shortSecureTempDir(t)
|
||||
target := filepath.Join(root, "target")
|
||||
if err := os.Mkdir(target, 0o700); err != nil {
|
||||
t.Fatalf("mkdir target: %v", err)
|
||||
}
|
||||
link := filepath.Join(root, "dws-event-test")
|
||||
if err := os.Symlink(target, link); err != nil {
|
||||
t.Fatalf("symlink: %v", err)
|
||||
}
|
||||
if _, err := Listen(filepath.Join(link, "bus.sock")); err == nil || !strings.Contains(err.Error(), "not a directory") {
|
||||
t.Fatalf("Listen error = %v, want symlink directory rejection", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageValidatePrivateSocketDirRejectsDifferentOwner(t *testing.T) {
|
||||
dir := shortSecureTempDir(t)
|
||||
st, err := os.Lstat(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("lstat: %v", err)
|
||||
}
|
||||
otherUID := uint32(os.Geteuid() + 1)
|
||||
if err := validatePrivateSocketDir(dir, st, otherUID); err == nil || !strings.Contains(err.Error(), "is owned by uid") {
|
||||
t.Fatalf("validatePrivateSocketDir error = %v, want owner mismatch", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageListenRejectsUntrustedRuntimeRoot(t *testing.T) {
|
||||
root := filepath.Join(shortSecureTempDir(t), "untrusted")
|
||||
if err := os.Mkdir(root, 0o700); err != nil {
|
||||
t.Fatalf("mkdir root: %v", err)
|
||||
}
|
||||
if err := os.Chmod(root, 0o777); err != nil {
|
||||
t.Fatalf("chmod root: %v", err)
|
||||
}
|
||||
dir := filepath.Join(root, "dws-event-test")
|
||||
if err := os.Mkdir(dir, 0o700); err != nil {
|
||||
t.Fatalf("mkdir socket dir: %v", err)
|
||||
}
|
||||
if _, err := Listen(filepath.Join(dir, "bus.sock")); err == nil || !strings.Contains(err.Error(), "neither private nor sticky") {
|
||||
t.Fatalf("Listen error = %v, want untrusted runtime root rejection", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageListenSharedWorkDirUsesLocalSecureRuntimeEndpoint(t *testing.T) {
|
||||
root := shortSecureTempDir(t)
|
||||
runtimeDir := filepath.Join(root, "runtime")
|
||||
if err := os.Mkdir(runtimeDir, 0o700); err != nil {
|
||||
t.Fatalf("mkdir runtime: %v", err)
|
||||
}
|
||||
t.Setenv("XDG_RUNTIME_DIR", runtimeDir)
|
||||
sharedWorkDir := filepath.Join(root, "simulated-nfs", "events", "open", "personal_stream", "identity")
|
||||
endpoint := dwsevent.IPCEndpoint(sharedWorkDir, "open", dwsevent.SourceKindPersonalStream, "identity")
|
||||
if strings.HasPrefix(endpoint, sharedWorkDir) {
|
||||
t.Fatalf("endpoint = %q, want socket outside shared WorkDir %q", endpoint, sharedWorkDir)
|
||||
}
|
||||
|
||||
l, err := Listen(endpoint)
|
||||
if err != nil {
|
||||
t.Fatalf("Listen on local runtime endpoint: %v", err)
|
||||
}
|
||||
defer l.Close()
|
||||
if mode, err := os.Stat(filepath.Dir(endpoint)); err != nil {
|
||||
t.Fatalf("stat runtime socket directory: %v", err)
|
||||
} else if mode.Mode().Perm() != 0o700 {
|
||||
t.Fatalf("runtime socket directory mode = %04o, want 0700", mode.Mode().Perm())
|
||||
}
|
||||
}
|
||||
|
||||
func TestListen_CloseUnlinksSocket(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "bus.sock")
|
||||
path := filepath.Join(shortSecureTempDir(t), "bus.sock")
|
||||
l, err := Listen(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Listen: %v", err)
|
||||
@@ -114,7 +247,7 @@ func TestListen_CloseUnlinksSocket(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDial_NoServerReturnsError(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "nonexistent.sock")
|
||||
path := filepath.Join(shortSecureTempDir(t), "nonexistent.sock")
|
||||
if _, err := Dial(path); err == nil {
|
||||
t.Fatal("Dial to nonexistent socket should error")
|
||||
}
|
||||
@@ -124,7 +257,7 @@ func TestDial_NoServerReturnsError(t *testing.T) {
|
||||
// surfaces as io.EOF to the server's Reader — the EOF signal is what bus
|
||||
// uses to unregister dead consumers (plan invariant #5).
|
||||
func TestReader_HandlesPeerCloseEOF(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "bus.sock")
|
||||
path := filepath.Join(shortSecureTempDir(t), "bus.sock")
|
||||
l, err := Listen(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Listen: %v", err)
|
||||
|
||||
@@ -853,6 +853,7 @@ func newAitableCommand() *cobra.Command {
|
||||
dws aitable form [list|delete|update] 表单管理
|
||||
dws aitable form field [list|update|hide] 表单字段管理
|
||||
dws aitable form share [get|update|notify] 表单分享管理
|
||||
dws aitable workflow [edit-example|create|update|enable|disable|get|list] 自动化工作流管理
|
||||
dws aitable dashboard [get|create|update|delete|config-example] 仪表盘管理
|
||||
dws aitable chart [get|create|update|delete|widgets-example] 图表管理
|
||||
dws aitable export data 数据导出
|
||||
@@ -3260,6 +3261,17 @@ valid=false 仍表示 DSL 校验或发布未通过,必须读取 issues 修正
|
||||
},
|
||||
}
|
||||
|
||||
workflowEditExampleCmd := &cobra.Command{
|
||||
Use: "edit-example",
|
||||
Short: "获取工作流编辑文档与示例",
|
||||
Long: `返回服务端提供的 AI 表格工作流编辑文档与示例。
|
||||
可作为 workflow create / workflow update 的 workflow-dsl/v1 结构参考;此命令不需要 Base ID 或其他参数。`,
|
||||
Example: ` dws aitable workflow edit-example`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return callAitableTool("edit_workflow_example", map[string]any{})
|
||||
},
|
||||
}
|
||||
|
||||
workflowUpdateCmd := &cobra.Command{
|
||||
Use: "update",
|
||||
Short: "更新并发布已有自动化工作流",
|
||||
@@ -4856,7 +4868,7 @@ parentSectionId 为空串表示该节点在 Base 根目录下。
|
||||
workflowListCmd.Flags().Int("limit", 0, "分页大小 [1, 100],不传走服务端默认 20")
|
||||
workflowListCmd.Flags().Int("offset", 0, "分页偏移量,>= 0,不传走服务端默认 0")
|
||||
workflowCmd.AddCommand(
|
||||
workflowCreateCmd, workflowUpdateCmd,
|
||||
workflowEditExampleCmd, workflowCreateCmd, workflowUpdateCmd,
|
||||
workflowEnableCmd, workflowDisableCmd,
|
||||
workflowGetCmd, workflowListCmd,
|
||||
)
|
||||
|
||||
@@ -94,6 +94,23 @@ func TestAitableWorkflowCreateMapsDSLWithoutRetry(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAitableWorkflowEditExampleMapsEmptyArguments(t *testing.T) {
|
||||
caller, err := runAitableWorkflowCommand(t, nil, "edit-example")
|
||||
if err != nil {
|
||||
t.Fatalf("workflow edit-example returned error: %v", err)
|
||||
}
|
||||
if len(caller.calls) != 1 {
|
||||
t.Fatalf("tool call count = %d, want 1", len(caller.calls))
|
||||
}
|
||||
call := caller.calls[0]
|
||||
if call.productID != "aitable" || call.toolName != "edit_workflow_example" {
|
||||
t.Fatalf("tool call = %s/%s, want aitable/edit_workflow_example", call.productID, call.toolName)
|
||||
}
|
||||
if len(call.args) != 0 {
|
||||
t.Fatalf("tool args = %#v, want empty arguments", call.args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAitableWorkflowUpdateReadsDSLFile(t *testing.T) {
|
||||
path := t.TempDir() + "/workflow.json"
|
||||
if err := os.WriteFile(path, []byte(`{"version":"workflow-dsl/v1","name":"updated"}`), 0o600); err != nil {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -93,7 +93,7 @@ cli_version: ">=1.0.15"
|
||||
| `mail` | 邮箱:邮箱地址查询/邮件搜索(KQL)/邮件详情/发送邮件 | [mail.md](./references/products/mail.md) |
|
||||
| `sheet` | 在线电子表格(axls):工作表 CRUD/区域读写/CSV 批量写入/行列增删/合并/查找替换/筛选视图/全局筛选/排序/下拉列表/条件格式/浮动图片/浮动图表/模板/导出 xlsx(单命令一站式) | [sheet.md](./references/products/sheet.md) |
|
||||
| `todo` | 待办:创建(含优先级/截止时间/循环)/查询/修改/标记完成/删除 | [todo.md](./references/products/todo.md) |
|
||||
| `wiki` | 知识库:空间创建/详情/列表/搜索 + 成员管理 | [wiki.md](./references/products/wiki.md) |
|
||||
| `wiki` | 知识库:空间创建/详情/列表/搜索 + 成员管理 + 知识库动态查询 | [wiki.md](./references/products/wiki.md) |
|
||||
| `event` | 个人 IM 事件:监听消息接收、指定发送人、已读、撤回、表情回应,NDJSON 输出(实时驱动 Agent)| [event.md](./references/products/event.md) |
|
||||
|
||||
## 意图判断决策树
|
||||
|
||||
@@ -105,6 +105,7 @@
|
||||
|
||||
| 命令 | 用途 | 必填参数 | 路由提醒 |
|
||||
|------|------|----------|----------|
|
||||
| `workflow edit-example` | 获取编辑文档与 DSL 示例 | 无 | create/update 前优先调用,内容由服务端提供 |
|
||||
| `workflow create` | 创建并发布工作流 | `--base-id` `--dsl` | `--dsl` 为完整 workflow-dsl/v1;非幂等,不自动重试 |
|
||||
| `workflow update` | 更新并发布工作流 | `--base-id` `--workflow-id` `--dsl` | 全量替换,先 get 留底;检查 `data.valid/issues` |
|
||||
| `workflow list` | 列出 Base 下所有工作流 | `--base-id` | 支持 `--limit [1,100]` / `--offset >=0`;list 出参字段叫 `flowId` |
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
| 命令 | 用途 |
|
||||
|------|------|
|
||||
| `workflow edit-example` | 获取工作流编辑文档与 workflow-dsl/v1 示例 |
|
||||
| `workflow create` | 创建并发布自动化工作流 |
|
||||
| `workflow update` | 更新并发布已有自动化工作流 |
|
||||
| `workflow list` | 列出 Base 下所有工作流(含状态/创建人/最后修改时间),支持分页 |
|
||||
@@ -14,11 +15,11 @@
|
||||
| `workflow enable` | 启用指定工作流(按配置的触发条件自动执行) |
|
||||
| `workflow disable` | 禁用指定工作流(高危,建议 `--yes` 二次确认) |
|
||||
|
||||
> 所有子命令的 `--base-id` 必填(可用隐藏别名 `--base`)。
|
||||
> `workflow edit-example` 无参数;其他子命令的 `--base-id` 必填(可用隐藏别名 `--base`)。
|
||||
|
||||
## DSL 入参格式与最小 Demo
|
||||
|
||||
`workflow create/update` 的 `--dsl` 接收完整的 `workflow-dsl/v1` JSON object,不是局部 patch。支持内联 JSON、`@文件路径` 或 `-` 从 stdin 读取。
|
||||
先运行 `workflow edit-example` 获取服务端提供的最新编辑文档和示例。`workflow create/update` 的 `--dsl` 接收完整的 `workflow-dsl/v1` JSON object,不是局部 patch。支持内联 JSON、`@文件路径` 或 `-` 从 stdin 读取。
|
||||
|
||||
复杂工作流应先用 `table get` / `field get` / `view list` 确认真实 `sheetId`、`fieldId`、`viewId`,并检查所有 `next`、`loopEntry`、branch `to` 和 ref。下面是一个不依赖数据表字段的最小定时消息工作流:
|
||||
|
||||
@@ -53,6 +54,14 @@ create 和 update 都必须同时满足 `status=success`、`data.valid=true`、`
|
||||
|
||||
## 命令详情
|
||||
|
||||
### workflow edit-example — 获取编辑文档与示例
|
||||
|
||||
```bash
|
||||
dws aitable workflow edit-example --format json
|
||||
```
|
||||
|
||||
该命令无业务参数,调用 `aitable/edit_workflow_example` 返回服务端提供的工作流编辑文档和示例。创建或更新复杂工作流前优先调用它,避免依赖可能过期的本地 DSL 结构。
|
||||
|
||||
### workflow create — 创建并发布工作流
|
||||
|
||||
```bash
|
||||
|
||||
@@ -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`。
|
||||
> - 下载凭证过期会自动刷新并继续下载,已完成的部分不会重下;单个分片失败会自动重试,无需手动处理。
|
||||
|
||||
### 创建文件夹
|
||||
|
||||
```
|
||||
|
||||
@@ -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) 交互优化原则。
|
||||
@@ -181,6 +181,30 @@ Flags:
|
||||
|
||||
> ⚠️ **返回字段限制**:`member list` 每条只返回 `name` / `role` / `type` 三个字段,**不含 userId**(服务端不返回)。因此**无法**从 `member list` 拿到 userId 再去串联 `member update` / `member remove`。要对某人改角色 / 移除,需另行拿到其 userId(例如用 `dws contact user search --query "<姓名>"` 按姓名反查)。
|
||||
|
||||
### 查询知识库动态
|
||||
```
|
||||
Usage:
|
||||
dws wiki feed list [flags]
|
||||
Aliases:
|
||||
list, ls
|
||||
Example:
|
||||
dws wiki feed list --workspace <workspaceId> --format json
|
||||
dws wiki feed list --workspace <workspaceId> --limit 10 --format json
|
||||
dws wiki feed list --workspace <workspaceId> --exclude-file --format json
|
||||
dws wiki feed list --workspace <workspaceId> --limit 10 --cursor <nextToken> --format json
|
||||
Flags:
|
||||
--workspace string 知识库 ID 或 URL (必填)
|
||||
--limit int 每页数量 (默认 20,最大 50)
|
||||
--cursor string 分页游标 (首页留空)
|
||||
--exclude-file 是否排除文件相关的动态 (默认 false)
|
||||
```
|
||||
|
||||
查询指定知识库的动态,返回谁在什么时间进行了更新、上传、评论等操作。
|
||||
支持传入知识库 ID 或知识库 URL,系统自动识别。
|
||||
支持分页,通过 `--cursor` 传入上次返回的 nextToken 获取下一页;出参 `hasMore` 指示是否还有下一页。
|
||||
|
||||
> **权限要求**:调用者需具备知识库的成员权限,非成员会被拒绝访问。
|
||||
|
||||
### 列出知识库节点
|
||||
```
|
||||
Usage:
|
||||
@@ -296,6 +320,8 @@ Flags:
|
||||
- 用户说"修改某人在知识库的权限/调整成员角色" → `member update`
|
||||
- 用户说"移除知识库成员/把某人从知识库移除/删除知识库成员" → `member remove`(需 `--workspace` + `--users`)
|
||||
- 用户说"知识库有哪些成员/查看知识库成员" → `member list`
|
||||
- 用户说"知识库动态/最近有什么更新/谁改了什么/知识库活动" → `feed list`(需 `--workspace`)
|
||||
- 用户说"知识库最近的评论/更新记录/操作日志" → `feed list`(需 `--workspace`)
|
||||
- 用户说"删除知识库/移除知识库/把知识库删了" → `space delete`(需 `--workspace`)
|
||||
|
||||
> **跨产品路由说明**:知识库节点的**内容操作**(读取/编辑/块级操作)仍由 `dws doc` 承担:
|
||||
@@ -382,6 +408,20 @@ dws wiki node move --workspace <workspaceId> --node <nodeId> --folder <targetFol
|
||||
# 删除节点(会要求确认)
|
||||
dws wiki node delete --workspace <workspaceId> --node <nodeId>
|
||||
|
||||
# ── 工作流: 查询知识库动态 ──
|
||||
|
||||
# 1. 获取知识库 ID
|
||||
dws wiki space list --format json
|
||||
|
||||
# 2. 查询知识库动态
|
||||
dws wiki feed list --workspace <workspaceId> --format json
|
||||
|
||||
# 3. 排除文件动态,只看文档操作
|
||||
dws wiki feed list --workspace <workspaceId> --exclude-file --format json
|
||||
|
||||
# 4. 翻页(cursor 取上一页返回的 nextToken)
|
||||
dws wiki feed list --workspace <workspaceId> --cursor <nextToken> --format json
|
||||
|
||||
# ── 工作流: 给知识库加成员 ──
|
||||
|
||||
# 1. 先确认知识库 ID(避免授权到「我的文档」)
|
||||
@@ -424,6 +464,7 @@ dws wiki space delete --workspace <workspaceId> --format json
|
||||
| `node list` | `nodeId` | node copy/move/delete 的 --node / `dws doc read` 的 --node |
|
||||
| `node search` | `nodeId` | node copy/move/delete 的 --node / `dws doc read` 的 --node |
|
||||
| `node create` | `nodeId` | node copy/move/delete 的 --node / `dws doc read` 的 --node |
|
||||
| `feed list` | `nextToken` | feed list 的 --cursor(翻页,`hasMore` 为 true 时继续)|
|
||||
| `member list` | `name` / `role` / `type`(**不含 userId**)| 仅用于查看成员名单;**无法**从这里取 userId 去串联 member update/remove,需另行按姓名反查 userId(如 `dws contact user search --query "<姓名>"`)|
|
||||
|
||||
## 相关产品
|
||||
|
||||
@@ -134,6 +134,7 @@ Flags:
|
||||
|
||||
| 命令 | 用途 | 必填参数 | 路由提醒 |
|
||||
|------|------|----------|----------|
|
||||
| `workflow edit-example` | 获取编辑文档与 DSL 示例 | 无 | create/update 前优先调用,内容由服务端提供 |
|
||||
| `workflow create` | 创建并发布自动化工作流 | `--base-id` `--dsl` | 按子文档 Demo 组装 DSL;必须检查返回的 `data.valid` / `issues`;create 不自动重试 |
|
||||
| `workflow update` | 更新并发布已有自动化工作流 | `--base-id` `--workflow-id` `--dsl` | 先 get 留底;提交完整目标 DSL;必须检查 `data.valid` / `issues` |
|
||||
| `workflow list` | 列出 Base 下所有工作流 | `--base-id` | 支持 `--limit [1,100]` / `--offset >=0`;list 出参字段叫 `flowId` |
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
| 命令 | 用途 |
|
||||
|------|------|
|
||||
| `workflow edit-example` | 获取工作流编辑文档与 workflow-dsl/v1 示例 |
|
||||
| `workflow create` | 创建并发布自动化工作流 |
|
||||
| `workflow update` | 更新并发布已有自动化工作流 |
|
||||
| `workflow list` | 列出 Base 下所有工作流(含状态/创建人/最后修改时间),支持分页 |
|
||||
@@ -14,15 +15,15 @@
|
||||
| `workflow enable` | 启用指定工作流(按配置的触发条件自动执行) |
|
||||
| `workflow disable` | 禁用指定工作流(高危,建议 `--yes` 二次确认) |
|
||||
|
||||
> 所有子命令的 `--base-id` 必填(可用隐藏别名 `--base`)。
|
||||
> `workflow edit-example` 无参数;其他子命令的 `--base-id` 必填(可用隐藏别名 `--base`)。
|
||||
|
||||
## DSL 入参格式与最小 Demo
|
||||
|
||||
`workflow create/update` 的 `--dsl` 接收钉钉 AI 表格 `workflow-dsl/v1` JSON object。当前同步范围只包含 create/update,没有新增 DSL 文档子命令;其他 Agent 可以直接使用下面的最小 Demo 理解调用格式。
|
||||
先运行 `workflow edit-example` 获取服务端提供的最新编辑文档和示例。`workflow create/update` 的 `--dsl` 接收钉钉 AI 表格 `workflow-dsl/v1` JSON object。
|
||||
|
||||
复杂工作流还应注意:
|
||||
|
||||
1. 如果 Agent 运行环境直接提供 AI 表格 MCP 的 `get_workflow_dsl_docs`,可用它获取最新 DSL Guide、Schema 和示例。
|
||||
1. 使用 `workflow edit-example` 获取最新 DSL Guide、结构和示例。
|
||||
2. 涉及数据表、字段或视图的节点,先用 `table get` / `field get` / `view list` 确认真实 `sheetId`、`fieldId`、`viewId`。
|
||||
3. create 和 update 都提交完整的 workflow-dsl/v1 JSON object,并检查所有 `next`、`loopEntry`、branch `to` 和 ref。
|
||||
|
||||
@@ -82,6 +83,14 @@ create 和 update 都必须同时满足 `status=success`、`data.valid=true`、`
|
||||
|
||||
## 命令详情
|
||||
|
||||
### workflow edit-example — 获取编辑文档与示例
|
||||
|
||||
```bash
|
||||
dws aitable workflow edit-example --format json
|
||||
```
|
||||
|
||||
该命令无业务参数,调用 `aitable/edit_workflow_example` 返回服务端提供的工作流编辑文档和示例。创建或更新复杂工作流前优先调用它,避免依赖可能过期的本地 DSL 结构。
|
||||
|
||||
### workflow create — 创建并发布工作流
|
||||
|
||||
```bash
|
||||
|
||||
@@ -170,14 +170,23 @@ Usage:
|
||||
Example:
|
||||
dws drive download --node <dentryUuid> --output ./report.pdf
|
||||
dws drive download --node <dentryUuid> --output ~/downloads/
|
||||
dws drive download --node <dentryUuid> --output ./big.zip --part-size 32MB --parallel 8
|
||||
Flags:
|
||||
--node string 文件 ID (dentryUuid) (必填)
|
||||
--output string 本地保存路径 (文件路径或目录,不传则保存到当前目录);如果指定目录,文件名从下载 URL 中自动推断 (可选)
|
||||
--space-id string 文件所属空间 ID (可选)
|
||||
--part-size string 分片下载的分片大小,支持 KB/MB/GB 单位,范围 1MB-1GB (默认 16MB)
|
||||
--parallel int 分片下载并发数,范围 1-8 (默认 4)
|
||||
--no-resume 关闭断点续传,忽略历史下载进度从头下载 (默认开启续传)
|
||||
```
|
||||
|
||||
> **提示**:`--output` 为可选参数,不传则保存到当前目录,文件名从下载 URL 中自动推断。
|
||||
|
||||
> **大文件分片下载**:
|
||||
> - 大文件自动分片并发下载,小文件整流下载,行为对用户透明,无需任何额外操作。
|
||||
> - 断点续传默认开启:下载中断后重跑同一命令会自动跳过已完成部分继续下载(`<目标文件>.dwspart` 为临时进度文件,下载完成后自动清理);不需要续传时加 `--no-resume`。
|
||||
> - 下载凭证过期会自动刷新并继续下载,已完成的部分不会重下;单个分片失败会自动重试,无需手动处理。
|
||||
|
||||
### 创建文件夹
|
||||
|
||||
```
|
||||
@@ -526,6 +535,7 @@ Flags:
|
||||
|
||||
> **两步下载流程**:先调用 MCP 工具获取历史版本下载 URL 和签名头,再 HTTP GET 下载文件内容到本地。
|
||||
> `--output` 指定目录时,优先从文件信息中获取原始文件名,获取不到时从下载 URL 推断。
|
||||
> 历史版本下载同样支持 `--part-size` / `--parallel` / `--no-resume` 分片下载参数,行为与最新版下载一致。
|
||||
|
||||
#### 回滚文件到指定历史版本
|
||||
|
||||
|
||||
@@ -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) 交互优化原则。
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user