Files
smartmate/backend/newAgent/tools/web_result_handlers.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

192 lines
5.5 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 (
"strings"
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
"github.com/LoveLosita/smartflow/backend/newAgent/tools/web"
webresult "github.com/LoveLosita/smartflow/backend/newAgent/tools/web_result"
)
// NewWebSearchToolHandler 返回 web_search 的结构化结果 handler。
//
// 职责边界:
// 1. 负责执行底层 web_search 工具,并保留原始 ObservationText 给模型。
// 2. 负责把工具参数投影成 web_result 子包需要的最小输入。
// 3. 不负责注册接线registry.go 由主代理统一切流。
func NewWebSearchToolHandler(provider web.SearchProvider) ToolHandler {
searchHandler := web.NewSearchToolHandler(provider)
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
observation := searchHandler.Handle(args)
legacy := LegacyResultWithState("web_search", args, state, observation)
view := webresult.BuildSearchView(webresult.SearchViewInput{
Observation: observation,
Query: readStringArg(args, "query"),
TopK: readIntArg(args, "top_k"),
DomainAllow: readStringSliceArg(args, "domain_allow"),
RecencyDays: readIntArg(args, "recency_days"),
})
return buildWebExecutionResult(legacy, args, view)
}
}
// NewWebFetchToolHandler 返回 web_fetch 的结构化结果 handler。
//
// 职责边界:
// 1. 负责执行底层 web_fetch 工具,并保留原始 ObservationText 给模型。
// 2. 负责把抓取参数投影成 web_result 子包需要的最小输入。
// 3. 不负责注册接线registry.go 由主代理统一切流。
func NewWebFetchToolHandler(fetcher *web.Fetcher) ToolHandler {
fetchHandler := web.NewFetchToolHandler(fetcher)
return func(state *schedule.ScheduleState, args map[string]any) ToolExecutionResult {
observation := fetchHandler.Handle(args)
legacy := LegacyResultWithState("web_fetch", args, state, observation)
view := webresult.BuildFetchView(webresult.FetchViewInput{
Observation: observation,
URL: readStringArg(args, "url"),
MaxChars: readIntArg(args, "max_chars"),
})
return buildWebExecutionResult(legacy, args, view)
}
}
// buildWebExecutionResult 负责把子包纯展示视图包回父包统一协议。
//
// 步骤化说明:
// 1. 先以 legacy 结果为基础,复用父包现有的参数预览、错误抽取与兜底字段。
// 2. 再用子包 collapsed.status 覆盖最终状态,支持“未启用 provider -> blocked”的卡片语义。
// 3. 最后补齐 raw_text / status_label保证 execute、SSE、timeline 都消费同一份 observation。
func buildWebExecutionResult(
legacy ToolExecutionResult,
args map[string]any,
view webresult.ResultView,
) ToolExecutionResult {
result := legacy
status := normalizeToolStatus(result.Status)
if status == "" {
status = ToolStatusDone
}
if collapsedStatus, ok := readStringAnyMap(view.Collapsed, "status"); ok {
if normalized := normalizeToolStatus(collapsedStatus); normalized != "" {
status = normalized
}
}
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
}
if _, exists := expanded["machine_payload"]; !exists {
expanded["machine_payload"] = map[string]any{}
}
viewType := strings.TrimSpace(view.ViewType)
if viewType == "" {
viewType = webresult.ViewTypeSearchResult
}
version := view.Version
if version <= 0 {
version = webresult.ViewVersionResult
}
result.Status = status
result.Success = status == ToolStatusDone
result.ResultView = &ToolDisplayView{
ViewType: viewType,
Version: 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 readStringArg(args map[string]any, key string) string {
if len(args) == 0 {
return ""
}
raw, exists := args[strings.TrimSpace(key)]
if !exists || raw == nil {
return ""
}
text, ok := raw.(string)
if !ok {
return ""
}
return strings.TrimSpace(text)
}
func readIntArg(args map[string]any, key string) int {
if len(args) == 0 {
return 0
}
value, ok := toInt(args[strings.TrimSpace(key)])
if !ok {
return 0
}
return value
}
func readStringSliceArg(args map[string]any, key string) []string {
if len(args) == 0 {
return nil
}
raw, exists := args[strings.TrimSpace(key)]
if !exists || raw == nil {
return nil
}
switch typed := raw.(type) {
case []string:
out := make([]string, 0, len(typed))
for _, item := range typed {
item = strings.TrimSpace(item)
if item == "" {
continue
}
out = append(out, item)
}
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
default:
return nil
}
}