脚本匹配与注入
Some checks failed
build / Build (push) Failing after 5s
test / Run tests (push) Failing after 8s

This commit is contained in:
王一之 2025-04-05 00:53:59 +08:00
parent 9ce1826a34
commit 651384f12c
6 changed files with 160 additions and 29 deletions

View File

@ -8,13 +8,22 @@ import { ScriptService } from "./script";
import { runScript, stopScript } from "../offscreen/client"; import { runScript, stopScript } from "../offscreen/client";
import { getRunAt } from "./utils"; import { getRunAt } from "./utils";
import { randomString } from "@App/pkg/utils/utils"; import { randomString } from "@App/pkg/utils/utils";
import { compileInjectScript, compileInjectScriptInfo, compileScriptCode } from "@App/runtime/content/utils"; import { compileInjectScript, compileScriptCode } from "@App/runtime/content/utils";
import Cache from "@App/app/cache"; import Cache from "@App/app/cache";
import { dealPatternMatches } from "@App/pkg/utils/match"; import { dealPatternMatches, UrlMatch } from "@App/pkg/utils/match";
// 为了优化性能存储到缓存时删除了code与value
export interface ScriptMatchInfo extends ScriptRunResouce {
matches: string[];
excludeMatches: string[];
}
export class RuntimeService { export class RuntimeService {
scriptDAO: ScriptDAO = new ScriptDAO(); scriptDAO: ScriptDAO = new ScriptDAO();
scriptMatch: UrlMatch<string> = new UrlMatch<string>();
scriptMatchCache: Map<string, ScriptMatchInfo> | null | undefined;
constructor( constructor(
private group: Group, private group: Group,
private sender: MessageSend, private sender: MessageSend,
@ -99,12 +108,23 @@ export class RuntimeService {
} }
async pageLoad(_, sender: GetSender) { async pageLoad(_, sender: GetSender) {
const scriptFlag = await this.messageFlag(); const [scriptFlag, match] = await Promise.all([this.messageFlag(), this.loadScriptMatchInfo()]);
const chromeSender = sender.getSender() as chrome.runtime.MessageSender; const chromeSender = sender.getSender() as chrome.runtime.MessageSender;
// 匹配当前页面的脚本
console.log("pageLoad");
return Promise.resolve({ flag: scriptFlag }); // 匹配当前页面的脚本
const matchScriptUuid = match.match(chromeSender.url!);
console.log("pageLoad", match.match(chromeSender.url!));
const scripts = await Promise.all(
matchScriptUuid.map(
(uuid) =>
new Promise((resolve) => {
// 获取value
const scriptRes = Object.assign({}, this.scriptMatchCache?.get(uuid));
resolve(scriptRes);
})
)
);
return Promise.resolve({ flag: scriptFlag, scripts: scripts });
} }
// 停止脚本 // 停止脚本
@ -155,6 +175,70 @@ export class RuntimeService {
}); });
} }
loadScripting: Promise<void> | null | undefined;
// 加载脚本匹配信息由于service_worker的机制如果由不活动状态恢复过来时会优先触发事件
// 可能当时会没有脚本匹配信息,所以使用脚本信息时,尽量使用此方法获取
async loadScriptMatchInfo() {
if (this.scriptMatchCache) {
return this.scriptMatch;
}
if (this.loadScripting) {
await this.loadScripting;
} else {
// 如果没有缓存, 则创建一个新的缓存
this.loadScripting = Cache.getInstance()
.get("scriptMatch")
.then((data: { [key: string]: ScriptMatchInfo }) => {
this.scriptMatchCache = new Map<string, ScriptMatchInfo>();
if (data) {
Object.keys(data).forEach((key) => {
const item = data[key];
this.scriptMatchCache!.set(item.uuid, item);
item.matches.forEach((match) => {
this.scriptMatch.add(match, item.uuid);
});
item.excludeMatches.forEach((match) => {
this.scriptMatch.exclude(match, item.uuid);
});
});
}
});
await this.loadScripting;
this.loadScripting = null;
}
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);
item.matches.forEach((match) => {
this.scriptMatch.add(match, item.uuid);
});
item.excludeMatches.forEach((match) => {
this.scriptMatch.exclude(match, item.uuid);
});
this.saveScriptMatchInfo();
}
async registryPageScript(script: Script) { async registryPageScript(script: Script) {
if (await Cache.getInstance().has("registryScript:" + script.uuid)) { if (await Cache.getInstance().has("registryScript:" + script.uuid)) {
return; return;
@ -170,6 +254,11 @@ export class RuntimeService {
matches.push(...(script.metadata["include"] || [])); matches.push(...(script.metadata["include"] || []));
const patternMatches = dealPatternMatches(matches); const patternMatches = dealPatternMatches(matches);
const scriptMatchInfo: ScriptMatchInfo = Object.assign(
{ matches: patternMatches.result, excludeMatches: [] },
scriptRes
);
const registerScript: chrome.userScripts.RegisteredUserScript = { const registerScript: chrome.userScripts.RegisteredUserScript = {
id: scriptRes.uuid, id: scriptRes.uuid,
js: [{ code: scriptRes.code }], js: [{ code: scriptRes.code }],
@ -186,13 +275,16 @@ export class RuntimeService {
const result = dealPatternMatches(excludeMatches); const result = dealPatternMatches(excludeMatches);
registerScript.excludeMatches = result.patternResult; registerScript.excludeMatches = result.patternResult;
scriptMatchInfo.excludeMatches = result.result;
} }
if (script.metadata["run-at"]) { if (script.metadata["run-at"]) {
registerScript.runAt = getRunAt(script.metadata["run-at"]); registerScript.runAt = getRunAt(script.metadata["run-at"]);
} }
chrome.userScripts.register([registerScript], () => { chrome.userScripts.register([registerScript], async () => {
// 标记为已注册 // 标记为已注册
Cache.getInstance().set("registryScript:" + script.uuid, true); Cache.getInstance().set("registryScript:" + script.uuid, true);
// 将脚本match信息放入缓存中
this.addScriptMatch(scriptMatchInfo);
}); });
} }

View File

@ -15,9 +15,9 @@ const logger = new LoggerCore({
const server = new Server("inject", msg); const server = new Server("inject", msg);
server.on("pageLoad", (data: ScriptRunResouce[]) => { server.on("pageLoad", (data: { scripts: ScriptRunResouce[] }) => {
logger.logger().debug("inject start"); logger.logger().debug("inject start");
console.log("inject", data); console.log("inject", data);
const runtime = new InjectRuntime(msg, data); const runtime = new InjectRuntime(msg, data.scripts);
runtime.start(); runtime.start();
}); });

View File

@ -12,6 +12,14 @@ export default class Match<T> {
protected rule = new Map<string, T[]>(); protected rule = new Map<string, T[]>();
protected kv = new Map<string, T>();
forEach(fn: (val: T, key: string) => void) {
this.kv.forEach((val, key) => {
fn(val, key);
});
}
protected parseURL(url: string): Url | undefined { protected parseURL(url: string): Url | undefined {
if (url.indexOf("*http") === 0) { if (url.indexOf("*http") === 0) {
url = url.substring(1); url = url.substring(1);
@ -112,6 +120,7 @@ export default class Match<T> {
this.rule.set(re, rule); this.rule.set(re, rule);
} }
rule.push(val); rule.push(val);
this.kv.set(Match.getId(val), val);
this.delCache(); this.delCache();
} }
@ -129,7 +138,6 @@ export default class Match<T> {
} }
}); });
} catch (e) { } catch (e) {
// eslint-disable-next-line no-console
console.warn("bad match rule", Logger.E(e)); console.warn("bad match rule", Logger.E(e));
// LoggerCore.getLogger({ component: "match" }).warn( // LoggerCore.getLogger({ component: "match" }).warn(
// "bad match rule", // "bad match rule",
@ -141,10 +149,7 @@ export default class Match<T> {
} }
protected static getId(val: any): string { protected static getId(val: any): string {
if (typeof val === "object") { return (<{ uuid: string }>(<unknown>val)).uuid;
return (<{ uuid: string }>(<unknown>val)).uuid;
}
return <string>(<unknown>val);
} }
public del(val: T) { public del(val: T) {

View File

@ -33,7 +33,12 @@ export default class ExecScript {
GM_info: any; GM_info: any;
constructor(scriptRes: ScriptRunResouce, message: Message, thisContext?: { [key: string]: any }) { constructor(
scriptRes: ScriptRunResouce,
message: Message,
code: string | ScriptFunc,
thisContext?: { [key: string]: any }
) {
this.scriptRes = scriptRes; this.scriptRes = scriptRes;
this.logger = LoggerCore.getInstance().logger({ this.logger = LoggerCore.getInstance().logger({
component: "exec", component: "exec",
@ -42,7 +47,11 @@ export default class ExecScript {
}); });
this.GM_info = GMApi.GM_info(this.scriptRes); this.GM_info = GMApi.GM_info(this.scriptRes);
// 构建脚本资源 // 构建脚本资源
this.scriptFunc = compileScript(this.scriptRes.code); if (typeof code === "string") {
this.scriptFunc = compileScript(code);
} else {
this.scriptFunc = code;
}
const grantMap: { [key: string]: boolean } = {}; const grantMap: { [key: string]: boolean } = {};
scriptRes.metadata.grant?.forEach((key) => { scriptRes.metadata.grant?.forEach((key) => {
grantMap[key] = true; grantMap[key] = true;

View File

@ -1,11 +1,48 @@
import { ScriptRunResouce } from "@App/app/repo/scripts"; import { ScriptRunResouce } from "@App/app/repo/scripts";
import { Message } from "@Packages/message/server"; import { Message } from "@Packages/message/server";
import ExecScript from "./exec_script";
import { addStyle, ScriptFunc } from "./utils";
export class InjectRuntime { export class InjectRuntime {
execList: ExecScript[] = [];
constructor( constructor(
private msg: Message, private msg: Message,
private scripts: ScriptRunResouce[] private scripts: ScriptRunResouce[]
) {} ) {}
start(){} start() {
this.scripts.forEach((script) => {
// @ts-ignore
const scriptFunc = window[script.flag];
if (scriptFunc) {
this.execScript(script, scriptFunc);
} else {
// 监听脚本加载,和屏蔽读取
Object.defineProperty(window, script.flag, {
configurable: true,
set: (val: ScriptFunc) => {
this.execScript(script, val);
},
});
}
});
}
execScript(script: ScriptRunResouce, scriptFunc: ScriptFunc) {
// @ts-ignore
delete window[script.flag];
const exec = new ExecScript(script, this.msg, scriptFunc);
this.execList.push(exec);
// 注入css
if (script.metadata["require-css"]) {
script.metadata["require-css"].forEach((val) => {
const res = script.resource[val];
if (res) {
addStyle(res.content);
}
});
}
exec.exec();
}
} }

View File

@ -32,18 +32,6 @@ export function compileInjectScript(script: ScriptRunResouce): string {
return `window['${script.flag}']=function(context,GM_info){\n${script.code}\n}`; return `window['${script.flag}']=function(context,GM_info){\n${script.code}\n}`;
} }
// 编译注入脚本信息
export function compileInjectScriptInfo(
messageFlag: string,
script: ScriptRunResouce,
injectScriptInfoCode: string
): string {
return (
`(function (MessageFlag, ScriptFlag, ScriptUuid) {\n${injectScriptInfoCode}\n})` +
`('${messageFlag}', '${script.flag}', '${script.uuid}');`
);
}
// 设置api依赖 // 设置api依赖
function setDepend(context: { [key: string]: any }, apiVal: ApiValue) { function setDepend(context: { [key: string]: any }, apiVal: ApiValue) {
if (apiVal.param.depend) { if (apiVal.param.depend) {