From 2ed922d145af38808e38e1550abc55316c2480e8 Mon Sep 17 00:00:00 2001 From: RyensX Date: Thu, 17 Sep 2026 04:14:50 +0000 Subject: [PATCH 1/5] feat(polyfill): serve Statsig telemetry guard as a skeleton provider --- gateway/runtime/http/static-assets.cjs | 6 + gateway/src/modification/catalog.ts | 2 + .../locales/runtime-compatibility-en-US.json | 1 + .../locales/runtime-compatibility-zh-CN.json | 1 + .../codex-statsig-telemetry-guard.js | 143 ++++++++++++++++++ web-shell/src/modification-browser-host.ts | 1 + 6 files changed, 154 insertions(+) create mode 100644 web-shell/internal/providers/codex-statsig-telemetry-guard.js diff --git a/gateway/runtime/http/static-assets.cjs b/gateway/runtime/http/static-assets.cjs index 52c108c..e77f9c6 100644 --- a/gateway/runtime/http/static-assets.cjs +++ b/gateway/runtime/http/static-assets.cjs @@ -71,6 +71,7 @@ const CODEX_REMOTE_FILE_ACTIONS_PATH = "/codex-remote-file-actions.js"; const CODEX_WORKSPACE_ROOT_PICKER_CSS_PATH = "/codex-workspace-root-picker.css"; const CODEX_WORKSPACE_ROOT_PICKER_PATH = "/codex-workspace-root-picker.js"; const CODEX_TOOLTIP_DISMISS_GUARD_PATH = "/codex-tooltip-dismiss-guard.js"; +const CODEX_STATSIG_TELEMETRY_GUARD_PATH = "/codex-statsig-telemetry-guard.js"; const FAVICON_PATH = "/favicon.ico"; const PWA_MANIFEST_PATH = "/manifest.webmanifest"; const OFFICIAL_LOADING_SHIMMER_POWER_GUARD = [ @@ -119,6 +120,7 @@ const BROWSER_PROVIDER_KEY_BY_FILE = new Map([ [path.join(INTERNAL_PROVIDER_DIR, "codex-remote-file-actions.js"), "remote-file-actions"], [path.join(INTERNAL_PROVIDER_DIR, "codex-workspace-root-picker.js"), "workspace-root-picker"], [path.join(INTERNAL_PROVIDER_DIR, "codex-tooltip-dismiss-guard.js"), "tooltip-dismiss"], + [path.join(INTERNAL_PROVIDER_DIR, "codex-statsig-telemetry-guard.js"), "statsig-telemetry-guard"], ]); const OFFICIAL_OPEN_IN_FOLDER_MESSAGE_ID = "artifactTab.preview.openInFolder"; const OPENCODEX_DOWNLOAD_FILE_MESSAGE_ID = "web.remoteFile.downloadFile"; @@ -289,6 +291,8 @@ const WEB_SHELL_STATIC_FILES = new Map([ OPENCODEX_OFFSCREEN_ANIMATION_GUARD_PATH, path.join(INTERNAL_PROVIDER_DIR, "codex-offscreen-animation-guard.js"), ], + // Statsig XHR/Beacon 遥测拦截由独立 Provider 承载,上游 bridge polyfill 保持零改动。 + [CODEX_STATSIG_TELEMETRY_GUARD_PATH, path.join(INTERNAL_PROVIDER_DIR, "codex-statsig-telemetry-guard.js")], [CODEX_APP_HOST_MESSAGE_CODEC_PATH, path.join(WEB_SHELL_DIR, "codex-app-host-message-codec.js")], [CODEX_BRIDGE_POLYFILL_PATH, path.join(INTERNAL_PROVIDER_DIR, "codex-bridge-polyfill.js")], [CODEX_REMOTE_FILE_ACTIONS_PATH, path.join(INTERNAL_PROVIDER_DIR, "codex-remote-file-actions.js")], @@ -703,6 +707,8 @@ function createStaticAssetService({ CODEX_REMOTE_FILE_ACTIONS_PATH, CODEX_WORKSPACE_ROOT_PICKER_PATH, CODEX_TOOLTIP_DISMISS_GUARD_PATH, + // 遥测拦截须在 Kernel 激活前装上,保证官方 SDK 初始化时 XHR 原型已被接管。 + CODEX_STATSIG_TELEMETRY_GUARD_PATH, OPENCODEX_MODIFICATION_ACTIVATE_PATH, ].map((reqPath) => WEB_SHELL_STATIC_FILES.get(reqPath)), }; diff --git a/gateway/src/modification/catalog.ts b/gateway/src/modification/catalog.ts index 712b398..7e580dd 100644 --- a/gateway/src/modification/catalog.ts +++ b/gateway/src/modification/catalog.ts @@ -301,6 +301,8 @@ export const POINT_DEFINITIONS = Object.freeze([ point("web.runtime.bridge.feature-gates", "注入 Web 环境所需桌面能力开关", "web-shell", G.rendererCore, A.desktopBridge), point("web.runtime.network.statsig", "本地响应 Statsig 初始化请求", "web-shell", G.webNetwork, A.networkRequest), point("web.runtime.network.telemetry", "阻止隐藏 Web 环境发送无效遥测", "web-shell", G.webNetwork, A.networkRequest), + // XHR/Beacon 与 fetch 是两条独立上报通道,单独成点才能分别观察是否真被本地吞掉。 + point("web.runtime.network.telemetry-guard", "本地吞掉 Statsig XHR 与 Beacon 遥测", "web-shell", G.webNetwork, A.networkRequest), point("web.runtime.protocol.connector-logo", "合并 connector logo IPC 请求", "web-shell", G.webNetwork, A.semanticProtocol), point("web.runtime.dom.webview-shim", "使用 iframe 模拟 Electron webview", "web-shell", G.rendererUi, A.semanticView), point("web.runtime.native.file-picker", "把 Electron 文件选择转换为浏览器文件选择", "web-shell", G.browserPlatform, A.browserNative), diff --git a/shared/i18n/locales/runtime-compatibility-en-US.json b/shared/i18n/locales/runtime-compatibility-en-US.json index 902dc15..eb636d8 100644 --- a/shared/i18n/locales/runtime-compatibility-en-US.json +++ b/shared/i18n/locales/runtime-compatibility-en-US.json @@ -206,6 +206,7 @@ "web.runtimeCompatibility.point.web.runtime.bridge.feature-gates.description": "Inject desktop capability gates required by the Web environment.", "web.runtimeCompatibility.point.web.runtime.network.statsig.description": "Respond to Statsig initialization requests locally.", "web.runtimeCompatibility.point.web.runtime.network.telemetry.description": "Prevent the hidden Web environment from sending unnecessary telemetry.", + "web.runtimeCompatibility.point.web.runtime.network.telemetry-guard.description": "Swallow Statsig telemetry sent over XHR and Beacon locally.", "web.runtimeCompatibility.point.web.runtime.protocol.connector-logo.description": "Coalesce connector logo IPC requests.", "web.runtimeCompatibility.point.web.runtime.dom.webview-shim.description": "Emulate Electron webview with an iframe.", "web.runtimeCompatibility.point.web.runtime.native.file-picker.description": "Convert the Electron file picker to a browser file picker.", diff --git a/shared/i18n/locales/runtime-compatibility-zh-CN.json b/shared/i18n/locales/runtime-compatibility-zh-CN.json index 53486e6..e33ebe5 100644 --- a/shared/i18n/locales/runtime-compatibility-zh-CN.json +++ b/shared/i18n/locales/runtime-compatibility-zh-CN.json @@ -6,6 +6,7 @@ "web.runtimeCompatibility.pluginTag": "插件", "web.runtimeCompatibility.points": "修改点", "web.runtimeCompatibility.pointsDescription": "每个修改点按开发者功能组归类;分组只用于查看,不影响启用、回退或执行决策。", + "web.runtimeCompatibility.point.web.runtime.network.telemetry-guard.description": "本地吞掉 Statsig XHR 与 Beacon 遥测。", "web.runtimeCompatibility.filters": "修改点筛选", "web.runtimeCompatibility.adapterFilter": "注入类型", "web.runtimeCompatibility.column.adapter": "注入类别", diff --git a/web-shell/internal/providers/codex-statsig-telemetry-guard.js b/web-shell/internal/providers/codex-statsig-telemetry-guard.js new file mode 100644 index 0000000..8f3d384 --- /dev/null +++ b/web-shell/internal/providers/codex-statsig-telemetry-guard.js @@ -0,0 +1,143 @@ +/** + * Statsig 遥测本地拦截 Provider。 + * + * 官方 Statsig SDK 的上报通道是 XMLHttpRequest 与 navigator.sendBeacon,而不是 fetch, + * 因此只改写 fetch 拦不住它:网络受限环境下每条 XHR 都会在控制台反复刷 NetworkError, + * 并且 SDK 会持续重试。这里把 XHR 与 Beacon 两条通道一起接管,命中遥测地址时本地模拟 + * 一次成功响应,让 SDK 认为上报已完成、不再重试。 + * + * 该 Provider 独立成文件,由骨架装配层按 provider key 自动注册,因此上游 + * codex-bridge-polyfill.js 可以保持零改动。 + */ +(function () { + const w = window; + const modificationScope = w.__OpenCodexCurrentProviderScope; + const modificationEffects = modificationScope?.effects; + const providerGeneration = modificationScope?.generation || document; + if (w.__opencodexStatsigTelemetryGuardInstalled === providerGeneration) return; + const adapterHost = w.__OpenCodexAdapterHost; + // 本 Provider 唯一依赖的宿主能力是定时器:模拟响应要等 SDK 在 send 返回后挂好监听器 + // 再派发,不能同步触发;宿主没给 scheduler 时退回 window 计时器。 + const scheduler = adapterHost?.scheduler?.capture?.() || w; + if (!scheduler || typeof scheduler.setTimeout !== "function") return; + w.__opencodexStatsigTelemetryGuardInstalled = providerGeneration; + + const MOCK_RESPONSE_BODY = "{}"; + + /** 只认官方 Statsig 上报端点,其余 URL 一律透传原实现。 */ + function isTelemetryUrl(url) { + try { + const parsed = new URL(url, location.href); + const pathname = parsed.pathname.replace(/\/+$/, ""); + return ( + parsed.hostname === "chatgpt.com" && + (pathname === "/ces/v1/rgstr" || pathname === "/ces/v1/log_event") + ); + } catch { + return false; + } + } + + /** + * XHR 的 status/statusText/response/responseText/readyState 是只读 IDL 属性, + * 非严格模式下直接赋值会被静默忽略,SDK 读到的仍是 readyState=0/status=0, + * 永远走不到“上报成功”分支。必须在实例上 defineProperty 覆盖只读 getter。 + */ + function defineReadOnly(target, name, value) { + try { + Object.defineProperty(target, name, { + configurable: true, + enumerable: true, + get: () => value, + }); + } catch {} + } + + function safeDispatch(target, type) { + try { + target.dispatchEvent(new Event(type)); + } catch {} + } + + const disposers = []; + + if (typeof w.XMLHttpRequest === "function") { + const originalOpen = w.XMLHttpRequest.prototype.open; + const originalSend = w.XMLHttpRequest.prototype.send; + + w.XMLHttpRequest.prototype.open = function (method, url, ...rest) { + // open 是唯一能拿到目标 URL 的时机,先记在实例上供 send 判定。 + this.__opencodexTelemetryUrl = typeof url === "string" ? url : ""; + return originalOpen.apply(this, [method, url, ...rest]); + }; + disposers.push(() => { + w.XMLHttpRequest.prototype.open = originalOpen; + }); + + w.XMLHttpRequest.prototype.send = function (body) { + const xhr = this; + let url = ""; + try { + url = new URL(String(xhr.__opencodexTelemetryUrl || ""), location.href).toString(); + } catch { + url = String(xhr.__opencodexTelemetryUrl || ""); + } + if (!isTelemetryUrl(url)) { + return originalSend.call(this, body); + } + + // 跳过真实网络请求:loadstart 同步补发,其余状态异步补齐, + // 因为 SDK 通常在 send 返回之后才注册 load 监听器。 + safeDispatch(xhr, "loadstart"); + scheduler.setTimeout(() => { + defineReadOnly(xhr, "status", 200); + defineReadOnly(xhr, "statusText", "OK"); + defineReadOnly(xhr, "response", MOCK_RESPONSE_BODY); + defineReadOnly(xhr, "responseText", MOCK_RESPONSE_BODY); + defineReadOnly(xhr, "readyState", 4); + safeDispatch(xhr, "readystatechange"); + safeDispatch(xhr, "load"); + safeDispatch(xhr, "loadend"); + }, 0); + return undefined; + }; + disposers.push(() => { + w.XMLHttpRequest.prototype.send = originalSend; + }); + } + + if (w.navigator && typeof w.navigator.sendBeacon === "function") { + const originalSendBeacon = w.navigator.sendBeacon; + // Beacon 的 this 绑定 navigator,转发前先固定接收者,避免 dispose 后仍引用补丁。 + const boundSendBeacon = originalSendBeacon.bind(w.navigator); + // Beacon 没有响应可模拟,本地吞掉并按“已送达”返回 true,即可阻止 SDK 重试。 + w.navigator.sendBeacon = (url, ...rest) => { + let target = ""; + try { + target = new URL(String(url || ""), location.href).toString(); + } catch { + target = String(url || ""); + } + if (isTelemetryUrl(target)) return true; + return boundSendBeacon(url, ...rest); + }; + disposers.push(() => { + w.navigator.sendBeacon = originalSendBeacon; + }); + } + + // 补丁装上即视为该修改点真实生效,上报一次命中。 + modificationEffects?.primary?.emit(); + + // 换页或关闭时由宿主逆序 dispose:还原原型与 Beacon,避免旧补丁继续吃掉新页面请求。 + modificationScope?.own?.(() => { + for (const dispose of disposers.splice(0).reverse()) { + try { + dispose(); + } catch {} + } + if (w.__opencodexStatsigTelemetryGuardInstalled === providerGeneration) { + w.__opencodexStatsigTelemetryGuardInstalled = undefined; + } + }); +})(); diff --git a/web-shell/src/modification-browser-host.ts b/web-shell/src/modification-browser-host.ts index b372818..50def10 100644 --- a/web-shell/src/modification-browser-host.ts +++ b/web-shell/src/modification-browser-host.ts @@ -231,6 +231,7 @@ const BROWSER_PROVIDER_DEFINITIONS: readonly BrowserProviderDefinition[] = Objec { key: "remote-file-actions", points: { primary: "web.runtime.dom.remote-file-menu" } }, { key: "workspace-root-picker", points: { primary: "web.runtime.workspace.root-picker" } }, { key: "tooltip-dismiss", points: { primary: "web.runtime.dom.tooltip-dismiss" } }, + { key: "statsig-telemetry-guard", points: { primary: "web.runtime.network.telemetry-guard" } }, ]); const MOBILE_SIDEBAR_TOUCH_SCROLL_STYLE_ID = "opencodex-mobile-sidebar-touch-scroll-styles"; From acece974ba6c4963a3c31f0bb88eacf3c604e1a6 Mon Sep 17 00:00:00 2001 From: RyensX Date: Thu, 17 Sep 2026 04:14:53 +0000 Subject: [PATCH 2/5] test(polyfill): cover Statsig telemetry guard behavior --- gateway/test/statsig-telemetry-guard.test.cjs | 395 ++++++++++++++++++ 1 file changed, 395 insertions(+) create mode 100644 gateway/test/statsig-telemetry-guard.test.cjs diff --git a/gateway/test/statsig-telemetry-guard.test.cjs b/gateway/test/statsig-telemetry-guard.test.cjs new file mode 100644 index 0000000..06520e4 --- /dev/null +++ b/gateway/test/statsig-telemetry-guard.test.cjs @@ -0,0 +1,395 @@ +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const test = require("node:test"); +const vm = require("node:vm"); + +const REPO_ROOT = path.resolve(__dirname, "..", ".."); +const PROVIDER_SOURCE = fs.readFileSync( + path.join(REPO_ROOT, "web-shell", "internal", "providers", "codex-statsig-telemetry-guard.js"), + "utf8" +); +const POLYFILL_SOURCE = fs.readFileSync( + path.join(REPO_ROOT, "web-shell", "internal", "providers", "codex-bridge-polyfill.js"), + "utf8" +); +const STATIC_ASSETS_SOURCE = fs.readFileSync( + path.join(REPO_ROOT, "gateway", "runtime", "http", "static-assets.cjs"), + "utf8" +); +const CATALOG_SOURCE = fs.readFileSync( + path.join(REPO_ROOT, "gateway", "src", "modification", "catalog.ts"), + "utf8" +); +const BROWSER_HOST_SOURCE = fs.readFileSync( + path.join(REPO_ROOT, "web-shell", "src", "modification-browser-host.ts"), + "utf8" +); +const { createStaticAssetService } = require("../runtime/http/static-assets.cjs"); +const { messagesForLocale } = require("../../shared/i18n/index.cjs"); + +const POINT_ID = "web.runtime.network.telemetry-guard"; +const PROVIDER_KEY = "statsig-telemetry-guard"; +const GUARD_URL_PATH = "/codex-statsig-telemetry-guard.js"; +const I18N_KEY = "web.runtimeCompatibility.point." + POINT_ID + ".description"; +const NATIVE_BEACON_RESULT = "native-beacon-result"; +const NEWLINE = String.fromCharCode(10); + +/** 可控计时器:验证模拟响应确实被推迟到 send 返回之后。 */ +function createScheduler() { + let nextId = 1; + const timers = new Map(); + const api = { + setTimeout(callback, delay) { + const id = nextId++; + timers.set(id, { callback, delay }); + return id; + }, + clearTimeout(id) { + timers.delete(id); + }, + }; + return { + api, + timers, + capture() { + return { + setTimeout: (callback, delay) => api.setTimeout(callback, delay), + clearTimeout: (id) => api.clearTimeout(id), + }; + }, + flush() { + const pending = Array.from(timers.values()); + timers.clear(); + for (const timer of pending) timer.callback(); + }, + }; +} + +function createHarness() { + const calls = { open: [], send: [], beacon: [] }; + + class FakeXHR { + constructor() { + this.listeners = new Map(); + this.events = []; + } + open(method, url) { + calls.open.push({ target: this, method, url }); + } + send(body) { + calls.send.push({ target: this, body }); + } + setRequestHeader() {} + addEventListener(type, handler) { + if (!this.listeners.has(type)) this.listeners.set(type, new Set()); + this.listeners.get(type).add(handler); + } + removeEventListener(type, handler) { + const handlers = this.listeners.get(type); + if (handlers) handlers.delete(handler); + } + dispatchEvent(event) { + this.events.push(event.type); + for (const handler of Array.from(this.listeners.get(event.type) || [])) { + handler({ type: event.type, target: this }); + } + return true; + } + } + + // 真实浏览器里 status/responseText/readyState 是原型上的只读 IDL getter, + // 给实例属性直接赋值会被静默忽略,所以测试必须复刻这个形状, + // 才能证明 Provider 的 defineProperty 覆盖真的生效。 + const readonlyProperties = [ + ["status", 0], + ["statusText", ""], + ["response", null], + ["responseText", ""], + ["readyState", 0], + ]; + for (const entry of readonlyProperties) { + Object.defineProperty(FakeXHR.prototype, entry[0], { + configurable: true, + get: () => entry[1], + }); + } + + const nativeBeacon = (url, data) => { + calls.beacon.push({ url, data }); + return NATIVE_BEACON_RESULT; + }; + + const scheduler = createScheduler(); + const scope = { + generation: 7, + emits: 0, + owned: [], + effects: { primary: { emit: () => { scope.emits += 1; } } }, + own(dispose) { + scope.owned.push(dispose); + return () => { + const index = scope.owned.indexOf(dispose); + if (index >= 0) scope.owned.splice(index, 1); + }; + }, + }; + + const window = { + XMLHttpRequest: FakeXHR, + navigator: { sendBeacon: nativeBeacon }, + location: { href: "https://chatgpt.com/developer-mode" }, + setTimeout: (callback, delay) => scheduler.api.setTimeout(callback, delay), + }; + window.__OpenCodexAdapterHost = { scheduler: { capture: () => scheduler.capture() } }; + window.__OpenCodexCurrentProviderScope = scope; + + const sandbox = { + URL, + Event: class TestEvent { + constructor(type) { + this.type = type; + } + }, + console, + document: {}, + location: window.location, + navigator: window.navigator, + window, + XMLHttpRequest: FakeXHR, + }; + + return { + FakeXHR, + calls, + nativeBeacon, + scheduler, + scope, + timers: scheduler.timers, + window, + install() { + vm.runInNewContext(PROVIDER_SOURCE, sandbox); + }, + }; +} + +test("telemetry XHR is swallowed locally and answered with a mock successful response", () => { + const harness = createHarness(); + harness.install(); + // 补丁装上即上报一次真实命中,运行时兼容调试页据此显示 active。 + assert.equal(harness.scope.emits, 1); + + const xhr = new harness.FakeXHR(); + const handled = []; + xhr.addEventListener("load", () => handled.push("load")); + xhr.addEventListener("loadend", () => handled.push("loadend")); + xhr.open("POST", "https://chatgpt.com/ces/v1/rgstr"); + assert.equal(harness.calls.open.length, 1); + + xhr.send("telemetry-payload"); + assert.equal(harness.calls.send.length, 0, "telemetry must not reach the native send"); + assert.deepEqual(xhr.events, ["loadstart"]); + // 状态补齐必须延后:SDK 通常在 send 返回之后才挂 load 监听器。 + assert.equal(harness.timers.size, 1); + + harness.scheduler.flush(); + assert.equal(xhr.status, 200); + assert.equal(xhr.statusText, "OK"); + assert.equal(xhr.readyState, 4); + assert.equal(xhr.responseText, "{}"); + assert.equal(xhr.response, "{}"); + assert.deepEqual(xhr.events, ["loadstart", "readystatechange", "load", "loadend"]); + assert.deepEqual(handled, ["load", "loadend"]); + assert.equal(harness.timers.size, 0); + // 覆盖只发生在实例上,原型 getter 仍是初始值,说明没有污染其他 XHR。 + const fresh = new harness.FakeXHR(); + assert.equal(Object.getOwnPropertyDescriptor(harness.FakeXHR.prototype, "status").get.call(fresh), 0); +}); + +test("non-telemetry XHR keeps using the native send", () => { + const harness = createHarness(); + harness.install(); + + const passthroughUrls = [ + "https://chatgpt.com/v1/models", + "https://chatgpt.com.evil.example/ces/v1/rgstr", + "https://example.com/ces/v1/rgstr", + ]; + passthroughUrls.forEach((url, index) => { + const xhr = new harness.FakeXHR(); + xhr.open("GET", url); + xhr.send(null); + assert.equal(harness.calls.send.length, index + 1, "passthrough url: " + url); + assert.equal(harness.timers.size, 0); + assert.equal(xhr.readyState, 0); + assert.deepEqual(xhr.events, []); + }); +}); + +test("telemetry beacons report success without touching the native API", () => { + const harness = createHarness(); + harness.install(); + + assert.equal( + harness.window.navigator.sendBeacon("https://chatgpt.com/ces/v1/rgstr", "payload"), + true + ); + assert.equal(harness.window.navigator.sendBeacon("/ces/v1/log_event", "payload"), true); + assert.equal(harness.calls.beacon.length, 0); + + assert.equal( + harness.window.navigator.sendBeacon("https://example.com/ces/v1/rgstr", "payload"), + NATIVE_BEACON_RESULT + ); + assert.equal(harness.calls.beacon.length, 1); + assert.equal(harness.calls.beacon[0].url, "https://example.com/ces/v1/rgstr"); +}); + +test("loading the provider twice in one page generation installs a single patch", () => { + const harness = createHarness(); + harness.install(); + const patchedOpen = harness.FakeXHR.prototype.open; + const patchedSend = harness.FakeXHR.prototype.send; + const patchedBeacon = harness.window.navigator.sendBeacon; + + harness.install(); + assert.equal(harness.FakeXHR.prototype.open, patchedOpen); + assert.equal(harness.FakeXHR.prototype.send, patchedSend); + assert.equal(harness.window.navigator.sendBeacon, patchedBeacon); + assert.equal(harness.scope.emits, 1); + assert.equal(harness.scope.owned.length, 1); +}); + +test("the dispose registered through own() restores the native prototypes", () => { + const harness = createHarness(); + const originalOpen = harness.FakeXHR.prototype.open; + const originalSend = harness.FakeXHR.prototype.send; + harness.install(); + assert.equal(harness.scope.owned.length, 1); + + // 宿主在换页时逆序 dispose 所有 own 登记的资源。 + harness.scope.owned[0](); + assert.equal(harness.FakeXHR.prototype.open, originalOpen); + assert.equal(harness.FakeXHR.prototype.send, originalSend); + assert.equal(harness.window.navigator.sendBeacon, harness.nativeBeacon); + assert.equal(harness.window.__opencodexStatsigTelemetryGuardInstalled, undefined); + + const xhr = new harness.FakeXHR(); + xhr.open("POST", "https://chatgpt.com/ces/v1/rgstr"); + xhr.send("payload"); + assert.equal(harness.calls.send.length, 1, "restored prototype must stop swallowing"); + assert.equal(harness.timers.size, 0); + + // 还原标记后新页面可以重新安装补丁。 + harness.install(); + assert.equal(harness.scope.emits, 2); +}); + +test("the guard lives in its own provider while the upstream polyfill keeps no telemetry patch", () => { + // 装配层负责把正文包成 providers.register 调用,脚本自身不得自注册。 + assert.ok(!PROVIDER_SOURCE.includes("providers.register")); + assert.ok(PROVIDER_SOURCE.includes("__opencodexStatsigTelemetryGuardInstalled")); + assert.ok(PROVIDER_SOURCE.includes("modificationScope?.own?.(")); + assert.ok(PROVIDER_SOURCE.includes("modificationEffects?.primary?.emit()")); + // fetch 通道的遥测拦截仍归上游 polyfill;XHR/Beacon 通道已搬进独立 Provider。 + assert.ok(POLYFILL_SOURCE.includes("isTelemetryRegisterUrl")); + const forbiddenPolyfillMarkers = [ + "__codexWebXhrPatched", + "__codexWebBeaconPatched", + "sendBeacon", + "XMLHttpRequest.prototype.open", + ]; + for (const marker of forbiddenPolyfillMarkers) { + assert.ok(!POLYFILL_SOURCE.includes(marker), "polyfill must stay free of " + marker); + } +}); + +test("the gateway serves the guard and registers it in the aggregated runtime bootstrap", (t) => { + const webviewDir = fs.mkdtempSync(path.join(os.tmpdir(), "opencodex-statsig-")); + t.after(() => fs.rmSync(webviewDir, { recursive: true, force: true })); + fs.writeFileSync( + path.join(webviewDir, "index.html"), + "Codex" + ); + const service = createStaticAssetService({ + compatibilityService: null, + getI18nSnapshot: () => ({ locale: "en-US", messages: messagesForLocale("en-US") }), + getOfficialBundle: () => ({ webviewDir }), + }); + + assert.equal( + path.basename(service.staticFile(GUARD_URL_PATH)), + "codex-statsig-telemetry-guard.js" + ); + + const res = { + body: Buffer.alloc(0), + headers: {}, + status: 0, + writeHead(status, headers) { + this.status = status; + this.headers = headers || {}; + }, + end(body) { + this.body = Buffer.isBuffer(body) ? body : Buffer.from(String(body || ""), "utf-8"); + }, + }; + service.serveRuntimeBootstrap({ headers: {} }, res); + assert.equal(res.status, 200); + const bootstrap = res.body.toString("utf-8"); + + const guardIndex = bootstrap.indexOf('providers.register("' + PROVIDER_KEY + '"'); + const tooltipIndex = bootstrap.indexOf('providers.register("tooltip-dismiss"'); + assert.ok(guardIndex > 0, "guard must be wrapped by the provider registry"); + assert.ok(tooltipIndex >= 0 && guardIndex > tooltipIndex, "guard loads after the tooltip guard"); + // 正文必须原样嵌进 register 回调,装配层不会改写实现。 + assert.ok(bootstrap.includes("__opencodexStatsigTelemetryGuardInstalled")); + + // 启动顺序:补丁要在 Kernel 激活脚本之前装上,官方 SDK 初始化时 XHR 原型才已被接管。 + const groups = STATIC_ASSETS_SOURCE.slice(STATIC_ASSETS_SOURCE.indexOf("function runtimeBootstrapFileGroups")); + const guardOrder = groups.indexOf("CODEX_STATSIG_TELEMETRY_GUARD_PATH"); + const activateOrder = groups.indexOf("OPENCODEX_MODIFICATION_ACTIVATE_PATH"); + assert.ok(guardOrder >= 0 && activateOrder > guardOrder, "guard must load before kernel activation"); +}); + +test("the new point is cataloged once, bound once, and localized in both locales", () => { + const declaration = '"' + POINT_ID + '"'; + const catalogLines = CATALOG_SOURCE.split(NEWLINE).filter((line) => line.includes(declaration)); + assert.equal(catalogLines.length, 1, "the point must be declared exactly once"); + assert.ok(catalogLines[0].includes("web-shell")); + assert.ok(catalogLines[0].includes("G.webNetwork")); + assert.ok(catalogLines[0].includes("A.networkRequest")); + + // 同一个点被两个 provider 绑定会在加载期抛错,这里按源码行守住唯一绑定。 + const bindingLines = BROWSER_HOST_SOURCE.split(NEWLINE).filter((line) => line.includes(POINT_ID)); + assert.equal(bindingLines.length, 1, "the point must bind exactly one provider"); + assert.ok(bindingLines[0].includes('key: "' + PROVIDER_KEY + '"')); + assert.ok(bindingLines[0].includes('primary: "' + POINT_ID + '"')); + + assert.ok( + STATIC_ASSETS_SOURCE.includes( + '[path.join(INTERNAL_PROVIDER_DIR, "codex-statsig-telemetry-guard.js"), "' + PROVIDER_KEY + '"]' + ), + "static assets must map the file to the provider key" + ); + assert.ok( + STATIC_ASSETS_SOURCE.includes('const CODEX_STATSIG_TELEMETRY_GUARD_PATH = "' + GUARD_URL_PATH + '"') + ); + assert.ok( + STATIC_ASSETS_SOURCE.includes( + "[CODEX_STATSIG_TELEMETRY_GUARD_PATH, path.join(INTERNAL_PROVIDER_DIR, \"codex-statsig-telemetry-guard.js\")]" + ), + "the guard file must be exposed as a static asset" + ); + + for (const locale of ["en-US", "zh-CN"]) { + const messages = JSON.parse( + fs.readFileSync( + path.join(REPO_ROOT, "shared", "i18n", "locales", "runtime-compatibility-" + locale + ".json"), + "utf8" + ) + ); + assert.ok(String(messages[I18N_KEY] || "").trim(), "missing " + I18N_KEY + " for " + locale); + } +}); From c0405a9eea75498a94beb07211640b1b6f798ffb Mon Sep 17 00:00:00 2001 From: RyensX Date: Thu, 17 Sep 2026 04:25:05 +0000 Subject: [PATCH 3/5] test(polyfill): sync point counts for the telemetry guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 web.runtime.network.telemetry-guard 后,仓库里 6 个测试文件把修改点总数与 host/前缀分布硬编码为 103,需要同步为 104(browser 37→38、web.runtime. 37→38)。同时移除 zh-CN 语言包里多余的新增 key:该文件此前不承载任何 runtimeCompatibility.point.* 文案(中文以 catalog.ts 的描述为准),保留会破坏既有约定。 --- gateway/test/compatibility-registry.test.cjs | 4 ++-- gateway/test/compatibility-service.test.cjs | 4 ++-- gateway/test/modification-equivalence.test.cjs | 4 ++-- gateway/test/modification-kernel.test.cjs | 14 +++++++------- gateway/test/official-desktop-compat.test.cjs | 2 +- .../i18n/locales/runtime-compatibility-zh-CN.json | 1 - 6 files changed, 14 insertions(+), 15 deletions(-) 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..9eebd8d 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(); 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..97b7d54 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] + [38, 36, 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] + [38, 36, 30] ); assert.equal(POINT_DEFINITIONS.every((point) => point.group && point.contributions.length > 0), true); assert.equal(POINT_DEFINITIONS.every((point) => point.contributions.every((item) => { 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/shared/i18n/locales/runtime-compatibility-zh-CN.json b/shared/i18n/locales/runtime-compatibility-zh-CN.json index e33ebe5..53486e6 100644 --- a/shared/i18n/locales/runtime-compatibility-zh-CN.json +++ b/shared/i18n/locales/runtime-compatibility-zh-CN.json @@ -6,7 +6,6 @@ "web.runtimeCompatibility.pluginTag": "插件", "web.runtimeCompatibility.points": "修改点", "web.runtimeCompatibility.pointsDescription": "每个修改点按开发者功能组归类;分组只用于查看,不影响启用、回退或执行决策。", - "web.runtimeCompatibility.point.web.runtime.network.telemetry-guard.description": "本地吞掉 Statsig XHR 与 Beacon 遥测。", "web.runtimeCompatibility.filters": "修改点筛选", "web.runtimeCompatibility.adapterFilter": "注入类型", "web.runtimeCompatibility.column.adapter": "注入类别", From e75523888bdedbaa66256d72dc3b92386a3c5193 Mon Sep 17 00:00:00 2001 From: RyensX Date: Thu, 17 Sep 2026 04:27:24 +0000 Subject: [PATCH 4/5] test(polyfill): rebalance point distribution assertions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 web.runtime.network.telemetry-guard 落在 web-network 组,分组分布断言该组计数 3 -> 4。 compatibility-service 中"合并一个外部插件点后的快照长度"断言基数随之从 104 变为 105。 --- gateway/test/compatibility-service.test.cjs | 2 +- gateway/test/modification-kernel.test.cjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gateway/test/compatibility-service.test.cjs b/gateway/test/compatibility-service.test.cjs index 9eebd8d..10b68a7 100644 --- a/gateway/test/compatibility-service.test.cjs +++ b/gateway/test/compatibility-service.test.cjs @@ -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-kernel.test.cjs b/gateway/test/modification-kernel.test.cjs index 97b7d54..e98973c 100644 --- a/gateway/test/modification-kernel.test.cjs +++ b/gateway/test/modification-kernel.test.cjs @@ -67,7 +67,7 @@ test("typed modification catalog assigns every point to a group and adapter chai "mobile-interaction": 5, "renderer-ui": 6, "browser-platform": 3, - "web-network": 3, + "web-network": 4, "gateway-runtime": 9, "gateway-ipc": 4, "official-main": 3, From f629b2fee0fceca0726846ee9107054aad563ec1 Mon Sep 17 00:00:00 2001 From: RyensX Date: Thu, 17 Sep 2026 04:35:36 +0000 Subject: [PATCH 5/5] fix(polyfill): close the telemetry guard wiring gaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按骨架约定补齐四处接线与规范缺口: 1. check-modification-boundaries 仍把点数硬编码为 103。它是 build:gateway 的 pretest 环节,新增点让 dist 变成 104 项后,全新构建会在编译阶段直接抛错, CI 与本地干净构建都会失败(实测 pnpm run build:gateway 退出码 1)。 2. 逐文件回退加载路径漏装该 Provider:官方 bundle 含 eager script 或存在外部插件时 canBundleRuntimeBootstrap 为 false,Provider 根本不加载,修改点会在 locate 阶段被判 unsupported。补进与 tooltip-dismiss 同样的 runtimeScript 序列。 3. Provider 在 installer 末尾无条件 emit,违反 docs/MODIFICATION_SKELETON.md 的 "安装完成只代表 ready""不得在 installer 完成时上报 hit"。改为在 send/sendBeacon 真正吞掉遥测时逐次上报,调试页由此反映真实流量;测试同步断言安装后 emits 为 0、 透传流量不计数、每条被吞请求计一次。 4. 新测试断言 zh-CN 也必须给点描述,与仓库现状冲突:zh-CN 语言包不含任何 runtimeCompatibility.point.* 键(中文由 catalog.ts 描述经 metadataText 回退)。 改为断言该约定,并顺手把测试文件登记进 package.json 的 test 脚本, 否则 pnpm test 永远不会执行它。 --- gateway/runtime/http/static-assets.cjs | 3 ++ gateway/test/statsig-telemetry-guard.test.cjs | 38 ++++++++++++------- package.json | 2 +- scripts/check-modification-boundaries.cjs | 10 ++--- .../codex-statsig-telemetry-guard.js | 12 ++++-- 5 files changed, 42 insertions(+), 23 deletions(-) diff --git a/gateway/runtime/http/static-assets.cjs b/gateway/runtime/http/static-assets.cjs index e77f9c6..1af7197 100644 --- a/gateway/runtime/http/static-assets.cjs +++ b/gateway/runtime/http/static-assets.cjs @@ -997,6 +997,9 @@ function createStaticAssetService({ runtimeScript(CODEX_REMOTE_FILE_ACTIONS_PATH), runtimeScript(CODEX_WORKSPACE_ROOT_PICKER_PATH), runtimeScript(CODEX_TOOLTIP_DISMISS_GUARD_PATH), + // 聚合启动路径之外的逐文件回退加载同样要带上遥测拦截,否则官方 bundle 含 eager script + // 或存在外部插件时该 Provider 根本不加载,修改点会在 locate 阶段被判 unsupported。 + runtimeScript(CODEX_STATSIG_TELEMETRY_GUARD_PATH), runtimeScript(OPENCODEX_MODIFICATION_ACTIVATE_PATH), ]; // manifest 在 Cloudflare Access 等前置认证后面也必须带同源凭据,否则 Chrome 可能拿不到受保护的 manifest。 diff --git a/gateway/test/statsig-telemetry-guard.test.cjs b/gateway/test/statsig-telemetry-guard.test.cjs index 06520e4..e023a6d 100644 --- a/gateway/test/statsig-telemetry-guard.test.cjs +++ b/gateway/test/statsig-telemetry-guard.test.cjs @@ -177,8 +177,8 @@ function createHarness() { test("telemetry XHR is swallowed locally and answered with a mock successful response", () => { const harness = createHarness(); harness.install(); - // 补丁装上即上报一次真实命中,运行时兼容调试页据此显示 active。 - assert.equal(harness.scope.emits, 1); + // 骨架约定:安装完成只代表 ready,没有真实遥测流量时不该上报命中。 + assert.equal(harness.scope.emits, 0); const xhr = new harness.FakeXHR(); const handled = []; @@ -189,6 +189,8 @@ test("telemetry XHR is swallowed locally and answered with a mock successful res xhr.send("telemetry-payload"); assert.equal(harness.calls.send.length, 0, "telemetry must not reach the native send"); + // 真实吞掉一条遥测才计一次命中,调试页据此从 ready 变为 active。 + assert.equal(harness.scope.emits, 1); assert.deepEqual(xhr.events, ["loadstart"]); // 状态补齐必须延后:SDK 通常在 send 返回之后才挂 load 监听器。 assert.equal(harness.timers.size, 1); @@ -225,6 +227,7 @@ test("non-telemetry XHR keeps using the native send", () => { assert.equal(xhr.readyState, 0); assert.deepEqual(xhr.events, []); }); + assert.equal(harness.scope.emits, 0, "passthrough traffic must not report a hit"); }); test("telemetry beacons report success without touching the native API", () => { @@ -237,6 +240,7 @@ test("telemetry beacons report success without touching the native API", () => { ); assert.equal(harness.window.navigator.sendBeacon("/ces/v1/log_event", "payload"), true); assert.equal(harness.calls.beacon.length, 0); + assert.equal(harness.scope.emits, 2, "each swallowed beacon reports one hit"); assert.equal( harness.window.navigator.sendBeacon("https://example.com/ces/v1/rgstr", "payload"), @@ -244,6 +248,7 @@ test("telemetry beacons report success without touching the native API", () => { ); assert.equal(harness.calls.beacon.length, 1); assert.equal(harness.calls.beacon[0].url, "https://example.com/ces/v1/rgstr"); + assert.equal(harness.scope.emits, 2, "a forwarded beacon must not report a hit"); }); test("loading the provider twice in one page generation installs a single patch", () => { @@ -257,7 +262,7 @@ test("loading the provider twice in one page generation installs a single patch" assert.equal(harness.FakeXHR.prototype.open, patchedOpen); assert.equal(harness.FakeXHR.prototype.send, patchedSend); assert.equal(harness.window.navigator.sendBeacon, patchedBeacon); - assert.equal(harness.scope.emits, 1); + assert.equal(harness.scope.emits, 0, "installing alone never reports a hit"); assert.equal(harness.scope.owned.length, 1); }); @@ -280,10 +285,12 @@ test("the dispose registered through own() restores the native prototypes", () = xhr.send("payload"); assert.equal(harness.calls.send.length, 1, "restored prototype must stop swallowing"); assert.equal(harness.timers.size, 0); + assert.equal(harness.scope.emits, 0, "traffic after dispose went through the native send"); // 还原标记后新页面可以重新安装补丁。 harness.install(); - assert.equal(harness.scope.emits, 2); + assert.notEqual(harness.FakeXHR.prototype.open, originalOpen, "reinstall must patch again"); + assert.equal(harness.scope.emits, 0); }); test("the guard lives in its own provider while the upstream polyfill keeps no telemetry patch", () => { @@ -383,13 +390,18 @@ test("the new point is cataloged once, bound once, and localized in both locales "the guard file must be exposed as a static asset" ); - for (const locale of ["en-US", "zh-CN"]) { - const messages = JSON.parse( - fs.readFileSync( - path.join(REPO_ROOT, "shared", "i18n", "locales", "runtime-compatibility-" + locale + ".json"), - "utf8" - ) - ); - assert.ok(String(messages[I18N_KEY] || "").trim(), "missing " + I18N_KEY + " for " + locale); - } + // 英文必须显式给描述;zh-CN 语言包按仓库约定不承载任何修改点文案(中文取自 catalog.ts 的 + // 中文描述并经 metadataText 回退),这里守住该约定不被悄悄破坏。 + const localeDir = path.join(REPO_ROOT, "shared", "i18n", "locales"); + const enMessages = JSON.parse( + fs.readFileSync(path.join(localeDir, "runtime-compatibility-en-US.json"), "utf8") + ); + assert.ok(String(enMessages[I18N_KEY] || "").trim(), "missing " + I18N_KEY + " for en-US"); + const zhMessages = JSON.parse( + fs.readFileSync(path.join(localeDir, "runtime-compatibility-zh-CN.json"), "utf8") + ); + const zhPointKeys = Object.keys(zhMessages).filter((key) => + key.startsWith("web.runtimeCompatibility.point.") + ); + assert.deepEqual(zhPointKeys, [], "zh-CN keeps point descriptions in catalog.ts only"); }); diff --git a/package.json b/package.json index 4ee2720..010eb18 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "sync:version": "node scripts/sync-app-version.cjs", "check:version": "node scripts/sync-app-version.cjs --check", "pretest": "pnpm run build:gateway", - "test": "node --test gateway/test/app-host-message-codec.test.cjs gateway/test/auth-rate-limit.test.cjs gateway/test/auth-request.test.cjs gateway/test/compatibility-registry.test.cjs gateway/test/compatibility-service.test.cjs gateway/test/gateway-lifecycle.test.cjs gateway/test/history-preview.test.cjs gateway/test/http-utils.test.cjs gateway/test/local-files.test.cjs gateway/test/loopback-host.test.cjs gateway/test/mobile-sidebar-auto-collapse.test.cjs gateway/test/model-router-classifier.test.cjs gateway/test/model-router-injection-health.test.cjs gateway/test/model-router-presentation.test.cjs gateway/test/model-router-transport.test.cjs gateway/test/model-router.test.cjs gateway/test/modification-browser-host.test.cjs gateway/test/modification-equivalence.test.cjs gateway/test/modification-kernel.test.cjs gateway/test/modification-node-provider.test.cjs gateway/test/modification-production.test.cjs gateway/test/official-desktop-compat.test.cjs gateway/test/official-electron-module-hook.test.cjs gateway/test/official-runtime.test.cjs gateway/test/open-file-context.test.cjs gateway/test/picked-files.test.cjs gateway/test/plugin-config.test.cjs gateway/test/project-recent-sort.test.cjs gateway/test/runtime-compatibility-page.test.cjs gateway/test/service-control.test.cjs gateway/test/smart-model-router.integration.test.cjs gateway/test/smart-scheduling-summary.test.cjs gateway/test/static-assets.test.cjs gateway/test/token-usage-capability.test.cjs gateway/test/virtual-model.test.cjs gateway/test/web-runtime-compatibility.test.cjs gateway/test/web-runtime-performance.test.cjs gateway/test/workspace-root-context.test.cjs gateway/test/workspace-root-picker.test.cjs gateway/test/workspace-roots.test.cjs gateway/test/ws-hub-client-ready.test.cjs gateway/test/ws-hub.test.cjs launcher/test/log-writer.test.cjs launcher/test/pwa-launcher.test.cjs", + "test": "node --test gateway/test/app-host-message-codec.test.cjs gateway/test/auth-rate-limit.test.cjs gateway/test/auth-request.test.cjs gateway/test/compatibility-registry.test.cjs gateway/test/compatibility-service.test.cjs gateway/test/gateway-lifecycle.test.cjs gateway/test/history-preview.test.cjs gateway/test/http-utils.test.cjs gateway/test/local-files.test.cjs gateway/test/loopback-host.test.cjs gateway/test/mobile-sidebar-auto-collapse.test.cjs gateway/test/model-router-classifier.test.cjs gateway/test/model-router-injection-health.test.cjs gateway/test/model-router-presentation.test.cjs gateway/test/model-router-transport.test.cjs gateway/test/model-router.test.cjs gateway/test/modification-browser-host.test.cjs gateway/test/modification-equivalence.test.cjs gateway/test/modification-kernel.test.cjs gateway/test/modification-node-provider.test.cjs gateway/test/modification-production.test.cjs gateway/test/official-desktop-compat.test.cjs gateway/test/official-electron-module-hook.test.cjs gateway/test/official-runtime.test.cjs gateway/test/open-file-context.test.cjs gateway/test/picked-files.test.cjs gateway/test/plugin-config.test.cjs gateway/test/project-recent-sort.test.cjs gateway/test/runtime-compatibility-page.test.cjs gateway/test/service-control.test.cjs gateway/test/smart-model-router.integration.test.cjs gateway/test/smart-scheduling-summary.test.cjs gateway/test/static-assets.test.cjs gateway/test/statsig-telemetry-guard.test.cjs gateway/test/token-usage-capability.test.cjs gateway/test/virtual-model.test.cjs gateway/test/web-runtime-compatibility.test.cjs gateway/test/web-runtime-performance.test.cjs gateway/test/workspace-root-context.test.cjs gateway/test/workspace-root-picker.test.cjs gateway/test/workspace-roots.test.cjs gateway/test/ws-hub-client-ready.test.cjs gateway/test/ws-hub.test.cjs launcher/test/log-writer.test.cjs launcher/test/pwa-launcher.test.cjs", "eval:model-router-prompts": "node gateway/dev/model-router-prompt-ab.cjs", "web:dev": "pnpm run build:gateway && node gateway/dev/run-gateway.cjs", "build": "pnpm run build:gateway", 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/web-shell/internal/providers/codex-statsig-telemetry-guard.js b/web-shell/internal/providers/codex-statsig-telemetry-guard.js index 8f3d384..b628afb 100644 --- a/web-shell/internal/providers/codex-statsig-telemetry-guard.js +++ b/web-shell/internal/providers/codex-statsig-telemetry-guard.js @@ -86,6 +86,8 @@ return originalSend.call(this, body); } + // 按骨架约定,安装完成只代表 ready,命中只能在真实拦截发生时上报。 + modificationEffects?.primary?.emit(); // 跳过真实网络请求:loadstart 同步补发,其余状态异步补齐, // 因为 SDK 通常在 send 返回之后才注册 load 监听器。 safeDispatch(xhr, "loadstart"); @@ -118,7 +120,10 @@ } catch { target = String(url || ""); } - if (isTelemetryUrl(target)) return true; + if (isTelemetryUrl(target)) { + modificationEffects?.primary?.emit(); + return true; + } return boundSendBeacon(url, ...rest); }; disposers.push(() => { @@ -126,9 +131,8 @@ }); } - // 补丁装上即视为该修改点真实生效,上报一次命中。 - modificationEffects?.primary?.emit(); - + // 安装完成只代表 ready:命中改到 send/sendBeacon 真正吞掉遥测时逐次上报, + // 运行时兼容调试页因此反映真实遥测流量,而不是"脚本装上了"。 // 换页或关闭时由宿主逆序 dispose:还原原型与 Beacon,避免旧补丁继续吃掉新页面请求。 modificationScope?.own?.(() => { for (const dispose of disposers.splice(0).reverse()) {