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

518 lines
12 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 (
"encoding/json"
"fmt"
"sort"
"strings"
)
// BuildResultView 统一封装 schedule.analysis_result 结构。
//
// 职责边界:
// 1. 负责把已经计算好的折叠态/展开态内容组装成标准视图;
// 2. 负责在子包内补齐 status / status_label避免依赖父包常量
// 3. 不负责 ToolExecutionResult 外层协议,也不改写 observation 原文。
func BuildResultView(input BuildResultViewInput) AnalysisResultView {
status := normalizeStatus(input.Status)
if status == "" {
status = StatusDone
}
collapsed := map[string]any{
"title": strings.TrimSpace(input.Title),
"subtitle": strings.TrimSpace(input.Subtitle),
"status": status,
"status_label": resolveStatusLabelCN(status),
"metrics": metricListToMaps(input.Metrics),
}
expanded := map[string]any{
"items": itemListToMaps(input.Items),
"sections": cloneSectionList(input.Sections),
"raw_text": input.Observation,
}
if len(input.MachinePayload) > 0 {
expanded["machine_payload"] = cloneAnyMap(input.MachinePayload)
}
return AnalysisResultView{
ViewType: ViewTypeAnalysisResult,
Version: ViewVersionAnalysisResult,
Collapsed: collapsed,
Expanded: expanded,
}
}
// BuildFailureView 统一生成 analysis 工具失败卡片视图。
//
// 职责边界:
// 1. 只从 observation 中提炼失败文案和参数回显;
// 2. 不负责判断失败条件,调用方需要先确认 observation 失败;
// 3. raw_text 仍保留原始 observation方便 debug 与下游排查。
func BuildFailureView(input BuildFailureViewInput) AnalysisResultView {
status := normalizeStatus(input.Status)
if status == "" {
status = StatusFailed
}
title := strings.TrimSpace(input.Title)
if title == "" {
title = fmt.Sprintf("%s失败", resolveToolLabelCN(input.ToolName))
}
subtitle := strings.TrimSpace(input.Subtitle)
if subtitle == "" {
subtitle = failureText(input.Observation, "诊断分析失败,请检查当前日程状态后重试。")
}
sections := []map[string]any{
BuildCalloutSection("执行失败", subtitle, "danger", []string{subtitle}),
}
appendSectionIfPresent(&sections, BuildArgsSection("分析参数", input.ArgFields))
return BuildResultView(BuildResultViewInput{
Status: status,
Title: title,
Subtitle: subtitle,
Sections: sections,
Observation: input.Observation,
})
}
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, items []ItemView) map[string]any {
return map[string]any{
"type": "items",
"title": strings.TrimSpace(title),
"items": itemListToMaps(items),
}
}
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 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),
}
}
// BuildArgsSection 把父包已经本地化的参数字段拼成展示 section。
//
// 职责边界:
// 1. 只接受纯 KVField不依赖父包 ToolArgumentView
// 2. 不解释 detail / threshold / hard_categories 是否真实参与计算;
// 3. 没有有效字段时返回 nil避免空 section 干扰前端。
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 {
if strings.TrimSpace(field.Label) == "" || strings.TrimSpace(field.Value) == "" {
continue
}
valid = append(valid, BuildKVField(field.Label, field.Value))
}
if len(valid) == 0 {
return nil
}
return BuildKVSection(title, valid)
}
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 isSuccessPayload(payload map[string]any) bool {
success, ok := readBool(payload, "success")
return ok && success
}
func failureText(observation string, fallback string) string {
if payload, ok := parseObservationJSON(observation); ok {
if message := firstString(payload, "error", "message", "reason", "err"); message != "" {
return message
}
}
if strings.TrimSpace(observation) != "" {
return strings.TrimSpace(observation)
}
return strings.TrimSpace(fallback)
}
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 resolveToolLabelCN(toolName string) string {
switch strings.TrimSpace(toolName) {
case "analyze_health":
return "综合体检"
case "analyze_rhythm":
return "学习节律分析"
default:
return "诊断分析"
}
}
func appendSectionIfPresent(target *[]map[string]any, section map[string]any) {
if section == nil {
return
}
*target = append(*target, section)
}
func metricListToMaps(metrics []MetricField) []map[string]any {
if len(metrics) == 0 {
return make([]map[string]any, 0)
}
out := 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
}
out = append(out, map[string]any{"label": label, "value": value})
}
if len(out) == 0 {
return make([]map[string]any, 0)
}
return out
}
func itemListToMaps(items []ItemView) []map[string]any {
if len(items) == 0 {
return make([]map[string]any, 0)
}
out := make([]map[string]any, 0, len(items))
for _, item := range items {
row := map[string]any{
"title": strings.TrimSpace(item.Title),
"subtitle": strings.TrimSpace(item.Subtitle),
"tags": normalizeStringSlice(item.Tags),
"detail_lines": normalizeStringSlice(item.DetailLines),
}
if len(item.Meta) > 0 {
row["meta"] = cloneAnyMap(item.Meta)
}
out = append(out, row)
}
return out
}
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 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 []any:
out := make([]any, 0, len(typed))
for _, item := range typed {
out = append(out, cloneAnyValue(item))
}
return out
case []map[string]any:
out := make([]map[string]any, 0, len(typed))
for _, item := range typed {
out = append(out, cloneAnyMap(item))
}
return out
case []string:
out := make([]string, len(typed))
copy(out, typed)
return out
default:
return typed
}
}
func readMap(input map[string]any, key string) map[string]any {
if len(input) == 0 {
return nil
}
value, ok := input[key]
if !ok {
return nil
}
row, _ := value.(map[string]any)
return row
}
func readList(input map[string]any, key string) []any {
if len(input) == 0 {
return nil
}
value, ok := input[key]
if !ok {
return nil
}
rows, _ := value.([]any)
return rows
}
func readString(input map[string]any, key string) string {
if len(input) == 0 {
return ""
}
value, ok := input[key]
if !ok || value == nil {
return ""
}
switch typed := value.(type) {
case string:
return strings.TrimSpace(typed)
default:
text := strings.TrimSpace(fmt.Sprintf("%v", typed))
if text == "<nil>" {
return ""
}
return text
}
}
func firstString(input map[string]any, keys ...string) string {
for _, key := range keys {
if value := readString(input, key); value != "" {
return value
}
}
return ""
}
func readBool(input map[string]any, key string) (bool, bool) {
if len(input) == 0 {
return false, false
}
value, ok := input[key]
if !ok {
return false, false
}
typed, ok := value.(bool)
return typed, ok
}
func readInt(input map[string]any, key string) int {
value := readFloat(input, key)
return int(value)
}
func readFloat(input map[string]any, key string) float64 {
if len(input) == 0 {
return 0
}
value, ok := input[key]
if !ok || value == nil {
return 0
}
switch typed := value.(type) {
case float64:
return typed
case float32:
return float64(typed)
case int:
return float64(typed)
case int64:
return float64(typed)
default:
return 0
}
}
func severityRank(severity string) int {
switch strings.ToLower(strings.TrimSpace(severity)) {
case "critical":
return 0
case "warning":
return 1
default:
return 2
}
}
func formatSeverityCN(severity string) string {
switch strings.ToLower(strings.TrimSpace(severity)) {
case "critical":
return "高风险"
case "warning":
return "需关注"
default:
return "提示"
}
}
func formatBoolCN(value bool) string {
if value {
return "是"
}
return "否"
}
func formatFloat(value float64) string {
return fmt.Sprintf("%.2f", value)
}
func formatPercent(value float64) string {
return fmt.Sprintf("%.0f%%", value*100)
}
func formatOperationCN(operation string) string {
switch strings.TrimSpace(operation) {
case "move":
return "移动"
case "swap":
return "交换"
case "close":
return "收口"
case "ask_user":
return "询问用户"
default:
if strings.TrimSpace(operation) == "" {
return "未指定"
}
return strings.TrimSpace(operation)
}
}
func formatEffectCN(effect string) string {
switch strings.TrimSpace(effect) {
case "improve":
return "明显改善"
case "partial_improve":
return "部分改善"
case "shift":
return "问题转移"
case "no_gain":
return "收益不足"
case "regress":
return "变差"
case "close":
return "收口"
default:
if strings.TrimSpace(effect) == "" {
return "未标注"
}
return strings.TrimSpace(effect)
}
}
func sortedKeys(input map[string]any) []string {
keys := make([]string, 0, len(input))
for key := range input {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
func compactJSON(value any) string {
raw, err := json.Marshal(value)
if err != nil {
return fmt.Sprintf("%v", value)
}
return string(raw)
}