diff options
Diffstat (limited to 'src/background/message-router.js')
| -rw-r--r-- | src/background/message-router.js | 151 |
1 files changed, 47 insertions, 104 deletions
diff --git a/src/background/message-router.js b/src/background/message-router.js index 9428312..cb9b69c 100644 --- a/src/background/message-router.js +++ b/src/background/message-router.js @@ -3,43 +3,25 @@ * * - runtime.Port ('translation-stream'): translate / cancel requests from * the content script, streaming started/chunk/completed/error back. - * - runtime.onMessage: one-shot listModels request for popup/options. + * - runtime.onMessage: one-shot requests for popup/options: + * - listModels + * - getPopupState (popup translation result mode) + * - regenerateLast * - * All inbound messages are validated; invalid ones are discarded - * (Specifications.md SEC-06, Design.md §8.4). + * The actual translation pipeline (validation, prompt, Ollama request, + * session/abort, shared state) lives in translation-service.js and is used + * by BOTH the content-script port flow and the context-menu flow, so there + * is a single request-generation implementation (Design.md §9.2). */ import { - CONTENT_TO_BG, - BG_TO_CONTENT, RUNTIME_MESSAGES, - MAX_SELECTION_LENGTH, ERROR_CODES } from '../shared/constants.js'; -import { isValidTranslateRequest, isValidCancelRequest, isTrustedSender } from '../shared/messages.js'; +import { isValidTranslateRequest, isValidCancelRequest, isTrustedSender, createRequestId } from '../shared/messages.js'; import { getSettings } from './settings-service.js'; -import { TranslationSession } from './translation-session.js'; -import { chatStream, listModels } from './ollama-client.js'; -import { buildSystemPrompt } from '../shared/prompt.js'; -import { buildOptions, toApiThinking } from '../shared/settings-schema.js'; - -const session = new TranslationSession(); - -/** User-facing messages per error code (kept minimal; no stack traces). */ -const ERROR_MESSAGES = { - [ERROR_CODES.INVALID_REQUEST]: '要求が不正です。', - [ERROR_CODES.TEXT_EMPTY]: '選択文字列が空です。', - [ERROR_CODES.TEXT_TOO_LONG]: `選択文字列が長すぎます(上限 ${MAX_SELECTION_LENGTH} 文字)。`, - [ERROR_CODES.OLLAMA_UNAVAILABLE]: 'Ollamaに接続できません。Ollamaが起動しているか確認してください。', - [ERROR_CODES.MODEL_NOT_FOUND]: '指定されたモデルが見つかりません。設定画面でモデルを選び直してください。', - [ERROR_CODES.INVALID_SETTINGS]: '設定値が不正です。設定画面を確認してください。', - [ERROR_CODES.REQUEST_ABORTED]: '翻訳を中断しました。', - [ERROR_CODES.STREAM_ERROR]: '翻訳中にエラーが発生しました。', - [ERROR_CODES.UNKNOWN_ERROR]: '不明なエラーが発生しました。' -}; - -/** Message shown when no model is configured at all. */ -const NO_MODEL_MESSAGE = 'モデルが設定されていません。ツールバーアイコンまたは設定画面でモデルを選択してください。'; +import { listModels } from './ollama-client.js'; +import { runTranslation, getSession, getTranslationState, addPopupPort, removePopupPort } from './translation-service.js'; /** * Handle one content-script port connection. @@ -52,12 +34,12 @@ export function handleConnection(port) { if (!isTrustedSender(port.sender)) return; if (isValidTranslateRequest(message)) { - void runTranslation(port, message.requestId, message.text); + void runTranslation({ text: message.text, requestId: message.requestId, source: 'content', sink: port }); return; } if (isValidCancelRequest(message)) { - if (session.isActive(message.requestId)) { - session.clear(message.requestId); + if (getSession().isActive(message.requestId)) { + getSession().clear(message.requestId); } return; } @@ -65,84 +47,27 @@ export function handleConnection(port) { }); port.onDisconnect.addListener(() => { - // If the tab goes away, abort the translation owned by this port. - if (session.hasActive() && session.active.port === port) { - const requestId = session.active.requestId; - session.clear(requestId); + // If the tab goes away, clear the session entry owned by this port. + // The popup-facing translation state keeps whatever was accumulated so + // reopening the popup still shows the latest text. + if (getSession().hasActive() && getSession().active.port === port) { + const requestId = getSession().active.requestId; + getSession().clear(requestId); } }); } /** - * Run one translation: settings -> prompt -> Ollama stream -> port messages. - * @param {object} port - * @param {string} requestId - * @param {string} text - */ -async function runTranslation(port, requestId, text) { - const settings = await getSettings(); - - if (!settings.enabled) { - postError(port, requestId, ERROR_CODES.INVALID_REQUEST, 'Translation is disabled'); - return; - } - if (!settings.model) { - postError(port, requestId, ERROR_CODES.INVALID_SETTINGS, NO_MODEL_MESSAGE); - return; - } - - const controller = session.start(requestId, port); - - port.postMessage({ type: BG_TO_CONTENT.STARTED, requestId, model: settings.model }); - - try { - await chatStream({ - host: settings.ollamaHost, - model: settings.model, - messages: [ - { role: 'system', content: buildSystemPrompt(settings) }, - { role: 'user', content: text } - ], - think: toApiThinking(settings), - options: buildOptions(settings), - signal: controller.signal, - onChunk: (content) => { - // Drop chunks if a newer request took over. - if (!session.isActive(requestId)) return; - port.postMessage({ type: BG_TO_CONTENT.CHUNK, requestId, text: content }); - } - }); - if (session.isActive(requestId)) { - port.postMessage({ type: BG_TO_CONTENT.COMPLETED, requestId }); - } - } catch (error) { - const code = (error && error.code) || ERROR_CODES.UNKNOWN_ERROR; - // Aborted requests are reported by TranslationSession.abortActive(); - // avoid a duplicate error for the same requestId. - if (code !== ERROR_CODES.REQUEST_ABORTED && session.isActive(requestId)) { - postError(port, requestId, code, error && error.message ? error.message : ''); - } - } finally { - session.clear(requestId); - } -} - -/** - * @param {object} port - * @param {string} requestId - * @param {string} code - * @param {string} detail optional concrete reason shown to the user + * Handle one popup-state port connection. The popup subscribes to live + * translation state updates (Design.md §7.4, §9.2). + * @param {object} port runtime.Port */ -function postError(port, requestId, code, detail) { - const base = ERROR_MESSAGES[code] || ERROR_MESSAGES[ERROR_CODES.UNKNOWN_ERROR]; - // Append the concrete server/detail reason when provided so the popover - // shows WHY it failed (e.g. the Ollama error body for HTTP 400). - const message = detail && detail !== base ? `${base}\n(${detail})` : base; - try { - port.postMessage({ type: BG_TO_CONTENT.ERROR, requestId, code, message }); - } catch { - // port may be disconnected - } +export function handlePopupStateConnection(port) { + if (!port || port.name !== 'popup-state') return; + addPopupPort(port); + port.onDisconnect.addListener(() => { + removePopupPort(port); + }); } /** @@ -164,6 +89,24 @@ export function registerRuntimeMessageHandler(browserApi) { } })(); } + if (message && message.type === RUNTIME_MESSAGES.GET_POPUP_STATE) { + return Promise.resolve({ ok: true, state: getTranslationState() }); + } + if (message && message.type === RUNTIME_MESSAGES.REGENERATE_LAST) { + return (async () => { + try { + const state = getTranslationState(); + if (!state.sourceText) { + return { ok: false, error: ERROR_CODES.TEXT_EMPTY }; + } + const requestId = createRequestId(); + void runTranslation({ text: state.sourceText, requestId, source: state.source || 'context-menu', sink: null }); + return { ok: true, requestId }; + } catch (error) { + return { ok: false, error: (error && error.code) || ERROR_CODES.UNKNOWN_ERROR }; + } + })(); + } return Promise.resolve({ ok: false, error: ERROR_CODES.INVALID_REQUEST }); }); } |
