Version: 0.9.75.dev.260505

后端:
1.收口阶段 6 agent 结构迁移,将 newAgent 内核与 agentsvc 编排层迁入 services/agent
- 切换 Agent 启动装配与 HTTP handler 直连 agent sv,移除旧 service agent bridge
- 补齐 Agent 对 memory、task、task-class、schedule 的 RPC 适配与契约字段
- 扩展 schedule、task、task-class RPC/contract 支撑 Agent 查询、写入与 provider 切流
- 更新迁移文档、README 与相关注释,明确 agent 当前切流点和剩余 memory 迁移面
This commit is contained in:
Losita
2026-05-05 16:00:57 +08:00
parent e1819c5653
commit d7184b776b
174 changed files with 2189 additions and 1236 deletions

View File

@@ -0,0 +1,241 @@
package web_result
import (
"encoding/json"
"fmt"
"strings"
)
type searchObservation struct {
Tool string `json:"tool"`
Query string `json:"query"`
Count int `json:"count"`
Items []searchObservationItem `json:"items"`
Error string `json:"error"`
Err string `json:"err"`
Reason string `json:"reason"`
}
type searchObservationItem struct {
Title string `json:"title"`
URL string `json:"url"`
Snippet string `json:"snippet"`
Domain string `json:"domain"`
PublishedAt string `json:"published_at"`
}
// BuildSearchView 负责把 web_search observation 构造成前端可直接消费的结果卡片。
//
// 职责边界:
// 1. 负责解析成功 / 失败 / provider 未启用 / 非 JSON 回退四类场景。
// 2. 负责保留 raw_text 与 machine_payload方便前端调试与后续交互。
// 3. 不负责执行搜索,也不改写传入 observation 原文。
func BuildSearchView(input SearchViewInput) ResultView {
payloadMap, ok := parseObservationJSON(input.Observation)
if !ok {
return buildSearchTextFallbackView(input)
}
payload := searchObservation{}
if err := json.Unmarshal([]byte(strings.TrimSpace(input.Observation)), &payload); err != nil {
return buildSearchTextFallbackView(input)
}
query := strings.TrimSpace(payload.Query)
if query == "" {
query = strings.TrimSpace(input.Query)
}
errorMessage := firstNonEmpty(payload.Error, payload.Err, payload.Reason)
if errorMessage == "" {
errorMessage = firstString(payloadMap, "message")
}
if errorMessage != "" {
return buildSearchFailureView(input, query, errorMessage, payloadMap)
}
items := buildSearchItems(payload.Items)
count := payload.Count
if count < len(items) {
count = len(items)
}
title := fmt.Sprintf("找到 %d 条网页结果", count)
subtitle := buildSearchSubtitle(query)
sections := make([]map[string]any, 0, 3)
appendSectionIfPresent(&sections, BuildArgsSection("搜索参数", buildSearchArgFields(input)))
if len(items) > 0 {
sections = append(sections, BuildItemsSection("搜索结果", items))
} else {
sections = append(sections, BuildCalloutSection(
"没有命中结果",
"当前关键词没有返回可展示结果,可以尝试缩短关键词或放宽筛选条件。",
"info",
[]string{"建议优先调整关键词,再决定是否继续抓取具体页面。"},
))
}
return BuildResultView(BuildResultViewInput{
ViewType: ViewTypeSearchResult,
Status: StatusDone,
Title: title,
Subtitle: subtitle,
Metrics: buildSearchMetrics(count, input.DomainAllow, input.RecencyDays),
Items: items,
Sections: sections,
Observation: input.Observation,
MachinePayload: payloadMap,
})
}
func buildSearchTextFallbackView(input SearchViewInput) ResultView {
subtitle := "搜索结果不是合法 JSON已回退为文本预览。"
if strings.TrimSpace(input.Observation) == "" {
subtitle = "搜索工具没有返回结构化结果,已回退为文本预览。"
}
sections := []map[string]any{
BuildCalloutSection("结果不可解析", subtitle, "danger", []string{subtitle}),
}
appendSectionIfPresent(&sections, BuildArgsSection("搜索参数", buildSearchArgFields(input)))
appendSectionIfPresent(&sections, buildRawPreviewSection(input.Observation))
return BuildResultView(BuildResultViewInput{
ViewType: ViewTypeSearchResult,
Status: StatusFailed,
Title: "网页搜索结果不可解析",
Subtitle: subtitle,
Metrics: buildSearchMetrics(0, input.DomainAllow, input.RecencyDays),
Items: make([]ItemView, 0),
Sections: sections,
Observation: input.Observation,
})
}
func buildSearchFailureView(
input SearchViewInput,
query string,
errorMessage string,
payloadMap map[string]any,
) ResultView {
status := classifyUnavailableStatus(errorMessage)
title := "网页搜索失败"
calloutTitle := "搜索执行失败"
tone := "danger"
if status == StatusBlocked {
title = "网页搜索未启用"
calloutTitle = "搜索 Provider 未启用"
tone = "warning"
}
sections := []map[string]any{
BuildCalloutSection(calloutTitle, errorMessage, tone, []string{errorMessage}),
}
appendSectionIfPresent(&sections, BuildArgsSection("搜索参数", buildSearchArgFields(input)))
return BuildResultView(BuildResultViewInput{
ViewType: ViewTypeSearchResult,
Status: status,
Title: title,
Subtitle: buildSearchFailureSubtitle(query, errorMessage),
Metrics: buildSearchMetrics(0, input.DomainAllow, input.RecencyDays),
Items: make([]ItemView, 0),
Sections: sections,
Observation: input.Observation,
MachinePayload: payloadMap,
})
}
func buildSearchItems(items []searchObservationItem) []ItemView {
if len(items) == 0 {
return make([]ItemView, 0)
}
out := make([]ItemView, 0, len(items))
for index, item := range items {
title := strings.TrimSpace(item.Title)
if title == "" {
title = fmt.Sprintf("结果 %d", index+1)
}
subtitle := strings.TrimSpace(item.URL)
if domain := strings.TrimSpace(item.Domain); domain != "" {
subtitle = domain
}
tags := make([]string, 0, 2)
if domain := strings.TrimSpace(item.Domain); domain != "" {
tags = append(tags, domain)
}
if publishedAt := strings.TrimSpace(item.PublishedAt); publishedAt != "" {
tags = append(tags, publishedAt)
}
detailLines := make([]string, 0, 2)
if snippet := strings.TrimSpace(item.Snippet); snippet != "" {
detailLines = append(detailLines, previewText(snippet, 120))
}
if rawURL := strings.TrimSpace(item.URL); rawURL != "" {
detailLines = append(detailLines, rawURL)
}
out = append(out, BuildItem(title, subtitle, tags, detailLines, map[string]any{
"url": strings.TrimSpace(item.URL),
"domain": strings.TrimSpace(item.Domain),
"published_at": strings.TrimSpace(item.PublishedAt),
}))
}
return out
}
func buildSearchMetrics(count int, domainAllow []string, recencyDays int) []MetricField {
metrics := []MetricField{
BuildMetric("结果数", fmt.Sprintf("%d", count)),
}
if len(domainAllow) > 0 {
metrics = append(metrics, BuildMetric("域名过滤", formatStringSliceCN(domainAllow, 2)))
}
if recencyDays > 0 {
metrics = append(metrics, BuildMetric("时效", fmt.Sprintf("近 %d 天", recencyDays)))
}
return metrics
}
func buildSearchArgFields(input SearchViewInput) []KVField {
fields := make([]KVField, 0, 4)
if query := strings.TrimSpace(input.Query); query != "" {
fields = append(fields, BuildKVField("关键词", query))
}
if input.TopK > 0 {
fields = append(fields, BuildKVField("结果上限", fmt.Sprintf("%d", input.TopK)))
}
if len(input.DomainAllow) > 0 {
fields = append(fields, BuildKVField("域名过滤", formatStringSliceCN(input.DomainAllow, 4)))
}
if input.RecencyDays > 0 {
fields = append(fields, BuildKVField("时效范围", fmt.Sprintf("近 %d 天", input.RecencyDays)))
}
return fields
}
func buildSearchSubtitle(query string) string {
if strings.TrimSpace(query) == "" {
return "已返回网页搜索结果。"
}
return fmt.Sprintf("关键词:%s", previewText(query, 40))
}
func buildSearchFailureSubtitle(query string, errorMessage string) string {
if strings.TrimSpace(query) == "" {
return strings.TrimSpace(errorMessage)
}
return fmt.Sprintf("关键词:%s", previewText(query, 40))
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}