From 37484b944d6f35eb14b9d4ccfdb8c922351eb084 Mon Sep 17 00:00:00 2001 From: CoderLambert Date: Wed, 21 Oct 2026 03:10:01 -0700 Subject: [PATCH 01/25] feat(release): split public and ChatGPT developer manifest builds --- src/shared/chatgpt-native-host.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/shared/chatgpt-native-host.js b/src/shared/chatgpt-native-host.js index c8dd2931..19caa9f7 100644 --- a/src/shared/chatgpt-native-host.js +++ b/src/shared/chatgpt-native-host.js @@ -2,6 +2,22 @@ export const CHATGPT_PLAN_NATIVE_HOST = "com.coderlambert.translateflow"; export const CHATGPT_PLAN_NATIVE_PERMISSION = "nativeMessaging"; export const CHATGPT_PLAN_MIN_HOST_VERSION = "0.8.0"; export const CHATGPT_PLAN_HOST_DOWNLOAD_URL = "https://github.com/CoderLambert/translateflow-plugin/releases/latest"; +export const CHATGPT_PLAN_DEV_GUIDE_URL = "https://github.com/CoderLambert/translateflow-plugin/blob/main/docs/CHATGPT_DEVELOPMENT_BUILD.md"; + +// Only the separately built, explicitly identified unpacked developer edition exposes this experiment. +// Accept manifest input rather than reading chrome.* inside a shared pure module. +export function isChatGPTPlanDeveloperBuild(manifest) { + return Boolean(manifest && typeof manifest.version === "string" && + manifest.version_name === `${manifest.version}-dev` && + manifest.optional_permissions?.includes("nativeMessaging")); +} + +export function requireChatGPTPlanDeveloperBuild(manifest) { + if (isChatGPTPlanDeveloperBuild(manifest)) return; + const error = new Error("ChatGPT subscription is available only in the GitHub developer build. Your existing settings are preserved."); + error.code = "CHATGPT_PLAN_DEV_ONLY"; + throw error; +} export function compareHostVersions(left, right) { const a = parseVersion(left), b = parseVersion(right); From 653e1ddace24cc612e7b2b2305be8d396b45d347 Mon Sep 17 00:00:00 2001 From: CoderLambert Date: Wed, 21 Oct 2026 03:10:04 -0700 Subject: [PATCH 02/25] feat(release): split public and ChatGPT developer manifest builds --- scripts/production-manifest.mjs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/production-manifest.mjs b/scripts/production-manifest.mjs index 5d549bae..7356ec63 100644 --- a/scripts/production-manifest.mjs +++ b/scripts/production-manifest.mjs @@ -7,14 +7,16 @@ export const GLOBAL_CONTENT_SCRIPT = Object.freeze({ run_at: "document_idle" }); -export function projectProductionManifest(baseline) { +export function projectProductionManifest(baseline, { developer = false } = {}) { const { content_scripts: _legacyContentScripts, ...manifest } = baseline; - return manifest; + // Chrome's version_name distinguishes the experimental unpacked build without changing + // version, storage layout, Content scripts or existing host permissions. + return developer ? { ...manifest, version_name: `${manifest.version}-dev` } : manifest; } -export function expectedProductionManifest(baseline) { +export function expectedProductionManifest(baseline, { developer = false } = {}) { return { - ...projectProductionManifest(baseline), + ...projectProductionManifest(baseline, { developer }), content_scripts: [{ matches: [...GLOBAL_CONTENT_SCRIPT.matches], js: [...GLOBAL_CONTENT_SCRIPT.js], From 16505a724b82d7dec6c65a7c4cc9c4172759ca2f Mon Sep 17 00:00:00 2001 From: CoderLambert Date: Wed, 21 Oct 2026 03:10:07 -0700 Subject: [PATCH 03/25] feat(release): split public and ChatGPT developer manifest builds --- wxt.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wxt.config.mjs b/wxt.config.mjs index 3c49bda4..57c77db1 100644 --- a/wxt.config.mjs +++ b/wxt.config.mjs @@ -7,7 +7,7 @@ import { projectProductionManifest } from "./scripts/production-manifest.mjs"; import { checkManifestLocales } from "./scripts/i18n-locales.mjs"; const { manifest_version: _version, ...baselineManifest } = JSON.parse(await readFile(resolve(ROOT, "manifest.json"), "utf8")); -const manifest = projectProductionManifest(baselineManifest); +const manifest = projectProductionManifest(baselineManifest, { developer: process.env.TF_CHATGPT_PLAN_DEV_BUILD === "1" }); const staging = process.env.TF_WXT_BUILD_ROOT; const reportDir = staging ? resolve(staging, "reports") : resolve(ROOT, ".wxt/reports"); const compiledChunks = []; From a9d2672101e607be25b7a9218787664a05b18914 Mon Sep 17 00:00:00 2001 From: CoderLambert Date: Wed, 21 Oct 2026 03:10:10 -0700 Subject: [PATCH 04/25] feat(release): split public and ChatGPT developer manifest builds --- scripts/audit-wxt-extension.mjs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/audit-wxt-extension.mjs b/scripts/audit-wxt-extension.mjs index c64e8889..9f8b098c 100644 --- a/scripts/audit-wxt-extension.mjs +++ b/scripts/audit-wxt-extension.mjs @@ -9,15 +9,15 @@ import { EXTENSION_PAGES, WORKER_PATHS, YOUTUBE_MAIN_BRIDGE_FILES } from "../src import { expectedProductionManifest } from "./production-manifest.mjs"; import { checkManifestLocales } from "./i18n-locales.mjs"; -export function assertProductionManifest(manifest, baseline) { - assert.deepEqual(manifest, expectedProductionManifest(baseline), "Production WXT Manifest must equal the approved compiled Content projection"); +export function assertProductionManifest(manifest, baseline, { developer = false } = {}) { + assert.deepEqual(manifest, expectedProductionManifest(baseline, { developer }), "WXT Manifest must equal the approved compiled Content projection for this build edition"); } -export async function auditWxtExtension({ output = resolve(ROOT, ".output/chrome-mv3"), reportDir = resolve(ROOT, ".wxt/reports") } = {}) { +export async function auditWxtExtension({ output = resolve(ROOT, ".output/chrome-mv3"), reportDir = resolve(ROOT, ".wxt/reports"), developer = false } = {}) { const manifest = JSON.parse(await readFile(resolve(output, "manifest.json"), "utf8")); const baseline = JSON.parse(await readFile(resolve(ROOT, "manifest.json"), "utf8")); // WXT's compiled Content entrypoint is the only approved build-time projection. - assertProductionManifest(manifest, baseline); + assertProductionManifest(manifest, baseline, { developer }); const runtime = await sourceClosure(runtimeAssetRoots()); const locales = await sourceClosure((await checkManifestLocales()).files); const assetMap = JSON.parse(await readFile(resolve(reportDir, "asset-map.json"), "utf8")); From 3f9fcedf9c244d4a34540ad00dd2b128c45f082d Mon Sep 17 00:00:00 2001 From: CoderLambert Date: Wed, 21 Oct 2026 03:10:47 -0700 Subject: [PATCH 05/25] feat(release): guard ChatGPT in public and build an explicit developer edition --- scripts/build-extension.mjs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/scripts/build-extension.mjs b/scripts/build-extension.mjs index 90b29415..6b220a72 100644 --- a/scripts/build-extension.mjs +++ b/scripts/build-extension.mjs @@ -11,8 +11,9 @@ import { auditWxtExtension } from "./audit-wxt-extension.mjs"; const ROOT = resolve(fileURLToPath(new URL("..", import.meta.url))); const DEFAULT_OUT = resolve(ROOT, "dist/extension"); +const DEVELOPER_OUT = resolve(ROOT, "dist/extension-dev"); -export async function buildExtension({ outDir = DEFAULT_OUT, allowExternalOutput = false } = {}) { +export async function buildExtension({ developer = false, outDir = developer ? DEVELOPER_OUT : DEFAULT_OUT, allowExternalOutput = false } = {}) { const options = { allowExternalOutput, allowWxtOutput: true }; const output = await assertBuildOutputPaths(ROOT, outDir, options); // Each invocation builds in its own staging directory, including parallel Node @@ -23,9 +24,9 @@ export async function buildExtension({ outDir = DEFAULT_OUT, allowExternalOutput const artifact = resolve(staging, "output/chrome-mv3"); await promisify(execFile)(process.execPath, [resolve(ROOT, "node_modules/wxt/bin/wxt.mjs"), "build", "-b", "chrome", "--mv3"], { cwd: ROOT, maxBuffer: 4 * 1024 * 1024, - env: { ...process.env, TF_WXT_BUILD_ROOT: staging } + env: { ...process.env, TF_WXT_BUILD_ROOT: staging, TF_CHATGPT_PLAN_DEV_BUILD: developer ? "1" : "0" } }); - const audit = await auditWxtExtension({ output: artifact, reportDir }); + const audit = await auditWxtExtension({ output: artifact, reportDir, developer }); await assertBuildOutputPaths(ROOT, output, options); await rm(output, { recursive: true, force: true }); await mkdir(output, { recursive: true }); @@ -35,16 +36,17 @@ export async function buildExtension({ outDir = DEFAULT_OUT, allowExternalOutput await mkdir(resolve(ROOT, ".wxt/reports"), { recursive: true }); await cp(reportDir, resolve(ROOT, ".wxt/reports"), { recursive: true }); } - return { output, builder: "WXT", fileCount: audit.fileCount, totalBytes: audit.totalBytes, + return { output, edition: developer ? "chatgpt-developer" : "public", builder: "WXT", fileCount: audit.fileCount, totalBytes: audit.totalBytes, lexicalBytes: audit.lexicalBytes, lexicalAssetsIncluded: audit.lexicalBytes > 0, largestFiles: [...audit.files].sort((a,b) => b.size - a.size || a.path.localeCompare(b.path)).slice(0,20) }; } finally { await rm(staging, { recursive: true, force: true }); } } function parseArgs(argv) { - const args = { outDir: DEFAULT_OUT }; + const args = { developer: false }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; + if (arg === "--developer") { args.developer = true; continue; } if (arg === "--out") { const value = argv[++index]; if (!value || value.startsWith("--")) throw new Error("missing value for --out"); From 7675b919a4108a3abcf4b7e56dad1360de9a424d Mon Sep 17 00:00:00 2001 From: CoderLambert Date: Wed, 21 Oct 2026 03:10:50 -0700 Subject: [PATCH 06/25] feat(release): guard ChatGPT in public and build an explicit developer edition --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 60b2e5c5..329e3ad1 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "lock:kaikki": "node scripts/lock-kaikki-source.mjs", "build:extension": "node scripts/build-extension.mjs", "build:extension:release": "node scripts/build-extension.mjs", + "build:extension:dev": "node scripts/build-extension.mjs --developer", "build:native-host:candidate": "node scripts/build-native-host-candidate.mjs", "measure:extension-footprint": "node scripts/measure-extension-footprint.mjs", "project:kaikki": "node scripts/project-kaikki-rich.mjs", From 0bfc7a27bd6c603d4935536b20f1003ea95d32f6 Mon Sep 17 00:00:00 2001 From: CoderLambert Date: Wed, 21 Oct 2026 03:10:53 -0700 Subject: [PATCH 07/25] feat(release): guard ChatGPT in public and build an explicit developer edition --- src/background/providers/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/background/providers/index.js b/src/background/providers/index.js index 92f59fb0..4bc9db3d 100644 --- a/src/background/providers/index.js +++ b/src/background/providers/index.js @@ -1,6 +1,7 @@ import { MACHINE_ENGINE } from "../../shared/machine-translation.js"; import { translateMachineRoute } from "./machine-translation-routing.js"; import { PROVIDER_IDS } from "../../shared/constants.js"; +import { requireChatGPTPlanDeveloperBuild } from "../../shared/chatgpt-native-host.js"; import { deepSeekProvider } from "./deepseek.js"; import { openAIChatProvider, openAICompatibleProvider } from "./openai-compatible.js"; import { chatGPTPlanProvider } from "./chatgpt-plan.js"; @@ -20,6 +21,7 @@ const PROVIDERS = new Map([ export function getProvider(config) { const providerId = String(config?.provider || PROVIDER_IDS.DEEPSEEK).trim().toLowerCase(); + if (providerId === PROVIDER_IDS.CHATGPT_PLAN) requireChatGPTPlanDeveloperBuild(globalThis.chrome?.runtime?.getManifest?.()); const provider = PROVIDERS.get(providerId); if (!provider) throw new Error(`不支持的翻译 Provider:${providerId}`); return provider; From 2a9060b1eb46413e8a8c3ac4ebef0d9fe4ef14a1 Mon Sep 17 00:00:00 2001 From: CoderLambert Date: Wed, 21 Oct 2026 03:10:56 -0700 Subject: [PATCH 08/25] feat(release): guard ChatGPT in public and build an explicit developer edition --- src/background/router.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/background/router.js b/src/background/router.js index 6bcbf0f8..02583f36 100644 --- a/src/background/router.js +++ b/src/background/router.js @@ -1,4 +1,5 @@ import { EXTENSION_PAGES } from "../shared/runtime-assets.js"; +import { requireChatGPTPlanDeveloperBuild } from "../shared/chatgpt-native-host.js"; import { MACHINE_SETTING_MESSAGES, handleMachineSettingsMessage } from "./machine-translation-settings.js"; import { handleCacheMessage } from "./cache/message-handler.js"; import { readingTranslationResult } from "./selection/reading-result.js"; @@ -152,7 +153,9 @@ export async function handleBackgroundMessage(message, sender) { case BACKGROUND_MESSAGES.QUICK_CONTROL_POSITION_SET: return saveQuickControlPosition(sender, message.position); case BACKGROUND_MESSAGES.PUBLIC_SETTINGS_SET: return writePublicSettings(message.settings); case BACKGROUND_MESSAGES.CHATGPT_PLAN_ACTION: - assertOptionsSender(sender); return handleChatGPTPlanAction(message.action, undefined, { accountId: message.accountId }); + assertOptionsSender(sender); + requireChatGPTPlanDeveloperBuild(globalThis.chrome?.runtime?.getManifest?.()); + return handleChatGPTPlanAction(message.action, undefined, { accountId: message.accountId }); case BACKGROUND_MESSAGES.SELECTION_RESOLVE: return resolveSelectionRequest({ text: message.text, From ad50e3ca80a96273a0a1cfae0d63e8f1764c1f06 Mon Sep 17 00:00:00 2001 From: CoderLambert Date: Wed, 21 Oct 2026 03:11:25 -0700 Subject: [PATCH 09/25] feat(ui): show GitHub developer-build notice in public settings --- src/options/ProviderConnectionsSection.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/options/ProviderConnectionsSection.tsx b/src/options/ProviderConnectionsSection.tsx index cfb68ce1..24570728 100644 --- a/src/options/ProviderConnectionsSection.tsx +++ b/src/options/ProviderConnectionsSection.tsx @@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from "react"; import { PROVIDER_PLATFORMS, PROVIDER_PROTOCOLS, providerPlatformPreset } from "../shared/provider-connections.js"; import type { OptionsClient, ProviderConnection, ProviderConnectionState, ServiceRef } from "./client"; import { useOptionsI18n } from "./LocaleContext"; +import { isChatGPTPlanDeveloperBuild } from "../shared/chatgpt-native-host.js"; type Draft = Omit; type ConnectionTestStatus = { connectionId: string; message: string; kind: "loading" | "success" | "error" }; @@ -17,6 +18,7 @@ export function ProviderConnectionsSection({ client, state, loading, error, disa const [testStatus, setTestStatus] = useState(null); const connections = state?.connections || []; const defaultValue = serviceValue(state?.defaultService || null); + const developer = isChatGPTPlanDeveloperBuild(globalThis.chrome?.runtime?.getManifest?.()); const platformOptions = useMemo(() => PROVIDER_PLATFORMS, []); const update = (patch: Partial) => setDraft(value => ({ ...value, ...patch })); const settings = (patch: Partial) => update({ settings: { ...draft.settings, ...patch } }); @@ -78,7 +80,7 @@ export function ProviderConnectionsSection({ client, state, loading, error, disa : draft.platform === "lmstudio" ? "options.connections.lmstudioHelp" : "options.connections.customHelp"; return

