diff --git a/internal/ai/deck_builder.go b/internal/ai/deck_builder.go index 7e66978..b4bdc33 100644 --- a/internal/ai/deck_builder.go +++ b/internal/ai/deck_builder.go @@ -147,6 +147,8 @@ func isTransientAICommandError(err error, stdout, stderr string) bool { "elocked", "enotacquired", "temporarily unavailable", + "internal_error", + "stream error", "timeout", "timed out", "connection reset", diff --git a/internal/ai/deck_builder_test.go b/internal/ai/deck_builder_test.go index 8867bb0..c2897d7 100644 --- a/internal/ai/deck_builder_test.go +++ b/internal/ai/deck_builder_test.go @@ -520,6 +520,27 @@ func TestBuildDeckJSONRetriesTransientAILockError(t *testing.T) { } } +func TestBuildDeckJSONRetriesInternalStreamError(t *testing.T) { + runner := &sequenceRunner{calls: []runnerCall{ + {stderr: "API Error: stream error: stream ID 1; INTERNAL_ERROR; received from peer", err: errors.New("exit status 1")}, + {stdout: `{"pages":[]}`}, + }} + b := Builder{Runner: runner} + b.SetCommand("ccs", []string{"codex"}) + b.SetRetryDelays([]time.Duration{0}) + + got, err := b.BuildDeckJSON("# title") + if err != nil { + t.Fatalf("BuildDeckJSON() error = %v", err) + } + if got != `{"pages":[]}` { + t.Fatalf("BuildDeckJSON() = %q, want JSON from retry", got) + } + if runner.count != 2 { + t.Fatalf("runner calls = %d, want 2", runner.count) + } +} + func TestBuildDeckJSONUsesConfiguredRetryDelays(t *testing.T) { runner := &sequenceRunner{calls: []runnerCall{ {stderr: "timeout", err: errors.New("exit status 1")}, diff --git a/internal/app/card_manifest_deck.go b/internal/app/card_manifest_deck.go index d0c7b9d..b2b7949 100644 --- a/internal/app/card_manifest_deck.go +++ b/internal/app/card_manifest_deck.go @@ -130,7 +130,7 @@ func cardManifestItemPage(item cardManifestItem, pageNumber int, pageCount int) if imageSrc != "" { variant = "image-caption" } - body := cardManifestItemBody(item, cardManifestBodyMaxRunes(variant)) + body := cardManifestItemBody(item, cardManifestBodyMaxRunes(variant), variant == "image-caption") if body == "" { body = title } @@ -208,12 +208,14 @@ func cardManifestBodyMaxRunes(variant string) int { return cardManifestTextCaptionBodyMaxRunes } -func cardManifestItemBody(item cardManifestItem, maxRunes int) string { +func cardManifestItemBody(item cardManifestItem, maxRunes int, includeSource bool) string { if body := cardManifestSectionsBody(item.Sections, maxRunes); body != "" { return body } - if source := cardManifestItemSourceText(item); source != "" { - return cardManifestFitSections(cardManifestNewsBodySections(item, source), maxRunes) + if includeSource { + if source := cardManifestItemSourceText(item); source != "" { + return cardManifestFitSections(cardManifestNewsBodySections(item, source), maxRunes) + } } summary, impact := cardManifestFitSummaryImpact(item.Summary, item.Impact, maxRunes) parts := make([]string, 0, 2) diff --git a/internal/app/service_test.go b/internal/app/service_test.go index 102cb12..25f08cc 100644 --- a/internal/app/service_test.go +++ b/internal/app/service_test.go @@ -279,6 +279,9 @@ func TestServiceGeneratePreviewBuildsCardManifestDeckWithoutAI(t *testing.T) { if textPage.Variant != "text-caption" || textPage.Content.Title != "AI 眼镜更新" { t.Fatalf("text page = %#v", textPage) } + if textPage.Content.Body != "**摘要:** 新品强调续航。\n\n**影响:** 穿戴设备竞争加剧。" { + t.Fatalf("text page body = %q", textPage.Content.Body) + } if textPage.Meta.CTA != "来源:Bloomberg / 2026-06-16 12:30" { t.Fatalf("text page cta = %q", textPage.Meta.CTA) } @@ -601,8 +604,11 @@ func TestServiceGeneratePreviewFitsCardManifestTextForNewsCards(t *testing.T) { if utf8.RuneCountInString(textBody) > cardManifestTextCaptionBodyMaxRunes { t.Fatalf("text-caption body runes = %d, want <= %d", utf8.RuneCountInString(textBody), cardManifestTextCaptionBodyMaxRunes) } - if !strings.Contains(textBody, "摘要:") || !strings.Contains(textBody, "影响:") || !strings.Contains(textBody, "来源:") || !strings.Contains(textBody, "Hacker News") || !strings.Contains(textBody, "…") { - t.Fatalf("text-caption body should preserve labels, source, and ellipsis: %q", textBody) + if !strings.Contains(textBody, "摘要:") || !strings.Contains(textBody, "影响:") || strings.Contains(textBody, "来源:") || strings.Contains(textBody, "Hacker News") || !strings.Contains(textBody, "…") { + t.Fatalf("text-caption body should preserve summary/impact labels and ellipsis without source: %q", textBody) + } + if r.rendered.Pages[2].Meta.CTA != "来源:Hacker News" { + t.Fatalf("text-caption cta = %q", r.rendered.Pages[2].Meta.CTA) } }