diff --git a/gateway/runtime/electron/official-net-fetch-statsig-hook.cjs b/gateway/runtime/electron/official-net-fetch-statsig-hook.cjs new file mode 100644 index 0000000..a8628c7 --- /dev/null +++ b/gateway/runtime/electron/official-net-fetch-statsig-hook.cjs @@ -0,0 +1,147 @@ +const { + registerOfficialElectronModuleOverride, +} = require("./official-electron-module-hook.cjs"); +const { diagnosticLog } = require("../core/diagnostics.cjs"); + +// Electron main 的 net.fetch 是官方隐藏 renderer 所有 Statsig/遥测请求的最终出口。 +// 无外网出口的服务器上,对 ab.chatgpt.com / chatgpt.com 遥测的 TCP 连接会一直黑洞挂起, +// Statsig 初始化永不返回,官方路由被 Suspense 永久挂起(浏览器镜像端只剩转圈); +// 用 /etc/hosts 把它指回本地又会变成快速失败并打挂页面。唯一稳的做法是在这里本地短路: +// initialize 回一份合法 gate 配置(与 web-shell polyfill 默认值一致),遥测/异常上报回空对象, +// 其余 URL 原样透传给官方 net.fetch。 +const STATSIG_DEFAULT_FEATURES_CONFIG = "statsig_default_enable_features"; +const STATSIG_I18N_LAYER_CONFIG = "72216192"; +const STATSIG_I18N_LAYER_VALUES = { enable_i18n: true, locale_source: "IDE" }; +// 505458 是官方"新工作树"入口门;Web 快照必须保留该能力,取值与 polyfill 保持一致。 +const STATSIG_DEFAULT_FEATURE_OVERRIDES = { + "3903742690": true, + "505458": true, + artifacts: true, +}; +// 官方 bundle 在 authed-route 模块初始化时调用 app-primary 的 side-effect 导出。真实网络下 Statsig +// 初始化有 100ms+ 往返,天然给官方 side-effect 模块留出注册窗口;若 0ms 返回会抢跑,概率性触发 +// "n is not a function"。给合成响应加一个小延迟,复刻真实网络节奏,消除该竞态。 +const STATSIG_INITIALIZE_DELAY_MS = Math.max(0, Number(process.env.OPENCODEX_STATSIG_INITIALIZE_DELAY_MS ?? 400) || 0); + +function buildStatsigInitializeNetResponse() { + const feature_gates = {}; + const dynamic_configs = { + [STATSIG_DEFAULT_FEATURES_CONFIG]: { + name: STATSIG_DEFAULT_FEATURES_CONFIG, + value: { ...STATSIG_DEFAULT_FEATURE_OVERRIDES }, + rule_id: "gateway_override", + secondary_exposures: [], + }, + }; + for (const [name, value] of Object.entries(STATSIG_DEFAULT_FEATURE_OVERRIDES)) { + feature_gates[name] = { name, value, rule_id: "gateway_override", secondary_exposures: [] }; + } + return { + has_updates: true, + time: Date.now(), + hash_used: "djb2", + feature_gates, + dynamic_configs, + layer_configs: { + [STATSIG_I18N_LAYER_CONFIG]: { + name: STATSIG_I18N_LAYER_CONFIG, + value: { ...STATSIG_I18N_LAYER_VALUES }, + rule_id: "gateway_override", + secondary_exposures: [], + }, + }, + param_stores: {}, + exposures: {}, + sdk_flags: {}, + }; +} + +// 返回本地响应体字符串;空串表示该 URL 不属于 Statsig 控制面,必须透传给官方实现。 +function statsigLocalResponseBodyForUrl(rawUrl) { + let parsed; + try { + parsed = new URL(String(rawUrl || "")); + } catch { + return ""; + } + if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return ""; + const pathname = parsed.pathname.replace(/\/+$/, ""); + if (parsed.hostname === "ab.chatgpt.com") { + if (pathname === "/v1/initialize") return JSON.stringify(buildStatsigInitializeNetResponse()); + if (pathname === "/v1/sdk_exception") return "{}"; + return ""; + } + if (parsed.hostname === "chatgpt.com" && (pathname === "/ces/v1/rgstr" || pathname === "/ces/v1/log_event")) { + return "{}"; + } + return ""; +} + +function extractUrlFromNetFetchArgs(args) { + const first = args && args[0]; + if (typeof first === "string") return first; + if (first && typeof first === "object") { + if (typeof first.url === "string") return first.url; + if (typeof first.href === "string") return first.href; + try { + return first.toString(); + } catch { + return ""; + } + } + return ""; +} + +// 构造官方 httpFetch 能消费的响应;优先用全局 Response,缺失时退回最小鸭子类型形状。 +function buildStatsigNetResponse(bodyJson, url, ResponseCtor) { + if (ResponseCtor) { + return new ResponseCtor(bodyJson, { + status: 200, + headers: { "content-type": "application/json; charset=utf-8" }, + }); + } + const buffer = Buffer.from(bodyJson, "utf-8"); + return { + ok: true, + status: 200, + statusText: "OK", + url, + headers: { get: (name) => (String(name).toLowerCase() === "content-type" ? "application/json; charset=utf-8" : null) }, + json: async () => JSON.parse(bodyJson), + text: async () => bodyJson, + arrayBuffer: async () => buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength), + }; +} + +function installOfficialNetFetchStatsigHook(electronModule, options = {}) { + const onIntercept = typeof options.onIntercept === "function" ? options.onIntercept : null; + const nativeNet = electronModule && electronModule.net; + if (!nativeNet || typeof nativeNet.fetch !== "function") { + return { installed: false, reason: "net.fetch unavailable" }; + } + const nativeFetch = nativeNet.fetch.bind(nativeNet); + const ResponseCtor = typeof Response === "function" ? Response : null; + const hookedNet = Object.assign(Object.create(Object.getPrototypeOf(nativeNet)), nativeNet, { + fetch(...args) { + const url = extractUrlFromNetFetchArgs(args); + const bodyJson = statsigLocalResponseBodyForUrl(url); + if (!bodyJson) return nativeFetch(...args); + if (onIntercept) onIntercept(url); + diagnosticLog("statsig-net-fetch", "net_fetch_served_local", { url: String(url).split("?")[0] }); + const deliver = () => buildStatsigNetResponse(bodyJson, url, ResponseCtor); + // 仅初始化响应加延迟以复刻真实往返、规避官方模块初始化竞态;遥测/异常上报保持即时。 + if (String(url).includes("/v1/initialize") && STATSIG_INITIALIZE_DELAY_MS > 0) { + return new Promise((resolve) => setTimeout(() => resolve(deliver()), STATSIG_INITIALIZE_DELAY_MS)); + } + return Promise.resolve(deliver()); + }, + }); + registerOfficialElectronModuleOverride(electronModule, "net", hookedNet); + // 返回覆写后的 net 便于单测直接断言;线上官方代码通过 require("electron") 拿到同一包装对象。 + return { installed: true, net: hookedNet }; +} + +module.exports = { + installOfficialNetFetchStatsigHook, + __test: { statsigLocalResponseBodyForUrl, buildStatsigInitializeNetResponse }, +}; diff --git a/gateway/runtime/ipc/official-runtime.cjs b/gateway/runtime/ipc/official-runtime.cjs index 40fb73b..60a5fd6 100644 --- a/gateway/runtime/ipc/official-runtime.cjs +++ b/gateway/runtime/ipc/official-runtime.cjs @@ -40,6 +40,7 @@ const { officialElectronModuleHookStatus, } = require("../electron/official-electron-module-hook.cjs"); const { hiddenTrayHookStatus, installOfficialTrayHook } = require("../electron/official-tray-hook.cjs"); +const { installOfficialNetFetchStatsigHook } = require("../electron/official-net-fetch-statsig-hook.cjs"); const { createOfficialLiveObserver } = require("./official-live-observer.cjs"); const { gateway: gatewayPointRefs, @@ -1471,6 +1472,143 @@ function fetchMessageFromIpcArgs(args) { if (payload.type !== "fetch") return null; return typeof payload.url === "string" ? payload : null; } +// Statsig 遥测(/ces/v1/rgstr 设备注册、/ces/v1/log_event 事件上报)由 renderer 通过 +// IPC fetch 委托给 Electron main 进程真实发 HTTP。受限网络下 Cloudflare 会返回 403 challenge, +// 回包被转发回 renderer 后 Statsig 反复报错重试,控制台持续刷 NetworkError。 +// 这里在 IPC 层直接本地短路:不回官方 handler,按官方 fetch-response 协议回一个 200 成功, +// 让 Statsig 认为上报完成,从源头消除该请求与控制台噪音。 +function isStatsigTelemetryFetchUrl(url) { + try { + const parsed = new URL(String(url || "")); + const pathname = parsed.pathname.replace(/\/+$/, ""); + return parsed.hostname === "chatgpt.com" && (pathname === "/ces/v1/rgstr" || pathname === "/ces/v1/log_event"); + } catch { + return false; + } +} + +function sendStatsigTelemetryNoopResponse(message) { + const requestId = stringRouteId(message && message.requestId); + if (!requestId) return false; + routeOfficialWebContentsSend(MESSAGE_FOR_VIEW_CHANNEL, [ + { + type: "fetch-response", + responseType: "success", + requestId, + status: 200, + headers: { "content-type": "application/json" }, + bodyJsonString: "{}", + }, + ]); + return true; +} + +function maybeHandleStatsigTelemetryFetchNoop(channel, args) { + if (channel !== MESSAGE_FROM_VIEW_CHANNEL) return false; + const message = fetchMessageFromIpcArgs(args); + if (!message || !isStatsigTelemetryFetchUrl(message.url)) return false; + diagnosticLog("statsig-telemetry", "fetch_blocked_local", { + method: message.method || "", + url: String(message.url).split("?")[0], + }); + return sendStatsigTelemetryNoopResponse(message); +} + +// Statsig 功能开关初始化(ab.chatgpt.com/v1/initialize)同样经 renderer→main 的 IPC fetch 通道发出。 +// 受限网络(服务器无外网出口)下 main 的 net.fetch 会一直卡在 TCP 连接上,Statsig 初始化永不 resolve, +// 官方路由被 Suspense 边界永久挂起,页面只剩转圈白屏。polyfill 只补了 window.fetch/XHR/beacon, +// 覆盖不到这条 relay→IPC 通道,因此必须在 gateway 侧本地短路:回一份合法的空 gate 配置 +// (保留 new-worktree 等关键门,取值与 polyfill 默认值一致),让 Statsig 认为初始化成功。 +// SDK 异常上报(ab.chatgpt.com/v1/sdk_exception)一并吞掉,消除失败重试噪音。 +const STATSIG_DEFAULT_FEATURES_CONFIG = "statsig_default_enable_features"; +const STATSIG_I18N_LAYER_CONFIG = "72216192"; +const STATSIG_I18N_LAYER_VALUES = { enable_i18n: true, locale_source: "IDE" }; +// 与 codex-bridge-polyfill 的默认门保持一致:505458 是官方"新工作树"入口,Web 快照必须保留该能力。 +const STATSIG_DEFAULT_FEATURE_OVERRIDES = { + "3903742690": true, + "505458": true, + artifacts: true, +}; + +// 构造与 polyfill.buildStatsigInitializeResponse 同形状的合法初始化响应,供无出口环境下本地兜底。 +function buildStatsigInitializeGatewayResponse() { + const feature_gates = {}; + const dynamic_configs = { + [STATSIG_DEFAULT_FEATURES_CONFIG]: { + name: STATSIG_DEFAULT_FEATURES_CONFIG, + value: { ...STATSIG_DEFAULT_FEATURE_OVERRIDES }, + rule_id: "gateway_override", + secondary_exposures: [], + }, + }; + for (const [name, value] of Object.entries(STATSIG_DEFAULT_FEATURE_OVERRIDES)) { + feature_gates[name] = { name, value, rule_id: "gateway_override", secondary_exposures: [] }; + } + return { + has_updates: true, + time: Date.now(), + hash_used: "djb2", + feature_gates, + dynamic_configs, + layer_configs: { + [STATSIG_I18N_LAYER_CONFIG]: { + name: STATSIG_I18N_LAYER_CONFIG, + value: { ...STATSIG_I18N_LAYER_VALUES }, + rule_id: "gateway_override", + secondary_exposures: [], + }, + }, + param_stores: {}, + exposures: {}, + sdk_flags: {}, + }; +} + +// 只认 ab.chatgpt.com 上的初始化/异常上报两条控制面路径;其余 fetch 一律放行给官方 handler。 +function classifyStatsigControlPlaneFetchUrl(url) { + try { + const parsed = new URL(String(url || "")); + if (parsed.hostname !== "ab.chatgpt.com") return ""; + const pathname = parsed.pathname.replace(/\/+$/, ""); + if (pathname === "/v1/initialize") return "initialize"; + if (pathname === "/v1/sdk_exception") return "sdk_exception"; + return ""; + } catch { + return ""; + } +} + +function sendStatsigControlPlaneNoopResponse(message, kind) { + const requestId = stringRouteId(message && message.requestId); + if (!requestId) return false; + routeOfficialWebContentsSend(MESSAGE_FOR_VIEW_CHANNEL, [ + { + type: "fetch-response", + responseType: "success", + requestId, + status: 200, + headers: { "content-type": "application/json; charset=utf-8" }, + // 初始化必须回合法 gate 配置;SDK 异常上报回空对象即可。 + bodyJsonString: + kind === "initialize" ? JSON.stringify(buildStatsigInitializeGatewayResponse()) : "{}", + }, + ]); + return true; +} + +function maybeHandleStatsigControlPlaneFetchNoop(channel, args) { + if (channel !== MESSAGE_FROM_VIEW_CHANNEL) return false; + const message = fetchMessageFromIpcArgs(args); + if (!message) return false; + const kind = classifyStatsigControlPlaneFetchUrl(message.url); + if (!kind) return false; + diagnosticLog("statsig-telemetry", "fetch_blocked_local", { + method: message.method || "", + url: String(message.url).split("?")[0], + kind, + }); + return sendStatsigControlPlaneNoopResponse(message, kind); +} function parseJsonLike(value) { if (value && typeof value === "object" && !Array.isArray(value)) return value; @@ -2158,6 +2296,8 @@ async function invokeOfficialIpc(channel, args = [], context = {}) { // Computer Use 锁屏授权由官方 Installer 决定;这里额外记录同进程直接 status,方便和官方回包对照。 logComputerUseAuthRequest(channel, invokeArgs); if (maybeHandleComputerUseAuthWriteNoop(channel, invokeArgs)) return true; + if (maybeHandleStatsigTelemetryFetchNoop(channel, invokeArgs)) return true; + if (maybeHandleStatsigControlPlaneFetchNoop(channel, invokeArgs)) return true; logDesktopFeatureAvailability(channel, invokeArgs); const handler = officialIpc.handlers.get(channel); if (handler) { @@ -2607,6 +2747,15 @@ function startOfficialRuntime(options = {}) { onIntercept: () => recordRuntimeCompatibilityHit(gatewayPointRefs.tray), }) ); + // 官方隐藏 renderer 的 Statsig 初始化 / 遥测通过 Electron main 的 net.fetch 真实发 HTTP。 + // 无外网出口的服务器上这条连接黑洞挂起,初始化永不 resolve,浏览器端被 Suspense 卡在加载页。 + // 这里覆写 electron.net,对 ab.chatgpt.com 初始化/异常上报和 chatgpt.com 遥测本地短路,其余透传。 + runRuntimeCompatibilityCapability( + gatewayPointRefs.netFetchStatsig, + () => installOfficialNetFetchStatsigHook(electron, { + onIntercept: () => recordRuntimeCompatibilityHit(gatewayPointRefs.netFetchStatsig), + }) + ); runRuntimeCompatibilityCapability( gatewayPointRefs.singleInstance, patchOfficialAppSingleton @@ -2675,5 +2824,7 @@ module.exports = { threadListInvalidationEnvelope, threadListInvalidationForOfficialMessage, threadListInvalidationRequest, + classifyStatsigControlPlaneFetchUrl, + buildStatsigInitializeGatewayResponse, }, }; diff --git a/gateway/runtime/modification/point-refs.cjs b/gateway/runtime/modification/point-refs.cjs index 220013e..c64832e 100644 --- a/gateway/runtime/modification/point-refs.cjs +++ b/gateway/runtime/modification/point-refs.cjs @@ -13,6 +13,7 @@ const gateway = Object.freeze({ hiddenChromiumServices: requiredPoint("gateway.runtime.chromium.hidden-services"), gcmProfile: requiredPoint("gateway.runtime.chromium.gcm-profile"), electronModuleLoader: requiredPoint("gateway.runtime.node.electron-module-loader"), + netFetchStatsig: requiredPoint("gateway.runtime.electron.net-fetch-statsig"), notification: requiredPoint("gateway.runtime.electron.notification"), tray: requiredPoint("gateway.runtime.electron.tray"), ipcMain: requiredPoint("gateway.runtime.electron.ipc-main"), diff --git a/gateway/src/modification/catalog.ts b/gateway/src/modification/catalog.ts index 712b398..ac45a95 100644 --- a/gateway/src/modification/catalog.ts +++ b/gateway/src/modification/catalog.ts @@ -334,6 +334,7 @@ export const POINT_DEFINITIONS = Object.freeze([ point("gateway.runtime.chromium.hidden-services", "关闭隐藏 Runtime 无效后台服务", "official-runtime", G.backgroundEfficiency, A.officialEnvironment), point("gateway.runtime.chromium.gcm-profile", "隔离隐藏 Runtime 的 GCM Profile", "official-runtime", G.notifications, A.officialEnvironment), point("gateway.runtime.node.electron-module-loader", "包装官方 electron 模块导出", "official-runtime", G.gatewayRuntime, A.electronApi), + point("gateway.runtime.electron.net-fetch-statsig", "本地短路隐藏 Renderer 的 Statsig 控制面", "official-runtime", G.gatewayRuntime, A.electronApi), point("gateway.runtime.electron.notification", "替换官方 Notification", "official-runtime", G.notifications, A.electronApi), point("gateway.runtime.electron.tray", "替换官方 Tray", "official-runtime", G.notifications, A.electronApi), point("gateway.runtime.electron.ipc-main", "捕获官方 ipcMain 注册", "official-runtime", G.rendererCore, A.gatewayIpc), diff --git a/gateway/test/compatibility-registry.test.cjs b/gateway/test/compatibility-registry.test.cjs index aea87e3..d30517a 100644 --- a/gateway/test/compatibility-registry.test.cjs +++ b/gateway/test/compatibility-registry.test.cjs @@ -88,7 +88,7 @@ function sourceFiles(directory) { } test("compatibility catalog declares groups and adapter chains for every stable point", () => { - assert.equal(POINT_DEFINITIONS.length, 103); + assert.equal(POINT_DEFINITIONS.length, 104); assert.equal(POINT_GROUP_DEFINITIONS.length, 17); assert.equal(ADAPTER_DEFINITIONS.length, 23); assert.equal(new Set(POINT_DEFINITIONS.map((point) => point.id)).size, POINT_DEFINITIONS.length); @@ -97,7 +97,7 @@ test("compatibility catalog declares groups and adapter chains for every stable const registry = registerCompatibilityCatalog(createCompatibilityRegistry()); const snapshot = registry.snapshot(); assert.equal(snapshot.schemaVersion, 2); - assert.equal(snapshot.points.length, 103); + assert.equal(snapshot.points.length, 104); assert.equal(snapshot.groups.length, 17); assert.equal(snapshot.adapterTypes.length, 23); assert.equal(snapshot.status, "pending"); diff --git a/gateway/test/compatibility-service.test.cjs b/gateway/test/compatibility-service.test.cjs index 4bb96e7..10b68a7 100644 --- a/gateway/test/compatibility-service.test.cjs +++ b/gateway/test/compatibility-service.test.cjs @@ -367,7 +367,7 @@ test("public compatibility API exposes only the read-only sanitized snapshot", ( service, ), true); assert.equal(getResponse.status, 200); - assert.equal(JSON.parse(getResponse.body).compatibility.points.length, 103); + assert.equal(JSON.parse(getResponse.body).compatibility.points.length, 104); const reportResponse = responseRecorder(); assert.equal(handlePublicRuntimeCompatibilityApi( @@ -399,7 +399,7 @@ test("authenticated API accepts only validated Browser Kernel reports", async () service, ), true); assert.equal(getResponse.status, 200); - assert.equal(JSON.parse(getResponse.body).compatibility.points.length, 103); + assert.equal(JSON.parse(getResponse.body).compatibility.points.length, 104); const point = browserKernelPoint("web.runtime.bridge.desktop-api", { active: true }); const reportResponse = responseRecorder(); @@ -470,7 +470,7 @@ test("authenticated Browser reports merge external Plugin SDK points into diagno assert.equal(response.status, 200); const snapshot = service.snapshot(); - assert.equal(snapshot.points.length, 104); + assert.equal(snapshot.points.length, 105); assert.deepEqual( snapshot.points.find((point) => point.id === fixture.point.id).plugin, fixture.catalog.plugin, diff --git a/gateway/test/modification-equivalence.test.cjs b/gateway/test/modification-equivalence.test.cjs index cbb4bcb..1defeff 100644 --- a/gateway/test/modification-equivalence.test.cjs +++ b/gateway/test/modification-equivalence.test.cjs @@ -14,7 +14,7 @@ function bindPoint(point, operation, snapshots) { return coordinator.bind(point, operation); } -test("all 103 modification points preserve synchronous call contracts through the production Kernel", () => { +test("all 104 modification points preserve synchronous call contracts through the production Kernel", () => { const calls = new Map(); const wrappers = new Map(); const snapshots = new Map(); @@ -41,7 +41,7 @@ test("all 103 modification points preserve synchronous call contracts through th } }); -test("all 103 modification points preserve Promise identity and thrown errors in production Kernel", async () => { +test("all 104 modification points preserve Promise identity and thrown errors in production Kernel", async () => { const contracts = new Map(); const snapshots = new Map(); for (const [index, point] of POINT_DEFINITIONS.entries()) { diff --git a/gateway/test/modification-kernel.test.cjs b/gateway/test/modification-kernel.test.cjs index 26afe59..7794f45 100644 --- a/gateway/test/modification-kernel.test.cjs +++ b/gateway/test/modification-kernel.test.cjs @@ -18,23 +18,23 @@ const { test("typed modification catalog assigns every point to a group and adapter chain", () => { assert.equal(POINT_GROUP_DEFINITIONS.length, 17); assert.equal(ADAPTER_DEFINITIONS.length, 23); - assert.equal(POINT_DEFINITIONS.length, 103); - assert.equal(POINT_TARGETS.length, 103); - assert.equal(MIGRATION_MATRIX.length, 103); + assert.equal(POINT_DEFINITIONS.length, 104); + assert.equal(POINT_TARGETS.length, 104); + assert.equal(MIGRATION_MATRIX.length, 104); assert.equal(MIGRATION_MATRIX.every((entry) => entry.migrationStatus === "migrated"), true); assert.deepEqual( ["browser", "gateway", "static", "runner"].map( (host) => MIGRATION_MATRIX.filter((entry) => entry.host === host).length ), - [37, 36, 25, 5] + [37, 37, 25, 5] ); - assert.equal(new Set(POINT_TARGETS).size, 103); - assert.equal(new Set(POINT_DEFINITIONS.map((point) => point.id)).size, 103); + assert.equal(new Set(POINT_TARGETS).size, 104); + assert.equal(new Set(POINT_DEFINITIONS.map((point) => point.id)).size, 104); assert.deepEqual( ["web.runtime.", "gateway.runtime.", "static.cache."].map( (prefix) => POINT_DEFINITIONS.filter((point) => point.id.startsWith(prefix)).length ), - [37, 36, 30] + [37, 37, 30] ); assert.equal(POINT_DEFINITIONS.every((point) => point.group && point.contributions.length > 0), true); assert.equal(POINT_DEFINITIONS.every((point) => point.contributions.every((item) => { @@ -68,7 +68,7 @@ test("typed modification catalog assigns every point to a group and adapter chai "renderer-ui": 6, "browser-platform": 3, "web-network": 3, - "gateway-runtime": 9, + "gateway-runtime": 10, "gateway-ipc": 4, "official-main": 3, "renderer-resources": 8, diff --git a/gateway/test/official-desktop-compat.test.cjs b/gateway/test/official-desktop-compat.test.cjs index 0f04861..3dac0e3 100644 --- a/gateway/test/official-desktop-compat.test.cjs +++ b/gateway/test/official-desktop-compat.test.cjs @@ -61,7 +61,7 @@ test("gateway compatibility service initializes from configured runtime paths", }); try { const snapshot = compatibilityService.snapshot(); - assert.equal(snapshot.points.length, 103); + assert.equal(snapshot.points.length, 104); assert.equal(snapshot.groups.length, 17); assert.equal(snapshot.adapterTypes.length, 23); assert.equal(Object.hasOwn(snapshot, "features"), false); diff --git a/gateway/test/official-runtime.test.cjs b/gateway/test/official-runtime.test.cjs index 6ab0211..a907e2a 100644 --- a/gateway/test/official-runtime.test.cjs +++ b/gateway/test/official-runtime.test.cjs @@ -16,6 +16,67 @@ const { setWsHub, } = require("../runtime/ipc/official-runtime.cjs"); const { __test: portableRunnerTest } = require("../runner/platform/portable.cjs"); +const { + __test: statsigNetTest, + installOfficialNetFetchStatsigHook, +} = require("../runtime/electron/official-net-fetch-statsig-hook.cjs"); + +test("Statsig net.fetch hook answers control-plane URLs locally and passes others through", () => { + const { statsigLocalResponseBodyForUrl, buildStatsigInitializeNetResponse } = statsigNetTest; + + // 初始化必须回合法 gate 配置,且保留官方"新工作树"能力门 505458。 + const initializeBody = statsigLocalResponseBodyForUrl( + "https://ab.chatgpt.com/v1/initialize?k=client-x&st=javascript-client" + ); + assert.ok(initializeBody); + const initialize = JSON.parse(initializeBody); + assert.equal(initialize.has_updates, true); + assert.equal(initialize.feature_gates["505458"].value, true); + assert.equal( + initialize.dynamic_configs.statsig_default_enable_features.value["505458"], + true + ); + assert.equal(initialize.layer_configs["72216192"].value.enable_i18n, true); + assert.deepEqual(buildStatsigInitializeNetResponse().sdk_flags, {}); + + // SDK 异常上报与 chatgpt.com 遥测吞成空对象;其余 URL 一律透传(空串)。 + assert.equal(statsigLocalResponseBodyForUrl("https://ab.chatgpt.com/v1/sdk_exception"), "{}"); + assert.equal(statsigLocalResponseBodyForUrl("https://chatgpt.com/ces/v1/rgstr?k=1"), "{}"); + assert.equal(statsigLocalResponseBodyForUrl("https://chatgpt.com/ces/v1/log_event"), "{}"); + assert.equal(statsigLocalResponseBodyForUrl("https://chatgpt.com/backend-api/me"), ""); + assert.equal(statsigLocalResponseBodyForUrl("https://ab.chatgpt.com/v1/other"), ""); + assert.equal(statsigLocalResponseBodyForUrl("not a url"), ""); + + // 覆写后的 net.fetch 命中 Statsig 地址时返回 200 Response,其它地址透传原生实现。 + const passthrough = []; + const nativeNet = { + fetch: async (...args) => { + passthrough.push(args); + return { ok: true, status: 201 }; + }, + request() {}, + }; + const electronModule = { net: nativeNet }; + const intercepted = []; + const hook = installOfficialNetFetchStatsigHook(electronModule, { + onIntercept: (url) => intercepted.push(url), + }); + assert.equal(hook.installed, true); + const hookedNet = hook.net; + + const served = hookedNet.fetch("https://ab.chatgpt.com/v1/initialize?k=x"); + assert.ok(served instanceof Promise); + return served.then(async (res) => { + assert.equal(res.status, 200); + const payload = JSON.parse(await res.text()); + assert.equal(payload.feature_gates["505458"].value, true); + assert.equal(intercepted.length, 1); + + const other = await hookedNet.fetch("https://chatgpt.com/backend-api/me"); + assert.equal(other.status, 201); + assert.equal(passthrough.length, 1); + }); +}); test("forwards structured official app-host messages and preserves close signals", () => { const forwarded = []; diff --git a/scripts/check-modification-boundaries.cjs b/scripts/check-modification-boundaries.cjs index a24f09a..6be736e 100644 --- a/scripts/check-modification-boundaries.cjs +++ b/scripts/check-modification-boundaries.cjs @@ -122,14 +122,14 @@ if (violations.length > 0) { const catalogPath = path.join(projectRoot, "gateway", "dist", "modification", "catalog.js"); if (!fs.existsSync(catalogPath)) throw new Error("缺少已编译的虚拟骨架目录"); const catalog = require(catalogPath); -if (catalog.POINT_DEFINITIONS.length !== 103) throw new Error("修改点迁移矩阵不是 103 项"); -if (catalog.POINT_TARGETS.length !== 103 || new Set(catalog.POINT_TARGETS).size !== 103) { - throw new Error("103 个修改点没有各自独立的强类型语义目标"); +if (catalog.POINT_DEFINITIONS.length !== 104) throw new Error("修改点迁移矩阵不是 104 项"); +if (catalog.POINT_TARGETS.length !== 104 || new Set(catalog.POINT_TARGETS).size !== 104) { + throw new Error("104 个修改点没有各自独立的强类型语义目标"); } -if (catalog.MIGRATION_MATRIX.length !== 103 || catalog.MIGRATION_MATRIX.some((entry) => { +if (catalog.MIGRATION_MATRIX.length !== 104 || catalog.MIGRATION_MATRIX.some((entry) => { return entry.migrationStatus !== "migrated" || !entry.groupId || !entry.targetId || !entry.host; })) { - throw new Error("103 点迁移矩阵仍有 legacy 或 unassigned 项"); + throw new Error("104 点迁移矩阵仍有 legacy 或 unassigned 项"); } if (catalog.POINT_DEFINITIONS.some((point) => !point.group || point.contributions.length === 0)) { throw new Error("存在未分组或没有适配器的修改点"); diff --git a/shared/i18n/locales/runtime-compatibility-en-US.json b/shared/i18n/locales/runtime-compatibility-en-US.json index 902dc15..d30c6d8 100644 --- a/shared/i18n/locales/runtime-compatibility-en-US.json +++ b/shared/i18n/locales/runtime-compatibility-en-US.json @@ -238,6 +238,7 @@ "web.runtimeCompatibility.point.gateway.runtime.chromium.hidden-services.description": "Disable unnecessary background services in the hidden Runtime.", "web.runtimeCompatibility.point.gateway.runtime.chromium.gcm-profile.description": "Isolate the hidden Runtime GCM profile.", "web.runtimeCompatibility.point.gateway.runtime.node.electron-module-loader.description": "Wrap official Electron module exports.", + "web.runtimeCompatibility.point.gateway.runtime.electron.net-fetch-statsig.description": "Short-circuit the hidden renderer Statsig control plane locally.", "web.runtimeCompatibility.point.gateway.runtime.electron.notification.description": "Replace the official Notification implementation.", "web.runtimeCompatibility.point.gateway.runtime.electron.tray.description": "Replace the official Tray implementation.", "web.runtimeCompatibility.point.gateway.runtime.electron.ipc-main.description": "Capture official ipcMain registration.",