feat(ai): split shared provider config from Anki runtime

This commit is contained in:
2026-03-08 16:10:51 -07:00
parent f10e905dbd
commit 9e46176519
19 changed files with 457 additions and 133 deletions

View File

@@ -1,5 +1,7 @@
import { AnkiIntegration } from '../../anki-integration';
import { mergeAiConfig } from '../../ai/config';
import {
AiConfig,
AnkiConnectConfig,
JimakuApiResponse,
JimakuEntry,
@@ -30,7 +32,7 @@ interface SubtitleTimingTrackerLike {
export interface AnkiJimakuIpcRuntimeOptions {
patchAnkiConnectEnabled: (enabled: boolean) => void;
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig };
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig; ai?: AiConfig };
getRuntimeOptionsManager: () => RuntimeOptionsManagerLike | null;
getSubtitleTimingTracker: () => SubtitleTimingTrackerLike | null;
getMpvClient: () => MpvClientLike | null;
@@ -100,6 +102,7 @@ export function registerAnkiJimakuIpcRuntime(
options.showDesktopNotification,
options.createFieldGroupingCallback(),
options.getKnownWordCacheStatePath(),
mergeAiConfig(config.ai, config.ankiConnect?.ai) as AiConfig,
);
integration.start();
options.setAnkiIntegration(integration);

View File

@@ -125,10 +125,10 @@ test('config hot reload runtime reports validation warnings from reload', () =>
config: deepCloneConfig(DEFAULT_CONFIG),
warnings: [
{
path: 'ankiConnect.openRouter',
message: 'Deprecated key; use ankiConnect.ai instead.',
path: 'ankiConnect.ai',
message: 'Expected boolean.',
value: { enabled: true },
fallback: {},
fallback: false,
},
],
path: '/tmp/config.jsonc',

View File

@@ -73,7 +73,11 @@ function classifyDiff(prev: ResolvedConfig, next: ResolvedConfig): ConfigHotRelo
if (key === 'ankiConnect') {
const normalizedPrev = {
...prev.ankiConnect,
ai: next.ankiConnect.ai,
ai: {
enabled: next.ankiConnect.ai.enabled,
model: prev.ankiConnect.ai.model,
systemPrompt: prev.ankiConnect.ai.systemPrompt,
},
};
if (!isEqual(normalizedPrev, next.ankiConnect)) {
restartRequiredFields.push('ankiConnect');

View File

@@ -109,6 +109,65 @@ test('initializeOverlayRuntime starts Anki integration when ankiConnect.enabled
assert.equal(setIntegrationCalls, 1);
});
test('initializeOverlayRuntime merges shared ai config with Anki overrides', () => {
initializeOverlayRuntime({
backendOverride: null,
createMainWindow: () => {},
registerGlobalShortcuts: () => {},
updateVisibleOverlayBounds: () => {},
isVisibleOverlayVisible: () => false,
updateVisibleOverlayVisibility: () => {},
getOverlayWindows: () => [],
syncOverlayShortcuts: () => {},
setWindowTracker: () => {},
getMpvSocketPath: () => '/tmp/mpv.sock',
createWindowTracker: () => null,
getResolvedConfig: () => ({
ankiConnect: {
enabled: true,
ai: {
enabled: true,
model: 'openrouter/anki-model',
systemPrompt: 'Translate mined sentence text.',
},
} as never,
ai: {
enabled: true,
apiKey: 'shared-key',
baseUrl: 'https://openrouter.ai/api',
model: 'openrouter/shared-model',
systemPrompt: 'Legacy shared prompt.',
requestTimeoutMs: 15000,
},
}),
getSubtitleTimingTracker: () => ({}),
getMpvClient: () => ({
send: () => {},
}),
getRuntimeOptionsManager: () => ({
getEffectiveAnkiConnectConfig: (config) => config as never,
}),
createAnkiIntegration: (args) => {
assert.equal(args.aiConfig.apiKey, 'shared-key');
assert.equal(args.aiConfig.baseUrl, 'https://openrouter.ai/api');
assert.equal(args.aiConfig.model, 'openrouter/anki-model');
assert.equal(args.aiConfig.systemPrompt, 'Translate mined sentence text.');
return {
start: () => {},
};
},
setAnkiIntegration: () => {},
showDesktopNotification: () => {},
createFieldGroupingCallback: () => async () => ({
keepNoteId: 5,
deleteNoteId: 6,
deleteDuplicate: false,
cancelled: false,
}),
getKnownWordCacheStatePath: () => '/tmp/known-words-cache.json',
});
});
test('initializeOverlayRuntime re-syncs overlay shortcuts when tracker focus changes', () => {
let syncCalls = 0;
const tracker = {

View File

@@ -1,5 +1,6 @@
import type { BrowserWindow } from 'electron';
import { BaseWindowTracker, createWindowTracker } from '../../window-trackers';
import { mergeAiConfig } from '../../ai/config';
import {
AiConfig,
AnkiConnectConfig,
@@ -124,7 +125,7 @@ export function initializeOverlayRuntime(options: {
const createAnkiIntegration = options.createAnkiIntegration ?? createDefaultAnkiIntegration;
const integration = createAnkiIntegration({
config: effectiveAnkiConfig,
aiConfig: config.ai ?? {},
aiConfig: mergeAiConfig(config.ai, config.ankiConnect?.ai),
subtitleTimingTracker,
mpvClient,
showDesktopNotification: options.showDesktopNotification,