Skip to content

Commit 11d149e

Browse files
cyfung1031CodFrm
andauthored
🐛 新增 extensionEnv 处理 isIncognito (early-start & bgScript), userAgent (bgScript) 及 run-in (bgScript) (#1368)
* 新增 extensionEnv 处理 isIncognito (early-start & bgScript), userAgent (bgScript) 及 run-in (bgScript) * 修正 extensionEnv 引用 * 代码修正 * typescript fix * ✅ 补 extensionEnv / BgExecScriptWarp / sandbox run-in 单元测试 - extension_env.test.ts:覆盖 getExtensionUserAgentData 的 navigator 缺失、platformInfo 异常、bitness 64/32 派生 - exec_warp.test.ts:覆盖 BgExecScriptWarp 把 extensionEnv 注入 GM_info 的各分支 - sandbox/runtime.test.ts:覆盖 execScript 中 run-in × inIncognitoContext 的过滤逻辑(含 extensionEnv 为 undefined 的 fail-open 行为) * 📝 新增 后台脚本 @run-in 与 isIncognito 示例 --------- Co-authored-by: 王一之 <yz@ggnb.top>
1 parent 81e11c0 commit 11d149e

18 files changed

Lines changed: 448 additions & 39 deletions

File tree

example/run-in/run-in_bg.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// ==UserScript==
2+
// @name Test @run-in background
3+
// @namespace https://bbs.tampermonkey.net.cn/
4+
// @version 0.1.0
5+
// @description 后台脚本支持 @run-in 区分正常窗口与隐身窗口;同时可透过 GM_info.isIncognito 与 GM_info.userAgentData 取得运行环境
6+
// @author You
7+
// @background
8+
// @run-in incognito-tabs
9+
// @grant GM_log
10+
// ==/UserScript==
11+
12+
return new Promise((resolve) => {
13+
// 后台脚本指定 @run-in incognito-tabs 后,仅在隐身窗口对应的扩展环境中执行
14+
// 若改为 normal-tabs 则仅在正常窗口环境执行;不写或写 @run-in normal-tabs 与 @run-in incognito-tabs 时两者皆执行
15+
GM_log(`run-in: ${GM_info.script["run-in"]}`);
16+
GM_log(`isIncognito: ${GM_info.isIncognito}`);
17+
GM_log(`userAgentData: ${JSON.stringify(GM_info.userAgentData)}`);
18+
resolve();
19+
});

packages/message/common.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { type TExtensionEnv } from "@App/app/service/extension/extension_env";
12
import { randomMessageFlag } from "@App/pkg/utils/utils";
23

34
// 避免页面载入后改动全域物件导致消息传递失败
@@ -21,7 +22,12 @@ export const pageDispatchCustomEvent = <T = any>(eventType: string, detail: T) =
2122
};
2223

2324
// flag协商
24-
export function negotiateEventFlag(messageFlag: string, readyCount: number, onInit: (eventFlag: string) => void): void {
25+
export function negotiateEventFlag(
26+
messageFlag: string,
27+
extensionEnv: TExtensionEnv,
28+
readyCount: number,
29+
onInit: (eventFlag: string) => void
30+
): void {
2531
const eventFlag = randomMessageFlag();
2632
onInit(eventFlag);
2733
// 监听 inject/content 发来的请求 eventFlag 的消息
@@ -40,27 +46,32 @@ export function negotiateEventFlag(messageFlag: string, readyCount: number, onIn
4046
break;
4147
case "requestEventFlag":
4248
// 广播通信 flag 给 inject/content
43-
pageDispatchCustomEvent(messageFlag, { action: "broadcastEventFlag", eventFlag: eventFlag });
49+
pageDispatchCustomEvent(messageFlag, { action: "broadcastEventFlag", eventFlag: eventFlag, extensionEnv });
4450
break;
4551
}
4652
};
4753

4854
// 设置事件,然后广播通信 flag 给 inject/content
4955
pageAddEventListener(messageFlag, fnEventFlagRequestHandler);
50-
pageDispatchCustomEvent(messageFlag, { action: "broadcastEventFlag", eventFlag: eventFlag });
56+
pageDispatchCustomEvent(messageFlag, { action: "broadcastEventFlag", eventFlag: eventFlag, extensionEnv });
5157
}
5258

