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
86 changes: 86 additions & 0 deletions src/app/service/content/external.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { ExternalWhitelist } from "@App/app/const";
import { sendMessage } from "@Packages/message/client";
import type { Message } from "@Packages/message/types";

// ================================
// 对外接口:external 注入
// ================================

// 判断当前 hostname 是否命中白名单(含子域名)
const isExternalWhitelisted = (hostname: string) => {
return ExternalWhitelist.some(
(t) => hostname.endsWith(t) && (hostname.length === t.length || hostname.endsWith(`.${t}`))
);
};

// 生成暴露给页面的 Scriptcat 外部接口
const createScriptcatExpose = (msg: Message) => {
const scriptExpose: App.ExternalScriptCat = {
isInstalled(name: string, namespace: string, callback: (res: App.IsInstalledResponse | undefined) => unknown) {
sendMessage<App.IsInstalledResponse>(msg, "scripting/script/isInstalled", { name, namespace }).then(callback);
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isInstalled 通过 sendMessage(...).then(callback) 触发回调,但当消息通道异常/目标 API 不存在时 sendMessage 会抛错并导致 Promise rejection:页面侧回调永远不会被调用,同时可能产生未处理的拒绝。建议在这里补充 .catch(...),保证失败时也以 callback(undefined)(或明确的失败结果)结束,并避免未处理异常外溢到页面。

Suggested change
sendMessage<App.IsInstalledResponse>(msg, "scripting/script/isInstalled", { name, namespace }).then(callback);
sendMessage<App.IsInstalledResponse>(msg, "scripting/script/isInstalled", { name, namespace })
.then(callback)
.catch(() => callback(undefined));

Copilot uses AI. Check for mistakes.
},
};
return scriptExpose;
};

// 尝试写入 external,失败则忽略
const safeSetExternal = <T extends object>(external: any, key: string, value: T) => {
try {
external[key] = value;
return true;
} catch {
// 无法注入到 external,忽略
return false;
}
};

// 当 TM 与 SC 同时存在时的兼容处理:TM 未安装脚本时回退查询 SC
const patchTampermonkeyIsInstalled = (external: any, scriptExpose: App.ExternalScriptCat) => {
const exposedTM = external.Tampermonkey;
const isInstalledTM = exposedTM?.isInstalled;
const isInstalledSC = scriptExpose.isInstalled;

// 满足这些字段时,认为是较完整的 TM 对象
if (isInstalledTM && exposedTM?.getVersion && exposedTM.openOptions) {
try {
exposedTM.isInstalled = (
name: string,
namespace: string,
callback: (res: App.IsInstalledResponse | undefined) => unknown
) => {
isInstalledTM(name, namespace, (res: App.IsInstalledResponse | undefined) => {
if (res?.installed) callback(res);
else isInstalledSC(name, namespace, callback);
});
};
} catch {
// 忽略错误
}
return true;
}

return false;
};

// inject 环境 pageLoad 后执行:按白名单对页面注入 external 接口
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

注释“inject 环境 pageLoad 后执行”与实际调用点不一致:inject.ts 里是在初始化后立即调用 runtime.externalMessage(),并不等待 pageLoad。建议更新这段注释,避免误导后续维护者对执行时机的判断。

Suggested change
// inject 环境 pageLoad 后执行:按白名单对页面注入 external 接口
// inject 环境初始化时由 inject.ts 直接调用:按白名单对页面注入 external 接口(不依赖 pageLoad)

Copilot uses AI. Check for mistakes.
export const onInjectPageLoaded = (msg: Message) => {
const hostname = window.location.hostname;

// 不在白名单则不对外暴露接口
if (!isExternalWhitelisted(hostname)) return;

// 确保 external 存在
const external: External = (window.external || (window.external = {} as External)) as External;

// 创建 Scriptcat 暴露对象
const scriptExpose = createScriptcatExpose(msg);

// 尝试设置 external.Scriptcat
safeSetExternal(external, "Scriptcat", scriptExpose);

// 如果页面已有 Tampermonkey,则做兼容补丁;否则将 Tampermonkey 也指向 Scriptcat 接口
const patched = patchTampermonkeyIsInstalled(external, scriptExpose);
if (!patched) {
safeSetExternal(external, "Tampermonkey", scriptExpose);
}
};
Comment on lines +66 to +86
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onInjectPageLoaded 新增了白名单判断、external 注入、以及 Tampermonkey 兼容补丁等关键行为,但当前没有对应单元测试覆盖(例如:命中/未命中白名单时的注入行为、TM 存在时 isInstalled 回退逻辑、以及写入 external 失败时的容错)。建议补充 vitest 用例,避免后续通讯机制/注入时机调整时再次回归。

Copilot generated this review using guidance from repository custom instructions.
58 changes: 2 additions & 56 deletions src/app/service/content/script_runtime.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { type Server } from "@Packages/message/server";
import type { Message } from "@Packages/message/types";
import { ExternalWhitelist } from "@App/app/const";
import { sendMessage } from "@Packages/message/client";
import { initEnvInfo, type ScriptExecutor } from "./script_executor";
import type { TScriptInfo } from "@App/app/repo/scripts";
import type { EmitEventRequest } from "../service_worker/types";
import type { GMInfoEnv, ValueUpdateDataEncoded } from "./types";
import type { ScriptEnvTag } from "@Packages/message/consts";
import { onInjectPageLoaded } from "./external";

export class ScriptRuntime {
constructor(
Expand Down Expand Up @@ -40,59 +39,6 @@ export class ScriptRuntime {
}

externalMessage() {
// 对外接口白名单
const hostname = window.location.hostname;
if (
ExternalWhitelist.some(
// 如果当前页面的 hostname 是白名单的网域或其子网域
(t) => hostname.endsWith(t) && (hostname.length === t.length || hostname.endsWith(`.${t}`))
)
) {
const msg = this.msg;
// 注入
const external: External = window.external || (window.external = {} as External);
const scriptExpose: App.ExternalScriptCat = {
isInstalled(name: string, namespace: string, callback: (res: App.IsInstalledResponse | undefined) => unknown) {
sendMessage<App.IsInstalledResponse>(msg, "content/script/isInstalled", {
name,
namespace,
}).then(callback);
},
};
try {
external.Scriptcat = scriptExpose;
} catch {
// 无法注入到 external,忽略
}
const exposedTM = external.Tampermonkey;
const isInstalledTM = exposedTM?.isInstalled;
const isInstalledSC = scriptExpose.isInstalled;
if (isInstalledTM && exposedTM?.getVersion && exposedTM.openOptions) {
// 当TM和SC同时启动的特殊处理:如TM没有安装,则查SC的安装状态
try {
exposedTM.isInstalled = (
name: string,
namespace: string,
callback: (res: App.IsInstalledResponse | undefined) => unknown
) => {
isInstalledTM(name, namespace, (res) => {
if (res?.installed) callback(res);
else
isInstalledSC(name, namespace, (res) => {
callback(res);
});
});
};
} catch {
// 忽略错误
}
} else {
try {
external.Tampermonkey = scriptExpose;
} catch {
// 无法注入到 external,忽略
}
}
}
onInjectPageLoaded(this.msg);
}
}
Loading