Compare commits
28
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecbd2e3009 | ||
|
|
33df6ee794 | ||
|
|
18e1c7870e | ||
|
|
1ff4941082 | ||
|
|
f3231ed2a8 | ||
|
|
7762abc1ae | ||
|
|
8d1b76caa7 | ||
|
|
9d0995a61d | ||
|
|
571eb20457 | ||
|
|
7937d09eed | ||
|
|
bc39d24559 | ||
|
|
3717053d24 | ||
|
|
2a3df50d0f | ||
|
|
03b3cf68e4 | ||
|
|
bbdf843ef3 | ||
|
|
eb2658ca68 | ||
|
|
a5ac09218b | ||
|
|
bfb812bfa0 | ||
|
|
4b0f71eedc | ||
|
|
5c30d01522 | ||
|
|
c94190f90e | ||
|
|
6763ddd154 | ||
|
|
235cad4cc7 | ||
|
|
96d0d430e6 | ||
|
|
0d866911e0 | ||
|
|
2bf5401f55 | ||
|
|
c76c30a0b7 | ||
|
|
51dc237d8a |
@@ -7,7 +7,8 @@ unrelated work, and use `gofmt` for every modified Go file.
|
||||
|
||||
- Build: `go build ./cmd`
|
||||
- Full test suite: `DWS_PACKAGE_VERSION=0.0.0-test go test ./...`
|
||||
- Generate Schema assets: `go generate ./internal/cli`
|
||||
- Generate Schema assets: `go generate ./internal/cli` (entry point: `internal/cli/gen.go`)
|
||||
- Refresh pinned MCP metadata: `make fetch-mcp-metadata` (requires `dws auth login`)
|
||||
- Check generated drift: `./scripts/policy/check-generated-drift.sh`
|
||||
- Check the Schema contract: `./scripts/policy/check-schema-catalog.sh`
|
||||
|
||||
@@ -56,8 +57,16 @@ The Schema data flow is one way:
|
||||
|
||||
6. One-way publication
|
||||
SchemaRegistry
|
||||
└─ internal/cli/schema_catalog.json
|
||||
└─ internal/cli/schema_catalog/
|
||||
(catalog.json + tools/<product>.json; split per product so
|
||||
concurrent feature PRs only rewrite their own shard)
|
||||
└─ dws schema list/product/group/leaf/--all
|
||||
|
||||
7. Runtime consumption (unified API)
|
||||
ResolveMeta(cliPath) → CommandMeta{Identity, Safety, Selection}
|
||||
└─ internal/cli/command_meta.go
|
||||
└─ all consumers (help, schema, agent, skill-gen) call this one function
|
||||
└─ backed by embedded catalog (sync.Once lazy map, O(1) lookup)
|
||||
```
|
||||
|
||||
Parameter overlays from metadata are merged into `EffectiveCommandRegistry`
|
||||
@@ -73,11 +82,37 @@ Build-time gates and the snapshot serializer consume that source-resolved typed
|
||||
registry/index. Runtime projections and delivery gates consume the typed
|
||||
registry/index returned by the production snapshot loader. Neither path may
|
||||
reopen annotations, merge source records, or use a previous Catalog or other
|
||||
generated JSON as a source. `schema_catalog.json` is output-only in the
|
||||
generated JSON as a source. `schema_catalog/` (catalog.json + per-product
|
||||
tools/<product>.json shards) is output-only in the
|
||||
generation graph. The production loader decoding the embedded published
|
||||
snapshot is a delivery boundary, not source resolution; it must never create or
|
||||
repair a Cobra command, flag, registry entry, or later Catalog generation.
|
||||
|
||||
### Generation vs consumption separation
|
||||
|
||||
The Schema system has two physically separated processes:
|
||||
|
||||
**Generation** (build-time, slow, reviewed, one-way):
|
||||
- Entry point: `internal/cli/gen.go` (all `//go:generate` pragmas isolated here,
|
||||
not in business code).
|
||||
- Tools: `internal/generator/cmd_schema_agent_metadata` + `cmd_schema_catalog`
|
||||
(standalone Go mains).
|
||||
- Inputs: 6 authored sources (registry + hints metadata + hints selection +
|
||||
MCP metadata + parameter bindings + cobra tree).
|
||||
- Output: `schema_catalog/` (per-product shards) + `schema_agent_metadata/`.
|
||||
- Refresh MCP metadata: `make fetch-mcp-metadata` (iterates 26 MCP server
|
||||
endpoints, merges with previous data for cross-server interface_ref).
|
||||
- Gates: `make generate-schema` (byte guards on inputs), `check-generated-drift.sh`,
|
||||
`check-command-surface.sh` (catalog structure).
|
||||
|
||||
**Consumption** (runtime, fast, read-only, unified API):
|
||||
- Entry point: `ResolveMeta(cliPath) → CommandMeta{Identity, Safety, Selection}`
|
||||
in `internal/cli/command_meta.go`.
|
||||
- Backed by embedded catalog (`embeddedSchemaCatalog()`, sync.Once, O(1) map).
|
||||
- Consumers: `--help` (Safety annotation via `RenderSafetyAnnotation`),
|
||||
`dws schema`, agent selection, future skill generation.
|
||||
- `SafetyForCLIPath` delegates to `ResolveMeta` (backward compatible).
|
||||
|
||||
This split is architecturally isomorphic to Lark's typed metadata registry,
|
||||
navigation catalog, and schema renderer. DWS intentionally preserves its
|
||||
existing flat JSON wire contract for compatibility; do not treat architectural
|
||||
@@ -117,7 +152,8 @@ When adding or changing an Agent-visible command, review all relevant inputs:
|
||||
must never materialize, infer, or override registry identity.
|
||||
- Flag-to-interface property mappings and required/default semantics.
|
||||
- Generated files under `internal/cli/schema_agent_metadata/` and
|
||||
`internal/cli/schema_catalog.json` after running generation.
|
||||
`internal/cli/schema_catalog/` (catalog.json + tools/<product>.json) after
|
||||
running generation.
|
||||
|
||||
Run the reverse-completeness tests whenever the Cobra tree changes. A command
|
||||
that works through `dws <path>` but cannot be found through the matching
|
||||
@@ -177,7 +213,7 @@ For every curated tool:
|
||||
2. Edit `selection/<product>.json` for selection prose (`reviewed: true`,
|
||||
`review_reason`, `source_refs`).
|
||||
3. Run `make generate-schema`. Do not hand-edit generated
|
||||
`schema_agent_metadata/` or `schema_catalog.json`.
|
||||
`schema_agent_metadata/` or `schema_catalog/`.
|
||||
|
||||
### Pull live MCP descriptions (personal token)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ POLICY_GOTMPDIR ?= $(DWS_POLICY_TMPDIR)/go
|
||||
POLICY_ENV = DWS_POLICY_TMPDIR="$(DWS_POLICY_TMPDIR)" GOTMPDIR="$(POLICY_GOTMPDIR)"
|
||||
GO_SOURCE_LIST = git ls-files -z --cached --others --exclude-standard -- '*.go'
|
||||
|
||||
.PHONY: all help build rebuild test test-plan test-auth-legacy-compat lint format-check 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 release-pre release-stable changelog-pre changelog-stable publish-homebrew-formula setup-hooks
|
||||
.PHONY: all help build rebuild test test-plan test-auth-legacy-compat lint format-check 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 fetch-mcp-metadata generate-schema-catalog package release release-pre release-stable changelog-pre changelog-stable publish-homebrew-formula setup-hooks
|
||||
|
||||
all: setup-hooks fmt lint build test rebuild
|
||||
|
||||
@@ -129,16 +129,16 @@ test-schema-agent-examples:
|
||||
|
||||
generate-schema:
|
||||
@set -e; \
|
||||
registry_guard=$$(mktemp); \
|
||||
registry_guard=$$(mktemp -d); \
|
||||
metadata_guard=$$(mktemp -d); \
|
||||
selection_guard=$$(mktemp -d); \
|
||||
trap 'rm -rf "$$registry_guard" "$$metadata_guard" "$$selection_guard"' EXIT HUP INT TERM; \
|
||||
cp internal/cli/schema_command_registry.json "$$registry_guard"; \
|
||||
cp -R internal/cli/schema_command_registry/ "$$registry_guard/"; \
|
||||
cp -R internal/cli/schema_hints/metadata/. "$$metadata_guard/"; \
|
||||
cp -R internal/cli/schema_hints/selection/. "$$selection_guard/"; \
|
||||
$(GO) generate ./internal/cli; \
|
||||
cmp -s internal/cli/schema_command_registry.json "$$registry_guard" || { \
|
||||
printf '%s\n' 'generation modified reviewed input internal/cli/schema_command_registry.json' >&2; \
|
||||
diff -qr internal/cli/schema_command_registry "$$registry_guard" >/dev/null || { \
|
||||
printf '%s\n' 'generation modified reviewed input internal/cli/schema_command_registry/' >&2; \
|
||||
exit 1; \
|
||||
}; \
|
||||
diff -qr internal/cli/schema_hints/metadata "$$metadata_guard" >/dev/null || { \
|
||||
@@ -153,14 +153,18 @@ generate-schema:
|
||||
generate-schema-agent-metadata:
|
||||
$(GO) run ./internal/generator/cmd_schema_agent_metadata \
|
||||
-root . \
|
||||
-registry internal/cli/schema_command_registry.json \
|
||||
-registry internal/cli/schema_command_registry \
|
||||
-output-dir internal/cli/schema_agent_metadata \
|
||||
-audit-output internal/cli/schema_agent_metadata_audit.json
|
||||
|
||||
generate-schema-catalog:
|
||||
$(GO) run -a ./internal/generator/cmd_schema_catalog \
|
||||
-root . \
|
||||
-output internal/cli/schema_catalog.json
|
||||
-output internal/cli/schema_catalog
|
||||
|
||||
fetch-mcp-metadata:
|
||||
@printf ' %sRefreshing MCP metadata from live server%s\n' "$(COLOR_RUN)" "$(COLOR_RESET)"
|
||||
@./scripts/dev/fetch_mcp_metadata.sh
|
||||
|
||||
package:
|
||||
@version="$(if $(VERSION),$(VERSION),v0.0.0-SNAPSHOT)"; VERSION="$${version#v}" ./scripts/dev/build-all.sh
|
||||
|
||||
@@ -0,0 +1,403 @@
|
||||
// Command fetch_mcp_metadata pulls tools/list from ALL live MCP server endpoints
|
||||
// and writes a refreshed schema_mcp_metadata.json. This is DWS's equivalent of
|
||||
// lark-cli's scripts/fetch_meta.py.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// dws auth login # ensure valid auth
|
||||
// make fetch-mcp-metadata # runs this tool
|
||||
//
|
||||
// The tool loads auth from the DWS keychain, iterates all 26 static server
|
||||
// endpoints (internal/syncdata.StaticServers), calls tools/list on each,
|
||||
// merges results, and writes schema_mcp_metadata.json.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/auth"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/syncdata"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/transport"
|
||||
)
|
||||
|
||||
// toolLister is the tools/list capability consumed by run; production code
|
||||
// uses transport.Client, tests inject fakes.
|
||||
type toolLister interface {
|
||||
ListTools(ctx context.Context, endpoint string) (transport.ToolsListResult, error)
|
||||
}
|
||||
|
||||
// Injection points so run() is fully testable without network/keychain/exit.
|
||||
var (
|
||||
osExit = os.Exit
|
||||
getenv = os.Getenv
|
||||
loadTokenData = auth.LoadTokenDataKeychain
|
||||
staticServers = syncdata.StaticServers
|
||||
registrySource = cli.EmbeddedCommandRegistryMergedJSON
|
||||
listToolsTimeout = 30 * time.Second
|
||||
gitHeadPath = ".git/HEAD"
|
||||
newToolLister = func(token string) toolLister {
|
||||
return transport.NewClient(&http.Client{Timeout: 60 * time.Second}).WithAuth(token, nil)
|
||||
}
|
||||
)
|
||||
|
||||
func main() {
|
||||
osExit(run(os.Args[1:], os.Stderr))
|
||||
}
|
||||
|
||||
func run(args []string, stderr io.Writer) int {
|
||||
flags := flag.NewFlagSet("fetch_mcp_metadata", flag.ContinueOnError)
|
||||
flags.SetOutput(stderr)
|
||||
output := flags.String("output", "internal/cli/schema_mcp_metadata.json", "output file path")
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 2
|
||||
}
|
||||
|
||||
token := resolveToken(stderr)
|
||||
if token == "" {
|
||||
fmt.Fprintln(stderr, "fetch_mcp_metadata: no auth token. Run 'dws auth login' first.")
|
||||
return 1
|
||||
}
|
||||
|
||||
client := newToolLister(token)
|
||||
|
||||
// Iterate ALL static server endpoints (26 servers covering all products).
|
||||
servers := staticServers()
|
||||
fmt.Fprintf(stderr, "fetch_mcp_metadata: querying %d server endpoints\n", len(servers))
|
||||
|
||||
// Load CLI registry to build tool_name → interface_ref mapping.
|
||||
registryMap := loadRegistryInterfaceRefs(stderr)
|
||||
fmt.Fprintf(stderr, "fetch_mcp_metadata: registry mapping: %d entries\n", len(registryMap))
|
||||
|
||||
// Load the previous schema_mcp_metadata.json to preserve hand-curated
|
||||
// cross-server interface_ref mappings that automated matching can't derive.
|
||||
prevData, prevErr := os.ReadFile(*output)
|
||||
prevTools := map[string]map[string]any{}
|
||||
if prevErr == nil {
|
||||
var prev struct {
|
||||
Tools map[string]map[string]any `json:"tools"`
|
||||
}
|
||||
if json.Unmarshal(prevData, &prev) == nil {
|
||||
prevTools = prev.Tools
|
||||
}
|
||||
}
|
||||
|
||||
// Start from previous data (preserves cross-server refs), then overwrite
|
||||
// with fresh MCP data where available.
|
||||
allTools := make(map[string]map[string]any)
|
||||
for k, v := range prevTools {
|
||||
allTools[k] = v
|
||||
}
|
||||
|
||||
// Reviewed cross-server interface_refs live only in the previous snapshot
|
||||
// (the registry stores canonical paths, not MCP identities). Build a
|
||||
// live-key → canonicals index so those tools get refreshed instead of
|
||||
// being skipped and frozen at the previous snapshot forever.
|
||||
crossRefs := buildCrossServerRefs(prevTools, registryMap)
|
||||
if len(crossRefs) > 0 {
|
||||
fmt.Fprintf(stderr, "fetch_mcp_metadata: cross-server ref index: %d live keys\n", len(crossRefs))
|
||||
}
|
||||
// Canonicals with a reviewed cross-server identity must only be fed by
|
||||
// that identity; a same-named tool on another server is a coincidence,
|
||||
// not a data source.
|
||||
crossOwned := map[string]bool{}
|
||||
for _, canonicals := range crossRefs {
|
||||
for _, canonical := range canonicals {
|
||||
crossOwned[canonical] = true
|
||||
}
|
||||
}
|
||||
totalRaw := 0
|
||||
failedServices := []string{}
|
||||
|
||||
for _, srv := range servers {
|
||||
endpoint := strings.TrimSpace(srv.Endpoint)
|
||||
if endpoint == "" {
|
||||
continue
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), listToolsTimeout)
|
||||
result, err := client.ListTools(ctx, endpoint)
|
||||
cancel()
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, " [skip] %s: %v\n", srv.ID, err)
|
||||
failedServices = append(failedServices, srv.ID)
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(stderr, " [ok] %s: %d tools\n", srv.ID, len(result.Tools))
|
||||
totalRaw += len(result.Tools)
|
||||
for _, tool := range result.Tools {
|
||||
name := strings.TrimSpace(tool.Name)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
// Direct match: CLI canonical equals server-prefixed tool name
|
||||
// (e.g., "doc.copy_document"). Cross-owned canonicals are skipped
|
||||
// here — their reviewed identity feeds them below.
|
||||
canonicalKey := srv.ID + "." + name
|
||||
if ref, hasRef := registryMap[canonicalKey]; hasRef && !crossOwned[canonicalKey] {
|
||||
mergeLiveMCPTool(allTools, canonicalKey, tool, ref)
|
||||
}
|
||||
// Cross-server match: registry canonicals whose reviewed
|
||||
// interface_ref points at this live tool (one live tool may feed
|
||||
// several canonicals, e.g. advperm_enable/disable → set_advanced_permission).
|
||||
for _, canonical := range crossRefs[canonicalKey] {
|
||||
mergeLiveMCPTool(allTools, canonical, tool, registryMap[canonical])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
matched := 0
|
||||
for _, t := range allTools {
|
||||
if _, ok := t["interface_ref"]; ok {
|
||||
matched++
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(stderr, "fetch_mcp_metadata: MCP matched=%d, with interface_ref=%d\n", len(allTools), matched)
|
||||
|
||||
// Fill gaps: for registry canonicals not covered by MCP tools/list OR
|
||||
// previous data, add stub entries (interface_ref only).
|
||||
stubs := 0
|
||||
for canonicalKey, ref := range registryMap {
|
||||
if _, exists := allTools[canonicalKey]; exists {
|
||||
continue
|
||||
}
|
||||
allTools[canonicalKey] = map[string]any{
|
||||
"interface_ref": ref,
|
||||
}
|
||||
stubs++
|
||||
}
|
||||
if stubs > 0 {
|
||||
fmt.Fprintf(stderr, "fetch_mcp_metadata: added %d registry stubs (no MCP data, interface_ref only)\n", stubs)
|
||||
}
|
||||
|
||||
// Compute coverage fields required by check-schema-catalog.sh. Failed
|
||||
// services must be reported honestly so policy can spot snapshot gaps.
|
||||
if len(failedServices) > 0 {
|
||||
fmt.Fprintf(stderr, "fetch_mcp_metadata: %d/%d services unreachable: %s\n",
|
||||
len(failedServices), len(servers), strings.Join(failedServices, ", "))
|
||||
}
|
||||
|
||||
metadata := map[string]any{
|
||||
"version": 1,
|
||||
"source": "mcp-tools-list+cli-registry",
|
||||
"coverage": buildCoverage(len(servers), failedServices, totalRaw, len(allTools), stubs),
|
||||
"tools": allTools,
|
||||
}
|
||||
|
||||
// source_revision: git commit hash (proves provenance).
|
||||
if rev, err := os.ReadFile(gitHeadPath); err == nil {
|
||||
metadata["source_revision"] = strings.TrimSpace(string(rev))
|
||||
}
|
||||
|
||||
if err := writeMetadata(*output, metadata); err != nil {
|
||||
fmt.Fprintf(stderr, "fetch_mcp_metadata: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
fmt.Fprintf(stderr, "fetch_mcp_metadata: wrote %d tools to %s\n", len(allTools), *output)
|
||||
return 0
|
||||
}
|
||||
|
||||
// resolveToken returns the access token from DWS_ACCESS_TOKEN or, as a
|
||||
// fallback, the DWS keychain.
|
||||
func resolveToken(stderr io.Writer) string {
|
||||
token := strings.TrimSpace(getenv("DWS_ACCESS_TOKEN"))
|
||||
if token != "" {
|
||||
return token
|
||||
}
|
||||
td, err := loadTokenData()
|
||||
if err != nil || td == nil || td.AccessToken == "" {
|
||||
return ""
|
||||
}
|
||||
fmt.Fprintf(stderr, "fetch_mcp_metadata: loaded token from keychain (%d chars)\n", len(td.AccessToken))
|
||||
return td.AccessToken
|
||||
}
|
||||
|
||||
// writeMetadata marshals the snapshot and writes it to the output path.
|
||||
func writeMetadata(path string, metadata map[string]any) error {
|
||||
data, err := json.MarshalIndent(metadata, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal failed: %w", err)
|
||||
}
|
||||
data = append(data, '\n')
|
||||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||
return fmt.Errorf("write %s failed: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildCoverage reports snapshot coverage honestly: snapshot_services only
|
||||
// counts services whose tools/list succeeded, missing_services names the
|
||||
// failures, and matched_tools excludes registry stubs (entries carrying no
|
||||
// live MCP metadata) so a stub-heavy snapshot cannot claim full matching.
|
||||
func buildCoverage(sourceServices int, failedServices []string, sourceTools, surfaceTools, stubs int) map[string]any {
|
||||
missing := failedServices
|
||||
if missing == nil {
|
||||
missing = []string{}
|
||||
}
|
||||
return map[string]any{
|
||||
"surface_scope": "source_revision",
|
||||
"source_services": sourceServices,
|
||||
"snapshot_services": sourceServices - len(missing),
|
||||
"missing_services": missing,
|
||||
"source_tools": sourceTools,
|
||||
"surface_tools": surfaceTools,
|
||||
"matched_tools": surfaceTools - stubs,
|
||||
"aliased_tools": 0,
|
||||
"unmatched_tools": stubs,
|
||||
}
|
||||
}
|
||||
|
||||
// mergeLiveMCPTool replaces stale live-derived fields while retaining an
|
||||
// existing reviewed interface_ref. Some CLI canonicals intentionally route to
|
||||
// a differently named product/RPC, so the previous cross-server mapping must
|
||||
// survive even though title, description, and parameters are refreshed.
|
||||
func mergeLiveMCPTool(allTools map[string]map[string]any, canonicalKey string, tool transport.ToolDescriptor, fallbackRef map[string]string) {
|
||||
interfaceRef := any(fallbackRef)
|
||||
if previous := allTools[canonicalKey]; previous != nil {
|
||||
if reviewedRef, ok := previous["interface_ref"]; ok && reviewedRef != nil {
|
||||
interfaceRef = reviewedRef
|
||||
}
|
||||
}
|
||||
|
||||
entry := map[string]any{
|
||||
"title": tool.Title,
|
||||
"description": tool.Description,
|
||||
"interface_ref": interfaceRef,
|
||||
}
|
||||
if tool.InputSchema != nil {
|
||||
entry["parameters"] = extractParams(tool.InputSchema)
|
||||
}
|
||||
allTools[canonicalKey] = entry
|
||||
}
|
||||
|
||||
// buildCrossServerRefs indexes reviewed cross-server mappings from the
|
||||
// previous snapshot: for every registry canonical whose interface_ref names a
|
||||
// different MCP identity (product_id.rpc_name != canonical), the live key is
|
||||
// mapped back to that canonical. One live tool may serve several canonicals,
|
||||
// so values are slices, sorted for deterministic merge order.
|
||||
func buildCrossServerRefs(prevTools map[string]map[string]any, registryMap map[string]map[string]string) map[string][]string {
|
||||
index := map[string][]string{}
|
||||
for canonical, entry := range prevTools {
|
||||
if _, inRegistry := registryMap[canonical]; !inRegistry {
|
||||
continue
|
||||
}
|
||||
ref, ok := entry["interface_ref"].(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
productID, _ := ref["product_id"].(string)
|
||||
rpcName, _ := ref["rpc_name"].(string)
|
||||
if productID == "" || rpcName == "" {
|
||||
continue
|
||||
}
|
||||
liveKey := productID + "." + rpcName
|
||||
if liveKey == canonical {
|
||||
continue
|
||||
}
|
||||
index[liveKey] = append(index[liveKey], canonical)
|
||||
}
|
||||
for _, canonicals := range index {
|
||||
sort.Strings(canonicals)
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
// loadRegistryInterfaceRefs loads the reviewed split CommandRegistry through
|
||||
// the cli package's reassembly API and builds a canonical_path →
|
||||
// {product_id, rpc_name} mapping for interface_ref injection.
|
||||
func loadRegistryInterfaceRefs(stderr io.Writer) map[string]map[string]string {
|
||||
data, err := registrySource()
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "fetch_mcp_metadata: warning: cannot load registry: %v\n", err)
|
||||
return map[string]map[string]string{}
|
||||
}
|
||||
var reg struct {
|
||||
Products []struct {
|
||||
ID string `json:"id"`
|
||||
Tools []struct {
|
||||
CanonicalPath string `json:"canonical_path"`
|
||||
} `json:"tools"`
|
||||
} `json:"products"`
|
||||
}
|
||||
if err := json.Unmarshal(data, ®); err != nil {
|
||||
// 与读文件失败同等告警:静默返回空映射会让所有 live tool 被丢弃、
|
||||
// 产出 stub-only 快照且零提示(P1#1 的故障模式)。
|
||||
fmt.Fprintf(stderr, "fetch_mcp_metadata: warning: cannot parse registry: %v\n", err)
|
||||
return map[string]map[string]string{}
|
||||
}
|
||||
out := make(map[string]map[string]string)
|
||||
for _, prod := range reg.Products {
|
||||
for _, tool := range prod.Tools {
|
||||
cp := strings.TrimSpace(tool.CanonicalPath)
|
||||
if cp == "" || !strings.Contains(cp, ".") {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(cp, ".", 2)
|
||||
out[cp] = map[string]string{
|
||||
"product_id": parts[0],
|
||||
"rpc_name": parts[1],
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// extractParams converts a JSON Schema inputSchema (from MCP tools/list) into
|
||||
// the flat param-name → metadata map used by schema_mcp_metadata.json.
|
||||
func extractParams(inputSchema map[string]any) map[string]map[string]any {
|
||||
if inputSchema == nil {
|
||||
return nil
|
||||
}
|
||||
properties, ok := inputSchema["properties"].(map[string]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
requiredSet := map[string]bool{}
|
||||
if req, ok := inputSchema["required"].([]any); ok {
|
||||
for _, r := range req {
|
||||
if s, ok := r.(string); ok {
|
||||
requiredSet[s] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
params := make(map[string]map[string]any, len(properties))
|
||||
for name, raw := range properties {
|
||||
prop, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
meta := map[string]any{}
|
||||
if t, ok := prop["type"].(string); ok {
|
||||
meta["type"] = t
|
||||
}
|
||||
if d, ok := prop["description"].(string); ok {
|
||||
meta["description"] = d
|
||||
}
|
||||
if d, ok := prop["default"].(string); ok {
|
||||
meta["default"] = d
|
||||
}
|
||||
if e, ok := prop["enum"].([]any); ok {
|
||||
enums := make([]string, 0, len(e))
|
||||
for _, v := range e {
|
||||
if s, ok := v.(string); ok {
|
||||
enums = append(enums, s)
|
||||
}
|
||||
}
|
||||
if len(enums) > 0 {
|
||||
meta["enum"] = enums
|
||||
}
|
||||
}
|
||||
meta["required"] = requiredSet[name]
|
||||
params[name] = meta
|
||||
}
|
||||
return params
|
||||
}
|
||||
@@ -0,0 +1,557 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/auth"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/syncdata"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/transport"
|
||||
)
|
||||
|
||||
func TestLoadRegistryInterfaceRefsUsesSplitRegistry(t *testing.T) {
|
||||
var stderr bytes.Buffer
|
||||
refs := loadRegistryInterfaceRefs(&stderr)
|
||||
if len(refs) == 0 {
|
||||
t.Fatal("loadRegistryInterfaceRefs() returned no reviewed commands")
|
||||
}
|
||||
|
||||
got, ok := refs["calendar.list_calendars"]
|
||||
if !ok {
|
||||
t.Fatal("calendar.list_calendars missing from reassembled split registry")
|
||||
}
|
||||
if got["product_id"] != "calendar" || got["rpc_name"] != "list_calendars" {
|
||||
t.Fatalf("calendar.list_calendars ref = %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCrossServerRefs(t *testing.T) {
|
||||
registryMap := map[string]map[string]string{
|
||||
"aitable.advperm_enable": {"product_id": "aitable", "rpc_name": "advperm_enable"},
|
||||
"aitable.advperm_disable": {"product_id": "aitable", "rpc_name": "advperm_disable"},
|
||||
"doc.copy_document": {"product_id": "doc", "rpc_name": "copy_document"},
|
||||
}
|
||||
prevTools := map[string]map[string]any{
|
||||
// Fan-out: two canonicals share one live tool; insertion order must
|
||||
// not affect the sorted result.
|
||||
"aitable.advperm_enable": {
|
||||
"interface_ref": map[string]any{"product_id": "aitable-helper", "rpc_name": "set_advanced_permission"},
|
||||
},
|
||||
"aitable.advperm_disable": {
|
||||
"interface_ref": map[string]any{"product_id": "aitable-helper", "rpc_name": "set_advanced_permission"},
|
||||
},
|
||||
// Identity ref (live key == canonical) needs no cross entry.
|
||||
"doc.copy_document": {
|
||||
"interface_ref": map[string]any{"product_id": "doc", "rpc_name": "copy_document"},
|
||||
},
|
||||
// Not in the registry: must be ignored.
|
||||
"ghost.tool": {
|
||||
"interface_ref": map[string]any{"product_id": "ghost-helper", "rpc_name": "haunt"},
|
||||
},
|
||||
}
|
||||
got := buildCrossServerRefs(prevTools, registryMap)
|
||||
want := map[string][]string{
|
||||
"aitable-helper.set_advanced_permission": {"aitable.advperm_disable", "aitable.advperm_enable"},
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("index = %#v, want %#v", got, want)
|
||||
}
|
||||
for k, v := range want {
|
||||
if gv := got[k]; len(gv) != len(v) || gv[0] != v[0] || gv[1] != v[1] {
|
||||
t.Fatalf("index[%q] = %v, want %v", k, gv, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCrossServerRefsSkipsMalformedRefs(t *testing.T) {
|
||||
registryMap := map[string]map[string]string{
|
||||
"a.x": {"product_id": "a", "rpc_name": "x"},
|
||||
"a.y": {"product_id": "a", "rpc_name": "y"},
|
||||
"a.z": {"product_id": "a", "rpc_name": "z"},
|
||||
}
|
||||
prevTools := map[string]map[string]any{
|
||||
"a.x": {"interface_ref": "not-a-map"},
|
||||
"a.y": {"interface_ref": map[string]any{"product_id": "", "rpc_name": "r"}},
|
||||
"a.z": {"title": "no ref at all"},
|
||||
}
|
||||
if got := buildCrossServerRefs(prevTools, registryMap); len(got) != 0 {
|
||||
t.Fatalf("index = %#v, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunRefreshesCrossServerTools(t *testing.T) {
|
||||
registry := func() ([]byte, error) {
|
||||
return []byte(`{"version":1,"products":[{"id":"aitable","tools":[{"canonical_path":"aitable.advperm_enable"},{"canonical_path":"aitable.advperm_disable"}]}]}`), nil
|
||||
}
|
||||
servers := []syncdata.ServerInfo{{ID: "aitable-helper", Endpoint: "https://helper.example"}}
|
||||
lister := &fakeLister{
|
||||
results: map[string]transport.ToolsListResult{
|
||||
"https://helper.example": {Tools: []transport.ToolDescriptor{
|
||||
{Name: "set_advanced_permission", Title: "live title", Description: "live desc"},
|
||||
}},
|
||||
},
|
||||
}
|
||||
stubDeps(t, "env-token", nil, servers, lister, registry)
|
||||
|
||||
output := filepath.Join(t.TempDir(), "snapshot.json")
|
||||
prev := `{"tools":{
|
||||
"aitable.advperm_enable":{"title":"stale","interface_ref":{"product_id":"aitable-helper","rpc_name":"set_advanced_permission"}},
|
||||
"aitable.advperm_disable":{"title":"stale","interface_ref":{"product_id":"aitable-helper","rpc_name":"set_advanced_permission"}}
|
||||
}}`
|
||||
if err := os.WriteFile(output, []byte(prev), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var stderr bytes.Buffer
|
||||
if code := run([]string{"--output", output}, &stderr); code != 0 {
|
||||
t.Fatalf("run() = %d, stderr=%s", code, stderr.String())
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "cross-server ref index: 1 live keys") {
|
||||
t.Fatalf("stderr = %q, want cross-server index log", stderr.String())
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(output)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var snapshot struct {
|
||||
Tools map[string]map[string]any `json:"tools"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &snapshot); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, canonical := range []string{"aitable.advperm_enable", "aitable.advperm_disable"} {
|
||||
entry := snapshot.Tools[canonical]
|
||||
if entry["title"] != "live title" || entry["description"] != "live desc" {
|
||||
t.Fatalf("%s = %#v, want live refresh", canonical, entry)
|
||||
}
|
||||
ref := entry["interface_ref"].(map[string]any)
|
||||
if ref["product_id"] != "aitable-helper" || ref["rpc_name"] != "set_advanced_permission" {
|
||||
t.Fatalf("%s reviewed ref lost: %#v", canonical, ref)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunCrossOwnedCanonicalIgnoresNameCoincidence:canonical 拥有评审过的
|
||||
// 跨 server 身份时,另一 server 上恰好同名的工具不得直连覆盖其元数据——
|
||||
// 数据源只能是评审身份指向的 live 工具。
|
||||
func TestRunCrossOwnedCanonicalIgnoresNameCoincidence(t *testing.T) {
|
||||
registry := func() ([]byte, error) {
|
||||
return []byte(`{"version":1,"products":[{"id":"aitable","tools":[{"canonical_path":"aitable.advperm_enable"}]}]}`), nil
|
||||
}
|
||||
servers := []syncdata.ServerInfo{
|
||||
{ID: "aitable", Endpoint: "https://aitable.example"},
|
||||
{ID: "aitable-helper", Endpoint: "https://helper.example"},
|
||||
}
|
||||
lister := &fakeLister{
|
||||
results: map[string]transport.ToolsListResult{
|
||||
// 同名巧合:aitable server 上恰好也有 advperm_enable。
|
||||
"https://aitable.example": {Tools: []transport.ToolDescriptor{
|
||||
{Name: "advperm_enable", Title: "coincidence title", Description: "coincidence desc"},
|
||||
}},
|
||||
"https://helper.example": {Tools: []transport.ToolDescriptor{
|
||||
{Name: "set_advanced_permission", Title: "owner title", Description: "owner desc"},
|
||||
}},
|
||||
},
|
||||
}
|
||||
stubDeps(t, "env-token", nil, servers, lister, registry)
|
||||
|
||||
output := filepath.Join(t.TempDir(), "snapshot.json")
|
||||
prev := `{"tools":{"aitable.advperm_enable":{"title":"stale","interface_ref":{"product_id":"aitable-helper","rpc_name":"set_advanced_permission"}}}}`
|
||||
if err := os.WriteFile(output, []byte(prev), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var stderr bytes.Buffer
|
||||
if code := run([]string{"--output", output}, &stderr); code != 0 {
|
||||
t.Fatalf("run() = %d, stderr=%s", code, stderr.String())
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(output)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var snapshot struct {
|
||||
Tools map[string]map[string]any `json:"tools"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &snapshot); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
entry := snapshot.Tools["aitable.advperm_enable"]
|
||||
if entry["title"] != "owner title" || entry["description"] != "owner desc" {
|
||||
t.Fatalf("entry = %#v, want reviewed-identity source to win over name coincidence", entry)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeLiveMCPToolRefreshesExistingMetadata(t *testing.T) {
|
||||
const canonical = "calendar.list_calendars"
|
||||
reviewedRef := map[string]any{
|
||||
"product_id": "calendar-helper",
|
||||
"rpc_name": "list_user_calendars",
|
||||
}
|
||||
allTools := map[string]map[string]any{
|
||||
canonical: {
|
||||
"title": "old title",
|
||||
"description": "old description",
|
||||
"interface_ref": reviewedRef,
|
||||
"parameters": map[string]any{
|
||||
"stale": map[string]any{"type": "string"},
|
||||
},
|
||||
},
|
||||
}
|
||||
live := transport.ToolDescriptor{
|
||||
Name: "list_calendars",
|
||||
Title: "new title",
|
||||
Description: "new description",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"cursor": map[string]any{
|
||||
"type": "string",
|
||||
"description": "next page cursor",
|
||||
},
|
||||
},
|
||||
"required": []any{"cursor"},
|
||||
},
|
||||
}
|
||||
fallbackRef := map[string]string{
|
||||
"product_id": "calendar",
|
||||
"rpc_name": "list_calendars",
|
||||
}
|
||||
|
||||
mergeLiveMCPTool(allTools, canonical, live, fallbackRef)
|
||||
|
||||
got := allTools[canonical]
|
||||
if got["title"] != "new title" || got["description"] != "new description" {
|
||||
t.Fatalf("live metadata was not refreshed: %#v", got)
|
||||
}
|
||||
if !reflect.DeepEqual(got["interface_ref"], reviewedRef) {
|
||||
t.Fatalf("interface_ref = %#v, want reviewed mapping %#v", got["interface_ref"], reviewedRef)
|
||||
}
|
||||
params, ok := got["parameters"].(map[string]map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("parameters type = %T, want refreshed parameter map", got["parameters"])
|
||||
}
|
||||
if _, stale := params["stale"]; stale {
|
||||
t.Fatalf("stale parameter survived refresh: %#v", params)
|
||||
}
|
||||
if cursor := params["cursor"]; cursor["type"] != "string" || cursor["description"] != "next page cursor" || cursor["required"] != true {
|
||||
t.Fatalf("cursor parameter = %#v", cursor)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCoverageReportsFailedServices(t *testing.T) {
|
||||
got := buildCoverage(26, []string{"doc", "sheet"}, 800, 813, 40)
|
||||
if got["source_services"] != 26 {
|
||||
t.Fatalf("source_services = %v, want 26", got["source_services"])
|
||||
}
|
||||
if got["snapshot_services"] != 24 {
|
||||
t.Fatalf("snapshot_services = %v, want 24 (26 sources - 2 failures)", got["snapshot_services"])
|
||||
}
|
||||
if !reflect.DeepEqual(got["missing_services"], []string{"doc", "sheet"}) {
|
||||
t.Fatalf("missing_services = %#v, want failed service IDs", got["missing_services"])
|
||||
}
|
||||
// matched 必须剔除 stub 占位,unmatched 据实等于 stub 数。
|
||||
if got["matched_tools"] != 773 || got["unmatched_tools"] != 40 {
|
||||
t.Fatalf("matched/unmatched = %v/%v, want 773/40 (813 surface - 40 stubs)", got["matched_tools"], got["unmatched_tools"])
|
||||
}
|
||||
if got["source_tools"] != 800 || got["surface_tools"] != 813 {
|
||||
t.Fatalf("tool counts = %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCoverageFullSnapshotHasNoMissingServices(t *testing.T) {
|
||||
got := buildCoverage(26, nil, 813, 813, 0)
|
||||
if got["snapshot_services"] != 26 {
|
||||
t.Fatalf("snapshot_services = %v, want 26", got["snapshot_services"])
|
||||
}
|
||||
if !reflect.DeepEqual(got["missing_services"], []string{}) {
|
||||
t.Fatalf("missing_services = %#v, want empty non-nil slice", got["missing_services"])
|
||||
}
|
||||
if got["matched_tools"] != 813 || got["unmatched_tools"] != 0 {
|
||||
t.Fatalf("matched/unmatched = %v/%v, want 813/0 for stub-free snapshot", got["matched_tools"], got["unmatched_tools"])
|
||||
}
|
||||
}
|
||||
|
||||
// fakeLister returns canned tools/list results per endpoint.
|
||||
type fakeLister struct {
|
||||
results map[string]transport.ToolsListResult
|
||||
errs map[string]error
|
||||
}
|
||||
|
||||
func (f *fakeLister) ListTools(_ context.Context, endpoint string) (transport.ToolsListResult, error) {
|
||||
if err := f.errs[endpoint]; err != nil {
|
||||
return transport.ToolsListResult{}, err
|
||||
}
|
||||
return f.results[endpoint], nil
|
||||
}
|
||||
|
||||
// stubDeps swaps every injection point for the duration of one test.
|
||||
func stubDeps(t *testing.T, token string, keychain func() (*auth.TokenData, error), servers []syncdata.ServerInfo, lister toolLister, registry func() ([]byte, error)) {
|
||||
t.Helper()
|
||||
origGetenv, origLoad, origServers, origNew, origRegistry := getenv, loadTokenData, staticServers, newToolLister, registrySource
|
||||
t.Cleanup(func() {
|
||||
getenv, loadTokenData, staticServers, newToolLister, registrySource = origGetenv, origLoad, origServers, origNew, origRegistry
|
||||
})
|
||||
getenv = func(key string) string {
|
||||
if key == "DWS_ACCESS_TOKEN" {
|
||||
return token
|
||||
}
|
||||
return ""
|
||||
}
|
||||
loadTokenData = keychain
|
||||
staticServers = func() []syncdata.ServerInfo { return servers }
|
||||
newToolLister = func(string) toolLister { return lister }
|
||||
registrySource = registry
|
||||
}
|
||||
|
||||
func testRegistryJSON() ([]byte, error) {
|
||||
return []byte(`{"version":1,"products":[{"id":"doc","tools":[{"canonical_path":"doc.copy_document"},{"canonical_path":"doc.get_document"},{"canonical_path":"bad-entry"}]}]}`), nil
|
||||
}
|
||||
|
||||
func TestRunNoTokenFails(t *testing.T) {
|
||||
stubDeps(t, "", func() (*auth.TokenData, error) { return nil, errors.New("no keychain") }, nil, &fakeLister{}, testRegistryJSON)
|
||||
var stderr bytes.Buffer
|
||||
if code := run(nil, &stderr); code != 1 {
|
||||
t.Fatalf("run() = %d, want 1", code)
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "no auth token") {
|
||||
t.Fatalf("stderr = %q, want no-auth-token hint", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunInvalidFlagFails(t *testing.T) {
|
||||
stubDeps(t, "tok", nil, nil, &fakeLister{}, testRegistryJSON)
|
||||
var stderr bytes.Buffer
|
||||
if code := run([]string{"--nonexistent"}, &stderr); code != 2 {
|
||||
t.Fatalf("run() = %d, want 2", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveTokenKeychainFallback(t *testing.T) {
|
||||
stubDeps(t, "", func() (*auth.TokenData, error) {
|
||||
return &auth.TokenData{AccessToken: "kc-token"}, nil
|
||||
}, nil, &fakeLister{}, testRegistryJSON)
|
||||
var stderr bytes.Buffer
|
||||
if got := resolveToken(&stderr); got != "kc-token" {
|
||||
t.Fatalf("resolveToken() = %q, want kc-token", got)
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "loaded token from keychain") {
|
||||
t.Fatalf("stderr = %q, want keychain log", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveTokenEmptyKeychainToken(t *testing.T) {
|
||||
stubDeps(t, "", func() (*auth.TokenData, error) { return &auth.TokenData{}, nil }, nil, &fakeLister{}, testRegistryJSON)
|
||||
var stderr bytes.Buffer
|
||||
if got := resolveToken(&stderr); got != "" {
|
||||
t.Fatalf("resolveToken() = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWritesSnapshotWithHonestCoverage(t *testing.T) {
|
||||
servers := []syncdata.ServerInfo{
|
||||
{ID: "doc", Endpoint: "https://doc.example"},
|
||||
{ID: "sheet", Endpoint: "https://sheet.example"},
|
||||
{ID: "blank", Endpoint: " "},
|
||||
}
|
||||
lister := &fakeLister{
|
||||
results: map[string]transport.ToolsListResult{
|
||||
"https://doc.example": {Tools: []transport.ToolDescriptor{
|
||||
{Name: "copy_document", Title: "复制文档", Description: "copy", InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"doc_id": map[string]any{"type": "string", "description": "文档 ID", "default": "d", "enum": []any{"a", "b", 3}},
|
||||
"bogus": "not-a-map",
|
||||
},
|
||||
"required": []any{"doc_id", 42},
|
||||
}},
|
||||
{Name: " "},
|
||||
{Name: "not_in_registry"},
|
||||
}},
|
||||
},
|
||||
errs: map[string]error{"https://sheet.example": errors.New("boom")},
|
||||
}
|
||||
stubDeps(t, "env-token", nil, servers, lister, testRegistryJSON)
|
||||
|
||||
dir := t.TempDir()
|
||||
output := filepath.Join(dir, "snapshot.json")
|
||||
prev := `{"tools":{"doc.get_document":{"interface_ref":{"product_id":"doc-helper","rpc_name":"fetch_document"}}}}`
|
||||
if err := os.WriteFile(output, []byte(prev), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var stderr bytes.Buffer
|
||||
if code := run([]string{"--output", output}, &stderr); code != 0 {
|
||||
t.Fatalf("run() = %d, stderr=%s", code, stderr.String())
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(output)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var snapshot struct {
|
||||
Version int `json:"version"`
|
||||
Coverage map[string]any `json:"coverage"`
|
||||
Tools map[string]map[string]any
|
||||
}
|
||||
if err := json.Unmarshal(data, &snapshot); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if snapshot.Version != 1 {
|
||||
t.Fatalf("version = %d", snapshot.Version)
|
||||
}
|
||||
if got := snapshot.Coverage["snapshot_services"].(float64); got != 2 {
|
||||
t.Fatalf("snapshot_services = %v, want 2 (3 servers - 1 failed; blank endpoint not counted as failed)", got)
|
||||
}
|
||||
if got := snapshot.Coverage["missing_services"].([]any); len(got) != 1 || got[0] != "sheet" {
|
||||
t.Fatalf("missing_services = %v, want [sheet]", got)
|
||||
}
|
||||
live := snapshot.Tools["doc.copy_document"]
|
||||
if live == nil || live["title"] != "复制文档" {
|
||||
t.Fatalf("doc.copy_document = %#v, want live metadata", live)
|
||||
}
|
||||
params := live["parameters"].(map[string]any)
|
||||
docID := params["doc_id"].(map[string]any)
|
||||
if docID["type"] != "string" || docID["required"] != true || docID["default"] != "d" {
|
||||
t.Fatalf("doc_id = %#v", docID)
|
||||
}
|
||||
if enum := docID["enum"].([]any); len(enum) != 2 {
|
||||
t.Fatalf("enum = %v, want the 2 string members only", enum)
|
||||
}
|
||||
if _, ok := params["bogus"]; ok {
|
||||
t.Fatal("non-map property should be skipped")
|
||||
}
|
||||
prevRef := snapshot.Tools["doc.get_document"]["interface_ref"].(map[string]any)
|
||||
if prevRef["product_id"] != "doc-helper" {
|
||||
t.Fatalf("previous reviewed ref lost: %#v", prevRef)
|
||||
}
|
||||
if _, ok := snapshot.Tools["not_in_registry"]; ok {
|
||||
t.Fatal("tools outside the registry must be dropped")
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "services unreachable: sheet") {
|
||||
t.Fatalf("stderr = %q, want unreachable log", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunIgnoresCorruptPreviousSnapshot(t *testing.T) {
|
||||
stubDeps(t, "env-token", nil, nil, &fakeLister{}, testRegistryJSON)
|
||||
output := filepath.Join(t.TempDir(), "snapshot.json")
|
||||
if err := os.WriteFile(output, []byte("{corrupt"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var stderr bytes.Buffer
|
||||
if code := run([]string{"--output", output}, &stderr); code != 0 {
|
||||
t.Fatalf("run() = %d, stderr=%s", code, stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunRegistryLoadFailureStillWritesStublessSnapshot(t *testing.T) {
|
||||
stubDeps(t, "env-token", nil, nil, &fakeLister{}, func() ([]byte, error) { return nil, errors.New("no registry") })
|
||||
output := filepath.Join(t.TempDir(), "snapshot.json")
|
||||
var stderr bytes.Buffer
|
||||
if code := run([]string{"--output", output}, &stderr); code != 0 {
|
||||
t.Fatalf("run() = %d, stderr=%s", code, stderr.String())
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "cannot load registry") {
|
||||
t.Fatalf("stderr = %q, want registry warning", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunUnparsableRegistryYieldsNoRefs(t *testing.T) {
|
||||
var stderr bytes.Buffer
|
||||
stubDeps(t, "env-token", nil, nil, &fakeLister{}, func() ([]byte, error) { return []byte("{bad"), nil })
|
||||
if refs := loadRegistryInterfaceRefs(&stderr); len(refs) != 0 {
|
||||
t.Fatalf("refs = %v, want empty for unparsable registry", refs)
|
||||
}
|
||||
// 解析失败必须有告警,不得静默产出空映射。
|
||||
if !strings.Contains(stderr.String(), "cannot parse registry") {
|
||||
t.Fatalf("stderr = %q, want parse warning", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWriteFailure(t *testing.T) {
|
||||
stubDeps(t, "env-token", nil, nil, &fakeLister{}, testRegistryJSON)
|
||||
var stderr bytes.Buffer
|
||||
badPath := filepath.Join(t.TempDir(), "missing-dir", "snapshot.json")
|
||||
if code := run([]string{"--output", badPath}, &stderr); code != 1 {
|
||||
t.Fatalf("run() = %d, want 1 on write failure", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteMetadataMarshalFailure(t *testing.T) {
|
||||
err := writeMetadata(filepath.Join(t.TempDir(), "out.json"), map[string]any{"bad": math.NaN()})
|
||||
if err == nil || !strings.Contains(err.Error(), "marshal failed") {
|
||||
t.Fatalf("err = %v, want marshal failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMainDelegatesToRun(t *testing.T) {
|
||||
stubDeps(t, "env-token", nil, nil, &fakeLister{}, testRegistryJSON)
|
||||
origExit, origArgs := osExit, os.Args
|
||||
t.Cleanup(func() { osExit, os.Args = origExit, origArgs })
|
||||
exitCode := -1
|
||||
osExit = func(code int) { exitCode = code }
|
||||
os.Args = []string{"fetch_mcp_metadata", "--output", filepath.Join(t.TempDir(), "snapshot.json")}
|
||||
main()
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("main() exited with %d, want 0", exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractParamsNilAndNonObjectSchemas(t *testing.T) {
|
||||
if got := extractParams(nil); got != nil {
|
||||
t.Fatalf("extractParams(nil) = %v, want nil", got)
|
||||
}
|
||||
if got := extractParams(map[string]any{"type": "object"}); got != nil {
|
||||
t.Fatalf("extractParams(no properties) = %v, want nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewToolListerBuildsAuthedClient(t *testing.T) {
|
||||
if lister := newToolLister("tok"); lister == nil {
|
||||
t.Fatal("newToolLister returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunRecordsSourceRevision(t *testing.T) {
|
||||
stubDeps(t, "env-token", nil, nil, &fakeLister{}, testRegistryJSON)
|
||||
dir := t.TempDir()
|
||||
head := filepath.Join(dir, "HEAD")
|
||||
if err := os.WriteFile(head, []byte("ref: refs/heads/feature\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
origHead := gitHeadPath
|
||||
t.Cleanup(func() { gitHeadPath = origHead })
|
||||
gitHeadPath = head
|
||||
|
||||
output := filepath.Join(dir, "snapshot.json")
|
||||
var stderr bytes.Buffer
|
||||
if code := run([]string{"--output", output}, &stderr); code != 0 {
|
||||
t.Fatalf("run() = %d, stderr=%s", code, stderr.String())
|
||||
}
|
||||
data, err := os.ReadFile(output)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var snapshot struct {
|
||||
SourceRevision string `json:"source_revision"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &snapshot); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if snapshot.SourceRevision != "ref: refs/heads/feature" {
|
||||
t.Fatalf("source_revision = %q", snapshot.SourceRevision)
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ DWS Schema 是当前二进制公开 CLI 的版本化 Agent 执行契约。它描
|
||||
|
||||
1. **Schema 描述 CLI,不制造 CLI。** `CommandRegistry`、manual hint、metadata 和 Catalog 都不能凭空创建 Cobra 命令或 flag;registry 中的每个路径都必须精确绑定真实 runnable Cobra leaf。
|
||||
2. **所有来源只解析一次。** 来源经过统一 resolver 进入 typed `SchemaRegistry`,所有查询、导出和门禁都消费同一个 `SchemaRegistry/SchemaIndex`。
|
||||
3. **Registry-first,Catalog 只出不进。** reviewed `CommandRegistry` 是稳定 command identity/navigation 的唯一事实源;`schema_catalog.json` 和其他生成 JSON 只是下游发布物,不能成为命令、metadata 或下一轮 Catalog 的来源。运行时 production loader 解码 embedded snapshot 只是交付边界,不是 source resolution。
|
||||
3. **Registry-first,Catalog 只出不进。** reviewed `CommandRegistry` 是稳定 command identity/navigation 的唯一事实源;`schema_catalog/`(`catalog.json` + 每产品 `tools/<product>.json`)和其他生成 JSON 只是下游发布物,不能成为命令、metadata 或下一轮 Catalog 的来源。运行时 production loader 解码 embedded snapshot 只是交付边界,不是 source resolution。
|
||||
|
||||
Schema 不调用 MCP `tools/list`,不访问网络,也不读取用户本地 discovery cache。
|
||||
|
||||
@@ -63,8 +63,9 @@ live Cobra flag facts / typed parameter metadata
|
||||
build-time typed gates snapshot serializer
|
||||
|
|
||||
v
|
||||
schema_catalog.json
|
||||
(release output only)
|
||||
schema_catalog/
|
||||
(catalog.json + tools/<product>.json,
|
||||
release output only)
|
||||
|
|
||||
v
|
||||
go:embed -> typed loader
|
||||
@@ -130,7 +131,7 @@ DWS 当前对外仍保留兼容 wire:leaf 使用 flat `parameters`,安全和
|
||||
| `schema_mcp_metadata.json` | pinned RPC identity、接口描述和脱敏参数事实 | CLI identity、运行时路由、risk 推断 |
|
||||
| `schema_hints/selection/*.json` | reviewed selection prose(summary / use_when / avoid_when / examples) | 创建 Cobra 命令或参数、改写 safety |
|
||||
| Skills/Markdown | 产品路由、工作流和使用建议 | 命令存在性和 flag 事实 |
|
||||
| `schema_catalog.json` 及其他 generated JSON | resolved registry 的兼容发布序列化;运行时由 production loader 解回 typed registry/index | generation/source resolution 输入、identity fallback、手工修复源 |
|
||||
| `schema_catalog/`(catalog.json + tools/<product>.json)及其他 generated JSON | resolved registry 的兼容发布序列化;运行时由 production loader 解回 typed registry/index | generation/source resolution 输入、identity fallback、手工修复源 |
|
||||
|
||||
`schema_command_registry.json` 承载 reviewed `CommandRegistry`。Manual command addition 先以确定性规则合并进 effective registry;从 binder 开始,下游只看到一个稳定 identity/navigation 模型。旧 wire 中的 `surface_hash` / `surface_tools` 字段仅为兼容名称,语义已经是 effective Registry hash/coverage,不构成第二事实源。
|
||||
|
||||
@@ -272,7 +273,8 @@ dws schema --all # 所有工具的完整 leaf 导
|
||||
- `internal/cli/schema_agent_metadata/index.json`
|
||||
- `internal/cli/schema_agent_metadata/<product>.json`
|
||||
- `internal/cli/schema_agent_metadata_audit.json`
|
||||
- `internal/cli/schema_catalog.json`
|
||||
- `internal/cli/schema_catalog/catalog.json`(全局信封 + Catalog)
|
||||
- `internal/cli/schema_catalog/tools/<product>.json`(每产品 leaf ToolSpecs,按产品分片以免并发 PR 冲突)
|
||||
|
||||
只编辑来源;不要手工编辑 Agent metadata 或 Catalog 输出。
|
||||
|
||||
@@ -303,7 +305,7 @@ go test ./internal/cli ./internal/app ./internal/generator/... -count=1
|
||||
## 10. 明确禁止
|
||||
|
||||
- 运行时调用 MCP `tools/list` 或访问网络生成 Schema。
|
||||
- 从旧 `schema_catalog.json` 或其他 generated JSON 反向创建/补齐 Cobra leaf、flag、CommandRegistry 或下一轮 Catalog。
|
||||
- 从 `schema_catalog/` 等生成 JSON 反向创建/补齐 Cobra leaf、flag、CommandRegistry 或下一轮 Catalog。
|
||||
- 把 native annotation、legacy registry 或 Catalog 当作 identity fallback;或在 `EffectiveCommandRegistry` 之后再次选择 identity winner。
|
||||
- renderer、query 或 gate 在 `SchemaRegistry` 之后重新读取 source 并做第二次 merge。
|
||||
- 用 prefix/wildcard exclusion 隐藏未来命令。
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
||||
)
|
||||
|
||||
func TestToolCallerAdapterDryRunNeverInvokesRunner(t *testing.T) {
|
||||
@@ -36,6 +37,66 @@ func TestToolCallerAdapterDryRunNeverInvokesRunner(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestToolCallerAdapterDryRunAllowsOnlyExplicitReadCapability(t *testing.T) {
|
||||
runner := &readOnlyDryRunRunner{}
|
||||
caller := newToolCallerAdapter(runner, &GlobalFlags{DryRun: true, Format: "json"})
|
||||
|
||||
result, err := caller.(edition.ReadToolCaller).CallReadTool(
|
||||
context.Background(),
|
||||
"im",
|
||||
"search_groups",
|
||||
map[string]any{"keyword": "project"},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("CallReadTool() error = %v", err)
|
||||
}
|
||||
if got := runner.readCalls.Load(); got != 1 {
|
||||
t.Fatalf("read calls = %d, want 1", got)
|
||||
}
|
||||
if got := runner.regularCalls.Load(); got != 0 {
|
||||
t.Fatalf("regular calls = %d, want 0", got)
|
||||
}
|
||||
if runner.invocation.DryRun {
|
||||
t.Fatal("read-only invocation was left in dry-run mode")
|
||||
}
|
||||
if result == nil || len(result.Content) != 1 || !strings.Contains(result.Content[0].Text, `"read":true`) {
|
||||
t.Fatalf("read result = %#v", result)
|
||||
}
|
||||
|
||||
failClosed := newToolCallerAdapter(&countingErrorRunner{}, &GlobalFlags{DryRun: true})
|
||||
if _, err := failClosed.(edition.ReadToolCaller).CallReadTool(
|
||||
context.Background(), "im", "search_groups", nil,
|
||||
); err == nil {
|
||||
t.Fatal("runner without read-only capability was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageReadOnlyGuardErrorPaths(t *testing.T) {
|
||||
var nilAdapter *toolCallerAdapter
|
||||
if _, err := nilAdapter.CallReadTool(context.Background(), "im", "search_groups", nil); err == nil {
|
||||
t.Fatal("nil adapter accepted a read-only call")
|
||||
}
|
||||
|
||||
regularRunner := &capturingSuccessRunner{}
|
||||
regular := newToolCallerAdapter(regularRunner, &GlobalFlags{DryRun: false, Format: "json"})
|
||||
if _, err := regular.(edition.ReadToolCaller).CallReadTool(context.Background(), "im", "search_groups", nil); err != nil {
|
||||
t.Fatalf("non-dry read should use the regular runner: %v", err)
|
||||
}
|
||||
if got := regularRunner.calls.Load(); got != 1 {
|
||||
t.Fatalf("regular runner calls = %d, want 1", got)
|
||||
}
|
||||
|
||||
readFailure := newToolCallerAdapter(&failingReadOnlyRunner{}, &GlobalFlags{DryRun: true, Format: "json"})
|
||||
if _, err := readFailure.(edition.ReadToolCaller).CallReadTool(context.Background(), "im", "search_groups", nil); err == nil {
|
||||
t.Fatal("read-only runner error was swallowed")
|
||||
}
|
||||
|
||||
var nilRuntime *runtimeRunner
|
||||
if _, err := nilRuntime.RunReadOnly(context.Background(), executor.Invocation{}); err == nil {
|
||||
t.Fatal("nil runtime runner accepted a read-only call")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeRunnerGlobalDryRunStopsBeforeInjectedFallback(t *testing.T) {
|
||||
fallback := &countingErrorRunner{}
|
||||
runner := &runtimeRunner{globalFlags: &GlobalFlags{DryRun: true}, fallback: fallback}
|
||||
@@ -56,6 +117,38 @@ func TestRuntimeRunnerGlobalDryRunStopsBeforeInjectedFallback(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeRunnerReadOnlyClonePreservesGlobalDryRunBarrier(t *testing.T) {
|
||||
fallback := &capturingSuccessRunner{}
|
||||
flags := &GlobalFlags{DryRun: true}
|
||||
runner := &runtimeRunner{globalFlags: flags, fallback: fallback}
|
||||
invocation := executor.NewHelperInvocation(
|
||||
"test",
|
||||
"im",
|
||||
"search_groups",
|
||||
map[string]any{"keyword": "project"},
|
||||
)
|
||||
|
||||
if _, err := runner.RunReadOnly(context.Background(), invocation); err != nil {
|
||||
t.Fatalf("RunReadOnly() error = %v", err)
|
||||
}
|
||||
if got := fallback.calls.Load(); got != 1 {
|
||||
t.Fatalf("fallback calls = %d, want 1", got)
|
||||
}
|
||||
if fallback.invocation.DryRun {
|
||||
t.Fatal("read-only fallback invocation was left in dry-run mode")
|
||||
}
|
||||
if !flags.DryRun {
|
||||
t.Fatal("RunReadOnly mutated the process-wide dry-run flag")
|
||||
}
|
||||
|
||||
if _, err := runner.Run(context.Background(), invocation); err != nil {
|
||||
t.Fatalf("ordinary Run() error = %v", err)
|
||||
}
|
||||
if got := fallback.calls.Load(); got != 1 {
|
||||
t.Fatalf("ordinary dry-run reached fallback; calls = %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
type countingErrorRunner struct {
|
||||
calls atomic.Int64
|
||||
}
|
||||
@@ -64,3 +157,47 @@ func (r *countingErrorRunner) Run(context.Context, executor.Invocation) (executo
|
||||
r.calls.Add(1)
|
||||
return executor.Result{}, errors.New("runner must not be called")
|
||||
}
|
||||
|
||||
type readOnlyDryRunRunner struct {
|
||||
regularCalls atomic.Int64
|
||||
readCalls atomic.Int64
|
||||
invocation executor.Invocation
|
||||
}
|
||||
|
||||
func (r *readOnlyDryRunRunner) Run(context.Context, executor.Invocation) (executor.Result, error) {
|
||||
r.regularCalls.Add(1)
|
||||
return executor.Result{}, errors.New("regular runner must not be called")
|
||||
}
|
||||
|
||||
func (r *readOnlyDryRunRunner) RunReadOnly(_ context.Context, invocation executor.Invocation) (executor.Result, error) {
|
||||
r.readCalls.Add(1)
|
||||
r.invocation = invocation
|
||||
return executor.Result{
|
||||
Invocation: invocation,
|
||||
Response: map[string]any{"read": true},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type capturingSuccessRunner struct {
|
||||
calls atomic.Int64
|
||||
invocation executor.Invocation
|
||||
}
|
||||
|
||||
func (r *capturingSuccessRunner) Run(_ context.Context, invocation executor.Invocation) (executor.Result, error) {
|
||||
r.calls.Add(1)
|
||||
r.invocation = invocation
|
||||
return executor.Result{
|
||||
Invocation: invocation,
|
||||
Response: map[string]any{"read": true},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type failingReadOnlyRunner struct{}
|
||||
|
||||
func (*failingReadOnlyRunner) Run(context.Context, executor.Invocation) (executor.Result, error) {
|
||||
return executor.Result{}, errors.New("regular runner must not be called")
|
||||
}
|
||||
|
||||
func (*failingReadOnlyRunner) RunReadOnly(context.Context, executor.Invocation) (executor.Result, error) {
|
||||
return executor.Result{}, errors.New("read failed")
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/i18n"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/tui"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
||||
@@ -41,6 +42,7 @@ func configureRootHelp(root *cobra.Command) {
|
||||
root.SetHelpFunc(func(cmd *cobra.Command, args []string) {
|
||||
if cmd != root {
|
||||
defaultHelpFunc(cmd, args)
|
||||
cli.RenderSafetyAnnotation(cmd)
|
||||
return
|
||||
}
|
||||
renderRootHelp(root)
|
||||
|
||||
@@ -380,7 +380,7 @@ func TestRootKeepsSVIPChatCompatibilityFlags(t *testing.T) {
|
||||
}
|
||||
|
||||
searchAdvanced := mustFindCommand(t, root, "chat", "message", "search-advanced")
|
||||
for _, flag := range []string{"sender", "senders", "sender-ids"} {
|
||||
for _, flag := range []string{"sender", "senders", "sender-ids", "message-type", "only-robot", "conversation-type"} {
|
||||
if searchAdvanced.Flags().Lookup(flag) == nil {
|
||||
t.Fatalf("chat message search-advanced missing --%s", flag)
|
||||
}
|
||||
|
||||
@@ -224,6 +224,24 @@ func (r *runtimeRunner) Run(ctx context.Context, invocation executor.Invocation)
|
||||
return r.runSingle(ctx, invocation, true)
|
||||
}
|
||||
|
||||
// RunReadOnly executes one already-classified read lookup for a semantic
|
||||
// Shortcut that is building a dry-run plan. It clones the runtime flags and
|
||||
// clears DryRun only on that clone: the process-wide caller and every ordinary
|
||||
// ToolCaller invocation retain the global execution barrier.
|
||||
func (r *runtimeRunner) RunReadOnly(ctx context.Context, invocation executor.Invocation) (executor.Result, error) {
|
||||
if r == nil {
|
||||
return executor.Result{}, fmt.Errorf("runtime runner is not configured")
|
||||
}
|
||||
clone := *r
|
||||
if r.globalFlags != nil {
|
||||
flags := *r.globalFlags
|
||||
flags.DryRun = false
|
||||
clone.globalFlags = &flags
|
||||
}
|
||||
invocation.DryRun = false
|
||||
return clone.Run(ctx, invocation)
|
||||
}
|
||||
|
||||
func (r *runtimeRunner) runSingle(ctx context.Context, invocation executor.Invocation, prefetchToken bool) (executor.Result, error) {
|
||||
if r.loader == nil || r.transport == nil {
|
||||
return r.fallback.Run(ctx, invocation)
|
||||
|
||||
@@ -11,22 +11,26 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
|
||||
)
|
||||
|
||||
const publicShortcutSchemaCount = 210
|
||||
const (
|
||||
publicShortcutCount = 265
|
||||
schemaPublishedShortcutCount = 210
|
||||
)
|
||||
|
||||
func TestEmbeddedSchemaPublishesEveryPublicShortcutContract(t *testing.T) {
|
||||
func TestEmbeddedSchemaCoversOrExactlyExcludesEveryPublicShortcutContract(t *testing.T) {
|
||||
tools := embeddedSchemaAllToolsForHelpFlagTest(t, NewRootCommand())
|
||||
public := make([]shortcut.Shortcut, 0, publicShortcutSchemaCount)
|
||||
public := make([]shortcut.Shortcut, 0, publicShortcutCount)
|
||||
for _, candidate := range shortcut.All() {
|
||||
if candidate.UserDefined || !shortcut.InPublicCatalog(candidate.Service, candidate.Command) {
|
||||
continue
|
||||
}
|
||||
public = append(public, candidate)
|
||||
}
|
||||
if got := len(public); got != publicShortcutSchemaCount {
|
||||
t.Fatalf("public built-in shortcuts = %d, want %d", got, publicShortcutSchemaCount)
|
||||
if got := len(public); got != publicShortcutCount {
|
||||
t.Fatalf("public built-in shortcuts = %d, want %d", got, publicShortcutCount)
|
||||
}
|
||||
|
||||
deliveredShortcuts := 0
|
||||
@@ -35,17 +39,35 @@ func TestEmbeddedSchemaPublishesEveryPublicShortcutContract(t *testing.T) {
|
||||
deliveredShortcuts++
|
||||
}
|
||||
}
|
||||
if deliveredShortcuts != publicShortcutSchemaCount {
|
||||
t.Fatalf("embedded schema --all shortcut tools = %d, want %d", deliveredShortcuts, publicShortcutSchemaCount)
|
||||
if deliveredShortcuts != schemaPublishedShortcutCount {
|
||||
t.Fatalf("embedded schema --all shortcut tools = %d, want %d", deliveredShortcuts, schemaPublishedShortcutCount)
|
||||
}
|
||||
|
||||
exclusions, err := cli.EmbeddedRuntimeSchemaExclusions()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
excludedPaths := make(map[string]bool, len(exclusions))
|
||||
for _, exclusion := range exclusions {
|
||||
if !exclusion.Reviewed || strings.TrimSpace(exclusion.Reason) == "" {
|
||||
t.Fatalf("unreviewed public command exclusion: %#v", exclusion)
|
||||
}
|
||||
excludedPaths[exclusion.CLIPath] = true
|
||||
}
|
||||
|
||||
excludedShortcuts := 0
|
||||
for _, declared := range public {
|
||||
declared := declared
|
||||
t.Run(declared.Service+"/"+strings.TrimPrefix(declared.Command, "+"), func(t *testing.T) {
|
||||
canonical := shortcutSchemaCanonical(declared)
|
||||
tool := tools[canonical]
|
||||
if tool == nil {
|
||||
t.Fatalf("embedded schema --all is missing %s (%s %s)", canonical, declared.Service, declared.Command)
|
||||
cliPath := declared.Service + " " + declared.Command
|
||||
if !excludedPaths[cliPath] {
|
||||
t.Fatalf("embedded schema --all is missing %s (%s) without an exact reviewed exclusion", canonical, cliPath)
|
||||
}
|
||||
excludedShortcuts++
|
||||
return
|
||||
}
|
||||
assertEmbeddedShortcutIdentityAndSelection(t, tool, declared, canonical)
|
||||
assertEmbeddedShortcutSafetyAndInterface(t, tool, declared, canonical)
|
||||
@@ -53,6 +75,9 @@ func TestEmbeddedSchemaPublishesEveryPublicShortcutContract(t *testing.T) {
|
||||
assertEmbeddedShortcutConstraints(t, tool, declared, canonical)
|
||||
})
|
||||
}
|
||||
if got, want := excludedShortcuts, publicShortcutCount-schemaPublishedShortcutCount; got != want {
|
||||
t.Fatalf("exactly excluded public shortcuts = %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmbeddedShortcutProgressiveQueriesReturnCompleteContracts(t *testing.T) {
|
||||
@@ -328,9 +353,9 @@ func assertEmbeddedShortcutConstraints(
|
||||
case shortcut.ConstraintCustom:
|
||||
for _, flagName := range flags {
|
||||
description := schemaContractString(schemaContractMap(tool["parameters"])[flagName]["description"])
|
||||
for _, requiredText := range []string{"原文不能为空", "不能重复"} {
|
||||
if !strings.Contains(description, requiredText) {
|
||||
t.Errorf("%s --%s description does not publish custom constraint %q: %q", canonical, flagName, requiredText, description)
|
||||
for _, evidence := range shortcutCustomConstraintEvidence(constraint.Description) {
|
||||
if !strings.Contains(description, evidence) {
|
||||
t.Errorf("%s --%s description does not publish custom constraint evidence %q: %q", canonical, flagName, evidence, description)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -349,6 +374,33 @@ func assertEmbeddedShortcutConstraints(
|
||||
}
|
||||
}
|
||||
|
||||
func shortcutCustomConstraintEvidence(description string) []string {
|
||||
// Custom constraints are prose rather than a typed wire contract. Require
|
||||
// their decision-relevant facts to survive in the delivered parameter
|
||||
// description while allowing the renderer to reorder connective wording.
|
||||
probes := []string{
|
||||
"原文=>替换",
|
||||
"不能为空",
|
||||
"不能重复",
|
||||
"大于 0",
|
||||
"工作目录",
|
||||
"相对路径",
|
||||
"绝对路径",
|
||||
"..",
|
||||
"最多 15 个字符",
|
||||
}
|
||||
evidence := make([]string, 0, len(probes))
|
||||
for _, probe := range probes {
|
||||
if strings.Contains(description, probe) {
|
||||
evidence = append(evidence, probe)
|
||||
}
|
||||
}
|
||||
if len(evidence) > 0 {
|
||||
return evidence
|
||||
}
|
||||
return []string{strings.TrimSpace(description)}
|
||||
}
|
||||
|
||||
func mustShortcutJSON(value any) string {
|
||||
encoded, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
|
||||
@@ -66,6 +66,33 @@ func (a *toolCallerAdapter) CallTool(ctx context.Context, productID, toolName st
|
||||
return convertResult(result), nil
|
||||
}
|
||||
|
||||
type dryRunReadRunner interface {
|
||||
RunReadOnly(context.Context, executor.Invocation) (executor.Result, error)
|
||||
}
|
||||
|
||||
// CallReadTool executes a Shortcut's explicitly classified read lookup while
|
||||
// the outer command is in dry-run mode. Ordinary CallTool remains protected by
|
||||
// the global execution barrier. The runner capability is optional and fails
|
||||
// closed so an injected runner cannot accidentally receive a real call.
|
||||
func (a *toolCallerAdapter) CallReadTool(ctx context.Context, productID, toolName string, args map[string]any) (*edition.ToolResult, error) {
|
||||
if a == nil || a.runner == nil {
|
||||
return nil, fmt.Errorf("ToolCaller runner is not configured")
|
||||
}
|
||||
if !a.DryRun() {
|
||||
return a.CallTool(ctx, productID, toolName, args)
|
||||
}
|
||||
readRunner, ok := a.runner.(dryRunReadRunner)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("ToolCaller runner does not support read-only dry-run lookups")
|
||||
}
|
||||
inv := executor.NewHelperInvocation("overlay."+productID+"."+toolName, productID, toolName, args)
|
||||
result, err := readRunner.RunReadOnly(ctx, inv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertResult(result), nil
|
||||
}
|
||||
|
||||
// CallToolWithToken invokes a helper with an in-memory token override. It is
|
||||
// used during login before the new token has been persisted to any profile
|
||||
// slot.
|
||||
|
||||
@@ -39,6 +39,19 @@ func (r recordingToolCaller) CallTool(ctx context.Context, product, tool string,
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (r recordingToolCaller) CallReadTool(ctx context.Context, product, tool string, args map[string]any) (*edition.ToolResult, error) {
|
||||
inner, ok := r.inner.(edition.ReadToolCaller)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("ToolCaller read-only dry-run lookup is not configured")
|
||||
}
|
||||
recordedArgs := cloneToolArgs(args)
|
||||
res, err := inner.CallReadTool(ctx, product, tool, args)
|
||||
// This is a real read even though the outer command is a dry-run. Record it
|
||||
// as such so usage evidence does not misclassify the lookup as skipped.
|
||||
usage.Append(product, tool, recordedArgs, err == nil, false)
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (r recordingToolCaller) CallToolWithToken(
|
||||
ctx context.Context,
|
||||
token, product, tool string,
|
||||
|
||||
@@ -27,6 +27,7 @@ type crossPlatformCoverageCaller struct {
|
||||
args map[string]any
|
||||
token string
|
||||
dryRun bool
|
||||
reads int
|
||||
}
|
||||
|
||||
func (c *crossPlatformCoverageCaller) CallTool(_ context.Context, _, _ string, args map[string]any) (*edition.ToolResult, error) {
|
||||
@@ -44,6 +45,16 @@ func (c *crossPlatformCoverageCaller) CallToolWithToken(
|
||||
return &edition.ToolResult{}, nil
|
||||
}
|
||||
|
||||
func (c *crossPlatformCoverageCaller) CallReadTool(
|
||||
_ context.Context,
|
||||
_, _ string,
|
||||
args map[string]any,
|
||||
) (*edition.ToolResult, error) {
|
||||
c.reads++
|
||||
c.args = args
|
||||
return &edition.ToolResult{}, nil
|
||||
}
|
||||
|
||||
func (*crossPlatformCoverageCaller) Format() string { return "json" }
|
||||
func (c *crossPlatformCoverageCaller) DryRun() bool { return c.dryRun }
|
||||
func (*crossPlatformCoverageCaller) Fields() string { return "id,name" }
|
||||
@@ -129,6 +140,51 @@ func TestCrossPlatformCoverageRecordingToolCaller(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageRecordingReadToolCaller(t *testing.T) {
|
||||
t.Setenv("DWS_CONFIG_DIR", t.TempDir())
|
||||
t.Setenv("DWS_USAGE_TRACKING", "1")
|
||||
|
||||
inner := &crossPlatformCoverageCaller{dryRun: true}
|
||||
caller := newRecordingToolCaller(inner)
|
||||
readCaller, ok := caller.(edition.ReadToolCaller)
|
||||
if !ok {
|
||||
t.Fatal("recording caller dropped read-only capability")
|
||||
}
|
||||
if _, err := readCaller.CallReadTool(
|
||||
context.Background(),
|
||||
"im",
|
||||
"search_groups",
|
||||
map[string]any{"open_conversation_id": "cid_x"},
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if inner.reads != 1 || inner.args["open_conversation_id"] != "cid_x" {
|
||||
t.Fatalf("read forwarding = reads %d args %#v", inner.reads, inner.args)
|
||||
}
|
||||
records, err := usage.Read()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(records) != 1 || !records[0].OK {
|
||||
t.Fatalf("real read must be recorded as successful: %#v", records)
|
||||
}
|
||||
|
||||
withoutRead := recordingToolCaller{inner: nonReadToolCaller{}}
|
||||
if _, err := withoutRead.CallReadTool(context.Background(), "im", "search_groups", nil); err == nil {
|
||||
t.Fatal("recording caller accepted an inner caller without read support")
|
||||
}
|
||||
}
|
||||
|
||||
type nonReadToolCaller struct{}
|
||||
|
||||
func (nonReadToolCaller) CallTool(context.Context, string, string, map[string]any) (*edition.ToolResult, error) {
|
||||
return &edition.ToolResult{}, nil
|
||||
}
|
||||
func (nonReadToolCaller) Format() string { return "json" }
|
||||
func (nonReadToolCaller) DryRun() bool { return false }
|
||||
func (nonReadToolCaller) Fields() string { return "" }
|
||||
func (nonReadToolCaller) JQ() string { return "" }
|
||||
|
||||
func TestCrossPlatformCoverageRootPublishesShortcutCommands(t *testing.T) {
|
||||
configDir := t.TempDir()
|
||||
t.Setenv("DWS_CONFIG_DIR", configDir)
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
// 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.
|
||||
|
||||
// command_meta.go provides the unified metadata consumption API. All runtime
|
||||
// consumers (help, schema, agent selection, skill generation) call ResolveMeta
|
||||
// to get a CommandMeta struct — one function, one struct, no need to know which
|
||||
// of the 6 generation layers a field comes from.
|
||||
//
|
||||
// This is the "simple consumption" half of the generation/consumption split:
|
||||
// - Generation (gen.go + internal/generator/): 6 inputs → catalog snapshot.
|
||||
// - Consumption (this file): catalog snapshot → ResolveMeta → CommandMeta.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// CommandMeta is the complete runtime metadata view for a single command.
|
||||
// Consumers read this struct; they never touch the raw catalog maps.
|
||||
type CommandMeta struct {
|
||||
Identity CommandIdentity
|
||||
Safety CommandSafety
|
||||
Selection CommandSelection
|
||||
}
|
||||
|
||||
// CommandIdentity is the stable identity of a command.
|
||||
type CommandIdentity struct {
|
||||
CLIPath string // "dev app delete"
|
||||
Canonical string // "dev.delete_dev_app"
|
||||
Aliases []string // ["search", ...]
|
||||
ProductID string // "devapp"
|
||||
Title string // one-line description
|
||||
}
|
||||
|
||||
// CommandSelection is the agent-facing selection metadata.
|
||||
type CommandSelection struct {
|
||||
AgentSummary string
|
||||
UseWhen []string
|
||||
AvoidWhen []string
|
||||
Examples []string
|
||||
}
|
||||
|
||||
var (
|
||||
metaByCLIPathOnce sync.Once
|
||||
metaByCLIPath map[string]CommandMeta
|
||||
)
|
||||
|
||||
// initMetaByCLIPath builds the cli_path → CommandMeta lookup from the embedded
|
||||
// catalog. Runs once (sync.Once); the catalog is already decoded at package init.
|
||||
func initMetaByCLIPath() {
|
||||
metaByCLIPath = buildMetaByCLIPath(embeddedSchemaCatalog())
|
||||
}
|
||||
|
||||
// buildMetaByCLIPath constructs the lookup from a loaded catalog snapshot.
|
||||
// Split from initMetaByCLIPath so malformed-snapshot guards stay testable.
|
||||
func buildMetaByCLIPath(loaded loadedSchemaCatalog) map[string]CommandMeta {
|
||||
lookup := make(map[string]CommandMeta)
|
||||
if loaded.Snapshot.Tools == nil {
|
||||
return lookup
|
||||
}
|
||||
metas := make([]CommandMeta, 0, len(loaded.Snapshot.Tools))
|
||||
for _, tool := range loaded.Snapshot.Tools {
|
||||
cliPath := schemaString(tool["cli_path"])
|
||||
if cliPath == "" {
|
||||
continue
|
||||
}
|
||||
meta := CommandMeta{
|
||||
Identity: CommandIdentity{
|
||||
CLIPath: cliPath,
|
||||
Canonical: schemaString(tool["canonical_path"]),
|
||||
Aliases: schemaStringSlice(tool["aliases"]),
|
||||
ProductID: schemaString(tool["product_id"]),
|
||||
Title: schemaString(tool["title"]),
|
||||
},
|
||||
Safety: CommandSafety{
|
||||
Effect: schemaString(tool["effect"]),
|
||||
Risk: schemaString(tool["risk"]),
|
||||
Confirmation: schemaString(tool["confirmation"]),
|
||||
Idempotency: schemaString(tool["idempotency"]),
|
||||
},
|
||||
Selection: CommandSelection{
|
||||
AgentSummary: schemaString(tool["agent_summary"]),
|
||||
UseWhen: schemaStringSlice(tool["use_when"]),
|
||||
AvoidWhen: schemaStringSlice(tool["avoid_when"]),
|
||||
Examples: schemaStringSlice(tool["examples"]),
|
||||
},
|
||||
}
|
||||
lookup[cliPath] = meta
|
||||
metas = append(metas, meta)
|
||||
}
|
||||
// Register compat alias paths (e.g. "report list") against the same
|
||||
// metadata in a second pass, sorted by primary cli_path: primary paths
|
||||
// always win (registered above, aliases only fill vacancies), and an
|
||||
// alias-vs-alias collision resolves deterministically to the owner with
|
||||
// the lexicographically smallest primary path — Snapshot.Tools is a map,
|
||||
// so relying on iteration order would make the winner vary per process.
|
||||
sort.Slice(metas, func(i, j int) bool {
|
||||
return metas[i].Identity.CLIPath < metas[j].Identity.CLIPath
|
||||
})
|
||||
for _, meta := range metas {
|
||||
for _, alias := range meta.Identity.Aliases {
|
||||
alias = strings.TrimSpace(alias)
|
||||
if alias == "" || alias == meta.Identity.CLIPath {
|
||||
continue
|
||||
}
|
||||
if _, exists := lookup[alias]; !exists {
|
||||
lookup[alias] = meta
|
||||
}
|
||||
}
|
||||
}
|
||||
return lookup
|
||||
}
|
||||
|
||||
// ResolveMeta returns the complete metadata for a command identified by its CLI
|
||||
// path (e.g. "dev app delete") or one of its compat aliases (e.g. "report list"
|
||||
// for "report inbox list"). Returns ok=false for commands not in the embedded
|
||||
// catalog (utility commands, hidden commands, shortcuts).
|
||||
func ResolveMeta(cliPath string) (CommandMeta, bool) {
|
||||
metaByCLIPathOnce.Do(initMetaByCLIPath)
|
||||
m, ok := metaByCLIPath[strings.TrimSpace(cliPath)]
|
||||
return m, ok
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// CommandSafety holds the safety metadata for a CLI command, resolved at
|
||||
// runtime from the embedded schema catalog. This is a read-only view over the
|
||||
// catalog — NOT a second safety source. The catalog remains the single
|
||||
// authoritative reviewed source; this struct merely provides typed access for
|
||||
// consumers (help rendering, skill generation).
|
||||
type CommandSafety struct {
|
||||
Effect string // read / write / destructive
|
||||
Risk string // low / medium / high
|
||||
Confirmation string // not_required / user_required
|
||||
Idempotency string // idempotent / non_idempotent
|
||||
}
|
||||
|
||||
// ShouldRender returns true when the safety metadata warrants a visible
|
||||
// annotation in --help. Only commands that need confirmation or carry
|
||||
// above-low risk are annotated; read-only low-risk commands stay clean.
|
||||
func (s CommandSafety) ShouldRender() bool {
|
||||
return s.Confirmation == "user_required" ||
|
||||
(s.Risk != "" && s.Risk != "low")
|
||||
}
|
||||
|
||||
// SafetyForCLIPath returns the safety metadata for a command identified by its
|
||||
// CLI path (e.g. "dev app delete"). Returns ok=false when the command is not
|
||||
// in the embedded catalog (utility commands, hidden commands, shortcuts).
|
||||
//
|
||||
// Deprecated: use ResolveMeta(cliPath).Safety for the complete metadata view.
|
||||
// Kept for backward compatibility with existing callers.
|
||||
func SafetyForCLIPath(cliPath string) (CommandSafety, bool) {
|
||||
meta, ok := ResolveMeta(cliPath)
|
||||
if !ok {
|
||||
return CommandSafety{}, false
|
||||
}
|
||||
return meta.Safety, true
|
||||
}
|
||||
|
||||
// RenderSafetyAnnotation writes a "Safety:" line to the command's stdout when
|
||||
// the command carries above-low risk or requires user confirmation. This is
|
||||
// the shared entry point for ALL help rendering paths (root HelpFunc, product
|
||||
// group custom HelpFuncs like calendar's). It avoids the timing issue where a
|
||||
// group captures origHelp before configureRootHelp sets the root's custom func.
|
||||
func RenderSafetyAnnotation(cmd *cobra.Command) {
|
||||
cliPath := strings.TrimSpace(strings.TrimPrefix(cmd.CommandPath(), cmd.Root().Name()+" "))
|
||||
safety, ok := SafetyForCLIPath(cliPath)
|
||||
if !ok || !safety.ShouldRender() {
|
||||
return
|
||||
}
|
||||
w := cmd.OutOrStdout()
|
||||
fmt.Fprintf(w, "\nSafety: effect=%s risk=%s confirmation=%s", safety.Effect, safety.Risk, safety.Confirmation)
|
||||
if safety.Idempotency != "" {
|
||||
fmt.Fprintf(w, " idempotency=%s", safety.Idempotency)
|
||||
}
|
||||
if safety.Confirmation == "user_required" {
|
||||
fmt.Fprint(w, " (requires --yes)")
|
||||
}
|
||||
fmt.Fprintln(w)
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSafetyForCLIPathKnownCommand(t *testing.T) {
|
||||
// dev app delete 是 destructive + high risk + user_required。
|
||||
s, ok := SafetyForCLIPath("dev app delete")
|
||||
if !ok {
|
||||
t.Fatal("SafetyForCLIPath(\"dev app delete\") returned ok=false; want true")
|
||||
}
|
||||
if s.Effect != "destructive" {
|
||||
t.Errorf("effect = %q, want destructive", s.Effect)
|
||||
}
|
||||
if s.Risk != "high" {
|
||||
t.Errorf("risk = %q, want high", s.Risk)
|
||||
}
|
||||
if s.Confirmation != "user_required" {
|
||||
t.Errorf("confirmation = %q, want user_required", s.Confirmation)
|
||||
}
|
||||
if !s.ShouldRender() {
|
||||
t.Error("ShouldRender() = false for destructive/high/user_required; want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafetyForCLIPathUnknownSkips(t *testing.T) {
|
||||
// 不存在的命令 → ok=false。
|
||||
_, ok := SafetyForCLIPath("nonexistent fake command")
|
||||
if ok {
|
||||
t.Fatal("SafetyForCLIPath for nonexistent command returned ok=true; want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafetyForCLIPathReadOnlyLowRiskSkips(t *testing.T) {
|
||||
// 找一个 read/low 风险命令,ShouldRender 应 false。
|
||||
// dev app list 是 read,默认 low risk。
|
||||
s, ok := SafetyForCLIPath("dev app list")
|
||||
if !ok {
|
||||
t.Skip("dev app list not in catalog; skipping")
|
||||
}
|
||||
if s.ShouldRender() {
|
||||
t.Errorf("ShouldRender() = true for effect=%s risk=%s; want false (read/low)", s.Effect, s.Risk)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafetyForCLIPathTrimsWhitespace(t *testing.T) {
|
||||
// 带首尾空格的 cli_path 也能查到。
|
||||
s1, _ := SafetyForCLIPath("dev app delete")
|
||||
s2, ok2 := SafetyForCLIPath(" dev app delete ")
|
||||
if !ok2 {
|
||||
t.Fatal("trimmed lookup failed; whitespace not trimmed")
|
||||
}
|
||||
if s1 != s2 {
|
||||
t.Errorf("whitespace-trimmed result differs: %+v vs %+v", s1, s2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveMetaComplete(t *testing.T) {
|
||||
// dev app delete: destructive + high + user_required + 有 selection。
|
||||
m, ok := ResolveMeta("dev app delete")
|
||||
if !ok {
|
||||
t.Fatal("ResolveMeta(\"dev app delete\") returned ok=false")
|
||||
}
|
||||
if m.Identity.CLIPath != "dev app delete" {
|
||||
t.Errorf("CLIPath = %q", m.Identity.CLIPath)
|
||||
}
|
||||
if m.Safety.Effect != "destructive" {
|
||||
t.Errorf("Effect = %q, want destructive", m.Safety.Effect)
|
||||
}
|
||||
if len(m.Selection.UseWhen) == 0 && m.Selection.AgentSummary == "" {
|
||||
t.Error("Selection is empty; expected use_when or agent_summary")
|
||||
}
|
||||
t.Logf("dev app delete: canonical=%s safety=%+v selection(use_when=%d avoid_when=%d examples=%d)",
|
||||
m.Identity.Canonical, m.Safety, len(m.Selection.UseWhen), len(m.Selection.AvoidWhen), len(m.Selection.Examples))
|
||||
}
|
||||
|
||||
func TestResolveMetaUnknownSkips(t *testing.T) {
|
||||
_, ok := ResolveMeta("nonexistent fake command")
|
||||
if ok {
|
||||
t.Fatal("ResolveMeta for unknown command returned ok=true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveMetaCopiesAliases(t *testing.T) {
|
||||
// report inbox list 在 Catalog 中声明了兼容别名 "report list"。
|
||||
m, ok := ResolveMeta("report inbox list")
|
||||
if !ok {
|
||||
t.Fatal("ResolveMeta(\"report inbox list\") returned ok=false")
|
||||
}
|
||||
found := false
|
||||
for _, alias := range m.Identity.Aliases {
|
||||
if alias == "report list" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("Identity.Aliases = %v, want to contain \"report list\"", m.Identity.Aliases)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveMetaAliasLookup(t *testing.T) {
|
||||
// 兼容别名路径必须解析到与主 cli_path 同一份元数据。
|
||||
primary, ok := ResolveMeta("report inbox list")
|
||||
if !ok {
|
||||
t.Fatal("primary path lookup failed")
|
||||
}
|
||||
aliased, ok := ResolveMeta("report list")
|
||||
if !ok {
|
||||
t.Fatal("ResolveMeta(\"report list\") alias lookup returned ok=false")
|
||||
}
|
||||
if aliased.Identity.CLIPath != primary.Identity.CLIPath || aliased.Identity.Canonical != primary.Identity.Canonical {
|
||||
t.Fatalf("alias metadata = %+v, want same identity as primary %+v", aliased.Identity, primary.Identity)
|
||||
}
|
||||
if aliased.Safety != primary.Safety {
|
||||
t.Fatalf("alias safety = %+v, want %+v", aliased.Safety, primary.Safety)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// 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.
|
||||
|
||||
// gen.go is the single entry point for schema metadata generation. It isolates
|
||||
// all //go:generate pragmas from business code so that:
|
||||
// - schema_agent_metadata.go / schema_catalog.go contain only types + embed.
|
||||
// - Generation is a standalone process (make generate-schema triggers this).
|
||||
// - The 6-input → 1-output contract is documented in one place.
|
||||
//
|
||||
// Generation inputs (authored, reviewed):
|
||||
// 1. schema_command_registry/ identity (canonical/aliases/navigation)
|
||||
// 2. schema_hints/metadata/*.json safety (effect/risk/confirmation)
|
||||
// 3. schema_hints/selection/*.json selection (use_when/avoid_when)
|
||||
// 4. schema_mcp_metadata.json MCP server tool definitions
|
||||
// 5. schema_parameter_bindings.json parameter type/property mappings
|
||||
// 6. cobra command tree (Go runtime) flags/usage/required (reflected)
|
||||
//
|
||||
// Generation outputs (embedded at build):
|
||||
// - schema_agent_metadata/*.json per-product agent metadata
|
||||
// - schema_catalog/ per-product catalog shards
|
||||
|
||||
package cli
|
||||
|
||||
//go:generate go run ../generator/cmd_schema_agent_metadata -root ../.. -registry internal/cli/schema_command_registry -output-dir schema_agent_metadata -audit-output schema_agent_metadata_audit.json
|
||||
// Rebuild all dependencies so the Catalog compiler cannot reuse the cli
|
||||
// package cached by the preceding metadata generator with the old embedded
|
||||
// JSON files.
|
||||
//go:generate go run -a ../generator/cmd_schema_catalog -root ../.. -output schema_catalog
|
||||
@@ -22,12 +22,6 @@ import (
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
//go:generate go run ../generator/cmd_schema_agent_metadata -root ../.. -registry internal/cli/schema_command_registry.json -output-dir schema_agent_metadata -audit-output schema_agent_metadata_audit.json
|
||||
// Rebuild all dependencies so the Catalog compiler cannot reuse the cli
|
||||
// package cached by the preceding metadata generator with the old embedded
|
||||
// JSON files.
|
||||
//go:generate go run -a ../generator/cmd_schema_catalog -root ../.. -output schema_catalog.json
|
||||
|
||||
//go:embed schema_agent_metadata/*.json
|
||||
var embeddedAgentMetadataFS embed.FS
|
||||
|
||||
|
||||
@@ -1259,7 +1259,7 @@
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"当你想新建一个会话分组来归类会话时使用;会实际创建分组并返回其 ID,需传分组名称 --title。"
|
||||
"当你想新建一个会话分组来归类会话时使用;会实际创建分组并返回其 ID,需传最多 15 个字符的分组名称 --title。"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -1268,7 +1268,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"当你想新建一个会话分组来归类会话时使用;会实际创建分组并返回其 ID,需传分组名称 --title。"
|
||||
"当你想新建一个会话分组来归类会话时使用;会实际创建分组并返回其 ID,需传最多 15 个字符的分组名称 --title。"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -1291,7 +1291,7 @@
|
||||
"internal/cli/schema_hints/selection/chat.json"
|
||||
],
|
||||
"use_when": [
|
||||
"当你想新建一个会话分组来归类会话时使用;会实际创建分组并返回其 ID,需传分组名称 --title。"
|
||||
"当你想新建一个会话分组来归类会话时使用;会实际创建分组并返回其 ID,需传最多 15 个字符的分组名称 --title。"
|
||||
]
|
||||
},
|
||||
"chat +category-delete": {
|
||||
@@ -2033,7 +2033,7 @@
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"当你想重命名已有的自定义会话分组时使用;会实际更新分组名称,需传 categoryId 和新名称 --title。"
|
||||
"当你想重命名已有的自定义会话分组时使用;会实际更新分组名称,需传 categoryId 和最多 15 个字符的新名称 --title。"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -2042,7 +2042,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"当你想重命名已有的自定义会话分组时使用;会实际更新分组名称,需传 categoryId 和新名称 --title。"
|
||||
"当你想重命名已有的自定义会话分组时使用;会实际更新分组名称,需传 categoryId 和最多 15 个字符的新名称 --title。"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -2065,7 +2065,7 @@
|
||||
"internal/cli/schema_hints/selection/chat.json"
|
||||
],
|
||||
"use_when": [
|
||||
"当你想重命名已有的自定义会话分组时使用;会实际更新分组名称,需传 categoryId 和新名称 --title。"
|
||||
"当你想重命名已有的自定义会话分组时使用;会实际更新分组名称,需传 categoryId 和最多 15 个字符的新名称 --title。"
|
||||
]
|
||||
},
|
||||
"chat +chat-bots": {
|
||||
@@ -7229,32 +7229,33 @@
|
||||
]
|
||||
},
|
||||
"chat +conversation-list-top": {
|
||||
"agent_summary": "拉取置顶会话列表",
|
||||
"agent_summary": "拉取置顶会话列表,可只看群聊或单聊",
|
||||
"agent_summary_source": "dws-agent-selection/chat",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"用户要查看群内被 Pin 的消息而不是侧边栏置顶会话时,改用 +messages-list-pin"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "read",
|
||||
"effect_source": "agent-hint",
|
||||
"examples": [
|
||||
"dws chat +conversation-list-top --limit 1000"
|
||||
"dws chat +conversation-list-top --type group --limit 1000",
|
||||
"dws chat +conversation-list-top --type direct --limit 1000"
|
||||
],
|
||||
"field_provenance": {
|
||||
"agent_summary": {
|
||||
"value": "拉取置顶会话列表",
|
||||
"value": "拉取置顶会话列表,可只看群聊或单聊",
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
|
||||
"review_reason": "Reviewed after live testing list_top_conversations: singleChat is present on every tested row and is normalized locally to group/direct without adding a lower-service parameter.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "拉取置顶会话列表",
|
||||
"value": "拉取置顶会话列表,可只看群聊或单聊",
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts."
|
||||
"review_reason": "Reviewed after live testing list_top_conversations: singleChat is present on every tested row and is normalized locally to group/direct without adding a lower-service parameter."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -7263,34 +7264,34 @@
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity.",
|
||||
"review_reason": "Reviewed against the mounted Shortcut: --type all|group|direct is a local semantic filter over list_top_conversations.singleChat, so the composite interface owns validation and normalized output without changing lower-service arguments.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "available",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity."
|
||||
"review_reason": "Reviewed against the mounted Shortcut: --type all|group|direct is a local semantic filter over list_top_conversations.singleChat, so the composite interface owns validation and normalized output without changing lower-service arguments."
|
||||
}
|
||||
]
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"用户要查看群内被 Pin 的消息而不是侧边栏置顶会话时,改用 +messages-list-pin"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
|
||||
"review_reason": "Reviewed after live testing list_top_conversations: singleChat is present on every tested row and is normalized locally to group/direct without adding a lower-service parameter.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"用户要查看群内被 Pin 的消息而不是侧边栏置顶会话时,改用 +messages-list-pin"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts."
|
||||
"review_reason": "Reviewed after live testing list_top_conversations: singleChat is present on every tested row and is normalized locally to group/direct without adding a lower-service parameter."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -7299,14 +7300,14 @@
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity.",
|
||||
"review_reason": "Reviewed against the mounted Shortcut: --type all|group|direct is a local semantic filter over list_top_conversations.singleChat, so the composite interface owns validation and normalized output without changing lower-service arguments.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "not_required",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity."
|
||||
"review_reason": "Reviewed against the mounted Shortcut: --type all|group|direct is a local semantic filter over list_top_conversations.singleChat, so the composite interface owns validation and normalized output without changing lower-service arguments."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -7315,34 +7316,36 @@
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity.",
|
||||
"review_reason": "Reviewed against the mounted Shortcut: --type all|group|direct is a local semantic filter over list_top_conversations.singleChat, so the composite interface owns validation and normalized output without changing lower-service arguments.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "read",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity."
|
||||
"review_reason": "Reviewed against the mounted Shortcut: --type all|group|direct is a local semantic filter over list_top_conversations.singleChat, so the composite interface owns validation and normalized output without changing lower-service arguments."
|
||||
}
|
||||
]
|
||||
},
|
||||
"examples": {
|
||||
"value": [
|
||||
"dws chat +conversation-list-top --limit 1000"
|
||||
"dws chat +conversation-list-top --type group --limit 1000",
|
||||
"dws chat +conversation-list-top --type direct --limit 1000"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
|
||||
"review_reason": "Reviewed after live testing list_top_conversations: singleChat is present on every tested row and is normalized locally to group/direct without adding a lower-service parameter.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"dws chat +conversation-list-top --limit 1000"
|
||||
"dws chat +conversation-list-top --type group --limit 1000",
|
||||
"dws chat +conversation-list-top --type direct --limit 1000"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts."
|
||||
"review_reason": "Reviewed after live testing list_top_conversations: singleChat is present on every tested row and is normalized locally to group/direct without adding a lower-service parameter."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -7351,14 +7354,14 @@
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity.",
|
||||
"review_reason": "Reviewed against the mounted Shortcut: --type all|group|direct is a local semantic filter over list_top_conversations.singleChat, so the composite interface owns validation and normalized output without changing lower-service arguments.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "idempotent",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity."
|
||||
"review_reason": "Reviewed against the mounted Shortcut: --type all|group|direct is a local semantic filter over list_top_conversations.singleChat, so the composite interface owns validation and normalized output without changing lower-service arguments."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -7367,14 +7370,14 @@
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity.",
|
||||
"review_reason": "Reviewed against the mounted Shortcut: --type all|group|direct is a local semantic filter over list_top_conversations.singleChat, so the composite interface owns validation and normalized output without changing lower-service arguments.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "composite",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity."
|
||||
"review_reason": "Reviewed against the mounted Shortcut: --type all|group|direct is a local semantic filter over list_top_conversations.singleChat, so the composite interface owns validation and normalized output without changing lower-service arguments."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -7383,14 +7386,14 @@
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity.",
|
||||
"review_reason": "Reviewed against the mounted Shortcut: --type all|group|direct is a local semantic filter over list_top_conversations.singleChat, so the composite interface owns validation and normalized output without changing lower-service arguments.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity."
|
||||
"review_reason": "Reviewed against the mounted Shortcut: --type all|group|direct is a local semantic filter over list_top_conversations.singleChat, so the composite interface owns validation and normalized output without changing lower-service arguments."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -7415,21 +7418,21 @@
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity.",
|
||||
"review_reason": "Reviewed against the mounted Shortcut: --type all|group|direct is a local semantic filter over list_top_conversations.singleChat, so the composite interface owns validation and normalized output without changing lower-service arguments.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": true,
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity."
|
||||
"review_reason": "Reviewed against the mounted Shortcut: --type all|group|direct is a local semantic filter over list_top_conversations.singleChat, so the composite interface owns validation and normalized output without changing lower-service arguments."
|
||||
},
|
||||
{
|
||||
"value": true,
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": false,
|
||||
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts."
|
||||
"review_reason": "Reviewed after live testing list_top_conversations: singleChat is present on every tested row and is normalized locally to group/direct without adding a lower-service parameter."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -7438,34 +7441,34 @@
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity.",
|
||||
"review_reason": "Reviewed against the mounted Shortcut: --type all|group|direct is a local semantic filter over list_top_conversations.singleChat, so the composite interface owns validation and normalized output without changing lower-service arguments.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "low",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity."
|
||||
"review_reason": "Reviewed against the mounted Shortcut: --type all|group|direct is a local semantic filter over list_top_conversations.singleChat, so the composite interface owns validation and normalized output without changing lower-service arguments."
|
||||
}
|
||||
]
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"当你只想查看被置顶的那些会话时使用;只读分页返回置顶会话列表,可用 --exclude-muted 排除已免打扰会话。"
|
||||
"当你只想查看被置顶的那些会话时使用;只读分页返回置顶会话列表,并把下层 singleChat 规范化为 conversationType=group|direct。可用 --type group 只看群聊、--type direct 只看单聊,或用 --exclude-muted 排除已免打扰会话。"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
|
||||
"review_reason": "Reviewed after live testing list_top_conversations: singleChat is present on every tested row and is normalized locally to group/direct without adding a lower-service parameter.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"当你只想查看被置顶的那些会话时使用;只读分页返回置顶会话列表,可用 --exclude-muted 排除已免打扰会话。"
|
||||
"当你只想查看被置顶的那些会话时使用;只读分页返回置顶会话列表,并把下层 singleChat 规范化为 conversationType=group|direct。可用 --type group 只看群聊、--type direct 只看单聊,或用 --exclude-muted 排除已免打扰会话。"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts."
|
||||
"review_reason": "Reviewed after live testing list_top_conversations: singleChat is present on every tested row and is normalized locally to group/direct without adding a lower-service parameter."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -7483,7 +7486,7 @@
|
||||
"internal/cli/schema_hints/selection/chat.json"
|
||||
],
|
||||
"use_when": [
|
||||
"当你只想查看被置顶的那些会话时使用;只读分页返回置顶会话列表,可用 --exclude-muted 排除已免打扰会话。"
|
||||
"当你只想查看被置顶的那些会话时使用;只读分页返回置顶会话列表,并把下层 singleChat 规范化为 conversationType=group|direct。可用 --type group 只看群聊、--type direct 只看单聊,或用 --exclude-muted 排除已免打扰会话。"
|
||||
]
|
||||
},
|
||||
"chat +dm": {
|
||||
@@ -8795,14 +8798,14 @@
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
|
||||
"review_reason": "Reviewed against the built-in Shortcut, executable Cobra flags, safe relative-path download contract, and per-resource failure ledger; this selection update advertises the new optional resource workflow without changing its runtime safety facts.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "根据消息 ID 批量查询消息(最多 50 条)",
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts."
|
||||
"review_reason": "Reviewed against the built-in Shortcut, executable Cobra flags, safe relative-path download contract, and per-resource failure ledger; this selection update advertises the new optional resource workflow without changing its runtime safety facts."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -8829,7 +8832,7 @@
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
|
||||
"review_reason": "Reviewed against the built-in Shortcut, executable Cobra flags, safe relative-path download contract, and per-resource failure ledger; this selection update advertises the new optional resource workflow without changing its runtime safety facts.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
@@ -8838,7 +8841,7 @@
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts."
|
||||
"review_reason": "Reviewed against the built-in Shortcut, executable Cobra flags, safe relative-path download contract, and per-resource failure ledger; this selection update advertises the new optional resource workflow without changing its runtime safety facts."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -8881,7 +8884,7 @@
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
|
||||
"review_reason": "Reviewed against the built-in Shortcut, executable Cobra flags, safe relative-path download contract, and per-resource failure ledger; this selection update advertises the new optional resource workflow without changing its runtime safety facts.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
@@ -8890,7 +8893,7 @@
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts."
|
||||
"review_reason": "Reviewed against the built-in Shortcut, executable Cobra flags, safe relative-path download contract, and per-resource failure ledger; this selection update advertises the new optional resource workflow without changing its runtime safety facts."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -8977,7 +8980,7 @@
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": false,
|
||||
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts."
|
||||
"review_reason": "Reviewed against the built-in Shortcut, executable Cobra flags, safe relative-path download contract, and per-resource failure ledger; this selection update advertises the new optional resource workflow without changing its runtime safety facts."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -8999,21 +9002,21 @@
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"当你已有一批消息 openMsgId、需要批量取回它们的详细内容时使用;只读,一次最多查询 50 条。"
|
||||
"当你已有一批消息 openMsgId、需要批量取回完整详情、reaction 和可执行资源引用时使用;一次最多 50 条。--download-resources 可把所有可识别 mediaId 安全下载到工作目录内,并逐资源返回成功/失败 ledger。"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
|
||||
"review_reason": "Reviewed against the built-in Shortcut, executable Cobra flags, safe relative-path download contract, and per-resource failure ledger; this selection update advertises the new optional resource workflow without changing its runtime safety facts.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"当你已有一批消息 openMsgId、需要批量取回它们的详细内容时使用;只读,一次最多查询 50 条。"
|
||||
"当你已有一批消息 openMsgId、需要批量取回完整详情、reaction 和可执行资源引用时使用;一次最多 50 条。--download-resources 可把所有可识别 mediaId 安全下载到工作目录内,并逐资源返回成功/失败 ledger。"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts."
|
||||
"review_reason": "Reviewed against the built-in Shortcut, executable Cobra flags, safe relative-path download contract, and per-resource failure ledger; this selection update advertises the new optional resource workflow without changing its runtime safety facts."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -9031,7 +9034,7 @@
|
||||
"internal/cli/schema_hints/selection/chat.json"
|
||||
],
|
||||
"use_when": [
|
||||
"当你已有一批消息 openMsgId、需要批量取回它们的详细内容时使用;只读,一次最多查询 50 条。"
|
||||
"当你已有一批消息 openMsgId、需要批量取回完整详情、reaction 和可执行资源引用时使用;一次最多 50 条。--download-resources 可把所有可识别 mediaId 安全下载到工作目录内,并逐资源返回成功/失败 ledger。"
|
||||
]
|
||||
},
|
||||
"chat +messages-query-send-status": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": 1,
|
||||
"source_hash": "sha256:f71480e58eda8f2be041e07d7faf6fb20f5c21f861995d3c4dacc311b46c3e81",
|
||||
"source_hash": "sha256:1fed2a8bc5328212fdf646c3e68d465a2a440b2d303c1ad120981e06c97da487",
|
||||
"surface_hash": "sha256:4cf8460240b19f896c3a330c69982cbb5f57aa8576cdf30373172082eea893ed",
|
||||
"coverage": {
|
||||
"surface_products": 26,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": 1,
|
||||
"source_hash": "sha256:f71480e58eda8f2be041e07d7faf6fb20f5c21f861995d3c4dacc311b46c3e81",
|
||||
"source_hash": "sha256:1fed2a8bc5328212fdf646c3e68d465a2a440b2d303c1ad120981e06c97da487",
|
||||
"surface_hash": "sha256:4cf8460240b19f896c3a330c69982cbb5f57aa8576cdf30373172082eea893ed",
|
||||
"source_files": 160,
|
||||
"hint_files": 54,
|
||||
|
||||
@@ -15,10 +15,11 @@ package cli
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
_ "embed"
|
||||
"embed"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -29,8 +30,20 @@ import (
|
||||
|
||||
const SchemaCatalogSnapshotVersion = 1
|
||||
|
||||
//go:embed schema_catalog.json
|
||||
var embeddedSchemaCatalogJSON []byte
|
||||
// The release Catalog is committed as a per-product split so concurrent
|
||||
// feature PRs only rewrite the shard for the product they touch:
|
||||
//
|
||||
// schema_catalog/catalog.json global envelope + Catalog map
|
||||
// schema_catalog/tools/<product>.json that product's leaf ToolSpecs
|
||||
//
|
||||
// The loader reassembles the exact same SchemaCatalogSnapshot, so the
|
||||
// source_hash integrity check is identical to the single-file layout.
|
||||
//
|
||||
//go:embed schema_catalog/catalog.json
|
||||
var embeddedSchemaCatalogEnvelopeJSON []byte
|
||||
|
||||
//go:embed schema_catalog/tools
|
||||
var embeddedSchemaCatalogTools embed.FS
|
||||
|
||||
// SchemaCatalogSnapshot is the release-stable Agent contract. Catalog holds
|
||||
// the progressive product/tool index; Tools holds full leaf parameter schemas.
|
||||
@@ -68,11 +81,80 @@ var runtimeEmbeddedSchemaCatalogLazyLoadCount atomic.Uint64
|
||||
func embeddedSchemaCatalog() loadedSchemaCatalog {
|
||||
runtimeEmbeddedSchemaCatalogOnce.Do(func() {
|
||||
runtimeEmbeddedSchemaCatalogLazyLoadCount.Add(1)
|
||||
runtimeEmbeddedSchemaCatalog, runtimeEmbeddedSchemaCatalogErr = decodeSchemaCatalogSnapshot(embeddedSchemaCatalogJSON)
|
||||
runtimeEmbeddedSchemaCatalog, runtimeEmbeddedSchemaCatalogErr = assembleEmbeddedSchemaCatalog()
|
||||
})
|
||||
return runtimeEmbeddedSchemaCatalog
|
||||
}
|
||||
|
||||
// schemaCatalogEnvelope is the global half of the split release Catalog. It
|
||||
// mirrors the generator's envelope struct; the Catalog map and release hashes
|
||||
// do not partition by product and stay in one file.
|
||||
type schemaCatalogEnvelope struct {
|
||||
Version int `json:"version"`
|
||||
SurfaceHash string `json:"surface_hash,omitempty"`
|
||||
SourceHash string `json:"source_hash"`
|
||||
Catalog map[string]any `json:"catalog"`
|
||||
}
|
||||
|
||||
// schemaCatalogToolShard mirrors the per-product shard written by the
|
||||
// generator. Only the product and its leaf ToolSpecs are stored here.
|
||||
type schemaCatalogToolShard struct {
|
||||
Product string `json:"product"`
|
||||
Tools map[string]map[string]any `json:"tools"`
|
||||
}
|
||||
|
||||
// assembleEmbeddedSchemaCatalog reassembles the split release Catalog shards
|
||||
// into the same SchemaCatalogSnapshot the single-file layout produced, then
|
||||
// validates it through the production loader. source_hash still covers the
|
||||
// whole payload: if any shard is missing, stale, or tampered, the content hash
|
||||
// check in loadSchemaCatalogSnapshot fails exactly as before.
|
||||
func assembleEmbeddedSchemaCatalog() (loadedSchemaCatalog, error) {
|
||||
snapshot, err := assembleSchemaCatalogSnapshot(embeddedSchemaCatalogEnvelopeJSON, embeddedSchemaCatalogTools, "schema_catalog/tools")
|
||||
if err != nil {
|
||||
return loadedSchemaCatalog{}, err
|
||||
}
|
||||
return loadSchemaCatalogSnapshot(snapshot)
|
||||
}
|
||||
|
||||
// assembleSchemaCatalogSnapshot merges an envelope document and a directory of
|
||||
// per-product tool shards back into the single-document snapshot shape. Split
|
||||
// from assembleEmbeddedSchemaCatalog so shard failure modes stay testable
|
||||
// against fake filesystems.
|
||||
func assembleSchemaCatalogSnapshot(envelopeJSON []byte, shards fs.FS, dir string) (SchemaCatalogSnapshot, error) {
|
||||
var envelope schemaCatalogEnvelope
|
||||
if err := decodeStrictSchemaJSON(envelopeJSON, &envelope); err != nil {
|
||||
return SchemaCatalogSnapshot{}, fmt.Errorf("decode embedded schema catalog.json: %w", err)
|
||||
}
|
||||
entries, err := fs.ReadDir(shards, dir)
|
||||
if err != nil {
|
||||
return SchemaCatalogSnapshot{}, fmt.Errorf("read embedded schema catalog tools directory: %w", err)
|
||||
}
|
||||
tools := make(map[string]map[string]any, len(entries)*8)
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
|
||||
continue
|
||||
}
|
||||
data, readErr := fs.ReadFile(shards, dir+"/"+entry.Name())
|
||||
if readErr != nil {
|
||||
return SchemaCatalogSnapshot{}, fmt.Errorf("read embedded schema catalog shard %s: %w", entry.Name(), readErr)
|
||||
}
|
||||
var shard schemaCatalogToolShard
|
||||
if err := decodeStrictSchemaJSON(data, &shard); err != nil {
|
||||
return SchemaCatalogSnapshot{}, fmt.Errorf("decode embedded schema catalog shard %s: %w", entry.Name(), err)
|
||||
}
|
||||
for canonical, spec := range shard.Tools {
|
||||
tools[canonical] = spec
|
||||
}
|
||||
}
|
||||
return SchemaCatalogSnapshot{
|
||||
Version: envelope.Version,
|
||||
SurfaceHash: envelope.SurfaceHash,
|
||||
SourceHash: envelope.SourceHash,
|
||||
Catalog: envelope.Catalog,
|
||||
Tools: tools,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func embeddedSchemaCatalogError() error {
|
||||
_ = embeddedSchemaCatalog()
|
||||
return runtimeEmbeddedSchemaCatalogErr
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,762 @@
|
||||
{
|
||||
"product": "devdoc",
|
||||
"tools": {
|
||||
"devdoc.search_open_platform_docs_rag": {
|
||||
"agent_metadata_source": "embedded-skill-metadata",
|
||||
"agent_source_refs": [
|
||||
"CommandRegistry:canonical_path=devdoc.search_open_platform_docs_rag",
|
||||
"Skill:skills/mono/references/products/devdoc.md",
|
||||
"cobra-help:dws devdoc article search",
|
||||
"dws-wukong-envelope@4574f7022c32:dws-wukong-discovery.devdoc.prod.json#devdoc.search_open_platform_docs",
|
||||
"internal/cli/schema_command_registry.json#devdoc.search_open_platform_docs_rag",
|
||||
"internal/cli/schema_hints/imported/wukong.json",
|
||||
"internal/cli/schema_hints/metadata/devdoc.json",
|
||||
"internal/cli/schema_hints/selection/devdoc.json",
|
||||
"internal/cli/schema_mcp_metadata.json#tools.devdoc.search_open_platform_docs_rag",
|
||||
"live-dws-schema:devdoc.search_open_platform_docs_rag",
|
||||
"skills/mono/references/products/devdoc.md",
|
||||
"skills/mono/references/products/simple.md",
|
||||
"structured-hint:internal/cli/schema_hints/imported/wukong.json#devdoc.search_open_platform_docs_rag",
|
||||
"structured-hint:internal/cli/schema_hints/selection-review.json#devdoc.search_open_platform_docs_rag"
|
||||
],
|
||||
"agent_summary": "搜索钉钉开放平台开发文档,返回资料与链接(不生成分析答案)",
|
||||
"agent_summary_source": "dws-agent-selection/devdoc",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"搜索用户业务文档用 drive/wiki/doc,不要用 devdoc",
|
||||
"要执行开放平台应用配置变更时用 dev"
|
||||
],
|
||||
"canonical_path": "devdoc.search_open_platform_docs_rag",
|
||||
"cli_name": "search",
|
||||
"cli_path": "devdoc article search",
|
||||
"confirmation": "not_required",
|
||||
"constraints": {
|
||||
"require_one_of": [
|
||||
[
|
||||
"query",
|
||||
"keyword"
|
||||
]
|
||||
]
|
||||
},
|
||||
"description": "按关键词搜索 open.dingtalk.com 文档,支持分页。默认表格输出,可用 -f json 获取完整响应。",
|
||||
"display": "开放平台文档搜索",
|
||||
"effect": "read",
|
||||
"effect_source": "command-verb",
|
||||
"examples": [
|
||||
"dws devdoc article search --query \"OAuth2 接入\" --format json",
|
||||
"dws devdoc article search --query \"errcode 40078\" --format json"
|
||||
],
|
||||
"field_provenance": {
|
||||
"agent_summary": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/devdoc.json",
|
||||
"value": "搜索钉钉开放平台开发文档,返回资料与链接(不生成分析答案)"
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
|
||||
"source": "internal/cli/schema_hints/selection/devdoc.json",
|
||||
"value": "搜索钉钉开放平台开发文档,返回资料与链接(不生成分析答案)"
|
||||
},
|
||||
"availability": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "Search text may be supplied as positional keyword or public --query; hidden --keyword remains a runtime compatibility alias and is intentionally not published as a parameter. The versioned binding owns query -\u003e keyword.",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/metadata/devdoc.json",
|
||||
"value": "available"
|
||||
},
|
||||
{
|
||||
"precedence": "mcp_fallback",
|
||||
"selected": false,
|
||||
"source": "internal/cli/schema_mcp_metadata.json#tools.devdoc.search_open_platform_docs_rag.interface_ref",
|
||||
"value": "available"
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Search text may be supplied as positional keyword or public --query; hidden --keyword remains a runtime compatibility alias and is intentionally not published as a parameter. The versioned binding owns query -\u003e keyword.",
|
||||
"source": "internal/cli/schema_hints/metadata/devdoc.json",
|
||||
"value": "available"
|
||||
},
|
||||
"avoid_when": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/devdoc.json",
|
||||
"value": [
|
||||
"搜索用户业务文档用 drive/wiki/doc,不要用 devdoc",
|
||||
"要执行开放平台应用配置变更时用 dev"
|
||||
]
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
|
||||
"source": "internal/cli/schema_hints/selection/devdoc.json",
|
||||
"value": [
|
||||
"搜索用户业务文档用 drive/wiki/doc,不要用 devdoc",
|
||||
"要执行开放平台应用配置变更时用 dev"
|
||||
]
|
||||
},
|
||||
"canonical_path": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "command_registry",
|
||||
"selected": true,
|
||||
"source": "reviewed_command_registry",
|
||||
"source_ref": "devdoc article search",
|
||||
"value": "devdoc.search_open_platform_docs_rag"
|
||||
},
|
||||
{
|
||||
"precedence": "reviewed_manual",
|
||||
"review_reason": "Search text may be supplied as positional keyword or public --query; hidden --keyword remains a runtime compatibility alias and is intentionally not published as a parameter. The versioned binding owns query -\u003e keyword.",
|
||||
"selected": false,
|
||||
"source": "reviewed_manual_hint",
|
||||
"source_ref": "devdoc article search",
|
||||
"value": "devdoc.search_open_platform_docs_rag"
|
||||
}
|
||||
],
|
||||
"precedence": "command_registry",
|
||||
"resolution": "registry_identity",
|
||||
"source": "reviewed_command_registry",
|
||||
"source_ref": "devdoc article search",
|
||||
"value": "devdoc.search_open_platform_docs_rag"
|
||||
},
|
||||
"confirmation": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "Search text may be supplied as positional keyword or public --query; hidden --keyword remains a runtime compatibility alias and is intentionally not published as a parameter. The versioned binding owns query -\u003e keyword.",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/metadata/devdoc.json",
|
||||
"value": "not_required"
|
||||
},
|
||||
{
|
||||
"precedence": "inference_or_default",
|
||||
"selected": false,
|
||||
"source": "risk-default",
|
||||
"value": "not_required"
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Search text may be supplied as positional keyword or public --query; hidden --keyword remains a runtime compatibility alias and is intentionally not published as a parameter. The versioned binding owns query -\u003e keyword.",
|
||||
"source": "internal/cli/schema_hints/metadata/devdoc.json",
|
||||
"value": "not_required"
|
||||
},
|
||||
"description": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_help",
|
||||
"selected": true,
|
||||
"source": "cobra_help",
|
||||
"value": "按关键词搜索 open.dingtalk.com 文档,支持分页。默认表格输出,可用 -f json 获取完整响应。"
|
||||
},
|
||||
{
|
||||
"precedence": "mcp_metadata",
|
||||
"selected": false,
|
||||
"source": "mcp_metadata",
|
||||
"value": "根据关键词搜索钉钉开放平台的开发文档,返回匹配的文档条目列表,包含标题、摘要、文档链接和相关标签。搜索结果按相关性排序。适用于开发者在集成或调试过程中快速查找 API 说明、接入指南、错误码解释等技术资料。"
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_help",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_help",
|
||||
"value": "按关键词搜索 open.dingtalk.com 文档,支持分页。默认表格输出,可用 -f json 获取完整响应。"
|
||||
},
|
||||
"effect": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "inference_or_default",
|
||||
"selected": true,
|
||||
"source": "command-verb",
|
||||
"value": "read"
|
||||
}
|
||||
],
|
||||
"precedence": "inference_or_default",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "command-verb",
|
||||
"value": "read"
|
||||
},
|
||||
"examples": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/devdoc.json",
|
||||
"value": [
|
||||
"dws devdoc article search --query \"OAuth2 接入\" --format json",
|
||||
"dws devdoc article search --query \"errcode 40078\" --format json"
|
||||
]
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
|
||||
"source": "internal/cli/schema_hints/selection/devdoc.json",
|
||||
"value": [
|
||||
"dws devdoc article search --query \"OAuth2 接入\" --format json",
|
||||
"dws devdoc article search --query \"errcode 40078\" --format json"
|
||||
]
|
||||
},
|
||||
"idempotency": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "inference_or_default",
|
||||
"selected": true,
|
||||
"source": "effect-default",
|
||||
"value": "idempotent"
|
||||
}
|
||||
],
|
||||
"precedence": "inference_or_default",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "effect-default",
|
||||
"value": "idempotent"
|
||||
},
|
||||
"interface_mode": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "Search text may be supplied as positional keyword or public --query; hidden --keyword remains a runtime compatibility alias and is intentionally not published as a parameter. The versioned binding owns query -\u003e keyword.",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/metadata/devdoc.json",
|
||||
"value": "mcp"
|
||||
},
|
||||
{
|
||||
"precedence": "mcp_fallback",
|
||||
"selected": false,
|
||||
"source": "internal/cli/schema_mcp_metadata.json#tools.devdoc.search_open_platform_docs_rag.interface_ref",
|
||||
"value": "mcp"
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Search text may be supplied as positional keyword or public --query; hidden --keyword remains a runtime compatibility alias and is intentionally not published as a parameter. The versioned binding owns query -\u003e keyword.",
|
||||
"source": "internal/cli/schema_hints/metadata/devdoc.json",
|
||||
"value": "mcp"
|
||||
},
|
||||
"interface_ref": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "imported",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/imported/wukong.json",
|
||||
"value": {
|
||||
"product_id": "devdoc",
|
||||
"rpc_name": "search_open_platform_docs"
|
||||
}
|
||||
},
|
||||
{
|
||||
"precedence": "mcp_fallback",
|
||||
"selected": false,
|
||||
"source": "internal/cli/schema_mcp_metadata.json#tools.devdoc.search_open_platform_docs_rag.interface_ref",
|
||||
"value": "devdoc.search_open_platform_docs"
|
||||
}
|
||||
],
|
||||
"precedence": "imported",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "internal/cli/schema_hints/imported/wukong.json",
|
||||
"value": {
|
||||
"product_id": "devdoc",
|
||||
"rpc_name": "search_open_platform_docs"
|
||||
}
|
||||
},
|
||||
"metadata_source": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "derived_resolution",
|
||||
"selected": true,
|
||||
"source": "metadata_source_resolution",
|
||||
"value": "embedded-mcp-metadata"
|
||||
}
|
||||
],
|
||||
"precedence": "derived_resolution",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "metadata_source_resolution",
|
||||
"value": "embedded-mcp-metadata"
|
||||
},
|
||||
"reviewed": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "Search text may be supplied as positional keyword or public --query; hidden --keyword remains a runtime compatibility alias and is intentionally not published as a parameter. The versioned binding owns query -\u003e keyword.",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/metadata/devdoc.json",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
|
||||
"selected": false,
|
||||
"source": "internal/cli/schema_hints/selection/devdoc.json",
|
||||
"value": true
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Search text may be supplied as positional keyword or public --query; hidden --keyword remains a runtime compatibility alias and is intentionally not published as a parameter. The versioned binding owns query -\u003e keyword.",
|
||||
"source": "internal/cli/schema_hints/metadata/devdoc.json",
|
||||
"value": true
|
||||
},
|
||||
"risk": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "inference_or_default",
|
||||
"selected": true,
|
||||
"source": "effect-default",
|
||||
"value": "low"
|
||||
}
|
||||
],
|
||||
"precedence": "inference_or_default",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "effect-default",
|
||||
"value": "low"
|
||||
},
|
||||
"title": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_help",
|
||||
"selected": true,
|
||||
"source": "cobra_help",
|
||||
"value": "搜索开放平台文档"
|
||||
},
|
||||
{
|
||||
"precedence": "mcp_metadata",
|
||||
"selected": false,
|
||||
"source": "mcp_metadata",
|
||||
"value": "search_open_platform_docs"
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_help",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_help",
|
||||
"value": "搜索开放平台文档"
|
||||
},
|
||||
"use_when": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/devdoc.json",
|
||||
"value": [
|
||||
"查 OpenAPI、字段、错误码、OAuth2、接入指南等开放平台开发问题"
|
||||
]
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
|
||||
"source": "internal/cli/schema_hints/selection/devdoc.json",
|
||||
"value": [
|
||||
"查 OpenAPI、字段、错误码、OAuth2、接入指南等开放平台开发问题"
|
||||
]
|
||||
}
|
||||
},
|
||||
"group": "article",
|
||||
"has_parameters": true,
|
||||
"idempotency": "idempotent",
|
||||
"interface_mode": "mcp",
|
||||
"interface_ref": {
|
||||
"product_id": "devdoc",
|
||||
"rpc_name": "search_open_platform_docs"
|
||||
},
|
||||
"is_alias": false,
|
||||
"metadata_source": "embedded-mcp-metadata",
|
||||
"name": "search_open_platform_docs_rag",
|
||||
"parameter_count": 3,
|
||||
"parameters": {
|
||||
"page": {
|
||||
"default": "1",
|
||||
"description": "页码,默认 1",
|
||||
"field_provenance": {
|
||||
"description": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_usage",
|
||||
"value": "页码,默认 1"
|
||||
},
|
||||
{
|
||||
"precedence": "mcp_metadata",
|
||||
"selected": false,
|
||||
"source": "mcp_metadata",
|
||||
"value": "分页页码"
|
||||
},
|
||||
{
|
||||
"precedence": "default",
|
||||
"selected": false,
|
||||
"source": "default",
|
||||
"value": ""
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_usage",
|
||||
"value": "页码,默认 1"
|
||||
},
|
||||
"interface_type": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "mcp_metadata",
|
||||
"selected": true,
|
||||
"source": "mcp_metadata",
|
||||
"value": "number"
|
||||
},
|
||||
{
|
||||
"precedence": "fallback",
|
||||
"selected": false,
|
||||
"source": "cobra_flag_type",
|
||||
"value": "string"
|
||||
}
|
||||
],
|
||||
"precedence": "mcp_metadata",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "mcp_metadata",
|
||||
"value": "number"
|
||||
},
|
||||
"property": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "inference",
|
||||
"selected": true,
|
||||
"source": "flag_name_inference",
|
||||
"value": "page"
|
||||
}
|
||||
],
|
||||
"precedence": "inference",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "flag_name_inference",
|
||||
"value": "page"
|
||||
},
|
||||
"required": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_nonzero_default",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"precedence": "default",
|
||||
"selected": false,
|
||||
"source": "default",
|
||||
"value": false
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_nonzero_default",
|
||||
"value": false
|
||||
},
|
||||
"required_when": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "default",
|
||||
"selected": true,
|
||||
"source": "default",
|
||||
"value": ""
|
||||
}
|
||||
],
|
||||
"precedence": "default",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "default",
|
||||
"value": ""
|
||||
},
|
||||
"type": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_flag_type",
|
||||
"value": "string"
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_flag_type",
|
||||
"value": "string"
|
||||
}
|
||||
},
|
||||
"interface_description": "分页页码",
|
||||
"interface_type": "number",
|
||||
"property": "page",
|
||||
"required": false,
|
||||
"type": "string"
|
||||
},
|
||||
"query": {
|
||||
"description": "搜索关键词 (必填)",
|
||||
"field_provenance": {
|
||||
"description": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_usage",
|
||||
"value": "搜索关键词 (必填)"
|
||||
},
|
||||
{
|
||||
"precedence": "mcp_metadata",
|
||||
"selected": false,
|
||||
"source": "mcp_metadata",
|
||||
"value": "搜索关键词"
|
||||
},
|
||||
{
|
||||
"precedence": "default",
|
||||
"selected": false,
|
||||
"source": "default",
|
||||
"value": ""
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_usage",
|
||||
"value": "搜索关键词 (必填)"
|
||||
},
|
||||
"property": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "versioned_binding",
|
||||
"selected": true,
|
||||
"source": "versioned_parameter_binding",
|
||||
"value": "keyword"
|
||||
},
|
||||
{
|
||||
"precedence": "inference",
|
||||
"selected": false,
|
||||
"source": "flag_name_inference",
|
||||
"value": "query"
|
||||
}
|
||||
],
|
||||
"precedence": "versioned_binding",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "versioned_parameter_binding",
|
||||
"value": "keyword"
|
||||
},
|
||||
"required": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_manual",
|
||||
"review_reason": "Search text may be supplied as positional keyword or public --query; hidden --keyword remains a runtime compatibility alias and is intentionally not published as a parameter. The versioned binding owns query -\u003e keyword.",
|
||||
"selected": true,
|
||||
"source": "reviewed_manual_hint",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"precedence": "command_constraint",
|
||||
"selected": false,
|
||||
"source": "require_one_of_constraint",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"precedence": "mcp_metadata",
|
||||
"selected": false,
|
||||
"source": "mcp_metadata",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"precedence": "inference",
|
||||
"selected": false,
|
||||
"source": "usage_required_inference",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"precedence": "default",
|
||||
"selected": false,
|
||||
"source": "default",
|
||||
"value": false
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_manual",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Search text may be supplied as positional keyword or public --query; hidden --keyword remains a runtime compatibility alias and is intentionally not published as a parameter. The versioned binding owns query -\u003e keyword.",
|
||||
"source": "reviewed_manual_hint",
|
||||
"value": false
|
||||
},
|
||||
"required_when": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "default",
|
||||
"selected": true,
|
||||
"source": "default",
|
||||
"value": ""
|
||||
}
|
||||
],
|
||||
"precedence": "default",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "default",
|
||||
"value": ""
|
||||
},
|
||||
"type": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_flag_type",
|
||||
"value": "string"
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_flag_type",
|
||||
"value": "string"
|
||||
}
|
||||
},
|
||||
"interface_description": "搜索关键词",
|
||||
"property": "keyword",
|
||||
"required": false,
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"default": "10",
|
||||
"description": "每页数量,默认 10",
|
||||
"field_provenance": {
|
||||
"description": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_usage",
|
||||
"value": "每页数量,默认 10"
|
||||
},
|
||||
{
|
||||
"precedence": "mcp_metadata",
|
||||
"selected": false,
|
||||
"source": "mcp_metadata",
|
||||
"value": "分页大小"
|
||||
},
|
||||
{
|
||||
"precedence": "default",
|
||||
"selected": false,
|
||||
"source": "default",
|
||||
"value": ""
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_usage",
|
||||
"value": "每页数量,默认 10"
|
||||
},
|
||||
"interface_type": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "mcp_metadata",
|
||||
"selected": true,
|
||||
"source": "mcp_metadata",
|
||||
"value": "number"
|
||||
},
|
||||
{
|
||||
"precedence": "fallback",
|
||||
"selected": false,
|
||||
"source": "cobra_flag_type",
|
||||
"value": "string"
|
||||
}
|
||||
],
|
||||
"precedence": "mcp_metadata",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "mcp_metadata",
|
||||
"value": "number"
|
||||
},
|
||||
"property": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "inference",
|
||||
"selected": true,
|
||||
"source": "flag_name_inference",
|
||||
"value": "size"
|
||||
}
|
||||
],
|
||||
"precedence": "inference",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "flag_name_inference",
|
||||
"value": "size"
|
||||
},
|
||||
"required": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_nonzero_default",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"precedence": "default",
|
||||
"selected": false,
|
||||
"source": "default",
|
||||
"value": false
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_nonzero_default",
|
||||
"value": false
|
||||
},
|
||||
"required_when": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "default",
|
||||
"selected": true,
|
||||
"source": "default",
|
||||
"value": ""
|
||||
}
|
||||
],
|
||||
"precedence": "default",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "default",
|
||||
"value": ""
|
||||
},
|
||||
"type": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_flag_type",
|
||||
"value": "string"
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_flag_type",
|
||||
"value": "string"
|
||||
}
|
||||
},
|
||||
"interface_description": "分页大小",
|
||||
"interface_type": "number",
|
||||
"property": "size",
|
||||
"required": false,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"path": "devdoc.search_open_platform_docs_rag",
|
||||
"positionals": [
|
||||
{
|
||||
"description": "搜索关键词;也可通过 --query 传入",
|
||||
"index": 0,
|
||||
"name": "keyword",
|
||||
"required": false,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"primary_cli_path": "devdoc article search",
|
||||
"product_id": "devdoc",
|
||||
"reviewed": true,
|
||||
"risk": "low",
|
||||
"source": "reviewed_command_registry",
|
||||
"title": "搜索开放平台文档",
|
||||
"use_when": [
|
||||
"查 OpenAPI、字段、错误码、OAuth2、接入指南等开放平台开发问题"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,360 @@
|
||||
{
|
||||
"product": "live",
|
||||
"tools": {
|
||||
"live.get_my_lives": {
|
||||
"agent_metadata_source": "embedded-skill-metadata",
|
||||
"agent_source_refs": [
|
||||
"CommandRegistry:canonical_path=live.get_my_lives",
|
||||
"Skill:skills/multi/dingtalk-live/SKILL.md",
|
||||
"cobra-help:dws live stream list",
|
||||
"dws-wukong-envelope@4574f7022c32:dws-wukong-discovery.live.prod.json#live.get_my_lives",
|
||||
"internal/cli/schema_command_registry.json#live.get_my_lives",
|
||||
"internal/cli/schema_hints/imported/wukong.json",
|
||||
"internal/cli/schema_hints/metadata/live.json",
|
||||
"internal/cli/schema_hints/selection/live.json",
|
||||
"internal/cli/schema_mcp_metadata.json#tools.live.get_my_lives",
|
||||
"live-dws-schema:live.get_my_lives",
|
||||
"skills/mono/references/products/live.md",
|
||||
"structured-hint:internal/cli/schema_hints/imported/wukong.json#live.get_my_lives",
|
||||
"structured-hint:internal/cli/schema_hints/selection-review.json#live.get_my_lives"
|
||||
],
|
||||
"agent_summary": "查看当前用户发起的直播列表与基础统计",
|
||||
"agent_summary_source": "dws-agent-selection/live",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"需要创建/开播/结束直播时不要使用;当前公开面仅列表查询"
|
||||
],
|
||||
"canonical_path": "live.get_my_lives",
|
||||
"cli_name": "list",
|
||||
"cli_path": "live stream list",
|
||||
"confirmation": "not_required",
|
||||
"description": "查看我的直播列表",
|
||||
"display": "直播列表 / 信息",
|
||||
"effect": "read",
|
||||
"effect_source": "command-verb",
|
||||
"examples": [
|
||||
"dws live stream list"
|
||||
],
|
||||
"field_provenance": {
|
||||
"agent_summary": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/live.json",
|
||||
"value": "查看当前用户发起的直播列表与基础统计"
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
|
||||
"source": "internal/cli/schema_hints/selection/live.json",
|
||||
"value": "查看当前用户发起的直播列表与基础统计"
|
||||
},
|
||||
"availability": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "dws-tool-metadata/live marks this tool as reviewed",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/metadata/live.json",
|
||||
"value": "available"
|
||||
},
|
||||
{
|
||||
"precedence": "mcp_fallback",
|
||||
"selected": false,
|
||||
"source": "internal/cli/schema_mcp_metadata.json#tools.live.get_my_lives.interface_ref",
|
||||
"value": "available"
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "dws-tool-metadata/live marks this tool as reviewed",
|
||||
"source": "internal/cli/schema_hints/metadata/live.json",
|
||||
"value": "available"
|
||||
},
|
||||
"avoid_when": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/live.json",
|
||||
"value": [
|
||||
"需要创建/开播/结束直播时不要使用;当前公开面仅列表查询"
|
||||
]
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
|
||||
"source": "internal/cli/schema_hints/selection/live.json",
|
||||
"value": [
|
||||
"需要创建/开播/结束直播时不要使用;当前公开面仅列表查询"
|
||||
]
|
||||
},
|
||||
"canonical_path": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "command_registry",
|
||||
"selected": true,
|
||||
"source": "reviewed_command_registry",
|
||||
"source_ref": "live stream list",
|
||||
"value": "live.get_my_lives"
|
||||
}
|
||||
],
|
||||
"precedence": "command_registry",
|
||||
"resolution": "registry_identity",
|
||||
"source": "reviewed_command_registry",
|
||||
"source_ref": "live stream list",
|
||||
"value": "live.get_my_lives"
|
||||
},
|
||||
"confirmation": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "inference_or_default",
|
||||
"selected": true,
|
||||
"source": "risk-default",
|
||||
"value": "not_required"
|
||||
}
|
||||
],
|
||||
"precedence": "inference_or_default",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "risk-default",
|
||||
"value": "not_required"
|
||||
},
|
||||
"description": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_help",
|
||||
"selected": true,
|
||||
"source": "cobra_help",
|
||||
"value": "查看我的直播列表"
|
||||
},
|
||||
{
|
||||
"precedence": "mcp_metadata",
|
||||
"selected": false,
|
||||
"source": "mcp_metadata",
|
||||
"value": "获取当前用户发起的直播列表,包含每场直播的基础信息与统计数据。返回字段包括:直播标题、封面图 URL、直播 ID、主播昵称、开始/结束时间戳、播放时长(毫秒)、观看量(UV)及直播状态(如已结束)。数据按分页返回,支持判断是否还有更多内容。适用于个人直播管理、历史记录查看等场景,仅展示当前用户有权限访问的直播内容。"
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_help",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_help",
|
||||
"value": "查看我的直播列表"
|
||||
},
|
||||
"effect": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "inference_or_default",
|
||||
"selected": true,
|
||||
"source": "command-verb",
|
||||
"value": "read"
|
||||
}
|
||||
],
|
||||
"precedence": "inference_or_default",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "command-verb",
|
||||
"value": "read"
|
||||
},
|
||||
"examples": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/live.json",
|
||||
"value": [
|
||||
"dws live stream list"
|
||||
]
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
|
||||
"source": "internal/cli/schema_hints/selection/live.json",
|
||||
"value": [
|
||||
"dws live stream list"
|
||||
]
|
||||
},
|
||||
"idempotency": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "inference_or_default",
|
||||
"selected": true,
|
||||
"source": "effect-default",
|
||||
"value": "idempotent"
|
||||
}
|
||||
],
|
||||
"precedence": "inference_or_default",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "effect-default",
|
||||
"value": "idempotent"
|
||||
},
|
||||
"interface_mode": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "dws-tool-metadata/live marks this tool as reviewed",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/metadata/live.json",
|
||||
"value": "mcp"
|
||||
},
|
||||
{
|
||||
"precedence": "mcp_fallback",
|
||||
"selected": false,
|
||||
"source": "internal/cli/schema_mcp_metadata.json#tools.live.get_my_lives.interface_ref",
|
||||
"value": "mcp"
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "dws-tool-metadata/live marks this tool as reviewed",
|
||||
"source": "internal/cli/schema_hints/metadata/live.json",
|
||||
"value": "mcp"
|
||||
},
|
||||
"interface_ref": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "imported",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/imported/wukong.json",
|
||||
"value": {
|
||||
"product_id": "live",
|
||||
"rpc_name": "get_my_lives"
|
||||
}
|
||||
},
|
||||
{
|
||||
"precedence": "mcp_fallback",
|
||||
"selected": false,
|
||||
"source": "internal/cli/schema_mcp_metadata.json#tools.live.get_my_lives.interface_ref",
|
||||
"value": "live.get_my_lives"
|
||||
}
|
||||
],
|
||||
"precedence": "imported",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "internal/cli/schema_hints/imported/wukong.json",
|
||||
"value": {
|
||||
"product_id": "live",
|
||||
"rpc_name": "get_my_lives"
|
||||
}
|
||||
},
|
||||
"metadata_source": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "derived_resolution",
|
||||
"selected": true,
|
||||
"source": "metadata_source_resolution",
|
||||
"value": "embedded-mcp-metadata"
|
||||
}
|
||||
],
|
||||
"precedence": "derived_resolution",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "metadata_source_resolution",
|
||||
"value": "embedded-mcp-metadata"
|
||||
},
|
||||
"reviewed": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "dws-tool-metadata/live marks this tool as reviewed",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/metadata/live.json",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
|
||||
"selected": false,
|
||||
"source": "internal/cli/schema_hints/selection/live.json",
|
||||
"value": true
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "dws-tool-metadata/live marks this tool as reviewed",
|
||||
"source": "internal/cli/schema_hints/metadata/live.json",
|
||||
"value": true
|
||||
},
|
||||
"risk": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "inference_or_default",
|
||||
"selected": true,
|
||||
"source": "effect-default",
|
||||
"value": "low"
|
||||
}
|
||||
],
|
||||
"precedence": "inference_or_default",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "effect-default",
|
||||
"value": "low"
|
||||
},
|
||||
"title": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_help",
|
||||
"selected": true,
|
||||
"source": "cobra_help",
|
||||
"value": "查看我的直播列表"
|
||||
},
|
||||
{
|
||||
"precedence": "mcp_metadata",
|
||||
"selected": false,
|
||||
"source": "mcp_metadata",
|
||||
"value": "获取当前用户发起的直播列表"
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_help",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_help",
|
||||
"value": "查看我的直播列表"
|
||||
},
|
||||
"use_when": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/live.json",
|
||||
"value": [
|
||||
"用户要看自己发起过的直播、状态或观看量等列表信息"
|
||||
]
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
|
||||
"source": "internal/cli/schema_hints/selection/live.json",
|
||||
"value": [
|
||||
"用户要看自己发起过的直播、状态或观看量等列表信息"
|
||||
]
|
||||
}
|
||||
},
|
||||
"group": "stream",
|
||||
"has_parameters": false,
|
||||
"idempotency": "idempotent",
|
||||
"interface_mode": "mcp",
|
||||
"interface_ref": {
|
||||
"product_id": "live",
|
||||
"rpc_name": "get_my_lives"
|
||||
},
|
||||
"is_alias": false,
|
||||
"metadata_source": "embedded-mcp-metadata",
|
||||
"name": "get_my_lives",
|
||||
"parameter_count": 0,
|
||||
"parameters": {},
|
||||
"path": "live.get_my_lives",
|
||||
"primary_cli_path": "live stream list",
|
||||
"product_id": "live",
|
||||
"reviewed": true,
|
||||
"risk": "low",
|
||||
"source": "reviewed_command_registry",
|
||||
"title": "查看我的直播列表",
|
||||
"use_when": [
|
||||
"用户要看自己发起过的直播、状态或观看量等列表信息"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,337 @@
|
||||
{
|
||||
"product": "mcp",
|
||||
"tools": {
|
||||
"mcp.url_get": {
|
||||
"agent_metadata_source": "embedded-skill-metadata",
|
||||
"agent_source_refs": [
|
||||
"cobra-help:dws mcp url get --help",
|
||||
"internal/app/mcp_url_command.go",
|
||||
"internal/cli/schema_command_registry.json#mcp.url_get",
|
||||
"internal/cli/schema_hints/metadata/mcp.json",
|
||||
"internal/cli/schema_hints/selection/mcp.json",
|
||||
"pkg/edition/default.go#openSupplementServers"
|
||||
],
|
||||
"agent_summary": "按 MCP 市场 mcpId 获取当前用户和组织可用的 Streamable HTTP 地址",
|
||||
"agent_summary_source": "dws-agent-selection/mcp",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"只是查询 DWS 已公开命令或参数时使用 dws schema",
|
||||
"用户要求把返回的凭据 URL 发送到群聊、文档、邮件、日志或代码仓库时不要执行或传播"
|
||||
],
|
||||
"canonical_path": "mcp.url_get",
|
||||
"cli_name": "get",
|
||||
"cli_path": "mcp url get",
|
||||
"confirmation": "not_required",
|
||||
"description": "输入 MCP 市场 mcpId,返回以当前用户和组织身份访问该 MCP 的 Streamable HTTP 服务地址。\n\n安全提示:返回的 mcpURL 和 mcpJSON 可能包含身份凭据,仅限个人使用,请勿分享到群聊、文档、邮件、代码仓库或日志。",
|
||||
"display": "管理 MCP 服务连接信息",
|
||||
"effect": "read",
|
||||
"effect_source": "agent-hint",
|
||||
"examples": [
|
||||
"dws mcp url get 10043 --format json"
|
||||
],
|
||||
"field_provenance": {
|
||||
"agent_summary": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "正式公开 MCP URL 解析命令的 Agent 选型文案,并明确凭据 URL 只能返回给当前用户、不得二次传播。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/mcp.json",
|
||||
"value": "按 MCP 市场 mcpId 获取当前用户和组织可用的 Streamable HTTP 地址"
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "正式公开 MCP URL 解析命令的 Agent 选型文案,并明确凭据 URL 只能返回给当前用户、不得二次传播。",
|
||||
"source": "internal/cli/schema_hints/selection/mcp.json",
|
||||
"value": "按 MCP 市场 mcpId 获取当前用户和组织可用的 Streamable HTTP 地址"
|
||||
},
|
||||
"availability": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/metadata/mcp.json",
|
||||
"value": "available"
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
|
||||
"source": "internal/cli/schema_hints/metadata/mcp.json",
|
||||
"value": "available"
|
||||
},
|
||||
"avoid_when": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "正式公开 MCP URL 解析命令的 Agent 选型文案,并明确凭据 URL 只能返回给当前用户、不得二次传播。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/mcp.json",
|
||||
"value": [
|
||||
"只是查询 DWS 已公开命令或参数时使用 dws schema",
|
||||
"用户要求把返回的凭据 URL 发送到群聊、文档、邮件、日志或代码仓库时不要执行或传播"
|
||||
]
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "正式公开 MCP URL 解析命令的 Agent 选型文案,并明确凭据 URL 只能返回给当前用户、不得二次传播。",
|
||||
"source": "internal/cli/schema_hints/selection/mcp.json",
|
||||
"value": [
|
||||
"只是查询 DWS 已公开命令或参数时使用 dws schema",
|
||||
"用户要求把返回的凭据 URL 发送到群聊、文档、邮件、日志或代码仓库时不要执行或传播"
|
||||
]
|
||||
},
|
||||
"canonical_path": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "command_registry",
|
||||
"selected": true,
|
||||
"source": "reviewed_command_registry",
|
||||
"source_ref": "mcp url get",
|
||||
"value": "mcp.url_get"
|
||||
}
|
||||
],
|
||||
"precedence": "command_registry",
|
||||
"resolution": "registry_identity",
|
||||
"source": "reviewed_command_registry",
|
||||
"source_ref": "mcp url get",
|
||||
"value": "mcp.url_get"
|
||||
},
|
||||
"confirmation": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/metadata/mcp.json",
|
||||
"value": "not_required"
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
|
||||
"source": "internal/cli/schema_hints/metadata/mcp.json",
|
||||
"value": "not_required"
|
||||
},
|
||||
"description": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_help",
|
||||
"selected": true,
|
||||
"source": "cobra_help",
|
||||
"value": "输入 MCP 市场 mcpId,返回以当前用户和组织身份访问该 MCP 的 Streamable HTTP 服务地址。\n\n安全提示:返回的 mcpURL 和 mcpJSON 可能包含身份凭据,仅限个人使用,请勿分享到群聊、文档、邮件、代码仓库或日志。"
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_help",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_help",
|
||||
"value": "输入 MCP 市场 mcpId,返回以当前用户和组织身份访问该 MCP 的 Streamable HTTP 服务地址。\n\n安全提示:返回的 mcpURL 和 mcpJSON 可能包含身份凭据,仅限个人使用,请勿分享到群聊、文档、邮件、代码仓库或日志。"
|
||||
},
|
||||
"effect": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/metadata/mcp.json",
|
||||
"value": "read"
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
|
||||
"source": "internal/cli/schema_hints/metadata/mcp.json",
|
||||
"value": "read"
|
||||
},
|
||||
"examples": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "正式公开 MCP URL 解析命令的 Agent 选型文案,并明确凭据 URL 只能返回给当前用户、不得二次传播。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/mcp.json",
|
||||
"value": [
|
||||
"dws mcp url get 10043 --format json"
|
||||
]
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "正式公开 MCP URL 解析命令的 Agent 选型文案,并明确凭据 URL 只能返回给当前用户、不得二次传播。",
|
||||
"source": "internal/cli/schema_hints/selection/mcp.json",
|
||||
"value": [
|
||||
"dws mcp url get 10043 --format json"
|
||||
]
|
||||
},
|
||||
"idempotency": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/metadata/mcp.json",
|
||||
"value": "idempotent"
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
|
||||
"source": "internal/cli/schema_hints/metadata/mcp.json",
|
||||
"value": "idempotent"
|
||||
},
|
||||
"interface_mode": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/metadata/mcp.json",
|
||||
"value": "composite"
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
|
||||
"source": "internal/cli/schema_hints/metadata/mcp.json",
|
||||
"value": "composite"
|
||||
},
|
||||
"interface_reason": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/metadata/mcp.json",
|
||||
"value": "Reviewed unpinned remote adapter: the public CLI wrapper calls the helper-only mcp-meta/get_mcp_server_url endpoint, which is intentionally absent from the public product catalog and pinned MCP metadata."
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
|
||||
"source": "internal/cli/schema_hints/metadata/mcp.json",
|
||||
"value": "Reviewed unpinned remote adapter: the public CLI wrapper calls the helper-only mcp-meta/get_mcp_server_url endpoint, which is intentionally absent from the public product catalog and pinned MCP metadata."
|
||||
},
|
||||
"interface_ref": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "final interface mode composite forbids a direct MCP interface_ref",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/metadata/mcp.json",
|
||||
"value": null
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "interface_disposition_matrix",
|
||||
"review_reason": "final interface mode composite forbids a direct MCP interface_ref",
|
||||
"source": "internal/cli/schema_hints/metadata/mcp.json",
|
||||
"value": null
|
||||
},
|
||||
"reviewed": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/metadata/mcp.json",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "正式公开 MCP URL 解析命令的 Agent 选型文案,并明确凭据 URL 只能返回给当前用户、不得二次传播。",
|
||||
"selected": false,
|
||||
"source": "internal/cli/schema_hints/selection/mcp.json",
|
||||
"value": true
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
|
||||
"source": "internal/cli/schema_hints/metadata/mcp.json",
|
||||
"value": true
|
||||
},
|
||||
"risk": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/metadata/mcp.json",
|
||||
"value": "medium"
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Expose the reviewed MCP URL resolver through the public CLI and Agent Schema while preserving the helper endpoint as a non-product supplemental server. The returned URL contains user- and organization-scoped credentials and must be treated as sensitive output.",
|
||||
"source": "internal/cli/schema_hints/metadata/mcp.json",
|
||||
"value": "medium"
|
||||
},
|
||||
"title": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_help",
|
||||
"selected": true,
|
||||
"source": "cobra_help",
|
||||
"value": "按 mcpId 获取 MCP 的 Streamable HTTP 服务地址"
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_help",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_help",
|
||||
"value": "按 mcpId 获取 MCP 的 Streamable HTTP 服务地址"
|
||||
},
|
||||
"use_when": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "正式公开 MCP URL 解析命令的 Agent 选型文案,并明确凭据 URL 只能返回给当前用户、不得二次传播。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/mcp.json",
|
||||
"value": [
|
||||
"已知钉钉 MCP 市场 mcpId,需要获得当前身份可用的 Streamable HTTP 连接地址"
|
||||
]
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "正式公开 MCP URL 解析命令的 Agent 选型文案,并明确凭据 URL 只能返回给当前用户、不得二次传播。",
|
||||
"source": "internal/cli/schema_hints/selection/mcp.json",
|
||||
"value": [
|
||||
"已知钉钉 MCP 市场 mcpId,需要获得当前身份可用的 Streamable HTTP 连接地址"
|
||||
]
|
||||
}
|
||||
},
|
||||
"group": "url",
|
||||
"has_parameters": false,
|
||||
"idempotency": "idempotent",
|
||||
"interface_mode": "composite",
|
||||
"interface_reason": "Reviewed unpinned remote adapter: the public CLI wrapper calls the helper-only mcp-meta/get_mcp_server_url endpoint, which is intentionally absent from the public product catalog and pinned MCP metadata.",
|
||||
"is_alias": false,
|
||||
"name": "url_get",
|
||||
"parameter_count": 0,
|
||||
"parameters": {},
|
||||
"path": "mcp.url_get",
|
||||
"positionals": [
|
||||
{
|
||||
"description": "钉钉 MCP 市场中的 mcpId",
|
||||
"index": 0,
|
||||
"name": "mcp_id",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"primary_cli_path": "mcp url get",
|
||||
"product_id": "mcp",
|
||||
"reviewed": true,
|
||||
"risk": "medium",
|
||||
"source": "reviewed_command_registry",
|
||||
"title": "按 mcpId 获取 MCP 的 Streamable HTTP 服务地址",
|
||||
"use_when": [
|
||||
"已知钉钉 MCP 市场 mcpId,需要获得当前身份可用的 Streamable HTTP 连接地址"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,373 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// schema_catalog.json is generated, but every command entry it delivers to
|
||||
// Agents must follow one unified closed structure: a fixed required core plus
|
||||
// a whitelisted optional set. Any new field must be registered here first,
|
||||
// which keeps the delivered command data structure uniform across products.
|
||||
// Command identity (names/paths) is owned by schema_command_registry.json and
|
||||
// is deliberately out of scope for this validator.
|
||||
|
||||
const schemaCatalogStructureMaxViolations = 25
|
||||
|
||||
// schemaCatalogToolRequiredKeys is the required core present on every tool
|
||||
// entry. Keep sorted; ValidateCatalogStructure rejects entries missing any of
|
||||
// these.
|
||||
var schemaCatalogToolRequiredKeys = []string{
|
||||
"agent_metadata_source",
|
||||
"agent_source_refs",
|
||||
"agent_summary",
|
||||
"agent_summary_source",
|
||||
"availability",
|
||||
"avoid_when",
|
||||
"canonical_path",
|
||||
"cli_name",
|
||||
"cli_path",
|
||||
"confirmation",
|
||||
"description",
|
||||
"display",
|
||||
"effect",
|
||||
"effect_source",
|
||||
"examples",
|
||||
"field_provenance",
|
||||
"has_parameters",
|
||||
"idempotency",
|
||||
"interface_mode",
|
||||
"is_alias",
|
||||
"name",
|
||||
"parameter_count",
|
||||
"parameters",
|
||||
"path",
|
||||
"primary_cli_path",
|
||||
"product_id",
|
||||
"reviewed",
|
||||
"risk",
|
||||
"source",
|
||||
"title",
|
||||
"use_when",
|
||||
}
|
||||
|
||||
// schemaCatalogToolOptionalKeys is the optional whitelist. interface_ref and
|
||||
// interface_reason are mutually exclusive and gated by interface_mode.
|
||||
var schemaCatalogToolOptionalKeys = []string{
|
||||
"aliases",
|
||||
"constraints",
|
||||
"dry_run",
|
||||
"group",
|
||||
"interface_reason",
|
||||
"interface_ref",
|
||||
"metadata_source",
|
||||
"positionals",
|
||||
}
|
||||
|
||||
var schemaCatalogToolEnums = map[string][]string{
|
||||
"effect": {"read", "write", "destructive"},
|
||||
"risk": {"low", "medium", "high"},
|
||||
"confirmation": {"not_required", "user_required"},
|
||||
"interface_mode": {InterfaceModeMCP, InterfaceModeComposite, InterfaceModeLocal},
|
||||
"availability": {InterfaceAvailable, InterfaceUnavailable},
|
||||
}
|
||||
|
||||
// schemaCatalogParamRequiredKeys is the required core of every parameter.
|
||||
var schemaCatalogParamRequiredKeys = []string{
|
||||
"description",
|
||||
"field_provenance",
|
||||
"required",
|
||||
"type",
|
||||
}
|
||||
|
||||
// schemaCatalogParamOptionalKeys is the parameter optional whitelist.
|
||||
var schemaCatalogParamOptionalKeys = []string{
|
||||
"cli_required",
|
||||
"default",
|
||||
"enum",
|
||||
"example",
|
||||
"format",
|
||||
"interface_description",
|
||||
"interface_type",
|
||||
"property",
|
||||
"required_when",
|
||||
}
|
||||
|
||||
var schemaCatalogParamTypes = []string{"string", "integer", "number", "boolean", "array", "object"}
|
||||
|
||||
type schemaCatalogStructureViolation struct {
|
||||
tool string
|
||||
message string
|
||||
}
|
||||
|
||||
// ValidateCatalogStructure checks that every tool entry in a schema_catalog
|
||||
// snapshot conforms to the unified command data structure. It returns an
|
||||
// error aggregating up to schemaCatalogStructureMaxViolations violations.
|
||||
func ValidateCatalogStructure(data []byte) error {
|
||||
var snapshot struct {
|
||||
Version int `json:"version"`
|
||||
Tools map[string]map[string]any `json:"tools"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &snapshot); err != nil {
|
||||
return fmt.Errorf("decode schema catalog: %w", err)
|
||||
}
|
||||
if snapshot.Version != SchemaCatalogSnapshotVersion {
|
||||
return fmt.Errorf("schema catalog version = %d, want %d", snapshot.Version, SchemaCatalogSnapshotVersion)
|
||||
}
|
||||
if len(snapshot.Tools) == 0 {
|
||||
return fmt.Errorf("schema catalog has no tools")
|
||||
}
|
||||
|
||||
violations := []schemaCatalogStructureViolation{}
|
||||
for toolID, entry := range snapshot.Tools {
|
||||
validateCatalogToolEntry(toolID, entry, &violations)
|
||||
}
|
||||
if len(violations) == 0 {
|
||||
return nil
|
||||
}
|
||||
sort.Slice(violations, func(i, j int) bool {
|
||||
if violations[i].tool != violations[j].tool {
|
||||
return violations[i].tool < violations[j].tool
|
||||
}
|
||||
return violations[i].message < violations[j].message
|
||||
})
|
||||
var b strings.Builder
|
||||
shown := violations
|
||||
truncated := 0
|
||||
if len(shown) > schemaCatalogStructureMaxViolations {
|
||||
truncated = len(shown) - schemaCatalogStructureMaxViolations
|
||||
shown = shown[:schemaCatalogStructureMaxViolations]
|
||||
}
|
||||
for _, v := range shown {
|
||||
fmt.Fprintf(&b, "\n %s: %s", v.tool, v.message)
|
||||
}
|
||||
if truncated > 0 {
|
||||
fmt.Fprintf(&b, "\n ... and %d more violations", truncated)
|
||||
}
|
||||
return fmt.Errorf("schema catalog entries violate the unified command structure (%d total):%s", len(violations), b.String())
|
||||
}
|
||||
|
||||
func validateCatalogToolEntry(toolID string, entry map[string]any, violations *[]schemaCatalogStructureViolation) {
|
||||
report := func(format string, args ...any) {
|
||||
*violations = append(*violations, schemaCatalogStructureViolation{
|
||||
tool: toolID,
|
||||
message: fmt.Sprintf(format, args...),
|
||||
})
|
||||
}
|
||||
|
||||
allowed := make(map[string]bool, len(schemaCatalogToolRequiredKeys)+len(schemaCatalogToolOptionalKeys))
|
||||
for _, k := range schemaCatalogToolRequiredKeys {
|
||||
allowed[k] = true
|
||||
}
|
||||
for _, k := range schemaCatalogToolOptionalKeys {
|
||||
allowed[k] = true
|
||||
}
|
||||
for key := range entry {
|
||||
if !allowed[key] {
|
||||
report("unknown field %q (register it in schema_catalog_structure.go first)", key)
|
||||
}
|
||||
}
|
||||
|
||||
stringKeys := []string{
|
||||
"agent_metadata_source", "agent_summary", "agent_summary_source",
|
||||
"availability", "canonical_path", "cli_name", "cli_path", "confirmation",
|
||||
"description", "display", "effect", "effect_source", "idempotency",
|
||||
"interface_mode", "name", "path", "primary_cli_path", "product_id",
|
||||
"risk", "source", "title",
|
||||
}
|
||||
for _, key := range stringKeys {
|
||||
if !requireNonEmptyString(entry, key) {
|
||||
report("field %q must be a non-empty string", key)
|
||||
}
|
||||
}
|
||||
stringArrayKeys := []string{"agent_source_refs", "avoid_when", "examples", "use_when"}
|
||||
for _, key := range stringArrayKeys {
|
||||
if !requireStringArray(entry, key) {
|
||||
report("field %q must be an array of strings", key)
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"has_parameters", "is_alias", "reviewed"} {
|
||||
if _, ok := entry[key].(bool); !ok {
|
||||
report("field %q must be a boolean", key)
|
||||
}
|
||||
}
|
||||
if _, ok := entry["field_provenance"].(map[string]any); !ok {
|
||||
report("field %q must be an object", "field_provenance")
|
||||
}
|
||||
for field, values := range schemaCatalogToolEnums {
|
||||
raw, ok := entry[field].(string)
|
||||
if !ok {
|
||||
continue // already reported as missing/typed above
|
||||
}
|
||||
if !stringSliceContains(values, raw) {
|
||||
report("field %q = %q, want one of {%s}", field, raw, strings.Join(values, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
parameters, paramsOK := entry["parameters"].(map[string]any)
|
||||
if !paramsOK {
|
||||
report("field %q must be an object", "parameters")
|
||||
parameters = nil
|
||||
}
|
||||
count, countOK := entry["parameter_count"].(float64)
|
||||
if !countOK {
|
||||
report("field %q must be a number", "parameter_count")
|
||||
} else if paramsOK && int(count) != len(parameters) {
|
||||
report("parameter_count = %d, want %d (len(parameters))", int(count), len(parameters))
|
||||
}
|
||||
if has, ok := entry["has_parameters"].(bool); ok && paramsOK && has != (len(parameters) > 0) {
|
||||
report("has_parameters = %v, want %v (len(parameters) > 0)", has, len(parameters) > 0)
|
||||
}
|
||||
|
||||
validateCatalogInterface(toolID, entry, violations)
|
||||
for paramName, raw := range parameters {
|
||||
param, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
report("parameter %q must be an object", paramName)
|
||||
continue
|
||||
}
|
||||
validateCatalogParam(toolID, paramName, param, violations)
|
||||
}
|
||||
}
|
||||
|
||||
func validateCatalogInterface(toolID string, entry map[string]any, violations *[]schemaCatalogStructureViolation) {
|
||||
report := func(format string, args ...any) {
|
||||
*violations = append(*violations, schemaCatalogStructureViolation{
|
||||
tool: toolID,
|
||||
message: fmt.Sprintf(format, args...),
|
||||
})
|
||||
}
|
||||
mode, _ := entry["interface_mode"].(string)
|
||||
ref, hasRef := entry["interface_ref"]
|
||||
reason, _ := entry["interface_reason"].(string)
|
||||
switch mode {
|
||||
case InterfaceModeMCP:
|
||||
if !hasRef {
|
||||
report("interface_mode=mcp requires interface_ref")
|
||||
return
|
||||
}
|
||||
refObj, ok := ref.(map[string]any)
|
||||
if !ok {
|
||||
report("interface_ref must be an object")
|
||||
return
|
||||
}
|
||||
for _, key := range []string{"product_id", "rpc_name"} {
|
||||
if s, ok := refObj[key].(string); !ok || strings.TrimSpace(s) == "" {
|
||||
report("interface_ref.%s must be a non-empty string", key)
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(reason) != "" {
|
||||
report("interface_mode=mcp must not set interface_reason")
|
||||
}
|
||||
case InterfaceModeComposite, InterfaceModeLocal:
|
||||
if hasRef {
|
||||
report("interface_mode=%s must not set interface_ref", mode)
|
||||
}
|
||||
if strings.TrimSpace(reason) == "" {
|
||||
report("interface_mode=%s requires a non-empty interface_reason", mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateCatalogParam(toolID, paramName string, param map[string]any, violations *[]schemaCatalogStructureViolation) {
|
||||
report := func(format string, args ...any) {
|
||||
*violations = append(*violations, schemaCatalogStructureViolation{
|
||||
tool: toolID,
|
||||
message: fmt.Sprintf(format, args...),
|
||||
})
|
||||
}
|
||||
|
||||
allowed := make(map[string]bool, len(schemaCatalogParamRequiredKeys)+len(schemaCatalogParamOptionalKeys))
|
||||
for _, k := range schemaCatalogParamRequiredKeys {
|
||||
allowed[k] = true
|
||||
}
|
||||
for _, k := range schemaCatalogParamOptionalKeys {
|
||||
allowed[k] = true
|
||||
}
|
||||
for key := range param {
|
||||
if !allowed[key] {
|
||||
report("parameter %q: unknown field %q", paramName, key)
|
||||
}
|
||||
}
|
||||
|
||||
typ, _ := param["type"].(string)
|
||||
if !stringSliceContains(schemaCatalogParamTypes, typ) {
|
||||
report("parameter %q: type = %q, want one of {%s}", paramName, typ, strings.Join(schemaCatalogParamTypes, ", "))
|
||||
}
|
||||
if s, ok := param["description"].(string); !ok || strings.TrimSpace(s) == "" {
|
||||
report("parameter %q: description must be a non-empty string", paramName)
|
||||
}
|
||||
if _, ok := param["required"].(bool); !ok {
|
||||
report("parameter %q: required must be a boolean", paramName)
|
||||
}
|
||||
if _, ok := param["field_provenance"].(map[string]any); !ok {
|
||||
report("parameter %q: field_provenance must be an object", paramName)
|
||||
}
|
||||
|
||||
for _, key := range []string{"property", "interface_description", "interface_type", "format", "default", "example", "required_when"} {
|
||||
if raw, present := param[key]; present {
|
||||
if _, ok := raw.(string); !ok {
|
||||
report("parameter %q: %s must be a string", paramName, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
if raw, present := param["cli_required"]; present {
|
||||
if _, ok := raw.(bool); !ok {
|
||||
report("parameter %q: cli_required must be a boolean", paramName)
|
||||
}
|
||||
}
|
||||
if raw, present := param["enum"]; present {
|
||||
items, ok := raw.([]any)
|
||||
if !ok {
|
||||
report("parameter %q: enum must be an array", paramName)
|
||||
} else {
|
||||
for i, item := range items {
|
||||
if _, ok := item.(string); !ok {
|
||||
report("parameter %q: enum[%d] must be a string", paramName, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func requireNonEmptyString(entry map[string]any, key string) bool {
|
||||
s, ok := entry[key].(string)
|
||||
return ok && strings.TrimSpace(s) != ""
|
||||
}
|
||||
|
||||
func requireStringArray(entry map[string]any, key string) bool {
|
||||
raw, ok := entry[key].([]any)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for _, item := range raw {
|
||||
if _, ok := item.(string); !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func stringSliceContains(values []string, target string) bool {
|
||||
for _, v := range values {
|
||||
if v == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestEmbeddedSchemaCatalogStructure gates the delivered catalog: every tool
|
||||
// entry must conform to the unified closed structure. If this fails after
|
||||
// regeneration, either fix the generator inputs or deliberately extend the
|
||||
// whitelist in schema_catalog_structure.go.
|
||||
func TestEmbeddedSchemaCatalogStructure(t *testing.T) {
|
||||
// After PR #656 the catalog is embedded as per-product shards, not a single
|
||||
// JSON file. The loaded snapshot is the reassembled result — serialize it
|
||||
// back to JSON and validate the same closed structure.
|
||||
loaded := embeddedSchemaCatalog()
|
||||
data, err := json.Marshal(loaded.Snapshot)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal embedded catalog snapshot: %v", err)
|
||||
}
|
||||
if err := ValidateCatalogStructure(data); err != nil {
|
||||
t.Fatalf("embedded schema catalog violates the unified command structure: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func validCatalogToolEntry() map[string]any {
|
||||
return map[string]any{
|
||||
"agent_metadata_source": "embedded-skill-metadata",
|
||||
"agent_source_refs": []any{"skills/mono/references/products/aitable.md"},
|
||||
"agent_summary": "创建多维表格记录",
|
||||
"agent_summary_source": "dws-agent-selection/aitable",
|
||||
"availability": "available",
|
||||
"avoid_when": []any{"批量更新时请用 record update"},
|
||||
"canonical_path": "aitable.record_create",
|
||||
"cli_name": "create",
|
||||
"cli_path": "aitable record create",
|
||||
"confirmation": "not_required",
|
||||
"description": "在数据表中创建一条或多条记录。",
|
||||
"display": "多维表格",
|
||||
"effect": "write",
|
||||
"effect_source": "command-verb",
|
||||
"examples": []any{"dws aitable record create --base-id x"},
|
||||
"field_provenance": map[string]any{"effect": map[string]any{"precedence": "reviewed_explicit"}},
|
||||
"has_parameters": true,
|
||||
"idempotency": "non_idempotent",
|
||||
"interface_mode": "mcp",
|
||||
"interface_ref": map[string]any{"product_id": "aitable", "rpc_name": "create_records"},
|
||||
"is_alias": false,
|
||||
"name": "record create",
|
||||
"parameter_count": float64(1),
|
||||
"parameters": map[string]any{
|
||||
"base-id": map[string]any{
|
||||
"type": "string",
|
||||
"description": "目标多维表格 ID。",
|
||||
"required": true,
|
||||
"field_provenance": map[string]any{"description": map[string]any{"precedence": "reviewed_explicit"}},
|
||||
},
|
||||
},
|
||||
"path": "aitable.record_create",
|
||||
"primary_cli_path": "aitable record create",
|
||||
"product_id": "aitable",
|
||||
"reviewed": true,
|
||||
"risk": "medium",
|
||||
"source": "cobra+registry",
|
||||
"title": "创建记录",
|
||||
"use_when": []any{"需要向多维表格写入新记录时"},
|
||||
}
|
||||
}
|
||||
|
||||
func catalogPayload(t *testing.T, entry map[string]any) []byte {
|
||||
t.Helper()
|
||||
payload, err := json.Marshal(map[string]any{
|
||||
"version": SchemaCatalogSnapshotVersion,
|
||||
"tools": map[string]any{"aitable.record_create": entry},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal payload: %v", err)
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func TestValidateCatalogStructureAcceptsValidEntry(t *testing.T) {
|
||||
if err := ValidateCatalogStructure(catalogPayload(t, validCatalogToolEntry())); err != nil {
|
||||
t.Fatalf("ValidateCatalogStructure() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCatalogStructureRejectsViolations(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
mutate func(entry map[string]any)
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "unknown tool field",
|
||||
mutate: func(e map[string]any) { e["surprise"] = "x" },
|
||||
want: `unknown field "surprise"`,
|
||||
},
|
||||
{
|
||||
name: "missing required core field",
|
||||
mutate: func(e map[string]any) { delete(e, "agent_summary") },
|
||||
want: `"agent_summary" must be a non-empty string`,
|
||||
},
|
||||
{
|
||||
name: "bad effect enum",
|
||||
mutate: func(e map[string]any) { e["effect"] = "mutate" },
|
||||
want: `"effect" = "mutate"`,
|
||||
},
|
||||
{
|
||||
// 非字符串枚举字段:类型错误由字符串校验报告,枚举循环跳过。
|
||||
name: "non-string enum field skips enum check",
|
||||
mutate: func(e map[string]any) { e["effect"] = float64(1) },
|
||||
want: `"effect" must be a non-empty string`,
|
||||
},
|
||||
{
|
||||
name: "mcp without interface_ref",
|
||||
mutate: func(e map[string]any) { delete(e, "interface_ref") },
|
||||
want: "interface_mode=mcp requires interface_ref",
|
||||
},
|
||||
{
|
||||
name: "composite without interface_reason",
|
||||
mutate: func(e map[string]any) {
|
||||
e["interface_mode"] = "composite"
|
||||
delete(e, "interface_ref")
|
||||
},
|
||||
want: "interface_mode=composite requires a non-empty interface_reason",
|
||||
},
|
||||
{
|
||||
name: "composite must not keep interface_ref",
|
||||
mutate: func(e map[string]any) {
|
||||
e["interface_mode"] = "composite"
|
||||
e["interface_reason"] = "本地组合多个 MCP 调用"
|
||||
},
|
||||
want: "interface_mode=composite must not set interface_ref",
|
||||
},
|
||||
{
|
||||
name: "parameter_count mismatch",
|
||||
mutate: func(e map[string]any) { e["parameter_count"] = float64(2) },
|
||||
want: "parameter_count = 2, want 1",
|
||||
},
|
||||
{
|
||||
name: "has_parameters mismatch",
|
||||
mutate: func(e map[string]any) { e["has_parameters"] = false },
|
||||
want: "has_parameters = false, want true",
|
||||
},
|
||||
{
|
||||
name: "param missing required",
|
||||
mutate: func(e map[string]any) {
|
||||
params := e["parameters"].(map[string]any)
|
||||
delete(params["base-id"].(map[string]any), "required")
|
||||
},
|
||||
want: `parameter "base-id": required must be a boolean`,
|
||||
},
|
||||
{
|
||||
name: "param unknown field",
|
||||
mutate: func(e map[string]any) {
|
||||
params := e["parameters"].(map[string]any)
|
||||
params["base-id"].(map[string]any)["mystery"] = true
|
||||
},
|
||||
want: `parameter "base-id": unknown field "mystery"`,
|
||||
},
|
||||
{
|
||||
name: "param bad type enum",
|
||||
mutate: func(e map[string]any) {
|
||||
params := e["parameters"].(map[string]any)
|
||||
params["base-id"].(map[string]any)["type"] = "text"
|
||||
},
|
||||
want: `parameter "base-id": type = "text"`,
|
||||
},
|
||||
{
|
||||
name: "param default must be string",
|
||||
mutate: func(e map[string]any) {
|
||||
params := e["parameters"].(map[string]any)
|
||||
params["base-id"].(map[string]any)["default"] = float64(3)
|
||||
},
|
||||
want: `parameter "base-id": default must be a string`,
|
||||
},
|
||||
{
|
||||
name: "reviewed must be bool",
|
||||
mutate: func(e map[string]any) { e["reviewed"] = "yes" },
|
||||
want: `"reviewed" must be a boolean`,
|
||||
},
|
||||
{
|
||||
name: "examples must be string array",
|
||||
mutate: func(e map[string]any) { e["examples"] = []any{"ok", float64(1)} },
|
||||
want: `"examples" must be an array of strings`,
|
||||
},
|
||||
{
|
||||
name: "use_when must be an array at all",
|
||||
mutate: func(e map[string]any) { e["use_when"] = "单个字符串" },
|
||||
want: `"use_when" must be an array of strings`,
|
||||
},
|
||||
{
|
||||
name: "field_provenance must be object",
|
||||
mutate: func(e map[string]any) { e["field_provenance"] = "not-an-object" },
|
||||
want: `"field_provenance" must be an object`,
|
||||
},
|
||||
{
|
||||
name: "parameters must be object",
|
||||
mutate: func(e map[string]any) { e["parameters"] = []any{"x"} },
|
||||
want: `"parameters" must be an object`,
|
||||
},
|
||||
{
|
||||
name: "parameter_count must be number",
|
||||
mutate: func(e map[string]any) { e["parameter_count"] = "1" },
|
||||
want: `"parameter_count" must be a number`,
|
||||
},
|
||||
{
|
||||
name: "parameter entry must be object",
|
||||
mutate: func(e map[string]any) {
|
||||
e["parameters"] = map[string]any{"base-id": "not-an-object"}
|
||||
e["parameter_count"] = float64(1)
|
||||
},
|
||||
want: `parameter "base-id" must be an object`,
|
||||
},
|
||||
{
|
||||
name: "interface_ref must be object",
|
||||
mutate: func(e map[string]any) { e["interface_ref"] = "aitable.create_records" },
|
||||
want: "interface_ref must be an object",
|
||||
},
|
||||
{
|
||||
name: "interface_ref keys must be non-empty",
|
||||
mutate: func(e map[string]any) {
|
||||
e["interface_ref"] = map[string]any{"product_id": " ", "rpc_name": float64(1)}
|
||||
},
|
||||
want: "interface_ref.product_id must be a non-empty string",
|
||||
},
|
||||
{
|
||||
name: "mcp must not set interface_reason",
|
||||
mutate: func(e map[string]any) { e["interface_reason"] = "多余理由" },
|
||||
want: "interface_mode=mcp must not set interface_reason",
|
||||
},
|
||||
{
|
||||
name: "param description must be non-empty",
|
||||
mutate: func(e map[string]any) {
|
||||
params := e["parameters"].(map[string]any)
|
||||
params["base-id"].(map[string]any)["description"] = " "
|
||||
},
|
||||
want: `parameter "base-id": description must be a non-empty string`,
|
||||
},
|
||||
{
|
||||
name: "param field_provenance must be object",
|
||||
mutate: func(e map[string]any) {
|
||||
params := e["parameters"].(map[string]any)
|
||||
params["base-id"].(map[string]any)["field_provenance"] = "x"
|
||||
},
|
||||
want: `parameter "base-id": field_provenance must be an object`,
|
||||
},
|
||||
{
|
||||
name: "param cli_required must be bool",
|
||||
mutate: func(e map[string]any) {
|
||||
params := e["parameters"].(map[string]any)
|
||||
params["base-id"].(map[string]any)["cli_required"] = "yes"
|
||||
},
|
||||
want: `parameter "base-id": cli_required must be a boolean`,
|
||||
},
|
||||
{
|
||||
name: "param enum must be array",
|
||||
mutate: func(e map[string]any) {
|
||||
params := e["parameters"].(map[string]any)
|
||||
params["base-id"].(map[string]any)["enum"] = "A,B"
|
||||
},
|
||||
want: `parameter "base-id": enum must be an array`,
|
||||
},
|
||||
{
|
||||
name: "param enum items must be strings",
|
||||
mutate: func(e map[string]any) {
|
||||
params := e["parameters"].(map[string]any)
|
||||
params["base-id"].(map[string]any)["enum"] = []any{"A", float64(2)}
|
||||
},
|
||||
want: `parameter "base-id": enum[1] must be a string`,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
entry := validCatalogToolEntry()
|
||||
tc.mutate(entry)
|
||||
err := ValidateCatalogStructure(catalogPayload(t, entry))
|
||||
if err == nil {
|
||||
t.Fatalf("ValidateCatalogStructure() = nil, want violation containing %q", tc.want)
|
||||
}
|
||||
if !strings.Contains(err.Error(), tc.want) {
|
||||
t.Fatalf("ValidateCatalogStructure() error = %v, want substring %q", err, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCatalogStructureRejectsBadEnvelope(t *testing.T) {
|
||||
if err := ValidateCatalogStructure([]byte(`{"version":99,"tools":{}}`)); err == nil {
|
||||
t.Fatal("ValidateCatalogStructure() = nil, want version error")
|
||||
}
|
||||
if err := ValidateCatalogStructure([]byte(`{"version":1,"tools":{}}`)); err == nil {
|
||||
t.Fatal("ValidateCatalogStructure() = nil, want empty-tools error")
|
||||
}
|
||||
if err := ValidateCatalogStructure([]byte(`not json`)); err == nil {
|
||||
t.Fatal("ValidateCatalogStructure() = nil, want decode error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCatalogStructureSortsViolationsAcrossTools(t *testing.T) {
|
||||
// 两个工具各制造两条违规,驱动排序比较器的 tool 与 message 两个分支。
|
||||
bad1 := validCatalogToolEntry()
|
||||
bad1["effect"] = "mutate"
|
||||
bad1["risk"] = "extreme"
|
||||
bad2 := validCatalogToolEntry()
|
||||
bad2["canonical_path"] = "zz.last_tool"
|
||||
bad2["effect"] = "mutate"
|
||||
bad2["risk"] = "extreme"
|
||||
payload, err := json.Marshal(map[string]any{
|
||||
"version": SchemaCatalogSnapshotVersion,
|
||||
"tools": map[string]any{"zz.last_tool": bad2, "aitable.record_create": bad1},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verr := ValidateCatalogStructure(payload)
|
||||
if verr == nil {
|
||||
t.Fatal("ValidateCatalogStructure() = nil, want sorted violations")
|
||||
}
|
||||
msg := verr.Error()
|
||||
if !strings.Contains(msg, "(4 total)") {
|
||||
t.Fatalf("error = %v, want 4 total violations", verr)
|
||||
}
|
||||
if first, second := strings.Index(msg, "aitable.record_create"), strings.Index(msg, "zz.last_tool"); first < 0 || second < 0 || first > second {
|
||||
t.Fatalf("violations not sorted by tool: %v", verr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCatalogStructureTruncatesViolationList(t *testing.T) {
|
||||
// 单工具制造超过 25 条违规,验证截断提示。
|
||||
entry := validCatalogToolEntry()
|
||||
for i := 0; i < 30; i++ {
|
||||
entry[fmt.Sprintf("unknown_field_%02d", i)] = true
|
||||
}
|
||||
err := ValidateCatalogStructure(catalogPayload(t, entry))
|
||||
if err == nil {
|
||||
t.Fatal("ValidateCatalogStructure() = nil, want truncated violation list")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "more violations") {
|
||||
t.Fatalf("error = %v, want truncation marker", err)
|
||||
}
|
||||
}
|
||||
@@ -849,7 +849,13 @@ func TestAssemblerUsesReviewedCommandVisibility(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCommandRegistrySourceValidationIsFailClosed(t *testing.T) {
|
||||
registry, err := ValidateCommandRegistrySource(embeddedSchemaCommandRegistryJSON)
|
||||
// After splitting registry into per-product shards, tests use the merged
|
||||
// JSON (reassembled from shards) for byte-level operations.
|
||||
merged, err := EmbeddedCommandRegistryMergedJSON()
|
||||
if err != nil {
|
||||
t.Fatalf("EmbeddedCommandRegistryMergedJSON() error = %v", err)
|
||||
}
|
||||
registry, err := ValidateCommandRegistrySource(merged)
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateCommandRegistrySource(embedded) error = %v", err)
|
||||
}
|
||||
@@ -861,8 +867,9 @@ func TestCommandRegistrySourceValidationIsFailClosed(t *testing.T) {
|
||||
t.Fatalf("embedded hash = %q, decoded hash = %q", hash, registry.SourceHash())
|
||||
}
|
||||
|
||||
drifted := strings.Replace(string(embeddedSchemaCommandRegistryJSON), `"cli_path": "aisearch person"`, `"cli_path": "aisearch people"`, 1)
|
||||
if drifted == string(embeddedSchemaCommandRegistryJSON) {
|
||||
// Match both pretty-printed ("cli_path": "...") and compact ("cli_path":"...") JSON.
|
||||
drifted := strings.Replace(string(merged), `"aisearch person"`, `"aisearch people"`, 1)
|
||||
if drifted == string(merged) {
|
||||
t.Fatal("test fixture did not mutate embedded registry")
|
||||
}
|
||||
_, err = ValidateCommandRegistrySource([]byte(drifted))
|
||||
@@ -870,7 +877,12 @@ func TestCommandRegistrySourceValidationIsFailClosed(t *testing.T) {
|
||||
t.Fatalf("drift error = %v", err)
|
||||
}
|
||||
|
||||
unknownField := strings.Replace(string(embeddedSchemaCommandRegistryJSON), `"version": 1`, `"version": 1, "unreviewed": true`, 1)
|
||||
// Test unknown-field rejection: inject an unrecognized top-level key.
|
||||
unknownField := strings.Replace(string(merged), `"version":1`, `"version":1,"unreviewed":true`, 1)
|
||||
if unknownField == string(merged) {
|
||||
// Try with space (pretty-printed format).
|
||||
unknownField = strings.Replace(string(merged), `"version": 1`, `"version": 1, "unreviewed": true`, 1)
|
||||
}
|
||||
_, err = ValidateCommandRegistrySource([]byte(unknownField))
|
||||
if err == nil || !strings.Contains(err.Error(), "unknown field") {
|
||||
t.Fatalf("unknown-field error = %v", err)
|
||||
|
||||
@@ -153,6 +153,68 @@
|
||||
"todo task list-sub",
|
||||
"todo task remove-attachment"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "chat-shortcuts-pending-schema-curation",
|
||||
"reason": "These reviewed public Chat shortcuts remain executable and discoverable through the Shortcut catalog while their typed Schema selection and metadata records are curated; exact paths keep reverse completeness strict without hiding future shortcuts.",
|
||||
"reviewed": true,
|
||||
"commands": [
|
||||
"chat +category-add-conversation",
|
||||
"chat +category-list-conversations",
|
||||
"chat +category-remove-conversation",
|
||||
"chat +chat-add-bot",
|
||||
"chat +chat-audit-join",
|
||||
"chat +chat-create",
|
||||
"chat +chat-get-by-id",
|
||||
"chat +chat-members-get",
|
||||
"chat +chat-members-list",
|
||||
"chat +chat-messages",
|
||||
"chat +chat-mute-member",
|
||||
"chat +chat-quit",
|
||||
"chat +chat-remove-bot",
|
||||
"chat +chat-role-remove",
|
||||
"chat +chat-role-remove-user",
|
||||
"chat +chat-transfer-owner",
|
||||
"chat +chat-update",
|
||||
"chat +chat-update-icon",
|
||||
"chat +chat-update-settings",
|
||||
"chat +conversation-clear-messages",
|
||||
"chat +conversation-clear-red-point",
|
||||
"chat +conversation-hide",
|
||||
"chat +conversation-mark-read",
|
||||
"chat +conversation-mark-unread",
|
||||
"chat +conversation-mute",
|
||||
"chat +conversation-set-top",
|
||||
"chat +feed-group-query-item",
|
||||
"chat +flag-cancel",
|
||||
"chat +flag-create",
|
||||
"chat +flag-list",
|
||||
"chat +messages-add-emoji",
|
||||
"chat +messages-add-text-emotion",
|
||||
"chat +messages-batch-recall-by-bot",
|
||||
"chat +messages-batch-send-by-bot",
|
||||
"chat +messages-combine-forward",
|
||||
"chat +messages-create-text-emotion",
|
||||
"chat +messages-forward",
|
||||
"chat +messages-forward-topic",
|
||||
"chat +messages-list",
|
||||
"chat +messages-recall",
|
||||
"chat +messages-recall-by-bot",
|
||||
"chat +messages-remove-emoji",
|
||||
"chat +messages-remove-text-emotion",
|
||||
"chat +messages-reply",
|
||||
"chat +messages-resource-download",
|
||||
"chat +messages-resource-url",
|
||||
"chat +messages-send",
|
||||
"chat +messages-send-by-bot",
|
||||
"chat +messages-send-card",
|
||||
"chat +messages-set-pin",
|
||||
"chat +messages-set-top",
|
||||
"chat +messages-unset-pin",
|
||||
"chat +messages-unset-top",
|
||||
"chat +search-msg",
|
||||
"chat +thread-replies"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,11 +6,12 @@ package cli
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
_ "embed"
|
||||
"embed"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -25,8 +26,11 @@ const commandRegistrySchemaRef = "./schema_command_registry.schema.json"
|
||||
// sole source of stable command identity and navigation. Catalog and generated
|
||||
// metadata are downstream views and must never be read back here.
|
||||
|
||||
//go:embed schema_command_registry.json
|
||||
var embeddedSchemaCommandRegistryJSON []byte
|
||||
//go:embed schema_command_registry/registry.json
|
||||
var embeddedSchemaCommandRegistryEnvelopeJSON []byte
|
||||
|
||||
//go:embed schema_command_registry/products
|
||||
var embeddedSchemaCommandRegistryProducts embed.FS
|
||||
|
||||
//go:embed schema_command_registry.schema.json
|
||||
var embeddedSchemaCommandRegistrySchemaJSON []byte
|
||||
@@ -96,11 +100,110 @@ var (
|
||||
|
||||
func loadEmbeddedCommandRegistry() (CommandRegistry, error) {
|
||||
embeddedSchemaCommandRegistryOnce.Do(func() {
|
||||
embeddedSchemaCommandRegistryData, embeddedSchemaCommandRegistryErr = decodeCommandRegistry(embeddedSchemaCommandRegistryJSON)
|
||||
embeddedSchemaCommandRegistryData, embeddedSchemaCommandRegistryErr = assembleCommandRegistry()
|
||||
})
|
||||
return cloneCommandRegistry(embeddedSchemaCommandRegistryData), embeddedSchemaCommandRegistryErr
|
||||
}
|
||||
|
||||
// assembleCommandRegistry reads the per-product shards embedded at build time
|
||||
// and reassembles them into a single CommandRegistry, identical to the previous
|
||||
// single-file layout. Mirrors assembleEmbeddedSchemaCatalog for catalog shards.
|
||||
func assembleCommandRegistry() (CommandRegistry, error) {
|
||||
return assembleCommandRegistryFrom(embeddedSchemaCommandRegistryEnvelopeJSON, embeddedSchemaCommandRegistryProducts, "schema_command_registry/products")
|
||||
}
|
||||
|
||||
// assembleCommandRegistryFrom reassembles an envelope plus product shard
|
||||
// directory. Split from assembleCommandRegistry so shard failure modes stay
|
||||
// testable against fake filesystems.
|
||||
func assembleCommandRegistryFrom(envelopeJSON []byte, shards fs.FS, dir string) (CommandRegistry, error) {
|
||||
var envelope struct {
|
||||
Schema string `json:"$schema,omitempty"`
|
||||
Version int `json:"version"`
|
||||
}
|
||||
if err := json.Unmarshal(envelopeJSON, &envelope); err != nil {
|
||||
return CommandRegistry{}, fmt.Errorf("decode embedded command registry envelope: %w", err)
|
||||
}
|
||||
|
||||
entries, err := fs.ReadDir(shards, dir)
|
||||
if err != nil {
|
||||
return CommandRegistry{}, fmt.Errorf("read embedded command registry products: %w", err)
|
||||
}
|
||||
|
||||
var snapshot schemaCommandRegistrySnapshot
|
||||
snapshot.Schema = envelope.Schema
|
||||
snapshot.Version = envelope.Version
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
|
||||
continue
|
||||
}
|
||||
data, readErr := fs.ReadFile(shards, dir+"/"+entry.Name())
|
||||
if readErr != nil {
|
||||
return CommandRegistry{}, fmt.Errorf("read embedded command registry shard %s: %w", entry.Name(), readErr)
|
||||
}
|
||||
var product schemaCommandRegistryProduct
|
||||
if err := json.Unmarshal(data, &product); err != nil {
|
||||
return CommandRegistry{}, fmt.Errorf("decode embedded command registry shard %s: %w", entry.Name(), err)
|
||||
}
|
||||
snapshot.Products = append(snapshot.Products, product)
|
||||
}
|
||||
|
||||
// Encode to bytes and decode via the existing pipeline so all validation
|
||||
// (canonical patterns, duplicate detection, visibility) runs identically.
|
||||
// Marshal of this plain struct cannot fail.
|
||||
data, _ := json.Marshal(snapshot)
|
||||
return decodeCommandRegistry(data)
|
||||
}
|
||||
|
||||
// EmbeddedCommandRegistryMergedJSON returns the per-product shards reassembled
|
||||
// into a single JSON document matching the pre-split layout. Used by tests that
|
||||
// need the full registry bytes.
|
||||
func EmbeddedCommandRegistryMergedJSON() ([]byte, error) {
|
||||
return mergedCommandRegistryJSON(embeddedSchemaCommandRegistryEnvelopeJSON, embeddedSchemaCommandRegistryProducts, "schema_command_registry/products")
|
||||
}
|
||||
|
||||
// mergedCommandRegistryJSON is the injectable core of
|
||||
// EmbeddedCommandRegistryMergedJSON.
|
||||
func mergedCommandRegistryJSON(envelopeJSON []byte, shards fs.FS, dir string) ([]byte, error) {
|
||||
var envelope struct {
|
||||
Schema string `json:"$schema,omitempty"`
|
||||
Version int `json:"version"`
|
||||
}
|
||||
if err := json.Unmarshal(envelopeJSON, &envelope); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries, err := fs.ReadDir(shards, dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
products := make([]json.RawMessage, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
|
||||
continue
|
||||
}
|
||||
data, err := fs.ReadFile(shards, dir+"/"+entry.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
products = append(products, json.RawMessage(data))
|
||||
}
|
||||
result := struct {
|
||||
Schema string `json:"$schema,omitempty"`
|
||||
Version int `json:"version"`
|
||||
Products json.RawMessage `json:"products"`
|
||||
}{
|
||||
Schema: envelope.Schema,
|
||||
Version: envelope.Version,
|
||||
}
|
||||
if result.Schema == "" {
|
||||
result.Schema = "./schema_command_registry.schema.json"
|
||||
}
|
||||
result.Products, err = json.Marshal(products)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(result)
|
||||
}
|
||||
|
||||
// ValidateCommandRegistrySource validates a compatibility --surface input and
|
||||
// requires it to be semantically identical to the embedded reviewed registry.
|
||||
// Generators may retain the flag for migration, but cannot use it to introduce
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"id": "aisearch",
|
||||
"tools": [
|
||||
{
|
||||
"canonical_path": "aisearch.search_enterprise_behavior",
|
||||
"cli_path": "aisearch behavior"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aisearch.search_enterprise",
|
||||
"cli_path": "aisearch enterprise"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aisearch.enterprise_person_search",
|
||||
"cli_path": "aisearch person"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,588 @@
|
||||
{
|
||||
"id": "aitable",
|
||||
"tools": [
|
||||
{
|
||||
"canonical_path": "aitable.advperm_disable",
|
||||
"cli_path": "aitable advperm disable"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.advperm_enable",
|
||||
"cli_path": "aitable advperm enable"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.advperm_role_create",
|
||||
"cli_path": "aitable advperm role-create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.advperm_role_delete",
|
||||
"cli_path": "aitable advperm role-delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.advperm_role_get",
|
||||
"cli_path": "aitable advperm role-get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.advperm_role_list",
|
||||
"cli_path": "aitable advperm role-list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.advperm_role_update",
|
||||
"cli_path": "aitable advperm role-update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.attachment_upload",
|
||||
"cli_path": "aitable attachment upload"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.base_copy",
|
||||
"cli_path": "aitable base copy"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.base_create",
|
||||
"cli_path": "aitable base create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.base_delete",
|
||||
"cli_path": "aitable base delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.base_get",
|
||||
"cli_path": "aitable base get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.base_get_primary_doc_id",
|
||||
"cli_path": "aitable base get-primary-doc-id"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.base_list",
|
||||
"cli_path": "aitable base list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.base_search",
|
||||
"cli_path": "aitable base search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.base_update",
|
||||
"cli_path": "aitable base update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.chart_create",
|
||||
"cli_path": "aitable chart create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.chart_delete",
|
||||
"cli_path": "aitable chart delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.chart_get",
|
||||
"cli_path": "aitable chart get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.chart_share_get",
|
||||
"cli_path": "aitable chart share get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.chart_share_update",
|
||||
"cli_path": "aitable chart share update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.chart_update",
|
||||
"cli_path": "aitable chart update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.chart_widgets_example",
|
||||
"cli_path": "aitable chart widgets-example"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.create",
|
||||
"cli_path": "aitable create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.dashboard_arrange",
|
||||
"cli_path": "aitable dashboard arrange"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.dashboard_config_example",
|
||||
"cli_path": "aitable dashboard config-example"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.dashboard_create",
|
||||
"cli_path": "aitable dashboard create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.dashboard_delete",
|
||||
"cli_path": "aitable dashboard delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.dashboard_get",
|
||||
"cli_path": "aitable dashboard get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.dashboard_share_get",
|
||||
"cli_path": "aitable dashboard share get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.dashboard_share_update",
|
||||
"cli_path": "aitable dashboard share update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.dashboard_update",
|
||||
"cli_path": "aitable dashboard update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.export_data",
|
||||
"cli_path": "aitable export data"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.field_create",
|
||||
"cli_path": "aitable field create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.field_delete",
|
||||
"cli_path": "aitable field delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.field_get",
|
||||
"cli_path": "aitable field get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.field_list",
|
||||
"cli_path": "aitable field list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.field_search_options",
|
||||
"cli_path": "aitable field search-options"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.field_update",
|
||||
"cli_path": "aitable field update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.form_create",
|
||||
"cli_path": "aitable form create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.form_delete",
|
||||
"cli_path": "aitable form delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.form_field_hide",
|
||||
"cli_path": "aitable form field hide"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.form_field_list",
|
||||
"cli_path": "aitable form field list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.form_field_update",
|
||||
"cli_path": "aitable form field update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.form_get",
|
||||
"cli_path": "aitable form get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.form_list",
|
||||
"cli_path": "aitable form list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.form_questions_create",
|
||||
"cli_path": "aitable form questions create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.form_questions_delete",
|
||||
"cli_path": "aitable form questions delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.form_share_get",
|
||||
"cli_path": "aitable form share get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.form_share_update",
|
||||
"cli_path": "aitable form share update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.form_update",
|
||||
"cli_path": "aitable form update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.import_data",
|
||||
"cli_path": "aitable import data"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.import_upload",
|
||||
"cli_path": "aitable import upload"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.info",
|
||||
"cli_path": "aitable info"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.list",
|
||||
"cli_path": "aitable list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.record_batch_update",
|
||||
"cli_path": "aitable record batch-update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.record_create",
|
||||
"cli_path": "aitable record create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.record_delete",
|
||||
"cli_path": "aitable record delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.record_get",
|
||||
"cli_path": "aitable record get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.record_history_list",
|
||||
"cli_path": "aitable record history-list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.record_primary_doc_create",
|
||||
"cli_path": "aitable record primary-doc-create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.record_primary_doc_get",
|
||||
"cli_path": "aitable record primary-doc-get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.query_records",
|
||||
"cli_path": "aitable record query",
|
||||
"aliases": [
|
||||
"aitable record list"
|
||||
]
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.record_query_empty",
|
||||
"cli_path": "aitable record query-empty"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.record_share_url",
|
||||
"cli_path": "aitable record share-url"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.record_update",
|
||||
"cli_path": "aitable record update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.record_upsert",
|
||||
"cli_path": "aitable record upsert"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.search",
|
||||
"cli_path": "aitable search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.section_create",
|
||||
"cli_path": "aitable section create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.section_delete",
|
||||
"cli_path": "aitable section delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.section_list_empty",
|
||||
"cli_path": "aitable section list-empty"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.section_list_nodes",
|
||||
"cli_path": "aitable section list-nodes"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.section_move_node",
|
||||
"cli_path": "aitable section move-node"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.section_rename",
|
||||
"cli_path": "aitable section rename"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.section_reorder",
|
||||
"cli_path": "aitable section reorder"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.table_create",
|
||||
"cli_path": "aitable table create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.table_delete",
|
||||
"cli_path": "aitable table delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.table_get",
|
||||
"cli_path": "aitable table get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.table_list",
|
||||
"cli_path": "aitable table list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.table_update",
|
||||
"cli_path": "aitable table update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.template_search",
|
||||
"cli_path": "aitable template search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.view_create",
|
||||
"cli_path": "aitable view create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.view_delete",
|
||||
"cli_path": "aitable view delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.view_duplicate",
|
||||
"cli_path": "aitable view duplicate"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.view_get_aggregate",
|
||||
"cli_path": "aitable view get aggregate"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.view_get_card",
|
||||
"cli_path": "aitable view get card"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.view_get_field_widths",
|
||||
"cli_path": "aitable view get field-widths"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.view_get_fill_color_rule",
|
||||
"cli_path": "aitable view get fill-color-rule"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.view_get_filter",
|
||||
"cli_path": "aitable view get filter"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.view_get_frozen_cols",
|
||||
"cli_path": "aitable view get frozen-cols"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.view_get_group",
|
||||
"cli_path": "aitable view get group"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.view_get_lock",
|
||||
"cli_path": "aitable view get lock"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.view_get_row_height",
|
||||
"cli_path": "aitable view get row-height"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.view_get_sort",
|
||||
"cli_path": "aitable view get sort"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.view_get_timebar",
|
||||
"cli_path": "aitable view get timebar"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.view_get_visible_fields",
|
||||
"cli_path": "aitable view get visible-fields"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.view_list",
|
||||
"cli_path": "aitable view list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.view_lock",
|
||||
"cli_path": "aitable view lock"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.view_update_aggregate",
|
||||
"cli_path": "aitable view update aggregate"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.view_update_card",
|
||||
"cli_path": "aitable view update card"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.view_update_field_widths",
|
||||
"cli_path": "aitable view update field-widths"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.view_update_fill_color_rule",
|
||||
"cli_path": "aitable view update fill-color-rule"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.view_update_filter",
|
||||
"cli_path": "aitable view update filter"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.view_update_frozen_cols",
|
||||
"cli_path": "aitable view update frozen-cols"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.view_update_group",
|
||||
"cli_path": "aitable view update group"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.view_update_name",
|
||||
"cli_path": "aitable view update name"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.view_update_row_height",
|
||||
"cli_path": "aitable view update row-height"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.view_update_sort",
|
||||
"cli_path": "aitable view update sort"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.view_update_timebar",
|
||||
"cli_path": "aitable view update timebar"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.workflow_enable",
|
||||
"cli_path": "aitable workflow enable"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.workflow_get",
|
||||
"cli_path": "aitable workflow get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.workflow_list",
|
||||
"cli_path": "aitable workflow list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.workflow_update",
|
||||
"cli_path": "aitable workflow update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_base_list",
|
||||
"cli_path": "aitable +base-list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_base_search",
|
||||
"cli_path": "aitable +base-search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_base_get",
|
||||
"cli_path": "aitable +base-get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_table_get",
|
||||
"cli_path": "aitable +table-get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_field_get",
|
||||
"cli_path": "aitable +field-get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_record_query",
|
||||
"cli_path": "aitable +record-query"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_record_query_empty",
|
||||
"cli_path": "aitable +record-query-empty"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_record_history_list",
|
||||
"cli_path": "aitable +record-history-list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_record_share_url",
|
||||
"cli_path": "aitable +record-share-url"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_template_search",
|
||||
"cli_path": "aitable +template-search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_view_get",
|
||||
"cli_path": "aitable +view-get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_view_get_lock",
|
||||
"cli_path": "aitable +view-get-lock"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_view_get_frozen_cols",
|
||||
"cli_path": "aitable +view-get-frozen-cols"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_view_get_row_height",
|
||||
"cli_path": "aitable +view-get-row-height"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_form_list",
|
||||
"cli_path": "aitable +form-list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_form_field_list",
|
||||
"cli_path": "aitable +form-field-list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_form_share_get",
|
||||
"cli_path": "aitable +form-share-get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_dashboard_config_example",
|
||||
"cli_path": "aitable +dashboard-config-example"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_dashboard_get",
|
||||
"cli_path": "aitable +dashboard-get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_chart_widgets_example",
|
||||
"cli_path": "aitable +chart-widgets-example"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_chart_get",
|
||||
"cli_path": "aitable +chart-get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_role_list",
|
||||
"cli_path": "aitable +role-list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_section_list_empty",
|
||||
"cli_path": "aitable +section-list-empty"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_section_list_nodes",
|
||||
"cli_path": "aitable +section-list-nodes"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_find_record",
|
||||
"cli_path": "aitable +find-record"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_list_tables",
|
||||
"cli_path": "aitable +list-tables"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_record_share_links",
|
||||
"cli_path": "aitable +record-share-links"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_resolve_base",
|
||||
"cli_path": "aitable +resolve-base"
|
||||
},
|
||||
{
|
||||
"canonical_path": "aitable.shortcut_resolve_table",
|
||||
"cli_path": "aitable +resolve-table"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
{
|
||||
"id": "attendance",
|
||||
"tools": [
|
||||
{
|
||||
"canonical_path": "attendance.adjustment_get",
|
||||
"cli_path": "attendance adjustment get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.adjustment_search",
|
||||
"cli_path": "attendance adjustment search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.approve_list",
|
||||
"cli_path": "attendance approve list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.approve_templates",
|
||||
"cli_path": "attendance approve templates"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.boss_check",
|
||||
"cli_path": "attendance boss-check"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.check_record",
|
||||
"cli_path": "attendance check record"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.check_result",
|
||||
"cli_path": "attendance check result"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.checkin_records",
|
||||
"cli_path": "attendance checkin records"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.class_create",
|
||||
"cli_path": "attendance class create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.class_get",
|
||||
"cli_path": "attendance class get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.class_search",
|
||||
"cli_path": "attendance class search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.class_update",
|
||||
"cli_path": "attendance class update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.globalsetting_get",
|
||||
"cli_path": "attendance globalsetting get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.globalsetting_save",
|
||||
"cli_path": "attendance globalsetting save"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.group_create",
|
||||
"cli_path": "attendance group create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.group_filtered_get",
|
||||
"cli_path": "attendance group filtered-get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.group_get",
|
||||
"cli_path": "attendance group get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.group_search",
|
||||
"cli_path": "attendance group search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.group_update",
|
||||
"cli_path": "attendance group update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.group_update_members",
|
||||
"cli_path": "attendance group update-members"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.overtime_get",
|
||||
"cli_path": "attendance overtime get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.overtime_search",
|
||||
"cli_path": "attendance overtime search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.get_user_attendance_record",
|
||||
"cli_path": "attendance record get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.report_columns",
|
||||
"cli_path": "attendance report columns"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.report_query_data",
|
||||
"cli_path": "attendance report query-data"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.report_query_leave",
|
||||
"cli_path": "attendance report query-leave"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.query_attendance_group_or_rules",
|
||||
"cli_path": "attendance rules"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.schedule_get",
|
||||
"cli_path": "attendance schedule get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.schedule_import",
|
||||
"cli_path": "attendance schedule import"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.selfsetting_get",
|
||||
"cli_path": "attendance selfsetting get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.selfsetting_save",
|
||||
"cli_path": "attendance selfsetting save"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.batch_get_employee_shifts",
|
||||
"cli_path": "attendance shift list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.get_attendance_summary",
|
||||
"cli_path": "attendance summary"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.vacation_balance",
|
||||
"cli_path": "attendance vacation balance"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.vacation_records",
|
||||
"cli_path": "attendance vacation records"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.vacation_save_balance",
|
||||
"cli_path": "attendance vacation save-balance"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.vacation_types",
|
||||
"cli_path": "attendance vacation types"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.vacation_update_type",
|
||||
"cli_path": "attendance vacation update-type"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.shortcut_check_result",
|
||||
"cli_path": "attendance +check-result"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.shortcut_check_record",
|
||||
"cli_path": "attendance +check-record"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.shortcut_list_approve",
|
||||
"cli_path": "attendance +list-approve"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.shortcut_get_approve_template",
|
||||
"cli_path": "attendance +get-approve-template"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.shortcut_get_schedule",
|
||||
"cli_path": "attendance +get-schedule"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.shortcut_search_class",
|
||||
"cli_path": "attendance +search-class"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.shortcut_get_adjustment_rule",
|
||||
"cli_path": "attendance +get-adjustment-rule"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.shortcut_search_adjustment_rule",
|
||||
"cli_path": "attendance +search-adjustment-rule"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.shortcut_get_overtime_rule",
|
||||
"cli_path": "attendance +get-overtime-rule"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.shortcut_search_overtime_rule",
|
||||
"cli_path": "attendance +search-overtime-rule"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.shortcut_search_group",
|
||||
"cli_path": "attendance +search-group"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.shortcut_get_summary",
|
||||
"cli_path": "attendance +get-summary"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.shortcut_get_self_setting",
|
||||
"cli_path": "attendance +get-self-setting"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.shortcut_query_report_data",
|
||||
"cli_path": "attendance +query-report-data"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.shortcut_list_leave_types",
|
||||
"cli_path": "attendance +list-leave-types"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.shortcut_get_leave_records",
|
||||
"cli_path": "attendance +get-leave-records"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.shortcut_get_checkin_record",
|
||||
"cli_path": "attendance +get-checkin-record"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.shortcut_my_attendance",
|
||||
"cli_path": "attendance +my-attendance"
|
||||
},
|
||||
{
|
||||
"canonical_path": "attendance.shortcut_this_month",
|
||||
"cli_path": "attendance +this-month"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"id": "audit",
|
||||
"tools": [
|
||||
{
|
||||
"canonical_path": "audit.export",
|
||||
"cli_path": "audit export"
|
||||
},
|
||||
{
|
||||
"canonical_path": "audit.tail",
|
||||
"cli_path": "audit tail"
|
||||
},
|
||||
{
|
||||
"canonical_path": "audit.verify",
|
||||
"cli_path": "audit verify"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
{
|
||||
"id": "calendar",
|
||||
"tools": [
|
||||
{
|
||||
"canonical_path": "calendar.list_acls",
|
||||
"cli_path": "calendar acl list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.add_attachments",
|
||||
"cli_path": "calendar attachment add"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.add_calendar_participant",
|
||||
"cli_path": "calendar attendee add"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.remove_calendar_participant",
|
||||
"cli_path": "calendar attendee delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.get_calendar_participants",
|
||||
"cli_path": "calendar attendee list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.get_calendar",
|
||||
"cli_path": "calendar book get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.list_calendars",
|
||||
"cli_path": "calendar book list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.search_calendar",
|
||||
"cli_path": "calendar book search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.query_busy_status",
|
||||
"cli_path": "calendar busy search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.create_calendar_event",
|
||||
"cli_path": "calendar event create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.delete_calendar_event",
|
||||
"cli_path": "calendar event delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.get_calendar_detail",
|
||||
"cli_path": "calendar event get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.list_calendar_events",
|
||||
"cli_path": "calendar event list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.respond",
|
||||
"cli_path": "calendar event respond"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.list_suggested_event_times",
|
||||
"cli_path": "calendar event suggest"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.update_calendar_event",
|
||||
"cli_path": "calendar event update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.add_meeting_room",
|
||||
"cli_path": "calendar room add"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.delete_meeting_room",
|
||||
"cli_path": "calendar room delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.list_meeting_room_groups",
|
||||
"cli_path": "calendar room list-groups"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.query_available_meeting_room",
|
||||
"cli_path": "calendar room search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.shortcut_agenda",
|
||||
"cli_path": "calendar +agenda"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.shortcut_attendee_list",
|
||||
"cli_path": "calendar +attendee-list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.shortcut_room_search",
|
||||
"cli_path": "calendar +room-search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.shortcut_room_groups",
|
||||
"cli_path": "calendar +room-groups"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.shortcut_freebusy",
|
||||
"cli_path": "calendar +freebusy"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.shortcut_book_list",
|
||||
"cli_path": "calendar +book-list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.shortcut_book_search",
|
||||
"cli_path": "calendar +book-search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.shortcut_book",
|
||||
"cli_path": "calendar +book"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.shortcut_cancel_event",
|
||||
"cli_path": "calendar +cancel-event"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.shortcut_conflicts",
|
||||
"cli_path": "calendar +conflicts"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.shortcut_free_slots",
|
||||
"cli_path": "calendar +free-slots"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.shortcut_free",
|
||||
"cli_path": "calendar +free"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.shortcut_invite",
|
||||
"cli_path": "calendar +invite"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.shortcut_my_free",
|
||||
"cli_path": "calendar +my-free"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.shortcut_next_event",
|
||||
"cli_path": "calendar +next-event"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.shortcut_reschedule",
|
||||
"cli_path": "calendar +reschedule"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.shortcut_suggest_time",
|
||||
"cli_path": "calendar +suggest-time"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.shortcut_today",
|
||||
"cli_path": "calendar +today"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.shortcut_tomorrow",
|
||||
"cli_path": "calendar +tomorrow"
|
||||
},
|
||||
{
|
||||
"canonical_path": "calendar.shortcut_week",
|
||||
"cli_path": "calendar +week"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
{
|
||||
"id": "chat",
|
||||
"tools": [
|
||||
{
|
||||
"canonical_path": "chat.search_bots",
|
||||
"cli_path": "chat bot find"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.search_my_robots",
|
||||
"cli_path": "chat bot search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.create_smart_conv_category",
|
||||
"cli_path": "chat category create-smart"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.list_user_define_conv_categories",
|
||||
"cli_path": "chat category list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.list_conversations_by_category",
|
||||
"cli_path": "chat category list-conversations"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.get_conversation_info",
|
||||
"cli_path": "chat conversation-info"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.list_group_bots",
|
||||
"cli_path": "chat group bots"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.create_group_conversation",
|
||||
"cli_path": "chat group create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.dismiss_group",
|
||||
"cli_path": "chat group dismiss"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.get_conv_info_by_group_id",
|
||||
"cli_path": "chat group get-by-group-id"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.get_group_invite_url",
|
||||
"cli_path": "chat group invite-url"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.list_owned_or_admin_groups",
|
||||
"cli_path": "chat group list-my-groups"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.add_group_member",
|
||||
"cli_path": "chat group members add"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.add_robot_to_group",
|
||||
"cli_path": "chat group members add-bot"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.remove_group_member",
|
||||
"cli_path": "chat group members remove"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.remove_robot_in_group",
|
||||
"cli_path": "chat group members remove-bot"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.quit_group",
|
||||
"cli_path": "chat group quit"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.update_group_name",
|
||||
"cli_path": "chat group rename"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.update_conv_member_roles",
|
||||
"cli_path": "chat group set-admin"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.update_show_history_msg_option",
|
||||
"cli_path": "chat group set-history"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.transfer_group_owner",
|
||||
"cli_path": "chat group transfer-owner"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.update_group_icon",
|
||||
"cli_path": "chat group update-icon"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.update_group_settings",
|
||||
"cli_path": "chat group update-settings"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.set_group_mute",
|
||||
"cli_path": "chat group-mute"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.set_group_member_mute_list",
|
||||
"cli_path": "chat group-mute-member"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.add_custom_group_role",
|
||||
"cli_path": "chat group-role add"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.list_custom_group_roles",
|
||||
"cli_path": "chat group-role list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.query_custom_user_roles",
|
||||
"cli_path": "chat group-role query-user"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.remove_custom_group_role",
|
||||
"cli_path": "chat group-role remove"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.remove_custom_user_roles",
|
||||
"cli_path": "chat group-role remove-user"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.set_custom_user_roles",
|
||||
"cli_path": "chat group-role set-user"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.update_custom_group_role",
|
||||
"cli_path": "chat group-role update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.list_top_conversations",
|
||||
"cli_path": "chat list-top-conversations"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.add_emoji_reaction",
|
||||
"cli_path": "chat message add-emoji"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.add_message_favorite",
|
||||
"cli_path": "chat message add-favorite"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.add_text_emotion",
|
||||
"cli_path": "chat message add-text-emotion"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.combine_forward_messages",
|
||||
"cli_path": "chat message combine-forward"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.create_text_emotion",
|
||||
"cli_path": "chat message create-text-emotion"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.download_media",
|
||||
"cli_path": "chat message download-media"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.forward_message",
|
||||
"cli_path": "chat message forward"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.forward_topic",
|
||||
"cli_path": "chat message forward-topic"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.list_conversation_message_v2",
|
||||
"cli_path": "chat message list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.search_messages_by_time_range",
|
||||
"cli_path": "chat message list-all"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.list_messages_by_ids",
|
||||
"cli_path": "chat message list-by-ids"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.search_messages_by_sender",
|
||||
"cli_path": "chat message list-by-sender"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.list_individual_chat_message",
|
||||
"cli_path": "chat message list-direct"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.list_message_favorites",
|
||||
"cli_path": "chat message list-favorites"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.list_special_focus_messages",
|
||||
"cli_path": "chat message list-focused"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.search_at_me_message",
|
||||
"cli_path": "chat message list-mentions"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.list_pin_messages",
|
||||
"cli_path": "chat message list-pin-msg"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.list_topic_replies",
|
||||
"cli_path": "chat message list-topic-replies"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.unread_message_conversation_list",
|
||||
"cli_path": "chat message list-unread-conversations"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.query_message_send_status",
|
||||
"cli_path": "chat message query-send-status"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.query_msg_read_status",
|
||||
"cli_path": "chat message read-status"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.recall_message",
|
||||
"cli_path": "chat message recall"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.recall_robot_message",
|
||||
"cli_path": "chat message recall-by-bot"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.remove_emoji_reaction",
|
||||
"cli_path": "chat message remove-emoji"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.remove_message_favorite",
|
||||
"cli_path": "chat message remove-favorite"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.remove_text_emotion",
|
||||
"cli_path": "chat message remove-text-emotion"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.reply_personal_message",
|
||||
"cli_path": "chat message reply"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.search_messages_by_keyword",
|
||||
"cli_path": "chat message search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.search_messages",
|
||||
"cli_path": "chat message search-advanced"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.send_personal_message",
|
||||
"cli_path": "chat message send"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.send_robot_message",
|
||||
"cli_path": "chat message send-by-bot"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.send_message_by_custom_robot",
|
||||
"cli_path": "chat message send-by-webhook"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.create_and_send_card",
|
||||
"cli_path": "chat message send-card"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.set_pin_message",
|
||||
"cli_path": "chat message set-pin-msg"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.unset_pin_message",
|
||||
"cli_path": "chat message unset-pin-msg"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.update_streaming_card",
|
||||
"cli_path": "chat message update-card"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.update_notification_off",
|
||||
"cli_path": "chat mute"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.search_groups",
|
||||
"cli_path": "chat search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.search_common_groups",
|
||||
"cli_path": "chat search-common"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.set_top_conversation",
|
||||
"cli_path": "chat set-top"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.edit_message",
|
||||
"cli_path": "chat message edit"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.get_conv_categories_info",
|
||||
"cli_path": "chat category batch-info"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.list_conv_categories_by_conv",
|
||||
"cli_path": "chat category list-by-conv"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.update_group_nick",
|
||||
"cli_path": "chat group update-nick"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.upgrade_group_to_external",
|
||||
"cli_path": "chat group upgrade-to-external"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_bot_search",
|
||||
"cli_path": "chat +bot-search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_bot_find",
|
||||
"cli_path": "chat +bot-find"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_conversation_info",
|
||||
"cli_path": "chat +conversation-info"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_conversation_clear_all_red_point",
|
||||
"cli_path": "chat +conversation-clear-all-red-point"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_conversation_list",
|
||||
"cli_path": "chat +conversation-list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_conversation_list_top",
|
||||
"cli_path": "chat +conversation-list-top"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_category_list",
|
||||
"cli_path": "chat +category-list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_category_create",
|
||||
"cli_path": "chat +category-create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_category_delete",
|
||||
"cli_path": "chat +category-delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_category_rename",
|
||||
"cli_path": "chat +category-rename"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_chat_search",
|
||||
"cli_path": "chat +chat-search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_chat_invite_url",
|
||||
"cli_path": "chat +chat-invite-url"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_chat_dismiss",
|
||||
"cli_path": "chat +chat-dismiss"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_chat_set_history",
|
||||
"cli_path": "chat +chat-set-history"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_chat_update_nick",
|
||||
"cli_path": "chat +chat-update-nick"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_chat_update_alias",
|
||||
"cli_path": "chat +chat-update-alias"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_chat_list_mine",
|
||||
"cli_path": "chat +chat-list-mine"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_chat_list_all",
|
||||
"cli_path": "chat +chat-list-all"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_chat_list_join_requests",
|
||||
"cli_path": "chat +chat-list-join-requests"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_chat_bots",
|
||||
"cli_path": "chat +chat-bots"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_chat_set_admin",
|
||||
"cli_path": "chat +chat-set-admin"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_chat_mute",
|
||||
"cli_path": "chat +chat-mute"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_chat_role_list",
|
||||
"cli_path": "chat +chat-role-list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_chat_role_add",
|
||||
"cli_path": "chat +chat-role-add"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_chat_role_update",
|
||||
"cli_path": "chat +chat-role-update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_chat_role_set_user",
|
||||
"cli_path": "chat +chat-role-set-user"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_chat_role_query_user",
|
||||
"cli_path": "chat +chat-role-query-user"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_messages_send_by_webhook",
|
||||
"cli_path": "chat +messages-send-by-webhook"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_messages_list_direct",
|
||||
"cli_path": "chat +messages-list-direct"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_messages_list_unread_conversations",
|
||||
"cli_path": "chat +messages-list-unread-conversations"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_messages_mget",
|
||||
"cli_path": "chat +messages-mget"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_messages_query_send_status",
|
||||
"cli_path": "chat +messages-query-send-status"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_messages_read_status",
|
||||
"cli_path": "chat +messages-read-status"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_messages_update_card",
|
||||
"cli_path": "chat +messages-update-card"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_messages_list_pin",
|
||||
"cli_path": "chat +messages-list-pin"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_at_me",
|
||||
"cli_path": "chat +at-me"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_broadcast",
|
||||
"cli_path": "chat +broadcast"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_dm",
|
||||
"cli_path": "chat +dm"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_group_members",
|
||||
"cli_path": "chat +group-members"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_my_groups",
|
||||
"cli_path": "chat +my-groups"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_send_to_group",
|
||||
"cli_path": "chat +send-to-group"
|
||||
},
|
||||
{
|
||||
"canonical_path": "chat.shortcut_unread_chats",
|
||||
"cli_path": "chat +unread-chats"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
{
|
||||
"id": "contact",
|
||||
"tools": [
|
||||
{
|
||||
"canonical_path": "contact.add_employee",
|
||||
"cli_path": "contact user invite"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.exclusive_account_create",
|
||||
"cli_path": "contact account create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.get_dept_info_by_dept_id",
|
||||
"cli_path": "contact dept get-info"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.get_sub_depts_by_dept_id",
|
||||
"cli_path": "contact dept list-children"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.get_dept_members_by_deptId",
|
||||
"cli_path": "contact dept list-members"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.search_dept_by_keyword",
|
||||
"cli_path": "contact dept search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.list_my_followings",
|
||||
"cli_path": "contact relation list-my-followings"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.org_create",
|
||||
"cli_path": "contact org create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.query_dismission_employee_list",
|
||||
"cli_path": "contact user dismission search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.get_user_info_by_user_ids",
|
||||
"cli_path": "contact user get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.get_current_user_profile",
|
||||
"cli_path": "contact user get-self"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.list_authorized_roster_fields",
|
||||
"cli_path": "contact user profile fields"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.get_authorized_emp_rosterInfo",
|
||||
"cli_path": "contact user profile get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.search_contact_by_key_word",
|
||||
"cli_path": "contact user search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.search_user_by_mobile",
|
||||
"cli_path": "contact user search-mobile"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.department_create",
|
||||
"cli_path": "contact dept create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.department_update",
|
||||
"cli_path": "contact dept update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.employee_update",
|
||||
"cli_path": "contact user update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.exclusive_account_user_update",
|
||||
"cli_path": "contact account update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.self_user_profile_update",
|
||||
"cli_path": "contact user update-self"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.shortcut_list_followings",
|
||||
"cli_path": "contact +list-followings"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.shortcut_search_user",
|
||||
"cli_path": "contact +search-user"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.shortcut_search_mobile",
|
||||
"cli_path": "contact +search-mobile"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.shortcut_list_roles",
|
||||
"cli_path": "contact +list-roles"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.shortcut_list_role_members",
|
||||
"cli_path": "contact +list-role-members"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.shortcut_list_sub_depts",
|
||||
"cli_path": "contact +list-sub-depts"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.shortcut_list_dept_members",
|
||||
"cli_path": "contact +list-dept-members"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.shortcut_by_mobile",
|
||||
"cli_path": "contact +by-mobile"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.shortcut_dept_members",
|
||||
"cli_path": "contact +dept-members"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.shortcut_lookup",
|
||||
"cli_path": "contact +lookup"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.shortcut_org",
|
||||
"cli_path": "contact +org"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.shortcut_resolve_dept",
|
||||
"cli_path": "contact +resolve-dept"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.shortcut_team",
|
||||
"cli_path": "contact +team"
|
||||
},
|
||||
{
|
||||
"canonical_path": "contact.shortcut_me",
|
||||
"cli_path": "contact +me"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
{
|
||||
"id": "dev",
|
||||
"tools": [
|
||||
{
|
||||
"canonical_path": "dev.create_dev_app",
|
||||
"cli_path": "dev app create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.get_dev_app_credentials",
|
||||
"cli_path": "dev app credentials get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.delete_dev_app",
|
||||
"cli_path": "dev app delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.disable_dev_app",
|
||||
"cli_path": "dev app disable"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.enable_dev_app",
|
||||
"cli_path": "dev app enable"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.list_dev_app_events",
|
||||
"cli_path": "dev app event list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.subscribe_dev_app_events",
|
||||
"cli_path": "dev app event subscribe"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.unsubscribe_dev_app_events",
|
||||
"cli_path": "dev app event unsubscribe"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.get_dev_app",
|
||||
"cli_path": "dev app get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.list_dev_app",
|
||||
"cli_path": "dev app list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.add_dev_app_members",
|
||||
"cli_path": "dev app member add"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.list_dev_app_members",
|
||||
"cli_path": "dev app member list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.remove_dev_app_members",
|
||||
"cli_path": "dev app member remove"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.apply_dev_app_permissions",
|
||||
"cli_path": "dev app permission add"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.list_dev_app_permissions",
|
||||
"cli_path": "dev app permission list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.remove_dev_app_permissions",
|
||||
"cli_path": "dev app permission remove"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.set_extension_robot_config",
|
||||
"cli_path": "dev app robot config"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.disable_dev_app_robot",
|
||||
"cli_path": "dev app robot disable"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.enable_dev_app_robot",
|
||||
"cli_path": "dev app robot enable"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.get_extension_robot_config",
|
||||
"cli_path": "dev app robot get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.query_robot_create_result",
|
||||
"cli_path": "dev app robot result"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.submit_robot_create_task",
|
||||
"cli_path": "dev app robot submit"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.update_dev_app_security_config",
|
||||
"cli_path": "dev app security config"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.update_dev_app",
|
||||
"cli_path": "dev app update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.create_dev_app_version",
|
||||
"cli_path": "dev app version create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.get_dev_app_version_detail",
|
||||
"cli_path": "dev app version get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.list_dev_app_versions",
|
||||
"cli_path": "dev app version list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.publish_dev_app_version",
|
||||
"cli_path": "dev app version publish"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.get_dev_app_version_status",
|
||||
"cli_path": "dev app version status"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.set_extension_webapp_config",
|
||||
"cli_path": "dev app webapp config"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.get_extension_webapp_config",
|
||||
"cli_path": "dev app webapp get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.connect_status",
|
||||
"cli_path": "dev connect status"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.connect_stop",
|
||||
"cli_path": "dev connect stop"
|
||||
},
|
||||
{
|
||||
"canonical_path": "dev.search_open_platform_docs_rag",
|
||||
"cli_path": "dev doc search"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"id": "devapp",
|
||||
"tools": [
|
||||
{
|
||||
"canonical_path": "devapp.shortcut_list",
|
||||
"cli_path": "devapp +list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "devapp.shortcut_get",
|
||||
"cli_path": "devapp +get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "devapp.shortcut_create",
|
||||
"cli_path": "devapp +create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "devapp.shortcut_update",
|
||||
"cli_path": "devapp +update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "devapp.shortcut_delete",
|
||||
"cli_path": "devapp +delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "devapp.shortcut_enable",
|
||||
"cli_path": "devapp +enable"
|
||||
},
|
||||
{
|
||||
"canonical_path": "devapp.shortcut_disable",
|
||||
"cli_path": "devapp +disable"
|
||||
},
|
||||
{
|
||||
"canonical_path": "devapp.shortcut_webapp_get",
|
||||
"cli_path": "devapp +webapp-get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "devapp.shortcut_webapp_config",
|
||||
"cli_path": "devapp +webapp-config"
|
||||
},
|
||||
{
|
||||
"canonical_path": "devapp.shortcut_permission_list",
|
||||
"cli_path": "devapp +permission-list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "devapp.shortcut_member_list",
|
||||
"cli_path": "devapp +member-list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "devapp.shortcut_member_add",
|
||||
"cli_path": "devapp +member-add"
|
||||
},
|
||||
{
|
||||
"canonical_path": "devapp.shortcut_member_remove",
|
||||
"cli_path": "devapp +member-remove"
|
||||
},
|
||||
{
|
||||
"canonical_path": "devapp.shortcut_robot_get",
|
||||
"cli_path": "devapp +robot-get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "devapp.shortcut_event_list",
|
||||
"cli_path": "devapp +event-list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "devapp.shortcut_version_list",
|
||||
"cli_path": "devapp +version-list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "devapp.shortcut_version_get",
|
||||
"cli_path": "devapp +version-get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "devapp.shortcut_version_check_approval",
|
||||
"cli_path": "devapp +version-check-approval"
|
||||
},
|
||||
{
|
||||
"canonical_path": "devapp.shortcut_version_status",
|
||||
"cli_path": "devapp +version-status"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"id": "devdoc",
|
||||
"tools": [
|
||||
{
|
||||
"canonical_path": "devdoc.search_open_platform_docs_rag",
|
||||
"cli_path": "devdoc article search"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"id": "ding",
|
||||
"tools": [
|
||||
{
|
||||
"canonical_path": "ding.recall_ding_message",
|
||||
"cli_path": "ding message recall"
|
||||
},
|
||||
{
|
||||
"canonical_path": "ding.send_ding_message",
|
||||
"cli_path": "ding message send"
|
||||
},
|
||||
{
|
||||
"canonical_path": "ding.shortcut_list",
|
||||
"cli_path": "ding +list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "ding.shortcut_receiver_status",
|
||||
"cli_path": "ding +receiver-status"
|
||||
},
|
||||
{
|
||||
"canonical_path": "ding.shortcut_send_personal",
|
||||
"cli_path": "ding +send-personal"
|
||||
},
|
||||
{
|
||||
"canonical_path": "ding.shortcut_recall_personal",
|
||||
"cli_path": "ding +recall-personal"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
{
|
||||
"id": "doc",
|
||||
"tools": [
|
||||
{
|
||||
"canonical_path": "doc.delete_document_block",
|
||||
"cli_path": "doc block delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.insert_document_block",
|
||||
"cli_path": "doc block insert"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.list_document_blocks",
|
||||
"cli_path": "doc block list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.update_document_block",
|
||||
"cli_path": "doc block update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.create_comment",
|
||||
"cli_path": "doc comment create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.create_inline_comment",
|
||||
"cli_path": "doc comment create-inline"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.delete_comment",
|
||||
"cli_path": "doc comment delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.list_comments",
|
||||
"cli_path": "doc comment list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.reply_comment",
|
||||
"cli_path": "doc comment reply"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.update_comment",
|
||||
"cli_path": "doc comment update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.copy_document",
|
||||
"cli_path": "doc copy"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.create_document",
|
||||
"cli_path": "doc create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.delete_document",
|
||||
"cli_path": "doc delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.download_file",
|
||||
"cli_path": "doc download"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.query_export_job",
|
||||
"cli_path": "doc export get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.create_file",
|
||||
"cli_path": "doc file create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.create_folder",
|
||||
"cli_path": "doc folder create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.import_get",
|
||||
"cli_path": "doc import get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.get_document_info",
|
||||
"cli_path": "doc info"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.list_nodes",
|
||||
"cli_path": "doc list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.download_doc_attachment",
|
||||
"cli_path": "doc media download"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.media_insert",
|
||||
"cli_path": "doc media insert"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.move_document",
|
||||
"cli_path": "doc move"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.add_permission",
|
||||
"cli_path": "doc permission add"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.list_permission",
|
||||
"cli_path": "doc permission list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.remove_permission",
|
||||
"cli_path": "doc permission remove"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.update_permission",
|
||||
"cli_path": "doc permission update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.get_document_content",
|
||||
"cli_path": "doc read"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.rename_document",
|
||||
"cli_path": "doc rename"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.search_documents",
|
||||
"cli_path": "doc search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.template_apply",
|
||||
"cli_path": "doc template apply"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.template_list",
|
||||
"cli_path": "doc template list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.template_search",
|
||||
"cli_path": "doc template search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.update_document",
|
||||
"cli_path": "doc update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.upload",
|
||||
"cli_path": "doc upload"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.version_list",
|
||||
"cli_path": "doc version list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.version_revert",
|
||||
"cli_path": "doc version revert"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.version_save",
|
||||
"cli_path": "doc version save"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.shortcut_search",
|
||||
"cli_path": "doc +search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.shortcut_list",
|
||||
"cli_path": "doc +list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.shortcut_copy",
|
||||
"cli_path": "doc +copy"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.shortcut_move",
|
||||
"cli_path": "doc +move"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.shortcut_comment_list",
|
||||
"cli_path": "doc +comment-list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.shortcut_comment_create",
|
||||
"cli_path": "doc +comment-create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.shortcut_comment_reply",
|
||||
"cli_path": "doc +comment-reply"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.shortcut_export_submit",
|
||||
"cli_path": "doc +export-submit"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.shortcut_export_get",
|
||||
"cli_path": "doc +export-get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.shortcut_version_save",
|
||||
"cli_path": "doc +version-save"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.shortcut_version_list",
|
||||
"cli_path": "doc +version-list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.shortcut_version_revert",
|
||||
"cli_path": "doc +version-revert"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.shortcut_template_list",
|
||||
"cli_path": "doc +template-list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.shortcut_template_search",
|
||||
"cli_path": "doc +template-search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.shortcut_doc_append",
|
||||
"cli_path": "doc +doc-append"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.shortcut_find_doc",
|
||||
"cli_path": "doc +find-doc"
|
||||
},
|
||||
{
|
||||
"canonical_path": "doc.shortcut_share_doc",
|
||||
"cli_path": "doc +share-doc"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"id": "drive",
|
||||
"tools": [
|
||||
{
|
||||
"canonical_path": "drive.commit_upload",
|
||||
"cli_path": "drive commit"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.copy_document",
|
||||
"cli_path": "drive copy"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.delete_document",
|
||||
"cli_path": "drive delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.download_file",
|
||||
"cli_path": "drive download"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.get_file_info",
|
||||
"cli_path": "drive info"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.list_files",
|
||||
"cli_path": "drive list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.list_spaces",
|
||||
"cli_path": "drive list-spaces"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.create_folder",
|
||||
"cli_path": "drive mkdir"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.move_document",
|
||||
"cli_path": "drive move"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.add_permission",
|
||||
"cli_path": "drive permission add"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.list_permission",
|
||||
"cli_path": "drive permission list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.permission_remove",
|
||||
"cli_path": "drive permission remove"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.permission_update",
|
||||
"cli_path": "drive permission update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.publish_get",
|
||||
"cli_path": "drive publish get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.publish_set",
|
||||
"cli_path": "drive publish set"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.publish_unset",
|
||||
"cli_path": "drive publish unset"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.recent",
|
||||
"cli_path": "drive recent"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.recycle_list",
|
||||
"cli_path": "drive recycle list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.recycle_restore",
|
||||
"cli_path": "drive recycle restore"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.rename_document",
|
||||
"cli_path": "drive rename"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.search_files",
|
||||
"cli_path": "drive search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.create_shortcut",
|
||||
"cli_path": "drive shortcut"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.get_node_stats",
|
||||
"cli_path": "drive stats"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.upload",
|
||||
"cli_path": "drive upload"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.get_upload_info",
|
||||
"cli_path": "drive upload-info"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.shortcut_info",
|
||||
"cli_path": "drive +info"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.shortcut_search",
|
||||
"cli_path": "drive +search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.shortcut_search_docs",
|
||||
"cli_path": "drive +search-docs"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.shortcut_copy",
|
||||
"cli_path": "drive +copy"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.shortcut_move",
|
||||
"cli_path": "drive +move"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.shortcut_recent",
|
||||
"cli_path": "drive +recent"
|
||||
},
|
||||
{
|
||||
"canonical_path": "drive.shortcut_find_file",
|
||||
"cli_path": "drive +find-file"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"id": "event",
|
||||
"tools": [
|
||||
{
|
||||
"canonical_path": "event.consume",
|
||||
"cli_path": "event consume"
|
||||
},
|
||||
{
|
||||
"canonical_path": "event.list",
|
||||
"cli_path": "event list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "event.schema",
|
||||
"cli_path": "event schema"
|
||||
},
|
||||
{
|
||||
"canonical_path": "event.status",
|
||||
"cli_path": "event status"
|
||||
},
|
||||
{
|
||||
"canonical_path": "event.stop",
|
||||
"cli_path": "event stop"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"id": "hrbrain",
|
||||
"tools": [
|
||||
{
|
||||
"canonical_path": "hrbrain.list_talent_pools",
|
||||
"cli_path": "hrbrain talent-pool list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "hrbrain.get_talent_pool_detail",
|
||||
"cli_path": "hrbrain talent-pool detail"
|
||||
},
|
||||
{
|
||||
"canonical_path": "hrbrain.list_pool_employees",
|
||||
"cli_path": "hrbrain talent-pool employees"
|
||||
},
|
||||
{
|
||||
"canonical_path": "hrbrain.get_profile_metadata",
|
||||
"cli_path": "hrbrain profile metadata"
|
||||
},
|
||||
{
|
||||
"canonical_path": "hrbrain.query_profile_data",
|
||||
"cli_path": "hrbrain profile query"
|
||||
},
|
||||
{
|
||||
"canonical_path": "hrbrain.get_profile_label",
|
||||
"cli_path": "hrbrain profile labels"
|
||||
},
|
||||
{
|
||||
"canonical_path": "hrbrain.get_employee_career",
|
||||
"cli_path": "hrbrain profile career"
|
||||
},
|
||||
{
|
||||
"canonical_path": "hrbrain.get_employee_performance",
|
||||
"cli_path": "hrbrain profile performance"
|
||||
},
|
||||
{
|
||||
"canonical_path": "hrbrain.search_employees",
|
||||
"cli_path": "hrbrain search employees"
|
||||
},
|
||||
{
|
||||
"canonical_path": "hrbrain.search_employees_structured",
|
||||
"cli_path": "hrbrain search employees-structured"
|
||||
},
|
||||
{
|
||||
"canonical_path": "hrbrain.get_search_fields",
|
||||
"cli_path": "hrbrain search fields"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"id": "live",
|
||||
"tools": [
|
||||
{
|
||||
"canonical_path": "live.get_my_lives",
|
||||
"cli_path": "live stream list"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
{
|
||||
"id": "mail",
|
||||
"tools": [
|
||||
{
|
||||
"canonical_path": "mail.create_download_session",
|
||||
"cli_path": "mail attachment download"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.list_mail_attachments",
|
||||
"cli_path": "mail attachment list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.batch_delete_user_mail_contacts",
|
||||
"cli_path": "mail contact batch-delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.create_user_mail_contact",
|
||||
"cli_path": "mail contact create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.list_user_mail_contacts",
|
||||
"cli_path": "mail contact list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.update_user_mail_contact",
|
||||
"cli_path": "mail contact update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.create_draft",
|
||||
"cli_path": "mail draft create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.send_draft",
|
||||
"cli_path": "mail draft send"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.update_draft",
|
||||
"cli_path": "mail draft update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.create_mail_folder",
|
||||
"cli_path": "mail folder create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.delete_mail_folder",
|
||||
"cli_path": "mail folder delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.list_folders",
|
||||
"cli_path": "mail folder list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.update_mail_folder",
|
||||
"cli_path": "mail folder update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.list_user_mailboxes",
|
||||
"cli_path": "mail mailbox list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.batch_delete_message",
|
||||
"cli_path": "mail message batch-delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.batch_move_message",
|
||||
"cli_path": "mail message batch-move"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.create_forward_draft",
|
||||
"cli_path": "mail message forward"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.get_email_by_message_id",
|
||||
"cli_path": "mail message get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.create_reply_draft",
|
||||
"cli_path": "mail message reply"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.create_replyall_draft",
|
||||
"cli_path": "mail message reply-all"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.search_emails",
|
||||
"cli_path": "mail message search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.list_emails",
|
||||
"cli_path": "mail message list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.send_email",
|
||||
"cli_path": "mail message send"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.list_tags",
|
||||
"cli_path": "mail tag list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.create_user_message_template",
|
||||
"cli_path": "mail template create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.delete_user_message_template",
|
||||
"cli_path": "mail template delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.get_user_message_template",
|
||||
"cli_path": "mail template get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.list_user_message_templates",
|
||||
"cli_path": "mail template list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.update_user_message_template",
|
||||
"cli_path": "mail template update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.get_thread",
|
||||
"cli_path": "mail thread get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.search_mail_users",
|
||||
"cli_path": "mail user search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.shortcut_thread_list",
|
||||
"cli_path": "mail +thread-list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.shortcut_folder_list",
|
||||
"cli_path": "mail +folder-list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.shortcut_tag_list",
|
||||
"cli_path": "mail +tag-list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.shortcut_user_search",
|
||||
"cli_path": "mail +user-search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.shortcut_template_list",
|
||||
"cli_path": "mail +template-list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.shortcut_contact_list",
|
||||
"cli_path": "mail +contact-list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.shortcut_find_mail_user",
|
||||
"cli_path": "mail +find-mail-user"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.shortcut_recent_mail",
|
||||
"cli_path": "mail +recent-mail"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.shortcut_search_mail",
|
||||
"cli_path": "mail +search-mail"
|
||||
},
|
||||
{
|
||||
"canonical_path": "mail.shortcut_unread_mail",
|
||||
"cli_path": "mail +unread-mail"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"id": "markdown",
|
||||
"tools": [
|
||||
{
|
||||
"canonical_path": "markdown.create",
|
||||
"cli_path": "markdown create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "markdown.fetch",
|
||||
"cli_path": "markdown fetch"
|
||||
},
|
||||
{
|
||||
"canonical_path": "markdown.overwrite",
|
||||
"cli_path": "markdown overwrite"
|
||||
},
|
||||
{
|
||||
"canonical_path": "markdown.patch",
|
||||
"cli_path": "markdown patch"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"id": "mcp",
|
||||
"tools": [
|
||||
{
|
||||
"canonical_path": "mcp.url_get",
|
||||
"cli_path": "mcp url get"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
{
|
||||
"id": "minutes",
|
||||
"tools": [
|
||||
{
|
||||
"canonical_path": "minutes.query_minutes_audio_url",
|
||||
"cli_path": "minutes get audio"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.batch_get_minutes_details",
|
||||
"cli_path": "minutes get batch"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.get_minutes_basic_info",
|
||||
"cli_path": "minutes get info"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.get_minutes_keywords",
|
||||
"cli_path": "minutes get keywords"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.get_minutes_ai_summary",
|
||||
"cli_path": "minutes get summary"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.list_minutes_todos",
|
||||
"cli_path": "minutes get todos"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.get_minutes_transcription",
|
||||
"cli_path": "minutes get transcription"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.add_personal_hot_word",
|
||||
"cli_path": "minutes hot-word add"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.list_my_hotwords",
|
||||
"cli_path": "minutes hot-word list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.list_accessible_minutes",
|
||||
"cli_path": "minutes list all"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.list_by_keyword_and_time_range",
|
||||
"cli_path": "minutes list mine"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.list_shared_minutes",
|
||||
"cli_path": "minutes list shared"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.create_mind_graph",
|
||||
"cli_path": "minutes mind-graph create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.query_mind_graph_status",
|
||||
"cli_path": "minutes mind-graph status"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.add_member_permission",
|
||||
"cli_path": "minutes permission add"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.remove_member_permission",
|
||||
"cli_path": "minutes permission remove"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.record_pause",
|
||||
"cli_path": "minutes record pause"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.record_resume",
|
||||
"cli_path": "minutes record resume"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.record_start",
|
||||
"cli_path": "minutes record start"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.record_stop",
|
||||
"cli_path": "minutes record stop"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.replace_minutes_text",
|
||||
"cli_path": "minutes replace-text"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.replace_speaker",
|
||||
"cli_path": "minutes speaker replace"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.create_speaker_summary",
|
||||
"cli_path": "minutes speaker summary create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.get_speaker_summary",
|
||||
"cli_path": "minutes speaker summary get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.query_user_tag_list",
|
||||
"cli_path": "minutes tag list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.query_minutes_by_tag_id",
|
||||
"cli_path": "minutes tag query"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.update_minutes_summary",
|
||||
"cli_path": "minutes update summary"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.update_minutes_title",
|
||||
"cli_path": "minutes update title"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.cancel_upload_session",
|
||||
"cli_path": "minutes upload cancel"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.complete_upload_session",
|
||||
"cli_path": "minutes upload complete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.create_upload_session",
|
||||
"cli_path": "minutes upload create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.shortcut_list_mine",
|
||||
"cli_path": "minutes +list-mine"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.shortcut_list_shared",
|
||||
"cli_path": "minutes +list-shared"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.shortcut_list_all",
|
||||
"cli_path": "minutes +list-all"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.shortcut_record_start",
|
||||
"cli_path": "minutes +record-start"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.shortcut_detail",
|
||||
"cli_path": "minutes +detail"
|
||||
},
|
||||
{
|
||||
"canonical_path": "minutes.shortcut_replace_batch",
|
||||
"cli_path": "minutes +replace-batch"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"id": "oa",
|
||||
"tools": [
|
||||
{
|
||||
"canonical_path": "oa.approve_processInstance",
|
||||
"cli_path": "oa approval approve"
|
||||
},
|
||||
{
|
||||
"canonical_path": "oa.get_processInstance_detail",
|
||||
"cli_path": "oa approval detail"
|
||||
},
|
||||
{
|
||||
"canonical_path": "oa.get_noticed_instances",
|
||||
"cli_path": "oa approval list-cc"
|
||||
},
|
||||
{
|
||||
"canonical_path": "oa.get_done_tasks",
|
||||
"cli_path": "oa approval list-executed"
|
||||
},
|
||||
{
|
||||
"canonical_path": "oa.list_user_visible_process",
|
||||
"cli_path": "oa approval list-forms"
|
||||
},
|
||||
{
|
||||
"canonical_path": "oa.list_initiated_instances",
|
||||
"cli_path": "oa approval list-initiated"
|
||||
},
|
||||
{
|
||||
"canonical_path": "oa.list_pending_approvals",
|
||||
"cli_path": "oa approval list-pending"
|
||||
},
|
||||
{
|
||||
"canonical_path": "oa.get_submitted_instances",
|
||||
"cli_path": "oa approval list-submitted"
|
||||
},
|
||||
{
|
||||
"canonical_path": "oa.oa_cc_noticer",
|
||||
"cli_path": "oa approval oa-cc-noticer"
|
||||
},
|
||||
{
|
||||
"canonical_path": "oa.dingflow_comments",
|
||||
"cli_path": "oa approval oa-comments"
|
||||
},
|
||||
{
|
||||
"canonical_path": "oa.get_processInstance_records",
|
||||
"cli_path": "oa approval records"
|
||||
},
|
||||
{
|
||||
"canonical_path": "oa.redirect_task",
|
||||
"cli_path": "oa approval redirect-task"
|
||||
},
|
||||
{
|
||||
"canonical_path": "oa.reject_processInstance",
|
||||
"cli_path": "oa approval reject"
|
||||
},
|
||||
{
|
||||
"canonical_path": "oa.revoke_processInstance",
|
||||
"cli_path": "oa approval revoke"
|
||||
},
|
||||
{
|
||||
"canonical_path": "oa.list_pending_tasks",
|
||||
"cli_path": "oa approval tasks"
|
||||
},
|
||||
{
|
||||
"canonical_path": "oa.shortcut_list_pending",
|
||||
"cli_path": "oa +list-pending"
|
||||
},
|
||||
{
|
||||
"canonical_path": "oa.shortcut_list_forms",
|
||||
"cli_path": "oa +list-forms"
|
||||
},
|
||||
{
|
||||
"canonical_path": "oa.shortcut_search_forms",
|
||||
"cli_path": "oa +search-forms"
|
||||
},
|
||||
{
|
||||
"canonical_path": "oa.shortcut_list_executed",
|
||||
"cli_path": "oa +list-executed"
|
||||
},
|
||||
{
|
||||
"canonical_path": "oa.shortcut_list_submitted",
|
||||
"cli_path": "oa +list-submitted"
|
||||
},
|
||||
{
|
||||
"canonical_path": "oa.shortcut_list_cc",
|
||||
"cli_path": "oa +list-cc"
|
||||
},
|
||||
{
|
||||
"canonical_path": "oa.shortcut_my_initiated",
|
||||
"cli_path": "oa +my-initiated"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "pat",
|
||||
"tools": [
|
||||
{
|
||||
"canonical_path": "pat.browser_policy",
|
||||
"cli_path": "pat browser-policy"
|
||||
},
|
||||
{
|
||||
"canonical_path": "pat.batch_grant",
|
||||
"cli_path": "pat chmod"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"id": "report",
|
||||
"tools": [
|
||||
{
|
||||
"canonical_path": "report.get_report_entry_details",
|
||||
"cli_path": "report entry get",
|
||||
"aliases": [
|
||||
"report detail"
|
||||
]
|
||||
},
|
||||
{
|
||||
"canonical_path": "report.get_report_statistics_by_id",
|
||||
"cli_path": "report entry stats",
|
||||
"aliases": [
|
||||
"report stats"
|
||||
]
|
||||
},
|
||||
{
|
||||
"canonical_path": "report.create_report",
|
||||
"cli_path": "report entry submit",
|
||||
"aliases": [
|
||||
"report create"
|
||||
]
|
||||
},
|
||||
{
|
||||
"canonical_path": "report.get_received_report_list",
|
||||
"cli_path": "report inbox list",
|
||||
"aliases": [
|
||||
"report list"
|
||||
]
|
||||
},
|
||||
{
|
||||
"canonical_path": "report.get_send_report_list",
|
||||
"cli_path": "report outbox list",
|
||||
"aliases": [
|
||||
"report created",
|
||||
"report sent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"canonical_path": "report.get_template_details_by_name",
|
||||
"cli_path": "report template get",
|
||||
"aliases": [
|
||||
"report template detail"
|
||||
]
|
||||
},
|
||||
{
|
||||
"canonical_path": "report.get_available_report_templates",
|
||||
"cli_path": "report template list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "report.shortcut_inbox_list",
|
||||
"cli_path": "report +inbox-list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "report.shortcut_outbox_list",
|
||||
"cli_path": "report +outbox-list"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
{
|
||||
"id": "sheet",
|
||||
"tools": [
|
||||
{
|
||||
"canonical_path": "sheet.add_dimension",
|
||||
"cli_path": "sheet add-dimension"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.append_rows",
|
||||
"cli_path": "sheet append"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.batch_update",
|
||||
"cli_path": "sheet batch-update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.chart_create",
|
||||
"cli_path": "sheet chart create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.chart_delete",
|
||||
"cli_path": "sheet chart delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.chart_list",
|
||||
"cli_path": "sheet chart list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.chart_update",
|
||||
"cli_path": "sheet chart update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.create_cond_format",
|
||||
"cli_path": "sheet cond-format create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.delete_cond_format",
|
||||
"cli_path": "sheet cond-format delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.get_cond_format",
|
||||
"cli_path": "sheet cond-format list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.update_cond_format",
|
||||
"cli_path": "sheet cond-format update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.copy_sheet",
|
||||
"cli_path": "sheet copy"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.create_workspace_sheet",
|
||||
"cli_path": "sheet create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.create_float_image",
|
||||
"cli_path": "sheet create-float-image"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.get_range_as_csv",
|
||||
"cli_path": "sheet csv-get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.set_range_from_csv",
|
||||
"cli_path": "sheet csv-put"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.delete_dimension",
|
||||
"cli_path": "sheet delete-dimension"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.delete_dropdown_lists",
|
||||
"cli_path": "sheet delete-dropdown"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.delete_float_image",
|
||||
"cli_path": "sheet delete-float-image"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.delete_sheet",
|
||||
"cli_path": "sheet delete-sheet"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.submit_export_job",
|
||||
"cli_path": "sheet export"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.filter_clear_criteria",
|
||||
"cli_path": "sheet filter clear-criteria"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.create_filter",
|
||||
"cli_path": "sheet filter create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.delete_filter",
|
||||
"cli_path": "sheet filter delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.get_filter",
|
||||
"cli_path": "sheet filter get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.sort_filter",
|
||||
"cli_path": "sheet filter sort"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.update_filter",
|
||||
"cli_path": "sheet filter update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.create_filter_view",
|
||||
"cli_path": "sheet filter-view create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.delete_filter_view",
|
||||
"cli_path": "sheet filter-view delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.filter_view_delete_criteria",
|
||||
"cli_path": "sheet filter-view delete-criteria"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.filter_view_get_criteria",
|
||||
"cli_path": "sheet filter-view get-criteria"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.filter_view_info",
|
||||
"cli_path": "sheet filter-view info"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.get_filter_views",
|
||||
"cli_path": "sheet filter-view list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.filter_view_list_criteria",
|
||||
"cli_path": "sheet filter-view list-criteria"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.update_filter_view",
|
||||
"cli_path": "sheet filter-view update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.filter_view_update_criteria",
|
||||
"cli_path": "sheet filter-view update-criteria"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.find_cells",
|
||||
"cli_path": "sheet find"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.get_dropdown_lists",
|
||||
"cli_path": "sheet get-dropdown"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.get_float_image",
|
||||
"cli_path": "sheet get-float-image"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.group_dimension",
|
||||
"cli_path": "sheet group-dimension"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.hide_gridline",
|
||||
"cli_path": "sheet hide-gridline"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.get_all_sheets",
|
||||
"cli_path": "sheet list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.list_float_images",
|
||||
"cli_path": "sheet list-float-images"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.media_upload",
|
||||
"cli_path": "sheet media-upload"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.merge_cells",
|
||||
"cli_path": "sheet merge-cells"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.move_dimension",
|
||||
"cli_path": "sheet move-dimension"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.create_sheet",
|
||||
"cli_path": "sheet new"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.create_pivot_table",
|
||||
"cli_path": "sheet pivot-table create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.delete_pivot_table",
|
||||
"cli_path": "sheet pivot-table delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.list_pivot_tables",
|
||||
"cli_path": "sheet pivot-table list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.update_pivot_table",
|
||||
"cli_path": "sheet pivot-table update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.range_batch_clear",
|
||||
"cli_path": "sheet range batch-clear"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.range_batch_set_style",
|
||||
"cli_path": "sheet range batch-set-style"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.clear_range",
|
||||
"cli_path": "sheet range clear"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.range_copy_to",
|
||||
"cli_path": "sheet range copy-to"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.fill_range",
|
||||
"cli_path": "sheet range fill"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.range_move_to",
|
||||
"cli_path": "sheet range move-to"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.range_read",
|
||||
"cli_path": "sheet range read"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.range_set_style",
|
||||
"cli_path": "sheet range set-style"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.sort_range",
|
||||
"cli_path": "sheet range sort"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.range_update",
|
||||
"cli_path": "sheet range update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.replace_all",
|
||||
"cli_path": "sheet replace"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.set_dropdown_lists",
|
||||
"cli_path": "sheet set-dropdown"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.show_gridline",
|
||||
"cli_path": "sheet show-gridline"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.table_get",
|
||||
"cli_path": "sheet table-get",
|
||||
"aliases": [
|
||||
"sheet table-read"
|
||||
]
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.table_put",
|
||||
"cli_path": "sheet table-put",
|
||||
"aliases": [
|
||||
"sheet table-write"
|
||||
]
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.template_apply",
|
||||
"cli_path": "sheet template apply"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.template_list",
|
||||
"cli_path": "sheet template list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.template_search",
|
||||
"cli_path": "sheet template search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.ungroup_dimension",
|
||||
"cli_path": "sheet ungroup-dimension"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.unmerge_range",
|
||||
"cli_path": "sheet unmerge-cells"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.update_sheet",
|
||||
"cli_path": "sheet update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.update_dimension",
|
||||
"cli_path": "sheet update-dimension"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.update_float_image",
|
||||
"cli_path": "sheet update-float-image"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.write_image",
|
||||
"cli_path": "sheet write-image"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.shortcut_list_sheets",
|
||||
"cli_path": "sheet +list-sheets"
|
||||
},
|
||||
{
|
||||
"canonical_path": "sheet.shortcut_read",
|
||||
"cli_path": "sheet +read"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
{
|
||||
"id": "todo",
|
||||
"tools": [
|
||||
{
|
||||
"canonical_path": "todo.add_todo_comment",
|
||||
"cli_path": "todo comment add"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.delete_todo_comment",
|
||||
"cli_path": "todo comment delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.list_todo_comment",
|
||||
"cli_path": "todo comment list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.add_todo_attachment",
|
||||
"cli_path": "todo task add-attachment"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.list_todo_attachment",
|
||||
"cli_path": "todo task list-attachment"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.add_task_executors",
|
||||
"cli_path": "todo task add-executor"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.add_task_participants",
|
||||
"cli_path": "todo task add-participant"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.add_todo_reminder",
|
||||
"cli_path": "todo task add-reminder"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.create_personal_todo",
|
||||
"cli_path": "todo task create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.create_personal_sub_todo",
|
||||
"cli_path": "todo task create-sub"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.delete_todo",
|
||||
"cli_path": "todo task delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.update_todo_done_status",
|
||||
"cli_path": "todo task done"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.get_todo_detail",
|
||||
"cli_path": "todo task get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.get_user_todos_in_current_org",
|
||||
"cli_path": "todo task list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.remove_task_executors",
|
||||
"cli_path": "todo task remove-executor"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.remove_task_participants",
|
||||
"cli_path": "todo task remove-participant"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.reset_todo_reminder",
|
||||
"cli_path": "todo task reset-reminder"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.update_todo_task",
|
||||
"cli_path": "todo task update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.create_todo_tag",
|
||||
"cli_path": "todo tag create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.delete_todo_tag",
|
||||
"cli_path": "todo tag delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.list_todo_tags",
|
||||
"cli_path": "todo tag list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.tag_todo",
|
||||
"cli_path": "todo tag add"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.update_todo_tag",
|
||||
"cli_path": "todo tag update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.shortcut_assign",
|
||||
"cli_path": "todo +assign"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.shortcut_assign_multi",
|
||||
"cli_path": "todo +assign-multi"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.shortcut_created_todos",
|
||||
"cli_path": "todo +created-todos"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.shortcut_overdue",
|
||||
"cli_path": "todo +overdue"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.shortcut_remind",
|
||||
"cli_path": "todo +remind"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.shortcut_todo_done",
|
||||
"cli_path": "todo +todo-done"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.shortcut_get_my_tasks",
|
||||
"cli_path": "todo +get-my-tasks"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.shortcut_list_sub",
|
||||
"cli_path": "todo +list-sub"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.shortcut_get",
|
||||
"cli_path": "todo +get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.shortcut_list_attachment",
|
||||
"cli_path": "todo +list-attachment"
|
||||
},
|
||||
{
|
||||
"canonical_path": "todo.shortcut_list_comment",
|
||||
"cli_path": "todo +list-comment"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"id": "wiki",
|
||||
"tools": [
|
||||
{
|
||||
"canonical_path": "wiki.add_member",
|
||||
"cli_path": "wiki member add"
|
||||
},
|
||||
{
|
||||
"canonical_path": "wiki.list_member",
|
||||
"cli_path": "wiki member list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "wiki.remove_member",
|
||||
"cli_path": "wiki member remove"
|
||||
},
|
||||
{
|
||||
"canonical_path": "wiki.update_member",
|
||||
"cli_path": "wiki member update"
|
||||
},
|
||||
{
|
||||
"canonical_path": "wiki.node_copy",
|
||||
"cli_path": "wiki node copy"
|
||||
},
|
||||
{
|
||||
"canonical_path": "wiki.create_file",
|
||||
"cli_path": "wiki node create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "wiki.delete_document",
|
||||
"cli_path": "wiki node delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "wiki.list_nodes",
|
||||
"cli_path": "wiki node list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "wiki.node_move",
|
||||
"cli_path": "wiki node move"
|
||||
},
|
||||
{
|
||||
"canonical_path": "wiki.node_search",
|
||||
"cli_path": "wiki node search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "wiki.create_wikiSpace",
|
||||
"cli_path": "wiki space create"
|
||||
},
|
||||
{
|
||||
"canonical_path": "wiki.delete_wikiSpace",
|
||||
"cli_path": "wiki space delete"
|
||||
},
|
||||
{
|
||||
"canonical_path": "wiki.get_wikiSpace",
|
||||
"cli_path": "wiki space get"
|
||||
},
|
||||
{
|
||||
"canonical_path": "wiki.list_wikiSpaces",
|
||||
"cli_path": "wiki space list"
|
||||
},
|
||||
{
|
||||
"canonical_path": "wiki.search_wikiSpaces",
|
||||
"cli_path": "wiki space search"
|
||||
},
|
||||
{
|
||||
"canonical_path": "wiki.shortcut_space_search",
|
||||
"cli_path": "wiki +space-search"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "./schema_command_registry.schema.json",
|
||||
"version": 1
|
||||
}
|
||||
@@ -34,8 +34,12 @@ func TestCommandRegistryJSONSchemaDocumentsClosedCommandSpec(t *testing.T) {
|
||||
}
|
||||
|
||||
var source map[string]any
|
||||
if err := json.Unmarshal(embeddedSchemaCommandRegistryJSON, &source); err != nil {
|
||||
t.Fatalf("decode schema_command_registry.json: %v", err)
|
||||
merged, err := EmbeddedCommandRegistryMergedJSON()
|
||||
if err != nil {
|
||||
t.Fatalf("EmbeddedCommandRegistryMergedJSON() error = %v", err)
|
||||
}
|
||||
if err := json.Unmarshal(merged, &source); err != nil {
|
||||
t.Fatalf("decode merged registry: %v", err)
|
||||
}
|
||||
if source["$schema"] != commandRegistrySchemaRef {
|
||||
t.Fatalf("registry source $schema = %#v, want %q", source["$schema"], commandRegistrySchemaRef)
|
||||
|
||||
@@ -875,7 +875,7 @@
|
||||
"availability": "available",
|
||||
"interface_reason": "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
||||
"reviewed": true,
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity.",
|
||||
"review_reason": "Reviewed against the mounted Shortcut: --type all|group|direct is a local semantic filter over list_top_conversations.singleChat, so the composite interface owns validation and normalized output without changing lower-service arguments.",
|
||||
"cli_path": "chat +conversation-list-top",
|
||||
"runtime_gate": "none"
|
||||
},
|
||||
|
||||
@@ -1692,18 +1692,19 @@
|
||||
]
|
||||
},
|
||||
"chat.shortcut_conversation_list_top": {
|
||||
"agent_summary": "拉取置顶会话列表",
|
||||
"agent_summary": "拉取置顶会话列表,可只看群聊或单聊",
|
||||
"use_when": [
|
||||
"当你只想查看被置顶的那些会话时使用;只读分页返回置顶会话列表,可用 --exclude-muted 排除已免打扰会话。"
|
||||
"当你只想查看被置顶的那些会话时使用;只读分页返回置顶会话列表,并把下层 singleChat 规范化为 conversationType=group|direct。可用 --type group 只看群聊、--type direct 只看单聊,或用 --exclude-muted 排除已免打扰会话。"
|
||||
],
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
"用户要查看群内被 Pin 的消息而不是侧边栏置顶会话时,改用 +messages-list-pin"
|
||||
],
|
||||
"examples": [
|
||||
"dws chat +conversation-list-top --limit 1000"
|
||||
"dws chat +conversation-list-top --type group --limit 1000",
|
||||
"dws chat +conversation-list-top --type direct --limit 1000"
|
||||
],
|
||||
"reviewed": true,
|
||||
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
|
||||
"review_reason": "Reviewed after live testing list_top_conversations: singleChat is present on every tested row and is normalized locally to group/direct without adding a lower-service parameter.",
|
||||
"source_refs": [
|
||||
"internal/cli/schema_command_registry.json#chat.shortcut_conversation_list_top",
|
||||
"cobra-help:dws chat +conversation-list-top",
|
||||
@@ -1732,7 +1733,7 @@
|
||||
"chat.shortcut_category_create": {
|
||||
"agent_summary": "创建用户自定义会话分组",
|
||||
"use_when": [
|
||||
"当你想新建一个会话分组来归类会话时使用;会实际创建分组并返回其 ID,需传分组名称 --title。"
|
||||
"当你想新建一个会话分组来归类会话时使用;会实际创建分组并返回其 ID,需传最多 15 个字符的分组名称 --title。"
|
||||
],
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
@@ -1770,7 +1771,7 @@
|
||||
"chat.shortcut_category_rename": {
|
||||
"agent_summary": "更新用户自定义会话分组的名称",
|
||||
"use_when": [
|
||||
"当你想重命名已有的自定义会话分组时使用;会实际更新分组名称,需传 categoryId 和新名称 --title。"
|
||||
"当你想重命名已有的自定义会话分组时使用;会实际更新分组名称,需传 categoryId 和最多 15 个字符的新名称 --title。"
|
||||
],
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
@@ -2169,7 +2170,7 @@
|
||||
"chat.shortcut_messages_mget": {
|
||||
"agent_summary": "根据消息 ID 批量查询消息(最多 50 条)",
|
||||
"use_when": [
|
||||
"当你已有一批消息 openMsgId、需要批量取回它们的详细内容时使用;只读,一次最多查询 50 条。"
|
||||
"当你已有一批消息 openMsgId、需要批量取回完整详情、reaction 和可执行资源引用时使用;一次最多 50 条。--download-resources 可把所有可识别 mediaId 安全下载到工作目录内,并逐资源返回成功/失败 ledger。"
|
||||
],
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
@@ -2178,7 +2179,7 @@
|
||||
"dws chat +messages-mget --msg-ids msgId1,msgId2"
|
||||
],
|
||||
"reviewed": true,
|
||||
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
|
||||
"review_reason": "Reviewed against the built-in Shortcut, executable Cobra flags, safe relative-path download contract, and per-resource failure ledger; this selection update advertises the new optional resource workflow without changing its runtime safety facts.",
|
||||
"source_refs": [
|
||||
"internal/cli/schema_command_registry.json#chat.shortcut_messages_mget",
|
||||
"cobra-help:dws chat +messages-mget",
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"version": 3,
|
||||
"baseline": {
|
||||
"manifest": "schema-parameter-bindings-v3",
|
||||
"sha256": "sha256:9008afd72243232d4bcb37b46f1e8fc10e2bc283c8af26c59fbdb4901ebeef2f",
|
||||
"reason": "Reviewed v3 baseline for the complete active canonical/flag/property manifest after splitting the fixed-scope minutes list commands into distinct registry identities.",
|
||||
"sha256": "sha256:f48d7b55b6ca9719c466475ee32d678eab0deaee1d79ac4e4790c7ee441ebe7b",
|
||||
"reason": "Reviewed v3 baseline after exposing the pinned chat.search_messages messageType, onlyRobotMessages, and searchConvType properties through exact Cobra flags.",
|
||||
"reviewed": true
|
||||
},
|
||||
"removals": {
|
||||
@@ -408,8 +408,11 @@
|
||||
},
|
||||
"chat.search_messages": {
|
||||
"at-ids": "atOpenDingTakIds",
|
||||
"conversation-type": "searchConvType",
|
||||
"conversation-ids": "openConversationIds",
|
||||
"end": "endTime",
|
||||
"message-type": "messageType",
|
||||
"only-robot": "onlyRobotMessages",
|
||||
"query": "keyword",
|
||||
"sender-ids": "senderOpenDingTakIds",
|
||||
"start": "startTime"
|
||||
|
||||
@@ -22,7 +22,14 @@ func TestCrossPlatformCoverageCommandRegistryLoaderErrorWrappers(t *testing.T) {
|
||||
})
|
||||
|
||||
loadReviewedCommandRegistry = func() (CommandRegistry, error) { return CommandRegistry{}, errors.New("registry failed") }
|
||||
if _, err := ValidateCommandRegistrySource(embeddedSchemaCommandRegistryJSON); err == nil || !strings.Contains(err.Error(), "registry failed") {
|
||||
// ValidateCommandRegistrySource first decodes the input independently, then
|
||||
// compares against embedded. The decode path succeeds even when the lazy
|
||||
// accessor is broken, so this test catches the accessor failure.
|
||||
merged, err := EmbeddedCommandRegistryMergedJSON()
|
||||
if err != nil {
|
||||
t.Fatalf("EmbeddedCommandRegistryMergedJSON() error = %v", err)
|
||||
}
|
||||
if _, err := ValidateCommandRegistrySource(merged); err == nil || !strings.Contains(err.Error(), "registry failed") {
|
||||
t.Fatalf("ValidateCommandRegistrySource() error = %v", err)
|
||||
}
|
||||
if _, err := EmbeddedCommandRegistrySourceHash(); err == nil || !strings.Contains(err.Error(), "registry failed") {
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// failingReadFS lists entries but fails ReadFile, driving shard-read errors.
|
||||
type failingReadFS struct{ fstest.MapFS }
|
||||
|
||||
func (f failingReadFS) ReadFile(name string) ([]byte, error) {
|
||||
if strings.HasSuffix(name, ".json") {
|
||||
return nil, errors.New("shard read denied")
|
||||
}
|
||||
return f.MapFS.ReadFile(name)
|
||||
}
|
||||
|
||||
func TestAssembleSchemaCatalogSnapshotMergesShards(t *testing.T) {
|
||||
envelope := []byte(`{"version":1,"surface_hash":"sha256:s","source_hash":"sha256:c","catalog":{"kind":"schema"}}`)
|
||||
shards := fstest.MapFS{
|
||||
"tools/doc.json": {Data: []byte(`{"product":"doc","tools":{"doc.copy":{"title":"复制"}}}`)},
|
||||
"tools/sheet.json": {Data: []byte(`{"product":"sheet","tools":{"sheet.get":{"title":"读取"}}}`)},
|
||||
"tools/README.md": {Data: []byte("not a shard")},
|
||||
}
|
||||
snapshot, err := assembleSchemaCatalogSnapshot(envelope, shards, "tools")
|
||||
if err != nil {
|
||||
t.Fatalf("assembleSchemaCatalogSnapshot() error = %v", err)
|
||||
}
|
||||
if snapshot.Version != 1 || snapshot.SurfaceHash != "sha256:s" || snapshot.SourceHash != "sha256:c" {
|
||||
t.Fatalf("envelope fields lost: %+v", snapshot)
|
||||
}
|
||||
if len(snapshot.Tools) != 2 || snapshot.Tools["doc.copy"] == nil || snapshot.Tools["sheet.get"] == nil {
|
||||
t.Fatalf("tools = %#v, want merged doc.copy + sheet.get", snapshot.Tools)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssembleSchemaCatalogSnapshotFailureModes(t *testing.T) {
|
||||
valid := []byte(`{"version":1,"source_hash":"sha256:c","catalog":{}}`)
|
||||
if _, err := assembleSchemaCatalogSnapshot([]byte("{bad"), fstest.MapFS{}, "tools"); err == nil || !strings.Contains(err.Error(), "decode embedded schema catalog.json") {
|
||||
t.Fatalf("bad envelope err = %v", err)
|
||||
}
|
||||
if _, err := assembleSchemaCatalogSnapshot(valid, fstest.MapFS{}, "missing-dir"); err == nil || !strings.Contains(err.Error(), "tools directory") {
|
||||
t.Fatalf("missing dir err = %v", err)
|
||||
}
|
||||
badShard := fstest.MapFS{"tools/doc.json": {Data: []byte("{bad")}}
|
||||
if _, err := assembleSchemaCatalogSnapshot(valid, badShard, "tools"); err == nil || !strings.Contains(err.Error(), "decode embedded schema catalog shard") {
|
||||
t.Fatalf("bad shard err = %v", err)
|
||||
}
|
||||
denied := failingReadFS{fstest.MapFS{"tools/doc.json": {Data: []byte(`{}`)}}}
|
||||
if _, err := assembleSchemaCatalogSnapshot(valid, denied, "tools"); err == nil || !strings.Contains(err.Error(), "read embedded schema catalog shard") {
|
||||
t.Fatalf("denied shard err = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssembleCommandRegistryFromShards(t *testing.T) {
|
||||
envelope := []byte(`{"$schema":"./schema_command_registry.schema.json","version":1}`)
|
||||
shards := fstest.MapFS{
|
||||
"products/doc.json": {Data: []byte(`{"id":"doc","tools":[{"canonical_path":"doc.copy_document","cli_path":"doc copy"}]}`)},
|
||||
"products/notes.md": {Data: []byte("skip me")},
|
||||
}
|
||||
registry, err := assembleCommandRegistryFrom(envelope, shards, "products")
|
||||
if err != nil {
|
||||
t.Fatalf("assembleCommandRegistryFrom() error = %v", err)
|
||||
}
|
||||
if len(registry.Commands) != 1 || registry.Commands[0].CanonicalPath != "doc.copy_document" {
|
||||
t.Fatalf("registry = %+v, want single doc.copy_document command", registry.Commands)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssembleCommandRegistryFromFailureModes(t *testing.T) {
|
||||
valid := []byte(`{"version":1}`)
|
||||
if _, err := assembleCommandRegistryFrom([]byte("{bad"), fstest.MapFS{}, "products"); err == nil || !strings.Contains(err.Error(), "envelope") {
|
||||
t.Fatalf("bad envelope err = %v", err)
|
||||
}
|
||||
if _, err := assembleCommandRegistryFrom(valid, fstest.MapFS{}, "missing-dir"); err == nil || !strings.Contains(err.Error(), "read embedded command registry products") {
|
||||
t.Fatalf("missing dir err = %v", err)
|
||||
}
|
||||
badShard := fstest.MapFS{"products/doc.json": {Data: []byte("{bad")}}
|
||||
if _, err := assembleCommandRegistryFrom(valid, badShard, "products"); err == nil || !strings.Contains(err.Error(), "decode embedded command registry shard") {
|
||||
t.Fatalf("bad shard err = %v", err)
|
||||
}
|
||||
denied := failingReadFS{fstest.MapFS{"products/doc.json": {Data: []byte(`{}`)}}}
|
||||
if _, err := assembleCommandRegistryFrom(valid, denied, "products"); err == nil || !strings.Contains(err.Error(), "read embedded command registry shard") {
|
||||
t.Fatalf("denied shard err = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergedCommandRegistryJSONFailureModes(t *testing.T) {
|
||||
valid := []byte(`{"version":1}`)
|
||||
if _, err := mergedCommandRegistryJSON([]byte("{bad"), fstest.MapFS{}, "products"); err == nil {
|
||||
t.Fatal("bad envelope should fail")
|
||||
}
|
||||
if _, err := mergedCommandRegistryJSON(valid, fstest.MapFS{}, "missing-dir"); err == nil {
|
||||
t.Fatal("missing dir should fail")
|
||||
}
|
||||
denied := failingReadFS{fstest.MapFS{"products/doc.json": {Data: []byte(`{}`)}}}
|
||||
if _, err := mergedCommandRegistryJSON(valid, denied, "products"); err == nil {
|
||||
t.Fatal("denied shard should fail")
|
||||
}
|
||||
// Invalid shard JSON survives assembly as RawMessage but fails the final
|
||||
// products marshal.
|
||||
badShard := fstest.MapFS{"products/doc.json": {Data: []byte("{bad")}}
|
||||
if _, err := mergedCommandRegistryJSON(valid, badShard, "products"); err == nil {
|
||||
t.Fatal("invalid RawMessage shard should fail marshal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergedCommandRegistryJSONDefaultsSchemaAndSkipsNonJSON(t *testing.T) {
|
||||
shards := fstest.MapFS{
|
||||
"products/doc.json": {Data: []byte(`{"id":"doc","tools":[]}`)},
|
||||
"products/notes.md": {Data: []byte("skip me")},
|
||||
}
|
||||
data, err := mergedCommandRegistryJSON([]byte(`{"version":1}`), shards, "products")
|
||||
if err != nil {
|
||||
t.Fatalf("mergedCommandRegistryJSON() error = %v", err)
|
||||
}
|
||||
var merged struct {
|
||||
Schema string `json:"$schema"`
|
||||
Version int `json:"version"`
|
||||
Products []json.RawMessage `json:"products"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &merged); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if merged.Schema != "./schema_command_registry.schema.json" {
|
||||
t.Fatalf("schema = %q, want defaulted schema path", merged.Schema)
|
||||
}
|
||||
if len(merged.Products) != 1 {
|
||||
t.Fatalf("products = %d, want 1 (markdown skipped)", len(merged.Products))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildMetaByCLIPathGuards(t *testing.T) {
|
||||
// nil Tools:损坏快照直接返回空表。
|
||||
if got := buildMetaByCLIPath(loadedSchemaCatalog{}); len(got) != 0 {
|
||||
t.Fatalf("nil tools lookup = %#v, want empty", got)
|
||||
}
|
||||
loaded := loadedSchemaCatalog{Snapshot: SchemaCatalogSnapshot{Tools: map[string]map[string]any{
|
||||
"doc.no_cli_path": {"canonical_path": "doc.no_cli_path"},
|
||||
"doc.primary": {
|
||||
"cli_path": "doc primary",
|
||||
"canonical_path": "doc.primary",
|
||||
"aliases": []any{"doc alias", "doc primary", " ", float64(1)},
|
||||
},
|
||||
"doc.other": {
|
||||
"cli_path": "doc alias",
|
||||
"canonical_path": "doc.other",
|
||||
},
|
||||
}}}
|
||||
lookup := buildMetaByCLIPath(loaded)
|
||||
if _, ok := lookup[""]; ok {
|
||||
t.Fatal("entry without cli_path must be skipped")
|
||||
}
|
||||
// 别名与其他命令的主路径冲突时,主路径必须胜出。
|
||||
if got := lookup["doc alias"].Identity.Canonical; got != "doc.other" {
|
||||
t.Fatalf("doc alias resolved to %q, want primary owner doc.other", got)
|
||||
}
|
||||
if got := lookup["doc primary"].Identity.Canonical; got != "doc.primary" {
|
||||
t.Fatalf("doc primary resolved to %q", got)
|
||||
}
|
||||
// Identity.Aliases 保留 Catalog 中的字符串成员(非字符串被丢弃);
|
||||
// 空白/自指别名仅在查找表注册时跳过。
|
||||
if aliases := lookup["doc primary"].Identity.Aliases; len(aliases) != 3 {
|
||||
t.Fatalf("aliases = %v, want the 3 string members (float dropped)", aliases)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchemaStringSliceNonSlice(t *testing.T) {
|
||||
if got := schemaStringSlice("not-a-slice"); got != nil {
|
||||
t.Fatalf("schemaStringSlice() = %v, want nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderSafetyAnnotation(t *testing.T) {
|
||||
// dev app delete: destructive/high/user_required —— 必须渲染完整注解。
|
||||
root := &cobra.Command{Use: "dws"}
|
||||
dev := &cobra.Command{Use: "dev"}
|
||||
app := &cobra.Command{Use: "app"}
|
||||
deleteCmd := &cobra.Command{Use: "delete"}
|
||||
root.AddCommand(dev)
|
||||
dev.AddCommand(app)
|
||||
app.AddCommand(deleteCmd)
|
||||
var out bytes.Buffer
|
||||
deleteCmd.SetOut(&out)
|
||||
RenderSafetyAnnotation(deleteCmd)
|
||||
rendered := out.String()
|
||||
if !strings.Contains(rendered, "Safety: effect=destructive") || !strings.Contains(rendered, "(requires --yes)") {
|
||||
t.Fatalf("rendered = %q, want destructive annotation with confirmation hint", rendered)
|
||||
}
|
||||
if !strings.Contains(rendered, "idempotency=") {
|
||||
t.Fatalf("rendered = %q, want idempotency field", rendered)
|
||||
}
|
||||
|
||||
// 未收录命令不输出任何内容。
|
||||
unknown := &cobra.Command{Use: "unknown-cmd"}
|
||||
root.AddCommand(unknown)
|
||||
var silent bytes.Buffer
|
||||
unknown.SetOut(&silent)
|
||||
RenderSafetyAnnotation(unknown)
|
||||
if silent.Len() != 0 {
|
||||
t.Fatalf("unknown command rendered %q, want empty", silent.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestAssembleEmbeddedSchemaCatalogPropagatesAssemblyError 通过临时替换嵌入的
|
||||
// envelope 字节,覆盖 assembleEmbeddedSchemaCatalog 的错误透传分支。
|
||||
func TestAssembleEmbeddedSchemaCatalogPropagatesAssemblyError(t *testing.T) {
|
||||
orig := embeddedSchemaCatalogEnvelopeJSON
|
||||
t.Cleanup(func() { embeddedSchemaCatalogEnvelopeJSON = orig })
|
||||
embeddedSchemaCatalogEnvelopeJSON = []byte("{bad")
|
||||
if _, err := assembleEmbeddedSchemaCatalog(); err == nil || !strings.Contains(err.Error(), "decode embedded schema catalog.json") {
|
||||
t.Fatalf("err = %v, want assembly error passthrough", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildMetaByCLIPathAliasCollisionDeterministic(CR C3):alias-vs-alias
|
||||
// 冲突必须跨进程稳定——归属字典序最小的主 cli_path,而非 map 遍历顺序。
|
||||
func TestBuildMetaByCLIPathAliasCollisionDeterministic(t *testing.T) {
|
||||
loaded := loadedSchemaCatalog{Snapshot: SchemaCatalogSnapshot{Tools: map[string]map[string]any{
|
||||
"doc.zeta": {
|
||||
"cli_path": "doc zeta",
|
||||
"canonical_path": "doc.zeta",
|
||||
"aliases": []any{"doc shared"},
|
||||
},
|
||||
"doc.alpha": {
|
||||
"cli_path": "doc alpha",
|
||||
"canonical_path": "doc.alpha",
|
||||
"aliases": []any{"doc shared"},
|
||||
},
|
||||
}}}
|
||||
// 多次构建,归属必须始终是字典序更小的 "doc alpha"。
|
||||
for i := 0; i < 20; i++ {
|
||||
lookup := buildMetaByCLIPath(loaded)
|
||||
if got := lookup["doc shared"].Identity.Canonical; got != "doc.alpha" {
|
||||
t.Fatalf("run %d: doc shared owned by %q, want deterministic doc.alpha", i, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ func main() {
|
||||
flag.StringVar(&outputPath, "output", "", "Output embedded Agent metadata JSON file (legacy single-file mode)")
|
||||
flag.StringVar(&outputDir, "output-dir", "", "Output directory for split embedded Agent metadata JSON")
|
||||
flag.StringVar(&auditOutputPath, "audit-output", "", "Optional output path for build-time source and CommandRegistry diagnostics")
|
||||
flag.StringVar(®istryPath, "registry", "internal/cli/schema_command_registry.json", "Reviewed CommandRegistry path, relative to --root; validation-only because the registry is embedded")
|
||||
flag.StringVar(®istryPath, "registry", "internal/cli/schema_command_registry", "Reviewed CommandRegistry path, relative to --root; validation-only because the registry is embedded")
|
||||
flag.StringVar(&legacySurfacePath, "surface", "", "Deprecated alias for --registry; the file must equal the embedded reviewed CommandRegistry")
|
||||
flag.IntVar(&maxExamples, "max-examples", 2, "Maximum examples retained per command")
|
||||
flag.IntVar(&maxInterfaceSummaryRunes, "max-interface-summary-runes", 120, "Maximum runes retained in an unreviewed MCP-derived Agent summary")
|
||||
@@ -95,7 +95,7 @@ func main() {
|
||||
{Name: "canonical product Skill input directory", Path: "skills/mono/references/products"},
|
||||
{Name: "canonical intent guide input", Path: "skills/mono/references/intent-guide.md"},
|
||||
{Name: "canonical structured hint input directory", Path: "internal/cli/schema_hints"},
|
||||
{Name: "canonical reviewed CommandRegistry input", Path: "internal/cli/schema_command_registry.json"},
|
||||
{Name: "canonical reviewed CommandRegistry input", Path: "internal/cli/schema_command_registry"},
|
||||
{Name: "canonical pinned interface metadata input", Path: "internal/cli/schema_mcp_metadata.json"},
|
||||
{Name: "main Skill input", Path: skillPath},
|
||||
{Name: "product Skill input directory", Path: productsDir},
|
||||
@@ -372,14 +372,72 @@ func validateCommandRegistryFile(rootPath, registryPath string) error {
|
||||
if strings.TrimSpace(registryPath) == "" {
|
||||
return nil
|
||||
}
|
||||
data, err := os.ReadFile(resolveRootPath(rootPath, registryPath))
|
||||
absPath := resolveRootPath(rootPath, registryPath)
|
||||
info, err := os.Stat(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var data []byte
|
||||
if info.IsDir() {
|
||||
// Per-product shards: merge envelope + products/*.json into one JSON.
|
||||
data, err = mergeRegistryShards(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
data, err = os.ReadFile(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = cli.ValidateCommandRegistrySource(data)
|
||||
return err
|
||||
}
|
||||
|
||||
// mergeRegistryShards reads registry.json (envelope) + products/*.json (shards)
|
||||
// and returns a single JSON document matching the pre-split layout.
|
||||
func mergeRegistryShards(dir string) ([]byte, error) {
|
||||
envelopeData, err := os.ReadFile(filepath.Join(dir, "registry.json"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read registry envelope: %w", err)
|
||||
}
|
||||
var envelope struct {
|
||||
Schema string `json:"$schema,omitempty"`
|
||||
Version int `json:"version"`
|
||||
}
|
||||
if err := json.Unmarshal(envelopeData, &envelope); err != nil {
|
||||
return nil, fmt.Errorf("decode registry envelope: %w", err)
|
||||
}
|
||||
entries, err := os.ReadDir(filepath.Join(dir, "products"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read registry products dir: %w", err)
|
||||
}
|
||||
var products []json.RawMessage
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
|
||||
continue
|
||||
}
|
||||
shardData, err := os.ReadFile(filepath.Join(dir, "products", entry.Name()))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read registry shard %s: %w", entry.Name(), err)
|
||||
}
|
||||
products = append(products, json.RawMessage(shardData))
|
||||
}
|
||||
result := struct {
|
||||
Schema string `json:"$schema,omitempty"`
|
||||
Version int `json:"version"`
|
||||
Products json.RawMessage `json:"products"`
|
||||
}{
|
||||
Schema: envelope.Schema,
|
||||
Version: envelope.Version,
|
||||
}
|
||||
result.Products, err = json.Marshal(products)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(result)
|
||||
}
|
||||
|
||||
func validateSelectionHintInput(rootPath, hintsDir string, registry commandRegistryProjection) error {
|
||||
hintsRoot := resolveRootPath(rootPath, hintsDir)
|
||||
selectionRoot := filepath.Join(hintsRoot, "selection")
|
||||
|
||||
@@ -42,7 +42,7 @@ func TestMainGeneratesMetadataToTemporaryDirectory(t *testing.T) {
|
||||
|
||||
func TestLoadEffectiveCommandRegistryProjectionReconcilesAliases(t *testing.T) {
|
||||
root := filepath.Join("..", "..", "..")
|
||||
registry, err := loadEffectiveCommandRegistryProjection(root, "internal/cli/schema_command_registry.json", true)
|
||||
registry, err := loadEffectiveCommandRegistryProjection(root, "internal/cli/schema_command_registry", true)
|
||||
if err != nil {
|
||||
t.Fatalf("loadEffectiveCommandRegistryProjection() error = %v", err)
|
||||
}
|
||||
@@ -61,7 +61,7 @@ func TestLoadEffectiveCommandRegistryProjectionReconcilesAliases(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadEffectiveCommandRegistryProjectionRejectsCompatibilityDrift(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "schema_command_registry.json")
|
||||
path := filepath.Join(t.TempDir(), "schema_command_registry")
|
||||
if err := os.WriteFile(path, []byte(`{"$schema":"./schema_command_registry.schema.json","version":1,"products":[{"id":"sample","tools":[{"canonical_path":"sample.run","cli_path":"sample run"}]}]}`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestMergeRegistryShardsFailureModes drives every error path of the split
|
||||
// registry merge used by validateCommandRegistryFile.
|
||||
func TestMergeRegistryShardsFailureModes(t *testing.T) {
|
||||
t.Run("missing envelope", func(t *testing.T) {
|
||||
if _, err := mergeRegistryShards(t.TempDir()); err == nil || !strings.Contains(err.Error(), "read registry envelope") {
|
||||
t.Fatalf("err = %v, want envelope read failure", err)
|
||||
}
|
||||
})
|
||||
t.Run("bad envelope json", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "registry.json"), []byte("{bad"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := mergeRegistryShards(dir); err == nil || !strings.Contains(err.Error(), "decode registry envelope") {
|
||||
t.Fatalf("err = %v, want envelope decode failure", err)
|
||||
}
|
||||
})
|
||||
t.Run("missing products dir", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "registry.json"), []byte(`{"version":1}`), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := mergeRegistryShards(dir); err == nil || !strings.Contains(err.Error(), "read registry products dir") {
|
||||
t.Fatalf("err = %v, want products dir failure", err)
|
||||
}
|
||||
})
|
||||
t.Run("unreadable shard", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "registry.json"), []byte(`{"version":1}`), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
products := filepath.Join(dir, "products")
|
||||
if err := os.MkdirAll(products, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 断链符号链接:ReadDir 能列出,ReadFile 必失败。
|
||||
if err := os.Symlink(filepath.Join(dir, "nonexistent"), filepath.Join(products, "doc.json")); err != nil {
|
||||
t.Skipf("symlink unavailable: %v", err)
|
||||
}
|
||||
if _, err := mergeRegistryShards(dir); err == nil || !strings.Contains(err.Error(), "read registry shard") {
|
||||
t.Fatalf("err = %v, want shard read failure", err)
|
||||
}
|
||||
})
|
||||
t.Run("invalid raw shard fails products marshal", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "registry.json"), []byte(`{"version":1}`), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
products := filepath.Join(dir, "products")
|
||||
if err := os.MkdirAll(products, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(products, "doc.json"), []byte("{bad"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := mergeRegistryShards(dir); err == nil {
|
||||
t.Fatal("err = nil, want RawMessage marshal failure for invalid shard JSON")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMergeRegistryShardsSkipsNonShardEntries(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "registry.json"), []byte(`{"$schema":"./s.json","version":1}`), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
products := filepath.Join(dir, "products")
|
||||
if err := os.MkdirAll(filepath.Join(products, "nested.json"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(products, "README.md"), []byte("skip"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(products, "doc.json"), []byte(`{"id":"doc","tools":[]}`), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, err := mergeRegistryShards(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("mergeRegistryShards() error = %v", err)
|
||||
}
|
||||
merged := string(data)
|
||||
if !strings.Contains(merged, `"id":"doc"`) || strings.Contains(merged, "skip") {
|
||||
t.Fatalf("merged = %s, want doc shard only", merged)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCommandRegistryFileShardDirectoryErrors(t *testing.T) {
|
||||
// 目录形态但缺 envelope:mergeRegistryShards 的错误必须透传。
|
||||
dir := t.TempDir()
|
||||
if err := validateCommandRegistryFile("", dir); err == nil || !strings.Contains(err.Error(), "read registry envelope") {
|
||||
t.Fatalf("err = %v, want merge failure passthrough", err)
|
||||
}
|
||||
// 单文件形态但不可读(无读权限)。
|
||||
if os.Getuid() == 0 {
|
||||
t.Skip("running as root; permission-based read failure not enforceable")
|
||||
}
|
||||
unreadable := filepath.Join(t.TempDir(), "registry.json")
|
||||
if err := os.WriteFile(unreadable, []byte(`{"version":1}`), 0o000); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := validateCommandRegistryFile("", unreadable); err == nil {
|
||||
t.Fatal("err = nil, want read failure for unreadable registry file")
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ func main() {
|
||||
var outputPath string
|
||||
flag.StringVar(&rootPath, "root", ".", "Repository root used to protect Schema generator inputs")
|
||||
flag.StringVar(&surfacePath, "surface", "", "Deprecated compatibility input relative to --root; when set it must equal the embedded reviewed CommandRegistry")
|
||||
flag.StringVar(&outputPath, "output", "internal/cli/schema_catalog.json", "Output embedded schema catalog")
|
||||
flag.StringVar(&outputPath, "output", "internal/cli/schema_catalog", "Output directory for the split embedded schema catalog (catalog.json + tools/<product>.json)")
|
||||
flag.Parse()
|
||||
resolvedSurfacePath := resolveCatalogRootPath(rootPath, surfacePath)
|
||||
if err := validateCatalogOutputIsolation(rootPath, outputPath, resolvedSurfacePath); err != nil {
|
||||
@@ -68,7 +68,7 @@ func validateCatalogOutputIsolation(rootPath, outputPath, surfacePath string) er
|
||||
{Name: "product Skill metadata source directory", Path: "skills/mono/references/products"},
|
||||
{Name: "intent guide metadata source", Path: "skills/mono/references/intent-guide.md"},
|
||||
{Name: "structured metadata source directory", Path: "internal/cli/schema_hints"},
|
||||
{Name: "reviewed CommandRegistry input", Path: "internal/cli/schema_command_registry.json"},
|
||||
{Name: "reviewed CommandRegistry input", Path: "internal/cli/schema_command_registry"},
|
||||
{Name: "generated Agent metadata input", Path: "internal/cli/schema_agent_metadata"},
|
||||
{Name: "pinned MCP metadata input", Path: "internal/cli/schema_mcp_metadata.json"},
|
||||
{Name: "reviewed MCP service disposition input", Path: "internal/cli/schema_mcp_service_review.json"},
|
||||
@@ -78,12 +78,12 @@ func validateCatalogOutputIsolation(rootPath, outputPath, surfacePath string) er
|
||||
if strings.TrimSpace(surfacePath) != "" {
|
||||
inputs = append(inputs, outputguard.Input{Name: "deprecated Registry compatibility input", Path: surfacePath})
|
||||
}
|
||||
if err := outputguard.Validate(rootPath, inputs, []outputguard.Target{{Name: "--output", Path: outputPath}}); err != nil {
|
||||
if err := outputguard.Validate(rootPath, inputs, []outputguard.Target{{Name: "--output", Path: outputPath, Directory: true}}); err != nil {
|
||||
return err
|
||||
}
|
||||
return outputguard.ValidateRepoTargetAllowlist(rootPath,
|
||||
outputguard.Target{Name: "--output", Path: outputPath},
|
||||
"internal/cli/schema_catalog.json",
|
||||
outputguard.Target{Name: "--output", Path: outputPath, Directory: true},
|
||||
"internal/cli/schema_catalog",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -125,18 +125,99 @@ func generateSchemaCatalogWithResolver(root *cobra.Command, surfacePath, outputP
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
encoded, _ := json.MarshalIndent(snapshot, "", " ")
|
||||
if err := makeCatalogDirectory(filepath.Dir(outputPath), 0o755); err != nil {
|
||||
return fmt.Errorf("create output directory: %w", err)
|
||||
if err := writeSchemaCatalogShards(snapshot, outputPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeCatalogFile(outputPath, append(encoded, '\n'), 0o644); err != nil {
|
||||
return fmt.Errorf("write catalog: %w", err)
|
||||
}
|
||||
_, _ = fmt.Fprintf(os.Stderr, "generated schema catalog: output=%s registry_commands=%d tools=%d registry_hash=%s source_hash=%s\n",
|
||||
outputPath, resolved.CommandCount(), len(snapshot.Tools), snapshot.SurfaceHash, snapshot.SourceHash)
|
||||
_, _ = fmt.Fprintf(os.Stderr, "generated schema catalog: output=%s registry_commands=%d tools=%d products=%d registry_hash=%s source_hash=%s\n",
|
||||
outputPath, resolved.CommandCount(), len(snapshot.Tools), countSchemaCatalogProducts(snapshot), snapshot.SurfaceHash, snapshot.SourceHash)
|
||||
return nil
|
||||
}
|
||||
|
||||
// schemaCatalogEnvelope is the global half of the split catalog. It carries
|
||||
// the release envelope (version + integrity hashes) and the Catalog map, whose
|
||||
// products array and cross-product aggregates do not partition by product.
|
||||
type schemaCatalogEnvelope struct {
|
||||
Version int `json:"version"`
|
||||
SurfaceHash string `json:"surface_hash,omitempty"`
|
||||
SourceHash string `json:"source_hash"`
|
||||
Catalog map[string]any `json:"catalog"`
|
||||
}
|
||||
|
||||
// schemaCatalogToolShard is the per-product half of the split catalog. Each
|
||||
// product's leaf ToolSpecs live in their own file so concurrent feature PRs
|
||||
// only rewrite the shard for the product they touch.
|
||||
type schemaCatalogToolShard struct {
|
||||
Product string `json:"product"`
|
||||
Tools map[string]map[string]any `json:"tools"`
|
||||
}
|
||||
|
||||
// writeSchemaCatalogShards partitions a validated snapshot into a release
|
||||
// directory: catalog.json holds the global envelope + Catalog map, and
|
||||
// tools/<product>.json holds each product's leaf ToolSpecs keyed by canonical
|
||||
// path. The split is a storage concern only: the loader reassembles the exact
|
||||
// same SchemaCatalogSnapshot, so source_hash still validates the whole payload.
|
||||
func writeSchemaCatalogShards(snapshot cli.SchemaCatalogSnapshot, outputDir string) error {
|
||||
if err := os.RemoveAll(outputDir); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("clear stale schema catalog output: %w", err)
|
||||
}
|
||||
toolsDir := filepath.Join(outputDir, "tools")
|
||||
if err := makeCatalogDirectory(toolsDir, 0o755); err != nil {
|
||||
return fmt.Errorf("create schema catalog tools directory: %w", err)
|
||||
}
|
||||
|
||||
envelope := schemaCatalogEnvelope{
|
||||
Version: snapshot.Version,
|
||||
SurfaceHash: snapshot.SurfaceHash,
|
||||
SourceHash: snapshot.SourceHash,
|
||||
Catalog: snapshot.Catalog,
|
||||
}
|
||||
if err := writeSchemaCatalogJSON(filepath.Join(outputDir, "catalog.json"), envelope); err != nil {
|
||||
return fmt.Errorf("write schema catalog.json: %w", err)
|
||||
}
|
||||
|
||||
for product, tools := range partitionSchemaCatalogTools(snapshot.Tools) {
|
||||
shard := schemaCatalogToolShard{Product: product, Tools: tools}
|
||||
if err := writeSchemaCatalogJSON(filepath.Join(toolsDir, product+".json"), shard); err != nil {
|
||||
return fmt.Errorf("write schema catalog tools/%s.json: %w", product, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// partitionSchemaCatalogTools groups leaf ToolSpecs by the product prefix of
|
||||
// their canonical path (the segment before the first dot). Products are
|
||||
// returned as a sorted map so generation is deterministic.
|
||||
func partitionSchemaCatalogTools(tools map[string]map[string]any) map[string]map[string]map[string]any {
|
||||
partitioned := map[string]map[string]map[string]any{}
|
||||
for canonical, spec := range tools {
|
||||
product := canonical
|
||||
if idx := strings.IndexByte(canonical, '.'); idx > 0 {
|
||||
product = canonical[:idx]
|
||||
}
|
||||
if partitioned[product] == nil {
|
||||
partitioned[product] = map[string]map[string]any{}
|
||||
}
|
||||
partitioned[product][canonical] = spec
|
||||
}
|
||||
return partitioned
|
||||
}
|
||||
|
||||
func countSchemaCatalogProducts(snapshot cli.SchemaCatalogSnapshot) int {
|
||||
maxProduct := 0
|
||||
for range partitionSchemaCatalogTools(snapshot.Tools) {
|
||||
maxProduct++
|
||||
}
|
||||
return maxProduct
|
||||
}
|
||||
|
||||
func writeSchemaCatalogJSON(path string, value any) error {
|
||||
encoded, err := json.MarshalIndent(value, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode %s: %w", filepath.Base(path), err)
|
||||
}
|
||||
return writeCatalogFile(path, append(encoded, '\n'), 0o644)
|
||||
}
|
||||
|
||||
func validateDeprecatedSurface(path string) error {
|
||||
if path == "" {
|
||||
return nil
|
||||
|
||||
@@ -27,7 +27,7 @@ func TestCrossPlatformCoverageMainGeneratesCatalogToTemporaryFile(t *testing.T)
|
||||
os.Args, flag.CommandLine = oldArgs, oldFlags
|
||||
})
|
||||
flag.CommandLine = flag.NewFlagSet("schema-catalog-coverage", flag.ContinueOnError)
|
||||
os.Args = []string{"cmd_schema_catalog", "-root", repositoryRoot, "-output", filepath.Join(t.TempDir(), "schema_catalog.json")}
|
||||
os.Args = []string{"cmd_schema_catalog", "-root", repositoryRoot, "-output", filepath.Join(t.TempDir(), "schema_catalog")}
|
||||
main()
|
||||
}
|
||||
|
||||
@@ -105,12 +105,12 @@ func TestCrossPlatformCoverageGenerateSchemaCatalogFailureEdges(t *testing.T) {
|
||||
return cli.SchemaCatalogSnapshot{}, nil
|
||||
}
|
||||
makeCatalogDirectory = func(string, os.FileMode) error { return errors.New("mkdir") }
|
||||
if err := generateSchemaCatalogWithResolver(root, "", output, resolver); err == nil || !strings.Contains(err.Error(), "create output") {
|
||||
if err := generateSchemaCatalogWithResolver(root, "", output, resolver); err == nil || !strings.Contains(err.Error(), "create schema catalog") {
|
||||
t.Fatalf("mkdir error = %v", err)
|
||||
}
|
||||
makeCatalogDirectory = func(string, os.FileMode) error { return nil }
|
||||
writeCatalogFile = func(string, []byte, os.FileMode) error { return errors.New("write") }
|
||||
if err := generateSchemaCatalogWithResolver(root, "", output, resolver); err == nil || !strings.Contains(err.Error(), "write catalog") {
|
||||
if err := generateSchemaCatalogWithResolver(root, "", output, resolver); err == nil || !strings.Contains(err.Error(), "write schema catalog") {
|
||||
t.Fatalf("write error = %v", err)
|
||||
}
|
||||
|
||||
@@ -145,41 +145,83 @@ func TestCrossPlatformCoverageGenerateSchemaCatalogResolvesBuildExactlyOnce(t *t
|
||||
}
|
||||
return resolved, err
|
||||
}
|
||||
outputPath := filepath.Join(t.TempDir(), "schema_catalog.json")
|
||||
outputPath := filepath.Join(t.TempDir(), "schema_catalog")
|
||||
if err := generateSchemaCatalogWithResolver(root, "", outputPath, resolver); err != nil {
|
||||
t.Fatalf("generateSchemaCatalogWithResolver() error = %v", err)
|
||||
}
|
||||
if resolveCalls != 1 {
|
||||
t.Fatalf("Schema build resolver calls = %d, want exactly 1", resolveCalls)
|
||||
}
|
||||
data, err := os.ReadFile(outputPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read generated temporary Catalog: %v", err)
|
||||
}
|
||||
var snapshot cli.SchemaCatalogSnapshot
|
||||
if err := json.Unmarshal(data, &snapshot); err != nil {
|
||||
t.Fatalf("decode generated temporary Catalog: %v", err)
|
||||
}
|
||||
snapshot := loadSplitSchemaCatalogSnapshot(t, outputPath)
|
||||
if snapshot.SurfaceHash != resolvedRegistryHash {
|
||||
t.Fatalf("snapshot Registry hash = %q, want once-resolved hash %q", snapshot.SurfaceHash, resolvedRegistryHash)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageValidateDeprecatedSurfaceAcceptsEmbeddedRegistrySource(t *testing.T) {
|
||||
path := filepath.Join("..", "..", "cli", "schema_command_registry.json")
|
||||
// loadSplitSchemaCatalogSnapshot reassembles the per-product split layout
|
||||
// (catalog.json + tools/<product>.json) written by the generator back into a
|
||||
// single SchemaCatalogSnapshot, mirroring the production loader.
|
||||
func loadSplitSchemaCatalogSnapshot(t *testing.T, dir string) cli.SchemaCatalogSnapshot {
|
||||
t.Helper()
|
||||
var envelope struct {
|
||||
Version int `json:"version"`
|
||||
SurfaceHash string `json:"surface_hash,omitempty"`
|
||||
SourceHash string `json:"source_hash"`
|
||||
Catalog map[string]any `json:"catalog"`
|
||||
}
|
||||
envData, err := os.ReadFile(filepath.Join(dir, "catalog.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("read generated catalog.json: %v", err)
|
||||
}
|
||||
if err := json.Unmarshal(envData, &envelope); err != nil {
|
||||
t.Fatalf("decode generated catalog.json: %v", err)
|
||||
}
|
||||
snapshot := cli.SchemaCatalogSnapshot{
|
||||
Version: envelope.Version,
|
||||
SurfaceHash: envelope.SurfaceHash,
|
||||
SourceHash: envelope.SourceHash,
|
||||
Catalog: envelope.Catalog,
|
||||
Tools: map[string]map[string]any{},
|
||||
}
|
||||
shards, err := filepath.Glob(filepath.Join(dir, "tools", "*.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("glob tool shards: %v", err)
|
||||
}
|
||||
for _, shardPath := range shards {
|
||||
data, err := os.ReadFile(shardPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read tool shard %s: %v", shardPath, err)
|
||||
}
|
||||
var shard struct {
|
||||
Tools map[string]map[string]any `json:"tools"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &shard); err != nil {
|
||||
t.Fatalf("decode tool shard %s: %v", shardPath, err)
|
||||
}
|
||||
for canonical, spec := range shard.Tools {
|
||||
snapshot.Tools[canonical] = spec
|
||||
}
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
||||
func TestValidateDeprecatedSurfaceAcceptsEmbeddedRegistrySource(t *testing.T) {
|
||||
// Registry is now per-product shards; merge into a temp file for the
|
||||
// deprecated -surface compatibility check (which reads a single JSON).
|
||||
merged := mergeRegistryShardsForTest(t, filepath.Join("..", "..", "cli", "schema_command_registry"))
|
||||
path := filepath.Join(t.TempDir(), "registry.json")
|
||||
if err := os.WriteFile(path, merged, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := validateDeprecatedSurface(path); err != nil {
|
||||
t.Fatalf("validateDeprecatedSurface() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageValidateDeprecatedSurfaceRejectsDifferentIdentitySource(t *testing.T) {
|
||||
sourcePath := filepath.Join("..", "..", "cli", "schema_command_registry.json")
|
||||
data, err := os.ReadFile(sourcePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
altered := strings.Replace(string(data), `"canonical_path": "aisearch.search_enterprise_behavior"`, `"canonical_path": "aisearch.not_reviewed"`, 1)
|
||||
if altered == string(data) {
|
||||
func TestValidateDeprecatedSurfaceRejectsDifferentIdentitySource(t *testing.T) {
|
||||
merged := mergeRegistryShardsForTest(t, filepath.Join("..", "..", "cli", "schema_command_registry"))
|
||||
altered := strings.Replace(string(merged), `"aisearch.search_enterprise_behavior"`, `"aisearch.not_reviewed"`, 1)
|
||||
if altered == string(merged) {
|
||||
t.Fatal("test fixture did not contain expected canonical path")
|
||||
}
|
||||
path := filepath.Join(t.TempDir(), "different-registry.json")
|
||||
@@ -187,24 +229,24 @@ func TestCrossPlatformCoverageValidateDeprecatedSurfaceRejectsDifferentIdentityS
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = validateDeprecatedSurface(path)
|
||||
err := validateDeprecatedSurface(path)
|
||||
if err == nil || !strings.Contains(err.Error(), "disagrees with the embedded reviewed registry") {
|
||||
t.Fatalf("validateDeprecatedSurface() error = %v, want registry disagreement", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageValidateDeprecatedSurfaceAllowsOmittedCompatibilityFlag(t *testing.T) {
|
||||
func TestValidateDeprecatedSurfaceAllowsOmittedCompatibilityFlag(t *testing.T) {
|
||||
if err := validateDeprecatedSurface(""); err != nil {
|
||||
t.Fatalf("validateDeprecatedSurface() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageValidateCatalogOutputIsolationProtectsEveryInputLayer(t *testing.T) {
|
||||
func TestValidateCatalogOutputIsolationProtectsEveryInputLayer(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
files := []string{
|
||||
"skills/mono/SKILL.md",
|
||||
"skills/mono/references/intent-guide.md",
|
||||
"internal/cli/schema_command_registry.json",
|
||||
"internal/cli/schema_command_registry",
|
||||
"internal/cli/schema_mcp_metadata.json",
|
||||
"internal/cli/schema_mcp_service_review.json",
|
||||
"internal/cli/schema_parameter_bindings.json",
|
||||
@@ -237,7 +279,7 @@ func TestCrossPlatformCoverageValidateCatalogOutputIsolationProtectsEveryInputLa
|
||||
output string
|
||||
want string
|
||||
}{
|
||||
{name: "registry", output: filepath.Join(root, "internal/cli/schema_command_registry.json"), want: "CommandRegistry"},
|
||||
{name: "registry", output: filepath.Join(root, "internal/cli/schema_command_registry"), want: "CommandRegistry"},
|
||||
{name: "hints", output: filepath.Join(root, "internal/cli/schema_hints"), want: "structured metadata source directory"},
|
||||
{name: "metadata member", output: filepath.Join(metadataDir, "replacement.json"), want: "Agent metadata"},
|
||||
{name: "metadata directory", output: metadataDir, want: "Agent metadata"},
|
||||
@@ -249,13 +291,63 @@ func TestCrossPlatformCoverageValidateCatalogOutputIsolationProtectsEveryInputLa
|
||||
}
|
||||
})
|
||||
}
|
||||
if err := validateCatalogOutputIsolation(root, filepath.Join(root, "internal/cli/schema_catalog.json"), ""); err != nil {
|
||||
if err := validateCatalogOutputIsolation(root, filepath.Join(root, "internal/cli/schema_catalog"), ""); err != nil {
|
||||
t.Fatalf("safe output rejected: %v", err)
|
||||
}
|
||||
if err := validateCatalogOutputIsolation(root, filepath.Join(t.TempDir(), "schema_catalog.json"), ""); err != nil {
|
||||
if err := validateCatalogOutputIsolation(root, filepath.Join(t.TempDir(), "schema_catalog"), ""); err != nil {
|
||||
t.Fatalf("external temporary output rejected: %v", err)
|
||||
}
|
||||
if err := validateCatalogOutputIsolation(root, filepath.Join(root, "skills/mono/overwrite.json"), ""); err == nil || !strings.Contains(err.Error(), "not a canonical generated delivery target") {
|
||||
t.Fatalf("non-canonical repository output error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// mergeRegistryShardsForTest reads per-product registry shards from dir and
|
||||
// returns a single merged JSON document matching the pre-split layout.
|
||||
func mergeRegistryShardsForTest(t *testing.T, dir string) []byte {
|
||||
t.Helper()
|
||||
envelopeData, err := os.ReadFile(filepath.Join(dir, "registry.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("read registry envelope: %v", err)
|
||||
}
|
||||
var envelope struct {
|
||||
Schema string `json:"$schema,omitempty"`
|
||||
Version int `json:"version"`
|
||||
}
|
||||
if err := json.Unmarshal(envelopeData, &envelope); err != nil {
|
||||
t.Fatalf("decode envelope: %v", err)
|
||||
}
|
||||
entries, err := os.ReadDir(filepath.Join(dir, "products"))
|
||||
if err != nil {
|
||||
t.Fatalf("read products dir: %v", err)
|
||||
}
|
||||
products := make([]json.RawMessage, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
|
||||
continue
|
||||
}
|
||||
shardData, err := os.ReadFile(filepath.Join(dir, "products", entry.Name()))
|
||||
if err != nil {
|
||||
t.Fatalf("read shard %s: %v", entry.Name(), err)
|
||||
}
|
||||
products = append(products, json.RawMessage(shardData))
|
||||
}
|
||||
productsJSON, err := json.Marshal(products)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal products: %v", err)
|
||||
}
|
||||
result := struct {
|
||||
Schema string `json:"$schema,omitempty"`
|
||||
Version int `json:"version"`
|
||||
Products json.RawMessage `json:"products"`
|
||||
}{
|
||||
Schema: envelope.Schema,
|
||||
Version: envelope.Version,
|
||||
}
|
||||
result.Products = productsJSON
|
||||
merged, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal merged: %v", err)
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
|
||||
)
|
||||
|
||||
func TestWriteSchemaCatalogShardsClearFailure(t *testing.T) {
|
||||
// 输出路径的父级是普通文件:RemoveAll 报非 IsNotExist 错误。
|
||||
parent := filepath.Join(t.TempDir(), "occupied")
|
||||
if err := os.WriteFile(parent, []byte("file"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err := writeSchemaCatalogShards(cli.SchemaCatalogSnapshot{}, filepath.Join(parent, "schema_catalog"))
|
||||
if err == nil {
|
||||
t.Fatal("err = nil, want clear/create failure under a plain file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteSchemaCatalogShardsShardWriteFailure(t *testing.T) {
|
||||
// canonical 前缀带路径分隔符时目标目录不存在,shard 写入必须失败。
|
||||
snapshot := cli.SchemaCatalogSnapshot{
|
||||
Version: 1,
|
||||
Tools: map[string]map[string]any{
|
||||
"bad/prod.tool": {"title": "x"},
|
||||
},
|
||||
}
|
||||
err := writeSchemaCatalogShards(snapshot, filepath.Join(t.TempDir(), "schema_catalog"))
|
||||
if err == nil || !strings.Contains(err.Error(), "write schema catalog tools/") {
|
||||
t.Fatalf("err = %v, want shard write failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteSchemaCatalogJSONEncodeFailure(t *testing.T) {
|
||||
err := writeSchemaCatalogJSON(filepath.Join(t.TempDir(), "catalog.json"), map[string]any{"bad": math.NaN()})
|
||||
if err == nil || !strings.Contains(err.Error(), "encode catalog.json") {
|
||||
t.Fatalf("err = %v, want encode failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteSchemaCatalogShardsEnvelopeWriteFailure(t *testing.T) {
|
||||
// envelope 自身编码失败(Catalog 含 NaN)走 write catalog.json 错误分支。
|
||||
snapshot := cli.SchemaCatalogSnapshot{
|
||||
Version: 1,
|
||||
Catalog: map[string]any{"bad": math.NaN()},
|
||||
}
|
||||
err := writeSchemaCatalogShards(snapshot, filepath.Join(t.TempDir(), "schema_catalog"))
|
||||
if err == nil || !strings.Contains(err.Error(), "write schema catalog.json") {
|
||||
t.Fatalf("err = %v, want envelope write failure", err)
|
||||
}
|
||||
}
|
||||
@@ -173,19 +173,19 @@ func TestCrossPlatformCoverageValidateRejectsHardLinkToProtectedDirectoryMember(
|
||||
|
||||
func TestCrossPlatformCoverageValidateRepoTargetAllowlist(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
allowed := filepath.Join(root, "internal/cli/schema_catalog.json")
|
||||
allowed := filepath.Join(root, "internal/cli/schema_catalog")
|
||||
if err := os.MkdirAll(filepath.Dir(allowed), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ValidateRepoTargetAllowlist(root, Target{Name: "--output", Path: allowed}, "internal/cli/schema_catalog.json"); err != nil {
|
||||
if err := ValidateRepoTargetAllowlist(root, Target{Name: "--output", Path: allowed}, "internal/cli/schema_catalog"); err != nil {
|
||||
t.Fatalf("canonical output rejected: %v", err)
|
||||
}
|
||||
err := ValidateRepoTargetAllowlist(root, Target{Name: "--output", Path: filepath.Join(root, "skills/mono/SKILL.md")}, "internal/cli/schema_catalog.json")
|
||||
err := ValidateRepoTargetAllowlist(root, Target{Name: "--output", Path: filepath.Join(root, "skills/mono/SKILL.md")}, "internal/cli/schema_catalog")
|
||||
if err == nil || !strings.Contains(err.Error(), "not a canonical generated delivery target") {
|
||||
t.Fatalf("repository source output error = %v", err)
|
||||
}
|
||||
outside := filepath.Join(t.TempDir(), "catalog.json")
|
||||
if err := ValidateRepoTargetAllowlist(root, Target{Name: "--output", Path: outside}, "internal/cli/schema_catalog.json"); err != nil {
|
||||
if err := ValidateRepoTargetAllowlist(root, Target{Name: "--output", Path: outside}, "internal/cli/schema_catalog"); err != nil {
|
||||
t.Fatalf("external temporary output rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -73,8 +74,16 @@ func installUnknownVerbFallback(group *cobra.Command) {
|
||||
printUnknownSubcmdError(cmd, bad)
|
||||
return
|
||||
}
|
||||
origHelp(cmd, args)
|
||||
return
|
||||
}
|
||||
// For non-group commands, render base help then apply the safety
|
||||
// annotation. Recursion safety hinges on NOT calling
|
||||
// cmd.Root().HelpFunc(): in test trees calendar IS the root, so that
|
||||
// would re-enter this wrapper. origHelp was captured before any
|
||||
// wrapping and is the plain cobra renderer.
|
||||
origHelp(cmd, args)
|
||||
cli.RenderSafetyAnnotation(cmd)
|
||||
})
|
||||
|
||||
prev := group.RunE
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
|
||||
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
|
||||
@@ -49,6 +50,20 @@ func resolveMessageForward(cmd *cobra.Command, defaultForward bool) (bool, error
|
||||
}
|
||||
}
|
||||
|
||||
const maxConversationCategoryTitleRunes = 15
|
||||
|
||||
func validatedConversationCategoryTitle(raw string) (string, error) {
|
||||
title := strings.TrimSpace(raw)
|
||||
if title == "" {
|
||||
return "", apperrors.NewValidation("--title 不能为空")
|
||||
}
|
||||
if utf8.RuneCountInString(title) > maxConversationCategoryTitleRunes {
|
||||
return "", apperrors.NewValidation(fmt.Sprintf(
|
||||
"--title 最多 %d 个字符", maxConversationCategoryTitleRunes))
|
||||
}
|
||||
return title, nil
|
||||
}
|
||||
|
||||
func chatIntFlagOrFallback(cmd *cobra.Command, primary string, aliases ...string) int {
|
||||
for _, alias := range aliases {
|
||||
if f := cmd.Flags().Lookup(alias); f != nil && f.Changed {
|
||||
@@ -2164,6 +2179,16 @@ func newChatCommand() *cobra.Command {
|
||||
}
|
||||
}
|
||||
|
||||
if v, _ := cmd.Flags().GetString("message-type"); v != "" {
|
||||
toolArgs["messageType"] = v
|
||||
}
|
||||
if cmd.Flags().Changed("only-robot") {
|
||||
toolArgs["onlyRobotMessages"], _ = cmd.Flags().GetBool("only-robot")
|
||||
}
|
||||
if v, _ := cmd.Flags().GetString("conversation-type"); v != "" {
|
||||
toolArgs["searchConvType"] = v
|
||||
}
|
||||
|
||||
// start -> startTime (ISO-8601 to milliseconds)
|
||||
if v, _ := cmd.Flags().GetString("start"); v != "" {
|
||||
ms, err := parseISOTimeToMillis("start", v)
|
||||
@@ -2659,6 +2684,9 @@ func newChatCommand() *cobra.Command {
|
||||
_ = chatMessageSearchAdvancedCmd.Flags().MarkHidden("groups")
|
||||
chatMessageSearchAdvancedCmd.Flags().String("group", "", "")
|
||||
_ = chatMessageSearchAdvancedCmd.Flags().MarkHidden("group")
|
||||
chatMessageSearchAdvancedCmd.Flags().String("message-type", "", "下层消息类型过滤值(可选,以当前 IM Schema 支持值为准)")
|
||||
chatMessageSearchAdvancedCmd.Flags().Bool("only-robot", false, "只搜索机器人消息(可选;显式传 false 时也会传给下层)")
|
||||
chatMessageSearchAdvancedCmd.Flags().String("conversation-type", "", "下层会话类型过滤值(可选,以当前 IM Schema 支持值为准)")
|
||||
chatMessageSearchAdvancedCmd.Flags().String("start", "", "开始时间,ISO-8601 格式(可选)")
|
||||
chatMessageSearchAdvancedCmd.Flags().String("end", "", "结束时间,ISO-8601 格式(可选)")
|
||||
chatMessageSearchAdvancedCmd.Flags().String("cursor", "0", "分页游标(默认 \"0\")")
|
||||
@@ -2867,8 +2895,12 @@ func newChatCommand() *cobra.Command {
|
||||
if err := validateRequiredFlags(cmd, "title"); err != nil {
|
||||
return err
|
||||
}
|
||||
title, err := validatedConversationCategoryTitle(mustGetFlag(cmd, "title"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return callMCPToolOnServer("im", "create_conv_category", map[string]any{
|
||||
"title": mustGetFlag(cmd, "title"),
|
||||
"title": title,
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -2902,9 +2934,13 @@ func newChatCommand() *cobra.Command {
|
||||
if err := validateRequiredFlags(cmd, "title"); err != nil {
|
||||
return err
|
||||
}
|
||||
title, err := validatedConversationCategoryTitle(mustGetFlag(cmd, "title"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return callMCPToolOnServer("im", "rename_conv_category", map[string]any{
|
||||
"categoryId": categoryId,
|
||||
"title": mustGetFlag(cmd, "title"),
|
||||
"title": title,
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -3851,7 +3887,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
|
||||
_ = chatCategoryConvsCmd.MarkFlagRequired("category-id")
|
||||
|
||||
// category create flags
|
||||
chatCategoryCreateCmd.Flags().String("title", "", "分组名称 (必填)")
|
||||
chatCategoryCreateCmd.Flags().String("title", "", "分组名称,最多 15 个字符 (必填)")
|
||||
_ = chatCategoryCreateCmd.MarkFlagRequired("title")
|
||||
|
||||
// category delete flags
|
||||
@@ -3859,7 +3895,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
|
||||
|
||||
// category rename flags
|
||||
chatCategoryRenameCmd.Flags().Int64("category-id", 0, "会话分组 ID (必填)")
|
||||
chatCategoryRenameCmd.Flags().String("title", "", "新的分组名称 (必填)")
|
||||
chatCategoryRenameCmd.Flags().String("title", "", "新的分组名称,最多 15 个字符 (必填)")
|
||||
_ = chatCategoryRenameCmd.MarkFlagRequired("title")
|
||||
|
||||
// category add-conv flags
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
// 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"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
||||
)
|
||||
|
||||
func TestValidatedConversationCategoryTitle(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
raw string
|
||||
want string
|
||||
wantErr string
|
||||
}{
|
||||
{name: "trim", raw: " 工作群 ", want: "工作群"},
|
||||
{name: "fifteen characters", raw: "123456789012345", want: "123456789012345"},
|
||||
{name: "blank", raw: " ", wantErr: "不能为空"},
|
||||
{name: "too long", raw: "1234567890123456", wantErr: "最多 15 个字符"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := validatedConversationCategoryTitle(tc.raw)
|
||||
if tc.wantErr != "" {
|
||||
if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
|
||||
t.Fatalf("error = %v, want containing %q", err, tc.wantErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Fatalf("title = %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageCategoryCommandsRejectInvalidTitles(t *testing.T) {
|
||||
caller := &categoryTitleCaller{}
|
||||
old := deps
|
||||
t.Cleanup(func() { deps = old })
|
||||
InitDeps(caller)
|
||||
|
||||
for _, argv := range [][]string{
|
||||
{"category", "create", "--title", "1234567890123456"},
|
||||
{"category", "rename", "--category-id", "42", "--title", " "},
|
||||
} {
|
||||
cmd := newChatCommand()
|
||||
cmd.SetArgs(argv)
|
||||
if err := cmd.Execute(); err == nil {
|
||||
t.Fatalf("newChatCommand(%v) unexpectedly accepted an invalid title", argv)
|
||||
}
|
||||
}
|
||||
if caller.calls != 0 {
|
||||
t.Fatalf("invalid category titles reached MCP %d times", caller.calls)
|
||||
}
|
||||
}
|
||||
|
||||
type categoryTitleCaller struct{ calls int }
|
||||
|
||||
func (c *categoryTitleCaller) CallTool(context.Context, string, string, map[string]any) (*edition.ToolResult, error) {
|
||||
c.calls++
|
||||
return &edition.ToolResult{}, nil
|
||||
}
|
||||
func (*categoryTitleCaller) Format() string { return "json" }
|
||||
func (*categoryTitleCaller) DryRun() bool { return false }
|
||||
func (*categoryTitleCaller) Fields() string { return "" }
|
||||
func (*categoryTitleCaller) JQ() string { return "" }
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user