后端: 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. 更新工具结果结构化交接文档,补记第四批切流范围、当前切流点与后续收尾建议。
268 lines
11 KiB
Go
268 lines
11 KiB
Go
package schedule_analysis
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
)
|
||
|
||
// BuildAnalyzeHealthView 把 analyze_health 的原始 JSON observation 转成诊断卡片。
|
||
//
|
||
// 步骤化说明:
|
||
// 1. 只解析 observation 的现有 JSON 字段,不改变字段名、层级或内容;
|
||
// 2. 展示层优先读取 feasibility / decision / metrics,避免依赖自然语言摘要;
|
||
// 3. 解析失败或 success=false 时返回失败卡片,raw_text 仍保留原始 observation。
|
||
func BuildAnalyzeHealthView(input AnalyzeHealthViewInput) AnalysisResultView {
|
||
payload, ok := parseObservationJSON(input.Observation)
|
||
if !ok || !isSuccessPayload(payload) {
|
||
return BuildFailureView(BuildFailureViewInput{
|
||
ToolName: "analyze_health",
|
||
Observation: input.Observation,
|
||
ArgFields: input.ArgFields,
|
||
})
|
||
}
|
||
|
||
metricsMap := readMap(payload, "metrics")
|
||
rhythm := readMap(metricsMap, "rhythm")
|
||
tightness := readMap(metricsMap, "tightness")
|
||
profile := readMap(metricsMap, "profile")
|
||
feasibility := readMap(payload, "feasibility")
|
||
decision := readMap(payload, "decision")
|
||
|
||
title := buildHealthTitle(feasibility, decision)
|
||
subtitle := buildHealthSubtitle(feasibility, decision)
|
||
metrics := buildHealthMetrics(rhythm, tightness, profile, feasibility)
|
||
candidateItems := buildHealthCandidateItems(decision)
|
||
issueItems := buildIssueItems(readList(payload, "issues"))
|
||
|
||
sections := []map[string]any{
|
||
BuildKVSection("裁决结论", buildHealthDecisionFields(feasibility, decision, metricsMap)),
|
||
BuildKVSection("关键指标", buildHealthMetricFields(rhythm, tightness, profile, metricsMap)),
|
||
}
|
||
if len(issueItems) > 0 {
|
||
sections = append(sections, BuildItemsSection("问题清单", issueItems))
|
||
} else {
|
||
sections = append(sections, BuildCalloutSection("问题清单", "当前没有结构化问题项。", "info", nil))
|
||
}
|
||
if len(candidateItems) > 0 {
|
||
sections = append(sections, BuildItemsSection("候选操作", candidateItems))
|
||
} else {
|
||
sections = append(sections, BuildCalloutSection("候选操作", "当前没有可执行候选。", "info", nil))
|
||
}
|
||
sections = append(sections, buildHealthNextStepSection(feasibility, decision, candidateItems))
|
||
appendSectionIfPresent(§ions, BuildArgsSection("分析参数", input.ArgFields))
|
||
|
||
return BuildResultView(BuildResultViewInput{
|
||
Status: StatusDone,
|
||
Title: title,
|
||
Subtitle: subtitle,
|
||
Metrics: metrics,
|
||
Items: candidateItems,
|
||
Sections: sections,
|
||
Observation: input.Observation,
|
||
MachinePayload: payload,
|
||
})
|
||
}
|
||
|
||
func buildHealthTitle(feasibility map[string]any, decision map[string]any) string {
|
||
if feasible, ok := readBool(feasibility, "is_feasible"); ok && !feasible {
|
||
return "综合体检:当前约束不可行"
|
||
}
|
||
if shouldContinue, ok := readBool(decision, "should_continue_optimize"); ok && shouldContinue {
|
||
return "综合体检:建议继续微调"
|
||
}
|
||
return "综合体检:可以收口"
|
||
}
|
||
|
||
func buildHealthSubtitle(feasibility map[string]any, decision map[string]any) string {
|
||
if feasible, ok := readBool(feasibility, "is_feasible"); ok && !feasible {
|
||
gap := readInt(feasibility, "capacity_gap")
|
||
reason := readString(feasibility, "reason_code")
|
||
if reason == "" {
|
||
reason = "capacity_insufficient"
|
||
}
|
||
return fmt.Sprintf("容量仍缺 %d 节,原因:%s。", gap, reason)
|
||
}
|
||
if problem := readString(decision, "primary_problem"); problem != "" {
|
||
return problem
|
||
}
|
||
return "当前没有发现需要继续处理的结构化问题。"
|
||
}
|
||
|
||
func buildHealthMetrics(rhythm, tightness, profile, feasibility map[string]any) []MetricField {
|
||
metrics := []MetricField{
|
||
BuildMetric("高认知相邻", fmt.Sprintf("%d 天", readInt(rhythm, "heavy_adjacent_days"))),
|
||
BuildMetric("最大切换", fmt.Sprintf("%d 次", readInt(rhythm, "max_switch_count"))),
|
||
BuildMetric("可局部移动", fmt.Sprintf("%d 项", readInt(tightness, "locally_movable_task_count"))),
|
||
BuildMetric("紧度", fallbackLabel(readString(tightness, "tightness_level"), "未标注")),
|
||
}
|
||
if gap := readInt(feasibility, "capacity_gap"); gap > 0 {
|
||
metrics = append(metrics, BuildMetric("容量缺口", fmt.Sprintf("%d 节", gap)))
|
||
return metrics
|
||
}
|
||
if missing := readInt(profile, "missing_complete_profile_count"); missing > 0 {
|
||
metrics = append(metrics, BuildMetric("画像缺失", fmt.Sprintf("%d 门", missing)))
|
||
}
|
||
return metrics
|
||
}
|
||
|
||
func buildHealthDecisionFields(feasibility map[string]any, decision map[string]any, metrics map[string]any) []KVField {
|
||
shouldContinue, _ := readBool(decision, "should_continue_optimize")
|
||
forced, _ := readBool(decision, "is_forced_imperfection")
|
||
canClose, _ := readBool(metrics, "can_close")
|
||
feasible, feasibleOK := readBool(feasibility, "is_feasible")
|
||
feasibleText := "未返回"
|
||
if feasibleOK {
|
||
feasibleText = formatBoolCN(feasible)
|
||
}
|
||
|
||
return []KVField{
|
||
BuildKVField("是否继续优化", formatBoolCN(shouldContinue)),
|
||
BuildKVField("当前可收口", formatBoolCN(canClose)),
|
||
BuildKVField("推荐动作", formatOperationCN(readString(decision, "recommended_operation"))),
|
||
BuildKVField("主问题", fallbackLabel(readString(decision, "primary_problem"), "当前没有发现值得继续处理的局部认知问题")),
|
||
BuildKVField("约束代价", formatBoolCN(forced)),
|
||
BuildKVField("约束可行", feasibleText),
|
||
BuildKVField("容量缺口", fmt.Sprintf("%d 节", readInt(feasibility, "capacity_gap"))),
|
||
BuildKVField("可行性原因", fallbackLabel(readString(feasibility, "reason_code"), "未返回")),
|
||
}
|
||
}
|
||
|
||
func buildHealthMetricFields(rhythm, tightness, profile, metrics map[string]any) []KVField {
|
||
canClose, _ := readBool(metrics, "can_close")
|
||
return []KVField{
|
||
BuildKVField("认知块平衡", fmt.Sprintf("%d", readInt(rhythm, "block_balance"))),
|
||
BuildKVField("偏碎天数", fmt.Sprintf("%d 天", readInt(rhythm, "fragmented_count"))),
|
||
BuildKVField("偏压缩天数", fmt.Sprintf("%d 天", readInt(rhythm, "compressed_run_count"))),
|
||
BuildKVField("平均每日切换", fmt.Sprintf("%.1f 次", readFloat(rhythm, "avg_switches_per_day"))),
|
||
BuildKVField("同类型切换占比", formatPercent(readFloat(rhythm, "same_type_transition_ratio"))),
|
||
BuildKVField("局部候选均值", fmt.Sprintf("%.1f 个", readFloat(tightness, "avg_local_alternative_slots"))),
|
||
BuildKVField("跨任务类交换机会", fmt.Sprintf("%d 个", readInt(tightness, "cross_class_swap_options"))),
|
||
BuildKVField("被迫高认知相邻", fmt.Sprintf("%d 天", readInt(tightness, "forced_heavy_adjacent_days"))),
|
||
BuildKVField("语义画像缺失", fmt.Sprintf("%d 门", readInt(profile, "missing_complete_profile_count"))),
|
||
BuildKVField("当前可收口", formatBoolCN(canClose)),
|
||
}
|
||
}
|
||
|
||
func buildHealthCandidateItems(decision map[string]any) []ItemView {
|
||
candidates := readList(decision, "candidates")
|
||
if len(candidates) == 0 {
|
||
return make([]ItemView, 0)
|
||
}
|
||
items := make([]ItemView, 0, len(candidates))
|
||
for _, raw := range candidates {
|
||
candidate, ok := raw.(map[string]any)
|
||
if !ok {
|
||
continue
|
||
}
|
||
after := readMap(candidate, "after")
|
||
canClose, _ := readBool(after, "can_close")
|
||
tool := readString(candidate, "tool")
|
||
effect := readString(candidate, "effect")
|
||
title := readString(candidate, "summary")
|
||
if title == "" {
|
||
title = fallbackLabel(readString(candidate, "candidate_id"), "候选操作")
|
||
}
|
||
subtitle := readString(after, "primary_problem")
|
||
if subtitle == "" {
|
||
subtitle = fmt.Sprintf("效果:%s", formatEffectCN(effect))
|
||
}
|
||
|
||
tags := []string{formatOperationCN(tool), formatEffectCN(effect)}
|
||
if canClose {
|
||
tags = append(tags, "执行后可收口")
|
||
}
|
||
detailLines := []string{
|
||
"候选 ID:" + fallbackLabel(readString(candidate, "candidate_id"), "未返回"),
|
||
"参数:" + compactJSON(candidate["arguments"]),
|
||
fmt.Sprintf("执行后高认知相邻:%d 天", readInt(after, "heavy_adjacent_days")),
|
||
fmt.Sprintf("执行后最大切换:%d 次", readInt(after, "max_switch_count")),
|
||
"执行后同类型切换占比:" + formatPercent(readFloat(after, "same_type_transition_ratio")),
|
||
}
|
||
items = append(items, BuildItem(title, subtitle, tags, detailLines, candidate))
|
||
}
|
||
return items
|
||
}
|
||
|
||
func buildIssueItems(rows []any) []ItemView {
|
||
if len(rows) == 0 {
|
||
return make([]ItemView, 0)
|
||
}
|
||
items := make([]ItemView, 0, len(rows))
|
||
for _, raw := range rows {
|
||
issue, ok := raw.(map[string]any)
|
||
if !ok {
|
||
continue
|
||
}
|
||
trigger := readMap(issue, "trigger")
|
||
severity := readString(issue, "severity")
|
||
dimension := readString(issue, "dimension")
|
||
title := describeIssue(issue)
|
||
detailLines := make([]string, 0, 3)
|
||
if metric := readString(trigger, "metric"); metric != "" {
|
||
detailLines = append(detailLines, fmt.Sprintf("触发指标:%s %s %.2f,实际 %.2f", metric, readString(trigger, "operator"), readFloat(trigger, "threshold"), readFloat(trigger, "actual")))
|
||
}
|
||
detailLines = append(detailLines, "问题 ID:"+fallbackLabel(readString(issue, "issue_id"), "未返回"))
|
||
items = append(items, BuildItem(
|
||
title,
|
||
fmt.Sprintf("%s,%s", fallbackLabel(dimension, "未标注维度"), formatSeverityCN(severity)),
|
||
[]string{formatSeverityCN(severity), fallbackLabel(dimension, "未标注维度")},
|
||
detailLines,
|
||
issue,
|
||
))
|
||
}
|
||
return items
|
||
}
|
||
|
||
func buildHealthNextStepSection(feasibility map[string]any, decision map[string]any, candidateItems []ItemView) map[string]any {
|
||
if feasible, ok := readBool(feasibility, "is_feasible"); ok && !feasible {
|
||
return BuildCalloutSection(
|
||
"建议后续动作",
|
||
"当前先不要继续写操作,应先与用户协商时间窗、约束或任务范围。",
|
||
"warning",
|
||
[]string{"可选方向:扩展时间窗、放宽排除约束、缩减任务量,或确认接受风险收口。"},
|
||
)
|
||
}
|
||
if shouldContinue, ok := readBool(decision, "should_continue_optimize"); ok && shouldContinue {
|
||
return BuildCalloutSection(
|
||
"建议后续动作",
|
||
"优先从候选操作里选择收益明确的一项执行。",
|
||
"info",
|
||
[]string{fmt.Sprintf("当前共有 %d 个候选项;执行后建议再次调用 analyze_health 复诊。", len(candidateItems))},
|
||
)
|
||
}
|
||
return BuildCalloutSection(
|
||
"建议后续动作",
|
||
"当前可以收口;如用户仍要求微调,再按具体偏好追加读取或局部调整。",
|
||
"info",
|
||
nil,
|
||
)
|
||
}
|
||
|
||
func describeIssue(issue map[string]any) string {
|
||
issueID := readString(issue, "issue_id")
|
||
dimension := readString(issue, "dimension")
|
||
switch {
|
||
case strings.Contains(issueID, "feasibility"):
|
||
return "容量可行性不足"
|
||
case strings.Contains(issueID, "semantic_profile"):
|
||
return "任务类语义画像不完整"
|
||
case strings.Contains(issueID, "heavy_adjacent"):
|
||
return "存在高认知任务相邻"
|
||
case strings.Contains(issueID, "switch"):
|
||
return "单日任务切换偏多"
|
||
case strings.Contains(issueID, "long_block"):
|
||
return "同类任务连续块偏长"
|
||
case strings.Contains(issueID, "info"):
|
||
return "节奏整体提示"
|
||
default:
|
||
return fallbackLabel(dimension, "诊断问题")
|
||
}
|
||
}
|
||
|
||
func fallbackLabel(value string, fallback string) string {
|
||
if strings.TrimSpace(value) == "" {
|
||
return strings.TrimSpace(fallback)
|
||
}
|
||
return strings.TrimSpace(value)
|
||
}
|