Files
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

268 lines
11 KiB
Go
Raw Permalink 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 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(&sections, 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)
}