2025-04-16 10:31:16 +08:00

472 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { MessageQueue } from "@Packages/message/message_queue";
import { ExtMessageSender, GetSender, Group, MessageSend } from "@Packages/message/server";
import {
Script,
SCRIPT_STATUS,
SCRIPT_STATUS_DISABLE,
SCRIPT_STATUS_ENABLE,
SCRIPT_TYPE_NORMAL,
ScriptDAO,
ScriptRunResouce,
} from "@App/app/repo/scripts";
import { ValueService } from "./value";
import GMApi from "./gm_api";
import { subscribeScriptDelete, subscribeScriptEnable, subscribeScriptInstall } from "../queue";
import { ScriptService } from "./script";
import { runScript, stopScript } from "../offscreen/client";
import { getRunAt } from "./utils";
import { randomString } from "@App/pkg/utils/utils";
import Cache from "@App/app/cache";
import { dealPatternMatches, UrlMatch } from "@App/pkg/utils/match";
import { ExtensionContentMessageSend } from "@Packages/message/extension_message";
import { sendMessage } from "@Packages/message/client";
import { compileInjectScript } from "../content/utils";
import { PopupService } from "./popup";
import Logger from "@App/app/logger/logger";
import LoggerCore from "@App/app/logger/core";
import PermissionVerify from "./permission_verify";
// 为了优化性能存储到缓存时删除了code与value
export interface ScriptMatchInfo extends ScriptRunResouce {
matches: string[];
excludeMatches: string[];
customizeExcludeMatches: string[];
}
export interface EmitEventRequest {
uuid: string;
event: string;
eventId: string;
data?: any;
}
export class RuntimeService {
scriptDAO: ScriptDAO = new ScriptDAO();
scriptMatch: UrlMatch<string> = new UrlMatch<string>();
scriptCustomizeMatch: UrlMatch<string> = new UrlMatch<string>();
scriptMatchCache: Map<string, ScriptMatchInfo> | null | undefined;
constructor(
private group: Group,
private sender: MessageSend,
private mq: MessageQueue,
private value: ValueService,
private script: ScriptService
) {}
async init() {
// 启动gm api
const permission = new PermissionVerify(this.group.group("permission"));
const gmApi = new GMApi(permission, this.group, this.sender, this.mq, this.value, this);
permission.init();
gmApi.start();
this.group.on("stopScript", this.stopScript.bind(this));
this.group.on("runScript", this.runScript.bind(this));
this.group.on("pageLoad", this.pageLoad.bind(this));
// 读取inject.js注入页面
this.registerInjectScript();
// 监听脚本开启
subscribeScriptEnable(this.mq, async (data) => {
const script = await this.scriptDAO.getAndCode(data.uuid);
if (!script) {
return;
}
// 如果是普通脚本, 在service worker中进行注册
// 如果是后台脚本, 在offscreen中进行处理
if (script.type === SCRIPT_TYPE_NORMAL) {
// 加载页面脚本
await this.loadPageScript(script);
if (!data.enable) {
await this.unregistryPageScript(script.uuid);
}
}
});
// 监听脚本安装
subscribeScriptInstall(this.mq, async (data) => {
const script = await this.scriptDAO.get(data.script.uuid);
if (!script) {
return;
}
if (script.type === SCRIPT_TYPE_NORMAL) {
await this.loadPageScript(script);
}
});
// 监听脚本删除
subscribeScriptDelete(this.mq, async ({ uuid }) => {
await this.unregistryPageScript(uuid);
this.deleteScriptMatch(uuid);
});
// 将开启的脚本发送一次enable消息
const scriptDao = new ScriptDAO();
const list = await scriptDao.all();
list.forEach((script) => {
if (script.type !== SCRIPT_TYPE_NORMAL) {
return;
}
this.mq.publish("enableScript", { uuid: script.uuid, enable: script.status === SCRIPT_STATUS_ENABLE });
});
// 监听offscreen环境初始化, 初始化完成后, 再将后台脚本运行起来
this.mq.subscribe("preparationOffscreen", () => {
list.forEach((script) => {
if (script.type === SCRIPT_TYPE_NORMAL) {
return;
}
this.mq.publish("enableScript", { uuid: script.uuid, enable: script.status === SCRIPT_STATUS_ENABLE });
});
});
this.loadScriptMatchInfo();
}
messageFlag() {
return Cache.getInstance().getOrSet("scriptInjectMessageFlag", () => {
return Promise.resolve(randomString(16));
});
}
// 给指定tab发送消息
sendMessageToTab(to: ExtMessageSender, action: string, data: any) {
if (to.tabId === -1) {
// 如果是-1, 代表给offscreen发送消息
return sendMessage(this.sender, "offscreen/runtime/" + action, data);
}
return sendMessage(
new ExtensionContentMessageSend(to.tabId, {
documentId: to.documentId,
frameId: to.frameId,
}),
"content/runtime/" + action,
data
);
}
// 给指定脚本触发事件
emitEventToTab(to: ExtMessageSender, req: EmitEventRequest) {
if (to.tabId === -1) {
// 如果是-1, 代表给offscreen发送消息
return sendMessage(this.sender, "offscreen/runtime/emitEvent", req);
}
return sendMessage(
new ExtensionContentMessageSend(to.tabId, {
documentId: to.documentId,
frameId: to.frameId,
}),
"content/runtime/emitEvent",
req
);
}
async getPageScriptUuidByUrl(url: string, includeCustomize?: boolean) {
const match = await this.loadScriptMatchInfo();
// 匹配当前页面的脚本
const matchScriptUuid = match.match(url!);
// 包含自定义排除的脚本
if (includeCustomize) {
const excludeScriptUuid = this.scriptCustomizeMatch.match(url!);
const match = new Set<string>();
excludeScriptUuid.forEach((uuid) => {
match.add(uuid);
});
matchScriptUuid.forEach((uuid) => {
match.add(uuid);
});
// 转化为数组
return Array.from(match);
}
return matchScriptUuid;
}
async getPageScriptByUrl(url: string, includeCustomize?: boolean) {
const matchScriptUuid = await this.getPageScriptUuidByUrl(url, includeCustomize);
return matchScriptUuid.map((uuid) => {
return Object.assign({}, this.scriptMatchCache?.get(uuid));
});
}
async pageLoad(_: any, sender: GetSender) {
const [scriptFlag] = await Promise.all([this.messageFlag(), this.loadScriptMatchInfo()]);
const chromeSender = sender.getSender() as chrome.runtime.MessageSender;
// 匹配当前页面的脚本
const matchScriptUuid = await this.getPageScriptUuidByUrl(chromeSender.url!);
const scripts = await Promise.all(
matchScriptUuid.map(async (uuid): Promise<undefined | ScriptRunResouce> => {
const scriptRes = Object.assign({}, this.scriptMatchCache?.get(uuid));
// 判断脚本是否开启
if (scriptRes.status === SCRIPT_STATUS_DISABLE) {
return undefined;
}
// 如果是iframe,判断是否允许在iframe里运行
if (chromeSender.frameId !== undefined) {
if (scriptRes.metadata.noframes) {
return undefined;
}
}
// 获取value
return scriptRes;
})
);
const enableScript = scripts.filter((item) => item);
// 加载value
await Promise.all(
enableScript.map(async (script) => {
const value = await this.value.getScriptValue(script!);
script!.value = value;
})
);
this.mq.emit("pageLoad", {
tabId: chromeSender.tab?.id,
frameId: chromeSender.frameId,
scripts: enableScript,
});
return Promise.resolve({ flag: scriptFlag, scripts: enableScript });
}
// 停止脚本
stopScript(uuid: string) {
return stopScript(this.sender, uuid);
}
// 运行脚本
async runScript(uuid: string) {
const script = await this.scriptDAO.get(uuid);
if (!script) {
return;
}
const res = await this.script.buildScriptRunResource(script);
return runScript(this.sender, res);
}
// 注册inject.js
registerInjectScript() {
chrome.userScripts.getScripts({ ids: ["scriptcat-inject"] }).then((res) => {
if (res.length == 0) {
chrome.userScripts.configureWorld({
csp: "script-src 'self' 'unsafe-inline' 'unsafe-eval' *",
messaging: true,
});
fetch("inject.js")
.then((res) => res.text())
.then(async (injectJs) => {
// 替换ScriptFlag
const code = `(function (MessageFlag) {\n${injectJs}\n})('${await this.messageFlag()}')`;
chrome.userScripts.register([
{
id: "scriptcat-inject",
js: [{ code }],
matches: ["<all_urls>"],
allFrames: true,
world: "MAIN",
runAt: "document_start",
},
// 注册content
{
id: "scriptcat-content",
js: [{ file: "src/content.js" }],
matches: ["<all_urls>"],
allFrames: true,
runAt: "document_start",
world: "USER_SCRIPT",
},
]);
});
}
});
}
loadingScript: Promise<void> | null | undefined;
// 加载脚本匹配信息由于service_worker的机制如果由不活动状态恢复过来时会优先触发事件
// 可能当时会没有脚本匹配信息,所以使用脚本信息时,尽量使用此方法获取
async loadScriptMatchInfo() {
if (this.scriptMatchCache) {
return this.scriptMatch;
}
if (this.loadingScript) {
await this.loadingScript;
} else {
// 如果没有缓存, 则创建一个新的缓存
const cache = new Map<string, ScriptMatchInfo>();
this.loadingScript = Cache.getInstance()
.get("scriptMatch")
.then((data: { [key: string]: ScriptMatchInfo }) => {
if (data) {
Object.keys(data).forEach((key) => {
const item = data[key];
cache.set(item.uuid, item);
this.syncAddScriptMatch(item);
});
}
});
await this.loadingScript;
this.loadingScript = null;
this.scriptMatchCache = cache;
}
return this.scriptMatch;
}
// 保存脚本匹配信息
async saveScriptMatchInfo() {
if (!this.scriptMatchCache) {
return;
}
const scriptMatch = {} as { [key: string]: ScriptMatchInfo };
this.scriptMatchCache.forEach((val, key) => {
scriptMatch[key] = val;
// 优化性能,将不需要的信息去掉
scriptMatch[key].code = "";
scriptMatch[key].value = {};
});
return await Cache.getInstance().set("scriptMatch", scriptMatch);
}
async addScriptMatch(item: ScriptMatchInfo) {
if (!this.scriptMatchCache) {
await this.loadScriptMatchInfo();
}
this.scriptMatchCache!.set(item.uuid, item);
this.syncAddScriptMatch(item);
this.saveScriptMatchInfo();
}
syncAddScriptMatch(item: ScriptMatchInfo) {
// 清理一下老数据
this.scriptMatch.del(item.uuid);
this.scriptCustomizeMatch.del(item.uuid);
// 添加新的数据
item.matches.forEach((match) => {
this.scriptMatch.add(match, item.uuid);
});
item.excludeMatches.forEach((match) => {
this.scriptMatch.exclude(match, item.uuid);
});
item.customizeExcludeMatches.forEach((match) => {
this.scriptCustomizeMatch.add(match, item.uuid);
});
}
async updateScriptStatus(uuid: string, status: SCRIPT_STATUS) {
if (!this.scriptMatchCache) {
await this.loadScriptMatchInfo();
}
this.scriptMatchCache!.get(uuid)!.status = status;
this.saveScriptMatchInfo();
}
async deleteScriptMatch(uuid: string) {
if (!this.scriptMatchCache) {
await this.loadScriptMatchInfo();
}
this.scriptMatchCache!.delete(uuid);
this.scriptMatch.del(uuid);
this.scriptCustomizeMatch.del(uuid);
this.saveScriptMatchInfo();
}
// 加载页面脚本, 会把脚本信息放入缓存中
// 如果脚本开启, 则注册脚本
async loadPageScript(script: Script) {
const matches = script.metadata["match"];
if (!matches) {
return;
}
const scriptRes = await this.script.buildScriptRunResource(script);
scriptRes.code = compileInjectScript(scriptRes);
matches.push(...(script.metadata["include"] || []));
const patternMatches = dealPatternMatches(matches);
const scriptMatchInfo: ScriptMatchInfo = Object.assign(
{ matches: patternMatches.result, excludeMatches: [], customizeExcludeMatches: [] },
scriptRes
);
const registerScript: chrome.userScripts.RegisteredUserScript = {
id: scriptRes.uuid,
js: [{ code: scriptRes.code }],
matches: patternMatches.patternResult,
world: "MAIN",
};
// 排除由loadPage时决定, 不使用userScript的excludeMatches处理
if (script.metadata["exclude"]) {
const excludeMatches = script.metadata["exclude"];
const result = dealPatternMatches(excludeMatches, {
exclude: true,
});
// registerScript.excludeMatches = result.patternResult;
scriptMatchInfo.excludeMatches = result.result;
}
// 自定义排除
if (script.selfMetadata && script.selfMetadata.exclude) {
const excludeMatches = script.selfMetadata.exclude;
const result = dealPatternMatches(excludeMatches, {
exclude: true,
});
if (!registerScript.excludeMatches) {
registerScript.excludeMatches = [];
}
// registerScript.excludeMatches.push(...result.patternResult);
scriptMatchInfo.customizeExcludeMatches = result.result;
}
// 将脚本match信息放入缓存中
this.addScriptMatch(scriptMatchInfo);
// 如果脚本开启, 则注册脚本
if (script.status === SCRIPT_STATUS_ENABLE) {
if (!script.metadata["noframes"]) {
registerScript.allFrames = true;
}
if (script.metadata["run-at"]) {
registerScript.runAt = getRunAt(script.metadata["run-at"]);
}
if (await Cache.getInstance().get("registryScript:" + script.uuid)) {
await chrome.userScripts.update([registerScript]);
} else {
await chrome.userScripts.register([registerScript], () => {
if (chrome.runtime.lastError) {
LoggerCore.logger().error("registerScript error", {
error: chrome.runtime.lastError,
name: script.name,
registerMatch: {
matches: registerScript.matches,
excludeMatches: registerScript.excludeMatches,
},
});
}
});
}
await Cache.getInstance().set("registryScript:" + script.uuid, true);
}
}
async unregistryPageScript(uuid: string) {
if (!(await Cache.getInstance().get("registryScript:" + uuid))) {
return;
}
chrome.userScripts.unregister(
{
ids: [uuid],
},
() => {
// 删除缓存
Cache.getInstance().del("registryScript:" + uuid);
// 修改脚本状态为disable
this.updateScriptStatus(uuid, SCRIPT_STATUS_DISABLE);
}
);
}
}