feat(plugins): add JavaScript plugin system with overlay panel (#303)

This commit is contained in:
sudacode authored and GitHub committed 2026-10-31 19:09:54 -07:00
1 parent 90fc7cf940
commit a9a4c4f083
110 files changed
+5897 -344

No files matched your search

+27 -3
View File
@@ -1,5 +1,6 @@
import http, { IncomingMessage, ServerResponse } from 'node:http';
import axios, { AxiosInstance } from 'axios';
import type { PluginMiningProvenance } from '../plugin-api';
interface StartProxyOptions {
host: string;
@@ -13,9 +14,13 @@ interface AnkiConnectEnvelope {
}
export interface AnkiConnectProxyServerDeps {
captureMiningProvenance?: () => PluginMiningProvenance | null;
onNotesAdded?: (noteIds: number[], provenance: PluginMiningProvenance | null) => void;
shouldAutoUpdateNewCards: () => boolean;
processNewCard: (noteId: number) => Promise<void>;
recordCardsAdded?: (count: number, noteIds: number[]) => void;
/** Called once SubMiner is done with an added note: after enrichment, or right away when it is off. */
onCardsReady?: (noteIds: number[]) => void;
trackAddedDuplicateNoteIds?: (noteId: number, duplicateNoteIds: number[]) => void;
getDeck?: () => string | undefined;
findNotes?: (
@@ -186,6 +191,12 @@ export class AnkiConnectProxyServer {
typeof req.headers['content-type'] === 'string'
? req.headers['content-type']
: 'application/json';
const action = requestJson?.action;
const provenance =
typeof action === 'string' &&
['addNote', 'addNotes', 'multi', 'updateNoteFields'].includes(action)
? (this.deps.captureMiningProvenance?.() ?? null)
: null;
const upstreamResponse = await this.client.request<ArrayBuffer>({
url: targetUrl,
method: req.method,
@@ -203,7 +214,7 @@ export class AnkiConnectProxyServer {
res.end(responseBody);
if (req.method === 'POST') {
this.maybeEnqueueFromRequest(requestJson, responseBody);
this.maybeEnqueueFromRequest(requestJson, responseBody, provenance);
}
} catch (error) {
this.deps.logWarn('[anki-proxy] Failed to forward request:', (error as Error).message);
@@ -215,16 +226,19 @@ export class AnkiConnectProxyServer {
private maybeEnqueueFromRequest(
requestJson: Record<string, unknown> | null,
responseBody: Buffer,
provenance: PluginMiningProvenance | null = null,
): void {
if (!requestJson || !this.deps.shouldAutoUpdateNewCards()) {
if (!requestJson) {
return;
}
const autoUpdate = this.deps.shouldAutoUpdateNewCards();
const action =
typeof requestJson.action === 'string'
? requestJson.action
: String(requestJson.action ?? '');
if (action === 'updateNoteFields') {
if (!autoUpdate) return;
const params = requestJson.params;
if (
!params ||
@@ -246,6 +260,7 @@ export class AnkiConnectProxyServer {
response.result !== null
)
return;
this.deps.onNotesAdded?.(this.collectSingleResultId(note.id), provenance);
this.enqueueNotes(this.collectSingleResultId(note.id), false);
return;
}
@@ -277,9 +292,16 @@ export class AnkiConnectProxyServer {
params.subminerEnrich === false
) {
// Stats owns the saved sentence and media; the live mpv context is unrelated.
if (noteIds.length > 0) this.deps.recordCardsAdded?.(noteIds.length, noteIds);
if (autoUpdate && noteIds.length > 0) this.deps.recordCardsAdded?.(noteIds.length, noteIds);
return;
}
if (!autoUpdate) {
// Nothing to enrich, so the added notes are already in their final state.
this.deps.onNotesAdded?.(noteIds, provenance);
if (noteIds.length > 0) this.deps.onCardsReady?.(noteIds);
return;
}
this.deps.onNotesAdded?.(noteIds, provenance);
this.maybeTrackDuplicateNoteIds(requestJson, action, responseResult);
if (noteIds.length === 0 && shouldFallbackToLatestAdded) {
void this.enqueueMostRecentAddedNote();
@@ -534,6 +556,7 @@ export class AnkiConnectProxyServer {
this.pendingNoteIdSet.delete(noteId);
if (!this.deps.shouldAutoUpdateNewCards()) {
this.deps.onCardsReady?.([noteId]);
continue;
}
@@ -547,6 +570,7 @@ export class AnkiConnectProxyServer {
);
} finally {
this.inFlightNoteIds.delete(noteId);
this.deps.onCardsReady?.([noteId]);
}
}
} finally {