Files
dingtalk-workspace-cli/pkg/cmdutil/leaf_merge.go
T
539d10f80f feat: 合并 pre-mcp-discovery 分支能力到 main(服务发现保持生产) (#391)
将 test/pre-mcp-discovery 分支上多位同学的能力改动整体合入 main,
服务发现端点保持生产 https://mcp.dingtalk.com,未携带分支上的预发改动。

主要内容:
- 修雨域(chat / report / todo / contact / mail)对齐 wukong 基线 (#355)
- DWS 内容生产能力增强 (#387)、文档导出与 OA 对齐 (#362 #388 #390)
- envelope 注册 cli.Aliases 为 cobra aliases、命令结构与 JSON 解析优化
- skills 多包同步 wukong 对齐内容(attendance/calendar/minutes/oa/sheet 等)
- product/oa 进一步对齐 wukong

排除项:未携带 566b4e1(test: point service discovery to pre-mcp.dingtalk.com);
以下 4 个常量保持生产:skill_command.go / auth/endpoints.go / cli/loader.go / market/registry.go。

Co-authored-by: john <32427341+wqyenjoy@users.noreply.github.com>
Co-authored-by: wxianfeng <wang.fl1429@gmail.com>
2026-06-02 21:04:12 +08:00

137 lines
6.0 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 cmdutil
import (
"log/slog"
"github.com/spf13/cobra"
)
// MergeHardcodedLeaves grafts leaves from hardcodedRoot onto dynamicRoot when
// the same-named path does not already exist. Groups recurse. On leaf
// conflicts the dynamic side wins by default, because the discovery envelope
// is the runtime authority — hardcoded commands are retained only as a
// leaf-level fallback for behaviour the envelope explicitly does not
// declare. See _docs/discovery-overlay-authority.md.
//
// Explicit opt-in override: a hardcoded leaf may promote itself over a
// same-named dynamic leaf by carrying a strictly higher OverridePriority
// (see SetOverridePriority / OverridePriority). This exists for the narrow
// case where the envelope exposes one dispatch path but the hardcoded leaf
// needs richer flag-based routing (e.g. `chat message send` fanning out to
// multiple MCP tools depending on --group vs --user), which the envelope
// cannot currently express. Helpers without the annotation still lose to
// the envelope, so the default authority contract is preserved.
//
// PRECONDITION: dynamicRoot must be envelope-sourced (carry the
// SourceAnnotation=SourceEnvelope marker set by BuildDynamicCommands via
// MarkEnvelopeSource). Callers that might otherwise pass a helper-fallback
// root with the same name are responsible for evicting it upstream —
// otherwise the "envelope is authority" rule silently promotes helper leaves
// over same-named hardcoded leaves and the overlay loses its ability to
// override routing. The wukong overlay's RegisterProducts gates this call on
// IsEnvelopeSourced(dynamicRoot); new callers must do the same.
//
// Conflict resolution table:
//
// dynamic hardcoded → action
// ------- --------------------------------- -----------------------------
// absent anything graft hardcoded subtree
// leaf leaf (hc priority ≤ dyn) dynamic wins (no-op)
// leaf leaf (hc priority > dyn) hardcoded replaces dynamic
// group group recurse
// leaf group (hc priority > dyn) hardcoded group replaces dyn leaf
// leaf group (hc priority ≤ dyn) dynamic wins, warn
// group leaf dynamic wins, warn
//
// The "leaf vs group" priority promotion path (added for issue #164) covers
// the case where the envelope exposes a single tool at a CLI path but the
// hardcoded helper restructures that path into a group of richer subcommands
// (e.g. `chat group members` published as a leaf for `get_group_members`,
// but the helper provides `list / add / remove / add-bot` siblings). Without
// this path the helper subtree is silently dropped on every release, which
// is exactly the regression the OverridePriority annotation exists to prevent
// for leaf-vs-leaf — extending it to leaf-vs-group keeps the contract honest.
//
// MergeHardcodedLeaves mutates dynamicRoot in place and returns it so callers
// can chain. hardcodedRoot is treated as a donor: grafted children are
// detached from it so their cobra parent pointer points at the new parent.
func MergeHardcodedLeaves(dynamicRoot, hardcodedRoot *cobra.Command) *cobra.Command {
if dynamicRoot == nil || hardcodedRoot == nil {
return dynamicRoot
}
// Snapshot children before mutating hardcodedRoot — RemoveCommand during
// iteration over hardcodedRoot.Commands() is unsafe because cobra returns
// a slice backed by an internal field that is re-sliced on removal.
children := append([]*cobra.Command(nil), hardcodedRoot.Commands()...)
for _, hc := range children {
dyn := findChildByName(dynamicRoot, hc.Name())
switch {
case dyn == nil:
hardcodedRoot.RemoveCommand(hc)
dynamicRoot.AddCommand(hc)
case IsLeafCmd(hc) && IsLeafCmd(dyn):
if OverridePriority(hc) > OverridePriority(dyn) {
hardcodedRoot.RemoveCommand(hc)
dynamicRoot.RemoveCommand(dyn)
dynamicRoot.AddCommand(hc)
}
// else: envelope is authority; hardcoded leaf is ignored.
case !IsLeafCmd(hc) && !IsLeafCmd(dyn) && OverridePriority(hc) > OverridePriority(dyn):
hardcodedRoot.RemoveCommand(hc)
dynamicRoot.RemoveCommand(dyn)
dynamicRoot.AddCommand(hc)
case !IsLeafCmd(hc) && !IsLeafCmd(dyn):
MergeHardcodedLeaves(dyn, hc)
case IsLeafCmd(dyn) && !IsLeafCmd(hc) && OverridePriority(hc) > OverridePriority(dyn):
// Helper restructures a dynamic leaf into a richer subcommand
// group; honour the explicit OverridePriority opt-in just like
// the leaf-vs-leaf case.
hardcodedRoot.RemoveCommand(hc)
dynamicRoot.RemoveCommand(dyn)
dynamicRoot.AddCommand(hc)
default:
slog.Warn("overlay: shape mismatch, keeping dynamic",
"name", hc.Name(),
"dynamicIsLeaf", IsLeafCmd(dyn),
"hardcodedIsLeaf", IsLeafCmd(hc))
}
}
return dynamicRoot
}
// IsLeafCmd reports whether a command has no subcommands. Leaves carry a RunE
// and are invocation targets; groups merely organise subcommands.
func IsLeafCmd(cmd *cobra.Command) bool {
if cmd == nil {
return false
}
return !cmd.HasSubCommands()
}
// findChildByName scans parent's direct children for a matching Name(). A
// local helper so pkg/cmdutil stays independent of internal/cobracmd.
func findChildByName(parent *cobra.Command, name string) *cobra.Command {
if parent == nil {
return nil
}
for _, child := range parent.Commands() {
if child.Name() == name {
return child
}
}
return nil
}