Files
smartmate/backend/newAgent/tools/context_tools.go
Losita d89e2830a9 Version: 0.9.52.dev.260428
后端:
1. 工具结果结构化切流继续推进:schedule 读工具改为“父包 adapter + 子包 view builder”,`queue_pop_head` / `queue_skip_head` 脱离 legacy wrapper,`analyze_health` / `analyze_rhythm` 补齐 `schedule.analysis_result` 诊断卡片。
2. 非 schedule 工具补齐专属结果协议:`web_search` / `web_fetch`、`upsert_task_class`、`context_tools_add` / `context_tools_remove` 全部接入结构化 `ResultView`,注册表继续去 legacy wrapper,同时保持原始 `ObservationText` 供模型链路复用。
3. 工具展示细节继续收口:参数本地化补齐 `domain` / `packs` / `mode` / `all`,deliver 阶段补发段落分隔,避免 execute 与总结正文黏连。

前端:
4. `ToolCardRenderer` 升级为多协议通用渲染器,补齐 read / analysis / web / taskclass / context 卡片渲染、参数折叠区、未知协议兜底与操作明细展示。
5. `AssistantPanel` 修正 `tool_result` 结果回填与卡片布局宽度问题,并新增结构化卡片 fixture / mock 调试入口,便于整体验收。

仓库:
6. 更新工具结果结构化交接文档,补记第四批切流范围、当前切流点与后续收尾建议。
2026-04-28 20:22:22 +08:00

