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. 更新工具结果结构化交接文档,补记第四批切流范围、当前切流点与后续收尾建议。
This commit is contained in:
Losita
2026-04-28 20:22:22 +08:00
parent 1a5b2ecd73
commit d89e2830a9
38 changed files with 9180 additions and 1577 deletions

View File

@@ -0,0 +1,653 @@
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
}