fix: fallback on invalid ai table output

This commit is contained in:
richjohnrambo
2026-09-29 17:37:57 +08:00
parent 192ab342f6
commit 1bcce3cdde
2 changed files with 167 additions and 3 deletions
+138 -3
View File
@@ -268,6 +268,7 @@ func (s *Service) AnalyzeTable(ctx context.Context, input TableAnalysisInput) (T
if err != nil {
return TableAnalysisResult{}, err
}
usage := usageFromResponse(resp)
var out struct {
Title string `json:"title"`
Summary string `json:"summary"`
@@ -277,18 +278,152 @@ func (s *Service) AnalyzeTable(ctx context.Context, input TableAnalysisInput) (T
Interpretation string `json:"interpretation"`
}
if err := json.Unmarshal([]byte(ExtractJSONObject(resp.Content)), &out); err != nil {
return TableAnalysisResult{}, fmt.Errorf("%w: %v", ErrBadOutput, err)
return fallbackTableAnalysis(input, usage, "AI 输出格式异常,已使用兜底解读。"), nil
}
out.Title = firstNonEmpty(out.Title, input.Topic, input.FileName, "智能解读报告")
if strings.TrimSpace(out.Interpretation) == "" {
return TableAnalysisResult{}, ErrBadOutput
return fallbackTableAnalysis(input, usage, "AI 输出缺少核心解读,已使用兜底解读。"), nil
}
return TableAnalysisResult{
Title: out.Title, Metrics: out.Metrics, Preview: out.Preview, Alerts: out.Alerts,
Interpretation: strings.TrimSpace(out.Interpretation), Summary: strings.TrimSpace(out.Summary), Usage: usageFromResponse(resp),
Interpretation: strings.TrimSpace(out.Interpretation), Summary: strings.TrimSpace(out.Summary), Usage: usage,
}, nil
}
func fallbackTableAnalysis(input TableAnalysisInput, usage Usage, reason string) TableAnalysisResult {
title := firstNonEmpty(input.Topic, input.FileName, "智能表格解读报告")
preview := fallbackTablePreview(input.Profile)
metrics := fallbackTableMetrics(input, preview)
alerts := []TableAlert{{
Level: "warning",
Title: "已启用兜底解读",
Description: reason,
}}
if !input.Profile.Parsed {
alerts = append(alerts, TableAlert{
Level: "warning",
Title: "表格内容未解析",
Description: firstNonEmpty(input.Profile.Message, "当前无法读取表格内容,仅基于文件元信息生成解读。"),
})
}
interpretation := fallbackTableInterpretation(input, preview, reason)
return TableAnalysisResult{
Title: title,
Summary: excerptPlain(interpretation, 120),
Metrics: metrics,
Preview: preview,
Alerts: alerts,
Interpretation: interpretation,
Usage: usage,
}
}
func fallbackTablePreview(profile TableProfile) TablePreview {
var columns []struct {
Name string `json:"name"`
Type string `json:"type"`
}
_ = decodeProfileAny(profile.Columns, &columns)
names := make([]string, 0, len(columns))
for _, column := range columns {
if name := strings.TrimSpace(column.Name); name != "" {
names = append(names, name)
}
}
var rows [][]string
_ = decodeProfileAny(profile.SampleRows, &rows)
if len(rows) > 5 {
rows = rows[:5]
}
return TablePreview{Columns: names, Rows: rows, TotalRows: profile.RowCount}
}
func fallbackTableMetrics(input TableAnalysisInput, preview TablePreview) []TableMetric {
metrics := []TableMetric{
{Name: "文件类型", Value: strings.ToUpper(input.FileType), ChangeText: "来自上传元信息", Trend: "flat"},
{Name: "文件大小", Value: fmt.Sprintf("%d bytes", input.FileSize), ChangeText: "来自上传元信息", Trend: "flat"},
}
if input.Profile.Parsed {
metrics = append(metrics,
TableMetric{Name: "数据行数", Value: fmt.Sprintf("%d", preview.TotalRows), ChangeText: "来自表格解析", Trend: "flat"},
TableMetric{Name: "字段数", Value: fmt.Sprintf("%d", len(preview.Columns)), ChangeText: "来自表格解析", Trend: "flat"},
)
} else {
metrics = append(metrics, TableMetric{Name: "解析状态", Value: "未解析", ChangeText: firstNonEmpty(input.Profile.Message, "source file is not available locally"), Trend: "flat"})
}
var stats map[string]struct {
Count int `json:"count"`
Sum float64 `json:"sum"`
Avg float64 `json:"avg"`
Min float64 `json:"min"`
Max float64 `json:"max"`
}
if err := decodeProfileAny(input.Profile.Stats, &stats); err == nil {
for name, stat := range stats {
if len(metrics) >= 6 {
break
}
metrics = append(metrics, TableMetric{
Name: name,
Value: fmt.Sprintf("平均 %.2f", stat.Avg),
ChangeText: fmt.Sprintf("样本 %d,范围 %.2f-%.2f", stat.Count, stat.Min, stat.Max),
Trend: "flat",
})
}
}
return metrics
}
func fallbackTableInterpretation(input TableAnalysisInput, preview TablePreview, reason string) string {
var b strings.Builder
b.WriteString(reason)
b.WriteString("\n\n")
b.WriteString("文件《")
b.WriteString(firstNonEmpty(input.FileName, input.FileID, "未命名文件"))
b.WriteString("》已进入智能表格解读流程。")
if input.Profile.Parsed {
b.WriteString(fmt.Sprintf("当前已解析到 %d 行数据、%d 个字段。", preview.TotalRows, len(preview.Columns)))
if len(preview.Columns) > 0 {
b.WriteString("字段包括:")
b.WriteString(strings.Join(preview.Columns, "、"))
b.WriteString("。")
}
b.WriteString("建议结合字段含义继续查看数值分布、异常值和业务口径。")
} else {
b.WriteString("当前未能读取表格内容,报告仅基于文件名、类型和大小生成。")
if msg := strings.TrimSpace(input.Profile.Message); msg != "" {
b.WriteString("未解析原因:")
b.WriteString(msg)
b.WriteString("。")
}
b.WriteString("建议确认文件已上传完成、格式正确后重新发起解读。")
}
return b.String()
}
func decodeProfileAny(value any, target any) error {
if value == nil {
return nil
}
data, err := json.Marshal(value)
if err != nil {
return err
}
return json.Unmarshal(data, target)
}
func excerptPlain(value string, maxRunes int) string {
value = strings.TrimSpace(value)
if maxRunes <= 0 {
return ""
}
runes := []rune(value)
if len(runes) <= maxRunes {
return value
}
return string(runes[:maxRunes])
}
func (s *Service) SummarizeMeeting(ctx context.Context, input MeetingSummaryInput) (MeetingSummaryResult, error) {
if s == nil || s.client == nil {
return MeetingSummaryResult{}, ErrNotConfigured