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

654 lines
18 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 toolcontextresult
import (
"fmt"
"strings"
)
const (
ViewTypeContextResult = "tool.context_result"
ViewVersionContextResult = 1
StatusDone = "done"
StatusFailed = "failed"
)
// ContextResultView 仅承载 context 工具卡片的纯展示数据。
//
// 职责边界:
// 1. 负责输出 view_type / version / collapsed / expanded 四段展示结构;
// 2. 不依赖父包 ToolExecutionResult避免形成反向 import
// 3. 不改写 ObservationText原始文本由父包原样挂到 expanded.raw_text。
type ContextResultView struct {
ViewType string `json:"view_type"`
Version int `json:"version"`
Collapsed map[string]any `json:"collapsed"`
Expanded map[string]any `json:"expanded"`
}
type MetricField struct {
Label string `json:"label"`
Value string `json:"value"`
}
type KVField struct {
Label string `json:"label"`
Value string `json:"value"`
}
type ItemView struct {
Title string `json:"title"`
Subtitle string `json:"subtitle"`
Tags []string `json:"tags"`
DetailLines []string `json:"detail_lines"`
Meta map[string]any `json:"meta,omitempty"`
}
// ContextToolsAddPayload 对齐 context_tools_add observation 的机器字段。
type ContextToolsAddPayload 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"`
}
// ContextToolsRemovePayload 对齐 context_tools_remove observation 的机器字段。
type ContextToolsRemovePayload 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"`
}
// BuildAddView 生成 context_tools_add 的结构化卡片。
func BuildAddView(payload ContextToolsAddPayload, observation string) ContextResultView {
status := statusFromSuccess(payload.Success)
summary := buildAddSummary(payload)
detailLines := buildAddDetailLines(payload)
item := BuildItem(
fallbackText(ResolveDomainLabelCN(payload.Domain), "工具域"),
summary,
buildAddTags(payload),
detailLines,
map[string]any{
"action": payload.Action,
"domain": strings.TrimSpace(payload.Domain),
"mode": strings.TrimSpace(payload.Mode),
},
)
sections := []map[string]any{
buildContextCalloutSection(
fallbackText(buildAddCalloutTitle(payload), "工具域变更"),
summary,
toneFromSuccess(payload.Success),
detailLines,
),
BuildKVSection("当前工具区参数", []KVField{
BuildKVField("工具域", fallbackText(ResolveDomainLabelCN(payload.Domain), "未指定")),
BuildKVField("工具包", buildAddPackField(payload)),
BuildKVField("注入模式", fallbackText(ResolveModeLabelCN(payload.Mode), "未指定")),
BuildKVField("清空全部", "否"),
BuildKVField("动作", fallbackText(ResolveActionLabelCN(payload.Action), strings.TrimSpace(payload.Action))),
}),
BuildItemsSection("变更摘要", "", []ItemView{item}),
}
if !payload.Success {
appendErrorSection(&sections, payload.Error, payload.ErrorCode)
}
return ContextResultView{
ViewType: ViewTypeContextResult,
Version: ViewVersionContextResult,
Collapsed: map[string]any{
"title": buildAddCollapsedTitle(payload),
"subtitle": summary,
"status": status,
"status_label": resolveStatusLabelCN(status),
"metrics": buildMetrics(
BuildMetric("域", fallbackText(shortDomainLabel(payload.Domain), "未指定")),
BuildMetric("包", fmt.Sprintf("%d 个", len(payload.Packs))),
BuildMetric("模式", fallbackText(ResolveModeLabelCN(payload.Mode), "未指定")),
),
},
Expanded: map[string]any{
"items": []map[string]any{item.Map()},
"sections": sections,
"raw_text": observation,
"machine_payload": buildAddMachinePayload(payload),
},
}
}
// BuildRemoveView 生成 context_tools_remove 的结构化卡片。
func BuildRemoveView(payload ContextToolsRemovePayload, observation string) ContextResultView {
status := statusFromSuccess(payload.Success)
summary := buildRemoveSummary(payload)
detailLines := buildRemoveDetailLines(payload)
item := BuildItem(
buildRemoveItemTitle(payload),
summary,
buildRemoveTags(payload),
detailLines,
map[string]any{
"action": payload.Action,
"domain": strings.TrimSpace(payload.Domain),
"all": payload.All,
},
)
sections := []map[string]any{
buildContextCalloutSection(
fallbackText(buildRemoveCalloutTitle(payload), "工具域变更"),
summary,
toneFromSuccess(payload.Success),
detailLines,
),
BuildKVSection("当前工具区参数", []KVField{
BuildKVField("工具域", buildRemoveDomainField(payload)),
BuildKVField("工具包", buildRemovePackField(payload)),
BuildKVField("注入模式", "不适用"),
BuildKVField("清空全部", formatBoolCN(payload.All)),
BuildKVField("动作", fallbackText(ResolveActionLabelCN(payload.Action), strings.TrimSpace(payload.Action))),
}),
BuildItemsSection("变更摘要", "", []ItemView{item}),
}
if !payload.Success {
appendErrorSection(&sections, payload.Error, payload.ErrorCode)
}
return ContextResultView{
ViewType: ViewTypeContextResult,
Version: ViewVersionContextResult,
Collapsed: map[string]any{
"title": buildRemoveCollapsedTitle(payload),
"subtitle": summary,
"status": status,
"status_label": resolveStatusLabelCN(status),
"metrics": buildMetrics(
BuildMetric("范围", buildRemoveMetricScope(payload)),
BuildMetric("包", fmt.Sprintf("%d 个", len(payload.Packs))),
BuildMetric("动作", fallbackText(ResolveActionLabelCN(payload.Action), strings.TrimSpace(payload.Action))),
),
},
Expanded: map[string]any{
"items": []map[string]any{item.Map()},
"sections": sections,
"raw_text": observation,
"machine_payload": buildRemoveMachinePayload(payload),
},
}
}
func ResolveDomainLabelCN(domain string) string {
switch strings.ToLower(strings.TrimSpace(domain)) {
case "schedule":
return "排程工具域"
case "taskclass":
return "任务类工具域"
default:
return ""
}
}
func ResolvePackLabelCN(pack string) string {
switch strings.ToLower(strings.TrimSpace(pack)) {
case "core":
return "固定 core"
case "mutation":
return "排程改写"
case "analyze":
return "健康分析"
case "detail_read":
return "明细读取"
case "deep_analyze":
return "深度分析"
case "queue":
return "队列微调"
case "web":
return "网页能力"
default:
return strings.TrimSpace(pack)
}
}
func FormatPacksCN(packs []string) string {
if len(packs) == 0 {
return "无"
}
parts := make([]string, 0, len(packs))
for _, pack := range packs {
label := strings.TrimSpace(ResolvePackLabelCN(pack))
if label == "" {
continue
}
parts = append(parts, label)
}
if len(parts) == 0 {
return "无"
}
return strings.Join(parts, "、")
}
func ResolveModeLabelCN(mode string) string {
switch strings.ToLower(strings.TrimSpace(mode)) {
case "replace":
return "替换"
case "merge":
return "合并"
default:
return strings.TrimSpace(mode)
}
}
func ResolveActionLabelCN(action string) string {
switch strings.ToLower(strings.TrimSpace(action)) {
case "activate":
return "激活"
case "deactivate":
return "移除域"
case "deactivate_packs":
return "移除包"
case "clear_all":
return "清空全部"
case "reject":
return "拒绝"
default:
return strings.TrimSpace(action)
}
}
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 BuildItemsSection(title string, summary string, items []ItemView) map[string]any {
normalized := make([]map[string]any, 0, len(items))
for _, item := range items {
normalized = append(normalized, item.Map())
}
return map[string]any{
"type": "items",
"title": strings.TrimSpace(title),
"summary": strings.TrimSpace(summary),
"items": normalized,
}
}
func BuildKVSection(title string, fields []KVField) map[string]any {
normalized := 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
}
normalized = append(normalized, map[string]any{
"label": label,
"value": value,
})
}
return map[string]any{
"type": "kv",
"title": strings.TrimSpace(title),
"fields": normalized,
}
}
func buildContextCalloutSection(title string, summary string, tone string, detailLines []string) map[string]any {
return map[string]any{
"type": "callout",
"title": strings.TrimSpace(title),
"summary": strings.TrimSpace(summary),
"tone": strings.TrimSpace(tone),
"detail_lines": normalizeStringSlice(detailLines),
}
}
func appendErrorSection(target *[]map[string]any, reason string, errorCode string) {
lines := make([]string, 0, 2)
if strings.TrimSpace(reason) != "" {
lines = append(lines, strings.TrimSpace(reason))
}
if strings.TrimSpace(errorCode) != "" {
lines = append(lines, fmt.Sprintf("错误码:%s", strings.TrimSpace(errorCode)))
}
*target = append(*target, buildContextCalloutSection("失败原因", fallbackText(reason, "工具调用失败"), "danger", lines))
}
func buildAddCollapsedTitle(payload ContextToolsAddPayload) string {
if !payload.Success {
return "激活工具域失败"
}
label := ResolveDomainLabelCN(payload.Domain)
if label == "" {
return "已激活工具域"
}
return fmt.Sprintf("已激活%s", label)
}
func buildRemoveCollapsedTitle(payload ContextToolsRemovePayload) string {
if !payload.Success {
return "移除工具域失败"
}
if payload.All {
return "已清空业务工具域"
}
if len(payload.Packs) > 0 {
return "已移除工具包"
}
label := ResolveDomainLabelCN(payload.Domain)
if label == "" {
return "已移除工具域"
}
return fmt.Sprintf("已移除%s", label)
}
func buildAddCalloutTitle(payload ContextToolsAddPayload) string {
if payload.Success {
return "动态工具区已更新"
}
return "激活失败"
}
func buildRemoveCalloutTitle(payload ContextToolsRemovePayload) string {
if payload.Success {
return "动态工具区已更新"
}
return "移除失败"
}
func buildAddSummary(payload ContextToolsAddPayload) string {
if !payload.Success {
return fallbackText(payload.Error, "激活工具域失败")
}
domainLabel := fallbackText(ResolveDomainLabelCN(payload.Domain), "工具域")
packsText := formatPackFieldText(payload.Packs)
modeLabel := fallbackText(ResolveModeLabelCN(payload.Mode), "替换")
if len(payload.Packs) == 0 {
return fmt.Sprintf("%s已激活模式=%s仅保留固定 core。", domainLabel, modeLabel)
}
return fmt.Sprintf("%s已激活模式=%s启用 %s。", domainLabel, modeLabel, packsText)
}
func buildRemoveSummary(payload ContextToolsRemovePayload) string {
if !payload.Success {
return fallbackText(payload.Error, "移除工具域失败")
}
if payload.All {
return "已清空全部业务工具域,仅保留 context 管理工具。"
}
domainLabel := fallbackText(ResolveDomainLabelCN(payload.Domain), "工具域")
if len(payload.Packs) > 0 {
return fmt.Sprintf("已从%s移除 %s。", domainLabel, FormatPacksCN(payload.Packs))
}
return fmt.Sprintf("已移除%s。", domainLabel)
}
func buildAddDetailLines(payload ContextToolsAddPayload) []string {
lines := []string{
fmt.Sprintf("工具域:%s", fallbackText(ResolveDomainLabelCN(payload.Domain), "未指定")),
fmt.Sprintf("工具包:%s", buildAddPackField(payload)),
fmt.Sprintf("注入模式:%s", fallbackText(ResolveModeLabelCN(payload.Mode), "未指定")),
}
if strings.TrimSpace(payload.Message) != "" {
lines = append(lines, strings.TrimSpace(payload.Message))
}
if !payload.Success && strings.TrimSpace(payload.Error) != "" {
lines = append(lines, fmt.Sprintf("失败原因:%s", strings.TrimSpace(payload.Error)))
}
return lines
}
func buildRemoveDetailLines(payload ContextToolsRemovePayload) []string {
lines := []string{
fmt.Sprintf("工具域:%s", buildRemoveDomainField(payload)),
fmt.Sprintf("工具包:%s", buildRemovePackField(payload)),
fmt.Sprintf("清空全部:%s", formatBoolCN(payload.All)),
}
if strings.TrimSpace(payload.Message) != "" {
lines = append(lines, strings.TrimSpace(payload.Message))
}
if !payload.Success && strings.TrimSpace(payload.Error) != "" {
lines = append(lines, fmt.Sprintf("失败原因:%s", strings.TrimSpace(payload.Error)))
}
return lines
}
func buildAddTags(payload ContextToolsAddPayload) []string {
tags := []string{
fallbackText(ResolveActionLabelCN(payload.Action), "激活"),
}
if modeLabel := strings.TrimSpace(ResolveModeLabelCN(payload.Mode)); modeLabel != "" {
tags = append(tags, modeLabel)
}
if len(payload.Packs) > 0 {
tags = append(tags, fmt.Sprintf("%d 个包", len(payload.Packs)))
}
return normalizeStringSlice(tags)
}
func buildRemoveTags(payload ContextToolsRemovePayload) []string {
tags := []string{
fallbackText(ResolveActionLabelCN(payload.Action), "移除"),
}
if payload.All {
tags = append(tags, "全部")
}
if len(payload.Packs) > 0 {
tags = append(tags, fmt.Sprintf("%d 个包", len(payload.Packs)))
}
return normalizeStringSlice(tags)
}
func buildRemoveItemTitle(payload ContextToolsRemovePayload) string {
if payload.All {
return "全部业务工具域"
}
return fallbackText(ResolveDomainLabelCN(payload.Domain), "工具域")
}
func buildRemoveDomainField(payload ContextToolsRemovePayload) string {
if payload.All {
return "全部业务工具域"
}
return fallbackText(ResolveDomainLabelCN(payload.Domain), "未指定")
}
func buildRemoveMetricScope(payload ContextToolsRemovePayload) string {
if payload.All {
return "全部"
}
return fallbackText(shortDomainLabel(payload.Domain), "单域")
}
func buildAddMachinePayload(payload ContextToolsAddPayload) map[string]any {
return map[string]any{
"tool": payload.Tool,
"success": payload.Success,
"action": strings.TrimSpace(payload.Action),
"domain": strings.TrimSpace(payload.Domain),
"packs": append([]string(nil), payload.Packs...),
"mode": strings.TrimSpace(payload.Mode),
"message": strings.TrimSpace(payload.Message),
"error": strings.TrimSpace(payload.Error),
"error_code": strings.TrimSpace(payload.ErrorCode),
}
}
func buildRemoveMachinePayload(payload ContextToolsRemovePayload) map[string]any {
return map[string]any{
"tool": payload.Tool,
"success": payload.Success,
"action": strings.TrimSpace(payload.Action),
"domain": strings.TrimSpace(payload.Domain),
"packs": append([]string(nil), payload.Packs...),
"all": payload.All,
"message": strings.TrimSpace(payload.Message),
"error": strings.TrimSpace(payload.Error),
"error_code": strings.TrimSpace(payload.ErrorCode),
}
}
func buildMetrics(metrics ...MetricField) []map[string]any {
normalized := make([]map[string]any, 0, len(metrics))
for _, metric := range metrics {
label := strings.TrimSpace(metric.Label)
value := strings.TrimSpace(metric.Value)
if label == "" || value == "" {
continue
}
normalized = append(normalized, map[string]any{
"label": label,
"value": value,
})
}
return normalized
}
func toneFromSuccess(success bool) string {
if success {
return "info"
}
return "danger"
}
func statusFromSuccess(success bool) string {
if success {
return StatusDone
}
return StatusFailed
}
func resolveStatusLabelCN(status string) string {
switch strings.ToLower(strings.TrimSpace(status)) {
case StatusDone:
return "已完成"
default:
return "失败"
}
}
func shortDomainLabel(domain string) string {
switch strings.ToLower(strings.TrimSpace(domain)) {
case "schedule":
return "排程"
case "taskclass":
return "任务类"
default:
return strings.TrimSpace(domain)
}
}
func formatPackFieldText(packs []string) string {
if len(packs) == 0 {
return "无"
}
return FormatPacksCN(packs)
}
func buildAddPackField(payload ContextToolsAddPayload) string {
if len(payload.Packs) == 0 {
return "仅固定 core"
}
return FormatPacksCN(payload.Packs)
}
func buildRemovePackField(payload ContextToolsRemovePayload) string {
if len(payload.Packs) == 0 {
if payload.All {
return "全部清空"
}
if strings.EqualFold(strings.TrimSpace(payload.Action), "deactivate") {
return "未指定(按整域处理)"
}
return "无"
}
return FormatPacksCN(payload.Packs)
}
func formatBoolCN(value bool) string {
if value {
return "是"
}
return "否"
}
func fallbackText(text string, fallback string) string {
if strings.TrimSpace(text) == "" {
return strings.TrimSpace(fallback)
}
return strings.TrimSpace(text)
}
func normalizeStringSlice(values []string) []string {
if len(values) == 0 {
return nil
}
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 nil
}
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] = value
}
return out
}
func (view ItemView) Map() map[string]any {
item := map[string]any{
"title": strings.TrimSpace(view.Title),
"subtitle": strings.TrimSpace(view.Subtitle),
"tags": normalizeStringSlice(view.Tags),
"detail_lines": normalizeStringSlice(view.DetailLines),
}
if len(view.Meta) > 0 {
item["meta"] = cloneAnyMap(view.Meta)
}
return item
}