feat(anki): add proxy transport and tokenizer annotation controls

This commit is contained in:
2026-02-27 21:25:26 -08:00
parent 1c70e486fe
commit 8aa2a45c7c
26 changed files with 1453 additions and 60 deletions

View File

@@ -66,3 +66,44 @@ test('warns and falls back for invalid nPlusOne.decks entries', () => {
);
assert.ok(warnings.some((warning) => warning.path === 'ankiConnect.nPlusOne.decks'));
});
test('accepts valid proxy settings', () => {
const { context, warnings } = makeContext({
proxy: {
enabled: true,
host: '127.0.0.1',
port: 9999,
upstreamUrl: 'http://127.0.0.1:8765',
},
});
applyAnkiConnectResolution(context);
assert.equal(context.resolved.ankiConnect.proxy.enabled, true);
assert.equal(context.resolved.ankiConnect.proxy.host, '127.0.0.1');
assert.equal(context.resolved.ankiConnect.proxy.port, 9999);
assert.equal(context.resolved.ankiConnect.proxy.upstreamUrl, 'http://127.0.0.1:8765');
assert.equal(
warnings.some((warning) => warning.path.startsWith('ankiConnect.proxy')),
false,
);
});
test('warns and falls back for invalid proxy settings', () => {
const { context, warnings } = makeContext({
proxy: {
enabled: 'yes',
host: '',
port: -1,
upstreamUrl: '',
},
});
applyAnkiConnectResolution(context);
assert.deepEqual(context.resolved.ankiConnect.proxy, DEFAULT_CONFIG.ankiConnect.proxy);
assert.ok(warnings.some((warning) => warning.path === 'ankiConnect.proxy.enabled'));
assert.ok(warnings.some((warning) => warning.path === 'ankiConnect.proxy.host'));
assert.ok(warnings.some((warning) => warning.path === 'ankiConnect.proxy.port'));
assert.ok(warnings.some((warning) => warning.path === 'ankiConnect.proxy.upstreamUrl'));
});

View File

@@ -12,6 +12,7 @@ export function applyAnkiConnectResolution(context: ResolveContext): void {
const fields = isObject(ac.fields) ? (ac.fields as Record<string, unknown>) : {};
const media = isObject(ac.media) ? (ac.media as Record<string, unknown>) : {};
const metadata = isObject(ac.metadata) ? (ac.metadata as Record<string, unknown>) : {};
const proxy = isObject(ac.proxy) ? (ac.proxy as Record<string, unknown>) : {};
const aiSource = isObject(ac.ai) ? ac.ai : isObject(ac.openRouter) ? ac.openRouter : {};
const legacyKeys = new Set([
'audioField',
@@ -85,6 +86,9 @@ export function applyAnkiConnectResolution(context: ResolveContext): void {
? (ac.behavior as (typeof context.resolved)['ankiConnect']['behavior'])
: {}),
},
proxy: {
...context.resolved.ankiConnect.proxy,
},
metadata: {
...context.resolved.ankiConnect.metadata,
...(isObject(ac.metadata)
@@ -153,6 +157,68 @@ export function applyAnkiConnectResolution(context: ResolveContext): void {
);
}
if (isObject(ac.proxy)) {
const proxyEnabled = asBoolean(proxy.enabled);
if (proxyEnabled !== undefined) {
context.resolved.ankiConnect.proxy.enabled = proxyEnabled;
} else if (proxy.enabled !== undefined) {
context.warn(
'ankiConnect.proxy.enabled',
proxy.enabled,
context.resolved.ankiConnect.proxy.enabled,
'Expected boolean.',
);
}
const proxyHost = asString(proxy.host);
if (proxyHost !== undefined && proxyHost.trim().length > 0) {
context.resolved.ankiConnect.proxy.host = proxyHost.trim();
} else if (proxy.host !== undefined) {
context.warn(
'ankiConnect.proxy.host',
proxy.host,
context.resolved.ankiConnect.proxy.host,
'Expected non-empty string.',
);
}
const proxyUpstreamUrl = asString(proxy.upstreamUrl);
if (proxyUpstreamUrl !== undefined && proxyUpstreamUrl.trim().length > 0) {
context.resolved.ankiConnect.proxy.upstreamUrl = proxyUpstreamUrl.trim();
} else if (proxy.upstreamUrl !== undefined) {
context.warn(
'ankiConnect.proxy.upstreamUrl',
proxy.upstreamUrl,
context.resolved.ankiConnect.proxy.upstreamUrl,
'Expected non-empty string.',
);
}
const proxyPort = asNumber(proxy.port);
if (
proxyPort !== undefined &&
Number.isInteger(proxyPort) &&
proxyPort >= 1 &&
proxyPort <= 65535
) {
context.resolved.ankiConnect.proxy.port = proxyPort;
} else if (proxy.port !== undefined) {
context.warn(
'ankiConnect.proxy.port',
proxy.port,
context.resolved.ankiConnect.proxy.port,
'Expected integer between 1 and 65535.',
);
}
} else if (ac.proxy !== undefined) {
context.warn(
'ankiConnect.proxy',
ac.proxy,
context.resolved.ankiConnect.proxy,
'Expected object.',
);
}
if (Array.isArray(ac.tags)) {
const normalizedTags = ac.tags
.filter((entry): entry is string => typeof entry === 'string')