Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions internal/summary/presenter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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 {
Expand Down
11 changes: 11 additions & 0 deletions internal/summary/presenter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 7 additions & 2 deletions internal/summary/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
9 changes: 6 additions & 3 deletions internal/summary/prompt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -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")
}
}
2 changes: 1 addition & 1 deletion internal/summary/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions internal/summary/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ type Config struct {
MessageCharBudget int
MaxHighlights int
MaxAttempts int
// Lookback はプロンプトの「過去N時間」表記に使う (収集期間と揃える)
Lookback time.Duration
}

// Summarizer は収集メッセージの要約を生成し、構造化ハイライトの生 JSON を返す。
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ func run() error {
MessageCharBudget: cfg.Summary.MessageCharBudget,
MaxHighlights: cfg.Summary.MaxHighlights,
MaxAttempts: cfg.Summary.MaxAttempts,
Lookback: cfg.Summary.Lookback,
},
}

Expand Down