脚本订阅功能

This commit is contained in:
王一之 2025-04-22 17:42:54 +08:00
parent 44066d9543
commit d7adffcd9f
11 changed files with 406 additions and 84 deletions

View File

@ -9,7 +9,7 @@ export abstract class Repo<T> {
return this.prefix + key; return this.prefix + key;
} }
protected async _save(key: string, val: T):Promise<T> { protected async _save(key: string, val: T): Promise<T> {
return new Promise((resolve) => { return new Promise((resolve) => {
const data = { const data = {
[this.joinKey(key)]: val, [this.joinKey(key)]: val,

View File

@ -12,7 +12,6 @@ export interface SubscribeScript {
} }
export interface Subscribe { export interface Subscribe {
id: number;
url: string; url: string;
name: string; name: string;
code: string; code: string;
@ -31,6 +30,10 @@ export class SubscribeDAO extends Repo<Subscribe> {
} }
public findByUrl(url: string) { public findByUrl(url: string) {
return this.findOne((key, value) => value.url === url); return this.get(url);
}
public save(val: Subscribe) {
return super._save(val.url, val);
} }
} }

View File

@ -1,6 +1,7 @@
import { MessageQueue } from "@Packages/message/message_queue"; import { MessageQueue } from "@Packages/message/message_queue";
import { Script, SCRIPT_RUN_STATUS } from "../repo/scripts"; import { Script, SCRIPT_RUN_STATUS } from "../repo/scripts";
import { InstallSource } from "./service_worker"; import { InstallSource } from "./service_worker";
import { Subscribe } from "../repo/subscribe";
export function subscribeScriptInstall( export function subscribeScriptInstall(
messageQueue: MessageQueue, messageQueue: MessageQueue,
@ -13,6 +14,17 @@ export function subscribeScriptDelete(messageQueue: MessageQueue, callback: (mes
return messageQueue.subscribe("deleteScript", callback); return messageQueue.subscribe("deleteScript", callback);
} }
export function subscribeSubscribeInstall(
messageQueue: MessageQueue,
callback: (message: { subscribe: Subscribe; update: boolean }) => void
) {
return messageQueue.subscribe("installSubscribe", callback);
}
export function publishSubscribeInstall(messageQueue: MessageQueue, message: { subscribe: Subscribe }) {
return messageQueue.publish("installSubscribe", message);
}
export type ScriptEnableCallbackValue = { uuid: string; enable: boolean }; export type ScriptEnableCallbackValue = { uuid: string; enable: boolean };
export function subscribeScriptEnable( export function subscribeScriptEnable(

View File

@ -9,6 +9,7 @@ import { FileSystemType } from "@Packages/filesystem/factory";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import Cache from "@App/app/cache"; import Cache from "@App/app/cache";
import CacheKey from "@App/app/cache_key"; import CacheKey from "@App/app/cache_key";
import { Subscribe } from "@App/app/repo/subscribe";
export class ServiceWorkerClient extends Client { export class ServiceWorkerClient extends Client {
constructor(msg: MessageSend) { constructor(msg: MessageSend) {
@ -185,3 +186,25 @@ export class SynchronizeClient extends Client {
}); });
} }
} }
export class SubscribeClient extends Client {
constructor(msg: MessageSend) {
super(msg, "serviceWorker/subscribe");
}
install(subscribe: Subscribe) {
return this.do("install", { subscribe });
}
delete(url: string) {
return this.do("delete", { url });
}
checkUpdate(url: string) {
return this.do("checkUpdate", { url });
}
enable(url: string, enable: boolean) {
return this.do("enable", { url, enable });
}
}

View File

@ -8,6 +8,7 @@ import { ServiceWorkerMessageSend } from "@Packages/message/window_message";
import { PopupService } from "./popup"; import { PopupService } from "./popup";
import { SystemConfig } from "@App/pkg/config/config"; import { SystemConfig } from "@App/pkg/config/config";
import { SynchronizeService } from "./synchronize"; import { SynchronizeService } from "./synchronize";
import { SubscribeService } from "./subscribe";
export type InstallSource = "user" | "system" | "sync" | "subscribe" | "vscode"; export type InstallSource = "user" | "system" | "sync" | "subscribe" | "vscode";
@ -49,6 +50,8 @@ export default class ServiceWorkerManager {
systemConfig systemConfig
); );
synchronize.init(); synchronize.init();
const subscribe = new SubscribeService(systemConfig, this.api.group("subscribe"), this.mq, script);
subscribe.init();
// 定时器处理 // 定时器处理
chrome.alarms.onAlarm.addListener((alarm) => { chrome.alarms.onAlarm.addListener((alarm) => {
@ -63,6 +66,10 @@ export default class ServiceWorkerManager {
synchronize.syncOnce(fs); synchronize.syncOnce(fs);
}); });
}); });
break;
case "checkSubscribeUpdate":
subscribe.checkSubscribeUpdate();
break;
} }
}); });

