feat(drive): document get-setting response schema and field semantics
Declare the drive.permission get_setting ResultSpec data schema from the server-side output metadata so the field-level contract (permissionMode, shareScope, policy codes and their typed value domains, inherited / allowedValues semantics) is reviewed code guarded by contract tests, and add field-quick-reference rows for the get-setting section in both mono and multi drive skill references.
This commit is contained in:
1 parent
95ecc99c41
commit
09ecaaa7a1
4 files changed
+200
No files matched your search
@@ -2369,6 +2369,60 @@ func newDriveCommand() *cobra.Command {
|
||||
PrimaryCLIPath: "drive permission get-setting",
|
||||
},
|
||||
Description: "查询文档空间节点的权限设置(权限模式/分享范围/权限策略)",
|
||||
Result: &contract.ResultSpec{
|
||||
Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomeFailure},
|
||||
DataSchema: json.RawMessage(`{
|
||||
"type":"object",
|
||||
"description":"节点权限设置(权限模式/分享范围/权限策略)",
|
||||
"properties":{
|
||||
"docUrl":{"type":"string","description":"当前查询节点的文档访问链接,可直接在浏览器中打开"},
|
||||
"nodeId":{"type":"string","description":"当前查询节点的 nodeId(入参解析后的规范形式)"},
|
||||
"permissionMode":{"type":["string","null"],"enum":["INHERITED","INDEPENDENT",null],"description":"权限模式:INHERITED=继承上级权限配置,INDEPENDENT=独立管理权限;未知时为 null"},
|
||||
"shareScope":{
|
||||
"type":"object",
|
||||
"description":"分享范围设置",
|
||||
"properties":{
|
||||
"visibility":{"type":["string","null"],"enum":["PRIVATE","ORGANIZATION","PUBLIC",null],"description":"PRIVATE=仅指定成员可见,ORGANIZATION=组织内公开,PUBLIC=互联网公开;未知时为 null"},
|
||||
"partnerIncluded":{"type":"boolean","description":"仅 visibility=ORGANIZATION 时有意义,true 表示组织内公开范围包含合作伙伴(含生态组织外部协作成员)。其余场景为 false。"},
|
||||
"defaultRole":{"type":["string","null"],"enum":["READER","DOWNLOADER","EDITOR","MANAGER",null],"description":"仅 visibility=ORGANIZATION 时有意义,通过链接获得访问的默认角色;未下发或不在值域内时为 null"},
|
||||
"canSearch":{"type":"boolean","description":"仅 visibility=ORGANIZATION 时有意义。"},
|
||||
"canRecommend":{"type":"boolean","description":"仅 visibility=ORGANIZATION 时有意义。"},
|
||||
"linkShare":{
|
||||
"type":"object",
|
||||
"description":"链接分享设置",
|
||||
"properties":{
|
||||
"requirePassword":{"type":"boolean","description":"true 表示通过链接访问需要提供密码。密码明文不会返回。"},
|
||||
"expireAt":{"type":["integer","null"],"description":"秒级 Unix 时间戳,未设置过期时为 null。"},
|
||||
"expireDays":{"type":["integer","null"],"description":"设置的有效天数,未设置时为 null。"},
|
||||
"forCurrentNode":{"type":"boolean","description":"true 表示该分享范围仅作用于当前节点;false 表示作用于当前节点及其子节点。"}
|
||||
},
|
||||
"additionalProperties":true
|
||||
}
|
||||
},
|
||||
"required":["linkShare"],
|
||||
"additionalProperties":true
|
||||
},
|
||||
"policies":{
|
||||
"type":"array",
|
||||
"description":"仅包含支持的策略项,未下发或不受支持的策略不会返回;node_spread_scope 仅文件夹类节点返回",
|
||||
"items":{
|
||||
"type":"object",
|
||||
"description":"权限策略项",
|
||||
"properties":{
|
||||
"code":{"type":"string","enum":["external_share","external_share_manager_only","member_invite","member_invite_org_only","comment","permission_apply","external_permission_apply","watermark","node_spread","online_content_copy","node_move_forbidden","node_spread_scope"],"description":"external_share=允许分享到组织外;external_share_manager_only=仅管理员可分享到组织外;member_invite=添加成员的角色门槛;member_invite_org_only=仅可添加组织内成员;comment=评论权限门槛;permission_apply=允许申请权限;external_permission_apply=允许组织外用户申请权限;watermark=水印;node_spread=转发/下载等传播的允许门槛;online_content_copy=在线文档内容复制的允许门槛;node_move_forbidden=是否禁止移动节点(ON=禁止移动,OFF=允许移动);node_spread_scope=文件夹传播范围(仅文件夹类节点)。"},
|
||||
"value":{"type":["string","null"],"description":"取值随策略类型不同:开关型(external_share、external_share_manager_only、member_invite_org_only、permission_apply、external_permission_apply、watermark、node_move_forbidden)为 ON/OFF;阈值型(member_invite、comment)为 READER_AND_ABOVE/DOWNLOADER_AND_ABOVE/EDITOR_AND_ABOVE/MANAGER_AND_ABOVE,阈值型(node_spread、online_content_copy)为 DOWNLOADER_AND_ABOVE/EDITOR_AND_ABOVE/MANAGER_AND_ABOVE/NOBODY,均表示不低于该角色才允许对应操作,NOBODY 表示所有人禁止;二值型(node_spread_scope):ALL_CONTENT=可传播全部内容,PREVIEWABLE_ONLY=开启限制、仅支持可预览文件;未知值时为 null"},
|
||||
"inherited":{"type":["boolean","null"],"description":"true 表示该策略值继承自上级(父节点/知识库)配置;未下发时为 null"},
|
||||
"allowedValues":{"type":["array","null"],"items":{"type":"string"},"description":"该策略当前可设置的取值(与 value 同一值域),未下发时为 null"}
|
||||
},
|
||||
"required":["code"],
|
||||
"additionalProperties":true
|
||||
}
|
||||
}
|
||||
},
|
||||
"required":["docUrl","nodeId","shareScope","policies"],
|
||||
"additionalProperties":true
|
||||
}`),
|
||||
},
|
||||
Interface: &contract.InterfaceSpec{
|
||||
Mode: "mcp",
|
||||
Availability: "available",
|
||||
|
||||
@@ -5,8 +5,13 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contractfinal"
|
||||
)
|
||||
|
||||
// ── drive permission get-setting:跨产品路由与 --node 别名归一化 ──
|
||||
@@ -62,3 +67,135 @@ func TestCrossPlatformCoverageDrivePermissionGetSettingRequiresNode(t *testing.T
|
||||
t.Fatalf("calls = %#v, want none before required-flag validation", caller.calls)
|
||||
}
|
||||
}
|
||||
|
||||
// ── drive permission get-setting:ResultSpec 返回值契约 ──
|
||||
|
||||
func TestCrossPlatformCoverageDrivePermissionGetSettingResultContract(t *testing.T) {
|
||||
drive := newDriveCommand()
|
||||
leaf, _, err := drive.Find([]string{"permission", "get-setting"})
|
||||
if err != nil || leaf == nil {
|
||||
t.Fatalf("find drive permission get-setting: command=%v err=%v", leaf, err)
|
||||
}
|
||||
final, ok := contractfinal.RuntimeContractFinal(leaf)
|
||||
if !ok || final.Identity == nil || final.Identity.CanonicalPath != "drive.get_permission_setting" {
|
||||
t.Fatalf("get-setting ContractFinal identity = %#v, found = %v", final.Identity, ok)
|
||||
}
|
||||
if final.Result == nil {
|
||||
t.Fatal("get-setting final Result is nil")
|
||||
}
|
||||
wantOutcomes := []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomeFailure}
|
||||
if !reflect.DeepEqual(final.Result.Outcomes, wantOutcomes) {
|
||||
t.Fatalf("outcomes = %#v, want %#v", final.Result.Outcomes, wantOutcomes)
|
||||
}
|
||||
|
||||
var root map[string]any
|
||||
if err := json.Unmarshal(final.Result.DataSchema, &root); err != nil {
|
||||
t.Fatalf("result data_schema is not JSON: %v\n%s", err, final.Result.DataSchema)
|
||||
}
|
||||
assertSchemaRequired(t, root, "docUrl", "nodeId", "shareScope", "policies")
|
||||
properties := resultSchemaProperties(t, final.Result.DataSchema)
|
||||
if got := sortedContractSchemaKeys(properties); !reflect.DeepEqual(got, []string{"docUrl", "nodeId", "permissionMode", "policies", "shareScope"}) {
|
||||
t.Fatalf("result properties = %#v", got)
|
||||
}
|
||||
|
||||
permissionMode, ok := properties["permissionMode"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("permissionMode = %#v, want schema object", properties["permissionMode"])
|
||||
}
|
||||
if got := schemaEnumValues(t, permissionMode); !reflect.DeepEqual(got, []string{"INHERITED", "INDEPENDENT", "<null>"}) {
|
||||
t.Fatalf("permissionMode enum = %#v", got)
|
||||
}
|
||||
if types, ok := permissionMode["type"].([]any); !ok || len(types) != 2 || types[0] != "string" || types[1] != "null" {
|
||||
t.Fatalf("permissionMode type = %#v, want [string null]", permissionMode["type"])
|
||||
}
|
||||
|
||||
shareScope := schemaObjectProperty(t, properties, "shareScope")
|
||||
shareScopeProperties := schemaProperties(t, shareScope)
|
||||
if got := sortedContractSchemaKeys(shareScopeProperties); !reflect.DeepEqual(got, []string{"canRecommend", "canSearch", "defaultRole", "linkShare", "partnerIncluded", "visibility"}) {
|
||||
t.Fatalf("shareScope properties = %#v", got)
|
||||
}
|
||||
assertSchemaRequired(t, shareScope, "linkShare")
|
||||
if got := schemaEnumValues(t, shareScopeProperties["visibility"].(map[string]any)); !reflect.DeepEqual(got, []string{"PRIVATE", "ORGANIZATION", "PUBLIC", "<null>"}) {
|
||||
t.Fatalf("visibility enum = %#v", got)
|
||||
}
|
||||
if got := schemaEnumValues(t, shareScopeProperties["defaultRole"].(map[string]any)); !reflect.DeepEqual(got, []string{"READER", "DOWNLOADER", "EDITOR", "MANAGER", "<null>"}) {
|
||||
t.Fatalf("defaultRole enum = %#v", got)
|
||||
}
|
||||
linkShareProperties := schemaProperties(t, schemaObjectProperty(t, shareScopeProperties, "linkShare"))
|
||||
if got := sortedContractSchemaKeys(linkShareProperties); !reflect.DeepEqual(got, []string{"expireAt", "expireDays", "forCurrentNode", "requirePassword"}) {
|
||||
t.Fatalf("linkShare properties = %#v", got)
|
||||
}
|
||||
|
||||
policies, ok := properties["policies"].(map[string]any)
|
||||
if !ok || policies["type"] != "array" {
|
||||
t.Fatalf("policies = %#v, want array schema", properties["policies"])
|
||||
}
|
||||
policiesDescription, _ := policies["description"].(string)
|
||||
if !strings.Contains(policiesDescription, "未下发或不受支持的策略不会返回") || !strings.Contains(policiesDescription, "node_spread_scope 仅文件夹类节点返回") {
|
||||
t.Fatalf("policies description = %q", policiesDescription)
|
||||
}
|
||||
items, ok := policies["items"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("policies items = %#v", policies["items"])
|
||||
}
|
||||
assertSchemaRequired(t, items, "code")
|
||||
itemProperties := schemaProperties(t, items)
|
||||
if got := sortedContractSchemaKeys(itemProperties); !reflect.DeepEqual(got, []string{"allowedValues", "code", "inherited", "value"}) {
|
||||
t.Fatalf("policy item properties = %#v", got)
|
||||
}
|
||||
code, ok := itemProperties["code"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("code = %#v", itemProperties["code"])
|
||||
}
|
||||
if got := schemaEnumValues(t, code); !reflect.DeepEqual(got, []string{
|
||||
"external_share", "external_share_manager_only", "member_invite", "member_invite_org_only",
|
||||
"comment", "permission_apply", "external_permission_apply", "watermark", "node_spread",
|
||||
"online_content_copy", "node_move_forbidden", "node_spread_scope",
|
||||
}) {
|
||||
t.Fatalf("code enum = %#v", got)
|
||||
}
|
||||
value, ok := itemProperties["value"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("value = %#v", itemProperties["value"])
|
||||
}
|
||||
valueDescription, _ := value["description"].(string)
|
||||
for _, fragment := range []string{"ON/OFF", "READER_AND_ABOVE", "NOBODY", "ALL_CONTENT", "PREVIEWABLE_ONLY", "不低于该角色才允许", "所有人禁止"} {
|
||||
if !strings.Contains(valueDescription, fragment) {
|
||||
t.Fatalf("value description missing %q: %s", fragment, valueDescription)
|
||||
}
|
||||
}
|
||||
inherited, ok := itemProperties["inherited"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("inherited = %#v", itemProperties["inherited"])
|
||||
}
|
||||
if inheritedDescription, _ := inherited["description"].(string); !strings.Contains(inheritedDescription, "继承自上级") {
|
||||
t.Fatalf("inherited description = %q", inherited["description"])
|
||||
}
|
||||
allowedValues, ok := itemProperties["allowedValues"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("allowedValues = %#v", itemProperties["allowedValues"])
|
||||
}
|
||||
if allowedValuesDescription, _ := allowedValues["description"].(string); !strings.Contains(allowedValuesDescription, "与 value 同一值域") {
|
||||
t.Fatalf("allowedValues description = %q", allowedValues["description"])
|
||||
}
|
||||
}
|
||||
|
||||
func schemaEnumValues(t *testing.T, property map[string]any) []string {
|
||||
t.Helper()
|
||||
raw, ok := property["enum"].([]any)
|
||||
if !ok {
|
||||
t.Fatalf("schema enum = %#v, want array", property["enum"])
|
||||
}
|
||||
values := make([]string, 0, len(raw))
|
||||
for _, value := range raw {
|
||||
switch typed := value.(type) {
|
||||
case string:
|
||||
values = append(values, typed)
|
||||
case nil:
|
||||
values = append(values, "<null>")
|
||||
default:
|
||||
t.Fatalf("schema enum value = %#v, want string or null", value)
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
@@ -758,6 +758,13 @@ Flags:
|
||||
|
||||
`get-setting` 返回节点权限配置(不是成员清单):`permissionMode`(INHERITED 继承上级 / INDEPENDENT 独立管理)、`shareScope`(可见范围与链接分享设置)、`policies`(水印、组织外分享、添加成员门槛等策略列表)。查询协作者清单仍用 `permission list`。
|
||||
|
||||
get-setting 返回字段说明:
|
||||
- `permissionMode` — INHERITED(继承上级)/ INDEPENDENT(独立管理),未知时为 null
|
||||
- `shareScope` — `visibility`(PRIVATE/ORGANIZATION/PUBLIC);`partnerIncluded`、`defaultRole`、`canSearch`、`canRecommend` 仅 ORGANIZATION 有意义;`linkShare`:`requirePassword`(密码明文不返回)、`expireAt`/`expireDays`(未设置为 null)、`forCurrentNode`
|
||||
- `policies[]` — 每项含 `code`(策略码)、`value`(当前值)、`inherited`(true=继承上级配置)、`allowedValues`(可设置值域);未下发或不支持的策略不返回;`node_spread_scope` 仅文件夹类节点返回
|
||||
- `value` 按策略分型:开关型(external_share、external_share_manager_only、member_invite_org_only、permission_apply、external_permission_apply、watermark、node_move_forbidden)为 ON/OFF;member_invite、comment 为 READER/DOWNLOADER/EDITOR/MANAGER_AND_ABOVE(无 NOBODY);node_spread、online_content_copy 为 DOWNLOADER/EDITOR/MANAGER_AND_ABOVE 或 NOBODY(无 READER_AND_ABOVE);node_spread_scope 为 ALL_CONTENT(可传播全部内容)/ PREVIEWABLE_ONLY(仅可预览文件)
|
||||
- 方向语义:NOBODY=该操作对所有人禁止;XXX_AND_ABOVE=不低于该角色才允许
|
||||
|
||||
### 文件互联网公开发布
|
||||
|
||||
管理文件的互联网公开发布状态。公开后任何人通过链接即可访问,无需登录钉钉。操作者需要是文件的管理员或拥有者。
|
||||
|
||||
@@ -80,6 +80,8 @@ dws drive +publish-unset --node <dentryUuid>
|
||||
|
||||
只在意图命中时读取一个精确 leaf Schema。成员变更、转移所有者、发起申请和公开状态变更必须明确节点、用户、角色与影响范围。转移所有者时,在构造最终命令前必须让用户分别明确决定 `--reserve-role <MANAGER|EDITOR|DOWNLOADER|READER|NONE>` 和 `--recursive=<true|false>`;Agent 不得根据默认值、对象类型或便捷性自行选择任一项。两项决策与目标、新所有者均明确后,才按 Runtime confirmation 构造首次正式调用。
|
||||
|
||||
`permission get-setting` 返回 `permissionMode`(INHERITED/INDEPENDENT,未知时为 null)、`shareScope`(可见范围与链接分享,密码明文不返回;`partnerIncluded`、`defaultRole` 等仅 ORGANIZATION 有意义)和 `policies[]`(code/value/inherited/allowedValues;未下发的策略不返回,`node_spread_scope` 仅文件夹)。`value` 按策略分型:开关型为 ON/OFF;member_invite、comment 为 READER/DOWNLOADER/EDITOR/MANAGER_AND_ABOVE;node_spread、online_content_copy 为 DOWNLOADER/EDITOR/MANAGER_AND_ABOVE 或 NOBODY;node_spread_scope 为 ALL_CONTENT(可传播全部内容)/ PREVIEWABLE_ONLY(仅可预览文件)。NOBODY=该操作对所有人禁止;XXX_AND_ABOVE=不低于该角色才允许。
|
||||
|
||||
发起权限申请先只读执行 `permission apply-info`。正式 `permission apply` 会通知审批人;调用前必须向用户逐项回显并确认资源、申请角色、审批人和理由。Agent 不得默认选择第一位审批人、最高/最低角色或代写申请理由;用户未明确同意完整申请内容时停在确认环节。
|
||||
|
||||
## 快捷方式节点
|
||||
|
||||
Reference in new issue
Block a user