diff --git a/internal/helpers/drive.go b/internal/helpers/drive.go index 32c28853..8d39c994 100644 --- a/internal/helpers/drive.go +++ b/internal/helpers/drive.go @@ -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", diff --git a/internal/helpers/drive_permission_get_setting_test.go b/internal/helpers/drive_permission_get_setting_test.go index 9e73f99f..fdb03b2a 100644 --- a/internal/helpers/drive_permission_get_setting_test.go +++ b/internal/helpers/drive_permission_get_setting_test.go @@ -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", ""}) { + 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", ""}) { + t.Fatalf("visibility enum = %#v", got) + } + if got := schemaEnumValues(t, shareScopeProperties["defaultRole"].(map[string]any)); !reflect.DeepEqual(got, []string{"READER", "DOWNLOADER", "EDITOR", "MANAGER", ""}) { + 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, "") + default: + t.Fatalf("schema enum value = %#v, want string or null", value) + } + } + return values +} diff --git a/skills/mono/references/products/drive.md b/skills/mono/references/products/drive.md index c2944f99..80a80b9f 100644 --- a/skills/mono/references/products/drive.md +++ b/skills/mono/references/products/drive.md @@ -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=不低于该角色才允许 + ### 文件互联网公开发布 管理文件的互联网公开发布状态。公开后任何人通过链接即可访问,无需登录钉钉。操作者需要是文件的管理员或拥有者。 diff --git a/skills/multi/dingtalk-drive/references/drive.md b/skills/multi/dingtalk-drive/references/drive.md index cef544d7..596beef6 100644 --- a/skills/multi/dingtalk-drive/references/drive.md +++ b/skills/multi/dingtalk-drive/references/drive.md @@ -80,6 +80,8 @@ dws drive +publish-unset --node 只在意图命中时读取一个精确 leaf Schema。成员变更、转移所有者、发起申请和公开状态变更必须明确节点、用户、角色与影响范围。转移所有者时,在构造最终命令前必须让用户分别明确决定 `--reserve-role ` 和 `--recursive=`;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 不得默认选择第一位审批人、最高/最低角色或代写申请理由;用户未明确同意完整申请内容时停在确认环节。 ## 快捷方式节点