{i18n.t("options.connections.title")}

{i18n.t("options.connections.summary")}

- +
{loading ?

{i18n.t("options.connections.loading")}

: error ?

{error}

: connections.map(connection =>
{connection.name}{connection.platform} · {connection.protocol} · {connection.defaultModel || i18n.t("options.connections.modelUnset")}{connection.baseUrl}{connection.hasApiKey ? i18n.t("options.connections.keySaved") : connection.settings.allowEmptyApiKey ? i18n.t("options.connections.keyOptional") : i18n.t("options.connections.keyMissing")}{testStatus?.connectionId === connection.id ? {testStatus.message} : null}
)}
void save(event)}>

{i18n.t(editing ? "options.connections.edit" : "options.connections.add")}

From e08e405dd66345526370ac1079285c2b74834811 Mon Sep 17 00:00:00 2001 From: CoderLambert Date: Wed, 21 Oct 2026 03:11:28 -0700 Subject: [PATCH 10/25] feat(ui): show GitHub developer-build notice in public settings --- src/options/CommonSections.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/options/CommonSections.tsx b/src/options/CommonSections.tsx index 0866bead..14d7cfe2 100644 --- a/src/options/CommonSections.tsx +++ b/src/options/CommonSections.tsx @@ -3,6 +3,7 @@ import { TRANSLATION_APPEARANCES } from "../shared/appearance.js"; import type { OptionsConfig } from "./client"; import { useOptionsI18n } from "./LocaleContext"; import { ChatGPTPlanSection } from "./ChatGPTPlanSection"; +import { CHATGPT_PLAN_DEV_GUIDE_URL, isChatGPTPlanDeveloperBuild } from "../shared/chatgpt-native-host.js"; import type { OptionsClient } from "./client"; import type { ProviderConnectionState } from "./client"; import { ProviderConnectionsSection } from "./ProviderConnectionsSection"; @@ -45,7 +46,15 @@ export function ShortcutsSection() { } export function ProviderSections({ config, disabled, update, client, connections, loading, error, reload, setStatus }: Props & { client: OptionsClient; connections: ProviderConnectionState | null; loading: boolean; error: string; reload: () => Promise; setStatus: (message: string, error?: boolean) => void }) { + const i18n = useOptionsI18n(); + const developer = isChatGPTPlanDeveloperBuild(globalThis.chrome?.runtime?.getManifest?.()); return <> - ; + {developer ? + : } + ; } function preventComposingSubmit(event: React.KeyboardEvent) { if (event.key === "Enter" && event.nativeEvent.isComposing) event.preventDefault(); } From 02067dcf8a0405b770680bca44fd2bec847480af Mon Sep 17 00:00:00 2001 From: CoderLambert Date: Wed, 21 Oct 2026 03:11:31 -0700 Subject: [PATCH 11/25] feat(ui): show GitHub developer-build notice in public settings --- src/i18n/catalog-options-chatgpt.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/i18n/catalog-options-chatgpt.js b/src/i18n/catalog-options-chatgpt.js index def4b379..fefa28f7 100644 --- a/src/i18n/catalog-options-chatgpt.js +++ b/src/i18n/catalog-options-chatgpt.js @@ -1,5 +1,9 @@ export const en = Object.freeze({ "options.chatgptPlan.providerName": "ChatGPT subscription", + "options.chatgptPlan.devOnlyTitle": "ChatGPT subscription · developer preview", + "options.chatgptPlan.devOnlyDescription": "The public edition does not offer ChatGPT subscription sign-in. To test this experimental feature, use the self-built GitHub developer edition and configure the native host yourself.", + "options.chatgptPlan.devGuide": "GitHub developer build and setup guide", + "options.chatgptPlan.savedDevChoice": "ChatGPT subscription (developer edition only; choose another service)", "options.chatgptPlan.title": "Provider · ChatGPT subscription", "options.chatgptPlan.summary": "Connect through the separately installed Native Messaging Host. Access tokens stay in system credential storage and never enter the extension.", "options.chatgptPlan.notChecked": "Connection has not been checked. The host starts only after you request a status check or connect.", @@ -74,6 +78,10 @@ export const en = Object.freeze({ export const zh_CN = Object.freeze({ "options.chatgptPlan.providerName": "ChatGPT 订阅", + "options.chatgptPlan.devOnlyTitle": "ChatGPT 订阅 · 开发者预览", + "options.chatgptPlan.devOnlyDescription": "公开版暂不提供 ChatGPT 订阅登录。需要体验此实验功能,请使用 GitHub 开发版并自行安装、配置本地组件。", + "options.chatgptPlan.devGuide": "GitHub 开发版及配置说明", + "options.chatgptPlan.savedDevChoice": "ChatGPT 订阅(仅开发版可用,请选择其他服务)", "options.chatgptPlan.title": "Provider · ChatGPT 订阅", "options.chatgptPlan.summary": "通过单独安装的 Native Messaging Host 连接。访问令牌保存在系统凭据存储中,不会进入扩展。", "options.chatgptPlan.notChecked": "尚未检查连接。只有你主动检查状态或连接时才会启动 host。", From 2f1a03a9cd524b8af21d2f01005f9c05c44e0179 Mon Sep 17 00:00:00 2001 From: CoderLambert Date: Wed, 21 Oct 2026 03:12:46 -0700 Subject: [PATCH 12/25] test(release): assert public ChatGPT isolation and developer build support --- tests/chatgpt-plan-provider.test.mjs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/chatgpt-plan-provider.test.mjs b/tests/chatgpt-plan-provider.test.mjs index e169ec35..988bc847 100644 --- a/tests/chatgpt-plan-provider.test.mjs +++ b/tests/chatgpt-plan-provider.test.mjs @@ -20,8 +20,20 @@ function fixtureClient(reply) { }; } -test("ChatGPT subscription is registered as a shared Provider", () => { - assert.equal(getProvider({ provider: "chatgpt-plan" }).id, "chatgpt-plan"); +test("ChatGPT subscription is registered only for the developer extension", () => { + const previous = globalThis.chrome; + try { + globalThis.chrome = { runtime: { getManifest: () => ({ version: "0.8.47" }) } }; + assert.throws(() => getProvider({ provider: "chatgpt-plan" }), { code: "CHATGPT_PLAN_DEV_ONLY" }); + assert.equal(getProvider({ provider: "deepseek" }).id, "deepseek"); + globalThis.chrome = { runtime: { getManifest: () => ({ + version: "0.8.47", version_name: "0.8.47-dev", optional_permissions: ["nativeMessaging"] + }) } }; + assert.equal(getProvider({ provider: "chatgpt-plan" }).id, "chatgpt-plan"); + } finally { + if (previous === undefined) delete globalThis.chrome; + else globalThis.chrome = previous; + } }); test("translation uses the injected host once and validates exact response IDs", async () => { From 8688434b24f4a05d708acd406c183bec4441764b Mon Sep 17 00:00:00 2001 From: CoderLambert Date: Wed, 21 Oct 2026 03:12:49 -0700 Subject: [PATCH 13/25] test(release): assert public ChatGPT isolation and developer build support --- tests/wxt-assets.test.mjs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/wxt-assets.test.mjs b/tests/wxt-assets.test.mjs index 4b2507e5..517af2ee 100644 --- a/tests/wxt-assets.test.mjs +++ b/tests/wxt-assets.test.mjs @@ -7,6 +7,7 @@ import { assertAssetPath, isInsideSourceRoot, runtimeAssetRoots, sourceClosure } import { CONTENT_SCRIPT_FILES, CONTENT_STYLE_FILES } from "../src/shared/constants.js"; import { EXTENSION_PAGES, WORKER_PATHS, YOUTUBE_MAIN_BRIDGE_FILES } from "../src/shared/runtime-assets.js"; import { GLOBAL_CONTENT_SCRIPT, projectProductionManifest } from "../scripts/production-manifest.mjs"; +import { isChatGPTPlanDeveloperBuild, requireChatGPTPlanDeveloperBuild } from "../src/shared/chatgpt-native-host.js"; import { YOUTUBE_MAIN_BRIDGE_FILES as runtimeMain } from "../src/background/youtube-bridge.js"; import { assertProductionManifest } from "../scripts/audit-wxt-extension.mjs"; @@ -56,6 +57,24 @@ test("bridge follows only relative imports and rejects symlink escapes", async ( } finally { await rm(root, { recursive: true, force: true }); await rm(outside, { recursive: true, force: true }); } }); +test("ChatGPT developer edition is explicitly marked without changing standard site permissions", async () => { + const baseline = JSON.parse(await readFile(new URL("../manifest.json", import.meta.url), "utf8")); + const publicManifest = projectProductionManifest(baseline); + const developerManifest = projectProductionManifest(baseline, { developer: true }); + assert.equal(publicManifest.version_name, undefined); + assert.equal(developerManifest.version_name, `${baseline.version}-dev`); + assert.equal(isChatGPTPlanDeveloperBuild(publicManifest), false); + assert.equal(isChatGPTPlanDeveloperBuild(developerManifest), true); + assert.deepEqual(developerManifest.host_permissions, publicManifest.host_permissions); + assert.deepEqual(developerManifest.permissions, publicManifest.permissions); + assert.throws(() => requireChatGPTPlanDeveloperBuild(publicManifest), { code: "CHATGPT_PLAN_DEV_ONLY" }); + assert.doesNotThrow(() => requireChatGPTPlanDeveloperBuild(developerManifest)); + const contentScripts = [{ matches: [...GLOBAL_CONTENT_SCRIPT.matches], js: [...CONTENT_SCRIPT_FILES], + css: [...CONTENT_STYLE_FILES], run_at: "document_idle" }]; + assertProductionManifest({ ...developerManifest, content_scripts: contentScripts }, baseline, { developer: true }); + assert.throws(() => assertProductionManifest({ ...developerManifest, content_scripts: contentScripts }, baseline)); +}); + test("production Manifest fails closed for permissions, static injection and development changes", async () => { const baseline = JSON.parse(await readFile(new URL("../manifest.json", import.meta.url), "utf8")); const production = { ...projectProductionManifest(baseline), content_scripts: [{ From be44a289147fb725e089c9be55c3841062b300a0 Mon Sep 17 00:00:00 2001 From: CoderLambert Date: Wed, 21 Oct 2026 03:13:24 -0700 Subject: [PATCH 14/25] test(ui): distinguish public notice and developer native-host flow --- tests/unit/options-react.test.tsx | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/unit/options-react.test.tsx b/tests/unit/options-react.test.tsx index 5a6967b5..9515983e 100644 --- a/tests/unit/options-react.test.tsx +++ b/tests/unit/options-react.test.tsx @@ -3,7 +3,7 @@ import { StrictMode } from "react"; import { cleanup, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, expect, test, vi } from "vitest"; -import { GeneralSection } from "../../src/options/CommonSections"; +import { GeneralSection, ProviderSections } from "../../src/options/CommonSections"; import { ChatGPTPlanSection, chatGPTSignInActionDisabled, nativeHostDevelopmentCommands, nativeHostDevelopmentTarget } from "../../src/options/ChatGPTPlanSection"; import { GlossarySection } from "../../src/options/GlossarySection"; import { ProviderConnectionsSection } from "../../src/options/ProviderConnectionsSection"; @@ -39,6 +39,25 @@ function stubNativeHostTarget(platform: string, userAgent: string) { }; } +test("public Provider settings show only the ChatGPT developer guide and preserve a previous selection", () => { + vi.stubGlobal("chrome", { ...chrome, runtime: { getManifest: () => ({ version: "0.8.47" }) } }); + const chatGPTPlanAction = vi.fn(); + const baseState = { version: 1, defaultService: { kind: "chatgpt-plan" }, connections: [] }; + render(zhNode()); + expect(screen.getByText("ChatGPT 订阅 · 开发者预览")).toBeTruthy(); + expect(screen.getByText(/公开版暂不提供 ChatGPT 订阅登录/u)).toBeTruthy(); + const guide = screen.getByRole("link", { name: "GitHub 开发版及配置说明" }); + expect(guide.getAttribute("href")).toBe("https://github.com/CoderLambert/translateflow-plugin/blob/main/docs/CHATGPT_DEVELOPMENT_BUILD.md"); + expect(screen.queryByRole("button", { name: "检查状态并加载模型" })).toBeNull(); + const selected = document.querySelector("#defaultService") as HTMLSelectElement; + expect(selected.value).toBe("chatgpt-plan"); + expect(selected.querySelector('option[value="chatgpt-plan"]')?.disabled).toBe(true); + expect(chatGPTPlanAction).not.toHaveBeenCalled(); +}); + test("provider connection test feedback stays visible in the tested card", async () => { let finish: ((value: string) => void) | undefined; const testProviderConnection = vi.fn(() => new Promise((resolve) => { finish = resolve; })); From 954ed042461dd65d015de93d1a094c5a1c9220f4 Mon Sep 17 00:00:00 2001 From: CoderLambert Date: Wed, 21 Oct 2026 03:13:27 -0700 Subject: [PATCH 15/25] test(ui): distinguish public notice and developer native-host flow --- e2e/chatgpt-plan-nativehost.spec.mjs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/e2e/chatgpt-plan-nativehost.spec.mjs b/e2e/chatgpt-plan-nativehost.spec.mjs index 4f9cfa96..51b10fc6 100644 --- a/e2e/chatgpt-plan-nativehost.spec.mjs +++ b/e2e/chatgpt-plan-nativehost.spec.mjs @@ -5,9 +5,26 @@ test.use({ nativeMessagingPregranted: true }); test.describe("ChatGPT Plan native host Settings", () => { test.beforeEach(async ({ harness }) => { await harness.reset(); }); + test("public WXT shows a GitHub developer guide rather than account controls", async ({ harness }) => { + const page = await harness.context.newPage(); + await page.goto(`chrome-extension://${harness.extensionId}/options.html#provider`); + const manifest = await page.evaluate(() => chrome.runtime.getManifest()); + test.skip(manifest.version_name === `${manifest.version}-dev`, "Public edition only"); + const section = page.locator("#settings-panel-provider"); + await expect(section.getByText("ChatGPT 订阅 · 开发者预览")).toBeVisible(); + await expect(section.getByRole("link", { name: "GitHub 开发版及配置说明" })) + .toHaveAttribute("href", "https://github.com/CoderLambert/translateflow-plugin/blob/main/docs/CHATGPT_DEVELOPMENT_BUILD.md"); + await expect(section.getByRole("button", { name: "检查状态并加载模型" })).toHaveCount(0); + await expect(section.getByRole("button", { name: "连接 ChatGPT 账号" })).toHaveCount(0); + await expect(section.getByRole("option", { name: "ChatGPT 订阅", exact: true })).toHaveCount(0); + await page.close(); + }); + test("the real WXT Settings surface recovers when the user-level host is absent", async ({ harness }, testInfo) => { const page = await harness.context.newPage(); await page.goto(`chrome-extension://${harness.extensionId}/options.html#provider`); + const manifest = await page.evaluate(() => chrome.runtime.getManifest()); + test.skip(manifest.version_name !== `${manifest.version}-dev`, "Developer edition only"); const section = page.getByRole("heading", { name: "Provider · ChatGPT 订阅", exact: true }).locator(".."); await expect(section).toBeVisible(); From 14b7cc281e73b189b8388783ed4fd28a84e5f4d2 Mon Sep 17 00:00:00 2001 From: CoderLambert Date: Wed, 21 Oct 2026 03:14:21 -0700 Subject: [PATCH 16/25] docs(release): self-built ChatGPT developer edition and host setup --- docs/CHATGPT_DEVELOPMENT_BUILD.md | 62 +++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 docs/CHATGPT_DEVELOPMENT_BUILD.md diff --git a/docs/CHATGPT_DEVELOPMENT_BUILD.md b/docs/CHATGPT_DEVELOPMENT_BUILD.md new file mode 100644 index 00000000..efd63b95 --- /dev/null +++ b/docs/CHATGPT_DEVELOPMENT_BUILD.md @@ -0,0 +1,62 @@ +# TranslateFlow ChatGPT 订阅实验开发版 + +> 开发者预览;**不是** Chrome Web Store 发行版。此功能需要使用自己的本机 Native Host、符合条件的 ChatGPT 账号及官方授权。实验实现或模拟测试通过不代表已获得公共应用接入许可、真实套餐权限或商店审核通过。 + +## 公开版与开发版 + +- **公开版(默认)**:保留普通网页、划词、字幕、本地词典、API Key/自托管模型翻译;在设置 → Provider 只显示 ChatGPT 订阅开发者预览提示及本页链接。隐藏登录、模型和 Host 命令。保存过的订阅配置不会被删除,但公开版拒绝订阅推理。 +- **开发版(单独构建)**:扩展 Manifest 额外带 `version_name: "-dev"`,解锁现有 ChatGPT 订阅设置和 Native Messaging 连接。Chrome 应使用“加载已解压的扩展程序”安装,不属于 Chrome 商店自动更新渠道。 +- 两个版本使用同一份 Provider/缓存/页面代码,不创建第二套翻译引擎;默认 `npm run build:extension` 和 `npm run build:extension:release` 始终生成公开版。 + +## 获取开发版 ZIP(GitHub Actions) + +在项目 [GitHub Actions — ChatGPT developer extension](https://github.com/CoderLambert/translateflow-plugin/actions/workflows/chatgpt-dev-build.yml) 页面,打开一次成功的运行并下载 `translateflow-chatgpt-developer-` Artifact(通常需登录 GitHub)。压缩包中含独立的 `translateflow-extension-dev.zip`、`SHA256SUMS` 和 `SOURCE_COMMIT.txt`。 + +如果暂无成功运行,维护者可从默认分支手动运行该 workflow;也可以按下方从源码构建。不要下载来源不明的“已配置好 ChatGPT 账号”的扩展包。 + +解压 `translateflow-extension-dev.zip` 到一个空目录,确认目录根部有 `manifest.json`,并确认其中 `version_name` 以 `-dev` 结尾。在 Chrome 打开 `chrome://extensions`、启用开发者模式,点击“加载已解压的扩展程序”选择此目录。复制页面显示的 **32 位扩展 ID**。本地安装路径变化或重新安装可能产生不同 ID,Host 必须使用这次实际显示的 ID。 + +## 自行从源码构建 + +环境:Node.js 24.21.x、npm 11.19.x;从可信 Git 提交获取源码: + +```bash +git clone https://github.com/CoderLambert/translateflow-plugin.git +cd translateflow-plugin +npm ci +npm run build:extension:dev +``` + +输出在 `dist/extension-dev/`,将该目录加载为 unpacked 扩展。默认公开构建仍在 `dist/extension/`,互不覆盖。需要自己压缩时,应从 `dist/extension-dev` 目录内部创建 ZIP,使 `manifest.json` 位于归档根部。 + +## Native Host 本机配置(需要 Go 1.26+) + +Native Host **不随扩展 ZIP 自动安装**,账号令牌不进入扩展 ZIP。当前尚无面向公众的已签名稳定 Host 可下载,开发者需自行从可信源码构建并进行当前用户级安装。 + +在**同一个仓库 checkout** 里,在扩展安装后将 `<扩展ID>` 换成 `chrome://extensions` 当前显示的 ID: + +```bash +TF_CHROME_EXTENSION_ID=<扩展ID> TF_NATIVE_HOST_VERSION=0.8.47 npm run build:native-host:candidate +``` + +`TF_NATIVE_HOST_VERSION` 必须与当前候选扩展版本相符;更新版本后请按实际 `manifest.json` 更新它。构建产物位于 `.output/native-host-candidate/`,生成 `release-candidate.json`、`SHA256SUMS` 和适用于 Linux/Windows x64 的候选二进制。候选标记为 `publishable:false`、`NOT_SIGNED`,**不得当作已签名公共安装包分发或绕过系统的未知发布者警告**。 + +Linux(Google Chrome)自建示例: + +```bash +./.output/native-host-candidate/translateflow-host-0.8.47-linux-amd64 install --extension-id <扩展ID> --browser chrome +``` + +如果使用 Chromium 而不是 Chrome,将最后一个参数改为 `--browser chromium`。Windows 在 PowerShell 中构建候选后,使用相应 `windows-amd64.exe install --extension-id <扩展ID>` 安装。具体平台、安装注册位置和安全边界见 [Native Host 安装文档](../native-host/INSTALLATION.md)。 + +Linux 图形会话还必须提供已解锁的 Secret Service;Windows 使用 DPAPI。插件不会替用户安装或解锁密钥环。Native Host 的卸载不会自动清除账号凭据,退出账号请先使用插件的“退出登录”操作。 + +## 使用与安全边界 + +1. 在开发版设置 → Provider → ChatGPT 订阅,明确点击启用 `nativeMessaging` 权限。 +2. 检测 Host 后,点击“连接 ChatGPT 账号”,在官方授权页确认 `chatgpt.tokens.use.direct` 等实际授权范围。 +3. 授权完成后从账号返回的列表选择模型、保存设置,再用非隐私测试句验证翻译。测试前不要输入账号密钥、网页私人内容或未脱敏日志。 +4. 服务可能因账号资格、套餐额度、地区、模型或 OpenAI 接入审批限制而拒绝请求;**不通过私人网页登录态、Cookie、非官方接口绕过限制**。 +5. OAuth 真实授权、套餐用量、系统安全存储与 Windows/Linux 安装均须由开发者在自己的环境中明确执行;自动化测试使用模拟输入,不触发真实账号操作。 + +Chrome Web Store 发行版不承诺此功能可用。开发者版独立试验不影响普通 API Key、Chrome 本地翻译、词典和字幕功能。 From cb5ca57d1a959dc124d33784482a579db8bc1e78 Mon Sep 17 00:00:00 2001 From: CoderLambert Date: Wed, 21 Oct 2026 03:15:00 -0700 Subject: [PATCH 17/25] ci(release): generate audited ChatGPT developer ZIP on PR or request --- .github/workflows/chatgpt-dev-build.yml | 94 +++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 .github/workflows/chatgpt-dev-build.yml diff --git a/.github/workflows/chatgpt-dev-build.yml b/.github/workflows/chatgpt-dev-build.yml new file mode 100644 index 00000000..c9bb92f4 --- /dev/null +++ b/.github/workflows/chatgpt-dev-build.yml @@ -0,0 +1,94 @@ +name: chatgpt-dev-build + +on: + workflow_dispatch: + pull_request: + paths: + - ".github/workflows/chatgpt-dev-build.yml" + - "scripts/build-extension.mjs" + - "scripts/production-manifest.mjs" + - "scripts/audit-wxt-extension.mjs" + - "src/options/**" + - "src/background/providers/**" + - "src/background/router.js" + - "src/shared/chatgpt-native-host.js" + - "src/i18n/catalog-options-chatgpt.js" + - "wxt.config.mjs" + - "tests/chatgpt-plan-provider.test.mjs" + - "tests/wxt-assets.test.mjs" + - "e2e/chatgpt-plan-nativehost.spec.mjs" + - "package.json" + - "docs/CHATGPT_DEVELOPMENT_BUILD.md" + +permissions: + contents: read + +jobs: + developer-artifact: + runs-on: ubuntu-latest + timeout-minutes: 35 + env: + TF_TEST_WORKERS: "1" + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "24.21.0" + - run: npm ci --no-audit --no-fund + - name: Focused release and provider checks + run: node --test tests/wxt-assets.test.mjs tests/chatgpt-plan-provider.test.mjs + - name: Public production package + run: npm run build:extension:release + - name: Audited ChatGPT developer package + run: npm run build:extension:dev + - name: Verify both Manifest editions + run: | + node --input-type=module <<'NODE' + import { readFileSync } from 'node:fs'; + import assert from 'node:assert/strict'; + const pub = JSON.parse(readFileSync('dist/extension/manifest.json', 'utf8')); + const dev = JSON.parse(readFileSync('dist/extension-dev/manifest.json', 'utf8')); + assert.equal(pub.version_name, undefined); + assert.equal(dev.version_name, `${dev.version}-dev`); + assert.deepEqual(dev.host_permissions, pub.host_permissions); + assert.deepEqual(dev.permissions, pub.permissions); + assert.deepEqual(dev.optional_permissions, pub.optional_permissions); + NODE + - name: Prepare Chromium + run: npx playwright install --with-deps chromium + - name: Public ChatGPT UI regression + env: + TF_E2E_ARTIFACT: dist/extension + TF_I18N_ARTIFACT: dist/extension + run: npm run test:e2e -- e2e/chatgpt-plan-nativehost.spec.mjs + - name: Developer ChatGPT UI regression + env: + TF_E2E_ARTIFACT: dist/extension-dev + TF_I18N_ARTIFACT: dist/extension-dev + run: npm run test:e2e -- e2e/chatgpt-plan-nativehost.spec.mjs + - name: Pack verified unpacked developer edition + run: | + mkdir -p artifacts + (cd dist/extension-dev && zip -qr ../../artifacts/translateflow-extension-dev.zip .) + unzip -p artifacts/translateflow-extension-dev.zip manifest.json | node -e ' + let s=""; process.stdin.on("data",c=>s+=c).on("end",()=>{ + const m=JSON.parse(s); + if (m.version_name !== m.version+"-dev") process.exit(1); + });' + (cd artifacts && sha256sum translateflow-extension-dev.zip > SHA256SUMS) + git rev-parse HEAD > artifacts/SOURCE_COMMIT.txt + - name: Upload developer extension and checksums + uses: actions/upload-artifact@v4 + with: + name: translateflow-chatgpt-developer-${{ github.run_id }} + path: artifacts/ + if-no-files-found: error + retention-days: 30 + - name: Upload focused E2E evidence + if: always() + uses: actions/upload-artifact@v4 + with: + name: chatgpt-dev-e2e-${{ github.run_id }} + path: test-results/ + if-no-files-found: ignore + retention-days: 7 From 3af23e277f83e3083bd6f4f5942340fda386e3af Mon Sep 17 00:00:00 2001 From: CoderLambert Date: Wed, 21 Oct 2026 03:17:29 -0700 Subject: [PATCH 18/25] fix(release): detect developer edition after test-only native permission pregrant --- src/shared/chatgpt-native-host.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/shared/chatgpt-native-host.js b/src/shared/chatgpt-native-host.js index 19caa9f7..afbb1f20 100644 --- a/src/shared/chatgpt-native-host.js +++ b/src/shared/chatgpt-native-host.js @@ -8,8 +8,7 @@ export const CHATGPT_PLAN_DEV_GUIDE_URL = "https://github.com/CoderLambert/trans // Accept manifest input rather than reading chrome.* inside a shared pure module. export function isChatGPTPlanDeveloperBuild(manifest) { return Boolean(manifest && typeof manifest.version === "string" && - manifest.version_name === `${manifest.version}-dev` && - manifest.optional_permissions?.includes("nativeMessaging")); + manifest.version_name === `${manifest.version}-dev`); } export function requireChatGPTPlanDeveloperBuild(manifest) { From 5f375df7eee0343917757c8a5cd2684e8b1547eb Mon Sep 17 00:00:00 2001 From: CoderLambert Date: Wed, 21 Oct 2026 03:17:32 -0700 Subject: [PATCH 19/25] test(release): developer marker survives approved E2E permission projection --- tests/wxt-assets.test.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/wxt-assets.test.mjs b/tests/wxt-assets.test.mjs index 517af2ee..3fe82fd5 100644 --- a/tests/wxt-assets.test.mjs +++ b/tests/wxt-assets.test.mjs @@ -65,6 +65,8 @@ test("ChatGPT developer edition is explicitly marked without changing standard s assert.equal(developerManifest.version_name, `${baseline.version}-dev`); assert.equal(isChatGPTPlanDeveloperBuild(publicManifest), false); assert.equal(isChatGPTPlanDeveloperBuild(developerManifest), true); + assert.equal(isChatGPTPlanDeveloperBuild({ ...developerManifest, + permissions: [...developerManifest.permissions, "nativeMessaging"], optional_permissions: undefined }), true); assert.deepEqual(developerManifest.host_permissions, publicManifest.host_permissions); assert.deepEqual(developerManifest.permissions, publicManifest.permissions); assert.throws(() => requireChatGPTPlanDeveloperBuild(publicManifest), { code: "CHATGPT_PLAN_DEV_ONLY" }); From e9f3b841ce6a6fb4c0ef1aa94ea0372a1dbf6178 Mon Sep 17 00:00:00 2001 From: CoderLambert Date: Tue, 27 Oct 2026 23:48:06 +0800 Subject: [PATCH 20/25] chore(build): integrate removal of legacy size budget --- docs/WXT_COMPAT_V1.md | 2 ++ scripts/audit-wxt-extension.mjs | 12 ++---------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/docs/WXT_COMPAT_V1.md b/docs/WXT_COMPAT_V1.md index 142e83bf..d8e26f1c 100644 --- a/docs/WXT_COMPAT_V1.md +++ b/docs/WXT_COMPAT_V1.md @@ -15,6 +15,8 @@ npm run test:wxt:smoke WXT compiles Background, Content, Popup, Options and the learning center from their existing owners. Only mapped MAIN/Worker relative-import closures and locales remain raw assets. `src/shared/runtime-assets.js` owns the stable runtime paths. The package audit rejects undeclared public assets and preserves source confinement, exact Worker/MAIN bytes, manifest permissions and compiled React boundaries. Source locks, corpora, compilers, docs and tests stay outside the package. +Package size, platform code size and runtime/UI closure sizes are reported as measurements. Builds have no package-size threshold or advisory budget; the former #245 platform-code budget and its overage warnings are retired. Historical budget figures below remain evidence of the original migration. + The default empty dictionary library is normal. Users explicitly download or import dictionaries; production never probes missing bundled resources. Smoke and browser tests start from actual audited production bytes and explicitly prepare synthetic user dictionaries in isolated storage for tests requiring hits. They do not add runtime dictionary assets to the extension copy. Content remains an ISOLATED classic/IIFE registry compiled to `content-scripts/content.js/.css`; MAIN and Workers retain their existing contexts. Permissions, OPFS schemas and user settings are unaffected by dictionary content removal. diff --git a/scripts/audit-wxt-extension.mjs b/scripts/audit-wxt-extension.mjs index 9f8b098c..3ff4ad77 100644 --- a/scripts/audit-wxt-extension.mjs +++ b/scripts/audit-wxt-extension.mjs @@ -82,16 +82,9 @@ export async function auditWxtExtension({ output = resolve(ROOT, ".output/chrome } } } - const codeBudgetBytes = 1576595; // #245: old code + max(10%, 100 KiB), excluding dictionary data. const uiExclusive = summary.files.filter(entry => approvedUiCompiled.has(entry.path) && !nonUiCompiled.has(entry.path)); const uiBytes = uiExclusive.reduce((sum, entry) => sum + entry.size, 0); const platformCodeBytes = summary.codeBytes - uiBytes; - const codeBudgetExceeded = platformCodeBytes > codeBudgetBytes; - const codeBudgetOverageBytes = Math.max(0, platformCodeBytes - codeBudgetBytes); - const codeBudgetWarning = codeBudgetExceeded - ? `Platform code exceeds #245 advisory budget by ${codeBudgetOverageBytes} bytes (${platformCodeBytes}/${codeBudgetBytes}).` - : null; - if (codeBudgetWarning) console.warn(`WARN: ${codeBudgetWarning}`); const sizes = new Map(summary.files.map((entry) => [entry.path, entry.size])); const chunks = new Map(compiled.map((item) => [item.fileName, item])); function compiledClosure(roots) { @@ -108,10 +101,9 @@ export async function auditWxtExtension({ output = resolve(ROOT, ".output/chrome const uiClosure = compiledClosure((await Promise.all(Object.values(EXTENSION_PAGES).map(pageRoots))).flat()); uiClosure.htmlBytes = Object.values(EXTENSION_PAGES).reduce((sum, path) => sum + sizes.get(path), 0); uiClosure.bytes += uiClosure.htmlBytes; - const report = { status: codeBudgetExceeded ? "PASS_WITH_WARNING" : "PASS", source: "WXT production .output/chrome-mv3", manifestDifferences: ["content_scripts"], + const report = { status: "PASS", source: "WXT production .output/chrome-mv3", manifestDifferences: ["content_scripts"], runtimeFiles: runtime.length, compiledContent: { js: [...CONTENT_SCRIPT_FILES], css: [...CONTENT_STYLE_FILES] }, ...summary, - codeBudgetBytes, platformCodeBytes, codeBudgetExceeded, codeBudgetOverageBytes, - warnings: codeBudgetWarning ? [codeBudgetWarning] : [], + platformCodeBytes, warnings: [], reactPageClosures: Object.fromEntries([...pageCompiled].map(([name, files]) => [name, [...files].sort()])), uiExclusiveClosure: { files: uiExclusive.map(entry => entry.path), bytes: uiBytes }, backgroundClosure, uiClosure, compiledOutputs: compiled.map((item) => ({ fileName: item.fileName, type: item.type })) }; From e2b8e62ab630eef7d8f12219728dea3b4246a027 Mon Sep 17 00:00:00 2001 From: CoderLambert Date: Sun, 18 Oct 2026 10:52:05 +0800 Subject: [PATCH 21/25] feat(translation): add semantic and format source contract --- src/shared/translation-source.js | 109 +++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 src/shared/translation-source.js diff --git a/src/shared/translation-source.js b/src/shared/translation-source.js new file mode 100644 index 00000000..0a47dfaf --- /dev/null +++ b/src/shared/translation-source.js @@ -0,0 +1,109 @@ +import { normalizeSourceText } from "./text.js"; + +const MARKERS = /⟦TF:(\d+):(S|E)⟧/g; +const TAGS = new Set(["a", "strong", "em", "code", "kbd", "mark"]); +const PROTECTED_TAGS = new Set(["code", "kbd"]); + +/** + * An auxiliary, deterministic source contract; NOT a Provider request, an HTML + * parser or a translation cache key. Keeping providerText untouched preserves + * the existing exact-cache protocol until a later, separately gated wave. + * + * @param {{text?: unknown, rich?: boolean, descriptors?: unknown[]}} source + */ +export function createTranslationUnitV1(source) { + const providerText = String(source?.text ?? ""); + const rich = source?.rich === true; + const inputDescriptors = source?.descriptors ?? []; + if (!Array.isArray(inputDescriptors) || (!rich && inputDescriptors.length)) { + throw new TypeError("invalid translation source descriptors"); + } + + const descriptors = inputDescriptors.map((descriptor) => { + const tag = descriptor?.tag; + if (!TAGS.has(tag)) throw new TypeError("unsupported source descriptor"); + const copy = { tag }; + if (PROTECTED_TAGS.has(tag)) { + if (typeof descriptor.protectedText !== "string") { + throw new TypeError("missing protected source text"); + } + copy.protectedText = descriptor.protectedText; + } + if (tag === "a") { + if (typeof descriptor.href === "string") copy.href = descriptor.href; + if (typeof descriptor.title === "string") copy.title = descriptor.title; + } + return Object.freeze(copy); + }); + + let semanticText; + let protectedSpans = []; + let layout = []; + if (!rich) { + // Literal marker-looking text in ordinary user content stays literal. + semanticText = normalizeSourceText(providerText); + } else { + if (!descriptors.length) throw new TypeError("structured text has no descriptors"); + const stack = []; + const opened = new Set(); + const closed = new Set(); + const raw = []; + const ranges = []; + let rawOffset = 0; + let lastIndex = 0; + for (const match of providerText.matchAll(MARKERS)) { + const chunk = providerText.slice(lastIndex, match.index); + if (chunk.includes("⟦TF:")) throw new TypeError("ambiguous source marker"); + raw.push(chunk); + rawOffset += chunk.length; + + const id = Number(match[1]); + const kind = match[2]; + if (!Number.isSafeInteger(id) || !descriptors[id]) throw new TypeError("unknown source marker"); + layout.push(Object.freeze([rawOffset, id, kind])); + if (kind === "S") { + if (opened.has(id)) throw new TypeError("duplicate source marker"); + opened.add(id); + stack.push({ id, start: rawOffset }); + } else { + const top = stack.pop(); + if (!top || top.id !== id || closed.has(id)) throw new TypeError("unbalanced source marker"); + closed.add(id); + if (PROTECTED_TAGS.has(descriptors[id].tag)) { + ranges.push({ descriptorId: id, tag: descriptors[id].tag, start: top.start, end: rawOffset }); + } + } + lastIndex = match.index + match[0].length; + } + const tail = providerText.slice(lastIndex); + if (tail.includes("⟦TF:") || stack.length || opened.size !== descriptors.length || closed.size !== descriptors.length) { + throw new TypeError("incomplete source markers"); + } + raw.push(tail); + const semanticRaw = raw.join(""); + semanticText = normalizeSourceText(semanticRaw); + protectedSpans = ranges.map(({ descriptorId, tag, start, end }) => { + const text = descriptors[descriptorId].protectedText; + if (normalizeSourceText(semanticRaw.slice(start, end)) !== normalizeSourceText(text)) { + throw new TypeError("protected source text mismatch"); + } + // The sentinel accounts for whitespace immediately before an inline tag, + // which trim() would otherwise remove from a prefix-only calculation. + const offset = (position) => normalizeSourceText(`${semanticRaw.slice(0, position)}\uE000`).length - 1; + return Object.freeze({ descriptorId, tag, start: offset(start), end: offset(end), text }); + }).sort((a, b) => a.start - b.start || a.descriptorId - b.descriptorId); + } + + const unit = { + version: 1, + semanticText, + providerText, + format: rich ? "structured" : "plain", + descriptors: Object.freeze(descriptors), + protectedSpans: Object.freeze(protectedSpans), + // Comparison signatures, deliberately NOT persisted IndexedDB keys. + semanticIdentity: JSON.stringify(["tf.semantic.v1", semanticText]), + formatIdentity: JSON.stringify(["tf.format.v1", rich ? "structured" : "plain", layout, descriptors]) + }; + return Object.freeze(unit); +} From 4e85c815a0a2d9ca3d9b296f25ed69b531338d89 Mon Sep 17 00:00:00 2001 From: CoderLambert Date: Sun, 18 Oct 2026 10:52:46 +0800 Subject: [PATCH 22/25] test(translation): cover semantic source identity and protection --- tests/translation-source.test.mjs | 76 +++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 tests/translation-source.test.mjs diff --git a/tests/translation-source.test.mjs b/tests/translation-source.test.mjs new file mode 100644 index 00000000..dd3105e1 --- /dev/null +++ b/tests/translation-source.test.mjs @@ -0,0 +1,76 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createTranslationUnitV1 } from "../src/shared/translation-source.js"; + +const text = "It is important to understand that useMemo does not prevent re-rendering."; +const rich = { + text: "It is important to understand that ⟦TF:0:S⟧useMemo⟦TF:0:E⟧ does not prevent re-rendering.", + rich: true, + descriptors: [{ tag: "code", protectedText: "useMemo" }] +}; + +test("plain and rich expose identical semantic identity, distinct format identity and untouched Provider text", () => { + const plainUnit = createTranslationUnitV1({ text, rich: false, descriptors: [] }); + const richUnit = createTranslationUnitV1(rich); + assert.equal(plainUnit.semanticText, richUnit.semanticText); + assert.equal(plainUnit.semanticIdentity, richUnit.semanticIdentity); + assert.notEqual(plainUnit.formatIdentity, richUnit.formatIdentity); + assert.equal(richUnit.providerText, rich.text); + assert.equal(richUnit.protectedSpans[0].text, "useMemo"); + assert.equal(richUnit.semanticText.slice(richUnit.protectedSpans[0].start, richUnit.protectedSpans[0].end), "useMemo"); + assert.equal(plainUnit.providerText, text); +}); + +test("descriptors preserve ordered code/kbd with strong and trusted source link metadata", () => { + const unit = createTranslationUnitV1({ + text: "Press ⟦TF:0:S⟧Ctrl⟦TF:0:E⟧ and ⟦TF:1:S⟧useMemo⟦TF:1:E⟧; then read ⟦TF:2:S⟧⟦TF:3:S⟧docs⟦TF:3:E⟧⟦TF:2:E⟧.", + rich: true, + descriptors: [ + { tag: "kbd", protectedText: "Ctrl" }, + { tag: "code", protectedText: "useMemo" }, + { tag: "strong" }, + { tag: "a", href: "https://example.test/help", title: "Help", onclick: "not copied" } + ] + }); + assert.equal(unit.semanticText, "Press Ctrl and useMemo; then read docs."); + assert.deepEqual(unit.protectedSpans.map((span) => unit.semanticText.slice(span.start, span.end)), ["Ctrl", "useMemo"]); + assert.equal(unit.descriptors[3].href, "https://example.test/help"); + assert.equal(Object.hasOwn(unit.descriptors[3], "onclick"), false); + assert.ok(Object.isFrozen(unit.descriptors[3])); +}); + +test("repeated technical tokens remain distinct protected spans", () => { + const unit = createTranslationUnitV1({ + text: "⟦TF:0:S⟧useMemo⟦TF:0:E⟧ and ⟦TF:1:S⟧useMemo⟦TF:1:E⟧", + rich: true, + descriptors: [{ tag: "code", protectedText: "useMemo" }, { tag: "code", protectedText: "useMemo" }] + }); + assert.deepEqual(unit.protectedSpans.map((p) => [p.start,p.end]), [[0,7],[12,19]]); +}); + +test("plain literal marker-like user text is preserved, structured collisions are rejected", () => { + const plain = createTranslationUnitV1({ text: "Literal ⟦TF:0:S⟧ should survive", rich: false }); + assert.equal(plain.semanticText, "Literal ⟦TF:0:S⟧ should survive"); + assert.throws(() => createTranslationUnitV1({ ...rich, text: "Literal ⟦TF:0:S⟧ " + rich.text }), /duplicate|ambiguous|incomplete/); +}); + +test("unknown, missing, duplicated, crossed or forged markers fail closed", () => { + const invalid = [ + rich.text.replace("⟦TF:0:E⟧", ""), + rich.text.replace("⟦TF:0:E⟧", "⟦TF:0:E⟧⟦TF:0:E⟧"), + rich.text.replace("⟦TF:0:E⟧", "⟦TF:1:E⟧"), + rich.text.replace("⟦TF:0:S⟧", "⟦TF:0:X⟧"), + rich.text.replace("useMemo", "fakeUseMemo") + ]; + for (const providerText of invalid) { + assert.throws(() => createTranslationUnitV1({ ...rich, text: providerText }), TypeError); + } + assert.throws(() => createTranslationUnitV1({ text: "hi", rich: false, descriptors: [{tag:"code"}] }), TypeError); +}); + +test("semantic normalization only affects comparison, not Provider payload", () => { + const original = " It\t is important\n"; + const unit = createTranslationUnitV1({ text: original, rich: false }); + assert.equal(unit.providerText, original); + assert.equal(unit.semanticText, "It is important"); +}); From b7e781b9877cab3db19419a4940cfbee1cb357d8 Mon Sep 17 00:00:00 2001 From: CoderLambert Date: Tue, 27 Oct 2026 23:49:37 +0800 Subject: [PATCH 23/25] build: prepare integrated 0.8.48 public and developer editions --- .github/workflows/chatgpt-dev-build.yml | 17 ------ README.md | 2 +- docs/CHATGPT_DEVELOPMENT_BUILD.md | 4 +- docs/tasks/index.json | 7 +++ docs/tasks/latest-main-build/state.json | 72 +++++++++++++++++++++++++ docs/tasks/latest-main-build/task.md | 9 ++++ manifest.json | 2 +- package-lock.json | 4 +- package.json | 2 +- tests/unit/options-react.test.tsx | 2 +- 10 files changed, 96 insertions(+), 25 deletions(-) create mode 100644 docs/tasks/latest-main-build/state.json create mode 100644 docs/tasks/latest-main-build/task.md diff --git a/.github/workflows/chatgpt-dev-build.yml b/.github/workflows/chatgpt-dev-build.yml index c9bb92f4..64614214 100644 --- a/.github/workflows/chatgpt-dev-build.yml +++ b/.github/workflows/chatgpt-dev-build.yml @@ -2,23 +2,6 @@ name: chatgpt-dev-build on: workflow_dispatch: - pull_request: - paths: - - ".github/workflows/chatgpt-dev-build.yml" - - "scripts/build-extension.mjs" - - "scripts/production-manifest.mjs" - - "scripts/audit-wxt-extension.mjs" - - "src/options/**" - - "src/background/providers/**" - - "src/background/router.js" - - "src/shared/chatgpt-native-host.js" - - "src/i18n/catalog-options-chatgpt.js" - - "wxt.config.mjs" - - "tests/chatgpt-plan-provider.test.mjs" - - "tests/wxt-assets.test.mjs" - - "e2e/chatgpt-plan-nativehost.spec.mjs" - - "package.json" - - "docs/CHATGPT_DEVELOPMENT_BUILD.md" permissions: contents: read diff --git a/README.md b/README.md index 9a759cd9..f7c50668 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# TranslateFlow v0.8.47 +# TranslateFlow v0.8.48 轻量、缓存优先的 Chrome Manifest V3 双语网页翻译扩展。单词查词与句段/全文分别保存一种翻译方式:单词可选免费在线、词典、Chrome 本地或 AI;短语、句子、段落和全文可选 AI、Chrome 本地或免费在线。未命中、未就绪或失败时保留所选方式,用户可在划选面板临时切换。字幕保持独立路径。直接支持 OpenAI、Anthropic、Gemini 与主流 OpenAI-compatible API,并按有效配置与原文指纹缓存结果。 diff --git a/docs/CHATGPT_DEVELOPMENT_BUILD.md b/docs/CHATGPT_DEVELOPMENT_BUILD.md index efd63b95..1c85169a 100644 --- a/docs/CHATGPT_DEVELOPMENT_BUILD.md +++ b/docs/CHATGPT_DEVELOPMENT_BUILD.md @@ -36,7 +36,7 @@ Native Host **不随扩展 ZIP 自动安装**,账号令牌不进入扩展 ZIP 在**同一个仓库 checkout** 里,在扩展安装后将 `<扩展ID>` 换成 `chrome://extensions` 当前显示的 ID: ```bash -TF_CHROME_EXTENSION_ID=<扩展ID> TF_NATIVE_HOST_VERSION=0.8.47 npm run build:native-host:candidate +TF_CHROME_EXTENSION_ID=<扩展ID> TF_NATIVE_HOST_VERSION=0.8.48 npm run build:native-host:candidate ``` `TF_NATIVE_HOST_VERSION` 必须与当前候选扩展版本相符;更新版本后请按实际 `manifest.json` 更新它。构建产物位于 `.output/native-host-candidate/`,生成 `release-candidate.json`、`SHA256SUMS` 和适用于 Linux/Windows x64 的候选二进制。候选标记为 `publishable:false`、`NOT_SIGNED`,**不得当作已签名公共安装包分发或绕过系统的未知发布者警告**。 @@ -44,7 +44,7 @@ TF_CHROME_EXTENSION_ID=<扩展ID> TF_NATIVE_HOST_VERSION=0.8.47 npm run build:na Linux(Google Chrome)自建示例: ```bash -./.output/native-host-candidate/translateflow-host-0.8.47-linux-amd64 install --extension-id <扩展ID> --browser chrome +./.output/native-host-candidate/translateflow-host-0.8.48-linux-amd64 install --extension-id <扩展ID> --browser chrome ``` 如果使用 Chromium 而不是 Chrome,将最后一个参数改为 `--browser chromium`。Windows 在 PowerShell 中构建候选后,使用相应 `windows-amd64.exe install --extension-id <扩展ID>` 安装。具体平台、安装注册位置和安全边界见 [Native Host 安装文档](../native-host/INSTALLATION.md)。 diff --git a/docs/tasks/index.json b/docs/tasks/index.json index 3d576d03..89b3ae60 100644 --- a/docs/tasks/index.json +++ b/docs/tasks/index.json @@ -328,6 +328,13 @@ "branch": "feat/free-online-mt-20261016", "candidateHead": "0d245001483b7b653452e0c0889f713c310be7fe" }, + { + "task": "latest-main-build", + "status": "working", + "dependencies": [], + "branch": "integration/latest-main-local-build", + "candidateHead": null + }, { "task": "offline-dictionary", "status": "working", diff --git a/docs/tasks/latest-main-build/state.json b/docs/tasks/latest-main-build/state.json new file mode 100644 index 00000000..71960d4b --- /dev/null +++ b/docs/tasks/latest-main-build/state.json @@ -0,0 +1,72 @@ +{ + "schema": 1, + "task": "latest-main-build", + "status": "working", + "owner": "main", + "dependencies": [], + "branch": "integration/latest-main-local-build", + "worktree": "/home/lambert/githubRepos/lambert/translateflow-latest-main-build", + "candidateHead": null, + "mergeHead": null, + "artifactRequired": true, + "validationCommands": [ + [ + "npm", + "run", + "check" + ], + [ + "npm", + "test" + ], + [ + "npm", + "run", + "typecheck" + ], + [ + "npm", + "run", + "test:unit" + ], + [ + "npm", + "run", + "build:extension" + ], + [ + "npm", + "run", + "build:extension:dev" + ], + [ + "env", + "TF_E2E_ARTIFACT=dist/extension", + "TF_I18N_ARTIFACT=dist/extension", + "npm", + "run", + "test:e2e", + "--", + "e2e/chatgpt-plan-nativehost.spec.mjs", + "e2e/provider-connections.spec.mjs" + ], + [ + "env", + "TF_E2E_ARTIFACT=dist/extension-dev", + "TF_I18N_ARTIFACT=dist/extension-dev", + "npm", + "run", + "test:e2e", + "--", + "e2e/chatgpt-plan-nativehost.spec.mjs" + ], + [ + "env", + "TF_SMOKE_ARTIFACT=dist/extension", + "npm", + "run", + "test:wxt:smoke" + ] + ], + "nextAction": "完成候选验收、自查与受保护合并,随后从 main 交付本地包供用户测试。" +} diff --git a/docs/tasks/latest-main-build/task.md b/docs/tasks/latest-main-build/task.md new file mode 100644 index 00000000..3b91e1b9 --- /dev/null +++ b/docs/tasks/latest-main-build/task.md @@ -0,0 +1,9 @@ +# 最新 main 集成与本地测试包 + +用户要求先将分支最新代码合入 main,再编译本地插件供其测试,之后继续 AI 翻译可靠性审核。 + +范围:集成 PR #371 的公开/ChatGPT 开发版拆分、分支 8920ca2 的旧体积预算移除、b9ea931/2e7c394 的原文标记辅助合同;复用已合入的 PR #363 记录修正。保留既有数据、权限和 Provider 行为,原文辅助合同不接入运行链路。交付 0.8.48 的公开/开发包及准确源码绑定。 + +兼容:公开版拒绝 ChatGPT 订阅操作但保留已有设置,订阅测试使用显式开发版;不更改 DB 或清空数据,不调用真实付费服务,不发布商店。Actions 延续仓库手动触发约定。修正候选引入的类型/构建问题属于集成范围,AI 可靠性修复仍在用户测试包交付之后进行。 + +验收:state.json 的 validationCommands 合计覆盖完整 validate 的所有阶段,并执行公开/开发包受影响 Chromium 流程及公开包 smoke。主 Agent 自查实际 diff、两种 manifest 权限一致、安装包文件边界;记录版本、提交、包指纹和截图。合入满足本地 gate、准确远端 head 和实际保护。用户真实账号与网页体验不由 mock 测试替代。 diff --git a/manifest.json b/manifest.json index f8bdcc1e..75d2bc43 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "__MSG_extensionName__", - "version": "0.8.47", + "version": "0.8.48", "description": "__MSG_extensionDescription__", "default_locale": "en", "permissions": [ diff --git a/package-lock.json b/package-lock.json index 457227b3..de18cc66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "translateflow-plugin", - "version": "0.8.47", + "version": "0.8.48", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "translateflow-plugin", - "version": "0.8.47", + "version": "0.8.48", "dependencies": { "markdown-it": "15.0.2", "p-limit": "7.2.0", diff --git a/package.json b/package.json index 329e3ad1..7188474b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "translateflow-plugin", - "version": "0.8.47", + "version": "0.8.48", "private": true, "type": "module", "scripts": { diff --git a/tests/unit/options-react.test.tsx b/tests/unit/options-react.test.tsx index 9515983e..b3c01cb4 100644 --- a/tests/unit/options-react.test.tsx +++ b/tests/unit/options-react.test.tsx @@ -54,7 +54,7 @@ test("public Provider settings show only the ChatGPT developer guide and preserv expect(screen.queryByRole("button", { name: "检查状态并加载模型" })).toBeNull(); const selected = document.querySelector("#defaultService") as HTMLSelectElement; expect(selected.value).toBe("chatgpt-plan"); - expect(selected.querySelector('option[value="chatgpt-plan"]')?.disabled).toBe(true); + expect(selected.querySelector('option[value="chatgpt-plan"]')?.disabled).toBe(true); expect(chatGPTPlanAction).not.toHaveBeenCalled(); }); From c8b5b59b91cfaa51f2ab261df7670f3fa8d5f1c2 Mon Sep 17 00:00:00 2001 From: CoderLambert Date: Tue, 27 Oct 2026 23:55:45 +0800 Subject: [PATCH 24/25] fix: resolve integrated build typing and legacy content upgrade failures --- docs/tasks/latest-main-build/state.json | 5 +++++ e2e/provider-connections.spec.mjs | 8 +++++++- src/content/subtitles/sources/youtube.js | 8 ++++++-- src/options/CommonSections.tsx | 2 +- src/options/ProviderConnectionsSection.tsx | 2 +- 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/docs/tasks/latest-main-build/state.json b/docs/tasks/latest-main-build/state.json index 71960d4b..06310b91 100644 --- a/docs/tasks/latest-main-build/state.json +++ b/docs/tasks/latest-main-build/state.json @@ -60,6 +60,11 @@ "--", "e2e/chatgpt-plan-nativehost.spec.mjs" ], + [ + "npm", + "run", + "build:extension:wxt" + ], [ "env", "TF_SMOKE_ARTIFACT=dist/extension", diff --git a/e2e/provider-connections.spec.mjs b/e2e/provider-connections.spec.mjs index 26d0c6df..a2024282 100644 --- a/e2e/provider-connections.spec.mjs +++ b/e2e/provider-connections.spec.mjs @@ -109,7 +109,13 @@ test.describe("API key provider connections", () => { await options.goto(`chrome-extension://${harness.extensionId}/options.html#provider`); await expect(options.getByRole("heading", { name: "API Key 连接" })).toBeVisible(); - await expect(options.getByRole("heading", { name: "Provider · ChatGPT 订阅" })).toBeVisible(); + const manifest = await options.evaluate(() => chrome.runtime.getManifest()); + if (manifest.version_name === `${manifest.version}-dev`) { + await expect(options.getByRole("heading", { name: "Provider · ChatGPT 订阅" })).toBeVisible(); + } else { + await expect(options.getByRole("link", { name: "GitHub 开发版及配置说明" })).toBeVisible(); + await expect(options.getByRole("heading", { name: "Provider · ChatGPT 订阅" })).toHaveCount(0); + } await expect(options.locator(".connection-card").filter({ hasText: "OpenAI-compatible" })).toBeVisible(); const editor = options.locator("form.connection-editor"); diff --git a/src/content/subtitles/sources/youtube.js b/src/content/subtitles/sources/youtube.js index f14d39ff..544d88ba 100644 --- a/src/content/subtitles/sources/youtube.js +++ b/src/content/subtitles/sources/youtube.js @@ -4,7 +4,6 @@ !app?.modules.subtitleSource || !app.modules.textTrackSubtitleSource || !app.modules.youtubeBridgeProtocol - || !app.modules.youtubeCaptionDom || !app.modules.youtubeTimedtext || app.modules.youtubeSubtitleSource ) return; @@ -13,7 +12,12 @@ const { createTextTrackSource } = app.modules.textTrackSubtitleSource; const protocol = app.modules.youtubeBridgeProtocol; const runtime = app.modules.runtime; - const { CAPTION_CONTAINER_SELECTORS, CAPTION_SEGMENT_SELECTORS, createCaptionDom } = app.modules.youtubeCaptionDom; + // An already registered legacy script list cannot load the new DOM helper until refresh. + // Keep TextTrack/timedtext and the rest of Content available without hiding native captions. + const { CAPTION_CONTAINER_SELECTORS, CAPTION_SEGMENT_SELECTORS, createCaptionDom } = app.modules.youtubeCaptionDom || { + CAPTION_CONTAINER_SELECTORS: [], CAPTION_SEGMENT_SELECTORS: [], + createCaptionDom: () => ({ readCues: () => [], suppress() {}, restore() {} }) + }; const PLAYER_SELECTORS = Object.freeze(["#movie_player", ".html5-video-player"]); const VIDEO_SELECTORS = Object.freeze(["video.html5-main-video", "video"]); diff --git a/src/options/CommonSections.tsx b/src/options/CommonSections.tsx index 14d7cfe2..04a7774f 100644 --- a/src/options/CommonSections.tsx +++ b/src/options/CommonSections.tsx @@ -47,7 +47,7 @@ export function ShortcutsSection() { export function ProviderSections({ config, disabled, update, client, connections, loading, error, reload, setStatus }: Props & { client: OptionsClient; connections: ProviderConnectionState | null; loading: boolean; error: string; reload: () => Promise; setStatus: (message: string, error?: boolean) => void }) { const i18n = useOptionsI18n(); - const developer = isChatGPTPlanDeveloperBuild(globalThis.chrome?.runtime?.getManifest?.()); + const developer = isChatGPTPlanDeveloperBuild(typeof chrome === "undefined" ? undefined : chrome.runtime?.getManifest?.()); return <> {developer ? :