优化细节

This commit is contained in:
王一之 2025-04-27 18:02:57 +08:00
parent e1a890a400
commit a26f1c5014
13 changed files with 234 additions and 118 deletions

View File

@ -1,6 +1,6 @@
{
"name": "scriptcat",
"version": "0.17.0-alpha.2",
"version": "0.17.0-alpha.4",
"description": "脚本猫,一个可以执行用户脚本的浏览器扩展,万物皆可脚本化,让你的浏览器可以做更多的事情!",
"author": "CodFrm",
"license": "GPLv3",

View File

@ -113,7 +113,6 @@ function renameField() {
if (subscribe.length) {
await Promise.all(
subscribe.map((s: Subscribe) => {
console.log("1234", s);
const { url, name, code, author, scripts, metadata, status, createtime, updatetime, checktime } = s;
return subscribeDAO.save({
url,

View File

@ -23,6 +23,7 @@ export default class ContentRuntime {
// 转发给inject
return sendMessage(this.msg, "inject/runtime/valueUpdate", data);
});
forwardMessage("serviceWorker", "script/isInstalled", this.server, this.extSend);
forwardMessage(
"serviceWorker",
"runtime/gmApi",

View File

@ -4,6 +4,8 @@ import ExecScript, { ValueUpdateData } from "./exec_script";
import { addStyle, ScriptFunc } from "./utils";
import { getStorageName } from "@App/pkg/utils/utils";
import { EmitEventRequest } from "../service_worker/runtime";
import { ExternalWhitelist } from "@App/app/const";
import { sendMessage } from "@Packages/message/client";
export class InjectRuntime {
execList: ExecScript[] = [];
@ -44,6 +46,40 @@ export class InjectRuntime {
val.valueUpdate(data);
});
});
// 注入允许外部调用
this.externalMessage();
}
externalMessage() {
// 对外接口白名单
let msg = this.msg;
for (let i = 0; i < ExternalWhitelist.length; i += 1) {
if (window.location.host.endsWith(ExternalWhitelist[i])) {
// 注入
(<{ external: any }>(<unknown>window)).external = window.external || {};
(<
{
external: {
Scriptcat: {
isInstalled: (name: string, namespace: string, callback: any) => void;
};
};
}
>(<unknown>window)).external.Scriptcat = {
async isInstalled(name: string, namespace: string, callback: any) {
const resp = await sendMessage(msg, "content/script/isInstalled", {
name,
namespace,
});
callback(resp);
},
};
(<{ external: { Tampermonkey: any } }>(<unknown>window)).external.Tampermonkey = (<
{ external: { Scriptcat: any } }
>(<unknown>window)).external.Scriptcat;
break;
}
}
}
execScript(script: ScriptRunResouce, scriptFunc: ScriptFunc) {

View File

@ -9,6 +9,8 @@ import { PopupService } from "./popup";
import { SystemConfig } from "@App/pkg/config/config";
import { SynchronizeService } from "./synchronize";
import { SubscribeService } from "./subscribe";
import { ExtServer, ExtVersion } from "@App/app/const";
import { systemConfig } from "@App/pages/store/global";
export type InstallSource = "user" | "system" | "sync" | "subscribe" | "vscode";
@ -78,8 +80,17 @@ export default class ServiceWorkerManager {
case "checkSubscribeUpdate":
subscribe.checkSubscribeUpdate();
break;
case "checkUpdate":
// 检查扩展更新
this.checkUpdate();
break;
}
});
// 8小时检查一次扩展更新
chrome.alarms.create("checkUpdate", {
delayInMinutes: 0,
periodInMinutes: 8 * 60,
});
// 监听配置变化
this.mq.subscribe("systemConfigChange", (msg) => {
@ -96,4 +107,18 @@ export default class ServiceWorkerManager {
synchronize.cloudSyncConfigChange(config);
});
}
checkUpdate() {
fetch(`${ExtServer}api/v1/system/version?version=${ExtVersion}`)
.then((resp) => resp.json())
.then((resp: { data: { notice: string; version: string } }) => {
systemConfig.getCheckUpdate().then((items) => {
if (items.notice !== resp.data.notice) {
systemConfig.setCheckUpdate(Object.assign(resp.data, { isRead: false }));
} else {
systemConfig.setCheckUpdate(Object.assign(resp.data, { isRead: items.isRead }));
}
});
});
}
}

View File

@ -15,7 +15,7 @@ import { subscribeScriptDelete, subscribeScriptEnable, subscribeScriptInstall }
import { ScriptService } from "./script";
import { runScript, stopScript } from "../offscreen/client";
import { getRunAt } from "./utils";
import { randomString } from "@App/pkg/utils/utils";
import { isUserScriptsAvailable, randomString } from "@App/pkg/utils/utils";
import Cache from "@App/app/cache";
import { dealPatternMatches, UrlMatch } from "@App/pkg/utils/match";
import { ExtensionContentMessageSend } from "@Packages/message/extension_message";
@ -70,6 +70,11 @@ export class RuntimeService {
this.group.on("runScript", this.runScript.bind(this));
this.group.on("pageLoad", this.pageLoad.bind(this));
// 检查是否开启了开发者模式
if(!isUserScriptsAvailable()){
// 未开启加上警告引导
}
// 读取inject.js注入页面
this.registerInjectScript();
// 监听脚本开启

View File

@ -21,6 +21,7 @@ import { ResourceService } from "./resource";
import { ValueService } from "./value";
import { compileScriptCode } from "../content/utils";
import { SystemConfig } from "@App/pkg/config/config";
import i18n from "@App/locales/locales";
export class ScriptService {
logger: Logger;
@ -59,50 +60,59 @@ export class ScriptService {
// 读取脚本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,
this.openInstallPageByUrl(targetUrl, "user")
.catch((e) => {
logger.error("install script error", Logger.E(e));
// 不再重定向当前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],
},
},
condition: {
regexFilter: targetUrl,
resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME],
requestMethods: [chrome.declarativeNetRequest.RequestMethod.GET],
},
},
],
},
() => {
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError);
],
},
() => {
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError);
}
}
}
);
});
);
})
.finally(() => {
// 回退到到安装页
chrome.scripting.executeScript({
target: { tabId: req.tabId },
func: function () {
history.back();
},
});
});
},
{
urls: [
"https://docs.scriptcat.org/docs/script_installation",
"https://docs.scriptcat.org/docs/script_installation/",
"https://docs.scriptcat.org/en/docs/script_installation/",
"https://www.tampermonkey.net/script_installation.php",
],
types: ["main_frame"],
}
);
// 获取i18n
let localePath = "";
if (i18n.language !== "zh-CN") {
localePath = `/en`;
}
// 重定向到脚本安装页
chrome.declarativeNetRequest.updateDynamicRules(
{
@ -114,7 +124,7 @@ export class ScriptService {
action: {
type: chrome.declarativeNetRequest.RuleActionType.REDIRECT,
redirect: {
regexSubstitution: "https://docs.scriptcat.org/docs/script_installation#url=\\0",
regexSubstitution: `https://docs.scriptcat.org${localePath}/docs/script_installation/#url=\\0`,
},
},
condition: {
@ -479,6 +489,15 @@ export class ScriptService {
return this.checkUpdate(uuid, "user");
}
isInstalled({ name, namespace }: { name: string; namespace: string }) {
return this.scriptDAO.findByNameAndNamespace(name, namespace).then((script) => {
if (script) {
return { installed: true, version: script.metadata.version && script.metadata.version[0] };
}
return { installed: false };
});
}
init() {
this.listenerScriptInstall();
@ -494,6 +513,7 @@ export class ScriptService {
this.group.on("resetMatch", this.resetMatch.bind(this));
this.group.on("resetExclude", this.resetExclude.bind(this));
this.group.on("requestCheckUpdate", this.requestCheckUpdate.bind(this));
this.group.on("isInstalled", this.isInstalled.bind(this));
// 定时检查更新, 每10分钟检查一次
chrome.alarms.create("checkScriptUpdate", {

View File

@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "__MSG_scriptcat__",
"version": "0.17.0.1003",
"version": "0.17.0.1005",
"author": "CodFrm",
"description": "__MSG_scriptcat_description__",
"options_ui": {

View File

@ -39,11 +39,9 @@ const CodeEditor: React.ForwardRefRenderFunction<{ editor: editor.IStandaloneCod
}, []);
useEffect(() => {
console.log("1231", code);
if (diffCode === undefined || code === undefined || !div.current) {
return () => {};
}
console.log("1232");
let edit: editor.IStandaloneDiffEditor | editor.IStandaloneCodeEditor;
const inlineDiv = document.getElementById(id) as HTMLDivElement;
// @ts-ignore

View File

@ -1,4 +1,4 @@
import { Script, ScriptAndCode, ScriptCodeDAO, ScriptDAO } from "@App/app/repo/scripts";
import { Script, SCRIPT_TYPE_NORMAL, ScriptAndCode, ScriptCodeDAO, ScriptDAO } from "@App/app/repo/scripts";
import CodeEditor from "@App/pages/components/CodeEditor";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
@ -16,7 +16,7 @@ import { prepareScriptByCode } from "@App/pkg/utils/script";
import ScriptStorage from "@App/pages/components/ScriptStorage";
import ScriptResource from "@App/pages/components/ScriptResource";
import ScriptSetting from "@App/pages/components/ScriptSetting";
import { scriptClient } from "@App/pages/store/features/script";
import { runtimeClient, scriptClient } from "@App/pages/store/features/script";
import { i18nName } from "@App/locales/locales";
import { useTranslation } from "react-i18next";
@ -188,57 +188,58 @@ function ScriptEditor() {
const save = (script: Script, e: editor.IStandaloneCodeEditor): Promise<Script> => {
// 解析code生成新的script并更新
return new Promise(() => {
prepareScriptByCode(e.getValue(), script.origin || "", script.uuid)
.then((prepareScript) => {
const newScript = prepareScript.script;
if (!newScript.name) {
Message.warning(t("script_name_cannot_be_set_to_empty"));
return;
}
scriptClient.install(newScript, e.getValue()).then(
(update) => {
if (!update) {
Message.success("新建成功,请注意后台脚本不会默认开启");
// 保存的时候如何左侧没有脚本即新建
setScriptList((prev) => {
setSelectSciptButtonAndTab(newScript.uuid);
return [newScript, ...prev];
});
} else {
setScriptList((prev) => {
// eslint-disable-next-line no-shadow, array-callback-return
prev.map((script: Script) => {
if (script.uuid === newScript.uuid) {
script.name = newScript.name;
}
});
return [...prev];
});
Message.success("保存成功");
}
setEditors((prev) => {
for (let i = 0; i < prev.length; i += 1) {
if (prev[i].script.uuid === newScript.uuid) {
prev[i].code = e.getValue();
prev[i].isChanged = false;
prev[i].script.name = newScript.name;
break;
return prepareScriptByCode(e.getValue(), script.origin || "", script.uuid)
.then((prepareScript) => {
const newScript = prepareScript.script;
if (!newScript.name) {
Message.warning(t("script_name_cannot_be_set_to_empty"));
return Promise.reject(new Error("script name cannot be empty"));
}
return scriptClient
.install(newScript, e.getValue())
.then((update): Script => {
if (!update) {
Message.success("新建成功,请注意后台脚本不会默认开启");
// 保存的时候如何左侧没有脚本即新建
setScriptList((prev) => {
setSelectSciptButtonAndTab(newScript.uuid);
return [newScript, ...prev];
});
} else {
setScriptList((prev) => {
prev.map((script: Script) => {
if (script.uuid === newScript.uuid) {
script.name = newScript.name;
}
}
});
return [...prev];
});
},
(err: any) => {
Message.error(`保存失败: ${err}`);
Message.success("保存成功");
}
);
})
.catch((err) => {
Message.error(`错误的脚本代码: ${err}`);
});
});
setEditors((prev) => {
for (let i = 0; i < prev.length; i += 1) {
if (prev[i].script.uuid === newScript.uuid) {
prev[i].code = e.getValue();
prev[i].isChanged = false;
prev[i].script.name = newScript.name;
break;
}
}
return [...prev];
});
return newScript;
})
.catch((err: any) => {
Message.error(`保存失败: ${err}`);
return Promise.reject(err);
});
})
.catch((err) => {
Message.error(`错误的脚本代码: ${err}`);
return Promise.reject(err);
});
};
const saveAs = (script: Script, e: editor.IStandaloneCodeEditor) => {
return new Promise<void>((resolve) => {
chrome.downloads.download(
@ -289,30 +290,35 @@ function ScriptEditor() {
title: t("run"),
items: [
{
id: "debug",
title: t("debug"),
id: "run",
title: t("run"),
hotKey: KeyMod.CtrlCmd | KeyCode.F5,
hotKeyString: "Ctrl+F5",
tooltip: "只有后台脚本/定时脚本才能调试, 且调试模式下不对进行权限校验(例如@connect)",
tooltip: "只有后台脚本/定时脚本才能运行",
action: async (script, e) => {
// 保存更新代码之后再调试
const newScript = await save(script, e);
// 判断脚本类型
if (newScript.type === SCRIPT_TYPE_NORMAL) {
Message.error("只有后台脚本/定时脚本才能运行");
return;
}
Message.loading({
id: "debug_script",
content: "正在准备脚本资源...",
duration: 3000,
});
runtimeCtrl
.debugScript(newScript)
runtimeClient
.runScript(newScript.uuid)
.then(() => {
Message.success({
id: "debug_script",
content: "构建成功, 可以打开开发者工具在控制台中查看输出",
content: "构建成功, 可以在扩展页打开开发者工具在控制台中查看输出",
duration: 3000,
});
})
.catch((err) => {
LoggerCore.logger(Logger.E(err)).debug("debug script error");
LoggerCore.logger(Logger.E(err)).debug("run script error");
Message.error({
id: "debug_script",
content: `构建失败: ${err}`,

View File

@ -17,6 +17,8 @@ import { useTranslation } from "react-i18next";
import ScriptMenuList from "../components/ScriptMenuList";
import { popupClient } from "../store/features/script";
import { ScriptMenu } from "@App/app/service/service_worker/popup";
import { systemConfig } from "../store/global";
import { SystemConfig } from "@App/pkg/config/config";
const CollapseItem = Collapse.Item;
@ -30,9 +32,11 @@ function App() {
const [scriptList, setScriptList] = useState<ScriptMenu[]>([]);
const [backScriptList, setBackScriptList] = useState<ScriptMenu[]>([]);
const [showAlert, setShowAlert] = useState(false);
const [notice, setNotice] = useState("");
const [isRead, setIsRead] = useState(true);
const [version, setVersion] = useState(ExtVersion);
const [checkUpdate, setCheckUpdate] = useState<Parameters<typeof systemConfig.setCheckUpdate>[0]>({
version: ExtVersion,
notice: "",
isRead: false,
});
const [currentUrl, setCurrentUrl] = useState("");
const [isEnableScript, setIsEnableScript] = useState(localStorage.enable_script !== "false");
const { t } = useTranslation();
@ -45,22 +49,15 @@ function App() {
}
useEffect(() => {
// systemManage.getNotice().then((res) => {
// if (res) {
// setNotice(res.notice);
// setIsRead(res.isRead);
// }
// });
// systemManage.getVersion().then((res) => {
// res && setVersion(res);
// });
systemConfig.getCheckUpdate().then((res) => {
setCheckUpdate(res);
});
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (!tabs.length) {
return;
}
setCurrentUrl(tabs[0].url || "");
popupClient.getPopupData({ url: tabs[0].url!, tabId: tabs[0].id! }).then((resp) => {
console.log(resp);
// 按照开启状态和更新时间排序
const list = resp.scriptList;
list.sort((a, b) => {
@ -109,15 +106,17 @@ function App() {
window.open("/src/options.html", "_blank");
}}
/>
<Badge count={isRead ? 0 : 1} dot offset={[-8, 6]}>
<Badge count={checkUpdate.isRead ? 0 : 1} dot offset={[-8, 6]}>
<Button
type="text"
icon={<IconNotification />}
iconOnly
onClick={() => {
setShowAlert(!showAlert);
setIsRead(true);
systemManage.setRead(true);
console.log(checkUpdate);
checkUpdate.isRead = true;
setCheckUpdate(checkUpdate);
systemConfig.setCheckUpdate(checkUpdate);
}}
/>
</Badge>
@ -179,9 +178,9 @@ function App() {
bodyStyle={{ padding: 0 }}
>
<Alert
style={{ marginBottom: 20, display: showAlert ? "flex" : "none" }}
style={{ display: showAlert ? "flex" : "none" }}
type="info"
content={<div dangerouslySetInnerHTML={{ __html: notice }} />}
content={<div dangerouslySetInnerHTML={{ __html: checkUpdate.notice || "" }} />}
/>
<Collapse bordered={false} defaultActiveKey={["script", "background"]} style={{ maxWidth: 640 }}>
<CollapseItem
@ -204,7 +203,7 @@ function App() {
</Collapse>
<div className="flex flex-row arco-card-header !h-6">
<span className="text-[12px] font-500">{`v${ExtVersion}`}</span>
{semver.lt(ExtVersion, version) && (
{semver.lt(ExtVersion, checkUpdate.version) && (
<span
onClick={() => {
window.open(`https://github.com/scriptscat/scriptcat/releases/tag/v${version}`);

View File

@ -5,6 +5,7 @@ import { FileSystemType } from "@Packages/filesystem/factory";
import { MessageQueue } from "@Packages/message/message_queue";
import i18n from "@App/locales/locales";
import dayjs from "dayjs";
import { ExtVersion } from "@App/app/const";
export const SystamConfigChange = "systemConfigChange";
@ -65,9 +66,7 @@ export class SystemConfig {
public set(key: string, val: any) {
this.cache.set(key, val);
this.storage.set(key, val).then(() => {
console.log(chrome.runtime.lastError, val);
});
this.storage.set(key, val);
// 发送消息通知更新
this.mq.publish(SystamConfigChange, {
key,
@ -247,4 +246,20 @@ export class SystemConfig {
i18n.changeLanguage(value);
dayjs.locale(value.toLocaleLowerCase());
}
setCheckUpdate(data: { notice: string; version: string; isRead: boolean }) {
this.set("check_update", {
notice: data.notice,
version: data.version,
isRead: data.isRead,
});
}
getCheckUpdate(): Promise<Parameters<typeof this.setCheckUpdate>[0]> {
return this.get("check_update", {
notice: "",
isRead: false,
version: ExtVersion,
});
}
}

View File

@ -264,3 +264,15 @@ export function errorMsg(e: any): string {
}
return "";
}
export function isUserScriptsAvailable() {
return false;
try {
// Property access which throws if developer mode is not enabled.
chrome.userScripts;
return true;
} catch {
// Not available.
return false;
}
}