Files

189 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 smart
import (
"fmt"
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
)
// ReplaceBatch: apply MANY word replacements to one minute (听记) in a single
// command.
//
// dws's atomic tool replace_minutes_text handles exactly one
// originalText→replacedText pair (see internal/helpers/minutes.go, the
// +word-replace 1:1 shortcut). Fixing several terms at once means calling it
// repeatedly and eyeballing each result. This shortcut takes multiple --pair
// "原文=>替换" entries, validates them (rejecting duplicate source words so two
// rules don't fight over the same term), applies each via replace_minutes_text
// and aggregates a per-pair {applied|error} report through rt.Output — one
// failing pair does not abort the rest.
//
// dws minutes +replace-batch --id <taskUuid> --pair "张三=>张三丰" --pair "Q2=>第二季度"
var ReplaceBatch = shortcut.Shortcut{
Service: "minutes",
Command: "+replace-batch",
Product: "minutes",
Description: "对一条妙记(听记)批量执行多组文字替换(原文=>替换)",
Intent: "当你要在同一条听记里一次性纠正多个词(如把多个错识别的人名/术语统一替换),而底层工具一次只能替换一组时使用;" +
"内部按多个 --pair \"原文=>替换\" 逐组调用替换工具,先在本地校验去重(同一个「原文」不能出现两次,避免两条规则互相打架)," +
"再逐组应用并聚合每组的成功/失败结果,某一组失败不会中断其余组。这是写操作,会实际修改听记文字内容,请确认 taskUuid 与替换规则无误。",
Risk: shortcut.RiskWrite,
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "user_required", Idempotency: "unknown",
},
Contract: corecmd.ContractDecl{
Identity: contract.ToolIdentitySpec{
ProductID: "minutes",
Name: "shortcut_replace_batch",
CanonicalPath: "minutes.shortcut_replace_batch",
CLIPath: "minutes +replace-batch",
PrimaryCLIPath: "minutes +replace-batch",
},
Description: "对一条妙记(听记)批量执行多组文字替换(原文=>替换)",
Interface: &contract.InterfaceSpec{
Mode: "composite",
Availability: "available",
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.",
},
Selection: contract.SelectionSpec{
AgentSummary: "对一条妙记(听记)批量执行多组文字替换(原文=>替换)",
UseWhen: []string{"当你要在同一条听记里一次性纠正多个词(如把多个错识别的人名/术语统一替换),而底层工具一次只能替换一组时使用;内部按多个 --pair \"原文=>替换\" 逐组调用替换工具,先在本地校验去重(同一个「原文」不能出现两次,避免两条规则互相打架),再逐组应用并聚合每组的成功/失败结果,某一组失败不会中断其余组。这是写操作,会实际修改听记文字内容,请确认 taskUuid 与替换规则无误。"},
AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
Examples: []string{
"dws minutes +replace-batch --id <taskUuid> --pair \"张三=>张三丰\"",
"dws minutes +replace-batch --id <taskUuid> --pair \"Q2=>第二季度\" --pair \"PM=>产品经理\"",
},
},
Parameters: []contract.ParamDecl{
{Name: "pair", Description: `替换规则,格式 "原文=>替换",可重复传多组(必填);每组原文不能为空且不能重复`},
},
},
Flags: []shortcut.Flag{
{Name: "id", Type: shortcut.FlagString, Desc: "听记 taskUuid(必填)", Required: true},
{Name: "pair", Type: shortcut.FlagStringSlice, Desc: `替换规则,格式 "原文=>替换",可重复传多组(必填)`, Required: true},
},
Constraints: []shortcut.Constraint{
{
Kind: shortcut.ConstraintCustom,
Flags: []string{"pair"},
Description: `每个 --pair 必须使用 "原文=>替换" 格式,原文不能为空且不能重复`,
},
},
Tips: []string{
`dws minutes +replace-batch --id <taskUuid> --pair "张三=>张三丰"`,
`dws minutes +replace-batch --id <taskUuid> --pair "Q2=>第二季度" --pair "PM=>产品经理"`,
},
Validate: func(rt *shortcut.RuntimeContext) error {
_, err := parseReplacePairs(rt.StrSlice("pair"))
return err
},
Execute: func(rt *shortcut.RuntimeContext) error {
taskUUID := rt.Str("id")
pairs, err := parseReplacePairs(rt.StrSlice("pair"))
if err != nil {
return err // already validated, but stay defensive
}
if rt.DryRun() {
replacements := make([]map[string]any, 0, len(pairs))
for _, p := range pairs {
replacements = append(replacements, map[string]any{
"originalText": p.orig,
"replacedText": p.repl,
})
}
return rt.Output(map[string]any{
"dryRun": true,
"taskUuid": taskUUID,
"total": len(pairs),
"replacements": replacements,
})
}
results := make([]map[string]any, 0, len(pairs))
applied := 0
for _, p := range pairs {
// Params mirror helpers replace_minutes_text call site: taskUuid /
// originalText / replacedText.
_, callErr := rt.CallMCPWriteData("minutes", "replace_minutes_text", map[string]any{
"taskUuid": taskUUID,
"originalText": p.orig,
"replacedText": p.repl,
})
entry := map[string]any{"originalText": p.orig, "replacedText": p.repl}
if callErr != nil {
entry["error"] = callErr.Error()
} else {
entry["applied"] = true
applied++
}
results = append(results, entry)
}
return rt.Output(map[string]any{
"taskUuid": taskUUID,
"total": len(pairs),
"applied": applied,
"failed": len(pairs) - applied,
"results": results,
})
},
}
// replacePair is one parsed "原文=>替换" rule.
type replacePair struct {
orig string
repl string
}
// replacePairSep separates original from replacement text in a --pair value.
// "=>" is chosen because a bare "=" commonly appears inside the source text.
const replacePairSep = "=>"
// parseReplacePairs parses "原文=>替换" entries and rejects malformed input,
// empty source text, and duplicate source words (which would make two rules
// contend for the same term).
func parseReplacePairs(raw []string) ([]replacePair, error) {
pairs := make([]replacePair, 0, len(raw))
seen := make(map[string]struct{}, len(raw))
for _, r := range raw {
idx := strings.Index(r, replacePairSep)
if idx < 0 {
return nil, apperrors.NewValidation(fmt.Sprintf("替换规则 %q 缺少分隔符 %q,应为 \"原文%s替换\"", r, replacePairSep, replacePairSep))
}
orig := strings.TrimSpace(r[:idx])
repl := strings.TrimSpace(r[idx+len(replacePairSep):])
if orig == "" {
return nil, apperrors.NewValidation(fmt.Sprintf("替换规则 %q 的「原文」不能为空", r))
}
if _, dup := seen[orig]; dup {
return nil, apperrors.NewValidation(fmt.Sprintf("重复的「原文」%q:同一个原文只能有一条替换规则", orig))
}
seen[orig] = struct{}{}
pairs = append(pairs, replacePair{orig: orig, repl: repl})
}
return pairs, nil
}
func init() {
shortcut.Register(ReplaceBatch)
}