检查更新

This commit is contained in:
王一之 2025-04-16 16:48:58 +08:00
parent 1a531dfad5
commit 44e1449e03
13 changed files with 228 additions and 140 deletions

View File

@ -173,7 +173,7 @@ export function forwardMessage(
} else if (resp !== false) {
return resp;
}
return handler(params, sender);
}
return handler(params, sender);
});
}

View File

@ -53,6 +53,10 @@ export class ScriptClient extends Client {
excludeUrl(uuid: string, url: string, remove: boolean) {
return this.do("excludeUrl", { uuid, url, remove });
}
requestCheckUpdate(uuid: string) {
return this.do("requestCheckUpdate", uuid);
}
}
export class ResourceClient extends Client {
@ -73,6 +77,10 @@ export class ValueClient extends Client {
getScriptValue(script: Script) {
return this.do("getScriptValue", script);
}
setScriptValue(uuid: string, key: string, value: any) {
return this.do("setScriptValue", { uuid, key, value });
}
}
export class RuntimeClient extends Client {

View File

@ -30,12 +30,21 @@ export default class ServiceWorkerManager {
const resource = new ResourceService(this.api.group("resource"), this.mq);
resource.init();
const value = new ValueService(this.api.group("value"), this.sender);
const script = new ScriptService(this.api.group("script"), this.mq, value, resource);
const script = new ScriptService(systemConfig, this.api.group("script"), this.mq, value, resource);
script.init();
const runtime = new RuntimeService(systemConfig, this.api.group("runtime"), this.sender, this.mq, value, script);
runtime.init();
const popup = new PopupService(this.api.group("popup"), this.mq, runtime);
popup.init();
value.init(runtime, popup);
// 定时器处理
chrome.alarms.onAlarm.addListener((alarm) => {
switch (alarm.name) {
case "checkScriptUpdate":
script.checkScriptUpdate();
break;
}
});
}
}

View File

@ -1,11 +1,11 @@
import { fetchScriptInfo } from "@App/pkg/utils/script";
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 { openInCurrentTab, randomString } from "@App/pkg/utils/utils";
import { checkSilenceUpdate, ltever, openInCurrentTab, randomString } from "@App/pkg/utils/utils";
import {
Script,
SCRIPT_RUN_STATUS,
@ -20,6 +20,7 @@ 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;
@ -27,6 +28,7 @@ export class ScriptService {
scriptCodeDAO: ScriptCodeDAO = new ScriptCodeDAO();
constructor(
private systemConfig: SystemConfig,
private group: Group,
private mq: MessageQueue,
private valueService: ValueService,
@ -305,6 +307,117 @@ export class ScriptService {
});
}
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: "" });
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));
});
}
checkScriptUpdate() {
this.scriptDAO.all().then(async (scripts) => {
const checkCycle = await this.systemConfig.getCheckScriptUpdateCycle();
if (!checkCycle) {
return;
}
const check = await this.systemConfig.getUpdateDisableScript();
scripts.forEach(async (script) => {
// 是否检查禁用脚本
if (!check && 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();
@ -317,5 +430,12 @@ export class ScriptService {
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("requestCheckUpdate", this.requestCheckUpdate.bind(this));
// 定时检查更新, 每10分钟检查一次
chrome.alarms.create("checkScriptUpdate", {
delayInMinutes: 10,
periodInMinutes: 10,
});
}
}

View File

@ -2,7 +2,7 @@ import LoggerCore from "@App/app/logger/core";
import Logger from "@App/app/logger/logger";
import { Script, SCRIPT_TYPE_NORMAL, ScriptDAO } from "@App/app/repo/scripts";
import { ValueDAO } from "@App/app/repo/value";
import { Group, MessageSend } from "@Packages/message/server";
import { GetSender, Group, MessageSend } from "@Packages/message/server";
import { RuntimeService } from "./runtime";
import { PopupService } from "./popup";
import { sendMessage } from "@Packages/message/client";
@ -95,9 +95,17 @@ export class ValueService {
return Promise.resolve(true);
}
setScriptValue(data: { uuid: string; key: string; value: any }, sender: GetSender) {
return this.setValue(data.uuid, data.key, data.value, {
runFlag: "user",
tabId: -2,
});
}
init(runtime: RuntimeService, popup: PopupService) {
this.popup = popup;
this.runtime = runtime;
this.group.on("getScriptValue", this.getScriptValue.bind(this));
this.group.on("setScriptValue", this.setScriptValue.bind(this));
}
}

View File

@ -23,6 +23,7 @@
"default_locale": "zh_CN",
"permissions": [
"tabs",
"alarms",
"storage",
"cookies",
"offscreen",

View File

@ -1,18 +1,10 @@
import React, { useEffect, useRef } from "react";
import { useTranslation } from "react-i18next"; // 添加这行导入语句
import { Script, UserConfig } from "@App/app/repo/scripts";
import {
Checkbox,
Form,
FormInstance,
Input,
InputNumber,
Message,
Modal,
Select,
Tabs,
} from "@arco-design/web-react";
import { Checkbox, Form, FormInstance, Input, InputNumber, Message, Modal, Select, Tabs } from "@arco-design/web-react";
import TabPane from "@arco-design/web-react/es/Tabs/tab-pane";
import { ValueClient } from "@App/app/service/service_worker/client";
import { message } from "@App/pages/store/global";
const FormItem = Form.Item;
@ -41,17 +33,13 @@ const UserConfigPanel: React.FC<{
if (formRefs.current[tab]) {
const saveValues = formRefs.current[tab].getFieldsValue();
// 更新value
const valueCtrl = IoC.instance(ValueController) as ValueController;
const valueClient = new ValueClient(message);
Object.keys(saveValues).forEach((key) => {
Object.keys(saveValues[key]).forEach((valueKey) => {
if (saveValues[key][valueKey] === undefined) {
return;
}
valueCtrl.setValue(
script.id,
`${key}.${valueKey}`,
saveValues[key][valueKey]
);
valueClient.setScriptValue(script.uuid, `${key}.${valueKey}`, saveValues[key][valueKey]);
});
});
Message.success(t("save_success")!); // 替换为键值对应的英文文本
@ -73,7 +61,7 @@ const UserConfigPanel: React.FC<{
return (
<TabPane key={itemKey} title={itemKey}>
<Form
key={script.id}
key={script.uuid}
style={{
width: "100%",
}}
@ -85,11 +73,7 @@ const UserConfigPanel: React.FC<{
}}
>
{Object.keys(value).map((key) => (
<FormItem
key={key}
label={value[key].title}
field={`${itemKey}.${key}`}
>
<FormItem key={key} label={value[key].title} field={`${itemKey}.${key}`}>
{() => {
const item = value[key];
let { type } = item;
@ -112,20 +96,9 @@ const UserConfigPanel: React.FC<{
switch (type) {
case "text":
if (item.password) {
return (
<Input.Password
placeholder={item.description}
maxLength={item.max}
/>
);
return <Input.Password placeholder={item.description} maxLength={item.max} />;
}
return (
<Input
placeholder={item.description}
maxLength={item.max}
showWordLimit
/>
);
return <Input placeholder={item.description} maxLength={item.max} showWordLimit />;
case "number":
return (
<InputNumber
@ -136,13 +109,7 @@ const UserConfigPanel: React.FC<{
/>
);
case "checkbox":
return (
<Checkbox
defaultChecked={values[`${itemKey}.${key}`]}
>
{item.description}
</Checkbox>
);
return <Checkbox defaultChecked={values[`${itemKey}.${key}`]}>{item.description}</Checkbox>;
case "select":
case "mult-select":
// eslint-disable-next-line no-case-declarations
@ -159,11 +126,7 @@ const UserConfigPanel: React.FC<{
}
return (
<Select
mode={
item.type === "mult-select"
? "multiple"
: undefined
}
mode={item.type === "mult-select" ? "multiple" : undefined}
placeholder={item.description}
>
{options!.map((option) => (

View File

@ -25,6 +25,7 @@ import {
SCRIPT_STATUS_ENABLE,
SCRIPT_TYPE_BACKGROUND,
SCRIPT_TYPE_NORMAL,
ScriptDAO,
UserConfig,
} from "@App/app/repo/scripts";
import {
@ -68,7 +69,7 @@ import CloudScriptPlan from "@App/pages/components/CloudScriptPlan";
import { useTranslation } from "react-i18next";
import { nextTime, semTime } from "@App/pkg/utils/utils";
import { i18nName } from "@App/locales/locales";
import { getValues, ListHomeRender, ScriptIcons } from "./utils";
import { ListHomeRender, ScriptIcons } from "./utils";
import { useAppDispatch, useAppSelector } from "@App/pages/store/hooks";
import {
requestEnableScript,
@ -79,8 +80,10 @@ import {
sortScript,
requestStopScript,
requestRunScript,
scriptClient,
} from "@App/pages/store/features/script";
import { systemConfig } from "@App/pages/store/global";
import { message, systemConfig } from "@App/pages/store/global";
import { ValueClient } from "@App/app/service/service_worker/client";
type ListType = Script & { loading?: boolean };
@ -399,35 +402,36 @@ function ScriptList() {
cursor: "pointer",
}}
onClick={() => {
// if (!script.checkUpdateUrl) {
// Message.warning(t("update_not_supported")!);
// return;
// }
// Message.info({
// id: "checkupdate",
// content: t("checking_for_updates"),
// });
// scriptCtrl
// .checkUpdate(script.id)
// .then((res) => {
// if (res) {
// Message.warning({
// id: "checkupdate",
// content: t("new_version_available"),
// });
// } else {
// Message.success({
// id: "checkupdate",
// content: t("latest_version"),
// });
// }
// })
// .catch((e) => {
// Message.error({
// id: "checkupdate",
// content: `${t("update_check_failed")}: ${e.message}`,
// });
// });
if (!script.checkUpdateUrl) {
Message.warning(t("update_not_supported")!);
return;
}
Message.info({
id: "checkupdate",
content: t("checking_for_updates"),
});
scriptClient
.requestCheckUpdate(script.uuid)
.then((res) => {
console.log("res", res);
if (res) {
Message.warning({
id: "checkupdate",
content: t("new_version_available"),
});
} else {
Message.success({
id: "checkupdate",
content: t("latest_version"),
});
}
})
.catch((e) => {
Message.error({
id: "checkupdate",
content: `${t("update_check_failed")}: ${e.message}`,
});
});
}}
>
{semTime(new Date(col))}
@ -473,7 +477,7 @@ function ScriptList() {
type="text"
icon={<RiSettings3Fill />}
onClick={() => {
getValues(item).then((newValues) => {
new ValueClient(message).getScriptValue(item).then((newValues) => {
setUserConfig({
userConfig: { ...item.config! },
script: item,
@ -545,16 +549,18 @@ function ScriptList() {
// 设置列和判断是否打开用户配置
useEffect(() => {
if (openUserConfig) {
const script = scriptList.find((item) => item.uuid === openUserConfig);
if (script && script.config) {
getValues(script).then((values) => {
setUserConfig({
script,
userConfig: script.config!,
values: values,
const dao = new ScriptDAO();
dao.get(openUserConfig).then((script) => {
if (script && script.config) {
new ValueClient(message).getScriptValue(script).then((values) => {
setUserConfig({
script,
userConfig: script.config!,
values: values,
});
});
});
}
}
});
}
systemConfig.getScriptListColumnWidth().then((columnWidth) => {
setNewColumns(
@ -697,57 +703,33 @@ function ScriptList() {
type="primary"
size="mini"
onClick={() => {
const ids: number[] = [];
const uuids: string[] = [];
switch (action) {
case "enable":
select.forEach((item) => {
scriptCtrl.enable(item.id).then(() => {
const list = scriptList.map((script) => {
if (script.id === item.id) {
script.status = SCRIPT_STATUS_ENABLE;
}
return script;
});
setScriptList(list);
});
dispatch(requestEnableScript({ uuid: item.uuid, enable: true }));
});
break;
case "disable":
select.forEach((item) => {
scriptCtrl.disable(item.id).then(() => {
const list = scriptList.map((script) => {
if (script.id === item.id) {
script.status = SCRIPT_STATUS_DISABLE;
}
return script;
});
setScriptList(list);
});
dispatch(requestEnableScript({ uuid: item.uuid, enable: false }));
});
break;
case "export":
select.forEach((item) => {
ids.push(item.id);
uuids.push(item.uuid);
});
synchronizeCtrl.backup(ids);
synchronizeCtrl.backup(uuids);
break;
case "delete":
// eslint-disable-next-line no-restricted-globals, no-alert
if (confirm(t("list.confirm_delete")!)) {
select.forEach((item) => {
scriptCtrl.delete(item.id).then(() => {
setScriptList((list) => {
return list.filter((script) => {
return script.id !== item.id;
});
});
});
dispatch(requestDeleteScript(item.uuid));
});
}
break;
// 批量检查更新
case "check_update":
// eslint-disable-next-line no-restricted-globals, no-alert
if (confirm(t("list.confirm_update")!)) {
select.forEach((item, index, array) => {
if (!item.checkUpdateUrl) {
@ -757,8 +739,8 @@ function ScriptList() {
id: "checkupdateStart",
content: t("starting_updates"),
});
scriptCtrl
.checkUpdate(item.id)
scriptClient
.requestCheckUpdate(item.uuid)
.then((res) => {
if (res) {
// 需要更新
@ -878,7 +860,7 @@ function ScriptList() {
newColumns.forEach((column) => {
newWidth[column.key! as string] = column.width as number;
});
systemConfig.scriptListColumnWidth = newWidth;
systemConfig.setScriptListColumnWidth(newWidth);
}}
>
{t("save")}

View File

@ -129,7 +129,7 @@ const emptyScript = async (template: string, hotKeys: any, target?: string) => {
return Promise.resolve({
script,
code: script.code,
code,
active: true,
hotKeys,
isChanged: false,
@ -218,7 +218,7 @@ function ScriptEditor() {
setEditors((prev) => {
for (let i = 0; i < prev.length; i += 1) {
if (prev[i].script.uuid === newScript.uuid) {
prev[i].script.code = newScript.code;
prev[i].script.code = prepareScript.scriptCode;
prev[i].isChanged = false;
prev[i].script.name = newScript.name;
break;

View File

@ -147,10 +147,6 @@ export function ListHomeRender({ script }: { script: Script }) {
);
}
export function getValues(script: Script) {
return Promise.resolve({});
}
export type ScriptIconsProps = {
script: { name: string; metadata: Metadata };
size?: number;

View File

@ -7,6 +7,7 @@ export default function storeSubscribe() {
subscribeScriptRunStatus(messageQueue, (data) => {
store.dispatch(scriptSlice.actions.updateRunStatus(data));
});
subscribeScriptInstall(messageQueue, (message) => {
store.dispatch(upsertScript(message.script));
});

View File

@ -139,7 +139,7 @@ export async function fetchScriptInfo(
return ret;
}
export function copyScript(script: ScriptAndCode, old: Script): ScriptAndCode {
export function copyScript(script: Script, old: Script): Script {
const ret = script;
ret.uuid = old.uuid;
ret.createtime = old.createtime;
@ -206,7 +206,7 @@ export function prepareScriptByCode(
url: string,
uuid?: string,
override?: boolean
): Promise<{ script: ScriptAndCode; oldScript?: ScriptAndCode }> {
): Promise<{ script: Script; oldScript?: Script; oldScriptCode?: string }> {
const dao = new ScriptDAO();
return new Promise((resolve, reject) => {
const metadata = parseMetadata(code);
@ -255,10 +255,9 @@ export function prepareScriptByCode(
} else {
newUUID = uuidv4();
}
let script: ScriptAndCode = {
let script: Script = {
uuid: newUUID,
name: metadata.name[0],
code: code,
author: metadata.author && metadata.author[0],
namespace: metadata.namespace && metadata.namespace[0],
originDomain: domain,
@ -309,7 +308,7 @@ export function prepareScriptByCode(
}
script.checktime = new Date().getTime();
}
resolve({ script, oldScript: old ? Object.assign(old, oldCode) : undefined });
resolve({ script, oldScript: old, oldScriptCode: oldCode?.code });
};
handler();
});

View File

@ -1,3 +1,4 @@
import Logger from "@App/app/logger/logger";
import { Metadata, Script } from "@App/app/repo/scripts";
import { CronTime } from "cron";
import crypto from "crypto-js";
@ -144,12 +145,12 @@ export function parseStorageValue(str: string): unknown {
}
// 对比版本大小
export function ltever(newVersion: string, oldVersion: string) {
export function ltever(newVersion: string, oldVersion: string, logger?: Logger) {
// 先验证符不符合语义化版本规范
try {
return semver.lte(newVersion, oldVersion);
} catch (e) {
console.error(e);
logger?.warn("does not conform to the Semantic Versioning specification", Logger.E(e));
}
const newVer = newVersion.split(".");
const oldVer = oldVersion.split(".");