5359
// 获取协商后的 eventFlag
54-
export function getEventFlag(messageFlag: string, onReady: (eventFlag: string) => void) {
60+
export function getEventFlag(
61+
messageFlag: string,
62+
onReady: (eventFlag: string, extensionEnv: TExtensionEnv | undefined) => void
63+
) {
5564
let eventFlag = "";
65+
let extensionEnv: TExtensionEnv | undefined = undefined;
5666
const fnEventFlagListener: EventListener = (ev: Event) => {
5767
if (!(ev instanceof CustomEvent)) return;
5868
if (ev.detail?.action != "broadcastEventFlag") return;
5969
eventFlag = ev.detail.eventFlag;
70+
extensionEnv = ev.detail.extensionEnv;
6071
pageRemoveEventListener(messageFlag, fnEventFlagListener);
6172
// 告知对方已收到 eventFlag
6273
pageDispatchCustomEvent(messageFlag, { action: "receivedEventFlag" });
63-
onReady(eventFlag);
74+
onReady(eventFlag, extensionEnv);
6475
};
6576

6677
// 设置事件,然后对 scripting 请求 flag
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { describe, expect, it } from "vitest";
2+
import { BgExecScriptWarp } from "./exec_warp";
3+
import type { ScriptLoadInfo } from "../service_worker/types";
4+
import type { Message } from "@Packages/message/types";
5+
import type { TExtensionEnv } from "../extension/extension_env";
6+
7+
const buildScriptRes = () =>
8+
({
9+
uuid: "uuid-bg",
10+
name: "bg-script",
11+
metadata: {
12+
grant: ["none"],
13+
version: ["1.0.0"],
14+
},
15+
code: "return GM_info;",
16+
sourceCode: "return GM_info;",
17+
value: {},
18+
}) as unknown as ScriptLoadInfo;
19+
20+
const fakeMessage = undefined as unknown as Message;
21+
22+
const getGMInfo = (exec: BgExecScriptWarp) => (exec as any).named?.GM_info;
23+
24+
describe("BgExecScriptWarp envInfo 注入", () => {
25+
it("extensionEnv 为 undefined 时使用默认值", () => {
26+
const exec = new BgExecScriptWarp(buildScriptRes(), fakeMessage, undefined);
27+
const gmInfo = getGMInfo(exec);
28+
expect(gmInfo.isIncognito).toBe(false);
29+
expect(gmInfo.userAgentData).toEqual({
30+
brands: [],
31+
mobile: false,
32+
platform: "",
33+
});
34+
expect(gmInfo.sandboxMode).toBe("raw");
35+
});
36+
37+
it("inIncognitoContext=true 时覆盖 isIncognito 为 true", () => {
38+
const extensionEnv: TExtensionEnv = { inIncognitoContext: true };
39+
const exec = new BgExecScriptWarp(buildScriptRes(), fakeMessage, extensionEnv);
40+
const gmInfo = getGMInfo(exec);
41+
expect(gmInfo.isIncognito).toBe(true);
42+
// 没传 userAgentData,沿用默认空值
43+
expect(gmInfo.userAgentData).toEqual({
44+
brands: [],
45+
mobile: false,
46+
platform: "",
47+
});
48+
});
49+
50+
it("传入完整 userAgentData 时整体替换默认值", () => {
51+
const extensionEnv: TExtensionEnv = {
52+
inIncognitoContext: false,
53+
userAgentData: {
54+
brands: [{ brand: "Chromium", version: "120" }],
55+
mobile: true,
56+
platform: "Android",
57+
architecture: "arm",
58+
bitness: "64",
59+
} as any,
60+
};
61+
const exec = new BgExecScriptWarp(buildScriptRes(), fakeMessage, extensionEnv);
62+
const gmInfo = getGMInfo(exec);
63+
expect(gmInfo.isIncognito).toBe(false);
64+
expect(gmInfo.userAgentData).toEqual({
65+
brands: [{ brand: "Chromium", version: "120" }],
66+
mobile: true,
67+
platform: "Android",
68+
architecture: "arm",
69+
bitness: "64",
70+
});
71+
});
72+
73+
it("userAgentData 为 null 时保留默认值", () => {
74+
const extensionEnv: TExtensionEnv = {
75+
inIncognitoContext: true,
76+
userAgentData: null,
77+
};
78+
const exec = new BgExecScriptWarp(buildScriptRes(), fakeMessage, extensionEnv);
79+
const gmInfo = getGMInfo(exec);
80+
expect(gmInfo.isIncognito).toBe(true);
81+
expect(gmInfo.userAgentData).toEqual({
82+
brands: [],
83+
mobile: false,
84+
platform: "",
85+
});
86+
});
87+
});

src/app/service/content/exec_warp.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import ExecScript from "./exec_script";
22
import type { Message } from "@Packages/message/types";
33
import type { ScriptLoadInfo } from "../service_worker/types";
44
import type { GMInfoEnv } from "./types";
5+
import { type TExtensionEnv } from "../extension/extension_env";
56

67
export class CATRetryError {
78
msg: string;
@@ -23,7 +24,7 @@ export class BgExecScriptWarp extends ExecScript {
2324

2425
setInterval: Map<number, boolean>;
2526

26-
constructor(scriptRes: ScriptLoadInfo, message: Message) {
27+
constructor(scriptRes: ScriptLoadInfo, message: Message, extensionEnv: TExtensionEnv | undefined) {
2728
const thisContext: { [key: string]: any } = {};
2829
const setTimeout = new Map<number, any>();
2930
const setInterval = new Map<number, any>();
@@ -73,6 +74,9 @@ export class BgExecScriptWarp extends ExecScript {
7374
},
7475
isIncognito: false,
7576
};
77+
const { inIncognitoContext, userAgentData } = extensionEnv || {};
78+
if (typeof inIncognitoContext === "boolean") envInfo.isIncognito = inIncognitoContext;
79+
if (userAgentData) envInfo.userAgentData = userAgentData;
7680
super(scriptRes, {
7781
envPrefix: "offscreen",
7882
message: message,

src/app/service/content/script_executor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ export type ExecScriptEntry = {
1717
scriptFunc: any;
1818
};
1919

20-
export const initEnvInfo = {
20+
export const initEnvInfo: GMInfoEnv = {
2121
/** userAgentData - 从全局变量获取 */
2222
userAgentData: typeof UserAgentData === "object" ? UserAgentData : {},
2323
/** sandboxMode - 预留字段,当前固定为 raw */
2424
sandboxMode: "raw",
25-
/** isIncognito - inject/content 环境下无法判断,固定为 false */
25+
/** isIncognito - inject/content 环境下透過 scripting 环境判断 */
2626
/** 使用者可透过 「 await navigator.storage.persisted() 」来判断,但ScriptCat不会主动执行此代码来判断 */
2727
isIncognito: false,
2828
} satisfies GMInfoEnv;

src/app/service/content/script_runtime.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ import type { GMInfoEnv, ValueUpdateDataEncoded } from "./types";
77
import type { ScriptEnvTag } from "@Packages/message/consts";
88
import { onInjectPageLoaded } from "./external";
99
import type { CustomEventMessage } from "@Packages/message/custom_event_message";
10+
import { type TExtensionEnv } from "../extension/extension_env";
1011

1112
export class ScriptRuntime {
1213
constructor(
1314
private readonly scripEnvTag: ScriptEnvTag,
1415
private readonly server: Server,
1516
private readonly msg: Message,
16-
private readonly scriptExecutor: ScriptExecutor
17+
private readonly scriptExecutor: ScriptExecutor,
18+
private readonly extensionEnv: TExtensionEnv | undefined
1719
) {}
1820

1921
// content环境的特殊初始化
@@ -66,8 +68,13 @@ export class ScriptRuntime {
6668
this.startScripts(data.scripts, data.envInfo);
6769
});
6870

71+
// 用于 early-start 的扩充参数
72+
const { inIncognitoContext } = this.extensionEnv || {};
73+
const initialEnvInfo = { ...initEnvInfo };
74+
if (typeof inIncognitoContext === "boolean") initialEnvInfo.isIncognito = inIncognitoContext;
75+
6976
// 检查early-start的脚本
70-
this.scriptExecutor.checkEarlyStartScript(this.scripEnvTag, initEnvInfo);
77+
this.scriptExecutor.checkEarlyStartScript(this.scripEnvTag, initialEnvInfo);
7178
}
7279

7380
startScripts(scripts: TScriptInfo[], envInfo: GMInfoEnv) {
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { afterEach, describe, expect, it, vi } from "vitest";
2+
import { extensionEnv, getExtensionUserAgentData } from "./extension_env";
3+
4+
describe("extensionEnv 常量", () => {
5+
it("从 chrome.extension.inIncognitoContext 读取 incognito 状态", () => {
6+
// mock 默认 inIncognitoContext = false
7+
expect(extensionEnv.inIncognitoContext).toBe(false);
8+
// userAgentData 为可选字段,常量初始化时不应填充
9+
expect(extensionEnv.userAgentData).toBeUndefined();
10+
});
11+
});
12+
13+
describe("getExtensionUserAgentData", () => {
14+
const originalUserAgentData = (navigator as any).userAgentData;
15+
const originalGetPlatformInfo = chrome.runtime.getPlatformInfo;
16+
17+
const setNavigatorUserAgentData = (value: any) => {
18+
Object.defineProperty(navigator, "userAgentData", {
19+
configurable: true,
20+
get: () => value,
21+
});
22+
};
23+
24+
afterEach(() => {
25+
setNavigatorUserAgentData(originalUserAgentData);
26+
if (originalGetPlatformInfo === undefined) {
27+
// @ts-ignore
28+
delete chrome.runtime.getPlatformInfo;
29+
} else {
30+
chrome.runtime.getPlatformInfo = originalGetPlatformInfo;
31+
}
32+
vi.restoreAllMocks();
33+
});
34+
35+
it("navigator.userAgentData 缺失时返回 null", async () => {
36+
setNavigatorUserAgentData(undefined);
37+
const result = await getExtensionUserAgentData();
38+
expect(result).toBeNull();
39+
});
40+
41+
it("没有 chrome.runtime.getPlatformInfo 时只返回基础字段", async () => {
42+
setNavigatorUserAgentData({
43+
brands: [{ brand: "Chromium", version: "120" }],
44+
mobile: false,
45+
platform: "macOS",
46+
});
47+
// @ts-ignore
48+
delete chrome.runtime.getPlatformInfo;
49+
50+
const result = await getExtensionUserAgentData();
51+
expect(result).toEqual({
52+
brands: [{ brand: "Chromium", version: "120" }],
53+
mobile: false,
54+
platform: "macOS",
55+
});
56+
expect((result as any).architecture).toBeUndefined();
57+
expect((result as any).bitness).toBeUndefined();
58+
});
59+
60+
it("getPlatformInfo 返回 x86-64 时 bitness 为 64", async () => {
61+
setNavigatorUserAgentData({
62+
brands: [],
63+
mobile: false,
64+
platform: "Linux",
65+
});
66+
chrome.runtime.getPlatformInfo = vi.fn().mockResolvedValue({
67+
os: "linux",
68+
arch: "x86-64",
69+
nacl_arch: "x86-64",
70+
}) as any;
71+
72+
const result = await getExtensionUserAgentData();
73+
expect(result?.architecture).toBe("x86-64");
74+
expect(result?.bitness).toBe("64");
75+
});
76+
77+
it("getPlatformInfo 返回 x86-32 时 bitness 为 32", async () => {
78+
setNavigatorUserAgentData({
79+
brands: [],
80+
mobile: false,
81+
platform: "Windows",
82+
});
83+
chrome.runtime.getPlatformInfo = vi.fn().mockResolvedValue({
84+
os: "win",
85+
arch: "x86-32",
86+
nacl_arch: "x86-32",
87+
}) as any;
88+
89+
const result = await getExtensionUserAgentData();
90+
expect(result?.architecture).toBe("x86-32");
91+
expect(result?.bitness).toBe("32");
92+
});
93+
94+
it("getPlatformInfo 抛异常时降级返回基础字段", async () => {
95+
setNavigatorUserAgentData({
96+
brands: [{ brand: "Chromium", version: "120" }],
97+
mobile: false,
98+
platform: "Android",
99+
});
100+
chrome.runtime.getPlatformInfo = vi.fn().mockRejectedValue(new Error("API not available")) as any;
101+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
102+
103+
const result = await getExtensionUserAgentData();
104+
expect(result).toEqual({
105+
brands: [{ brand: "Chromium", version: "120" }],
106+
mobile: false,
107+
platform: "Android",
108+
});
109+
expect((result as any).architecture).toBeUndefined();
110+
expect((result as any).bitness).toBeUndefined();
111+
expect(warnSpy).toHaveBeenCalled();
112+
});
113+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
export type TExtensionEnv = {
2+
inIncognitoContext: boolean;
3+
userAgentData?: GMUserAgentData | null;
4+
};
5+
6+
type GMUserAgentData = typeof GM_info.userAgentData;
7+
8+
export const extensionEnv: TExtensionEnv = {
9+
inIncognitoContext: chrome.extension.inIncognitoContext,
10+
} satisfies TExtensionEnv;
11+
12+
export const getExtensionUserAgentData = async (): Promise<GMUserAgentData | null> => {
13+
// @ts-ignore
14+
const userAgentData = navigator.userAgentData;
15+
if (userAgentData) {
16+
const resultData: GMUserAgentData = {
17+
brands: userAgentData.brands,
18+
mobile: userAgentData.mobile,
19+
platform: userAgentData.platform,
20+
} satisfies GMUserAgentData;
21+
// 处理architecture和bitness
22+
if (chrome.runtime.getPlatformInfo) {
23+
try {
24+
const platformInfo = await chrome.runtime.getPlatformInfo();
25+
resultData.architecture = platformInfo.nacl_arch;
26+
resultData.bitness = platformInfo.arch.includes("64") ? "64" : "32";
27+
} catch (e) {
28+
// 避免 API 无法执行的问题。不影响整体运作
29+
console.warn(e);
30+
}
31+
}
32+
return resultData;
33+
}
34+
return null;
35+
};

src/app/service/offscreen/client.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ export function preparationSandbox(windowMessage: WindowMessage) {
88
return sendMessage(windowMessage, "offscreen/preparationSandbox");
99
}
1010

11+
export function getExtensionEnv(windowMessage: WindowMessage) {
12+
return sendMessage(windowMessage, "offscreen/getExtensionEnv", { requireUAD: true });
13+
}
14+
1115
// 代理发送消息到ServiceWorker
1216
export function sendMessageToServiceWorker(windowMessage: WindowMessage, action: string, data?: any) {
1317
return sendMessage(windowMessage, "offscreen/sendMessageToServiceWorker", { action, data });

0 commit comments

Comments
 (0)