match
Some checks failed
build / Build (push) Failing after 6s
test / Run tests (push) Failing after 8s

This commit is contained in:
王一之 2025-04-01 18:04:30 +08:00
parent db8c5ec7b5
commit 20124be0e4
10 changed files with 162 additions and 65 deletions

View File

@ -1,4 +1,4 @@
import { Message, MessageConnect, MessageSend } from "./server"; import { Message, MessageConnect, MessageSend, MessageSender } from "./server";
export class ExtensionMessageSend implements MessageSend { export class ExtensionMessageSend implements MessageSend {
constructor() {} constructor() {}
@ -21,6 +21,28 @@ export class ExtensionMessageSend implements MessageSend {
} }
} }
// 由于service worker的限制特殊处理chrome.runtime.onConnect/Message
export class ServiceWorkerMessage extends ExtensionMessageSend implements Message {
onConnect(callback: (data: any, con: MessageConnect) => void): void {
chrome.runtime.onConnect.addListener((port) => {
const handler = (msg: any) => {
port.onMessage.removeListener(handler);
callback(msg, new ExtensionMessageConnect(port));
};
port.onMessage.addListener(handler);
});
}
onMessage(callback: (data: any, sendResponse: (data: any) => void, sender: MessageSender) => void): void {
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.action === "messageQueue") {
return false;
}
return callback(msg, sendResponse, sender);
});
}
}
export class ExtensionMessage extends ExtensionMessageSend implements Message { export class ExtensionMessage extends ExtensionMessageSend implements Message {
onConnect(callback: (data: any, con: MessageConnect) => void) { onConnect(callback: (data: any, con: MessageConnect) => void) {
chrome.runtime.onConnect.addListener((port) => { chrome.runtime.onConnect.addListener((port) => {
@ -33,12 +55,12 @@ export class ExtensionMessage extends ExtensionMessageSend implements Message {
} }
// 注意chrome.runtime.onMessage.addListener的回调函数需要返回true才能处理异步请求 // 注意chrome.runtime.onMessage.addListener的回调函数需要返回true才能处理异步请求
onMessage(callback: (data: any, sendResponse: (data: any) => void) => void) { onMessage(callback: (data: any, sendResponse: (data: any) => void, sender: MessageSender) => void): void {
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.action === "messageQueue") { if (msg.action === "messageQueue") {
return false; return false;
} }
return callback(msg, sendResponse); return callback(msg, sendResponse, sender);
}); });
} }
} }

View File

@ -1,5 +1,4 @@
import EventEmitter from "eventemitter3"; import EventEmitter from "eventemitter3";
import Logger from "@App/app/logger/logger";
import LoggerCore from "@App/app/logger/core"; import LoggerCore from "@App/app/logger/core";
export type SubscribeCallback = (message: any) => void; export type SubscribeCallback = (message: any) => void;

View File

