runtime
Some checks failed
test / Run tests (push) Failing after 17s
build / Build (push) Failing after 24s

This commit is contained in:
2025-01-24 16:49:20 +08:00
parent af15d67cb3
commit 415f00a3d1
33 changed files with 1115 additions and 4118 deletions

View File

@ -1,6 +1,24 @@
import { WindowMessage } from "@Packages/message/window_message";
import { sendMessage } from "../utils";
import { SCRIPT_RUN_STATUS } from "@App/app/repo/scripts";
export function preparationSandbox(msg: WindowMessage) {
return sendMessage(msg, "preparationSandbox");
}
// 代理发送消息到ServiceWorker
export function sendMessageToServiceWorker(msg: WindowMessage, action: string, data?: any) {
return sendMessage(msg, "sendMessageToServiceWorker", { action, data });
}
// 代理连接ServiceWorker
export function connectServiceWorker(msg: WindowMessage) {
return sendMessage(msg, "connectServiceWorker");
}
export function proxyUpdateRunStatus(
msg: WindowMessage,
data: { uuid: string; runStatus: SCRIPT_RUN_STATUS; error?: any; nextruntime?: number }
) {
return sendMessageToServiceWorker(msg, "updateRunStatus", data);
}

View File

@ -5,6 +5,7 @@ import { Logger, LoggerDAO } from "@App/app/repo/logger";
import { WindowMessage } from "@Packages/message/window_message";
import { ExtensionMessage } from "@Packages/message/extension_message";
import { ServiceWorkerClient } from "../service_worker/client";
import { sendMessage } from "@Packages/message/client";
// offscreen环境的管理器
export class OffscreenManager {
@ -31,11 +32,16 @@ export class OffscreenManager {
serviceWorker.preparationOffscreen();
}
sendMessageToServiceWorker(data: { action: string; data: any }) {
return sendMessage(data.action, data.data);
}
initManager() {
// 监听消息
const group = this.api.group("offscreen");
this.windowApi.on("logger", this.logger.bind(this));
this.windowApi.on("preparationSandbox", this.preparationSandbox.bind(this));
this.windowApi.on("sendMessageToServiceWorker", this.sendMessageToServiceWorker.bind(this));
const script = new ScriptService(group.group("script"), this.mq, this.windowMessage, this.broker);
script.init();
}

View File

@ -3,13 +3,23 @@ import Logger from "@App/app/logger/logger";
import { Broker, MessageQueue } from "@Packages/message/message_queue";
import { Group } from "@Packages/message/server";
import { WindowMessage } from "@Packages/message/window_message";
import { ScriptClient, subscribeScriptEnable } from "../service_worker/client";
import { SCRIPT_TYPE_NORMAL } from "@App/app/repo/scripts";
import {
ResourceClient,
ScriptClient,
subscribeScriptEnable,
subscribeScriptInstall,
ValueClient,
} from "../service_worker/client";
import { SCRIPT_STATUS_ENABLE, SCRIPT_TYPE_NORMAL } from "@App/app/repo/scripts";
import { disableScript, enableScript } from "../sandbox/client";
export class ScriptService {
logger: Logger;
scriptClient: ScriptClient = new ScriptClient();
resourceClient: ResourceClient = new ResourceClient();
valueClient: ValueClient = new ValueClient();
constructor(
private group: Group,
private mq: MessageQueue,
@ -21,16 +31,26 @@ export class ScriptService {
async init() {
subscribeScriptEnable(this.broker, async (data) => {
const info = await new ScriptClient().info(data.uuid);
const info = await this.scriptClient.info(data.uuid);
if (info.type === SCRIPT_TYPE_NORMAL) {
return;
}
if (data.enable) {
// 发送给沙盒运行
enableScript(this.windowMessage, info);
// 构造脚本运行资源,发送给沙盒运行
enableScript(this.windowMessage, await this.scriptClient.getScriptRunResource(info));
} else {
// 发送给沙盒停止
disableScript(this.windowMessage, info);
disableScript(this.windowMessage, info.uuid);
}
});
subscribeScriptInstall(this.broker, async (data) => {
// 判断是开启还是关闭
if (data.script.status === SCRIPT_STATUS_ENABLE) {
// 构造脚本运行资源,发送给沙盒运行
enableScript(this.windowMessage, await this.scriptClient.getScriptRunResource(data.script));
} else {
// 发送给沙盒停止
disableScript(this.windowMessage, data.script.uuid);
}
});
}

View File

@ -1,11 +1,11 @@
import { Script } from "@App/app/repo/scripts";
import { ScriptRunResouce } from "@App/app/repo/scripts";
import { WindowMessage } from "@Packages/message/window_message";
import { sendMessage } from "../utils";
export function enableScript(msg: WindowMessage, data: Script) {
export function enableScript(msg: WindowMessage, data: ScriptRunResouce) {
return sendMessage(msg, "enableScript", data);
}
export function disableScript(msg: WindowMessage, data: Script) {
return sendMessage(msg, "disableScript", data);
export function disableScript(msg: WindowMessage, uuid: string) {
return sendMessage(msg, "disableScript", uuid);
}

View File

@ -1,10 +1,18 @@
import LoggerCore from "@App/app/logger/core";
import Logger from "@App/app/logger/logger";
import { Script, SCRIPT_TYPE_BACKGROUND, ScriptRunResouce } from "@App/app/repo/scripts";
import {
SCRIPT_RUN_STATUS_COMPLETE,
SCRIPT_RUN_STATUS_ERROR,
SCRIPT_RUN_STATUS_RUNNING,
SCRIPT_TYPE_BACKGROUND,
ScriptRunResouce,
} from "@App/app/repo/scripts";
import ExecScript from "@App/runtime/content/exec_script";
import { BgExecScriptWarp, CATRetryError } from "@App/runtime/content/exec_warp";
import { Server } from "@Packages/message/server";
import { WindowMessage } from "@Packages/message/window_message";
import { CronJob } from "cron";
import { proxyUpdateRunStatus } from "../offscreen/client";
export class Runtime {
cronJob: Map<string, Array<CronJob>> = new Map();
@ -55,34 +63,219 @@ export class Runtime {
}
}
removeRetryList(scriptId: number) {
removeRetryList(uuid: string) {
for (let i = 0; i < this.retryList.length; i += 1) {
if (this.retryList[i].script.id === scriptId) {
if (this.retryList[i].script.uuid === uuid) {
this.retryList.splice(i, 1);
i -= 1;
}
}
}
enableScript(data: Script) {
// 开启脚本, 判断脚本是后台脚本还是定时脚本
if (data.type === SCRIPT_TYPE_BACKGROUND) {
async enableScript(script: ScriptRunResouce) {
// 开启脚本
// 如果正在运行,先释放
if (this.execScripts.has(script.uuid)) {
await this.disableScript(script.uuid);
}
if (script.type === SCRIPT_TYPE_BACKGROUND) {
// 后台脚本直接运行起来
return this.execScript(script);
} else {
// 定时脚本加入定时任务
return this.crontabScript(script);
}
eval("console.log('hello')");
console.log("enableScript", data);
}
disableScript(data: Script) {
// 关闭脚本, 判断脚本是后台脚本还是定时脚本
if (data.type === SCRIPT_TYPE_BACKGROUND) {
// 后台脚本直接停止
} else {
// 定时脚本停止定时任务
disableScript(uuid: string) {
// 关闭脚本
// 停止定时任务
this.stopCronJob(uuid);
// 移除重试队列
this.removeRetryList(uuid);
// 发送运行状态变更
proxyUpdateRunStatus(this.windowMessage, { uuid, runStatus: SCRIPT_RUN_STATUS_COMPLETE });
// 停止脚本运行
return this.stopScript(uuid);
}
// 执行脚本
async execScript(script: ScriptRunResouce, execOnce?: boolean) {
const logger = this.logger.with({ script: script.uuid, name: script.name });
if (this.execScripts.has(script.uuid)) {
// 释放掉资源
// 暂未实现执行完成后立马释放,会在下一次执行时释放
await this.stopScript(script.uuid);
}
console.log("disableScript", data);
const exec = new BgExecScriptWarp(script);
this.execScripts.set(script.uuid, exec);
proxyUpdateRunStatus(this.windowMessage, { uuid: script.uuid, runStatus: SCRIPT_RUN_STATUS_RUNNING });
// 修改掉脚本掉最后运行时间, 数据库也需要修改
script.lastruntime = new Date().getTime();
const ret = exec.exec();
if (ret instanceof Promise) {
ret
.then((resp) => {
// 发送执行完成消息
proxyUpdateRunStatus(this.windowMessage, { uuid: script.uuid, runStatus: SCRIPT_RUN_STATUS_COMPLETE });
logger.info("exec script complete", {
value: resp,
});
})
.catch((err) => {
// 发送执行完成+错误消息
let errMsg;
let nextruntime = 0;
if (err instanceof CATRetryError) {
// @ts-ignore
errMsg = { error: err.msg };
if (!execOnce) {
// 下一次执行时间
// @ts-ignore
nextruntime = err.time.getTime();
script.nextruntime = nextruntime;
this.joinRetryList(script);
}
} else {
errMsg = Logger.E(err);
}
logger.error("exec script error", errMsg);
proxyUpdateRunStatus(this.windowMessage, {
uuid: script.uuid,
runStatus: SCRIPT_RUN_STATUS_ERROR,
error: errMsg,
nextruntime,
});
// 错误还是抛出,方便排查
throw err;
});
} else {
logger.warn("backscript return not promise");
}
return ret;
}
crontabScript(script: ScriptRunResouce) {
// 执行定时脚本 运行表达式
if (!script.metadata.crontab) {
throw new Error("错误的crontab表达式");
}
// 如果有nextruntime,则加入重试队列
this.joinRetryList(script);
let flag = false;
const cronJobList: Array<CronJob> = [];
script.metadata.crontab.forEach((val) => {
let oncePos = 0;
let crontab = val;
if (crontab.indexOf("once") !== -1) {
const vals = crontab.split(" ");
vals.forEach((item, index) => {
if (item === "once") {
oncePos = index;
}
});
if (vals.length === 5) {
oncePos += 1;
}
crontab = crontab.replace(/once/g, "*");
}
try {
const cron = new CronJob(crontab, this.crontabExec(script, oncePos));
cron.start();
cronJobList.push(cron);
} catch (e) {
flag = true;
this.logger.error(
"create cronjob failed",
{
uuid: script.uuid,
crontab: val,
},
Logger.E(e)
);
}
});
if (cronJobList.length !== script.metadata.crontab.length) {
// 有表达式失败了
cronJobList.forEach((crontab) => {
crontab.stop();
});
} else {
this.cronJob.set(script.uuid, cronJobList);
}
return Promise.resolve(!flag);
}
crontabExec(script: ScriptRunResouce, oncePos: number) {
if (oncePos) {
return () => {
// 没有最后一次执行时间表示之前都没执行过,直接执行
if (!script.lastruntime) {
this.execScript(script);
return;
}
const now = new Date();
const last = new Date(script.lastruntime);
let flag = false;
// 根据once所在的位置去判断执行
switch (oncePos) {
case 1: // 每分钟
flag = last.getMinutes() !== now.getMinutes();
break;
case 2: // 每小时
flag = last.getHours() !== now.getHours();
break;
case 3: // 每天
flag = last.getDay() !== now.getDay();
break;
case 4: // 每月
flag = last.getMonth() !== now.getMonth();
break;
case 5: // 每周
flag = this.getWeek(last) !== this.getWeek(now);
break;
default:
}
if (flag) {
this.execScript(script);
}
};
}
return () => {
this.execScript(script);
};
}
// 获取本周是第几周
getWeek(date: Date) {
const nowDate = new Date(date);
const firstDay = new Date(date);
firstDay.setMonth(0); // 设置1月
firstDay.setDate(1); // 设置1号
const diffDays = Math.ceil((nowDate.getTime() - firstDay.getTime()) / (24 * 60 * 60 * 1000));
const week = Math.ceil(diffDays / 7);
return week === 0 ? 1 : week;
}
// 停止计时器
stopCronJob(uuid: string) {
const list = this.cronJob.get(uuid);
if (list) {
list.forEach((val) => {
val.stop();
});
this.cronJob.delete(uuid);
}
}
stopScript(uuid: string) {
const exec = this.execScripts.get(uuid);
if (!exec) {
return Promise.resolve(false);
}
exec.stop();
this.execScripts.delete(uuid);
return Promise.resolve(true);
}
init() {

View File

@ -1,7 +1,8 @@
import { Script } from "@App/app/repo/scripts";
import { Script, ScriptCode, ScriptRunResouce } from "@App/app/repo/scripts";
import { Client } from "@Packages/message/client";
import { InstallSource } from ".";
import { Broker } from "@Packages/message/message_queue";
import { Resource } from "@App/app/repo/resource";
export class ServiceWorkerClient extends Client {
constructor() {
@ -23,8 +24,8 @@ export class ScriptClient extends Client {
return this.do("getInstallInfo", uuid);
}
install(script: Script, upsertBy: InstallSource = "user") {
return this.do("install", { script, upsertBy });
install(script: Script, code: string, upsertBy: InstallSource = "user") {
return this.do("install", { script, code, upsertBy });
}
delete(uuid: string) {
@ -38,6 +39,34 @@ export class ScriptClient extends Client {
info(uuid: string): Promise<Script> {
return this.do("fetchInfo", uuid);
}
getCode(uuid: string): Promise<ScriptCode | undefined> {
return this.do("getCode", uuid);
}
getScriptRunResource(script: Script): Promise<ScriptRunResouce> {
return this.do("getScriptRunResource", script);
}
}
export class ResourceClient extends Client {
constructor() {
super("serviceWorker/resource");
}
getScriptResources(script: Script): Promise<{ [key: string]: Resource }> {
return this.do("getScriptResources", script);
}
}
export class ValueClient extends Client {
constructor() {
super("serviceWorker/value");
}
getScriptValue(script: Script) {
return this.do("getScriptValue", script);
}
}
export function subscribeScriptInstall(

View File

@ -2,6 +2,9 @@ import { Server } from "@Packages/message/server";
import { MessageQueue } from "@Packages/message/message_queue";
import { ScriptService } from "./script";
import { ExtensionMessage } from "@Packages/message/extension_message";
import { ResourceService } from "./resource";
import { ValueService } from "./value";
import { RuntimeService } from "./runtime";
export type InstallSource = "user" | "system" | "sync" | "subscribe" | "vscode";
@ -19,7 +22,14 @@ export default class ServiceWorkerManager {
// 准备好环境
this.mq.emit("preparationOffscreen", {});
});
const script = new ScriptService(group.group("script"), this.mq);
const resource = new ResourceService(group.group("resource"), this.mq);
resource.init();
const value = new ValueService(group.group("value"), this.mq);
value.init();
const script = new ScriptService(group.group("script"), this.mq, value, resource);
script.init();
const runtime = new RuntimeService(group.group("runtime"), this.mq);
runtime.init();
}
}

View File

@ -0,0 +1,285 @@
import LoggerCore from "@App/app/logger/core";
import crypto from "crypto-js";
import Logger from "@App/app/logger/logger";
import { Resource, ResourceDAO, ResourceHash, ResourceType } from "@App/app/repo/resource";
import { Script } from "@App/app/repo/scripts";
import { MessageQueue } from "@Packages/message/message_queue";
import { Group } from "@Packages/message/server";
import { isText } from "@App/pkg/utils/istextorbinary";
import { blobToBase64 } from "@App/pkg/utils/script";
export class ResourceService {
logger: Logger;
resourceDAO: ResourceDAO = new ResourceDAO();
constructor(
private group: Group,
private mq: MessageQueue
) {
this.logger = LoggerCore.logger().with({ service: "resource" });
}
public async getResource(uuid: string, url: string, type: ResourceType): Promise<Resource | undefined> {
let res = await this.getResourceModel(url);
if (res) {
return Promise.resolve(res);
}
try {
res = await this.addResource(url, uuid, type);
if (res) {
return Promise.resolve(res);
}
} catch (e: any) {
// ignore
this.logger.error("get resource failed", { uuid, url }, Logger.E(e));
}
return Promise.resolve(undefined);
}
public async getScriptResources(script: Script): Promise<{ [key: string]: Resource }> {
return Promise.resolve({
...((await this.getResourceByType(script, "require")) || {}),
...((await this.getResourceByType(script, "require-css")) || {}),
...((await this.getResourceByType(script, "resource")) || {}),
});
}
async getResourceByType(script: Script, type: ResourceType): Promise<{ [key: string]: Resource }> {
if (!script.metadata[type]) {
return Promise.resolve({});
}
const ret: { [key: string]: Resource } = {};
await Promise.allSettled(
script.metadata[type].map(async (u) => {
if (type === "resource") {
const split = u.split(/\s+/);
if (split.length === 2) {
const res = await this.getResource(script.uuid, split[1], "resource");
if (res) {
ret[split[0]] = res;
}
}
} else {
const res = await this.getResource(script.uuid, u, type);
if (res) {
ret[u] = res;
}
}
})
);
return Promise.resolve(ret);
}
// 更新资源
async checkScriptResource(script: Script) {
return Promise.resolve({
...((await this.checkResourceByType(script, "require")) || {}),
...((await this.checkResourceByType(script, "require-css")) || {}),
...((await this.checkResourceByType(script, "resource")) || {}),
});
}
async checkResourceByType(script: Script, type: ResourceType): Promise<{ [key: string]: Resource }> {
if (!script.metadata[type]) {
return Promise.resolve({});
}
const ret: { [key: string]: Resource } = {};
await Promise.allSettled(
script.metadata[type].map(async (u) => {
if (type === "resource") {
const split = u.split(/\s+/);
if (split.length === 2) {
const res = await this.checkResource(script.uuid, split[1], "resource");
if (res) {
ret[split[0]] = res;
}
}
} else {
const res = await this.checkResource(script.uuid, u, type);
if (res) {
ret[u] = res;
}
}
})
);
return Promise.resolve(ret);
}
async checkResource(uuid: string, url: string, type: ResourceType) {
let res = await this.getResourceModel(url);
if (res) {
// 判断1分钟过期
if ((res.updatetime || 0) > new Date().getTime() - 1000 * 60) {
return Promise.resolve(res);
}
}
try {
res = await this.updateResource(url, uuid, type);
if (res) {
return Promise.resolve(res);
}
} catch (e: any) {
// ignore
this.logger.error("check resource failed", { uuid, url }, Logger.E(e));
}
return Promise.resolve(undefined);
}
async updateResource(url: string, uuid: string, type: ResourceType) {
// 重新加载
const u = this.parseUrl(url);
let result = await this.getResourceModel(u.url);
try {
const resource = await this.loadByUrl(u.url, type);
resource.updatetime = new Date().getTime();
if (!result) {
// 资源不存在,保存
resource.createtime = new Date().getTime();
resource.link = { uuid: true };
await this.resourceDAO.save(resource);
result = resource;
this.logger.info("reload new resource success", { url: u.url });
} else {
result.base64 = resource.base64;
result.content = resource.content;
result.contentType = resource.contentType;
result.hash = resource.hash;
result.updatetime = resource.updatetime;
result.link[uuid] = true;
await this.resourceDAO.update(result.url, result);
this.logger.info("reload resource success", {
url: u.url,
});
}
} catch (e) {
this.logger.error("load resource error", { url: u.url }, Logger.E(e));
throw e;
}
return Promise.resolve(result);
}
public async addResource(url: string, uuid: string, type: ResourceType): Promise<Resource> {
const u = this.parseUrl(url);
let result = await this.getResourceModel(u.url);
// 资源不存在,重新加载
if (!result) {
try {
const resource = await this.loadByUrl(u.url, type);
resource.link[uuid] = true;
resource.createtime = new Date().getTime();
resource.updatetime = new Date().getTime();
await this.resourceDAO.save(resource);
result = resource;
this.logger.info("load resource success", { url: u.url });
} catch (e) {
this.logger.error("load resource error", { url: u.url }, Logger.E(e));
throw e;
}
}
return Promise.resolve(result);
}
async getResourceModel(url: string) {
const u = this.parseUrl(url);
const resource = await this.resourceDAO.get(u.url);
if (resource) {
// 校验hash
if (u.hash) {
if (
(u.hash.md5 && u.hash.md5 !== resource.hash.md5) ||
(u.hash.sha1 && u.hash.sha1 !== resource.hash.sha1) ||
(u.hash.sha256 && u.hash.sha256 !== resource.hash.sha256) ||
(u.hash.sha384 && u.hash.sha384 !== resource.hash.sha384) ||
(u.hash.sha512 && u.hash.sha512 !== resource.hash.sha512)
) {
resource.content = `console.warn("ScriptCat: couldn't load resource from URL ${url} due to a SRI error ");`;
}
}
return Promise.resolve(resource);
}
return Promise.resolve(undefined);
}
calculateHash(blob: Blob): Promise<ResourceHash> {
return new Promise((resolve) => {
const reader = new FileReader();
reader.readAsArrayBuffer(blob);
reader.onloadend = () => {
if (!reader.result) {
resolve({
md5: "",
sha1: "",
sha256: "",
sha384: "",
sha512: "",
});
} else {
resolve({
md5: crypto.MD5(<string>reader.result).toString(),
sha1: crypto.SHA1(<string>reader.result).toString(),
sha256: crypto.SHA256(<string>reader.result).toString(),
sha384: crypto.SHA384(<string>reader.result).toString(),
sha512: crypto.SHA512(<string>reader.result).toString(),
});
}
};
});
}
loadByUrl(url: string, type: ResourceType): Promise<Resource> {
const u = this.parseUrl(url);
return fetch(u.url)
.then(async (resp) => {
if (resp.status !== 200) {
throw new Error(`resource response status not 200:${resp.status}`);
}
return {
data: await resp.blob(),
headers: resp.headers,
};
})
.then(async (response) => {
const resource: Resource = {
url: u.url,
content: "",
contentType: (response.headers.get("content-type") || "application/octet-stream").split(";")[0],
hash: await this.calculateHash(<Blob>response.data),
base64: "",
link: {},
type,
createtime: new Date().getTime(),
};
const arrayBuffer = await (<Blob>response.data).arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
if (isText(uint8Array)) {
resource.content = await (<Blob>response.data).text();
}
resource.base64 = (await blobToBase64(<Blob>response.data)) || "";
return resource;
});
}
parseUrl(url: string): {
url: string;
hash?: { [key: string]: string };
} {
const urls = url.split("#");
if (urls.length < 2) {
return { url: urls[0], hash: undefined };
}
const hashs = urls[1].split(/[,;]/);
const hash: { [key: string]: string } = {};
hashs.forEach((val) => {
const kv = val.split("=");
if (kv.length < 2) {
return;
}
hash[kv[0]] = kv[1].toLocaleLowerCase();
});
return { url: urls[0], hash };
}
init() {
this.group.on("getScriptResources", this.getScriptResources.bind(this));
}
}

View File

@ -0,0 +1,56 @@
import { MessageQueue } from "@Packages/message/message_queue";
import { ScriptEnableCallbackValue } from "./client";
import { Group } from "@Packages/message/server";
import { Script, SCRIPT_STATUS_ENABLE, SCRIPT_TYPE_NORMAL, ScriptDAO } from "@App/app/repo/scripts";
export class RuntimeService {
scriptDAO: ScriptDAO = new ScriptDAO();
constructor(
private group: Group,
private mq: MessageQueue
) {}
async init() {
// 监听脚本开启
this.mq.addListener("enableScript", async (data: ScriptEnableCallbackValue) => {
const script = await this.scriptDAO.get(data.uuid);
if (!script) {
return;
}
// 如果是普通脚本, 在service worker中进行注册
// 如果是后台脚本, 在offscreen中进行处理
if (script.type === SCRIPT_TYPE_NORMAL) {
// 注册入页面脚本
if (data.enable) {
this.registryPageScript(script);
} else {
this.unregistryPageScript(script);
}
}
});
// 将开启的脚本发送一次enable消息
const scriptDao = new ScriptDAO();
const list = await scriptDao.all();
list.forEach((script) => {
if (script.status !== SCRIPT_STATUS_ENABLE || script.type !== SCRIPT_TYPE_NORMAL) {
return;
}
this.mq.publish("enableScript", { uuid: script.uuid, enable: true });
});
// 监听offscreen环境初始化, 初始化完成后, 再将后台脚本运行起来
this.mq.addListener("preparationOffscreen", () => {
list.forEach((script) => {
if (script.status !== SCRIPT_STATUS_ENABLE || script.type === SCRIPT_TYPE_NORMAL) {
return;
}
this.mq.publish("enableScript", { uuid: script.uuid, enable: true });
});
});
}
registryPageScript(script: Script) {}
unregistryPageScript(script: Script) {}
}

View File

@ -5,24 +5,34 @@ 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 { openInCurrentTab } from "@App/pkg/utils/utils";
import { openInCurrentTab, randomString } from "@App/pkg/utils/utils";
import {
Script,
SCRIPT_RUN_STATUS,
SCRIPT_STATUS_DISABLE,
SCRIPT_STATUS_ENABLE,
SCRIPT_TYPE_NORMAL,
ScriptCodeDAO,
ScriptDAO,
ScriptRunResouce,
} from "@App/app/repo/scripts";
import { MessageQueue } from "@Packages/message/message_queue";
import { InstallSource } from ".";
import { ScriptEnableCallbackValue } from "./client";
import { ResourceService } from "./resource";
import { ValueService } from "./value";
import { compileScriptCode } from "@App/runtime/content/utils";
export class ScriptService {
logger: Logger;
scriptDAO: ScriptDAO = new ScriptDAO();
scriptCodeDAO: ScriptCodeDAO = new ScriptCodeDAO();
constructor(
private group: Group,
private mq: MessageQueue
private mq: MessageQueue,
private valueService: ValueService,
private resourceService: ResourceService
) {
this.logger = LoggerCore.logger().with({ service: "script" });
}
@ -143,7 +153,7 @@ export class ScriptService {
}
// 安装脚本
async installScript(param: { script: Script; upsertBy: InstallSource }) {
async installScript(param: { script: Script; code: string; upsertBy: InstallSource }) {
param.upsertBy = param.upsertBy || "user";
const { script, upsertBy } = param;
const logger = this.logger.with({
@ -225,6 +235,45 @@ export class ScriptService {
return script;
}
async updateRunStatus(params: { uuid: string; runStatus: SCRIPT_RUN_STATUS; error?: string; nextruntime?: number }) {
await new ScriptDAO().update(params.uuid, {
runStatus: params.runStatus,
lastruntime: new Date().getTime(),
error: params.error,
nextruntime: params.nextruntime,
});
this.mq.publish("updateRunStatus", params);
}
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 = compileScriptCode(ret, code.code);
return Promise.resolve(ret);
}
async init() {
this.listenerScriptInstall();
@ -233,51 +282,8 @@ export class ScriptService {
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.listenScript();
this.group.on("updateRunStatus", this.updateRunStatus.bind(this));
this.group.on("getCode", this.getCode.bind(this));
this.group.on("getScriptRunResource", this.buildScriptRunResource.bind(this));
}
// 监听脚本
async listenScript() {
// 监听脚本开启
this.mq.addListener("enableScript", async (data: ScriptEnableCallbackValue) => {
const script = await new ScriptDAO().findByUUID(data.uuid);
if (!script) {
return;
}
// 如果是普通脚本, 在service worker中进行注册
// 如果是后台脚本, 在offscreen中进行处理
if (script.type === SCRIPT_TYPE_NORMAL) {
// 注册入页面脚本
if (data.enable) {
this.registryPageScript(script);
} else {
this.unregistryPageScript(script);
}
}
});
// 将开启的脚本发送一次enable消息
const scriptDao = new ScriptDAO();
const list = await scriptDao.all();
list.forEach((script) => {
if (script.status !== SCRIPT_STATUS_ENABLE || script.type !== SCRIPT_TYPE_NORMAL) {
return;
}
this.mq.publish("enableScript", { uuid: script.uuid, enable: true });
});
// 监听offscreen环境初始化, 初始化完成后, 再将后台脚本运行起来
this.mq.addListener("preparationOffscreen", () => {
list.forEach((script) => {
if (script.status !== SCRIPT_STATUS_ENABLE || script.type === SCRIPT_TYPE_NORMAL) {
return;
}
this.mq.publish("enableScript", { uuid: script.uuid, enable: true });
});
});
}
registryPageScript(script: Script) {}
unregistryPageScript(script: Script) {}
}

View File

@ -0,0 +1,37 @@
import LoggerCore from "@App/app/logger/core";
import Logger from "@App/app/logger/logger";
import { Script } from "@App/app/repo/scripts";
import { ValueDAO } from "@App/app/repo/value";
import { MessageQueue } from "@Packages/message/message_queue";
import { Group } from "@Packages/message/server";
export class ValueService {
logger: Logger;
valueDAO: ValueDAO = new ValueDAO();
constructor(
private group: Group,
private mq: MessageQueue
) {
this.logger = LoggerCore.logger().with({ service: "value" });
}
storageKey(script: Script): string {
if (script.metadata.storagename) {
return script.metadata.storagename[0];
}
return script.uuid;
}
async getScriptValue(script: Script) {
const ret = await this.valueDAO.get(this.storageKey(script));
if (!ret) {
return {};
}
return Promise.resolve(ret?.data);
}
init() {
this.group.on("getScriptValue", this.getScriptValue.bind(this));
}
}