View File

@ -59,7 +59,7 @@ export class ScriptService {
// 读取脚本url内容, 进行安装 // 读取脚本url内容, 进行安装
const logger = this.logger.with({ url: targetUrl }); const logger = this.logger.with({ url: targetUrl });
logger.debug("install script"); logger.debug("install script");
this.openInstallPageByUrl(targetUrl).catch((e) => { this.openInstallPageByUrl(targetUrl, "user").catch((e) => {
logger.error("install script error", Logger.E(e)); logger.error("install script error", Logger.E(e));
// 如果打开失败, 则重定向到安装页 // 如果打开失败, 则重定向到安装页
chrome.scripting.executeScript({ chrome.scripting.executeScript({
@ -135,9 +135,9 @@ export class ScriptService {
); );
} }
public openInstallPageByUrl(url: string) { public openInstallPageByUrl(url: string, source: InstallSource) {
const uuid = uuidv4(); const uuid = uuidv4();
return fetchScriptInfo(url, "user", false, uuidv4()).then((info) => { return fetchScriptInfo(url, source, false, uuidv4()).then((info) => {
Cache.getInstance().set(CacheKey.scriptInstallInfo(uuid), info); Cache.getInstance().set(CacheKey.scriptInstallInfo(uuid), info);
setTimeout(() => { setTimeout(() => {
// 清理缓存 // 清理缓存
@ -147,6 +147,19 @@ export class ScriptService {
}); });
} }
// 直接通过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) { getInstallInfo(uuid: string) {
return Cache.getInstance().get(CacheKey.scriptInstallInfo(uuid)); return Cache.getInstance().get(CacheKey.scriptInstallInfo(uuid));
@ -330,7 +343,7 @@ export class ScriptService {
} }
const newVersion = metadata.version && metadata.version[0]; const newVersion = metadata.version && metadata.version[0];
if (!newVersion) { if (!newVersion) {
logger.error("parse version failed", { version: "" }); logger.error("parse version failed", { version: metadata.version });
return Promise.resolve(false); return Promise.resolve(false);
} }
let oldVersion = script.metadata.version && script.metadata.version[0]; let oldVersion = script.metadata.version && script.metadata.version[0];
@ -393,16 +406,16 @@ export class ScriptService {
}); });
} }
checkScriptUpdate() { async checkScriptUpdate() {
const checkCycle = await this.systemConfig.getCheckScriptUpdateCycle();
if (!checkCycle) {
return;
}
this.scriptDAO.all().then(async (scripts) => { this.scriptDAO.all().then(async (scripts) => {
const checkCycle = await this.systemConfig.getCheckScriptUpdateCycle(); const checkDisableScript = await this.systemConfig.getUpdateDisableScript();
if (!checkCycle) {
return;
}
const check = await this.systemConfig.getUpdateDisableScript();
scripts.forEach(async (script) => { scripts.forEach(async (script) => {
// 是否检查禁用脚本 // 是否检查禁用脚本
if (!check && script.status === SCRIPT_STATUS_DISABLE) { if (!checkDisableScript && script.status === SCRIPT_STATUS_DISABLE) {
return; return;
} }
// 检查是否符合 // 检查是否符合

View File

@ -0,0 +1,280 @@
import LoggerCore from "@App/app/logger/core";
import Logger from "@App/app/logger/logger";
import { ScriptDAO } from "@App/app/repo/scripts";
import {
Subscribe,
SUBSCRIBE_STATUS_DISABLE,
SUBSCRIBE_STATUS_ENABLE,
SubscribeDAO,
SubscribeScript,
} from "@App/app/repo/subscribe";
import { SystemConfig } from "@App/pkg/config/config";
import { MessageQueue } from "@Packages/message/message_queue";
import { Group } from "@Packages/message/server";
import { InstallSource } from ".";
import { publishSubscribeInstall, subscribeSubscribeInstall } from "../queue";
import { ScriptService } from "./script";
import { checkSilenceUpdate, InfoNotification, ltever } from "@App/pkg/utils/utils";
import { fetchScriptInfo, prepareSubscribeByCode, ScriptInfo } from "@App/pkg/utils/script";
import Cache from "@App/app/cache";
import CacheKey from "@App/app/cache_key";
export class SubscribeService {
logger: Logger;
subscribeDAO = new SubscribeDAO();
scriptDAO = new ScriptDAO();
constructor(
private systemConfig: SystemConfig,
private group: Group,
private mq: MessageQueue,
private scriptService: ScriptService
) {
this.logger = LoggerCore.logger().with({ service: "subscribe" });
}
async install(param: { subscribe: Subscribe }) {
const logger = this.logger.with({
subscribeUrl: param.subscribe.url,
name: param.subscribe.name,
});
try {
await this.subscribeDAO.save(param.subscribe);
logger.info("upsert subscribe success");
publishSubscribeInstall(this.mq, {
subscribe: param.subscribe,
});
return Promise.resolve(param.subscribe.url);
} catch (e) {
logger.error("upsert subscribe error", Logger.E(e));
return Promise.reject(e);
}
}
async delete(param: { url: string }) {
const logger = this.logger.with({
subscribeUrl: param.url,
});
const subscribe = await this.subscribeDAO.get(param.url);
if (!subscribe) {
logger.warn("subscribe not found");
return Promise.resolve(false);
}
try {
// 删除相关脚本
const scripts = await this.scriptDAO.find((_, value) => {
return value.subscribeUrl === param.url;
});
scripts.forEach((script) => {
this.scriptService.deleteScript(script.uuid);
});
// 删除订阅
await this.subscribeDAO.delete(param.url);
logger.info("delete subscribe success");
return Promise.resolve(true);
} catch (e) {
logger.error("uninstall subscribe error", Logger.E(e));
return Promise.reject(e);
}
}
// 更新订阅的脚本
async upsertScript(subscribe: Subscribe) {
const logger = this.logger.with({
url: subscribe.url,
name: subscribe.name,
});
// 对比脚本是否有变化
const addScript: string[] = [];
const removeScript: SubscribeScript[] = [];
const scriptUrl = subscribe.metadata.scripturl || [];
const scripts = Object.keys(subscribe.scripts);
scriptUrl.forEach((url) => {
// 不存在于已安装的脚本中, 则添加
if (!scripts.includes(url)) {
addScript.push(url);
}
});
scripts.forEach((url) => {
// 不存在于订阅的脚本中, 则删除
if (!scriptUrl.includes(url)) {
removeScript.push(subscribe.scripts[url]);
}
});
const notification: string[][] = [[], []];
const result: Promise<any>[] = [];
// 添加脚本
addScript.forEach((url) => {
result.push(
(async () => {
const script = await this.scriptService.installByUrl(url, "subscribe", subscribe.url);
subscribe.scripts[url] = {
url,
uuid: script.uuid,
};
notification[0].push(script.name);
return Promise.resolve(true);
})().catch((e) => {
logger.error("install script failed", Logger.E(e));
return Promise.resolve(false);
})
);
});
// 删除脚本
removeScript.forEach((item) => {
// 通过uuid查询脚本id
result.push(
(async () => {
const script = await this.scriptDAO.findByUUID(item.uuid);
if (script) {
notification[1].push(script.name);
// 删除脚本
this.scriptService.deleteScript(script.uuid);
}
return Promise.resolve(true);
})().catch((e) => {
logger.error("delete script failed", Logger.E(e));
return Promise.resolve(false);
})
);
});
await Promise.allSettled(result);
await this.subscribeDAO.update(subscribe.url, subscribe);
InfoNotification("订阅更新", `安装了:${notification[0].join(",")}\n删除了:${notification[1].join("\n")}`);
logger.info("subscribe update", {
install: notification[0],
update: notification[1],
});
return Promise.resolve(true);
}
// 检查更新
async checkUpdate(url: string, source: InstallSource) {
const subscribe = await this.subscribeDAO.get(url);
if (!subscribe) {
return Promise.resolve(false);
}
const logger = this.logger.with({
url: subscribe.url,
name: subscribe.name,
});
await this.subscribeDAO.update(url, { checktime: new Date().getTime() });
try {
const info = await fetchScriptInfo(subscribe.url, source, false, subscribe.url);
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 = subscribe.metadata.version && subscribe.metadata.version[0];
if (!oldVersion) {
oldVersion = "0.0.0";
}
// 对比版本大小
if (ltever(newVersion, oldVersion, logger)) {
return Promise.resolve(false);
}
// 进行更新
this.openUpdatePage(info);
return Promise.resolve(true);
} catch (e) {
logger.error("check update failed", Logger.E(e));
return Promise.resolve(false);
}
}
async openUpdatePage(info: ScriptInfo) {
const logger = this.logger.with({
url: info.url,
});
// 是否静默更新
const silenceUpdate = await this.systemConfig.getSilenceUpdateScript();
if (silenceUpdate) {
try {
const newSubscribe = await prepareSubscribeByCode(info.code, info.url);
if (checkSilenceUpdate(newSubscribe.oldSubscribe!.metadata, newSubscribe.subscribe.metadata)) {
logger.info("silence update subscribe");
this.install({
subscribe: newSubscribe.subscribe,
});
return;
}
} catch (e) {
logger.error("prepare script failed", Logger.E(e));
}
}
Cache.getInstance().set(CacheKey.scriptInstallInfo(info.uuid), info);
chrome.tabs.create({
url: `/src/install.html?uuid=${info.uuid}`,
});
}
async checkSubscribeUpdate() {
const checkCycle = await this.systemConfig.getCheckScriptUpdateCycle();
if (!checkCycle) {
return;
}
this.logger.debug("start check update");
const checkDisable = await this.systemConfig.getUpdateDisableScript();
const list = await this.subscribeDAO.find((_, value) => {
return value.checktime + checkCycle * 1000 < Date.now();
});
list.forEach((subscribe) => {
if (!checkDisable && subscribe.status === SUBSCRIBE_STATUS_ENABLE) {
return;
}
this.checkUpdate(subscribe.url, "system");
});
}
requestCheckUpdate(url: string) {
return this.checkUpdate(url, "user");
}
enable(param: { url: string; enable: boolean }) {
const logger = this.logger.with({
url: param.url,
});
return this.subscribeDAO
.update(param.url, {
status: param.enable ? SUBSCRIBE_STATUS_ENABLE : SUBSCRIBE_STATUS_DISABLE,
})
.then(() => {
logger.info("enable subscribe success");
return Promise.resolve(true);
})
.catch((e) => {
logger.error("enable subscribe error", Logger.E(e));
return Promise.reject(e);
});
}
init() {
this.group.on("install", this.install.bind(this));
this.group.on("delete", this.delete.bind(this));
this.group.on("checkUpdate", this.requestCheckUpdate.bind(this));
this.group.on("enable", this.enable.bind(this));
subscribeSubscribeInstall(this.mq, (message) => {
this.upsertScript(message.subscribe);
});
// 定时检查更新, 每10分钟检查一次
chrome.alarms.create("checkSubscribeUpdate", {
delayInMinutes: 10,
periodInMinutes: 10,
});
}
}

View File

@ -7,7 +7,7 @@ import { i18nDescription, i18nName } from "@App/locales/locales";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { prepareScriptByCode, prepareSubscribeByCode, ScriptInfo } from "@App/pkg/utils/script"; import { prepareScriptByCode, prepareSubscribeByCode, ScriptInfo } from "@App/pkg/utils/script";
import { nextTime } from "@App/pkg/utils/utils"; import { nextTime } from "@App/pkg/utils/utils";
import { scriptClient } from "../store/features/script"; import { scriptClient, subscribeClient } from "../store/features/script";
type Permission = { label: string; color?: string; value: string[] }[]; type Permission = { label: string; color?: string; value: string[] }[];
@ -253,18 +253,18 @@ function App() {
return; return;
} }
if (scriptInfo?.userSubscribe) { if (scriptInfo?.userSubscribe) {
// subscribeCtrl subscribeClient
// .upsert(upsertScript as Subscribe) .install(upsertScript as Subscribe)
// .then(() => { .then(() => {
// Message.success(t("subscribe_success")!); Message.success(t("subscribe_success")!);
// setBtnText(t("subscribe_success")!); setBtnText(t("subscribe_success")!);
// setTimeout(() => { setTimeout(() => {
// closeWindow(); closeWindow();
// }, 200); }, 500);
// }) })
// .catch((e) => { .catch((e) => {
// Message.error(`${t("subscribe_failed")}: ${e}`); Message.error(`${t("subscribe_failed")}: ${e}`);
// }); });
return; return;
} }
scriptClient scriptClient

View File

@ -1,47 +1,27 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import Text from "@arco-design/web-react/es/Typography/text"; import Text from "@arco-design/web-react/es/Typography/text";
import { import { Button, Card, Input, Message, Popconfirm, Switch, Table, Tag, Tooltip } from "@arco-design/web-react";
Button, import { Subscribe, SUBSCRIBE_STATUS_DISABLE, SUBSCRIBE_STATUS_ENABLE, SubscribeDAO } from "@App/app/repo/subscribe";
Card,
Input,
Message,
Popconfirm,
Switch,
Table,
Tag,
Tooltip,
} from "@arco-design/web-react";
import {
Subscribe,
SUBSCRIBE_STATUS_DISABLE,
SUBSCRIBE_STATUS_ENABLE,
SubscribeDAO,
} from "@App/app/repo/subscribe";
import { ColumnProps } from "@arco-design/web-react/es/Table"; import { ColumnProps } from "@arco-design/web-react/es/Table";
import { IconSearch, IconUserAdd } from "@arco-design/web-react/icon"; import { IconSearch, IconUserAdd } from "@arco-design/web-react/icon";
import { RefInputType } from "@arco-design/web-react/es/Input/interface"; import { RefInputType } from "@arco-design/web-react/es/Input/interface";
import { semTime } from "@App/pkg/utils/utils"; import { semTime } from "@App/pkg/utils/utils";
import { RiDeleteBin5Fill } from "react-icons/ri"; import { RiDeleteBin5Fill } from "react-icons/ri";
import { useTranslation } from "react-i18next"; // 添加了 react-i18next 的引用 import { useTranslation } from "react-i18next"; // 添加了 react-i18next 的引用
import { subscribeClient } from "@App/pages/store/features/script";
type ListType = Subscribe & { loading?: boolean }; type ListType = Subscribe & { loading?: boolean };
function SubscribeList() { function SubscribeList() {
const dao = new SubscribeDAO(); const dao = new SubscribeDAO();
const subscribeCtrl = IoC.instance(
SubscribeController
) as SubscribeController;
const [list, setList] = useState<ListType[]>([]); const [list, setList] = useState<ListType[]>([]);
const inputRef = useRef<RefInputType>(null); const inputRef = useRef<RefInputType>(null);
const { t } = useTranslation(); // 使用 useTranslation hook const { t } = useTranslation(); // 使用 useTranslation hook
useEffect(() => { useEffect(() => {
dao.table dao.all().then((subscribes) => {
.orderBy("id") setList(subscribes);
.toArray() });
.then((subscribes) => {
setList(subscribes);
});
}, []); }, []);
const columns: ColumnProps[] = [ const columns: ColumnProps[] = [
@ -50,7 +30,13 @@ function SubscribeList() {
dataIndex: "id", dataIndex: "id",
width: 70, width: 70,
key: "#", key: "#",
sorter: (a, b) => a.id - b.id, sorter: (a: Subscribe, b) => a.createtime - b.createtime,
render(col) {
if (col < 0) {
return "-";
}
return col + 1;
},
}, },
{ {
title: t("enable"), title: t("enable"),
@ -79,22 +65,18 @@ function SubscribeList() {
onChange={(checked) => { onChange={(checked) => {
list[index].loading = true; list[index].loading = true;
setList([...list]); setList([...list]);
let p: Promise<any>; subscribeClient
if (checked) { .enable(item.url, checked)
p = subscribeCtrl.enable(item.id).then(() => { .then(() => {
list[index].status = SUBSCRIBE_STATUS_ENABLE; list[index].status = checked ? SUBSCRIBE_STATUS_ENABLE : SUBSCRIBE_STATUS_DISABLE;
})
.catch((err) => {
Message.error(err);
})
.finally(() => {
list[index].loading = false;
setList([...list]);
}); });
} else {
p = subscribeCtrl.disable(item.id).then(() => {
list[index].status = SUBSCRIBE_STATUS_DISABLE;
});
}
p.catch((err) => {
Message.error(err);
}).finally(() => {
list[index].loading = false;
setList([...list]);
});
}} }}
/> />
); );
@ -169,14 +151,7 @@ function SubscribeList() {
return <div />; return <div />;
} }
return (item.metadata.connect as string[]).map((val) => { return (item.metadata.connect as string[]).map((val) => {
return ( return <img src={`https://${val}/favicon.ico`} alt={val} height={16} width={16} />;
<img
src={`https://${val}/favicon.ico`}
alt={val}
height={16}
width={16}
/>
);
}); });
}, },
}, },
@ -227,8 +202,8 @@ function SubscribeList() {
id: "checkupdate", id: "checkupdate",
content: t("checking_for_updates"), content: t("checking_for_updates"),
}); });
subscribeCtrl subscribeClient
.checkUpdate(subscribe.id) .checkUpdate(subscribe.url)
.then((res) => { .then((res) => {
if (res) { if (res) {
Message.warning({ Message.warning({
@ -267,10 +242,15 @@ function SubscribeList() {
title={t("confirm_delete_subscription")} title={t("confirm_delete_subscription")}
icon={<RiDeleteBin5Fill />} icon={<RiDeleteBin5Fill />}
onOk={() => { onOk={() => {
setList(list.filter((val) => val.id !== item.id)); subscribeClient
subscribeCtrl.delete(item.id).catch((e) => { .delete(item.url)
Message.error(`${t("delete_failed")}: ${e}`); .then(() => {
}); setList(list.filter((val) => val.url !== item.url));
Message.success(t("delete_success"));
})
.catch((e) => {
Message.error(`${t("delete_failed")}: ${e}`);
});
}} }}
> >
<Button <Button

View File

@ -13,11 +13,13 @@ import {
PopupClient, PopupClient,
RuntimeClient, RuntimeClient,
ScriptClient, ScriptClient,
SubscribeClient,
ValueClient, ValueClient,
} from "@App/app/service/service_worker/client"; } from "@App/app/service/service_worker/client";
import { message } from "../global"; import { message } from "../global";
export const scriptClient = new ScriptClient(message); export const scriptClient = new ScriptClient(message);
export const subscribeClient = new SubscribeClient(message);
export const runtimeClient = new RuntimeClient(message); export const runtimeClient = new RuntimeClient(message);
export const popupClient = new PopupClient(message); export const popupClient = new PopupClient(message);
export const permissionClient = new PermissionClient(message); export const permissionClient = new PermissionClient(message);

View File

@ -65,7 +65,9 @@ export class SystemConfig {
public set(key: string, val: any) { public set(key: string, val: any) {
this.cache.set(key, val); this.cache.set(key, val);
this.storage.set(key, val); this.storage.set(key, val).then(() => {
console.log(chrome.runtime.lastError, val);
});
// 发送消息通知更新 // 发送消息通知更新
this.mq.publish(SystamConfigChange, { this.mq.publish(SystamConfigChange, {
key, key,