Files
dingtalk-workspace-cli/internal/helpers/connect_cmd_class.go
玉澜andCursor 941bf01e30 Remove unused dws recovery CLI and continue multi-skill content framework.
Drop the recovery package/commands and related Schema/skill teaching so agents
stop being steered at a dead surface, while keeping mono↔multi content QA work.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 20:23:37 +08:00

290 lines
8.6 KiB
Go

// 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 "strings"
// CmdClass describes whether a dws command is read-only or mutating. It is the
// signal a connector confirmation gate consumes to decide whether a robot may
// run a command directly (read-only) or must first ask the principal to
// approve it (write / mutating).
//
// SAFETY CONTRACT: callers MUST treat CmdClassUnknown conservatively, i.e. as
// if it were CmdClassWrite (require confirmation). The classifier deliberately
// returns Unknown rather than silently coercing it to Write so that the
// confirmation gate keeps full information and can, for example, log/telemeter
// "unclassified" commands separately. Never auto-allow an Unknown command.
type CmdClass int
const (
// CmdClassUnknown means the leaf verb was not recognised by the
// heuristics or any override. Callers must default to requiring
// confirmation (treat as write) for safety.
CmdClassUnknown CmdClass = iota
// CmdClassRead is a read-only / non-mutating command that may be run
// without principal confirmation.
CmdClassRead
// CmdClassWrite is a mutating / state-changing command that must be
// confirmed by the principal before it runs.
CmdClassWrite
)
// String renders the class as a stable lowercase token, handy for logs and
// telemetry.
func (c CmdClass) String() string {
switch c {
case CmdClassRead:
return "read"
case CmdClassWrite:
return "write"
default:
return "unknown"
}
}
// readVerbs and writeVerbs are calibrated against the verbs that real dws
// cobra commands expose (scanned from `Use:` definitions across the repo, e.g.
// todo task create/list/get/update/delete/done, attendance get, approval
// submit, chat send / recall, drive upload / download / mkdir / chmod, doc
// export, contact search, etc). They are NOT invented: every entry below has
// at least one real command using it (or a "<verb>-by-..." / "batch-<verb>"
// compound that normalises to it).
var (
readVerbs = map[string]struct{}{
"list": {},
"get": {},
"search": {},
"read": {},
"view": {},
"query": {},
"show": {},
"detail": {},
"details": {},
"status": {},
"download": {},
"export": {},
"fetch": {},
"info": {},
"find": {},
"stats": {},
"stat": {},
"summary": {},
"check": {},
"inspect": {},
"doctor": {},
"version": {},
"whoami": {},
"ls": {},
"cat": {},
"describe": {},
"diff": {},
"preview": {},
}
writeVerbs = map[string]struct{}{
"create": {},
"update": {},
"delete": {},
"submit": {},
"send": {},
"done": {},
"cancel": {},
"offline": {},
"online": {},
"enable": {},
"disable": {},
"remove": {},
"add": {},
"set": {},
"unset": {},
"approve": {},
"reject": {},
"write": {},
"upload": {},
"move": {},
"mv": {},
"copy": {},
"cp": {},
"rename": {},
"reply": {},
"recall": {},
"publish": {},
"insert": {},
"import": {},
"install": {},
"uninstall": {},
"mkdir": {},
"share": {},
"unshare": {},
"commit": {},
"reset": {},
"stop": {},
"start": {},
"restart": {},
"hide": {},
"unhide": {},
"finalize": {},
"execute": {},
"run": {},
"exec": {},
"chmod": {},
"rm": {},
"clean": {},
"clear": {},
"refresh": {},
"recover": {},
"login": {},
"logout": {},
"register": {},
"connect": {},
"disconnect": {},
"upgrade": {},
"setup": {},
"generate": {},
"batch": {},
"apply": {},
"patch": {},
"put": {},
"post": {},
"sync": {},
"link": {},
"unlink": {},
"grant": {},
"revoke": {},
"assign": {},
"close": {},
"open": {},
"archive": {},
"restore": {},
"finish": {},
}
)
// defaultClassOverrides is the package-level override table. It is intentionally
// empty by default: heuristics are expected to cover the common case, and the
// override table exists only to correct individual misclassifications without
// touching the heuristic verb tables. Mutate it via SetCmdClassOverride.
//
// Keys may be either a single verb ("download") or a full space-joined command
// path ("doc export"); both are matched case-insensitively. A full-path key
// wins over a single-verb key when both are present.
var defaultClassOverrides = map[string]CmdClass{}
// SetCmdClassOverride registers a process-wide classification override for the
// given key. The key may be a single verb or a space-joined command path; it is
// normalised to lowercase. Passing CmdClassUnknown removes the override.
func SetCmdClassOverride(key string, class CmdClass) {
key = strings.ToLower(strings.TrimSpace(key))
if key == "" {
return
}
if class == CmdClassUnknown {
delete(defaultClassOverrides, key)
return
}
defaultClassOverrides[key] = class
}
// ClassifyDwsCommand classifies a dws command given its path segments (e.g.
// "todo", "task", "create"). It consults the package-level override table and
// then the read/write verb heuristics, scanning segments right-to-left so the
// leaf action verb dominates a container/noun segment.
//
// Remember the SAFETY CONTRACT on CmdClass: a CmdClassUnknown result MUST be
// treated as write (require confirmation) by the caller.
func ClassifyDwsCommand(parts ...string) CmdClass {
return ClassifyDwsCommandWith(defaultClassOverrides, parts...)
}
// ClassifyDwsCommandWith is like ClassifyDwsCommand but lets the caller supply
// an explicit override table (e.g. a per-tenant or per-request map) instead of
// the package-level one. A nil overrides map is allowed and means "no
// overrides". Override lookups always win over the heuristics.
func ClassifyDwsCommandWith(overrides map[string]CmdClass, parts ...string) CmdClass {
norm := normalizeParts(parts)
if len(norm) == 0 {
return CmdClassUnknown
}
// 1. Full-path override (most specific) wins.
if overrides != nil {
fullKey := strings.Join(norm, " ")
if c, ok := overrides[fullKey]; ok {
return c
}
}
// 2. Walk segments right-to-left: the leaf action verb is the most
// meaningful, but a leaf may be a noun/placeholder (e.g. an id) that
// we do not recognise, so fall back toward the root.
for i := len(norm) - 1; i >= 0; i-- {
seg := norm[i]
if overrides != nil {
if c, ok := overrides[seg]; ok {
return c
}
}
if c, ok := classifyVerb(seg); ok {
return c
}
}
return CmdClassUnknown
}
// classifyVerb classifies a single segment, handling compound verbs such as
// "send-by-bot", "list-forms", "batch-update" or "create_inline" by also
// trying the token before the first separator. Returns (class, true) when the
// segment maps to a known verb.
func classifyVerb(seg string) (CmdClass, bool) {
candidates := verbCandidates(seg)
for _, v := range candidates {
if _, ok := readVerbs[v]; ok {
return CmdClassRead, true
}
if _, ok := writeVerbs[v]; ok {
return CmdClassWrite, true
}
}
return CmdClassUnknown, false
}
// verbCandidates expands a segment into the tokens worth checking against the
// verb tables, in priority order: the whole segment first, then the leading
// token before the first '-' or '_' separator (so "batch-update" tries
// "update"-as-prefix... no: it tries "batch" first which is itself a write
// verb; and "send-by-bot" tries "send"). This handles both "<verb>-<qualifier>"
// and "<verb>_<qualifier>" compounds where the action sits at the front.
func verbCandidates(seg string) []string {
out := []string{seg}
if i := strings.IndexAny(seg, "-_"); i > 0 {
out = append(out, seg[:i])
}
return out
}
// normalizeParts lowercases, trims and drops empty/whitespace segments so that
// callers can pass raw argv-style slices without pre-cleaning them.
func normalizeParts(parts []string) []string {
out := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.ToLower(strings.TrimSpace(p))
if p == "" {
continue
}
out = append(out, p)
}
return out
}