From fc6baf3402c4ab27d8fd4c07ebec574eed90fb75 Mon Sep 17 00:00:00 2001 From: dj-oyu <68707227+dj-oyu@users.noreply.github.com> Date: Sat, 21 Mar 2026 03:25:42 +0900 Subject: [PATCH] fix: OCR keywords leaking into LLM prompt + streaming preview issues (#74) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Strip consumed OCR option keywords (縦書き, figures, etc.) from message content after PDF processing so they don't get interpreted as LLM instructions - Apply StripThinkBlocks to streaming preview to hide tags - Clear orphan draft on SendDraft failure to prevent stale preview bubbles Co-authored-by: Claude Opus 4.6 (1M context) --- pkg/agent/loop_hooks.go | 2 +- pkg/agent/loop_media.go | 39 ++++++++++++++++++++++++++++++++++++++- pkg/channels/manager.go | 2 ++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/pkg/agent/loop_hooks.go b/pkg/agent/loop_hooks.go index 531a29ac0..629b3d604 100644 --- a/pkg/agent/loop_hooks.go +++ b/pkg/agent/loop_hooks.go @@ -256,7 +256,7 @@ func (al *AgentLoop) setupStreamingHook(opts processOptions, task *activeTask) ( go func() { defer close(streamDone) for up := range streamCh { - display := buildStreamingDisplay(up.accumulated, up.reasoning) + display := buildStreamingDisplay(utils.StripThinkBlocks(up.accumulated), up.reasoning) outMsg := bus.OutboundMessage{ Channel: opts.Channel, ChatID: opts.ChatID, diff --git a/pkg/agent/loop_media.go b/pkg/agent/loop_media.go index 837dd3fdd..bf4deeb4c 100644 --- a/pkg/agent/loop_media.go +++ b/pkg/agent/loop_media.go @@ -471,6 +471,38 @@ func detectReadingOrder(content string, cfgDefault string) string { return "auto" } +// stripConsumedOCRKeywords removes OCR option keywords from the message +// content after they have been consumed by PDF processing, so the LLM +// does not interpret them as user instructions. +func stripConsumedOCRKeywords(content string, withFigures bool, readingOrder string) string { + lower := strings.ToLower(content) + if withFigures { + for _, kw := range figureKeywords { + if idx := strings.Index(lower, kw); idx >= 0 { + content = content[:idx] + content[idx+len(kw):] + lower = lower[:idx] + lower[idx+len(kw):] + } + } + } + if readingOrder != "auto" { + for _, kw := range readingOrderKeywords { + if kw.order != readingOrder { + continue + } + if idx := strings.Index(lower, kw.keyword); idx >= 0 { + content = content[:idx] + content[idx+len(kw.keyword):] + lower = lower[:idx] + lower[idx+len(kw.keyword):] + } + } + } + // Normalize runs of whitespace left by removal. + for strings.Contains(content, " ") { + content = strings.ReplaceAll(content, " ", " ") + } + content = strings.TrimSpace(content) + return content +} + const pdfHintMessage = "PDF OCR in progress. " + "Tip: include \"figures\" or \"\u56f3\u7248\" to extract images. " + "Add \"\u7e26\u66f8\u304d\" or \"\u6a2a\u66f8\u304d\" to set reading order." @@ -495,9 +527,14 @@ func (al *AgentLoop) processPDFsInMessages( cfgRO = ocrCfg.ReadingOrder } readingOrder := detectReadingOrder(m.Content, cfgRO) - result[i].Content = al.replacePDFTags( + newContent := al.replacePDFTags( ctx, m.Content, ocrCfg, channel, chatID, withFigures, readingOrder, ) + // PDF was actually processed (content changed) → strip consumed keywords + if newContent != m.Content { + newContent = stripConsumedOCRKeywords(newContent, withFigures, readingOrder) + } + result[i].Content = newContent } return result diff --git a/pkg/channels/manager.go b/pkg/channels/manager.go index 738be58b3..fc803a5ce 100644 --- a/pkg/channels/manager.go +++ b/pkg/channels/manager.go @@ -184,6 +184,8 @@ func (m *Manager) preSend(ctx context.Context, name string, msg bus.OutboundMess } return true } + // Clear orphan draft on failure to prevent stale streaming preview. + _ = drafter.SendDraft(ctx, msg.ChatID, entry.draftID, "") } m.statusEditTimes.Delete(key) // Draft update failed → fall through to placeholder path.