diff --git a/internal/summary/presenter.go b/internal/summary/presenter.go index 662d469..285daf9 100644 --- a/internal/summary/presenter.go +++ b/internal/summary/presenter.go @@ -80,11 +80,14 @@ func BuildSummaryMessage(header string, highlights []Highlight, digests []Channe embed := &discordgo.MessageEmbed{ Title: title, Description: description, - URL: link, Color: color, } - if msg := findMessageByURL(digests, link); msg != nil { - embed.Author = &discordgo.MessageEmbedAuthor{Name: msg.Author, IconURL: msg.AvatarURL} + // モデルが幻覚した Discord 外 URL をリンク化しない + if isDiscordMessageLink(link) { + embed.URL = link + if msg := findMessageByURL(digests, link); msg != nil { + embed.Author = &discordgo.MessageEmbedAuthor{Name: msg.Author, IconURL: msg.AvatarURL} + } } embeds = append(embeds, embed) } @@ -96,6 +99,11 @@ func BuildSummaryMessage(header string, highlights []Highlight, digests []Channe return header, embeds } +func isDiscordMessageLink(link string) bool { + return strings.HasPrefix(link, "https://discord.com/channels/") || + strings.HasPrefix(link, "https://discordapp.com/channels/") +} + func findMessageByURL(digests []ChannelDigest, url string) *MessageDigest { for i := range digests { for j := range digests[i].Messages { diff --git a/internal/summary/presenter_test.go b/internal/summary/presenter_test.go index 2aba3af..4acf20d 100644 --- a/internal/summary/presenter_test.go +++ b/internal/summary/presenter_test.go @@ -139,6 +139,17 @@ func TestBuildSummaryMessage(t *testing.T) { } }) + t.Run("non-discord link is not linked", func(t *testing.T) { + highlights := []Highlight{{Title: "t", Description: "d", Link: "https://evil.example.com/x"}} + _, embeds := BuildSummaryMessage("h", highlights, digests) + if len(embeds) != 1 { + t.Fatalf("highlight should be kept, embeds = %d", len(embeds)) + } + if embeds[0].URL != "" || embeds[0].Author != nil { + t.Errorf("external URL should not be linked: %+v", embeds[0]) + } + }) + t.Run("incomplete highlights are dropped", func(t *testing.T) { highlights := []Highlight{{Title: "", Description: "d", Link: "l"}} _, embeds := BuildSummaryMessage("h", highlights, nil) diff --git a/internal/summary/prompt.go b/internal/summary/prompt.go index 56c3966..049ed6f 100644 --- a/internal/summary/prompt.go +++ b/internal/summary/prompt.go @@ -3,18 +3,23 @@ package summary import ( "fmt" "strings" + "time" ) -func buildSummaryPrompt(digests []ChannelDigest, maxHighlights int) string { +func buildSummaryPrompt(digests []ChannelDigest, maxHighlights int, lookback time.Duration) string { if len(digests) == 0 { return "" } if maxHighlights <= 0 { maxHighlights = 3 } + lookbackHours := int(lookback.Hours()) + if lookbackHours <= 0 { + lookbackHours = 24 + } var builder strings.Builder - builder.WriteString("あなたはDiscordサーバーの編集者です。以下は過去24時間に投稿されたメッセージ一覧です。\n") + fmt.Fprintf(&builder, "あなたはDiscordサーバーの編集者です。以下は過去%d時間に投稿されたメッセージ一覧です。\n", lookbackHours) fmt.Fprintf(&builder, "最も注目すべき出来事を最大%d件選び、日本語の夕刊ハイライトを作成してください。\n", maxHighlights) builder.WriteString("出力は必ず次のJSON配列のみとし、整形や説明文を入れないでください。\n") builder.WriteString("例: [{\"title\":\"見出し\",\"emoji\":\"\",\"description\":\"本文\",\"link\":\"https://discord.com/channels/...\",\"color\":\"#b0b0b0\"}]\n") diff --git a/internal/summary/prompt_test.go b/internal/summary/prompt_test.go index bc55227..b5e9266 100644 --- a/internal/summary/prompt_test.go +++ b/internal/summary/prompt_test.go @@ -17,11 +17,14 @@ func TestBuildSummaryPrompt(t *testing.T) { }, }} - prompt := buildSummaryPrompt(digests, 3) + prompt := buildSummaryPrompt(digests, 3, 24*time.Hour) if !strings.Contains(prompt, "最大3件") { t.Errorf("prompt should mention max highlights: %q", prompt) } + if !strings.Contains(prompt, "過去24時間") { + t.Errorf("prompt should mention lookback hours") + } if !strings.Contains(prompt, "# general") { t.Errorf("prompt should contain channel header") } @@ -36,11 +39,11 @@ func TestBuildSummaryPrompt(t *testing.T) { } func TestBuildSummaryPromptEdgeCases(t *testing.T) { - if got := buildSummaryPrompt(nil, 3); got != "" { + if got := buildSummaryPrompt(nil, 3, 24*time.Hour); got != "" { t.Errorf("empty digests should return empty prompt, got %q", got) } digests := []ChannelDigest{{Name: "c", Messages: []MessageDigest{{Author: "a", Content: "x"}}}} - if got := buildSummaryPrompt(digests, 0); !strings.Contains(got, "最大3件") { + if got := buildSummaryPrompt(digests, 0, 0); !strings.Contains(got, "最大3件") { t.Errorf("non-positive maxHighlights should fall back to 3") } } diff --git a/internal/summary/service.go b/internal/summary/service.go index 57de690..5017d38 100644 --- a/internal/summary/service.go +++ b/internal/summary/service.go @@ -126,7 +126,7 @@ func (s *Service) Generate(ctx context.Context, req Request) (Result, error) { summaryCtx = context.Background() } - prompt := buildSummaryPrompt(collectorResult.Digests, s.Config.MaxHighlights) + prompt := buildSummaryPrompt(collectorResult.Digests, s.Config.MaxHighlights, s.Config.Lookback) if prompt == "" { s.info(ctx, "Gemini呼び出しスキップ (プロンプトなし)") } else { diff --git a/internal/summary/types.go b/internal/summary/types.go index 98bad35..ea70a28 100644 --- a/internal/summary/types.go +++ b/internal/summary/types.go @@ -65,6 +65,8 @@ type Config struct { MessageCharBudget int MaxHighlights int MaxAttempts int + // Lookback はプロンプトの「過去N時間」表記に使う (収集期間と揃える) + Lookback time.Duration } // Summarizer は収集メッセージの要約を生成し、構造化ハイライトの生 JSON を返す。 diff --git a/main.go b/main.go index b25a74f..e401d38 100644 --- a/main.go +++ b/main.go @@ -86,6 +86,7 @@ func run() error { MessageCharBudget: cfg.Summary.MessageCharBudget, MaxHighlights: cfg.Summary.MaxHighlights, MaxAttempts: cfg.Summary.MaxAttempts, + Lookback: cfg.Summary.Lookback, }, }