2025-04-25 15:41:02 +08:00

505 lines
16 KiB
TypeScript

import { fetchScriptInfo, prepareScriptByCode } from "@App/pkg/utils/script";
import { v4 as uuidv4 } from "uuid";
import { Group } from "@Packages/message/server";
import Logger from "@App/app/logger/logger";
import LoggerCore from "@App/app/logger/core";
import Cache from "@App/app/cache";
import CacheKey from "@App/app/cache_key";
import { checkSilenceUpdate, ltever, openInCurrentTab, randomString } from "@App/pkg/utils/utils";
import {
Script,
SCRIPT_RUN_STATUS,
SCRIPT_STATUS_DISABLE,
SCRIPT_STATUS_ENABLE,
ScriptCodeDAO,
ScriptDAO,
ScriptRunResouce,
} from "@App/app/repo/scripts";
import { MessageQueue } from "@Packages/message/message_queue";
import { InstallSource } from ".";
import { ResourceService } from "./resource";
import { ValueService } from "./value";
import { compileScriptCode } from "../content/utils";
import { SystemConfig } from "@App/pkg/config/config";
export class ScriptService {
logger: Logger;
scriptDAO: ScriptDAO = new ScriptDAO();
scriptCodeDAO: ScriptCodeDAO = new ScriptCodeDAO();
constructor(
private systemConfig: SystemConfig,
private group: Group,
private mq: MessageQueue,
private valueService: ValueService,
private resourceService: ResourceService
) {
this.logger = LoggerCore.logger().with({ service: "script" });
}
listenerScriptInstall() {
// 初始化脚本安装监听
chrome.webRequest.onBeforeRequest.addListener(
(req: chrome.webRequest.WebRequestBodyDetails) => {
// 处理url, 实现安装脚本
if (req.method !== "GET") {
return;
}
const url = new URL(req.url);
// 判断是否有hash
if (!url.hash) {
return;
}
// 判断是否有url参数
if (!url.hash.includes("url=")) {
return;
}
// 获取url参数
const targetUrl = url.hash.split("url=")[1];
// 读取脚本url内容, 进行安装
const logger = this.logger.with({ url: targetUrl });
logger.debug("install script");
this.openInstallPageByUrl(targetUrl, "user").catch((e) => {
logger.error("install script error", Logger.E(e));
// 如果打开失败, 则重定向到安装页
chrome.scripting.executeScript({
target: { tabId: req.tabId },
func: function () {
history.back();
},
});
// 并不再重定向当前url
chrome.declarativeNetRequest.updateDynamicRules(
{
removeRuleIds: [2],
addRules: [
{
id: 2,
priority: 1,
action: {
type: chrome.declarativeNetRequest.RuleActionType.ALLOW,
},
condition: {
regexFilter: targetUrl,
resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME],
requestMethods: [chrome.declarativeNetRequest.RequestMethod.GET],
},
},
],
},
() => {
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError);
}
}
);
});
},
{
urls: [
"https://docs.scriptcat.org/docs/script_installation",
"https://www.tampermonkey.net/script_installation.php",
],
types: ["main_frame"],
}
);
// 重定向到脚本安装页
chrome.declarativeNetRequest.updateDynamicRules(
{
removeRuleIds: [1, 2],
addRules: [
{
id: 1,
priority: 1,
action: {
type: chrome.declarativeNetRequest.RuleActionType.REDIRECT,
redirect: {
regexSubstitution: "https://docs.scriptcat.org/docs/script_installation#url=\\0",
},
},
condition: {
regexFilter: "^([^#]+?)\\.user(\\.bg|\\.sub)?\\.js((\\?).*|$)",
resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME],
requestMethods: [chrome.declarativeNetRequest.RequestMethod.GET],
// 排除常见的符合上述条件的域名
excludedRequestDomains: ["github.com"],
},
},
],
},
() => {
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError);
}
}
);
}
public openInstallPageByUrl(url: string, source: InstallSource) {
const uuid = uuidv4();
return fetchScriptInfo(url, source, false, uuidv4()).then((info) => {
Cache.getInstance().set(CacheKey.scriptInstallInfo(uuid), info);
setTimeout(() => {
// 清理缓存
Cache.getInstance().del(CacheKey.scriptInstallInfo(uuid));
}, 30 * 1000);
openInCurrentTab(`/src/install.html?uuid=${uuid}`);
});
}
// 直接通过url静默安装脚本
async installByUrl(url: string, source: InstallSource, subscribeUrl?: string) {
const info = await fetchScriptInfo(url, source, false, uuidv4());
const prepareScript = await prepareScriptByCode(info.code, url, info.uuid);
prepareScript.script.subscribeUrl = subscribeUrl;
this.installScript({
script: prepareScript.script,
code: info.code,
upsertBy: source,
});
return Promise.resolve(prepareScript.script);
}
// 获取安装信息
getInstallInfo(uuid: string) {
return Cache.getInstance().get(CacheKey.scriptInstallInfo(uuid));
}
// 安装脚本
async installScript(param: { script: Script; code: string; upsertBy: InstallSource }) {
param.upsertBy = param.upsertBy || "user";
const { script, upsertBy } = param;
const logger = this.logger.with({
name: script.name,
uuid: script.uuid,
version: script.metadata.version![0],
upsertBy,
});
let update = false;
// 判断是否已经安装
const oldScript = await this.scriptDAO.get(script.uuid);
if (oldScript) {
// 执行更新逻辑
update = true;
script.selfMetadata = oldScript.selfMetadata;
}
return this.scriptDAO
.save(script)
.then(async () => {
await this.scriptCodeDAO.save({
uuid: script.uuid,
code: param.code,
});
logger.info("install success");
// 广播一下
this.mq.publish("installScript", { script, update, upsertBy });
return Promise.resolve({ update });
})
.catch((e: any) => {
logger.error("install error", Logger.E(e));
throw e;
});
}
async deleteScript(uuid: string) {
const logger = this.logger.with({ uuid });
const script = await this.scriptDAO.get(uuid);
if (!script) {
logger.error("script not found");
throw new Error("script not found");
}
return this.scriptDAO
.delete(uuid)
.then(() => {
logger.info("delete success");
this.mq.publish("deleteScript", { uuid });
return true;
})
.catch((e) => {
logger.error("delete error", Logger.E(e));
throw e;
});
}
async enableScript(param: { uuid: string; enable: boolean }) {
const logger = this.logger.with({ uuid: param.uuid, enable: param.enable });
const script = await this.scriptDAO.get(param.uuid);
if (!script) {
logger.error("script not found");
throw new Error("script not found");
}
return this.scriptDAO
.update(param.uuid, { status: param.enable ? SCRIPT_STATUS_ENABLE : SCRIPT_STATUS_DISABLE })
.then(() => {
logger.info("enable success");
this.mq.publish("enableScript", { uuid: param.uuid, enable: param.enable });
return {};
})
.catch((e) => {
logger.error("enable error", Logger.E(e));
throw e;
});
}
async fetchInfo(uuid: string) {
const script = await this.scriptDAO.get(uuid);
if (!script) {
return null;
}
return script;
}
async updateRunStatus(params: { uuid: string; runStatus: SCRIPT_RUN_STATUS; error?: string; nextruntime?: number }) {
if (
(await this.scriptDAO.update(params.uuid, {
runStatus: params.runStatus,
lastruntime: new Date().getTime(),
error: params.error,
nextruntime: params.nextruntime,
})) === false
) {
return Promise.reject("update error");
}
this.mq.publish("scriptRunStatus", params);
return Promise.resolve(true);
}
getCode(uuid: string) {
return this.scriptCodeDAO.get(uuid);
}
async buildScriptRunResource(script: Script): Promise<ScriptRunResouce> {
const ret: ScriptRunResouce = <ScriptRunResouce>Object.assign(script);
// 自定义配置
if (ret.selfMetadata) {
ret.metadata = { ...ret.metadata };
Object.keys(ret.selfMetadata).forEach((key) => {
ret.metadata[key] = ret.selfMetadata![key];
});
}
ret.value = await this.valueService.getScriptValue(ret);
ret.resource = await this.resourceService.getScriptResources(ret);
ret.flag = randomString(16);
const code = await this.getCode(ret.uuid);
if (!code) {
throw new Error("code is null");
}
ret.code = code.code;
ret.code = compileScriptCode(ret);
return Promise.resolve(ret);
}
async excludeUrl({ uuid, url, remove }: { uuid: string; url: string; remove: boolean }) {
const script = await this.scriptDAO.get(uuid);
if (!script) {
throw new Error("script not found");
}
script.selfMetadata = script.selfMetadata || {};
let excludes = script.selfMetadata.exclude || script.metadata.exclude || [];
if (remove) {
excludes = excludes.filter((item) => item !== url);
} else {
excludes.push(url);
}
script.selfMetadata.exclude = excludes;
return this.scriptDAO
.update(uuid, script)
.then(() => {
// 广播一下
this.mq.publish("installScript", { script, update: true });
return true;
})
.catch((e) => {
this.logger.error("exclude url error", Logger.E(e));
throw e;
});
}
async resetExclude({ uuid, exclude }: { uuid: string; exclude: string[] | undefined }) {
const script = await this.scriptDAO.get(uuid);
if (!script) {
throw new Error("script not found");
}
script.selfMetadata = script.selfMetadata || {};
if (exclude) {
script.selfMetadata.exclude = exclude;
} else {
delete script.selfMetadata.exclude;
}
return this.scriptDAO
.update(uuid, script)
.then(() => {
// 广播一下
this.mq.publish("installScript", { script, update: true });
return true;
})
.catch((e) => {
this.logger.error("reset exclude error", Logger.E(e));
throw e;
});
}
async resetMatch({ uuid, match }: { uuid: string; match: string[] | undefined }) {
const script = await this.scriptDAO.get(uuid);
if (!script) {
throw new Error("script not found");
}
script.selfMetadata = script.selfMetadata || {};
if (match) {
script.selfMetadata.match = match;
} else {
delete script.selfMetadata.match;
}
return this.scriptDAO
.update(uuid, script)
.then(() => {
// 广播一下
this.mq.publish("installScript", { script, update: true });
return true;
})
.catch((e) => {
this.logger.error("reset match error", Logger.E(e));
throw e;
});
}
async checkUpdate(uuid: string, source: "user" | "system") {
// 检查更新
const script = await this.scriptDAO.get(uuid);
if (!script) {
return Promise.resolve(false);
}
await this.scriptDAO.update(uuid, { checktime: new Date().getTime() });
if (!script.checkUpdateUrl) {
return Promise.resolve(false);
}
const logger = LoggerCore.logger({
uuid: script.uuid,
name: script.name,
});
try {
const info = await fetchScriptInfo(script.checkUpdateUrl, source, false, script.uuid);
const { metadata } = info;
if (!metadata) {
logger.error("parse metadata failed");
return Promise.resolve(false);
}
const newVersion = metadata.version && metadata.version[0];
if (!newVersion) {
logger.error("parse version failed", { version: metadata.version });
return Promise.resolve(false);
}
let oldVersion = script.metadata.version && script.metadata.version[0];
if (!oldVersion) {
oldVersion = "0.0.0";
}
// 对比版本大小
if (ltever(newVersion, oldVersion, logger)) {
return Promise.resolve(false);
}
// 进行更新
this.openUpdatePage(script, source);
} catch (e) {
logger.error("check update failed", Logger.E(e));
return Promise.resolve(false);
}
return Promise.resolve(true);
}
// 打开更新窗口
public openUpdatePage(script: Script, source: "user" | "system") {
const logger = this.logger.with({
uuid: script.uuid,
name: script.name,
downloadUrl: script.downloadUrl,
checkUpdateUrl: script.checkUpdateUrl,
});
fetchScriptInfo(script.downloadUrl || script.checkUpdateUrl!, source, true, script.uuid)
.then(async (info) => {
// 是否静默更新
if (await this.systemConfig.getSilenceUpdateScript()) {
try {
const prepareScript = await prepareScriptByCode(
info.code,
script.downloadUrl || script.checkUpdateUrl!,
script.uuid
);
if (checkSilenceUpdate(prepareScript.oldScript!.metadata, prepareScript.script.metadata)) {
logger.info("silence update script");
this.installScript({
script: prepareScript.script,
code: info.code,
upsertBy: source,
});
return;
}
} catch (e) {
logger.error("prepare script failed", Logger.E(e));
}
return;
}
// 打开安装页面
Cache.getInstance().set(CacheKey.scriptInstallInfo(info.uuid), info);
chrome.tabs.create({
url: `/src/install.html?uuid=${info.uuid}`,
});
})
.catch((e) => {
logger.error("fetch script info failed", Logger.E(e));
});
}
async checkScriptUpdate() {
const checkCycle = await this.systemConfig.getCheckScriptUpdateCycle();
if (!checkCycle) {
return;
}
this.scriptDAO.all().then(async (scripts) => {
const checkDisableScript = await this.systemConfig.getUpdateDisableScript();
scripts.forEach(async (script) => {
// 是否检查禁用脚本
if (!checkDisableScript && script.status === SCRIPT_STATUS_DISABLE) {
return;
}
// 检查是否符合
if (script.checktime + checkCycle * 1000 > Date.now()) {
return;
}
this.checkUpdate(script.uuid, "system");
});
});
}
requestCheckUpdate(uuid: string) {
return this.checkUpdate(uuid, "user");
}
init() {
this.listenerScriptInstall();
this.group.on("getInstallInfo", this.getInstallInfo);
this.group.on("install", this.installScript.bind(this));
this.group.on("delete", this.deleteScript.bind(this));
this.group.on("enable", this.enableScript.bind(this));
this.group.on("fetchInfo", this.fetchInfo.bind(this));
this.group.on("updateRunStatus", this.updateRunStatus.bind(this));
this.group.on("getCode", this.getCode.bind(this));
this.group.on("getScriptRunResource", this.buildScriptRunResource.bind(this));
this.group.on("excludeUrl", this.excludeUrl.bind(this));
this.group.on("resetMatch", this.resetMatch.bind(this));
this.group.on("resetExclude", this.resetExclude.bind(this));
this.group.on("requestCheckUpdate", this.requestCheckUpdate.bind(this));
// 定时检查更新, 每10分钟检查一次
chrome.alarms.create("checkScriptUpdate", {
delayInMinutes: 10,
periodInMinutes: 10,
});
}
}