408 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package newagenttools
import (
"encoding/json"
"strings"
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
toolcontextresult "github.com/LoveLosita/smartflow/backend/newAgent/tools/tool_context_result"
)
type contextToolsAddResult struct {
Tool string `json:"tool"`
Success bool `json:"success"`
Action string `json:"action"`
Domain string `json:"domain,omitempty"`
Packs []string `json:"packs,omitempty"`
Mode string `json:"mode,omitempty"`
Message string `json:"message,omitempty"`
Error string `json:"error,omitempty"`
ErrorCode string `json:"error_code,omitempty"`
}
type contextToolsRemoveResult struct {
Tool string `json:"tool"`
Success bool `json:"success"`
Action string `json:"action"`
Domain string `json:"domain,omitempty"`
Packs []string `json:"packs,omitempty"`
All bool `json:"all,omitempty"`
Message string `json:"message,omitempty"`
Error string `json:"error,omitempty"`
ErrorCode string `json:"error_code,omitempty"`
}
// NewContextToolsAddHandler 创建 context_tools_add 工具。
//
// 职责边界:
// 1. 这里只负责校验 domain / packs / mode并产出结构化结果
// 2. 不直接改 CommonState真正的激活切流仍由 execute 层读取 observation 后更新快照;
// 3. 因为这里拿不到 CommonState所以卡片展示的是“本次工具结果返回的 domain/packs/mode”不是全局最终快照。
func NewContextToolsAddHandler() ToolHandler {
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
_ = state
domain := NormalizeToolDomain(readContextToolString(args["domain"]))
if domain == "" {
return buildContextToolsAddExecutionResult(args, contextToolsAddResult{
Tool: ToolNameContextToolsAdd,
Success: false,
Action: "reject",
Error: "参数非法domain 仅支持 schedule/taskclass",
ErrorCode: "invalid_domain",
})
}
mode := strings.ToLower(strings.TrimSpace(readContextToolString(args["mode"])))
if mode == "" {
mode = "replace"
}
if mode != "replace" && mode != "merge" {
return buildContextToolsAddExecutionResult(args, contextToolsAddResult{
Tool: ToolNameContextToolsAdd,
Success: false,
Action: "reject",
Domain: domain,
Error: "参数非法mode 仅支持 replace/merge",
ErrorCode: "invalid_mode",
})
}
packsRaw := readContextToolStringSlice(args["packs"])
packs, errCode, errText := validateContextPacks(domain, packsRaw, false)
if errCode != "" {
return buildContextToolsAddExecutionResult(args, contextToolsAddResult{
Tool: ToolNameContextToolsAdd,
Success: false,
Action: "reject",
Domain: domain,
Error: errText,
ErrorCode: errCode,
})
}
// 1. schedule 未显式传 packs 时,默认激活最小可用包 mutation+analyze。
// 2. taskclass 当前没有可选包,所以这里会保持空切片,由 execute 层只保留固定 core。
// 3. 这样做可以让 observation 直接表达“本次实际生效的可选包集合”,减少展示层再二次猜测。
if domain == ToolDomainSchedule && len(packsRaw) == 0 {
packs = ResolveEffectiveToolPacks(domain, nil)
}
return buildContextToolsAddExecutionResult(args, contextToolsAddResult{
Tool: ToolNameContextToolsAdd,
Success: true,
Action: "activate",
Domain: domain,
Packs: packs,
Mode: mode,
Message: "已激活目标工具域,可继续调用对应业务工具。",
})
}
}
// NewContextToolsRemoveHandler 创建 context_tools_remove 工具。
//
// 职责边界:
// 1. 这里只解释 domain / packs / all 的语义,并返回结构化结果;
// 2. all=true 表示清空全部业务工具域domain+packs 表示移除某域下的可选包;
// 3. 实际 CommonState 的域/包更新,仍由 execute 层统一消费 observation 完成。
func NewContextToolsRemoveHandler() ToolHandler {
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
_ = state
all := readContextToolBool(args["all"])
domainRaw := strings.ToLower(strings.TrimSpace(readContextToolString(args["domain"])))
packsRaw := readContextToolStringSlice(args["packs"])
// 兼容旧写法domain=all 也视为清空全部业务工具域。
if domainRaw == "all" {
all = true
}
if all {
return buildContextToolsRemoveExecutionResult(args, contextToolsRemoveResult{
Tool: ToolNameContextToolsRemove,
Success: true,
Action: "clear_all",
All: true,
Message: "已移除全部业务工具域,仅保留 context 管理工具。",
})
}
domain := NormalizeToolDomain(domainRaw)
if domain == "" {
return buildContextToolsRemoveExecutionResult(args, contextToolsRemoveResult{
Tool: ToolNameContextToolsRemove,
Success: false,
Action: "reject",
Error: "参数非法:需要提供 domain=schedule/taskclass 或 all=true",
ErrorCode: "invalid_domain",
})
}
packs, errCode, errText := validateContextPacks(domain, packsRaw, true)
if errCode != "" {
return buildContextToolsRemoveExecutionResult(args, contextToolsRemoveResult{
Tool: ToolNameContextToolsRemove,
Success: false,
Action: "reject",
Domain: domain,
Error: errText,
ErrorCode: errCode,
})
}
if len(packs) > 0 {
return buildContextToolsRemoveExecutionResult(args, contextToolsRemoveResult{
Tool: ToolNameContextToolsRemove,
Success: true,
Action: "deactivate_packs",
Domain: domain,
Packs: packs,
Message: "已移除指定工具包。",
})
}
return buildContextToolsRemoveExecutionResult(args, contextToolsRemoveResult{
Tool: ToolNameContextToolsRemove,
Success: true,
Action: "deactivate",
Domain: domain,
Message: "已移除指定工具域。",
})
}
}
func buildContextToolsAddExecutionResult(args map[string]any, payload contextToolsAddResult) ToolExecutionResult {
observation := marshalContextToolsAddResult(payload)
legacy := LegacyResult(ToolNameContextToolsAdd, args, observation)
view := toolcontextresult.BuildAddView(toContextToolsAddPayload(payload), observation)
return buildContextToolExecutionResult(legacy, args, view)
}
func buildContextToolsRemoveExecutionResult(args map[string]any, payload contextToolsRemoveResult) ToolExecutionResult {
observation := marshalContextToolsRemoveResult(payload)
legacy := LegacyResult(ToolNameContextToolsRemove, args, observation)
view := toolcontextresult.BuildRemoveView(toContextToolsRemovePayload(payload), observation)
return buildContextToolExecutionResult(legacy, args, view)
}
// buildContextToolExecutionResult 负责把子包纯展示结构包回 ToolExecutionResult。
//
// 职责边界:
// 1. 只做 ContextResultView -> ToolDisplayView 的协议桥接;
// 2. 不改写 ObservationText确保模型侧仍消费原始 observation JSON
// 3. 错误码与错误文案继续复用父包现有 JSON/text 解析逻辑,避免多套失败判定分叉。
func buildContextToolExecutionResult(
legacy ToolExecutionResult,
args map[string]any,
view toolcontextresult.ContextResultView,
) ToolExecutionResult {
result := legacy
status := normalizeToolStatus(result.Status)
if status == "" {
status = ToolStatusDone
}
collapsed := cloneAnyMap(view.Collapsed)
if collapsed == nil {
collapsed = make(map[string]any)
}
expanded := cloneAnyMap(view.Expanded)
if expanded == nil {
expanded = make(map[string]any)
}
collapsed["status"] = status
if _, exists := collapsed["status_label"]; !exists {
collapsed["status_label"] = resolveToolStatusLabelCN(status)
}
if _, exists := expanded["raw_text"]; !exists {
expanded["raw_text"] = result.ObservationText
}
result.Status = status
result.Success = status == ToolStatusDone
result.ResultView = &ToolDisplayView{
ViewType: strings.TrimSpace(view.ViewType),
Version: view.Version,
Collapsed: collapsed,
Expanded: expanded,
}
if title, ok := readStringAnyMap(collapsed, "title"); ok {
result.Summary = title
}
if !result.Success {
errorCode, errorMessage := extractToolErrorInfo(result.ObservationText, status)
if strings.TrimSpace(result.ErrorCode) == "" {
result.ErrorCode = strings.TrimSpace(errorCode)
}
if strings.TrimSpace(result.ErrorMessage) == "" {
result.ErrorMessage = strings.TrimSpace(errorMessage)
}
}
return EnsureToolResultDefaults(result, args)
}
func toContextToolsAddPayload(payload contextToolsAddResult) toolcontextresult.ContextToolsAddPayload {
return toolcontextresult.ContextToolsAddPayload{
Tool: payload.Tool,
Success: payload.Success,
Action: payload.Action,
Domain: payload.Domain,
Packs: append([]string(nil), payload.Packs...),
Mode: payload.Mode,
Message: payload.Message,
Error: payload.Error,
ErrorCode: payload.ErrorCode,
}
}
func toContextToolsRemovePayload(payload contextToolsRemoveResult) toolcontextresult.ContextToolsRemovePayload {
return toolcontextresult.ContextToolsRemovePayload{
Tool: payload.Tool,
Success: payload.Success,
Action: payload.Action,
Domain: payload.Domain,
Packs: append([]string(nil), payload.Packs...),
All: payload.All,
Message: payload.Message,
Error: payload.Error,
ErrorCode: payload.ErrorCode,
}
}
func validateContextPacks(domain string, packs []string, forRemove bool) ([]string, string, string) {
normalizedDomain := NormalizeToolDomain(domain)
if normalizedDomain == "" {
return nil, "invalid_domain", "参数非法domain 非法"
}
if len(packs) == 0 {
return nil, "", ""
}
if normalizedDomain == ToolDomainTaskClass {
return nil, "unsupported_packs_for_domain", "参数非法taskclass 暂不支持 packs"
}
normalized := make([]string, 0, len(packs))
seen := make(map[string]struct{}, len(packs))
for _, raw := range packs {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
continue
}
pack := NormalizeToolPack(normalizedDomain, trimmed)
if pack == "" {
return nil, "invalid_pack", "参数非法:存在不支持的 pack"
}
if IsFixedToolPack(normalizedDomain, pack) {
if forRemove {
return nil, "fixed_pack_forbidden", "参数非法core 为固定包,不允许 remove"
}
return nil, "fixed_pack_forbidden", "参数非法core 为固定包,不允许 add"
}
if _, exists := seen[pack]; exists {
continue
}
seen[pack] = struct{}{}
normalized = append(normalized, pack)
}
if len(normalized) == 0 {
return nil, "invalid_pack", "参数非法packs 为空或无效"
}
return normalized, "", ""
}
func readContextToolString(raw any) string {
text, _ := raw.(string)
return strings.TrimSpace(text)
}
func readContextToolStringSlice(raw any) []string {
switch typed := raw.(type) {
case []string:
out := make([]string, 0, len(typed))
for _, item := range typed {
text := strings.TrimSpace(item)
if text == "" {
continue
}
out = append(out, text)
}
return out
case []any:
out := make([]string, 0, len(typed))
for _, item := range typed {
text, ok := item.(string)
if !ok {
continue
}
text = strings.TrimSpace(text)
if text == "" {
continue
}
out = append(out, text)
}
return out
case string:
text := strings.TrimSpace(typed)
if text == "" {
return nil
}
parts := strings.Split(text, ",")
out := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
out = append(out, part)
}
return out
default:
return nil
}
}
func readContextToolBool(raw any) bool {
switch v := raw.(type) {
case bool:
return v
case string:
value := strings.ToLower(strings.TrimSpace(v))
return value == "1" || value == "true" || value == "yes"
case float64:
return v != 0
case float32:
return v != 0
case int:
return v != 0
case int8:
return v != 0
case int16:
return v != 0
case int32:
return v != 0
case int64:
return v != 0
default:
return false
}
}
func marshalContextToolsAddResult(result contextToolsAddResult) string {
raw, err := json.Marshal(result)
if err != nil {
return `{"tool":"context_tools_add","success":false,"action":"reject","error":"result encode failed","error_code":"encode_failed"}`
}
return string(raw)
}
func marshalContextToolsRemoveResult(result contextToolsRemoveResult) string {
raw, err := json.Marshal(result)
if err != nil {
return `{"tool":"context_tools_remove","success":false,"action":"reject","error":"result encode failed","error_code":"encode_failed"}`
}
return string(raw)
}