后端: 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. 更新工具结果结构化交接文档,补记第四批切流范围、当前切流点与后续收尾建议。
408 lines
12 KiB
Go
408 lines
12 KiB
Go
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)
|
||
}
|