diff --git a/AGENTS.md b/AGENTS.md index cd7366db..edee745c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,7 +28,8 @@ changes must not rewrite it mechanically. - Framework definition: `docs/rfc-command-framework-convergence.md` **§5.0** - Today: `helpers.LeafSpec` / `shortcut.Shortcut` → `corecmd.Spec` (+ optional `Schema`) → `corecmd.New` - **Declare = final Schema source**: `Flags` / `Constraints` / `Safety` / `ConstParams` / `Schema` (ToolSpec groups) -- `Safety` uses `contract.SafetySpec` (`internal/corecmd/contract`; `cli.SafetySpec` is a thin alias). Its `confirmation` drives the runtime gate; `effect` / `risk` / `idempotency` are published unchanged. When `Schema` is set, convert once → `RegisterRuntimeContractFinal` (map store, no JSON/deep-clone); assembly **pass-throughs** Final. +- `Safety` uses `contract.SafetySpec` (`internal/corecmd/contract` only — no `cli.*` type alias). Its `confirmation` drives the runtime gate; `effect` / `risk` / `idempotency` are published unchanged. When `Schema` is set, convert once → `contract.RegisterRuntimeContractFinal` (map store, no JSON/deep-clone) after `cli.AnnotateRuntimeContract`; assembly **pass-throughs** Final. +- Package seam: types/registries → `corecmd/contract`; Cobra annotation helpers (`AnnotateRuntime*`) and Catalog/`ResolveMeta` delivery → `internal/cli`. Framework may call annotation helpers; cli does not redefine contract types. - **Execute** = hooks (`Validate` / `Call` / `RunE` / `PostMount`) — not a second surface authority - Declaration path has **no reviewed parallel fields**; migration-only `runtime_gate` annotate until `Safety` is declared diff --git a/docs/command-domain-model.md b/docs/command-domain-model.md index 6920b32d..c843bd90 100644 --- a/docs/command-domain-model.md +++ b/docs/command-domain-model.md @@ -27,7 +27,7 @@ │ └────────────────────────────────────────┘ │ │ │ │ ┌─── 安全模型 ──────────────────────────────────────────────────┐ │ -│ │ Safety cli.SafetySpec │ │ +│ │ Safety contract.SafetySpec │ │ │ │ ├─ Effect (read / write / destructive) │ │ │ │ ├─ Risk (low / medium / high) │ │ │ │ ├─ Confirmation (not_required / user_required) ──▶ 运行时门 │ │ @@ -84,7 +84,7 @@ corecmd.Spec ──── corecmd.New() ────▶ cobra.Command ─── | **corecmd.Spec** | struct | 一个命令的完整契约(声明 + 执行) | | **FlagSpec** | struct | 一个参数的注册、回退链、绑定规则 | | **Constraint** | struct | 参数间的关系约束 | -| **Safety** | cli.SafetySpec | 运行时与 Schema 共用的安全契约 | +| **Safety** | contract.SafetySpec | 运行时与 Schema 共用的安全契约 | | **SchemaDecl** | struct | Agent 可见的完整工具规格声明 | | **SelectionDecl** | struct | Agent 选择该工具的语义指引 | | **InterfaceDecl** | struct | 工具的接口模式与可用性 | @@ -96,10 +96,10 @@ corecmd.Spec ──── corecmd.New() ────▶ cobra.Command ─── ## SafetySpec -`corecmd.Spec.Safety` 直接复用 `cli.SafetySpec`,没有 command 自定义 Risk/Safety 枚举,也没有 `SafetyDecl` 覆盖层: +`corecmd.Spec.Safety` 使用 `corecmd/contract.SafetySpec`(无 cli 类型别名),没有 command 自定义 Risk/Safety 枚举,也没有 `SafetyDecl` 覆盖层: ```go -Safety: cli.SafetySpec{ +Safety: contract.SafetySpec{ Effect: "write", Risk: "high", Confirmation: "user_required", diff --git a/docs/command-framework-architecture.md b/docs/command-framework-architecture.md index 182e7caf..69b86331 100644 --- a/docs/command-framework-architecture.md +++ b/docs/command-framework-architecture.md @@ -30,7 +30,7 @@ type Spec struct { Example string Flags []FlagSpec Constraints []Constraint - Safety cli.SafetySpec // 运行时与 Schema 的单一安全来源 + Safety contract.SafetySpec // 运行时与 Schema 的单一安全来源 ConfirmFirst bool // 确认门先于参数校验 ConstParams map[string]any Schema SchemaDecl // 完整 ToolSpec 载荷 @@ -48,7 +48,7 @@ type Spec struct { ### SafetySpec(单一安全来源) -`Spec.Safety` 直接使用现有 Agent Runtime Schema 的 `cli.SafetySpec`: +`Spec.Safety` 使用 `corecmd/contract.SafetySpec`: | 字段 | 职责 | |------|------| @@ -60,7 +60,7 @@ type Spec struct { 四个字段彼此独立。框架只读取 `Confirmation` 决定运行时确认,其余字段原样发布到 Schema,不从一个字段机械推导另一个。非空 SafetySpec 必须一次声明完整: ```go -Safety: cli.SafetySpec{ +Safety: contract.SafetySpec{ Effect: "write", Risk: "high", Confirmation: "user_required", @@ -167,7 +167,7 @@ func newDevAppCreateCommand(runner executor.Runner) *cobra.Command { Use: "create", Short: "创建开放平台企业内部应用", Tool: devAppCreateTool, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, @@ -237,7 +237,7 @@ spec := FromShortcut(Shortcut{ ``` Shortcut 当前仍保留自身的 `Risk`,adapter 只在边界将它展开成完整 -`cli.SafetySpec`;command/Leaf 不再保留该枚举。Shortcut 的 Cobra +`contract.SafetySpec`;command/Leaf 不再保留该枚举。Shortcut 的 Cobra type/default/usage provenance 保持不变,command 统一补充 Required、Enum 和关系约束投影。 ## 文件结构 diff --git a/docs/command-framework-comparison.md b/docs/command-framework-comparison.md index 9d54cbe2..02f4657e 100644 --- a/docs/command-framework-comparison.md +++ b/docs/command-framework-comparison.md @@ -20,7 +20,7 @@ ``` corecmd.Spec (声明) → corecmd.New() → cobra.Command │ - ├── cli.SafetySpec (运行时 + Schema 单一安全来源) + ├── contract.SafetySpec (运行时 + Schema 单一安全来源) ├── FlagSpec[] (参数 + 回退链 + 绑定) ├── Constraint[] (互斥/至少一个) ├── SchemaDecl (Agent Selection/DryRun/Interface) @@ -103,7 +103,7 @@ API Discovery → 代码生成 → surface command |------|-------------|----------|-----| | 工具发现 | `dws schema --all` (静态 catalog) | `--print-schema` (运行时) | API Discovery | | 选择指引 | SelectionDecl (UseWhen/AvoidWhen) | Description + Tips | 无 | -| 安全声明 | cli.SafetySpec 直接声明 | Risk string | 无 | +| 安全声明 | contract.SafetySpec 直接声明 | Risk string | 无 | | dry-run 能力声明 | DryRunDecl (reviewed) | DryRun hook 存在性 | 无 | | 接口模式 | InterfaceDecl (local/pinned/composite) | 隐式 (全部 REST) | 隐式 (全部 REST) | @@ -169,7 +169,7 @@ NewLeafCommand(LeafSpec{ Use: "create", Short: "创建应用", Tool: "create_dev_app", - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/docs/flag-help-schema-homology.md b/docs/flag-help-schema-homology.md index 9e1de37e..fc36981e 100644 --- a/docs/flag-help-schema-homology.md +++ b/docs/flag-help-schema-homology.md @@ -69,7 +69,7 @@ command/Leaf 不再写 `dws.schema.risk`;SafetySpec 走类型化 Final 载荷 |---|---|---|---| | `Flags[]`(`FlagSpec` / `LeafFlag`) | 用户可见参数面:名、类型、默认、必填、usage | 注册 cobra flag;装配 toolArgs | `dws.schema.property` / `type` / `required`;`--help` Flags | | `Constraints[]` | 跨 flag 关系:`at_least_one` / `exactly_one` / `mutually_exclusive`;`custom` 记录钩子校验 | 通用关系由 `ValidateConstraints` 执行;`custom` 由 `Validate` 执行 | `dws.schema.constraints`;`--help`「参数约束」 | -| `Safety`(`cli.SafetySpec`) | effect/risk/confirmation/idempotency 四个独立事实 | `confirmation=user_required` 时 `ConfirmSafety`;`--yes` / `--dry-run` 跳过 | 同一个 SafetySpec 原样进入 Contract Final(`HOM-S1`) | +| `Safety`(`contract.SafetySpec`) | effect/risk/confirmation/idempotency 四个独立事实 | `confirmation=user_required` 时 `ConfirmSafety`;`--yes` / `--dry-run` 跳过 | 同一个 SafetySpec 原样进入 Contract Final(`HOM-S1`) | | `ConstParams` | 固定载荷(不上 flag 表) | 并入 toolArgs;不满足 Required | **不**投影为用户 parameter | | `Use` / `Short` / `Long` / `Example` | 命令身份文案与示例 | cobra 自身 | help;identity 仍以 registry 为准 | @@ -98,7 +98,7 @@ command/Leaf 不再写 `dws.schema.risk`;SafetySpec 走类型化 Final 载荷 // 读:Safety 四字段显式对齐 Schema NewLeafCommand(LeafSpec{ Use: "get", Short: "…", Tool: "…", - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -112,7 +112,7 @@ NewLeafCommand(LeafSpec{ NewLeafCommand(LeafSpec{ Use: "publish", Short: "…", Tool: "…", Flags: []LeafFlag{ /* … */ }, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/docs/rfc-command-framework-convergence.md b/docs/rfc-command-framework-convergence.md index 58776439..028614d1 100644 --- a/docs/rfc-command-framework-convergence.md +++ b/docs/rfc-command-framework-convergence.md @@ -273,9 +273,10 @@ Definition(仅声明;不可编译) | 层 | 含义 | 今日落点 | |---|---|---| -| **声明(declare)** | `corecmd.Spec` / `LeafSpec` / `SchemaDecl` **数据字段** = Schema 最终值 | `Flags`/`Constraints`/`Risk`/`ConstParams`/`Schema` | -| **框架转换** | 类型转换并注册(**禁止** JSON 注解桥) | `embedSchemaDecl` → `RegisterRuntimeContractFinal` | -| **Schema 透传** | 组装读取注册表,原样投影为 `ToolSpec` | `runtimeToolSpecFromContractFinal` | +| **声明(declare)** | `corecmd.Spec` / `LeafSpec` / `SchemaDecl` **数据字段** = Schema 最终值 | `Flags`/`Constraints`/`Risk`/`ConstParams`/`Schema`;类型真身在 `corecmd/contract`(`SafetySpec`/`ParamDecl`/`ProductDecl`/`ContractFinalPayload`) | +| **框架转换** | 类型转换并注册(**禁止** JSON 注解桥) | `embedSchemaDecl` → `cli.AnnotateRuntimeContract` + `contract.RegisterRuntimeContractFinal` | +| **注解 seam** | Cobra `dws.schema.*` 写入 | `cli.AnnotateRuntime*`(framework 可调用;**不**在 cli 再定义 contract 类型) | +| **Schema 透传** / 交付 | 组装读取注册表,原样投影为 `ToolSpec`;Catalog embed / `ResolveMeta` | `internal/cli`(交付边界,不搬入 contract) | | **执行(execute)** | 钩子不发明表面 | `Validate` / `Call` / `RunE` / `PostMount` | 硬规则: @@ -351,7 +352,7 @@ Definition(仅声明;不可编译) | | `interface_description`, `interface_type`, `interface_default` | 评审源(interface) | `schema_mcp_metadata` + bindings | 否;**不得创建 CLI flag**(`HOM-I1`) | | **Constraints** | `require_one_of`, `mutually_exclusive`, `require_together` | **声明** | `Constraints` → `AnnotateConstraints` | **是** | | **Positionals** | 位置参数名/必填/说明 | **声明** 或显式 annotate | 目标 `Args`/`PositionalSpec`;今日少量 cobra Args + 注解 | 受管命令应声明,禁止推断 | -| **Safety** | `effect`, `risk`, `confirmation`, `idempotency` | **声明**(完整 `cli.SafetySpec`)**或标注**(`runtime_gate`) | `Safety` / `AnnotateRuntimeGate`(metadata 壳 `tools: {}`,不再承载 reviewed Safety) | 四字段独立;confirmation 单独驱动运行时 | +| **Safety** | `effect`, `risk`, `confirmation`, `idempotency` | **声明**(完整 `contract.SafetySpec`)**或标注**(`runtime_gate`) | `Safety` / `AnnotateRuntimeGate`(metadata 壳 `tools: {}`,不再承载 reviewed Safety) | 四字段独立;confirmation 单独驱动运行时 | | | `idempotency` | 评审源(或未来 Contract) | reviewed metadata | 今日非框架声明;不得推断 | | | `effect_source` / provenance | 组装派生物 | resolver 写入 `FieldProvenance` | 派生,不手写 | | **DryRun** | `preview_kind`, `remote_reads` | 评审源 | `schema_dry_run_capabilities`(正能力声明) | 否;无条目 ≠ 推断「不支持」之外的假能力 | @@ -1162,7 +1163,7 @@ cmd.RunE = func(cmd *cobra.Command, args []string) error { | 非 Shortcut 受管定义的 Cobra 命令 Hidden | 可执行 Contract | 挂载的命令可见性与声明匹配 | | Shortcut 列表成员资格与语义 disposition | 经评审的 Shortcut 可见性解析器 | public/all 列表成员资格与经评审决策匹配 | | Runtime Schema / Agent 暴露 | 经评审的 CommandRegistry 加精确排除 | 每个暴露叶子解析到活 Contract;排除显式且不重叠 | -| Safety 与运行时确认 | 可执行 Contract 的完整 `cli.SafetySpec`,或迁移期显式 annotate(如 `runtime_gate`);见 §5.0 | `confirmation` 单独驱动运行时门,`effect` / `risk` / `idempotency` 原样发布,禁止跨字段机械推导;任一 Safety 字段非空时四字段必须齐全,否则构造期 panic;`ConfirmFirst` 只在 `confirmation=user_required` 时合法 | +| Safety 与运行时确认 | 可执行 Contract 的完整 `contract.SafetySpec`,或迁移期显式 annotate(如 `runtime_gate`);见 §5.0 | `confirmation` 单独驱动运行时门,`effect` / `risk` / `idempotency` 原样发布,禁止跨字段机械推导;任一 Safety 字段非空时四字段必须齐全,否则构造期 panic;`ConfirmFirst` 只在 `confirmation=user_required` 时合法 | | 后端 product/tool/载荷绑定 | mcpbind + 后端元数据 | 每个绑定引用真实的 flag/属性 | | Agent 选择文案(`use_when`、`avoid_when`、摘要) | 经评审的 hints/catalog | 身份解析到活契约 | diff --git a/internal/app/audit_command.go b/internal/app/audit_command.go index 249e8ccd..2ab488f3 100644 --- a/internal/app/audit_command.go +++ b/internal/app/audit_command.go @@ -6,7 +6,6 @@ import ( "encoding/csv" "encoding/json" "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers" "os" "path/filepath" @@ -15,6 +14,7 @@ import ( "time" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/audit" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output" "github.com/spf13/cobra" ) @@ -30,9 +30,9 @@ var ( func newAuditCommand() *cobra.Command { // Product-level Agent routing Decl (migrated from selection/audit.json // products.audit). Catalog assembly stamps provenance contract_final. - cli.RegisterProductDecl(cli.ProductDecl{ + contract.RegisterProductDecl(contract.ProductDecl{ ID: "audit", - Selection: cli.ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "查看、导出和校验本地操作审计日志", UseWhen: []string{ "需要排查本机 CLI 操作审计记录,或验证审计文件完整性", @@ -81,7 +81,7 @@ func newAuditTailCommand() *cobra.Command { } cmd.Flags().IntVarP(&n, "lines", "n", 20, "显示最近 N 条记录") helpers.DeclareLeafMetadata(cmd, helpers.LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -141,7 +141,7 @@ func newAuditExportCommand() *cobra.Command { cmd.Flags().StringVar(&until, "until", "", "截止日期 (YYYY-MM-DD)") cmd.Flags().StringVar(&format, "format", "jsonl", "输出格式: jsonl 或 csv") helpers.DeclareLeafMetadata(cmd, helpers.LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -219,7 +219,7 @@ func newAuditVerifyCommand() *cobra.Command { } cmd.Flags().StringVar(&file, "file", "", "指定审计文件路径(默认最新文件)") helpers.DeclareLeafMetadata(cmd, helpers.LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/app/event_command.go b/internal/app/event_command.go index e29ab356..3d42226f 100644 --- a/internal/app/event_command.go +++ b/internal/app/event_command.go @@ -32,6 +32,7 @@ import ( authpkg "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/auth" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" dwsevent "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/bus" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/busctl" @@ -75,9 +76,9 @@ var ( func newEventCommand() *cobra.Command { // Product-level Agent routing Decl (migrated from selection/event.json // products.event). Catalog assembly stamps provenance contract_final. - cli.RegisterProductDecl(cli.ProductDecl{ + contract.RegisterProductDecl(contract.ProductDecl{ ID: "event", - Selection: cli.ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "订阅/消费个人消息、动作与群生命周期事件,并管理订阅生命周期", UseWhen: []string{ "需要实时监听个人消息接收、全量消息、已读、撤回、表情回应或群生命周期事件,或管理个人事件订阅生命周期", @@ -367,7 +368,7 @@ SIGTERM、关 stdin,或先用 dws event stop --dry-run 预览 f.StringVar(&streamOpts.TicketURL, "stream-ticket-url", strings.TrimSpace(os.Getenv("DWS_STREAM_TICKET_URL")), "个人 Stream 取票 URL;默认由 MCP base 派生 /stream/connections/ticket") hideEventInternalFlags(cmd, "as") - cli.AnnotateRuntimePositionals(cmd, cli.RuntimeSchemaPositional{ + cli.AnnotateRuntimePositionals(cmd, contract.RuntimeSchemaPositional{ Name: "event_key", Type: "string", Description: "要消费的一个或多个个人事件码;多个事件必须共享同一目标和过滤上下文", @@ -376,7 +377,7 @@ SIGTERM、关 stdin,或先用 dws event stop --dry-run 预览 Index: 0, }) helpers.DeclareLeafMetadata(cmd, helpers.LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "non_idempotent", }, @@ -775,7 +776,7 @@ func newEventListCommand() *cobra.Command { cmd.Flags().BoolVar(&includePending, "include-pending", false, "个人事件目录包含 pending 项") hideEventInternalFlags(cmd, "as", "all", "all-editions", "client-id") helpers.DeclareLeafMetadata(cmd, helpers.LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -865,7 +866,7 @@ func newEventStatusCommand() *cobra.Command { "个人事件 sourceId;开源版默认 open,可由 edition 覆盖") hideEventInternalFlags(cmd, "as", "all", "all-editions", "client-id", "fail-on-orphan") helpers.DeclareLeafMetadata(cmd, helpers.LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1165,7 +1166,7 @@ func newEventStopCommand() *cobra.Command { "个人事件 sourceId;开源版默认 open,可由 edition 覆盖") cmd.Flags().BoolVar(&opts.All, "all", false, "取消当前身份下本地记录的所有个人订阅") hideEventInternalFlags(cmd, "as") - cli.AnnotateRuntimePositionals(cmd, cli.RuntimeSchemaPositional{ + cli.AnnotateRuntimePositionals(cmd, contract.RuntimeSchemaPositional{ Name: "subscribe_id", Type: "string", Description: "要取消的个人事件订阅 ID;与 --all 二选一", @@ -1173,7 +1174,7 @@ func newEventStopCommand() *cobra.Command { Index: 0, }) helpers.DeclareLeafMetadata(cmd, helpers.LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/app/event_personal_command.go b/internal/app/event_personal_command.go index b61fda53..7d67cf45 100644 --- a/internal/app/event_personal_command.go +++ b/internal/app/event_personal_command.go @@ -32,6 +32,7 @@ import ( authpkg "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/auth" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" dwsevent "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/bus" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/busctl" @@ -171,7 +172,7 @@ func newEventSchemaCommand() *cobra.Command { cmd.Flags().StringVarP(&formatRaw, "format", "f", "json", "输出格式: json") cmd.Flags().BoolVar(&flatten, "flatten", false, "显示 --flatten 消费模式对应的顶层业务字段 schema") hideEventInternalFlags(cmd, "as") - cli.AnnotateRuntimePositionals(cmd, cli.RuntimeSchemaPositional{ + cli.AnnotateRuntimePositionals(cmd, contract.RuntimeSchemaPositional{ Name: "event_key", Type: "string", Description: "要查询 payload 字段定义的个人事件码", @@ -179,7 +180,7 @@ func newEventSchemaCommand() *cobra.Command { Index: 0, }) helpers.DeclareLeafMetadata(cmd, helpers.LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/app/mcp_url_command.go b/internal/app/mcp_url_command.go index bc298769..9c3615b6 100644 --- a/internal/app/mcp_url_command.go +++ b/internal/app/mcp_url_command.go @@ -20,6 +20,7 @@ import ( "strings" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition" @@ -75,7 +76,7 @@ func newMCPURLGetCommand(caller edition.ToolCaller) *cobra.Command { return writeMCPURLResult(cmd, result) }, } - cli.AnnotateRuntimePositionals(cmd, cli.RuntimeSchemaPositional{ + cli.AnnotateRuntimePositionals(cmd, contract.RuntimeSchemaPositional{ Name: "mcp_id", Type: "string", Description: "钉钉 MCP 市场中的 mcpId", @@ -83,7 +84,7 @@ func newMCPURLGetCommand(caller edition.ToolCaller) *cobra.Command { Index: 0, }) helpers.DeclareLeafMetadata(cmd, helpers.LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "medium", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/app/schema_agent_examples_dry_run_test.go b/internal/app/schema_agent_examples_dry_run_test.go index ec7e1bc2..394a8249 100644 --- a/internal/app/schema_agent_examples_dry_run_test.go +++ b/internal/app/schema_agent_examples_dry_run_test.go @@ -27,6 +27,7 @@ import ( "testing" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition" "github.com/fatih/color" @@ -392,19 +393,19 @@ func manualAgentExampleDryRunObserved(capture manualAgentExampleCapture) bool { func manualAgentExampleDryRunEvidence(capture manualAgentExampleCapture) (string, bool) { normalized := strings.ToLower(capture.Output) if manualAgentExampleDryRunJSONPattern.MatchString(capture.Output) && manualAgentExampleDryRunPlanPattern.MatchString(capture.Output) { - return cli.DryRunPreviewPlan, true + return contract.DryRunPreviewPlan, true } if manualAgentExampleDryRunJSONPattern.MatchString(capture.Output) && manualAgentExampleDryRunInvocationPattern.MatchString(capture.Output) { - return cli.DryRunPreviewInvocation, true + return contract.DryRunPreviewInvocation, true } if manualAgentExampleDryRunJSONPattern.MatchString(capture.Output) { - return cli.DryRunPreviewRequest, true + return contract.DryRunPreviewRequest, true } if strings.Contains(normalized, "[dry-run]") { - return cli.DryRunPreviewInvocation, true + return contract.DryRunPreviewInvocation, true } if capture.DryRunChecks > 0 && strings.Contains(capture.Output, "操作:") { - return cli.DryRunPreviewPlan, true + return contract.DryRunPreviewPlan, true } return "", false } @@ -439,7 +440,7 @@ func TestCrossPlatformCoverageManualAgentExampleDryRunEvidenceClassifiesStructur Output: `{"dry_run":true,"executed":false,"preview_kind":"plan"}`, DryRunChecks: 1, }) - if !observed || kind != cli.DryRunPreviewPlan { + if !observed || kind != contract.DryRunPreviewPlan { t.Fatalf("structured plan classified as kind=%q observed=%v", kind, observed) } if manualAgentExampleDryRunObserved(manualAgentExampleCapture{ @@ -460,23 +461,23 @@ func TestManualAgentExampleDryRunEvidenceClassifiesExecutorEnvelopeAsInvocation( "note": "execution skipped by --dry-run"} }` kind, observed := manualAgentExampleDryRunEvidence(manualAgentExampleCapture{Output: envelope}) - if !observed || kind != cli.DryRunPreviewInvocation { + if !observed || kind != contract.DryRunPreviewInvocation { t.Fatalf("executor envelope classified as kind=%q observed=%v, want invocation", kind, observed) } for _, invKind := range []string{"compat_invocation", "workflow_invocation", "connect_preview"} { out := strings.Replace(envelope, "helper_invocation", invKind, 1) - if kind, ok := manualAgentExampleDryRunEvidence(manualAgentExampleCapture{Output: out}); !ok || kind != cli.DryRunPreviewInvocation { + if kind, ok := manualAgentExampleDryRunEvidence(manualAgentExampleCapture{Output: out}); !ok || kind != contract.DryRunPreviewInvocation { t.Fatalf("kind %q envelope classified as %q/%v, want invocation", invKind, kind, ok) } } // A bare MCP dry-run document has no invocation kind and stays request. mcpDoc := `{"dry_run": true, "executed": false, "tool": "get_dev_app", "arguments": {}}` - if kind, ok := manualAgentExampleDryRunEvidence(manualAgentExampleCapture{Output: mcpDoc}); !ok || kind != cli.DryRunPreviewRequest { + if kind, ok := manualAgentExampleDryRunEvidence(manualAgentExampleCapture{Output: mcpDoc}); !ok || kind != contract.DryRunPreviewRequest { t.Fatalf("bare MCP dry-run classified as %q/%v, want request", kind, ok) } // Plan still wins over an invocation-shaped payload. planDoc := `{"dry_run": true, "preview_kind": "plan", "kind": "helper_invocation"}` - if kind, ok := manualAgentExampleDryRunEvidence(manualAgentExampleCapture{Output: planDoc}); !ok || kind != cli.DryRunPreviewPlan { + if kind, ok := manualAgentExampleDryRunEvidence(manualAgentExampleCapture{Output: planDoc}); !ok || kind != contract.DryRunPreviewPlan { t.Fatalf("plan precedence broken: %q/%v", kind, ok) } } diff --git a/internal/app/schema_agent_selection_contract_test.go b/internal/app/schema_agent_selection_contract_test.go index b57f866d..905134e6 100644 --- a/internal/app/schema_agent_selection_contract_test.go +++ b/internal/app/schema_agent_selection_contract_test.go @@ -31,7 +31,7 @@ func TestManualAgentSelectionScenariosCoverEveryExecutableSchemaTool(t *testing. } fixture, report, err := cli.BuildAgentSelectionEvalFixture(bound) if err != nil { - t.Fatalf("BuildManualAgentSelectionEvalFixture() error = %v", err) + t.Fatalf("BuildAgentSelectionEvalFixture() error = %v", err) } if report.Tools != len(bound.Commands) { t.Fatalf("selection tools = %d, bound commands = %d", report.Tools, len(bound.Commands)) diff --git a/internal/app/schema_agent_selection_live_test.go b/internal/app/schema_agent_selection_live_test.go index b10d1f22..ca7b6a7d 100644 --- a/internal/app/schema_agent_selection_live_test.go +++ b/internal/app/schema_agent_selection_live_test.go @@ -98,7 +98,7 @@ func TestManualAgentSelectionArkLive(t *testing.T) { } } -func buildManualAgentSelectionLiveInput(batch []cli.ManualAgentSelectionCase, hints cli.ManualAgentHintSet) manualAgentSelectionLiveInput { +func buildManualAgentSelectionLiveInput(batch []cli.AgentSelectionCase, hints cli.ManualAgentHintSet) manualAgentSelectionLiveInput { input := manualAgentSelectionLiveInput{Cases: make([]manualAgentSelectionLiveCase, 0, len(batch))} if len(batch) == 0 { return input @@ -122,7 +122,7 @@ func buildManualAgentSelectionLiveInput(batch []cli.ManualAgentSelectionCase, hi return input } -func manualAgentSelectionLiveFixture(t testing.TB) (cli.ManualAgentSelectionFixture, cli.ManualAgentHintSet) { +func manualAgentSelectionLiveFixture(t testing.TB) (cli.AgentSelectionFixture, cli.ManualAgentHintSet) { t.Helper() root := NewRootCommand() effective, err := cli.BuildEffectiveCommandRegistry(root) @@ -140,7 +140,7 @@ func manualAgentSelectionLiveFixture(t testing.TB) (cli.ManualAgentSelectionFixt return fixture, cli.ManualAgentHintSet{} } -func selectManualAgentSelectionLiveCases(t testing.TB, cases []cli.ManualAgentSelectionCase) []cli.ManualAgentSelectionCase { +func selectManualAgentSelectionLiveCases(t testing.TB, cases []cli.AgentSelectionCase) []cli.AgentSelectionCase { t.Helper() if raw := strings.TrimSpace(os.Getenv("DWS_AGENT_SELECTION_CASES")); raw != "" { selected := map[string]bool{} @@ -149,7 +149,7 @@ func selectManualAgentSelectionLiveCases(t testing.TB, cases []cli.ManualAgentSe selected[id] = true } } - result := make([]cli.ManualAgentSelectionCase, 0, len(selected)) + result := make([]cli.AgentSelectionCase, 0, len(selected)) for _, selectionCase := range cases { if selected[selectionCase.ID] { result = append(result, selectionCase) @@ -167,13 +167,13 @@ func selectManualAgentSelectionLiveCases(t testing.TB, cases []cli.ManualAgentSe return result } if os.Getenv("DWS_AGENT_SELECTION_FULL") == "1" { - return append([]cli.ManualAgentSelectionCase(nil), cases...) + return append([]cli.AgentSelectionCase(nil), cases...) } // Smoke mode exercises one positive and one negative scenario per product. seenPositive := map[string]bool{} seenNegative := map[string]bool{} - result := make([]cli.ManualAgentSelectionCase, 0) + result := make([]cli.AgentSelectionCase, 0) for _, selectionCase := range cases { if selectionCase.ExpectedCanonical != "" && !seenPositive[selectionCase.ProductID] { seenPositive[selectionCase.ProductID] = true @@ -187,11 +187,11 @@ func selectManualAgentSelectionLiveCases(t testing.TB, cases []cli.ManualAgentSe return result } -func batchManualAgentSelectionLiveCases(cases []cli.ManualAgentSelectionCase, batchSize int) [][]cli.ManualAgentSelectionCase { +func batchManualAgentSelectionLiveCases(cases []cli.AgentSelectionCase, batchSize int) [][]cli.AgentSelectionCase { if batchSize <= 0 { batchSize = 1 } - grouped := map[string][]cli.ManualAgentSelectionCase{} + grouped := map[string][]cli.AgentSelectionCase{} products := make([]string, 0) for _, selectionCase := range cases { if _, ok := grouped[selectionCase.ProductID]; !ok { @@ -200,7 +200,7 @@ func batchManualAgentSelectionLiveCases(cases []cli.ManualAgentSelectionCase, ba grouped[selectionCase.ProductID] = append(grouped[selectionCase.ProductID], selectionCase) } sort.Strings(products) - result := make([][]cli.ManualAgentSelectionCase, 0) + result := make([][]cli.AgentSelectionCase, 0) for _, productID := range products { productCases := grouped[productID] for start := 0; start < len(productCases); start += batchSize { @@ -208,7 +208,7 @@ func batchManualAgentSelectionLiveCases(cases []cli.ManualAgentSelectionCase, ba if end > len(productCases) { end = len(productCases) } - result = append(result, append([]cli.ManualAgentSelectionCase(nil), productCases[start:end]...)) + result = append(result, append([]cli.AgentSelectionCase(nil), productCases[start:end]...)) } } return result @@ -290,7 +290,7 @@ func marshalManualAgentSelectionLiveRequest(baseURL, model string, input manualA return json.Marshal(requestBody) } -func assertManualAgentSelectionLiveResults(t testing.TB, cases []cli.ManualAgentSelectionCase, results []manualAgentSelectionLiveResult) { +func assertManualAgentSelectionLiveResults(t testing.TB, cases []cli.AgentSelectionCase, results []manualAgentSelectionLiveResult) { t.Helper() byID := make(map[string]manualAgentSelectionLiveResult, len(results)) for _, result := range results { @@ -387,7 +387,7 @@ func sanitizeManualAgentSelectionLiveTestID(value string) string { } func TestManualAgentSelectionLiveResultContract(t *testing.T) { - cases := []cli.ManualAgentSelectionCase{ + cases := []cli.AgentSelectionCase{ {ID: "sample.search/use_when/0", Scenario: "find an item", ExpectedCanonical: "sample.search", CandidateCanonicals: []string{"sample.create", "sample.search"}}, {ID: "sample.search/avoid_when/0", Scenario: "create an item", ForbiddenCanonical: "sample.search", CandidateCanonicals: []string{"sample.create", "sample.search"}}, } @@ -400,7 +400,7 @@ func TestManualAgentSelectionLiveResultContract(t *testing.T) { } func TestManualAgentSelectionLiveInputDoesNotLeakAssertionsOrRepeatCandidates(t *testing.T) { - batch := []cli.ManualAgentSelectionCase{ + batch := []cli.AgentSelectionCase{ {ID: "sample.search/use_when/0", Scenario: "find an item", ExpectedCanonical: "sample.search", CandidateCanonicals: []string{"sample.create", "sample.search"}}, {ID: "sample.search/avoid_when/0", Scenario: "create an item", ForbiddenCanonical: "sample.search", CandidateCanonicals: []string{"sample.create", "sample.search"}}, } diff --git a/internal/cli/api_coverage_more_test.go b/internal/cli/api_coverage_more_test.go index ca78a362..b5342e7e 100644 --- a/internal/cli/api_coverage_more_test.go +++ b/internal/cli/api_coverage_more_test.go @@ -4,6 +4,7 @@ import ( "reflect" "testing" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -46,10 +47,10 @@ func TestCrossPlatformCoverageRuntimeAnnotationAPIsCoverage(t *testing.T) { AnnotateRuntimeConstraints(cmd, RuntimeSchemaConstraints{RequireOneOf: [][]string{{"value", "other"}}}) AnnotateRuntimeConstraints(cmd, RuntimeSchemaConstraints{RequireTogether: [][]string{{"value", "other"}}}) AnnotateRuntimePositionals(cmd, - RuntimeSchemaPositional{Name: "", Index: 0}, - RuntimeSchemaPositional{Name: "bad", Index: -1}, - RuntimeSchemaPositional{Name: " second ", Index: 1, Description: " desc "}, - RuntimeSchemaPositional{Name: "first", Index: 0, Type: " number "}, + contract.RuntimeSchemaPositional{Name: "", Index: 0}, + contract.RuntimeSchemaPositional{Name: "bad", Index: -1}, + contract.RuntimeSchemaPositional{Name: " second ", Index: 1, Description: " desc "}, + contract.RuntimeSchemaPositional{Name: "first", Index: 0, Type: " number "}, ) ExcludeFromRuntimeSchema(cmd) setRuntimeCommandAnnotation(cmd, "empty", " ") diff --git a/internal/cli/canonical.go b/internal/cli/canonical.go index 2c60c3b1..4de6f78d 100644 --- a/internal/cli/canonical.go +++ b/internal/cli/canonical.go @@ -19,6 +19,7 @@ import ( "sort" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output" @@ -57,9 +58,9 @@ type FlagSpec struct { func NewMCPCommand(_ context.Context, _ CatalogLoader, _ executor.Runner, _ *pipeline.Engine) *cobra.Command { // Product-level Agent routing Decl (migrated from selection/mcp.json // products.mcp). Catalog assembly stamps provenance contract_final. - RegisterProductDecl(ProductDecl{ + contract.RegisterProductDecl(contract.ProductDecl{ ID: "mcp", - Selection: ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "解析和管理当前身份可用的 MCP 服务连接信息", UseWhen: []string{ "需要把钉钉 MCP 市场中的服务连接到支持 Streamable HTTP 的 Agent 或客户端", diff --git a/internal/cli/contract_aliases.go b/internal/cli/contract_aliases.go deleted file mode 100644 index 9f9caddb..00000000 --- a/internal/cli/contract_aliases.go +++ /dev/null @@ -1,154 +0,0 @@ -// 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 cli - -import ( - "strings" - - "github.com/spf13/cobra" - - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" -) - -// Leaf Schema contract types are defined in internal/corecmd/contract. -// Aliases keep existing cli.* call sites compiling while assembly migrates. - -type ( - ToolIdentitySpec = contract.ToolIdentitySpec - RuntimeSchemaPositional = contract.RuntimeSchemaPositional - DryRunSpec = contract.DryRunSpec - SafetySpec = contract.SafetySpec - InterfaceRefSpec = contract.InterfaceRefSpec - InterfaceSpec = contract.InterfaceSpec - SelectionSpec = contract.SelectionSpec - FieldProvenance = contract.FieldProvenance - FieldCandidateProvenance = contract.FieldCandidateProvenance - ParamDecl = contract.ParamDecl - ContractFinalPayload = contract.ContractFinalPayload - ProductSelectionDecl = contract.ProductSelectionDecl - ProductDecl = contract.ProductDecl -) - -const ( - DryRunPreviewInvocation = contract.DryRunPreviewInvocation - DryRunPreviewRequest = contract.DryRunPreviewRequest - DryRunPreviewPlan = contract.DryRunPreviewPlan - DryRunPreviewDiff = contract.DryRunPreviewDiff - - InterfaceModeMCP = contract.InterfaceModeMCP - InterfaceModeLocal = contract.InterfaceModeLocal - InterfaceModeComposite = contract.InterfaceModeComposite - InterfaceAvailable = contract.InterfaceAvailable - InterfaceUnavailable = contract.InterfaceUnavailable - - ProductDeclProvenanceSource = contract.ProductDeclProvenanceSource - ProductDeclSourceRef = contract.ProductDeclSourceRef -) - -// RegisterRuntimeContractFinal stores the typed final Schema overlay and marks -// the leaf with the runtime contract annotation for delivery consumers. -func RegisterRuntimeContractFinal(cmd *cobra.Command, payload ContractFinalPayload) { - if cmd == nil { - return - } - AnnotateRuntimeContract(cmd) - contract.RegisterRuntimeContractFinal(cmd, payload) -} - -// RuntimeContractFinal returns the registered final Schema overlay (read-only). -func RuntimeContractFinal(cmd *cobra.Command) (ContractFinalPayload, bool) { - return contract.RuntimeContractFinal(cmd) -} - -// HasRuntimeContractFinal reports whether the leaf has a registered final overlay. -func HasRuntimeContractFinal(cmd *cobra.Command) bool { - return contract.HasRuntimeContractFinal(cmd) -} - -// ClearRuntimeContractFinalForTest removes a registration (tests only). -func ClearRuntimeContractFinalForTest(cmd *cobra.Command) { - contract.ClearRuntimeContractFinalForTest(cmd) -} - -// storeRuntimeContractFinalRawForTest injects a raw map value (tests only). -func storeRuntimeContractFinalRawForTest(cmd *cobra.Command, raw any) { - contract.StoreRuntimeContractFinalRawForTest(cmd, raw) -} - -// RegisterProductDecl stores a product-level routing declaration. -func RegisterProductDecl(decl ProductDecl) { - contract.RegisterProductDecl(decl) -} - -// LookupProductDecl returns the registered product declaration, if any. -func LookupProductDecl(productID string) (ProductDecl, bool) { - return contract.LookupProductDecl(productID) -} - -// HasProductDecl reports whether product-level routing is declared in code. -func HasProductDecl(productID string) bool { - return contract.HasProductDecl(productID) -} - -// RegisteredProductDeclIDs returns sorted product IDs with an in-code Decl. -func RegisteredProductDeclIDs() []string { - return contract.RegisteredProductDeclIDs() -} - -// ClearProductDeclForTest removes a registration (tests only). -func ClearProductDeclForTest(productID string) { - contract.ClearProductDeclForTest(productID) -} - -// ProductSelectionFromDecl projects a ProductDecl into SelectionSpec plus -// contract_final FieldProvenance for ProductSpec assembly. -func ProductSelectionFromDecl(decl ProductDecl) (SelectionSpec, map[string]FieldProvenance) { - return contract.ProductSelectionFromDecl(decl) -} - -// ApplyParamDecls emits parameter declarations as dws.schema.* annotations on the -// command's flags. Called at assembly time when all flags exist on the tree. -func ApplyParamDecls(cmd *cobra.Command, decls []ParamDecl) { - if cmd == nil || len(decls) == 0 { - return - } - for _, p := range decls { - name := strings.TrimSpace(p.Name) - if name == "" { - continue - } - if prop := strings.TrimSpace(p.Property); prop != "" { - AnnotateRuntimeFlagProperty(cmd, name, prop) - } - if p.Required != nil { - AnnotateRuntimeFlagRequiredValue(cmd, name, *p.Required) - } - if it := strings.TrimSpace(p.InterfaceType); it != "" { - AnnotateRuntimeFlagInterfaceType(cmd, name, it) - } - if desc := strings.TrimSpace(p.Description); desc != "" { - AnnotateRuntimeFlagDescription(cmd, name, desc) - } - if rw := strings.TrimSpace(p.RequiredWhen); rw != "" { - AnnotateRuntimeFlagRequiredWhen(cmd, name, rw) - } - if len(p.Enum) > 0 { - AnnotateRuntimeFlagEnum(cmd, name, p.Enum...) - } - } -} - -func resolvedFieldProvenance(value any, source, sourceRef, precedence, resolution, reviewReason string) FieldProvenance { - return contract.ResolvedFieldProvenance(value, source, sourceRef, precedence, resolution, reviewReason) -} diff --git a/internal/cli/contract_delivery_seam.go b/internal/cli/contract_delivery_seam.go new file mode 100644 index 00000000..6609126b --- /dev/null +++ b/internal/cli/contract_delivery_seam.go @@ -0,0 +1,86 @@ +// 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 cli + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" +) + +// Delivery-seam helpers that must live in cli because they emit Cobra +// annotations (AnnotateRuntime*). Types and registries live in +// corecmd/contract; callers author contract.SafetySpec / contract.ParamDecl / +// contract.ContractFinalPayload directly — there is no cli type alias layer. + +// RegisterRuntimeContractFinal stores the typed final Schema overlay and marks +// the leaf with the runtime contract annotation for delivery consumers. +func RegisterRuntimeContractFinal(cmd *cobra.Command, payload contract.ContractFinalPayload) { + if cmd == nil { + return + } + AnnotateRuntimeContract(cmd) + contract.RegisterRuntimeContractFinal(cmd, payload) +} + +// ApplyParamDecls emits parameter declarations as dws.schema.* annotations on the +// command's flags. Called at assembly time when all flags exist on the tree. +// Each non-blank ParamDecl.Name must resolve to an existing Cobra flag; +// unknown names fail closed so typos cannot silently drop during generation. +func ApplyParamDecls(cmd *cobra.Command, decls []contract.ParamDecl) error { + if cmd == nil || len(decls) == 0 { + return nil + } + for _, p := range decls { + name := strings.TrimSpace(p.Name) + if name == "" { + continue + } + if runtimeCommandFlag(cmd, name) == nil { + return fmt.Errorf("ParamDecl %q references unknown flag on %q", name, cmd.CommandPath()) + } + } + for _, p := range decls { + name := strings.TrimSpace(p.Name) + if name == "" { + continue + } + if prop := strings.TrimSpace(p.Property); prop != "" { + AnnotateRuntimeFlagProperty(cmd, name, prop) + } + if p.Required != nil { + AnnotateRuntimeFlagRequiredValue(cmd, name, *p.Required) + } + if it := strings.TrimSpace(p.InterfaceType); it != "" { + AnnotateRuntimeFlagInterfaceType(cmd, name, it) + } + if desc := strings.TrimSpace(p.Description); desc != "" { + AnnotateRuntimeFlagDescription(cmd, name, desc) + } + if rw := strings.TrimSpace(p.RequiredWhen); rw != "" { + AnnotateRuntimeFlagRequiredWhen(cmd, name, rw) + } + if len(p.Enum) > 0 { + AnnotateRuntimeFlagEnum(cmd, name, p.Enum...) + } + } + return nil +} + +func resolvedFieldProvenance(value any, source, sourceRef, precedence, resolution, reviewReason string) contract.FieldProvenance { + return contract.ResolvedFieldProvenance(value, source, sourceRef, precedence, resolution, reviewReason) +} diff --git a/internal/cli/contract_final_consistency_external_test.go b/internal/cli/contract_final_consistency_external_test.go index d6772db8..cbc9b36d 100644 --- a/internal/cli/contract_final_consistency_external_test.go +++ b/internal/cli/contract_final_consistency_external_test.go @@ -16,6 +16,7 @@ import ( "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/app" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" ) // TestAllCommandsContractFinalConsistentWithLiveAndEmbeddedCatalog walks every @@ -81,10 +82,10 @@ func TestAllCommandsContractFinalConsistentWithLiveAndEmbeddedCatalog(t *testing rows = append(rows, row{canonical, cliPath, "FAIL", "absent from embedded full leaf shard"}) continue } - final, has := cli.RuntimeContractFinal(cmd.PrimaryCommand) + final, has := contract.RuntimeContractFinal(cmd.PrimaryCommand) if !has { failCount++ - rows = append(rows, row{canonical, cliPath, "FAIL", "no RuntimeContractFinal on PrimaryCommand"}) + rows = append(rows, row{canonical, cliPath, "FAIL", "no contract.RuntimeContractFinal on PrimaryCommand"}) continue } checked++ @@ -259,7 +260,7 @@ func loadEmbeddedFullLeafTools() (map[string]embedToolView, error) { return out, nil } -func compareFinalToLive(canonical string, final cli.ContractFinalPayload, tool liveToolView) []string { +func compareFinalToLive(canonical string, final contract.ContractFinalPayload, tool liveToolView) []string { var problems []string add := func(field, want, got string) { if strings.TrimSpace(want) == "" { diff --git a/internal/cli/contract_final_test.go b/internal/cli/contract_final_test.go index c7b33a0e..2877a99f 100644 --- a/internal/cli/contract_final_test.go +++ b/internal/cli/contract_final_test.go @@ -18,27 +18,28 @@ import ( "strings" "testing" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) func TestContractFinalTypedRegistryNoJSON(t *testing.T) { cmd := &cobra.Command{Use: "x"} - t.Cleanup(func() { ClearRuntimeContractFinalForTest(cmd) }) + t.Cleanup(func() { contract.ClearRuntimeContractFinalForTest(cmd) }) - RegisterRuntimeContractFinal(cmd, ContractFinalPayload{ + RegisterRuntimeContractFinal(cmd, contract.ContractFinalPayload{ Title: "T", - Safety: &SafetySpec{ + Safety: &contract.SafetySpec{ Effect: "write", Confirmation: "user_required", Idempotency: "retryable", }, - Selection: &SelectionSpec{AgentSummary: "sum", UseWhen: []string{"u"}}, - Identity: &ToolIdentitySpec{ProductID: "p", Name: "n"}, + Selection: &contract.SelectionSpec{AgentSummary: "sum", UseWhen: []string{"u"}}, + Identity: &contract.ToolIdentitySpec{ProductID: "p", Name: "n"}, }) if cmd.Annotations != nil { if _, ok := cmd.Annotations["dws.schema.final"]; ok { t.Fatal("must not write JSON annotation dws.schema.final") } } - got, ok := RuntimeContractFinal(cmd) + got, ok := contract.RuntimeContractFinal(cmd) if !ok || got.Title != "T" || got.Safety == nil || got.Safety.Idempotency != "retryable" { t.Fatalf("got %#v ok=%v", got, ok) } @@ -49,20 +50,20 @@ func TestContractFinalTypedRegistryNoJSON(t *testing.T) { func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalPassThrough(t *testing.T) { cmd := &cobra.Command{Use: "create", Short: "s", Long: "l"} - t.Cleanup(func() { ClearRuntimeContractFinalForTest(cmd) }) + t.Cleanup(func() { contract.ClearRuntimeContractFinalForTest(cmd) }) cmd.Flags().String("mode", "", "usage") AnnotateRuntimeFlag(cmd, "mode", "mode", "string", false, "") - RegisterRuntimeContractFinal(cmd, ContractFinalPayload{ + RegisterRuntimeContractFinal(cmd, contract.ContractFinalPayload{ Title: "Final Title", - Safety: &SafetySpec{ + Safety: &contract.SafetySpec{ Effect: "write", Confirmation: "user_required", Idempotency: "none", }, - DryRun: &DryRunSpec{PreviewKind: DryRunPreviewInvocation}, - Selection: &SelectionSpec{ + DryRun: &contract.DryRunSpec{PreviewKind: contract.DryRunPreviewInvocation}, + Selection: &contract.SelectionSpec{ AgentSummary: "from contract", UseWhen: []string{"create things"}, }, - Identity: &ToolIdentitySpec{ + Identity: &contract.ToolIdentitySpec{ ProductID: "dev", Name: "create_thing", CanonicalPath: "dev.create_thing", }, }) @@ -87,7 +88,7 @@ func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalPassThrough(t *tes if spec.Safety.Confirmation != "user_required" || spec.Safety.Idempotency != "none" { t.Fatalf("safety = %#v", spec.Safety) } - if spec.DryRun == nil || spec.DryRun.PreviewKind != DryRunPreviewInvocation { + if spec.DryRun == nil || spec.DryRun.PreviewKind != contract.DryRunPreviewInvocation { t.Fatalf("dry_run = %#v", spec.DryRun) } if spec.Selection.AgentSummary != "from contract" { @@ -111,7 +112,7 @@ func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalIdentityMismatchFa Command: &cobra.Command{Use: "create"}, Source: "test", } - for name, id := range map[string]ToolIdentitySpec{ + for name, id := range map[string]contract.ToolIdentitySpec{ "wrong name": {Name: "delete_thing"}, "wrong canonical path": {CanonicalPath: "dev.delete_thing"}, "wrong product": {ProductID: "other"}, @@ -119,13 +120,13 @@ func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalIdentityMismatchFa "wrong aliases": {Aliases: []string{"dev rm"}}, } { t.Run(name, func(t *testing.T) { - _, err := runtimeToolSpecFromContractFinal(entry, ContractFinalPayload{Identity: &id}, runtimeSchemaMetadataSources{}) + _, err := runtimeToolSpecFromContractFinal(entry, contract.ContractFinalPayload{Identity: &id}, runtimeSchemaMetadataSources{}) if err == nil { t.Fatal("declared identity conflicting with bound entry must fail assembly") } }) } - consistent := ContractFinalPayload{Identity: &ToolIdentitySpec{ + consistent := contract.ContractFinalPayload{Identity: &contract.ToolIdentitySpec{ ProductID: "dev", Name: "create_thing", CLIName: "create", CanonicalPath: "dev.create_thing", CLIPath: "dev create", PrimaryCLIPath: "dev create", Source: "test", @@ -142,17 +143,17 @@ func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalRejectsReviewedSel ToolName: "create_thing", Command: &cobra.Command{Use: "create"}, } - _, err := runtimeToolSpecFromContractFinal(entry, ContractFinalPayload{ - Selection: &SelectionSpec{AgentSummary: "sum", Reviewed: &reviewed}, + _, err := runtimeToolSpecFromContractFinal(entry, contract.ContractFinalPayload{ + Selection: &contract.SelectionSpec{AgentSummary: "sum", Reviewed: &reviewed}, }, runtimeSchemaMetadataSources{}) if err == nil { t.Fatal("declaration payload carrying Reviewed must fail assembly (reviewed is legacy-path only)") } } -func mustFinal(t *testing.T, cmd *cobra.Command) ContractFinalPayload { +func mustFinal(t *testing.T, cmd *cobra.Command) contract.ContractFinalPayload { t.Helper() - final, ok := RuntimeContractFinal(cmd) + final, ok := contract.RuntimeContractFinal(cmd) if !ok { t.Fatal("missing final") } @@ -161,28 +162,30 @@ func mustFinal(t *testing.T, cmd *cobra.Command) ContractFinalPayload { func TestCrossPlatformCoverageContractFinalNilCommandGuards(t *testing.T) { // Nil command registration/lookup must be inert no-ops. - RegisterRuntimeContractFinal(nil, ContractFinalPayload{Title: "ignored"}) - if _, ok := RuntimeContractFinal(nil); ok { - t.Fatal("RuntimeContractFinal(nil) must report no payload") + RegisterRuntimeContractFinal(nil, contract.ContractFinalPayload{Title: "ignored"}) + if _, ok := contract.RuntimeContractFinal(nil); ok { + t.Fatal("contract.RuntimeContractFinal(nil) must report no payload") } - if HasRuntimeContractFinal(nil) { - t.Fatal("HasRuntimeContractFinal(nil) must be false") + if contract.HasRuntimeContractFinal(nil) { + t.Fatal("contract.HasRuntimeContractFinal(nil) must be false") } - ClearRuntimeContractFinalForTest(nil) + contract.ClearRuntimeContractFinalForTest(nil) } func TestCrossPlatformCoverageApplyParamDeclsSkipsBlankAndAnnotatesEnum(t *testing.T) { cmd := &cobra.Command{Use: "apply-params"} cmd.Flags().String("mode", "", "mode") required := false - ApplyParamDecls(cmd, []ParamDecl{ + if err := ApplyParamDecls(cmd, []contract.ParamDecl{ {Name: " "}, // blank names are skipped { Name: "mode", Property: "mode", Required: &required, InterfaceType: "string", Description: "mode desc", RequiredWhen: "when create", Enum: []string{"a", "b"}, }, - }) + }); err != nil { + t.Fatalf("ApplyParamDecls() error = %v", err) + } flag := cmd.Flags().Lookup("mode") if flag == nil { t.Fatal("missing mode flag") @@ -193,23 +196,52 @@ func TestCrossPlatformCoverageApplyParamDeclsSkipsBlankAndAnnotatesEnum(t *testi if got := flag.Annotations["x-cli-enum"]; len(got) != 2 || got[0] != "a" || got[1] != "b" { t.Fatalf("enum = %#v", flag.Annotations["x-cli-enum"]) } - ApplyParamDecls(nil, []ParamDecl{{Name: "mode"}}) - ApplyParamDecls(cmd, nil) + if err := ApplyParamDecls(nil, []contract.ParamDecl{{Name: "mode"}}); err != nil { + t.Fatalf("ApplyParamDecls(nil) error = %v", err) + } + if err := ApplyParamDecls(cmd, nil); err != nil { + t.Fatalf("ApplyParamDecls(nil decls) error = %v", err) + } +} + +func TestApplyParamDeclsRejectsUnknownFlag(t *testing.T) { + cmd := &cobra.Command{Use: "apply-params"} + cmd.Flags().String("mode", "", "mode") + err := ApplyParamDecls(cmd, []contract.ParamDecl{ + {Name: "mode", Property: "mode"}, + {Name: "missing-flag", Property: "missing"}, + }) + if err == nil { + t.Fatal("ApplyParamDecls() error = nil, want unknown flag") + } + if !strings.Contains(err.Error(), "missing-flag") { + t.Fatalf("ApplyParamDecls() error = %v, want missing-flag", err) + } + if !strings.Contains(err.Error(), "unknown flag") { + t.Fatalf("ApplyParamDecls() error = %v, want unknown flag", err) + } + flag := cmd.Flags().Lookup("mode") + if flag == nil { + t.Fatal("missing mode flag") + } + if got := flag.Annotations["dws.schema.property"]; len(got) != 0 { + t.Fatalf("fail-closed must not annotate before unknown ParamDecl; property = %#v", got) + } } func TestCrossPlatformCoverageRuntimeContractFinalRejectsForeignStoredValue(t *testing.T) { cmd := &cobra.Command{Use: "x"} - t.Cleanup(func() { ClearRuntimeContractFinalForTest(cmd) }) + t.Cleanup(func() { contract.ClearRuntimeContractFinalForTest(cmd) }) - // Defensive branch: a stored value that is not a *ContractFinalPayload + // Defensive branch: a stored value that is not a *contract.ContractFinalPayload // (or a typed nil) must fail the read instead of panicking. - storeRuntimeContractFinalRawForTest(cmd, "not-a-payload") - if _, ok := RuntimeContractFinal(cmd); ok { - t.Fatal("foreign stored value must not decode as ContractFinalPayload") + contract.StoreRuntimeContractFinalRawForTest(cmd, "not-a-payload") + if _, ok := contract.RuntimeContractFinal(cmd); ok { + t.Fatal("foreign stored value must not decode as contract.ContractFinalPayload") } - storeRuntimeContractFinalRawForTest(cmd, (*ContractFinalPayload)(nil)) - if _, ok := RuntimeContractFinal(cmd); ok { - t.Fatal("typed nil payload must not decode as ContractFinalPayload") + contract.StoreRuntimeContractFinalRawForTest(cmd, (*contract.ContractFinalPayload)(nil)) + if _, ok := contract.RuntimeContractFinal(cmd); ok { + t.Fatal("typed nil payload must not decode as contract.ContractFinalPayload") } } @@ -227,7 +259,7 @@ func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalIdentityOverridesA Command: cmd, Source: "test", } - final := ContractFinalPayload{Identity: &ToolIdentitySpec{ + final := contract.ContractFinalPayload{Identity: &contract.ToolIdentitySpec{ ProductID: "dev", SourceProductID: "src", Name: "create_thing", @@ -270,7 +302,7 @@ func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalSameLengthAliasMis } // Same-length alias sets with different members must be detected by the // element-wise comparison, not only by the length fast-path. - final := ContractFinalPayload{Identity: &ToolIdentitySpec{Aliases: []string{"dev other"}}} + final := contract.ContractFinalPayload{Identity: &contract.ToolIdentitySpec{Aliases: []string{"dev other"}}} _, err := runtimeToolSpecFromContractFinal(entry, final, runtimeSchemaMetadataSources{}) if err == nil || !strings.Contains(err.Error(), "aliases") { t.Fatalf("same-length alias mismatch error = %v, want aliases mismatch", err) @@ -291,7 +323,7 @@ func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalSafetyAnnotationFa riskCmd := &cobra.Command{Use: "create"} AnnotateRuntimeRisk(riskCmd, "write") entry.Command = riskCmd - spec, err := runtimeToolSpecFromContractFinal(entry, ContractFinalPayload{}, runtimeSchemaMetadataSources{}) + spec, err := runtimeToolSpecFromContractFinal(entry, contract.ContractFinalPayload{}, runtimeSchemaMetadataSources{}) if err != nil { t.Fatalf("risk-annotated declared leaf failed: %v", err) } @@ -306,7 +338,7 @@ func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalSafetyAnnotationFa gateCmd := &cobra.Command{Use: "create"} AnnotateRuntimeGate(gateCmd, "devAppRequireWriteGuard") entry.Command = gateCmd - spec, err = runtimeToolSpecFromContractFinal(entry, ContractFinalPayload{}, runtimeSchemaMetadataSources{}) + spec, err = runtimeToolSpecFromContractFinal(entry, contract.ContractFinalPayload{}, runtimeSchemaMetadataSources{}) if err != nil { t.Fatalf("gate-annotated declared leaf failed: %v", err) } @@ -329,7 +361,7 @@ func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalParameterResolutio ToolName: "create_thing", Command: &cobra.Command{Use: "create"}, } - _, err := runtimeToolSpecFromContractFinal(entry, ContractFinalPayload{}, runtimeSchemaMetadataSources{}) + _, err := runtimeToolSpecFromContractFinal(entry, contract.ContractFinalPayload{}, runtimeSchemaMetadataSources{}) if err == nil || !strings.Contains(err.Error(), "resolve Contract Schema parameters") { t.Fatalf("parameter resolution error = %v, want resolve Contract Schema parameters", err) } diff --git a/internal/cli/contract_safety_homology_external_test.go b/internal/cli/contract_safety_homology_external_test.go index f878d99f..f08b3448 100644 --- a/internal/cli/contract_safety_homology_external_test.go +++ b/internal/cli/contract_safety_homology_external_test.go @@ -16,6 +16,7 @@ import ( "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/app" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers" ) @@ -86,7 +87,7 @@ func TestUserRequiredSafetyHomologyWithRuntimeGate(t *testing.T) { fails = append(fails, row{canonical, cmd.PrimaryCLIPath, "", "nil PrimaryCommand"}) continue } - final, hasFinal := cli.RuntimeContractFinal(leaf) + final, hasFinal := contract.RuntimeContractFinal(leaf) if !hasFinal || final.Safety == nil { continue } @@ -118,7 +119,7 @@ func TestUserRequiredSafetyHomologyWithRuntimeGate(t *testing.T) { gate = "sheet_protect" default: // NewLeafCommand / Shortcut: NewCommand registers ContractFinal from the - // same SafetySpec it wires into ConfirmSafety. Prove that SafetySpec is + // same contract.SafetySpec it wires into ConfirmSafety. Prove that contract.SafetySpec is // still user_required-operable (closed stdin → confirmation gate). leaf.SetIn(strings.NewReader("")) leaf.SetOut(io.Discard) diff --git a/internal/cli/contract_schema_embed_test.go b/internal/cli/contract_schema_embed_test.go index 192afcc2..985a7e3c 100644 --- a/internal/cli/contract_schema_embed_test.go +++ b/internal/cli/contract_schema_embed_test.go @@ -16,6 +16,7 @@ package cli import ( "testing" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -36,7 +37,7 @@ func TestCrossPlatformCoverageAnnotateRuntimeRiskEmbedsContractMarker(t *testing } func TestCrossPlatformCoverageApplyContractRiskToSafety(t *testing.T) { - base := SafetySpec{Idempotency: "unknown", Risk: "low", Confirmation: "not_required", Effect: "read"} + base := contract.SafetySpec{Idempotency: "unknown", Risk: "low", Confirmation: "not_required", Effect: "read"} got := applyContractRiskToSafety(base, "high-risk-write") if got.Effect != "destructive" || got.Risk != "high" || got.Confirmation != "user_required" { t.Fatalf("high-risk overlay = %#v", got) @@ -73,7 +74,7 @@ func TestCrossPlatformCoverageAnnotateRuntimeGateDeclareOrAnnotate(t *testing.T) } func TestCrossPlatformCoverageApplyContractGateToSafety(t *testing.T) { - base := SafetySpec{Confirmation: "not_required", Effect: "read", Risk: "low"} + base := contract.SafetySpec{Confirmation: "not_required", Effect: "read", Risk: "low"} got := applyContractGateToSafety(base, "devAppRequireWriteGuard") if got.Confirmation != "user_required" { t.Fatalf("gate must force user_required, got %#v", got) @@ -84,7 +85,7 @@ func TestCrossPlatformCoverageApplyContractGateToSafety(t *testing.T) { if got.Risk != "medium" { t.Fatalf("gate risk overlay = %#v", got) } - reviewed := SafetySpec{Confirmation: "not_required", Effect: "destructive", Risk: "high", EffectSource: "reviewed"} + reviewed := contract.SafetySpec{Confirmation: "not_required", Effect: "destructive", Risk: "high", EffectSource: "reviewed"} got = applyContractGateToSafety(reviewed, "devAppRequireWriteGuard") if got.Effect != "destructive" || got.Risk != "high" || got.Confirmation != "user_required" { t.Fatalf("gate must keep reviewed effect/risk: %#v", got) @@ -121,12 +122,12 @@ func TestCrossPlatformCoverageRuntimeContractAnnotationNilAndBlankGuards(t *test func TestCrossPlatformCoverageHasDeclaredOrAnnotatedConfirmationDeclaredAndRiskBranches(t *testing.T) { declared := &cobra.Command{Use: "declared"} - t.Cleanup(func() { ClearRuntimeContractFinalForTest(declared) }) - RegisterRuntimeContractFinal(declared, ContractFinalPayload{ - Safety: &SafetySpec{Confirmation: "not_required"}, + t.Cleanup(func() { contract.ClearRuntimeContractFinalForTest(declared) }) + RegisterRuntimeContractFinal(declared, contract.ContractFinalPayload{ + Safety: &contract.SafetySpec{Confirmation: "not_required"}, }) if !HasDeclaredOrAnnotatedConfirmation(declared) { - t.Fatal("typed Contract SafetySpec confirmation must satisfy declare-OR-annotate") + t.Fatal("typed Contract contract.SafetySpec confirmation must satisfy declare-OR-annotate") } risky := &cobra.Command{Use: "risky"} diff --git a/internal/cli/contractfinal_lookup_test.go b/internal/cli/contractfinal_lookup_test.go index d2e6d252..87cfba27 100644 --- a/internal/cli/contractfinal_lookup_test.go +++ b/internal/cli/contractfinal_lookup_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/app" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" ) func TestContractFinalFoundForHrbrain(t *testing.T) { @@ -13,9 +13,9 @@ func TestContractFinalFoundForHrbrain(t *testing.T) { if err != nil || hrbrain == nil { t.Fatalf("hrbrain search employees not found: %v", err) } - final, ok := cli.RuntimeContractFinal(hrbrain) + final, ok := contract.RuntimeContractFinal(hrbrain) if !ok { - t.Fatal("ContractFinal NOT found for hrbrain search employees — this is the root cause of ParamDecl not working") + t.Fatal("ContractFinal NOT found for hrbrain search employees — this is the root cause of contract.ParamDecl not working") } t.Logf("ContractFinal found: title=%s params=%d", final.Title, len(final.Parameters)) } @@ -26,7 +26,7 @@ func TestContractFinalFoundForAisearch(t *testing.T) { if err != nil || enterprise == nil { t.Fatalf("aisearch enterprise not found: %v", err) } - final, ok := cli.RuntimeContractFinal(enterprise) + final, ok := contract.RuntimeContractFinal(enterprise) if !ok { t.Fatal("ContractFinal NOT found for aisearch enterprise") } diff --git a/internal/cli/gen.go b/internal/cli/gen.go index e26c9d39..968632cb 100644 --- a/internal/cli/gen.go +++ b/internal/cli/gen.go @@ -19,7 +19,7 @@ // // Generation inputs (authored, reviewed): // 1. schema_command_registry/ identity (canonical/aliases/navigation) -// 2. ProductDecl + leaf ContractFinal Agent routing / selection prose +// 2. contract.ProductDecl + leaf ContractFinal Agent routing / selection prose // 3. schema_mcp_metadata.json MCP server tool definitions // 4. schema_parameter_bindings.json parameter type/property mappings // 5. param_concepts.json + schema reviewed parameter synonym policy diff --git a/internal/cli/product_decl_test.go b/internal/cli/product_decl_test.go index 9c16b59c..98d6370e 100644 --- a/internal/cli/product_decl_test.go +++ b/internal/cli/product_decl_test.go @@ -3,36 +3,39 @@ package cli -import "testing" +import ( + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" + "testing" +) func TestProductDeclRegistryRoundTrip(t *testing.T) { - t.Cleanup(func() { ClearProductDeclForTest("sample") }) - ClearProductDeclForTest("sample") + t.Cleanup(func() { contract.ClearProductDeclForTest("sample") }) + contract.ClearProductDeclForTest("sample") - if HasProductDecl("sample") { - t.Fatal("HasProductDecl before register must be false") + if contract.HasProductDecl("sample") { + t.Fatal("contract.HasProductDecl before register must be false") } - RegisterProductDecl(ProductDecl{}) - if HasProductDecl("") { + contract.RegisterProductDecl(contract.ProductDecl{}) + if contract.HasProductDecl("") { t.Fatal("empty ID must not register") } - RegisterProductDecl(ProductDecl{ + contract.RegisterProductDecl(contract.ProductDecl{ ID: " sample ", - Selection: ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "Manage samples", UseWhen: []string{"target is a sample"}, AvoidWhen: []string{"target is another product"}, }, }) - if !HasProductDecl("sample") { - t.Fatal("HasProductDecl after register must be true") + if !contract.HasProductDecl("sample") { + t.Fatal("contract.HasProductDecl after register must be true") } - got, ok := LookupProductDecl("sample") + got, ok := contract.LookupProductDecl("sample") if !ok || got.ID != "sample" || got.Selection.AgentSummary != "Manage samples" { - t.Fatalf("LookupProductDecl = %#v, ok=%v", got, ok) + t.Fatalf("contract.LookupProductDecl = %#v, ok=%v", got, ok) } - ids := RegisteredProductDeclIDs() + ids := contract.RegisteredProductDeclIDs() found := false for _, id := range ids { if id == "sample" { @@ -41,31 +44,31 @@ func TestProductDeclRegistryRoundTrip(t *testing.T) { } } if !found { - t.Fatalf("RegisteredProductDeclIDs missing sample: %#v", ids) + t.Fatalf("contract.RegisteredProductDeclIDs missing sample: %#v", ids) } - selection, provenance := ProductSelectionFromDecl(got) - if selection.AgentSummary != "Manage samples" || selection.AgentSummarySource != ProductDeclSourceRef { - t.Fatalf("ProductSelectionFromDecl selection = %#v", selection) + selection, provenance := contract.ProductSelectionFromDecl(got) + if selection.AgentSummary != "Manage samples" || selection.AgentSummarySource != contract.ProductDeclSourceRef { + t.Fatalf("contract.ProductSelectionFromDecl selection = %#v", selection) } for _, field := range []string{"agent_summary", "use_when", "avoid_when"} { prov, ok := provenance[field] - if !ok || prov.Precedence != "contract_final" || prov.Source != ProductDeclProvenanceSource { + if !ok || prov.Precedence != "contract_final" || prov.Source != contract.ProductDeclProvenanceSource { t.Fatalf("field %s provenance = %#v", field, prov) } } - ClearProductDeclForTest("sample") - if HasProductDecl("sample") { - t.Fatal("ClearProductDeclForTest must remove registration") + contract.ClearProductDeclForTest("sample") + if contract.HasProductDecl("sample") { + t.Fatal("contract.ClearProductDeclForTest must remove registration") } } func TestProductDeclRegisterPanicsOnIncompleteSelection(t *testing.T) { defer func() { if recover() == nil { - t.Fatal("expected panic for incomplete ProductDecl") + t.Fatal("expected panic for incomplete contract.ProductDecl") } }() - RegisterProductDecl(ProductDecl{ID: "broken"}) + contract.RegisterProductDecl(contract.ProductDecl{ID: "broken"}) } diff --git a/internal/cli/runtime_schema.go b/internal/cli/runtime_schema.go index 06acee69..86aee5a8 100644 --- a/internal/cli/runtime_schema.go +++ b/internal/cli/runtime_schema.go @@ -25,6 +25,7 @@ import ( "sync" "sync/atomic" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -309,8 +310,7 @@ func AnnotateRuntimeFlagFormat(cmd *cobra.Command, flagName, format string) { // AnnotateRuntimeFlagInterfaceType records the wire type for a flag's interface // property (e.g. "string", "integer", "boolean", "array"). The resolver reads -// this as a native_annotation candidate for interface_type, outranking -// tool_schema_hint. +// this as a native_annotation candidate for interface_type. func AnnotateRuntimeFlagInterfaceType(cmd *cobra.Command, flagName, interfaceType string) { if cmd == nil { return @@ -407,10 +407,10 @@ func RuntimeContractGate(cmd *cobra.Command) (string, bool) { } // HasDeclaredOrAnnotatedConfirmation reports whether confirmation semantics are -// covered by a typed Contract SafetySpec, the legacy Shortcut Risk bridge, or +// covered by a typed Contract contract.SafetySpec, the legacy Shortcut Risk bridge, or // an explicit runtime_gate annotation. func HasDeclaredOrAnnotatedConfirmation(cmd *cobra.Command) bool { - if final, ok := RuntimeContractFinal(cmd); ok && final.Safety != nil && + if final, ok := contract.RuntimeContractFinal(cmd); ok && final.Safety != nil && strings.TrimSpace(final.Safety.Confirmation) != "" { return true } @@ -424,7 +424,7 @@ func HasDeclaredOrAnnotatedConfirmation(cmd *cobra.Command) bool { // applyContractRiskToSafety overlays Schema Safety fields from an embedded // Contract Risk value. Path A: Contract wins effect/risk/confirmation for the // managed surface; other Safety fields (e.g. idempotency) are preserved. -func applyContractRiskToSafety(base SafetySpec, contractRisk string) SafetySpec { +func applyContractRiskToSafety(base contract.SafetySpec, contractRisk string) contract.SafetySpec { out := base switch strings.TrimSpace(contractRisk) { case "write": @@ -448,7 +448,7 @@ func applyContractRiskToSafety(base SafetySpec, contractRisk string) SafetySpec // applyContractGateToSafety ensures a write-guard annotation cannot leave Schema // claiming confirmation is not required. Reviewed effect/risk are kept when set. -func applyContractGateToSafety(base SafetySpec, gate string) SafetySpec { +func applyContractGateToSafety(base contract.SafetySpec, gate string) contract.SafetySpec { out := base if strings.TrimSpace(gate) == "" { return out @@ -484,11 +484,11 @@ func AnnotateRuntimeConstraints(cmd *cobra.Command, constraints RuntimeSchemaCon } // AnnotateRuntimePositionals records ordered positional arguments for agents. -func AnnotateRuntimePositionals(cmd *cobra.Command, positionals ...RuntimeSchemaPositional) { +func AnnotateRuntimePositionals(cmd *cobra.Command, positionals ...contract.RuntimeSchemaPositional) { if cmd == nil { return } - clean := make([]RuntimeSchemaPositional, 0, len(positionals)) + clean := make([]contract.RuntimeSchemaPositional, 0, len(positionals)) for _, positional := range positionals { positional.Name = strings.TrimSpace(positional.Name) positional.Type = strings.TrimSpace(positional.Type) @@ -570,7 +570,7 @@ type runtimeSchemaEntry struct { PrimaryCLIPath string Aliases []string IsAlias bool - IdentityField FieldProvenance + IdentityField contract.FieldProvenance } func collectRuntimeSchemaEntries(root *cobra.Command) ([]runtimeSchemaEntry, error) { @@ -758,7 +758,7 @@ func schemaProductToolCount(product map[string]any) int { return 0 } -func runtimeToolTextMetadataFromMetadata(entry runtimeSchemaEntry, metadata runtimeSchemaMetadataSources) (title, description, metadataSource string, provenance map[string]FieldProvenance, err error) { +func runtimeToolTextMetadataFromMetadata(entry runtimeSchemaEntry, metadata runtimeSchemaMetadataSources) (title, description, metadataSource string, provenance map[string]contract.FieldProvenance, err error) { baseSource := "cobra_help" baseTitle := runtimeSchemaStringCandidate(entry.Title, baseSource) baseDescription := runtimeSchemaStringCandidate(entry.Description, baseSource) @@ -789,8 +789,6 @@ func runtimeToolTextMetadataFromMetadata(entry runtimeSchemaEntry, metadata runt selectedSource = titleWinner.Source } switch selectedSource { - case "tool_schema_hint": - metadataSource = "tool-schema-hint" case "mcp_metadata", "pinned_mcp_metadata": metadataSource = "embedded-mcp-metadata" case "cobra_help": @@ -798,7 +796,7 @@ func runtimeToolTextMetadataFromMetadata(entry runtimeSchemaEntry, metadata runt default: metadataSource = strings.TrimSpace(selectedSource) } - provenance = map[string]FieldProvenance{} + provenance = map[string]contract.FieldProvenance{} if titleWinner.Present { provenance["title"] = runtimeSchemaFieldProvenance(titleWinner) } @@ -828,27 +826,23 @@ const ( runtimeSchemaRankInference = 100 runtimeSchemaRankMCP = 400 runtimeSchemaRankCobraHelp = 450 - runtimeSchemaRankToolHint = 500 runtimeSchemaRankCobraDefault = 600 runtimeSchemaRankCobraContract = 610 runtimeSchemaRankNativeAnnotation = 620 runtimeSchemaRankTypedMetadata = 630 runtimeSchemaRankConstraint = 640 runtimeSchemaRankVersionedBinding = 650 - runtimeSchemaRankReviewedManual = 700 runtimeSchemaPrecedenceDefault = "default" runtimeSchemaPrecedenceDerived = "derived_resolution" runtimeSchemaPrecedenceInference = "inference" runtimeSchemaPrecedenceMCP = "mcp_metadata" runtimeSchemaPrecedenceCobraHelp = "cobra_help" - runtimeSchemaPrecedenceToolHint = "tool_schema_hint" runtimeSchemaPrecedenceCobra = "cobra_contract" runtimeSchemaPrecedenceNativeAnnotation = "native_annotation" runtimeSchemaPrecedenceTypedMetadata = "typed_metadata" runtimeSchemaPrecedenceConstraint = "command_constraint" runtimeSchemaPrecedenceVersionedBinding = "versioned_binding" - runtimeSchemaPrecedenceReviewedManual = "reviewed_manual" runtimeSchemaPrecedenceMappingExclusion = "reviewed_mapping_exclusion" ) @@ -877,12 +871,6 @@ func runtimeSchemaStringCandidateAtPriority(value any, present bool, source stri } } -func runtimeSchemaManualCandidate(value any, present bool, reason string) runtimeSchemaFieldCandidate { - candidate := runtimeSchemaCandidate(value, present, "reviewed_manual_hint") - candidate.ReviewReason = strings.TrimSpace(reason) - return candidate -} - // resolveRuntimeSchemaCandidate is the only scalar resolver used while // assembling the typed runtime contract. Call-site order is deliberately // irrelevant: source rank selects the winner, values never do, and two @@ -982,15 +970,15 @@ func resolveRequiredProjection(cobraHard bool, candidates ...runtimeSchemaFieldC return floor, nil } -func runtimeSchemaFieldProvenance(candidate runtimeSchemaFieldCandidate) FieldProvenance { +func runtimeSchemaFieldProvenance(candidate runtimeSchemaFieldCandidate) contract.FieldProvenance { if !candidate.Present { - return FieldProvenance{} + return contract.FieldProvenance{} } value, err := json.Marshal(candidate.Value) if err != nil { value = json.RawMessage("null") } - provenance := FieldProvenance{ + provenance := contract.FieldProvenance{ Value: value, Source: candidate.Source, Precedence: candidate.Precedence, @@ -1003,7 +991,7 @@ func runtimeSchemaFieldProvenance(candidate runtimeSchemaFieldCandidate) FieldPr copyCandidate.Compared = nil compared = []runtimeSchemaFieldCandidate{copyCandidate} } - provenance.Candidates = make([]FieldCandidateProvenance, 0, len(compared)) + provenance.Candidates = make([]contract.FieldCandidateProvenance, 0, len(compared)) for idx, item := range compared { selected := idx == 0 value, err := json.Marshal(item.Value) @@ -1013,7 +1001,7 @@ func runtimeSchemaFieldProvenance(candidate runtimeSchemaFieldCandidate) FieldPr // that contract; the resolved typed field remains authoritative. value = json.RawMessage("null") } - provenance.Candidates = append(provenance.Candidates, FieldCandidateProvenance{ + provenance.Candidates = append(provenance.Candidates, contract.FieldCandidateProvenance{ Value: value, Source: item.Source, Precedence: item.Precedence, @@ -1028,8 +1016,6 @@ func runtimeSchemaSourcePriority(source string) (int, string) { switch strings.TrimSpace(source) { case "reviewed_mapping_exclusion": return runtimeSchemaRankVersionedBinding, runtimeSchemaPrecedenceMappingExclusion - case "reviewed_manual_hint": - return runtimeSchemaRankReviewedManual, runtimeSchemaPrecedenceReviewedManual case "require_one_of_constraint": return runtimeSchemaRankConstraint, runtimeSchemaPrecedenceConstraint case "versioned_parameter_binding": @@ -1043,8 +1029,6 @@ func runtimeSchemaSourcePriority(source string) (int, string) { return runtimeSchemaRankCobraDefault, runtimeSchemaPrecedenceCobra } return runtimeSchemaRankCobraContract, runtimeSchemaPrecedenceCobra - case "tool_schema_hint": - return runtimeSchemaRankToolHint, runtimeSchemaPrecedenceToolHint case "mcp_metadata", "pinned_mcp_metadata": return runtimeSchemaRankMCP, runtimeSchemaPrecedenceMCP case "cobra_help": @@ -1221,7 +1205,7 @@ func runtimeCommandParameterSpecs(cmd *cobra.Command, canonicalPath string, embe } required, _ := requiredWinner.Value.(bool) - fieldProvenance := map[string]FieldProvenance{ + fieldProvenance := map[string]contract.FieldProvenance{ "type": runtimeSchemaFieldProvenance(runtimeSchemaStringCandidate(paramType, "cobra_flag_type")), "description": runtimeSchemaFieldProvenance(descriptionWinner), "required": runtimeSchemaFieldProvenance(requiredWinner), @@ -1466,7 +1450,7 @@ func runtimeCommandConstraints(cmd *cobra.Command) RuntimeSchemaConstraints { return normalizeRuntimeSchemaConstraints(constraints) } -func runtimeCommandPositionals(cmd *cobra.Command) []RuntimeSchemaPositional { +func runtimeCommandPositionals(cmd *cobra.Command) []contract.RuntimeSchemaPositional { if cmd == nil || cmd.Annotations == nil { return nil } @@ -1474,7 +1458,7 @@ func runtimeCommandPositionals(cmd *cobra.Command) []RuntimeSchemaPositional { if raw == "" { return nil } - var positionals []RuntimeSchemaPositional + var positionals []contract.RuntimeSchemaPositional if json.Unmarshal([]byte(raw), &positionals) != nil { return nil } diff --git a/internal/cli/runtime_schema_coverage_more_test.go b/internal/cli/runtime_schema_coverage_more_test.go index 2842da45..f29acd79 100644 --- a/internal/cli/runtime_schema_coverage_more_test.go +++ b/internal/cli/runtime_schema_coverage_more_test.go @@ -10,6 +10,7 @@ import ( "strings" "testing" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -31,7 +32,7 @@ func TestCrossPlatformCoverageRuntimeSchemaLoaderAndAnnotationEdges(t *testing.T if cmd.Annotations[runtimeSchemaTitleAnnotation] != "title" { t.Fatalf("annotations = %#v", cmd.Annotations) } - AnnotateRuntimePositionals(cmd, RuntimeSchemaPositional{Name: " ", Index: -1}) + AnnotateRuntimePositionals(cmd, contract.RuntimeSchemaPositional{Name: " ", Index: -1}) if _, ok := cmd.Annotations[runtimeSchemaArgsAnnotation]; ok { t.Fatal("invalid positional should not be annotated") } @@ -134,7 +135,7 @@ func TestCrossPlatformCoverageRuntimeSchemaCandidateAndProvenanceEdges(t *testin if err != nil || winner.Precedence != "a" { t.Fatalf("precedence tie ordering winner = %#v, err = %v", winner, err) } - if got := runtimeSchemaFieldProvenance(runtimeSchemaFieldCandidate{}); !reflect.DeepEqual(got, FieldProvenance{}) { + if got := runtimeSchemaFieldProvenance(runtimeSchemaFieldCandidate{}); !reflect.DeepEqual(got, contract.FieldProvenance{}) { t.Fatalf("absent provenance = %#v", got) } bad := runtimeSchemaCandidate(func() {}, true, "custom") diff --git a/internal/cli/schema_agent_examples.go b/internal/cli/schema_agent_examples.go index 51889942..bc3ec0ac 100644 --- a/internal/cli/schema_agent_examples.go +++ b/internal/cli/schema_agent_examples.go @@ -18,6 +18,7 @@ import ( "sort" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -71,7 +72,7 @@ type ManualAgentExampleExecution struct { Index int Example string Mode ManualAgentExampleMode - DryRun *DryRunSpec + DryRun *contract.DryRunSpec ReasonCode ManualAgentExampleReasonCode Reason string Source ManualAgentExampleDispositionSource @@ -185,7 +186,7 @@ func buildAgentExampleExecutionPlan(bound BoundCommandRegistry, typedTools map[s canonicalPaths := make([]string, 0, len(bound.Commands)) for _, command := range bound.Commands { canonical := strings.TrimSpace(command.CanonicalPath) - if !HasRuntimeContractFinal(command.PrimaryCommand) { + if !contract.HasRuntimeContractFinal(command.PrimaryCommand) { return ManualAgentExampleExecutionPlan{}, fmt.Errorf("bound tool %q has no ContractFinal declaration; Schema examples require leaf Schema.Selection", canonical) } canonicalPaths = append(canonicalPaths, canonical) @@ -495,7 +496,7 @@ func agentExamplePlaceholderAt(input string, start int) (string, int, bool) { return input[start : end+1], end, true } -func validateAgentExampleCobraContract(command *cobra.Command, arguments []string, constraints RuntimeSchemaConstraints, positionalSpecs []RuntimeSchemaPositional) error { +func validateAgentExampleCobraContract(command *cobra.Command, arguments []string, constraints RuntimeSchemaConstraints, positionalSpecs []contract.RuntimeSchemaPositional) error { if command == nil { return fmt.Errorf("bound Cobra command is nil") } @@ -598,8 +599,8 @@ func validateAgentExampleCobraContract(command *cobra.Command, arguments []strin return nil } -func mergeAgentExamplePositionals(groups ...[]RuntimeSchemaPositional) []RuntimeSchemaPositional { - byIdentity := map[string]RuntimeSchemaPositional{} +func mergeAgentExamplePositionals(groups ...[]contract.RuntimeSchemaPositional) []contract.RuntimeSchemaPositional { + byIdentity := map[string]contract.RuntimeSchemaPositional{} for _, group := range groups { for _, positional := range group { key := fmt.Sprintf("%d\x00%s", positional.Index, strings.TrimSpace(positional.Name)) @@ -608,7 +609,7 @@ func mergeAgentExamplePositionals(groups ...[]RuntimeSchemaPositional) []Runtime } } } - result := make([]RuntimeSchemaPositional, 0, len(byIdentity)) + result := make([]contract.RuntimeSchemaPositional, 0, len(byIdentity)) for _, positional := range byIdentity { result = append(result, positional) } diff --git a/internal/cli/schema_agent_metadata.go b/internal/cli/schema_agent_metadata.go index e11ec4a8..8014622b 100644 --- a/internal/cli/schema_agent_metadata.go +++ b/internal/cli/schema_agent_metadata.go @@ -16,6 +16,7 @@ package cli import ( "encoding/json" "fmt" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "io/fs" "strings" "sync" @@ -53,35 +54,35 @@ type embeddedAgentMetadataCoverage struct { } type agentProductMetadata struct { - AgentSummary string `json:"agent_summary,omitempty"` - AgentSummarySource string `json:"agent_summary_source,omitempty"` - UseWhen []string `json:"use_when,omitempty"` - AvoidWhen []string `json:"avoid_when,omitempty"` - SourceRefs []string `json:"source_refs,omitempty"` - FieldProvenance map[string]FieldProvenance `json:"field_provenance,omitempty"` + AgentSummary string `json:"agent_summary,omitempty"` + AgentSummarySource string `json:"agent_summary_source,omitempty"` + UseWhen []string `json:"use_when,omitempty"` + AvoidWhen []string `json:"avoid_when,omitempty"` + SourceRefs []string `json:"source_refs,omitempty"` + FieldProvenance map[string]contract.FieldProvenance `json:"field_provenance,omitempty"` } type agentToolMetadata struct { - AgentSummary string `json:"agent_summary,omitempty"` - AgentSummarySource string `json:"agent_summary_source,omitempty"` - UseWhen []string `json:"use_when,omitempty"` - AvoidWhen []string `json:"avoid_when,omitempty"` - Prerequisites []string `json:"prerequisites,omitempty"` - Tips []string `json:"tips,omitempty"` - Effect string `json:"effect,omitempty"` - EffectSource string `json:"effect_source,omitempty"` - Risk string `json:"risk,omitempty"` - Confirmation string `json:"confirmation,omitempty"` - Idempotency string `json:"idempotency,omitempty"` - WorkflowRefs []string `json:"workflow_refs,omitempty"` - Examples []string `json:"examples,omitempty"` - Reviewed *bool `json:"reviewed,omitempty"` - SourceRefs []string `json:"source_refs,omitempty"` - InterfaceRef *embeddedMCPInterfaceRef `json:"interface_ref,omitempty"` - InterfaceMode string `json:"interface_mode,omitempty"` - Availability string `json:"availability,omitempty"` - InterfaceReason string `json:"interface_reason,omitempty"` - FieldProvenance map[string]FieldProvenance `json:"field_provenance,omitempty"` + AgentSummary string `json:"agent_summary,omitempty"` + AgentSummarySource string `json:"agent_summary_source,omitempty"` + UseWhen []string `json:"use_when,omitempty"` + AvoidWhen []string `json:"avoid_when,omitempty"` + Prerequisites []string `json:"prerequisites,omitempty"` + Tips []string `json:"tips,omitempty"` + Effect string `json:"effect,omitempty"` + EffectSource string `json:"effect_source,omitempty"` + Risk string `json:"risk,omitempty"` + Confirmation string `json:"confirmation,omitempty"` + Idempotency string `json:"idempotency,omitempty"` + WorkflowRefs []string `json:"workflow_refs,omitempty"` + Examples []string `json:"examples,omitempty"` + Reviewed *bool `json:"reviewed,omitempty"` + SourceRefs []string `json:"source_refs,omitempty"` + InterfaceRef *embeddedMCPInterfaceRef `json:"interface_ref,omitempty"` + InterfaceMode string `json:"interface_mode,omitempty"` + Availability string `json:"availability,omitempty"` + InterfaceReason string `json:"interface_reason,omitempty"` + FieldProvenance map[string]contract.FieldProvenance `json:"field_provenance,omitempty"` } type embeddedAgentMetadataDomain struct { @@ -191,25 +192,25 @@ func emptyEmbeddedAgentMetadata() embeddedAgentMetadata { // metadata to runtime contract assembly. Path resolution happens once; all // consumers receive the same resolved safety, interface, selection and // provenance values without performing downstream map merges. -func agentToolContractForPathsFromMetadata(source embeddedAgentMetadata, paths ...string) (SafetySpec, InterfaceSpec, SelectionSpec, map[string]FieldProvenance, bool) { +func agentToolContractForPathsFromMetadata(source embeddedAgentMetadata, paths ...string) (contract.SafetySpec, contract.InterfaceSpec, contract.SelectionSpec, map[string]contract.FieldProvenance, bool) { metadata, ok := lookupAgentToolMetadataFrom(source, paths...) if !ok { - return SafetySpec{}, InterfaceSpec{}, SelectionSpec{}, nil, false + return contract.SafetySpec{}, contract.InterfaceSpec{}, contract.SelectionSpec{}, nil, false } - safety := SafetySpec{ + safety := contract.SafetySpec{ Effect: strings.TrimSpace(metadata.Effect), EffectSource: strings.TrimSpace(metadata.EffectSource), Risk: strings.TrimSpace(metadata.Risk), Confirmation: strings.TrimSpace(metadata.Confirmation), Idempotency: strings.TrimSpace(metadata.Idempotency), } - interfaceSpec := InterfaceSpec{ + interfaceSpec := contract.InterfaceSpec{ Mode: strings.TrimSpace(metadata.InterfaceMode), Availability: strings.TrimSpace(metadata.Availability), Reason: strings.TrimSpace(metadata.InterfaceReason), } if metadata.InterfaceRef != nil { - interfaceSpec.Ref = &InterfaceRefSpec{ + interfaceSpec.Ref = &contract.InterfaceRefSpec{ ProductID: strings.TrimSpace(metadata.InterfaceRef.ProductID), RPCName: strings.TrimSpace(metadata.InterfaceRef.RPCName), } @@ -225,7 +226,7 @@ func agentToolContractForPathsFromMetadata(source embeddedAgentMetadata, paths . // null. The final typed contract stores an object or JSON null, so project the // concrete compact identity here. Missing provenance is never synthesized: // constructors and snapshot loaders remain validate-only and fail closed. -func resolvedAgentToolProvenance(source map[string]FieldProvenance, interfaceSpec InterfaceSpec, selection SelectionSpec) map[string]FieldProvenance { +func resolvedAgentToolProvenance(source map[string]contract.FieldProvenance, interfaceSpec contract.InterfaceSpec, selection contract.SelectionSpec) map[string]contract.FieldProvenance { out := cloneFieldProvenance(source) if provenance, ok := out["interface_ref"]; ok { out["interface_ref"] = projectAgentInterfaceRefProvenance(provenance, interfaceSpec.Ref) @@ -233,7 +234,7 @@ func resolvedAgentToolProvenance(source map[string]FieldProvenance, interfaceSpe return out } -func projectAgentInterfaceRefProvenance(provenance FieldProvenance, ref *InterfaceRefSpec) FieldProvenance { +func projectAgentInterfaceRefProvenance(provenance contract.FieldProvenance, ref *contract.InterfaceRefSpec) contract.FieldProvenance { finalValue, _ := json.Marshal(ref) legacy := "" if ref != nil { @@ -259,19 +260,19 @@ func projectAgentInterfaceRefProvenance(provenance FieldProvenance, ref *Interfa } // agentProductSelectionForIDsFromMetadata exposes generated product routing -// through the same typed SelectionSpec used by ToolSpec. -func agentProductSelectionForIDsFromMetadata(source embeddedAgentMetadata, ids ...string) (SelectionSpec, bool) { +// through the same typed contract.SelectionSpec used by ToolSpec. +func agentProductSelectionForIDsFromMetadata(source embeddedAgentMetadata, ids ...string) (contract.SelectionSpec, bool) { selection, _, ok := agentProductContractForIDsFromMetadata(source, ids...) return selection, ok } -func agentProductContractForIDsFromMetadata(source embeddedAgentMetadata, ids ...string) (SelectionSpec, map[string]FieldProvenance, bool) { +func agentProductContractForIDsFromMetadata(source embeddedAgentMetadata, ids ...string) (contract.SelectionSpec, map[string]contract.FieldProvenance, bool) { for _, id := range ids { metadata, ok := source.Products[strings.TrimSpace(id)] if !ok { continue } - selection := SelectionSpec{ + selection := contract.SelectionSpec{ AgentSummary: strings.TrimSpace(metadata.AgentSummary), AgentSummarySource: strings.TrimSpace(metadata.AgentSummarySource), UseWhen: cloneOptionalStrings(metadata.UseWhen), @@ -281,16 +282,16 @@ func agentProductContractForIDsFromMetadata(source embeddedAgentMetadata, ids .. }.Normalized() return selection, cloneFieldProvenance(metadata.FieldProvenance), true } - return SelectionSpec{}, nil, false + return contract.SelectionSpec{}, nil, false } -func agentToolSelection(metadata agentToolMetadata) SelectionSpec { +func agentToolSelection(metadata agentToolMetadata) contract.SelectionSpec { var reviewed *bool if metadata.Reviewed != nil { value := *metadata.Reviewed reviewed = &value } - return SelectionSpec{ + return contract.SelectionSpec{ AgentSummary: strings.TrimSpace(metadata.AgentSummary), AgentSummarySource: strings.TrimSpace(metadata.AgentSummarySource), UseWhen: cloneOptionalStrings(metadata.UseWhen), @@ -305,11 +306,11 @@ func agentToolSelection(metadata agentToolMetadata) SelectionSpec { }.Normalized() } -func cloneFieldProvenance(source map[string]FieldProvenance) map[string]FieldProvenance { +func cloneFieldProvenance(source map[string]contract.FieldProvenance) map[string]contract.FieldProvenance { if len(source) == 0 { return nil } - out := make(map[string]FieldProvenance, len(source)) + out := make(map[string]contract.FieldProvenance, len(source)) for field, provenance := range source { provenance.Value = append(json.RawMessage(nil), provenance.Value...) provenance.Candidates = cloneFieldCandidates(provenance.Candidates) @@ -319,11 +320,11 @@ func cloneFieldProvenance(source map[string]FieldProvenance) map[string]FieldPro return out } -func cloneFieldCandidates(source []FieldCandidateProvenance) []FieldCandidateProvenance { +func cloneFieldCandidates(source []contract.FieldCandidateProvenance) []contract.FieldCandidateProvenance { if len(source) == 0 { return nil } - out := make([]FieldCandidateProvenance, len(source)) + out := make([]contract.FieldCandidateProvenance, len(source)) for index, candidate := range source { candidate.Value = append(json.RawMessage(nil), candidate.Value...) if candidate.Selected != nil { diff --git a/internal/cli/schema_agent_metadata_test.go b/internal/cli/schema_agent_metadata_test.go index 66fb7a0b..1b9ae676 100644 --- a/internal/cli/schema_agent_metadata_test.go +++ b/internal/cli/schema_agent_metadata_test.go @@ -19,6 +19,7 @@ import ( "testing" "testing/fstest" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -151,7 +152,7 @@ func TestAgentMetadataTypedAccessorRoundTripsProvenance(t *testing.T) { if !ok { t.Fatal("typed Agent metadata lookup failed") } - if safety != (SafetySpec{Effect: "write", EffectSource: "agent-hint", Risk: "low", Confirmation: "not_required", Idempotency: "non_idempotent"}) { + if safety != (contract.SafetySpec{Effect: "write", EffectSource: "agent-hint", Risk: "low", Confirmation: "not_required", Idempotency: "non_idempotent"}) { t.Fatalf("safety = %#v", safety) } if interfaceSpec.Ref == nil || interfaceSpec.Ref.ProductID != "calendar" || interfaceSpec.Ref.RPCName != "update_attendee" || interfaceSpec.Mode != "mcp" || interfaceSpec.Availability != "available" || interfaceSpec.Reason != "reviewed RPC mapping" { @@ -189,14 +190,14 @@ func TestAgentMetadataTypedAccessorRoundTripsProvenance(t *testing.T) { func TestAgentMetadataTypedAdapterProjectsInterfaceProvenance(t *testing.T) { selected := true - legacyRef := func(value string) FieldProvenance { + legacyRef := func(value string) contract.FieldProvenance { raw, _ := json.Marshal(value) - return FieldProvenance{ + return contract.FieldProvenance{ Value: raw, Source: "agent-metadata.json", Precedence: "explicit", Resolution: "highest_precedence", - Candidates: []FieldCandidateProvenance{{ + Candidates: []contract.FieldCandidateProvenance{{ Value: append(json.RawMessage(nil), raw...), Source: "agent-metadata.json", Precedence: "explicit", @@ -213,7 +214,7 @@ func TestAgentMetadataTypedAdapterProjectsInterfaceProvenance(t *testing.T) { InterfaceRef: &embeddedMCPInterfaceRef{ProductID: "calendar", RPCName: "get_event"}, InterfaceMode: "mcp", Availability: "available", - FieldProvenance: map[string]FieldProvenance{ + FieldProvenance: map[string]contract.FieldProvenance{ "interface_ref": legacyRef("calendar.get_event"), }, }, @@ -221,7 +222,7 @@ func TestAgentMetadataTypedAdapterProjectsInterfaceProvenance(t *testing.T) { InterfaceMode: "local", Availability: "available", InterfaceReason: "reviewed local wrapper", - FieldProvenance: map[string]FieldProvenance{ + FieldProvenance: map[string]contract.FieldProvenance{ "interface_ref": legacyRef(""), "interface_mode": mode, }, @@ -229,7 +230,7 @@ func TestAgentMetadataTypedAdapterProjectsInterfaceProvenance(t *testing.T) { "calendar helper inspect": { InterfaceMode: "local", Availability: "available", - FieldProvenance: map[string]FieldProvenance{ + FieldProvenance: map[string]contract.FieldProvenance{ "interface_mode": mode, }, }, @@ -279,20 +280,20 @@ func TestAgentMetadataTypedAdapterProjectsInterfaceProvenance(t *testing.T) { func TestAgentMetadataTypedAdapterDoesNotLaunderInterfaceConflict(t *testing.T) { selected := true wrong, _ := json.Marshal("calendar.wrong_rpc") - provenance := FieldProvenance{ + provenance := contract.FieldProvenance{ Value: wrong, Source: "bad.json", Precedence: "explicit", Resolution: "highest_precedence", - Candidates: []FieldCandidateProvenance{{ + Candidates: []contract.FieldCandidateProvenance{{ Value: wrong, Source: "bad.json", Precedence: "explicit", Selected: &selected, }}, } - projected := projectAgentInterfaceRefProvenance(provenance, &InterfaceRefSpec{ProductID: "calendar", RPCName: "get_event"}) + projected := projectAgentInterfaceRefProvenance(provenance, &contract.InterfaceRefSpec{ProductID: "calendar", RPCName: "get_event"}) if string(projected.Value) != string(wrong) { t.Fatalf("conflicting winner was rewritten: %s", projected.Value) } - if err := validateFinalFieldProvenance("calendar.event_get", "interface_ref", projected, &InterfaceRefSpec{ProductID: "calendar", RPCName: "get_event"}); err == nil { + if err := validateFinalFieldProvenance("calendar.event_get", "interface_ref", projected, &contract.InterfaceRefSpec{ProductID: "calendar", RPCName: "get_event"}); err == nil { t.Fatal("conflicting interface_ref provenance unexpectedly validated") } } @@ -307,7 +308,7 @@ func TestAgentProductSelectionUsesTypedAccessor(t *testing.T) { UseWhen: []string{"create a document", "create a document"}, AvoidWhen: []string{"manage a spreadsheet"}, SourceRefs: []string{"z.md", "a.md"}, - FieldProvenance: map[string]FieldProvenance{"agent_summary": provenance}, + FieldProvenance: map[string]contract.FieldProvenance{"agent_summary": provenance}, }, }, Tools: map[string]agentToolMetadata{}, @@ -325,7 +326,7 @@ func TestAgentProductSelectionUsesTypedAccessor(t *testing.T) { t.Fatalf("product provenance = %#v, ok=%v", deliveredProvenance, ok) } selection.UseWhen[0] = "mutated" - deliveredProvenance["agent_summary"] = FieldProvenance{} + deliveredProvenance["agent_summary"] = contract.FieldProvenance{} again, _ := agentProductSelectionForIDsFromMetadata(metadataFixture, "doc") if again.UseWhen[0] != "create a document" { t.Fatalf("product accessor leaked mutable state: %#v", again) @@ -423,7 +424,13 @@ func TestRuntimeSchemaIncludesEmbeddedAgentMetadata(t *testing.T) { } func TestRuntimeSchemaAllPayloadContainsFullLeafParameters(t *testing.T) { - payload, err := runtimeSchemaAllPayloadForTest(buildRuntimeSchemaTestRoot()) + // Synthetic fixture has no ContractFinal/ProductDecl; exercise the + // test-isolated legacy assembly path (production fails closed). + registry, err := schemaRegistryForTestWithMetadata(buildRuntimeSchemaTestRoot(), emptyEmbeddedAgentMetadata(), embeddedMCPMetadata{}) + if err != nil { + t.Fatal(err) + } + payload, err := runtimeSchemaAllPayloadFromRegistry(registry) if err != nil { t.Fatal(err) } diff --git a/internal/cli/schema_agent_selection.go b/internal/cli/schema_agent_selection.go index f067ee4a..eab70a5f 100644 --- a/internal/cli/schema_agent_selection.go +++ b/internal/cli/schema_agent_selection.go @@ -21,6 +21,7 @@ import ( "sort" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -42,20 +43,14 @@ type AgentSelectionCase struct { CandidateCanonicals []string `json:"candidate_canonicals"` } -// ManualAgentSelectionCase is a compatibility alias for AgentSelectionCase. -type ManualAgentSelectionCase = AgentSelectionCase - // AgentSelectionFixture is the stable input to an optional live Agent -// evaluation. It is built from ProductDecl/ContractFinal selection prose and +// evaluation. It is built from contract.ProductDecl/ContractFinal selection prose and // the real bound command tree; it is not a second authored hint source. type AgentSelectionFixture struct { Version int `json:"version"` Cases []AgentSelectionCase `json:"cases"` } -// ManualAgentSelectionFixture is a compatibility alias for AgentSelectionFixture. -type ManualAgentSelectionFixture = AgentSelectionFixture - // AgentSelectionReport records deterministic coverage and the exact fixture // digest. It proves that all reviewed assertions are well-formed and // executable; it deliberately does not claim that a language model understood @@ -67,9 +62,6 @@ type AgentSelectionReport struct { FixtureSHA256 string } -// ManualAgentSelectionReport is a compatibility alias for AgentSelectionReport. -type ManualAgentSelectionReport = AgentSelectionReport - // BuildAgentSelectionEvalFixture turns every ContractFinal use_when and // avoid_when entry into a typed, reproducible evaluation case and validates it // against the exact BoundCommandRegistry. @@ -81,7 +73,7 @@ func BuildAgentSelectionEvalFixture(bound BoundCommandRegistry) (AgentSelectionF for _, command := range bound.Commands { canonical := strings.TrimSpace(command.CanonicalPath) expectedTools[canonical] = true - if !HasRuntimeContractFinal(command.PrimaryCommand) { + if !contract.HasRuntimeContractFinal(command.PrimaryCommand) { return fixture, report, fmt.Errorf("agent selection expected canonical %q has no ContractFinal declaration", canonical) } productID, _, ok := strings.Cut(canonical, ".") @@ -167,28 +159,17 @@ func BuildAgentSelectionEvalFixture(bound BoundCommandRegistry) (AgentSelectionF return fixture, report, nil } -// BuildManualAgentSelectionEvalFixture is a compatibility wrapper that ignores -// residual ManualAgentHintSet input and builds from ContractFinal only. -func BuildManualAgentSelectionEvalFixture(bound BoundCommandRegistry, _ ManualAgentHintSet) (AgentSelectionFixture, AgentSelectionReport, error) { - return BuildAgentSelectionEvalFixture(bound) -} - // ValidateAgentSelectionContract is the lightweight generator-facing gate. func ValidateAgentSelectionContract(bound BoundCommandRegistry) (AgentSelectionReport, error) { _, report, err := BuildAgentSelectionEvalFixture(bound) return report, err } -// ValidateManualAgentSelectionContract is a compatibility wrapper. -func ValidateManualAgentSelectionContract(bound BoundCommandRegistry, _ ManualAgentHintSet) (AgentSelectionReport, error) { - return ValidateAgentSelectionContract(bound) -} - // contractFinalSelectionHint synthesizes selection assertions of a declared // tool from its ContractFinal overlay. func contractFinalSelectionHint(command *cobra.Command) ManualAgentToolHint { hint := ManualAgentToolHint{Reviewed: true, Revision: "contract", Reason: "Contract final declaration (corecmd.SchemaDecl)"} - payload, ok := RuntimeContractFinal(command) + payload, ok := contract.RuntimeContractFinal(command) if !ok || payload.Selection == nil { return hint } diff --git a/internal/cli/schema_catalog_structure.go b/internal/cli/schema_catalog_structure.go index 829a1f61..2baf0d04 100644 --- a/internal/cli/schema_catalog_structure.go +++ b/internal/cli/schema_catalog_structure.go @@ -16,6 +16,7 @@ package cli import ( "encoding/json" "fmt" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "sort" "strings" ) @@ -83,8 +84,8 @@ var schemaCatalogToolEnums = map[string][]string{ "effect": {"read", "write", "destructive"}, "risk": {"low", "medium", "high"}, "confirmation": {"not_required", "user_required"}, - "interface_mode": {InterfaceModeMCP, InterfaceModeComposite, InterfaceModeLocal}, - "availability": {InterfaceAvailable, InterfaceUnavailable}, + "interface_mode": {contract.InterfaceModeMCP, contract.InterfaceModeComposite, contract.InterfaceModeLocal}, + "availability": {contract.InterfaceAvailable, contract.InterfaceUnavailable}, } // schemaCatalogParamRequiredKeys is the required core of every parameter. @@ -256,7 +257,7 @@ func validateCatalogInterface(toolID string, entry map[string]any, violations *[ ref, hasRef := entry["interface_ref"] reason, _ := entry["interface_reason"].(string) switch mode { - case InterfaceModeMCP: + case contract.InterfaceModeMCP: if !hasRef { report("interface_mode=mcp requires interface_ref") return @@ -274,7 +275,7 @@ func validateCatalogInterface(toolID string, entry map[string]any, violations *[ if strings.TrimSpace(reason) != "" { report("interface_mode=mcp must not set interface_reason") } - case InterfaceModeComposite, InterfaceModeLocal: + case contract.InterfaceModeComposite, contract.InterfaceModeLocal: if hasRef { report("interface_mode=%s must not set interface_ref", mode) } diff --git a/internal/cli/schema_catalog_test.go b/internal/cli/schema_catalog_test.go index 983a0dbe..6ccd41b5 100644 --- a/internal/cli/schema_catalog_test.go +++ b/internal/cli/schema_catalog_test.go @@ -130,15 +130,13 @@ func TestEmbeddedCatalogPreservesRegistryIdentityAndManualParameterContract(t *t t.Fatalf("canonical identity provenance = %#v", identity) } parameters := schemaMap(leaf["parameters"]) - assertReviewedManual := func(flagName, field string) { + assertNativeAnnotation := func(flagName, field string) { t.Helper() provenance := schemaMap(parameters[flagName]["field_provenance"]) winner := provenance[field] src, _ := winner["source"].(string) prec, _ := winner["precedence"].(string) - // Accept both the legacy hints source and the migrated declaration source. - if (src != "reviewed_manual_hint" || prec != "reviewed_manual") && - (src != "native_annotation" || prec != "native_annotation") { + if src != "native_annotation" || prec != "native_annotation" { t.Fatalf("%s.%s provenance = %#v", flagName, field, winner) } } @@ -146,8 +144,8 @@ func TestEmbeddedCatalogPreservesRegistryIdentityAndManualParameterContract(t *t if name["property"] != "categoryName" || name["required"] != true { t.Fatalf("name parameter = %#v", name) } - assertReviewedManual("name", "property") - assertReviewedManual("name", "required") + assertNativeAnnotation("name", "property") + assertNativeAnnotation("name", "required") for flagName, property := range map[string]string{ "keywords": "groupNameKeywords", "members": "memberOpenDingTalkIds", @@ -157,7 +155,7 @@ func TestEmbeddedCatalogPreservesRegistryIdentityAndManualParameterContract(t *t t.Fatalf("%s parameter = %#v", flagName, parameter) } for _, field := range []string{"property", "interface_type", "required"} { - assertReviewedManual(flagName, field) + assertNativeAnnotation(flagName, field) } } } @@ -165,7 +163,7 @@ func TestEmbeddedCatalogPreservesRegistryIdentityAndManualParameterContract(t *t func TestEmbeddedCatalogMailMessageListLimitMapsToSize(t *testing.T) { // mail message list is a composite wrapper over search_emails and shares // that RPC with mail message search. list has no versioned binding entry - // (composite identity), so clearing hints without a leaf-local ParamDecl + // (composite identity), so clearing hints without a leaf-local contract.ParamDecl // previously fell back to flag_name_inference → "limit". listLeaf, err := embeddedSchemaPayload([]string{"mail message list"}) if err != nil { @@ -187,7 +185,7 @@ func TestEmbeddedCatalogMailMessageListLimitMapsToSize(t *testing.T) { t.Fatal("mail message list must not publish search-only --query") } - // Sibling search leaf maps the same --limit → size via binding + ParamDecl + // Sibling search leaf maps the same --limit → size via binding + contract.ParamDecl // and must keep the free-form KQL surface (not fold into list). searchLeaf, err := embeddedSchemaPayload([]string{"mail message search"}) if err != nil { @@ -241,14 +239,14 @@ func TestEmbeddedCatalogSharedRenameDocumentParamDeclsStayOnDriveLeaf(t *testing } docDescProv := schemaMap(docName["field_provenance"])["description"] if src, _ := docDescProv["source"].(string); src == "native_annotation" && strings.Contains(docDesc, "读取节点类型") { - t.Fatalf("doc rename --name description source/value look like a misplaced drive ParamDecl: %#v", docDescProv) + t.Fatalf("doc rename --name description source/value look like a misplaced drive contract.ParamDecl: %#v", docDescProv) } } func TestEmbeddedCatalogDriveUploadCompositeNodeParamDecl(t *testing.T) { // drive.upload is a composite multi-step leaf (no single RPCName / // interface_ref). The reviewed --node → nodeId overlay must stay on the - // upload leaf via ParamDecl, not on the manual upload-info / commit steps. + // upload leaf via contract.ParamDecl, not on the manual upload-info / commit steps. leaf, err := embeddedSchemaPayload([]string{"drive upload"}) if err != nil { t.Fatal(err) @@ -278,7 +276,7 @@ func TestEmbeddedCatalogDriveUploadCompositeNodeParamDecl(t *testing.T) { } siblingParams := schemaMap(sibling["parameters"]) if _, ok := siblingParams["node"]; ok { - t.Fatalf("%s unexpectedly publishes --node (upload overwrite ParamDecl leaked)", path) + t.Fatalf("%s unexpectedly publishes --node (upload overwrite contract.ParamDecl leaked)", path) } if got := schemaString(sibling["interface_mode"]); got != "mcp" { t.Fatalf("%s interface_mode = %q, want mcp", path, got) @@ -288,7 +286,7 @@ func TestEmbeddedCatalogDriveUploadCompositeNodeParamDecl(t *testing.T) { func TestEmbeddedCatalogDocReadParamDeclsMatchMergeBaseContract(t *testing.T) { // Former schema_hints overlays for get_document_content must stay on the - // doc read leaf after ParamDecl migration (87910880 value parity). + // doc read leaf after contract.ParamDecl migration (87910880 value parity). leaf, err := embeddedSchemaPayload([]string{"doc read"}) if err != nil { t.Fatal(err) @@ -365,7 +363,7 @@ func TestEmbeddedCatalogDocCommentParamDeclsMatchMergeBaseContract(t *testing.T) func TestEmbeddedCatalogDocRenameKeepsPassthroughNameDescription(t *testing.T) { // Shared-RPC sibling of drive rename: doc must keep the merge-base - // passthrough usage, not the drive extension-stripping ParamDecl. + // passthrough usage, not the drive extension-stripping contract.ParamDecl. leaf, err := embeddedSchemaPayload([]string{"doc rename"}) if err != nil { t.Fatal(err) @@ -376,7 +374,7 @@ func TestEmbeddedCatalogDocRenameKeepsPassthroughNameDescription(t *testing.T) { t.Fatalf("doc rename --name description = %q, want passthrough Cobra usage", desc) } if strings.Contains(desc, "避免双扩展名") || strings.Contains(desc, "仅对非文件夹") { - t.Fatalf("doc rename --name description incorrectly inherited drive ParamDecl: %q", desc) + t.Fatalf("doc rename --name description incorrectly inherited drive contract.ParamDecl: %q", desc) } descProv := schemaMap(name["field_provenance"])["description"] if src, _ := descProv["source"].(string); src != "cobra_usage" { @@ -742,10 +740,10 @@ func TestEmbeddedCatalogMinutesReplaceBatchPairDescriptionIsNative(t *testing.T) } func TestEmbeddedCatalogTodoParamDeclsFrom87910880Hints(t *testing.T) { - // Spot-check former metadata/todo.json overlays now declared as ParamDecl (87910880). + // Spot-check former metadata/todo.json overlays now declared as contract.ParamDecl (87910880). // --query-all is a Cobra Bool: merge-base catalog publishes type=boolean from // cobra_flag_type (no separate interface_type field), while required/description - // stay native_annotation after ParamDecl migration. + // stay native_annotation after contract.ParamDecl migration. listLeaf, err := embeddedSchemaPayload([]string{"todo task list"}) if err != nil { t.Fatal(err) @@ -858,7 +856,7 @@ func TestEmbeddedCatalogTodoParamDeclsFrom87910880Hints(t *testing.T) { } func TestEmbeddedCatalogSheetParamDeclsFrom87910880Hints(t *testing.T) { - // Former metadata/sheet.json overlays now declared as ParamDecl (87910880). + // Former metadata/sheet.json overlays now declared as contract.ParamDecl (87910880). for _, tc := range []struct { path string flag string @@ -884,8 +882,7 @@ func TestEmbeddedCatalogSheetParamDeclsFrom87910880Hints(t *testing.T) { } func TestEmbeddedCatalogPatSheetWikiRegisterSchemaHintParamDecls(t *testing.T) { - // Former RegisterSchemaHints required / required_when pins for pat, sheet, - // and wiki must publish via ParamDecl (native_annotation). + // pat/sheet/wiki required and required_when pins publish via ParamDecl. for _, tc := range []struct { path string flag string @@ -966,7 +963,7 @@ func TestEmbeddedCatalogReportCreateToChatRequiredAndAliasView(t *testing.T) { func TestEmbeddedCatalogAitableParamDeclsMatchMergeBaseContract(t *testing.T) { // Former schema_hints overlays (87910880) must stay on helpers leaves via - // ParamDecl after hint clearance — not on aitable +shortcuts. + // contract.ParamDecl after hint clearance — not on aitable +shortcuts. queryLeaf, err := embeddedSchemaPayload([]string{"aitable record query"}) if err != nil { t.Fatal(err) @@ -981,7 +978,7 @@ func TestEmbeddedCatalogAitableParamDeclsMatchMergeBaseContract(t *testing.T) { } // Sibling atomic get keeps Cobra-required --record-ids; shortcut has its - // own optional StringSlice surface and must not inherit helpers ParamDecl. + // own optional StringSlice surface and must not inherit helpers contract.ParamDecl. getLeaf, err := embeddedSchemaPayload([]string{"aitable record get"}) if err != nil { t.Fatal(err) @@ -1070,8 +1067,7 @@ func TestEmbeddedCatalogAitableParamDeclsMatchMergeBaseContract(t *testing.T) { func TestEmbeddedCatalogAitableQueryKeywordAndHintParamDecls(t *testing.T) { // helpers record query: --query → keyword must not fall back to - // flag_name_inference; JSON list flags keep wire interface_type via ParamDecl - // (outranking RegisterSchemaHints tool_schema_hint). + // flag_name_inference; JSON list flags keep wire interface_type via ParamDecl. queryLeaf, err := embeddedSchemaPayload([]string{"aitable record query"}) if err != nil { t.Fatal(err) @@ -1104,7 +1100,7 @@ func TestEmbeddedCatalogAitableQueryKeywordAndHintParamDecls(t *testing.T) { // shortcut +record-query Execute maps --query → MCP keyword at runtime, but // merge-base Schema published property=query (flag_name_inference). Keep that - // delivery for schema-compatibility; do not reintroduce ParamDecl keyword. + // delivery for schema-compatibility; do not reintroduce contract.ParamDecl keyword. shortcutLeaf, err := embeddedSchemaPayload([]string{"aitable +record-query"}) if err != nil { t.Fatal(err) @@ -1123,7 +1119,7 @@ func TestEmbeddedCatalogAitableQueryKeywordAndHintParamDecls(t *testing.T) { } } - // Former RegisterSchemaHints required pins that match runtime validation. + // Required pins that match runtime validation. for _, tc := range []struct { path string flag string @@ -1154,7 +1150,7 @@ func TestEmbeddedCatalogAitableQueryKeywordAndHintParamDecls(t *testing.T) { func TestEmbeddedCatalogShortcutQueryKeepsMergeBaseProperty(t *testing.T) { // Composite shortcuts whose Execute maps --query → MCP keyword still publish // property=query at merge-base (flag_name_inference). Publishing keyword via - // ParamDecl breaks schema-compatibility; keep Schema delivery aligned. + // contract.ParamDecl breaks schema-compatibility; keep Schema delivery aligned. for _, path := range []string{ "aitable +find-record", "doc +find-doc", @@ -1185,7 +1181,7 @@ func TestEmbeddedCatalogShortcutQueryKeepsMergeBaseProperty(t *testing.T) { func TestEmbeddedCatalogContactParamDeclsMatchMergeBaseContract(t *testing.T) { // Former metadata/contact.json overlays at 87910880 must stay on the - // published primary flags after ParamDecl migration. Hidden Cobra + // published primary flags after contract.ParamDecl migration. Hidden Cobra // aliases (--id/--userid/--dept-name/--super-dept*) must not appear. type wantParam struct { property string @@ -1345,7 +1341,7 @@ func TestEmbeddedCatalogChatParamDeclsFrom87910880Hints(t *testing.T) { } // Former metadata/chat.json overlays (87910880) must stay on - // helpers/shortcut leaves via ParamDecl after hint clearance. + // helpers/shortcut leaves via contract.ParamDecl after hint clearance. for _, tc := range []struct { path string flag string diff --git a/internal/cli/schema_cobra_binding.go b/internal/cli/schema_cobra_binding.go index 99d4eef0..e6b996f1 100644 --- a/internal/cli/schema_cobra_binding.go +++ b/internal/cli/schema_cobra_binding.go @@ -12,6 +12,7 @@ import ( "sort" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -162,7 +163,7 @@ func BindEffectiveCommandRegistry(root *cobra.Command, effective EffectiveComman // Index Contract-declared dry_run capabilities at bind time: every // process that resolves the command tree gets the reviewed set, not // only processes that also run Schema assembly. - if payload, ok := RuntimeContractFinal(item.PrimaryCommand); ok && payload.DryRun != nil { + if payload, ok := contract.RuntimeContractFinal(item.PrimaryCommand); ok && payload.DryRun != nil { recordDeclaredDryRunCapability(item.CanonicalPath, *payload.DryRun) } } @@ -293,7 +294,7 @@ func validateCompatibilityLeafContract(spec CommandSpec, primary, alias *cobra.C } // filterSchemaAnnotations returns a copy of annotations without dws.schema.* -// keys. Those are Schema-level metadata (ParamDecl / AttachSchema) that do not +// keys. Those are Schema-level metadata (contract.ParamDecl / AttachSchema) that do not // affect command execution and are resolved through the shared assembly path. func filterSchemaAnnotations(annotations map[string][]string) map[string][]string { if len(annotations) == 0 { @@ -313,7 +314,7 @@ func filterSchemaAnnotations(annotations map[string][]string) map[string][]strin // requiredContractEquivalent compares two Required contracts, tolerating // NativeRequired / NativeRequiredWhen being present on one side but empty on -// the other. This happens when a ParamDecl annotation exists on the primary +// the other. This happens when a contract.ParamDecl annotation exists on the primary // command but not on its compatibility alias. Genuine drift (both non-empty // but different) is still caught. func requiredContractEquivalent(a, b compatibilityFlagRequiredContract) bool { @@ -486,7 +487,7 @@ func compatibilityFlagContractProblems(canonicalPath string, primary, alias *cob name, compatibilityJSON(primaryFlag.Required), compatibilityJSON(aliasFlag.Required))) } // Compare annotations excluding dws.schema.* keys: those are Schema-level - // metadata declarations (ParamDecl / AttachSchema) that do not affect + // metadata declarations (contract.ParamDecl / AttachSchema) that do not affect // command execution. The compatibility check verifies executable equivalence; // Schema facts are resolved through the shared assembly path. primaryExecAnnotations := filterSchemaAnnotations(primaryFlag.Annotations) @@ -753,7 +754,7 @@ func canonicalCompatibilityGroups(groups [][]string) [][]string { return canonical } -func strictCompatibilityPositionals(command *cobra.Command) ([]RuntimeSchemaPositional, error) { +func strictCompatibilityPositionals(command *cobra.Command) ([]contract.RuntimeSchemaPositional, error) { if command == nil || command.Annotations == nil { return nil, nil } @@ -761,7 +762,7 @@ func strictCompatibilityPositionals(command *cobra.Command) ([]RuntimeSchemaPosi if raw == "" { return nil, nil } - var positionals []RuntimeSchemaPositional + var positionals []contract.RuntimeSchemaPositional if err := decodeStrictSchemaJSON([]byte(raw), &positionals); err != nil { return nil, err } diff --git a/internal/cli/schema_cobra_binding_coverage_more_test.go b/internal/cli/schema_cobra_binding_coverage_more_test.go index 905cc783..25e03e33 100644 --- a/internal/cli/schema_cobra_binding_coverage_more_test.go +++ b/internal/cli/schema_cobra_binding_coverage_more_test.go @@ -7,6 +7,7 @@ import ( "strings" "testing" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -294,7 +295,7 @@ func TestCrossPlatformCoverageCompatibilityPathAndHelperRemainingEdges(t *testin if got := compatibilityJSON(func() {}); !strings.Contains(got, "0x") { t.Fatalf("compatibilityJSON(func) = %q", got) } - positionals := []RuntimeSchemaPositional{{Index: 0, Name: "z"}, {Index: 0, Name: "a"}, {Index: 1, Name: "x"}} + positionals := []contract.RuntimeSchemaPositional{{Index: 0, Name: "z"}, {Index: 0, Name: "a"}, {Index: 1, Name: "x"}} command := &cobra.Command{Annotations: map[string]string{runtimeSchemaArgsAnnotation: compatibilityJSON(positionals)}} got, err := strictCompatibilityPositionals(command) if err != nil || got[0].Name != "a" || got[2].Index != 1 { diff --git a/internal/cli/schema_completeness_test.go b/internal/cli/schema_completeness_test.go index ad9cb2a2..eb25db84 100644 --- a/internal/cli/schema_completeness_test.go +++ b/internal/cli/schema_completeness_test.go @@ -10,6 +10,7 @@ import ( "strings" "testing" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -267,7 +268,7 @@ type schemaDeliveryTestTool struct { CLIPath string Aliases []string Parameters []ParameterSpec - Selection SelectionSpec + Selection contract.SelectionSpec } func schemaDeliveryTestRoot(tools ...schemaDeliveryTestTool) *cobra.Command { @@ -337,7 +338,7 @@ func schemaDeliveryTestRegistry(tools ...schemaDeliveryTestTool) SchemaRegistry provenanceValues["reviewed"] = *tool.Selection.Reviewed } spec, err := ToolSpecFromRuntime(RuntimeToolSpecInput{ - Identity: ToolIdentitySpec{ + Identity: contract.ToolIdentitySpec{ ProductID: parts[0], Name: parts[1], CLIName: strings.Fields(tool.CLIPath)[len(strings.Fields(tool.CLIPath))-1], @@ -348,7 +349,7 @@ func schemaDeliveryTestRegistry(tools ...schemaDeliveryTestTool) SchemaRegistry Source: "test", }, Parameters: append([]ParameterSpec(nil), tool.Parameters...), - Interface: InterfaceSpec{Mode: "local", Availability: "available", Reason: interfaceReason}, + Interface: contract.InterfaceSpec{Mode: "local", Availability: "available", Reason: interfaceReason}, Selection: tool.Selection, FieldProvenance: schemaDeliveryTestProvenance(provenanceValues), }) @@ -373,20 +374,20 @@ func schemaDeliveryTestRegistry(tools ...schemaDeliveryTestTool) SchemaRegistry return registry } -func schemaDeliveryTestProvenance(fields map[string]any) map[string]FieldProvenance { - provenance := make(map[string]FieldProvenance, len(fields)) +func schemaDeliveryTestProvenance(fields map[string]any) map[string]contract.FieldProvenance { + provenance := make(map[string]contract.FieldProvenance, len(fields)) for field, value := range fields { encoded, err := json.Marshal(value) if err != nil { panic(err) } selected := true - provenance[field] = FieldProvenance{ + provenance[field] = contract.FieldProvenance{ Value: encoded, Source: "test", Precedence: "test", Resolution: "test", - Candidates: []FieldCandidateProvenance{{Value: append(json.RawMessage(nil), encoded...), Source: "test", Precedence: "test", Selected: &selected}}, + Candidates: []contract.FieldCandidateProvenance{{Value: append(json.RawMessage(nil), encoded...), Source: "test", Precedence: "test", Selected: &selected}}, } } return provenance diff --git a/internal/cli/schema_contract_model.go b/internal/cli/schema_contract_model.go index 884846c9..68aa8c31 100644 --- a/internal/cli/schema_contract_model.go +++ b/internal/cli/schema_contract_model.go @@ -17,6 +17,7 @@ import ( "bytes" "encoding/json" "fmt" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "reflect" "sort" "strings" @@ -43,8 +44,8 @@ type ProductSpec struct { Description string Runtime bool Tools []ToolSpec - Selection SelectionSpec - FieldProvenance map[string]FieldProvenance + Selection contract.SelectionSpec + FieldProvenance map[string]contract.FieldProvenance Extensions map[string]json.RawMessage } @@ -52,19 +53,19 @@ type ProductSpec struct { // Safety, interface and selection are typed sub-models even though ToPayload // preserves the historical flat JSON shape for compatibility. type ToolSpec struct { - Identity ToolIdentitySpec + Identity contract.ToolIdentitySpec Display string Title string Description string MetadataSource string Parameters []ParameterSpec Constraints RuntimeSchemaConstraints - Positionals []RuntimeSchemaPositional - DryRun *DryRunSpec - Safety SafetySpec - Interface InterfaceSpec - Selection SelectionSpec - FieldProvenance map[string]FieldProvenance + Positionals []contract.RuntimeSchemaPositional + DryRun *contract.DryRunSpec + Safety contract.SafetySpec + Interface contract.InterfaceSpec + Selection contract.SelectionSpec + FieldProvenance map[string]contract.FieldProvenance Extensions map[string]json.RawMessage } @@ -87,7 +88,7 @@ type ParameterSpec struct { Enum []string InterfaceDescription string InterfaceType string - FieldProvenance map[string]FieldProvenance + FieldProvenance map[string]contract.FieldProvenance Extensions map[string]json.RawMessage } @@ -127,19 +128,19 @@ var requiredParameterProvenanceFields = [...]string{ // contract assembly. Adapters resolve candidates first, then call // ToolSpecFromRuntime exactly once; payload rendering does no further merging. type RuntimeToolSpecInput struct { - Identity ToolIdentitySpec + Identity contract.ToolIdentitySpec Display string Title string Description string MetadataSource string Parameters []ParameterSpec Constraints RuntimeSchemaConstraints - Positionals []RuntimeSchemaPositional - DryRun *DryRunSpec - Safety SafetySpec - Interface InterfaceSpec - Selection SelectionSpec - FieldProvenance map[string]FieldProvenance + Positionals []contract.RuntimeSchemaPositional + DryRun *contract.DryRunSpec + Safety contract.SafetySpec + Interface contract.InterfaceSpec + Selection contract.SelectionSpec + FieldProvenance map[string]contract.FieldProvenance Extensions map[string]json.RawMessage } @@ -569,7 +570,7 @@ func (t ToolSpec) Validate() error { return nil } -func validateFinalFieldProvenance(owner, field string, provenance FieldProvenance, finalValue any) error { +func validateFinalFieldProvenance(owner, field string, provenance contract.FieldProvenance, finalValue any) error { expected, err := json.Marshal(finalValue) if err != nil { return fmt.Errorf("%s field %s cannot encode final provenance value: %w", owner, field, err) @@ -733,7 +734,7 @@ func (t ToolSpec) normalized() ToolSpec { dryRun.PreviewKind = strings.TrimSpace(dryRun.PreviewKind) out.DryRun = &dryRun } - out.Positionals = append([]RuntimeSchemaPositional(nil), t.Positionals...) + out.Positionals = append([]contract.RuntimeSchemaPositional(nil), t.Positionals...) sort.Slice(out.Positionals, func(i, j int) bool { if out.Positionals[i].Index != out.Positionals[j].Index { return out.Positionals[i].Index < out.Positionals[j].Index @@ -1079,7 +1080,7 @@ func (p ParameterSpec) ToPayload() (map[string]any, error) { return payload, nil } -func applySafetyPayload(payload map[string]any, safety SafetySpec) { +func applySafetyPayload(payload map[string]any, safety contract.SafetySpec) { setOptionalString(payload, "effect", safety.Effect) setOptionalString(payload, "effect_source", safety.EffectSource) setOptionalString(payload, "risk", safety.Risk) @@ -1087,7 +1088,7 @@ func applySafetyPayload(payload map[string]any, safety SafetySpec) { setOptionalString(payload, "idempotency", safety.Idempotency) } -func applyInterfacePayload(payload map[string]any, spec InterfaceSpec) { +func applyInterfacePayload(payload map[string]any, spec contract.InterfaceSpec) { if spec.Ref != nil { payload["interface_ref"] = map[string]any{ "product_id": spec.Ref.ProductID, @@ -1099,7 +1100,7 @@ func applyInterfacePayload(payload map[string]any, spec InterfaceSpec) { setOptionalString(payload, "interface_reason", spec.Reason) } -func applySelectionPayload(payload map[string]any, selection SelectionSpec, full bool) { +func applySelectionPayload(payload map[string]any, selection contract.SelectionSpec, full bool) { setOptionalString(payload, "agent_summary", selection.AgentSummary) setOptionalString(payload, "agent_summary_source", selection.AgentSummarySource) setOptionalStrings(payload, "use_when", selection.UseWhen) diff --git a/internal/cli/schema_contract_model_coverage_more_test.go b/internal/cli/schema_contract_model_coverage_more_test.go index 5b59be11..2c4da87f 100644 --- a/internal/cli/schema_contract_model_coverage_more_test.go +++ b/internal/cli/schema_contract_model_coverage_more_test.go @@ -5,20 +5,21 @@ package cli import ( "encoding/json" "errors" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "strings" "testing" ) func contractCoverageTool(product, name, cliPath string) ToolSpec { canonical := product + "." + name - return ToolSpec{Identity: ToolIdentitySpec{ + return ToolSpec{Identity: contract.ToolIdentitySpec{ ProductID: product, Name: name, CLIName: name, CanonicalPath: canonical, Path: canonical, CLIPath: cliPath, PrimaryCLIPath: cliPath, }} } func TestCrossPlatformCoverageContractModelProvenanceValueAndDryRunEdges(t *testing.T) { - if err := (DryRunSpec{}).Validate(""); err == nil || !strings.Contains(err.Error(), "no preview_kind") { + if err := (contract.DryRunSpec{}).Validate(""); err == nil || !strings.Contains(err.Error(), "no preview_kind") { t.Fatalf("empty dry-run error = %v", err) } tool := contractCoverageTool("sample", "run", "sample run") @@ -141,14 +142,14 @@ func TestCrossPlatformCoverageToolSpecValidationRemainingEdges(t *testing.T) { v.Parameters = []ParameterSpec{{Name: "id", InterfaceDefault: json.RawMessage(`{`)}} }, want: "invalid JSON interface_default"}, {name: "example json", mutate: func(v *ToolSpec) { v.Parameters = []ParameterSpec{{Name: "id", Example: json.RawMessage(`{`)}} }, want: "invalid JSON example"}, - {name: "interface ref", mutate: func(v *ToolSpec) { v.Interface.Ref = &InterfaceRefSpec{} }, want: "incomplete interface_ref"}, - {name: "dry run", mutate: func(v *ToolSpec) { v.DryRun = &DryRunSpec{} }, want: "preview_kind"}, + {name: "interface ref", mutate: func(v *ToolSpec) { v.Interface.Ref = &contract.InterfaceRefSpec{} }, want: "incomplete interface_ref"}, + {name: "dry run", mutate: func(v *ToolSpec) { v.DryRun = &contract.DryRunSpec{} }, want: "preview_kind"}, {name: "interface", mutate: func(v *ToolSpec) { v.Interface.Mode = "unknown" }, want: "unknown interface mode"}, {name: "tool provenance", mutate: func(v *ToolSpec) { - v.FieldProvenance = map[string]FieldProvenance{"title": {Value: json.RawMessage(`"wrong"`)}} + v.FieldProvenance = map[string]contract.FieldProvenance{"title": {Value: json.RawMessage(`"wrong"`)}} }, want: "winner does not equal"}, {name: "parameter provenance", mutate: func(v *ToolSpec) { - v.Parameters = []ParameterSpec{{Name: "id", FieldProvenance: map[string]FieldProvenance{"required": {Value: json.RawMessage(`true`)}}}} + v.Parameters = []ParameterSpec{{Name: "id", FieldProvenance: map[string]contract.FieldProvenance{"required": {Value: json.RawMessage(`true`)}}}} }, want: "winner does not equal"}, } for _, tc := range cases { @@ -163,24 +164,24 @@ func TestCrossPlatformCoverageToolSpecValidationRemainingEdges(t *testing.T) { } func TestCrossPlatformCoverageFinalFieldProvenanceRemainingEdges(t *testing.T) { - if err := validateFinalFieldProvenance("owner", "field", FieldProvenance{}, func() {}); err == nil || !strings.Contains(err.Error(), "cannot encode") { + if err := validateFinalFieldProvenance("owner", "field", contract.FieldProvenance{}, func() {}); err == nil || !strings.Contains(err.Error(), "cannot encode") { t.Fatalf("final encode error = %v", err) } selected := true - base := FieldProvenance{ + base := contract.FieldProvenance{ Value: json.RawMessage(`"value"`), Source: "source", Precedence: "high", Resolution: "selected", - Candidates: []FieldCandidateProvenance{{Value: json.RawMessage(`"value"`), Source: "source", Precedence: "high", Selected: &selected}}, + Candidates: []contract.FieldCandidateProvenance{{Value: json.RawMessage(`"value"`), Source: "source", Precedence: "high", Selected: &selected}}, } disagrees := base - disagrees.Candidates = []FieldCandidateProvenance{{Value: json.RawMessage(`"value"`), Source: "other", Precedence: "high", Selected: &selected}} + disagrees.Candidates = []contract.FieldCandidateProvenance{{Value: json.RawMessage(`"value"`), Source: "other", Precedence: "high", Selected: &selected}} if err := validateFinalFieldProvenance("owner", "field", disagrees, "value"); err == nil || !strings.Contains(err.Error(), "disagrees with winner") { t.Fatalf("candidate disagreement = %v", err) } overridden := base - overridden.OverriddenCandidates = []FieldCandidateProvenance{{Value: json.RawMessage(`{`)}} + overridden.OverriddenCandidates = []contract.FieldCandidateProvenance{{Value: json.RawMessage(`{`)}} if err := validateFinalFieldProvenance("owner", "field", overridden, "value"); err == nil || !strings.Contains(err.Error(), "overridden provenance candidate with invalid") { t.Fatalf("overridden invalid error = %v", err) } @@ -193,8 +194,8 @@ func TestCrossPlatformCoverageContractModelNormalizationSortEdges(t *testing.T) registry := SchemaRegistry{Products: []ProductSpec{{ ID: " sample ", Tools: []ToolSpec{ - {Identity: ToolIdentitySpec{ProductID: "sample", Name: "same", CLIPath: "sample z"}}, - {Identity: ToolIdentitySpec{ProductID: "sample", Name: "same", CLIPath: "sample a"}}, + {Identity: contract.ToolIdentitySpec{ProductID: "sample", Name: "same", CLIPath: "sample z"}}, + {Identity: contract.ToolIdentitySpec{ProductID: "sample", Name: "same", CLIPath: "sample a"}}, }, }}} sorted := registry.Sorted() @@ -202,7 +203,7 @@ func TestCrossPlatformCoverageContractModelNormalizationSortEdges(t *testing.T) t.Fatalf("tool sort = %#v", sorted.Products[0].Tools) } tool := contractCoverageTool("sample", "run", "sample run") - tool.Positionals = []RuntimeSchemaPositional{{Index: 0, Name: "z"}, {Index: 0, Name: "a"}, {Index: 1, Name: "x"}} + tool.Positionals = []contract.RuntimeSchemaPositional{{Index: 0, Name: "z"}, {Index: 0, Name: "a"}, {Index: 1, Name: "x"}} normalized := tool.normalized() if normalized.Positionals[0].Name != "a" || normalized.Positionals[2].Index != 1 { t.Fatalf("positionals = %#v", normalized.Positionals) @@ -220,7 +221,7 @@ func TestCrossPlatformCoverageContractModelPayloadErrorEdges(t *testing.T) { if _, err := invalidRegistry.ToOverviewPayload(); err == nil { t.Fatal("invalid registry overview succeeded") } - overview, err := (SchemaRegistry{Products: []ProductSpec{{ID: "sample", Selection: SelectionSpec{UseWhen: []string{"use it"}}}}}).ToOverviewPayload() + overview, err := (SchemaRegistry{Products: []ProductSpec{{ID: "sample", Selection: contract.SelectionSpec{UseWhen: []string{"use it"}}}}}).ToOverviewPayload() if err != nil || schemaMapSlice(overview["products"])[0]["use_when"] == nil { t.Fatalf("use_when overview = %#v, %v", overview, err) } @@ -257,7 +258,7 @@ func TestCrossPlatformCoverageContractModelPayloadErrorEdges(t *testing.T) { if _, err := (ProductSpec{Extensions: map[string]json.RawMessage{"bad": badRaw}}).ToSummaryPayload(); err == nil { t.Fatal("product summary invalid extension rendered") } - badProvenance := map[string]FieldProvenance{"agent_summary": {Value: badRaw}} + badProvenance := map[string]contract.FieldProvenance{"agent_summary": {Value: badRaw}} if _, err := (ProductSpec{FieldProvenance: badProvenance}).ToPayload(); err == nil { t.Fatal("product invalid provenance rendered") } @@ -281,13 +282,13 @@ func TestCrossPlatformCoverageContractModelPayloadErrorEdges(t *testing.T) { } withDetails := validTool withDetails.Constraints = RuntimeSchemaConstraints{RequireOneOf: [][]string{{"id"}}} - withDetails.Positionals = []RuntimeSchemaPositional{{Index: 0, Name: "id"}} - withDetails.DryRun = &DryRunSpec{PreviewKind: DryRunPreviewPlan} + withDetails.Positionals = []contract.RuntimeSchemaPositional{{Index: 0, Name: "id"}} + withDetails.DryRun = &contract.DryRunSpec{PreviewKind: contract.DryRunPreviewPlan} if _, err := withDetails.ToPayload(); err != nil { t.Fatalf("typed details render error = %v", err) } withProvenance := validTool - withProvenance.FieldProvenance = map[string]FieldProvenance{"unknown": {Value: badRaw}} + withProvenance.FieldProvenance = map[string]contract.FieldProvenance{"unknown": {Value: badRaw}} if _, err := withProvenance.ToPayload(); err == nil { t.Fatal("tool invalid provenance rendered") } @@ -300,7 +301,7 @@ func TestCrossPlatformCoverageContractModelPayloadErrorEdges(t *testing.T) { "default": {Default: badRaw}, "interface default": {InterfaceDefault: badRaw}, "example": {Example: badRaw}, - "provenance": {FieldProvenance: map[string]FieldProvenance{"required": {Value: badRaw}}}, + "provenance": {FieldProvenance: map[string]contract.FieldProvenance{"required": {Value: badRaw}}}, } { if _, err := parameter.ToPayload(); err == nil { t.Errorf("parameter %s error was nil", name) diff --git a/internal/cli/schema_contract_model_test.go b/internal/cli/schema_contract_model_test.go index d82491a7..142fd739 100644 --- a/internal/cli/schema_contract_model_test.go +++ b/internal/cli/schema_contract_model_test.go @@ -15,31 +15,32 @@ package cli import ( "encoding/json" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "reflect" "strings" "testing" ) func TestInterfaceSpecValidateDispositionMatrix(t *testing.T) { - ref := &InterfaceRefSpec{ProductID: "calendar", RPCName: "get_event"} + ref := &contract.InterfaceRefSpec{ProductID: "calendar", RPCName: "get_event"} for _, test := range []struct { name string - spec InterfaceSpec + spec contract.InterfaceSpec wantError string executable bool }{ - {name: "mcp available", spec: InterfaceSpec{Mode: InterfaceModeMCP, Availability: InterfaceAvailable, Ref: ref}, executable: true}, - {name: "local available", spec: InterfaceSpec{Mode: InterfaceModeLocal, Availability: InterfaceAvailable}, executable: true}, - {name: "composite available", spec: InterfaceSpec{Mode: InterfaceModeComposite, Availability: InterfaceAvailable, Reason: "orchestrates operations"}, executable: true}, - {name: "mcp unavailable", spec: InterfaceSpec{Mode: InterfaceModeMCP, Availability: InterfaceUnavailable, Reason: "RPC temporarily disabled"}}, - {name: "local unavailable", spec: InterfaceSpec{Mode: InterfaceModeLocal, Availability: InterfaceUnavailable, Reason: "compatibility command frozen"}}, - {name: "composite unavailable", spec: InterfaceSpec{Mode: InterfaceModeComposite, Availability: InterfaceUnavailable, Reason: "workflow retired"}}, - {name: "legacy unavailable mode", spec: InterfaceSpec{Mode: InterfaceUnavailable, Availability: InterfaceUnavailable, Reason: "legacy"}, wantError: "legacy interface_mode=unavailable; migrate"}, - {name: "mcp available missing ref", spec: InterfaceSpec{Mode: InterfaceModeMCP, Availability: InterfaceAvailable}, wantError: "has no interface_ref"}, - {name: "local available with ref", spec: InterfaceSpec{Mode: InterfaceModeLocal, Availability: InterfaceAvailable, Ref: ref}, wantError: "must not declare interface_ref"}, - {name: "composite available missing reason", spec: InterfaceSpec{Mode: InterfaceModeComposite, Availability: InterfaceAvailable}, wantError: "must declare interface_reason"}, - {name: "unavailable with ref", spec: InterfaceSpec{Mode: InterfaceModeMCP, Availability: InterfaceUnavailable, Reason: "retired", Ref: ref}, wantError: "unavailable interface must not declare interface_ref"}, - {name: "unavailable missing reason", spec: InterfaceSpec{Mode: InterfaceModeLocal, Availability: InterfaceUnavailable}, wantError: "must declare interface_reason"}, + {name: "mcp available", spec: contract.InterfaceSpec{Mode: contract.InterfaceModeMCP, Availability: contract.InterfaceAvailable, Ref: ref}, executable: true}, + {name: "local available", spec: contract.InterfaceSpec{Mode: contract.InterfaceModeLocal, Availability: contract.InterfaceAvailable}, executable: true}, + {name: "composite available", spec: contract.InterfaceSpec{Mode: contract.InterfaceModeComposite, Availability: contract.InterfaceAvailable, Reason: "orchestrates operations"}, executable: true}, + {name: "mcp unavailable", spec: contract.InterfaceSpec{Mode: contract.InterfaceModeMCP, Availability: contract.InterfaceUnavailable, Reason: "RPC temporarily disabled"}}, + {name: "local unavailable", spec: contract.InterfaceSpec{Mode: contract.InterfaceModeLocal, Availability: contract.InterfaceUnavailable, Reason: "compatibility command frozen"}}, + {name: "composite unavailable", spec: contract.InterfaceSpec{Mode: contract.InterfaceModeComposite, Availability: contract.InterfaceUnavailable, Reason: "workflow retired"}}, + {name: "legacy unavailable mode", spec: contract.InterfaceSpec{Mode: contract.InterfaceUnavailable, Availability: contract.InterfaceUnavailable, Reason: "legacy"}, wantError: "legacy interface_mode=unavailable; migrate"}, + {name: "mcp available missing ref", spec: contract.InterfaceSpec{Mode: contract.InterfaceModeMCP, Availability: contract.InterfaceAvailable}, wantError: "has no interface_ref"}, + {name: "local available with ref", spec: contract.InterfaceSpec{Mode: contract.InterfaceModeLocal, Availability: contract.InterfaceAvailable, Ref: ref}, wantError: "must not declare interface_ref"}, + {name: "composite available missing reason", spec: contract.InterfaceSpec{Mode: contract.InterfaceModeComposite, Availability: contract.InterfaceAvailable}, wantError: "must declare interface_reason"}, + {name: "unavailable with ref", spec: contract.InterfaceSpec{Mode: contract.InterfaceModeMCP, Availability: contract.InterfaceUnavailable, Reason: "retired", Ref: ref}, wantError: "unavailable interface must not declare interface_ref"}, + {name: "unavailable missing reason", spec: contract.InterfaceSpec{Mode: contract.InterfaceModeLocal, Availability: contract.InterfaceUnavailable}, wantError: "must declare interface_reason"}, } { t.Run(test.name, func(t *testing.T) { err := test.spec.Validate("calendar.event_get") @@ -61,7 +62,7 @@ func TestToolSpecFromRuntimeBuildsOneTypedResolvedContract(t *testing.T) { selected := true reviewed := false spec, err := ToolSpecFromRuntime(RuntimeToolSpecInput{ - Identity: ToolIdentitySpec{ + Identity: contract.ToolIdentitySpec{ ProductID: " calendar ", Name: " attendee_delete ", CLIName: "delete", @@ -80,13 +81,13 @@ func TestToolSpecFromRuntimeBuildsOneTypedResolvedContract(t *testing.T) { Property: "eventId", Required: false, CLIRequired: true, - FieldProvenance: map[string]FieldProvenance{ + FieldProvenance: map[string]contract.FieldProvenance{ "required": { Value: json.RawMessage("false"), Source: "reviewed_manual_hint", Precedence: "reviewed_manual", Resolution: "highest_precedence", - Candidates: []FieldCandidateProvenance{{ + Candidates: []contract.FieldCandidateProvenance{{ Value: json.RawMessage("false"), Source: "reviewed_manual_hint", Precedence: "reviewed_manual", @@ -96,18 +97,18 @@ func TestToolSpecFromRuntimeBuildsOneTypedResolvedContract(t *testing.T) { }, }, }, - Safety: SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "low", Confirmation: "not_required", Idempotency: "non_idempotent", }, - Interface: InterfaceSpec{ - Ref: &InterfaceRefSpec{ProductID: "calendar", RPCName: "delete_attendee"}, + Interface: contract.InterfaceSpec{ + Ref: &contract.InterfaceRefSpec{ProductID: "calendar", RPCName: "delete_attendee"}, Mode: "mcp", Availability: "available", }, - Selection: SelectionSpec{ + Selection: contract.SelectionSpec{ UseWhen: []string{"remove attendee", "delete attendee", "remove attendee"}, AvoidWhen: []string{"keep attendee"}, Reviewed: &reviewed, @@ -141,16 +142,16 @@ func TestToolSpecFromRuntimeBuildsOneTypedResolvedContract(t *testing.T) { } func TestToolSpecDryRunCapabilityProjectsAndRoundTripsAtomically(t *testing.T) { - dryRun := &DryRunSpec{PreviewKind: DryRunPreviewDiff, RemoteReads: true} + dryRun := &contract.DryRunSpec{PreviewKind: contract.DryRunPreviewDiff, RemoteReads: true} spec, err := ToolSpecFromRuntime(RuntimeToolSpecInput{ - Identity: ToolIdentitySpec{ + Identity: contract.ToolIdentitySpec{ ProductID: "doc", Name: "update", CLIName: "update", CLIPath: "doc update", }, DryRun: dryRun, - FieldProvenance: map[string]FieldProvenance{ + FieldProvenance: map[string]contract.FieldProvenance{ "dry_run": resolvedFieldProvenance( dryRun, "reviewed_manual_hint", @@ -173,7 +174,7 @@ func TestToolSpecDryRunCapabilityProjectsAndRoundTripsAtomically(t *testing.T) { if !ok { t.Fatalf("dry_run = %#v", full["dry_run"]) } - if got := schemaString(delivered["preview_kind"]); got != DryRunPreviewDiff { + if got := schemaString(delivered["preview_kind"]); got != contract.DryRunPreviewDiff { t.Fatalf("dry_run.preview_kind = %q", got) } if got, ok := delivered["remote_reads"].(bool); !ok || !got { @@ -214,7 +215,7 @@ func TestToolSpecDryRunCapabilityProjectsAndRoundTripsAtomically(t *testing.T) { } func TestToolSpecDryRunCapabilityIsPositiveOnlyAndStrict(t *testing.T) { - base := RuntimeToolSpecInput{Identity: ToolIdentitySpec{ + base := RuntimeToolSpecInput{Identity: contract.ToolIdentitySpec{ ProductID: "sample", Name: "run", CLIName: "run", @@ -233,12 +234,12 @@ func TestToolSpecDryRunCapabilityIsPositiveOnlyAndStrict(t *testing.T) { t.Fatalf("nil capability unexpectedly emitted dry_run: %#v", payload["dry_run"]) } - base.DryRun = &DryRunSpec{PreviewKind: "unsupported"} + base.DryRun = &contract.DryRunSpec{PreviewKind: "unsupported"} if _, err := ToolSpecFromRuntime(base); err == nil || !strings.Contains(err.Error(), "unknown preview_kind") { t.Fatalf("invalid preview_kind error = %v", err) } - base.DryRun = &DryRunSpec{PreviewKind: DryRunPreviewInvocation} + base.DryRun = &contract.DryRunSpec{PreviewKind: contract.DryRunPreviewInvocation} withCapability, err := ToolSpecFromRuntime(base) if err != nil { t.Fatalf("ToolSpecFromRuntime(valid dry_run) error = %v", err) @@ -259,17 +260,17 @@ func TestToolSpecDryRunCapabilityIsPositiveOnlyAndStrict(t *testing.T) { func TestToolSpecDryRunProvenanceRejectsAtomicDrift(t *testing.T) { selected := true - dryRun := &DryRunSpec{PreviewKind: DryRunPreviewRequest} + dryRun := &contract.DryRunSpec{PreviewKind: contract.DryRunPreviewRequest} _, err := ToolSpecFromRuntime(RuntimeToolSpecInput{ - Identity: ToolIdentitySpec{ProductID: "sample", Name: "run", CLIPath: "sample run"}, + Identity: contract.ToolIdentitySpec{ProductID: "sample", Name: "run", CLIPath: "sample run"}, DryRun: dryRun, - FieldProvenance: map[string]FieldProvenance{ + FieldProvenance: map[string]contract.FieldProvenance{ "dry_run": { Value: json.RawMessage(`{"preview_kind":"plan"}`), Source: "reviewed_manual_hint", Precedence: "reviewed_manual", Resolution: "highest_precedence", - Candidates: []FieldCandidateProvenance{{ + Candidates: []contract.FieldCandidateProvenance{{ Value: json.RawMessage(`{"preview_kind":"plan"}`), Source: "reviewed_manual_hint", Precedence: "reviewed_manual", @@ -287,7 +288,7 @@ func TestToolSpecToPayloadKeepsCompatibleFlatShape(t *testing.T) { selected := true reviewed := true spec, err := ToolSpecFromRuntime(RuntimeToolSpecInput{ - Identity: ToolIdentitySpec{ + Identity: contract.ToolIdentitySpec{ ProductID: "chat", Name: "category_create_smart", CLIName: "create-smart", @@ -308,14 +309,14 @@ func TestToolSpecToPayloadKeepsCompatibleFlatShape(t *testing.T) { Example: json.RawMessage("true"), Enum: []string{"true", "false"}, InterfaceType: "boolean", - FieldProvenance: map[string]FieldProvenance{ + FieldProvenance: map[string]contract.FieldProvenance{ "required": { Value: json.RawMessage("false"), Source: "reviewed_manual_hint", Precedence: "reviewed_manual", Resolution: "highest_precedence", ReviewReason: "preview remains optional", - Candidates: []FieldCandidateProvenance{{ + Candidates: []contract.FieldCandidateProvenance{{ Value: json.RawMessage("false"), Source: "reviewed_manual_hint", Precedence: "reviewed_manual", @@ -324,27 +325,27 @@ func TestToolSpecToPayloadKeepsCompatibleFlatShape(t *testing.T) { }, }, }}, - Safety: SafetySpec{Effect: "write", Risk: "medium", Confirmation: "user_required"}, - Interface: InterfaceSpec{ - Ref: &InterfaceRefSpec{ProductID: "chat", RPCName: "create_smart_category"}, + Safety: contract.SafetySpec{Effect: "write", Risk: "medium", Confirmation: "user_required"}, + Interface: contract.InterfaceSpec{ + Ref: &contract.InterfaceRefSpec{ProductID: "chat", RPCName: "create_smart_category"}, Mode: "mcp", Availability: "available", Reason: "reviewed interface", }, - Selection: SelectionSpec{ + Selection: contract.SelectionSpec{ AgentSummary: "Create a category from rules.", UseWhen: []string{"create smart category"}, Examples: []string{"dws chat category create-smart --dry-run"}, Reviewed: &reviewed, MetadataSource: "resolved-agent-metadata", }, - FieldProvenance: map[string]FieldProvenance{ + FieldProvenance: map[string]contract.FieldProvenance{ "canonical_path": { Value: json.RawMessage(`"chat.category_create_smart"`), Source: "reviewed_manual_hint", Precedence: "reviewed_manual", Resolution: "highest_precedence", - Candidates: []FieldCandidateProvenance{{ + Candidates: []contract.FieldCandidateProvenance{{ Value: json.RawMessage(`"chat.category_create_smart"`), Source: "reviewed_manual_hint", Precedence: "reviewed_manual", @@ -403,7 +404,7 @@ func TestToolSpecToPayloadKeepsCompatibleFlatShape(t *testing.T) { func TestSchemaRegistryToPayloadIsDeterministic(t *testing.T) { tool := func(product, name, cliPath string) ToolSpec { - spec, err := ToolSpecFromRuntime(RuntimeToolSpecInput{Identity: ToolIdentitySpec{ + spec, err := ToolSpecFromRuntime(RuntimeToolSpecInput{Identity: contract.ToolIdentitySpec{ ProductID: product, Name: name, CLIName: name, @@ -450,10 +451,10 @@ func TestProductFieldProvenanceSurvivesTypedPayloadAndFailsOnDrift(t *testing.T) provenance := resolvedFieldProvenance("Calendar operations", "manual", "schema_manual_hints.json", "reviewed_manual", "highest_precedence", "reviewed") product := ProductSpec{ ID: "calendar", - Selection: SelectionSpec{ + Selection: contract.SelectionSpec{ AgentSummary: "Calendar operations", }, - FieldProvenance: map[string]FieldProvenance{"agent_summary": provenance}, + FieldProvenance: map[string]contract.FieldProvenance{"agent_summary": provenance}, } registry, err := SchemaRegistryFromRuntime("runtime-command", []ProductSpec{product}) if err != nil { @@ -480,7 +481,7 @@ func TestProductFieldProvenanceSurvivesTypedPayloadAndFailsOnDrift(t *testing.T) } func TestSchemaRegistryIndexResolvesCanonicalCLIAndAlias(t *testing.T) { - spec, err := ToolSpecFromRuntime(RuntimeToolSpecInput{Identity: ToolIdentitySpec{ + spec, err := ToolSpecFromRuntime(RuntimeToolSpecInput{Identity: contract.ToolIdentitySpec{ ProductID: "calendar", Name: "attendee_delete", CLIName: "delete", @@ -537,7 +538,7 @@ func TestSchemaRegistryIndexResolvesCanonicalCLIAndAlias(t *testing.T) { func TestSchemaRegistryIndexRejectsAmbiguousCLIIdentity(t *testing.T) { build := func(name, cliPath string, aliases ...string) ToolSpec { - spec, err := ToolSpecFromRuntime(RuntimeToolSpecInput{Identity: ToolIdentitySpec{ + spec, err := ToolSpecFromRuntime(RuntimeToolSpecInput{Identity: contract.ToolIdentitySpec{ ProductID: "chat", Name: name, CLIName: name, @@ -563,7 +564,7 @@ func TestSchemaRegistryIndexRejectsAmbiguousCLIIdentity(t *testing.T) { func TestSchemaRegistrySnapshotUsesSameToolForSummaryAndFullExport(t *testing.T) { spec, err := ToolSpecFromRuntime(RuntimeToolSpecInput{ - Identity: ToolIdentitySpec{ + Identity: contract.ToolIdentitySpec{ ProductID: "agoal", Name: "strategy_list", CLIName: "list", @@ -616,7 +617,7 @@ func TestSchemaRegistrySnapshotUsesSameToolForSummaryAndFullExport(t *testing.T) } func TestToolSpecStructuralValidationDoesNotEncodePrecedencePolicy(t *testing.T) { - _, err := ToolSpecFromRuntime(RuntimeToolSpecInput{Identity: ToolIdentitySpec{ + _, err := ToolSpecFromRuntime(RuntimeToolSpecInput{Identity: contract.ToolIdentitySpec{ ProductID: "chat", Name: "read", CanonicalPath: "chat.wrong", @@ -627,7 +628,7 @@ func TestToolSpecStructuralValidationDoesNotEncodePrecedencePolicy(t *testing.T) } _, err = ToolSpecFromRuntime(RuntimeToolSpecInput{ - Identity: ToolIdentitySpec{ProductID: "chat", Name: "read", CLIPath: "chat read"}, + Identity: contract.ToolIdentitySpec{ProductID: "chat", Name: "read", CLIPath: "chat read"}, Parameters: []ParameterSpec{{ Name: "query", Default: json.RawMessage("not-json"), @@ -652,12 +653,12 @@ func TestFinalFieldProvenancePreservesFalseAndEmptyJSONValues(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - provenance := FieldProvenance{ + provenance := contract.FieldProvenance{ Value: append(json.RawMessage(nil), test.raw...), Source: "reviewed", Precedence: "reviewed_explicit", Resolution: "highest_precedence", - Candidates: []FieldCandidateProvenance{{ + Candidates: []contract.FieldCandidateProvenance{{ Value: append(json.RawMessage(nil), test.raw...), Source: "reviewed", Precedence: "reviewed_explicit", @@ -674,12 +675,12 @@ func TestFinalFieldProvenancePreservesFalseAndEmptyJSONValues(t *testing.T) { func TestFinalFieldProvenanceIgnoresFormattingWhitespaceOnly(t *testing.T) { selected := true formatted := json.RawMessage("[\n \"first\",\n \"second\"\n]") - provenance := FieldProvenance{ + provenance := contract.FieldProvenance{ Value: formatted, Source: "manual", Precedence: "reviewed_manual", Resolution: "highest_precedence", - Candidates: []FieldCandidateProvenance{{ + Candidates: []contract.FieldCandidateProvenance{{ Value: formatted, Source: "manual", Precedence: "reviewed_manual", Selected: &selected, }}, } @@ -725,19 +726,19 @@ func TestToolSpecFromRuntimeDoesNotRepairResolvedProvenance(t *testing.T) { selected := true base := func() RuntimeToolSpecInput { return RuntimeToolSpecInput{ - Identity: ToolIdentitySpec{ + Identity: contract.ToolIdentitySpec{ ProductID: "sample", Name: "run", CanonicalPath: "sample.run", CLIPath: "sample run", }, - FieldProvenance: map[string]FieldProvenance{ + FieldProvenance: map[string]contract.FieldProvenance{ "canonical_path": { Value: json.RawMessage(`"sample.run"`), Source: "reviewed_command_registry", Precedence: "command_registry", Resolution: "registry_identity", - Candidates: []FieldCandidateProvenance{{ + Candidates: []contract.FieldCandidateProvenance{{ Value: json.RawMessage(`"sample.run"`), Source: "reviewed_command_registry", Precedence: "command_registry", @@ -750,26 +751,26 @@ func TestToolSpecFromRuntimeDoesNotRepairResolvedProvenance(t *testing.T) { tests := []struct { name string - mutate func(*FieldProvenance) + mutate func(*contract.FieldProvenance) wantErr string }{ { name: "winner value is not synthesized", - mutate: func(provenance *FieldProvenance) { + mutate: func(provenance *contract.FieldProvenance) { provenance.Value = nil }, wantErr: "winner does not equal final value", }, { name: "selected value is not rewritten", - mutate: func(provenance *FieldProvenance) { + mutate: func(provenance *contract.FieldProvenance) { provenance.Candidates[0].Value = json.RawMessage(`"sample.other"`) }, wantErr: "selected provenance candidate does not equal final value", }, { name: "precedence is not inferred", - mutate: func(provenance *FieldProvenance) { + mutate: func(provenance *contract.FieldProvenance) { provenance.Precedence = "" }, wantErr: "winner is incomplete", @@ -792,13 +793,13 @@ func TestToolSpecFromRuntimeDoesNotRepairResolvedProvenance(t *testing.T) { func TestFinalFieldProvenanceRequiresExactlyOneTypedWinner(t *testing.T) { selected := true unselected := false - valid := func() FieldProvenance { - return FieldProvenance{ + valid := func() contract.FieldProvenance { + return contract.FieldProvenance{ Value: json.RawMessage("false"), Source: "reviewed", Precedence: "reviewed_explicit", Resolution: "highest_precedence", - Candidates: []FieldCandidateProvenance{{ + Candidates: []contract.FieldCandidateProvenance{{ Value: json.RawMessage("false"), Source: "reviewed", Precedence: "reviewed_explicit", @@ -808,41 +809,41 @@ func TestFinalFieldProvenanceRequiresExactlyOneTypedWinner(t *testing.T) { } tests := []struct { name string - mutate func(*FieldProvenance) + mutate func(*contract.FieldProvenance) wantErr string }{ { name: "JSON type mismatch", - mutate: func(provenance *FieldProvenance) { + mutate: func(provenance *contract.FieldProvenance) { provenance.Value = json.RawMessage(`"false"`) }, wantErr: "winner does not equal final value", }, { name: "no candidates", - mutate: func(provenance *FieldProvenance) { + mutate: func(provenance *contract.FieldProvenance) { provenance.Candidates = nil }, wantErr: "has no candidates", }, { name: "no selected candidate", - mutate: func(provenance *FieldProvenance) { + mutate: func(provenance *contract.FieldProvenance) { provenance.Candidates[0].Selected = &unselected }, wantErr: "0 selected candidates", }, { name: "two selected candidates", - mutate: func(provenance *FieldProvenance) { + mutate: func(provenance *contract.FieldProvenance) { provenance.Candidates = append(provenance.Candidates, provenance.Candidates[0]) }, wantErr: "2 selected candidates", }, { name: "selected overridden candidate", - mutate: func(provenance *FieldProvenance) { - provenance.OverriddenCandidates = []FieldCandidateProvenance{{ + mutate: func(provenance *contract.FieldProvenance) { + provenance.OverriddenCandidates = []contract.FieldCandidateProvenance{{ Value: json.RawMessage("true"), Source: "imported", Precedence: "imported", Selected: &selected, }} }, @@ -850,8 +851,8 @@ func TestFinalFieldProvenanceRequiresExactlyOneTypedWinner(t *testing.T) { }, { name: "invalid unselected candidate value", - mutate: func(provenance *FieldProvenance) { - provenance.Candidates = append(provenance.Candidates, FieldCandidateProvenance{ + mutate: func(provenance *contract.FieldProvenance) { + provenance.Candidates = append(provenance.Candidates, contract.FieldCandidateProvenance{ Value: json.RawMessage("not-json"), Source: "bad", Precedence: "imported", Selected: &unselected, }) }, @@ -899,20 +900,20 @@ func TestProductionSnapshotLoaderDoesNotSynthesizeRequiredProvenance(t *testing. selectionCases := []struct { field string - selection SelectionSpec + selection contract.SelectionSpec }{ - {field: "use_when", selection: SelectionSpec{UseWhen: []string{}}}, - {field: "avoid_when", selection: SelectionSpec{AvoidWhen: []string{"avoid"}}}, - {field: "prerequisites", selection: SelectionSpec{Prerequisites: []string{"token"}}}, - {field: "tips", selection: SelectionSpec{Tips: []string{"tip"}}}, - {field: "workflow_refs", selection: SelectionSpec{WorkflowRefs: []string{"sample.other"}}}, - {field: "examples", selection: SelectionSpec{Examples: []string{"dws sample run"}}}, + {field: "use_when", selection: contract.SelectionSpec{UseWhen: []string{}}}, + {field: "avoid_when", selection: contract.SelectionSpec{AvoidWhen: []string{"avoid"}}}, + {field: "prerequisites", selection: contract.SelectionSpec{Prerequisites: []string{"token"}}}, + {field: "tips", selection: contract.SelectionSpec{Tips: []string{"tip"}}}, + {field: "workflow_refs", selection: contract.SelectionSpec{WorkflowRefs: []string{"sample.other"}}}, + {field: "examples", selection: contract.SelectionSpec{Examples: []string{"dws sample run"}}}, } reviewed := false selectionCases = append(selectionCases, struct { field string - selection SelectionSpec - }{field: "reviewed", selection: SelectionSpec{Reviewed: &reviewed}}) + selection contract.SelectionSpec + }{field: "reviewed", selection: contract.SelectionSpec{Reviewed: &reviewed}}) for _, test := range selectionCases { t.Run("selection "+test.field, func(t *testing.T) { snapshot := schemaDeliveryTestSnapshot(schemaDeliveryTestTool{ diff --git a/internal/cli/schema_delivery_invariants_test.go b/internal/cli/schema_delivery_invariants_test.go index b819efd9..a0178fc7 100644 --- a/internal/cli/schema_delivery_invariants_test.go +++ b/internal/cli/schema_delivery_invariants_test.go @@ -5,6 +5,7 @@ package cli import ( "encoding/json" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "strings" "testing" ) @@ -56,7 +57,7 @@ func TestValidateSchemaDeliveryInvariantsRejectsSelectionProvenanceOmission(t *t fixture := schemaDeliveryTestTool{ Canonical: "sample.run", CLIPath: "sample run", - Selection: SelectionSpec{Examples: []string{"dws sample run"}}, + Selection: contract.SelectionSpec{Examples: []string{"dws sample run"}}, } source := schemaDeliveryTestRegistry(fixture) snapshot := schemaDeliveryTestSnapshot(fixture) @@ -111,7 +112,7 @@ func TestSelectionExplicitEmptyListSurvivesFinalDelivery(t *testing.T) { Canonical: "sample.run", CLIPath: "sample category run", Aliases: []string{"sample legacy execute"}, - Selection: SelectionSpec{UseWhen: []string{}}, + Selection: contract.SelectionSpec{UseWhen: []string{}}, }) full, ok := snapshot.Tools["sample.run"]["use_when"].([]string) diff --git a/internal/cli/schema_dry_run_capabilities.go b/internal/cli/schema_dry_run_capabilities.go index 6559a311..8a6aaf4b 100644 --- a/internal/cli/schema_dry_run_capabilities.go +++ b/internal/cli/schema_dry_run_capabilities.go @@ -5,6 +5,7 @@ package cli import ( "fmt" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "sort" "strings" "sync" @@ -32,7 +33,7 @@ var reviewedDryRunCapabilityGroups = []dryRunCapabilityGroup{ var reviewedDryRunCapabilitiesLazy struct { once sync.Once - byCanonical map[string]DryRunSpec + byCanonical map[string]contract.DryRunSpec err error } @@ -42,12 +43,12 @@ var reviewedDryRunCapabilitiesLazy struct { // that resolves the tree gets the reviewed set, not only processes that run // Schema assembly. A declaration in reviewed code is itself the reviewed // capability, so no manual allowlist entry is required. -var declaredDryRunCapabilities sync.Map // string → DryRunSpec +var declaredDryRunCapabilities sync.Map // string → contract.DryRunSpec // recordDeclaredDryRunCapability registers one Contract-declared dry_run // capability. Conflicting re-declaration of the same canonical is a // programming error surfaced at the next delivery gate read. -func recordDeclaredDryRunCapability(canonical string, spec DryRunSpec) { +func recordDeclaredDryRunCapability(canonical string, spec contract.DryRunSpec) { canonical = strings.TrimSpace(canonical) if canonical == "" { return @@ -63,11 +64,11 @@ func clearDeclaredDryRunCapabilitiesForTest() { }) } -func loadManualDryRunCapabilities() (map[string]DryRunSpec, error) { +func loadManualDryRunCapabilities() (map[string]contract.DryRunSpec, error) { reviewedDryRunCapabilitiesLazy.once.Do(func() { - byCanonical := make(map[string]DryRunSpec) + byCanonical := make(map[string]contract.DryRunSpec) for _, group := range reviewedDryRunCapabilityGroups { - spec := DryRunSpec{PreviewKind: group.PreviewKind} + spec := contract.DryRunSpec{PreviewKind: group.PreviewKind} if err := spec.Validate(""); err != nil { reviewedDryRunCapabilitiesLazy.err = err return @@ -96,14 +97,14 @@ func loadManualDryRunCapabilities() (map[string]DryRunSpec, error) { if reviewedDryRunCapabilitiesLazy.err != nil { return nil, reviewedDryRunCapabilitiesLazy.err } - out := make(map[string]DryRunSpec, len(reviewedDryRunCapabilitiesLazy.byCanonical)) + out := make(map[string]contract.DryRunSpec, len(reviewedDryRunCapabilitiesLazy.byCanonical)) for canonical, spec := range reviewedDryRunCapabilitiesLazy.byCanonical { out[canonical] = spec } return out, nil } -func loadReviewedDryRunCapabilities() (map[string]DryRunSpec, error) { +func loadReviewedDryRunCapabilities() (map[string]contract.DryRunSpec, error) { out, err := loadManualDryRunCapabilities() if err != nil { return nil, err @@ -115,9 +116,9 @@ func loadReviewedDryRunCapabilities() (map[string]DryRunSpec, error) { mergeErr = fmt.Errorf("declared dry-run capability has non-string key %v", key) return false } - spec, ok := value.(DryRunSpec) + spec, ok := value.(contract.DryRunSpec) if !ok { - mergeErr = fmt.Errorf("declared dry-run capability %s has non-DryRunSpec value", canonical) + mergeErr = fmt.Errorf("declared dry-run capability %s has non-contract.DryRunSpec value", canonical) return false } if manual, exists := out[canonical]; exists && manual != spec { @@ -135,11 +136,11 @@ func loadReviewedDryRunCapabilities() (map[string]DryRunSpec, error) { // ReviewedDryRunCapabilities returns a defensive copy of the positive, // reviewed capability registry for delivery gates. -func ReviewedDryRunCapabilities() (map[string]DryRunSpec, error) { +func ReviewedDryRunCapabilities() (map[string]contract.DryRunSpec, error) { return loadReviewedDryRunCapabilities() } -func reviewedDryRunCapability(canonical string) (*DryRunSpec, error) { +func reviewedDryRunCapability(canonical string) (*contract.DryRunSpec, error) { capabilities, err := loadReviewedDryRunCapabilities() if err != nil { return nil, err @@ -159,7 +160,7 @@ func ValidateReviewedDryRunCapabilityDelivery(registry SchemaRegistry) error { if err != nil { return err } - actual := make(map[string]DryRunSpec) + actual := make(map[string]contract.DryRunSpec) for _, product := range registry.Products { for _, tool := range product.Tools { if tool.DryRun != nil { diff --git a/internal/cli/schema_hint_emptiness_contract_test.go b/internal/cli/schema_hint_emptiness_contract_test.go index 712cad84..ba99f0d0 100644 --- a/internal/cli/schema_hint_emptiness_contract_test.go +++ b/internal/cli/schema_hint_emptiness_contract_test.go @@ -14,6 +14,7 @@ package cli import ( + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "os" "path/filepath" "sort" @@ -22,7 +23,7 @@ import ( ) // TestDeletedProductionSchemaHintFilesStayGone locks stage-3 cleanup: the -// former production RegisterSchemaHints init files must not return. +// retired production hint init files must not return. func TestDeletedProductionSchemaHintFilesStayGone(t *testing.T) { deleted := []string{ "schema_hints_aitable.go", @@ -57,7 +58,7 @@ func TestSchemaHintsDirectoryRetired(t *testing.T) { // TestEmbeddedCatalogHasNoToolSchemaHintProvenance asserts the published // Catalog no longer selects tool_schema_hint as a provenance winner or -// retained candidate after RegisterSchemaHints → ParamDecl migration. +// retained candidate after RegisterSchemaHints → contract.ParamDecl migration. func TestEmbeddedCatalogHasNoToolSchemaHintProvenance(t *testing.T) { if !embeddedSchemaCatalogAvailable() { t.Fatal("embedded schema catalog unavailable") @@ -82,7 +83,7 @@ func TestEmbeddedCatalogHasNoToolSchemaHintProvenance(t *testing.T) { } } -func toolSchemaHintProvenanceHits(prefix string, provenance map[string]FieldProvenance) []string { +func toolSchemaHintProvenanceHits(prefix string, provenance map[string]contract.FieldProvenance) []string { if len(provenance) == 0 { return nil } @@ -94,16 +95,16 @@ func toolSchemaHintProvenanceHits(prefix string, provenance map[string]FieldProv sort.Strings(fields) for _, field := range fields { prov := provenance[field] - if prov.Source == runtimeSchemaPrecedenceToolHint { + if prov.Source == "tool_schema_hint" { hits = append(hits, prefix+"."+field+"#winner") } for i, candidate := range prov.Candidates { - if candidate.Source == runtimeSchemaPrecedenceToolHint { + if candidate.Source == "tool_schema_hint" { hits = append(hits, prefix+"."+field+"#candidate["+strconv.Itoa(i)+"]") } } for i, candidate := range prov.OverriddenCandidates { - if candidate.Source == runtimeSchemaPrecedenceToolHint { + if candidate.Source == "tool_schema_hint" { hits = append(hits, prefix+"."+field+"#overridden["+strconv.Itoa(i)+"]") } } diff --git a/internal/cli/schema_identity_resolver.go b/internal/cli/schema_identity_resolver.go index 3125fc91..c9dbad3d 100644 --- a/internal/cli/schema_identity_resolver.go +++ b/internal/cli/schema_identity_resolver.go @@ -7,6 +7,7 @@ import ( "encoding/json" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -14,12 +15,12 @@ import ( // any agreeing implementation evidence. Native/manual annotations never // compete for identity here: the Cobra binder has already rejected every // disagreement before a ToolSpec can be assembled. -func commandRegistryIdentityProvenance(command BoundCommandSpec) FieldProvenance { +func commandRegistryIdentityProvenance(command BoundCommandSpec) contract.FieldProvenance { canonical := strings.TrimSpace(command.CanonicalPath) value, _ := json.Marshal(canonical) selected := true - precedence := commandIdentityPrecedence(command.Source) - candidates := []FieldCandidateProvenance{{ + precedence := "command_registry" + candidates := []contract.FieldCandidateProvenance{{ Value: append(json.RawMessage(nil), value...), Source: command.Source, SourceRef: command.PrimaryCLIPath, @@ -37,7 +38,7 @@ func commandRegistryIdentityProvenance(command BoundCommandSpec) FieldProvenance if productID, toolName, sourceRef := runtimeSchemaAnnotations(leaf); productID != "" && toolName != "" { nativeValue, _ := json.Marshal(productID + "." + toolName) unselected := false - candidates = append(candidates, FieldCandidateProvenance{ + candidates = append(candidates, contract.FieldCandidateProvenance{ Value: nativeValue, Source: "native_annotation", SourceRef: defaultString(strings.TrimSpace(sourceRef), path), @@ -51,7 +52,7 @@ func commandRegistryIdentityProvenance(command BoundCommandSpec) FieldProvenance for _, alias := range command.AliasCommands { appendEvidence(alias.Path, alias.Command) } - return FieldProvenance{ + return contract.FieldProvenance{ Value: value, Source: command.Source, SourceRef: command.PrimaryCLIPath, @@ -61,12 +62,3 @@ func commandRegistryIdentityProvenance(command BoundCommandSpec) FieldProvenance Candidates: candidates, } } - -func commandIdentityPrecedence(source string) string { - switch strings.TrimSpace(source) { - case "reviewed_manual_hint": - return "reviewed_manual" - default: - return "command_registry" - } -} diff --git a/internal/cli/schema_interface_registry.go b/internal/cli/schema_interface_registry.go index a6df1fa4..ed09b968 100644 --- a/internal/cli/schema_interface_registry.go +++ b/internal/cli/schema_interface_registry.go @@ -15,6 +15,7 @@ package cli import ( "fmt" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "sort" "strings" ) @@ -31,7 +32,7 @@ type InterfaceRefKey struct { // MCP metadata. CanonicalPath is a metadata lookup key, not command identity. type InterfaceRegistryEntry struct { CanonicalPath string - Ref InterfaceRefSpec + Ref contract.InterfaceRefSpec Metadata embeddedMCPToolMetadata } @@ -86,7 +87,7 @@ func buildInterfaceRegistry(tools map[string]embeddedMCPToolMetadata) (Interface entry := InterfaceRegistryEntry{ CanonicalPath: canonical, - Ref: InterfaceRefSpec(key), + Ref: contract.InterfaceRefSpec(key), Metadata: metadata, } registry.ByCanonical[canonical] = entry @@ -127,7 +128,7 @@ func validateSchemaRegistryInterfacesWithMetadata(schema SchemaRegistry, metadat problems = append(problems, err.Error()) continue } - if mode := strings.TrimSpace(tool.Interface.Mode); mode == InterfaceModeMCP && tool.Interface.AgentExecutable() { + if mode := strings.TrimSpace(tool.Interface.Mode); mode == contract.InterfaceModeMCP && tool.Interface.AgentExecutable() { if err := validateToolInterfaceRef(canonical, mode, tool.Interface.Ref, interfaces); err != nil { problems = append(problems, err.Error()) } @@ -141,7 +142,7 @@ func validateSchemaRegistryInterfacesWithMetadata(schema SchemaRegistry, metadat return nil } -func validateToolInterfaceRef(canonical, mode string, ref *InterfaceRefSpec, interfaces InterfaceRegistry) error { +func validateToolInterfaceRef(canonical, mode string, ref *contract.InterfaceRefSpec, interfaces InterfaceRegistry) error { if ref == nil { return fmt.Errorf("schema tool %s with interface mode %s has no interface_ref", canonical, mode) } diff --git a/internal/cli/schema_interface_registry_test.go b/internal/cli/schema_interface_registry_test.go index e3c5464d..7d194092 100644 --- a/internal/cli/schema_interface_registry_test.go +++ b/internal/cli/schema_interface_registry_test.go @@ -14,6 +14,7 @@ package cli import ( + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "reflect" "strings" "testing" @@ -63,10 +64,10 @@ func TestValidateSchemaRegistryInterfacesUsesExplicitRefNotCanonical(t *testing. err := validateSchemaRegistryInterfacesWithMetadata(schemaWithInterfaceTool( "calendar.command_name_differs", - InterfaceSpec{ + contract.InterfaceSpec{ Mode: "mcp", Availability: "available", - Ref: &InterfaceRefSpec{ProductID: "calendar-rpc", RPCName: "get_event"}, + Ref: &contract.InterfaceRefSpec{ProductID: "calendar-rpc", RPCName: "get_event"}, }, ), metadata) if err != nil { @@ -77,7 +78,7 @@ func TestValidateSchemaRegistryInterfacesUsesExplicitRefNotCanonical(t *testing. func TestValidateSchemaRegistryInterfacesRejectsMissingMCPRef(t *testing.T) { err := validateSchemaRegistryInterfacesForTest(schemaWithInterfaceTool( "calendar.event_get", - InterfaceSpec{Mode: "mcp", Availability: "available"}, + contract.InterfaceSpec{Mode: "mcp", Availability: "available"}, )) if err == nil || !strings.Contains(err.Error(), "has no interface_ref") { t.Fatalf("validateSchemaRegistryInterfaces() error = %v, want missing ref", err) @@ -87,10 +88,10 @@ func TestValidateSchemaRegistryInterfacesRejectsMissingMCPRef(t *testing.T) { func TestValidateSchemaRegistryInterfacesRejectsIncompleteMCPRef(t *testing.T) { err := validateSchemaRegistryInterfacesForTest(schemaWithInterfaceTool( "calendar.event_get", - InterfaceSpec{ + contract.InterfaceSpec{ Mode: "mcp", Availability: "available", - Ref: &InterfaceRefSpec{ProductID: "calendar"}, + Ref: &contract.InterfaceRefSpec{ProductID: "calendar"}, }, )) if err == nil || !strings.Contains(err.Error(), "has incomplete interface_ref") { @@ -101,10 +102,10 @@ func TestValidateSchemaRegistryInterfacesRejectsIncompleteMCPRef(t *testing.T) { func TestValidateSchemaRegistryInterfacesRejectsPhantomRef(t *testing.T) { err := validateSchemaRegistryInterfacesForTest(schemaWithInterfaceTool( "calendar.event_get", - InterfaceSpec{ + contract.InterfaceSpec{ Mode: "mcp", Availability: "available", - Ref: &InterfaceRefSpec{ProductID: "calendar", RPCName: "phantom"}, + Ref: &contract.InterfaceRefSpec{ProductID: "calendar", RPCName: "phantom"}, }, )) if err == nil || !strings.Contains(err.Error(), "unknown MCP interface calendar.phantom") { @@ -115,7 +116,7 @@ func TestValidateSchemaRegistryInterfacesRejectsPhantomRef(t *testing.T) { func TestValidateSchemaRegistryInterfacesAllowsLocalWithoutRef(t *testing.T) { err := validateSchemaRegistryInterfacesForTest(schemaWithInterfaceTool( "calendar.local_cache", - InterfaceSpec{Mode: "local", Availability: "available", Reason: "implemented by the local CLI"}, + contract.InterfaceSpec{Mode: "local", Availability: "available", Reason: "implemented by the local CLI"}, )) if err != nil { t.Fatalf("validateSchemaRegistryInterfaces() error = %v", err) @@ -125,7 +126,7 @@ func TestValidateSchemaRegistryInterfacesAllowsLocalWithoutRef(t *testing.T) { func TestValidateSchemaRegistryInterfacesAllowsCompositeWithoutRef(t *testing.T) { err := validateSchemaRegistryInterfacesForTest(schemaWithInterfaceTool( "calendar.composite_sync", - InterfaceSpec{Mode: "composite", Availability: "available", Reason: "orchestrates multiple operations"}, + contract.InterfaceSpec{Mode: "composite", Availability: "available", Reason: "orchestrates multiple operations"}, )) if err != nil { t.Fatalf("validateSchemaRegistryInterfaces() error = %v", err) @@ -135,11 +136,11 @@ func TestValidateSchemaRegistryInterfacesAllowsCompositeWithoutRef(t *testing.T) func TestValidateSchemaRegistryInterfacesRejectsAnyLocalRef(t *testing.T) { err := validateSchemaRegistryInterfacesForTest(schemaWithInterfaceTool( "calendar.local_cache", - InterfaceSpec{ + contract.InterfaceSpec{ Mode: "local", Availability: "available", Reason: "implemented by the local CLI", - Ref: &InterfaceRefSpec{ProductID: "calendar", RPCName: "get_event"}, + Ref: &contract.InterfaceRefSpec{ProductID: "calendar", RPCName: "get_event"}, }, )) if err == nil || !strings.Contains(err.Error(), "must not declare interface_ref") { @@ -150,7 +151,7 @@ func TestValidateSchemaRegistryInterfacesRejectsAnyLocalRef(t *testing.T) { func TestValidateSchemaRegistryInterfacesAcceptsReviewedUnavailable(t *testing.T) { err := validateSchemaRegistryInterfacesForTest(schemaWithInterfaceTool( "calendar.retired", - InterfaceSpec{Mode: "local", Availability: "unavailable", Reason: "the upstream operation was retired"}, + contract.InterfaceSpec{Mode: "local", Availability: "unavailable", Reason: "the upstream operation was retired"}, )) if err != nil { t.Fatalf("validateSchemaRegistryInterfaces() error = %v", err) @@ -158,29 +159,29 @@ func TestValidateSchemaRegistryInterfacesAcceptsReviewedUnavailable(t *testing.T } func TestInterfaceSpecDispositionMatrix(t *testing.T) { - validRef := &InterfaceRefSpec{ProductID: "calendar", RPCName: "get_event"} + validRef := &contract.InterfaceRefSpec{ProductID: "calendar", RPCName: "get_event"} for _, test := range []struct { name string - spec InterfaceSpec + spec contract.InterfaceSpec wantError string executable bool }{ - {name: "mcp available", spec: InterfaceSpec{Mode: "mcp", Availability: "available", Ref: validRef}, executable: true}, - {name: "local available", spec: InterfaceSpec{Mode: "local", Availability: "available"}, executable: true}, - {name: "composite available", spec: InterfaceSpec{Mode: "composite", Availability: "available", Reason: "orchestrates operations"}, executable: true}, - {name: "mcp unavailable", spec: InterfaceSpec{Mode: "mcp", Availability: "unavailable", Reason: "RPC retired"}}, - {name: "local unavailable", spec: InterfaceSpec{Mode: "local", Availability: "unavailable", Reason: "compatibility command frozen"}}, - {name: "composite unavailable", spec: InterfaceSpec{Mode: "composite", Availability: "unavailable", Reason: "workflow retired"}}, - {name: "unavailable is not a mode", spec: InterfaceSpec{Mode: "unavailable", Availability: "unavailable", Reason: "retired"}, wantError: "legacy interface_mode=unavailable; migrate"}, - {name: "missing mode", spec: InterfaceSpec{Availability: "available"}, wantError: "has no interface mode"}, - {name: "missing availability", spec: InterfaceSpec{Mode: "local"}, wantError: "has no interface availability"}, - {name: "unknown availability", spec: InterfaceSpec{Mode: "local", Availability: "disabled"}, wantError: `unknown interface availability "disabled"`}, - {name: "unavailable with ref", spec: InterfaceSpec{Mode: "mcp", Availability: "unavailable", Reason: "retired", Ref: validRef}, wantError: "unavailable interface must not declare interface_ref"}, - {name: "unavailable without reason", spec: InterfaceSpec{Mode: "local", Availability: "unavailable"}, wantError: "must declare interface_reason"}, - {name: "available mcp without ref", spec: InterfaceSpec{Mode: "mcp", Availability: "available"}, wantError: "has no interface_ref"}, - {name: "available local with ref", spec: InterfaceSpec{Mode: "local", Availability: "available", Ref: validRef}, wantError: "must not declare interface_ref"}, - {name: "available composite with ref", spec: InterfaceSpec{Mode: "composite", Availability: "available", Reason: "orchestrates operations", Ref: validRef}, wantError: "must not declare a single interface_ref"}, - {name: "available composite without reason", spec: InterfaceSpec{Mode: "composite", Availability: "available"}, wantError: "must declare interface_reason"}, + {name: "mcp available", spec: contract.InterfaceSpec{Mode: "mcp", Availability: "available", Ref: validRef}, executable: true}, + {name: "local available", spec: contract.InterfaceSpec{Mode: "local", Availability: "available"}, executable: true}, + {name: "composite available", spec: contract.InterfaceSpec{Mode: "composite", Availability: "available", Reason: "orchestrates operations"}, executable: true}, + {name: "mcp unavailable", spec: contract.InterfaceSpec{Mode: "mcp", Availability: "unavailable", Reason: "RPC retired"}}, + {name: "local unavailable", spec: contract.InterfaceSpec{Mode: "local", Availability: "unavailable", Reason: "compatibility command frozen"}}, + {name: "composite unavailable", spec: contract.InterfaceSpec{Mode: "composite", Availability: "unavailable", Reason: "workflow retired"}}, + {name: "unavailable is not a mode", spec: contract.InterfaceSpec{Mode: "unavailable", Availability: "unavailable", Reason: "retired"}, wantError: "legacy interface_mode=unavailable; migrate"}, + {name: "missing mode", spec: contract.InterfaceSpec{Availability: "available"}, wantError: "has no interface mode"}, + {name: "missing availability", spec: contract.InterfaceSpec{Mode: "local"}, wantError: "has no interface availability"}, + {name: "unknown availability", spec: contract.InterfaceSpec{Mode: "local", Availability: "disabled"}, wantError: `unknown interface availability "disabled"`}, + {name: "unavailable with ref", spec: contract.InterfaceSpec{Mode: "mcp", Availability: "unavailable", Reason: "retired", Ref: validRef}, wantError: "unavailable interface must not declare interface_ref"}, + {name: "unavailable without reason", spec: contract.InterfaceSpec{Mode: "local", Availability: "unavailable"}, wantError: "must declare interface_reason"}, + {name: "available mcp without ref", spec: contract.InterfaceSpec{Mode: "mcp", Availability: "available"}, wantError: "has no interface_ref"}, + {name: "available local with ref", spec: contract.InterfaceSpec{Mode: "local", Availability: "available", Ref: validRef}, wantError: "must not declare interface_ref"}, + {name: "available composite with ref", spec: contract.InterfaceSpec{Mode: "composite", Availability: "available", Reason: "orchestrates operations", Ref: validRef}, wantError: "must not declare a single interface_ref"}, + {name: "available composite without reason", spec: contract.InterfaceSpec{Mode: "composite", Availability: "available"}, wantError: "must declare interface_reason"}, } { t.Run(test.name, func(t *testing.T) { err := validateSchemaRegistryInterfacesForTest(schemaWithInterfaceTool("calendar.event_get", test.spec)) @@ -202,7 +203,7 @@ func TestValidateSchemaRegistryInterfacesRejectsUnknownAndMissingMode(t *testing t.Run("missing", func(t *testing.T) { err := validateSchemaRegistryInterfacesForTest(schemaWithInterfaceTool( "calendar.event_get", - InterfaceSpec{Availability: "available"}, + contract.InterfaceSpec{Availability: "available"}, )) if err == nil || !strings.Contains(err.Error(), "has no interface mode") { t.Fatalf("validateSchemaRegistryInterfaces() error = %v, want missing mode", err) @@ -212,7 +213,7 @@ func TestValidateSchemaRegistryInterfacesRejectsUnknownAndMissingMode(t *testing t.Run("unknown", func(t *testing.T) { err := validateSchemaRegistryInterfacesForTest(schemaWithInterfaceTool( "calendar.event_get", - InterfaceSpec{Mode: "remote", Availability: "available"}, + contract.InterfaceSpec{Mode: "remote", Availability: "available"}, )) if err == nil || !strings.Contains(err.Error(), `unknown interface mode "remote"`) { t.Fatalf("validateSchemaRegistryInterfaces() error = %v, want unknown mode", err) @@ -220,9 +221,9 @@ func TestValidateSchemaRegistryInterfacesRejectsUnknownAndMissingMode(t *testing }) } -func schemaWithInterfaceTool(canonical string, interfaceSpec InterfaceSpec) SchemaRegistry { +func schemaWithInterfaceTool(canonical string, interfaceSpec contract.InterfaceSpec) SchemaRegistry { parts := strings.SplitN(canonical, ".", 2) - identity := ToolIdentitySpec{CanonicalPath: canonical} + identity := contract.ToolIdentitySpec{CanonicalPath: canonical} if len(parts) == 2 { identity.ProductID = parts[0] identity.Name = parts[1] diff --git a/internal/cli/schema_parameter_bindings.go b/internal/cli/schema_parameter_bindings.go index 97dc8bc9..323b2e05 100644 --- a/internal/cli/schema_parameter_bindings.go +++ b/internal/cli/schema_parameter_bindings.go @@ -26,6 +26,7 @@ import ( "sync" "sync/atomic" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -366,7 +367,7 @@ func finalSchemaParameterByName(tool ToolSpec, flagName string) (ParameterSpec, return ParameterSpec{}, false } -func schemaParameterProvenanceHasStringCandidate(provenance FieldProvenance, source, value string) bool { +func schemaParameterProvenanceHasStringCandidate(provenance contract.FieldProvenance, source, value string) bool { if provenance.Source == source { var selected string return json.Unmarshal(provenance.Value, &selected) == nil && selected == value diff --git a/internal/cli/schema_parameter_bindings_coverage_more_test.go b/internal/cli/schema_parameter_bindings_coverage_more_test.go index 03a18a12..70457f57 100644 --- a/internal/cli/schema_parameter_bindings_coverage_more_test.go +++ b/internal/cli/schema_parameter_bindings_coverage_more_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -117,9 +118,9 @@ func TestCrossPlatformCoverageSchemaParameterBindingDeliveryRemainingEdges(t *te command.Flags().String("id", "", "id") boundSpec := BoundCommandSpec{CommandSpec: CommandSpec{CanonicalPath: "sample.read", Visibility: SchemaVisibilityPublic}, PrimaryCommand: command} bound := BoundCommandRegistry{Commands: []BoundCommandSpec{boundSpec}, ByCanonical: map[string]BoundCommandSpec{"sample.read": boundSpec}} - versioned := FieldProvenance{Value: json.RawMessage(`"itemId"`), Source: "versioned_parameter_binding", Resolution: "selected"} - parameter := ParameterSpec{Name: "id", Property: "itemId", FieldProvenance: map[string]FieldProvenance{"property": versioned}} - registry := SchemaRegistry{Products: []ProductSpec{{ID: "sample", Tools: []ToolSpec{{Identity: ToolIdentitySpec{CanonicalPath: "sample.read"}, Parameters: []ParameterSpec{parameter}}}}}} + versioned := contract.FieldProvenance{Value: json.RawMessage(`"itemId"`), Source: "versioned_parameter_binding", Resolution: "selected"} + parameter := ParameterSpec{Name: "id", Property: "itemId", FieldProvenance: map[string]contract.FieldProvenance{"property": versioned}} + registry := SchemaRegistry{Products: []ProductSpec{{ID: "sample", Tools: []ToolSpec{{Identity: contract.ToolIdentitySpec{CanonicalPath: "sample.read"}, Parameters: []ParameterSpec{parameter}}}}}} cases := []struct { name string @@ -134,8 +135,8 @@ func TestCrossPlatformCoverageSchemaParameterBindingDeliveryRemainingEdges(t *te snapshot: schemaParameterBindingSnapshot{}, bound: bound, registry: SchemaRegistry{Products: []ProductSpec{{Tools: []ToolSpec{ - {Identity: ToolIdentitySpec{CanonicalPath: "sample.read"}}, - {Identity: ToolIdentitySpec{CanonicalPath: "sample.read"}}, + {Identity: contract.ToolIdentitySpec{CanonicalPath: "sample.read"}}, + {Identity: contract.ToolIdentitySpec{CanonicalPath: "sample.read"}}, }}}}, want: "duplicate canonical", }, @@ -147,7 +148,7 @@ func TestCrossPlatformCoverageSchemaParameterBindingDeliveryRemainingEdges(t *te snapshot: schemaParameterBindingSnapshot{Bindings: map[string]map[string]string{"sample.read": {"id": "itemId"}}}, bound: bound, registry: SchemaRegistry{Products: []ProductSpec{{Tools: []ToolSpec{{ - Identity: ToolIdentitySpec{CanonicalPath: "sample.read"}, + Identity: contract.ToolIdentitySpec{CanonicalPath: "sample.read"}, Parameters: []ParameterSpec{{Name: "id", Property: "itemId"}}, }}}}}, want: "no exact versioned", @@ -162,8 +163,8 @@ func TestCrossPlatformCoverageSchemaParameterBindingDeliveryRemainingEdges(t *te snapshot: schemaParameterBindingSnapshot{}, bound: bound, registry: SchemaRegistry{Products: []ProductSpec{{Tools: []ToolSpec{{ - Identity: ToolIdentitySpec{CanonicalPath: "sample.read"}, - Parameters: []ParameterSpec{{Name: "id", FieldProvenance: map[string]FieldProvenance{ + Identity: contract.ToolIdentitySpec{CanonicalPath: "sample.read"}, + Parameters: []ParameterSpec{{Name: "id", FieldProvenance: map[string]contract.FieldProvenance{ "property": {Source: "reviewed_mapping_exclusion"}, }}}, }}}}}, @@ -180,7 +181,7 @@ func TestCrossPlatformCoverageSchemaParameterBindingDeliveryRemainingEdges(t *te } noPropertyProvenance := registry - noPropertyProvenance.Products[0].Tools[0].Parameters[0].FieldProvenance = map[string]FieldProvenance{} + noPropertyProvenance.Products[0].Tools[0].Parameters[0].FieldProvenance = map[string]contract.FieldProvenance{} if err := validateSchemaParameterBindingDelivery(schemaParameterBindingSnapshot{}, bound, noPropertyProvenance); err != nil { t.Fatalf("absent optional property provenance rejected: %v", err) } @@ -190,14 +191,14 @@ func TestCrossPlatformCoverageSchemaParameterBindingHelpersAndLoaderErrors(t *te if _, ok := finalSchemaParameterByName(ToolSpec{}, "missing"); ok { t.Fatal("missing parameter found") } - selected := FieldProvenance{Value: json.RawMessage(`"value"`), Source: "source"} + selected := contract.FieldProvenance{Value: json.RawMessage(`"value"`), Source: "source"} if !schemaParameterProvenanceHasStringCandidate(selected, "source", "value") { t.Fatal("selected provenance not found") } - if schemaParameterProvenanceHasStringCandidate(FieldProvenance{Value: json.RawMessage(`{`), Source: "source"}, "source", "value") { + if schemaParameterProvenanceHasStringCandidate(contract.FieldProvenance{Value: json.RawMessage(`{`), Source: "source"}, "source", "value") { t.Fatal("invalid selected provenance matched") } - candidates := FieldProvenance{Candidates: []FieldCandidateProvenance{{Source: "other", Value: json.RawMessage(`"value"`)}, {Source: "source", Value: json.RawMessage(`{`)}, {Source: "source", Value: json.RawMessage(`"value"`)}}} + candidates := contract.FieldProvenance{Candidates: []contract.FieldCandidateProvenance{{Source: "other", Value: json.RawMessage(`"value"`)}, {Source: "source", Value: json.RawMessage(`{`)}, {Source: "source", Value: json.RawMessage(`"value"`)}}} if !schemaParameterProvenanceHasStringCandidate(candidates, "source", "value") || schemaParameterProvenanceHasStringCandidate(candidates, "missing", "value") { t.Fatal("candidate provenance lookup failed") } diff --git a/internal/cli/schema_parameter_bindings_test.go b/internal/cli/schema_parameter_bindings_test.go index 63f28f7d..b9966a60 100644 --- a/internal/cli/schema_parameter_bindings_test.go +++ b/internal/cli/schema_parameter_bindings_test.go @@ -11,6 +11,7 @@ import ( "strings" "testing" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -212,7 +213,7 @@ func TestValidateSchemaParameterBindingDeliveryRejectsStaleReviewedKeys(t *testi parameter := ParameterSpec{ Name: "item-id", Property: "itemId", - FieldProvenance: map[string]FieldProvenance{ + FieldProvenance: map[string]contract.FieldProvenance{ "property": { Value: json.RawMessage(`"itemId"`), Source: "versioned_parameter_binding", @@ -221,7 +222,7 @@ func TestValidateSchemaParameterBindingDeliveryRejectsStaleReviewedKeys(t *testi }, } registry := SchemaRegistry{Products: []ProductSpec{{ID: "sample", Tools: []ToolSpec{{ - Identity: ToolIdentitySpec{CanonicalPath: "sample.read"}, + Identity: contract.ToolIdentitySpec{CanonicalPath: "sample.read"}, Parameters: []ParameterSpec{parameter}, }}}}} valid := schemaParameterBindingSnapshot{Bindings: map[string]map[string]string{ diff --git a/internal/cli/schema_parameter_mapping_completeness_test.go b/internal/cli/schema_parameter_mapping_completeness_test.go index 1dafa4e7..1e66f998 100644 --- a/internal/cli/schema_parameter_mapping_completeness_test.go +++ b/internal/cli/schema_parameter_mapping_completeness_test.go @@ -10,6 +10,7 @@ import ( "strings" "testing" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -81,7 +82,7 @@ func TestEmbeddedCatalogLocalInterfacesAreExactAndReviewed(t *testing.T) { } gotLocal := make(map[string]bool) for canonical, tool := range loaded.Snapshot.Tools { - if schemaString(tool["interface_mode"]) != InterfaceModeLocal { + if schemaString(tool["interface_mode"]) != contract.InterfaceModeLocal { continue } gotLocal[canonical] = true @@ -90,7 +91,7 @@ func TestEmbeddedCatalogLocalInterfacesAreExactAndReviewed(t *testing.T) { t.Errorf("%s is classified local without a reviewed pure-local implementation", canonical) continue } - if schemaString(tool["availability"]) != InterfaceAvailable { + if schemaString(tool["availability"]) != contract.InterfaceAvailable { t.Errorf("%s local availability = %q, want available", canonical, schemaString(tool["availability"])) } if tool["interface_ref"] != nil { @@ -207,7 +208,7 @@ func TestSchemaParameterMappingAuditExclusionRules(t *testing.T) { "parameters": map[string]any{"selector": parameter}, }, } - if mode == InterfaceModeComposite { + if mode == contract.InterfaceModeComposite { tools["sample.read"]["interface_ref"] = nil tools["sample.read"]["interface_reason"] = "Reviewed composite fixture" tools["sample.read"]["field_provenance"] = map[string]any{ @@ -229,32 +230,32 @@ func TestSchemaParameterMappingAuditExclusionRules(t *testing.T) { }{ { name: "reviewed MCP mismatch", - tools: tool(InterfaceModeMCP, excludedParameter("cliOnly")), + tools: tool(contract.InterfaceModeMCP, excludedParameter("cliOnly")), metadata: directMetadata, snapshot: schemaParameterBindingSnapshot{MappingExclusions: map[string]string{key: "CLI selector is resolved before the RPC"}}, }, { name: "redundant MCP exclusion", - tools: tool(InterfaceModeMCP, excludedParameter("request.child")), + tools: tool(contract.InterfaceModeMCP, excludedParameter("request.child")), metadata: directMetadata, snapshot: schemaParameterBindingSnapshot{MappingExclusions: map[string]string{key: "stale"}}, wantProblem: "already resolves", }, { name: "local selector", - tools: tool(InterfaceModeLocal, excludedParameter("selector")), + tools: tool(contract.InterfaceModeLocal, excludedParameter("selector")), metadata: embeddedMCPMetadata{Tools: map[string]embeddedMCPToolMetadata{}}, snapshot: schemaParameterBindingSnapshot{MappingExclusions: map[string]string{key: "local planner input"}}, }, { name: "composite selector", - tools: tool(InterfaceModeComposite, excludedParameter("selector")), + tools: tool(contract.InterfaceModeComposite, excludedParameter("selector")), metadata: embeddedMCPMetadata{Tools: map[string]embeddedMCPToolMetadata{}}, snapshot: schemaParameterBindingSnapshot{MappingExclusions: map[string]string{key: "fans out to multiple calls"}}, }, { name: "binding conflict", - tools: tool(InterfaceModeLocal, excludedParameter("selector")), + tools: tool(contract.InterfaceModeLocal, excludedParameter("selector")), metadata: embeddedMCPMetadata{Tools: map[string]embeddedMCPToolMetadata{}}, snapshot: schemaParameterBindingSnapshot{ MappingExclusions: map[string]string{key: "local planner input"}, @@ -264,7 +265,7 @@ func TestSchemaParameterMappingAuditExclusionRules(t *testing.T) { }, { name: "unknown exact key", - tools: tool(InterfaceModeLocal, excludedParameter("selector")), + tools: tool(contract.InterfaceModeLocal, excludedParameter("selector")), metadata: embeddedMCPMetadata{Tools: map[string]embeddedMCPToolMetadata{}}, snapshot: schemaParameterBindingSnapshot{MappingExclusions: map[string]string{"sample.read --missing": "not exact"}}, wantProblem: "does not reference an exact final Catalog parameter", @@ -328,7 +329,7 @@ func TestRuntimeSchemaReviewedMappingExclusionSelectsEmptyProperty(t *testing.T) } payload, err := (ParameterSpec{ Name: "local-only", Type: "boolean", Description: "test", Property: "", - FieldProvenance: map[string]FieldProvenance{"property": provenance}, + FieldProvenance: map[string]contract.FieldProvenance{"property": provenance}, }).ToPayload() if err != nil { t.Fatal(err) @@ -353,9 +354,12 @@ func TestRuntimeSchemaReviewedMappingExclusionSelectsEmptyProperty(t *testing.T) if rank != runtimeSchemaRankVersionedBinding || precedence != runtimeSchemaPrecedenceMappingExclusion { t.Fatalf("mapping exclusion precedence = %d/%q", rank, precedence) } - manualWinner, err := resolveRuntimeSchemaCandidate("property", exclusion, runtimeSchemaManualCandidate("reviewedProperty", true, "reviewed override")) - if err != nil || manualWinner.Source != "reviewed_manual_hint" || manualWinner.Value != "reviewedProperty" { - t.Fatalf("reviewed manual must remain above mapping exclusion: winner=%#v err=%v", manualWinner, err) + // Mapping exclusion (650) outranks native_annotation (620); retired + // reviewed_manual_hint / tool_schema_hint ranks are gone. + native := runtimeSchemaCandidate("nativeProperty", true, "native_annotation") + exclusionWinner, err := resolveRuntimeSchemaCandidate("property", exclusion, native) + if err != nil || exclusionWinner.Source != "reviewed_mapping_exclusion" || exclusionWinner.Value != "" { + t.Fatalf("mapping exclusion must outrank native_annotation: winner=%#v err=%v", exclusionWinner, err) } } @@ -377,11 +381,11 @@ func TestReviewedCompositeParameterMappingRejectsInference(t *testing.T) { want bool }{ {name: "versioned binding", parameter: parameter("request.id", "versioned_parameter_binding", ""), want: true}, - {name: "reviewed manual", parameter: parameter("request.id", "reviewed_manual_hint", "reviewed mapping"), want: true}, - {name: "reviewed tool hint", parameter: parameter("request.id", "tool_schema_hint", "reviewed mapping"), want: true}, + {name: "native annotation", parameter: parameter("request.id", "native_annotation", ""), want: true}, {name: "mapping exclusion", parameter: parameter("", "reviewed_mapping_exclusion", "reviewed CLI-only input"), want: true}, {name: "flag inference", parameter: parameter("requestId", "flag_name_inference", ""), want: false}, - {name: "unreviewed tool hint", parameter: parameter("request.id", "tool_schema_hint", ""), want: false}, + {name: "retired tool hint", parameter: parameter("request.id", "tool_schema_hint", "reviewed mapping"), want: false}, + {name: "retired manual hint", parameter: parameter("request.id", "reviewed_manual_hint", "reviewed mapping"), want: false}, {name: "nonempty exclusion", parameter: parameter("request.id", "reviewed_mapping_exclusion", "reviewed"), want: false}, } for _, test := range tests { @@ -484,7 +488,7 @@ func auditSchemaParameterMappings(tools map[string]map[string]any, metadata embe tool := tools[canonical] mode := strings.TrimSpace(schemaString(tool["interface_mode"])) availability := strings.TrimSpace(schemaString(tool["availability"])) - if mode == InterfaceModeComposite && availability == InterfaceAvailable { + if mode == contract.InterfaceModeComposite && availability == contract.InterfaceAvailable { problems = append(problems, auditCompositeSchemaDisposition(canonical, tool)...) if strings.HasPrefix(strings.TrimSpace(schemaString(tool["interface_reason"])), "Reviewed unpinned remote adapter:") { parameters := schemaMap(tool["parameters"]) @@ -499,7 +503,7 @@ func auditSchemaParameterMappings(tools map[string]map[string]any, metadata embe } continue } - if mode != InterfaceModeMCP || availability != InterfaceAvailable { + if mode != contract.InterfaceModeMCP || availability != contract.InterfaceAvailable { continue } metadataKey, pinned, resolveProblems := pinnedMCPParameterMetadata(canonical, tool, metadata) @@ -558,12 +562,12 @@ func auditSchemaParameterMappings(tools map[string]map[string]any, metadata embe problems = append(problems, fmt.Sprintf("mapping_exclusions %q must deliver an omitted property, got %q", key, flag.property)) } switch flag.mode { - case InterfaceModeLocal, InterfaceModeComposite: + case contract.InterfaceModeLocal, contract.InterfaceModeComposite: // These modes do not claim one direct MCP parameter map. The exact, // reviewed reason and omitted final property are the whole contract. continue - case InterfaceModeMCP: - if flag.availability != InterfaceAvailable || len(flag.metadataParams) == 0 { + case contract.InterfaceModeMCP: + if flag.availability != contract.InterfaceAvailable || len(flag.metadataParams) == 0 { problems = append(problems, fmt.Sprintf("mapping_exclusions %q is not attached to an available pinned MCP parameter map", key)) continue } @@ -589,21 +593,12 @@ func reviewedCompositeParameterMapping(parameter map[string]any) (bool, string) property := strings.TrimSpace(schemaString(parameter["property"])) provenance := schemaMap(parameter["field_provenance"])["property"] source := strings.TrimSpace(schemaString(provenance["source"])) - reviewReason := strings.TrimSpace(schemaString(provenance["review_reason"])) switch source { case "versioned_parameter_binding", "typed_parameter_metadata", "native_annotation": if property == "" { return false, fmt.Sprintf("%s selected an empty property", source) } return true, "" - case "reviewed_manual_hint", "tool_schema_hint": - if reviewReason == "" { - return false, fmt.Sprintf("%s has no review_reason", source) - } - if property == "" { - return false, fmt.Sprintf("%s selected an empty property without a mapping exclusion", source) - } - return true, "" case "reviewed_mapping_exclusion": if property != "" { return false, fmt.Sprintf("reviewed_mapping_exclusion delivered property %q", property) diff --git a/internal/cli/schema_registry_test_helpers_test.go b/internal/cli/schema_registry_test_helpers_test.go index 20e98494..d090783d 100644 --- a/internal/cli/schema_registry_test_helpers_test.go +++ b/internal/cli/schema_registry_test_helpers_test.go @@ -8,6 +8,7 @@ import ( "sort" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -72,16 +73,16 @@ func schemaRegistryForTestWithMetadata(root *cobra.Command, agent embeddedAgentM if err != nil { return SchemaRegistry{}, err } - // Fixture-driven tests exercise the legacy agent-metadata overlay path. - // Clear bind-time migrated hint attaches so injected fixtures remain - // authoritative for the commands under test. + // Fixture-driven tests exercise the retired agent-metadata overlay path. + // Clear bind-time ContractFinal so injected fixtures remain authoritative + // for the commands under test. Production assembly never takes this path. for _, command := range bound.Commands { - ClearRuntimeContractFinalForTest(command.PrimaryCommand) + contract.ClearRuntimeContractFinalForTest(command.PrimaryCommand) for _, alias := range command.AliasCommands { - ClearRuntimeContractFinalForTest(alias.Command) + contract.ClearRuntimeContractFinalForTest(alias.Command) } } - return assembleSchemaRegistryFromBound(bound, runtimeSchemaMetadataSources{Agent: agent, MCP: mcp}) + return assembleSchemaRegistryFromBoundAllowingLegacy(bound, runtimeSchemaMetadataSources{Agent: agent, MCP: mcp}) } func runtimeSchemaPayloadForTest(root *cobra.Command, args []string) (map[string]any, error) { diff --git a/internal/cli/schema_runtime_registry.go b/internal/cli/schema_runtime_registry.go index b0554f35..31b7c74f 100644 --- a/internal/cli/schema_runtime_registry.go +++ b/internal/cli/schema_runtime_registry.go @@ -9,6 +9,7 @@ import ( "sort" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/spf13/cobra" ) @@ -77,7 +78,7 @@ func embeddedRuntimeSchemaMetadataSources() runtimeSchemaMetadataSources { // ResolveSchemaBuild is the only assembly path from executable Cobra commands // and reviewed metadata into the typed Agent contract. It resolves identity // once, binds Cobra once, and assembles one SchemaRegistry from ContractFinal / -// ProductDecl leaf declarations. Catalog gates and serialization consume the +// contract.ProductDecl leaf declarations. Catalog gates and serialization consume the // returned value directly; they never re-read overlays or merge sources. func ResolveSchemaBuild(root *cobra.Command) (ResolvedSchemaBuild, error) { if root == nil { @@ -127,28 +128,46 @@ func AssembleSchemaRegistryFromBound(bound BoundCommandRegistry) (SchemaRegistry return assembleSchemaRegistryFromBound(bound, embeddedRuntimeSchemaMetadataSources()) } +// assembleSchemaOptions controls production vs test-isolated assembly. +// allowLegacyMetadata is never set on the production Catalog path: missing +// ContractFinal / ProductDecl must fail closed rather than reopen skill/MCP +// /agent-inject overlays. +type assembleSchemaOptions struct { + allowLegacyMetadata bool +} + func assembleSchemaRegistryFromBound(bound BoundCommandRegistry, metadata runtimeSchemaMetadataSources) (SchemaRegistry, error) { + return assembleSchemaRegistryFromBoundWithOptions(bound, metadata, assembleSchemaOptions{}) +} + +// assembleSchemaRegistryFromBoundAllowingLegacy is test-only isolation for the +// retired skill/MCP/agent-metadata overlay path. Production assembly must never +// call this entry. +func assembleSchemaRegistryFromBoundAllowingLegacy(bound BoundCommandRegistry, metadata runtimeSchemaMetadataSources) (SchemaRegistry, error) { + return assembleSchemaRegistryFromBoundWithOptions(bound, metadata, assembleSchemaOptions{allowLegacyMetadata: true}) +} + +func assembleSchemaRegistryFromBoundWithOptions(bound BoundCommandRegistry, metadata runtimeSchemaMetadataSources, opts assembleSchemaOptions) (SchemaRegistry, error) { entries, err := assembleCollectEntries(bound) if err != nil { return SchemaRegistry{}, err } byProduct := make(map[string]*ProductSpec) for _, entry := range entries { - tool, err := assembleRuntimeToolSpec(entry, metadata) + var tool ToolSpec + if opts.allowLegacyMetadata { + tool, err = runtimeToolSpecAllowingLegacy(entry, metadata) + } else { + tool, err = assembleRuntimeToolSpec(entry, metadata) + } if err != nil { return SchemaRegistry{}, err } product := byProduct[entry.ProductID] if product == nil { - var selection SelectionSpec - var provenance map[string]FieldProvenance - // ProductDecl is the final product routing source (contract_final), - // symmetric to leaf RuntimeContractFinal. Selection JSON remains a - // fallback only when no Decl is registered. - if decl, ok := LookupProductDecl(entry.ProductID); ok { - selection, provenance = ProductSelectionFromDecl(decl) - } else { - selection, provenance, _ = agentProductContractForIDsFromMetadata(metadata.Agent, entry.ProductID, entry.SourceProductID) + selection, provenance, err := assembleProductSelection(entry, metadata, opts.allowLegacyMetadata) + if err != nil { + return SchemaRegistry{}, err } product = &ProductSpec{ ID: entry.ProductID, @@ -188,7 +207,31 @@ func assembleSchemaRegistryFromBound(bound BoundCommandRegistry, metadata runtim } func runtimeToolSpecFromMetadata(entry runtimeSchemaEntry, metadata runtimeSchemaMetadataSources) (ToolSpec, error) { - if final, ok := RuntimeContractFinal(entry.Command); ok { + if final, ok := contract.RuntimeContractFinal(entry.Command); ok { + return runtimeToolSpecFromContractFinal(entry, final, metadata) + } + canonicalPath := entry.ProductID + "." + entry.ToolName + return ToolSpec{}, fmt.Errorf("assemble Schema tool %s: missing RuntimeContractFinal (legacy skill/MCP/agent-metadata assembly is retired)", canonicalPath) +} + +// assembleProductSelection requires ProductDecl on the production path. +// Legacy agent-product JSON overlay is test-isolation only. +func assembleProductSelection(entry runtimeSchemaEntry, metadata runtimeSchemaMetadataSources, allowLegacy bool) (contract.SelectionSpec, map[string]contract.FieldProvenance, error) { + if decl, ok := contract.LookupProductDecl(entry.ProductID); ok { + selection, provenance := contract.ProductSelectionFromDecl(decl) + return selection, provenance, nil + } + if allowLegacy { + selection, provenance, _ := agentProductContractForIDsFromMetadata(metadata.Agent, entry.ProductID, entry.SourceProductID) + return selection, provenance, nil + } + return contract.SelectionSpec{}, nil, fmt.Errorf("assemble Schema product %q: missing ProductDecl (legacy agent-metadata product selection is retired)", entry.ProductID) +} + +// runtimeToolSpecAllowingLegacy is the test-isolated overlay path. Prefer +// ContractFinal when present; otherwise reopen retired skill/MCP/agent inject. +func runtimeToolSpecAllowingLegacy(entry runtimeSchemaEntry, metadata runtimeSchemaMetadataSources) (ToolSpec, error) { + if final, ok := contract.RuntimeContractFinal(entry.Command); ok { return runtimeToolSpecFromContractFinal(entry, final, metadata) } return runtimeToolSpecFromLegacyMetadata(entry, metadata) @@ -200,21 +243,23 @@ func runtimeToolSpecFromMetadata(entry runtimeSchemaEntry, metadata runtimeSchem // interface_type and interface-side required are interface facts the CLI // declaration does not own, and dropping them is a published-Schema // compatibility break, not a declaration takeover. -func runtimeToolSpecFromContractFinal(entry runtimeSchemaEntry, final ContractFinalPayload, metadata runtimeSchemaMetadataSources) (ToolSpec, error) { +func runtimeToolSpecFromContractFinal(entry runtimeSchemaEntry, final contract.ContractFinalPayload, metadata runtimeSchemaMetadataSources) (ToolSpec, error) { canonicalPath := entry.ProductID + "." + entry.ToolName constraints := runtimeCommandConstraints(entry.Command) embeddedMeta, _ := embeddedMCPMetadataForEntryFrom(entry, metadata.Agent, metadata.MCP) - // Apply parameter declarations from the ContractFinalPayload before the + // Apply parameter declarations from the contract.ContractFinalPayload before the // resolver reads them. The decls were put there by AttachSchema at // DeclareLeafMetadata time; now that all flags exist on the fully-built // command tree, they can be emitted as dws.schema.* annotations. - ApplyParamDecls(entry.Command, final.Parameters) + if err := ApplyParamDecls(entry.Command, final.Parameters); err != nil { + return ToolSpec{}, fmt.Errorf("apply Contract Schema ParamDecls for %s: %w", canonicalPath, err) + } parameters, err := resolveRuntimeParameters(entry.Command, canonicalPath, embeddedMeta.Parameters, constraints) if err != nil { return ToolSpec{}, fmt.Errorf("resolve Contract Schema parameters for %s: %w", canonicalPath, err) } - identity := ToolIdentitySpec{ + identity := contract.ToolIdentitySpec{ ProductID: entry.ProductID, SourceProductID: strings.TrimSpace(entry.SourceProductID), Name: entry.ToolName, @@ -278,7 +323,7 @@ func runtimeToolSpecFromContractFinal(entry runtimeSchemaEntry, final ContractFi description = strings.TrimSpace(entry.Command.Long) } - safety := SafetySpec{} + safety := contract.SafetySpec{} if final.Safety != nil { safety = *final.Safety } else if risk, ok := RuntimeContractRisk(entry.Command); ok { @@ -292,11 +337,11 @@ func runtimeToolSpecFromContractFinal(entry runtimeSchemaEntry, final ContractFi positionals = runtimeCommandPositionals(entry.Command) } - var interfaceSpec InterfaceSpec + var interfaceSpec contract.InterfaceSpec if final.Interface != nil { interfaceSpec = *final.Interface } - var selection SelectionSpec + var selection contract.SelectionSpec if final.Selection != nil { if final.Selection.Reviewed != nil { return ToolSpec{}, fmt.Errorf("contract final selection for %s must not carry reviewed field: declaration is the final source", canonicalPath) @@ -345,8 +390,8 @@ func runtimeToolSpecFromContractFinal(entry runtimeSchemaEntry, final ContractFi // field (safety/interface/agent_summary unconditionally, selection slices and // dry_run when present), so declared leaves must emit the full set, not only // the fields they happened to author. -func contractFinalProvenance(identity ToolIdentitySpec, title, description string, safety SafetySpec, iface InterfaceSpec, selection SelectionSpec, dryRun *DryRunSpec) map[string]FieldProvenance { - prov := func(value any, sourceRef string) FieldProvenance { +func contractFinalProvenance(identity contract.ToolIdentitySpec, title, description string, safety contract.SafetySpec, iface contract.InterfaceSpec, selection contract.SelectionSpec, dryRun *contract.DryRunSpec) map[string]contract.FieldProvenance { + prov := func(value any, sourceRef string) contract.FieldProvenance { return resolvedFieldProvenance( value, "corecmd.contract", @@ -356,7 +401,7 @@ func contractFinalProvenance(identity ToolIdentitySpec, title, description strin "Contract final Schema pass-through", ) } - out := map[string]FieldProvenance{ + out := map[string]contract.FieldProvenance{ "canonical_path": prov(identity.CanonicalPath, "corecmd.SchemaDecl"), "title": prov(title, "corecmd.SchemaDecl"), "description": prov(description, "corecmd.SchemaDecl"), @@ -375,8 +420,8 @@ func contractFinalProvenance(identity ToolIdentitySpec, title, description strin } out["interface_ref"] = prov(ref, "corecmd.SchemaDecl") if strings.TrimSpace(iface.Reason) != "" || - strings.TrimSpace(iface.Mode) == InterfaceModeComposite || - strings.TrimSpace(iface.Availability) == InterfaceUnavailable { + strings.TrimSpace(iface.Mode) == contract.InterfaceModeComposite || + strings.TrimSpace(iface.Availability) == contract.InterfaceUnavailable { out["interface_reason"] = prov(iface.Reason, "corecmd.SchemaDecl") } for field, values := range map[string][]string{ @@ -404,7 +449,7 @@ func contractFinalProvenance(identity ToolIdentitySpec, title, description strin // (managed) leaf, a declared identity must agree with the bound tree entry. // Otherwise the Schema catalog would publish an identity the registry never // indexed. Non-empty declared fields that differ from the entry fail assembly. -func validateContractFinalIdentity(entry runtimeSchemaEntry, id ToolIdentitySpec, canonicalPath string) error { +func validateContractFinalIdentity(entry runtimeSchemaEntry, id contract.ToolIdentitySpec, canonicalPath string) error { mismatches := make([]string, 0, 10) check := func(field, declared, bound string) { declared = strings.TrimSpace(declared) @@ -449,6 +494,9 @@ func stringSetsEqual(a, b []string) bool { return true } +// runtimeToolSpecFromLegacyMetadata rebuilds a ToolSpec from skill/MCP/agent +// inject overlays. Production assembly never calls this; only +// assembleSchemaRegistryFromBoundAllowingLegacy (tests) may. func runtimeToolSpecFromLegacyMetadata(entry runtimeSchemaEntry, metadata runtimeSchemaMetadataSources) (ToolSpec, error) { canonicalPath := entry.ProductID + "." + entry.ToolName dryRun, err := resolveReviewedDryRun(canonicalPath) @@ -481,7 +529,7 @@ func runtimeToolSpecFromLegacyMetadata(entry runtimeSchemaEntry, metadata runtim ) } if provenance == nil { - provenance = map[string]FieldProvenance{} + provenance = map[string]contract.FieldProvenance{} } for field, fieldProvenance := range textProvenance { provenance[field] = fieldProvenance @@ -546,7 +594,7 @@ func runtimeToolSpecFromLegacyMetadata(entry runtimeSchemaEntry, metadata runtim sourceProductID = "" } return ToolSpecFromRuntime(RuntimeToolSpecInput{ - Identity: ToolIdentitySpec{ + Identity: contract.ToolIdentitySpec{ ProductID: entry.ProductID, SourceProductID: sourceProductID, Name: entry.ToolName, @@ -794,7 +842,7 @@ func validateSchemaRegistryAgentMetadata(registry SchemaRegistry) error { // per-field equality invariant itself is enforced by ToolSpec.Validate. func validateFinalSchemaProvenanceCoverage(registry SchemaRegistry) error { var problems []string - require := func(owner, field string, provenance map[string]FieldProvenance) { + require := func(owner, field string, provenance map[string]contract.FieldProvenance) { if _, ok := provenance[field]; !ok { problems = append(problems, fmt.Sprintf("Schema %s has no provenance for %s", owner, field)) } @@ -826,8 +874,8 @@ func validateFinalSchemaProvenanceCoverage(registry SchemaRegistry) error { // local available command with no reason has no resolver winner to // invent; unavailable/composite commands fail closed without one. if strings.TrimSpace(tool.Interface.Reason) != "" || - strings.TrimSpace(tool.Interface.Mode) == InterfaceModeComposite || - strings.TrimSpace(tool.Interface.Availability) == InterfaceUnavailable { + strings.TrimSpace(tool.Interface.Mode) == contract.InterfaceModeComposite || + strings.TrimSpace(tool.Interface.Availability) == contract.InterfaceUnavailable { require("tool "+canonical, "interface_reason", tool.FieldProvenance) } selectionValues := map[string][]string{ diff --git a/internal/cli/schema_runtime_registry_contract_test.go b/internal/cli/schema_runtime_registry_contract_test.go index 77fea8be..f6c94119 100644 --- a/internal/cli/schema_runtime_registry_contract_test.go +++ b/internal/cli/schema_runtime_registry_contract_test.go @@ -6,10 +6,13 @@ package cli import ( "strings" "testing" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" + "github.com/spf13/cobra" ) func TestValidateSchemaRegistryAgainstCommandRegistryChecksFullIdentity(t *testing.T) { - tool := ToolSpec{Identity: ToolIdentitySpec{ + tool := ToolSpec{Identity: contract.ToolIdentitySpec{ ProductID: "sample", SourceProductID: "implementation_a", Name: "run", @@ -40,7 +43,7 @@ func TestValidateSchemaRegistryAgainstCommandRegistryChecksFullIdentity(t *testi want: "source product", }, "identity source": { - mutate: func(spec *CommandSpec) { spec.Source = "reviewed_manual_hint" }, + mutate: func(spec *CommandSpec) { spec.Source = "stale_identity_source" }, want: "identity source", }, } { @@ -60,7 +63,7 @@ func TestValidateSchemaRegistryAgainstCommandRegistryChecksFullIdentity(t *testi } func TestValidateSchemaRegistryAgainstCommandRegistryRejectsAliasViewAsCanonical(t *testing.T) { - baseTool := ToolSpec{Identity: ToolIdentitySpec{ + baseTool := ToolSpec{Identity: contract.ToolIdentitySpec{ ProductID: "sample", Name: "run", CanonicalPath: "sample.run", @@ -82,15 +85,15 @@ func TestValidateSchemaRegistryAgainstCommandRegistryRejectsAliasViewAsCanonical } for name, test := range map[string]struct { - mutate func(*ToolIdentitySpec) + mutate func(*contract.ToolIdentitySpec) want string }{ "alternate cli path": { - mutate: func(identity *ToolIdentitySpec) { identity.CLIPath = "sample execute" }, + mutate: func(identity *contract.ToolIdentitySpec) { identity.CLIPath = "sample execute" }, want: "must equal primary_cli_path", }, "alias marker": { - mutate: func(identity *ToolIdentitySpec) { identity.IsAlias = true }, + mutate: func(identity *contract.ToolIdentitySpec) { identity.IsAlias = true }, want: "must have is_alias=false", }, } { @@ -105,3 +108,128 @@ func TestValidateSchemaRegistryAgainstCommandRegistryRejectsAliasViewAsCanonical }) } } + +func TestAssembleSchemaRegistryFailClosedMissingContractFinal(t *testing.T) { + root := &cobra.Command{Use: "dws"} + leaf := &cobra.Command{Use: "run", Short: "Run", Run: func(*cobra.Command, []string) {}} + AttachRuntimeSchema(leaf, "sample", "run", "test") + product := &cobra.Command{Use: "sample"} + product.AddCommand(leaf) + root.AddCommand(product) + + t.Cleanup(func() { contract.ClearProductDeclForTest("sample") }) + contract.RegisterProductDecl(contract.ProductDecl{ + ID: "sample", + Selection: contract.ProductSelectionDecl{ + AgentSummary: "Sample product", + UseWhen: []string{"sample routing"}, + AvoidWhen: []string{"not sample"}, + }, + }) + + _, err := schemaRegistryForTest(root) + if err == nil || !strings.Contains(err.Error(), "missing RuntimeContractFinal") { + t.Fatalf("production assembly error = %v, want missing RuntimeContractFinal", err) + } +} + +func TestAssembleSchemaRegistryFailClosedMissingProductDecl(t *testing.T) { + root := &cobra.Command{Use: "dws"} + leaf := &cobra.Command{Use: "run", Short: "Run", Run: func(*cobra.Command, []string) {}} + AttachRuntimeSchema(leaf, "orphan", "run", "test") + RegisterRuntimeContractFinal(leaf, contract.ContractFinalPayload{ + Title: "Orphan run", + Description: "Has ContractFinal but no ProductDecl", + Selection: &contract.SelectionSpec{AgentSummary: "orphan leaf"}, + }) + t.Cleanup(func() { + contract.ClearRuntimeContractFinalForTest(leaf) + contract.ClearProductDeclForTest("orphan") + }) + product := &cobra.Command{Use: "orphan"} + product.AddCommand(leaf) + root.AddCommand(product) + + _, err := schemaRegistryForTest(root) + if err == nil || !strings.Contains(err.Error(), "missing ProductDecl") { + t.Fatalf("production assembly error = %v, want missing ProductDecl", err) + } +} + +func TestAssembleSchemaRegistryAllowingLegacyIsolatesOverlayPath(t *testing.T) { + root := buildRuntimeSchemaTestRoot() + bound, err := boundTestCommandRegistry(root) + if err != nil { + t.Fatal(err) + } + for _, command := range bound.Commands { + contract.ClearRuntimeContractFinalForTest(command.PrimaryCommand) + } + agent := embeddedAgentMetadata{ + Version: 1, + Products: map[string]agentProductMetadata{ + "doc": {AgentSummary: "docs", UseWhen: []string{"read docs"}}, + }, + Tools: map[string]agentToolMetadata{ + "doc create": {Effect: "write", InterfaceMode: "local", Availability: "available", InterfaceReason: "test"}, + }, + } + registry, err := assembleSchemaRegistryFromBoundAllowingLegacy(bound, runtimeSchemaMetadataSources{Agent: agent}) + if err != nil { + t.Fatalf("legacy-isolated assembly: %v", err) + } + if len(registry.Products) != 1 || registry.Products[0].ID != "doc" { + t.Fatalf("legacy-isolated products = %#v", registry.Products) + } +} + +func TestAssembleSchemaRegistryRequiresContractFinalAndProductDecl(t *testing.T) { + root := &cobra.Command{Use: "dws"} + leaf := &cobra.Command{Use: "run", Short: "Run sample", Long: "Run the sample tool", Run: func(*cobra.Command, []string) {}} + AttachRuntimeSchema(leaf, "sample", "run", "test") + RegisterRuntimeContractFinal(leaf, contract.ContractFinalPayload{ + Title: "Sample run", + Description: "Declared sample tool", + Safety: &contract.SafetySpec{ + Effect: "read", Risk: "low", Confirmation: "none", Idempotency: "idempotent", + }, + Interface: &contract.InterfaceSpec{ + Mode: "local", Availability: "available", Reason: "test local leaf", + }, + Selection: &contract.SelectionSpec{ + AgentSummary: "Run a sample tool", + UseWhen: []string{"need sample run"}, + AvoidWhen: []string{"need other product"}, + }, + }) + t.Cleanup(func() { + contract.ClearRuntimeContractFinalForTest(leaf) + contract.ClearProductDeclForTest("sample") + }) + contract.RegisterProductDecl(contract.ProductDecl{ + ID: "sample", + Selection: contract.ProductSelectionDecl{ + AgentSummary: "Sample product", + UseWhen: []string{"sample routing"}, + AvoidWhen: []string{"not sample"}, + }, + }) + product := &cobra.Command{Use: "sample"} + product.AddCommand(leaf) + root.AddCommand(product) + + registry, err := schemaRegistryForTest(root) + if err != nil { + t.Fatalf("declared production assembly: %v", err) + } + if len(registry.Products) != 1 || len(registry.Products[0].Tools) != 1 { + t.Fatalf("registry = %#v", registry.Products) + } + tool := registry.Products[0].Tools[0] + if tool.MetadataSource != "corecmd.contract" { + t.Fatalf("metadata_source = %q, want corecmd.contract", tool.MetadataSource) + } + if registry.Products[0].Selection.AgentSummary != "Sample product" { + t.Fatalf("product selection = %#v", registry.Products[0].Selection) + } +} diff --git a/internal/cli/schema_snapshot_adapter.go b/internal/cli/schema_snapshot_adapter.go index 7693f09d..e269623a 100644 --- a/internal/cli/schema_snapshot_adapter.go +++ b/internal/cli/schema_snapshot_adapter.go @@ -7,6 +7,7 @@ import ( "bytes" "encoding/json" "fmt" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "io" "sort" "strings" @@ -26,82 +27,82 @@ type schemaCatalogWire struct { } type schemaProductWire struct { - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Runtime bool `json:"runtime"` - ToolCount int `json:"tool_count"` - Tools []schemaToolWire `json:"tools"` - AgentSummary string `json:"agent_summary"` - AgentSummarySource string `json:"agent_summary_source"` - UseWhen []string `json:"use_when"` - AvoidWhen []string `json:"avoid_when"` - SourceRefs []string `json:"agent_source_refs"` - MetadataSource string `json:"agent_metadata_source"` - FieldProvenance map[string]FieldProvenance `json:"field_provenance"` + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Runtime bool `json:"runtime"` + ToolCount int `json:"tool_count"` + Tools []schemaToolWire `json:"tools"` + AgentSummary string `json:"agent_summary"` + AgentSummarySource string `json:"agent_summary_source"` + UseWhen []string `json:"use_when"` + AvoidWhen []string `json:"avoid_when"` + SourceRefs []string `json:"agent_source_refs"` + MetadataSource string `json:"agent_metadata_source"` + FieldProvenance map[string]contract.FieldProvenance `json:"field_provenance"` } type schemaToolWire struct { - Name string `json:"name"` - CLIName string `json:"cli_name"` - CanonicalPath string `json:"canonical_path"` - Path string `json:"path"` - CLIPath string `json:"cli_path"` - PrimaryCLIPath string `json:"primary_cli_path"` - Aliases []string `json:"aliases"` - IsAlias bool `json:"is_alias"` - Source string `json:"source"` - ProductID string `json:"product_id"` - SourceProductID string `json:"source_product_id"` - Group string `json:"group"` - Display string `json:"display"` - Title string `json:"title"` - Description string `json:"description"` - MetadataSource string `json:"metadata_source"` - Parameters map[string]schemaParamWire `json:"parameters"` - HasParameters bool `json:"has_parameters"` - ParameterCount int `json:"parameter_count"` - Constraints RuntimeSchemaConstraints `json:"constraints"` - Positionals []RuntimeSchemaPositional `json:"positionals"` - DryRun *DryRunSpec `json:"dry_run"` - Effect string `json:"effect"` - EffectSource string `json:"effect_source"` - Risk string `json:"risk"` - Confirmation string `json:"confirmation"` - Idempotency string `json:"idempotency"` - InterfaceRef *InterfaceRefSpec `json:"interface_ref"` - InterfaceMode string `json:"interface_mode"` - Availability string `json:"availability"` - InterfaceReason string `json:"interface_reason"` - AgentSummary string `json:"agent_summary"` - AgentSummarySource string `json:"agent_summary_source"` - UseWhen []string `json:"use_when"` - AvoidWhen []string `json:"avoid_when"` - Prerequisites []string `json:"prerequisites"` - Tips []string `json:"tips"` - WorkflowRefs []string `json:"workflow_refs"` - Examples []string `json:"examples"` - Reviewed *bool `json:"reviewed"` - SourceRefs []string `json:"agent_source_refs"` - AgentMetadataSource string `json:"agent_metadata_source"` - FieldProvenance map[string]FieldProvenance `json:"field_provenance"` + Name string `json:"name"` + CLIName string `json:"cli_name"` + CanonicalPath string `json:"canonical_path"` + Path string `json:"path"` + CLIPath string `json:"cli_path"` + PrimaryCLIPath string `json:"primary_cli_path"` + Aliases []string `json:"aliases"` + IsAlias bool `json:"is_alias"` + Source string `json:"source"` + ProductID string `json:"product_id"` + SourceProductID string `json:"source_product_id"` + Group string `json:"group"` + Display string `json:"display"` + Title string `json:"title"` + Description string `json:"description"` + MetadataSource string `json:"metadata_source"` + Parameters map[string]schemaParamWire `json:"parameters"` + HasParameters bool `json:"has_parameters"` + ParameterCount int `json:"parameter_count"` + Constraints RuntimeSchemaConstraints `json:"constraints"` + Positionals []contract.RuntimeSchemaPositional `json:"positionals"` + DryRun *contract.DryRunSpec `json:"dry_run"` + Effect string `json:"effect"` + EffectSource string `json:"effect_source"` + Risk string `json:"risk"` + Confirmation string `json:"confirmation"` + Idempotency string `json:"idempotency"` + InterfaceRef *contract.InterfaceRefSpec `json:"interface_ref"` + InterfaceMode string `json:"interface_mode"` + Availability string `json:"availability"` + InterfaceReason string `json:"interface_reason"` + AgentSummary string `json:"agent_summary"` + AgentSummarySource string `json:"agent_summary_source"` + UseWhen []string `json:"use_when"` + AvoidWhen []string `json:"avoid_when"` + Prerequisites []string `json:"prerequisites"` + Tips []string `json:"tips"` + WorkflowRefs []string `json:"workflow_refs"` + Examples []string `json:"examples"` + Reviewed *bool `json:"reviewed"` + SourceRefs []string `json:"agent_source_refs"` + AgentMetadataSource string `json:"agent_metadata_source"` + FieldProvenance map[string]contract.FieldProvenance `json:"field_provenance"` } type schemaParamWire struct { - Type string `json:"type"` - Description string `json:"description"` - Property string `json:"property"` - Required bool `json:"required"` - CLIRequired bool `json:"cli_required"` - RequiredWhen string `json:"required_when"` - Default json.RawMessage `json:"default"` - InterfaceDefault json.RawMessage `json:"interface_default"` - Example json.RawMessage `json:"example"` - Format string `json:"format"` - Enum []string `json:"enum"` - InterfaceDescription string `json:"interface_description"` - InterfaceType string `json:"interface_type"` - FieldProvenance map[string]FieldProvenance `json:"field_provenance"` + Type string `json:"type"` + Description string `json:"description"` + Property string `json:"property"` + Required bool `json:"required"` + CLIRequired bool `json:"cli_required"` + RequiredWhen string `json:"required_when"` + Default json.RawMessage `json:"default"` + InterfaceDefault json.RawMessage `json:"interface_default"` + Example json.RawMessage `json:"example"` + Format string `json:"format"` + Enum []string `json:"enum"` + InterfaceDescription string `json:"interface_description"` + InterfaceType string `json:"interface_type"` + FieldProvenance map[string]contract.FieldProvenance `json:"field_provenance"` } func schemaRegistryFromSnapshot(snapshot SchemaCatalogSnapshot) (SchemaRegistry, SchemaIndex, error) { @@ -122,7 +123,7 @@ func schemaRegistryFromSnapshot(snapshot SchemaCatalogSnapshot) (SchemaRegistry, Description: productWire.Description, Runtime: productWire.Runtime, FieldProvenance: cloneFieldProvenance(productWire.FieldProvenance), - Selection: SelectionSpec{ + Selection: contract.SelectionSpec{ AgentSummary: productWire.AgentSummary, AgentSummarySource: productWire.AgentSummarySource, UseWhen: productWire.UseWhen, @@ -214,7 +215,7 @@ func schemaToolSpecFromPayload(payload map[string]any) (ToolSpec, error) { }) } return toolSpecFromSnapshot(RuntimeToolSpecInput{ - Identity: ToolIdentitySpec{ + Identity: contract.ToolIdentitySpec{ ProductID: wire.ProductID, SourceProductID: wire.SourceProductID, Name: wire.Name, @@ -236,20 +237,20 @@ func schemaToolSpecFromPayload(payload map[string]any) (ToolSpec, error) { Constraints: wire.Constraints, Positionals: wire.Positionals, DryRun: wire.DryRun, - Safety: SafetySpec{ + Safety: contract.SafetySpec{ Effect: wire.Effect, EffectSource: wire.EffectSource, Risk: wire.Risk, Confirmation: wire.Confirmation, Idempotency: wire.Idempotency, }, - Interface: InterfaceSpec{ + Interface: contract.InterfaceSpec{ Ref: wire.InterfaceRef, Mode: wire.InterfaceMode, Availability: wire.Availability, Reason: wire.InterfaceReason, }, - Selection: SelectionSpec{ + Selection: contract.SelectionSpec{ AgentSummary: wire.AgentSummary, AgentSummarySource: wire.AgentSummarySource, UseWhen: wire.UseWhen, diff --git a/internal/corecmd/contract/doc.go b/internal/corecmd/contract/doc.go index f89ab4d8..f107cf86 100644 --- a/internal/corecmd/contract/doc.go +++ b/internal/corecmd/contract/doc.go @@ -21,11 +21,21 @@ // positionals / ParamDecl // - FieldProvenance / FieldCandidateProvenance // -// Boundary: -// - Authoring / registration: corecmd.SchemaDecl → ContractFinalPayload; -// ProductDecl for product-level Agent routing. -// - Delivery / assembly (catalog embed, ResolveMeta, ToolSpec projection) -// stays in internal/cli and consumes these types and registries. +// Package boundary (corecmd → cli seam): // -// Provenance stamp for declared leaf Safety remains "corecmd.contract". +// - Types and registries → corecmd/contract (this package). Callers author +// contract.SafetySpec / contract.ParamDecl / contract.ProductDecl / +// contract.ContractFinalPayload directly; there is no cli type-alias layer. +// - Cobra annotation seam (AnnotateRuntime*) → internal/cli. Framework code +// (corecmd.New / AttachSchema) may call those helpers to embed +// dws.schema.* facts; cli must not redefine the contract types. +// - Delivery helpers that must emit annotations +// (cli.RegisterRuntimeContractFinal, cli.ApplyParamDecls) stay in cli but +// accept contract.* payloads. +// - Catalog assembly / ResolveMeta / go:embed → internal/cli (delivery +// boundary; not moved into contract). +// +// Authoring path: corecmd.SchemaDecl → ContractFinalPayload; ProductDecl for +// product-level Agent routing. Provenance stamp for declared leaf Safety +// remains "corecmd.contract". package contract diff --git a/internal/corecmd/corecmd_test.go b/internal/corecmd/corecmd_test.go index 5fb0088b..49c4b6c9 100644 --- a/internal/corecmd/corecmd_test.go +++ b/internal/corecmd/corecmd_test.go @@ -20,7 +20,7 @@ import ( "strings" "testing" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/spf13/cobra" ) @@ -36,22 +36,22 @@ func newTestCommand() *cobra.Command { return cmd } -func testReadSafety() cli.SafetySpec { - return cli.SafetySpec{ +func testReadSafety() contract.SafetySpec { + return contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", } } -func testWriteSafety() cli.SafetySpec { - return cli.SafetySpec{ +func testWriteSafety() contract.SafetySpec { + return contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", } } -func testDestructiveSafety() cli.SafetySpec { - return cli.SafetySpec{ +func testDestructiveSafety() contract.SafetySpec { + return contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", } @@ -628,7 +628,7 @@ func TestCrossPlatformCoverageConfirmSafety(t *testing.T) { if err := ConfirmSafety(newSafetyCmd(""), testReadSafety()); err != nil { t.Fatal("not_required safety must pass") } - if err := ConfirmSafety(newSafetyCmd(""), cli.SafetySpec{Effect: "destructive", Risk: "high"}); err != nil { + if err := ConfirmSafety(newSafetyCmd(""), contract.SafetySpec{Effect: "destructive", Risk: "high"}); err != nil { t.Fatal("effect/risk must not imply confirmation") } // --yes and --dry-run bypass the prompt @@ -1559,7 +1559,7 @@ func TestCrossPlatformCoverageEmbedContractCobraProjection(t *testing.T) { t.Fatalf("hidden flag must not be projected, got %#v", got) } // The authored SchemaDecl still lands as the typed ContractFinal. - final, ok := cli.RuntimeContractFinal(cmd) + final, ok := contract.RuntimeContractFinal(cmd) if !ok || final.Description != "desc" { t.Fatalf("cobra projection must still embed SchemaDecl, final=%#v ok=%v", final, ok) } @@ -1570,18 +1570,18 @@ func TestCrossPlatformCoverageAttachSchemaNilAndEmptyGuards(t *testing.T) { AttachSchema(nil, testWriteSafety(), SchemaDecl{Description: "d"}, "s", "l") cmd := newTestCommand() AttachSchema(cmd, testWriteSafety(), SchemaDecl{}, "s", "l") - if cli.HasRuntimeContractFinal(cmd) { + if contract.HasRuntimeContractFinal(cmd) { t.Fatal("empty SchemaDecl must not register a ContractFinal") } } func TestCrossPlatformCoverageEffectiveSafetySpecZeroValueDefaultsToRead(t *testing.T) { - got := effectiveSafetySpec(cli.SafetySpec{}) - want := cli.SafetySpec{Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent"} + got := effectiveSafetySpec(contract.SafetySpec{}) + want := contract.SafetySpec{Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent"} if got != want { t.Fatalf("effectiveSafetySpec(zero) = %+v, want %+v", got, want) } - partial := effectiveSafetySpec(cli.SafetySpec{Effect: " write ", Risk: "high", Confirmation: "user_required", Idempotency: "unknown"}) + partial := effectiveSafetySpec(contract.SafetySpec{Effect: " write ", Risk: "high", Confirmation: "user_required", Idempotency: "unknown"}) if partial.Effect != "write" || partial.Risk != "high" { t.Fatalf("effectiveSafetySpec(trim) = %+v", partial) } diff --git a/internal/corecmd/schema_decl.go b/internal/corecmd/schema_decl.go index 12756335..942adc76 100644 --- a/internal/corecmd/schema_decl.go +++ b/internal/corecmd/schema_decl.go @@ -41,12 +41,12 @@ type SchemaDecl struct { // ParamDecl declares Schema facts about a flag that the command registers // itself. It exists because metadata-mode commands (DeclareLeafMetadata) own // their flag registration and cannot use FlagSpec; without this channel their -// parameter facts could only live in schema_hints/metadata overlays. +// parameter facts would have no declare-path into Schema assembly. // -// Each field maps to a dws.schema.* annotation at rank native_annotation (620), -// which outranks tool_schema_hint (500). A declared value therefore wins over -// any remaining hint overlay for the same flag, making the overlay redundant -// once the declaration is in place. +// Each field maps to a dws.schema.* annotation at rank native_annotation (620). +// Hint overlays (RegisterSchemaHints / schema_hints/) are fully retired; +// ParamDecl (or FlagSpec on helpers leaves) is the sole declare path for these +// parameter facts. type ParamDecl struct { // Name is the cobra flag name (kebab-case), e.g. "record-ids". Name string diff --git a/internal/corecmd/schema_decl_test.go b/internal/corecmd/schema_decl_test.go index 920a4f31..339330ea 100644 --- a/internal/corecmd/schema_decl_test.go +++ b/internal/corecmd/schema_decl_test.go @@ -17,7 +17,7 @@ import ( "strings" "testing" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" ) func TestCrossPlatformCoverageNewCommandEmbedsFullSchemaDeclAsFinalSource(t *testing.T) { @@ -32,7 +32,7 @@ func TestCrossPlatformCoverageNewCommandEmbedsFullSchemaDeclAsFinalSource(t *tes RequiredWhen: "when x", SchemaDescription: "schema desc", }, }, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "retryable", }, @@ -64,7 +64,7 @@ func TestCrossPlatformCoverageNewCommandEmbedsFullSchemaDeclAsFinalSource(t *tes t.Fatal("framework must convert typed SchemaDecl; must not write JSON dws.schema.final") } } - final, ok := cli.RuntimeContractFinal(cmd) + final, ok := contract.RuntimeContractFinal(cmd) if !ok { t.Fatal("expected typed ContractFinal registration") } @@ -113,7 +113,7 @@ func TestNewCommandFallsBackToDeclaredDescriptionWithoutLong(t *testing.T) { cmd := New(Spec{ Use: "create", Short: "short", - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -133,7 +133,7 @@ func TestNewCommandFallsBackToDeclaredDescriptionWithoutLong(t *testing.T) { }, Invoke: func(*Ctx, map[string]any) error { return nil }, }) - final, ok := cli.RuntimeContractFinal(cmd) + final, ok := contract.RuntimeContractFinal(cmd) if !ok { t.Fatal("expected typed ContractFinal registration") } @@ -236,16 +236,16 @@ func TestNewCommandSafetySpecPassThrough(t *testing.T) { }, } } - build := func(spec Spec) *cli.SafetySpec { + build := func(spec Spec) *contract.SafetySpec { cmd := New(spec) - final, ok := cli.RuntimeContractFinal(cmd) + final, ok := contract.RuntimeContractFinal(cmd) if !ok || final.Safety == nil { t.Fatalf("expected declared safety, final=%#v ok=%v", final, ok) } return final.Safety } - declared := cli.SafetySpec{ + declared := contract.SafetySpec{ Effect: "write", Risk: "low", Confirmation: "not_required", Idempotency: "non_idempotent", } @@ -271,7 +271,7 @@ func TestSchemaDeclEmptySkipsFinal(t *testing.T) { Safety: testWriteSafety(), Invoke: func(*Ctx, map[string]any) error { return nil }, }) - if cli.HasRuntimeContractFinal(cmd) { + if contract.HasRuntimeContractFinal(cmd) { t.Fatal("Safety without Schema must not register Final (keep runtime write light)") } if _, ok := cmd.Annotations["dws.schema.risk"]; ok { @@ -291,7 +291,7 @@ func TestCrossPlatformCoverageNewCommandRejectsPartialSafetySpec(t *testing.T) { }() New(Spec{ Use: "partial", - Safety: cli.SafetySpec{Effect: "write", Risk: "medium", Idempotency: "unknown"}, + Safety: contract.SafetySpec{Effect: "write", Risk: "medium", Idempotency: "unknown"}, Invoke: func(*Ctx, map[string]any) error { return nil }, }) } diff --git a/internal/generator/agentmetadata/contract_final_product_test.go b/internal/generator/agentmetadata/contract_final_product_test.go index 7a4a6ec6..32c3fbcf 100644 --- a/internal/generator/agentmetadata/contract_final_product_test.go +++ b/internal/generator/agentmetadata/contract_final_product_test.go @@ -6,14 +6,14 @@ package agentmetadata import ( "testing" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" ) func TestApplyContractFinalProductDeclarations(t *testing.T) { - t.Cleanup(func() { cli.ClearProductDeclForTest("sample") }) - cli.RegisterProductDecl(cli.ProductDecl{ + t.Cleanup(func() { contract.ClearProductDeclForTest("sample") }) + contract.RegisterProductDecl(contract.ProductDecl{ ID: "sample", - Selection: cli.ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "Manage samples", UseWhen: []string{"target is a sample"}, AvoidWhen: []string{"target is another product"}, @@ -36,7 +36,7 @@ func TestApplyContractFinalProductDeclarations(t *testing.T) { if metadata.useWhenRank != selectionRankContractFinal || metadata.avoidWhenRank != selectionRankContractFinal { t.Fatalf("list ranks use=%d avoid=%d", metadata.useWhenRank, metadata.avoidWhenRank) } - if metadata.AgentSummarySource != cli.ProductDeclSourceRef { + if metadata.AgentSummarySource != contract.ProductDeclSourceRef { t.Fatalf("AgentSummarySource = %q", metadata.AgentSummarySource) } normalizeFile(file, 0) @@ -65,10 +65,10 @@ func TestApplyContractFinalProductDeclarations(t *testing.T) { } func TestApplyContractFinalDeclarationsIncludesProducts(t *testing.T) { - t.Cleanup(func() { cli.ClearProductDeclForTest("wired") }) - cli.RegisterProductDecl(cli.ProductDecl{ + t.Cleanup(func() { contract.ClearProductDeclForTest("wired") }) + contract.RegisterProductDecl(contract.ProductDecl{ ID: "wired", - Selection: cli.ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "Wired product", UseWhen: []string{"use wired"}, AvoidWhen: []string{"avoid wired"}, diff --git a/internal/generator/agentmetadata/metadata.go b/internal/generator/agentmetadata/metadata.go index 3c96deb8..2b106a20 100644 --- a/internal/generator/agentmetadata/metadata.go +++ b/internal/generator/agentmetadata/metadata.go @@ -267,11 +267,13 @@ const ( const contractFinalOrigin = "corecmd.contract" type Options struct { - Root string - SkillPath string - ProductsDir string - IntentGuidePath string - HintsDir string + Root string + SkillPath string + ProductsDir string + IntentGuidePath string + // HintsDir is retired. Empty is required; any non-empty value fails closed. + HintsDir string + // ManualHintsPath is retired. Empty is required; any non-empty value fails closed. ManualHintsPath string InterfaceMetadataPath string MaxExamples int @@ -381,9 +383,9 @@ func Generate(opts Options) (File, Stats, error) { // generateFromSources retains the lower-precedence evidence parsers as a // package-internal seam for focused tests. Production callers must use Generate. -// HintsDir is optional and retired for production: ProductDecl / ContractFinal -// own routing and leaf facts. When HintsDir is empty or absent, hint parsing is -// skipped; Skill Markdown remains evidence-only. +// HintsDir and ManualHintsPath are retired: ProductDecl / ContractFinal own +// routing and leaf facts. Non-empty values fail closed in loadSources; +// Skill Markdown remains evidence-only. func generateFromSources(opts Options) (File, Stats, error) { if opts.Root == "" { opts.Root = "." @@ -1706,33 +1708,11 @@ func loadSources(opts Options) ([]sourceFile, error) { return nil, fmt.Errorf("walk product references: %w", err) } paths = append(paths, productPaths...) - if strings.TrimSpace(opts.HintsDir) != "" { - hintsRoot := resolvePath(opts.Root, opts.HintsDir) - info, statErr := os.Stat(hintsRoot) - switch { - case statErr == nil && info.IsDir(): - hintPaths := []string{} - err := filepath.WalkDir(hintsRoot, func(path string, entry fs.DirEntry, walkErr error) error { - if walkErr != nil { - return walkErr - } - if entry.IsDir() || !strings.EqualFold(filepath.Ext(entry.Name()), ".json") { - return nil - } - hintPaths = append(hintPaths, path) - return nil - }) - if err != nil { - return nil, fmt.Errorf("walk Agent hint sources: %w", err) - } - paths = append(paths, hintPaths...) - case statErr != nil && !os.IsNotExist(statErr): - return nil, fmt.Errorf("stat Agent hint sources: %w", statErr) - // Absent HintsDir is the retired production end-state; skip quietly. - } + if hintsDir := strings.TrimSpace(opts.HintsDir); hintsDir != "" { + return nil, fmt.Errorf("schema_hints/ is retired; clear HintsDir and declare ProductDecl/ContractFinal instead (got %q)", hintsDir) } - if strings.TrimSpace(opts.ManualHintsPath) != "" { - paths = append(paths, resolvePath(opts.Root, opts.ManualHintsPath)) + if manualHints := strings.TrimSpace(opts.ManualHintsPath); manualHints != "" { + return nil, fmt.Errorf("manual hints are retired; clear ManualHintsPath and declare ProductDecl/ContractFinal instead (got %q)", manualHints) } if strings.TrimSpace(opts.InterfaceMetadataPath) != "" { paths = append(paths, resolvePath(opts.Root, opts.InterfaceMetadataPath)) diff --git a/internal/generator/agentmetadata/metadata_helpers_coverage_test.go b/internal/generator/agentmetadata/metadata_helpers_coverage_test.go index 2bff82a1..4940f013 100644 --- a/internal/generator/agentmetadata/metadata_helpers_coverage_test.go +++ b/internal/generator/agentmetadata/metadata_helpers_coverage_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -281,19 +282,18 @@ func TestCrossPlatformCoverageMetadataSourceAndPathEdges(t *testing.T) { if err := os.WriteFile(same, []byte("same"), 0o600); err != nil { t.Fatal(err) } - files, err := loadSources(Options{Root: root, ProductsDir: "products", SkillPath: "same.md", IntentGuidePath: "same.md", HintsDir: "missing"}) - if err != nil || len(files) != 1 { - t.Fatalf("absent HintsDir should be skipped: %#v, %v", files, err) + if _, err := loadSources(Options{Root: root, ProductsDir: "products", SkillPath: "same.md", IntentGuidePath: "same.md", HintsDir: "missing"}); err == nil || !strings.Contains(err.Error(), "schema_hints/ is retired") { + t.Fatalf("non-empty HintsDir should fail closed: %v", err) } if err := os.WriteFile(filepath.Join(products, "skip.txt"), []byte("x"), 0o600); err != nil { t.Fatal(err) } - files, err = loadSources(Options{Root: root, ProductsDir: "products", SkillPath: "same.md", IntentGuidePath: "same.md"}) + files, err := loadSources(Options{Root: root, ProductsDir: "products", SkillPath: "same.md", IntentGuidePath: "same.md"}) if err != nil || len(files) != 1 { t.Fatalf("deduplicated sources = %#v, %v", files, err) } - if _, err := loadSources(Options{Root: root, ProductsDir: "products", ManualHintsPath: "missing.json"}); err == nil || !strings.Contains(err.Error(), "read") { - t.Fatalf("missing source error = %v", err) + if _, err := loadSources(Options{Root: root, ProductsDir: "products", ManualHintsPath: "missing.json"}); err == nil || !strings.Contains(err.Error(), "manual hints are retired") { + t.Fatalf("non-empty ManualHintsPath should fail closed: %v", err) } if got := sourceProductIDs(sourceFile{path: filepath.Join(root, "products", "sample", "guide.md")}, products, nil, nil); len(got) != 1 || got[0] != "sample" { @@ -483,13 +483,12 @@ func TestCrossPlatformCoverageMetadataParserConflictEdges(t *testing.T) { } } - func TestCrossPlatformCoverageContractFinalDeclarationFailureEdges(t *testing.T) { declared := &cobra.Command{Use: "run"} - cli.RegisterRuntimeContractFinal(declared, cli.ContractFinalPayload{ - Selection: &cli.SelectionSpec{AgentSummary: "declared summary"}, + cli.RegisterRuntimeContractFinal(declared, contract.ContractFinalPayload{ + Selection: &contract.SelectionSpec{AgentSummary: "declared summary"}, }) - t.Cleanup(func() { cli.ClearRuntimeContractFinalForTest(declared) }) + t.Cleanup(func() { contract.ClearRuntimeContractFinalForTest(declared) }) bound := cli.BoundCommandRegistry{ByCanonical: map[string]cli.BoundCommandSpec{ "sample.run": {PrimaryCommand: declared}, }} diff --git a/internal/generator/agentmetadata/metadata_test.go b/internal/generator/agentmetadata/metadata_test.go index 6a835efe..218f9550 100644 --- a/internal/generator/agentmetadata/metadata_test.go +++ b/internal/generator/agentmetadata/metadata_test.go @@ -292,7 +292,6 @@ func TestValidateEffectiveToolProjectionRejectsCountOnlyFalseGreen(t *testing.T) } } - func TestMergeToolMetadataUsesSourcePrecedenceAcrossAliases(t *testing.T) { merged, err := mergeToolMetadata( ToolMetadata{Risk: "medium", riskRank: selectionRankImported, Confirmation: "not_required", confirmationRank: selectionRankImported}, @@ -319,7 +318,6 @@ func TestMergeToolMetadataUsesSourcePrecedenceAcrossAliases(t *testing.T) { } } - func TestClassifyEffectPathUsesActionSegment(t *testing.T) { tests := map[string]string{ "aitable view get visible-fields": "read", diff --git a/internal/generator/cmd_schema_agent_metadata/coverage_more_test.go b/internal/generator/cmd_schema_agent_metadata/coverage_more_test.go index 2ee694ac..1de92236 100644 --- a/internal/generator/cmd_schema_agent_metadata/coverage_more_test.go +++ b/internal/generator/cmd_schema_agent_metadata/coverage_more_test.go @@ -13,6 +13,7 @@ import ( "testing" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/generator/agentmetadata" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/generator/outputguard" "github.com/spf13/cobra" @@ -281,8 +282,8 @@ func TestCrossPlatformCoverageMetadataRegistryAndSelectionFailureEdges(t *testin func TestCrossPlatformCoverageSelectionHintInputExemptsDeclaredTools(t *testing.T) { declared := &cobra.Command{Use: "run"} - cli.RegisterRuntimeContractFinal(declared, cli.ContractFinalPayload{}) - t.Cleanup(func() { cli.ClearRuntimeContractFinalForTest(declared) }) + cli.RegisterRuntimeContractFinal(declared, contract.ContractFinalPayload{}) + t.Cleanup(func() { contract.ClearRuntimeContractFinalForTest(declared) }) registry := commandRegistryProjection{ CanonicalToolPaths: map[string]string{ @@ -300,10 +301,10 @@ func TestCrossPlatformCoverageSelectionHintInputExemptsDeclaredTools(t *testing. } func TestCrossPlatformCoverageSelectionHintInputExemptsDeclaredProducts(t *testing.T) { - t.Cleanup(func() { cli.ClearProductDeclForTest("declared") }) - cli.RegisterProductDecl(cli.ProductDecl{ + t.Cleanup(func() { contract.ClearProductDeclForTest("declared") }) + contract.RegisterProductDecl(contract.ProductDecl{ ID: "declared", - Selection: cli.ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "Declared product", UseWhen: []string{"use declared"}, AvoidWhen: []string{"avoid declared"}, diff --git a/internal/helpers/aisearch.go b/internal/helpers/aisearch.go index 01e89db2..8c32c402 100644 --- a/internal/helpers/aisearch.go +++ b/internal/helpers/aisearch.go @@ -5,8 +5,8 @@ import ( "github.com/spf13/cobra" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" ) // boolPtr returns a pointer to v, for ParamDecl.Required declarations. @@ -167,9 +167,9 @@ func normalizeAisearchSearchTypes(values []string) []string { func newAisearchCommand() *cobra.Command { // Product-level Agent routing Decl (migrated from selection/aisearch.json // products.aisearch). Catalog assembly stamps provenance contract_final. - cli.RegisterProductDecl(cli.ProductDecl{ + contract.RegisterProductDecl(contract.ProductDecl{ ID: "aisearch", - Selection: cli.ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "企业内智能搜人、搜知识内容与搜行为记录", UseWhen: []string{ "语义找人、按主题搜企业知识,或追溯发送/创建/分享等行为", @@ -242,7 +242,7 @@ func newAisearchCommand() *cobra.Command { RunE: runAisearchPerson, } DeclareLeafMetadata(personCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -296,7 +296,7 @@ func newAisearchCommand() *cobra.Command { enterpriseCmd.Flags().String("queries", "", "内容关键词列表,多个用逗号分隔;汇总类场景可留空") enterpriseCmd.Flags().String("time-range", "", "时间范围,仅当用户显式给出时间词时填写,如 今天/本周/9月/过去一周") DeclareLeafMetadata(enterpriseCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -351,7 +351,7 @@ func newAisearchCommand() *cobra.Command { RunE: runAisearchBehavior, } DeclareLeafMetadata(behaviorCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/helpers/aitable.go b/internal/helpers/aitable.go index 9fef3cff..aa276c43 100644 --- a/internal/helpers/aitable.go +++ b/internal/helpers/aitable.go @@ -14,6 +14,7 @@ import ( "github.com/spf13/cobra" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" ) // parseBoolFlag reads a string flag and parses it as a boolean, accepting the @@ -927,9 +928,9 @@ func runAitableViewUpdateArray(cmd *cobra.Command, blockKey string) error { func newAitableCommand() *cobra.Command { // Product-level Agent routing Decl (migrated from selection/aitable.json // products.aitable). Catalog assembly stamps provenance contract_final. - cli.RegisterProductDecl(cli.ProductDecl{ + contract.RegisterProductDecl(contract.ProductDecl{ ID: "aitable", - Selection: cli.ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "管理 AI 表格 Base、数据表、字段、记录、视图、表单、仪表盘、权限、导入导出与自动化工作流。", UseWhen: []string{ "需要读取或管理 AI 表格中的结构、数据、视图、权限、导入导出或工作流时", @@ -1656,8 +1657,7 @@ config 结构参考: AvoidWhen: []string{"改字段名/配置用 field update(不可改类型);删字段用 field delete;调列顺序用 view update"}, Examples: []string{"dws aitable field create --base-id --table-id --name \"状态\" --type singleSelect"}, }, - // fields required was RegisterSchemaHints tool_schema_hint; publish via - // ParamDecl (Agent may still use --name/--type as a single-field path). + // Agent may still use --name/--type as a single-field path. Parameters: []corecmd.ParamDecl{ {Name: "fields", Required: boolPtr(true), InterfaceType: "array"}, }, @@ -2636,7 +2636,6 @@ fieldId 必须是 primaryDoc 类型的字段。`, Examples: []string{"dws aitable attachment upload --base-id BASE_ID --file-name report.xlsx --size 204800"}, }, // size is validated in RunE (Int64); publish required via ParamDecl - // instead of relying on RegisterSchemaHints tool_schema_hint. Parameters: []corecmd.ParamDecl{ {Name: "size", Required: boolPtr(true)}, }, @@ -3246,7 +3245,6 @@ colorConfigs (JSON 数组) / officialHoliday (bool)。`, AvoidWhen: []string{"没有独立 field reorder;只读用 get visible-fields;首列字段须保留第一位"}, Examples: []string{"dws aitable view update visible-fields --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID --field-ids fld1,fld2,fld3"}, }, - // field-ids required was RegisterSchemaHints tool_schema_hint. Parameters: []corecmd.ParamDecl{ {Name: "field-ids", Required: boolPtr(true)}, }, @@ -4135,7 +4133,7 @@ locked 为 true 表示视图已锁定,false 表示未锁定。`, }, } DeclareLeafMetadata(formFieldHideCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -4268,7 +4266,7 @@ valid=false 仍表示 DSL 校验或发布未通过,必须读取 issues 修正 }, } DeclareLeafMetadata(workflowCreateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "non_idempotent", }, @@ -4404,7 +4402,7 @@ valid=false 仍表示 DSL 校验或发布未通过,必须读取 issues 修正 }, } DeclareLeafMetadata(workflowDisableCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, @@ -4610,8 +4608,7 @@ valid=false 仍表示 DSL 校验或发布未通过,必须读取 issues 修正 AvoidWhen: []string{"配置模板先看 config-example;删仪表盘用 delete"}, Examples: []string{"dws aitable dashboard create --base-id --name \"运营看板\""}, }, - // name required was RegisterSchemaHints tool_schema_hint (Agent prefers - // --name; runtime still accepts --config alone). + // Agent prefers --name; runtime still accepts --config alone. Parameters: []corecmd.ParamDecl{ {Name: "name", Required: boolPtr(true)}, }, @@ -4669,8 +4666,7 @@ valid=false 仍表示 DSL 校验或发布未通过,必须读取 issues 修正 AvoidWhen: []string{"排版布局用 dashboard arrange;删用 delete"}, Examples: []string{"dws aitable dashboard update --base-id --dashboard-id --name \"新看板\""}, }, - // name required was RegisterSchemaHints tool_schema_hint (Agent prefers - // --name; runtime still accepts --config alone). + // Agent prefers --name; runtime still accepts --config alone. Parameters: []corecmd.ParamDecl{ {Name: "name", Required: boolPtr(true)}, }, @@ -5294,7 +5290,7 @@ message: "the current user must be a manager (administrator) of this base to man }, } DeclareLeafMetadata(advpermDisableCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/helpers/aitable_schema.go b/internal/helpers/aitable_schema.go index f46afbeb..91d58217 100644 --- a/internal/helpers/aitable_schema.go +++ b/internal/helpers/aitable_schema.go @@ -13,35 +13,37 @@ package helpers -import "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" +import ( + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" +) // aitable_schema.go holds shared Safety / Interface factories for aitable's // DeclareLeafMetadata declarations (metadata-only mode). Selection prose and // per-command payloads live in aitable_schema_decls_generated.go. -func aitableSafetyRead() cli.SafetySpec { - return cli.SafetySpec{ +func aitableSafetyRead() contract.SafetySpec { + return contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", } } -func aitableSafetyWrite() cli.SafetySpec { - return cli.SafetySpec{ +func aitableSafetyWrite() contract.SafetySpec { + return contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", } } -func aitableSafetyWriteConfirm() cli.SafetySpec { - return cli.SafetySpec{ +func aitableSafetyWriteConfirm() contract.SafetySpec { + return contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", } } -func aitableSafetyDestructive() cli.SafetySpec { - return cli.SafetySpec{ +func aitableSafetyDestructive() contract.SafetySpec { + return contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", } diff --git a/internal/helpers/aitable_schema_test.go b/internal/helpers/aitable_schema_test.go index 2e14505f..417909dc 100644 --- a/internal/helpers/aitable_schema_test.go +++ b/internal/helpers/aitable_schema_test.go @@ -22,7 +22,7 @@ import ( "github.com/spf13/cobra" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" ) func TestDeclareLeafMetadataRejectsExecutionSurface(t *testing.T) { @@ -80,7 +80,7 @@ func TestDeclareLeafMetadataDoesNotRewriteRunE(t *testing.T) { }, }, }) - if !cli.HasRuntimeContractFinal(cmd) { + if !contract.HasRuntimeContractFinal(cmd) { t.Fatal("expected ContractFinal") } // function values are not comparable; ensure pointer identity via uintptr trick is unnecessary — @@ -128,7 +128,7 @@ func TestAitableDeclareLeafMetadataCoversRegistryHelpers(t *testing.T) { missing = append(missing, tool.CLIPath+" (command missing)") continue } - if !cli.HasRuntimeContractFinal(leaf) { + if !contract.HasRuntimeContractFinal(leaf) { missing = append(missing, tool.CLIPath) } } diff --git a/internal/helpers/attendance.go b/internal/helpers/attendance.go index 7d68e92d..152e504b 100644 --- a/internal/helpers/attendance.go +++ b/internal/helpers/attendance.go @@ -10,9 +10,8 @@ import ( "github.com/spf13/cobra" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" ) // ────────────────────────────────────────────────────────── @@ -518,9 +517,9 @@ func readGlobalSettingSaveFlagValue(cmd *cobra.Command, spec globalSettingSaveFl func newAttendanceCommand() *cobra.Command { // Product-level Agent routing Decl (migrated from selection/attendance.json // products.attendance). Catalog assembly stamps provenance contract_final. - cli.RegisterProductDecl(cli.ProductDecl{ + contract.RegisterProductDecl(contract.ProductDecl{ ID: "attendance", - Selection: cli.ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "查询考勤记录、排班、班次、考勤组、审批、报表、个人规则和假期,并执行经确认的考勤配置变更。", UseWhen: []string{ "用户要查询或管理钉钉考勤数据、规则、排班、考勤组、审批表单或假期余额。", @@ -575,7 +574,7 @@ func newAttendanceCommand() *cobra.Command { }, } DeclareLeafMetadata(attendanceRecordGetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -667,7 +666,7 @@ func newAttendanceCommand() *cobra.Command { }, } DeclareLeafMetadata(attendanceCheckResultCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -736,7 +735,7 @@ func newAttendanceCommand() *cobra.Command { }, } DeclareLeafMetadata(attendanceCheckRecordCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -868,7 +867,7 @@ func newAttendanceCommand() *cobra.Command { }, } DeclareLeafMetadata(attendanceApproveListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -926,7 +925,7 @@ func newAttendanceCommand() *cobra.Command { }, } DeclareLeafMetadata(attendanceApproveTemplatesCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -992,7 +991,7 @@ func newAttendanceCommand() *cobra.Command { }, } DeclareLeafMetadata(attendanceShiftListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1063,7 +1062,7 @@ func newAttendanceCommand() *cobra.Command { }, } DeclareLeafMetadata(attendanceClassSearchCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1106,7 +1105,7 @@ func newAttendanceCommand() *cobra.Command { }, } DeclareLeafMetadata(attendanceClassGetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1208,7 +1207,7 @@ checkTime 字段统一使用 "HH:mm" 格式(如 "09:00"),CLI 自动转换 }, } DeclareLeafMetadata(attendanceClassCreateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -1309,7 +1308,7 @@ checkTime 字段统一使用 "HH:mm" 格式(如 "09:00"),CLI 自动转换 }, } DeclareLeafMetadata(attendanceClassUpdateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -1357,7 +1356,7 @@ checkTime 字段统一使用 "HH:mm" 格式(如 "09:00"),CLI 自动转换 }, } DeclareLeafMetadata(attendanceAdjustmentGetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1414,7 +1413,7 @@ checkTime 字段统一使用 "HH:mm" 格式(如 "09:00"),CLI 自动转换 }, } DeclareLeafMetadata(attendanceAdjustmentSearchCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1466,7 +1465,7 @@ checkTime 字段统一使用 "HH:mm" 格式(如 "09:00"),CLI 自动转换 }, } DeclareLeafMetadata(attendanceOvertimeGetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1523,7 +1522,7 @@ checkTime 字段统一使用 "HH:mm" 格式(如 "09:00"),CLI 自动转换 }, } DeclareLeafMetadata(attendanceOvertimeSearchCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1618,7 +1617,7 @@ CLI 会在未传筛选条件时补齐默认查询字段,在未传分页参数 }, } DeclareLeafMetadata(attendanceGroupSearchCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1666,7 +1665,7 @@ CLI 会在未传筛选条件时补齐默认查询字段,在未传分页参数 }, } DeclareLeafMetadata(attendanceGroupGetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1723,7 +1722,7 @@ CLI 会在未传筛选条件时补齐默认查询字段,在未传分页参数 }, } DeclareLeafMetadata(attendanceGroupFilteredGetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1801,7 +1800,7 @@ CLI 会在未传筛选条件时补齐默认查询字段,在未传分页参数 }, } DeclareLeafMetadata(attendanceGroupUpdateMembersCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -2045,7 +2044,7 @@ CLI 会在未传筛选条件时补齐默认查询字段,在未传分页参数 }, } DeclareLeafMetadata(attendanceGroupCreateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -2255,7 +2254,7 @@ CLI 会在未传筛选条件时补齐默认查询字段,在未传分页参数 }, } DeclareLeafMetadata(attendanceGroupUpdateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -2311,7 +2310,7 @@ statsType 统计类型支持:week(周统计)、month(月统计)。`, }, } DeclareLeafMetadata(attendanceSummaryCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2371,7 +2370,7 @@ statsType 统计类型支持:week(周统计)、month(月统计)。`, }, } DeclareLeafMetadata(attendanceRulesCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2508,7 +2507,7 @@ statsType 统计类型支持:week(周统计)、month(月统计)。`, }, } DeclareLeafMetadata(attendanceSelfSettingGetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2610,7 +2609,7 @@ statsType 统计类型支持:week(周统计)、month(月统计)。`, }, } DeclareLeafMetadata(attendanceSelfSettingSaveCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -2681,7 +2680,7 @@ statsType 统计类型支持:week(周统计)、month(月统计)。`, }, } DeclareLeafMetadata(attendanceGlobalSettingGetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2765,7 +2764,7 @@ statsType 统计类型支持:week(周统计)、month(月统计)。`, }, } DeclareLeafMetadata(attendanceGlobalSettingSaveCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -2817,7 +2816,7 @@ statsType 统计类型支持:week(周统计)、month(月统计)。`, }, } DeclareLeafMetadata(reportColumnsCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2889,7 +2888,7 @@ statsType 统计类型支持:week(周统计)、month(月统计)。`, }, } DeclareLeafMetadata(reportQueryDataCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2960,7 +2959,7 @@ statsType 统计类型支持:week(周统计)、month(月统计)。`, }, } DeclareLeafMetadata(reportQueryLeaveCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -3018,7 +3017,7 @@ statsType 统计类型支持:week(周统计)、month(月统计)。`, }, } DeclareLeafMetadata(vacationTypesCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -3073,7 +3072,7 @@ statsType 统计类型支持:week(周统计)、month(月统计)。`, }, } DeclareLeafMetadata(vacationBalanceCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -3135,7 +3134,7 @@ statsType 统计类型支持:week(周统计)、month(月统计)。`, }, } DeclareLeafMetadata(vacationRecordsCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -3329,7 +3328,7 @@ statsType 统计类型支持:week(周统计)、month(月统计)。`, }, } DeclareLeafMetadata(vacationUpdateTypeCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -3425,7 +3424,7 @@ statsType 统计类型支持:week(周统计)、month(月统计)。`, }, } DeclareLeafMetadata(vacationSaveBalanceCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -3587,7 +3586,7 @@ statsType 统计类型支持:week(周统计)、month(月统计)。`, }, } DeclareLeafMetadata(scheduleImportCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -3673,7 +3672,7 @@ statsType 统计类型支持:week(周统计)、month(月统计)。`, }, } DeclareLeafMetadata(scheduleGetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -3996,7 +3995,7 @@ statsType 统计类型支持:week(周统计)、month(月统计)。`, }, } DeclareLeafMetadata(checkinRecordsCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -4115,7 +4114,7 @@ statsType 统计类型支持:week(周统计)、month(月统计)。`, }, } DeclareLeafMetadata(bossCheckCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/helpers/calendar.go b/internal/helpers/calendar.go index 44a04fa5..61312804 100644 --- a/internal/helpers/calendar.go +++ b/internal/helpers/calendar.go @@ -11,6 +11,7 @@ import ( "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -212,9 +213,9 @@ func findUnknownFlag(cmd *cobra.Command) string { func newCalendarCommand() *cobra.Command { // Product-level Agent routing Decl (migrated from selection/calendar.json // products.calendar). Catalog assembly stamps provenance contract_final. - cli.RegisterProductDecl(cli.ProductDecl{ + contract.RegisterProductDecl(contract.ProductDecl{ ID: "calendar", - Selection: cli.ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "管理日历本、日程、参与人、附件、会议室,并查询人员或会议室闲忙状态。", UseWhen: []string{ "需要查询或管理钉钉日历、日程、参与人、会议室和闲忙信息时。", @@ -313,7 +314,7 @@ func newCalendarCommand() *cobra.Command { }, } DeclareLeafMetadata(eventListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -356,7 +357,7 @@ func newCalendarCommand() *cobra.Command { }, } DeclareLeafMetadata(eventGetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -461,7 +462,7 @@ func newCalendarCommand() *cobra.Command { }, } DeclareLeafMetadata(eventCreateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -483,8 +484,6 @@ func newCalendarCommand() *cobra.Command { "dws calendar event create --title \"周会\" --start \"2026-03-10T14:00:00+08:00\" --end \"2026-03-10T15:00:00+08:00\" --attendees userId1,userId2", }, }, - // Former RegisterSchemaHints pins (create required + recurrence - // required_when) — native_annotation outranks tool_schema_hint. Parameters: append([]corecmd.ParamDecl{ {Name: "title", Required: boolPtr(true)}, {Name: "start", Required: boolPtr(true)}, @@ -548,7 +547,7 @@ func newCalendarCommand() *cobra.Command { }, } DeclareLeafMetadata(eventUpdateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -591,7 +590,7 @@ func newCalendarCommand() *cobra.Command { }, } DeclareLeafMetadata(eventDeleteCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -641,7 +640,7 @@ func newCalendarCommand() *cobra.Command { }, } DeclareLeafMetadata(eventSuggestCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -702,7 +701,7 @@ func newCalendarCommand() *cobra.Command { }, } DeclareLeafMetadata(eventRespondCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -757,7 +756,7 @@ func newCalendarCommand() *cobra.Command { }, } DeclareLeafMetadata(participantListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -807,7 +806,7 @@ func newCalendarCommand() *cobra.Command { }, } DeclareLeafMetadata(participantAddCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -859,7 +858,7 @@ func newCalendarCommand() *cobra.Command { }, } DeclareLeafMetadata(participantDeleteCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1008,7 +1007,7 @@ func newCalendarCommand() *cobra.Command { }, } DeclareLeafMetadata(roomSearchCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1060,7 +1059,7 @@ func newCalendarCommand() *cobra.Command { }, } DeclareLeafMetadata(roomAddCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1111,7 +1110,7 @@ func newCalendarCommand() *cobra.Command { }, } DeclareLeafMetadata(roomDeleteCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1153,7 +1152,7 @@ func newCalendarCommand() *cobra.Command { }, } DeclareLeafMetadata(roomListGroupsCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1231,7 +1230,7 @@ func newCalendarCommand() *cobra.Command { }, } DeclareLeafMetadata(busySearchCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1301,7 +1300,7 @@ func newCalendarCommand() *cobra.Command { }, } DeclareLeafMetadata(attachmentAddCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1340,7 +1339,7 @@ func newCalendarCommand() *cobra.Command { }, } DeclareLeafMetadata(aclListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1424,7 +1423,7 @@ func newCalendarCommand() *cobra.Command { }, } DeclareLeafMetadata(bookListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1461,7 +1460,7 @@ func newCalendarCommand() *cobra.Command { }, } DeclareLeafMetadata(bookGetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1501,7 +1500,7 @@ func newCalendarCommand() *cobra.Command { }, } DeclareLeafMetadata(bookSearchCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2527,9 +2526,8 @@ var recurrenceFlagNames = []string{ "recurrence-range-type", "recurrence-end-date", "recurrence-count", } -// calendarRecurrenceParamDecls migrates former RegisterSchemaHints recurrence -// required/required_when pins onto create/update event leaves so Schema -// assembly resolves them as native_annotation (outranking tool_schema_hint). +// calendarRecurrenceParamDecls declares recurrence required/required_when pins +// for create/update event leaves (native_annotation). func calendarRecurrenceParamDecls() []corecmd.ParamDecl { return []corecmd.ParamDecl{ {Name: "recurrence-day-of-month", Required: boolPtr(false), RequiredWhen: "recurrence-type is absoluteMonthly or absoluteYearly"}, diff --git a/internal/helpers/chat.go b/internal/helpers/chat.go index a645d5c6..e7326238 100644 --- a/internal/helpers/chat.go +++ b/internal/helpers/chat.go @@ -22,6 +22,7 @@ import ( "github.com/spf13/cobra" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" ) func resolveMessageForward(cmd *cobra.Command, defaultForward bool) (bool, error) { @@ -1066,9 +1067,9 @@ func firstStringField(data map[string]any, keys ...string) string { func newChatCommand() *cobra.Command { // Product-level Agent routing Decl (migrated from selection/chat.json // products.chat). Catalog assembly stamps provenance contract_final. - cli.RegisterProductDecl(cli.ProductDecl{ + contract.RegisterProductDecl(contract.ProductDecl{ ID: "chat", - Selection: cli.ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "管理钉钉会话、群聊、群成员、机器人、消息检索与发送", UseWhen: []string{ "请求涉及群聊管理、聊天记录、消息发送、会话设置或群机器人", @@ -1238,7 +1239,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatGroupCreateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1292,7 +1293,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatSearchCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1356,7 +1357,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatGroupMembersAddBotCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1394,7 +1395,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatGroupRenameCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1434,7 +1435,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatGroupMemberAddCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1477,7 +1478,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatGroupMemberRemoveCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1574,7 +1575,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatMessageListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1640,7 +1641,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatMessageListDirectCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1901,7 +1902,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatMessageSendCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -2019,7 +2020,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatMessageSendByBotCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -2073,7 +2074,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatMessageRecallByBotCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -2157,7 +2158,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatMessageSendByWebhookCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -2212,7 +2213,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatMessageListTopicRepliesCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2253,7 +2254,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatMessageListAllCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2326,7 +2327,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatMessageListBySenderCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2385,7 +2386,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatMessageListMentionsCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2422,7 +2423,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatMessageListFocusedCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2462,7 +2463,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatMessageListTopConversationsCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2499,7 +2500,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatMessageListUnreadConversationsCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2561,7 +2562,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatMessageSearchCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2695,7 +2696,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatMessageSearchAdvancedCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2736,7 +2737,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatMessageQuerySendStatusCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2778,7 +2779,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatMessageRecallCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -2860,7 +2861,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatMessageEditCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -2923,7 +2924,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatMessageReadStatusCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2952,7 +2953,7 @@ func newChatCommand() *cobra.Command { RunE: runChatSearchCommon, } DeclareLeafMetadata(chatSearchCommonCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -3015,7 +3016,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatBotSearchCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -3142,7 +3143,7 @@ func newChatCommand() *cobra.Command { MutuallyExclusive: [][]string{{"group", "user", "open-dingtalk-id"}}, RequireOneOf: [][]string{{"group", "user", "open-dingtalk-id"}}, }) - cli.AnnotateRuntimePositionals(chatMessageSendCmd, cli.RuntimeSchemaPositional{ + cli.AnnotateRuntimePositionals(chatMessageSendCmd, contract.RuntimeSchemaPositional{ Name: "content", Type: "string", Description: "消息内容(也可使用 --text;富媒体消息可省略)", @@ -3439,7 +3440,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatConversationInfoCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -3523,7 +3524,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatCategoryListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -3559,7 +3560,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatCategoryConvsCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -3706,7 +3707,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatCategoryListByConvCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -3750,7 +3751,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatCategoryBatchInfoCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -3787,7 +3788,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatGroupInfoByIdCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -3827,7 +3828,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatMessageListByIdsCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -3868,7 +3869,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatMessageAddEmojiCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -3919,7 +3920,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatMessageRemoveEmojiCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -3972,7 +3973,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatMessageAddTextEmotionCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -4028,7 +4029,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatMessageRemoveTextEmotionCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -4082,7 +4083,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatMessageUpdateTextEmotionCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -4153,7 +4154,7 @@ func newChatCommand() *cobra.Command { }, } DeclareLeafMetadata(chatMessageCreateTextEmotionCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -4216,7 +4217,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatMessageSendCardCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -4262,7 +4263,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatMessageUpdateCardCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -4397,7 +4398,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatMessageDownloadMediaCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -4480,7 +4481,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatGroupTransferOwnerCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -4527,7 +4528,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatGroupInviteUrlCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -4570,7 +4571,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatMuteCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -4617,7 +4618,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatGroupQuitCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -4662,7 +4663,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatGroupUpdateIconCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -4727,7 +4728,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatGroupUpdateSettingsCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -4802,7 +4803,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatMessageReplyCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -4860,7 +4861,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatMessageForwardCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -4911,7 +4912,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatSetTopCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -4953,7 +4954,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatGroupGetMuteConfigCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -4996,7 +4997,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatGroupMuteCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -5082,7 +5083,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatGroupMuteMemberCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -5152,7 +5153,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatGroupSetAdminCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -5256,7 +5257,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatGroupRoleListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -5292,7 +5293,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatGroupRoleAddCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -5331,7 +5332,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatGroupRoleUpdateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -5371,7 +5372,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatGroupRoleRemoveCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -5422,7 +5423,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatGroupRoleSetUserCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -5474,7 +5475,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatGroupRoleRemoveUserCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -5524,7 +5525,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatGroupRoleQueryUserCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -5576,7 +5577,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatGroupBotsCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -5615,7 +5616,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatGroupMembersRemoveBotCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -5676,7 +5677,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatBotFindCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -5719,7 +5720,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatGroupDismissCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, @@ -5774,7 +5775,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatGroupSetHistoryCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -5824,7 +5825,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatMessageCombineForwardCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -5879,7 +5880,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatMessageForwardTopicCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -5928,7 +5929,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatMessageSetPinCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -5971,7 +5972,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatMessageUnsetPinCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -6022,7 +6023,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatMessageListPinCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -6065,7 +6066,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatMessageAddFavoriteCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -6106,7 +6107,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatMessageRemoveFavoriteCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -6155,7 +6156,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatMessageListFavoritesCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -6208,7 +6209,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 }, } DeclareLeafMetadata(chatGroupListMyGroupsCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -6572,7 +6573,7 @@ status 可选值: }, } DeclareLeafMetadata(chatGroupUpdateNickCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -6948,7 +6949,7 @@ status 可选值: }, } DeclareLeafMetadata(chatGroupUpgradeToExternalCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, @@ -7008,7 +7009,7 @@ status 可选值: }, } DeclareLeafMetadata(chatCategoryCreateSmartCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -7122,7 +7123,7 @@ pl_PL, sv_SE, fi_FI, cs_CZ, ar_SA, tl_PH, he_IL, nl_NL, lo_LA, it_IT`, }, } DeclareLeafMetadata(chatGroupUserSettingsQueryCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -7173,7 +7174,7 @@ pl_PL, sv_SE, fi_FI, cs_CZ, ar_SA, tl_PH, he_IL, nl_NL, lo_LA, it_IT`, }, } DeclareLeafMetadata(chatGroupUserSettingsSetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/helpers/cmdcore_phase2_coverage_test.go b/internal/helpers/cmdcore_phase2_coverage_test.go index 8c0db5e1..f159bf32 100644 --- a/internal/helpers/cmdcore_phase2_coverage_test.go +++ b/internal/helpers/cmdcore_phase2_coverage_test.go @@ -23,7 +23,7 @@ import ( "github.com/spf13/cobra" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition" ) @@ -158,7 +158,7 @@ func TestCrossPlatformCoverageDeclareLeafMetadataValidateWithoutConfirmRunsInner } cmd.Flags().String("id", "", "") DeclareLeafMetadata(cmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -221,7 +221,7 @@ func TestCrossPlatformCoverageDeclareLeafMetadataConfirmFallbackWithoutCaller(t cmd.Flags().Bool("yes", false, "") cmd.Flags().Bool("dry-run", false, "") DeclareLeafMetadata(cmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, @@ -275,7 +275,7 @@ func TestCrossPlatformCoverageDeclareLeafMetadataDeferredConfirmAfterRunEWithout cmd.Flags().Bool("yes", false, "") cmd.Flags().Bool("dry-run", false, "") DeclareLeafMetadata(cmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, @@ -330,7 +330,7 @@ func TestCrossPlatformCoverageContractConfirmReadCallerDelegatesReads(t *testing cmd := &cobra.Command{Use: "read"} gate := &contractConfirmCaller{ inner: rc, cmd: cmd, - safety: cli.SafetySpec{ + safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, @@ -462,7 +462,7 @@ func TestCrossPlatformCoverageSheetMutationGuardRunsContractValidateBeforeTarget cmd.Flags().String("node", "", "") cmd.Flags().String("range", "", "") DeclareLeafMetadata(cmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/helpers/connect_daemon.go b/internal/helpers/connect_daemon.go index 848676d3..951a8a0f 100644 --- a/internal/helpers/connect_daemon.go +++ b/internal/helpers/connect_daemon.go @@ -17,7 +17,6 @@ import ( "context" "encoding/json" "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "io" "os" "os/exec" @@ -28,6 +27,7 @@ import ( "time" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cobracmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/logging" @@ -679,7 +679,7 @@ func newDevAppRobotConnectStatusCommand() *cobra.Command { cmd.Flags().String("unified-app-id", "", "统一应用 ID(当未用 clientId 起守护进程时定位)") cmd.Flags().Bool("json", false, "以 JSON 输出健康报告(供 launchd/systemd/pm2/cron 判断是否重启)") DeclareLeafMetadata(cmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -721,7 +721,7 @@ func newDevAppRobotConnectStopCommand() *cobra.Command { cmd.Flags().String("robot-client-id", "", "机器人 clientId(定位守护进程)") cmd.Flags().String("unified-app-id", "", "统一应用 ID(当未用 clientId 起守护进程时定位)") DeclareLeafMetadata(cmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, diff --git a/internal/helpers/contact.go b/internal/helpers/contact.go index ece3a332..0b2c3766 100644 --- a/internal/helpers/contact.go +++ b/internal/helpers/contact.go @@ -11,6 +11,7 @@ import ( "github.com/spf13/cobra" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" ) // ────────────────────────────────────────────────────────── @@ -372,9 +373,9 @@ func newContactAccountUpdateCommand() *cobra.Command { func newContactCommand() *cobra.Command { // Product-level Agent routing Decl (migrated from selection/contact.json // products.contact). Catalog assembly stamps provenance contract_final. - cli.RegisterProductDecl(cli.ProductDecl{ + contract.RegisterProductDecl(contract.ProductDecl{ ID: "contact", - Selection: cli.ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "查询通讯录与花名册,并管理企业、部门、员工及企业账号", UseWhen: []string{ "按姓名/手机号/userId/部门条件做通讯录精确查询,或明确执行企业与员工入企管理", @@ -433,7 +434,7 @@ func newContactCommand() *cobra.Command { }, } DeclareLeafMetadata(contactUserGetSelfCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -467,7 +468,7 @@ func newContactCommand() *cobra.Command { }, } DeclareLeafMetadata(contactRelationListMyFollowingsCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -502,7 +503,7 @@ func newContactCommand() *cobra.Command { }, } DeclareLeafMetadata(contactUserSearchCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -539,7 +540,7 @@ func newContactCommand() *cobra.Command { }, } DeclareLeafMetadata(contactUserSearchMobileCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -595,7 +596,7 @@ func newContactCommand() *cobra.Command { }, } DeclareLeafMetadata(contactUserGetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -727,7 +728,7 @@ func newContactCommand() *cobra.Command { }, } DeclareLeafMetadata(contactDeptSearchCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -762,7 +763,7 @@ func newContactCommand() *cobra.Command { }, } DeclareLeafMetadata(contactDeptListChildrenCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -797,7 +798,7 @@ func newContactCommand() *cobra.Command { }, } DeclareLeafMetadata(contactDeptGetInfoCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -842,7 +843,7 @@ func newContactCommand() *cobra.Command { }, } DeclareLeafMetadata(contactDeptListMembersCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -894,7 +895,7 @@ func newContactCommand() *cobra.Command { }, } DeclareLeafMetadata(contactUserProfileFieldsCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -951,7 +952,7 @@ contact user profile fields 获取可用字段列表。 }, } DeclareLeafMetadata(contactUserProfileGetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1064,7 +1065,7 @@ contact user profile fields 获取可用字段列表。 }, } DeclareLeafMetadata(contactUserDismissionSearchCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1132,7 +1133,7 @@ contact user profile fields 获取可用字段列表。 }, } DeclareLeafMetadata(contactUserInviteCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "non_idempotent", }, @@ -1160,7 +1161,7 @@ contact user profile fields 获取可用字段列表。 contactUserInviteCmd.Flags().String("depts", "", "员工所属部门列表 JSON 数组(可选),格式: [{\"deptId\":1}]") contactUserUpdateCmd := newContactUserUpdateCommand() DeclareLeafMetadata(contactUserUpdateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -1188,7 +1189,7 @@ contact user profile fields 获取可用字段列表。 }) contactUserUpdateSelfCmd := newContactUserUpdateSelfCommand() DeclareLeafMetadata(contactUserUpdateSelfCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -1253,7 +1254,7 @@ contact user profile fields 获取可用字段列表。 } contactDeptCreateCmd := newContactDeptCreateCommand() DeclareLeafMetadata(contactDeptCreateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "non_idempotent", }, @@ -1281,7 +1282,7 @@ contact user profile fields 获取可用字段列表。 }) contactDeptUpdateCmd := newContactDeptUpdateCommand() DeclareLeafMetadata(contactDeptUpdateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -1368,7 +1369,7 @@ contact user profile fields 获取可用字段列表。 }, } DeclareLeafMetadata(contactOrgCreateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "non_idempotent", }, @@ -1451,7 +1452,7 @@ contact user profile fields 获取可用字段列表。 }, } DeclareLeafMetadata(contactAccountCreateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "non_idempotent", }, @@ -1485,7 +1486,7 @@ contact user profile fields 获取可用字段列表。 contactAccountCreateCmd.Flags().Bool("send-pwd-via-sms", false, "是否通过手机短信/邮件发送登录邀请(可选)") contactAccountUpdateCmd := newContactAccountUpdateCommand() DeclareLeafMetadata(contactAccountUpdateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/helpers/contract_defects_test.go b/internal/helpers/contract_defects_test.go index eb967345..f7fd7ff5 100644 --- a/internal/helpers/contract_defects_test.go +++ b/internal/helpers/contract_defects_test.go @@ -12,7 +12,7 @@ import ( "strings" "testing" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition" "github.com/spf13/cobra" @@ -156,7 +156,7 @@ func TestDocVersionRevertPublishesRuntimeSafety(t *testing.T) { if err != nil || len(remaining) != 0 { t.Fatalf("find doc version revert: command=%v remaining=%v err=%v", cmd, remaining, err) } - final, ok := cli.RuntimeContractFinal(cmd) + final, ok := contract.RuntimeContractFinal(cmd) if !ok || final.Safety == nil { t.Fatal("doc version revert must publish ContractFinal Safety") } diff --git a/internal/helpers/contract_runtime.go b/internal/helpers/contract_runtime.go index 91875a09..babe6e33 100644 --- a/internal/helpers/contract_runtime.go +++ b/internal/helpers/contract_runtime.go @@ -18,7 +18,7 @@ import ( "github.com/spf13/cobra" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" ) // contractRuntime holds DeclareLeafMetadata execution hooks keyed by command @@ -26,7 +26,7 @@ import ( // confirmation without relying on PreRunE (direct RunE calls skip PreRunE). type contractRuntime struct { validate func(*cobra.Command, []string) error - safety cli.SafetySpec // zero when Confirmation is not user_required + safety contract.SafetySpec // zero when Confirmation is not user_required confirm bool } diff --git a/internal/helpers/dev.go b/internal/helpers/dev.go index 8ff5dff0..122d8c22 100644 --- a/internal/helpers/dev.go +++ b/internal/helpers/dev.go @@ -14,7 +14,7 @@ package helpers import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor" "github.com/spf13/cobra" ) @@ -47,9 +47,9 @@ func (devHandler) Name() string { func (devHandler) Command(runner executor.Runner) *cobra.Command { // Product-level Agent routing Decl (migrated from selection/dev.json // products.dev). Catalog assembly stamps provenance contract_final. - cli.RegisterProductDecl(cli.ProductDecl{ + contract.RegisterProductDecl(contract.ProductDecl{ ID: "dev", - Selection: cli.ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "管理开放平台应用、权限、机器人、版本发布与本地连接器", UseWhen: []string{ "创建/配置开放平台应用、机器人、权限、事件订阅或发布版本", diff --git a/internal/helpers/devapp.go b/internal/helpers/devapp.go index 6108343a..8e8559de 100644 --- a/internal/helpers/devapp.go +++ b/internal/helpers/devapp.go @@ -18,8 +18,8 @@ import ( "fmt" "strings" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cobracmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor" "github.com/spf13/cobra" @@ -85,9 +85,9 @@ const ( func newDevAppCommand(runner executor.Runner) *cobra.Command { // Product-level Agent routing Decl (migrated from selection/devapp.json // products.devapp). Catalog assembly stamps provenance contract_final. - cli.RegisterProductDecl(cli.ProductDecl{ + contract.RegisterProductDecl(contract.ProductDecl{ ID: "devapp", - Selection: cli.ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "管理钉钉开放平台企业内部应用、成员、权限、机器人、事件与版本", UseWhen: []string{ "请求涉及企业内部应用的查询、创建、配置、成员权限、机器人、事件订阅或版本管理", @@ -1492,35 +1492,34 @@ func devAppMeta(tool string) func(*cobra.Command) { } // devAppCompositeInterfaceReason 是 devapp 全树共用的评审 interface 说明(非 pin -// MCP 元数据的远程适配器)。从 schema_hints/metadata/dev.json 逐字迁入,作为 -// InterfaceDecl.Reason 的最终发布值。 +// MCP 元数据的远程适配器),作为 InterfaceDecl.Reason 的最终发布值。 const devAppCompositeInterfaceReason = "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." // devapp 的 SafetySpec 直接对齐 Agent Runtime Schema。函数每次返回一个值 // 副本,避免共享可变状态;四个字段彼此独立,不从 effect 或 risk 推导。 -func devAppSafetyRead() cli.SafetySpec { - return cli.SafetySpec{ +func devAppSafetyRead() contract.SafetySpec { + return contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", } } -func devAppSafetyWrite() cli.SafetySpec { - return cli.SafetySpec{ +func devAppSafetyWrite() contract.SafetySpec { + return contract.SafetySpec{ Effect: "write", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", } } -func devAppSafetyHighWrite() cli.SafetySpec { - return cli.SafetySpec{ +func devAppSafetyHighWrite() contract.SafetySpec { + return contract.SafetySpec{ Effect: "write", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", } } -func devAppSafetyDestructive() cli.SafetySpec { - return cli.SafetySpec{ +func devAppSafetyDestructive() contract.SafetySpec { + return contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", } diff --git a/internal/helpers/devdoc.go b/internal/helpers/devdoc.go index 198ee183..b66f89fe 100644 --- a/internal/helpers/devdoc.go +++ b/internal/helpers/devdoc.go @@ -7,14 +7,15 @@ import ( "github.com/spf13/cobra" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" ) func newDevdocCommand() *cobra.Command { // Product-level Agent routing Decl (migrated from selection/devdoc.json // products.devdoc). Catalog assembly stamps provenance contract_final. - cli.RegisterProductDecl(cli.ProductDecl{ + contract.RegisterProductDecl(contract.ProductDecl{ ID: "devdoc", - Selection: cli.ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "搜索钉钉开放平台开发文档与错误排查资料", UseWhen: []string{ "查询 OpenAPI、字段、错误码、SDK、鉴权或接入指南", @@ -74,7 +75,7 @@ func newDevdocArticleSearchCommand() *cobra.Command { _ = cmd.Flags().MarkHidden("keyword") cmd.Flags().String("page", "1", "页码,默认 1") cmd.Flags().String("size", "10", "每页数量,默认 10") - cli.AnnotateRuntimePositionals(cmd, cli.RuntimeSchemaPositional{ + cli.AnnotateRuntimePositionals(cmd, contract.RuntimeSchemaPositional{ // Keep the positional identity aligned with Cobra's authoritative // `search [keyword]` contract. The public --query flag is the other // member of the Schema require-one-of group; the hidden --keyword @@ -86,7 +87,7 @@ func newDevdocArticleSearchCommand() *cobra.Command { Index: 0, }) DeclareLeafMetadata(cmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -154,7 +155,7 @@ func newDevDocSearchCommand() *cobra.Command { _ = cmd.Flags().MarkHidden("keyword") cmd.Flags().String("page", "1", "页码,默认 1") cmd.Flags().String("size", "10", "每页数量,默认 10") - cli.AnnotateRuntimePositionals(cmd, cli.RuntimeSchemaPositional{ + cli.AnnotateRuntimePositionals(cmd, contract.RuntimeSchemaPositional{ Name: "keyword", Type: "string", Description: "搜索关键词;也可通过 --query 传入", @@ -162,7 +163,7 @@ func newDevDocSearchCommand() *cobra.Command { Index: 0, }) DeclareLeafMetadata(cmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/helpers/ding.go b/internal/helpers/ding.go index 6880496f..a474d97a 100644 --- a/internal/helpers/ding.go +++ b/internal/helpers/ding.go @@ -7,8 +7,8 @@ import ( "github.com/spf13/cobra" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" ) // ────────────────────────────────────────────────────────── @@ -21,9 +21,9 @@ var dingRemindTypeMap = map[string]int{"app": 1, "sms": 2, "call": 3} func newDingCommand() *cobra.Command { // Product-level Agent routing Decl (migrated from selection/ding.json // products.ding). Catalog assembly stamps provenance contract_final. - cli.RegisterProductDecl(cli.ProductDecl{ + contract.RegisterProductDecl(contract.ProductDecl{ ID: "ding", - Selection: cli.ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "以企业机器人发送或撤回应用内/短信/电话 DING", UseWhen: []string{ "需要机器人身份发送或撤回 DING", @@ -84,7 +84,7 @@ func newDingCommand() *cobra.Command { }, } DeclareLeafMetadata(dingMessageSendCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -137,7 +137,7 @@ func newDingCommand() *cobra.Command { }, } DeclareLeafMetadata(dingMessageRecallCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, diff --git a/internal/helpers/doc.go b/internal/helpers/doc.go index 67448b95..db2a1df7 100644 --- a/internal/helpers/doc.go +++ b/internal/helpers/doc.go @@ -18,6 +18,7 @@ import ( "github.com/spf13/cobra" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" ) // ────────────────────────────────────────────────────────── @@ -791,9 +792,9 @@ func renderDocOverwriteDiff(nodeID, before, after string) string { func newDocCommand() *cobra.Command { // Product-level Agent routing Decl (migrated from selection/doc.json // products.doc). Catalog assembly stamps provenance contract_final. - cli.RegisterProductDecl(cli.ProductDecl{ + contract.RegisterProductDecl(contract.ProductDecl{ ID: "doc", - Selection: cli.ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "管理钉钉在线文档的正文、块、评论、导入导出、模板与版本", UseWhen: []string{ "创建、读取或编辑在线文档内容,或处理文档块、评论、导入导出、模板和版本时", @@ -897,7 +898,7 @@ func newDocCommand() *cobra.Command { }, } DeclareLeafMetadata(searchCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -950,7 +951,7 @@ func newDocCommand() *cobra.Command { }, } DeclareLeafMetadata(listCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -987,7 +988,7 @@ func newDocCommand() *cobra.Command { }, } DeclareLeafMetadata(infoCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1083,7 +1084,7 @@ func newDocCommand() *cobra.Command { }, } DeclareLeafMetadata(readCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1208,7 +1209,7 @@ func newDocCommand() *cobra.Command { }, } DeclareLeafMetadata(createCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1335,7 +1336,7 @@ WARNING: --mode overwrite 为破坏性写入,会清空原文档全部内容。 }, } DeclareLeafMetadata(updateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1412,7 +1413,7 @@ WARNING: --mode overwrite 为破坏性写入,会清空原文档全部内容。 }, } DeclareLeafMetadata(fileCreateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1463,7 +1464,7 @@ WARNING: --mode overwrite 为破坏性写入,会清空原文档全部内容。 }, } DeclareLeafMetadata(folderCreateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1499,7 +1500,7 @@ WARNING: --mode overwrite 为破坏性写入,会清空原文档全部内容。 RunE: runDocUpload, } DeclareLeafMetadata(uploadCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1539,7 +1540,7 @@ WARNING: --mode overwrite 为破坏性写入,会清空原文档全部内容。 RunE: runDocDownload, } DeclareLeafMetadata(downloadCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1607,7 +1608,7 @@ WARNING: --mode overwrite 为破坏性写入,会清空原文档全部内容。 }, } DeclareLeafMetadata(blockListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1730,7 +1731,7 @@ WARNING: --mode overwrite 为破坏性写入,会清空原文档全部内容。 }, } DeclareLeafMetadata(blockInsertCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1808,7 +1809,7 @@ WARNING: --mode overwrite 为破坏性写入,会清空原文档全部内容。 }, } DeclareLeafMetadata(blockUpdateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1850,7 +1851,7 @@ WARNING: --mode overwrite 为破坏性写入,会清空原文档全部内容。 }, } DeclareLeafMetadata(blockDeleteCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, @@ -1887,7 +1888,7 @@ WARNING: --mode overwrite 为破坏性写入,会清空原文档全部内容。 RunE: buildNodeTransferRunE("copy_document"), } DeclareLeafMetadata(copyCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1930,7 +1931,7 @@ WARNING: --mode overwrite 为破坏性写入,会清空原文档全部内容。 RunE: buildNodeTransferRunE("move_document"), } DeclareLeafMetadata(moveCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1979,7 +1980,7 @@ WARNING: --mode overwrite 为破坏性写入,会清空原文档全部内容。 }, } DeclareLeafMetadata(renameCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -2027,7 +2028,7 @@ WARNING: --mode overwrite 为破坏性写入,会清空原文档全部内容。 }, } DeclareLeafMetadata(deleteCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, @@ -2280,7 +2281,7 @@ resourceId 需通过 dws doc block list 获取:查询目标文档的块列表 }, } DeclareLeafMetadata(mediaDownloadCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2324,7 +2325,7 @@ resourceId 需通过 dws doc block list 获取:查询目标文档的块列表 RunE: runMediaInsert, } DeclareLeafMetadata(mediaInsertCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -2424,7 +2425,7 @@ resourceId 需通过 dws doc block list 获取:查询目标文档的块列表 }, } DeclareLeafMetadata(commentListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2491,7 +2492,7 @@ resourceId 需通过 dws doc block list 获取:查询目标文档的块列表 }, } DeclareLeafMetadata(commentCreateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -2576,7 +2577,7 @@ commentKey可从 dws doc comment create 或 dws doc comment list 返回结果中 }, } DeclareLeafMetadata(commentReplyCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -2642,7 +2643,7 @@ commentKey可从 dws doc comment create 或 dws doc comment list 返回结果中 }, } DeclareLeafMetadata(commentUpdateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -2696,7 +2697,7 @@ commentKey可从 dws doc comment create 或 dws doc comment list 返回结果中 }, } DeclareLeafMetadata(commentDeleteCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -2763,7 +2764,7 @@ commentKey可从 dws doc comment create 或 dws doc comment list 返回结果中 }, } DeclareLeafMetadata(commentCreateInlineCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -2865,7 +2866,7 @@ commentKey可从 dws doc comment create 或 dws doc comment list 返回结果中 }, } DeclareLeafMetadata(permissionAddCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -2937,7 +2938,7 @@ commentKey可从 dws doc comment create 或 dws doc comment list 返回结果中 }, } DeclareLeafMetadata(permissionUpdateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -3001,7 +3002,7 @@ commentKey可从 dws doc comment create 或 dws doc comment list 返回结果中 }, } DeclareLeafMetadata(permissionListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -3065,7 +3066,7 @@ commentKey可从 dws doc comment create 或 dws doc comment list 返回结果中 }, } DeclareLeafMetadata(permissionRemoveCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -3300,7 +3301,7 @@ CLI 内部自动完成全部流程: }, } DeclareLeafMetadata(exportGetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -3398,7 +3399,7 @@ CLI 内部自动完成全部流程: }, } DeclareLeafMetadata(importGetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -3443,7 +3444,7 @@ CLI 内部自动完成全部流程: }, } DeclareLeafMetadata(versionSaveCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -3485,7 +3486,7 @@ CLI 内部自动完成全部流程: }, } DeclareLeafMetadata(versionListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -3507,7 +3508,7 @@ CLI 内部自动完成全部流程: versionListCmd.Flags().Int("limit", 0, "返回版本数量上限") versionListCmd.Flags().String("cursor", "", "分页游标") - versionRevertSafety := cli.SafetySpec{ + versionRevertSafety := contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", @@ -3611,7 +3612,7 @@ CLI 内部自动完成全部流程: }, } DeclareLeafMetadata(templateListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -3667,7 +3668,7 @@ CLI 内部自动完成全部流程: }, } DeclareLeafMetadata(templateSearchCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -3724,7 +3725,7 @@ CLI 内部自动完成全部流程: }, } DeclareLeafMetadata(templateApplyCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, diff --git a/internal/helpers/doc_style.go b/internal/helpers/doc_style.go index 88562a03..a52f4dea 100644 --- a/internal/helpers/doc_style.go +++ b/internal/helpers/doc_style.go @@ -3,11 +3,11 @@ package helpers import ( "context" "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "os" "path/filepath" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -50,7 +50,7 @@ func newDocStyleCommand() *cobra.Command { RunE: runDocStyleCoverSet, } DeclareLeafMetadata(coverSetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -75,7 +75,7 @@ func newDocStyleCommand() *cobra.Command { RunE: runDocStyleCoverClear, } DeclareLeafMetadata(coverClearCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -107,7 +107,7 @@ func newDocStyleCommand() *cobra.Command { RunE: runDocStyleBackgroundSet, } DeclareLeafMetadata(backgroundSetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -132,7 +132,7 @@ func newDocStyleCommand() *cobra.Command { RunE: runDocStyleBackgroundClear, } DeclareLeafMetadata(backgroundClearCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -158,7 +158,7 @@ func newDocStyleCommand() *cobra.Command { RunE: runDocStyleGet, } DeclareLeafMetadata(getCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/helpers/drive.go b/internal/helpers/drive.go index 0409fa39..e709ea40 100644 --- a/internal/helpers/drive.go +++ b/internal/helpers/drive.go @@ -12,9 +12,8 @@ import ( "github.com/spf13/cobra" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" ) // ────────────────────────────────────────────────────────── @@ -286,9 +285,9 @@ func parseDriveUploadInfo(text string) (resourceURL, uploadID string, headers ma func newDriveCommand() *cobra.Command { // Product-level Agent routing Decl (migrated from selection/drive.json // products.drive). Catalog assembly stamps provenance contract_final. - cli.RegisterProductDecl(cli.ProductDecl{ + contract.RegisterProductDecl(contract.ProductDecl{ ID: "drive", - Selection: cli.ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "管理钉盘及文档空间中的文件、目录、上传下载、回收站与公开发布", UseWhen: []string{ "浏览、搜索、上传、下载或整理钉盘和文档空间文件时", @@ -461,7 +460,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(driveListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -512,7 +511,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(driveInfoCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -608,7 +607,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(driveDownloadCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -705,7 +704,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(driveDownloadVersionCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -754,7 +753,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(driveMkdirCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -815,7 +814,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(driveUploadInfoCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -867,7 +866,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(driveCommitCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -961,7 +960,7 @@ func newDriveCommand() *cobra.Command { RunE: runDriveUpload, } DeclareLeafMetadata(driveUploadCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1040,7 +1039,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(driveListSpacesCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1198,7 +1197,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(driveSearchCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1271,7 +1270,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(driveDeleteCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, @@ -1326,7 +1325,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(driveCopyCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1390,7 +1389,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(driveMoveCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1454,7 +1453,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(driveRenameCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1501,7 +1500,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(driveStatsCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1553,7 +1552,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(driveShortcutCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "non_idempotent", }, @@ -1623,7 +1622,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(drivePermAddCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1686,7 +1685,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(drivePermUpdateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1744,7 +1743,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(drivePermListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1797,7 +1796,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(drivePermRemoveCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1915,7 +1914,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(drivePermTransferOwnerCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "high", Confirmation: "user_required", Idempotency: "non_idempotent", }, @@ -1952,7 +1951,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(drivePermApplyInfoCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2007,7 +2006,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(drivePermApplyCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "non_idempotent", }, @@ -2085,7 +2084,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(recycleListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2128,7 +2127,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(recycleRestoreCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -2219,7 +2218,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(drivePublishSetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -2270,7 +2269,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(drivePublishUnsetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -2314,7 +2313,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(drivePublishGetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -2415,7 +2414,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(driveRecentCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2472,7 +2471,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(driveStarAddCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2505,7 +2504,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(driveStarRemoveCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2553,7 +2552,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(driveStarListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2596,7 +2595,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(driveCoverCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2639,7 +2638,7 @@ func newDriveCommand() *cobra.Command { }, } DeclareLeafMetadata(driveRevertCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "non_idempotent", }, diff --git a/internal/helpers/hrbrain.go b/internal/helpers/hrbrain.go index 5a5dbfa2..172da26f 100644 --- a/internal/helpers/hrbrain.go +++ b/internal/helpers/hrbrain.go @@ -3,11 +3,11 @@ package helpers import ( "encoding/json" "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/spf13/cobra" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" ) // ────────────────────────────────────────────────────────── @@ -17,9 +17,9 @@ import ( func newHrbrainCommand() *cobra.Command { // Product-level Agent routing Decl (migrated from selection/hrbrain.json // products.hrbrain). Catalog assembly stamps provenance contract_final. - cli.RegisterProductDecl(cli.ProductDecl{ + contract.RegisterProductDecl(contract.ProductDecl{ ID: "hrbrain", - Selection: cli.ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "钉钉组织大脑:人才池管理、员工档案查询与人才搜索", UseWhen: []string{ "需要查询人才池、员工档案(元数据/标签/职业历程/绩效)或搜索员工时", @@ -83,7 +83,7 @@ func newHrbrainCommand() *cobra.Command { }, } DeclareLeafMetadata(talentPoolListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -137,7 +137,7 @@ func newHrbrainCommand() *cobra.Command { }, } DeclareLeafMetadata(talentPoolDetailCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -179,7 +179,7 @@ func newHrbrainCommand() *cobra.Command { }, } DeclareLeafMetadata(talentPoolEmployeesCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -230,7 +230,7 @@ func newHrbrainCommand() *cobra.Command { }, } DeclareLeafMetadata(profileMetadataCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -279,7 +279,7 @@ func newHrbrainCommand() *cobra.Command { }, } DeclareLeafMetadata(profileQueryCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -327,7 +327,7 @@ func newHrbrainCommand() *cobra.Command { }, } DeclareLeafMetadata(profileLabelsCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -366,7 +366,7 @@ func newHrbrainCommand() *cobra.Command { }, } DeclareLeafMetadata(profileCareerCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -403,7 +403,7 @@ func newHrbrainCommand() *cobra.Command { }, } DeclareLeafMetadata(profilePerformanceCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -464,7 +464,7 @@ func newHrbrainCommand() *cobra.Command { }, } DeclareLeafMetadata(employeeSearchCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -547,7 +547,7 @@ func newHrbrainCommand() *cobra.Command { }, } DeclareLeafMetadata(employeeSearchStructuredCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -591,7 +591,7 @@ func newHrbrainCommand() *cobra.Command { }, } DeclareLeafMetadata(searchFieldsCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/helpers/leaf_constraints_test.go b/internal/helpers/leaf_constraints_test.go index 972cd669..1909d6e5 100644 --- a/internal/helpers/leaf_constraints_test.go +++ b/internal/helpers/leaf_constraints_test.go @@ -23,8 +23,8 @@ import ( "github.com/spf13/cobra" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" ) func leafConstraintTestSpec(captured *map[string]any) LeafSpec { @@ -480,28 +480,28 @@ func TestCrossPlatformCoverageLeafSpecCorePaths(t *testing.T) { var errTransformTest = errors.New("transform failed") -func leafTestReadSafety() cli.SafetySpec { - return cli.SafetySpec{ +func leafTestReadSafety() contract.SafetySpec { + return contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", } } -func leafTestWriteSafety() cli.SafetySpec { - return cli.SafetySpec{ +func leafTestWriteSafety() contract.SafetySpec { + return contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", } } -func leafTestDestructiveSafety() cli.SafetySpec { - return cli.SafetySpec{ +func leafTestDestructiveSafety() contract.SafetySpec { + return contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", } } -func leafSafetySpec(safety cli.SafetySpec, called *bool) LeafSpec { +func leafSafetySpec(safety contract.SafetySpec, called *bool) LeafSpec { return LeafSpec{ Use: "danger", Short: "危险", @@ -515,7 +515,7 @@ func leafSafetySpec(safety cli.SafetySpec, called *bool) LeafSpec { } } -func leafSafetyRun(t *testing.T, safety cli.SafetySpec, stdin string, args ...string) (bool, error) { +func leafSafetyRun(t *testing.T, safety contract.SafetySpec, stdin string, args ...string) (bool, error) { t.Helper() called := false cmd := NewLeafCommand(leafSafetySpec(safety, &called)) @@ -581,7 +581,7 @@ func TestCrossPlatformCoverageLeafSafetyReadNeverPrompts(t *testing.T) { t.Fatalf("read safety err = %v called = %v", err, called) } // 空 Safety 保留只读默认。 - if called, err := leafSafetyRun(t, cli.SafetySpec{}, "", "--id", "x"); err != nil || !called { + if called, err := leafSafetyRun(t, contract.SafetySpec{}, "", "--id", "x"); err != nil || !called { t.Fatalf("empty safety err = %v called = %v", err, called) } } diff --git a/internal/helpers/leaf_test.go b/internal/helpers/leaf_test.go index 0b3302a9..8ef74d36 100644 --- a/internal/helpers/leaf_test.go +++ b/internal/helpers/leaf_test.go @@ -22,8 +22,8 @@ import ( "github.com/spf13/cobra" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition" ) @@ -409,7 +409,7 @@ func TestDeclareLeafMetadataInstallsConfirmSafetyForUserRequired(t *testing.T) { cmd.Flags().Bool("yes", false, "") cmd.Flags().Bool("dry-run", false, "") DeclareLeafMetadata(cmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, @@ -476,7 +476,7 @@ func TestDeclareLeafMetadataDefersConfirmUntilCallTool(t *testing.T) { cmd.Flags().Bool("dry-run", false, "") cmd.Flags().String("id", "", "") DeclareLeafMetadata(cmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, @@ -532,7 +532,7 @@ func TestDeclareLeafMetadataValidateRunsBeforeConfirmSafety(t *testing.T) { cmd.Flags().Bool("dry-run", false, "") cmd.Flags().String("id", "", "") DeclareLeafMetadata(cmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/helpers/live.go b/internal/helpers/live.go index 68cb7360..fa0da119 100644 --- a/internal/helpers/live.go +++ b/internal/helpers/live.go @@ -1,16 +1,16 @@ package helpers import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) func newLiveCommand() *cobra.Command { // Product-level Agent routing Decl (migrated from selection/live.json // products.live). Catalog assembly stamps provenance contract_final. - cli.RegisterProductDecl(cli.ProductDecl{ + contract.RegisterProductDecl(contract.ProductDecl{ ID: "live", - Selection: cli.ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "查询当前用户发起的直播列表", UseWhen: []string{ "用户要查看自己的直播列表或基础统计", @@ -38,7 +38,7 @@ func newLiveCommand() *cobra.Command { }, } DeclareLeafMetadata(streamListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/helpers/mail.go b/internal/helpers/mail.go index 3a53b3aa..95a4b5f2 100644 --- a/internal/helpers/mail.go +++ b/internal/helpers/mail.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "io" "net/http" "os" @@ -15,6 +14,7 @@ import ( "github.com/spf13/cobra" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" ) // ────────────────────────────────────────────────────────── @@ -100,9 +100,9 @@ func validateMailRuleConditions(conditions []any) error { func newMailCommand() *cobra.Command { // Product-level Agent routing Decl (migrated from selection/mail.json // products.mail). Catalog assembly stamps provenance contract_final. - cli.RegisterProductDecl(cli.ProductDecl{ + contract.RegisterProductDecl(contract.ProductDecl{ ID: "mail", - Selection: cli.ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "管理邮箱、邮件、草稿、附件、文件夹、联系人与邮件模板", UseWhen: []string{ "查收、搜索、阅读、回复、发送或整理邮件", @@ -138,7 +138,7 @@ func newMailCommand() *cobra.Command { }, } DeclareLeafMetadata(mailboxListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -259,7 +259,7 @@ func newMailCommand() *cobra.Command { }, } DeclareLeafMetadata(messageSearchCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -323,7 +323,7 @@ func newMailCommand() *cobra.Command { }, } DeclareLeafMetadata(messageListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -373,7 +373,7 @@ func newMailCommand() *cobra.Command { }, } DeclareLeafMetadata(messageGetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -453,7 +453,7 @@ func newMailCommand() *cobra.Command { }, } DeclareLeafMetadata(messageSendCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -509,7 +509,7 @@ func newMailCommand() *cobra.Command { }, } DeclareLeafMetadata(folderListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -564,7 +564,7 @@ func newMailCommand() *cobra.Command { }, } DeclareLeafMetadata(folderCreateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -608,7 +608,7 @@ func newMailCommand() *cobra.Command { }, } DeclareLeafMetadata(folderDeleteCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -656,7 +656,7 @@ func newMailCommand() *cobra.Command { }, } DeclareLeafMetadata(folderUpdateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -721,7 +721,7 @@ func newMailCommand() *cobra.Command { }, } DeclareLeafMetadata(tagListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -931,7 +931,7 @@ func newMailCommand() *cobra.Command { }, } DeclareLeafMetadata(threadGetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1227,7 +1227,7 @@ internetMessageId 来源:message send / draft send / message reply / message r }, } DeclareLeafMetadata(messageReplyCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1295,7 +1295,7 @@ internetMessageId 来源:message send / draft send / message reply / message r }, } DeclareLeafMetadata(messageReplyAllCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1363,7 +1363,7 @@ internetMessageId 来源:message send / draft send / message reply / message r }, } DeclareLeafMetadata(messageForwardCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1404,7 +1404,7 @@ internetMessageId 来源:message send / draft send / message reply / message r }, } DeclareLeafMetadata(messageBatchMoveCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1442,7 +1442,7 @@ internetMessageId 来源:message send / draft send / message reply / message r }, } DeclareLeafMetadata(messageBatchDeleteCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1629,7 +1629,7 @@ internetMessageId 来源:message send / draft send / message reply / message r }, } DeclareLeafMetadata(draftCreateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1706,7 +1706,7 @@ internetMessageId 来源:message send / draft send / message reply / message r }, } DeclareLeafMetadata(draftUpdateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1752,7 +1752,7 @@ internetMessageId 来源:message send / draft send / message reply / message r }, } DeclareLeafMetadata(attachmentListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1806,7 +1806,7 @@ internetMessageId 来源:message send / draft send / message reply / message r }, } DeclareLeafMetadata(attachmentDownloadCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2048,7 +2048,7 @@ internetMessageId 来源:message send / draft send / message reply / message r }, } DeclareLeafMetadata(draftSendCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -2136,7 +2136,7 @@ user 对象字段: }, } DeclareLeafMetadata(userSearchCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2212,7 +2212,7 @@ user 对象字段: }, } DeclareLeafMetadata(templateCreateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -2272,7 +2272,7 @@ user 对象字段: }, } DeclareLeafMetadata(templateListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2319,7 +2319,7 @@ user 对象字段: }, } DeclareLeafMetadata(templateGetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2390,7 +2390,7 @@ user 对象字段: }, } DeclareLeafMetadata(templateUpdateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -2444,7 +2444,7 @@ user 对象字段: }, } DeclareLeafMetadata(templateDeleteCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -2505,7 +2505,7 @@ user 对象字段: }, } DeclareLeafMetadata(contactCreateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -2561,7 +2561,7 @@ user 对象字段: }, } DeclareLeafMetadata(contactListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2622,7 +2622,7 @@ user 对象字段: }, } DeclareLeafMetadata(contactUpdateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -2668,7 +2668,7 @@ user 对象字段: }, } DeclareLeafMetadata(contactBatchDeleteCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, diff --git a/internal/helpers/markdown.go b/internal/helpers/markdown.go index 8e3d3e54..61b9a6b4 100644 --- a/internal/helpers/markdown.go +++ b/internal/helpers/markdown.go @@ -27,6 +27,7 @@ import ( "github.com/spf13/cobra" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" ) var markdownUploadStat = os.Stat @@ -34,9 +35,9 @@ var markdownUploadStat = os.Stat func newMarkdownCommand() *cobra.Command { // Product-level Agent routing Decl (migrated from selection/markdown.json // products.markdown). Catalog assembly stamps provenance contract_final. - cli.RegisterProductDecl(cli.ProductDecl{ + contract.RegisterProductDecl(contract.ProductDecl{ ID: "markdown", - Selection: cli.ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "跨钉盘与文档空间创建、获取、覆盖和局部修补原生 Markdown 文件", UseWhen: []string{ "目标是原生 .md 文件,并需要在 Drive/Doc 路由间安全处理内容时", @@ -86,7 +87,7 @@ func newMarkdownFetchCmd() *cobra.Command { MutuallyExclusive: [][]string{{"space-id", "workspace"}}, }) DeclareLeafMetadata(cmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -240,7 +241,7 @@ func newMarkdownCreateCmd() *cobra.Command { }) cli.AnnotateRuntimeFlagRequiredWhen(cmd, "name", "--content is used") DeclareLeafMetadata(cmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "non_idempotent", }, @@ -406,7 +407,7 @@ func newMarkdownOverwriteCmd() *cobra.Command { RequireOneOf: [][]string{{"content", "file"}}, }) DeclareLeafMetadata(cmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, @@ -573,7 +574,7 @@ func newMarkdownPatchCmd() *cobra.Command { MutuallyExclusive: [][]string{{"space-id", "workspace"}}, }) DeclareLeafMetadata(cmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/helpers/minutes.go b/internal/helpers/minutes.go index 2d37f734..dc717927 100644 --- a/internal/helpers/minutes.go +++ b/internal/helpers/minutes.go @@ -4,8 +4,8 @@ import ( "fmt" "strconv" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -16,9 +16,9 @@ import ( func newMinutesCommand() *cobra.Command { // Product-level Agent routing Decl (migrated from selection/minutes.json // products.minutes). Catalog assembly stamps provenance contract_final. - cli.RegisterProductDecl(cli.ProductDecl{ + contract.RegisterProductDecl(contract.ProductDecl{ ID: "minutes", - Selection: cli.ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "查询和维护钉钉听记的转写、摘要、待办、权限、录音、标签、说话人总结及文件上传会话。", UseWhen: []string{ "用户要查找、读取、编辑或管理钉钉听记及其录音、转写、摘要和衍生内容。", @@ -43,7 +43,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(minutesListMineCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -74,7 +74,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(minutesListSharedCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -109,7 +109,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(minutesListAllCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -146,7 +146,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(minutesGetInfoCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -181,7 +181,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(minutesGetSummaryCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -223,7 +223,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(minutesGetKeywordsCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -277,7 +277,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(minutesGetTranscriptionCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -315,7 +315,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(minutesGetTodosCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -365,7 +365,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(minutesGetAudioCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -402,7 +402,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(minutesGetBatchCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -441,7 +441,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(minutesUpdateTitleCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -544,7 +544,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(minutesRecordStartCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -586,7 +586,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(minutesRecordPauseCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -628,7 +628,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(minutesRecordResumeCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -670,7 +670,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(minutesRecordStopCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -728,7 +728,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(minutesUpdateSummaryCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -776,7 +776,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(mindGraphCreateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -821,7 +821,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(mindGraphStatusCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -878,7 +878,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(speakerReplaceCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -929,7 +929,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(speakerSummaryCreateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -973,7 +973,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(speakerSummaryGetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1021,7 +1021,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(hotWordAddCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1056,7 +1056,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(hotWordListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1096,7 +1096,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(replaceTextCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1190,7 +1190,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(uploadCreateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1243,7 +1243,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(uploadCompleteCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1287,7 +1287,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(uploadCancelCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1385,7 +1385,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(permissionAddCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1446,7 +1446,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(permissionRemoveCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1502,7 +1502,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(tagListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1553,7 +1553,7 @@ func newMinutesCommand() *cobra.Command { }, } DeclareLeafMetadata(tagQueryCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/helpers/oa.go b/internal/helpers/oa.go index 5ed90621..dfa02d1c 100644 --- a/internal/helpers/oa.go +++ b/internal/helpers/oa.go @@ -2,10 +2,10 @@ package helpers import ( "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "strconv" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -21,9 +21,9 @@ import ( func newOaCommand() *cobra.Command { // Product-level Agent routing Decl (migrated from selection/oa.json // products.oa). Catalog assembly stamps provenance contract_final. - cli.RegisterProductDecl(cli.ProductDecl{ + contract.RegisterProductDecl(contract.ProductDecl{ ID: "oa", - Selection: cli.ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "查询和处理 OA 审批实例、任务、记录、抄送与评论", UseWhen: []string{ "查看待审、已办、已发起或抄送审批,并执行同意、拒绝、撤销、转交等审批动作时", @@ -79,7 +79,7 @@ func newOaCommand() *cobra.Command { }, } DeclareLeafMetadata(approvalListPendingCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -118,7 +118,7 @@ func newOaCommand() *cobra.Command { }, } DeclareLeafMetadata(approvalDetailCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -164,7 +164,7 @@ func newOaCommand() *cobra.Command { }, } DeclareLeafMetadata(approvalApproveCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -209,7 +209,7 @@ func newOaCommand() *cobra.Command { }, } DeclareLeafMetadata(approvalRejectCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -254,7 +254,7 @@ func newOaCommand() *cobra.Command { }, } DeclareLeafMetadata(approvalRevokeCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, @@ -290,7 +290,7 @@ func newOaCommand() *cobra.Command { }, } DeclareLeafMetadata(approvalRecordsCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -346,7 +346,7 @@ func newOaCommand() *cobra.Command { }, } DeclareLeafMetadata(approvalListInitiatedCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -385,7 +385,7 @@ func newOaCommand() *cobra.Command { }, } DeclareLeafMetadata(approvalTasksCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -424,7 +424,7 @@ func newOaCommand() *cobra.Command { }, } DeclareLeafMetadata(approvalListFormsCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -499,7 +499,7 @@ func newOaCommand() *cobra.Command { }, } DeclareLeafMetadata(approvalExecutedListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -542,7 +542,7 @@ func newOaCommand() *cobra.Command { }, } DeclareLeafMetadata(approvalSubmittedListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -585,7 +585,7 @@ func newOaCommand() *cobra.Command { }, } DeclareLeafMetadata(approvalCcListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -631,7 +631,7 @@ func newOaCommand() *cobra.Command { }, } DeclareLeafMetadata(approvalTransferCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -676,7 +676,7 @@ func newOaCommand() *cobra.Command { }, } DeclareLeafMetadata(approvalCommentCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -724,7 +724,7 @@ func newOaCommand() *cobra.Command { }, } DeclareLeafMetadata(approvalCcCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, diff --git a/internal/helpers/remaining_unit_coverage_test.go b/internal/helpers/remaining_unit_coverage_test.go index 1ae0c0a8..84f87a30 100644 --- a/internal/helpers/remaining_unit_coverage_test.go +++ b/internal/helpers/remaining_unit_coverage_test.go @@ -18,7 +18,7 @@ import ( "github.com/open-dingtalk/dingtalk-stream-sdk-go/chatbot" "github.com/spf13/cobra" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition" ) @@ -282,7 +282,7 @@ func TestSheetMutationGuardRejectsPipedYesEvenWithContractConfirmSafety(t *testi cmd.Flags().Bool("dry-run", false, "") cmd.Flags().String("node", "", "") DeclareLeafMetadata(cmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/helpers/report.go b/internal/helpers/report.go index c49f9afd..cdf128e7 100644 --- a/internal/helpers/report.go +++ b/internal/helpers/report.go @@ -16,6 +16,7 @@ import ( "github.com/spf13/cobra" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" ) const ( @@ -52,9 +53,9 @@ var ( func newReportCommand() *cobra.Command { // Product-level Agent routing Decl (migrated from selection/report.json // products.report). Catalog assembly stamps provenance contract_final. - cli.RegisterProductDecl(cli.ProductDecl{ + contract.RegisterProductDecl(contract.ProductDecl{ ID: "report", - Selection: cli.ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "查询日志模板、收发日志、日志正文与统计,并按模板提交日志", UseWhen: []string{ "查看或提交日报、周报等钉钉日志时", @@ -96,7 +97,7 @@ func newReportCommand() *cobra.Command { RunE: runReportTemplateList, } DeclareLeafMetadata(templateListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -125,7 +126,7 @@ func newReportCommand() *cobra.Command { RunE: runReportTemplateDetail, } DeclareLeafMetadata(templateGetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -169,7 +170,7 @@ func newReportCommand() *cobra.Command { RunE: runReportDetail, } DeclareLeafMetadata(entryGetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -203,7 +204,7 @@ func newReportCommand() *cobra.Command { RunE: runReportStats, } DeclareLeafMetadata(entryStatsCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -247,7 +248,7 @@ func newReportCommand() *cobra.Command { RunE: runReportCreate, } DeclareLeafMetadata(entrySubmitCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -296,7 +297,7 @@ func newReportCommand() *cobra.Command { RunE: runReportList, } DeclareLeafMetadata(inboxListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -333,7 +334,7 @@ func newReportCommand() *cobra.Command { RunE: runReportSent, } DeclareLeafMetadata(outboxListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -373,9 +374,11 @@ func newReportCommand() *cobra.Command { // create alias. Schema assembly still reads ContractFinal from the primary // leaf; this keeps compatibility-equivalence NativeRequired annotations // symmetric instead of relying only on one-sided tolerance. - cli.ApplyParamDecls(createCmd, []cli.ParamDecl{ + if err := cli.ApplyParamDecls(createCmd, []contract.ParamDecl{ {Name: "to-chat", Required: boolPtr(false)}, - }) + }); err != nil { + panic(err) + } detailCmd := &cobra.Command{ Use: "detail", diff --git a/internal/helpers/sheet.go b/internal/helpers/sheet.go index 02cdae05..06900fd5 100644 --- a/internal/helpers/sheet.go +++ b/internal/helpers/sheet.go @@ -2,9 +2,9 @@ package helpers import ( "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -23,9 +23,9 @@ func resolveSheetName(cmd *cobra.Command) string { func newSheetCommand() *cobra.Command { // Product-level Agent routing Decl (migrated from selection/sheet.json // products.sheet). Catalog assembly stamps provenance contract_final. - cli.RegisterProductDecl(cli.ProductDecl{ + contract.RegisterProductDecl(contract.ProductDecl{ ID: "sheet", - Selection: cli.ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "导入本地 Excel,或创建、读取、编辑和导出钉钉在线电子表格(axls),并管理工作表、区域、筛选、图表、图片与格式。", UseWhen: []string{ "用户要处理钉钉在线电子表格中的工作表、单元格、范围、筛选、图表、图片或格式时", @@ -129,7 +129,7 @@ func newSheetCommand() *cobra.Command { chartCmd := newChartCmd() exportCmd := newExportCmd() DeclareLeafMetadata(exportCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -155,7 +155,7 @@ func newSheetCommand() *cobra.Command { batchUpdateCmd := newBatchUpdateCmd() DeclareLeafMetadata(batchUpdateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -175,7 +175,7 @@ func newSheetCommand() *cobra.Command { }) rangeBatchClearCmd := newRangeBatchClearCmd() DeclareLeafMetadata(rangeBatchClearCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/helpers/sheet_chart.go b/internal/helpers/sheet_chart.go index ba3503cc..bc850f14 100644 --- a/internal/helpers/sheet_chart.go +++ b/internal/helpers/sheet_chart.go @@ -3,10 +3,10 @@ package helpers import ( "encoding/json" "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "os" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -150,7 +150,7 @@ func newChartCmd() *cobra.Command { }, } DeclareLeafMetadata(chartListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -244,7 +244,7 @@ position.col 使用列字母表示法(如 "A"、"AA"),不支持数字形 }, } DeclareLeafMetadata(chartCreateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -320,7 +320,7 @@ position.col 使用列字母表示法(如 "A"、"AA"),不支持数字形 }, } DeclareLeafMetadata(chartUpdateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -362,7 +362,7 @@ chart-id 可通过 chart list 获取。`, }, } DeclareLeafMetadata(chartDeleteCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/helpers/sheet_comment.go b/internal/helpers/sheet_comment.go index 0e9e263d..f68c3088 100644 --- a/internal/helpers/sheet_comment.go +++ b/internal/helpers/sheet_comment.go @@ -1,7 +1,7 @@ package helpers import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -44,7 +44,7 @@ func newSheetCommentCmd() *cobra.Command { }, } DeclareLeafMetadata(commentListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -95,7 +95,7 @@ func newSheetCommentCmd() *cobra.Command { }, } DeclareLeafMetadata(commentCreateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "low", Confirmation: "not_required", Idempotency: "non_idempotent", }, @@ -147,7 +147,7 @@ func newSheetCommentCmd() *cobra.Command { }, } DeclareLeafMetadata(commentReplyCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "low", Confirmation: "not_required", Idempotency: "non_idempotent", }, @@ -191,7 +191,7 @@ func newSheetCommentCmd() *cobra.Command { }, } DeclareLeafMetadata(commentUpdateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -233,7 +233,7 @@ func newSheetCommentCmd() *cobra.Command { }, } DeclareLeafMetadata(commentDeleteCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "non_idempotent", }, diff --git a/internal/helpers/sheet_cond_format.go b/internal/helpers/sheet_cond_format.go index 7d7a4f57..0c577be2 100644 --- a/internal/helpers/sheet_cond_format.go +++ b/internal/helpers/sheet_cond_format.go @@ -3,8 +3,8 @@ package helpers import ( "encoding/json" "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -39,7 +39,7 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 }, } DeclareLeafMetadata(condFormatListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -180,7 +180,7 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 }, } DeclareLeafMetadata(condFormatCreateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -285,7 +285,7 @@ ruleId 可通过 cond-format list 获取。`, }, } DeclareLeafMetadata(condFormatUpdateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -336,7 +336,7 @@ ruleId 可通过 cond-format list 获取。`, }, } DeclareLeafMetadata(condFormatDeleteCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/helpers/sheet_data.go b/internal/helpers/sheet_data.go index 281d4e02..18e2cebf 100644 --- a/internal/helpers/sheet_data.go +++ b/internal/helpers/sheet_data.go @@ -3,11 +3,11 @@ package helpers import ( "encoding/json" "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "io" "os" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -76,7 +76,7 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 }, } DeclareLeafMetadata(findCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -158,7 +158,7 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 }, } DeclareLeafMetadata(replaceCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -218,7 +218,7 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 }, } DeclareLeafMetadata(appendCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -296,7 +296,7 @@ range update 与合并区域冲突时返回 MERGED_CELLS_CONFLICT 的行为。 }, } DeclareLeafMetadata(csvPutCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -369,7 +369,7 @@ dws sheet info --node NODE_ID --sheet-id SHEET_ID --format json,并读取 merg }, } DeclareLeafMetadata(csvGetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/helpers/sheet_dimension.go b/internal/helpers/sheet_dimension.go index 5ba97496..84bd85a9 100644 --- a/internal/helpers/sheet_dimension.go +++ b/internal/helpers/sheet_dimension.go @@ -3,9 +3,9 @@ package helpers import ( "encoding/json" "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -60,7 +60,7 @@ func newDimensionCmds() []*cobra.Command { }, } DeclareLeafMetadata(insertDimensionCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -144,7 +144,7 @@ func newDimensionCmds() []*cobra.Command { }, } DeclareLeafMetadata(moveDimensionCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -206,7 +206,7 @@ func newDimensionCmds() []*cobra.Command { }, } DeclareLeafMetadata(addDimensionCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -271,7 +271,7 @@ rangeAddress 也支持带工作表前缀的写法,如 Sheet1!A1:B3,此时将 }, } DeclareLeafMetadata(mergeCellsCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -318,7 +318,7 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 }, } DeclareLeafMetadata(unmergeRangeCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -388,7 +388,7 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 }, } DeclareLeafMetadata(deleteDimensionCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -483,7 +483,7 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 }, } DeclareLeafMetadata(updateDimensionCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -551,7 +551,7 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 }, } DeclareLeafMetadata(groupDimensionCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -602,7 +602,7 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 }, } DeclareLeafMetadata(ungroupDimensionCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -682,7 +682,7 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 }, } DeclareLeafMetadata(setDropdownCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -734,7 +734,7 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 }, } DeclareLeafMetadata(getDropdownCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -780,7 +780,7 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 }, } DeclareLeafMetadata(deleteDropdownCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/helpers/sheet_filter_view.go b/internal/helpers/sheet_filter_view.go index 29169aa1..3da6146c 100644 --- a/internal/helpers/sheet_filter_view.go +++ b/internal/helpers/sheet_filter_view.go @@ -4,8 +4,8 @@ import ( "context" "encoding/json" "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -183,7 +183,7 @@ func newFilterCmd() *cobra.Command { }, } DeclareLeafMetadata(filterGetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -239,7 +239,7 @@ func newFilterCmd() *cobra.Command { }, } DeclareLeafMetadata(filterCreateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -278,7 +278,7 @@ func newFilterCmd() *cobra.Command { }, } DeclareLeafMetadata(filterDeleteCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -333,7 +333,7 @@ func newFilterCmd() *cobra.Command { }, } DeclareLeafMetadata(filterUpdateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -378,7 +378,7 @@ func newFilterCmd() *cobra.Command { }, } DeclareLeafMetadata(filterClearCriteriaCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -426,7 +426,7 @@ func newFilterCmd() *cobra.Command { }, } DeclareLeafMetadata(filterSortCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -484,7 +484,7 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 }, } DeclareLeafMetadata(filterViewListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -555,7 +555,7 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 }, } DeclareLeafMetadata(filterViewCreateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -639,7 +639,7 @@ filterViewId 可通过 filter-view list 获取。 }, } DeclareLeafMetadata(filterViewUpdateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -691,7 +691,7 @@ filterViewId 可通过 filter-view list 获取。`, }, } DeclareLeafMetadata(filterViewDeleteCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -795,7 +795,7 @@ condition 类型支持的 operator(必须使用 kebab-case 格式): }, } DeclareLeafMetadata(filterViewSetCriteriaCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -859,7 +859,7 @@ filterViewId 可通过 filter-view list 获取。 }, } DeclareLeafMetadata(filterViewClearCriteriaCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -900,7 +900,7 @@ filterViewId 可通过 filter-view list 获取。`, RunE: runFilterViewInfo, } DeclareLeafMetadata(filterViewInfoCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -943,7 +943,7 @@ filterViewId 可通过 filter-view list 获取。`, RunE: runFilterViewListCriteria, } DeclareLeafMetadata(filterViewListCriteriaCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -992,7 +992,7 @@ filterViewId 可通过 filter-view list 获取。`, RunE: runFilterViewGetCriteria, } DeclareLeafMetadata(filterViewGetCriteriaCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/helpers/sheet_float_image.go b/internal/helpers/sheet_float_image.go index a015ae07..a6b1c1e6 100644 --- a/internal/helpers/sheet_float_image.go +++ b/internal/helpers/sheet_float_image.go @@ -2,8 +2,8 @@ package helpers import ( "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -77,7 +77,7 @@ func newFloatImageCmds() []*cobra.Command { }, } DeclareLeafMetadata(createFloatImageCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -121,7 +121,7 @@ floatImageId 可通过 list-float-images 获取。`, }, } DeclareLeafMetadata(getFloatImageCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -158,7 +158,7 @@ floatImageId 可通过 list-float-images 获取。`, }, } DeclareLeafMetadata(listFloatImagesCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -251,7 +251,7 @@ floatImageId 可通过 list-float-images 获取。`, }, } DeclareLeafMetadata(updateFloatImageCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -296,7 +296,7 @@ floatImageId 可通过 list-float-images 获取。`, }, } DeclareLeafMetadata(deleteFloatImageCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/helpers/sheet_formula_verify.go b/internal/helpers/sheet_formula_verify.go index b85323ad..1e4fa3c0 100644 --- a/internal/helpers/sheet_formula_verify.go +++ b/internal/helpers/sheet_formula_verify.go @@ -4,11 +4,11 @@ import ( "context" "encoding/json" "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "io" "os" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -62,7 +62,7 @@ func newSheetFormulaVerifyCmd() *cobra.Command { cmd.Flags().Int("max-cells", 0, "最多扫描的单元格数") cmd.Flags().Bool("exit-on-error", false, "发现公式错误时返回非 0 退出码,便于 CI/自动化使用") DeclareLeafMetadata(cmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/helpers/sheet_import.go b/internal/helpers/sheet_import.go index 46291af0..0f6f6649 100644 --- a/internal/helpers/sheet_import.go +++ b/internal/helpers/sheet_import.go @@ -16,7 +16,7 @@ package helpers import ( "github.com/spf13/cobra" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" ) const sheetImportLong = `将本地表格文件导入为一个新的钉钉在线电子表格,与 dws sheet export(导出)对称。 @@ -74,7 +74,7 @@ func newSheetImportCmdWithConfig(cfg importFlowConfig) *cobra.Command { }, } DeclareLeafMetadata(importCreateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "non_idempotent", }, @@ -117,7 +117,7 @@ func newSheetImportCmdWithConfig(cfg importFlowConfig) *cobra.Command { }, } DeclareLeafMetadata(importGetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/helpers/sheet_media.go b/internal/helpers/sheet_media.go index 4ec6ccbc..4b69f9f4 100644 --- a/internal/helpers/sheet_media.go +++ b/internal/helpers/sheet_media.go @@ -4,10 +4,10 @@ import ( "context" "encoding/json" "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "os" "path/filepath" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -287,7 +287,7 @@ func newMediaCmds() []*cobra.Command { RunE: runSheetMediaUpload, } DeclareLeafMetadata(mediaUploadCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -339,7 +339,7 @@ func newMediaCmds() []*cobra.Command { RunE: runSheetWriteImage, } DeclareLeafMetadata(writeImageCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, diff --git a/internal/helpers/sheet_pivot_table.go b/internal/helpers/sheet_pivot_table.go index 0d939405..d9ceb36c 100644 --- a/internal/helpers/sheet_pivot_table.go +++ b/internal/helpers/sheet_pivot_table.go @@ -16,13 +16,13 @@ package helpers import ( "encoding/json" "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "os" "strings" "github.com/spf13/cobra" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" ) var validPivotSummarizeBy = map[string]bool{ @@ -161,7 +161,7 @@ func newPivotTableCmd() *cobra.Command { }, } DeclareLeafMetadata(listCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -216,7 +216,7 @@ func newPivotTableCmd() *cobra.Command { }, } DeclareLeafMetadata(createCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "non_idempotent", }, @@ -265,7 +265,7 @@ func newPivotTableCmd() *cobra.Command { }, } DeclareLeafMetadata(updateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -308,7 +308,7 @@ func newPivotTableCmd() *cobra.Command { }, } DeclareLeafMetadata(deleteCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/helpers/sheet_range_ops.go b/internal/helpers/sheet_range_ops.go index 0637115e..b639a8af 100644 --- a/internal/helpers/sheet_range_ops.go +++ b/internal/helpers/sheet_range_ops.go @@ -3,8 +3,8 @@ package helpers import ( "encoding/json" "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -71,7 +71,7 @@ dws sheet info --node NODE_ID --sheet-id SHEET_ID --format json,并读取 merg }, } DeclareLeafMetadata(rangeReadCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -228,7 +228,7 @@ dws sheet info --node NODE_ID --sheet-id SHEET_ID --format json,并读取 merg }, } DeclareLeafMetadata(rangeUpdateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -286,7 +286,7 @@ dws sheet info --node NODE_ID --sheet-id SHEET_ID --format json,并读取 merg }, } DeclareLeafMetadata(rangeClearCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -348,7 +348,7 @@ column 使用字母列名(如 "A"、"B"、"AA"),表示排序的目标列 }, } DeclareLeafMetadata(rangeSortCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -405,7 +405,7 @@ column 使用字母列名(如 "A"、"B"、"AA"),表示排序的目标列 }, } DeclareLeafMetadata(rangeFillCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -466,7 +466,7 @@ column 使用字母列名(如 "A"、"B"、"AA"),表示排序的目标列 }, } DeclareLeafMetadata(rangeCopyCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -525,7 +525,7 @@ column 使用字母列名(如 "A"、"B"、"AA"),表示排序的目标列 }, } DeclareLeafMetadata(rangeMoveCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/helpers/sheet_style.go b/internal/helpers/sheet_style.go index b261f249..5f08db5c 100644 --- a/internal/helpers/sheet_style.go +++ b/internal/helpers/sheet_style.go @@ -4,10 +4,10 @@ import ( "context" "encoding/json" "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "os" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -358,7 +358,7 @@ func newRangeSetStyleCmd() *cobra.Command { cmd.Flags().String("range", "", "目标单元格区域地址,如 A1:B3 (必填)") bindStyleFlags(cmd) DeclareLeafMetadata(cmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -564,7 +564,7 @@ func newRangeBatchSetStyleCmd() *cobra.Command { cmd.Flags().String("batch", "", "批次配置 JSON 文件路径 (必填)") cmd.Flags().Bool("continue-on-error", false, "遇到失败时继续执行后续条目(默认遇错即停)") DeclareLeafMetadata(cmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, diff --git a/internal/helpers/sheet_table.go b/internal/helpers/sheet_table.go index bb354a29..b98f4734 100644 --- a/internal/helpers/sheet_table.go +++ b/internal/helpers/sheet_table.go @@ -16,7 +16,6 @@ package helpers import ( "encoding/json" "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "io" "os" "strings" @@ -24,6 +23,7 @@ import ( "github.com/spf13/cobra" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" ) func newTableCmds() []*cobra.Command { @@ -59,7 +59,7 @@ func newTableCmds() []*cobra.Command { }, } DeclareLeafMetadata(tableGetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -124,7 +124,7 @@ func newTableCmds() []*cobra.Command { }, } DeclareLeafMetadata(tablePutCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, diff --git a/internal/helpers/sheet_template.go b/internal/helpers/sheet_template.go index 36eaee2b..d00bc1c2 100644 --- a/internal/helpers/sheet_template.go +++ b/internal/helpers/sheet_template.go @@ -2,8 +2,8 @@ package helpers import ( "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -38,7 +38,7 @@ func newSheetTemplateCmd() *cobra.Command { }, } DeclareLeafMetadata(templateListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -91,7 +91,7 @@ func newSheetTemplateCmd() *cobra.Command { }, } DeclareLeafMetadata(templateSearchCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -145,7 +145,7 @@ func newSheetTemplateCmd() *cobra.Command { }, } DeclareLeafMetadata(templateApplyCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, diff --git a/internal/helpers/sheet_version.go b/internal/helpers/sheet_version.go index ce0708d3..b472b03e 100644 --- a/internal/helpers/sheet_version.go +++ b/internal/helpers/sheet_version.go @@ -2,8 +2,8 @@ package helpers import ( "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -30,7 +30,7 @@ func newSheetVersionCmd() *cobra.Command { }, } DeclareLeafMetadata(versionSaveCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "low", Confirmation: "not_required", Idempotency: "non_idempotent", }, @@ -72,7 +72,7 @@ func newSheetVersionCmd() *cobra.Command { }, } DeclareLeafMetadata(versionListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -114,7 +114,7 @@ func newSheetVersionCmd() *cobra.Command { }, } DeclareLeafMetadata(versionRevertCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "non_idempotent", }, diff --git a/internal/helpers/sheet_workbook.go b/internal/helpers/sheet_workbook.go index f22e6b89..220f2e14 100644 --- a/internal/helpers/sheet_workbook.go +++ b/internal/helpers/sheet_workbook.go @@ -3,8 +3,8 @@ package helpers import ( "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -32,7 +32,7 @@ func newWorkbookCmds() []*cobra.Command { }, } DeclareLeafMetadata(createCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -52,7 +52,6 @@ func newWorkbookCmds() []*cobra.Command { }, }, // name is validated via mustGetFlag; publish required via ParamDecl - // instead of relying on RegisterSchemaHints tool_schema_hint. Parameters: []corecmd.ParamDecl{ {Name: "name", Required: boolPtr(true)}, }, @@ -79,7 +78,7 @@ nodeId 支持传入文档链接 URL 或文档 ID(dentryUuid),系统自动 }, } DeclareLeafMetadata(listCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -96,7 +95,6 @@ nodeId 支持传入文档链接 URL 或文档 ID(dentryUuid),系统自动 Examples: []string{"dws sheet list --node "}, }, // node is validated via mustGetFlag; publish required via ParamDecl - // instead of relying on RegisterSchemaHints tool_schema_hint. Parameters: []corecmd.ParamDecl{ {Name: "node", Required: boolPtr(true)}, }, @@ -138,7 +136,7 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 }, } DeclareLeafMetadata(infoCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -175,7 +173,7 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 }, } DeclareLeafMetadata(newCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -192,7 +190,6 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 Examples: []string{"dws sheet new --node --name \"Sheet2\""}, }, // node/name validated via mustGetFlag; publish required via ParamDecl - // instead of relying on RegisterSchemaHints tool_schema_hint. Parameters: []corecmd.ParamDecl{ {Name: "node", Required: boolPtr(true)}, {Name: "name", Required: boolPtr(true)}, @@ -289,7 +286,7 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 }, } DeclareLeafMetadata(updateSheetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -358,7 +355,7 @@ name 不能包含 / \ ? * [ ] : 等特殊字符,最长 100 字符。`, }, } DeclareLeafMetadata(copySheetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -399,7 +396,7 @@ name 不能包含 / \ ? * [ ] : 等特殊字符,最长 100 字符。`, }, } DeclareLeafMetadata(deleteSheetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -440,7 +437,7 @@ name 不能包含 / \ ? * [ ] : 等特殊字符,最长 100 字符。`, }, } DeclareLeafMetadata(showGridlineCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -481,7 +478,7 @@ name 不能包含 / \ ? * [ ] : 等特殊字符,最长 100 字符。`, }, } DeclareLeafMetadata(hideGridlineCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/helpers/todo.go b/internal/helpers/todo.go index 2bdd1925..8f32a895 100644 --- a/internal/helpers/todo.go +++ b/internal/helpers/todo.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "net/url" "os" "path/filepath" @@ -17,6 +16,7 @@ import ( "github.com/spf13/cobra" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" ) // ────────────────────────────────────────────────────────── @@ -58,9 +58,9 @@ func ensureTodoTaskExists(ctx context.Context, taskID string) error { func newTodoCommand() *cobra.Command { // Product-level Agent routing Decl (migrated from selection/todo.json // products.todo). Catalog assembly stamps provenance contract_final. - cli.RegisterProductDecl(cli.ProductDecl{ + contract.RegisterProductDecl(contract.ProductDecl{ ID: "todo", - Selection: cli.ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "管理待办任务、标签、子任务、执行人、参与人、评论、附件与提醒", UseWhen: []string{ "查询、创建或更新个人待办及其协作信息时", @@ -123,7 +123,7 @@ func newTodoCommand() *cobra.Command { }, } DeclareLeafMetadata(todoTaskCreateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -203,7 +203,7 @@ func newTodoCommand() *cobra.Command { }, } DeclareLeafMetadata(todoTaskCreateSubCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -260,7 +260,7 @@ func newTodoCommand() *cobra.Command { }, } DeclareLeafMetadata(todoTaskListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -347,7 +347,7 @@ func newTodoCommand() *cobra.Command { }, } DeclareLeafMetadata(todoTaskUpdateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -396,7 +396,7 @@ func newTodoCommand() *cobra.Command { }, } DeclareLeafMetadata(todoTaskDoneCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -468,7 +468,7 @@ reset-reminder 写入的提醒规则。提醒写命令的成功响应只能作 }, } DeclareLeafMetadata(todoTaskGetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -513,7 +513,7 @@ reset-reminder 写入的提醒规则。提醒写命令的成功响应只能作 }, } DeclareLeafMetadata(todoTaskDeleteCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, @@ -556,7 +556,7 @@ reset-reminder 写入的提醒规则。提醒写命令的成功响应只能作 }, } DeclareLeafMetadata(todoTaskAddExecutorCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -602,7 +602,7 @@ reset-reminder 写入的提醒规则。提醒写命令的成功响应只能作 }, } DeclareLeafMetadata(todoTaskRemoveExecutorCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -647,7 +647,7 @@ reset-reminder 写入的提醒规则。提醒写命令的成功响应只能作 }, } DeclareLeafMetadata(todoTaskAddParticipantCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -693,7 +693,7 @@ reset-reminder 写入的提醒规则。提醒写命令的成功响应只能作 }, } DeclareLeafMetadata(todoTaskRemoveParticipantCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -768,7 +768,7 @@ reset-reminder 写入的提醒规则。提醒写命令的成功响应只能作 }, } DeclareLeafMetadata(todoTaskAddReminderCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -828,7 +828,7 @@ reset-reminder 写入的提醒规则。提醒写命令的成功响应只能作 }, } DeclareLeafMetadata(todoTaskUpdateReminderCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -910,7 +910,7 @@ reset-reminder 写入的提醒规则。提醒写命令的成功响应只能作 }, } DeclareLeafMetadata(todoTaskAddAttachment, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -959,7 +959,7 @@ reset-reminder 写入的提醒规则。提醒写命令的成功响应只能作 }, } DeclareLeafMetadata(todoTaskListAttachmentCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1120,7 +1120,7 @@ reset-reminder 写入的提醒规则。提醒写命令的成功响应只能作 }, } DeclareLeafMetadata(todoCommentAddCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1164,7 +1164,7 @@ reset-reminder 写入的提醒规则。提醒写命令的成功响应只能作 }, } DeclareLeafMetadata(todoCommentListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1208,7 +1208,7 @@ reset-reminder 写入的提醒规则。提醒写命令的成功响应只能作 }, } DeclareLeafMetadata(todoCommentDeleteCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, @@ -1270,7 +1270,7 @@ reset-reminder 写入的提醒规则。提醒写命令的成功响应只能作 }, } DeclareLeafMetadata(todoTagAddCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1315,7 +1315,7 @@ reset-reminder 写入的提醒规则。提醒写命令的成功响应只能作 }, } DeclareLeafMetadata(todoTagDeleteCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, @@ -1366,7 +1366,7 @@ reset-reminder 写入的提醒规则。提醒写命令的成功响应只能作 }, } DeclareLeafMetadata(todoTagUpdateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1397,7 +1397,7 @@ reset-reminder 写入的提醒规则。提醒写命令的成功响应只能作 }, } DeclareLeafMetadata(todoTagListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1436,7 +1436,7 @@ reset-reminder 写入的提醒规则。提醒写命令的成功响应只能作 }, } DeclareLeafMetadata(todoTagCreateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "non_idempotent", }, diff --git a/internal/helpers/wiki.go b/internal/helpers/wiki.go index 3eb99e15..85e89d3e 100644 --- a/internal/helpers/wiki.go +++ b/internal/helpers/wiki.go @@ -6,8 +6,8 @@ import ( "strconv" "strings" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -145,9 +145,9 @@ func proxySubCmd(use, targetProduct, targetPath string, flagRenames map[string]s func newWikiCommand() *cobra.Command { // Product-level Agent routing Decl (migrated from selection/wiki.json // products.wiki). Catalog assembly stamps provenance contract_final. - cli.RegisterProductDecl(cli.ProductDecl{ + contract.RegisterProductDecl(contract.ProductDecl{ ID: "wiki", - Selection: cli.ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "管理钉钉知识库空间、节点与成员权限", UseWhen: []string{ "查找或管理知识库、知识库内节点及知识库成员时", @@ -192,7 +192,7 @@ func newWikiCommand() *cobra.Command { }, } DeclareLeafMetadata(spaceCreateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -212,7 +212,6 @@ func newWikiCommand() *cobra.Command { }, }, // name is validated in RunE; publish required via ParamDecl - // instead of relying on RegisterSchemaHints tool_schema_hint. Parameters: []corecmd.ParamDecl{ {Name: "name", Required: boolPtr(true)}, }, @@ -239,7 +238,7 @@ func newWikiCommand() *cobra.Command { }, } DeclareLeafMetadata(spaceGetCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -256,7 +255,6 @@ func newWikiCommand() *cobra.Command { Examples: []string{"dws wiki space get --workspace --format json"}, }, // workspace is validated in RunE; publish required via ParamDecl - // (former RegisterSchemaHints keyed stale "space"). Parameters: []corecmd.ParamDecl{ {Name: "workspace", Required: boolPtr(true)}, }, @@ -312,7 +310,7 @@ func newWikiCommand() *cobra.Command { }, } DeclareLeafMetadata(spaceListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -371,7 +369,7 @@ func newWikiCommand() *cobra.Command { }, } DeclareLeafMetadata(spaceSearchCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -441,7 +439,7 @@ func newWikiCommand() *cobra.Command { }, } DeclareLeafMetadata(spaceDeleteCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, @@ -521,7 +519,7 @@ func newWikiCommand() *cobra.Command { }, } DeclareLeafMetadata(memberAddCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -591,7 +589,7 @@ func newWikiCommand() *cobra.Command { }, } DeclareLeafMetadata(memberUpdateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -652,7 +650,7 @@ ORG 类型授权不会出现在查询结果中。`, }, } DeclareLeafMetadata(memberListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -709,7 +707,7 @@ ORG 类型授权不会出现在查询结果中。`, }, } DeclareLeafMetadata(memberRemoveCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -792,7 +790,7 @@ ORG 类型授权不会出现在查询结果中。`, }, } DeclareLeafMetadata(nodeListCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -864,7 +862,7 @@ ORG 类型授权不会出现在查询结果中。`, }, } DeclareLeafMetadata(nodeCreateCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -926,7 +924,7 @@ ORG 类型授权不会出现在查询结果中。`, }, } DeclareLeafMetadata(nodeCopyCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -985,7 +983,7 @@ ORG 类型授权不会出现在查询结果中。`, }, } DeclareLeafMetadata(nodeMoveCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, @@ -1032,7 +1030,7 @@ ORG 类型授权不会出现在查询结果中。`, }, } DeclareLeafMetadata(nodeDeleteCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, @@ -1092,7 +1090,7 @@ ORG 类型授权不会出现在查询结果中。`, }, } DeclareLeafMetadata(nodeSearchCmd, LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/pat/browser_policy.go b/internal/pat/browser_policy.go index eb957b5d..5768333d 100644 --- a/internal/pat/browser_policy.go +++ b/internal/pat/browser_policy.go @@ -16,13 +16,13 @@ package pat import ( "encoding/json" "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "os" "path/filepath" "strings" "github.com/spf13/cobra" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition" @@ -227,7 +227,7 @@ func newBrowserPolicyCommand() *cobra.Command { cmd.Flags().Bool("enabled", false, "PAT 撞墙时是否允许本地打开浏览器") cmd.Flags().String("agentCode", "", "Agent 唯一标识(可选;不填则写入全局默认策略,不从 env DINGTALK_DWS_AGENTCODE 回退)") helpers.DeclareLeafMetadata(cmd, helpers.LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, diff --git a/internal/pat/chmod.go b/internal/pat/chmod.go index 03ea3f24..a91cb646 100644 --- a/internal/pat/chmod.go +++ b/internal/pat/chmod.go @@ -30,6 +30,7 @@ import ( authpkg "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/auth" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition" @@ -413,7 +414,7 @@ agentCode 配置: cli.AnnotateRuntimeConstraints(chmodCmd, cli.RuntimeSchemaConstraints{ RequireOneOf: [][]string{{"scope", "product", "products", "domain", "domains", "recommend"}}, }) - cli.AnnotateRuntimePositionals(chmodCmd, cli.RuntimeSchemaPositional{ + cli.AnnotateRuntimePositionals(chmodCmd, contract.RuntimeSchemaPositional{ Name: "scope", Type: "array", Description: "权限 scope,格式为 .:;可重复", @@ -423,7 +424,7 @@ agentCode 配置: }) helpers.DeclareLeafMetadata(chmodCmd, helpers.LeafSpec{ - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, @@ -465,7 +466,6 @@ agentCode 配置: Examples: []string{"dws pat chmod --products calendar,aitable --grant-type session --session-id --dry-run --format json"}, }, // session-id is conditionally required in Validate; publish via - // ParamDecl instead of relying on RegisterSchemaHints tool_schema_hint. Parameters: []corecmd.ParamDecl{ { Name: "session-id", diff --git a/internal/pat/pat.go b/internal/pat/pat.go index 19e8cfd5..d4a17c36 100644 --- a/internal/pat/pat.go +++ b/internal/pat/pat.go @@ -18,7 +18,7 @@ package pat import ( "github.com/spf13/cobra" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/cmdutil" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition" ) @@ -27,9 +27,9 @@ import ( func RegisterCommands(root *cobra.Command, c edition.ToolCaller) { // Product-level Agent routing Decl (migrated from selection/pat.json // products.pat). Catalog assembly stamps provenance contract_final. - cli.RegisterProductDecl(cli.ProductDecl{ + contract.RegisterProductDecl(contract.ProductDecl{ ID: "pat", - Selection: cli.ProductSelectionDecl{ + Selection: contract.ProductSelectionDecl{ AgentSummary: "管理 Agent 的 PAT 行为授权与本地浏览器策略", UseWhen: []string{ "缺少行为授权需要预览/授予 scope,或配置授权是否打开浏览器", diff --git a/internal/shortcut/adapter.go b/internal/shortcut/adapter.go index 21fc4d46..4e08f930 100644 --- a/internal/shortcut/adapter.go +++ b/internal/shortcut/adapter.go @@ -17,8 +17,8 @@ import ( "fmt" "strings" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/spf13/cobra" ) @@ -65,7 +65,7 @@ func FromShortcut(s Shortcut) corecmd.Spec { } } -func safetySpecDeclared(safety cli.SafetySpec) bool { +func safetySpecDeclared(safety contract.SafetySpec) bool { return strings.TrimSpace(safety.Effect) != "" || strings.TrimSpace(safety.Risk) != "" || strings.TrimSpace(safety.Confirmation) != "" || @@ -91,20 +91,20 @@ func fromShortcutValidate(s Shortcut) func(*cobra.Command, []string) error { // shortcutSafetySpec is the temporary compatibility boundary while the live // Shortcut framework still owns its legacy Risk enum. command and Leaf do not // retain that enum: the adapter expands it once into the existing Schema model. -func shortcutSafetySpec(risk Risk) cli.SafetySpec { +func shortcutSafetySpec(risk Risk) contract.SafetySpec { switch risk { case RiskWrite: - return cli.SafetySpec{ + return contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", } case RiskHighWrite: - return cli.SafetySpec{ + return contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", } default: - return cli.SafetySpec{ + return contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", } diff --git a/internal/shortcut/aitable/aitable.go b/internal/shortcut/aitable/aitable.go index b98ba8ef..f7f423e8 100644 --- a/internal/shortcut/aitable/aitable.go +++ b/internal/shortcut/aitable/aitable.go @@ -24,9 +24,9 @@ package aitable import ( "encoding/json" "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -70,7 +70,7 @@ var BaseList = shortcut.Shortcut{ Description: "获取当前用户可访问的 AI 表格 Base 列表(最近访问,支持游标分页)", Intent: "当你不知道具体 baseId、想先浏览自己最近用过或可访问的 AI 表格清单以便定位目标时使用;支持游标分页,返回 Base 列表及其 baseId。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -182,7 +182,7 @@ var BaseSearch = shortcut.Shortcut{ Description: "按名称关键词搜索 AI 表格 Base", Intent: "当你知道某个 AI 表格的名字或部分关键词、想直接定位到它并拿到 baseId 时使用;输入名称关键词,返回匹配的 Base 列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -226,7 +226,7 @@ var BaseGet = shortcut.Shortcut{ Description: "获取指定 Base 的目录信息(tables / dashboards summary)", Intent: "当你已有 baseId、需要了解这个表格里有哪些数据表和仪表盘(拿到 tableId/dashboardId)以便进一步操作时使用;返回 Base 的目录结构概要。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -359,7 +359,7 @@ var TableGet = shortcut.Shortcut{ Description: "批量获取指定数据表的表级信息、字段目录与视图目录", Intent: "当你已进入某个 Base、需要了解其中某些数据表有哪些字段(拿 fieldId)、有哪些视图(拿 viewId)以便读写数据时使用;批量返回表信息、字段目录和视图目录。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -466,7 +466,7 @@ var FieldGet = shortcut.Shortcut{ Description: "批量获取字段详情(含类型相关完整配置)", Intent: "当你需要查看字段的完整类型配置(如单选选项、关联表设置、AI 配置)以便正确写入数据或改配置时使用;批量返回字段详情。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -582,7 +582,7 @@ var RecordQuery = shortcut.Shortcut{ Description: "查询表格记录(按 ID 取 / 条件筛选 / 关键词 / 分页)", Intent: "当你要读取表格里的行数据——按 recordId 精确取、按结构化条件筛选、按关键词全文搜索或分页遍历时使用;返回匹配记录及其单元格值。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -708,7 +708,7 @@ var RecordQueryEmpty = shortcut.Shortcut{ Description: "扫描并过滤出完全没填用户字段的空行", Intent: "当你想清理表格、需要先找出那些所有用户字段都为空的空行时使用;扫描并返回空行列表,支持分页预算。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -755,7 +755,7 @@ var RecordHistoryList = shortcut.Shortcut{ Description: "按 recordId 查询单条记录的变更历史", Intent: "当你要追溯某条记录曾被谁在何时改过哪些字段时使用;按 recordId 分页返回该行的变更历史。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -804,7 +804,7 @@ var RecordShareURL = shortcut.Shortcut{ Description: "按 recordId 批量获取记录分享链接,单次最多 20 条", Intent: "当你要把某几条记录以链接形式分享给他人(可带视图上下文)时使用;按 recordId 批量返回分享链接,单次最多 20 条。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -928,7 +928,7 @@ var TemplateSearch = shortcut.Shortcut{ Description: "按名称关键词搜索 AI 表格模板", Intent: "当你要新建表格并想套用现成模板、需要先按关键词找模板(不传关键词则返回热门)时使用;返回模板列表及其模板 ID。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1074,7 +1074,7 @@ var ViewGet = shortcut.Shortcut{ Description: "获取视图完整信息(列顺序、筛选、排序、分组等)", Intent: "当你要了解某个视图当前的列顺序、筛选、排序、分组等完整配置以便复用或修改时使用;批量返回视图详情。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1284,7 +1284,7 @@ var ViewGetLock = shortcut.Shortcut{ Description: "获取视图锁定状态", Intent: "当你想确认某视图是否已被锁定(以防他人误改其配置)时使用;返回该视图的锁定状态。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1353,7 +1353,7 @@ var ViewGetFrozenCols = shortcut.Shortcut{ Description: "获取视图当前冻结的左侧列数", Intent: "当你想知道某视图当前冻结了左侧几列时使用;返回冻结列数。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1418,7 +1418,7 @@ var ViewGetRowHeight = shortcut.Shortcut{ Description: "获取视图单元格行高(像素)", Intent: "当你想知道某视图当前的行高档位时使用;返回单元格行高的像素值。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1517,7 +1517,7 @@ var FormList = shortcut.Shortcut{ Description: "列出指定数据表下的所有表单视图", Intent: "当你要查看某数据表下已有哪些收集表单时使用;返回该表的全部表单视图列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1680,7 +1680,7 @@ var FormFieldList = shortcut.Shortcut{ Description: "列出表单视图当前可见的字段及其配置", Intent: "当你要查看某表单当前放出了哪些字段供填写及其是否必填等配置时使用;返回表单可见字段列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1781,7 +1781,7 @@ var FormShareGet = shortcut.Shortcut{ Description: "读取视图当前的分享表单配置", Intent: "当你要查看某视图的表单分享是否已开启及其分享配置时使用;返回当前的分享表单配置。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2015,7 +2015,7 @@ var DashboardConfigExample = shortcut.Shortcut{ Description: "获取 dashboard config 的结构示例", Intent: "当你准备创建或更新仪表盘、需要先了解 dashboard config 的字段结构长什么样时使用;返回一份配置结构示例供参考。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2046,7 +2046,7 @@ var DashboardGet = shortcut.Shortcut{ Description: "获取指定 dashboard 的详细信息(含 charts summary)", Intent: "当你要查看某仪表盘的配置详情及它包含哪些图表(拿 chartId)时使用;返回 dashboard 信息与 charts 概要。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2227,7 +2227,7 @@ var ChartWidgetsExample = shortcut.Shortcut{ Description: "获取所有图表类型的 widget config 示例", Intent: "当你准备创建或修改图表、需要先参考各类图表 widget config 的示例结构时使用;返回所有图表类型的配置示例。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2258,7 +2258,7 @@ var ChartGet = shortcut.Shortcut{ Description: "获取指定 chart 的详细信息", Intent: "当你要查看某个图表的配置详情(统计维度、样式等)时使用;返回指定 chart 的详细信息。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2592,7 +2592,7 @@ var RoleList = shortcut.Shortcut{ Description: "列出指定 Base 下的全部角色", Intent: "当你要查看某 Base 下配置了哪些角色(拿 roleId)时使用;返回全部角色列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2849,7 +2849,7 @@ var SectionListEmpty = shortcut.Shortcut{ Description: "列出指定 Base 下所有没有子节点的空文件夹", Intent: "当你想清理 Base、需要先找出所有没有任何子节点的空文件夹时使用;返回空文件夹列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -2883,7 +2883,7 @@ var SectionListNodes = shortcut.Shortcut{ Description: "列出指定 Base 当前版本下的全部 nsheet 节点", Intent: "当你要总览某 Base 当前版本下的全部节点(表、仪表盘、文件夹等)目录结构时使用;返回所有 nsheet 节点。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/attendance/attendance.go b/internal/shortcut/attendance/attendance.go index 1a4a624d..fc70ac93 100644 --- a/internal/shortcut/attendance/attendance.go +++ b/internal/shortcut/attendance/attendance.go @@ -26,12 +26,12 @@ package attendance import ( "encoding/json" "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strconv" "strings" "time" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -170,7 +170,7 @@ var CheckResult = shortcut.Shortcut{ Description: "查询用户打卡结果(迟到/早退/缺卡等)", Intent: "当你需要批量统计一批员工在某段时间的打卡结果(正常/迟到/早退/缺卡等判定结论)时使用,例如做月度考勤汇总或异常人员排查;输入多个 userId(最多 100 人)和不超过 1 个月的日期区间,返回每人每天的打卡结果状态,支持分页。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -224,7 +224,7 @@ var CheckRecord = shortcut.Shortcut{ Description: "查询用户打卡流水(打卡时间/地点/定位方式)", Intent: "当你要查看员工每一次实际打卡的原始记录(具体打卡时刻、打卡地点、定位/Wifi/蓝牙等方式)而不是判定结论时使用,例如核实某人是否在指定地点打卡;输入 userId 列表和不超过 1 个月的日期区间,返回逐条打卡流水。与 +check-result 的区别是这里返回明细流水而非迟到早退结论。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -276,7 +276,7 @@ var ListApprove = shortcut.Shortcut{ Description: "查询用户考勤审批单(补卡/加班/请假/出差外出)", Intent: "当你想查看某些员工已提交的考勤类审批单(加班、请假、出差外出、补卡)时使用,例如核对某人这段时间请了几次假或有没有补卡审批;输入 userId 列表、审批类型(overtime/加班、leave/请假、trip/出差外出、patch/补卡)和日期区间,返回匹配的审批单记录。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -340,7 +340,7 @@ var GetApproveTemplate = shortcut.Shortcut{ Description: "查询补卡/请假/加班/外出/出差审批提交链接", Intent: "当用户想自己发起一条考勤审批(补卡、请假、加班、外出、出差)、需要拿到对应审批表单的提交入口链接时使用;输入审批类型(如 leave/请假),返回可直接打开填写并提交的审批链接。本命令只返回链接、不代替用户提交审批。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -391,7 +391,7 @@ var GetSchedule = shortcut.Shortcut{ Description: "获取指定用户一段时间内的排班记录", Intent: "当你要查看排班制考勤组下员工在某段时间的排班记录(含排班 id、班次、是否休息)时使用,尤其是需要拿到排班 id 用于后续 BOSS 改签打卡(+boss-check)的场景;输入 userId 列表和起止时间,返回逐日排班明细。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -491,7 +491,7 @@ var SearchClass = shortcut.Shortcut{ Description: "查询当前用户可管理的班次详情列表", Intent: "当你要浏览或按名称查找当前用户能管理的班次、以便拿到班次 ID 用于建组、排班或改班次时使用;可选按班次名关键字模糊搜索、按 ALL/我负责的过滤并分页,返回班次列表及详情。要看某个具体班次的完整配置用 +get-class。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -721,7 +721,7 @@ var GetAdjustmentRule = shortcut.Shortcut{ Description: "根据补卡规则主键 ID 查询补卡规则详情", Intent: "当你已知某条补卡规则的主键 ID、想查看它的具体规则内容(每月可补卡次数、时限、适用范围等)时使用;输入 adjustmentId,返回该补卡规则的完整详情。不知道 ID 时先用 +search-adjustment-rule 列出。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -757,7 +757,7 @@ var SearchAdjustmentRule = shortcut.Shortcut{ Description: "查询当前用户可管理的补卡规则列表", Intent: "当你要浏览或按名称查找当前用户可管理的补卡规则、以便拿到规则 ID 做进一步查看时使用;可选按规则名关键字模糊搜索并分页,返回补卡规则列表。要看某条规则的完整内容用 +get-adjustment-rule。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -851,7 +851,7 @@ var GetOvertimeRule = shortcut.Shortcut{ Description: "根据加班规则主键 ID 查询加班规则详情", Intent: "当你已知某条加班规则的主键 ID、想查看它的具体内容(工作日/休息日/节假日加班计算方式、适用范围等)时使用;输入 overtimeId,返回该加班规则的完整详情。不知道 ID 时先用 +search-overtime-rule 列出。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -887,7 +887,7 @@ var SearchOvertimeRule = shortcut.Shortcut{ Description: "查询当前用户可管理的加班规则列表", Intent: "当你要浏览或按名称查找当前用户可管理的加班规则、以便拿到规则 ID 做进一步查看时使用;可选按规则名关键字模糊搜索并分页,返回加班规则列表。要看某条规则的完整内容用 +get-overtime-rule。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -946,7 +946,7 @@ var SearchGroup = shortcut.Shortcut{ Description: "查询当前用户可管理的考勤组列表", Intent: "当你要浏览或按名称查找当前用户可管理的考勤组、以便拿到考勤组 ID 用于查看详情、改成员或改配置时使用;可选按名称关键字、考勤组类型(固定班制/排班制/自由工时)过滤,并可选带出定位/Wifi/蓝牙信息,分页返回考勤组简要列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1254,7 +1254,7 @@ var GetSummary = shortcut.Shortcut{ Description: "查询某个人的考勤统计摘要(周/月)", Intent: "当你想快速了解某个人一周或一月的考勤汇总(出勤天数、迟到早退次数、加班、请假时长等统计口径)而不是逐天明细时使用;输入 userId、所在周期内的任意日期和统计类型(week/month),返回该周期的考勤统计摘要。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1303,7 +1303,7 @@ var GetSelfSetting = shortcut.Shortcut{ Description: "查询个人规则设置(打卡提醒/极速打卡/缺卡提醒等)", Intent: "当你想查看某个用户在指定场景下的个人考勤规则开关配置(如打卡提醒、极速打卡、缺卡提醒、考勤结果通知等是否开启)时使用;输入设置场景 settingScene 和用户 userId,返回该用户对应场景的个人设置。查企业全局设置用 +get-global-setting。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1383,7 +1383,7 @@ var QueryReportData = shortcut.Shortcut{ Description: "根据字段查询考勤报表数据(仅管理员)", Intent: "当你要按指定考勤字段导出/查询一批员工在某时间段的报表数值(如各人的出勤天数、迟到次数、加班时长)时使用,常用于做考勤汇总或核算;输入 userId 列表(最多 20 人)、字段 ID 列表(来自 +list-report-columns)和不超过 32 天的时间区间,返回对应字段的数据。仅管理员可用。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1484,7 +1484,7 @@ var ListLeaveTypes = shortcut.Shortcut{ Description: "查询当前用户可用的假期规则列表", Intent: "当你想知道企业有哪些假期类型(年假、事假、病假等)及其对应的假期编码 code、单位、是否带薪时使用,通常是查余额或改假期规则前先拿到 leaveCode;无需参数,返回当前用户可用的假期规则列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1543,7 +1543,7 @@ var GetLeaveRecords = shortcut.Shortcut{ Description: "查询指定员工的假期余额变更记录", Intent: "当你想追溯某个员工假期额度的变动流水(何时发放、扣减、因请假消耗多少)以核对余额来龙去脉时使用;输入单个 userId、日期区间,可选假期 code(不传查所有假期),返回该员工在该时间段的余额变更记录。只想看当前余额用 +get-leave-balance。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1718,7 +1718,7 @@ var GetCheckinRecord = shortcut.Shortcut{ Description: "查询指定员工一段时间内的签到记录", Intent: "当你要查看员工的外勤/移动办公签到记录(signin/checkin,区别于考勤打卡)时使用,例如核实业务员的拜访签到轨迹;需提供操作者企业 ID 与员工 ID、目标员工 ID 列表(最多 100 人)以及不超过 7 天的时间区间,返回这些人的签到明细。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/calendar/calendar.go b/internal/shortcut/calendar/calendar.go index 4f9651d1..5b636ab1 100644 --- a/internal/shortcut/calendar/calendar.go +++ b/internal/shortcut/calendar/calendar.go @@ -19,11 +19,11 @@ package calendar import ( "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strings" "time" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -48,7 +48,7 @@ var EventList = shortcut.Shortcut{ Description: "查询日程列表(不传时间默认查询今天)", Intent: "当你想了解某人(默认自己)在某段时间内的日程安排、看看今天/本周有哪些会时使用;可传 --start/--end 圈定时间范围、--calendar-id 指定日历,返回该区间内的日程列表(含日程 ID,可配合 +get 看详情)。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -209,7 +209,7 @@ var AttendeeList = shortcut.Shortcut{ Description: "查看日程参会人", Intent: "当你想知道某个日程都有谁参加、各人的出席响应状态时使用;输入 --event 日程 ID,返回参会人列表(userId 及其接受/拒绝等状态)。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -323,7 +323,7 @@ var RoomSearch = shortcut.Shortcut{ Description: "按名称模糊搜索会议室(不检查可用性)", Intent: "当你只知道会议室名字、想拿到它的 roomId 以便后续预定时使用;输入 --room-name 名称关键词(建议只填核心专名,去掉“会议室”等后缀),返回名称匹配的会议室列表。它只按名字找、不判断该时段是否空闲,查可用性请用 +room-find。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -495,7 +495,7 @@ var RoomGroups = shortcut.Shortcut{ Description: "会议室分组列表", Intent: "当你想按楼层/园区等分组浏览会议室、或需要拿到 groupId 以便在 +room-find 里按分组过滤时使用;返回会议室分组列表,支持 --limit/--page 分页。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -610,7 +610,7 @@ var BusySearch = shortcut.Shortcut{ Description: "查询用户 / 会议室闲忙状态(--users 与 --rooms 至少其一)", Intent: "当你要在约会前确认某些人或会议室在指定时间段是否有空、避免冲突时使用;传入 --start/--end 时间段并至少给出 --users 或 --rooms 其一,返回各对象在该区间的忙/闲时段。只看忙闲结果、不看具体日程内容,需要系统给出建议时段可用 +suggestion。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -680,7 +680,7 @@ var BookList = shortcut.Shortcut{ Description: "查询用户的日历本列表", Intent: "当你想知道自己有哪些日历本(主日历、项目日历、订阅日历等)、或需要拿到某个日历的 calendarId 以便在 +agenda/+create 中指定时使用;无需参数,返回全部日历本列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -743,7 +743,7 @@ var BookSearch = shortcut.Shortcut{ Description: "按名称模糊搜索日历本", Intent: "当你只记得日历本名字的一部分、想据此找到对应的 calendarId 时使用;输入 --query 名称关键词,返回名称匹配的日历本列表,便于后续在其它命令里指定 --calendar-id。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/chat/chat_bot.go b/internal/shortcut/chat/chat_bot.go index 581ef6dd..b871672f 100644 --- a/internal/shortcut/chat/chat_bot.go +++ b/internal/shortcut/chat/chat_bot.go @@ -14,8 +14,8 @@ package chat import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -27,7 +27,7 @@ var BotSearch = shortcut.Shortcut{ Description: "搜索当前用户自己创建的机器人", Intent: "当你要管理或复用自己创建的机器人(比如查到其 robotCode 以便让它进群或发消息)时使用;按机器人名称模糊搜索,只返回当前用户名下创建的机器人列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -138,7 +138,7 @@ var BotFind = shortcut.Shortcut{ Description: "搜索全部可用机器人(含他人/官方,返回 openDingTalkId 可发单聊)", Intent: "当你想找到平台上任意可用机器人(含他人创建或官方助手,例如某个日报/审批机器人)以便与其发起单聊时使用;输入关键词,返回含 openDingTalkId 的机器人列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/chat/chat_conversation.go b/internal/shortcut/chat/chat_conversation.go index 94396708..1a43b6f0 100644 --- a/internal/shortcut/chat/chat_conversation.go +++ b/internal/shortcut/chat/chat_conversation.go @@ -15,11 +15,11 @@ package chat import ( "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strings" "unicode/utf8" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chatmsg" @@ -33,7 +33,7 @@ var ConversationInfo = shortcut.Shortcut{ Description: "获取会话信息(群聊传 --group,单聊传 --open-dingtalk-id)", Intent: "当你已有群 openConversationId 或单聊对方 openDingTalkId、需要查看该会话的名称/类型/成员数等基础信息时使用;只读,群聊传 --group、单聊传 --open-dingtalk-id 二选一。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -239,7 +239,7 @@ var ConversationClearAllRedPoint = shortcut.Shortcut{ Description: "清除所有会话红点(全部已读)", Intent: "当你想一键把全部会话标记为已读、清空所有红点时使用;会实际清除当前用户所有会话的红点,无需任何参数。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -270,7 +270,7 @@ var ConversationList = shortcut.Shortcut{ Description: "分页获取当前用户的全部会话列表(单聊+群聊)", Intent: "当你想遍历当前用户的所有会话(单聊+群聊)做统计、清理或批量处理时使用;只读分页返回,可用 --exclude-muted 排除已免打扰会话。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -384,7 +384,7 @@ var ConversationListTop = shortcut.Shortcut{ Description: "拉取置顶会话列表,可只看群聊或单聊", Intent: "当你只想查看被置顶的那些会话时使用;只读分页返回置顶会话列表,并把下层 singleChat 规范化为 conversationType=group|direct。可用 --type group 只看群聊、--type direct 只看单聊,或用 --exclude-muted 排除已免打扰会话。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -637,7 +637,7 @@ var CategoryList = shortcut.Shortcut{ Description: "获取用户自定义会话分组", Intent: "当你想查看当前用户自建了哪些会话分组(如'工作群''项目群')时使用;只读返回分组列表及其 categoryId,供后续按分组拉会话或增删。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -838,7 +838,7 @@ var CategoryCreate = shortcut.Shortcut{ Description: "创建用户自定义会话分组", Intent: "当你想新建一个会话分组来归类会话时使用;会实际创建分组并返回其 ID,需传最多 15 个字符的分组名称 --title。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -882,7 +882,7 @@ var CategoryDelete = shortcut.Shortcut{ Description: "删除用户自定义会话分组", Intent: "当你想删除某个自定义会话分组时使用;会实际删除分组(不影响其中会话本身),不可逆,需传 categoryId。", Risk: shortcut.RiskHighWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, @@ -916,7 +916,7 @@ var CategoryRename = shortcut.Shortcut{ Description: "更新用户自定义会话分组的名称", Intent: "当你想重命名已有的自定义会话分组时使用;会实际更新分组名称,需传 categoryId 和最多 15 个字符的新名称 --title。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/shortcut/chat/chat_group.go b/internal/shortcut/chat/chat_group.go index 37e8915e..9a678125 100644 --- a/internal/shortcut/chat/chat_group.go +++ b/internal/shortcut/chat/chat_group.go @@ -15,11 +15,11 @@ package chat import ( "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strconv" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chatmsg" ) @@ -32,7 +32,7 @@ var ChatSearch = shortcut.Shortcut{ Description: "按关键词搜索群聊", Intent: "当你只记得群名称关键词、需要拿到群 openConversationId 以便发消息或管理该群时使用;按群名模糊搜索,只读分页返回匹配的群列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -140,7 +140,7 @@ var ChatInviteURL = shortcut.Shortcut{ Description: "获取群邀请链接", Intent: "当你想拿到一条群邀请链接分享给别人加群时使用;只读生成链接,需传群 openConversationId,可用 --expires-seconds 设置有效期(0 表示永久)。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -243,7 +243,7 @@ var ChatDismiss = shortcut.Shortcut{ Description: "解散群聊(不可逆,需群主权限)", Intent: "当你要彻底解散一个群时使用;会实际销毁群聊,不可逆且需群主权限,仅需传群 openConversationId,操作前务必确认。", Risk: shortcut.RiskHighWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, @@ -277,7 +277,7 @@ var ChatSetHistory = shortcut.Shortcut{ Description: "设置新成员入群可查看历史消息范围", Intent: "当你想控制新成员入群后能看到多少历史消息时使用;会实际修改群配置,需传群 openConversationId 和范围(FORBIDDEN 不可见 / RECENT_100 最近100条 / ALL 全部)。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -315,7 +315,7 @@ var ChatUpdateNick = shortcut.Shortcut{ Description: "设置当前用户在群内的群昵称", Intent: "当你想设置当前用户在某个群里显示的群昵称时使用;会实际更新本人在该群的昵称,需传群 openConversationId 和昵称。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -353,7 +353,7 @@ var ChatUpdateAlias = shortcut.Shortcut{ Description: "设置群备注(仅自己可见)", Intent: "当你想给某个群设置仅自己可见的备注名以便区分同名群时使用;会实际保存本人对该群的备注,需传群 openConversationId 和备注标题。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -391,7 +391,7 @@ var ChatListMine = shortcut.Shortcut{ Description: "拉取我创建/管理的群", Intent: "当你想查看自己作为群主或管理员在管理哪些群时使用;只读分页返回,可用 --role OWNER/ADMIN 按角色过滤。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -477,7 +477,7 @@ var ChatListAll = shortcut.Shortcut{ Description: "分页拉取我加入的所有群列表", Intent: "当你想遍历当前用户加入的所有群做统计或批量操作时使用;只读分页返回全部已加入的群列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -594,7 +594,7 @@ var ChatListJoinRequests = shortcut.Shortcut{ Description: "分页拉取入群验证记录", Intent: "当你作为群主/管理员想查看待处理的入群申请时使用;只读分页返回入群验证记录(含 recordId、申请人与邀请人 ID),供后续用 chat-audit-join 审批。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -706,7 +706,7 @@ var ChatBots = shortcut.Shortcut{ Description: "查看群内所有机器人", Intent: "当你想查看某个群里已添加了哪些机器人时使用;需传群 openConversationId,只读返回群内机器人列表(含 openBotId,供后续移除)。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -792,7 +792,7 @@ var ChatSetAdmin = shortcut.Shortcut{ Description: "设置 / 取消群管理员", Intent: "当你想把某些成员设为或取消群管理员时使用;会实际变更成员角色,需传群 openConversationId 和成员 userId/openDingTalkId 列表,加 --off 取消管理员。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -839,7 +839,7 @@ var ChatMute = shortcut.Shortcut{ Description: "全员禁言 / 取消全员禁言", Intent: "当你想对整个群开启或取消全员禁言时使用;会实际切换群的全员禁言状态,需传群 openConversationId,加 --off 取消禁言。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -1049,7 +1049,7 @@ var ChatRoleList = shortcut.Shortcut{ Description: "拉取会话的群身份列表", Intent: "当你想查看某群自定义的群身份(如'班长''值日')都有哪些时使用;需传群 openConversationId,只读返回群身份列表及 openRoleId。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1115,7 +1115,7 @@ var ChatRoleAdd = shortcut.Shortcut{ Description: "添加群身份", Intent: "当你想在群里新增一个自定义群身份/头衔时使用;会实际创建群身份,需传群 openConversationId 和身份名称。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -1153,7 +1153,7 @@ var ChatRoleUpdate = shortcut.Shortcut{ Description: "更新群身份名称", Intent: "当你想重命名已有的群身份时使用;会实际更新身份名称,需传群 openConversationId、身份 openRoleId 和新名称。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -1214,7 +1214,7 @@ var ChatRoleSetUser = shortcut.Shortcut{ Description: "设置用户的群身份(覆盖该用户的全部群身份)", Intent: "当你想为某成员整体设定其在群内的身份时使用;会实际改写该用户的群身份集合(覆盖其原有全部身份),需传群、用户和 openRoleId 列表(传空则清除全部)。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -1289,7 +1289,7 @@ var ChatRoleQueryUser = shortcut.Shortcut{ Description: "查询群成员的群身份", Intent: "当你想查看某个群成员当前拥有哪些群身份时使用;只读,需传群 openConversationId 和用户 userId 或 openDingTalkId。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/chat/chat_message.go b/internal/shortcut/chat/chat_message.go index d346281b..4d80631a 100644 --- a/internal/shortcut/chat/chat_message.go +++ b/internal/shortcut/chat/chat_message.go @@ -16,12 +16,12 @@ package chat import ( "encoding/json" "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "path/filepath" "sort" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chatmsg" ) @@ -112,7 +112,7 @@ var MessagesSendByWebhook = shortcut.Shortcut{ Description: "自定义机器人 Webhook 发送群消息", Intent: "当你只有自定义机器人的 Webhook token、想往其所在群推送消息时使用;会实际通过 Webhook 发群消息,需传 token、标题、正文,可 @手机号/userId 或 @所有人。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -400,7 +400,7 @@ var MessagesListDirect = shortcut.Shortcut{ Description: "拉取单聊会话消息", Intent: "当你想按时间拉取与某人单聊的历史消息时使用;只读,需传对方 userId 或 openDingTalkId 及起始时间,--forward 控制翻页方向。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -469,7 +469,7 @@ var MessagesListUnreadConversations = shortcut.Shortcut{ Description: "获取有未读消息的会话列表", Intent: "当你想快速定位哪些会话还有未读消息时使用;只读返回有未读的会话列表,可用 --exclude-muted 排除已免打扰会话。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -581,7 +581,7 @@ var MessagesMget = shortcut.Shortcut{ Description: "根据消息 ID 批量查询消息(最多 50 条)", Intent: "当你已有一批消息 openMsgId、需要批量取回完整详情、reaction 和可执行资源引用时使用;一次最多 50 条。--download-resources 可把所有可识别 mediaId/fileId 安全下载到工作目录内,并逐资源返回成功/失败 ledger;本地下载路径受限于工作目录、默认不覆盖同名文件,按既有安全下载约定无需交互确认。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -902,7 +902,7 @@ var MessagesQuerySendStatus = shortcut.Shortcut{ Description: "查询消息发送状态", Intent: "当你发消息后拿到 openTaskId、想确认这条消息是否发送成功时使用;只读返回发送状态,需传 --open-task-id。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -936,7 +936,7 @@ var MessagesReadStatus = shortcut.Shortcut{ Description: "查询消息的已读/未读状态", Intent: "当你想知道自己发出的某条消息有哪些人已读/未读时使用;只读,需传会话 openConversationId 和该消息 openMessageId,可指定目标成员列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1121,7 +1121,7 @@ var MessagesSendCard = shortcut.Shortcut{ Description: "创建流式卡片,可在同一次调用中写入内容并结束", Intent: "当你要发送一张流式卡片消息时使用;群 openConversationId、单聊 userId、单聊 openDingTalkId 严格三选一,分别使用 --group、--receiver、--receiver-open-dingtalk-id。--receiver 始终按 userId 通过通讯录关键词搜索做精确匹配,即使值以 D/d 开头也不会猜成 openDingTalkId;已有 openDingTalkId 时必须用显式参数直传。userId 包括在 --dry-run 时也会先解析。只传目标时创建卡片并返回 bizId,供后续 messages-update-card 流式更新;同时传 --content 时会自动串联创建和更新,默认以 flowStatus=3 完成,避免卡片停留在加载中。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -1307,7 +1307,7 @@ var MessagesUpdateCard = shortcut.Shortcut{ Description: "流式更新卡片内容(最后一次 --flow-status 应为 3)", Intent: "当你要向已发送的流式卡片持续追加/更新内容时使用;会实际更新卡片,需传 send-card 返回的 bizId、新内容及 flowStatus(最后一次应为 3 表示完成)。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -1506,7 +1506,7 @@ var MessagesListPin = shortcut.Shortcut{ Description: "拉取会话中钉住的消息列表", Intent: "当你想查看某会话里当前钉住的消息有哪些时使用;只读分页返回,需传会话 openConversationId。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/chat/unified_send.go b/internal/shortcut/chat/unified_send.go index 6495ba22..5f1c7efb 100644 --- a/internal/shortcut/chat/unified_send.go +++ b/internal/shortcut/chat/unified_send.go @@ -8,12 +8,12 @@ import ( "context" "encoding/json" "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strings" "time" "unicode/utf8" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" @@ -32,7 +32,7 @@ var MessagesSend = shortcut.Shortcut{ Description: "统一发送文本、Markdown、当前用户文件或已有 mediaId 图片", Intent: "当你希望用同一个入口选择 current-user、bot 或 webhook 身份发送消息时使用;命令会按身份校验目标、内容和凭据并路由到真实下层。current-user 支持文本/Markdown、已有 mediaId 图片、安全相对路径文件上传和幂等键;--user 传 userId 时包括在 --dry-run 中也会先通过通讯录关键词搜索并按 userId 精确匹配 openDingTalkId。bot 支持群聊或批量单聊文本/Markdown;webhook 的目标由 token 所在群决定。不会把 user 文件能力伪装成 bot/webhook 等价能力。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/shortcut/contact/contact.go b/internal/shortcut/contact/contact.go index 82d0b48c..eb60f83e 100644 --- a/internal/shortcut/contact/contact.go +++ b/internal/shortcut/contact/contact.go @@ -18,8 +18,8 @@ package contact import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -32,7 +32,7 @@ var ListFollowings = shortcut.Shortcut{ Description: "获取当前用户的特别关注列表", Intent: "当你想查看本人在通讯录里「特别关注」的联系人名单(例如常打交道的同事、上级)时使用;无需输入,返回关注对象的用户列表,可用于快速定位这些人的 userId 再发消息或排日程。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -106,7 +106,7 @@ var SearchUser = shortcut.Shortcut{ Description: "按关键词搜索通讯录用户", Intent: "当你只知道某人的姓名、花名或部分名字,需要把它解析成 userId 及部门等信息以便后续发消息、排日程或指派任务时使用;输入搜索关键词(--query),返回匹配的用户列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -177,7 +177,7 @@ var SearchMobile = shortcut.Shortcut{ Description: "按手机号搜索通讯录用户", Intent: "当你手里只有某人的手机号、需要反查出对应的通讯录用户和 userId 时使用;输入手机号(--mobile),返回该手机号所属的用户信息,适合从电话或名片信息定位到具体员工。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -221,7 +221,7 @@ var ListRoles = shortcut.Shortcut{ Description: "获取企业所有角色(标签)列表", Intent: "当你想总览企业里都有哪些角色/员工标签(如「管理员」「财务」「销售」)及其角色 ID 时使用;无需输入,返回全量角色列表,常用于按角色圈定人群前先摸清有哪些角色可选。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -355,7 +355,7 @@ var ListRoleMembers = shortcut.Shortcut{ Description: "查询角色下的成员列表", Intent: "当你已知某个角色 ID、想列出该角色(标签)下的全部成员以便群发通知或统计人群时使用;输入角色 ID(--id),返回该角色下的用户列表,通常先用 +search-role 拿到角色 ID 再调用。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -471,7 +471,7 @@ var ListSubDepts = shortcut.Shortcut{ Description: "查看指定部门的子部门", Intent: "当你想逐层浏览组织架构、查看某个部门下一级的子部门时使用;输入父部门 ID(--dept,根部门为 1),返回其直属子部门列表,可用于自顶向下遍历部门树。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -578,7 +578,7 @@ var ListDeptMembers = shortcut.Shortcut{ Description: "查看部门成员(仅本部门,不含下级)", Intent: "当你想列出一个或多个部门本级的员工(不含下级子部门)以便群发通知、统计或指派任务时使用;输入部门 ID 列表(--depts,逗号分隔),返回这些部门下的成员,如需含下级需自行遍历子部门。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/devapp/devapp.go b/internal/shortcut/devapp/devapp.go index 504a3de9..30be22b9 100644 --- a/internal/shortcut/devapp/devapp.go +++ b/internal/shortcut/devapp/devapp.go @@ -25,10 +25,10 @@ package devapp import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -66,7 +66,7 @@ var ListApp = shortcut.Shortcut{ Description: "查询开放平台企业内部应用列表", Intent: "当你要在开发者后台盘点或定位某个企业内部应用(例如按应用名、appKey、创建人或机器人名搜索,拿到其 unifiedAppId 以便后续查看详情、配置或发布)时使用;支持关键词过滤、排序和分页,返回应用列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -215,7 +215,7 @@ var GetApp = shortcut.Shortcut{ Description: "查询开放平台企业内部应用详情", Intent: "当你已知某应用的 unifiedAppId、需要查看它的完整配置信息(如名称、描述、图标、能力开关等)以便核对现状或作为修改前的依据时使用;输入 unifiedAppId,返回单个应用的详情。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -248,7 +248,7 @@ var CreateApp = shortcut.Shortcut{ Description: "创建开放平台企业内部应用", Intent: "当你要在开放平台从零新建一个企业内部应用(H5/机器人等的载体)时使用;传入应用名称、可选描述与图标 mediaId,会实际创建出一个新应用并返回其 unifiedAppId 供后续配置。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -290,7 +290,7 @@ var UpdateApp = shortcut.Shortcut{ Description: "修改开放平台企业内部应用基础信息", Intent: "当你要改动一个已存在应用的基础信息(更名、改描述或换图标)时使用;指定 unifiedAppId 及要更新的字段,会实际写回并覆盖对应的应用基础资料。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -336,7 +336,7 @@ var DeleteApp = shortcut.Shortcut{ Description: "删除开放平台企业内部应用(不可逆)", Intent: "当你确认要彻底废弃某个企业内部应用时使用;传入 unifiedAppId 会真实且不可逆地删除该应用及其配置,执行前务必确认无误。", Risk: shortcut.RiskHighWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, @@ -369,7 +369,7 @@ var EnableApp = shortcut.Shortcut{ Description: "启用开放平台企业内部应用", Intent: "当某个应用处于停用状态、你要让它重新生效可用时使用;传入 unifiedAppId 会实际将应用状态切换为启用。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -402,7 +402,7 @@ var DisableApp = shortcut.Shortcut{ Description: "停用开放平台企业内部应用", Intent: "当你要临时下线某个应用、让它对用户不可用又不删除时使用;传入 unifiedAppId 会实际将应用状态切换为停用,可日后再启用恢复。", Risk: shortcut.RiskHighWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, @@ -455,7 +455,7 @@ var WebappGet = shortcut.Shortcut{ Description: "查询网页应用配置", Intent: "当你要查看某应用的网页(H5)能力现状,如移动端/PC 首页地址、管理后台地址等,以便核对或作为改配置前的参考时使用;输入 unifiedAppId,返回当前网页应用配置。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -488,7 +488,7 @@ var WebappConfig = shortcut.Shortcut{ Description: "配置网页应用能力", Intent: "当你要为应用开通或调整网页(H5)入口,如设置移动端/PC 端首页 URL、管理后台地址或页面类型时使用;指定 unifiedAppId 及相应地址,会实际写入该应用的网页应用配置。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -542,7 +542,7 @@ var PermissionList = shortcut.Shortcut{ Description: "查询开放平台应用权限列表", Intent: "当你要查看某应用已申请/可申请的 API 权限点及其授权状态(用于排查接口报权限错、或确认某 scopeValue 是否已开通)时使用;可按关键词、scopeValue、授权状态等过滤,返回权限点列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -719,7 +719,7 @@ var MemberList = shortcut.Shortcut{ Description: "查询开放平台应用成员", Intent: "当你要查看某应用有哪些成员及其角色(如谁是开发者/管理员),用于核对协作人员或权限归属时使用;输入 unifiedAppId,返回成员列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -752,7 +752,7 @@ var MemberAdd = shortcut.Shortcut{ Description: "添加开放平台应用成员", Intent: "当你要给某应用增加协作人员(如把某人加为开发者)时使用;传入 unifiedAppId、userId 列表和成员类型(如 DEVELOPER),会实际把这些人加入应用成员并赋予对应角色。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -792,7 +792,7 @@ var MemberRemove = shortcut.Shortcut{ Description: "移除开放平台应用成员", Intent: "当某人离职或不再参与、你要取消其对应用的访问/协作权限时使用;传入 unifiedAppId、userId 列表和成员类型,会实际把这些人从应用成员中移除。", Risk: shortcut.RiskHighWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, @@ -869,7 +869,7 @@ var RobotGet = shortcut.Shortcut{ Description: "查询现有应用的机器人配置", Intent: "当你要查看某应用已有的机器人配置(名称、回调地址、模式 HTTPS/STREAM/AISKILL、技能等)以核对现状或作为改配置前的依据时使用;输入 unifiedAppId,返回当前机器人配置。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -996,7 +996,7 @@ var EventList = shortcut.Shortcut{ Description: "查询应用已订阅的事件列表", Intent: "当你要确认某应用当前订阅了哪些事件回调(用于排查漏收事件、或退订前先查事件码)时使用;输入 unifiedAppId,可按事件码/名称关键词过滤并分页,返回已订阅事件列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1179,7 +1179,7 @@ var VersionList = shortcut.Shortcut{ Description: "分页查询应用版本列表", Intent: "当你要查看某应用的历史版本(找某个 versionId、看各版本发布状态或回顾迭代记录)时使用;输入 unifiedAppId 并分页,返回版本列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1290,7 +1290,7 @@ var VersionGet = shortcut.Shortcut{ Description: "查询指定版本详情", Intent: "当你已知某个 versionId、要查看该版本的具体内容(版本号、描述、包含的配置等)以核对发布内容时使用;输入 unifiedAppId 和 versionId,返回单个版本的详情。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1329,7 +1329,7 @@ var VersionCheckApproval = shortcut.Shortcut{ Description: "预检版本发布是否需要审批(不实际发布)", Intent: "当你在正式发布某版本前想先确认它是否会触发审批、是否含高敏权限等发布前置要求时使用;传入 unifiedAppId 和 versionId,仅做预检返回审批要求,不会真正发布。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -1399,7 +1399,7 @@ var VersionStatus = shortcut.Shortcut{ Description: "查询版本发布/审批状态", Intent: "当你已提交发布、想跟进某版本当前处于什么阶段(审批中、已发布、被驳回等)时使用;输入 unifiedAppId 和 versionId,返回该版本的发布/审批状态。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/ding/ding.go b/internal/shortcut/ding/ding.go index ac48a4da..a548e538 100644 --- a/internal/shortcut/ding/ding.go +++ b/internal/shortcut/ding/ding.go @@ -17,15 +17,15 @@ package ding import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) var List = shortcut.Shortcut{ Service: "ding", Command: "+list", Product: "im", Description: "查询 DING 消息列表", Intent: "当你想查看当前身份收到或发出的 DING 消息、回顾有哪些强提醒或获取某条 DING 的 openDingId 以便后续查已读或撤回时使用;可选按类型过滤并用 cursor 翻页,只读不产生副作用。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -63,7 +63,7 @@ var List = shortcut.Shortcut{ var ReceiverStatus = shortcut.Shortcut{ Service: "ding", Command: "+receiver-status", Product: "im", Description: "查询 DING 消息接收人已读状态", Intent: "当你发出一条 DING 后想确认每位接收人是否已读、追踪谁还没看到以便催办时使用;需提供该 DING 的 openDingId,返回各接收人的已读/未读状态,只读操作。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -93,7 +93,7 @@ var ReceiverStatus = shortcut.Shortcut{ var SendPersonal = shortcut.Shortcut{ Service: "ding", Command: "+send-personal", Product: "im", Description: "以本人身份发送 DING 给指定人", Intent: "当你想以自己(而非机器人)的身份直接给某些同事发 DING 强提醒,让对方看到是本人发起时使用;需提供接收人的 openDingTalkId 列表和内容,可选提醒方式与幂等 uuid,会真实向这些人发出 DING。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -156,7 +156,7 @@ var SendByMessage = shortcut.Shortcut{ var RecallPersonal = shortcut.Shortcut{ Service: "ding", Command: "+recall-personal", Product: "im", Description: "撤回本人发起的 DING", Intent: "当你以本人身份发出的某条 DING 发错人或内容有误、想收回时使用(对应 send-personal/send-by-message 发出的 DING);需提供该 DING 的 openDingId,会真实撤回它,接收人将不再看到该提醒。", Risk: shortcut.RiskHighWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/shortcut/doc/doc.go b/internal/shortcut/doc/doc.go index 543896b0..5a374683 100644 --- a/internal/shortcut/doc/doc.go +++ b/internal/shortcut/doc/doc.go @@ -17,8 +17,8 @@ package doc import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -36,7 +36,7 @@ var Search = shortcut.Shortcut{ Description: "按关键词搜索有权限的文档 (不传则返回最近访问)", Intent: "当你只记得文档的标题或主题词、需要先定位到某篇钉钉文档拿到它的 nodeId/URL 以便后续阅读或编辑时使用;可按关键词、扩展名、创建/访问时间、创建者等条件过滤,不传关键词则返回最近访问的文档,返回匹配的文档列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -200,7 +200,7 @@ var List = shortcut.Shortcut{ Description: "列出文件夹或知识库下的直接子节点", Intent: "当你已知某个文档文件夹或知识库的 ID、想浏览它下面直接包含的文档与子文件夹(不递归深层)以便逐层导航时使用;输入 folder 或 workspace,返回该层级的子节点列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -292,7 +292,7 @@ var Copy = shortcut.Shortcut{ Description: "复制文档/文件到指定文件夹或知识库", Intent: "当你想保留原件、在另一个文件夹或知识库里生成一份文档/文件副本(例如以某篇文档为模板另存)时使用;输入源 node 与目标 folder/workspace,会实际创建一个副本。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -334,7 +334,7 @@ var Move = shortcut.Shortcut{ Description: "移动文档/文件到指定文件夹或知识库", Intent: "当你要整理文档归属、把某篇文档/文件从当前位置挪到另一个文件夹或知识库(原位置不再保留)时使用;输入 node 与目标 folder/workspace,会实际改变文件的存放位置。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -384,7 +384,7 @@ var CommentList = shortcut.Shortcut{ Description: "查询文档评论列表", Intent: "当你想查看某篇文档上已有的评论、了解有哪些反馈或待处理意见(可按全文/划词、已解决/未解决过滤)时使用;输入 node,返回评论列表及其 commentKey 以便后续回复。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -437,7 +437,7 @@ var CommentCreate = shortcut.Shortcut{ Description: "在文档上创建一条评论", Intent: "当你想对整篇文档留一条全文评论、给出反馈或 @ 相关同事时使用;输入 node 与评论 content(可带 mention),会实际在文档上发布一条新评论。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -479,7 +479,7 @@ var CommentReply = shortcut.Shortcut{ Description: "回复文档中的一条评论", Intent: "当你要针对某条已有评论进行回复、参与讨论或用表情贴图回应时使用;先从评论列表拿到 comment-key,再输入 node、comment-key 与 content(--emoji 则作为表情回复),会实际发布一条回复。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -566,7 +566,7 @@ var ExportSubmit = shortcut.Shortcut{ Description: "提交在线文档导出任务 (docx/markdown/pdf),返回 jobId", Intent: "当你想把在线文档导出成 docx/markdown/pdf 文件(例如离线保存或外发)时使用;这是异步任务的第一步,输入 node 与 export-format 提交导出,返回 jobId,随后用 +export-get 轮询结果。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -607,7 +607,7 @@ var ExportGet = shortcut.Shortcut{ Description: "根据 jobId 查询文档导出任务结果", Intent: "当你已用 +export-submit 提交了导出任务、想查询它是否完成并拿到导出文件的下载链接时使用;输入上一步返回的 job-id,返回任务状态与结果。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -642,7 +642,7 @@ var VersionSave = shortcut.Shortcut{ Description: "手动保存文档版本快照", Intent: "当你在做重大改动前后、想手动打一个可回滚的版本存档点时使用;输入 node,会实际为该文档保存一个当前内容的历史版本快照。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -675,7 +675,7 @@ var VersionList = shortcut.Shortcut{ Description: "查看文档历史版本列表", Intent: "当你想查看某篇文档有哪些历史版本、以便挑一个版本号用于回滚时使用;输入 node,返回历史版本列表及其版本号。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -717,7 +717,7 @@ var VersionRevert = shortcut.Shortcut{ Description: "回滚文档到指定历史版本", Intent: "当文档被误改、你想把它整体恢复到某个历史版本时使用;先用 +version-list 找到目标版本号,再输入 node 与 version,会实际把文档内容覆盖回该版本,属于高风险写操作,需谨慎确认。", Risk: shortcut.RiskHighWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, @@ -756,7 +756,7 @@ var TemplateList = shortcut.Shortcut{ Description: "获取文档模板列表", Intent: "当你想基于模板新建文档、需要先浏览可用的模板(自己的 MY 或公共 PUBLIC)并拿到 templateId 时使用;返回模板列表,随后可配合 +template-apply 套用。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -801,7 +801,7 @@ var TemplateSearch = shortcut.Shortcut{ Description: "根据关键词搜索文档模板", Intent: "当模板较多、你想按关键词(如“周报”“合同”)快速找到合适的模板并拿到 templateId 时使用;输入 query,返回匹配的模板列表,随后可配合 +template-apply 套用。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/drive/drive.go b/internal/shortcut/drive/drive.go index 8d5bec0a..4436d290 100644 --- a/internal/shortcut/drive/drive.go +++ b/internal/shortcut/drive/drive.go @@ -20,8 +20,8 @@ package drive import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -142,7 +142,7 @@ var Info = shortcut.Shortcut{ Description: "获取钉盘文件/文件夹元数据", Intent: "当你已经知道某个节点的 dentryUuid、想查看它的详细信息(名称、大小、类型、创建/修改时间、所属空间等)而不是仅列表概览时使用;输入 node(节点 ID),返回该单个文件或文件夹的完整元数据。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -207,7 +207,7 @@ var Search = shortcut.Shortcut{ Description: "搜索钉盘文件", Intent: "当你只记得文件名或内容关键词、不知道它在哪个目录时用它全局检索钉盘文件;输入 query,可按文件类型、扩展名、创建者、创建/修改时间范围过滤,返回匹配文件及其 ID,便于再做下载或整理。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -358,7 +358,7 @@ var SearchDocs = shortcut.Shortcut{ Description: "搜索文档空间文档", Intent: "当你只记得文档标题或关键词、想在文档空间/知识库中检索在线文档(区别于 +search 检索钉盘文件)时使用;输入 query 关键词,返回匹配的文档及其节点信息。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -458,7 +458,7 @@ var Copy = shortcut.Shortcut{ Description: "复制文件/文档到指定位置", Intent: "当你想保留原件、把某个文件/文档拷贝一份到指定文件夹或知识库时使用;输入源节点 node 及目标 folder/workspace,会实际生成一个副本,原文件位置不变。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -501,7 +501,7 @@ var Move = shortcut.Shortcut{ Description: "移动文件/文档到指定位置", Intent: "当你要把某个文件/文档从当前位置转移到另一个文件夹或知识库(整理归档、调整目录结构)时使用;输入源节点 node 及目标 folder/workspace,会实际改变文件所在位置,原位置不再保留该文件。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, @@ -549,7 +549,7 @@ var Recent = shortcut.Shortcut{ Description: "获取最近访问/编辑的文档列表", Intent: "当你想快速找回「我最近看过/改过的那个文档」而不记得它放在哪时使用;可按操作类型(最近访问/最近编辑)和创建人(全部/我创建/他人创建)过滤,返回近期文档列表及其节点信息。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/mail/mail.go b/internal/shortcut/mail/mail.go index f09464a3..5fbb5ec7 100644 --- a/internal/shortcut/mail/mail.go +++ b/internal/shortcut/mail/mail.go @@ -18,8 +18,8 @@ package mail import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -51,7 +51,7 @@ var ThreadList = shortcut.Shortcut{ Description: "列出指定邮箱文件夹下的邮件会话(thread)", Intent: "当你想按会话(同一往来主题的邮件串)而非单封邮件来浏览某个文件夹时使用;传入邮箱和文件夹 ID,可按时间范围和升降序筛选,返回会话列表及其 conversationId,供 +thread 查看详情。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -187,7 +187,7 @@ var FolderList = shortcut.Shortcut{ Description: "列出顶层文件夹或指定父文件夹下的子文件夹", Intent: "当你需要了解某个邮箱有哪些文件夹、或要取得文件夹 ID 以便移动邮件、按文件夹列信/建规则时使用;传入邮箱(可选父文件夹 ID 查子级),返回文件夹列表及其 ID。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -302,7 +302,7 @@ var TagList = shortcut.Shortcut{ Description: "列出指定邮箱下的所有邮件标签", Intent: "当你要查看邮箱里有哪些标签、或需要取得标签 ID 以便给邮件/会话加标签时使用;传入邮箱地址,返回全部邮件标签及其 ID。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -412,7 +412,7 @@ var UserSearch = shortcut.Shortcut{ Description: "按关键词或工号搜索邮箱用户(仅企业邮箱)", Intent: "当你只知道同事的姓名或工号、需要查出其企业邮箱地址以便发信或添加联系人时使用;提供关键词或工号(至少其一),返回匹配的企业邮箱用户列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -551,7 +551,7 @@ var TemplateList = shortcut.Shortcut{ Description: "列出指定邮箱的所有邮件模板", Intent: "当你想查看某个邮箱下已有哪些邮件模板、或需要取得模板 ID 以便查看详情或更新时使用;传入邮箱和每页数量,返回模板列表,支持分页。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -669,7 +669,7 @@ var ContactList = shortcut.Shortcut{ Description: "列出指定邮箱的所有邮件联系人", Intent: "当你想查看某邮箱通讯录里有哪些联系人、或需要取得联系人 ID 以便更新或删除时使用;传入邮箱和每页数量,返回联系人列表,支持分页。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/minutes/minutes.go b/internal/shortcut/minutes/minutes.go index 0b646b97..82ee4391 100644 --- a/internal/shortcut/minutes/minutes.go +++ b/internal/shortcut/minutes/minutes.go @@ -16,8 +16,8 @@ package minutes import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -35,7 +35,7 @@ var ListMine = shortcut.Shortcut{ Description: "查询我创建的听记列表", Intent: "当你想找回自己发起或录制的某次听记(会议纪要),却只记得大概的标题关键字时使用;可按关键字筛选并分页,返回自己创建的听记列表及其 taskUuid,便于后续查看摘要、转写或待办。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -70,7 +70,7 @@ var ListShared = shortcut.Shortcut{ Description: "查询他人共享给我的听记列表", Intent: "当你要找同事分享给你的会议听记、想快速定位别人共享过来的纪要时使用;可按关键字筛选并分页,返回他人共享给你的听记列表及 taskUuid。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -105,7 +105,7 @@ var ListAll = shortcut.Shortcut{ Description: "查询我有权限访问的所有听记列表", Intent: "当你不确定某条听记是自己创建还是别人共享、想在所有可访问的听记中一次性检索时使用;合并「我创建的」和「共享给我的」,按关键字筛选并分页返回全部有权限的听记及 taskUuid。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -251,7 +251,7 @@ var RecordStart = shortcut.Shortcut{ Description: "发起听记(开始录音)", Intent: "当你要开始一场实时会议/通话的 AI 听记、立刻启动录音并生成一条新听记任务时使用;可选传入 AI 助理会话 ID,会真实发起录音,返回新建听记的 taskUuid 供后续暂停/恢复/结束。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/shortcut/oa/oa.go b/internal/shortcut/oa/oa.go index dbdcea3a..afb9ca40 100644 --- a/internal/shortcut/oa/oa.go +++ b/internal/shortcut/oa/oa.go @@ -16,8 +16,8 @@ package oa import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -29,7 +29,7 @@ var ListPending = shortcut.Shortcut{ Description: "查询待我处理的审批(时间范围为 epoch 毫秒)", Intent: "当你想知道自己当前有哪些审批还没处理、需要清理审批待办或按时间段/关键字盘点待审批单时使用;传入起止时间(epoch 毫秒,可选关键字与分页),返回待我审批的实例列表,是后续 +get 查详情、+approve/+reject 处理的入口。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -112,7 +112,7 @@ var ListForms = shortcut.Shortcut{ Description: "获取当前用户可见的审批表单列表", Intent: "当你想浏览自己有权限发起哪些审批模板、或需要为 +list-initiated 等操作枚举 processCode 时使用;无需关键字,按游标分页返回当前用户可见的全部审批表单,适合不确定表单名称时先整体看一遍。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -230,7 +230,7 @@ var SearchForms = shortcut.Shortcut{ Description: "按关键字模糊搜索当前用户可见的审批表单", Intent: "当你已知想找的审批大致名称(如「报销」「请假」)、想快速定位对应表单及其 processCode 时使用,比 +list-forms 全量列举更高效;传入关键字,返回名称或 processCode 匹配的表单,供后续 +list-initiated 按模板查询。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -291,7 +291,7 @@ var ListExecuted = shortcut.Shortcut{ Description: "获取当前用户已经处理过的审批单列表", Intent: "当你想回顾自己历史上审批过(已同意/拒绝等)的单子、做复盘或查找某条已办审批时使用,区别于 +list-pending 的待办;按页码/关键字分页返回我已处理的审批单。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -411,7 +411,7 @@ var ListSubmitted = shortcut.Shortcut{ Description: "获取当前用户已发起的审批单列表", Intent: "当你想查看自己提交发起的审批单及其审批进度(如某笔报销/请假审到哪一步)时使用;按页码/关键字分页返回我发起的审批单,可据此决定是否 +revoke 撤销或催办。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -479,7 +479,7 @@ var ListCc = shortcut.Shortcut{ Description: "获取抄送当前用户的审批单列表", Intent: "当你想查看抄送给自己、需要知悉但无需审批的单子时使用;按页码/关键字分页返回抄送我的审批单列表,适合了解与自己相关但不用自己动手处理的审批动态。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/report/report.go b/internal/shortcut/report/report.go index 4802f1d1..db55c1dc 100644 --- a/internal/shortcut/report/report.go +++ b/internal/shortcut/report/report.go @@ -19,11 +19,11 @@ package report import ( "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strings" "time" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -48,7 +48,7 @@ var InboxList = shortcut.Shortcut{ Description: "列出我收到的日报(按时间范围分页)", Intent: "当你要查看下属或同事发给自己的日报周报、想在某个时间段内浏览或审阅收到的汇报时使用;输入起止时间(ISO-8601),可按发送人 staffId 过滤,分页返回收到的日报列表及其 reportId,供后续 +entry-get 读正文。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -191,7 +191,7 @@ var OutboxList = shortcut.Shortcut{ Description: "列出我发出的日报(可选时间/模版名过滤)", Intent: "当你要回顾自己写过、提交过的日报周报,比如确认某天是否已交、找回历史汇报内容或统计提交情况时使用;可按创建/修改时间范围和模版名过滤,分页返回自己发出的日报列表及 reportId,供后续 +entry-get 查看正文。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/sheet/sheet.go b/internal/shortcut/sheet/sheet.go index effe5b58..6a1b811f 100644 --- a/internal/shortcut/sheet/sheet.go +++ b/internal/shortcut/sheet/sheet.go @@ -17,8 +17,8 @@ package sheet import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -33,7 +33,7 @@ var ListSheets = shortcut.Shortcut{ Description: "获取表格文档中全部工作表列表", Intent: "当你拿到一个表格文档、想先了解它里面有哪些工作表(sheet)以及各自的 sheetId 时使用,通常作为读写具体数据前的第一步;传入表格文档 ID 或 URL,返回工作表清单。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -155,7 +155,7 @@ var Read = shortcut.Shortcut{ Description: "读取工作表指定范围的结构化单元格数据", Intent: "当你需要按单元格逐格获取数据(含类型、公式或格式化值等结构化信息)以便程序处理时使用;传入表格与可选范围(A1 表示法,不传则全部),可指定取格式化值/原始值/公式,返回结构化单元格数组。若只想要纯文本可改用 +csv-get。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/shortcut_test.go b/internal/shortcut/shortcut_test.go index 81881c68..a30095f0 100644 --- a/internal/shortcut/shortcut_test.go +++ b/internal/shortcut/shortcut_test.go @@ -19,8 +19,8 @@ import ( "strings" "testing" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition" "github.com/spf13/cobra" @@ -521,7 +521,7 @@ func TestLiveMountEOFRequiresConfirmation(t *testing.T) { func TestLiveMountExplicitSafetyDrivesRuntimeAndContractFinal(t *testing.T) { called := false - explicit := cli.SafetySpec{ + explicit := contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", } @@ -553,7 +553,7 @@ func TestLiveMountExplicitSafetyDrivesRuntimeAndContractFinal(t *testing.T) { cmd := mount(s) root.AddCommand(cmd) - final, ok := cli.RuntimeContractFinal(cmd) + final, ok := contract.RuntimeContractFinal(cmd) if !ok || final.Safety == nil { t.Fatal("mounted Shortcut must publish ContractFinal Safety") } diff --git a/internal/shortcut/smart/assign.go b/internal/shortcut/smart/assign.go index 19f92155..272193f9 100644 --- a/internal/shortcut/smart/assign.go +++ b/internal/shortcut/smart/assign.go @@ -14,8 +14,8 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -34,7 +34,7 @@ var Assign = shortcut.Shortcut{ Intent: "当你想把一件事指派给某位同事、但只知道对方姓名不想先查 userId 时使用;" + "内部先按姓名解析出唯一 userId,再创建待办并把 TA 设为执行人。会真实创建待办。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/shortcut/smart/assign_multi.go b/internal/shortcut/smart/assign_multi.go index 0e8ae122..1094fd18 100644 --- a/internal/shortcut/smart/assign_multi.go +++ b/internal/shortcut/smart/assign_multi.go @@ -15,10 +15,10 @@ package smart import ( "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -49,7 +49,7 @@ var AssignMulti = shortcut.Shortcut{ "就把这些问题一次性汇总报错、并且完全不创建待办(不会建出只指派了一半人的残缺待办)。" + "全部姓名都解析成功后,才用这些 userId 一次性创建这条待办并指派给所有人。会真实创建一条新的待办。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/shortcut/smart/at_me.go b/internal/shortcut/smart/at_me.go index 82cfc449..8b616ae6 100644 --- a/internal/shortcut/smart/at_me.go +++ b/internal/shortcut/smart/at_me.go @@ -14,12 +14,12 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strconv" "strings" "time" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" chatshortcut "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chat" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chatmsg" @@ -56,7 +56,7 @@ var AtMe = shortcut.Shortcut{ "再在本地把每条消息投影成发送人、时间、内容、所在会话四个关键字段。" + "默认只读且不会发送、撤回或标记任何消息;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/book.go b/internal/shortcut/smart/book.go index a45ff36a..b0545966 100644 --- a/internal/shortcut/smart/book.go +++ b/internal/shortcut/smart/book.go @@ -15,10 +15,10 @@ package smart import ( "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -45,7 +45,7 @@ var Book = shortcut.Shortcut{ "如果加参会人失败,会自动删除刚建好的日程回滚,避免留下一个没人的空日程。" + "会真实创建日程并发出参会邀请。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/shortcut/smart/broadcast.go b/internal/shortcut/smart/broadcast.go index d07da51a..284e4f95 100644 --- a/internal/shortcut/smart/broadcast.go +++ b/internal/shortcut/smart/broadcast.go @@ -16,10 +16,10 @@ package smart import ( "encoding/json" "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -42,7 +42,7 @@ var Broadcast = shortcut.Shortcut{ "内部把姓名列表逐个解析成唯一用户后,用 openDingTalkId 对每个人单独发一条单聊消息,并汇总成功/失败人数。" + "某个姓名匹配不到人或匹配到多人时,会跳过该人并在结尾报出,不影响其他人收到消息。会真实发出多条消息。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/shortcut/smart/by_mobile.go b/internal/shortcut/smart/by_mobile.go index 29882843..697ba059 100644 --- a/internal/shortcut/smart/by_mobile.go +++ b/internal/shortcut/smart/by_mobile.go @@ -15,10 +15,10 @@ package smart import ( "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strconv" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -40,7 +40,7 @@ var ByMobile = shortcut.Shortcut{ Intent: "当你只知道对方手机号、想一步拿到其完整资料(部门、职位、联系方式、是否管理员等)而不想先按手机号搜出 userId 再单独查详情时使用;" + "内部先用手机号在通讯录里查出对应的 userId,若没有人绑定该手机号会明确报错,再用该 userId 取完整详情。这是纯只读操作,不会修改任何数据。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/cancel_event.go b/internal/shortcut/smart/cancel_event.go index 82193111..1eb62b6d 100644 --- a/internal/shortcut/smart/cancel_event.go +++ b/internal/shortcut/smart/cancel_event.go @@ -14,10 +14,10 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -47,7 +47,7 @@ var CancelEvent = shortcut.Shortcut{ "如果 eventId 查不到会直接报错,不会盲目删除。" + "这是高危写操作,会真实删除该日程,框架会二次确认。", Risk: shortcut.RiskHighWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/shortcut/smart/chat_messages.go b/internal/shortcut/smart/chat_messages.go index 53858a27..d18dab2b 100644 --- a/internal/shortcut/smart/chat_messages.go +++ b/internal/shortcut/smart/chat_messages.go @@ -14,11 +14,11 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strings" "time" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" chatshortcut "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chat" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chatmsg" @@ -61,7 +61,7 @@ var ChatMessages = shortcut.Shortcut{ "内部据此调用群聊或单聊的消息列表接口,再在本地投影出每条消息的发言人、文本和时间。" + "默认只读且不会发送或修改任何消息;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/conflicts.go b/internal/shortcut/smart/conflicts.go index 4cfbae0d..32d57d6a 100644 --- a/internal/shortcut/smart/conflicts.go +++ b/internal/shortcut/smart/conflicts.go @@ -14,12 +14,12 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "sort" "strings" "time" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -43,7 +43,7 @@ var Conflicts = shortcut.Shortcut{ "再在本地两两比对开始/结束时间,找出所有时间段重叠的日程对并报告。" + "只读操作,不修改任何日程;没有冲突时明确告诉你「无冲突」。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/created_todos.go b/internal/shortcut/smart/created_todos.go index 24bea27c..537a8ebf 100644 --- a/internal/shortcut/smart/created_todos.go +++ b/internal/shortcut/smart/created_todos.go @@ -14,8 +14,8 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -49,7 +49,7 @@ var CreatedTodos = shortcut.Shortcut{ "任务 ID(taskId) 和截止时间(dueTime) 打印出来。这是纯只读操作,只做列表与投影," + "不会创建或修改任何待办;若没有你创建的待办则返回空列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/dept_members.go b/internal/shortcut/smart/dept_members.go index 97914359..6fe1f970 100644 --- a/internal/shortcut/smart/dept_members.go +++ b/internal/shortcut/smart/dept_members.go @@ -16,11 +16,11 @@ package smart import ( "encoding/json" "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strconv" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -52,7 +52,7 @@ var DeptMembers = shortcut.Shortcut{ "而是列出候选部门名与 deptId 让你选),再打印该部门的直接成员列表" + "(仅本部门,不递归下级部门)。只读,不做任何修改。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/dm.go b/internal/shortcut/smart/dm.go index 45eb80ae..3d0d7a1b 100644 --- a/internal/shortcut/smart/dm.go +++ b/internal/shortcut/smart/dm.go @@ -19,9 +19,9 @@ package smart import ( "encoding/json" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -41,7 +41,7 @@ var DM = shortcut.Shortcut{ Intent: "当你只知道对方姓名、想直接发一条单聊消息而不想先查 userId 时使用;" + "内部先按姓名搜通讯录解析出唯一用户,并用其 openDingTalkId 发送,姓名匹配到多人时会列出候选让你区分。会真实发出消息。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/shortcut/smart/doc_append.go b/internal/shortcut/smart/doc_append.go index 193d10bf..c74b4ba3 100644 --- a/internal/shortcut/smart/doc_append.go +++ b/internal/shortcut/smart/doc_append.go @@ -14,10 +14,10 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -43,7 +43,7 @@ var DocAppend = shortcut.Shortcut{ "不需要你先去查文档块列表、算末尾位置或手工拼块结构。" + "会真实写入文档内容。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/shortcut/smart/find_doc.go b/internal/shortcut/smart/find_doc.go index 80c8e3c3..aaeb2323 100644 --- a/internal/shortcut/smart/find_doc.go +++ b/internal/shortcut/smart/find_doc.go @@ -14,10 +14,10 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -48,7 +48,7 @@ var FindDoc = shortcut.Shortcut{ "可选地用 --limit 限制返回条数(pageSize),再在本地把每条命中结果精简为「标题、URL、类型、token」四个字段后打印。" + "这是纯只读操作,只做搜索与本地投影,不会创建、修改或删除任何文档;未命中时提示「没搜到文档」。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/find_file.go b/internal/shortcut/smart/find_file.go index 972c33b7..faca5915 100644 --- a/internal/shortcut/smart/find_file.go +++ b/internal/shortcut/smart/find_file.go @@ -14,10 +14,10 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -47,7 +47,7 @@ var FindFile = shortcut.Shortcut{ "并限定搜索范围为钉盘文件(searchTarget=file),再在本地把每条命中结果精简为「文件名、类型、dentryId、大小」四个字段后打印。" + "这是纯只读操作,只做搜索与本地投影,不会创建、移动或删除任何文件;未命中时返回空列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/find_mail_user.go b/internal/shortcut/smart/find_mail_user.go index 82553538..1ee46b7e 100644 --- a/internal/shortcut/smart/find_mail_user.go +++ b/internal/shortcut/smart/find_mail_user.go @@ -14,10 +14,10 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strconv" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -57,7 +57,7 @@ var FindMailUser = shortcut.Shortcut{ "这是纯只读操作,只做搜索与本地投影,不会修改任何数据;" + "注意仅企业邮箱可用(个人邮箱如 xxx@dingtalk.com 会因无权限报错);若没有命中则提示「没搜到邮箱联系人」。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/find_record.go b/internal/shortcut/smart/find_record.go index b7851ebc..d38280ed 100644 --- a/internal/shortcut/smart/find_record.go +++ b/internal/shortcut/smart/find_record.go @@ -14,10 +14,10 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -43,7 +43,7 @@ var FindRecord = shortcut.Shortcut{ "却不想手写结构化过滤条件时使用;内部直接调用 query_records,把 --query 作为全文关键词(keyword)在该表里检索并打印匹配记录。" + "不传 --query 时则返回该表的前若干条记录。这是只读操作,不会修改任何数据。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/free_slots.go b/internal/shortcut/smart/free_slots.go index 6e7c1fc5..7d870053 100644 --- a/internal/shortcut/smart/free_slots.go +++ b/internal/shortcut/smart/free_slots.go @@ -15,11 +15,11 @@ package smart import ( "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "sort" "time" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -44,7 +44,7 @@ var FreeSlots = shortcut.Shortcut{ "再在工作时间范围内(默认 09:00-18:00,可用 --from/--to 指定起止小时)算出所有空闲窗口并给出每段的起止与时长。" + "只读操作,不修改任何日程;用于快速定位可预约的空档。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/freebusy.go b/internal/shortcut/smart/freebusy.go index d895ae45..da5eb9c2 100644 --- a/internal/shortcut/smart/freebusy.go +++ b/internal/shortcut/smart/freebusy.go @@ -15,10 +15,10 @@ package smart import ( "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "time" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -38,7 +38,7 @@ var FreeBusy = shortcut.Shortcut{ Intent: "当你只知道对方姓名、想知道 TA 在某段时间内是空闲还是被日程占用(比如约会前先看看有没有空)而不想先手动查 userId 时使用;" + "内部先按姓名搜通讯录解析出唯一 userId,姓名匹配到多人时会列出候选让你区分,再按时间范围查询忙闲。只读,不产生任何日程变更。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/group_members.go b/internal/shortcut/smart/group_members.go index 6109d77b..26fb3d10 100644 --- a/internal/shortcut/smart/group_members.go +++ b/internal/shortcut/smart/group_members.go @@ -15,10 +15,10 @@ package smart import ( "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chatmsg" @@ -45,7 +45,7 @@ var GroupMembers = shortcut.Shortcut{ "内部先按群名搜索群聊解析出唯一 openConversationId,再拉取该群的成员列表。" + "群名匹配到多个群时会列出候选让你区分、绝不自行假定。只读,不改动任何数据。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/invite.go b/internal/shortcut/smart/invite.go index 1eea2cd5..43e80f8e 100644 --- a/internal/shortcut/smart/invite.go +++ b/internal/shortcut/smart/invite.go @@ -14,10 +14,10 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -38,7 +38,7 @@ var Invite = shortcut.Shortcut{ "内部先把 --with 里每个姓名解析成唯一 userId,再一次性把他们全部加到 --event 指定的日程里。" + "会真实修改日程并发出参会邀请。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/shortcut/smart/list_tables.go b/internal/shortcut/smart/list_tables.go index 44f45f65..0814c480 100644 --- a/internal/shortcut/smart/list_tables.go +++ b/internal/shortcut/smart/list_tables.go @@ -14,11 +14,11 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strconv" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -45,7 +45,7 @@ var ListTables = shortcut.Shortcut{ "再在本地把每张表投影成 tableId、tableName 两个关键字段打印出来。" + "这是纯只读操作,只做列举与本地投影,不会创建、修改或删除任何表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/lookup.go b/internal/shortcut/smart/lookup.go index 38236b8a..abcffd6e 100644 --- a/internal/shortcut/smart/lookup.go +++ b/internal/shortcut/smart/lookup.go @@ -14,8 +14,8 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -34,7 +34,7 @@ var Lookup = shortcut.Shortcut{ Intent: "当你只知道对方姓名、想一步拿到其完整资料(部门、职位、联系方式等)而不想先搜 userId 再查详情时使用;" + "内部先按姓名搜通讯录解析出唯一 userId,再取详情,姓名匹配到多人时会列出候选让你区分。只读操作。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/minutes_detail.go b/internal/shortcut/smart/minutes_detail.go index c4d2899f..ccd3716c 100644 --- a/internal/shortcut/smart/minutes_detail.go +++ b/internal/shortcut/smart/minutes_detail.go @@ -14,10 +14,10 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -48,7 +48,7 @@ var MinutesDetail = shortcut.Shortcut{ "内部按 --artifacts 选择要拉的产物(默认全部:basic/summary/keywords/transcript/todos),逐个调用对应的原子工具并聚合成一个结果," + "某一项失败不会中断整体(会以错误字符串记录在该项下)。这是纯只读操作,不会修改听记;--direction 仅影响逐字稿排序(0=正序默认,1=倒序)。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/minutes_search.go b/internal/shortcut/smart/minutes_search.go index 6c78a3de..7507ffda 100644 --- a/internal/shortcut/smart/minutes_search.go +++ b/internal/shortcut/smart/minutes_search.go @@ -14,8 +14,8 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -43,7 +43,7 @@ var MinutesSearch = shortcut.Shortcut{ "内部按 --query 关键词列出你创建的听记(最多 20 条),再在本地投影出每条的标题、创建时间和 taskUuid。" + "这是纯只读操作,只做搜索与本地投影,不会修改任何听记;若没有匹配的听记则提示「没搜到妙记」。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/my_attendance.go b/internal/shortcut/smart/my_attendance.go index 61524341..0e8d4c09 100644 --- a/internal/shortcut/smart/my_attendance.go +++ b/internal/shortcut/smart/my_attendance.go @@ -14,10 +14,10 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "time" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -48,7 +48,7 @@ var MyAttendance = shortcut.Shortcut{ "再手动填写今天的起止时间时使用;内部先取当前登录用户的 userId,再按本地时区算出今天 00:00 到次日 00:00 的时间窗," + "最后查询你今天的打卡流水记录。只读操作,不会修改任何考勤数据;今天若还没有任何打卡则返回空结果。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/my_free.go b/internal/shortcut/smart/my_free.go index 97c28a8b..138168da 100644 --- a/internal/shortcut/smart/my_free.go +++ b/internal/shortcut/smart/my_free.go @@ -14,10 +14,10 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "time" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -46,7 +46,7 @@ var MyFree = shortcut.Shortcut{ "默认查今天(本地时区 00:00 到次日 00:00),也可用 --start/--end 指定 ISO8601 时间范围。" + "只读操作,只查忙闲、不创建或修改任何日程;返回按时间排列的忙碌时段,空则表示这段时间全空。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/my_groups.go b/internal/shortcut/smart/my_groups.go index 2bcdc395..aa22c48d 100644 --- a/internal/shortcut/smart/my_groups.go +++ b/internal/shortcut/smart/my_groups.go @@ -14,11 +14,11 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strconv" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chatmsg" ) @@ -48,7 +48,7 @@ var MyGroups = shortcut.Shortcut{ "内部分页拉取你加入的群列表,把每个群防御式地投影成 会话id / 名称 / 群主 / 人数 / 类型 等关键字段,输出成干净的结果。" + "可选 --type 在本地按群类型过滤(底层接口本身不带类型参数,故为客户端过滤)。这是只读操作,不会改动任何群或成员关系。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/my_initiated.go b/internal/shortcut/smart/my_initiated.go index a57ed014..98b44826 100644 --- a/internal/shortcut/smart/my_initiated.go +++ b/internal/shortcut/smart/my_initiated.go @@ -14,11 +14,11 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strconv" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -57,7 +57,7 @@ var MyInitiated = shortcut.Shortcut{ "可用 --query 按关键字过滤、--page/--limit 翻页(默认第 1 页、每页 20 条)。" + "这是纯只读操作,只做列表与本地投影,不会同意、拒绝、撤销或修改任何审批单据;若没有发起过审批则返回空列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/next_event.go b/internal/shortcut/smart/next_event.go index 6d8d350e..9bc9819d 100644 --- a/internal/shortcut/smart/next_event.go +++ b/internal/shortcut/smart/next_event.go @@ -14,11 +14,11 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strings" "time" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -43,7 +43,7 @@ var NextEvent = shortcut.Shortcut{ "按开始时间升序挑出最近的那一个并打印摘要(标题、开始/结束时间、地点)。" + "若这 7 天内没有任何日程,会明确提示『近 7 天无日程』。只读,不做任何修改。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/org.go b/internal/shortcut/smart/org.go index 57efa09c..96e21b5d 100644 --- a/internal/shortcut/smart/org.go +++ b/internal/shortcut/smart/org.go @@ -16,10 +16,10 @@ package smart import ( "encoding/json" "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strconv" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -43,7 +43,7 @@ var Org = shortcut.Shortcut{ "内部先按姓名解析出唯一 userId,再取 TA 的组织信息拿到主部门 deptId," + "最后打印该部门的详情。只读,不做任何修改。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/overdue.go b/internal/shortcut/smart/overdue.go index 763a0310..159fd8fb 100644 --- a/internal/shortcut/smart/overdue.go +++ b/internal/shortcut/smart/overdue.go @@ -14,11 +14,11 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strings" "time" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -49,7 +49,7 @@ var Overdue = shortcut.Shortcut{ "最后只打印这些逾期待办的标题(subject)、截止时间(dueTime) 和任务 ID(taskId)。" + "这是纯只读操作,只做列表与本地过滤,不会修改或完成任何待办;若没有逾期待办则返回空列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/recent_mail.go b/internal/shortcut/smart/recent_mail.go index 0ebeffdf..a2e9da00 100644 --- a/internal/shortcut/smart/recent_mail.go +++ b/internal/shortcut/smart/recent_mail.go @@ -14,8 +14,8 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -58,7 +58,7 @@ var RecentMail = shortcut.Shortcut{ "最后在本地把每条会话投影成 {subject, from, date, threadId} 打印出来,可配合 --format/--jq/--fields。" + "这是纯只读操作,只做列举与本地投影,不会修改、发送或删除任何邮件;若最近没有邮件则提示「最近没有邮件」。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/record_share_links.go b/internal/shortcut/smart/record_share_links.go index 942fc2f2..a21e5d40 100644 --- a/internal/shortcut/smart/record_share_links.go +++ b/internal/shortcut/smart/record_share_links.go @@ -14,8 +14,8 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -48,7 +48,7 @@ var RecordShareLinks = shortcut.Shortcut{ "最后把各批返回的 {recordId, shareUrl} 合并成一个列表;某一批失败会记录错误但不影响其余批。" + "这是只读操作,只生成/获取分享链接、不修改记录。可选 --view-id 生成带视图上下文的链接。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/remind.go b/internal/shortcut/smart/remind.go index 20eeb534..a59f0bd8 100644 --- a/internal/shortcut/smart/remind.go +++ b/internal/shortcut/smart/remind.go @@ -15,10 +15,10 @@ package smart import ( "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "time" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -41,7 +41,7 @@ var Remind = shortcut.Shortcut{ Intent: "当你想给自己记一件事、并(可选)设一个截止时间,又不想先查自己的 userId 时使用;" + "内部先解析当前登录用户的 userId,再显式设置 executorIds,--at 只会按 ISO8601 写入截止时间 dueTime,不会创建独立提醒规则。会真实创建待办。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/shortcut/smart/replace_batch.go b/internal/shortcut/smart/replace_batch.go index 8a194399..3a1ed1cf 100644 --- a/internal/shortcut/smart/replace_batch.go +++ b/internal/shortcut/smart/replace_batch.go @@ -15,10 +15,10 @@ package smart import ( "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -45,7 +45,7 @@ var ReplaceBatch = shortcut.Shortcut{ "内部按多个 --pair \"原文=>替换\" 逐组调用替换工具,先在本地校验去重(同一个「原文」不能出现两次,避免两条规则互相打架)," + "再逐组应用并聚合每组的成功/失败结果,某一组失败不会中断其余组。这是写操作,会实际修改听记文字内容,请确认 taskUuid 与替换规则无误。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/shortcut/smart/reschedule.go b/internal/shortcut/smart/reschedule.go index daf36d68..be9a643c 100644 --- a/internal/shortcut/smart/reschedule.go +++ b/internal/shortcut/smart/reschedule.go @@ -14,10 +14,10 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -43,7 +43,7 @@ var Reschedule = shortcut.Shortcut{ "如果 eventId 查不到会直接报错,不会误改别的日程。" + "会真实修改该日程的时间。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/shortcut/smart/resolve_base.go b/internal/shortcut/smart/resolve_base.go index dc2c63eb..dee6e58f 100644 --- a/internal/shortcut/smart/resolve_base.go +++ b/internal/shortcut/smart/resolve_base.go @@ -14,8 +14,8 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -46,7 +46,7 @@ var ResolveBase = shortcut.Shortcut{ "如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。" + "这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/resolve_dept.go b/internal/shortcut/smart/resolve_dept.go index c3049b80..616238f5 100644 --- a/internal/shortcut/smart/resolve_dept.go +++ b/internal/shortcut/smart/resolve_dept.go @@ -15,11 +15,11 @@ package smart import ( "encoding/json" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strconv" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -51,7 +51,7 @@ var ResolveDept = shortcut.Shortcut{ "如果只命中一个部门就直接返回它的 deptId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。" + "这是纯只读操作,只做搜索与本地投影,不会修改任何部门,也不会列出部门成员。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/resolve_table.go b/internal/shortcut/smart/resolve_table.go index 8060a776..4a86ff0c 100644 --- a/internal/shortcut/smart/resolve_table.go +++ b/internal/shortcut/smart/resolve_table.go @@ -14,11 +14,11 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strconv" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -53,7 +53,7 @@ var ResolveTable = shortcut.Shortcut{ "如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。" + "这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/search_mail.go b/internal/shortcut/smart/search_mail.go index 7651efa7..5eaeae11 100644 --- a/internal/shortcut/smart/search_mail.go +++ b/internal/shortcut/smart/search_mail.go @@ -14,8 +14,8 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -50,7 +50,7 @@ var SearchMail = shortcut.Shortcut{ "最后在本地把每封邮件投影成 {subject, from, date, messageId} 打印出来,可配合 --format/--jq/--fields。" + "这是纯只读操作,只做搜索与本地投影,不会修改、发送或删除任何邮件;若没有命中则返回空列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/search_msg.go b/internal/shortcut/smart/search_msg.go index 9ffc31c3..e20f8650 100644 --- a/internal/shortcut/smart/search_msg.go +++ b/internal/shortcut/smart/search_msg.go @@ -15,12 +15,12 @@ package smart import ( "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strconv" "strings" "time" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" chatshortcut "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chat" @@ -41,7 +41,7 @@ var SearchMsg = shortcut.Shortcut{ "也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。" + "--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/send_to_group.go b/internal/shortcut/smart/send_to_group.go index 7372d34b..d9ba26da 100644 --- a/internal/shortcut/smart/send_to_group.go +++ b/internal/shortcut/smart/send_to_group.go @@ -16,10 +16,10 @@ package smart import ( "encoding/json" "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -44,7 +44,7 @@ var SendToGroup = shortcut.Shortcut{ Intent: "当你只知道群的名字、想直接往这个群里发一条消息而不想先手动查群 ID 时使用;" + "内部先按群名搜索群聊解析出唯一 openConversationId 再发送,群名匹配到多个群时会列出候选让你区分、绝不自行假定。会真实发出群消息。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/shortcut/smart/share_doc.go b/internal/shortcut/smart/share_doc.go index 5468fc96..0b24f786 100644 --- a/internal/shortcut/smart/share_doc.go +++ b/internal/shortcut/smart/share_doc.go @@ -15,10 +15,10 @@ package smart import ( "encoding/json" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -41,7 +41,7 @@ var ShareDoc = shortcut.Shortcut{ "内部先按姓名搜通讯录解析出唯一用户,再用 openDingTalkId 把链接拼成一条 Markdown 消息发出去," + "姓名匹配到多人时会列出候选让你区分。只发链接、不读取或改动文档本身,会真实发出消息。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/shortcut/smart/suggest_time.go b/internal/shortcut/smart/suggest_time.go index e96775c3..9c2d0bb8 100644 --- a/internal/shortcut/smart/suggest_time.go +++ b/internal/shortcut/smart/suggest_time.go @@ -14,10 +14,10 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -40,7 +40,7 @@ var SuggestTime = shortcut.Shortcut{ "内部会把 --with 里的姓名逐个搜通讯录解析成唯一 userId(任何一个没匹配到或匹配到多人都会明确报出来,绝不瞎猜)," + "再基于所有参与者的忙闲,在给定时间范围内推荐若干可用时段。只读,不创建任何日程。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/team.go b/internal/shortcut/smart/team.go index ddbe5bd4..a4d3692e 100644 --- a/internal/shortcut/smart/team.go +++ b/internal/shortcut/smart/team.go @@ -15,10 +15,10 @@ package smart import ( "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strconv" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -45,7 +45,7 @@ var Team = shortcut.Shortcut{ "内部先按姓名解析出唯一 userId,再取 TA 的组织信息拿到主部门 deptId," + "最后打印该部门的直接成员列表(仅本部门,不递归下级部门)。只读,不做任何修改。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/this_month_attendance.go b/internal/shortcut/smart/this_month_attendance.go index a5309869..ed7173fc 100644 --- a/internal/shortcut/smart/this_month_attendance.go +++ b/internal/shortcut/smart/this_month_attendance.go @@ -14,10 +14,10 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "time" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -48,7 +48,7 @@ var ThisMonthAttendance = shortcut.Shortcut{ "再手动填写本月的起止时间时使用;内部先取当前登录用户的 userId,再按本地时区算出本月 1 号 00:00 到下月 1 号 00:00 的时间窗," + "最后查询你本月的打卡流水记录。只读操作,不会修改任何考勤数据;本月若还没有任何打卡则返回空结果。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/thread_replies.go b/internal/shortcut/smart/thread_replies.go index 58d56b98..1925e7f2 100644 --- a/internal/shortcut/smart/thread_replies.go +++ b/internal/shortcut/smart/thread_replies.go @@ -14,8 +14,8 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" chatshortcut "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chat" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chatmsg" @@ -46,7 +46,7 @@ var ThreadReplies = shortcut.Shortcut{ "可选 --time 指定起始时间、--limit 指定每页条数,再在本地投影出每条回复的发言人、文本和回复时间。" + "默认只读且不会发送或修改任何消息;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/today.go b/internal/shortcut/smart/today.go index 25af2c69..70289aa3 100644 --- a/internal/shortcut/smart/today.go +++ b/internal/shortcut/smart/today.go @@ -14,8 +14,8 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -41,7 +41,7 @@ var Today = shortcut.Shortcut{ "内部用本地时区自动把时间范围算成今天 00:00 到次日 00:00,转成毫秒时间戳," + "查询主日历(primary)下今天的全部日程。只读,不会创建或修改任何日程。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/todo_done.go b/internal/shortcut/smart/todo_done.go index 90a0a55f..700ec3bd 100644 --- a/internal/shortcut/smart/todo_done.go +++ b/internal/shortcut/smart/todo_done.go @@ -15,10 +15,10 @@ package smart import ( "fmt" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -47,7 +47,7 @@ var TodoDone = shortcut.Shortcut{ "内部先拉取你当前组织下作为执行人的待办列表,按标题(subject)包含关键词匹配:没匹配到会提示「没找到匹配待办」," + "匹配到多条会列出候选(标题+taskId)让你写得更精确,唯一命中时才把它标记为已完成。这会真实修改待办完成状态。", Risk: shortcut.RiskWrite, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, diff --git a/internal/shortcut/smart/tomorrow.go b/internal/shortcut/smart/tomorrow.go index 0faf292a..1466c031 100644 --- a/internal/shortcut/smart/tomorrow.go +++ b/internal/shortcut/smart/tomorrow.go @@ -14,8 +14,8 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -40,7 +40,7 @@ var Tomorrow = shortcut.Shortcut{ "内部用本地时区自动把时间范围算成明天 00:00 到后天 00:00,转成毫秒时间戳," + "查询主日历(primary)下明天的全部日程,并投影出标题、开始时间、结束时间、地点、eventId。只读,不会创建或修改任何日程。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/unread_chats.go b/internal/shortcut/smart/unread_chats.go index 97397009..bc14e0f7 100644 --- a/internal/shortcut/smart/unread_chats.go +++ b/internal/shortcut/smart/unread_chats.go @@ -14,11 +14,11 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strconv" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -52,7 +52,7 @@ var UnreadChats = shortcut.Shortcut{ "用 --exclude-muted 排除你已设置免打扰的会话;再在本地把每个会话投影成会话名、未读数和会话 ID 三个关键字段。" + "这是纯只读操作,只做列表与本地投影,不会把任何会话标记为已读或未读;若没有未读会话则返回空列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/unread_mail.go b/internal/shortcut/smart/unread_mail.go index 0d57b86e..0111715e 100644 --- a/internal/shortcut/smart/unread_mail.go +++ b/internal/shortcut/smart/unread_mail.go @@ -14,8 +14,8 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -50,7 +50,7 @@ var UnreadMail = shortcut.Shortcut{ "最后在本地把每封邮件投影成 {subject, from, date, messageId} 打印出来,可配合 --format/--jq/--fields。" + "这是纯只读操作,只做搜索与本地投影,不会把邮件标记为已读,也不会修改、发送或删除任何邮件;若没有未读邮件则返回空列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/week.go b/internal/shortcut/smart/week.go index 01f2b7c2..42a17f9a 100644 --- a/internal/shortcut/smart/week.go +++ b/internal/shortcut/smart/week.go @@ -14,10 +14,10 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "time" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -45,7 +45,7 @@ var Week = shortcut.Shortcut{ "内部用本地时区、以周一为一周开始,自动把时间范围算成本周一 00:00 到下周一 00:00,转成毫秒时间戳," + "查询主日历(primary)下本周的全部日程,并投影出标题、开始时间、结束时间、eventId。只读,不会创建或修改任何日程。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/smart/whoami.go b/internal/shortcut/smart/whoami.go index 29da7b47..ee586ac5 100644 --- a/internal/shortcut/smart/whoami.go +++ b/internal/shortcut/smart/whoami.go @@ -14,8 +14,8 @@ package smart import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -37,7 +37,7 @@ var Whoami = shortcut.Shortcut{ "内部调用零参数的 get_current_user_profile(永远是「我」,无需传姓名),再把冗长的原始资料投影成 {name,userId,mobile,dept,org,email} 几个关键字段。" + "这是纯只读操作,不修改任何资料。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/todo/todo.go b/internal/shortcut/todo/todo.go index 83584344..7ed4161d 100644 --- a/internal/shortcut/todo/todo.go +++ b/internal/shortcut/todo/todo.go @@ -17,10 +17,10 @@ package todo import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "strconv" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -34,7 +34,7 @@ var GetMyTasks = shortcut.Shortcut{ Description: "查询当前组织下我的待办列表", Intent: "当你想查看自己在当前组织下的待办清单、盘点未完成事项或按条件筛选任务时使用;可按完成状态、优先级、角色(创建者/执行者/参与者)和截止时间范围过滤并分页,返回匹配的待办列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -139,7 +139,7 @@ var ListSub = shortcut.Shortcut{ Description: "查询子待办列表", Intent: "当你已知某个待办任务 ID、想了解它被拆解出的所有子任务时使用;输入父任务 ID,返回其下的子待办列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -248,7 +248,7 @@ var Get = shortcut.Shortcut{ Description: "查询待办详情", Intent: "当你已知某条待办的任务 ID、想查看它的完整信息(标题、执行者、截止时间、优先级、状态等)时使用;输入任务 ID,返回该待办的详细内容。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -290,7 +290,7 @@ var ListAttachment = shortcut.Shortcut{ Description: "查询待办任务的附件列表", Intent: "当你想查看某条待办上挂了哪些附件、或需要拿到附件 ID 以便后续删除时使用;输入任务 ID,返回该待办的附件列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, @@ -401,7 +401,7 @@ var ListComment = shortcut.Shortcut{ Description: "查询待办评论列表", Intent: "当你想查看某条待办下的讨论记录、了解协作沟通历史或获取评论 ID 以便删除时使用;输入任务 ID 并可分页,返回该待办的评论列表。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/internal/shortcut/wiki/wiki.go b/internal/shortcut/wiki/wiki.go index cb8318de..79f1bad4 100644 --- a/internal/shortcut/wiki/wiki.go +++ b/internal/shortcut/wiki/wiki.go @@ -17,8 +17,8 @@ package wiki import ( - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" ) @@ -134,7 +134,7 @@ var SpaceSearch = shortcut.Shortcut{ Description: "搜索知识库", Intent: "当你只记得知识库名称的部分关键词、想快速按名称定位某个知识库时使用;输入关键词返回匹配的知识库列表,比逐页 +space-list 更快找到目标 workspaceId。", Risk: shortcut.RiskRead, - Safety: cli.SafetySpec{ + Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, diff --git a/scripts/policy/check-runtime-confirmation-truth.sh b/scripts/policy/check-runtime-confirmation-truth.sh index b8ff2900..2e64fd0c 100755 --- a/scripts/policy/check-runtime-confirmation-truth.sh +++ b/scripts/policy/check-runtime-confirmation-truth.sh @@ -1,11 +1,16 @@ #!/bin/sh set -eu -# Ensure catalog confirmation=user_required exactly matches executable truth: -# typed corecmd Contract SafetySpec declarations. +# Ensure confirmation=user_required matches live typed Contract SafetySpec and +# that every such leaf has an executable confirmation gate (DeclareLeafMetadata, +# Sheet protect marker, or framework ConfirmSafety / RunE). # -# schema_hints/ is retired; residual metadata runtime_gate overlays are gone. -# Contract SafetySpec is the sole production truth source for confirmation. +# Do NOT compare Catalog fields to Catalog provenance labels: that is a tautology +# (both sides read the same embedded snapshot). The real homology gate is +# TestUserRequiredSafetyHomologyWithRuntimeGate in +# internal/cli/contract_safety_homology_external_test.go — it walks the live +# Cobra tree, reads ContractFinal.Safety, compares to AssembleSchemaRegistry +# ToolSpec.Confirmation, and probes the runtime gate. ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)" cd "$ROOT" @@ -15,34 +20,8 @@ if [ -e internal/cli/schema_hints ]; then exit 1 fi -tmp="$(mktemp -d)" -trap 'rm -rf "$tmp"' EXIT HUP INT TERM +go test ./internal/cli \ + -run '^TestUserRequiredSafetyHomologyWithRuntimeGate$' \ + -count=1 -# The release Catalog is committed as a per-product split; reassemble it into -# the single-document shape these jq queries consume. -catalog="$tmp/catalog-combined.json" -scripts/policy/with-catalog.sh >"$catalog" - -jq -r ' - .tools - | to_entries[] - | select(.value.confirmation == "user_required") - | select(.value.field_provenance.confirmation.source == "corecmd.contract") - | .key -' "$catalog" | sort -u >"$tmp/truth_gated" - -jq -r ' - .tools - | to_entries[] - | select(.value.confirmation == "user_required") - | .key -' "$catalog" | sort >"$tmp/catalog_required" - -if ! cmp -s "$tmp/truth_gated" "$tmp/catalog_required"; then - printf '%s\n' 'catalog confirmation=user_required differs from Contract SafetySpec truth' >&2 - printf '%s\n' 'declare corecmd Safety.Confirmation, then regenerate schema' >&2 - diff -u "$tmp/truth_gated" "$tmp/catalog_required" || true - exit 1 -fi - -printf '%s\n' "runtime confirmation truth ok ($(wc -l <"$tmp/truth_gated" | tr -d ' ') gated)" +printf '%s\n' 'runtime confirmation truth ok (live Contract SafetySpec homology)' diff --git a/scripts/policy/check-schema-catalog.sh b/scripts/policy/check-schema-catalog.sh index fda9f85e..cc5c9d02 100755 --- a/scripts/policy/check-schema-catalog.sh +++ b/scripts/policy/check-schema-catalog.sh @@ -241,14 +241,18 @@ if policy_search_go '\.ListTools\(' internal/app internal/cli; then exit 1 fi -# Gated confirmation truth is Contract SafetySpec only (schema_hints retired). +# Gated confirmation truth: live Contract SafetySpec ↔ ToolSpec ↔ runtime gate +# (not Catalog-vs-Catalog provenance). Invokes +# TestUserRequiredSafetyHomologyWithRuntimeGate; also listed in the focused +# ./internal/cli -run whitelist below so a direct schema-catalog run stays +# complete if the helper script is skipped. ./scripts/policy/check-runtime-confirmation-truth.sh # Run the typed content gates as policy, rather than treating non-empty # correction/exclusion maps as proof that their exact keys and winners are # valid against the shipped Catalog and pinned MCP metadata. go test ./internal/cli \ - -run '^(TestEmbeddedSchemaCatalog.*|TestEmbeddedSchemaAllPayload.*|TestRuntimeSchemaAllPayload.*|TestSchemaAllReturnsCompleteEmbeddedLeafSchemas|TestSchemaCatalogDeliveryCompleteness.*|TestValidateSchemaDeliveryInvariants.*|TestSchemaAliasViewProblem.*|TestSchemaDeliveryToolsByCanonical.*|TestSchemaUsesEmbeddedCatalogWithoutRuntimeLoad|TestWalkLeafCommandsTraversesAnnotatedHiddenSubtree|TestSchemaParameterBindingsMatchReviewedBaselineAndEmbeddedCatalog|TestDecodeSchemaParameterBindingsFailsClosed|TestSchemaParameterBindingManifestHashIsExactContentNotCount|TestBuildEffectiveCommandRegistryFailsClosedOnInvalidParameterBindingSource|TestValidateSchemaParameterBindingDeliveryRejectsStaleReviewedKeys|TestEmbeddedCatalogMCPParameterMappingsAreComplete|TestSchemaParameterMappingAuditExclusionRules|TestRuntimeSchemaReviewedMappingExclusionSelectsEmptyProperty|TestRuntimeCommandParameterSpecsPreserveReviewedEmptyPropertyProvenance|TestSchemaParameterBindingCorrectionsAreReviewed)$' \ + -run '^(TestEmbeddedSchemaCatalog.*|TestEmbeddedSchemaAllPayload.*|TestRuntimeSchemaAllPayload.*|TestSchemaAllReturnsCompleteEmbeddedLeafSchemas|TestSchemaCatalogDeliveryCompleteness.*|TestValidateSchemaDeliveryInvariants.*|TestSchemaAliasViewProblem.*|TestSchemaDeliveryToolsByCanonical.*|TestSchemaUsesEmbeddedCatalogWithoutRuntimeLoad|TestWalkLeafCommandsTraversesAnnotatedHiddenSubtree|TestSchemaParameterBindingsMatchReviewedBaselineAndEmbeddedCatalog|TestDecodeSchemaParameterBindingsFailsClosed|TestSchemaParameterBindingManifestHashIsExactContentNotCount|TestBuildEffectiveCommandRegistryFailsClosedOnInvalidParameterBindingSource|TestValidateSchemaParameterBindingDeliveryRejectsStaleReviewedKeys|TestEmbeddedCatalogMCPParameterMappingsAreComplete|TestSchemaParameterMappingAuditExclusionRules|TestRuntimeSchemaReviewedMappingExclusionSelectsEmptyProperty|TestRuntimeCommandParameterSpecsPreserveReviewedEmptyPropertyProvenance|TestSchemaParameterBindingCorrectionsAreReviewed|TestUserRequiredSafetyHomologyWithRuntimeGate)$' \ -count=1 go test ./internal/helpers \ -run '^TestSheetConfirmationGuardCoversEveryProtectedLeaf$' \