解决 devapp→dev 重构与 connect/数字分身工作的合并冲突,方向:全面对齐 wxianfeng 的 dev 命名空间,保留我方两处实质内容(动态端点 + connect 功能)。 冲突解决要点: - release.yml:取 wxianfeng 更干净的 npm 守卫(repository_owner + 注释); 保留我新增的「-dws-devapp 翻成非 prerelease+latest」步骤 - devapp.go / devapp_test.go / 路由文档:取 wxianfeng(新 MCP 工具名 *_dev_app_*/extension、新增 event 工具、删 robot create、config 合 upsert) - 端点:保留我方动态 devappMCPEndpoint()(网关 base 推导、生产默认), 否则会把「建号对所有人打预发」的 bug 倒灌回来;同步修 helper_tool_fetcher.go 与 direct_runtime_test.go 改用该函数 + wxianfeng 工具名 list_dev_app - devapp_connect.go:connect 命令对齐 dev connect 命名,保留我方 --agent-cmd 自研渠道示例(#37),建号引用修正为真实存在的 robot submit - skills:跟随 wxianfeng 删除旧 dingtalk-devapp(已被重写的 dingtalk-dev 覆盖) - robot-quickstart.md:命令全改 dws dev(建号两步 submit+result、dev connect), 下载源改 wxianfeng,去掉写死版本号用 releases/latest 验证:go build ./... 通过;go vet 干净;internal/app、internal/helpers 单测全过
137 lines
4.5 KiB
Go
137 lines
4.5 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 app
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/transport"
|
|
)
|
|
|
|
// newHelperToolFetcher returns a cli.HelperToolFetcher that loads a helper MCP
|
|
// server's tools/list LIVE (by source) and projects each tool into a
|
|
// cli.HelperToolSchema (name, description, inputSchema properties/required). It
|
|
// is injected into the schema command so the cli package can render
|
|
// `dws schema dev.*` from real server schema without importing app/transport.
|
|
//
|
|
// Sources: "op-app" backs the dev app commands (pinned endpoint); "devdoc"
|
|
// backs `dws dev doc search` (endpoint resolved dynamically, see
|
|
// helperSourceEndpoint). Results are memoized per source per process so
|
|
// repeated `dws schema dev.*` hit the network at most once per source. A failed
|
|
// fetch is not cached, allowing a later retry within the same process.
|
|
func newHelperToolFetcher() cli.HelperToolFetcher {
|
|
var (
|
|
mu sync.Mutex
|
|
cached = map[string]map[string]cli.HelperToolSchema{}
|
|
)
|
|
return func(ctx context.Context, source string) (map[string]cli.HelperToolSchema, error) {
|
|
mu.Lock()
|
|
if got, ok := cached[source]; ok {
|
|
mu.Unlock()
|
|
return got, nil
|
|
}
|
|
mu.Unlock()
|
|
|
|
endpoint, err := helperSourceEndpoint(source)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
schemas, err := fetchHelperToolSchemas(ctx, endpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mu.Lock()
|
|
cached[source] = schemas
|
|
mu.Unlock()
|
|
return schemas, nil
|
|
}
|
|
}
|
|
|
|
// helperSourceEndpoint maps a schema source to its MCP endpoint. op-app (dev
|
|
// app) is pinned in source (devappMCPEndpoint, derived from the active gateway
|
|
// base — production by default, pre when ~/.dws/mcp_url points at pre); other
|
|
// sources (e.g. devdoc) are resolved the same way the runner resolves a product
|
|
// endpoint — env override → discovery → edition StaticServers/SupplementServers.
|
|
func helperSourceEndpoint(source string) (string, error) {
|
|
switch source {
|
|
case "", "op-app", "devapp":
|
|
return devappMCPEndpoint(), nil
|
|
default:
|
|
if endpoint, ok := directRuntimeEndpoint(source, ""); ok {
|
|
return endpoint, nil
|
|
}
|
|
return "", fmt.Errorf("no MCP endpoint resolved for source %q (not injected by edition/discovery)", source)
|
|
}
|
|
}
|
|
|
|
// fetchHelperToolSchemas performs the live tools/list call against endpoint and
|
|
// converts the descriptors. Auth and identity headers are resolved the same way
|
|
// the runner does for direct-runtime invocations.
|
|
func fetchHelperToolSchemas(ctx context.Context, endpoint string) (map[string]cli.HelperToolSchema, error) {
|
|
token := resolveRuntimeAuthToken(ctx, "")
|
|
headers := resolveIdentityHeaders()
|
|
client := transport.NewClient(nil).WithAuth(token, headers)
|
|
|
|
result, err := client.ListTools(ctx, endpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out := make(map[string]cli.HelperToolSchema, len(result.Tools))
|
|
for _, td := range result.Tools {
|
|
out[td.Name] = cli.HelperToolSchema{
|
|
Name: td.Name,
|
|
Description: td.Description,
|
|
Properties: inputSchemaProperties(td.InputSchema),
|
|
Required: inputSchemaRequired(td.InputSchema),
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// inputSchemaProperties pulls the "properties" object out of a deserialized
|
|
// MCP inputSchema map. Returns an empty (non-nil) map when absent.
|
|
func inputSchemaProperties(schema map[string]any) map[string]any {
|
|
if schema == nil {
|
|
return map[string]any{}
|
|
}
|
|
props, _ := schema["properties"].(map[string]any)
|
|
if props == nil {
|
|
return map[string]any{}
|
|
}
|
|
return props
|
|
}
|
|
|
|
// inputSchemaRequired pulls the "required" string list out of a deserialized
|
|
// MCP inputSchema map.
|
|
func inputSchemaRequired(schema map[string]any) []string {
|
|
if schema == nil {
|
|
return nil
|
|
}
|
|
raw, ok := schema["required"].([]any)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
out := make([]string, 0, len(raw))
|
|
for _, v := range raw {
|
|
if s, ok := v.(string); ok && s != "" {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
}
|