Skip to content
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "scriptcat",
"version": "1.2.0-beta.4",
"version": "1.2.0-beta.5",
"description": "脚本猫,一个可以执行用户脚本的浏览器扩展,万物皆可脚本化,让你的浏览器可以做更多的事情!",
"author": "CodFrm",
"license": "GPLv3",
Expand Down
3 changes: 1 addition & 2 deletions packages/message/custom_event_message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,9 @@ export class CustomEventMessage implements Message {
relatedTarget: Map<number, EventTarget> = new Map();

constructor(
flags: MessageFlags | string,
messageFlag: string,
protected readonly isContent: boolean
) {
const messageFlag = typeof flags === "string" ? flags : flags.messageFlag;
this.receiveFlag = `evt${messageFlag}${isContent ? DefinedFlags.contentFlag : DefinedFlags.injectFlag}${DefinedFlags.domEvent}`;
this.sendFlag = `evt${messageFlag}${isContent ? DefinedFlags.injectFlag : DefinedFlags.contentFlag}${DefinedFlags.domEvent}`;
window.addEventListener(this.receiveFlag, (event) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/message/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ let client: CustomEventMessage;
const nextTick = () => Promise.resolve().then(() => {});

const setupGlobal = () => {
const flags = { messageFlag: "-test.server" };
const flags = "-test.server";
// 创建 content 和 inject 之间的消息通道
contentMessage = new CustomEventMessage(flags, true); // content 端
injectMessage = new CustomEventMessage(flags, false); // inject 端
Expand Down
41 changes: 41 additions & 0 deletions src/app/repo/scripts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Repo } from "./repo";
import type { Resource } from "./resource";
import type { SCMetadata } from "./metadata";
import type { GMInfoEnv } from "../service/content/types";

// 脚本模型
export type SCRIPT_TYPE = 1 | 2 | 3;
Expand Down Expand Up @@ -101,6 +102,46 @@ export interface ScriptRunResource extends Script {
originalMetadata: SCMetadata; // 原本的 Metadata (目前只需要 match, include, exclude)
}

/**
* 脚本加载信息。( service_worker / sandbox / popup 环境用 )
* 包含脚本元数据与用户配置。
*/
export interface ScriptLoadInfo extends ScriptRunResource {
/** 脚本元数据字符串 */
metadataStr: string;
/** 用户配置字符串 */
userConfigStr: string;
/** 用户配置对象(可选) */
userConfig?: UserConfig;
}

/**
* 脚本加载信息。( Inject / Content 环境用,避免过多不必要资讯公开,减少页面加载资讯储存量 )
* 包含脚本元数据与用户配置。
*/
Comment on lines +118 to +121
Copy link

Copilot AI Nov 16, 2025

Choose a reason for hiding this comment

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

注释中的"環境"使用了繁体中文字符。为了与项目中的其他简体中文注释保持一致,建议改为简体中文:"脚本加载信息。( Inject / Content 环境用,避免过多不必要资讯公开,减少页面加载资讯存储量 )"。

注意:这个注释也包含了"資訊"和"儲存"等繁体字,建议一并修改为简体中文。

Copilot uses AI. Check for mistakes.
export type TScriptInfo = Override<
ScriptLoadInfo,
{
originalMetadata?: Partial<Record<string, string[]>>;
resource: Record<string, { base64?: string; content: string; contentType: string }>;
code: "" | string;
sort?: number;
flag: string;
runStatus?: SCRIPT_RUN_STATUS;
type?: SCRIPT_TYPE;
status?: SCRIPT_STATUS;
}
>;

export type TClientPageLoadInfo =
| {
ok: true;
injectScriptList: TScriptInfo[];
contentScriptList: TScriptInfo[];
envInfo: GMInfoEnv;
}
| { ok: false };

export class ScriptDAO extends Repo<Script> {
scriptCodeDAO: ScriptCodeDAO = new ScriptCodeDAO();

Expand Down
68 changes: 26 additions & 42 deletions src/app/service/content/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,26 @@ import { Client, sendMessage } from "@Packages/message/client";
import { type CustomEventMessage } from "@Packages/message/custom_event_message";
import { forwardMessage, type Server } from "@Packages/message/server";
import type { MessageSend } from "@Packages/message/types";
import type { GMInfoEnv } from "./types";
import type { ScriptLoadInfo } from "../service_worker/types";
import type { ScriptExecutor } from "./script_executor";
import { isInjectIntoContent } from "./utils";
import { RuntimeClient } from "../service_worker/client";

// content页的处理
export default class ContentRuntime {
// 运行在content页面的脚本
contentScript: Map<string, ScriptLoadInfo> = new Map();
private readonly contentScriptSet: Set<string> = new Set();

constructor(
// 监听来自service_worker的消息
private extServer: Server,
private readonly extServer: Server,
// 监听来自inject的消息
private server: Server,
private readonly server: Server,
// 发送给扩展service_worker的通信接口
private senderToExt: MessageSend,
private readonly senderToExt: MessageSend,
// 发送给inject的消息接口
private senderToInject: CustomEventMessage,
private readonly senderToInject: CustomEventMessage,
// 脚本执行器消息接口
private scriptExecutorMsg: CustomEventMessage,
private scriptExecutor: ScriptExecutor
private readonly scriptExecutorMsg: CustomEventMessage,
private readonly scriptExecutor: ScriptExecutor
) {}

init() {
Expand Down Expand Up @@ -76,7 +73,7 @@ export default class ContentRuntime {
let parentNode: EventTarget | undefined;
// 判断是不是content脚本发过来的
let msg: CustomEventMessage;
if (this.contentScript.has(data.uuid) || this.scriptExecutor.execMap.has(data.uuid)) {
if (this.contentScriptSet.has(data.uuid) || this.scriptExecutor.execMap.has(data.uuid)) {
msg = this.scriptExecutorMsg;
} else {
msg = this.senderToInject;
Expand Down Expand Up @@ -126,38 +123,25 @@ export default class ContentRuntime {
);
}

pageLoad(messageFlags: MessageFlags) {
this.scriptExecutor.checkEarlyStartScript("content", messageFlags);

pageLoad(messageFlag: string) {
this.scriptExecutor.checkEarlyStartScript("content", messageFlag);
const client = new RuntimeClient(this.senderToExt);
// 向service_worker请求脚本列表
client.pageLoad().then((data) => {
this.start(data.scripts, data.envInfo);
});
}

start(scripts: ScriptLoadInfo[], envInfo: GMInfoEnv) {
// 启动脚本
const client = new Client(this.senderToInject, "inject");
// 根据@inject-into content过滤脚本
const injectScript: ScriptLoadInfo[] = [];
const contentScript: ScriptLoadInfo[] = [];
for (const script of scripts) {
if (isInjectIntoContent(script.metadata)) {
contentScript.push(script);
continue;
// 向service_worker请求脚本列表及环境信息
client.pageLoad().then((o) => {
if (!o.ok) return;
const { injectScriptList, contentScriptList, envInfo } = o;
// 启动脚本:向 inject页面 发送脚本列表及环境信息
const client = new Client(this.senderToInject, "inject");
// 根据@inject-into content过滤脚本
client.do("pageLoad", { injectScriptList, envInfo });
// 处理注入到content环境的脚本
for (const script of contentScriptList) {
this.contentScriptSet.add(script.uuid);
}
injectScript.push(script);
}
client.do("pageLoad", { scripts: injectScript, envInfo });

// 处理注入到content环境的脚本
for (const script of contentScript) {
this.contentScript.set(script.uuid, script);
}
// 监听事件
this.scriptExecutor.init(envInfo);
// 启动脚本
this.scriptExecutor.start(contentScript);
// 监听事件
this.scriptExecutor.setEnvInfo(envInfo);
// 启动脚本
this.scriptExecutor.startScripts(contentScriptList);
});
}
}
4 changes: 2 additions & 2 deletions src/app/service/content/create_context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type ScriptRunResource } from "@App/app/repo/scripts";
import type { TScriptInfo } from "@App/app/repo/scripts";
import { v4 as uuidv4 } from "uuid";
import type { Message } from "@Packages/message/types";
import EventEmitter from "eventemitter3";
Expand All @@ -10,7 +10,7 @@ import { createGMBase } from "./gm_api/gm_api";

// 构建沙盒上下文
export const createContext = (
scriptRes: ScriptRunResource,
scriptRes: TScriptInfo,
GMInfo: any,
envPrefix: string,
message: Message,
Expand Down
6 changes: 3 additions & 3 deletions src/app/service/content/exec_script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { createContext, createProxyContext } from "./create_context";
import type { GMInfoEnv, ScriptFunc } from "./types";
import { compileScript } from "./utils";
import type { Message } from "@Packages/message/types";
import type { ScriptLoadInfo } from "../service_worker/types";
import type { ValueUpdateDataEncoded } from "./types";
import { evaluateGMInfo } from "./gm_api/gm_info";
import type { IGM_Base } from "./gm_api/gm_api";
import type { TScriptInfo } from "@App/app/repo/scripts";

// 执行脚本,控制脚本执行与停止
export default class ExecScript {
scriptRes: ScriptLoadInfo;
scriptRes: TScriptInfo;

scriptFunc: ScriptFunc;

Expand All @@ -24,7 +24,7 @@ export default class ExecScript {
named?: { [key: string]: any };

constructor(
scriptRes: ScriptLoadInfo,
scriptRes: TScriptInfo,
envPrefix: "content" | "offscreen",
message: Message,
code: string | ScriptFunc,
Expand Down
7 changes: 3 additions & 4 deletions src/app/service/content/gm_api/gm_info.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ExtVersion } from "@App/app/const";
import type { GMInfoEnv } from "../types";
import type { ScriptLoadInfo } from "@App/app/service/service_worker/types";
import type { TScriptInfo } from "@App/app/repo/scripts";

// 获取脚本信息和管理器信息
export function evaluateGMInfo(envInfo: GMInfoEnv, script: ScriptLoadInfo) {
export function evaluateGMInfo(envInfo: GMInfoEnv, script: TScriptInfo) {
const options = {
description: script.metadata.description?.[0] || null,
matches: script.metadata.match || [],
Expand All @@ -21,15 +21,14 @@ export function evaluateGMInfo(envInfo: GMInfoEnv, script: ScriptLoadInfo) {
isIncognito: envInfo.isIncognito,
// relaxedCsp
sandboxMode: envInfo.sandboxMode,
scriptWillUpdate: true,
scriptWillUpdate: !!script.checkUpdate,
scriptHandler: "ScriptCat",
userAgentData: envInfo.userAgentData,
// "" => null
scriptUpdateURL: script.downloadUrl || null,
scriptMetaStr: script.metadataStr,
userConfig: script.userConfig,
userConfigStr: script.userConfigStr,
// scriptSource: script.sourceCode,
version: ExtVersion,
script: {
// TODO: 更多完整的信息(为了兼容Tampermonkey,后续待定)
Expand Down
27 changes: 16 additions & 11 deletions src/app/service/content/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,38 @@ import type { Message } from "@Packages/message/types";
import { ExternalWhitelist } from "@App/app/const";
import { sendMessage } from "@Packages/message/client";
import type { ScriptExecutor } from "./script_executor";
import type { EmitEventRequest, ScriptLoadInfo } from "../service_worker/types";
import type { TScriptInfo } from "@App/app/repo/scripts";
import type { EmitEventRequest } from "../service_worker/types";
import type { GMInfoEnv, ValueUpdateDataEncoded } from "./types";

export class InjectRuntime {
constructor(
private server: Server,
private msg: Message,
private scriptExecutor: ScriptExecutor
private readonly server: Server,
private readonly msg: Message,
private readonly scriptExecutor: ScriptExecutor
) {}

init(envInfo: GMInfoEnv) {
this.scriptExecutor.init(envInfo);

init() {
this.server.on("runtime/emitEvent", (data: EmitEventRequest) => {
// 转发给脚本
this.scriptExecutor.emitEvent(data);
});
this.server.on("runtime/valueUpdate", (data: ValueUpdateDataEncoded) => {
this.scriptExecutor.valueUpdate(data);
});
}

// 注入允许外部调用
this.externalMessage();
setEnvInfo(envInfo: GMInfoEnv) {
this.scriptExecutor.setEnvInfo(envInfo);
}

start(scripts: ScriptLoadInfo[]) {
this.scriptExecutor.start(scripts);
startScripts(injectScriptList: TScriptInfo[]) {
this.scriptExecutor.startScripts(injectScriptList);
}

onInjectPageLoaded() {
// 注入允许外部调用
this.externalMessage();
}

externalMessage() {
Expand Down
37 changes: 18 additions & 19 deletions src/app/service/content/script_executor.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { Message } from "@Packages/message/types";
import { getStorageName } from "@App/pkg/utils/utils";
import type { EmitEventRequest, ScriptLoadInfo } from "../service_worker/types";
import type { EmitEventRequest } from "../service_worker/types";
import ExecScript from "./exec_script";
import type { GMInfoEnv, ScriptFunc, PreScriptFunc, ValueUpdateDataEncoded } from "./types";
import type { GMInfoEnv, ScriptFunc, ValueUpdateDataEncoded } from "./types";
import { addStyle, definePropertyListener } from "./utils";
import type { TScriptInfo } from "@App/app/repo/scripts";
import { DefinedFlags } from "../service_worker/runtime.consts";

export type ExecScriptEntry = {
scriptLoadInfo: ScriptLoadInfo;
scriptLoadInfo: TScriptInfo;
scriptFlag: string;
envInfo: any;
scriptFunc: any;
Expand All @@ -22,7 +23,7 @@ export class ScriptExecutor {

constructor(private msg: Message) {}

init(envInfo: GMInfoEnv) {
setEnvInfo(envInfo: GMInfoEnv) {
this.envInfo = envInfo;
}

Expand All @@ -43,8 +44,8 @@ export class ScriptExecutor {
}
}

start(scripts: ScriptLoadInfo[]) {
const loadExec = (script: ScriptLoadInfo, scriptFunc: any) => {
startScripts(scripts: TScriptInfo[]) {
const loadExec = (script: TScriptInfo, scriptFunc: any) => {
this.execScriptEntry({
scriptLoadInfo: script,
scriptFlag: script.flag,
Expand All @@ -71,21 +72,19 @@ export class ScriptExecutor {
});
}

checkEarlyStartScript(env: "content" | "inject", messageFlags: MessageFlags) {
checkEarlyStartScript(env: "content" | "inject", messageFlag: string) {
const isContent = env === "content";
const messageFlag = messageFlags.messageFlag;
const eventNamePrefix = `evt${messageFlag}${isContent ? DefinedFlags.contentFlag : DefinedFlags.injectFlag}`;
const scriptLoadCompleteEvtName = `${eventNamePrefix}${DefinedFlags.scriptLoadComplete}`;
const envLoadCompleteEvtName = `${eventNamePrefix}${DefinedFlags.envLoadComplete}`;
// 监听 脚本加载
// 适用于此「通知环境加载完成」代码执行后的脚本加载
window.addEventListener(scriptLoadCompleteEvtName, (event) => {
if (event instanceof CustomEvent) {
if (typeof event.detail.scriptFlag === "string") {
event.preventDefault(); // dispatchEvent 会回传 false -> 分离环境也能得知环境加载代码已执行
const scriptFlag = event.detail.scriptFlag;
this.execEarlyScript(scriptFlag);
}
window.addEventListener(scriptLoadCompleteEvtName, (ev) => {
const detail = (ev as CustomEvent).detail;
const scriptFlag = detail?.scriptFlag;
if (typeof scriptFlag === "string") {
ev.preventDefault(); // dispatchEvent 会回传 false -> 分离环境也能得知环境加载代码已执行
this.execEarlyScript(scriptFlag, detail.scriptInfo);
}
});
// 通知 环境 加载完成
Expand All @@ -94,11 +93,11 @@ export class ScriptExecutor {
window.dispatchEvent(ev);
}

execEarlyScript(flag: string) {
const scriptFunc = (window as any)[flag] as PreScriptFunc;
execEarlyScript(flag: string, scriptInfo: TScriptInfo) {
const scriptFunc = (window as any)[flag] as ScriptFunc;
this.execScriptEntry({
scriptLoadInfo: scriptFunc.scriptInfo,
scriptFunc: scriptFunc.func,
scriptLoadInfo: scriptInfo,
scriptFunc: scriptFunc,
scriptFlag: flag,
envInfo: {},
});
Expand Down
Loading
Loading