Files
smartmate/backend/services/agent/tools/web_result/common.go
Losita d7184b776b 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 迁移面
2026-05-05 16:00:57 +08:00

433 lines
10 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 web_result
import (
"encoding/json"
"fmt"
"net/url"
"strings"
)
// 设计说明:
// 1. 本轮只处理 web 工具卡片,按 AGENTS.md 的迁移约束避免同一轮跨多个能力域抽公共 toolview 层。
// 2. 因此这里先在 web_result 包内保留最小公共 helper保证 web_search / web_fetch 先完成切流。
// 3. 若后续 taskclass / context 也出现同类卡片 helper再由主代理统一评估是否下沉成公共层。
// BuildResultView 统一封装 web 结果卡片结构。
//
// 职责边界:
// 1. 负责把已经计算好的折叠态、展开态内容组装成标准视图。
// 2. 负责在子包内补齐 status / status_label避免依赖父包状态常量。
// 3. 不负责 ToolExecutionResult 外层协议,也不改写 observation 原文。
func BuildResultView(input BuildResultViewInput) ResultView {
status := normalizeStatus(input.Status)
if status == "" {
status = StatusDone
}
collapsed := CollapsedView{
Title: input.Title,
Subtitle: input.Subtitle,
Status: status,
StatusLabel: resolveStatusLabelCN(status),
Metrics: appendMetricCopy(input.Metrics),
}
expanded := ExpandedView{
Items: appendItemCopy(input.Items),
Sections: cloneSectionList(input.Sections),
RawText: input.Observation,
MachinePayload: cloneAnyMap(input.MachinePayload),
}
return ResultView{
ViewType: normalizeViewType(input.ViewType),
Version: ViewVersionResult,
Collapsed: collapsed.Map(),
Expanded: expanded.Map(),
}
}
func BuildMetric(label string, value string) MetricField {
return MetricField{
Label: strings.TrimSpace(label),
Value: strings.TrimSpace(value),
}
}
func BuildKVField(label string, value string) KVField {
return KVField{
Label: strings.TrimSpace(label),
Value: strings.TrimSpace(value),
}
}
func BuildItem(title string, subtitle string, tags []string, detailLines []string, meta map[string]any) ItemView {
return ItemView{
Title: strings.TrimSpace(title),
Subtitle: strings.TrimSpace(subtitle),
Tags: normalizeStringSlice(tags),
DetailLines: normalizeStringSlice(detailLines),
Meta: cloneAnyMap(meta),
}
}
func BuildKVSection(title string, fields []KVField) map[string]any {
rows := make([]map[string]any, 0, len(fields))
for _, field := range fields {
label := strings.TrimSpace(field.Label)
value := strings.TrimSpace(field.Value)
if label == "" || value == "" {
continue
}
rows = append(rows, map[string]any{
"label": label,
"value": value,
})
}
return map[string]any{
"type": "kv",
"title": strings.TrimSpace(title),
"fields": rows,
}
}
func BuildItemsSection(title string, items []ItemView) map[string]any {
rows := make([]map[string]any, 0, len(items))
for _, item := range items {
rows = append(rows, item.Map())
}
return map[string]any{
"type": "items",
"title": strings.TrimSpace(title),
"items": rows,
}
}
func BuildCalloutSection(title string, subtitle string, tone string, detailLines []string) map[string]any {
return map[string]any{
"type": "callout",
"title": strings.TrimSpace(title),
"subtitle": strings.TrimSpace(subtitle),
"tone": strings.TrimSpace(tone),
"detail_lines": normalizeStringSlice(detailLines),
}
}
func BuildArgsSection(title string, fields []KVField) map[string]any {
if len(fields) == 0 {
return nil
}
valid := make([]KVField, 0, len(fields))
for _, field := range fields {
label := strings.TrimSpace(field.Label)
value := strings.TrimSpace(field.Value)
if label == "" || value == "" {
continue
}
valid = append(valid, BuildKVField(label, value))
}
if len(valid) == 0 {
return nil
}
return BuildKVSection(title, valid)
}
func appendSectionIfPresent(target *[]map[string]any, section map[string]any) {
if section == nil {
return
}
*target = append(*target, section)
}
func appendMetricCopy(metrics []MetricField) []MetricField {
if len(metrics) == 0 {
return make([]MetricField, 0)
}
out := make([]MetricField, 0, len(metrics))
for _, metric := range metrics {
label := strings.TrimSpace(metric.Label)
value := strings.TrimSpace(metric.Value)
if label == "" || value == "" {
continue
}
out = append(out, MetricField{Label: label, Value: value})
}
if len(out) == 0 {
return make([]MetricField, 0)
}
return out
}
func appendItemCopy(items []ItemView) []ItemView {
if len(items) == 0 {
return make([]ItemView, 0)
}
out := make([]ItemView, 0, len(items))
for _, item := range items {
out = append(out, BuildItem(item.Title, item.Subtitle, item.Tags, item.DetailLines, item.Meta))
}
return out
}
func normalizeViewType(viewType string) string {
switch strings.TrimSpace(viewType) {
case ViewTypeFetchResult:
return ViewTypeFetchResult
case ViewTypeSearchResult:
return ViewTypeSearchResult
default:
return ViewTypeSearchResult
}
}
func normalizeStatus(status string) string {
switch strings.ToLower(strings.TrimSpace(status)) {
case StatusDone:
return StatusDone
case StatusBlocked:
return StatusBlocked
case StatusFailed:
return StatusFailed
default:
return ""
}
}
func resolveStatusLabelCN(status string) string {
switch normalizeStatus(status) {
case StatusDone:
return "已完成"
case StatusBlocked:
return "已阻断"
default:
return "失败"
}
}
func normalizeStringSlice(values []string) []string {
if len(values) == 0 {
return make([]string, 0)
}
out := make([]string, 0, len(values))
for _, value := range values {
text := strings.TrimSpace(value)
if text == "" {
continue
}
out = append(out, text)
}
if len(out) == 0 {
return make([]string, 0)
}
return out
}
func parseObservationJSON(observation string) (map[string]any, bool) {
trimmed := strings.TrimSpace(observation)
if trimmed == "" || !strings.HasPrefix(trimmed, "{") {
return nil, false
}
var payload map[string]any
if err := json.Unmarshal([]byte(trimmed), &payload); err != nil {
return nil, false
}
return payload, true
}
func cloneSectionList(sections []map[string]any) []map[string]any {
if len(sections) == 0 {
return make([]map[string]any, 0)
}
out := make([]map[string]any, 0, len(sections))
for _, section := range sections {
out = append(out, cloneAnyMap(section))
}
return out
}
func cloneAnyMap(input map[string]any) map[string]any {
if len(input) == 0 {
return nil
}
out := make(map[string]any, len(input))
for key, value := range input {
out[key] = cloneAnyValue(value)
}
return out
}
func cloneAnyValue(value any) any {
switch typed := value.(type) {
case map[string]any:
return cloneAnyMap(typed)
case []map[string]any:
out := make([]map[string]any, 0, len(typed))
for _, item := range typed {
out = append(out, cloneAnyMap(item))
}
return out
case []any:
out := make([]any, 0, len(typed))
for _, item := range typed {
out = append(out, cloneAnyValue(item))
}
return out
case []string:
out := make([]string, len(typed))
copy(out, typed)
return out
default:
return typed
}
}
func firstString(input map[string]any, keys ...string) string {
for _, key := range keys {
if value := readString(input, key); value != "" {
return value
}
}
return ""
}
func readString(input map[string]any, key string) string {
if len(input) == 0 {
return ""
}
value, exists := input[key]
if !exists || value == nil {
return ""
}
switch typed := value.(type) {
case string:
return strings.TrimSpace(typed)
default:
text := strings.TrimSpace(fmt.Sprintf("%v", typed))
if text == "" || text == "<nil>" {
return ""
}
return text
}
}
func readBool(input map[string]any, key string) (bool, bool) {
if len(input) == 0 {
return false, false
}
value, exists := input[key]
if !exists {
return false, false
}
typed, ok := value.(bool)
return typed, ok
}
func readInt(input map[string]any, key string) int {
if len(input) == 0 {
return 0
}
value, exists := input[key]
if !exists || value == nil {
return 0
}
switch typed := value.(type) {
case int:
return typed
case int32:
return int(typed)
case int64:
return int(typed)
case float64:
return int(typed)
default:
return 0
}
}
func previewText(text string, limit int) string {
trimmed := strings.TrimSpace(text)
if trimmed == "" {
return ""
}
runes := []rune(trimmed)
if limit <= 0 || len(runes) <= limit {
return string(runes)
}
return string(runes[:limit]) + "..."
}
func previewLines(text string, maxLines int, maxChars int) []string {
trimmed := strings.TrimSpace(text)
if trimmed == "" {
return make([]string, 0)
}
lines := strings.Split(trimmed, "\n")
out := make([]string, 0, maxLines)
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
out = append(out, previewText(line, maxChars))
if maxLines > 0 && len(out) >= maxLines {
break
}
}
if len(out) == 0 {
out = append(out, previewText(trimmed, maxChars))
}
return out
}
func formatStringSliceCN(items []string, limit int) string {
normalized := normalizeStringSlice(items)
if len(normalized) == 0 {
return ""
}
if limit <= 0 || len(normalized) <= limit {
return strings.Join(normalized, "、")
}
return fmt.Sprintf("%s 等 %d 个", strings.Join(normalized[:limit], "、"), len(normalized))
}
func formatBoolCN(value bool) string {
if value {
return "是"
}
return "否"
}
func classifyUnavailableStatus(message string) string {
trimmed := strings.TrimSpace(message)
lower := strings.ToLower(trimmed)
switch {
case strings.Contains(trimmed, "暂未启用"),
strings.Contains(trimmed, "未启用"),
strings.Contains(trimmed, "暂未初始化"),
strings.Contains(trimmed, "未初始化"),
strings.Contains(trimmed, "未配置"),
strings.Contains(lower, "not enabled"),
strings.Contains(lower, "not configured"),
strings.Contains(lower, "unavailable"):
return StatusBlocked
default:
return StatusFailed
}
}
func buildRawPreviewSection(rawText string) map[string]any {
preview := previewText(rawText, 160)
if preview == "" {
return nil
}
return BuildCalloutSection("原始结果预览", preview, "info", previewLines(rawText, 3, 120))
}
func hostnameFromURL(rawURL string) string {
parsed, err := url.Parse(strings.TrimSpace(rawURL))
if err != nil {
return ""
}
return strings.TrimSpace(parsed.Hostname())
}