@ -2,7 +2,7 @@ import LoggerCore from "@App/app/logger/core";
export interface Message extends MessageSend { export interface Message extends MessageSend {
onConnect(callback: (data: any, con: MessageConnect) => void): void; onConnect(callback: (data: any, con: MessageConnect) => void): void;
onMessage(callback: (data: any, sendResponse: (data: any) => void) => void): void; onMessage(callback: (data: any, sendResponse: (data: any) => void, sender?: MessageSender) => void): void;
} }
export interface MessageSend { export interface MessageSend {
@ -17,11 +17,21 @@ export interface MessageConnect {
onDisconnect(callback: () => void): void; onDisconnect(callback: () => void): void;
} }
export type MessageSender = { export type MessageSender = any;
tabId: number;
};
export type ApiFunction = (params: any, con: MessageConnect | null) => Promise<any> | void; export class GetSender {
constructor(private sender: MessageConnect | MessageSender) {}
getSender(): MessageSender {
return this.sender as MessageSender;
}
getConnect(): MessageConnect {
return this.sender as MessageConnect;
}
}
export type ApiFunction = (params: any, con: GetSender) => Promise<any> | void;
export class Server { export class Server {
private apiFunctionMap: Map<string, ApiFunction> = new Map(); private apiFunctionMap: Map<string, ApiFunction> = new Map();
@ -37,10 +47,10 @@ export class Server {
return false; return false;
}); });
message.onMessage((msg: { action: string; data: any }, sendResponse) => { message.onMessage((msg: { action: string; data: any }, sendResponse, sender) => {
this.logger.trace("server onMessage", { msg: msg as any }); this.logger.trace("server onMessage", { msg: msg as any });
if (msg.action.startsWith(prefix)) { if (msg.action.startsWith(prefix)) {
return this.messageHandle(msg.action.slice(prefix.length + 1), msg.data, sendResponse); return this.messageHandle(msg.action.slice(prefix.length + 1), msg.data, sendResponse, sender);
} }
return false; return false;
}); });
@ -57,15 +67,15 @@ export class Server {
private connectHandle(msg: string, params: any, con: MessageConnect) { private connectHandle(msg: string, params: any, con: MessageConnect) {
const func = this.apiFunctionMap.get(msg); const func = this.apiFunctionMap.get(msg);
if (func) { if (func) {
func(params, con); func(params, new GetSender(con));
} }
} }
private messageHandle(msg: string, params: any, sendResponse: (response: any) => void) { private messageHandle(msg: string, params: any, sendResponse: (response: any) => void, sender?: MessageSender) {
const func = this.apiFunctionMap.get(msg); const func = this.apiFunctionMap.get(msg);
if (func) { if (func) {
try { try {
const ret = func(params, null); const ret = func(params, new GetSender(sender!));
if (ret instanceof Promise) { if (ret instanceof Promise) {
ret.then((data) => { ret.then((data) => {
sendResponse({ code: 0, data }); sendResponse({ code: 0, data });
@ -108,18 +118,19 @@ export function forwardMessage(prefix: string, path: string, from: Server, to: M
from.on(path, (params, fromCon) => { from.on(path, (params, fromCon) => {
console.log("forwardMessage", path, prefix, params); console.log("forwardMessage", path, prefix, params);
if (fromCon) { if (fromCon) {
const fromConnect = fromCon.getConnect();
to.connect({ action: prefix + "/" + path, data: params }).then((toCon) => { to.connect({ action: prefix + "/" + path, data: params }).then((toCon) => {
fromCon.onMessage((data) => { fromConnect.onMessage((data) => {
toCon.sendMessage(data); toCon.sendMessage(data);
}); });
toCon.onMessage((data) => { toCon.onMessage((data) => {
fromCon.sendMessage(data); fromConnect.sendMessage(data);
}); });
fromCon.onDisconnect(() => { fromConnect.onDisconnect(() => {
toCon.disconnect(); toCon.disconnect();
}); });
toCon.onDisconnect(() => { toCon.onDisconnect(() => {
fromCon.disconnect(); fromConnect.disconnect();
}); });
}); });
} else { } else {

View File

@ -1,7 +1,7 @@
import LoggerCore from "@App/app/logger/core"; import LoggerCore from "@App/app/logger/core";
import Logger from "@App/app/logger/logger"; import Logger from "@App/app/logger/logger";
import { Script, ScriptDAO } from "@App/app/repo/scripts"; import { Script, ScriptDAO } from "@App/app/repo/scripts";
import { Group, MessageConnect, MessageSend, MessageSender } from "@Packages/message/server"; import { GetSender, Group, MessageConnect, MessageSend, MessageSender } from "@Packages/message/server";
import { ValueService } from "@App/app/service/service_worker/value"; import { ValueService } from "@App/app/service/service_worker/value";
import PermissionVerify from "./permission_verify"; import PermissionVerify from "./permission_verify";
import { connect } from "@Packages/message/client"; import { connect } from "@Packages/message/client";
@ -40,7 +40,7 @@ export default class GMApi {
this.logger = LoggerCore.logger().with({ service: "runtime/gm_api" }); this.logger = LoggerCore.logger().with({ service: "runtime/gm_api" });
} }
async handlerRequest(data: MessageRequest, con: MessageConnect | null) { async handlerRequest(data: MessageRequest, con: GetSender) {
this.logger.trace("GM API request", { api: data.api, uuid: data.uuid, param: data.params }); this.logger.trace("GM API request", { api: data.api, uuid: data.uuid, param: data.params });
const api = PermissionVerify.apis.get(data.api); const api = PermissionVerify.apis.get(data.api);
if (!api) { if (!api) {
@ -53,7 +53,7 @@ export default class GMApi {
this.logger.error("verify error", { api: data.api }, Logger.E(e)); this.logger.error("verify error", { api: data.api }, Logger.E(e));
return Promise.reject(e); return Promise.reject(e);
} }
return api.api.call(this, req, con); return api.api.call(this, req, con.getConnect());
} }
// 解析请求 // 解析请求

View File

@ -1,15 +1,16 @@
import { MessageQueue } from "@Packages/message/message_queue"; import { MessageQueue } from "@Packages/message/message_queue";
import { Group, MessageSend } from "@Packages/message/server"; import { GetSender, Group, MessageSend } from "@Packages/message/server";
import { Script, SCRIPT_STATUS_ENABLE, SCRIPT_TYPE_NORMAL, ScriptDAO, ScriptRunResouce } from "@App/app/repo/scripts"; import { Script, SCRIPT_STATUS_ENABLE, SCRIPT_TYPE_NORMAL, ScriptDAO, ScriptRunResouce } from "@App/app/repo/scripts";
import { ValueService } from "./value"; import { ValueService } from "./value";
import GMApi from "./gm_api"; import GMApi from "./gm_api";
import { subscribeScriptDelete, subscribeScriptEnable, subscribeScriptInstall } from "../queue"; import { subscribeScriptDelete, subscribeScriptEnable, subscribeScriptInstall } from "../queue";
import { ScriptService } from "./script"; import { ScriptService } from "./script";
import { runScript, stopScript } from "../offscreen/client"; import { runScript, stopScript } from "../offscreen/client";
import { dealMatches, getRunAt } from "./utils"; import { getRunAt } from "./utils";
import { randomString } from "@App/pkg/utils/utils"; import { randomString } from "@App/pkg/utils/utils";
import { compileInjectScript, 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 { dealMatches } from "@App/pkg/utils/match";
export class RuntimeService { export class RuntimeService {
scriptDAO: ScriptDAO = new ScriptDAO(); scriptDAO: ScriptDAO = new ScriptDAO();
@ -99,7 +100,10 @@ export class RuntimeService {
this.group.on("pageLoad", this.pageLoad.bind(this)); this.group.on("pageLoad", this.pageLoad.bind(this));
} }
pageLoad() { pageLoad(_, sender: GetSender) {
const chromeSender = sender.getSender() as chrome.runtime.MessageSender;
// 匹配当前页面的脚本
return Promise.resolve({ flag: this.scriptFlag }); return Promise.resolve({ flag: this.scriptFlag });
} }
@ -119,6 +123,8 @@ export class RuntimeService {
} }
registerInjectScript() { registerInjectScript() {
chrome.userScripts.getScripts({ ids: ["scriptcat-inject"] }).then((res) => {
if (res.length == 0) {
fetch("inject.js") fetch("inject.js")
.then((res) => res.text()) .then((res) => res.text())
.then((injectJs) => { .then((injectJs) => {
@ -135,9 +141,23 @@ export class RuntimeService {
}, },
]); ]);
}); });
chrome.scripting.registerContentScripts([
{
id: "scriptcat-content",
js: ["src/content.js"],
matches: ["<all_urls>"],
allFrames: true,
runAt: "document_start",
},
]);
}
});
} }
async registryPageScript(script: Script) { async registryPageScript(script: Script) {
if (await Cache.getInstance().has("registryScript:" + script.uuid)) {
return;
}
const matches = script.metadata["match"]; const matches = script.metadata["match"];
if (!matches) { if (!matches) {
return; return;
@ -167,12 +187,21 @@ export class RuntimeService {
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], () => {
Cache.getInstance().set("registryScript:" + script.uuid, true);
});
// 标记为已注册
} }
unregistryPageScript(script: Script) { unregistryPageScript(script: Script) {
chrome.userScripts.unregister({ chrome.userScripts.unregister(
{
ids: [script.uuid], ids: [script.uuid],
}); },
() => {
// 删除缓存
Cache.getInstance().del("registryScript:" + script.uuid);
}
);
} }
} }

View File

@ -5,11 +5,6 @@ export function isExtensionRequest(details: chrome.webRequest.ResourceRequest &
); );
} }
// 处理油猴的match和include为chrome的matches
export function dealMatches(matches: string[]) {
return matches;
}
export function getRunAt(runAts: string[]): chrome.userScripts.RunAt { export function getRunAt(runAts: string[]): chrome.userScripts.RunAt {
if (runAts.length === 0) { if (runAts.length === 0) {
return "document_idle"; return "document_idle";

View File

@ -17,18 +17,6 @@
"128": "assets/logo.png" "128": "assets/logo.png"
} }
}, },
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": [
"src/content.js"
],
"run_at": "document_start",
"all_frames": true
}
],
"icons": { "icons": {
"128": "assets/logo.png" "128": "assets/logo.png"
}, },

View File

@ -0,0 +1,11 @@
import { describe, it } from "vitest";
import { dealMatches, parseURL } from "./match";
// https://developer.chrome.com/docs/extensions/mv3/match_patterns/
describe("dealMatches", () => {
it("*://link.17173.com*", () => {
const url = parseURL("*://link.17173.com*");
const matches = dealMatches(["*://link.17173.com*"]);
console.log(url, matches);
});
});

52
src/pkg/utils/match.ts Normal file
View File

@ -0,0 +1,52 @@
export interface Url {
scheme: string;
host: string;
path: string;
search: string;
}
// 根据https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns?hl=zh-cn进行匹配
export class Match {}
export function parseURL(url: string): Url | undefined {
const match = /^(.+?):\/\/(.*?)((\/.*?)(\?.*?|)|)$/.exec(url);
if (match) {
return {
scheme: match[1],
host: match[2],
path: match[4] || (url[url.length - 1] === "*" ? "*" : "/"),
search: match[5],
};
}
// 处理一些特殊情况
switch (url) {
case "*":
return {
scheme: "*",
host: "*",
path: "*",
search: "*",
};
default:
}
return undefined;
}
// 处理油猴的match和include为chrome的matches
export function dealMatches(matches: string[]) {
const result: string[] = [];
for (let i = 0; i < matches.length; i++) {
const url = parseURL(matches[i]);
if (url) {
// *开头但是不是*.的情况
if (url.host.startsWith("*")) {
if (!url.host.startsWith("*.")) {
// 删除开头的*号
url.host = url.host.slice(1);
}
}
result.push(`${url.scheme}://${url.host}${url.path}` + (url.search ? "?" + url.search : ""));
}
}
return result;
}

View File

@ -217,13 +217,3 @@ export function sleep(time: number) {
setTimeout(resolve, time); setTimeout(resolve, time);
}); });
} }
// 使service_worker长时间存活
export async function waitUntil(promise: Promise<any>) {
const keepAlive = setInterval(chrome.runtime.getPlatformInfo, 25 * 1000);
try {
await promise;
} finally {
clearInterval(keepAlive);
}
}