将 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>
283 lines
8.1 KiB
Go
283 lines
8.1 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 (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers/docjsonml"
|
|
jsonrepair "github.com/RealAlexandreAI/json-repair"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var docDangerousUnicode = [...]rune{
|
|
0x200B,
|
|
0x200C,
|
|
0x200E,
|
|
0x200F,
|
|
0x202A,
|
|
0x202B,
|
|
0x202C,
|
|
0x202D,
|
|
0x202E,
|
|
0x2066,
|
|
0x2067,
|
|
0x2068,
|
|
0x2069,
|
|
0xFEFF,
|
|
0x00AD,
|
|
}
|
|
|
|
var docDangerousSet = func() map[rune]bool {
|
|
m := make(map[rune]bool, len(docDangerousUnicode))
|
|
for _, r := range docDangerousUnicode {
|
|
m[r] = true
|
|
}
|
|
return m
|
|
}()
|
|
|
|
func stripDocDangerousUnicode(s string) string {
|
|
return strings.Map(func(r rune) rune {
|
|
if docDangerousSet[r] {
|
|
return -1
|
|
}
|
|
return r
|
|
}, s)
|
|
}
|
|
|
|
type docJSONMLFixMode int
|
|
|
|
const (
|
|
docJSONMLFixDefault docJSONMLFixMode = iota
|
|
docJSONMLFixFull
|
|
docJSONMLFixNone
|
|
)
|
|
|
|
func docResolveFixMode(cmd *cobra.Command) docJSONMLFixMode {
|
|
noFix, _ := cmd.Flags().GetBool("no-fix-jsonml")
|
|
fix, _ := cmd.Flags().GetBool("fix-jsonml")
|
|
if noFix && fix {
|
|
fmt.Fprintln(cmd.ErrOrStderr(), "[WARN] --fix-jsonml 和 --no-fix-jsonml 同时传入,以 --no-fix-jsonml 为准(全部修复关闭)")
|
|
return docJSONMLFixNone
|
|
}
|
|
if noFix {
|
|
return docJSONMLFixNone
|
|
}
|
|
if fix {
|
|
return docJSONMLFixFull
|
|
}
|
|
return docJSONMLFixDefault
|
|
}
|
|
|
|
func docCoerceJSONMLBodyShape(raw string) (string, []string, error) {
|
|
if raw == "" {
|
|
return raw, nil, nil
|
|
}
|
|
var probe any
|
|
if err := json.Unmarshal([]byte(raw), &probe); err != nil {
|
|
return raw, nil, nil
|
|
}
|
|
if _, ok := probe.([]any); ok {
|
|
wrapped, err := json.Marshal(map[string]any{"jsonml": probe})
|
|
if err != nil {
|
|
return raw, nil, fmt.Errorf("wrap bare jsonml array: %w", err)
|
|
}
|
|
return string(wrapped), nil, nil
|
|
}
|
|
return raw, nil, nil
|
|
}
|
|
|
|
func docCoerceJSONMLNodeShape(raw string) (string, []string, error) {
|
|
if raw == "" {
|
|
return raw, nil, nil
|
|
}
|
|
var probe any
|
|
if err := json.Unmarshal([]byte(raw), &probe); err != nil {
|
|
return raw, nil, nil
|
|
}
|
|
if _, ok := probe.([]any); ok {
|
|
return raw, nil, nil
|
|
}
|
|
wrapper, ok := probe.(map[string]any)
|
|
if !ok {
|
|
return raw, nil, nil
|
|
}
|
|
inner, hasKey := wrapper["jsonml"]
|
|
if !hasKey {
|
|
return raw, nil, nil
|
|
}
|
|
arr, ok := inner.([]any)
|
|
if !ok {
|
|
return raw, nil, nil
|
|
}
|
|
switch len(arr) {
|
|
case 0:
|
|
return "", nil, fmt.Errorf(`--content-format jsonml 输入 {"jsonml":[]}: wrapper 中 jsonml 数组为空`)
|
|
case 1:
|
|
out, err := json.Marshal(arr[0])
|
|
if err != nil {
|
|
return raw, nil, fmt.Errorf("unwrap single jsonml node: %w", err)
|
|
}
|
|
return string(out), []string{`输入为 {"jsonml":[node]} body 形态,已自动解包为单节点以符合 block 命令协议`}, nil
|
|
default:
|
|
return "", nil, fmt.Errorf(`block insert/update 一次只能处理一个 JSONML 节点,输入 {"jsonml":[...]} 包含 %d 个节点。请分多次调用,或使用 doc update --content-format jsonml 整篇覆盖`, len(arr))
|
|
}
|
|
}
|
|
|
|
func prepareDocJSONMLBody(cmd *cobra.Command, raw string) (string, error) {
|
|
mode := docResolveFixMode(cmd)
|
|
|
|
coerced, coerceNotes, err := docCoerceJSONMLBodyShape(raw)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
docEmitJSONMLFixNotes(cmd, coerceNotes)
|
|
raw = coerced
|
|
|
|
var wrapper map[string]any
|
|
if err := json.Unmarshal([]byte(raw), &wrapper); err != nil {
|
|
if mode == docJSONMLFixFull {
|
|
repaired, repairErr := jsonrepair.RepairJSON(raw)
|
|
if repairErr != nil {
|
|
return "", fmt.Errorf("JSON 语法错误且自动修复失败: %w\n原始错误: %v", repairErr, err)
|
|
}
|
|
fmt.Fprintln(cmd.ErrOrStderr(), "[FIX] JSON 语法已自动修复(括号/逗号等结构性错误)")
|
|
if err2 := json.Unmarshal([]byte(repaired), &wrapper); err2 != nil {
|
|
return "", fmt.Errorf("JSON 修复后仍无法解析: %w", err2)
|
|
}
|
|
} else {
|
|
return "", fmt.Errorf("JSON 语法错误: %w\n输入不是有效的 JSON(可能缺少括号或逗号)。如果输入来自 LLM 生成,可通过 --fix-jsonml 尝试自动修复", err)
|
|
}
|
|
}
|
|
|
|
bodyAny, ok := wrapper["jsonml"]
|
|
if !ok {
|
|
return "", fmt.Errorf(`--content-format jsonml 输入 JSON 必须包含 "jsonml" 字段,格式: {"jsonml": [...]}`)
|
|
}
|
|
bodyArr, ok := bodyAny.([]any)
|
|
if !ok {
|
|
return "", fmt.Errorf(`--content-format jsonml 字段 "jsonml" 必须是数组`)
|
|
}
|
|
|
|
if mode != docJSONMLFixNone {
|
|
fixed, notes := docjsonml.NormalizeJsonMLBody(bodyArr)
|
|
bodyArr = fixed
|
|
docEmitJSONMLFixNotes(cmd, notes)
|
|
|
|
wrapped, wrapNotes := docjsonml.EnsureRootWrappedBody(bodyArr)
|
|
bodyArr = wrapped
|
|
docEmitJSONMLFixNotes(cmd, wrapNotes)
|
|
}
|
|
|
|
vr := docjsonml.ValidateJsonMLBodyV2(bodyArr)
|
|
if vr.HasErrors() {
|
|
return "", fmt.Errorf("JSONML 格式校验失败:\n%s\n请确认输入格式是否正确,或通过 --no-fix-jsonml 关闭自动修复以排查原始错误", vr.Summary())
|
|
}
|
|
if summary := vr.Summary(); summary != "" {
|
|
fmt.Fprintln(cmd.ErrOrStderr(), "[WARN] "+summary)
|
|
}
|
|
|
|
out, err := json.Marshal(bodyArr)
|
|
if err != nil {
|
|
return "", fmt.Errorf("marshal normalized jsonml: %w", err)
|
|
}
|
|
return stripDocDangerousUnicode(string(out)), nil
|
|
}
|
|
|
|
func prepareDocJSONMLNode(cmd *cobra.Command, rawElement string) (string, error) {
|
|
if rawElement == "" {
|
|
return "", fmt.Errorf("--content-format jsonml 要求通过 --element 提供 JSONML 数组")
|
|
}
|
|
mode := docResolveFixMode(cmd)
|
|
|
|
coerced, coerceNotes, err := docCoerceJSONMLNodeShape(rawElement)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
docEmitJSONMLFixNotes(cmd, coerceNotes)
|
|
rawElement = coerced
|
|
|
|
var node any
|
|
if err := json.Unmarshal([]byte(rawElement), &node); err != nil {
|
|
if mode == docJSONMLFixFull {
|
|
repaired, repairErr := jsonrepair.RepairJSON(rawElement)
|
|
if repairErr != nil {
|
|
return "", fmt.Errorf("JSON 语法错误且自动修复失败: %w\n原始错误: %v", repairErr, err)
|
|
}
|
|
fmt.Fprintln(cmd.ErrOrStderr(), "[FIX] JSON 语法已自动修复(括号/逗号等结构性错误)")
|
|
if err2 := json.Unmarshal([]byte(repaired), &node); err2 != nil {
|
|
return "", fmt.Errorf("JSON 修复后仍无法解析: %w", err2)
|
|
}
|
|
} else {
|
|
return "", fmt.Errorf("JSON 语法错误: %w\n输入不是有效的 JSON(可能缺少括号或逗号)。如果输入来自 LLM 生成,可通过 --fix-jsonml 尝试自动修复", err)
|
|
}
|
|
}
|
|
if _, ok := node.([]any); !ok {
|
|
return "", fmt.Errorf("--content-format jsonml 要求 --element 为 JSON 数组,实际类型: %T", node)
|
|
}
|
|
|
|
if mode != docJSONMLFixNone {
|
|
fixed, notes := docjsonml.NormalizeJsonMLNode(node)
|
|
node = fixed
|
|
docEmitJSONMLFixNotes(cmd, notes)
|
|
}
|
|
|
|
vr := docjsonml.ValidateJsonMLNodeV2(node)
|
|
if vr.HasErrors() {
|
|
return "", fmt.Errorf("JSONML 格式校验失败:\n%s\n请确认输入格式是否正确,或通过 --no-fix-jsonml 关闭自动修复以排查原始错误", vr.Summary())
|
|
}
|
|
if summary := vr.Summary(); summary != "" {
|
|
fmt.Fprintln(cmd.ErrOrStderr(), "[WARN] "+summary)
|
|
}
|
|
|
|
out, err := json.Marshal(node)
|
|
if err != nil {
|
|
return "", fmt.Errorf("marshal normalized jsonml: %w", err)
|
|
}
|
|
return string(out), nil
|
|
}
|
|
|
|
func docEmitJSONMLFixNotes(cmd *cobra.Command, notes []string) {
|
|
if len(notes) == 0 {
|
|
return
|
|
}
|
|
fmt.Fprintf(cmd.ErrOrStderr(), "[FIX] JSONML 自动修复(%d 项):\n", len(notes))
|
|
for i, n := range notes {
|
|
fmt.Fprintf(cmd.ErrOrStderr(), " %d. %s\n", i+1, n)
|
|
}
|
|
}
|
|
|
|
func sniffJsonMLLike(content string) bool {
|
|
const lookahead = 64
|
|
s := strings.TrimLeft(content, " \t\r\n")
|
|
if s == "" {
|
|
return false
|
|
}
|
|
scan := s
|
|
if len(scan) > lookahead {
|
|
scan = scan[:lookahead]
|
|
}
|
|
switch s[0] {
|
|
case '[':
|
|
rest := strings.TrimLeft(scan[1:], " \t\r\n")
|
|
return strings.HasPrefix(rest, `"`) || strings.HasPrefix(rest, `[`)
|
|
case '{':
|
|
rest := strings.TrimLeft(scan[1:], " \t\r\n")
|
|
return strings.HasPrefix(rest, `"jsonml"`)
|
|
}
|
|
return false
|
|
}
|