Merge remote-tracking branch 'origin/main' into codex/fix-pr-592-merge-gates
# Conflicts: # internal/cli/schema_agent_metadata/index.json # internal/cli/schema_agent_metadata_audit.json # internal/cli/schema_catalog.json
This commit is contained in:
commit
6c192c2bf4
57 files changed
+5892
-419
No files matched your search
@@ -298,9 +298,6 @@ jobs:
|
||||
- name: Policy
|
||||
run: make policy
|
||||
|
||||
- name: Generated Drift
|
||||
run: ./scripts/policy/check-generated-drift.sh
|
||||
|
||||
interface-integrity:
|
||||
name: Interface Integrity
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -8,6 +8,7 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th
|
||||
|
||||
### Added
|
||||
|
||||
- **Sheet imports and Aitable workflow writes** (#624) — adds `dws sheet import` / `sheet import create` for converting local xlsx/xls files into new online sheets, `sheet import get` for polling import tasks, and `dws aitable workflow create/update` for applying validated `workflow-dsl/v1` definitions, with matching reviewed Agent Schema and bundled Skill guidance.
|
||||
- **Official multi-platform Homebrew channel** — stable `Formula/dingtalk-workspace-cli.rb` and keg-only `Formula/dingtalk-workspace-cli-beta.rb` live in this repository and select signed macOS Intel/Apple Silicon or Linux amd64/arm64 artifacts at install time. Stable and beta releases open isolated Formula update PRs after final artifact signing, so beta never replaces the stable Formula. Agent Skills stay under `pkgshare` without mutating the user's home directory, and both tracks are covered by the six-channel post-release verifier.
|
||||
|
||||
## [1.0.52] - 2026-07-14
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
GO ?= go
|
||||
DWS_POLICY_TMPDIR ?= $(CURDIR)/.worktrees/policy-tmp
|
||||
POLICY_GOTMPDIR ?= $(DWS_POLICY_TMPDIR)/go
|
||||
POLICY_ENV = DWS_POLICY_TMPDIR="$(DWS_POLICY_TMPDIR)" GOTMPDIR="$(POLICY_GOTMPDIR)"
|
||||
|
||||
.PHONY: all help build rebuild test lint fmt policy edition-test interface-integrity authoritative-interface-integrity coverage-gate coverage-gate-platform update-interface-baseline reset-interface-baseline schema-compatibility skill-command-integrity cli-smoke mock-mcp-smoke test-schema-agent-examples generate-schema generate-schema-agent-metadata generate-schema-catalog package release publish-homebrew-formula setup-hooks
|
||||
|
||||
@@ -10,7 +13,7 @@ help:
|
||||
@printf " make test - Run the Go test suite\n"
|
||||
@printf " make lint - Run formatting checks and golangci-lint when available\n"
|
||||
@printf " make fmt - Format Go source files\n"
|
||||
@printf " make policy - Run open-source asset and Schema registry checks\n"
|
||||
@printf " make policy - Check the built dws plus open-source and Schema policies\n"
|
||||
@printf " make interface-integrity - Check historical commands and help contracts still work\n"
|
||||
@printf " make authoritative-interface-integrity BASE_REF=<ref> - Check the Git-owned PR merge-base\n"
|
||||
@printf " make coverage-gate BASE_REF=<ref> - Enforce overall non-regression and changed-code coverage\n"
|
||||
@@ -45,13 +48,14 @@ fmt:
|
||||
@find cmd internal test scripts/policy -name '*.go' -print0 2>/dev/null | xargs -0r gofmt -w
|
||||
|
||||
policy:
|
||||
@./scripts/policy/check-open-source-assets.sh
|
||||
@./scripts/policy/check-schema-command-registry.sh
|
||||
@./scripts/policy/check-command-surface.sh --strict
|
||||
@./scripts/policy/check-generated-drift.sh
|
||||
@./scripts/policy/check-schema-catalog.sh
|
||||
@./scripts/policy/check-schema-binary.sh
|
||||
@$(MAKE) test-schema-agent-examples
|
||||
@mkdir -p "$(POLICY_GOTMPDIR)"
|
||||
@$(POLICY_ENV) ./scripts/policy/check-open-source-assets.sh
|
||||
@$(POLICY_ENV) ./scripts/policy/check-schema-command-registry.sh
|
||||
@$(POLICY_ENV) ./scripts/policy/check-command-surface.sh --strict
|
||||
@$(POLICY_ENV) ./scripts/policy/check-generated-drift.sh
|
||||
@$(POLICY_ENV) ./scripts/policy/check-schema-catalog.sh
|
||||
@$(POLICY_ENV) ./scripts/policy/check-schema-binary.sh
|
||||
@$(POLICY_ENV) $(MAKE) test-schema-agent-examples
|
||||
|
||||
edition-test:
|
||||
$(GO) test -v -count=1 ./pkg/editiontest/...
|
||||
|
||||
@@ -29017,6 +29017,288 @@
|
||||
"用户要移动字段/调整列顺序时必须走本命令"
|
||||
]
|
||||
},
|
||||
"aitable workflow create": {
|
||||
"agent_summary": "创建并发布 AI 表格自动化工作流。",
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"已有工作流要修改时用 workflow update;仅启停已有工作流用 enable/disable;请求结果不确定时先 list 核对,避免非幂等重复创建"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "write",
|
||||
"effect_source": "agent-hint",
|
||||
"examples": [
|
||||
"dws aitable workflow create --base-id \u003cBASE_ID\u003e --dsl @workflow.json --locale zh-CN"
|
||||
],
|
||||
"field_provenance": {
|
||||
"agent_summary": {
|
||||
"value": "创建并发布 AI 表格自动化工作流。",
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "依据真实 Cobra leaf、Wukong 同步契约和 Aitable workflow Skill 审阅创建语义,强调完整 DSL、结果 valid/issues 校验及非幂等边界。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "创建并发布 AI 表格自动化工作流。",
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "依据真实 Cobra leaf、Wukong 同步契约和 Aitable workflow Skill 审阅创建语义,强调完整 DSL、结果 valid/issues 校验及非幂等边界。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"availability": {
|
||||
"value": "available",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command maps --base-id and a decoded workflow-dsl/v1 --dsl object to aitable/create_workflow. The call intentionally bypasses retries because creation is non-idempotent; locale remains optional.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "available",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "The command maps --base-id and a decoded workflow-dsl/v1 --dsl object to aitable/create_workflow. The call intentionally bypasses retries because creation is non-idempotent; locale remains optional."
|
||||
}
|
||||
]
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"已有工作流要修改时用 workflow update;仅启停已有工作流用 enable/disable;请求结果不确定时先 list 核对,避免非幂等重复创建"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "依据真实 Cobra leaf、Wukong 同步契约和 Aitable workflow Skill 审阅创建语义,强调完整 DSL、结果 valid/issues 校验及非幂等边界。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"已有工作流要修改时用 workflow update;仅启停已有工作流用 enable/disable;请求结果不确定时先 list 核对,避免非幂等重复创建"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "依据真实 Cobra leaf、Wukong 同步契约和 Aitable workflow Skill 审阅创建语义,强调完整 DSL、结果 valid/issues 校验及非幂等边界。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"confirmation": {
|
||||
"value": "not_required",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command maps --base-id and a decoded workflow-dsl/v1 --dsl object to aitable/create_workflow. The call intentionally bypasses retries because creation is non-idempotent; locale remains optional.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "not_required",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "The command maps --base-id and a decoded workflow-dsl/v1 --dsl object to aitable/create_workflow. The call intentionally bypasses retries because creation is non-idempotent; locale remains optional."
|
||||
},
|
||||
{
|
||||
"value": "not_required",
|
||||
"source": "risk-default",
|
||||
"precedence": "inference_or_default",
|
||||
"selected": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"effect": {
|
||||
"value": "write",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command maps --base-id and a decoded workflow-dsl/v1 --dsl object to aitable/create_workflow. The call intentionally bypasses retries because creation is non-idempotent; locale remains optional.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "write",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "The command maps --base-id and a decoded workflow-dsl/v1 --dsl object to aitable/create_workflow. The call intentionally bypasses retries because creation is non-idempotent; locale remains optional."
|
||||
},
|
||||
{
|
||||
"value": "write",
|
||||
"source": "command-verb",
|
||||
"precedence": "inference_or_default",
|
||||
"selected": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"examples": {
|
||||
"value": [
|
||||
"dws aitable workflow create --base-id \u003cBASE_ID\u003e --dsl @workflow.json --locale zh-CN"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "依据真实 Cobra leaf、Wukong 同步契约和 Aitable workflow Skill 审阅创建语义,强调完整 DSL、结果 valid/issues 校验及非幂等边界。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"dws aitable workflow create --base-id \u003cBASE_ID\u003e --dsl @workflow.json --locale zh-CN"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "依据真实 Cobra leaf、Wukong 同步契约和 Aitable workflow Skill 审阅创建语义,强调完整 DSL、结果 valid/issues 校验及非幂等边界。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"idempotency": {
|
||||
"value": "non_idempotent",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command maps --base-id and a decoded workflow-dsl/v1 --dsl object to aitable/create_workflow. The call intentionally bypasses retries because creation is non-idempotent; locale remains optional.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "non_idempotent",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "The command maps --base-id and a decoded workflow-dsl/v1 --dsl object to aitable/create_workflow. The call intentionally bypasses retries because creation is non-idempotent; locale remains optional."
|
||||
},
|
||||
{
|
||||
"value": "unknown",
|
||||
"source": "effect-default",
|
||||
"precedence": "inference_or_default",
|
||||
"selected": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"interface_mode": {
|
||||
"value": "composite",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command maps --base-id and a decoded workflow-dsl/v1 --dsl object to aitable/create_workflow. The call intentionally bypasses retries because creation is non-idempotent; locale remains optional.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "composite",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "The command maps --base-id and a decoded workflow-dsl/v1 --dsl object to aitable/create_workflow. The call intentionally bypasses retries because creation is non-idempotent; locale remains optional."
|
||||
}
|
||||
]
|
||||
},
|
||||
"interface_reason": {
|
||||
"value": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command maps --base-id and a decoded workflow-dsl/v1 --dsl object to aitable/create_workflow. The call intentionally bypasses retries because creation is non-idempotent; locale remains optional.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "The command maps --base-id and a decoded workflow-dsl/v1 --dsl object to aitable/create_workflow. The call intentionally bypasses retries because creation is non-idempotent; locale remains optional."
|
||||
}
|
||||
]
|
||||
},
|
||||
"interface_ref": {
|
||||
"value": null,
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "interface_disposition_matrix",
|
||||
"review_reason": "final interface mode composite forbids a direct MCP interface_ref",
|
||||
"candidates": [
|
||||
{
|
||||
"value": null,
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "final interface mode composite forbids a direct MCP interface_ref"
|
||||
}
|
||||
]
|
||||
},
|
||||
"reviewed": {
|
||||
"value": true,
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command maps --base-id and a decoded workflow-dsl/v1 --dsl object to aitable/create_workflow. The call intentionally bypasses retries because creation is non-idempotent; locale remains optional.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": true,
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "The command maps --base-id and a decoded workflow-dsl/v1 --dsl object to aitable/create_workflow. The call intentionally bypasses retries because creation is non-idempotent; locale remains optional."
|
||||
},
|
||||
{
|
||||
"value": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": false,
|
||||
"review_reason": "依据真实 Cobra leaf、Wukong 同步契约和 Aitable workflow Skill 审阅创建语义,强调完整 DSL、结果 valid/issues 校验及非幂等边界。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"risk": {
|
||||
"value": "medium",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command maps --base-id and a decoded workflow-dsl/v1 --dsl object to aitable/create_workflow. The call intentionally bypasses retries because creation is non-idempotent; locale remains optional.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "medium",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "The command maps --base-id and a decoded workflow-dsl/v1 --dsl object to aitable/create_workflow. The call intentionally bypasses retries because creation is non-idempotent; locale remains optional."
|
||||
},
|
||||
{
|
||||
"value": "medium",
|
||||
"source": "effect-default",
|
||||
"precedence": "inference_or_default",
|
||||
"selected": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"用户明确要求在已知 Base 中创建自动化,且已准备完整 workflow-dsl/v1 定义时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "依据真实 Cobra leaf、Wukong 同步契约和 Aitable workflow Skill 审阅创建语义,强调完整 DSL、结果 valid/issues 校验及非幂等边界。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"用户明确要求在已知 Base 中创建自动化,且已准备完整 workflow-dsl/v1 定义时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "依据真实 Cobra leaf、Wukong 同步契约和 Aitable workflow Skill 审阅创建语义,强调完整 DSL、结果 valid/issues 校验及非幂等边界。"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"idempotency": "non_idempotent",
|
||||
"interface_mode": "composite",
|
||||
"interface_reason": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
|
||||
"reviewed": true,
|
||||
"risk": "medium",
|
||||
"source_refs": [
|
||||
"cobra-help:dws aitable workflow create --help",
|
||||
"internal/cli/schema_command_registry.json#aitable.workflow_create",
|
||||
"internal/cli/schema_hints/metadata/aitable.json",
|
||||
"internal/cli/schema_hints/selection/aitable.json",
|
||||
"skills/mono/references/products/aitable/aitable-workflow.md"
|
||||
],
|
||||
"use_when": [
|
||||
"用户明确要求在已知 Base 中创建自动化,且已准备完整 workflow-dsl/v1 定义时"
|
||||
]
|
||||
},
|
||||
"aitable workflow disable": {
|
||||
"agent_summary": "禁用工作流(高危,需确认;status 变 STOP)。",
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
@@ -30072,6 +30354,288 @@
|
||||
"use_when": [
|
||||
"查看有哪些工作流及 status/flowId 时"
|
||||
]
|
||||
},
|
||||
"aitable workflow update": {
|
||||
"agent_summary": "用完整 DSL 更新并发布已有 AI 表格自动化工作流。",
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"新建工作流用 create;只改变运行状态用 enable/disable;不要把局部 patch 或 workflow get 返回的 flowSchema 直接当作 DSL"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "write",
|
||||
"effect_source": "agent-hint",
|
||||
"examples": [
|
||||
"dws aitable workflow update --base-id \u003cBASE_ID\u003e --workflow-id \u003cFLOW_ID\u003e --dsl @workflow.json --locale zh-CN"
|
||||
],
|
||||
"field_provenance": {
|
||||
"agent_summary": {
|
||||
"value": "用完整 DSL 更新并发布已有 AI 表格自动化工作流。",
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "依据真实 Cobra leaf、Wukong 同步契约和 Aitable workflow Skill 审阅更新语义,强调全量替换、事前备份和 valid/issues 结果校验。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "用完整 DSL 更新并发布已有 AI 表格自动化工作流。",
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "依据真实 Cobra leaf、Wukong 同步契约和 Aitable workflow Skill 审阅更新语义,强调全量替换、事前备份和 valid/issues 结果校验。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"availability": {
|
||||
"value": "available",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command maps --base-id, --workflow-id and a decoded complete workflow-dsl/v1 --dsl object to aitable/update_workflow. This is full replacement rather than a partial patch; locale remains optional.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "available",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "The command maps --base-id, --workflow-id and a decoded complete workflow-dsl/v1 --dsl object to aitable/update_workflow. This is full replacement rather than a partial patch; locale remains optional."
|
||||
}
|
||||
]
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"新建工作流用 create;只改变运行状态用 enable/disable;不要把局部 patch 或 workflow get 返回的 flowSchema 直接当作 DSL"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "依据真实 Cobra leaf、Wukong 同步契约和 Aitable workflow Skill 审阅更新语义,强调全量替换、事前备份和 valid/issues 结果校验。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"新建工作流用 create;只改变运行状态用 enable/disable;不要把局部 patch 或 workflow get 返回的 flowSchema 直接当作 DSL"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "依据真实 Cobra leaf、Wukong 同步契约和 Aitable workflow Skill 审阅更新语义,强调全量替换、事前备份和 valid/issues 结果校验。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"confirmation": {
|
||||
"value": "not_required",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command maps --base-id, --workflow-id and a decoded complete workflow-dsl/v1 --dsl object to aitable/update_workflow. This is full replacement rather than a partial patch; locale remains optional.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "not_required",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "The command maps --base-id, --workflow-id and a decoded complete workflow-dsl/v1 --dsl object to aitable/update_workflow. This is full replacement rather than a partial patch; locale remains optional."
|
||||
},
|
||||
{
|
||||
"value": "not_required",
|
||||
"source": "risk-default",
|
||||
"precedence": "inference_or_default",
|
||||
"selected": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"effect": {
|
||||
"value": "write",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command maps --base-id, --workflow-id and a decoded complete workflow-dsl/v1 --dsl object to aitable/update_workflow. This is full replacement rather than a partial patch; locale remains optional.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "write",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "The command maps --base-id, --workflow-id and a decoded complete workflow-dsl/v1 --dsl object to aitable/update_workflow. This is full replacement rather than a partial patch; locale remains optional."
|
||||
},
|
||||
{
|
||||
"value": "write",
|
||||
"source": "command-verb",
|
||||
"precedence": "inference_or_default",
|
||||
"selected": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"examples": {
|
||||
"value": [
|
||||
"dws aitable workflow update --base-id \u003cBASE_ID\u003e --workflow-id \u003cFLOW_ID\u003e --dsl @workflow.json --locale zh-CN"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "依据真实 Cobra leaf、Wukong 同步契约和 Aitable workflow Skill 审阅更新语义,强调全量替换、事前备份和 valid/issues 结果校验。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"dws aitable workflow update --base-id \u003cBASE_ID\u003e --workflow-id \u003cFLOW_ID\u003e --dsl @workflow.json --locale zh-CN"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "依据真实 Cobra leaf、Wukong 同步契约和 Aitable workflow Skill 审阅更新语义,强调全量替换、事前备份和 valid/issues 结果校验。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"idempotency": {
|
||||
"value": "unknown",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command maps --base-id, --workflow-id and a decoded complete workflow-dsl/v1 --dsl object to aitable/update_workflow. This is full replacement rather than a partial patch; locale remains optional.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "unknown",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "The command maps --base-id, --workflow-id and a decoded complete workflow-dsl/v1 --dsl object to aitable/update_workflow. This is full replacement rather than a partial patch; locale remains optional."
|
||||
},
|
||||
{
|
||||
"value": "unknown",
|
||||
"source": "effect-default",
|
||||
"precedence": "inference_or_default",
|
||||
"selected": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"interface_mode": {
|
||||
"value": "composite",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command maps --base-id, --workflow-id and a decoded complete workflow-dsl/v1 --dsl object to aitable/update_workflow. This is full replacement rather than a partial patch; locale remains optional.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "composite",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "The command maps --base-id, --workflow-id and a decoded complete workflow-dsl/v1 --dsl object to aitable/update_workflow. This is full replacement rather than a partial patch; locale remains optional."
|
||||
}
|
||||
]
|
||||
},
|
||||
"interface_reason": {
|
||||
"value": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command maps --base-id, --workflow-id and a decoded complete workflow-dsl/v1 --dsl object to aitable/update_workflow. This is full replacement rather than a partial patch; locale remains optional.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "The command maps --base-id, --workflow-id and a decoded complete workflow-dsl/v1 --dsl object to aitable/update_workflow. This is full replacement rather than a partial patch; locale remains optional."
|
||||
}
|
||||
]
|
||||
},
|
||||
"interface_ref": {
|
||||
"value": null,
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "interface_disposition_matrix",
|
||||
"review_reason": "final interface mode composite forbids a direct MCP interface_ref",
|
||||
"candidates": [
|
||||
{
|
||||
"value": null,
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "final interface mode composite forbids a direct MCP interface_ref"
|
||||
}
|
||||
]
|
||||
},
|
||||
"reviewed": {
|
||||
"value": true,
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command maps --base-id, --workflow-id and a decoded complete workflow-dsl/v1 --dsl object to aitable/update_workflow. This is full replacement rather than a partial patch; locale remains optional.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": true,
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "The command maps --base-id, --workflow-id and a decoded complete workflow-dsl/v1 --dsl object to aitable/update_workflow. This is full replacement rather than a partial patch; locale remains optional."
|
||||
},
|
||||
{
|
||||
"value": true,
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": false,
|
||||
"review_reason": "依据真实 Cobra leaf、Wukong 同步契约和 Aitable workflow Skill 审阅更新语义,强调全量替换、事前备份和 valid/issues 结果校验。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"risk": {
|
||||
"value": "medium",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "The command maps --base-id, --workflow-id and a decoded complete workflow-dsl/v1 --dsl object to aitable/update_workflow. This is full replacement rather than a partial patch; locale remains optional.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "medium",
|
||||
"source": "internal/cli/schema_hints/metadata/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "The command maps --base-id, --workflow-id and a decoded complete workflow-dsl/v1 --dsl object to aitable/update_workflow. This is full replacement rather than a partial patch; locale remains optional."
|
||||
},
|
||||
{
|
||||
"value": "medium",
|
||||
"source": "effect-default",
|
||||
"precedence": "inference_or_default",
|
||||
"selected": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"用户明确要求修改已知工作流,且已先留底当前详情并准备完整 workflow-dsl/v1 目标定义时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "依据真实 Cobra leaf、Wukong 同步契约和 Aitable workflow Skill 审阅更新语义,强调全量替换、事前备份和 valid/issues 结果校验。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"用户明确要求修改已知工作流,且已先留底当前详情并准备完整 workflow-dsl/v1 目标定义时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/aitable.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "依据真实 Cobra leaf、Wukong 同步契约和 Aitable workflow Skill 审阅更新语义,强调全量替换、事前备份和 valid/issues 结果校验。"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"idempotency": "unknown",
|
||||
"interface_mode": "composite",
|
||||
"interface_reason": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
|
||||
"reviewed": true,
|
||||
"risk": "medium",
|
||||
"source_refs": [
|
||||
"cobra-help:dws aitable workflow update --help",
|
||||
"internal/cli/schema_command_registry.json#aitable.workflow_update",
|
||||
"internal/cli/schema_hints/metadata/aitable.json",
|
||||
"internal/cli/schema_hints/selection/aitable.json",
|
||||
"skills/mono/references/products/aitable/aitable-workflow.md"
|
||||
],
|
||||
"use_when": [
|
||||
"用户明确要求修改已知工作流,且已先留底当前详情并准备完整 workflow-dsl/v1 目标定义时"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"version": 1,
|
||||
"source_hash": "sha256:84ae56ee689d80254b9f590733f7bcaa77f9c0f94ac9e0e5e6762956b1b102a2",
|
||||
"surface_hash": "sha256:e858be68701d256818eccd085a6f7f97655835b9c7cbbce6275ed930c2436dc3",
|
||||
"source_hash": "sha256:53567141622d90e02ce50fe3bc1733357a25b9860a0795eb396d9361f43c41d1",
|
||||
"surface_hash": "sha256:69e1f9fcd3768d49a2a197c40fb26c0bc07385e868753210761c3ea7f676eadb",
|
||||
"coverage": {
|
||||
"surface_products": 22,
|
||||
"products_with_metadata": 22,
|
||||
"surface_tools": 564,
|
||||
"tools_with_metadata": 564,
|
||||
"tools_with_agent_summary": 564,
|
||||
"tools_with_use_when": 564,
|
||||
"tools_with_avoid_when": 564,
|
||||
"tools_with_examples": 564,
|
||||
"tools_with_interface_mode": 564,
|
||||
"surface_tools": 568,
|
||||
"tools_with_metadata": 568,
|
||||
"tools_with_agent_summary": 568,
|
||||
"tools_with_use_when": 568,
|
||||
"tools_with_avoid_when": 568,
|
||||
"tools_with_examples": 568,
|
||||
"tools_with_interface_mode": 568,
|
||||
"unmatched_skill_tools": 125,
|
||||
"unreviewed_skill_tools": 7
|
||||
},
|
||||
@@ -1371,20 +1371,21 @@
|
||||
]
|
||||
},
|
||||
"sheet": {
|
||||
"agent_summary": "创建、读取、编辑和导出钉钉在线电子表格(axls),并管理工作表、区域、筛选、图表、图片与格式。",
|
||||
"agent_summary": "导入本地 Excel,或创建、读取、编辑和导出钉钉在线电子表格(axls),并管理工作表、区域、筛选、图表、图片与格式。",
|
||||
"agent_summary_source": "dws-agent-selection/sheet",
|
||||
"avoid_when": [
|
||||
"目标是 AI 表格 Base 的结构化记录、钉钉文档正文或本地 Excel 文件时不要使用 sheet"
|
||||
"目标是 AI 表格 Base 的结构化记录或钉钉文档正文时不要使用 sheet",
|
||||
"只读取已上传但未转换的 xlsx/xls 节点时先用 doc download;不要把文件节点直接传给单元格命令"
|
||||
],
|
||||
"field_provenance": {
|
||||
"agent_summary": {
|
||||
"value": "创建、读取、编辑和导出钉钉在线电子表格(axls),并管理工作表、区域、筛选、图表、图片与格式。",
|
||||
"value": "导入本地 Excel,或创建、读取、编辑和导出钉钉在线电子表格(axls),并管理工作表、区域、筛选、图表、图片与格式。",
|
||||
"source": "internal/cli/schema_hints/selection/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "创建、读取、编辑和导出钉钉在线电子表格(axls),并管理工作表、区域、筛选、图表、图片与格式。",
|
||||
"value": "导入本地 Excel,或创建、读取、编辑和导出钉钉在线电子表格(axls),并管理工作表、区域、筛选、图表、图片与格式。",
|
||||
"source": "internal/cli/schema_hints/selection/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true
|
||||
@@ -1393,7 +1394,8 @@
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"目标是 AI 表格 Base 的结构化记录、钉钉文档正文或本地 Excel 文件时不要使用 sheet"
|
||||
"目标是 AI 表格 Base 的结构化记录或钉钉文档正文时不要使用 sheet",
|
||||
"只读取已上传但未转换的 xlsx/xls 节点时先用 doc download;不要把文件节点直接传给单元格命令"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -1401,7 +1403,8 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"目标是 AI 表格 Base 的结构化记录、钉钉文档正文或本地 Excel 文件时不要使用 sheet"
|
||||
"目标是 AI 表格 Base 的结构化记录或钉钉文档正文时不要使用 sheet",
|
||||
"只读取已上传但未转换的 xlsx/xls 节点时先用 doc download;不要把文件节点直接传给单元格命令"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -1411,7 +1414,8 @@
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"用户要处理钉钉在线电子表格中的工作表、单元格、范围、筛选、图表、图片或格式时"
|
||||
"用户要处理钉钉在线电子表格中的工作表、单元格、范围、筛选、图表、图片或格式时",
|
||||
"用户要把本地 xlsx/xls 转换为新的钉钉在线电子表格时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -1419,7 +1423,8 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"用户要处理钉钉在线电子表格中的工作表、单元格、范围、筛选、图表、图片或格式时"
|
||||
"用户要处理钉钉在线电子表格中的工作表、单元格、范围、筛选、图表、图片或格式时",
|
||||
"用户要把本地 xlsx/xls 转换为新的钉钉在线电子表格时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -1440,7 +1445,8 @@
|
||||
"skills/mono/references/products/sheet.md"
|
||||
],
|
||||
"use_when": [
|
||||
"用户要处理钉钉在线电子表格中的工作表、单元格、范围、筛选、图表、图片或格式时"
|
||||
"用户要处理钉钉在线电子表格中的工作表、单元格、范围、筛选、图表、图片或格式时",
|
||||
"用户要把本地 xlsx/xls 转换为新的钉钉在线电子表格时"
|
||||
]
|
||||
},
|
||||
"todo": {
|
||||
|
||||
@@ -11251,6 +11251,581 @@
|
||||
"需要关闭指定工作表的网格线显示时"
|
||||
]
|
||||
},
|
||||
"sheet import create": {
|
||||
"agent_summary": "将本地 xlsx/xls 文件导入为新的钉钉在线电子表格。",
|
||||
"agent_summary_source": "dws-agent-selection/sheet",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"读取或修改已有在线表格时应使用 sheet list/info/range 等命令",
|
||||
"查询已提交导入任务的状态时使用 sheet import get,不要重复创建导入任务"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "write",
|
||||
"effect_source": "agent-hint",
|
||||
"examples": [
|
||||
"dws sheet import create --file ./report.xlsx --folder-token \u003cFOLDER_TOKEN\u003e --format json",
|
||||
"dws sheet import create --file ./data.xls --workspace \u003cWORKSPACE_ID\u003e --name \"月度报表\" --format json"
|
||||
],
|
||||
"field_provenance": {
|
||||
"agent_summary": {
|
||||
"value": "将本地 xlsx/xls 文件导入为新的钉钉在线电子表格。",
|
||||
"source": "internal/cli/schema_hints/selection/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:公开 leaf 与兼容入口 sheet import 复用同一导入流程,使 Agent Schema 能发现并调用真正的导入动作。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "将本地 xlsx/xls 文件导入为新的钉钉在线电子表格。",
|
||||
"source": "internal/cli/schema_hints/selection/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:公开 leaf 与兼容入口 sheet import 复用同一导入流程,使 Agent Schema 能发现并调用真正的导入动作。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"availability": {
|
||||
"value": "available",
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:导入会新建在线电子表格,不修改已有文档;重复执行会创建多个文档,因此标记为 non_idempotent。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "available",
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:导入会新建在线电子表格,不修改已有文档;重复执行会创建多个文档,因此标记为 non_idempotent。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"读取或修改已有在线表格时应使用 sheet list/info/range 等命令",
|
||||
"查询已提交导入任务的状态时使用 sheet import get,不要重复创建导入任务"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:公开 leaf 与兼容入口 sheet import 复用同一导入流程,使 Agent Schema 能发现并调用真正的导入动作。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"读取或修改已有在线表格时应使用 sheet list/info/range 等命令",
|
||||
"查询已提交导入任务的状态时使用 sheet import get,不要重复创建导入任务"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:公开 leaf 与兼容入口 sheet import 复用同一导入流程,使 Agent Schema 能发现并调用真正的导入动作。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"confirmation": {
|
||||
"value": "not_required",
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:导入会新建在线电子表格,不修改已有文档;重复执行会创建多个文档,因此标记为 non_idempotent。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "not_required",
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:导入会新建在线电子表格,不修改已有文档;重复执行会创建多个文档,因此标记为 non_idempotent。"
|
||||
},
|
||||
{
|
||||
"value": "not_required",
|
||||
"source": "risk-default",
|
||||
"precedence": "inference_or_default",
|
||||
"selected": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"effect": {
|
||||
"value": "write",
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:导入会新建在线电子表格,不修改已有文档;重复执行会创建多个文档,因此标记为 non_idempotent。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "write",
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:导入会新建在线电子表格,不修改已有文档;重复执行会创建多个文档,因此标记为 non_idempotent。"
|
||||
},
|
||||
{
|
||||
"value": "write",
|
||||
"source": "command-verb",
|
||||
"precedence": "inference_or_default",
|
||||
"selected": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"examples": {
|
||||
"value": [
|
||||
"dws sheet import create --file ./report.xlsx --folder-token \u003cFOLDER_TOKEN\u003e --format json",
|
||||
"dws sheet import create --file ./data.xls --workspace \u003cWORKSPACE_ID\u003e --name \"月度报表\" --format json"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:公开 leaf 与兼容入口 sheet import 复用同一导入流程,使 Agent Schema 能发现并调用真正的导入动作。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"dws sheet import create --file ./report.xlsx --folder-token \u003cFOLDER_TOKEN\u003e --format json",
|
||||
"dws sheet import create --file ./data.xls --workspace \u003cWORKSPACE_ID\u003e --name \"月度报表\" --format json"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:公开 leaf 与兼容入口 sheet import 复用同一导入流程,使 Agent Schema 能发现并调用真正的导入动作。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"idempotency": {
|
||||
"value": "non_idempotent",
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:导入会新建在线电子表格,不修改已有文档;重复执行会创建多个文档,因此标记为 non_idempotent。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "non_idempotent",
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:导入会新建在线电子表格,不修改已有文档;重复执行会创建多个文档,因此标记为 non_idempotent。"
|
||||
},
|
||||
{
|
||||
"value": "unknown",
|
||||
"source": "effect-default",
|
||||
"precedence": "inference_or_default",
|
||||
"selected": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"interface_mode": {
|
||||
"value": "composite",
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:导入会新建在线电子表格,不修改已有文档;重复执行会创建多个文档,因此标记为 non_idempotent。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "composite",
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:导入会新建在线电子表格,不修改已有文档;重复执行会创建多个文档,因此标记为 non_idempotent。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"interface_reason": {
|
||||
"value": "该 CLI 导入流程包含本地文件校验、创建导入会话、OSS 上传、确认转换和任务轮询,不能绑定为单一 interface_ref。",
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:导入会新建在线电子表格,不修改已有文档;重复执行会创建多个文档,因此标记为 non_idempotent。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "该 CLI 导入流程包含本地文件校验、创建导入会话、OSS 上传、确认转换和任务轮询,不能绑定为单一 interface_ref。",
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:导入会新建在线电子表格,不修改已有文档;重复执行会创建多个文档,因此标记为 non_idempotent。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"interface_ref": {
|
||||
"value": null,
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "interface_disposition_matrix",
|
||||
"review_reason": "final interface mode composite forbids a direct MCP interface_ref",
|
||||
"candidates": [
|
||||
{
|
||||
"value": null,
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "final interface mode composite forbids a direct MCP interface_ref"
|
||||
}
|
||||
]
|
||||
},
|
||||
"reviewed": {
|
||||
"value": true,
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:导入会新建在线电子表格,不修改已有文档;重复执行会创建多个文档,因此标记为 non_idempotent。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": true,
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:导入会新建在线电子表格,不修改已有文档;重复执行会创建多个文档,因此标记为 non_idempotent。"
|
||||
},
|
||||
{
|
||||
"value": true,
|
||||
"source": "internal/cli/schema_hints/selection/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": false,
|
||||
"review_reason": "人工审阅:公开 leaf 与兼容入口 sheet import 复用同一导入流程,使 Agent Schema 能发现并调用真正的导入动作。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"risk": {
|
||||
"value": "medium",
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:导入会新建在线电子表格,不修改已有文档;重复执行会创建多个文档,因此标记为 non_idempotent。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "medium",
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:导入会新建在线电子表格,不修改已有文档;重复执行会创建多个文档,因此标记为 non_idempotent。"
|
||||
},
|
||||
{
|
||||
"value": "medium",
|
||||
"source": "effect-default",
|
||||
"precedence": "inference_or_default",
|
||||
"selected": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"用户要把本地 Excel 文件转换为可在线编辑的钉钉表格时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:公开 leaf 与兼容入口 sheet import 复用同一导入流程,使 Agent Schema 能发现并调用真正的导入动作。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"用户要把本地 Excel 文件转换为可在线编辑的钉钉表格时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:公开 leaf 与兼容入口 sheet import 复用同一导入流程,使 Agent Schema 能发现并调用真正的导入动作。"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"idempotency": "non_idempotent",
|
||||
"interface_mode": "composite",
|
||||
"interface_reason": "该 CLI 导入流程包含本地文件校验、创建导入会话、OSS 上传、确认转换和任务轮询,不能绑定为单一 interface_ref。",
|
||||
"reviewed": true,
|
||||
"risk": "medium",
|
||||
"source_refs": [
|
||||
"cobra-help:dws sheet import create --help",
|
||||
"internal/cli/schema_command_registry.json",
|
||||
"internal/cli/schema_hints/metadata/sheet.json",
|
||||
"internal/cli/schema_hints/selection/sheet.json",
|
||||
"live-mcp:none (Cobra+Skill; composite/unpinned)",
|
||||
"skills/mono/references/products/sheet/sheet-import.md"
|
||||
],
|
||||
"use_when": [
|
||||
"用户要把本地 Excel 文件转换为可在线编辑的钉钉表格时"
|
||||
]
|
||||
},
|
||||
"sheet import get": {
|
||||
"agent_summary": "根据 taskId 查询表格导入任务结果。",
|
||||
"agent_summary_source": "dws-agent-selection/sheet",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"发起新的 xlsx/xls 导入应使用 sheet import;不要用本命令代替导入",
|
||||
"状态仍为 processing 时不要重新提交 import,稍后继续查询同一 taskId"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "read",
|
||||
"effect_source": "agent-hint",
|
||||
"examples": [
|
||||
"dws sheet import get --task-id \u003cTASK_ID\u003e --format json"
|
||||
],
|
||||
"field_provenance": {
|
||||
"agent_summary": {
|
||||
"value": "根据 taskId 查询表格导入任务结果。",
|
||||
"source": "internal/cli/schema_hints/selection/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:该命令与 Wukong sheet import get 契约一致,复用 doc 导入任务查询工具,仅用于已有任务续查。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "根据 taskId 查询表格导入任务结果。",
|
||||
"source": "internal/cli/schema_hints/selection/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:该命令与 Wukong sheet import get 契约一致,复用 doc 导入任务查询工具,仅用于已有任务续查。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"availability": {
|
||||
"value": "available",
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:仅查询已有导入任务,不创建、覆盖或删除文档。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "available",
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:仅查询已有导入任务,不创建、覆盖或删除文档。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"发起新的 xlsx/xls 导入应使用 sheet import;不要用本命令代替导入",
|
||||
"状态仍为 processing 时不要重新提交 import,稍后继续查询同一 taskId"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:该命令与 Wukong sheet import get 契约一致,复用 doc 导入任务查询工具,仅用于已有任务续查。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"发起新的 xlsx/xls 导入应使用 sheet import;不要用本命令代替导入",
|
||||
"状态仍为 processing 时不要重新提交 import,稍后继续查询同一 taskId"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:该命令与 Wukong sheet import get 契约一致,复用 doc 导入任务查询工具,仅用于已有任务续查。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"confirmation": {
|
||||
"value": "not_required",
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:仅查询已有导入任务,不创建、覆盖或删除文档。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "not_required",
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:仅查询已有导入任务,不创建、覆盖或删除文档。"
|
||||
},
|
||||
{
|
||||
"value": "not_required",
|
||||
"source": "risk-default",
|
||||
"precedence": "inference_or_default",
|
||||
"selected": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"effect": {
|
||||
"value": "read",
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:仅查询已有导入任务,不创建、覆盖或删除文档。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "read",
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:仅查询已有导入任务,不创建、覆盖或删除文档。"
|
||||
},
|
||||
{
|
||||
"value": "write",
|
||||
"source": "command-verb",
|
||||
"precedence": "inference_or_default",
|
||||
"selected": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"examples": {
|
||||
"value": [
|
||||
"dws sheet import get --task-id \u003cTASK_ID\u003e --format json"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:该命令与 Wukong sheet import get 契约一致,复用 doc 导入任务查询工具,仅用于已有任务续查。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"dws sheet import get --task-id \u003cTASK_ID\u003e --format json"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:该命令与 Wukong sheet import get 契约一致,复用 doc 导入任务查询工具,仅用于已有任务续查。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"idempotency": {
|
||||
"value": "idempotent",
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:仅查询已有导入任务,不创建、覆盖或删除文档。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "idempotent",
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:仅查询已有导入任务,不创建、覆盖或删除文档。"
|
||||
},
|
||||
{
|
||||
"value": "unknown",
|
||||
"source": "effect-default",
|
||||
"precedence": "inference_or_default",
|
||||
"selected": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"interface_mode": {
|
||||
"value": "composite",
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:仅查询已有导入任务,不创建、覆盖或删除文档。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "composite",
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:仅查询已有导入任务,不创建、覆盖或删除文档。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"interface_reason": {
|
||||
"value": "该 CLI 续查命令显式复用 doc server 的 query_import_task;当前 Sheet 静态接口快照没有可直接绑定的单一 interface_ref。",
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:仅查询已有导入任务,不创建、覆盖或删除文档。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "该 CLI 续查命令显式复用 doc server 的 query_import_task;当前 Sheet 静态接口快照没有可直接绑定的单一 interface_ref。",
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:仅查询已有导入任务,不创建、覆盖或删除文档。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"interface_ref": {
|
||||
"value": null,
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "interface_disposition_matrix",
|
||||
"review_reason": "final interface mode composite forbids a direct MCP interface_ref",
|
||||
"candidates": [
|
||||
{
|
||||
"value": null,
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "final interface mode composite forbids a direct MCP interface_ref"
|
||||
}
|
||||
]
|
||||
},
|
||||
"reviewed": {
|
||||
"value": true,
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:仅查询已有导入任务,不创建、覆盖或删除文档。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": true,
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:仅查询已有导入任务,不创建、覆盖或删除文档。"
|
||||
},
|
||||
{
|
||||
"value": true,
|
||||
"source": "internal/cli/schema_hints/selection/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": false,
|
||||
"review_reason": "人工审阅:该命令与 Wukong sheet import get 契约一致,复用 doc 导入任务查询工具,仅用于已有任务续查。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"risk": {
|
||||
"value": "low",
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:仅查询已有导入任务,不创建、覆盖或删除文档。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "low",
|
||||
"source": "internal/cli/schema_hints/metadata/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:仅查询已有导入任务,不创建、覆盖或删除文档。"
|
||||
},
|
||||
{
|
||||
"value": "medium",
|
||||
"source": "effect-default",
|
||||
"precedence": "inference_or_default",
|
||||
"selected": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"已有 sheet import 返回的 taskId,需要在导入超时或中断后续查转换状态时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:该命令与 Wukong sheet import get 契约一致,复用 doc 导入任务查询工具,仅用于已有任务续查。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"已有 sheet import 返回的 taskId,需要在导入超时或中断后续查转换状态时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/sheet.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:该命令与 Wukong sheet import get 契约一致,复用 doc 导入任务查询工具,仅用于已有任务续查。"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"idempotency": "idempotent",
|
||||
"interface_mode": "composite",
|
||||
"interface_reason": "该 CLI 续查命令显式复用 doc server 的 query_import_task;当前 Sheet 静态接口快照没有可直接绑定的单一 interface_ref。",
|
||||
"reviewed": true,
|
||||
"risk": "low",
|
||||
"source_refs": [
|
||||
"cobra-help:dws sheet import get --help",
|
||||
"internal/cli/schema_command_registry.json",
|
||||
"internal/cli/schema_hints/metadata/sheet.json",
|
||||
"internal/cli/schema_hints/selection/sheet.json",
|
||||
"live-mcp:none (Cobra+Skill; composite/unpinned)",
|
||||
"skills/mono/references/products/sheet/sheet-import.md"
|
||||
],
|
||||
"use_when": [
|
||||
"已有 sheet import 返回的 taskId,需要在导入超时或中断后续查转换状态时"
|
||||
]
|
||||
},
|
||||
"sheet info": {
|
||||
"agent_summary": "获取指定工作表详情(行列数、非空范围、合并区等)。",
|
||||
"agent_summary_source": "dws-agent-selection/sheet",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"version": 1,
|
||||
"source_hash": "sha256:84ae56ee689d80254b9f590733f7bcaa77f9c0f94ac9e0e5e6762956b1b102a2",
|
||||
"surface_hash": "sha256:e858be68701d256818eccd085a6f7f97655835b9c7cbbce6275ed930c2436dc3",
|
||||
"source_files": 149,
|
||||
"source_hash": "sha256:53567141622d90e02ce50fe3bc1733357a25b9860a0795eb396d9361f43c41d1",
|
||||
"surface_hash": "sha256:69e1f9fcd3768d49a2a197c40fb26c0bc07385e868753210761c3ea7f676eadb",
|
||||
"source_files": 150,
|
||||
"hint_files": 46,
|
||||
"hint_products": 39,
|
||||
"hint_tools": 1280,
|
||||
"hint_tools": 1288,
|
||||
"interface_metadata": {
|
||||
"source": "mcp-tools-list+cli-registry",
|
||||
"revision": "4574f7022c32cf4c033e9b7b4156e2fec815fed8",
|
||||
@@ -29,13 +29,13 @@
|
||||
"coverage": {
|
||||
"surface_products": 22,
|
||||
"products_with_metadata": 22,
|
||||
"surface_tools": 564,
|
||||
"tools_with_metadata": 564,
|
||||
"tools_with_agent_summary": 564,
|
||||
"tools_with_use_when": 564,
|
||||
"tools_with_avoid_when": 564,
|
||||
"tools_with_examples": 564,
|
||||
"tools_with_interface_mode": 564,
|
||||
"surface_tools": 568,
|
||||
"tools_with_metadata": 568,
|
||||
"tools_with_agent_summary": 568,
|
||||
"tools_with_use_when": 568,
|
||||
"tools_with_avoid_when": 568,
|
||||
"tools_with_examples": 568,
|
||||
"tools_with_interface_mode": 568,
|
||||
"unmatched_skill_tools": 125,
|
||||
"unreviewed_skill_tools": 7
|
||||
},
|
||||
@@ -3877,7 +3877,7 @@
|
||||
{
|
||||
"tool_path": "sheet range",
|
||||
"source": "skills/mono/references/products/sheet.md",
|
||||
"line": 65,
|
||||
"line": 66,
|
||||
"candidates": [
|
||||
"sheet range batch-clear",
|
||||
"sheet range batch-set-style",
|
||||
@@ -3891,7 +3891,7 @@
|
||||
{
|
||||
"tool_path": "sheet filter-view",
|
||||
"source": "skills/mono/references/products/sheet.md",
|
||||
"line": 66,
|
||||
"line": 67,
|
||||
"candidates": [
|
||||
"sheet filter-view create",
|
||||
"sheet filter-view delete",
|
||||
|
||||
+2760
-23
File diff suppressed because it is too large.
Load diff
@@ -465,6 +465,10 @@
|
||||
"canonical_path": "aitable.view_update_visible_fields",
|
||||
"cli_path": "aitable view update visible-fields"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.workflow_create",
|
||||
"cli_path": "aitable workflow create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.workflow_disable",
|
||||
"cli_path": "aitable workflow disable"
|
||||
@@ -480,6 +484,10 @@
|
||||
{
|
||||
"canonical_path": "aitable.workflow_list",
|
||||
"cli_path": "aitable workflow list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.workflow_update",
|
||||
"cli_path": "aitable workflow update"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2110,6 +2118,14 @@
|
||||
"canonical_path": "sheet.info",
|
||||
"cli_path": "sheet info"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.import",
|
||||
"cli_path": "sheet import create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.import_get",
|
||||
"cli_path": "sheet import get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.insert_dimension",
|
||||
"cli_path": "sheet insert-dimension"
|
||||
|
||||
@@ -89,6 +89,7 @@ func init() {
|
||||
registerRequireOneOf("sheet.update_filter_view", "name", "range", "criteria")
|
||||
registerRequireOneOf("sheet.update_float_image", "src", "range", "width", "height", "offset-x", "offset-y")
|
||||
registerRequireOneOf("sheet.update_sheet", "name", "index", "hidden", "frozen-row-count", "frozen-column-count", "tab-color")
|
||||
registerRequireOneOf("sheet.import", "folder-token", "workspace")
|
||||
registerRequireOneOf("wiki.search_wikiSpaces", "query", "type")
|
||||
}
|
||||
|
||||
|
||||
@@ -959,6 +959,33 @@
|
||||
"availability": "available",
|
||||
"reviewed": true
|
||||
},
|
||||
"aitable.workflow_create": {
|
||||
"effect": "write",
|
||||
"risk": "medium",
|
||||
"confirmation": "not_required",
|
||||
"idempotency": "non_idempotent",
|
||||
"interface_mode": "composite",
|
||||
"availability": "available",
|
||||
"interface_reason": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
|
||||
"reviewed": true,
|
||||
"parameters": {
|
||||
"base-id": {
|
||||
"property": "baseId",
|
||||
"required": true
|
||||
},
|
||||
"dsl": {
|
||||
"property": "dsl",
|
||||
"required": true,
|
||||
"interface_type": "object"
|
||||
},
|
||||
"locale": {
|
||||
"property": "locale"
|
||||
}
|
||||
},
|
||||
"review_reason": "The command maps --base-id and a decoded workflow-dsl/v1 --dsl object to aitable/create_workflow. The call intentionally bypasses retries because creation is non-idempotent; locale remains optional.",
|
||||
"cli_path": "aitable workflow create",
|
||||
"runtime_gate": "none"
|
||||
},
|
||||
"aitable.workflow_disable": {
|
||||
"effect": "write",
|
||||
"risk": "high",
|
||||
@@ -998,6 +1025,37 @@
|
||||
"interface_mode": "mcp",
|
||||
"availability": "available",
|
||||
"reviewed": true
|
||||
},
|
||||
"aitable.workflow_update": {
|
||||
"effect": "write",
|
||||
"risk": "medium",
|
||||
"confirmation": "not_required",
|
||||
"idempotency": "unknown",
|
||||
"interface_mode": "composite",
|
||||
"availability": "available",
|
||||
"interface_reason": "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
|
||||
"reviewed": true,
|
||||
"parameters": {
|
||||
"base-id": {
|
||||
"property": "baseId",
|
||||
"required": true
|
||||
},
|
||||
"workflow-id": {
|
||||
"property": "workflowId",
|
||||
"required": true
|
||||
},
|
||||
"dsl": {
|
||||
"property": "dsl",
|
||||
"required": true,
|
||||
"interface_type": "object"
|
||||
},
|
||||
"locale": {
|
||||
"property": "locale"
|
||||
}
|
||||
},
|
||||
"review_reason": "The command maps --base-id, --workflow-id and a decoded complete workflow-dsl/v1 --dsl object to aitable/update_workflow. This is full replacement rather than a partial patch; locale remains optional.",
|
||||
"cli_path": "aitable workflow update",
|
||||
"runtime_gate": "none"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -392,6 +392,28 @@
|
||||
"availability": "available",
|
||||
"reviewed": true
|
||||
},
|
||||
"sheet.import": {
|
||||
"effect": "write",
|
||||
"risk": "medium",
|
||||
"confirmation": "not_required",
|
||||
"idempotency": "non_idempotent",
|
||||
"interface_mode": "composite",
|
||||
"availability": "available",
|
||||
"interface_reason": "该 CLI 导入流程包含本地文件校验、创建导入会话、OSS 上传、确认转换和任务轮询,不能绑定为单一 interface_ref。",
|
||||
"reviewed": true,
|
||||
"review_reason": "人工审阅:导入会新建在线电子表格,不修改已有文档;重复执行会创建多个文档,因此标记为 non_idempotent。"
|
||||
},
|
||||
"sheet.import_get": {
|
||||
"effect": "read",
|
||||
"risk": "low",
|
||||
"confirmation": "not_required",
|
||||
"idempotency": "idempotent",
|
||||
"interface_mode": "composite",
|
||||
"availability": "available",
|
||||
"interface_reason": "该 CLI 续查命令显式复用 doc server 的 query_import_task;当前 Sheet 静态接口快照没有可直接绑定的单一 interface_ref。",
|
||||
"reviewed": true,
|
||||
"review_reason": "人工审阅:仅查询已有导入任务,不创建、覆盖或删除文档。"
|
||||
},
|
||||
"sheet.insert_dimension": {
|
||||
"interface_mode": "mcp",
|
||||
"availability": "available",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"channel": "open-source"
|
||||
},
|
||||
"coverage": {
|
||||
"source_tools": 564,
|
||||
"source_tools": 568,
|
||||
"matched_tools": 71
|
||||
},
|
||||
"tools": {
|
||||
|
||||
@@ -2211,6 +2211,25 @@
|
||||
"dws-schema-live:aitable.update_view"
|
||||
]
|
||||
},
|
||||
"aitable.workflow_create": {
|
||||
"agent_summary": "创建并发布 AI 表格自动化工作流。",
|
||||
"use_when": [
|
||||
"用户明确要求在已知 Base 中创建自动化,且已准备完整 workflow-dsl/v1 定义时"
|
||||
],
|
||||
"avoid_when": [
|
||||
"已有工作流要修改时用 workflow update;仅启停已有工作流用 enable/disable;请求结果不确定时先 list 核对,避免非幂等重复创建"
|
||||
],
|
||||
"examples": [
|
||||
"dws aitable workflow create --base-id <BASE_ID> --dsl @workflow.json --locale zh-CN"
|
||||
],
|
||||
"reviewed": true,
|
||||
"review_reason": "依据真实 Cobra leaf、Wukong 同步契约和 Aitable workflow Skill 审阅创建语义,强调完整 DSL、结果 valid/issues 校验及非幂等边界。",
|
||||
"source_refs": [
|
||||
"internal/cli/schema_command_registry.json#aitable.workflow_create",
|
||||
"cobra-help:dws aitable workflow create --help",
|
||||
"skills/mono/references/products/aitable/aitable-workflow.md"
|
||||
]
|
||||
},
|
||||
"aitable.workflow_disable": {
|
||||
"agent_summary": "禁用工作流(高危,需确认;status 变 STOP)。",
|
||||
"use_when": [
|
||||
@@ -2290,6 +2309,25 @@
|
||||
"skills/mono/references/products/aitable.md",
|
||||
"dws-schema-live:none (helper/composite; Skill+Cobra)"
|
||||
]
|
||||
},
|
||||
"aitable.workflow_update": {
|
||||
"agent_summary": "用完整 DSL 更新并发布已有 AI 表格自动化工作流。",
|
||||
"use_when": [
|
||||
"用户明确要求修改已知工作流,且已先留底当前详情并准备完整 workflow-dsl/v1 目标定义时"
|
||||
],
|
||||
"avoid_when": [
|
||||
"新建工作流用 create;只改变运行状态用 enable/disable;不要把局部 patch 或 workflow get 返回的 flowSchema 直接当作 DSL"
|
||||
],
|
||||
"examples": [
|
||||
"dws aitable workflow update --base-id <BASE_ID> --workflow-id <FLOW_ID> --dsl @workflow.json --locale zh-CN"
|
||||
],
|
||||
"reviewed": true,
|
||||
"review_reason": "依据真实 Cobra leaf、Wukong 同步契约和 Aitable workflow Skill 审阅更新语义,强调全量替换、事前备份和 valid/issues 结果校验。",
|
||||
"source_refs": [
|
||||
"internal/cli/schema_command_registry.json#aitable.workflow_update",
|
||||
"cobra-help:dws aitable workflow update --help",
|
||||
"skills/mono/references/products/aitable/aitable-workflow.md"
|
||||
]
|
||||
}
|
||||
},
|
||||
"products": {
|
||||
|
||||
@@ -849,6 +849,49 @@
|
||||
"dws-schema-live:sheet.get_sheet"
|
||||
]
|
||||
},
|
||||
"sheet.import": {
|
||||
"agent_summary": "将本地 xlsx/xls 文件导入为新的钉钉在线电子表格。",
|
||||
"use_when": [
|
||||
"用户要把本地 Excel 文件转换为可在线编辑的钉钉表格时"
|
||||
],
|
||||
"avoid_when": [
|
||||
"读取或修改已有在线表格时应使用 sheet list/info/range 等命令",
|
||||
"查询已提交导入任务的状态时使用 sheet import get,不要重复创建导入任务"
|
||||
],
|
||||
"examples": [
|
||||
"dws sheet import create --file ./report.xlsx --folder-token <FOLDER_TOKEN> --format json",
|
||||
"dws sheet import create --file ./data.xls --workspace <WORKSPACE_ID> --name \"月度报表\" --format json"
|
||||
],
|
||||
"reviewed": true,
|
||||
"review_reason": "人工审阅:公开 leaf 与兼容入口 sheet import 复用同一导入流程,使 Agent Schema 能发现并调用真正的导入动作。",
|
||||
"source_refs": [
|
||||
"internal/cli/schema_command_registry.json",
|
||||
"cobra-help:dws sheet import create --help",
|
||||
"skills/mono/references/products/sheet/sheet-import.md",
|
||||
"live-mcp:none (Cobra+Skill; composite/unpinned)"
|
||||
]
|
||||
},
|
||||
"sheet.import_get": {
|
||||
"agent_summary": "根据 taskId 查询表格导入任务结果。",
|
||||
"use_when": [
|
||||
"已有 sheet import 返回的 taskId,需要在导入超时或中断后续查转换状态时"
|
||||
],
|
||||
"avoid_when": [
|
||||
"发起新的 xlsx/xls 导入应使用 sheet import;不要用本命令代替导入",
|
||||
"状态仍为 processing 时不要重新提交 import,稍后继续查询同一 taskId"
|
||||
],
|
||||
"examples": [
|
||||
"dws sheet import get --task-id <TASK_ID> --format json"
|
||||
],
|
||||
"reviewed": true,
|
||||
"review_reason": "人工审阅:该命令与 Wukong sheet import get 契约一致,复用 doc 导入任务查询工具,仅用于已有任务续查。",
|
||||
"source_refs": [
|
||||
"internal/cli/schema_command_registry.json",
|
||||
"cobra-help:dws sheet import get --help",
|
||||
"skills/mono/references/products/sheet/sheet-import.md",
|
||||
"live-mcp:none (Cobra+Skill; composite/unpinned)"
|
||||
]
|
||||
},
|
||||
"sheet.insert_dimension": {
|
||||
"agent_summary": "在指定位置之前插入空行或空列。",
|
||||
"use_when": [
|
||||
@@ -1552,12 +1595,14 @@
|
||||
},
|
||||
"products": {
|
||||
"sheet": {
|
||||
"agent_summary": "创建、读取、编辑和导出钉钉在线电子表格(axls),并管理工作表、区域、筛选、图表、图片与格式。",
|
||||
"agent_summary": "导入本地 Excel,或创建、读取、编辑和导出钉钉在线电子表格(axls),并管理工作表、区域、筛选、图表、图片与格式。",
|
||||
"use_when": [
|
||||
"用户要处理钉钉在线电子表格中的工作表、单元格、范围、筛选、图表、图片或格式时"
|
||||
"用户要处理钉钉在线电子表格中的工作表、单元格、范围、筛选、图表、图片或格式时",
|
||||
"用户要把本地 xlsx/xls 转换为新的钉钉在线电子表格时"
|
||||
],
|
||||
"avoid_when": [
|
||||
"目标是 AI 表格 Base 的结构化记录、钉钉文档正文或本地 Excel 文件时不要使用 sheet"
|
||||
"目标是 AI 表格 Base 的结构化记录或钉钉文档正文时不要使用 sheet",
|
||||
"只读取已上传但未转换的 xlsx/xls 节点时先用 doc download;不要把文件节点直接传给单元格命令"
|
||||
],
|
||||
"reviewed": true,
|
||||
"review_reason": "人工结合实时 MCP/Skill/Cobra 审阅产品级选择边界。",
|
||||
|
||||
@@ -199,17 +199,13 @@ func TestReadFileBoundedEmptyFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFileBoundedPermissionDenied(t *testing.T) {
|
||||
func TestReadFileBoundedRejectsDirectory(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "noperm.txt")
|
||||
writeTestFile(t, path, []byte("secret"))
|
||||
os.Chmod(path, 0o000)
|
||||
t.Cleanup(func() { os.Chmod(path, 0o644) })
|
||||
|
||||
_, _, err := ReadFileArg("@" + path)
|
||||
_, _, err := ReadFileArg("@" + dir)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for permission denied")
|
||||
t.Fatal("expected error when reading a directory as a file")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+124
-3
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -81,6 +82,44 @@ func resolveRecordsFlag(cmd *cobra.Command) (string, error) {
|
||||
return "", fmt.Errorf("missing required flag(s): --records (example: --records '[{\"cells\":{\"fldTextId\":\"文本内容\"}}]')\n hint: for large payloads or Windows, use --records-file ./path/to/records.json")
|
||||
}
|
||||
|
||||
// resolveWorkflowDSL reads --dsl from inline JSON, @file, or stdin (-), then
|
||||
// decodes the MCP-facing workflow-dsl/v1 object. Detailed DSL validation stays
|
||||
// on the workflow service so callers receive its structured issues response.
|
||||
func resolveWorkflowDSL(cmd *cobra.Command) (map[string]any, error) {
|
||||
raw := mustGetFlag(cmd, "dsl")
|
||||
switch {
|
||||
case raw == "-":
|
||||
data, err := io.ReadAll(cmd.InOrStdin())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("--dsl stdin read failed: %w", err)
|
||||
}
|
||||
raw = string(data)
|
||||
case strings.HasPrefix(raw, "@"):
|
||||
path := strings.TrimSpace(strings.TrimPrefix(raw, "@"))
|
||||
if path == "" {
|
||||
return nil, fmt.Errorf("--dsl file path must not be empty\n hint: use --dsl @workflow.json")
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("--dsl file read failed: %w\n hint: ensure the file exists and contains a workflow-dsl/v1 JSON object", err)
|
||||
}
|
||||
raw = string(data)
|
||||
}
|
||||
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return nil, fmt.Errorf("--dsl must not be empty\n hint: pass inline JSON, @workflow.json, or - for stdin")
|
||||
}
|
||||
|
||||
var dsl map[string]any
|
||||
if err := json.Unmarshal([]byte(raw), &dsl); err != nil {
|
||||
return nil, fmt.Errorf("--dsl JSON parse failed: %w\n hint: --dsl must be a workflow-dsl/v1 JSON object", err)
|
||||
}
|
||||
if dsl == nil {
|
||||
return nil, fmt.Errorf("--dsl must be a JSON object, got null")
|
||||
}
|
||||
return dsl, nil
|
||||
}
|
||||
|
||||
// recordQueryFetchAll implements --all auto-pagination for record query.
|
||||
// - pageLimit controls max pages (default 50, 0 = unlimited)
|
||||
// - Mid-loop errors break gracefully, outputting already-fetched data
|
||||
@@ -3204,16 +3243,87 @@ locked 为 true 表示视图已锁定,false 表示未锁定。`,
|
||||
|
||||
workflowCmd := &cobra.Command{
|
||||
Use: "workflow",
|
||||
Short: "自动化工作流管理(启停 / 查看 / 列表)",
|
||||
Short: "自动化工作流管理(创建 / 更新 / 启停 / 查看 / 列表)",
|
||||
RunE: groupRunE,
|
||||
}
|
||||
|
||||
workflowCreateCmd := &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "创建并发布自动化工作流",
|
||||
Long: `在指定 Base 中创建并发布自动化工作流。
|
||||
--dsl 必须是完整的 workflow-dsl/v1 JSON 对象;涉及数据表、字段或视图的节点应使用真实的 sheetId / fieldId / viewId。
|
||||
|
||||
--dsl 支持内联 JSON、@文件路径,或 - 从 stdin 读取。创建属于非幂等操作,CLI 不会自动重试。
|
||||
返回 data.valid、flowId、flowSchema、stepNodeIds、referenceMap、issues;即使 status=success,
|
||||
valid=false 仍表示 DSL 校验或发布未通过,必须读取 issues 修正后再调用。`,
|
||||
Example: ` dws aitable workflow create --base-id BASE_ID --dsl @workflow.json --locale zh-CN
|
||||
cat workflow.json | dws aitable workflow create --base-id BASE_ID --dsl -`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := validateRequiredFlags(cmd, "dsl"); err != nil {
|
||||
return err
|
||||
}
|
||||
baseID, err := mustFlagOrFallback(cmd, "base-id", "base")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dsl, err := resolveWorkflowDSL(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
toolArgs := map[string]any{
|
||||
"baseId": baseID,
|
||||
"dsl": dsl,
|
||||
}
|
||||
if locale, _ := cmd.Flags().GetString("locale"); strings.TrimSpace(locale) != "" {
|
||||
toolArgs["locale"] = locale
|
||||
}
|
||||
// create_workflow is non-idempotent. Bypass the retry wrapper to
|
||||
// prevent an uncertain first response from creating a duplicate.
|
||||
return callMCPToolOnServer("aitable", "create_workflow", toolArgs)
|
||||
},
|
||||
}
|
||||
|
||||
workflowUpdateCmd := &cobra.Command{
|
||||
Use: "update",
|
||||
Short: "更新并发布已有自动化工作流",
|
||||
Long: `在指定 Base 中更新并发布已有自动化工作流。
|
||||
建议先用 workflow get 留底当前详情;--dsl 必须是完整的 workflow-dsl/v1 JSON 对象,而不是局部 patch。
|
||||
|
||||
--dsl 支持内联 JSON、@文件路径,或 - 从 stdin 读取。更新会发布传入的目标 DSL;请提供完整、可独立校验的 workflow-dsl/v1 对象。
|
||||
返回 data.valid、flowId、flowSchema、stepNodeIds、referenceMap、issues;即使 status=success,
|
||||
valid=false 仍表示 DSL 校验或发布未通过,必须读取 issues 修正后再调用。`,
|
||||
Example: ` dws aitable workflow update --base-id BASE_ID --workflow-id WORKFLOW_ID --dsl @workflow.json --locale zh-CN
|
||||
cat workflow.json | dws aitable workflow update --base-id BASE_ID --workflow-id WORKFLOW_ID --dsl -`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := validateRequiredFlags(cmd, "workflow-id", "dsl"); err != nil {
|
||||
return err
|
||||
}
|
||||
baseID, err := mustFlagOrFallback(cmd, "base-id", "base")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dsl, err := resolveWorkflowDSL(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
toolArgs := map[string]any{
|
||||
"baseId": baseID,
|
||||
"workflowId": mustGetFlag(cmd, "workflow-id"),
|
||||
"dsl": dsl,
|
||||
}
|
||||
if locale, _ := cmd.Flags().GetString("locale"); strings.TrimSpace(locale) != "" {
|
||||
toolArgs["locale"] = locale
|
||||
}
|
||||
return callAitableTool("update_workflow", toolArgs)
|
||||
},
|
||||
}
|
||||
|
||||
workflowEnableCmd := &cobra.Command{
|
||||
Use: "enable",
|
||||
Short: "启用指定工作流",
|
||||
Long: `启用指定 Base 中的自动化工作流。启用后工作流将按配置的触发条件自动执行。
|
||||
返回 {workflowId, enabled} 用于确认操作结果(enabled 为动作确认而非状态查询)。
|
||||
当前不支持通过 CLI 新建工作流,请在 AI 表格 Web 端配置好后用 workflow list 拿到 workflowId 再启停。`,
|
||||
可用 workflow create 创建工作流,或用 workflow list 获取已有 workflowId。`,
|
||||
Example: ` dws aitable workflow enable --base-id BASE_ID --workflow-id WORKFLOW_ID
|
||||
# 查询 workflowId: dws aitable workflow list --base-id <baseId>`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -4750,6 +4860,13 @@ parentSectionId 为空串表示该节点在 Base 根目录下。
|
||||
formCmd.AddCommand(formListCmd, formGetCmd, formCreateCmd, formDeleteCmd, formUpdateCmd, formFieldCmd, formShareCmd, formQuestionsCmd)
|
||||
|
||||
// workflow
|
||||
workflowCreateCmd.Flags().String("base-id", "", "目标 Base ID (必填)")
|
||||
workflowCreateCmd.Flags().String("dsl", "", "workflow-dsl/v1 JSON 对象;支持内联 JSON、@文件路径或 - 从 stdin 读取 (必填)")
|
||||
workflowCreateCmd.Flags().String("locale", "", "请求语言,例如 zh-CN 或 zh_CN (可选)")
|
||||
workflowUpdateCmd.Flags().String("base-id", "", "目标 Base ID (必填)")
|
||||
workflowUpdateCmd.Flags().String("workflow-id", "", "目标工作流 ID (必填)")
|
||||
workflowUpdateCmd.Flags().String("dsl", "", "workflow-dsl/v1 JSON 对象;支持内联 JSON、@文件路径或 - 从 stdin 读取 (必填)")
|
||||
workflowUpdateCmd.Flags().String("locale", "", "请求语言,例如 zh-CN 或 zh_CN (可选)")
|
||||
workflowEnableCmd.Flags().String("base-id", "", "目标 Base ID (必填)")
|
||||
workflowEnableCmd.Flags().String("workflow-id", "", "目标工作流 ID (必填)")
|
||||
workflowDisableCmd.Flags().String("base-id", "", "目标 Base ID (必填)")
|
||||
@@ -4759,7 +4876,11 @@ parentSectionId 为空串表示该节点在 Base 根目录下。
|
||||
workflowListCmd.Flags().String("base-id", "", "目标 Base ID (必填)")
|
||||
workflowListCmd.Flags().Int("limit", 0, "分页大小 [1, 100],不传走服务端默认 20")
|
||||
workflowListCmd.Flags().Int("offset", 0, "分页偏移量,>= 0,不传走服务端默认 0")
|
||||
workflowCmd.AddCommand(workflowEnableCmd, workflowDisableCmd, workflowGetCmd, workflowListCmd)
|
||||
workflowCmd.AddCommand(
|
||||
workflowCreateCmd, workflowUpdateCmd,
|
||||
workflowEnableCmd, workflowDisableCmd,
|
||||
workflowGetCmd, workflowListCmd,
|
||||
)
|
||||
|
||||
// dashboard
|
||||
dashboardGetCmd.Flags().String("base-id", "", "所属 Base ID (必填)")
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
// 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.
|
||||
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
||||
)
|
||||
|
||||
type aitableWorkflowCall struct {
|
||||
productID string
|
||||
toolName string
|
||||
args map[string]any
|
||||
}
|
||||
|
||||
type aitableWorkflowCaller struct {
|
||||
calls []aitableWorkflowCall
|
||||
}
|
||||
|
||||
func (c *aitableWorkflowCaller) CallTool(_ context.Context, productID, toolName string, args map[string]any) (*edition.ToolResult, error) {
|
||||
c.calls = append(c.calls, aitableWorkflowCall{productID: productID, toolName: toolName, args: args})
|
||||
return &edition.ToolResult{Content: []edition.ContentBlock{{
|
||||
Type: "text",
|
||||
Text: `{"status":"success","data":{"valid":true,"flowId":"flow-test","issues":[]}}`,
|
||||
}}}, nil
|
||||
}
|
||||
|
||||
func (*aitableWorkflowCaller) Format() string { return "json" }
|
||||
func (*aitableWorkflowCaller) DryRun() bool { return false }
|
||||
func (*aitableWorkflowCaller) Fields() string { return "" }
|
||||
func (*aitableWorkflowCaller) JQ() string { return "" }
|
||||
|
||||
func runAitableWorkflowCommand(t *testing.T, stdin io.Reader, args ...string) (*aitableWorkflowCaller, error) {
|
||||
t.Helper()
|
||||
previousDeps := deps
|
||||
previousArgs := os.Args
|
||||
t.Cleanup(func() {
|
||||
deps = previousDeps
|
||||
os.Args = previousArgs
|
||||
})
|
||||
|
||||
caller := &aitableWorkflowCaller{}
|
||||
InitDeps(caller)
|
||||
deps.Out.w = io.Discard
|
||||
os.Args = append([]string{"dws", "aitable", "workflow"}, args...)
|
||||
|
||||
cmd := newAitableCommand()
|
||||
cmd.PersistentFlags().String("format", "json", "output format")
|
||||
cmd.SilenceErrors = true
|
||||
cmd.SilenceUsage = true
|
||||
cmd.SetArgs(append([]string{"workflow"}, args...))
|
||||
if stdin != nil {
|
||||
cmd.SetIn(stdin)
|
||||
}
|
||||
return caller, cmd.Execute()
|
||||
}
|
||||
|
||||
func TestAitableWorkflowCreateMapsDSLWithoutRetry(t *testing.T) {
|
||||
wantDSL := map[string]any{
|
||||
"version": "workflow-dsl/v1",
|
||||
"name": "create test",
|
||||
}
|
||||
caller, err := runAitableWorkflowCommand(t, nil,
|
||||
"create",
|
||||
"--base-id", "base-create",
|
||||
"--dsl", `{"version":"workflow-dsl/v1","name":"create test"}`,
|
||||
"--locale", "zh-CN",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("workflow create returned error: %v", err)
|
||||
}
|
||||
if len(caller.calls) != 1 {
|
||||
t.Fatalf("tool call count = %d, want 1", len(caller.calls))
|
||||
}
|
||||
call := caller.calls[0]
|
||||
if call.productID != "aitable" || call.toolName != "create_workflow" {
|
||||
t.Fatalf("tool call = %s/%s, want aitable/create_workflow", call.productID, call.toolName)
|
||||
}
|
||||
wantArgs := map[string]any{
|
||||
"baseId": "base-create",
|
||||
"dsl": wantDSL,
|
||||
"locale": "zh-CN",
|
||||
}
|
||||
if !reflect.DeepEqual(call.args, wantArgs) {
|
||||
t.Fatalf("tool args = %#v, want %#v", call.args, wantArgs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAitableWorkflowUpdateReadsDSLFile(t *testing.T) {
|
||||
path := t.TempDir() + "/workflow.json"
|
||||
if err := os.WriteFile(path, []byte(`{"version":"workflow-dsl/v1","name":"updated"}`), 0o600); err != nil {
|
||||
t.Fatalf("write workflow fixture: %v", err)
|
||||
}
|
||||
caller, err := runAitableWorkflowCommand(t, nil,
|
||||
"update",
|
||||
"--base-id", "base-update",
|
||||
"--workflow-id", "flow-existing",
|
||||
"--dsl", "@"+path,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("workflow update returned error: %v", err)
|
||||
}
|
||||
if len(caller.calls) != 1 {
|
||||
t.Fatalf("tool call count = %d, want 1", len(caller.calls))
|
||||
}
|
||||
call := caller.calls[0]
|
||||
if call.productID != "aitable" || call.toolName != "update_workflow" {
|
||||
t.Fatalf("tool call = %s/%s, want aitable/update_workflow", call.productID, call.toolName)
|
||||
}
|
||||
wantArgs := map[string]any{
|
||||
"baseId": "base-update",
|
||||
"workflowId": "flow-existing",
|
||||
"dsl": map[string]any{
|
||||
"version": "workflow-dsl/v1",
|
||||
"name": "updated",
|
||||
},
|
||||
}
|
||||
if !reflect.DeepEqual(call.args, wantArgs) {
|
||||
t.Fatalf("tool args = %#v, want %#v", call.args, wantArgs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAitableWorkflowCreateReadsDSLFromStdin(t *testing.T) {
|
||||
caller, err := runAitableWorkflowCommand(t,
|
||||
strings.NewReader(`{"version":"workflow-dsl/v1","name":"stdin"}`),
|
||||
"create", "--base-id", "base-stdin", "--dsl", "-",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("workflow create from stdin returned error: %v", err)
|
||||
}
|
||||
if got := caller.calls[0].args["dsl"].(map[string]any)["name"]; got != "stdin" {
|
||||
t.Fatalf("dsl name = %#v, want stdin", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAitableWorkflowWriteRejectsInvalidInput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
want string
|
||||
}{
|
||||
{name: "create missing dsl", args: []string{"create", "--base-id", "base"}, want: "dsl"},
|
||||
{name: "update missing workflow", args: []string{"update", "--base-id", "base", "--dsl", `{}`}, want: "workflow-id"},
|
||||
{name: "malformed json", args: []string{"create", "--base-id", "base", "--dsl", "{not-json"}, want: "JSON parse failed"},
|
||||
{name: "array", args: []string{"create", "--base-id", "base", "--dsl", `[]`}, want: "JSON parse failed"},
|
||||
{name: "null", args: []string{"create", "--base-id", "base", "--dsl", `null`}, want: "JSON object"},
|
||||
{name: "empty file path", args: []string{"create", "--base-id", "base", "--dsl", "@"}, want: "file path"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
caller, err := runAitableWorkflowCommand(t, nil, tc.args...)
|
||||
if err == nil || !strings.Contains(err.Error(), tc.want) {
|
||||
t.Fatalf("error = %v, want substring %q", err, tc.want)
|
||||
}
|
||||
if len(caller.calls) != 0 {
|
||||
t.Fatalf("invalid input reached MCP: %#v", caller.calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -27,13 +28,15 @@ func TestAtomicWrite_Basic(t *testing.T) {
|
||||
t.Fatalf("got %q, want %q", got, data)
|
||||
}
|
||||
|
||||
// Check permissions
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Stat() error = %v", err)
|
||||
}
|
||||
if perm := info.Mode().Perm(); perm != 0600 {
|
||||
t.Fatalf("permissions = %o, want 0600", perm)
|
||||
// Windows does not expose POSIX permission bits.
|
||||
if runtime.GOOS != "windows" {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Stat() error = %v", err)
|
||||
}
|
||||
if perm := info.Mode().Perm(); perm != 0600 {
|
||||
t.Fatalf("permissions = %o, want 0600", perm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,12 +152,14 @@ func TestAtomicWriteJSON_Basic(t *testing.T) {
|
||||
t.Fatalf("got %q, want %q", got, data)
|
||||
}
|
||||
|
||||
// Check permissions are 0600
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Stat() error = %v", err)
|
||||
}
|
||||
if perm := info.Mode().Perm(); perm != 0600 {
|
||||
t.Fatalf("permissions = %o, want 0600", perm)
|
||||
// Windows does not expose POSIX permission bits.
|
||||
if runtime.GOOS != "windows" {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Stat() error = %v", err)
|
||||
}
|
||||
if perm := info.Mode().Perm(); perm != 0600 {
|
||||
t.Fatalf("permissions = %o, want 0600", perm)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -24,7 +25,20 @@ import (
|
||||
// writeExecStub drops an executable shell stub named name into dir so PATH
|
||||
// lookups resolve without the real CLI installed.
|
||||
func writeExecStub(dir, name string) error {
|
||||
return os.WriteFile(filepath.Join(dir, name), []byte("#!/bin/sh\n"), 0o755)
|
||||
path := filepath.Join(dir, name)
|
||||
body := []byte("#!/bin/sh\n")
|
||||
if runtime.GOOS == "windows" {
|
||||
path += ".exe"
|
||||
body = nil
|
||||
}
|
||||
return os.WriteFile(path, body, 0o755)
|
||||
}
|
||||
|
||||
func requirePOSIXShell(t *testing.T) {
|
||||
t.Helper()
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("test fixture requires a POSIX shell executable")
|
||||
}
|
||||
}
|
||||
|
||||
// TestConvSessions covers the per-conversation session contract: first message
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
|
||||
func writeShellExecutable(t *testing.T, dir, name, body string) string {
|
||||
t.Helper()
|
||||
requirePOSIXShell(t)
|
||||
path := filepath.Join(dir, name)
|
||||
if err := os.WriteFile(path, []byte("#!/bin/sh\n"+body), 0o755); err != nil {
|
||||
t.Fatalf("write stub %s: %v", name, err)
|
||||
@@ -37,7 +38,9 @@ func TestForwarderForChannelCodexPrefersAppServer(t *testing.T) {
|
||||
clearChannelEnv(t)
|
||||
t.Setenv("DWS_CONNECT_NO_INSTALL", "1")
|
||||
stub := t.TempDir()
|
||||
writeShellExecutable(t, stub, "codex", "exit 0\n")
|
||||
if err := writeExecStub(stub, "codex"); err != nil {
|
||||
t.Fatalf("write codex stub: %v", err)
|
||||
}
|
||||
t.Setenv("PATH", stub)
|
||||
|
||||
fwd, err := forwarderForChannel("codex", "", connectAgentOptions{Memory: true})
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -89,12 +90,14 @@ func TestStageDaemonExecutable(t *testing.T) {
|
||||
if string(got) != "test-binary" {
|
||||
t.Fatalf("content = %q, want test-binary", got)
|
||||
}
|
||||
info, err := os.Stat(dst)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if info.Mode().Perm() != daemonExecutablePerm {
|
||||
t.Fatalf("mode = %o, want %o", info.Mode().Perm(), daemonExecutablePerm)
|
||||
if runtime.GOOS != "windows" {
|
||||
info, err := os.Stat(dst)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if info.Mode().Perm() != daemonExecutablePerm {
|
||||
t.Fatalf("mode = %o, want %o", info.Mode().Perm(), daemonExecutablePerm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
)
|
||||
|
||||
func TestExecForwarderRetriesMissingSessionOnce(t *testing.T) {
|
||||
requirePOSIXShell(t)
|
||||
dir := t.TempDir()
|
||||
bin := filepath.Join(dir, "agent")
|
||||
logPath := filepath.Join(dir, "calls.log")
|
||||
@@ -68,6 +69,7 @@ echo fresh-ok
|
||||
}
|
||||
|
||||
func TestExecForwarderStreamRetriesMissingSessionOnce(t *testing.T) {
|
||||
requirePOSIXShell(t)
|
||||
dir := t.TempDir()
|
||||
bin := filepath.Join(dir, "agent-stream")
|
||||
logPath := filepath.Join(dir, "calls.log")
|
||||
@@ -113,6 +115,7 @@ printf '%s\n' '{"type":"result","result":"fresh-stream-ok"}'
|
||||
}
|
||||
|
||||
func TestExecForwarderDoesNotRetryNonSessionErrors(t *testing.T) {
|
||||
requirePOSIXShell(t)
|
||||
dir := t.TempDir()
|
||||
bin := filepath.Join(dir, "agent")
|
||||
logPath := filepath.Join(dir, "calls.log")
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
eventprocess "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/process"
|
||||
)
|
||||
|
||||
// connectLockDir is a var so tests can isolate lock files.
|
||||
@@ -67,16 +67,8 @@ func sanitizeLockID(s string) string {
|
||||
}, s)
|
||||
}
|
||||
|
||||
// processAlive reports whether pid refers to a live process (unix signal-0
|
||||
// probe; a permission error still means "alive").
|
||||
// processAlive reports whether pid refers to a live process using the shared
|
||||
// Unix signal-0 / Windows OpenProcess implementation.
|
||||
func processAlive(pid int) bool {
|
||||
proc, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
err = proc.Signal(syscall.Signal(0))
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
return errors.Is(err, syscall.EPERM)
|
||||
return eventprocess.Alive(pid)
|
||||
}
|
||||
@@ -108,6 +108,7 @@ func TestForwarderProviderBaseURLDropsHaiku(t *testing.T) {
|
||||
// agent that prints "API Error: 422 ..." to stdout (and exits 0) must NOT have
|
||||
// that raw error forwarded as the answer; forward returns an actionable hint.
|
||||
func TestForwardReturnsHintOnAPIError(t *testing.T) {
|
||||
requirePOSIXShell(t)
|
||||
stub := t.TempDir()
|
||||
bin := filepath.Join(stub, "fakeagent")
|
||||
// Print a provider 422 to stdout and exit 0, exactly like claude does when
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
|
||||
func writeQoderStreamStub(t *testing.T, dir string) string {
|
||||
t.Helper()
|
||||
requirePOSIXShell(t)
|
||||
logPath := filepath.Join(dir, "qoder-starts.log")
|
||||
script := `#!/usr/bin/env python3
|
||||
import json
|
||||
|
||||
@@ -40,6 +40,7 @@ func TestForwardConnectTurnPreservesAttachmentsForCapableAgent(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestExecForwarderAllowsOnlyAttachmentDirectory(t *testing.T) {
|
||||
requirePOSIXShell(t)
|
||||
dir := t.TempDir()
|
||||
logPath := filepath.Join(dir, "args.log")
|
||||
stub := filepath.Join(dir, "agent")
|
||||
|
||||
@@ -223,7 +223,9 @@ func TestResolveAgentYoloMode(t *testing.T) {
|
||||
func TestForwarderForChannel(t *testing.T) {
|
||||
clearChannelEnv(t)
|
||||
stub := t.TempDir()
|
||||
writeShellExecutable(t, stub, "codex", "exit 0\n")
|
||||
if err := writeExecStub(stub, "codex"); err != nil {
|
||||
t.Fatalf("write codex stub: %v", err)
|
||||
}
|
||||
t.Setenv("PATH", stub)
|
||||
// DWS_AGENT_CMD covers ordinary stream-bridge channels so the test does not
|
||||
// depend on those binaries being installed. Codex ignores it and stays on
|
||||
|
||||
+2
-221
@@ -2543,133 +2543,7 @@ CLI 内部自动完成全部流程:
|
||||
# 自定义导入后的文档名称
|
||||
dws doc import --file ./draft.md --name "项目周报"`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
filePath := mustGetFlag(cmd, "file")
|
||||
if filePath == "" && len(args) > 0 {
|
||||
filePath = args[0]
|
||||
}
|
||||
if filePath == "" {
|
||||
return fmt.Errorf("flag --file is required (or pass file path as argument)")
|
||||
}
|
||||
|
||||
fi, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot read file %s: %w", filePath, err)
|
||||
}
|
||||
if fi.IsDir() {
|
||||
return fmt.Errorf("%s is a directory, not a file", filePath)
|
||||
}
|
||||
|
||||
const maxFileSize = 20 * 1024 * 1024
|
||||
fileSize := fi.Size()
|
||||
if fileSize > maxFileSize {
|
||||
return fmt.Errorf("file size %d bytes exceeds 20MB limit", fileSize)
|
||||
}
|
||||
if fileSize == 0 {
|
||||
return fmt.Errorf("file is empty: %s", filePath)
|
||||
}
|
||||
|
||||
ext := strings.TrimPrefix(strings.ToLower(filepath.Ext(filePath)), ".")
|
||||
supportedFormats := map[string]bool{
|
||||
"docx": true, "doc": true, "xlsx": true, "xls": true,
|
||||
"md": true, "txt": true, "xmind": true, "mark": true,
|
||||
}
|
||||
if !supportedFormats[ext] {
|
||||
return fmt.Errorf("unsupported file format %q, supported: docx, doc, xlsx, xls, md, txt, xmind, mark", ext)
|
||||
}
|
||||
|
||||
fileName := filepath.Base(filePath)
|
||||
name, _ := cmd.Flags().GetString("name")
|
||||
if name == "" {
|
||||
name = strings.TrimSuffix(fileName, filepath.Ext(fileName))
|
||||
}
|
||||
folder := flagOrFallback(cmd, "folder", "folder-id")
|
||||
workspace := flagOrFallback(cmd, "workspace", "workspace-id")
|
||||
|
||||
if deps.Caller.DryRun() {
|
||||
deps.Out.PrintKeyValue("操作", "导入本地文件为在线文档")
|
||||
deps.Out.PrintKeyValue("文件", filePath)
|
||||
deps.Out.PrintKeyValue("名称", name)
|
||||
deps.Out.PrintKeyValue("格式", ext)
|
||||
deps.Out.PrintKeyValue("大小", fmt.Sprintf("%d bytes", fileSize))
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
deps.Out.PrintInfo("[1/4] 创建导入会话...")
|
||||
sessionArgs := map[string]any{
|
||||
"fileName": name,
|
||||
"suffix": ext,
|
||||
"fileSize": fileSize,
|
||||
}
|
||||
if folder != "" {
|
||||
sessionArgs["targetFolderId"] = folder
|
||||
}
|
||||
if workspace != "" {
|
||||
sessionArgs["workspaceId"] = workspace
|
||||
}
|
||||
|
||||
sessionText, err := callMCPToolReturnText(ctx, "create_import_session", sessionArgs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建导入会话失败: %w", err)
|
||||
}
|
||||
|
||||
var sessionResult map[string]any
|
||||
if err := json.Unmarshal([]byte(sessionText), &sessionResult); err != nil {
|
||||
return fmt.Errorf("解析导入会话响应失败: %w", err)
|
||||
}
|
||||
sessionID, _ := sessionResult["sessionId"].(string)
|
||||
uploadURL, _ := sessionResult["uploadUrl"].(string)
|
||||
if sessionID == "" || uploadURL == "" {
|
||||
deps.Out.PrintRaw(sessionText)
|
||||
return fmt.Errorf("创建导入会话成功但缺少 sessionId 或 uploadUrl")
|
||||
}
|
||||
deps.Out.PrintInfo(fmt.Sprintf(" 会话已创建,sessionId: %s", sessionID))
|
||||
|
||||
deps.Out.PrintInfo("[2/4] 上传文件...")
|
||||
if err := httpPutFile(ctx, uploadURL, nil, filePath, fileSize); err != nil {
|
||||
return fmt.Errorf("文件上传失败 (sessionId=%s): %w", sessionID, err)
|
||||
}
|
||||
deps.Out.PrintInfo(" 文件上传完成")
|
||||
|
||||
deps.Out.PrintInfo("[3/4] 确认导入,启动格式转换...")
|
||||
confirmText, err := callMCPToolReturnText(ctx, "confirm_import", map[string]any{
|
||||
"sessionId": sessionID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("确认导入失败 (sessionId=%s): %w", sessionID, err)
|
||||
}
|
||||
|
||||
var confirmResult map[string]any
|
||||
if err := json.Unmarshal([]byte(confirmText), &confirmResult); err != nil {
|
||||
return fmt.Errorf("解析确认导入响应失败: %w", err)
|
||||
}
|
||||
taskID, _ := confirmResult["taskId"].(string)
|
||||
if taskID == "" {
|
||||
deps.Out.PrintRaw(confirmText)
|
||||
return fmt.Errorf("确认导入成功但未返回 taskId")
|
||||
}
|
||||
deps.Out.PrintInfo(fmt.Sprintf(" 转换任务已提交,taskId: %s", taskID))
|
||||
|
||||
deps.Out.PrintInfo("[4/4] 等待格式转换完成...")
|
||||
importResult, err := pollDocImportTask(ctx, taskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
documentURL, _ := importResult["documentUrl"].(string)
|
||||
documentName, _ := importResult["documentName"].(string)
|
||||
documentType, _ := importResult["documentType"].(string)
|
||||
|
||||
deps.Out.PrintInfo(fmt.Sprintf("导入完成: %s", documentURL))
|
||||
deps.Out.PrintJSON(map[string]any{
|
||||
"success": true,
|
||||
"taskId": taskID,
|
||||
"documentUrl": documentURL,
|
||||
"documentName": documentName,
|
||||
"documentType": documentType,
|
||||
})
|
||||
return nil
|
||||
return runImportCommand(cmd, args, docImportFlowConfig())
|
||||
},
|
||||
}
|
||||
importCmd.Flags().String("file", "", "本地文件路径 (必填)")
|
||||
@@ -2694,43 +2568,7 @@ CLI 内部自动完成全部流程:
|
||||
failed 导入失败`,
|
||||
Example: ` dws doc import get --task-id <TASK_ID>`,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
taskID := mustGetFlag(cmd, "task-id")
|
||||
if taskID == "" {
|
||||
return fmt.Errorf("flag --task-id is required")
|
||||
}
|
||||
|
||||
if deps.Caller.DryRun() {
|
||||
deps.Out.PrintKeyValue("操作", "查询导入任务结果")
|
||||
deps.Out.PrintKeyValue("任务ID", taskID)
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
text, err := callMCPToolReturnText(ctx, "query_import_task", map[string]any{"taskId": taskID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var result map[string]any
|
||||
if err := json.Unmarshal([]byte(text), &result); err != nil {
|
||||
deps.Out.PrintRaw(text)
|
||||
return nil
|
||||
}
|
||||
|
||||
status, _ := result["status"].(string)
|
||||
message, _ := result["message"].(string)
|
||||
|
||||
switch strings.ToLower(status) {
|
||||
case "completed", "processing":
|
||||
deps.Out.PrintJSON(result)
|
||||
return nil
|
||||
default:
|
||||
deps.Out.PrintJSON(result)
|
||||
if message != "" {
|
||||
return fmt.Errorf("导入任务失败 (status=%s): %s", status, message)
|
||||
}
|
||||
return fmt.Errorf("导入任务失败 (status=%s)", status)
|
||||
}
|
||||
return runImportGetCommand(cmd, docImportFlowConfig())
|
||||
},
|
||||
}
|
||||
importGetCmd.Flags().String("task-id", "", "导入任务 ID (必填)")
|
||||
@@ -3237,63 +3075,6 @@ func pollDocExportJob(ctx context.Context, jobID string) (downloadURL string, er
|
||||
return "", fmt.Errorf("导出任务超时:已轮询 %d 次仍在处理中 (jobId=%s),请稍后使用 dws doc export get --job-id %s 手动查询", maxPolls, jobID, jobID)
|
||||
}
|
||||
|
||||
// pollDocImportTask polls the import task status with progressive backoff.
|
||||
func pollDocImportTask(ctx context.Context, taskID string) (map[string]any, error) {
|
||||
const maxPolls = 30
|
||||
|
||||
pollInterval := func(attempt int) time.Duration {
|
||||
switch {
|
||||
case attempt <= 5:
|
||||
return 2 * time.Second
|
||||
case attempt <= 10:
|
||||
return 5 * time.Second
|
||||
case attempt <= 20:
|
||||
return 10 * time.Second
|
||||
default:
|
||||
return 15 * time.Second
|
||||
}
|
||||
}
|
||||
|
||||
for attempt := 1; attempt <= maxPolls; attempt++ {
|
||||
interval := pollInterval(attempt)
|
||||
deps.Out.PrintInfo(fmt.Sprintf(" 第 %d/%d 次查询,等待 %v ...", attempt, maxPolls, interval))
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("导入轮询被取消 (taskId=%s): %w", taskID, ctx.Err())
|
||||
case <-time.After(interval):
|
||||
}
|
||||
|
||||
text, queryErr := callMCPToolReturnText(ctx, "query_import_task", map[string]any{"taskId": taskID})
|
||||
if queryErr != nil {
|
||||
return nil, fmt.Errorf("查询导入任务失败 (taskId=%s): %w", taskID, queryErr)
|
||||
}
|
||||
|
||||
var result map[string]any
|
||||
if parseErr := json.Unmarshal([]byte(text), &result); parseErr != nil {
|
||||
return nil, fmt.Errorf("解析查询结果失败 (taskId=%s): %w", taskID, parseErr)
|
||||
}
|
||||
|
||||
status, _ := result["status"].(string)
|
||||
switch strings.ToLower(status) {
|
||||
case "completed":
|
||||
return result, nil
|
||||
case "processing":
|
||||
continue
|
||||
case "failed":
|
||||
message, _ := result["message"].(string)
|
||||
if message != "" {
|
||||
return nil, fmt.Errorf("导入任务失败 (taskId=%s): %s", taskID, message)
|
||||
}
|
||||
return nil, fmt.Errorf("导入任务失败 (taskId=%s)", taskID)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("导入任务超时:已轮询 %d 次仍在处理中 (taskId=%s),请稍后使用 dws doc import get --task-id %s 手动查询", maxPolls, taskID, taskID)
|
||||
}
|
||||
|
||||
// stripDuplicateTitle removes the leading H1 heading from markdown content
|
||||
// when it matches the document name (set via --name). This prevents the title
|
||||
// from appearing twice: once as document metadata and once in the body.
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const importMaxFileSize int64 = 20 * 1024 * 1024
|
||||
|
||||
type importPollTimeoutError struct {
|
||||
taskID string
|
||||
maxPolls int
|
||||
}
|
||||
|
||||
func (e *importPollTimeoutError) Error() string {
|
||||
return fmt.Sprintf("导入任务超时:已轮询 %d 次仍在处理中 (taskId=%s)", e.maxPolls, e.taskID)
|
||||
}
|
||||
|
||||
type importPollPolicy struct {
|
||||
maxPolls int
|
||||
interval func(attempt int) time.Duration
|
||||
wait func(context.Context, time.Duration) error
|
||||
}
|
||||
|
||||
type importFlowConfig struct {
|
||||
operation string
|
||||
queryOperation string
|
||||
supportedFormats map[string]bool
|
||||
supportedFormatsText string
|
||||
folderFlags []string
|
||||
workspaceFlags []string
|
||||
requireTarget bool
|
||||
serverID string
|
||||
includeNodeID bool
|
||||
timeoutAsResult bool
|
||||
nextCommand string
|
||||
poll importPollPolicy
|
||||
}
|
||||
|
||||
type preparedImportFile struct {
|
||||
path string
|
||||
name string
|
||||
extension string
|
||||
size int64
|
||||
folder string
|
||||
workspace string
|
||||
}
|
||||
|
||||
func defaultImportPollPolicy() importPollPolicy {
|
||||
return importPollPolicy{
|
||||
maxPolls: 30,
|
||||
interval: func(attempt int) time.Duration {
|
||||
switch {
|
||||
case attempt <= 5:
|
||||
return 2 * time.Second
|
||||
case attempt <= 10:
|
||||
return 5 * time.Second
|
||||
case attempt <= 20:
|
||||
return 10 * time.Second
|
||||
default:
|
||||
return 15 * time.Second
|
||||
}
|
||||
},
|
||||
wait: waitForImportPoll,
|
||||
}
|
||||
}
|
||||
|
||||
func docImportFlowConfig() importFlowConfig {
|
||||
return importFlowConfig{
|
||||
operation: "导入本地文件为在线文档",
|
||||
queryOperation: "查询导入任务结果",
|
||||
supportedFormats: map[string]bool{"docx": true, "doc": true, "xlsx": true, "xls": true, "md": true, "txt": true, "xmind": true, "mark": true},
|
||||
supportedFormatsText: "docx, doc, xlsx, xls, md, txt, xmind, mark",
|
||||
folderFlags: []string{"folder", "folder-id"},
|
||||
workspaceFlags: []string{"workspace", "workspace-id"},
|
||||
nextCommand: "dws doc import get --task-id %s",
|
||||
poll: defaultImportPollPolicy(),
|
||||
}
|
||||
}
|
||||
|
||||
func sheetImportFlowConfig() importFlowConfig {
|
||||
return importFlowConfig{
|
||||
operation: "导入本地表格文件为在线电子表格",
|
||||
queryOperation: "查询表格导入任务结果",
|
||||
supportedFormats: map[string]bool{"xlsx": true, "xls": true},
|
||||
supportedFormatsText: "xlsx, xls",
|
||||
folderFlags: []string{"folder-token", "folder"},
|
||||
workspaceFlags: []string{"workspace"},
|
||||
requireTarget: true,
|
||||
serverID: "doc",
|
||||
includeNodeID: true,
|
||||
timeoutAsResult: true,
|
||||
nextCommand: "dws sheet import get --task-id %s",
|
||||
poll: defaultImportPollPolicy(),
|
||||
}
|
||||
}
|
||||
|
||||
func waitForImportPoll(ctx context.Context, duration time.Duration) error {
|
||||
timer := time.NewTimer(duration)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-timer.C:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func importFlagValue(cmd *cobra.Command, names ...string) string {
|
||||
for _, name := range names {
|
||||
if cmd.Flags().Lookup(name) == nil {
|
||||
continue
|
||||
}
|
||||
if value, _ := cmd.Flags().GetString(name); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func prepareImportFile(cmd *cobra.Command, args []string, cfg importFlowConfig) (preparedImportFile, error) {
|
||||
filePath := mustGetFlag(cmd, "file")
|
||||
if filePath == "" && len(args) > 0 {
|
||||
filePath = args[0]
|
||||
}
|
||||
if filePath == "" {
|
||||
return preparedImportFile{}, fmt.Errorf("flag --file is required (or pass file path as argument)")
|
||||
}
|
||||
|
||||
fileInfo, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
return preparedImportFile{}, fmt.Errorf("cannot read file %s: %w", filePath, err)
|
||||
}
|
||||
if fileInfo.IsDir() {
|
||||
return preparedImportFile{}, fmt.Errorf("%s is a directory, not a file", filePath)
|
||||
}
|
||||
if fileInfo.Size() > importMaxFileSize {
|
||||
return preparedImportFile{}, fmt.Errorf("file size %d bytes exceeds 20MB limit", fileInfo.Size())
|
||||
}
|
||||
if fileInfo.Size() == 0 {
|
||||
return preparedImportFile{}, fmt.Errorf("file is empty: %s", filePath)
|
||||
}
|
||||
|
||||
extension := strings.TrimPrefix(strings.ToLower(filepath.Ext(filePath)), ".")
|
||||
if !cfg.supportedFormats[extension] {
|
||||
return preparedImportFile{}, fmt.Errorf("unsupported file format %q, supported: %s", extension, cfg.supportedFormatsText)
|
||||
}
|
||||
|
||||
name, _ := cmd.Flags().GetString("name")
|
||||
if name == "" {
|
||||
fileName := filepath.Base(filePath)
|
||||
name = strings.TrimSuffix(fileName, filepath.Ext(fileName))
|
||||
}
|
||||
folder := importFlagValue(cmd, cfg.folderFlags...)
|
||||
workspace := importFlagValue(cmd, cfg.workspaceFlags...)
|
||||
if cfg.requireTarget && folder == "" && workspace == "" {
|
||||
return preparedImportFile{}, fmt.Errorf("--folder-token 与 --workspace 至少需要提供一个(导入目标位置)")
|
||||
}
|
||||
|
||||
return preparedImportFile{
|
||||
path: filePath,
|
||||
name: name,
|
||||
extension: extension,
|
||||
size: fileInfo.Size(),
|
||||
folder: folder,
|
||||
workspace: workspace,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cfg importFlowConfig) callTool(ctx context.Context, toolName string, args map[string]any) (string, error) {
|
||||
if cfg.serverID != "" {
|
||||
return callMCPToolReturnTextOnServer(ctx, cfg.serverID, toolName, args)
|
||||
}
|
||||
return callMCPToolReturnText(ctx, toolName, args)
|
||||
}
|
||||
|
||||
func runImportCommand(cmd *cobra.Command, args []string, cfg importFlowConfig) error {
|
||||
file, err := prepareImportFile(cmd, args, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if deps.Caller.DryRun() {
|
||||
deps.Out.PrintKeyValue("操作", cfg.operation)
|
||||
deps.Out.PrintKeyValue("文件", file.path)
|
||||
deps.Out.PrintKeyValue("名称", file.name)
|
||||
deps.Out.PrintKeyValue("格式", file.extension)
|
||||
deps.Out.PrintKeyValue("大小", fmt.Sprintf("%d bytes", file.size))
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx := cmd.Context()
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
deps.Out.PrintInfo("[1/4] 创建导入会话...")
|
||||
sessionArgs := map[string]any{
|
||||
"fileName": file.name,
|
||||
"suffix": file.extension,
|
||||
"fileSize": file.size,
|
||||
}
|
||||
if file.folder != "" {
|
||||
sessionArgs["targetFolderId"] = file.folder
|
||||
}
|
||||
if file.workspace != "" {
|
||||
sessionArgs["workspaceId"] = file.workspace
|
||||
}
|
||||
|
||||
sessionText, err := cfg.callTool(ctx, "create_import_session", sessionArgs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建导入会话失败: %w", err)
|
||||
}
|
||||
var sessionResult map[string]any
|
||||
if err := json.Unmarshal([]byte(sessionText), &sessionResult); err != nil {
|
||||
return fmt.Errorf("解析导入会话响应失败: %w", err)
|
||||
}
|
||||
sessionID, _ := sessionResult["sessionId"].(string)
|
||||
uploadURL, _ := sessionResult["uploadUrl"].(string)
|
||||
if sessionID == "" || uploadURL == "" {
|
||||
deps.Out.PrintRaw(sessionText)
|
||||
return fmt.Errorf("创建导入会话成功但缺少 sessionId 或 uploadUrl")
|
||||
}
|
||||
deps.Out.PrintInfo(fmt.Sprintf(" 会话已创建,sessionId: %s", sessionID))
|
||||
|
||||
deps.Out.PrintInfo("[2/4] 上传文件...")
|
||||
if err := httpPutFile(ctx, uploadURL, nil, file.path, file.size); err != nil {
|
||||
return fmt.Errorf("文件上传失败 (sessionId=%s): %w", sessionID, err)
|
||||
}
|
||||
deps.Out.PrintInfo(" 文件上传完成")
|
||||
|
||||
deps.Out.PrintInfo("[3/4] 确认导入,启动格式转换...")
|
||||
confirmText, err := cfg.callTool(ctx, "confirm_import", map[string]any{"sessionId": sessionID})
|
||||
if err != nil {
|
||||
return fmt.Errorf("确认导入失败 (sessionId=%s): %w", sessionID, err)
|
||||
}
|
||||
var confirmResult map[string]any
|
||||
if err := json.Unmarshal([]byte(confirmText), &confirmResult); err != nil {
|
||||
return fmt.Errorf("解析确认导入响应失败: %w", err)
|
||||
}
|
||||
taskID, _ := confirmResult["taskId"].(string)
|
||||
if taskID == "" {
|
||||
deps.Out.PrintRaw(confirmText)
|
||||
return fmt.Errorf("确认导入成功但未返回 taskId")
|
||||
}
|
||||
deps.Out.PrintInfo(fmt.Sprintf(" 转换任务已提交,taskId: %s", taskID))
|
||||
|
||||
deps.Out.PrintInfo("[4/4] 等待格式转换完成...")
|
||||
result, err := pollImportTask(ctx, taskID, cfg)
|
||||
if err != nil {
|
||||
var timeoutErr *importPollTimeoutError
|
||||
if !errors.As(err, &timeoutErr) {
|
||||
return err
|
||||
}
|
||||
if cfg.timeoutAsResult {
|
||||
deps.Out.PrintInfo(timeoutErr.Error())
|
||||
_ = deps.Out.PrintJSON(map[string]any{
|
||||
"success": false,
|
||||
"timed_out": true,
|
||||
"taskId": taskID,
|
||||
"status": "processing",
|
||||
"next_command": fmt.Sprintf(cfg.nextCommand, taskID),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%s,请稍后使用 %s 手动查询", timeoutErr.Error(), fmt.Sprintf(cfg.nextCommand, taskID))
|
||||
}
|
||||
|
||||
documentURL, _ := result["documentUrl"].(string)
|
||||
documentName, _ := result["documentName"].(string)
|
||||
documentType, _ := result["documentType"].(string)
|
||||
finalResult := map[string]any{
|
||||
"success": true,
|
||||
"taskId": taskID,
|
||||
"documentUrl": documentURL,
|
||||
"documentName": documentName,
|
||||
"documentType": documentType,
|
||||
}
|
||||
if cfg.includeNodeID {
|
||||
finalResult["nodeId"] = extractNodeIDFromDocURL(documentURL)
|
||||
}
|
||||
deps.Out.PrintInfo(fmt.Sprintf("导入完成: %s", documentURL))
|
||||
_ = deps.Out.PrintJSON(finalResult)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runImportGetCommand(cmd *cobra.Command, cfg importFlowConfig) error {
|
||||
taskID := mustGetFlag(cmd, "task-id")
|
||||
if taskID == "" {
|
||||
return fmt.Errorf("flag --task-id is required")
|
||||
}
|
||||
if deps.Caller.DryRun() {
|
||||
deps.Out.PrintKeyValue("操作", cfg.queryOperation)
|
||||
deps.Out.PrintKeyValue("任务ID", taskID)
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx := cmd.Context()
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
text, err := cfg.callTool(ctx, "query_import_task", map[string]any{"taskId": taskID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var result map[string]any
|
||||
if err := json.Unmarshal([]byte(text), &result); err != nil {
|
||||
deps.Out.PrintRaw(text)
|
||||
return nil
|
||||
}
|
||||
status, _ := result["status"].(string)
|
||||
message, _ := result["message"].(string)
|
||||
if strings.EqualFold(status, "completed") {
|
||||
if cfg.includeNodeID {
|
||||
documentURL, _ := result["documentUrl"].(string)
|
||||
result["nodeId"] = extractNodeIDFromDocURL(documentURL)
|
||||
}
|
||||
_ = deps.Out.PrintJSON(result)
|
||||
return nil
|
||||
}
|
||||
if strings.EqualFold(status, "processing") {
|
||||
_ = deps.Out.PrintJSON(result)
|
||||
return nil
|
||||
}
|
||||
|
||||
_ = deps.Out.PrintJSON(result)
|
||||
if message != "" {
|
||||
return fmt.Errorf("导入任务失败 (status=%s): %s", status, message)
|
||||
}
|
||||
return fmt.Errorf("导入任务失败 (status=%s)", status)
|
||||
}
|
||||
|
||||
func pollImportTask(ctx context.Context, taskID string, cfg importFlowConfig) (map[string]any, error) {
|
||||
poll := cfg.poll
|
||||
if poll.maxPolls <= 0 || poll.interval == nil || poll.wait == nil {
|
||||
poll = defaultImportPollPolicy()
|
||||
}
|
||||
for attempt := 1; attempt <= poll.maxPolls; attempt++ {
|
||||
interval := poll.interval(attempt)
|
||||
deps.Out.PrintInfo(fmt.Sprintf(" 第 %d/%d 次查询,等待 %v ...", attempt, poll.maxPolls, interval))
|
||||
if err := poll.wait(ctx, interval); err != nil {
|
||||
return nil, fmt.Errorf("导入轮询被取消 (taskId=%s): %w", taskID, err)
|
||||
}
|
||||
|
||||
text, err := cfg.callTool(ctx, "query_import_task", map[string]any{"taskId": taskID})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询导入任务失败 (taskId=%s): %w", taskID, err)
|
||||
}
|
||||
var result map[string]any
|
||||
if err := json.Unmarshal([]byte(text), &result); err != nil {
|
||||
return nil, fmt.Errorf("解析查询结果失败 (taskId=%s): %w", taskID, err)
|
||||
}
|
||||
status, _ := result["status"].(string)
|
||||
switch strings.ToLower(status) {
|
||||
case "completed":
|
||||
return result, nil
|
||||
case "processing":
|
||||
continue
|
||||
case "failed":
|
||||
message, _ := result["message"].(string)
|
||||
if message != "" {
|
||||
return nil, fmt.Errorf("导入任务失败 (taskId=%s): %s", taskID, message)
|
||||
}
|
||||
return nil, fmt.Errorf("导入任务失败 (taskId=%s)", taskID)
|
||||
}
|
||||
}
|
||||
return nil, &importPollTimeoutError{taskID: taskID, maxPolls: poll.maxPolls}
|
||||
}
|
||||
|
||||
func extractNodeIDFromDocURL(rawURL string) string {
|
||||
parsed, err := url.Parse(rawURL)
|
||||
if err != nil || parsed.Path == "" {
|
||||
return ""
|
||||
}
|
||||
nodeID := path.Base(strings.TrimRight(parsed.Path, "/"))
|
||||
if nodeID == "." || nodeID == "/" {
|
||||
return ""
|
||||
}
|
||||
return nodeID
|
||||
}
|
||||
@@ -93,6 +93,7 @@ func newSheetCommand() *cobra.Command {
|
||||
dws sheet chart update 更新浮动图表
|
||||
dws sheet chart delete 删除浮动图表
|
||||
dws sheet export 导出表格为 xlsx(异步任务一站式:提交→轮询→可选下载)
|
||||
dws sheet import 导入 xlsx/xls 为在线电子表格
|
||||
dws sheet template list 获取表格模板列表
|
||||
dws sheet template search 搜索表格模板
|
||||
dws sheet template apply 应用表格模板创建新表格文档`,
|
||||
@@ -110,6 +111,7 @@ func newSheetCommand() *cobra.Command {
|
||||
floatImageCmds := newFloatImageCmds()
|
||||
chartCmd := newChartCmd()
|
||||
exportCmd := newExportCmd()
|
||||
importCmd := newSheetImportCmd()
|
||||
templateCmd := newSheetTemplateCmd()
|
||||
tableCmds := newTableCmds()
|
||||
pivotTableCmd := newPivotTableCmd()
|
||||
@@ -134,7 +136,7 @@ func newSheetCommand() *cobra.Command {
|
||||
standaloneCmds = append(standaloneCmds, mediaCmds...)
|
||||
standaloneCmds = append(standaloneCmds, floatImageCmds...)
|
||||
standaloneCmds = append(standaloneCmds, tableCmds...)
|
||||
standaloneCmds = append(standaloneCmds, exportCmd, batchUpdateCmd)
|
||||
standaloneCmds = append(standaloneCmds, exportCmd, importCmd, batchUpdateCmd)
|
||||
|
||||
// Register cross-product aliases
|
||||
for _, cmd := range standaloneCmds {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package helpers
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
const sheetImportLong = `将本地表格文件导入为一个新的钉钉在线电子表格,与 dws sheet export(导出)对称。
|
||||
|
||||
支持的文件格式 (按扩展名):
|
||||
xlsx, xls Microsoft Excel 表格
|
||||
|
||||
文件大小限制: 20MB
|
||||
固定导入为电子表格类型;只新建文档,不操作已有表格。
|
||||
|
||||
CLI 内部自动完成全部流程:
|
||||
1. 创建导入会话(获取 OSS 上传凭证)
|
||||
2. 上传文件到 OSS
|
||||
3. 确认导入(触发格式转换)
|
||||
4. 渐进式退避轮询等待完成(最多约 5 分钟)
|
||||
|
||||
如果轮询超时仍未完成,会输出 taskId 供后续手动查询:
|
||||
dws sheet import get --task-id <taskId>`
|
||||
|
||||
func newSheetImportCmd() *cobra.Command {
|
||||
return newSheetImportCmdWithConfig(sheetImportFlowConfig())
|
||||
}
|
||||
|
||||
func newSheetImportCmdWithConfig(cfg importFlowConfig) *cobra.Command {
|
||||
importCmd := &cobra.Command{
|
||||
Use: "import",
|
||||
Short: "导入本地表格文件为在线电子表格 (xlsx / xls)",
|
||||
Long: sheetImportLong,
|
||||
Example: ` # 导入 xlsx 为在线电子表格(默认表格名取文件名)
|
||||
dws sheet import --file ./quote.xlsx --folder-token <FOLDER_TOKEN>
|
||||
|
||||
# 指定目标文件夹与导入后表格名称
|
||||
dws sheet import --file ./report.xls --folder-token <FOLDER_TOKEN> --name "月度报表"
|
||||
|
||||
# 导入到指定知识库
|
||||
dws sheet import --file ./data.xls --workspace <WORKSPACE_ID>`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runImportCommand(cmd, args, cfg)
|
||||
},
|
||||
}
|
||||
addSheetImportFlags(importCmd)
|
||||
|
||||
// Schema deliberately binds only runnable leaves. Keep the historical
|
||||
// runnable parent for CLI compatibility, and expose the same action through
|
||||
// a leaf so agents can discover and invoke the import operation.
|
||||
importCreateCmd := &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "导入本地表格文件为在线电子表格 (xlsx / xls)",
|
||||
Long: sheetImportLong,
|
||||
Example: ` dws sheet import create --file ./quote.xlsx --folder-token <FOLDER_TOKEN>
|
||||
dws sheet import create --file ./data.xls --workspace <WORKSPACE_ID> --name "月度报表"`,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runImportCommand(cmd, args, cfg)
|
||||
},
|
||||
}
|
||||
addSheetImportFlags(importCreateCmd)
|
||||
|
||||
importGetCmd := &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "查询表格导入任务结果(手动兜底)",
|
||||
Long: `根据 taskId 查询表格导入任务的执行结果。
|
||||
通常不需要手动调用,dws sheet import 会自动完成轮询。
|
||||
仅在导入命令超时或中断后,用于手动查询任务状态。
|
||||
|
||||
任务状态:
|
||||
processing 转换中
|
||||
completed 导入成功,返回 documentUrl
|
||||
failed 导入失败`,
|
||||
Example: ` dws sheet import get --task-id <TASK_ID>`,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
return runImportGetCommand(cmd, cfg)
|
||||
},
|
||||
}
|
||||
importGetCmd.Flags().String("task-id", "", "导入任务 ID (必填)")
|
||||
importCmd.AddCommand(importCreateCmd, importGetCmd)
|
||||
return importCmd
|
||||
}
|
||||
|
||||
func addSheetImportFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().String("file", "", "本地表格文件路径 (必填,支持 xlsx/xls)")
|
||||
cmd.Flags().String("folder-token", "", "目标文件夹 ID 或 URL (与 --workspace 至少传一个)")
|
||||
cmd.Flags().String("workspace", "", "目标知识库 ID 或 URL (与 --folder-token 至少传一个)")
|
||||
cmd.Flags().StringP("name", "n", "", "导入后表格名称 (可选,默认取文件名)")
|
||||
cmd.Flags().String("folder", "", "")
|
||||
_ = cmd.Flags().MarkHidden("folder")
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
||||
)
|
||||
|
||||
type sheetImportCall struct {
|
||||
server string
|
||||
tool string
|
||||
args map[string]any
|
||||
}
|
||||
|
||||
type sheetImportCaller struct {
|
||||
calls []sheetImportCall
|
||||
responses map[string][]string
|
||||
dryRun bool
|
||||
}
|
||||
|
||||
func (c *sheetImportCaller) CallTool(_ context.Context, server, tool string, args map[string]any) (*edition.ToolResult, error) {
|
||||
c.calls = append(c.calls, sheetImportCall{server: server, tool: tool, args: args})
|
||||
responses := c.responses[tool]
|
||||
if len(responses) == 0 {
|
||||
return nil, fmt.Errorf("unexpected tool call %s/%s", server, tool)
|
||||
}
|
||||
c.responses[tool] = responses[1:]
|
||||
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: responses[0]}}}, nil
|
||||
}
|
||||
|
||||
func (*sheetImportCaller) Format() string { return "json" }
|
||||
func (c *sheetImportCaller) DryRun() bool { return c.dryRun }
|
||||
func (*sheetImportCaller) Fields() string { return "" }
|
||||
func (*sheetImportCaller) JQ() string { return "" }
|
||||
|
||||
func fastSheetImportConfig() importFlowConfig {
|
||||
cfg := sheetImportFlowConfig()
|
||||
cfg.poll.maxPolls = 3
|
||||
cfg.poll.interval = func(int) time.Duration { return 0 }
|
||||
cfg.poll.wait = func(context.Context, time.Duration) error { return nil }
|
||||
return cfg
|
||||
}
|
||||
|
||||
func executeSheetImportCommand(t *testing.T, caller *sheetImportCaller, cfg importFlowConfig, args ...string) (string, error) {
|
||||
t.Helper()
|
||||
previousDeps := deps
|
||||
previousArgs := os.Args
|
||||
t.Cleanup(func() {
|
||||
deps = previousDeps
|
||||
os.Args = previousArgs
|
||||
SetHTTPPutFile(nil)
|
||||
})
|
||||
|
||||
InitDeps(caller)
|
||||
var output bytes.Buffer
|
||||
deps.Out.w = &output
|
||||
deps.Out.errW = &output
|
||||
os.Args = append([]string{"dws", "sheet"}, args...)
|
||||
root := newSheetImportCmdWithConfig(cfg)
|
||||
root.SilenceErrors = true
|
||||
root.SilenceUsage = true
|
||||
root.SetArgs(args)
|
||||
err := root.Execute()
|
||||
return output.String(), err
|
||||
}
|
||||
|
||||
func writeImportFixture(t *testing.T, ext string) string {
|
||||
t.Helper()
|
||||
filePath := filepath.Join(t.TempDir(), "sales."+ext)
|
||||
if err := os.WriteFile(filePath, []byte("test workbook"), 0o600); err != nil {
|
||||
t.Fatalf("write fixture: %v", err)
|
||||
}
|
||||
return filePath
|
||||
}
|
||||
|
||||
func TestSheetImportRejectsInvalidInputBeforeRemoteCall(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fileExt string
|
||||
args func(string) []string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "target is required",
|
||||
fileExt: "xlsx",
|
||||
args: func(path string) []string { return []string{"--file", path} },
|
||||
wantErr: "--folder-token 与 --workspace 至少需要提供一个",
|
||||
},
|
||||
{
|
||||
name: "only excel formats are accepted",
|
||||
fileExt: "csv",
|
||||
args: func(path string) []string { return []string{"--file", path, "--workspace", "ws1"} },
|
||||
wantErr: `unsupported file format "csv", supported: xlsx, xls`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
caller := &sheetImportCaller{}
|
||||
_, err := executeSheetImportCommand(t, caller, fastSheetImportConfig(), tt.args(writeImportFixture(t, tt.fileExt))...)
|
||||
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
||||
t.Fatalf("error = %v, want containing %q", err, tt.wantErr)
|
||||
}
|
||||
if len(caller.calls) != 0 {
|
||||
t.Fatalf("remote calls = %d, want 0", len(caller.calls))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSheetImportRunsSharedDocImportFlow(t *testing.T) {
|
||||
caller := &sheetImportCaller{responses: map[string][]string{
|
||||
"create_import_session": {`{"sessionId":"session-1","uploadUrl":"https://upload.example.test/object"}`},
|
||||
"confirm_import": {`{"taskId":"task-1"}`},
|
||||
"query_import_task": {`{"status":"completed","documentUrl":"https://alidocs.dingtalk.com/i/nodes/node-1?from=test","documentName":"Sales","documentType":"1"}`},
|
||||
}}
|
||||
SetHTTPPutFile(func(_ context.Context, uploadURL string, _ map[string]string, _ string, size int64) error {
|
||||
if uploadURL != "https://upload.example.test/object" || size <= 0 {
|
||||
return fmt.Errorf("unexpected upload url=%s size=%d", uploadURL, size)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
output, err := executeSheetImportCommand(t, caller, fastSheetImportConfig(),
|
||||
"--file", writeImportFixture(t, "xlsx"),
|
||||
"--workspace", "workspace-1",
|
||||
"--name", "Sales",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("sheet import returned error: %v", err)
|
||||
}
|
||||
|
||||
wantTools := []string{"create_import_session", "confirm_import", "query_import_task"}
|
||||
if len(caller.calls) != len(wantTools) {
|
||||
t.Fatalf("calls = %#v", caller.calls)
|
||||
}
|
||||
for i, call := range caller.calls {
|
||||
if call.server != "doc" || call.tool != wantTools[i] {
|
||||
t.Fatalf("call[%d] = %#v, want doc/%s", i, call, wantTools[i])
|
||||
}
|
||||
}
|
||||
wantSession := map[string]any{
|
||||
"fileName": "Sales",
|
||||
"suffix": "xlsx",
|
||||
"fileSize": int64(len("test workbook")),
|
||||
"workspaceId": "workspace-1",
|
||||
}
|
||||
if !reflect.DeepEqual(caller.calls[0].args, wantSession) {
|
||||
t.Fatalf("session args = %#v, want %#v", caller.calls[0].args, wantSession)
|
||||
}
|
||||
|
||||
if !strings.Contains(output, `"nodeId": "node-1"`) || !strings.Contains(output, `"success": true`) {
|
||||
t.Fatalf("output missing success contract:\n%s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSheetImportCreateLeafMatchesLegacyDryRun(t *testing.T) {
|
||||
filePath := writeImportFixture(t, "xlsx")
|
||||
legacy, err := executeSheetImportCommand(t, &sheetImportCaller{dryRun: true}, fastSheetImportConfig(),
|
||||
"--file", filePath, "--workspace", "workspace-1", "--name", "Sales")
|
||||
if err != nil {
|
||||
t.Fatalf("legacy sheet import dry-run returned error: %v", err)
|
||||
}
|
||||
leaf, err := executeSheetImportCommand(t, &sheetImportCaller{dryRun: true}, fastSheetImportConfig(),
|
||||
"create", "--file", filePath, "--workspace", "workspace-1", "--name", "Sales")
|
||||
if err != nil {
|
||||
t.Fatalf("sheet import create dry-run returned error: %v", err)
|
||||
}
|
||||
if leaf != legacy {
|
||||
t.Fatalf("create leaf output differs from legacy entry:\nlegacy:\n%s\nleaf:\n%s", legacy, leaf)
|
||||
}
|
||||
|
||||
root := newSheetImportCmdWithConfig(fastSheetImportConfig())
|
||||
create, _, err := root.Find([]string{"create"})
|
||||
if err != nil || create == nil {
|
||||
t.Fatalf("find create leaf: command=%v err=%v", create, err)
|
||||
}
|
||||
if !create.Runnable() || create.HasSubCommands() {
|
||||
t.Fatalf("create command must be a runnable leaf: runnable=%v hasSubcommands=%v", create.Runnable(), create.HasSubCommands())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSheetImportTimeoutIsStructuredSuccessExit(t *testing.T) {
|
||||
cfg := fastSheetImportConfig()
|
||||
cfg.poll.maxPolls = 2
|
||||
caller := &sheetImportCaller{responses: map[string][]string{
|
||||
"create_import_session": {`{"sessionId":"session-1","uploadUrl":"https://upload.example.test/object"}`},
|
||||
"confirm_import": {`{"taskId":"task-1"}`},
|
||||
"query_import_task": {
|
||||
`{"status":"processing"}`,
|
||||
`{"status":"processing"}`,
|
||||
},
|
||||
}}
|
||||
SetHTTPPutFile(func(context.Context, string, map[string]string, string, int64) error { return nil })
|
||||
|
||||
output, err := executeSheetImportCommand(t, caller, cfg,
|
||||
"--file", writeImportFixture(t, "xls"), "--folder-token", "folder-1")
|
||||
if err != nil {
|
||||
t.Fatalf("timeout should exit successfully, got %v", err)
|
||||
}
|
||||
for _, fragment := range []string{
|
||||
`"success": false`,
|
||||
`"timed_out": true`,
|
||||
`"status": "processing"`,
|
||||
`"next_command": "dws sheet import get --task-id task-1"`,
|
||||
} {
|
||||
if !strings.Contains(output, fragment) {
|
||||
t.Fatalf("output missing %s:\n%s", fragment, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSheetImportGetAddsNodeIDAndUsesDocServer(t *testing.T) {
|
||||
caller := &sheetImportCaller{responses: map[string][]string{
|
||||
"query_import_task": {`{"status":"completed","documentUrl":"https://alidocs.dingtalk.com/i/nodes/node-2/","documentType":"1"}`},
|
||||
}}
|
||||
output, err := executeSheetImportCommand(t, caller, fastSheetImportConfig(), "get", "--task-id", "task-2")
|
||||
if err != nil {
|
||||
t.Fatalf("sheet import get returned error: %v", err)
|
||||
}
|
||||
if len(caller.calls) != 1 || caller.calls[0].server != "doc" || caller.calls[0].tool != "query_import_task" {
|
||||
t.Fatalf("calls = %#v", caller.calls)
|
||||
}
|
||||
if !reflect.DeepEqual(caller.calls[0].args, map[string]any{"taskId": "task-2"}) {
|
||||
t.Fatalf("args = %#v", caller.calls[0].args)
|
||||
}
|
||||
|
||||
var result map[string]any
|
||||
jsonStart := strings.Index(output, "{")
|
||||
if jsonStart < 0 || json.Unmarshal([]byte(output[jsonStart:]), &result) != nil {
|
||||
t.Fatalf("invalid JSON output:\n%s", output)
|
||||
}
|
||||
if result["nodeId"] != "node-2" {
|
||||
t.Fatalf("nodeId = %#v, want node-2", result["nodeId"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocImportConfigPreservesExistingContract(t *testing.T) {
|
||||
cfg := docImportFlowConfig()
|
||||
for _, ext := range []string{"docx", "doc", "xlsx", "xls", "md", "txt", "xmind", "mark"} {
|
||||
if !cfg.supportedFormats[ext] {
|
||||
t.Errorf("doc import no longer supports %s", ext)
|
||||
}
|
||||
}
|
||||
if cfg.requireTarget || cfg.includeNodeID || cfg.timeoutAsResult {
|
||||
t.Fatalf("doc import compatibility changed: %#v", cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocImportDryRunStillAcceptsMarkdownWithoutTarget(t *testing.T) {
|
||||
previousDeps := deps
|
||||
previousArgs := os.Args
|
||||
t.Cleanup(func() {
|
||||
deps = previousDeps
|
||||
os.Args = previousArgs
|
||||
})
|
||||
|
||||
caller := &sheetImportCaller{dryRun: true}
|
||||
InitDeps(caller)
|
||||
var output bytes.Buffer
|
||||
deps.Out.w = &output
|
||||
filePath := writeImportFixture(t, "md")
|
||||
os.Args = []string{"dws", "doc", "import", "--file", filePath}
|
||||
root := newDocCommand()
|
||||
root.SilenceErrors = true
|
||||
root.SilenceUsage = true
|
||||
root.SetArgs([]string{"import", "--file", filePath})
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("doc import dry-run changed behavior: %v", err)
|
||||
}
|
||||
if len(caller.calls) != 0 {
|
||||
t.Fatalf("dry-run made %d remote calls", len(caller.calls))
|
||||
}
|
||||
if !strings.Contains(output.String(), "导入本地文件为在线文档") || !strings.Contains(output.String(), "md") {
|
||||
t.Fatalf("unexpected dry-run output:\n%s", output.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractNodeIDFromDocURL(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
"https://alidocs.dingtalk.com/i/nodes/node-1": "node-1",
|
||||
"https://alidocs.dingtalk.com/i/nodes/node-2/?from=test": "node-2",
|
||||
"https://alidocs.dingtalk.com/i/nodes/node-3#sheet=Sheet1": "node-3",
|
||||
"": "",
|
||||
}
|
||||
for rawURL, want := range tests {
|
||||
if got := extractNodeIDFromDocURL(rawURL); got != want {
|
||||
t.Errorf("extractNodeIDFromDocURL(%q) = %q, want %q", rawURL, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,12 @@ func TestWukongSyncDocCommands(t *testing.T) {
|
||||
|
||||
func TestWukongSyncSheetCommands(t *testing.T) {
|
||||
root := newSheetCommand()
|
||||
importCmd := requireWukongSyncCommand(t, root, "import")
|
||||
requireWukongSyncFlags(t, importCmd, "file", "folder-token", "workspace", "name", "folder")
|
||||
|
||||
importGetCmd := requireWukongSyncCommand(t, root, "import", "get")
|
||||
requireWukongSyncFlags(t, importGetCmd, "task-id")
|
||||
|
||||
tableGetCmd := requireWukongSyncCommand(t, root, "table-get")
|
||||
requireWukongSyncFlags(t, tableGetCmd, "node", "sheet-id", "range", "no-header")
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ These repo-local entrypoints are the supported shell entrypoints for building, t
|
||||
- `make test`: run `go test ./...`
|
||||
- `make lint`: run formatting checks and required `golangci-lint`
|
||||
- `make fmt`: format Go source files under `cmd/`, `internal/`, and `test/`
|
||||
- `make policy`: reuse the current `dws` binary and run the complete policy suite (`make build` first)
|
||||
- `make package`: build all release artifacts locally via `goreleaser --snapshot`
|
||||
- `make release`: build and publish a release via `goreleaser`
|
||||
|
||||
@@ -15,4 +16,5 @@ Script groups:
|
||||
- Product convenience installers: `./scripts/install-devapp.sh`, `./scripts/install-devapp.ps1`, `./scripts/install-event.sh`
|
||||
- Dev helpers: `./scripts/dev/build.sh`, `./scripts/dev/lint.sh`, `./scripts/dev/ci-local.sh`, `./scripts/dev/run-mock-e2e.sh`, `./scripts/dev/coverage.sh`
|
||||
- Policy checks: `./scripts/policy/check-schema-command-registry.sh`, `./scripts/policy/check-generated-drift.sh`, `./scripts/policy/check-command-surface.sh`, `./scripts/policy/check-schema-catalog.sh`, `./scripts/policy/check-schema-binary.sh`, `./scripts/policy/check-open-source-assets.sh`
|
||||
- Policy runtime files: set `DWS_POLICY_TMPDIR` to override the default `.worktrees/policy-tmp` workspace
|
||||
- Release helpers: `./scripts/release/post-goreleaser.sh`, `./scripts/release/verify-package-managers.sh`, `./scripts/release/publish-homebrew-formula.sh`
|
||||
@@ -34,12 +34,15 @@ done
|
||||
[ -n "$BASE_REF" ] || { usage; exit 2; }
|
||||
|
||||
cd "$ROOT"
|
||||
. "$ROOT/scripts/policy/policy-runtime.sh"
|
||||
policy_prepare_runtime "$ROOT"
|
||||
|
||||
git rev-parse --verify --quiet "${BASE_REF}^{commit}" >/dev/null || {
|
||||
printf 'error: interface authority is not available locally: %s\n' "$BASE_REF" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/dws-interface-authority.XXXXXX")"
|
||||
TMP_ROOT="$(policy_runtime_mktemp_dir dws-interface-authority)"
|
||||
BASE_WORKTREE="$TMP_ROOT/base-worktree"
|
||||
BASELINE="$TMP_ROOT/merge-base.txt"
|
||||
CANDIDATE_BIN="$TMP_ROOT/interface-baseline"
|
||||
|
||||
@@ -35,6 +35,9 @@ done
|
||||
[ -n "$BASE_REF" ] || { usage; exit 2; }
|
||||
|
||||
cd "$ROOT"
|
||||
. "$ROOT/scripts/policy/policy-runtime.sh"
|
||||
policy_prepare_runtime "$ROOT"
|
||||
|
||||
git rev-parse --verify --quiet "${BASE_REF}^{commit}" >/dev/null || {
|
||||
printf 'error: Schema authority is not available locally: %s\n' "$BASE_REF" >&2
|
||||
exit 2
|
||||
@@ -44,7 +47,7 @@ if [ ! -x "$BIN" ]; then
|
||||
exit 2
|
||||
fi
|
||||
|
||||
TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/dws-schema-authority.XXXXXX")"
|
||||
TMP_ROOT="$(policy_runtime_mktemp_dir dws-schema-authority)"
|
||||
BASE_WORKTREE="$TMP_ROOT/base-worktree"
|
||||
BASE_BIN="$TMP_ROOT/base-dws"
|
||||
BASE_RAW="$TMP_ROOT/base-schema.json"
|
||||
|
||||
@@ -5,6 +5,7 @@ set -eu
|
||||
# Stub placeholder — extend with actual surface comparison logic as needed.
|
||||
|
||||
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
|
||||
BIN="${DWS_BIN:-$ROOT/dws}"
|
||||
|
||||
STRICT=0
|
||||
if [ "${1:-}" = "--strict" ]; then
|
||||
@@ -13,32 +14,27 @@ fi
|
||||
|
||||
cd "$ROOT"
|
||||
|
||||
# Verify that all expected top-level commands exist in the built binary
|
||||
if command -v go >/dev/null 2>&1; then
|
||||
BIN_PATH="/tmp/dws-surface-check"
|
||||
trap 'rm -f "$BIN_PATH"' EXIT HUP INT TERM
|
||||
go build -ldflags="-s -w" -o "$BIN_PATH" ./cmd 2>/dev/null || { echo "build failed"; exit 1; }
|
||||
if [ ! -x "$BIN" ]; then
|
||||
printf 'error: dws binary not found at %s (run make build first)\n' "$BIN" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# These utility commands are stable across the current open-source CLI shape.
|
||||
EXPECTED_COMMANDS="auth cache completion version"
|
||||
missing=0
|
||||
for cmd in $EXPECTED_COMMANDS; do
|
||||
# Use `help <command>` so hidden commands are also validated.
|
||||
if ! "$BIN_PATH" help "$cmd" >/dev/null 2>&1; then
|
||||
printf 'missing command: %s\n' "$cmd" >&2
|
||||
missing=$((missing + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
# Full Schema canonical/primary/alias delivery is exercised once by
|
||||
# check-schema-binary.sh. Keep this check focused on the basic command tree.
|
||||
rm -f "$BIN_PATH"
|
||||
trap - EXIT HUP INT TERM
|
||||
|
||||
if [ "$STRICT" -eq 1 ] && [ "$missing" -gt 0 ]; then
|
||||
printf 'command surface check: %d missing commands\n' "$missing" >&2
|
||||
exit 1
|
||||
# These utility commands are stable across the current open-source CLI shape.
|
||||
EXPECTED_COMMANDS="auth cache completion version"
|
||||
missing=0
|
||||
for cmd in $EXPECTED_COMMANDS; do
|
||||
# Use `help <command>` so hidden commands are also validated.
|
||||
if ! "$BIN" help "$cmd" >/dev/null 2>&1; then
|
||||
printf 'missing command: %s\n' "$cmd" >&2
|
||||
missing=$((missing + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
# Full Schema canonical/primary/alias delivery is exercised once by
|
||||
# check-schema-binary.sh. Keep this check focused on the basic command tree.
|
||||
if [ "$STRICT" -eq 1 ] && [ "$missing" -gt 0 ]; then
|
||||
printf 'command surface check: %d missing commands\n' "$missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'command surface check: ok\n'
|
||||
@@ -62,13 +62,21 @@ done
|
||||
|
||||
[ -n "$BASE_REF" ] || { usage; exit 2; }
|
||||
cd "$ROOT"
|
||||
. "$ROOT/scripts/policy/policy-runtime.sh"
|
||||
policy_prepare_runtime "$ROOT"
|
||||
|
||||
git rev-parse --verify --quiet "${BASE_REF}^{commit}" >/dev/null || {
|
||||
printf 'error: coverage base ref is not available locally: %s\n' "$BASE_REF" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
TMP_ROOT="$(policy_runtime_mktemp_dir dws-coverage-gate)"
|
||||
CHECKER="$TMP_ROOT/coverage-gate"
|
||||
trap 'rm -rf "$TMP_ROOT"' EXIT HUP INT TERM
|
||||
go build -o "$CHECKER" ./scripts/policy/coverage-gate
|
||||
|
||||
module="$(go list -m -f '{{.Path}}')"
|
||||
set -- go run ./scripts/policy/coverage-gate \
|
||||
set -- "$CHECKER" \
|
||||
--diff-profile "$DIFF_PROFILE" \
|
||||
--base-ref "$BASE_REF" \
|
||||
--module "$module" \
|
||||
|
||||
@@ -7,9 +7,17 @@ set -eu
|
||||
|
||||
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
|
||||
cd "$ROOT"
|
||||
. "$ROOT/scripts/policy/policy-runtime.sh"
|
||||
policy_prepare_runtime "$ROOT"
|
||||
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
|
||||
exec_tmp="$(policy_runtime_mktemp_dir dws-generated-drift)"
|
||||
metadata_generator="$exec_tmp/schema-agent-metadata"
|
||||
catalog_generator="$exec_tmp/schema-catalog"
|
||||
trap 'rm -rf "$tmp" "$exec_tmp"' EXIT HUP INT TERM
|
||||
|
||||
go build -o "$metadata_generator" ./internal/generator/cmd_schema_agent_metadata
|
||||
go build -a -o "$catalog_generator" ./internal/generator/cmd_schema_catalog
|
||||
|
||||
# CommandRegistry is a reviewed input, never a generated artifact. Keep an
|
||||
# independent byte-for-byte guard around the ordinary downstream generators.
|
||||
@@ -26,7 +34,7 @@ audit_tmp="$tmp/audit.json"
|
||||
catalog_tmp="$tmp/catalog.json"
|
||||
catalog_tmp_second="$tmp/catalog-second.json"
|
||||
|
||||
go run ./internal/generator/cmd_schema_agent_metadata \
|
||||
"$metadata_generator" \
|
||||
-root . \
|
||||
-registry internal/cli/schema_command_registry.json \
|
||||
-output-dir "$metadata_tmp" \
|
||||
@@ -46,11 +54,11 @@ if ! cmp -s internal/cli/schema_agent_metadata_audit.json "$audit_tmp"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
go run -a ./internal/generator/cmd_schema_catalog \
|
||||
"$catalog_generator" \
|
||||
-root . \
|
||||
-output "$catalog_tmp"
|
||||
|
||||
go run -a ./internal/generator/cmd_schema_catalog \
|
||||
"$catalog_generator" \
|
||||
-root . \
|
||||
-output "$catalog_tmp_second"
|
||||
|
||||
|
||||
@@ -7,12 +7,17 @@ set -eu
|
||||
|
||||
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
|
||||
BASELINE="${INTERFACE_BASELINE:-$ROOT/test/fixtures/cli-interface-baseline.txt}"
|
||||
SNAPSHOT_HOME="$(mktemp -d)"
|
||||
SNAPSHOT_BIN="$(mktemp)"
|
||||
CURRENT="$(mktemp)"
|
||||
trap 'rm -rf "$CURRENT" "$SNAPSHOT_HOME" "$SNAPSHOT_BIN"' EXIT
|
||||
|
||||
cd "$ROOT"
|
||||
. "$ROOT/scripts/policy/policy-runtime.sh"
|
||||
policy_prepare_runtime "$ROOT"
|
||||
|
||||
TMP_ROOT="$(policy_runtime_mktemp_dir dws-interface-baseline)"
|
||||
SNAPSHOT_HOME="$TMP_ROOT/home"
|
||||
SNAPSHOT_BIN="$TMP_ROOT/interface-baseline"
|
||||
CURRENT="$TMP_ROOT/current.txt"
|
||||
mkdir -p "$SNAPSHOT_HOME"
|
||||
trap 'rm -rf "$TMP_ROOT"' EXIT HUP INT TERM
|
||||
|
||||
# Compile with the caller's normal Go cache, then isolate only the execution
|
||||
# HOME so user-installed DWS plugins cannot alter the public command tree.
|
||||
go build -o "$SNAPSHOT_BIN" ./scripts/policy/interface-baseline
|
||||
|
||||
@@ -2,16 +2,26 @@
|
||||
set -eu
|
||||
|
||||
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
|
||||
BIN="${DWS_BIN:-$ROOT/dws}"
|
||||
cd "$ROOT"
|
||||
. "$ROOT/scripts/policy/policy-runtime.sh"
|
||||
policy_prepare_runtime "$ROOT"
|
||||
|
||||
if [ ! -x "$BIN" ]; then
|
||||
printf 'error: dws binary not found at %s (run make build first)\n' "$BIN" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
|
||||
exec_tmp="$(policy_runtime_mktemp_dir dws-schema-binary)"
|
||||
smoke_generator="$exec_tmp/schema-registry-smoke"
|
||||
trap 'rm -rf "$tmp" "$exec_tmp"' EXIT HUP INT TERM
|
||||
|
||||
go build -o "$tmp/dws" ./cmd
|
||||
go run ./internal/generator/cmd_schema_registry_smoke >"$tmp/smoke-vector.json"
|
||||
go build -o "$smoke_generator" ./internal/generator/cmd_schema_registry_smoke
|
||||
"$smoke_generator" >"$tmp/smoke-vector.json"
|
||||
|
||||
"$tmp/dws" schema list --format json >"$tmp/list.json"
|
||||
"$tmp/dws" schema --all --format json >"$tmp/all.json"
|
||||
"$BIN" schema list --format json >"$tmp/list.json"
|
||||
"$BIN" schema --all --format json >"$tmp/all.json"
|
||||
|
||||
if ! jq -e '
|
||||
.version == 1 and
|
||||
@@ -111,8 +121,8 @@ while IFS= read -r command; do
|
||||
canonical="$(printf '%s\n' "$command" | jq -r '.canonical_path')"
|
||||
primary="$(printf '%s\n' "$command" | jq -r '.primary_cli_path')"
|
||||
|
||||
"$tmp/dws" schema "$canonical" --format json >"$tmp/canonical.json"
|
||||
"$tmp/dws" schema --cli-path "$primary" --format json >"$tmp/primary.json"
|
||||
"$BIN" schema "$canonical" --format json >"$tmp/canonical.json"
|
||||
"$BIN" schema --cli-path "$primary" --format json >"$tmp/primary.json"
|
||||
jq -S --arg canonical "$canonical" '
|
||||
[.products[].tools[] | select(.canonical_path == $canonical)] |
|
||||
if length == 1 then .[0] else null end
|
||||
@@ -132,8 +142,8 @@ alias_command="$(jq -c 'first(.commands[] | select((.alias_cli_paths | length) >
|
||||
if [ -n "$alias_command" ]; then
|
||||
canonical="$(printf '%s\n' "$alias_command" | jq -r '.canonical_path')"
|
||||
alias_path="$(printf '%s\n' "$alias_command" | jq -r '.alias_cli_paths[0]')"
|
||||
"$tmp/dws" schema "$canonical" --format json >"$tmp/canonical.json"
|
||||
"$tmp/dws" schema --cli-path "$alias_path" --format json >"$tmp/alias.json"
|
||||
"$BIN" schema "$canonical" --format json >"$tmp/canonical.json"
|
||||
"$BIN" schema --cli-path "$alias_path" --format json >"$tmp/alias.json"
|
||||
jq -S 'del(.cli_path, .is_alias)' "$tmp/canonical.json" >"$tmp/canonical-content.json"
|
||||
jq -S 'del(.cli_path, .is_alias)' "$tmp/alias.json" >"$tmp/alias-content.json"
|
||||
if ! cmp -s "$tmp/canonical-content.json" "$tmp/alias-content.json"; then
|
||||
|
||||
@@ -4,6 +4,8 @@ set -eu
|
||||
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
|
||||
cd "$ROOT"
|
||||
. "$ROOT/scripts/policy/search.sh"
|
||||
. "$ROOT/scripts/policy/policy-runtime.sh"
|
||||
policy_prepare_runtime "$ROOT"
|
||||
|
||||
if [ -e internal/cli/schema_native_contracts.go ] ||
|
||||
[ -e internal/cli/schema_native_contracts_generated.go ] ||
|
||||
|
||||
@@ -8,6 +8,8 @@ set -eu
|
||||
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
|
||||
cd "$ROOT"
|
||||
. "$ROOT/scripts/policy/search.sh"
|
||||
. "$ROOT/scripts/policy/policy-runtime.sh"
|
||||
policy_prepare_runtime "$ROOT"
|
||||
|
||||
if [ -e internal/cli/schema_native_contracts.go ] ||
|
||||
[ -e internal/cli/schema_native_contracts_generated.go ] ||
|
||||
|
||||
@@ -2,10 +2,15 @@
|
||||
set -eu
|
||||
|
||||
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
|
||||
CHECKER="$(mktemp)"
|
||||
CHECK_HOME="$(mktemp -d)"
|
||||
trap 'rm -rf "$CHECKER" "$CHECK_HOME"' EXIT
|
||||
|
||||
cd "$ROOT"
|
||||
. "$ROOT/scripts/policy/policy-runtime.sh"
|
||||
policy_prepare_runtime "$ROOT"
|
||||
|
||||
TMP_ROOT="$(policy_runtime_mktemp_dir dws-skill-command-check)"
|
||||
CHECKER="$TMP_ROOT/skill-command-check"
|
||||
CHECK_HOME="$TMP_ROOT/home"
|
||||
mkdir -p "$CHECK_HOME"
|
||||
trap 'rm -rf "$TMP_ROOT"' EXIT HUP INT TERM
|
||||
|
||||
go build -o "$CHECKER" ./scripts/policy/skill-command-check
|
||||
HOME="$CHECK_HOME" DWS_LANG=zh "$CHECKER"
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Shared runtime workspace for policy helpers that compile and execute Go
|
||||
# programs. Callers may override DWS_POLICY_TMPDIR and GOTMPDIR.
|
||||
policy_prepare_runtime() {
|
||||
policy_root="$1"
|
||||
DWS_POLICY_TMPDIR="${DWS_POLICY_TMPDIR:-$policy_root/.worktrees/policy-tmp}"
|
||||
mkdir -p "$DWS_POLICY_TMPDIR/go"
|
||||
DWS_POLICY_TMPDIR="$(CDPATH= cd -- "$DWS_POLICY_TMPDIR" && pwd)"
|
||||
export DWS_POLICY_TMPDIR
|
||||
|
||||
if [ -z "${GOTMPDIR:-}" ]; then
|
||||
GOTMPDIR="$DWS_POLICY_TMPDIR/go"
|
||||
fi
|
||||
mkdir -p "$GOTMPDIR"
|
||||
GOTMPDIR="$(CDPATH= cd -- "$GOTMPDIR" && pwd)"
|
||||
export GOTMPDIR
|
||||
}
|
||||
|
||||
policy_runtime_mktemp_dir() {
|
||||
prefix="$1"
|
||||
mktemp -d "$DWS_POLICY_TMPDIR/${prefix}.XXXXXX"
|
||||
}
|
||||
@@ -39,19 +39,24 @@ done
|
||||
|
||||
[ -n "$BASE_REF" ] && [ -n "$PROFILE" ] || { usage; exit 2; }
|
||||
cd "$ROOT"
|
||||
. "$ROOT/scripts/policy/policy-runtime.sh"
|
||||
policy_prepare_runtime "$ROOT"
|
||||
|
||||
git rev-parse --verify --quiet "${BASE_REF}^{commit}" >/dev/null || {
|
||||
printf 'error: coverage base ref is not available locally: %s\n' "$BASE_REF" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/dws-platform-coverage.XXXXXX")"
|
||||
TMP_ROOT="$(policy_runtime_mktemp_dir dws-platform-coverage)"
|
||||
PACKAGES="$TMP_ROOT/packages"
|
||||
SORTED_PACKAGES="$TMP_ROOT/packages.sorted"
|
||||
TEST_PACKAGES="$TMP_ROOT/test-packages"
|
||||
cleanup() {
|
||||
rm -rf "$TMP_ROOT"
|
||||
}
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
: >"$PACKAGES"
|
||||
: >"$TEST_PACKAGES"
|
||||
|
||||
git diff --name-only --diff-filter=ACMR "$BASE_REF" -- '*.go' |
|
||||
while IFS= read -r file; do
|
||||
@@ -70,9 +75,11 @@ git diff --name-only --diff-filter=ACMR "$BASE_REF" -- '*.go' |
|
||||
sort -u "$PACKAGES" >"$SORTED_PACKAGES"
|
||||
set --
|
||||
COVERPKG=""
|
||||
FIRST_PACKAGE=""
|
||||
while IFS= read -r package; do
|
||||
if [ -n "$package" ]; then
|
||||
set -- "$@" "$package"
|
||||
[ -n "$FIRST_PACKAGE" ] || FIRST_PACKAGE="$package"
|
||||
if [ -z "$COVERPKG" ]; then
|
||||
COVERPKG="$package"
|
||||
else
|
||||
@@ -87,15 +94,33 @@ else
|
||||
printf 'native changed coverage packages:'
|
||||
printf ' %s' "$@"
|
||||
printf '\n'
|
||||
for package in "$@"; do
|
||||
tests="$(go test -list '^(TestAllShortcuts|TestCrossPlatformCoverage)' "$package")"
|
||||
if printf '%s\n' "$tests" | grep -Eq '^(TestAllShortcuts|TestCrossPlatformCoverage)'; then
|
||||
printf '%s\n' "$package" >>"$TEST_PACKAGES"
|
||||
fi
|
||||
done
|
||||
set --
|
||||
while IFS= read -r package; do
|
||||
[ -n "$package" ] && set -- "$@" "$package"
|
||||
done <"$TEST_PACKAGES"
|
||||
if [ "$#" -eq 0 ]; then
|
||||
# Keep a valid zero-coverage profile so the fail-closed gate can report
|
||||
# the missing coverage when no explicit platform test exists.
|
||||
set -- "$FIRST_PACKAGE"
|
||||
fi
|
||||
printf 'native coverage test packages:'
|
||||
printf ' %s' "$@"
|
||||
printf '\n'
|
||||
# The shortcut integration harness lives in internal/shortcut/builtin but
|
||||
# executes code from every product package. Cross-package instrumentation is
|
||||
# required for those real executions to count toward the owning source file.
|
||||
#
|
||||
# Platform runners intentionally execute only the exhaustive shortcut
|
||||
# harness and explicitly cross-platform tests. All changed production
|
||||
# packages remain instrumented, so unexercised statements still count as
|
||||
# uncovered. This keeps the native Windows gate independent of unrelated
|
||||
# tests that require Unix permissions, shell scripts, or process semantics.
|
||||
# Platform runners intentionally execute only packages that actually declare
|
||||
# an exhaustive shortcut harness or explicit cross-platform tests. All
|
||||
# changed production packages remain instrumented via coverpkg, so unexercised
|
||||
# statements still count as uncovered. Avoiding zero-test driver binaries also
|
||||
# keeps profiles small enough for the fail-closed gate to process reliably.
|
||||
go test -count=1 -timeout="$TIMEOUT" -run '^(TestAllShortcuts|TestCrossPlatformCoverage)' \
|
||||
-coverpkg="$COVERPKG" -coverprofile="$PROFILE" -covermode=atomic "$@"
|
||||
fi
|
||||
|
||||
@@ -105,12 +105,14 @@
|
||||
|
||||
| 命令 | 用途 | 必填参数 | 路由提醒 |
|
||||
|------|------|----------|----------|
|
||||
| `workflow create` | 创建并发布工作流 | `--base-id` `--dsl` | `--dsl` 为完整 workflow-dsl/v1;非幂等,不自动重试 |
|
||||
| `workflow update` | 更新并发布工作流 | `--base-id` `--workflow-id` `--dsl` | 全量替换,先 get 留底;检查 `data.valid/issues` |
|
||||
| `workflow list` | 列出 Base 下所有工作流 | `--base-id` | 支持 `--limit [1,100]` / `--offset >=0`;list 出参字段叫 `flowId` |
|
||||
| `workflow get` | 获取单个工作流详情(含 flowSchema) | `--base-id` `--workflow-id` | `--workflow-id` 接受 list 里的 `flowId`(同值) |
|
||||
| `workflow enable` | 启用工作流 | `--base-id` `--workflow-id` | 返回 `{enabled: true}` 是动作确认;要确认真启用看 list 的 `status` |
|
||||
| `workflow disable` | 禁用工作流(高危) | `--base-id` `--workflow-id` `--yes` | 影响业务自动化,建议二次确认;status 变 STOP |
|
||||
|
||||
> **当前不支持通过 CLI 新建/修改/删除工作流**,请去 AI 表格 Web 端(数据表页面 → 自动化)配置。
|
||||
> 当前支持创建、更新、查询和启停;删除、运行历史及手动触发仍未开放。
|
||||
|
||||
### dashboard & chart → 详见 [aitable-dashboard-chart.md](./aitable/aitable-dashboard-chart.md)
|
||||
|
||||
|
||||
@@ -1,22 +1,100 @@
|
||||
# workflow — 自动化工作流管理
|
||||
|
||||
启停 / 查看 / 列出 Base 下的自动化工作流(在 AI 表格 Web 端配置的 "当 X 时自动 Y" 流程)。
|
||||
适用场景:用户问 "停掉这个流程"、"看下都有哪些自动化流程"、"流程 X 的配置是什么"。
|
||||
创建 / 更新 / 启停 / 查看 / 列出 Base 下的自动化工作流("当 X 时自动 Y" 流程)。
|
||||
适用场景:用户要求创建自动化、修改流程、停掉流程、查询已有流程或恢复运行。
|
||||
|
||||
## 命令一览
|
||||
|
||||
| 命令 | 用途 |
|
||||
|------|------|
|
||||
| `workflow create` | 创建并发布自动化工作流 |
|
||||
| `workflow update` | 更新并发布已有自动化工作流 |
|
||||
| `workflow list` | 列出 Base 下所有工作流(含状态/创建人/最后修改时间),支持分页 |
|
||||
| `workflow get` | 获取单个工作流详情(含 flowSchema 完整节点定义) |
|
||||
| `workflow enable` | 启用指定工作流(按配置的触发条件自动执行) |
|
||||
| `workflow disable` | 禁用指定工作流(高危,建议 `--yes` 二次确认) |
|
||||
|
||||
> 所有子命令的 `--base-id` 必填(可用隐藏别名 `--base`)。
|
||||
> 当前**不支持通过 CLI 新建工作流**,请在 AI 表格 Web 端配置好后用 `workflow list` 拿到 ID 再启停。
|
||||
|
||||
## DSL 入参格式与最小 Demo
|
||||
|
||||
`workflow create/update` 的 `--dsl` 接收完整的 `workflow-dsl/v1` JSON object,不是局部 patch。支持内联 JSON、`@文件路径` 或 `-` 从 stdin 读取。
|
||||
|
||||
复杂工作流应先用 `table get` / `field get` / `view list` 确认真实 `sheetId`、`fieldId`、`viewId`,并检查所有 `next`、`loopEntry`、branch `to` 和 ref。下面是一个不依赖数据表字段的最小定时消息工作流:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "workflow-dsl/v1",
|
||||
"name": "每日提醒",
|
||||
"description": "可选说明",
|
||||
"trigger": "start",
|
||||
"steps": {
|
||||
"start": {
|
||||
"type": "Scheduled",
|
||||
"next": "send",
|
||||
"data": {
|
||||
"mode": "daily",
|
||||
"time": "09:00",
|
||||
"timezone": "GMT+08:00"
|
||||
}
|
||||
},
|
||||
"send": {
|
||||
"type": "SendMessage",
|
||||
"data": {
|
||||
"title": "定时任务已触发",
|
||||
"to": {"users": [{"ref": "$.system_node.ownerUserId"}]}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
create 和 update 都必须同时满足 `status=success`、`data.valid=true`、`data.issues=[]` 才表示发布成功。
|
||||
|
||||
## 命令详情
|
||||
|
||||
### workflow create — 创建并发布工作流
|
||||
|
||||
```bash
|
||||
dws aitable workflow create \
|
||||
--base-id BASE_ID \
|
||||
--dsl @workflow.json \
|
||||
--locale zh-CN \
|
||||
--format json
|
||||
|
||||
cat workflow.json | dws aitable workflow create --base-id BASE_ID --dsl - --format json
|
||||
```
|
||||
|
||||
| flag | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--base-id` | 是 | 所属 Base ID |
|
||||
| `--dsl` | 是 | workflow-dsl/v1 JSON object;支持内联、`@filepath`、`-` stdin |
|
||||
| `--locale` | 否 | 请求语言,如 `zh-CN` / `zh_CN` |
|
||||
|
||||
`create` 非幂等,CLI 不自动重试。若网络中断导致结果不确定,先 `workflow list` 按名称确认是否已经创建,再决定是否重试。`status=success` 只说明服务正常返回;`data.valid=false` 仍是发布失败,必须读取 `issues`。
|
||||
|
||||
### workflow update — 更新并发布工作流
|
||||
|
||||
```bash
|
||||
# 先留底,再提交完整目标 DSL
|
||||
dws aitable workflow get --base-id BASE_ID --workflow-id WORKFLOW_ID --format json > /tmp/workflow-backup.json
|
||||
dws aitable workflow update \
|
||||
--base-id BASE_ID \
|
||||
--workflow-id WORKFLOW_ID \
|
||||
--dsl @workflow.json \
|
||||
--locale zh_CN \
|
||||
--format json
|
||||
```
|
||||
|
||||
| flag | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--base-id` | 是 | 所属 Base ID |
|
||||
| `--workflow-id` | 是 | 目标工作流 ID,对应 list 的 `flowId` |
|
||||
| `--dsl` | 是 | 完整目标 workflow-dsl/v1 JSON object;支持内联、`@filepath`、`-` stdin |
|
||||
| `--locale` | 否 | 请求语言,如 `zh-CN` / `zh_CN` |
|
||||
|
||||
update 返回结构与 create 相同,并会对瞬态错误重试。成功时 `data.flowId` 应与传入的工作流 ID 一致;随后用 `workflow get/list` 验证发布结果和运行状态。
|
||||
|
||||
### workflow list — 列出工作流
|
||||
|
||||
```bash
|
||||
@@ -119,12 +197,12 @@ dws aitable workflow disable --base-id BASE_ID --workflow-id WORKFLOW_ID --yes -
|
||||
|
||||
| 能力 | 状态 |
|
||||
|------|------|
|
||||
| 新建工作流 | ✅ 创建并发布 |
|
||||
| 修改工作流配置 | ✅ 更新并发布 |
|
||||
| 列出工作流 | ✅ |
|
||||
| 看工作流详情(含 flowSchema) | ✅ |
|
||||
| 启用/禁用 | ✅ |
|
||||
| 新建工作流 | ❌ 当前不支持,请去 AI 表格 Web 端 → 数据表 → 自动化 创建 |
|
||||
| 修改工作流配置 | ❌ 同上,需 Web UI 编辑 |
|
||||
| 删除工作流 | ❌ 同上 |
|
||||
| 删除工作流 | ❌ 暂未开放 |
|
||||
| 查看运行历史/执行日志 | ❌ 暂未开放 |
|
||||
| 手动触发/单次运行 | ❌ 暂未开放 |
|
||||
|
||||
@@ -132,6 +210,9 @@ dws aitable workflow disable --base-id BASE_ID --workflow-id WORKFLOW_ID --yes -
|
||||
|
||||
| 场景 | code | type | 备注 |
|
||||
|------|------|------|------|
|
||||
| create/update 返回 `valid=false` | — | success envelope | 读取 `data.issues` 修正 DSL,不能当作发布成功 |
|
||||
| create 下游失败 | `CREATE_WORKFLOW_ERROR` | `SYSTEM_ERROR` | create 不自动重试;先 list 排查是否已创建 |
|
||||
| update 下游失败 | `UPDATE_WORKFLOW_ERROR` | `SYSTEM_ERROR` | update 会重试瞬态错误,最终失败时保留 DSL 和 workflowId 排查 |
|
||||
| `workflow-id` 不存在调 get | `GET_WORKFLOW_ERROR` | `SYSTEM_ERROR` | message 可能为 null,先 `workflow list` 核对 ID |
|
||||
| `workflow-id` 不存在调 enable | `ENABLE_WORKFLOW_ERROR` | `SYSTEM_ERROR` | message 含 "场域中不存在该 namespace" |
|
||||
| `workflow-id` 不存在调 disable | `DISABLE_WORKFLOW_ERROR` | `SYSTEM_ERROR` | 同上 |
|
||||
@@ -142,6 +223,18 @@ dws aitable workflow disable --base-id BASE_ID --workflow-id WORKFLOW_ID --yes -
|
||||
|
||||
## 典型工作流
|
||||
|
||||
### 创建并确认一个工作流
|
||||
|
||||
```bash
|
||||
dws aitable workflow create --base-id BASE_ID --dsl @/tmp/workflow.json --locale zh-CN --format json \
|
||||
| tee /tmp/workflow-result.json
|
||||
|
||||
jq '{valid: .data.valid, flowId: .data.flowId, issues: .data.issues}' /tmp/workflow-result.json
|
||||
FLOW_ID=$(jq -r '.data.flowId' /tmp/workflow-result.json)
|
||||
dws aitable workflow list --base-id BASE_ID --format json \
|
||||
| jq --arg id "$FLOW_ID" '.data.list[] | select(.flowId == $id) | {flowId, name, status}'
|
||||
```
|
||||
|
||||
### 看看 Base 里有哪些自动化在跑
|
||||
|
||||
```bash
|
||||
@@ -175,6 +268,9 @@ done
|
||||
## 注意事项
|
||||
|
||||
- `--workflow-id` 接受的就是 `list` 返回里的 `flowId`(同值,CLI 屏蔽了服务端字段名差异)。
|
||||
- create / update 的 `--dsl` 必须是 JSON object,不能传数组、二次字符串化 JSON 或 `flowSchema`。
|
||||
- `status=success` 且 `data.valid=false` 仍是 DSL 校验失败;`issues` 才是下一步修复依据。
|
||||
- create 不自动重试;update 仅对网络、5xx、`retryable:true` 等瞬态错误自动重试。
|
||||
- enable / disable 出参里的 `enabled` / `disabled` 是 **动作确认 flag**,不是当前状态字段。要确认真生效请走 `workflow list` 查 `status`。
|
||||
- `workflow get` 的 `flowSchema` 结构随触发器/动作类型变化,不要假设固定字段。
|
||||
- 新建/修改/删除工作流目前必须在 AI 表格 Web 端(数据表页面 → 自动化)完成。
|
||||
- 删除、运行历史和手动触发当前仍未开放。
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
## 适用范围(重要)
|
||||
|
||||
`sheet` 产品线仅支持钉钉在线电子表格(`contentType=ALIDOC`、`extension=axls`),不支持上传的 `xlsx` / `xls` / `xlsm` / `csv` 等本地表格文件。
|
||||
`sheet` 的单元格与工作表操作仅支持钉钉在线电子表格(`contentType=ALIDOC`、`extension=axls`)。`sheet import create` 是 Agent 可发现的文件入口:它接收本地 `xlsx` / `xls`,转换并新建在线电子表格;不能把已上传的 xlsx 节点直接当作 axls 操作。
|
||||
|
||||
| 文件类型 | 处理方式 |
|
||||
|---------|---------|
|
||||
| 在线电子表格(`axls`) | 走 `sheet` 全部命令(读/写/筛选/合并/导出等服务端原子操作) |
|
||||
| `xlsx` / `xls` / `xlsm` / `csv` 等本地表格文件 | 必须用 `dws doc download --node <ID> --output <路径>` 先下载到本地再解析处理,禁止调用任何 `sheet` 子命令(sheet 底层 MCP 工具仅认 `axls`,传入 xlsx 节点会直接报错) |
|
||||
| 本地路径上的 `xlsx` / `xls` | 用 `dws sheet import create --file <路径> --folder-token <ID>` 或 `--workspace <ID>` 转换为新的在线电子表格 |
|
||||
| 钉盘/文档中已有的 `xlsx` / `xls` / `xlsm` / `csv` 节点 | 不能直接调用工作表/单元格命令;先用 `dws doc download --node <ID> --output <路径>` 下载。若用户要转为在线表格,再对下载文件执行 `sheet import` |
|
||||
| 想把在线表格导出为 xlsx | 用 `dws sheet export` ——输入是 `axls`,输出是 xlsx(axls → xlsx 的格式转换) |
|
||||
|
||||
> 用户直接粘贴原始 `alidocs` URL 时必须先 probe:先执行 `dws doc info --node <URL> --format json`,按 [链接规范](../url-patterns.md#alidocs-url-类型探测流程) 校验 `contentType` 和 `extension`:
|
||||
@@ -125,6 +126,7 @@ dws sheet filter-view --help
|
||||
| `sheet list-float-images` | 列出工作表所有浮动图片 |
|
||||
| `sheet update-float-image` | 更新浮动图片属性 |
|
||||
| `sheet delete-float-image` | 删除浮动图片 |
|
||||
| `sheet import create` | 导入本地 xlsx/xls 为新的在线电子表格(`sheet import` 保留兼容) |
|
||||
| `sheet export` | 导出表格为 xlsx |
|
||||
| `sheet filter get` | 获取全局筛选信息 |
|
||||
| `sheet filter create` | 创建全局筛选 |
|
||||
@@ -176,6 +178,7 @@ dws sheet filter-view --help
|
||||
| 查找替换 | [sheet/sheet-search-replace.md](./sheet/sheet-search-replace.md) | find / replace |
|
||||
| 全局筛选 | [sheet/sheet-filter.md](./sheet/sheet-filter.md) | filter get / create / delete / update / clear-criteria / sort |
|
||||
| 筛选视图 | [sheet/sheet-filter-view.md](./sheet/sheet-filter-view.md) | filter-view 全系列 |
|
||||
| 导入 | [sheet/sheet-import.md](./sheet/sheet-import.md) | import / import get |
|
||||
| 导出 | [sheet/sheet-export.md](./sheet/sheet-export.md) | export |
|
||||
|
||||
> `template list/search/apply` 无独立子文档,直接执行 `dws sheet template <子命令> --help` 查看用法。
|
||||
@@ -369,6 +372,15 @@ dws sheet filter-view --help
|
||||
- 清除筛选视图列条件 → `filter-view delete-criteria`
|
||||
- 注意与 `filter-view delete`(删除整个筛选视图)区分:`delete-criteria` 仅清除指定列的条件,不删除筛选视图本身
|
||||
|
||||
### 导入
|
||||
|
||||
用户说"导入Excel/把xlsx转为在线表格/上传本地表格并在线编辑":
|
||||
- 使用 `sheet import`,本地文件仅支持 `xlsx` / `xls`
|
||||
- `--folder-token` 与 `--workspace` 至少提供一个;不指定 `--name` 时使用文件名
|
||||
- CLI 内部完成上传、转换和轮询,Agent 不要自行拆分流程或重复提交
|
||||
- 若返回 `timed_out:true`,执行返回的 `next_command` 续查,不要重新执行 import
|
||||
- 详见 [sheet/sheet-import.md](./sheet/sheet-import.md)
|
||||
|
||||
### 导出
|
||||
|
||||
用户说"导出/下载xlsx/存为Excel/存成表格文件/把表格变成xlsx/导出表格/下载表格/导出为 excel":
|
||||
@@ -2139,7 +2151,7 @@ dws sheet export --node <NODE_ID> --output ./
|
||||
- `copy` 当指定名称与已有工作表重复时,系统会自动重命名为合法值
|
||||
- `copy` 的 `--index` 可选,不传时副本将放置在源工作表之后的默认位置
|
||||
- ★ 关键区分: sheet(电子表格/单元格读写) vs aitable(AI多维表/结构化记录/字段定义) vs doc(文档编辑/阅读)
|
||||
- sheet 产品线仅支持 `axls`(在线电子表格,`contentType=ALIDOC`),不支持 `xlsx` / `xls` / `xlsm` / `csv` 等本地表格文件
|
||||
- 工作表与单元格命令仅支持 `axls`;本地 `xlsx` / `xls` 可通过 `sheet import` 转换为新的在线电子表格
|
||||
- 遇到未知 `alidocs` URL 时,必须先 probe(`dws doc info --node <URL> --format json`)确认 `contentType` 和 `extension`,才能决定是否走 sheet
|
||||
- 当节点 `extension=xlsx` / `xls` / `xlsm` / `csv`(`contentType≠ALIDOC`)时,必须用 `dws doc download --node <ID> --output <路径>` 先下载到本地再处理,禁止调用任何 sheet 子命令(sheet 底层 MCP 工具只识别 axls,调用 xlsx 节点必失败)
|
||||
- 要把在线表格导出为 xlsx 文件——走 `dws sheet export`(axls → xlsx 的格式转换);要读已有的 xlsx 文件——走 `dws doc download` 后在本地解析,两者方向相反
|
||||
- 要把在线表格导出为 xlsx 文件,走 `dws sheet export`;要把本地 xlsx/xls 转为在线表格,走 `dws sheet import`;要读取已上传但未转换的 xlsx 节点,先走 `dws doc download`
|
||||
@@ -0,0 +1,76 @@
|
||||
# 导入本地表格(import)
|
||||
|
||||
## 使用场景
|
||||
|
||||
用户说“导入 Excel”“把 xlsx 变成在线表格”“上传本地表格并在线编辑”时,使用 Agent 可发现入口 `dws sheet import create`。它只新建在线电子表格,不覆盖已有表格。旧入口 `dws sheet import` 继续兼容人工调用。
|
||||
|
||||
- 支持本地 `xlsx` / `xls`,文件上限 20MB
|
||||
- `--folder-token` 与 `--workspace` 至少提供一个
|
||||
- CLI 内部完成创建会话、OSS 上传、确认转换和轮询,Agent 不要自行拆分或重试
|
||||
- `drive upload` 只上传二进制文件,不会转换为可编辑的在线表格,不能替代本命令
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
# 导入到指定文件夹
|
||||
dws sheet import create \
|
||||
--file ./quote.xlsx \
|
||||
--folder-token <FOLDER_TOKEN> \
|
||||
--format json
|
||||
|
||||
# 导入到指定知识库,并自定义名称
|
||||
dws sheet import create \
|
||||
--file ./report.xls \
|
||||
--workspace <WORKSPACE_ID> \
|
||||
--name "月度报表" \
|
||||
--format json
|
||||
|
||||
# 导入超时或中断后续查
|
||||
dws sheet import get --task-id <TASK_ID> --format json
|
||||
```
|
||||
|
||||
公开参数:
|
||||
|
||||
| 命令 | 参数 | 说明 |
|
||||
|------|------|------|
|
||||
| `sheet import create` | `--file` | 本地 xlsx/xls 文件(必填) |
|
||||
| `sheet import create` | `--folder-token` | 目标文件夹 ID 或 URL,与 `--workspace` 至少传一个 |
|
||||
| `sheet import create` | `--workspace` | 目标知识库 ID 或 URL,与 `--folder-token` 至少传一个 |
|
||||
| `sheet import create` | `--name`, `-n` | 导入后的名称;默认取文件名并去掉扩展名 |
|
||||
| `sheet import get` | `--task-id` | 导入任务 ID |
|
||||
|
||||
## 返回与续查
|
||||
|
||||
成功时返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"taskId": "<TASK_ID>",
|
||||
"documentUrl": "<DOCUMENT_URL>",
|
||||
"documentName": "月度报表",
|
||||
"documentType": "1",
|
||||
"nodeId": "<NODE_ID>"
|
||||
}
|
||||
```
|
||||
|
||||
轮询达到上限时命令以退出码 0 返回业务态,避免 Agent 重复创建文档:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"timed_out": true,
|
||||
"taskId": "<TASK_ID>",
|
||||
"status": "processing",
|
||||
"next_command": "dws sheet import get --task-id <TASK_ID>"
|
||||
}
|
||||
```
|
||||
|
||||
检测到 `timed_out:true` 后,只执行 `next_command` 续查。不要重新执行 `sheet import create`。
|
||||
|
||||
## 边界
|
||||
|
||||
- 本命令接受的是本地文件路径;它把 xlsx/xls 转换成新的 axls 在线表格
|
||||
- 已经存在于钉盘或文档中的 xlsx 节点不能直接传给工作表/单元格命令;先用 `doc download` 下载,需要在线编辑时再执行 `sheet import create`
|
||||
- 向已有在线表格写数据应使用 `range update`、`append`、`csv-put` 或 `table-put`
|
||||
- md/doc/docx 等文字文档导入使用 `dws doc import`
|
||||
@@ -105,12 +105,14 @@
|
||||
|
||||
| 命令 | 用途 | 必填参数 | 路由提醒 |
|
||||
|------|------|----------|----------|
|
||||
| `workflow create` | 创建并发布工作流 | `--base-id` `--dsl` | `--dsl` 为完整 workflow-dsl/v1;非幂等,不自动重试 |
|
||||
| `workflow update` | 更新并发布工作流 | `--base-id` `--workflow-id` `--dsl` | 全量替换,先 get 留底;检查 `data.valid/issues` |
|
||||
| `workflow list` | 列出 Base 下所有工作流 | `--base-id` | 支持 `--limit [1,100]` / `--offset >=0`;list 出参字段叫 `flowId` |
|
||||
| `workflow get` | 获取单个工作流详情(含 flowSchema) | `--base-id` `--workflow-id` | `--workflow-id` 接受 list 里的 `flowId`(同值) |
|
||||
| `workflow enable` | 启用工作流 | `--base-id` `--workflow-id` | 返回 `{enabled: true}` 是动作确认;要确认真启用看 list 的 `status` |
|
||||
| `workflow disable` | 禁用工作流(高危) | `--base-id` `--workflow-id` `--yes` | 影响业务自动化,建议二次确认;status 变 STOP |
|
||||
|
||||
> **当前不支持通过 CLI 新建/修改/删除工作流**,请去 AI 表格 Web 端(数据表页面 → 自动化)配置。
|
||||
> 当前支持创建、更新、查询和启停;删除、运行历史及手动触发仍未开放。
|
||||
|
||||
### dashboard & chart → 详见 [aitable-dashboard-chart.md](./aitable/aitable-dashboard-chart.md)
|
||||
|
||||
|
||||
@@ -1,22 +1,100 @@
|
||||
# workflow — 自动化工作流管理
|
||||
|
||||
启停 / 查看 / 列出 Base 下的自动化工作流(在 AI 表格 Web 端配置的 "当 X 时自动 Y" 流程)。
|
||||
适用场景:用户问 "停掉这个流程"、"看下都有哪些自动化流程"、"流程 X 的配置是什么"。
|
||||
创建 / 更新 / 启停 / 查看 / 列出 Base 下的自动化工作流("当 X 时自动 Y" 流程)。
|
||||
适用场景:用户要求创建自动化、修改流程、停掉流程、查询已有流程或恢复运行。
|
||||
|
||||
## 命令一览
|
||||
|
||||
| 命令 | 用途 |
|
||||
|------|------|
|
||||
| `workflow create` | 创建并发布自动化工作流 |
|
||||
| `workflow update` | 更新并发布已有自动化工作流 |
|
||||
| `workflow list` | 列出 Base 下所有工作流(含状态/创建人/最后修改时间),支持分页 |
|
||||
| `workflow get` | 获取单个工作流详情(含 flowSchema 完整节点定义) |
|
||||
| `workflow enable` | 启用指定工作流(按配置的触发条件自动执行) |
|
||||
| `workflow disable` | 禁用指定工作流(高危,建议 `--yes` 二次确认) |
|
||||
|
||||
> 所有子命令的 `--base-id` 必填(可用隐藏别名 `--base`)。
|
||||
> 当前**不支持通过 CLI 新建工作流**,请在 AI 表格 Web 端配置好后用 `workflow list` 拿到 ID 再启停。
|
||||
|
||||
## DSL 入参格式与最小 Demo
|
||||
|
||||
`workflow create/update` 的 `--dsl` 接收完整的 `workflow-dsl/v1` JSON object,不是局部 patch。支持内联 JSON、`@文件路径` 或 `-` 从 stdin 读取。
|
||||
|
||||
复杂工作流应先用 `table get` / `field get` / `view list` 确认真实 `sheetId`、`fieldId`、`viewId`,并检查所有 `next`、`loopEntry`、branch `to` 和 ref。下面是一个不依赖数据表字段的最小定时消息工作流:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "workflow-dsl/v1",
|
||||
"name": "每日提醒",
|
||||
"description": "可选说明",
|
||||
"trigger": "start",
|
||||
"steps": {
|
||||
"start": {
|
||||
"type": "Scheduled",
|
||||
"next": "send",
|
||||
"data": {
|
||||
"mode": "daily",
|
||||
"time": "09:00",
|
||||
"timezone": "GMT+08:00"
|
||||
}
|
||||
},
|
||||
"send": {
|
||||
"type": "SendMessage",
|
||||
"data": {
|
||||
"title": "定时任务已触发",
|
||||
"to": {"users": [{"ref": "$.system_node.ownerUserId"}]}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
create 和 update 都必须同时满足 `status=success`、`data.valid=true`、`data.issues=[]` 才表示发布成功。
|
||||
|
||||
## 命令详情
|
||||
|
||||
### workflow create — 创建并发布工作流
|
||||
|
||||
```bash
|
||||
dws aitable workflow create \
|
||||
--base-id BASE_ID \
|
||||
--dsl @workflow.json \
|
||||
--locale zh-CN \
|
||||
--format json
|
||||
|
||||
cat workflow.json | dws aitable workflow create --base-id BASE_ID --dsl - --format json
|
||||
```
|
||||
|
||||
| flag | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--base-id` | 是 | 所属 Base ID |
|
||||
| `--dsl` | 是 | workflow-dsl/v1 JSON object;支持内联、`@filepath`、`-` stdin |
|
||||
| `--locale` | 否 | 请求语言,如 `zh-CN` / `zh_CN` |
|
||||
|
||||
`create` 非幂等,CLI 不自动重试。若网络中断导致结果不确定,先 `workflow list` 按名称确认是否已经创建,再决定是否重试。`status=success` 只说明服务正常返回;`data.valid=false` 仍是发布失败,必须读取 `issues`。
|
||||
|
||||
### workflow update — 更新并发布工作流
|
||||
|
||||
```bash
|
||||
# 先留底,再提交完整目标 DSL
|
||||
dws aitable workflow get --base-id BASE_ID --workflow-id WORKFLOW_ID --format json > /tmp/workflow-backup.json
|
||||
dws aitable workflow update \
|
||||
--base-id BASE_ID \
|
||||
--workflow-id WORKFLOW_ID \
|
||||
--dsl @workflow.json \
|
||||
--locale zh_CN \
|
||||
--format json
|
||||
```
|
||||
|
||||
| flag | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--base-id` | 是 | 所属 Base ID |
|
||||
| `--workflow-id` | 是 | 目标工作流 ID,对应 list 的 `flowId` |
|
||||
| `--dsl` | 是 | 完整目标 workflow-dsl/v1 JSON object;支持内联、`@filepath`、`-` stdin |
|
||||
| `--locale` | 否 | 请求语言,如 `zh-CN` / `zh_CN` |
|
||||
|
||||
update 返回结构与 create 相同,并会对瞬态错误重试。成功时 `data.flowId` 应与传入的工作流 ID 一致;随后用 `workflow get/list` 验证发布结果和运行状态。
|
||||
|
||||
### workflow list — 列出工作流
|
||||
|
||||
```bash
|
||||
@@ -119,12 +197,12 @@ dws aitable workflow disable --base-id BASE_ID --workflow-id WORKFLOW_ID --yes -
|
||||
|
||||
| 能力 | 状态 |
|
||||
|------|------|
|
||||
| 新建工作流 | ✅ 创建并发布 |
|
||||
| 修改工作流配置 | ✅ 更新并发布 |
|
||||
| 列出工作流 | ✅ |
|
||||
| 看工作流详情(含 flowSchema) | ✅ |
|
||||
| 启用/禁用 | ✅ |
|
||||
| 新建工作流 | ❌ 当前不支持,请去 AI 表格 Web 端 → 数据表 → 自动化 创建 |
|
||||
| 修改工作流配置 | ❌ 同上,需 Web UI 编辑 |
|
||||
| 删除工作流 | ❌ 同上 |
|
||||
| 删除工作流 | ❌ 暂未开放 |
|
||||
| 查看运行历史/执行日志 | ❌ 暂未开放 |
|
||||
| 手动触发/单次运行 | ❌ 暂未开放 |
|
||||
|
||||
@@ -132,6 +210,9 @@ dws aitable workflow disable --base-id BASE_ID --workflow-id WORKFLOW_ID --yes -
|
||||
|
||||
| 场景 | code | type | 备注 |
|
||||
|------|------|------|------|
|
||||
| create/update 返回 `valid=false` | — | success envelope | 读取 `data.issues` 修正 DSL,不能当作发布成功 |
|
||||
| create 下游失败 | `CREATE_WORKFLOW_ERROR` | `SYSTEM_ERROR` | create 不自动重试;先 list 排查是否已创建 |
|
||||
| update 下游失败 | `UPDATE_WORKFLOW_ERROR` | `SYSTEM_ERROR` | update 会重试瞬态错误,最终失败时保留 DSL 和 workflowId 排查 |
|
||||
| `workflow-id` 不存在调 get | `GET_WORKFLOW_ERROR` | `SYSTEM_ERROR` | message 可能为 null,先 `workflow list` 核对 ID |
|
||||
| `workflow-id` 不存在调 enable | `ENABLE_WORKFLOW_ERROR` | `SYSTEM_ERROR` | message 含 "场域中不存在该 namespace" |
|
||||
| `workflow-id` 不存在调 disable | `DISABLE_WORKFLOW_ERROR` | `SYSTEM_ERROR` | 同上 |
|
||||
@@ -142,6 +223,18 @@ dws aitable workflow disable --base-id BASE_ID --workflow-id WORKFLOW_ID --yes -
|
||||
|
||||
## 典型工作流
|
||||
|
||||
### 创建并确认一个工作流
|
||||
|
||||
```bash
|
||||
dws aitable workflow create --base-id BASE_ID --dsl @/tmp/workflow.json --locale zh-CN --format json \
|
||||
| tee /tmp/workflow-result.json
|
||||
|
||||
jq '{valid: .data.valid, flowId: .data.flowId, issues: .data.issues}' /tmp/workflow-result.json
|
||||
FLOW_ID=$(jq -r '.data.flowId' /tmp/workflow-result.json)
|
||||
dws aitable workflow list --base-id BASE_ID --format json \
|
||||
| jq --arg id "$FLOW_ID" '.data.list[] | select(.flowId == $id) | {flowId, name, status}'
|
||||
```
|
||||
|
||||
### 看看 Base 里有哪些自动化在跑
|
||||
|
||||
```bash
|
||||
@@ -175,6 +268,9 @@ done
|
||||
## 注意事项
|
||||
|
||||
- `--workflow-id` 接受的就是 `list` 返回里的 `flowId`(同值,CLI 屏蔽了服务端字段名差异)。
|
||||
- create / update 的 `--dsl` 必须是 JSON object,不能传数组、二次字符串化 JSON 或 `flowSchema`。
|
||||
- `status=success` 且 `data.valid=false` 仍是 DSL 校验失败;`issues` 才是下一步修复依据。
|
||||
- create 不自动重试;update 仅对网络、5xx、`retryable:true` 等瞬态错误自动重试。
|
||||
- enable / disable 出参里的 `enabled` / `disabled` 是 **动作确认 flag**,不是当前状态字段。要确认真生效请走 `workflow list` 查 `status`。
|
||||
- `workflow get` 的 `flowSchema` 结构随触发器/动作类型变化,不要假设固定字段。
|
||||
- 新建/修改/删除工作流目前必须在 AI 表格 Web 端(数据表页面 → 自动化)完成。
|
||||
- 删除、运行历史和手动触发当前仍未开放。
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: dingtalk-sheet
|
||||
description: 钉钉电子表格。Use when 用户说 电子表格/工作表/单元格读写/单元格追加/查找/公式/超链接/插入图片/浮动图片/sheet。Distinct from dingtalk-aitable(AI表格/多维表/字段类型)、dingtalk-doc(普通文档)。命令前缀:dws sheet。
|
||||
description: 钉钉电子表格。Use when 用户说 电子表格/导入Excel为在线表格/工作表/单元格读写/单元格追加/查找/公式/超链接/插入图片/浮动图片/sheet。Distinct from dingtalk-aitable(AI表格/多维表/字段类型)、dingtalk-doc(普通文档)。命令前缀:dws sheet。
|
||||
cli_version: ">=0.2.14"
|
||||
metadata:
|
||||
category: product
|
||||
@@ -43,6 +43,7 @@ metadata:
|
||||
| 用户说 | 命令 |
|
||||
|--------|------|
|
||||
| "创建电子表格" | `dws sheet create --name "<标题>"` |
|
||||
| "导入本地 Excel 为在线表格" | `dws sheet import create --file <xlsx或xls> --folder-token <ID>` 或 `--workspace <ID>` |
|
||||
| "新建工作表" | `dws sheet new --node <nodeId或URL> --name "<sheet名>"` |
|
||||
| "读取单元格" | `dws sheet range read --node <nodeId或URL> --sheet-id <sheetId> --range A1:B2` |
|
||||
| "写入单元格" | `dws sheet range update --node <nodeId或URL> --sheet-id <sheetId> --range A1:B2 --values '[[..]]'` |
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
## 适用范围(重要)
|
||||
|
||||
`sheet` 产品线仅支持钉钉在线电子表格(`contentType=ALIDOC`、`extension=axls`),不支持上传的 `xlsx` / `xls` / `xlsm` / `csv` 等本地表格文件。
|
||||
`sheet` 的单元格与工作表操作仅支持钉钉在线电子表格(`contentType=ALIDOC`、`extension=axls`)。`sheet import create` 是 Agent 可发现的文件入口:它接收本地 `xlsx` / `xls`,转换并新建在线电子表格;不能把已上传的 xlsx 节点直接当作 axls 操作。
|
||||
|
||||
| 文件类型 | 处理方式 |
|
||||
|---------|---------|
|
||||
| 在线电子表格(`axls`) | 走 `sheet` 全部命令(读/写/筛选/合并/导出等服务端原子操作) |
|
||||
| `xlsx` / `xls` / `xlsm` / `csv` 等本地表格文件 | 必须用 `dws doc download --node <ID> --output <路径>` 先下载到本地再解析处理,禁止调用任何 `sheet` 子命令(sheet 底层 MCP 工具仅认 `axls`,传入 xlsx 节点会直接报错) |
|
||||
| 本地路径上的 `xlsx` / `xls` | 用 `dws sheet import create --file <路径> --folder-token <ID>` 或 `--workspace <ID>` 转换为新的在线电子表格 |
|
||||
| 钉盘/文档中已有的 `xlsx` / `xls` / `xlsm` / `csv` 节点 | 不能直接调用工作表/单元格命令;先用 `dws doc download --node <ID> --output <路径>` 下载。若用户要转为在线表格,再对下载文件执行 `sheet import` |
|
||||
| 想把在线表格导出为 xlsx | 用 `dws sheet export` ——输入是 `axls`,输出是 xlsx(axls → xlsx 的格式转换) |
|
||||
|
||||
> 用户直接粘贴原始 `alidocs` URL 时必须先 probe:先执行 `dws doc info --node <URL> --format json`,按 [链接规范](../url-patterns.md#alidocs-url-类型探测流程) 校验 `contentType` 和 `extension`:
|
||||
@@ -125,6 +126,7 @@ dws sheet filter-view --help
|
||||
| `sheet list-float-images` | 列出工作表所有浮动图片 |
|
||||
| `sheet update-float-image` | 更新浮动图片属性 |
|
||||
| `sheet delete-float-image` | 删除浮动图片 |
|
||||
| `sheet import create` | 导入本地 xlsx/xls 为新的在线电子表格(`sheet import` 保留兼容) |
|
||||
| `sheet export` | 导出表格为 xlsx |
|
||||
| `sheet filter get` | 获取全局筛选信息 |
|
||||
| `sheet filter create` | 创建全局筛选 |
|
||||
@@ -176,6 +178,7 @@ dws sheet filter-view --help
|
||||
| 查找替换 | [sheet/sheet-search-replace.md](./sheet/sheet-search-replace.md) | find / replace |
|
||||
| 全局筛选 | [sheet/sheet-filter.md](./sheet/sheet-filter.md) | filter get / create / delete / update / clear-criteria / sort |
|
||||
| 筛选视图 | [sheet/sheet-filter-view.md](./sheet/sheet-filter-view.md) | filter-view 全系列 |
|
||||
| 导入 | [sheet/sheet-import.md](./sheet/sheet-import.md) | import / import get |
|
||||
| 导出 | [sheet/sheet-export.md](./sheet/sheet-export.md) | export |
|
||||
|
||||
> `template list/search/apply` 无独立子文档,直接执行 `dws sheet template <子命令> --help` 查看用法。
|
||||
@@ -369,6 +372,15 @@ dws sheet filter-view --help
|
||||
- 清除筛选视图列条件 → `filter-view delete-criteria`
|
||||
- 注意与 `filter-view delete`(删除整个筛选视图)区分:`delete-criteria` 仅清除指定列的条件,不删除筛选视图本身
|
||||
|
||||
### 导入
|
||||
|
||||
用户说"导入Excel/把xlsx转为在线表格/上传本地表格并在线编辑":
|
||||
- 使用 `sheet import`,本地文件仅支持 `xlsx` / `xls`
|
||||
- `--folder-token` 与 `--workspace` 至少提供一个;不指定 `--name` 时使用文件名
|
||||
- CLI 内部完成上传、转换和轮询,Agent 不要自行拆分流程或重复提交
|
||||
- 若返回 `timed_out:true`,执行返回的 `next_command` 续查,不要重新执行 import
|
||||
- 详见 [sheet/sheet-import.md](./sheet/sheet-import.md)
|
||||
|
||||
### 导出
|
||||
|
||||
用户说"导出/下载xlsx/存为Excel/存成表格文件/把表格变成xlsx/导出表格/下载表格/导出为 excel":
|
||||
@@ -2139,7 +2151,7 @@ dws sheet export --node <NODE_ID> --output ./
|
||||
- `copy` 当指定名称与已有工作表重复时,系统会自动重命名为合法值
|
||||
- `copy` 的 `--index` 可选,不传时副本将放置在源工作表之后的默认位置
|
||||
- ★ 关键区分: sheet(电子表格/单元格读写) vs aitable(AI多维表/结构化记录/字段定义) vs doc(文档编辑/阅读)
|
||||
- sheet 产品线仅支持 `axls`(在线电子表格,`contentType=ALIDOC`),不支持 `xlsx` / `xls` / `xlsm` / `csv` 等本地表格文件
|
||||
- 工作表与单元格命令仅支持 `axls`;本地 `xlsx` / `xls` 可通过 `sheet import` 转换为新的在线电子表格
|
||||
- 遇到未知 `alidocs` URL 时,必须先 probe(`dws doc info --node <URL> --format json`)确认 `contentType` 和 `extension`,才能决定是否走 sheet
|
||||
- 当节点 `extension=xlsx` / `xls` / `xlsm` / `csv`(`contentType≠ALIDOC`)时,必须用 `dws doc download --node <ID> --output <路径>` 先下载到本地再处理,禁止调用任何 sheet 子命令(sheet 底层 MCP 工具只识别 axls,调用 xlsx 节点必失败)
|
||||
- 要把在线表格导出为 xlsx 文件——走 `dws sheet export`(axls → xlsx 的格式转换);要读已有的 xlsx 文件——走 `dws doc download` 后在本地解析,两者方向相反
|
||||
- 要把在线表格导出为 xlsx 文件,走 `dws sheet export`;要把本地 xlsx/xls 转为在线表格,走 `dws sheet import`;要读取已上传但未转换的 xlsx 节点,先走 `dws doc download`
|
||||
@@ -0,0 +1,76 @@
|
||||
# 导入本地表格(import)
|
||||
|
||||
## 使用场景
|
||||
|
||||
用户说“导入 Excel”“把 xlsx 变成在线表格”“上传本地表格并在线编辑”时,使用 Agent 可发现入口 `dws sheet import create`。它只新建在线电子表格,不覆盖已有表格。旧入口 `dws sheet import` 继续兼容人工调用。
|
||||
|
||||
- 支持本地 `xlsx` / `xls`,文件上限 20MB
|
||||
- `--folder-token` 与 `--workspace` 至少提供一个
|
||||
- CLI 内部完成创建会话、OSS 上传、确认转换和轮询,Agent 不要自行拆分或重试
|
||||
- `drive upload` 只上传二进制文件,不会转换为可编辑的在线表格,不能替代本命令
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
# 导入到指定文件夹
|
||||
dws sheet import create \
|
||||
--file ./quote.xlsx \
|
||||
--folder-token <FOLDER_TOKEN> \
|
||||
--format json
|
||||
|
||||
# 导入到指定知识库,并自定义名称
|
||||
dws sheet import create \
|
||||
--file ./report.xls \
|
||||
--workspace <WORKSPACE_ID> \
|
||||
--name "月度报表" \
|
||||
--format json
|
||||
|
||||
# 导入超时或中断后续查
|
||||
dws sheet import get --task-id <TASK_ID> --format json
|
||||
```
|
||||
|
||||
公开参数:
|
||||
|
||||
| 命令 | 参数 | 说明 |
|
||||
|------|------|------|
|
||||
| `sheet import create` | `--file` | 本地 xlsx/xls 文件(必填) |
|
||||
| `sheet import create` | `--folder-token` | 目标文件夹 ID 或 URL,与 `--workspace` 至少传一个 |
|
||||
| `sheet import create` | `--workspace` | 目标知识库 ID 或 URL,与 `--folder-token` 至少传一个 |
|
||||
| `sheet import create` | `--name`, `-n` | 导入后的名称;默认取文件名并去掉扩展名 |
|
||||
| `sheet import get` | `--task-id` | 导入任务 ID |
|
||||
|
||||
## 返回与续查
|
||||
|
||||
成功时返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"taskId": "<TASK_ID>",
|
||||
"documentUrl": "<DOCUMENT_URL>",
|
||||
"documentName": "月度报表",
|
||||
"documentType": "1",
|
||||
"nodeId": "<NODE_ID>"
|
||||
}
|
||||
```
|
||||
|
||||
轮询达到上限时命令以退出码 0 返回业务态,避免 Agent 重复创建文档:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"timed_out": true,
|
||||
"taskId": "<TASK_ID>",
|
||||
"status": "processing",
|
||||
"next_command": "dws sheet import get --task-id <TASK_ID>"
|
||||
}
|
||||
```
|
||||
|
||||
检测到 `timed_out:true` 后,只执行 `next_command` 续查。不要重新执行 `sheet import create`。
|
||||
|
||||
## 边界
|
||||
|
||||
- 本命令接受的是本地文件路径;它把 xlsx/xls 转换成新的 axls 在线表格
|
||||
- 已经存在于钉盘或文档中的 xlsx 节点不能直接传给工作表/单元格命令;先用 `doc download` 下载,需要在线编辑时再执行 `sheet import create`
|
||||
- 向已有在线表格写数据应使用 `range update`、`append`、`csv-put` 或 `table-put`
|
||||
- md/doc/docx 等文字文档导入使用 `dws doc import`
|
||||
@@ -577,8 +577,8 @@ func TestBuildEntrypointsUseStripLdflags(t *testing.T) {
|
||||
want: `go build -ldflags="-s -w" -o $tmpBin "$Root/cmd"`,
|
||||
},
|
||||
{
|
||||
relPath: filepath.Join("..", "..", "scripts", "policy", "check-command-surface.sh"),
|
||||
want: `go build -ldflags="-s -w" -o "$BIN_PATH" ./cmd`,
|
||||
relPath: filepath.Join("..", "..", "scripts", "dev", "build.sh"),
|
||||
want: `-ldflags="-s -w"`,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -599,6 +599,32 @@ func TestBuildEntrypointsUseStripLdflags(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolicyBinaryChecksReuseBuiltDWS(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, relPath := range []string{
|
||||
filepath.Join("..", "..", "scripts", "policy", "check-command-surface.sh"),
|
||||
filepath.Join("..", "..", "scripts", "policy", "check-schema-binary.sh"),
|
||||
} {
|
||||
scriptPath, err := filepath.Abs(relPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Abs(%s) error = %v", relPath, err)
|
||||
}
|
||||
data, err := os.ReadFile(scriptPath)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(%s) error = %v", scriptPath, err)
|
||||
}
|
||||
text := string(data)
|
||||
if !strings.Contains(text, `BIN="${DWS_BIN:-$ROOT/dws}"`) {
|
||||
t.Fatalf("%s does not reuse DWS_BIN", scriptPath)
|
||||
}
|
||||
if strings.Contains(text, `go build -o "$tmp/dws" ./cmd`) ||
|
||||
strings.Contains(text, `go build -ldflags="-s -w"`) {
|
||||
t.Fatalf("%s unexpectedly rebuilds dws", scriptPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestInstallScriptsCacheMultiSkills verifies install.sh / install.ps1 /
|
||||
// install-skills.sh / build/npm/install.js all carry the wiring that caches
|
||||
// the multi/ tree to ~/.dws/skills/multi/ during install. This is what lets
|
||||
|
||||
Reference in new issue
Block a user