Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/app/service/content/exec_script.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const envInfo: GMInfoEnv = {
platform: "",
},
isIncognito: false,
userLanguage: "ja-JP",
};

function makeScript(overrides: Partial<ScriptLoadInfo> = {}): ScriptLoadInfo {
Expand Down Expand Up @@ -68,6 +69,16 @@ describe.concurrent("GM_info", () => {
expect(ret.GM_info.script.version).toEqual("1.0.0");
expect(ret._this).not.toEqual(global);
});
it.concurrent("userLanguage 取自环境信息中的脚本猫界面语言", async () => {
const { exec } = makeExec("return GM_info.userLanguage;", ["none"]);
expect(await exec.exec()).toEqual("ja-JP");
});
it.concurrent("早期脚本收到页面环境后更新 userLanguage", async () => {
const { exec } = makeExec("return GM_info;", ["none"]);
const gmInfo = await exec.exec();
exec.updateEarlyScriptGMInfo({ ...envInfo, userLanguage: "de-DE" });
expect(gmInfo.userLanguage).toEqual("de-DE");
});
});

describe.concurrent("unsafeWindow", () => {
Expand Down
1 change: 1 addition & 0 deletions src/app/service/content/exec_script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export default class ExecScript {
GM_info.isIncognito = envInfo.isIncognito;
GM_info.sandboxMode = envInfo.sandboxMode;
GM_info.userAgentData = envInfo.userAgentData;
GM_info.userLanguage = envInfo.userLanguage;
}

stop() {
Expand Down
13 changes: 9 additions & 4 deletions src/app/service/content/exec_warp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const getGMInfo = (exec: BgExecScriptWarp) => (exec as any).named?.GM_info;

describe("BgExecScriptWarp envInfo 注入", () => {
it("extensionEnv 为 undefined 时使用默认值", () => {
const exec = new BgExecScriptWarp(buildScriptRes(), fakeMessage, undefined);
const exec = new BgExecScriptWarp(buildScriptRes(), fakeMessage, undefined, "");
const gmInfo = getGMInfo(exec);
expect(gmInfo.isIncognito).toBe(false);
expect(gmInfo.userAgentData).toEqual({
Expand All @@ -34,9 +34,14 @@ describe("BgExecScriptWarp envInfo 注入", () => {
expect(gmInfo.sandboxMode).toBe("raw");
});

it("userLanguage 使用沙盒当前的脚本猫界面语言", () => {
const exec = new BgExecScriptWarp(buildScriptRes(), fakeMessage, undefined, "zh-TW");
expect(getGMInfo(exec).userLanguage).toBe("zh-TW");
});

it("inIncognitoContext=true 时覆盖 isIncognito 为 true", () => {
const extensionEnv: TExtensionEnv = { inIncognitoContext: true };
const exec = new BgExecScriptWarp(buildScriptRes(), fakeMessage, extensionEnv);
const exec = new BgExecScriptWarp(buildScriptRes(), fakeMessage, extensionEnv, "");
const gmInfo = getGMInfo(exec);
expect(gmInfo.isIncognito).toBe(true);
// 没传 userAgentData,沿用默认空值
Expand All @@ -58,7 +63,7 @@ describe("BgExecScriptWarp envInfo 注入", () => {
bitness: "64",
} as any,
};
const exec = new BgExecScriptWarp(buildScriptRes(), fakeMessage, extensionEnv);
const exec = new BgExecScriptWarp(buildScriptRes(), fakeMessage, extensionEnv, "");
const gmInfo = getGMInfo(exec);
expect(gmInfo.isIncognito).toBe(false);
expect(gmInfo.userAgentData).toEqual({
Expand All @@ -75,7 +80,7 @@ describe("BgExecScriptWarp envInfo 注入", () => {
inIncognitoContext: true,
userAgentData: null,
};
const exec = new BgExecScriptWarp(buildScriptRes(), fakeMessage, extensionEnv);
const exec = new BgExecScriptWarp(buildScriptRes(), fakeMessage, extensionEnv, "");
const gmInfo = getGMInfo(exec);
expect(gmInfo.isIncognito).toBe(true);
expect(gmInfo.userAgentData).toEqual({
Expand Down
8 changes: 7 additions & 1 deletion src/app/service/content/exec_warp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ export class BgExecScriptWarp extends ExecScript {

setInterval: Map<number, boolean>;

constructor(scriptRes: ScriptLoadInfo, message: Message, extensionEnv: TExtensionEnv | undefined) {
constructor(
scriptRes: ScriptLoadInfo,
message: Message,
extensionEnv: TExtensionEnv | undefined,
userLanguage: string
) {
const thisContext: { [key: string]: any } = {};
const setTimeout = new Map<number, any>();
const setInterval = new Map<number, any>();
Expand Down Expand Up @@ -73,6 +78,7 @@ export class BgExecScriptWarp extends ExecScript {
platform: "",
},
isIncognito: false,
userLanguage,
};
const { inIncognitoContext, userAgentData } = extensionEnv || {};
if (typeof inIncognitoContext === "boolean") envInfo.isIncognito = inIncognitoContext;
Expand Down
1 change: 1 addition & 0 deletions src/app/service/content/gm_api/gm_api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const envInfo: GMInfoEnv = {
platform: "",
},
isIncognito: false,
userLanguage: "en-US",
};

const makeResource = (url: string, content: string, type: "require" | "require-css" | "resource") => ({
Expand Down
1 change: 1 addition & 0 deletions src/app/service/content/gm_api/gm_info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export function evaluateGMInfo(envInfo: GMInfoEnv, script: TScriptInfo) {
scriptWillUpdate: !!script.checkUpdate,
scriptHandler: "ScriptCat",
userAgentData: envInfo.userAgentData,
userLanguage: envInfo.userLanguage,
// "" => null
scriptUpdateURL: script.downloadUrl || null,
scriptMetaStr: script.metadataStr,
Expand Down
2 changes: 2 additions & 0 deletions src/app/service/content/script_executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export const initEnvInfo: GMInfoEnv = {
/** isIncognito - inject/content 环境下透過 scripting 环境判断 */
/** 使用者可透过 「 await navigator.storage.persisted() 」来判断,但ScriptCat不会主动执行此代码来判断 */
isIncognito: false,
/** userLanguage - 页面环境未到达前未知,由 updateEarlyScriptGMInfo 补上 */
userLanguage: "",
} satisfies GMInfoEnv;

// 脚本执行器
Expand Down
1 change: 1 addition & 0 deletions src/app/service/content/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,5 @@ export interface GMInfoEnv {
userAgentData: typeof GM_info.userAgentData;
sandboxMode: typeof GM_info.sandboxMode; // 目前固定为 "raw",预留
isIncognito: typeof GM_info.isIncognito;
userLanguage: typeof GM_info.userLanguage;
}
14 changes: 13 additions & 1 deletion src/app/service/sandbox/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ vi.mock("../offscreen/client", () => ({
}));

import { Runtime } from "./runtime";
import { initLanguage } from "@App/locales/locales";

initTestEnv();
// 生产中 sandbox init() 会先 initLanguage(),setSandboxLanguage 依赖已初始化的 i18n
initLanguage();

const buildScript = (runIn?: string, type: SCRIPT_TYPE = SCRIPT_TYPE_BACKGROUND): ScriptLoadInfo =>
({
Expand Down Expand Up @@ -115,6 +118,13 @@ describe("Runtime.execScript run-in 过滤", () => {
expect(BgExecScriptWarpCtor).toHaveBeenCalledTimes(1);
});

it("执行时向 BgExecScriptWarp 传入最近一次同步的界面语言", async () => {
const runtime = setup({ inIncognitoContext: false });
runtime.setSandboxLanguage("ko-KR");
await runtime.execScript(buildScript());
expect(BgExecScriptWarpCtor.mock.calls[0][3]).toBe("ko-KR");
});

it("extensionEnv 为 undefined 时,过滤被跳过照常执行(fail-open)", async () => {
// 当前实现:拿不到 incognito 状态时不做过滤,避免静默丢失任务
const runtime = setup(undefined);
Expand All @@ -135,9 +145,10 @@ describe("Runtime.executeSkillScript 执行环境", () => {
vi.useRealTimers();
});

it("执行 skill 脚本时向 BgExecScriptWarp 传入扩展环境", async () => {
it("执行 skill 脚本时向 BgExecScriptWarp 传入扩展环境与界面语言", async () => {
const extensionEnv: TExtensionEnv = { inIncognitoContext: true };
const runtime = setup(extensionEnv);
runtime.setSandboxLanguage("pt-BR");

await runtime.executeSkillScript({
uuid: "skill-uuid",
Expand All @@ -149,5 +160,6 @@ describe("Runtime.executeSkillScript 执行环境", () => {

expect(BgExecScriptWarpCtor).toHaveBeenCalledTimes(1);
expect(BgExecScriptWarpCtor.mock.calls[0][2]).toBe(extensionEnv);
expect(BgExecScriptWarpCtor.mock.calls[0][3]).toBe("pt-BR");
});
});
8 changes: 6 additions & 2 deletions src/app/service/sandbox/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ export class Runtime {
retryTime: number;
}[] = [];

// 供 GM_info.userLanguage 使用,由 SW 经 setSandboxLanguage 同步;收到之前未知,为空串
userLanguage = "";

constructor(
private windowMessage: WindowMessage,
private api: Server,
Expand Down Expand Up @@ -190,7 +193,7 @@ export class Runtime {
}
}

const exec = new BgExecScriptWarp(script, this.windowMessage, extensionEnv);
const exec = new BgExecScriptWarp(script, this.windowMessage, extensionEnv, this.userLanguage);
this.execScriptMap.set(script.uuid, exec);
proxyUpdateRunStatus(this.windowMessage, { uuid: script.uuid, runStatus: SCRIPT_RUN_STATUS_RUNNING });
// 修改掉脚本掉最后运行时间, 数据库也需要修改
Expand Down Expand Up @@ -372,6 +375,7 @@ export class Runtime {
}

setSandboxLanguage(lang: string) {
this.userLanguage = lang;
changeLanguage(lang);
}

Expand Down Expand Up @@ -434,7 +438,7 @@ export class Runtime {

// 使用 BgExecScriptWarp 执行,它会自动构建 setTimeout/setInterval 等
const extensionEnv = await this.extensionEnvAsync;
const exec = new BgExecScriptWarp(scriptLoadInfo, this.windowMessage, extensionEnv);
const exec = new BgExecScriptWarp(scriptLoadInfo, this.windowMessage, extensionEnv, this.userLanguage);
// 通过 sandboxContext 注入 args(BgExecScriptWarp 通过 globalInjection 注入了 setTimeout 等,
// sandboxContext 已经包含了这些,再追加 args 即可)
if ((exec as any).sandboxContext) {
Expand Down
30 changes: 30 additions & 0 deletions src/app/service/service_worker/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe.concurrent("RuntimeService - getPageScriptMatchingResultByUrl 脚本匹
let runtime: RuntimeService;
let mockSystemConfig: {
getBlacklist: MockedFunction<() => string>;
getLanguage: MockedFunction<() => Promise<string>>;
};
let mockScriptService: {
buildScriptRunResource: MockedFunction<(script: Script, scriptFlag?: string) => ScriptRunResource>;
Expand Down Expand Up @@ -73,6 +74,7 @@ describe.concurrent("RuntimeService - getPageScriptMatchingResultByUrl 脚本匹
const createRuntimeTestContext = () => {
const localMockSystemConfig = {
getBlacklist: vi.fn().mockReturnValue(""),
getLanguage: vi.fn().mockResolvedValue("en-US"),
};

const localMockScriptService = {
Expand Down Expand Up @@ -1101,6 +1103,17 @@ describe("getScriptsForTab 附加边界场景", () => {
expect(result?.envInfo.isIncognito).toBe(true);
});

it("返回的 GM_info 环境携带脚本猫当前界面语言", async () => {
const { runtime, script, scriptRes, mockScriptDAO, mockSystemConfig } = createFullContext();
mockSystemConfig.getLanguage.mockResolvedValue("ja-JP");
await runtime.applyScriptMatchInfo(scriptRes);
(mockScriptDAO as any).gets = vi.fn().mockResolvedValue([script]);

const result = await runtime.getScriptsForTab({ url: pageUrl, tabId: 12, frameId: 0 });

expect(result?.envInfo.userLanguage).toBe("ja-JP");
});

it("compiledResource 不存在时应调用 buildAndSaveCompiledResourceFromScript 重新构建", async () => {
const { runtime, scriptRes, compiledResource, mockCompiledResourceDAO } = createFullContext();
await runtime.applyScriptMatchInfo(scriptRes);
Expand Down Expand Up @@ -1255,6 +1268,23 @@ describe("sandbox verified 初始化重放", () => {
expect(mockSystemConfig.addListener).toHaveBeenCalledTimes(1);
expect(mockSystemConfig.addListener).toHaveBeenCalledWith("language", expect.any(Function));
});

it("先下发界面语言再启用后台脚本,保证启动即运行的脚本读到的 GM_info.userLanguage 已就绪", async () => {
const { runtime, mockScriptDAO } = _createRuntimeContext();
mockScriptDAO.all.mockResolvedValue([
_createMockScript({ uuid: "background-script", type: SCRIPT_TYPE_BACKGROUND, status: SCRIPT_STATUS_ENABLE }),
]);
const publish = (runtime.mq as unknown as { publish: ReturnType<typeof vi.fn> }).publish;
publish.mockClear();

await (
runtime as unknown as { handlePreparationOffscreen(data: { verified: boolean }): Promise<void> }
).handlePreparationOffscreen({ verified: true });

const topics = publish.mock.calls.map(([topic]) => topic);
expect(topics.indexOf("setSandboxLanguage")).toBeGreaterThanOrEqual(0);
expect(topics.indexOf("setSandboxLanguage")).toBeLessThan(topics.indexOf("enableScripts"));
});
});

// ─────────────────────────────────────────────────────────────────────────────
Expand Down
4 changes: 3 additions & 1 deletion src/app/service/service_worker/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1687,6 +1687,7 @@ export class RuntimeService {
sandboxMode: "raw",
isIncognito: incognito,
userAgentData: this.userAgentData ?? undefined,
userLanguage: await this.systemConfig.getLanguage(),
} as GMInfoEnv,
scriptmenus,
} satisfies TScriptsForTab;
Expand All @@ -1706,10 +1707,11 @@ export class RuntimeService {
enable: script.status === SCRIPT_STATUS_ENABLE,
});
}
// 语言须先于 enableScripts 到达沙盒:后台脚本启动时即读取 GM_info.userLanguage
this.mq.publish("setSandboxLanguage", language);
if (scripts.length > 0) {
this.mq.publish<TEnableScript[]>("enableScripts", scripts);
}
this.mq.publish("setSandboxLanguage", language);
this.systemConfig.addListener("language", (lng) => {
this.mq.publish("setSandboxLanguage", lng);
});
Expand Down
1 change: 1 addition & 0 deletions src/template/scriptcat.d.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ declare const GM_info: {
userConfig?: UserConfig;
userConfigStr?: string;
isIncognito: boolean;
userLanguage: string; // ScriptCat 界面语言,如 "zh-CN";尚未获知时为 ""
sandboxMode: "raw"; // "js" | "raw" | "none";
userAgentData: {
brands?: {
Expand Down
5 changes: 5 additions & 0 deletions src/types/scriptcat.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ declare const GM_info: {
userConfigStr?: string;
/** Whether running in an incognito/private window. */
isIncognito: boolean;
/**
* ScriptCat's current interface language, e.g. `"en-US"`, `"zh-CN"`, `"ja-JP"`.
* Captured when the script starts; empty string while still unknown (e.g. read synchronously by an early-start script).
*/
userLanguage: string;
/** Sandbox mode (ScriptCat always uses `"raw"`). */
sandboxMode: "raw";
userAgentData: {
Expand Down
5 changes: 5 additions & 0 deletions src/types/scriptcat.zh-CN.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ declare const GM_info: {
userConfigStr?: string;
/** 是否在隐私/无痕窗口中运行。 */
isIncognito: boolean;
/**
* ScriptCat 当前界面语言,如 `"en-US"`、`"zh-CN"`、`"ja-JP"`。
* 在脚本启动时取值;尚未获知时为空字符串(例如早期脚本同步读取时)。
*/
userLanguage: string;
/** 沙箱模式(ScriptCat 始终使用 `"raw"`)。 */
sandboxMode: "raw";
userAgentData: {
Expand Down
Loading