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:
517
backend/newAgent/tools/schedule_analysis/common.go
Normal file
517
backend/newAgent/tools/schedule_analysis/common.go
Normal file
@@ -0,0 +1,517 @@
|
||||
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(§ions, 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)
|
||||
}
|
||||
267
backend/newAgent/tools/schedule_analysis/health.go
Normal file
267
backend/newAgent/tools/schedule_analysis/health.go
Normal file
@@ -0,0 +1,267 @@
|
||||
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)
|
||||
}
|
||||
326
backend/newAgent/tools/schedule_analysis/rhythm.go
Normal file
326
backend/newAgent/tools/schedule_analysis/rhythm.go
Normal file
@@ -0,0 +1,326 @@
|
||||
package schedule_analysis
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BuildAnalyzeRhythmView 把 analyze_rhythm 的原始 JSON observation 转成诊断卡片。
|
||||
//
|
||||
// 步骤化说明:
|
||||
// 1. 只读取现有 metrics / issues / next_actions,不改变 observation JSON;
|
||||
// 2. collapsed 聚焦节律结论和关键指标,expanded 展开问题日、问题清单和建议动作;
|
||||
// 3. detail / hard_categories 等参数只在父包参数区回显,不在这里声明它们已影响算法。
|
||||
func BuildAnalyzeRhythmView(input AnalyzeRhythmViewInput) AnalysisResultView {
|
||||
payload, ok := parseObservationJSON(input.Observation)
|
||||
if !ok || !isSuccessPayload(payload) {
|
||||
return BuildFailureView(BuildFailureViewInput{
|
||||
ToolName: "analyze_rhythm",
|
||||
Observation: input.Observation,
|
||||
ArgFields: input.ArgFields,
|
||||
})
|
||||
}
|
||||
|
||||
metricsMap := readMap(payload, "metrics")
|
||||
overview := readMap(metricsMap, "overview")
|
||||
days := readList(metricsMap, "days")
|
||||
issues := readList(payload, "issues")
|
||||
actions := readList(payload, "next_actions")
|
||||
|
||||
actionItems := buildRhythmActionItems(actions)
|
||||
problemDayItems := buildRhythmProblemDayItems(days)
|
||||
issueItems := buildIssueItems(issues)
|
||||
|
||||
sections := []map[string]any{
|
||||
BuildKVSection("节律概览", buildRhythmOverviewFields(overview)),
|
||||
}
|
||||
if len(problemDayItems) > 0 {
|
||||
sections = append(sections, BuildItemsSection("问题日", problemDayItems))
|
||||
} else {
|
||||
sections = append(sections, BuildCalloutSection("问题日", "当前没有命中的高风险问题日。", "info", nil))
|
||||
}
|
||||
if len(issueItems) > 0 {
|
||||
sections = append(sections, BuildItemsSection("问题清单", issueItems))
|
||||
}
|
||||
if len(actionItems) > 0 {
|
||||
sections = append(sections, BuildItemsSection("建议动作", actionItems))
|
||||
} else {
|
||||
sections = append(sections, BuildCalloutSection("建议动作", "当前节律诊断没有返回候选动作。", "info", nil))
|
||||
}
|
||||
appendSectionIfPresent(§ions, BuildArgsSection("分析参数", input.ArgFields))
|
||||
|
||||
return BuildResultView(BuildResultViewInput{
|
||||
Status: StatusDone,
|
||||
Title: buildRhythmTitle(issues),
|
||||
Subtitle: buildRhythmSubtitle(issues, overview),
|
||||
Metrics: buildRhythmMetrics(overview),
|
||||
Items: actionItems,
|
||||
Sections: sections,
|
||||
Observation: input.Observation,
|
||||
MachinePayload: payload,
|
||||
})
|
||||
}
|
||||
|
||||
func buildRhythmTitle(issues []any) string {
|
||||
severity := highestSeverity(issues)
|
||||
switch severity {
|
||||
case "critical":
|
||||
return "学习节律分析:存在高风险"
|
||||
case "warning":
|
||||
return "学习节律分析:有待微调"
|
||||
default:
|
||||
return "学习节律分析:整体平稳"
|
||||
}
|
||||
}
|
||||
|
||||
func buildRhythmSubtitle(issues []any, overview map[string]any) string {
|
||||
if issue := firstHighPriorityIssue(issues); issue != nil {
|
||||
return describeIssue(issue)
|
||||
}
|
||||
maxDay := readInt(overview, "max_switch_day")
|
||||
maxSwitch := readInt(overview, "max_switch_count")
|
||||
if maxDay > 0 {
|
||||
return fmt.Sprintf("最大切换出现在第 %d 天,共 %d 次。", maxDay, maxSwitch)
|
||||
}
|
||||
return "当前学习节律没有明显异常信号。"
|
||||
}
|
||||
|
||||
func buildRhythmMetrics(overview map[string]any) []MetricField {
|
||||
return []MetricField{
|
||||
BuildMetric("平均切换", fmt.Sprintf("%.1f 次/天", readFloat(overview, "avg_switches_per_day"))),
|
||||
BuildMetric("最大切换", fmt.Sprintf("%d 次", readInt(overview, "max_switch_count"))),
|
||||
BuildMetric("高认知相邻", fmt.Sprintf("%d 天", readInt(overview, "heavy_adjacent_days"))),
|
||||
BuildMetric("块平衡", fmt.Sprintf("%d", readInt(overview, "block_balance"))),
|
||||
BuildMetric("同类型占比", formatPercent(readFloat(overview, "same_type_transition_ratio"))),
|
||||
}
|
||||
}
|
||||
|
||||
func buildRhythmOverviewFields(overview map[string]any) []KVField {
|
||||
return []KVField{
|
||||
BuildKVField("平均每日切换", fmt.Sprintf("%.1f 次", readFloat(overview, "avg_switches_per_day"))),
|
||||
BuildKVField("最大切换日", formatDayIndex(readInt(overview, "max_switch_day"))),
|
||||
BuildKVField("最大切换次数", fmt.Sprintf("%d 次", readInt(overview, "max_switch_count"))),
|
||||
BuildKVField("平均块长度", fmt.Sprintf("%.1f 节", readFloat(overview, "avg_block_size"))),
|
||||
BuildKVField("最长同科连续", fmt.Sprintf("%d 节", readInt(overview, "longest_same_subject_run"))),
|
||||
BuildKVField("高认知相邻", fmt.Sprintf("%d 天", readInt(overview, "heavy_adjacent_days"))),
|
||||
BuildKVField("高强度连续过长", fmt.Sprintf("%d 天", readInt(overview, "long_high_intensity_days"))),
|
||||
BuildKVField("偏碎天数", fmt.Sprintf("%d 天", readInt(overview, "fragmented_count"))),
|
||||
BuildKVField("偏压缩天数", fmt.Sprintf("%d 天", readInt(overview, "compressed_run_count"))),
|
||||
BuildKVField("同类型切换占比", formatPercent(readFloat(overview, "same_type_transition_ratio"))),
|
||||
}
|
||||
}
|
||||
|
||||
func buildRhythmProblemDayItems(days []any) []ItemView {
|
||||
if len(days) == 0 {
|
||||
return make([]ItemView, 0)
|
||||
}
|
||||
items := make([]ItemView, 0)
|
||||
for _, raw := range days {
|
||||
day, ok := raw.(map[string]any)
|
||||
if !ok || !isProblemRhythmDay(day) {
|
||||
continue
|
||||
}
|
||||
dayIndex := readInt(day, "day_index")
|
||||
switchCount := readInt(day, "switch_count")
|
||||
fragmentation := readFloat(day, "fragmentation")
|
||||
maxBlock := readInt(day, "max_block")
|
||||
heavyAdjacent, _ := readBool(day, "heavy_adjacent")
|
||||
tags := []string{}
|
||||
if switchCount >= 3 || fragmentation >= 0.55 {
|
||||
tags = append(tags, "偏碎")
|
||||
}
|
||||
if heavyAdjacent {
|
||||
tags = append(tags, "高认知相邻")
|
||||
}
|
||||
if maxBlock >= 5 {
|
||||
tags = append(tags, "连续块偏长")
|
||||
}
|
||||
detailLines := []string{
|
||||
fmt.Sprintf("切换次数:%d 次", switchCount),
|
||||
"碎片化程度:" + formatFloat(fragmentation),
|
||||
fmt.Sprintf("最长连续块:%d 节", maxBlock),
|
||||
"科目序列:" + formatSequence(readList(day, "sequence")),
|
||||
}
|
||||
items = append(items, BuildItem(
|
||||
formatDayIndex(dayIndex),
|
||||
fmt.Sprintf("切换 %d 次,最长连续 %d 节", switchCount, maxBlock),
|
||||
tags,
|
||||
detailLines,
|
||||
day,
|
||||
))
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func buildRhythmActionItems(actions []any) []ItemView {
|
||||
if len(actions) == 0 {
|
||||
return make([]ItemView, 0)
|
||||
}
|
||||
items := make([]ItemView, 0, len(actions))
|
||||
for _, raw := range actions {
|
||||
action, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
scope := readMap(action, "candidate_scope")
|
||||
title := formatIntentCN(readString(action, "intent_code"))
|
||||
if title == "" {
|
||||
title = fallbackLabel(readString(action, "action_id"), "建议动作")
|
||||
}
|
||||
reads := formatStringList(readList(action, "required_reads"))
|
||||
writes := formatStringList(readList(action, "candidate_write_tools"))
|
||||
tags := []string{
|
||||
fmt.Sprintf("优先级 %d", readInt(action, "priority")),
|
||||
fallbackLabel(writes, "无写工具"),
|
||||
}
|
||||
detailLines := []string{
|
||||
"需要读取:" + fallbackLabel(reads, "无"),
|
||||
"候选写工具:" + fallbackLabel(writes, "无"),
|
||||
"作用范围:" + formatCandidateScope(scope),
|
||||
"成功标准:" + compactJSON(action["success_criteria"]),
|
||||
}
|
||||
items = append(items, BuildItem(
|
||||
title,
|
||||
fmt.Sprintf("先读 %s,再考虑 %s", fallbackLabel(reads, "相关事实"), fallbackLabel(writes, "局部调整")),
|
||||
tags,
|
||||
detailLines,
|
||||
action,
|
||||
))
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func highestSeverity(issues []any) string {
|
||||
best := "info"
|
||||
bestRank := severityRank(best)
|
||||
for _, raw := range issues {
|
||||
issue, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
severity := readString(issue, "severity")
|
||||
if rank := severityRank(severity); rank < bestRank {
|
||||
best = severity
|
||||
bestRank = rank
|
||||
}
|
||||
}
|
||||
return strings.ToLower(strings.TrimSpace(best))
|
||||
}
|
||||
|
||||
func firstHighPriorityIssue(issues []any) map[string]any {
|
||||
var best map[string]any
|
||||
bestRank := 99
|
||||
for _, raw := range issues {
|
||||
issue, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
rank := severityRank(readString(issue, "severity"))
|
||||
if best == nil || rank < bestRank {
|
||||
best = issue
|
||||
bestRank = rank
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
func isProblemRhythmDay(day map[string]any) bool {
|
||||
heavyAdjacent, _ := readBool(day, "heavy_adjacent")
|
||||
return readInt(day, "switch_count") >= 3 ||
|
||||
readFloat(day, "fragmentation") >= 0.55 ||
|
||||
readInt(day, "max_block") >= 5 ||
|
||||
heavyAdjacent
|
||||
}
|
||||
|
||||
func formatDayIndex(day int) string {
|
||||
if day <= 0 {
|
||||
return "未知日期"
|
||||
}
|
||||
return fmt.Sprintf("第 %d 天", day)
|
||||
}
|
||||
|
||||
func formatSequence(rows []any) string {
|
||||
if len(rows) == 0 {
|
||||
return "无"
|
||||
}
|
||||
parts := make([]string, 0, len(rows))
|
||||
for _, raw := range rows {
|
||||
text := strings.TrimSpace(fmt.Sprintf("%v", raw))
|
||||
if text == "" || text == "<nil>" {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, text)
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return "无"
|
||||
}
|
||||
return strings.Join(parts, " -> ")
|
||||
}
|
||||
|
||||
func formatStringList(rows []any) string {
|
||||
if len(rows) == 0 {
|
||||
return ""
|
||||
}
|
||||
parts := make([]string, 0, len(rows))
|
||||
for _, raw := range rows {
|
||||
text := strings.TrimSpace(fmt.Sprintf("%v", raw))
|
||||
if text == "" || text == "<nil>" {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, text)
|
||||
}
|
||||
return strings.Join(parts, "、")
|
||||
}
|
||||
|
||||
func formatCandidateScope(scope map[string]any) string {
|
||||
if len(scope) == 0 {
|
||||
return "未返回"
|
||||
}
|
||||
parts := make([]string, 0, 3)
|
||||
if days := formatNumberList(readList(scope, "day_range"), "第 %d 天"); days != "" {
|
||||
parts = append(parts, "日期:"+days)
|
||||
}
|
||||
if categories := formatStringList(readList(scope, "categories")); categories != "" {
|
||||
parts = append(parts, "类别:"+categories)
|
||||
}
|
||||
if pool := readString(scope, "task_pool"); pool != "" {
|
||||
parts = append(parts, "任务池:"+pool)
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return compactJSON(scope)
|
||||
}
|
||||
return strings.Join(parts, ";")
|
||||
}
|
||||
|
||||
func formatNumberList(rows []any, pattern string) string {
|
||||
if len(rows) == 0 {
|
||||
return ""
|
||||
}
|
||||
parts := make([]string, 0, len(rows))
|
||||
for _, raw := range rows {
|
||||
number := 0
|
||||
switch typed := raw.(type) {
|
||||
case float64:
|
||||
number = int(typed)
|
||||
case int:
|
||||
number = typed
|
||||
default:
|
||||
continue
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf(pattern, number))
|
||||
}
|
||||
return strings.Join(parts, "、")
|
||||
}
|
||||
|
||||
func formatIntentCN(intent string) string {
|
||||
switch strings.TrimSpace(intent) {
|
||||
case "reduce_switch":
|
||||
return "减少同日切换"
|
||||
case "smooth_rhythm":
|
||||
return "平滑高认知相邻"
|
||||
case "prefer_swap":
|
||||
return "优先寻找交换机会"
|
||||
default:
|
||||
return strings.TrimSpace(intent)
|
||||
}
|
||||
}
|
||||
87
backend/newAgent/tools/schedule_analysis/types.go
Normal file
87
backend/newAgent/tools/schedule_analysis/types.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package schedule_analysis
|
||||
|
||||
const (
|
||||
// ViewTypeAnalysisResult 是第三批诊断分析结果卡片的前端识别类型。
|
||||
ViewTypeAnalysisResult = "schedule.analysis_result"
|
||||
|
||||
// ViewVersionAnalysisResult 是当前诊断分析结果结构版本。
|
||||
ViewVersionAnalysisResult = 1
|
||||
|
||||
// 这里不依赖父包状态常量,避免子包反向 import tools 形成循环依赖。
|
||||
StatusDone = "done"
|
||||
StatusFailed = "failed"
|
||||
StatusBlocked = "blocked"
|
||||
)
|
||||
|
||||
// AnalysisResultView 是子包暴露给父包 adapter 的纯展示结构。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责承载 view_type / version / collapsed / expanded 四段展示数据;
|
||||
// 2. 不负责 ToolExecutionResult、SSE、registry 等父包协议;
|
||||
// 3. collapsed / expanded 保持 map 形态,方便父包直接桥接到现有展示协议。
|
||||
type AnalysisResultView struct {
|
||||
ViewType string `json:"view_type"`
|
||||
Version int `json:"version"`
|
||||
Collapsed map[string]any `json:"collapsed"`
|
||||
Expanded map[string]any `json:"expanded"`
|
||||
}
|
||||
|
||||
// KVField 是展开态 kv section 的轻量键值结构。
|
||||
type KVField struct {
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// MetricField 是 collapsed.metrics 的轻量键值结构。
|
||||
type MetricField struct {
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// ItemView 是 expanded.items / section.items 的通用结构。
|
||||
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"`
|
||||
}
|
||||
|
||||
// BuildResultViewInput 是通用 analysis 结果视图 builder 的输入。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责承载已经计算好的标题、副标题、指标、列表、分区;
|
||||
// 2. 不负责执行分析工具,observation 必须由父包 adapter 传入;
|
||||
// 3. observation 会原样写入 raw_text,不能在这里改写给下游消费的 JSON。
|
||||
type BuildResultViewInput struct {
|
||||
Status string
|
||||
Title string
|
||||
Subtitle string
|
||||
Metrics []MetricField
|
||||
Items []ItemView
|
||||
Sections []map[string]any
|
||||
Observation string
|
||||
MachinePayload map[string]any
|
||||
}
|
||||
|
||||
// BuildFailureViewInput 是失败视图 builder 的输入。
|
||||
type BuildFailureViewInput struct {
|
||||
ToolName string
|
||||
Status string
|
||||
Title string
|
||||
Subtitle string
|
||||
Observation string
|
||||
ArgFields []KVField
|
||||
}
|
||||
|
||||
// AnalyzeHealthViewInput 是 analyze_health 视图构造输入。
|
||||
type AnalyzeHealthViewInput struct {
|
||||
Observation string
|
||||
ArgFields []KVField
|
||||
}
|
||||
|
||||
// AnalyzeRhythmViewInput 是 analyze_rhythm 视图构造输入。
|
||||
type AnalyzeRhythmViewInput struct {
|
||||
Observation string
|
||||
ArgFields []KVField
|
||||
}
|
||||
Reference in New Issue
Block a user