Compare commits

..

15 Commits

Author SHA1 Message Date
ec28795dbb 脚本开启总开关 2025-04-29 14:14:10 +08:00
44041f4735 打开文档页面/更新日志 2025-04-29 11:57:16 +08:00
ffabe268b1 修复匹配问题与优化批量开启速度 2025-04-29 11:53:59 +08:00
ddd3219bae 更大范围的脚本匹配 2025-04-29 11:28:01 +08:00
14baa176d9 🐛 修复隐藏排序问题 #317 2025-04-29 10:42:29 +08:00
3c1e30182f 优化打包体积 2025-04-29 10:25:15 +08:00
1aaf1bbd4a 修复首次打开浏览器加载脚本的问题 2025-04-28 23:24:11 +08:00
8a216933ca vscode reconnect 2025-04-28 18:04:20 +08:00
51fe2a89e1 开启开发者模式引导 2025-04-28 15:20:26 +08:00
a26f1c5014 优化细节 2025-04-27 18:02:57 +08:00
e1a890a400 处理gm log和新建脚本问题 2025-04-25 16:44:00 +08:00
79e8b8869a 脚本设置 2025-04-25 15:41:02 +08:00
d761c62500 value设置 2025-04-24 22:48:22 +08:00
d200809fee temp 2025-04-24 18:05:12 +08:00
67ba515b2c 数据迁移 2025-04-24 17:22:02 +08:00
45 changed files with 1226 additions and 740 deletions

View File

@ -0,0 +1,34 @@
// ==UserScript==
// @name gm value
// @namespace https://bbs.tampermonkey.net.cn/
// @version 0.1.0
// @description 可以持久化存储数据, 并且可以监听数据变化
// @author You
// @match https://bbs.tampermonkey.net.cn/
// @run-at document-start
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addValueChangeListener
// @grant GM_listValues
// @grant GM_deleteValue
// @grant GM_cookie
// ==/UserScript==
GM_addValueChangeListener("test_set", function (name, oldval, newval, remote) {
console.log("test_set change", name, oldval, newval, remote);
});
setInterval(() => {
console.log(GM_getValue("test_set"));
console.log(GM_listValues());
}, 2000);
setTimeout(() => {
GM_deleteValue("test_set");
}, 3000);
GM_setValue("test_set", new Date().getTime());
console.log(GM_getValue("test_set2"));
GM_setValue("test_set2", new Date().getTime());

View File

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

View File

@ -208,6 +208,16 @@ export default defineConfig({
minimizerOptions: { targets }, minimizerOptions: { targets },
}), }),
], ],
splitChunks: {
chunks: (chunk) => {
// 排除这些文件,不进行分离
return !["editor.worker", "ts.worker", "linter.worker", "service_worker", "content", "inject"].includes(
chunk.name || ""
);
},
minSize: 307200,
maxSize: 4194304,
},
}, },
experiments: { experiments: {
css: true, css: true,

View File

@ -5,13 +5,16 @@ import { MessageSend } from "@Packages/message/server";
export default class MessageWriter implements Writer { export default class MessageWriter implements Writer {
send: MessageSend; send: MessageSend;
constructor(connect: MessageSend) { constructor(
connect: MessageSend,
private action: string = "logger"
) {
this.send = connect; this.send = connect;
} }
write(level: LogLevel, message: string, label: LogLabel): void { write(level: LogLevel, message: string, label: LogLabel): void {
this.send.sendMessage({ this.send.sendMessage({
action: "logger", action: this.action,
data: { data: {
id: 0, id: 0,
level, level,

View File

@ -1,6 +1,9 @@
import { getStorageName } from "@App/pkg/utils/utils";
import { db } from "./repo/dao"; import { db } from "./repo/dao";
import { Script, ScriptAndCode, ScriptCodeDAO, ScriptDAO } from "./repo/scripts"; import { Script, ScriptAndCode, ScriptCodeDAO, ScriptDAO } from "./repo/scripts";
import { Subscribe, SubscribeDAO } from "./repo/subscribe"; import { Subscribe, SubscribeDAO } from "./repo/subscribe";
import { Value, ValueDAO } from "./repo/value";
import { Permission, PermissionDAO } from "./repo/permission";
// 0.10.0重构,重命名字段,统一使用小峰驼 // 0.10.0重构,重命名字段,统一使用小峰驼
function renameField() { function renameField() {
@ -34,98 +37,194 @@ function renameField() {
export: "++id,&scriptId", export: "++id,&scriptId",
}); });
// 将脚本数据迁移到chrome.storage // 将脚本数据迁移到chrome.storage
db.version(18).upgrade(async (tx) => { db.version(18).upgrade(() => {
// 迁移脚本 // 默认使用的事务这里加个延时用db.open()打开数据库后,再执行
const scripts = await tx.table("scripts").toArray(); setTimeout(async () => {
const scriptDAO = new ScriptDAO(); try {
const scriptCodeDAO = new ScriptCodeDAO(); // 迁移脚本
await Promise.all( const scripts = await db.table("scripts").toArray();
scripts.map((script: ScriptAndCode) => { const scriptDAO = new ScriptDAO();
const { const scriptCodeDAO = new ScriptCodeDAO();
uuid, console.log("开始迁移脚本数据", scripts.length);
name, await Promise.all(
namespace, scripts.map(async (script: ScriptAndCode) => {
author, const {
originDomain, uuid,
subscribeUrl, name,
type, namespace,
sort, author,
status, originDomain,
runStatus, subscribeUrl,
metadata, type,
createtime, sort,
checktime, status,
code, runStatus,
} = script; metadata,
return scriptDAO createtime,
.save({ checktime,
uuid,
name,
namespace,
author,
originDomain,
subscribeUrl,
type,
sort,
status,
runStatus,
metadata,
createtime,
checktime,
})
.then((s) =>
scriptCodeDAO.save({
uuid: s.uuid,
code, code,
checkUpdateUrl,
downloadUrl,
selfMetadata,
config,
error,
updatetime,
lastruntime,
nextruntime,
} = script;
const s = await scriptDAO.save({
uuid,
name,
namespace,
author,
originDomain,
origin,
checkUpdateUrl,
downloadUrl,
metadata,
selfMetadata,
subscribeUrl,
config,
type,
status,
sort,
runStatus,
error,
createtime,
updatetime,
checktime,
lastruntime,
nextruntime,
});
return scriptCodeDAO
.save({
uuid: s.uuid,
code,
})
.catch((e) => {
console.log("脚本代码迁移失败", e);
return Promise.reject(e);
});
})
);
// 迁移订阅
const subscribe = await db.table("subscribe").toArray();
const subscribeDAO = new SubscribeDAO();
if (subscribe.length) {
await Promise.all(
subscribe.map((s: Subscribe) => {
const { url, name, code, author, scripts, metadata, status, createtime, updatetime, checktime } = s;
return subscribeDAO.save({
url,
name,
code,
author,
scripts,
metadata,
status,
createtime,
updatetime,
checktime,
});
}) })
); );
}) }
); console.log("订阅数据迁移完成", subscribe.length);
// 迁移订阅 // 迁移value
const subscribe = await tx.table("subscribe").toArray(); interface MV2Value {
const subscribeDAO = new SubscribeDAO(); id: number;
await Promise.all( scriptId: number;
subscribe.map((s: Subscribe) => { storageName?: string;
const { url, name, code, author, scripts, metadata, status, createtime, updatetime, checktime } = s; key: string;
return subscribeDAO.save({ value: any;
url, createtime: number;
name, updatetime: number;
code, }
author, const values = await db.table("value").toArray();
scripts, const valueDAO = new ValueDAO();
metadata, const valueMap = new Map<string, Value>();
status, await Promise.all(
createtime, values.map((v: MV2Value) => {
updatetime, const { scriptId, storageName, key, value, createtime } = v;
checktime, return db
}); .table("scripts")
}) .where("id")
); .equals(scriptId)
// 迁移value .first((script: Script) => {
interface MV2Value { if (script) {
id: number; let data: { [key: string]: any } = {};
scriptId: number; if (!valueMap.has(script.uuid)) {
storageName?: string; valueMap.set(script.uuid, {
key: string; uuid: script.uuid,
value: any; storageName: getStorageName(script),
createtime: number; data: data,
updatetime: number; createtime,
} updatetime: 0,
const values = await tx.table("value").toArray(); });
const valueDAO = new ScriptCodeDAO(); } else {
await Promise.all( data = valueMap.get(script.uuid)!.data;
values.map((v) => { }
const { scriptId, storageName, key, value, createtime } = v; data[key] = value;
return valueDAO.save({ }
scriptId, });
storageName, })
key, );
value, // 保存到数据库
createtime, await Promise.all(
}); Array.from(valueMap.keys()).map((uuid) => {
}) const { storageName, data, createtime } = valueMap.get(uuid)!;
); return valueDAO.save(storageName!, {
// 迁移permission uuid,
storageName,
data,
createtime,
updatetime: 0,
});
})
);
console.log("脚本value数据迁移完成", values.length);
// 迁移permission
const permissions = await db.table("permission").toArray();
const permissionDAO = new PermissionDAO();
await Promise.all(
permissions.map((p: Permission & { scriptId: number }) => {
const { scriptId, permission, permissionValue, createtime, updatetime, allow } = p;
return db
.table("scripts")
.where("id")
.equals(scriptId)
.first((script: Script) => {
if (script) {
return permissionDAO.save({
uuid: script.uuid,
permission,
permissionValue,
createtime,
updatetime,
allow,
});
}
});
})
);
console.log("脚本permission数据迁移完成", permissions.length);
// 打开页面,告知数据储存+升级至了mv3重启一次扩展
setTimeout(async () => {
const scripts = await scriptDAO.all();
console.log("脚本数据迁移完成", scripts.length);
if (scripts.length > 0) {
chrome.tabs.create({
url: "https://docs.scriptcat.org/docs/change/v0.17/",
});
setTimeout(() => {
chrome.runtime.reload();
}, 1000);
}
}, 2000);
} catch (e) {
console.error("脚本数据迁移失败", e);
}
}, 200);
}); });
return db.open(); return db.open();
} }

View File

@ -1,3 +1,5 @@
import LoggerCore from "@App/app/logger/core";
import Logger from "@App/app/logger/logger";
import { ScriptRunResouce } from "@App/app/repo/scripts"; import { ScriptRunResouce } from "@App/app/repo/scripts";
import { Client, sendMessage } from "@Packages/message/client"; import { Client, sendMessage } from "@Packages/message/client";
import { CustomEventMessage } from "@Packages/message/custom_event_message"; import { CustomEventMessage } from "@Packages/message/custom_event_message";
@ -21,6 +23,7 @@ export default class ContentRuntime {
// 转发给inject // 转发给inject
return sendMessage(this.msg, "inject/runtime/valueUpdate", data); return sendMessage(this.msg, "inject/runtime/valueUpdate", data);
}); });
forwardMessage("serviceWorker", "script/isInstalled", this.server, this.extSend);
forwardMessage( forwardMessage(
"serviceWorker", "serviceWorker",
"runtime/gmApi", "runtime/gmApi",
@ -83,7 +86,17 @@ export default class ContentRuntime {
case "GM_log": case "GM_log":
// 拦截GM_log打印到控制台 // 拦截GM_log打印到控制台
// 由于某些页面会处理掉console.log所以丢到这里来打印 // 由于某些页面会处理掉console.log所以丢到这里来打印
console.log(...data.params); switch (data.params.length) {
case 1:
console.log(data.params[0]);
break;
case 2:
console.log("[" + data.params[1] + "]", data.params[0]);
break;
case 3:
console.log("[" + data.params[1] + "]", data.params[0], data.params[2]);
break;
}
break; break;
} }
return false; return false;

View File

@ -159,10 +159,11 @@ export default class GMApi {
} }
if (value === undefined) { if (value === undefined) {
delete this.scriptRes.value[key]; delete this.scriptRes.value[key];
return this.sendMessage("GM_setValue", [key]);
} else { } else {
this.scriptRes.value[key] = value; this.scriptRes.value[key] = value;
return this.sendMessage("GM_setValue", [key, value]);
} }
return this.sendMessage("GM_setValue", [key, value]);
} }
@GMContext.API({ depend: ["GM_setValue"] }) @GMContext.API({ depend: ["GM_setValue"] })
@ -391,7 +392,7 @@ export default class GMApi {
anonymous: details.anonymous, anonymous: details.anonymous,
user: details.user, user: details.user,
password: details.password, password: details.password,
maxRedirects: details.maxRedirects, redirect: details.redirect,
}; };
if (!param.headers) { if (!param.headers) {
param.headers = {}; param.headers = {};

View File

@ -4,6 +4,8 @@ import ExecScript, { ValueUpdateData } from "./exec_script";
import { addStyle, ScriptFunc } from "./utils"; import { addStyle, ScriptFunc } from "./utils";
import { getStorageName } from "@App/pkg/utils/utils"; import { getStorageName } from "@App/pkg/utils/utils";
import { EmitEventRequest } from "../service_worker/runtime"; import { EmitEventRequest } from "../service_worker/runtime";
import { ExternalWhitelist } from "@App/app/const";
import { sendMessage } from "@Packages/message/client";
export class InjectRuntime { export class InjectRuntime {
execList: ExecScript[] = []; execList: ExecScript[] = [];
@ -44,6 +46,40 @@ export class InjectRuntime {
val.valueUpdate(data); 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) { execScript(script: ScriptRunResouce, scriptFunc: ScriptFunc) {

View File

@ -20,6 +20,10 @@ export class OffscreenManager {
private serviceWorker = new ServiceWorkerClient(this.extensionMessage); private serviceWorker = new ServiceWorkerClient(this.extensionMessage);
constructor(private extensionMessage:MessageSend) {
}
logger(data: Logger) { logger(data: Logger) {
const dao = new LoggerDAO(); const dao = new LoggerDAO();
dao.save(data); dao.save(data);

View File

@ -10,6 +10,7 @@ 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"; import { Subscribe } from "@App/app/repo/subscribe";
import { Permission } from "@App/app/repo/permission";
export class ServiceWorkerClient extends Client { export class ServiceWorkerClient extends Client {
constructor(msg: MessageSend) { constructor(msg: MessageSend) {
@ -59,6 +60,16 @@ export class ScriptClient extends Client {
return this.do("excludeUrl", { uuid, url, remove }); return this.do("excludeUrl", { uuid, url, remove });
} }
// 重置匹配项
resetMatch(uuid: string, match: string[] | undefined) {
return this.do("resetMatch", { uuid, match });
}
// 重置排除项
resetExclude(uuid: string, exclude: string[] | undefined) {
return this.do("resetExclude", { uuid, exclude });
}
requestCheckUpdate(uuid: string) { requestCheckUpdate(uuid: string) {
return this.do("requestCheckUpdate", uuid); return this.do("requestCheckUpdate", uuid);
} }
@ -72,6 +83,10 @@ export class ResourceClient extends Client {
getScriptResources(script: Script): Promise<{ [key: string]: Resource }> { getScriptResources(script: Script): Promise<{ [key: string]: Resource }> {
return this.do("getScriptResources", script); return this.do("getScriptResources", script);
} }
deleteResource(url: string) {
return this.do("deleteResource", url);
}
} }
export class ValueClient extends Client { export class ValueClient extends Client {
@ -79,7 +94,7 @@ export class ValueClient extends Client {
super(msg, "serviceWorker/value"); super(msg, "serviceWorker/value");
} }
getScriptValue(script: Script) { getScriptValue(script: Script): Promise<{ [key: string]: any }> {
return this.do("getScriptValue", script); return this.do("getScriptValue", script);
} }
@ -154,6 +169,22 @@ export class PermissionClient extends Client {
getPermissionInfo(uuid: string): ReturnType<PermissionVerify["getInfo"]> { getPermissionInfo(uuid: string): ReturnType<PermissionVerify["getInfo"]> {
return this.do("getInfo", uuid); return this.do("getInfo", uuid);
} }
deletePermission(uuid: string, permission: string, permissionValue: string) {
return this.do("deletePermission", { uuid, permission, permissionValue });
}
getScriptPermissions(uuid: string): ReturnType<PermissionVerify["getScriptPermissions"]> {
return this.do("getScriptPermissions", uuid);
}
addPermission(permission: Permission) {
return this.do("addPermission", permission);
}
resetPermission(uuid: string) {
return this.do("resetPermission", uuid);
}
} }
export class SynchronizeClient extends Client { export class SynchronizeClient extends Client {

View File

@ -242,7 +242,7 @@ export default class GMApi {
@PermissionVerify.API() @PermissionVerify.API()
async GM_setValue(request: Request, sender: GetSender) { async GM_setValue(request: Request, sender: GetSender) {
if (!request.params || request.params.length !== 2) { if (!request.params || request.params.length < 1) {
throw new Error("param is failed"); throw new Error("param is failed");
} }
const [key, value] = request.params; const [key, value] = request.params;

View File

@ -9,6 +9,8 @@ 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"; 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"; export type InstallSource = "user" | "system" | "sync" | "subscribe" | "vscode";
@ -78,8 +80,17 @@ export default class ServiceWorkerManager {
case "checkSubscribeUpdate": case "checkSubscribeUpdate":
subscribe.checkSubscribeUpdate(); subscribe.checkSubscribeUpdate();
break; break;
case "checkUpdate":
// 检查扩展更新
this.checkUpdate();
break;
} }
}); });
// 8小时检查一次扩展更新
chrome.alarms.create("checkUpdate", {
delayInMinutes: 0,
periodInMinutes: 8 * 60,
});
// 监听配置变化 // 监听配置变化
this.mq.subscribe("systemConfigChange", (msg) => { this.mq.subscribe("systemConfigChange", (msg) => {
@ -95,5 +106,31 @@ export default class ServiceWorkerManager {
systemConfig.getCloudSync().then((config) => { systemConfig.getCloudSync().then((config) => {
synchronize.cloudSyncConfigChange(config); synchronize.cloudSyncConfigChange(config);
}); });
if (process.env.NODE_ENV === "production") {
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === "install") {
chrome.tabs.create({ url: "https://docs.scriptcat.org/" });
} else if (details.reason === "update") {
chrome.tabs.create({
url: `https://docs.scriptcat.org/docs/change/#${ExtVersion}`,
});
}
});
}
}
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

@ -96,15 +96,6 @@ export default class PermissionVerify {
reject: (reason: any) => void; reject: (reason: any) => void;
}> = new Queue(); }> = new Queue();
async removePermissionCache(uuid: string) {
// 先删除缓存
(await Cache.getInstance().list()).forEach((key) => {
if (key.startsWith(`permission:${uuid}:`)) {
Cache.getInstance().del(key);
}
});
}
private permissionDAO: PermissionDAO = new PermissionDAO(); private permissionDAO: PermissionDAO = new PermissionDAO();
constructor(private group: Group) {} constructor(private group: Group) {}
@ -310,9 +301,45 @@ export default class PermissionVerify {
return Promise.resolve({ script, confirm, likeNum }); return Promise.resolve({ script, confirm, likeNum });
} }
async deletePermission(data: { uuid: string; permission: string; permissionValue: string }) {
const oldConfirm = await this.permissionDAO.findByKey(data.uuid, data.permission, data.permissionValue);
if (!oldConfirm) {
throw new Error("permission not found");
} else {
await this.permissionDAO.delete(this.permissionDAO.key(oldConfirm));
// 删除缓存
Cache.getInstance().del(CacheKey.permissionConfirm(data.uuid, oldConfirm));
}
}
getScriptPermissions(uuid: string) {
// 获取脚本的所有权限
return this.permissionDAO.find((key, item) => item.uuid === uuid);
}
// 添加权限
async addPermission(permission: Permission) {
await this.permissionDAO.save(permission);
Cache.getInstance().del(CacheKey.permissionConfirm(permission.uuid, permission));
}
// 重置权限
async resetPermission(uuid: string) {
// 删除所有权限
const permissions = await this.permissionDAO.find((key, item) => item.uuid === uuid);
permissions.forEach((item) => {
this.permissionDAO.delete(this.permissionDAO.key(item));
Cache.getInstance().del(CacheKey.permissionConfirm(uuid, item));
});
}
init() { init() {
this.dealConfirmQueue(); this.dealConfirmQueue();
this.group.on("confirm", this.userConfirm.bind(this)); this.group.on("confirm", this.userConfirm.bind(this));
this.group.on("getInfo", this.getInfo.bind(this)); this.group.on("getInfo", this.getInfo.bind(this));
this.group.on("deletePermission", this.deletePermission.bind(this));
this.group.on("getScriptPermissions", this.getScriptPermissions.bind(this));
this.group.on("addPermission", this.getInfo.bind(this));
this.group.on("resetPermission", this.resetPermission.bind(this));
} }
} }

View File

@ -257,7 +257,7 @@ export class ResourceService {
return fetch(u.url) return fetch(u.url)
.then(async (resp) => { .then(async (resp) => {
if (resp.status !== 200) { if (resp.status !== 200) {
throw new Error(`resource response status not 200:${resp.status}`); throw new Error(`resource response status not 200: ${resp.status}`);
} }
return { return {
data: await resp.blob(), data: await resp.blob(),
@ -305,7 +305,20 @@ export class ResourceService {
return { url: urls[0], hash }; return { url: urls[0], hash };
} }
async deleteResource(url: string) {
// 删除缓存
const res = await this.resourceDAO.get(url);
if (!res) {
throw new Error("resource not found");
}
Object.keys(res.link).forEach((key) => {
this.cache.delete(key);
});
return this.resourceDAO.delete(url);
}
init() { init() {
this.group.on("getScriptResources", this.getScriptResources.bind(this)); this.group.on("getScriptResources", this.getScriptResources.bind(this));
this.group.on("deleteResource", this.deleteResource.bind(this));
} }
} }

View File

@ -1,4 +1,4 @@
import { MessageQueue } from "@Packages/message/message_queue"; import { MessageQueue, Unsubscribe } from "@Packages/message/message_queue";
import { ExtMessageSender, GetSender, Group, MessageSend } from "@Packages/message/server"; import { ExtMessageSender, GetSender, Group, MessageSend } from "@Packages/message/server";
import { import {
Script, Script,
@ -15,18 +15,17 @@ import { subscribeScriptDelete, subscribeScriptEnable, subscribeScriptInstall }
import { ScriptService } from "./script"; import { ScriptService } from "./script";
import { runScript, stopScript } from "../offscreen/client"; import { runScript, stopScript } from "../offscreen/client";
import { getRunAt } from "./utils"; 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 Cache from "@App/app/cache";
import { dealPatternMatches, UrlMatch } from "@App/pkg/utils/match"; import { dealPatternMatches, UrlMatch } from "@App/pkg/utils/match";
import { ExtensionContentMessageSend } from "@Packages/message/extension_message"; import { ExtensionContentMessageSend } from "@Packages/message/extension_message";
import { sendMessage } from "@Packages/message/client"; import { sendMessage } from "@Packages/message/client";
import { compileInjectScript } from "../content/utils"; import { compileInjectScript } from "../content/utils";
import { PopupService } from "./popup";
import Logger from "@App/app/logger/logger";
import LoggerCore from "@App/app/logger/core"; import LoggerCore from "@App/app/logger/core";
import PermissionVerify from "./permission_verify"; import PermissionVerify from "./permission_verify";
import { SystemConfig } from "@App/pkg/config/config"; import { SystemConfig } from "@App/pkg/config/config";
import { ResourceService } from "./resource"; import { ResourceService } from "./resource";
import { LocalStorageDAO } from "@App/app/repo/localStorage";
// 为了优化性能存储到缓存时删除了code、value与resource // 为了优化性能存储到缓存时删除了code、value与resource
export interface ScriptMatchInfo extends ScriptRunResouce { export interface ScriptMatchInfo extends ScriptRunResouce {
@ -49,6 +48,9 @@ export class RuntimeService {
scriptCustomizeMatch: UrlMatch<string> = new UrlMatch<string>(); scriptCustomizeMatch: UrlMatch<string> = new UrlMatch<string>();
scriptMatchCache: Map<string, ScriptMatchInfo> | null | undefined; scriptMatchCache: Map<string, ScriptMatchInfo> | null | undefined;
isEnableDeveloperMode = false;
isEnableUserscribe = true;
constructor( constructor(
private systemConfig: SystemConfig, private systemConfig: SystemConfig,
private group: Group, private group: Group,
@ -70,8 +72,35 @@ export class RuntimeService {
this.group.on("runScript", this.runScript.bind(this)); this.group.on("runScript", this.runScript.bind(this));
this.group.on("pageLoad", this.pageLoad.bind(this)); this.group.on("pageLoad", this.pageLoad.bind(this));
// 读取inject.js注入页面 // 检查是否开启了开发者模式
this.registerInjectScript(); this.isEnableDeveloperMode = isUserScriptsAvailable();
if (!this.isEnableDeveloperMode) {
// 未开启加上警告引导
// 判断是否首次
const localStorage = new LocalStorageDAO();
localStorage.get("firstShowDeveloperMode").then((res) => {
if (!res) {
localStorage.save({
key: "firstShowDeveloperMode",
value: true,
});
// 打开页面
chrome.tabs.create({
url: `https://docs.scriptcat.org/docs/use/open-dev/`,
});
}
});
chrome.action.setBadgeBackgroundColor({
color: "#ff8c00",
});
chrome.action.setBadgeTextColor({
color: "#ffffff",
});
chrome.action.setBadgeText({
text: "!",
});
}
// 监听脚本开启 // 监听脚本开启
subscribeScriptEnable(this.mq, async (data) => { subscribeScriptEnable(this.mq, async (data) => {
const script = await this.scriptDAO.getAndCode(data.uuid); const script = await this.scriptDAO.getAndCode(data.uuid);
@ -82,6 +111,7 @@ export class RuntimeService {
// 如果是后台脚本, 在offscreen中进行处理 // 如果是后台脚本, 在offscreen中进行处理
if (script.type === SCRIPT_TYPE_NORMAL) { if (script.type === SCRIPT_TYPE_NORMAL) {
// 加载页面脚本 // 加载页面脚本
// 不管开没开启都要加载一次脚本信息
await this.loadPageScript(script); await this.loadPageScript(script);
if (!data.enable) { if (!data.enable) {
await this.unregistryPageScript(script.uuid); await this.unregistryPageScript(script.uuid);
@ -104,6 +134,32 @@ export class RuntimeService {
this.deleteScriptMatch(uuid); this.deleteScriptMatch(uuid);
}); });
this.systemConfig.addListener("enable_script", (enable) => {
this.isEnableUserscribe = enable;
if (enable) {
this.registerUserscripts();
} else {
this.unregisterUserscripts();
}
});
// 检查是否开启
this.isEnableUserscribe = await this.systemConfig.getEnableScript();
if (this.isEnableUserscribe) {
this.registerUserscripts();
}
}
unsubscribe: Unsubscribe[] = [];
// 取消脚本注册
unregisterUserscripts() {
chrome.userScripts.unregister();
this.deleteMessageFlag();
}
async registerUserscripts() {
// 读取inject.js注入页面
this.registerInjectScript();
// 将开启的脚本发送一次enable消息 // 将开启的脚本发送一次enable消息
const scriptDao = new ScriptDAO(); const scriptDao = new ScriptDAO();
const list = await scriptDao.all(); const list = await scriptDao.all();
@ -132,6 +188,14 @@ export class RuntimeService {
}); });
} }
deleteMessageFlag() {
return Cache.getInstance().del("scriptInjectMessageFlag");
}
getMessageFlag() {
return Cache.getInstance().get("scriptInjectMessageFlag");
}
// 给指定tab发送消息 // 给指定tab发送消息
sendMessageToTab(to: ExtMessageSender, action: string, data: any) { sendMessageToTab(to: ExtMessageSender, action: string, data: any) {
if (to.tabId === -1) { if (to.tabId === -1) {
@ -205,7 +269,7 @@ export class RuntimeService {
return undefined; return undefined;
} }
// 如果是iframe,判断是否允许在iframe里运行 // 如果是iframe,判断是否允许在iframe里运行
if (chromeSender.frameId !== undefined) { if (chromeSender.frameId) {
if (scriptRes.metadata.noframes) { if (scriptRes.metadata.noframes) {
return undefined; return undefined;
} }
@ -254,40 +318,57 @@ export class RuntimeService {
} }
// 注册inject.js // 注册inject.js
registerInjectScript() { async registerInjectScript() {
chrome.userScripts.getScripts({ ids: ["scriptcat-inject"] }).then((res) => { // 如果没设置过, 则更新messageFlag
if (res.length == 0) { let messageFlag = await this.getMessageFlag();
chrome.userScripts.configureWorld({ if (!messageFlag) {
csp: "script-src 'self' 'unsafe-inline' 'unsafe-eval' *", messageFlag = await this.messageFlag();
messaging: true, const injectJs = await fetch("inject.js").then((res) => res.text());
// 替换ScriptFlag
const code = `(function (MessageFlag) {\n${injectJs}\n})('${messageFlag}')`;
chrome.userScripts.configureWorld({
csp: "script-src 'self' 'unsafe-inline' 'unsafe-eval' *",
messaging: true,
});
const scripts: chrome.userScripts.RegisteredUserScript[] = [
{
id: "scriptcat-inject",
js: [{ code }],
matches: ["<all_urls>"],
allFrames: true,
world: "MAIN",
runAt: "document_start",
},
// 注册content
{
id: "scriptcat-content",
js: [{ file: "src/content.js" }],
matches: ["<all_urls>"],
allFrames: true,
runAt: "document_start",
world: "USER_SCRIPT",
},
];
try {
// 如果使用getScripts来判断, 会出现找不到的问题
// 另外如果使用
await chrome.userScripts.register(scripts);
} catch (e: any) {
LoggerCore.logger().error("register inject.js error", {
error: e,
}); });
fetch("inject.js") if (e.message?.indexOf("Duplicate script ID") !== -1) {
.then((res) => res.text()) // 如果是重复注册, 则更新
.then(async (injectJs) => { chrome.userScripts.update(scripts, () => {
// 替换ScriptFlag if (chrome.runtime.lastError) {
const code = `(function (MessageFlag) {\n${injectJs}\n})('${await this.messageFlag()}')`; LoggerCore.logger().error("update inject.js error", {
chrome.userScripts.register([ error: chrome.runtime.lastError,
{ });
id: "scriptcat-inject", }
js: [{ code }],
matches: ["<all_urls>"],
allFrames: true,
world: "MAIN",
runAt: "document_start",
},
// 注册content
{
id: "scriptcat-content",
js: [{ file: "src/content.js" }],
matches: ["<all_urls>"],
allFrames: true,
runAt: "document_start",
world: "USER_SCRIPT",
},
]);
}); });
}
} }
}); }
} }
loadingScript: Promise<void> | null | undefined; loadingScript: Promise<void> | null | undefined;
@ -367,8 +448,11 @@ export class RuntimeService {
if (!this.scriptMatchCache) { if (!this.scriptMatchCache) {
await this.loadScriptMatchInfo(); await this.loadScriptMatchInfo();
} }
this.scriptMatchCache!.get(uuid)!.status = status; const script = await this.scriptMatchCache!.get(uuid);
this.saveScriptMatchInfo(); if (script) {
script.status = status;
this.saveScriptMatchInfo();
}
} }
async deleteScriptMatch(uuid: string) { async deleteScriptMatch(uuid: string) {
@ -384,15 +468,15 @@ export class RuntimeService {
// 加载页面脚本, 会把脚本信息放入缓存中 // 加载页面脚本, 会把脚本信息放入缓存中
// 如果脚本开启, 则注册脚本 // 如果脚本开启, 则注册脚本
async loadPageScript(script: Script) { async loadPageScript(script: Script) {
const matches = script.metadata["match"]; const scriptRes = await this.script.buildScriptRunResource(script);
const matches = scriptRes.metadata["match"];
if (!matches) { if (!matches) {
return; return;
} }
const scriptRes = await this.script.buildScriptRunResource(script);
scriptRes.code = compileInjectScript(scriptRes); scriptRes.code = compileInjectScript(scriptRes);
matches.push(...(script.metadata["include"] || [])); matches.push(...(scriptRes.metadata["include"] || []));
const patternMatches = dealPatternMatches(matches); const patternMatches = dealPatternMatches(matches);
const scriptMatchInfo: ScriptMatchInfo = Object.assign( const scriptMatchInfo: ScriptMatchInfo = Object.assign(
{ matches: patternMatches.result, excludeMatches: [], customizeExcludeMatches: [] }, { matches: patternMatches.result, excludeMatches: [], customizeExcludeMatches: [] },
@ -434,25 +518,36 @@ export class RuntimeService {
this.addScriptMatch(scriptMatchInfo); this.addScriptMatch(scriptMatchInfo);
// 如果脚本开启, 则注册脚本 // 如果脚本开启, 则注册脚本
if (script.status === SCRIPT_STATUS_ENABLE) { if (this.isEnableDeveloperMode && this.isEnableUserscribe && script.status === SCRIPT_STATUS_ENABLE) {
if (!script.metadata["noframes"]) { if (scriptRes.metadata["noframes"]) {
registerScript.allFrames = false;
} else {
registerScript.allFrames = true; registerScript.allFrames = true;
} }
if (script.metadata["run-at"]) { if (scriptRes.metadata["run-at"]) {
registerScript.runAt = getRunAt(script.metadata["run-at"]); registerScript.runAt = getRunAt(scriptRes.metadata["run-at"]);
} }
if (await Cache.getInstance().get("registryScript:" + script.uuid)) { const res = await chrome.userScripts.getScripts({ ids: [script.uuid] });
await chrome.userScripts.update([registerScript]); const logger = LoggerCore.logger({
name: script.name,
registerMatch: {
matches: registerScript.matches,
excludeMatches: registerScript.excludeMatches,
},
});
if (res.length > 0) {
await chrome.userScripts.update([registerScript], () => {
if (chrome.runtime.lastError) {
logger.error("update registerScript error", {
error: chrome.runtime.lastError,
});
}
});
} else { } else {
await chrome.userScripts.register([registerScript], () => { await chrome.userScripts.register([registerScript], () => {
if (chrome.runtime.lastError) { if (chrome.runtime.lastError) {
LoggerCore.logger().error("registerScript error", { logger.error("registerScript error", {
error: chrome.runtime.lastError, error: chrome.runtime.lastError,
name: script.name,
registerMatch: {
matches: registerScript.matches,
excludeMatches: registerScript.excludeMatches,
},
}); });
} }
}); });
@ -462,19 +557,17 @@ export class RuntimeService {
} }
async unregistryPageScript(uuid: string) { async unregistryPageScript(uuid: string) {
if (!(await Cache.getInstance().get("registryScript:" + uuid))) { if (
!this.isEnableDeveloperMode ||
!this.isEnableUserscribe ||
!(await Cache.getInstance().get("registryScript:" + uuid))
) {
return; return;
} }
chrome.userScripts.unregister( // 删除缓存
{ Cache.getInstance().del("registryScript:" + uuid);
ids: [uuid], // 修改脚本状态为disable
}, this.updateScriptStatus(uuid, SCRIPT_STATUS_DISABLE);
() => { chrome.userScripts.unregister({ ids: [uuid] });
// 删除缓存
Cache.getInstance().del("registryScript:" + uuid);
// 修改脚本状态为disable
this.updateScriptStatus(uuid, SCRIPT_STATUS_DISABLE);
}
);
} }
} }

View File

@ -21,6 +21,7 @@ import { ResourceService } from "./resource";
import { ValueService } from "./value"; import { ValueService } from "./value";
import { compileScriptCode } from "../content/utils"; import { compileScriptCode } from "../content/utils";
import { SystemConfig } from "@App/pkg/config/config"; import { SystemConfig } from "@App/pkg/config/config";
import i18n, { localePath } from "@App/locales/locales";
export class ScriptService { export class ScriptService {
logger: Logger; logger: Logger;
@ -59,50 +60,55 @@ 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, "user").catch((e) => { this.openInstallPageByUrl(targetUrl, "user")
logger.error("install script error", Logger.E(e)); .catch((e) => {
// 如果打开失败, 则重定向到安装页 logger.error("install script error", Logger.E(e));
chrome.scripting.executeScript({ // 不再重定向当前url
target: { tabId: req.tabId }, chrome.declarativeNetRequest.updateDynamicRules(
func: function () { {
history.back(); removeRuleIds: [2],
}, addRules: [
}); {
// 并不再重定向当前url id: 2,
chrome.declarativeNetRequest.updateDynamicRules( priority: 1,
{ action: {
removeRuleIds: [2], type: chrome.declarativeNetRequest.RuleActionType.ALLOW,
addRules: [ },
{ condition: {
id: 2, regexFilter: targetUrl,
priority: 1, resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME],
action: { requestMethods: [chrome.declarativeNetRequest.RequestMethod.GET],
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);
}, }
],
},
() => {
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError);
} }
} );
); })
}); .finally(() => {
// 回退到到安装页
chrome.scripting.executeScript({
target: { tabId: req.tabId },
func: function () {
history.back();
},
});
});
}, },
{ {
urls: [ 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", "https://www.tampermonkey.net/script_installation.php",
], ],
types: ["main_frame"], types: ["main_frame"],
} }
); );
// 获取i18n
// 重定向到脚本安装页 // 重定向到脚本安装页
chrome.declarativeNetRequest.updateDynamicRules( chrome.declarativeNetRequest.updateDynamicRules(
{ {
@ -114,7 +120,7 @@ export class ScriptService {
action: { action: {
type: chrome.declarativeNetRequest.RuleActionType.REDIRECT, type: chrome.declarativeNetRequest.RuleActionType.REDIRECT,
redirect: { redirect: {
regexSubstitution: "https://docs.scriptcat.org/docs/script_installation#url=\\0", regexSubstitution: `https://docs.scriptcat.org${localePath}/docs/script_installation/#url=\\0`,
}, },
}, },
condition: { condition: {
@ -320,6 +326,54 @@ export class ScriptService {
}); });
} }
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") { async checkUpdate(uuid: string, source: "user" | "system") {
// 检查更新 // 检查更新
const script = await this.scriptDAO.get(uuid); const script = await this.scriptDAO.get(uuid);
@ -431,6 +485,15 @@ export class ScriptService {
return this.checkUpdate(uuid, "user"); 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() { init() {
this.listenerScriptInstall(); this.listenerScriptInstall();
@ -443,7 +506,10 @@ export class ScriptService {
this.group.on("getCode", this.getCode.bind(this)); this.group.on("getCode", this.getCode.bind(this));
this.group.on("getScriptRunResource", this.buildScriptRunResource.bind(this)); this.group.on("getScriptRunResource", this.buildScriptRunResource.bind(this));
this.group.on("excludeUrl", this.excludeUrl.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)); this.group.on("requestCheckUpdate", this.requestCheckUpdate.bind(this));
this.group.on("isInstalled", this.isInstalled.bind(this));
// 定时检查更新, 每10分钟检查一次 // 定时检查更新, 每10分钟检查一次
chrome.alarms.create("checkScriptUpdate", { chrome.alarms.create("checkScriptUpdate", {

View File

@ -54,7 +54,11 @@ export class ValueService {
}); });
} else { } else {
oldValue = valueModel.data[key]; oldValue = valueModel.data[key];
valueModel.data[key] = value; if (value === undefined) {
delete valueModel.data[key];
} else {
valueModel.data[key] = value;
}
await this.valueDAO.save(storageName, valueModel); await this.valueDAO.save(storageName, valueModel);
} }
return true; return true;

View File

@ -27,10 +27,15 @@ i18n.use(initReactI18next).init({
}, },
}); });
export let localePath = "";
chrome.i18n.getAcceptLanguages((lngs) => { chrome.i18n.getAcceptLanguages((lngs) => {
systemConfig.getLanguage().then((lng) => { systemConfig.getLanguage(lngs).then((lng) => {
i18n.changeLanguage(lng); i18n.changeLanguage(lng);
dayjs.locale(lng.toLocaleLowerCase()); dayjs.locale(lng.toLocaleLowerCase());
if (lng !== "zh-CN") {
localePath = "en";
}
}); });
}); });

View File

@ -68,6 +68,7 @@
"confirm_delete_backup_file": "确认删除备份文件", "confirm_delete_backup_file": "确认删除备份文件",
"confirm_update": "确认更新", "confirm_update": "确认更新",
"delete_success": "删除成功", "delete_success": "删除成功",
"deleting": "删除中",
"backup_strategy": "备份策略", "backup_strategy": "备份策略",
"under_construction": "建设中", "under_construction": "建设中",
"development_debugging": "开发调试", "development_debugging": "开发调试",
@ -367,5 +368,8 @@
"eslint_config_format_error": "eslint配置格式错误", "eslint_config_format_error": "eslint配置格式错误",
"export_success": "导出成功", "export_success": "导出成功",
"get_backup_dir_url_failed": "获取备份目录地址失败", "get_backup_dir_url_failed": "获取备份目录地址失败",
"get_backup_files_failed": "获取备份文件失败" "get_backup_files_failed": "获取备份文件失败",
"develop_mode_guide": "检测到当前未开启开发者模式,您的脚本无法正常使用,<a href=\"https://docs.scriptcat.org/docs/use/open-dev/\" target=\"black\" style=\"color: var(--color-text-1)\">👉点我了解如何开启</a>",
"enable_script_failed": "脚本开启失败",
"disable_script_failed": "脚本关闭失败"
} }

View File

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

View File

@ -1,20 +1,19 @@
import migrate from "./app/migrate"; import { MessageSend } from "@Packages/message/server";
import LoggerCore from "./app/logger/core"; import LoggerCore from "./app/logger/core";
import DBWriter from "./app/logger/db_writer"; import MessageWriter from "./app/logger/message_writer";
import { LoggerDAO } from "./app/repo/logger";
import { OffscreenManager } from "./app/service/offscreen"; import { OffscreenManager } from "./app/service/offscreen";
import { ExtensionMessageSend } from "@Packages/message/extension_message";
migrate();
function main() { function main() {
// 初始化日志组件 // 初始化日志组件
const extensionMessage: MessageSend = new ExtensionMessageSend();
const loggerCore = new LoggerCore({ const loggerCore = new LoggerCore({
writer: new DBWriter(new LoggerDAO()), writer: new MessageWriter(extensionMessage),
labels: { env: "offscreen" }, labels: { env: "offscreen" },
}); });
loggerCore.logger().debug("offscreen start"); loggerCore.logger().debug("offscreen start");
// 初始化管理器 // 初始化管理器
const manager = new OffscreenManager(); const manager = new OffscreenManager(extensionMessage);
manager.initManager(); manager.initManager();
} }

View File

@ -17,7 +17,6 @@ const CodeEditor: React.ForwardRefRenderFunction<{ editor: editor.IStandaloneCod
{ id, className, code, diffCode, editable }, { id, className, code, diffCode, editable },
ref ref
) => { ) => {
const settings = useAppSelector((state) => state.setting);
const [monacoEditor, setEditor] = useState<editor.IStandaloneCodeEditor>(); const [monacoEditor, setEditor] = useState<editor.IStandaloneCodeEditor>();
const [enableEslint, setEnableEslint] = useState(false); const [enableEslint, setEnableEslint] = useState(false);
const [eslintConfig, setEslintConfig] = useState(""); const [eslintConfig, setEslintConfig] = useState("");

View File

@ -1,22 +1,12 @@
import { Resource } from "@App/app/repo/resource"; import { Resource } from "@App/app/repo/resource";
import { Script } from "@App/app/repo/scripts"; import { Script } from "@App/app/repo/scripts";
import { ResourceClient } from "@App/app/service/service_worker/client";
import { message } from "@App/pages/store/global";
import { base64ToBlob } from "@App/pkg/utils/script"; import { base64ToBlob } from "@App/pkg/utils/script";
import { import { Button, Drawer, Input, Message, Popconfirm, Space, Table } from "@arco-design/web-react";
Button,
Drawer,
Input,
Message,
Popconfirm,
Space,
Table,
} from "@arco-design/web-react";
import { RefInputType } from "@arco-design/web-react/es/Input/interface"; import { RefInputType } from "@arco-design/web-react/es/Input/interface";
import { ColumnProps } from "@arco-design/web-react/es/Table"; import { ColumnProps } from "@arco-design/web-react/es/Table";
import { import { IconDelete, IconDownload, IconSearch } from "@arco-design/web-react/icon";
IconDelete,
IconDownload,
IconSearch,
} from "@arco-design/web-react/icon";
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -33,14 +23,14 @@ const ScriptResource: React.FC<{
}> = ({ script, visible, onCancel, onOk }) => { }> = ({ script, visible, onCancel, onOk }) => {
const [data, setData] = useState<ResourceListItem[]>([]); const [data, setData] = useState<ResourceListItem[]>([]);
const inputRef = useRef<RefInputType>(null); const inputRef = useRef<RefInputType>(null);
// const resourceCtrl = IoC.instance(ResourceController) as ResourceController;
const { t } = useTranslation(); const { t } = useTranslation();
const resourceClient = new ResourceClient(message);
useEffect(() => { useEffect(() => {
if (!script) { if (!script) {
return () => {}; return () => {};
} }
resourceCtrl.getResource(script).then((res) => { resourceClient.getScriptResources(script).then((res) => {
const arr: ResourceListItem[] = []; const arr: ResourceListItem[] = [];
Object.keys(res).forEach((key) => { Object.keys(res).forEach((key) => {
// @ts-ignore // @ts-ignore
@ -120,10 +110,21 @@ const ScriptResource: React.FC<{
title={t("confirm_delete_resource")} title={t("confirm_delete_resource")}
onOk={() => { onOk={() => {
Message.info({ Message.info({
content: t("delete_success"), content: t("deleting"),
}); });
resourceCtrl.deleteResource(value.id); resourceClient
setData(data.filter((_, i) => i !== index)); .deleteResource(value.url)
.then(() => {
Message.info({
content: t("delete_success"),
});
setData(data.filter((_, i) => i !== index));
})
.catch((e) => {
Message.error({
content: t("delete_failed") + ": " + e.message,
});
});
}} }}
> >
<Button type="text" iconOnly icon={<IconDelete />} /> <Button type="text" iconOnly icon={<IconDelete />} />
@ -154,7 +155,7 @@ const ScriptResource: React.FC<{
onOk={() => { onOk={() => {
setData((prev) => { setData((prev) => {
prev.forEach((v) => { prev.forEach((v) => {
resourceCtrl.deleteResource(v.id); resourceClient.deleteResource(v.url);
}); });
Message.info({ Message.info({
content: t("clear_success"), content: t("clear_success"),

View File

@ -1,31 +1,24 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Script } from "@App/app/repo/scripts"; import { Script, ScriptDAO } from "@App/app/repo/scripts";
import { import { Space, Popconfirm, Button, Divider, Typography, Modal, Input } from "@arco-design/web-react";
Space,
Popconfirm,
Button,
Divider,
Typography,
Modal,
Input,
} from "@arco-design/web-react";
import Table, { ColumnProps } from "@arco-design/web-react/es/Table"; import Table, { ColumnProps } from "@arco-design/web-react/es/Table";
import { IconDelete } from "@arco-design/web-react/icon"; import { IconDelete } from "@arco-design/web-react/icon";
import { scriptClient } from "@App/pages/store/features/script";
type MatchItem = { type MatchItem = {
// id是为了避免match重复 // id是为了避免match重复
id: number; id: number;
match: string; match: string;
self: boolean; byUser: boolean;
hasMatch: boolean; hasMatch: boolean; // 是否已经匹配
isExclude: boolean; isExclude: boolean; // 是否是排除项
}; };
const Match: React.FC<{ const Match: React.FC<{
script: Script; script: Script;
}> = ({ script }) => { }> = ({ script }) => {
// const scriptCtrl = IoC.instance(ScriptController) as ScriptController; const scriptDAO = new ScriptDAO();
const [match, setMatch] = useState<MatchItem[]>([]); const [match, setMatch] = useState<MatchItem[]>([]);
const [exclude, setExclude] = useState<MatchItem[]>([]); const [exclude, setExclude] = useState<MatchItem[]>([]);
const [matchValue, setMatchValue] = useState<string>(""); const [matchValue, setMatchValue] = useState<string>("");
@ -37,7 +30,7 @@ const Match: React.FC<{
useEffect(() => { useEffect(() => {
if (script) { if (script) {
// 从数据库中获取是简单处理数据一致性的问题 // 从数据库中获取是简单处理数据一致性的问题
scriptCtrl.scriptDAO.findById(script.id).then((res) => { scriptDAO.get(script.uuid).then((res) => {
if (!res) { if (!res) {
return; return;
} }
@ -48,28 +41,17 @@ const Match: React.FC<{
}); });
const v: MatchItem[] = []; const v: MatchItem[] = [];
matchArr.forEach((value, index) => { matchArr.forEach((value, index) => {
if (matchMap.has(value)) { v.push({
v.push({ id: index,
id: index, match: value,
match: value, byUser: !matchMap.has(value),
self: false, hasMatch: false,
hasMatch: false, isExclude: false,
isExclude: false, });
});
} else {
v.push({
id: index,
match: value,
self: true,
hasMatch: false,
isExclude: false,
});
}
}); });
setMatch(v); setMatch(v);
const excludeArr = const excludeArr = res.selfMetadata?.exclude || res.metadata.exclude || [];
res.selfMetadata?.exclude || res.metadata.exclude || [];
const excludeMap = new Map<string, boolean>(); const excludeMap = new Map<string, boolean>();
res.metadata.exclude?.forEach((m) => { res.metadata.exclude?.forEach((m) => {
excludeMap.set(m, true); excludeMap.set(m, true);
@ -77,23 +59,13 @@ const Match: React.FC<{
const e: MatchItem[] = []; const e: MatchItem[] = [];
excludeArr.forEach((value, index) => { excludeArr.forEach((value, index) => {
const hasMatch = matchMap.has(value); const hasMatch = matchMap.has(value);
if (excludeMap.has(value)) { e.push({
e.push({ id: index,
id: index, match: value,
match: value, byUser: !excludeMap.has(value),
self: false, hasMatch,
hasMatch, isExclude: true,
isExclude: true, });
});
} else {
e.push({
id: index,
match: value,
self: true,
hasMatch,
isExclude: true,
});
}
}); });
setExclude(e); setExclude(e);
}); });
@ -108,8 +80,8 @@ const Match: React.FC<{
}, },
{ {
title: t("user_setting"), title: t("user_setting"),
dataIndex: "self", dataIndex: "byUser",
key: "self", key: "byUser",
width: 100, width: 100,
render(col) { render(col) {
if (col) { if (col) {
@ -125,23 +97,24 @@ const Match: React.FC<{
return ( return (
<Space> <Space>
<Popconfirm <Popconfirm
title={`${t("confirm_delete_exclude")}${ title={`${t("confirm_delete_exclude")}${item.hasMatch ? ` ${t("after_deleting_match_item")}` : ""}`}
item.hasMatch ? ` ${t("after_deleting_match_item")}` : ""
}`}
onOk={() => { onOk={() => {
exclude.splice(exclude.indexOf(item), 1); exclude.splice(exclude.indexOf(item), 1);
scriptCtrl // 删除所有排除
scriptClient
.resetExclude( .resetExclude(
script.id, script.uuid,
exclude.map((m) => m.match) exclude.map((m) => m.match)
) )
.then(() => { .then(() => {
setExclude([...exclude]); setExclude([...exclude]);
// 如果包含在里面再加回match
if (item.hasMatch) { if (item.hasMatch) {
match.push(item); match.push(item);
scriptCtrl // 重置匹配
scriptClient
.resetMatch( .resetMatch(
script.id, script.uuid,
match.map((m) => m.match) match.map((m) => m.match)
) )
.then(() => { .then(() => {
@ -159,24 +132,22 @@ const Match: React.FC<{
return ( return (
<Space> <Space>
<Popconfirm <Popconfirm
title={`${t("confirm_delete_match")}${ title={`${t("confirm_delete_match")}${item.byUser ? "" : ` ${t("after_deleting_exclude_item")}`}`}
item.self ? "" : ` ${t("after_deleting_exclude_item")}`
}`}
onOk={() => { onOk={() => {
match.splice(match.indexOf(item), 1); match.splice(match.indexOf(item), 1);
scriptCtrl scriptClient
.resetMatch( .resetMatch(
script.id, script.uuid,
match.map((m) => m.match) match.map((m) => m.match)
) )
.then(() => { .then(() => {
setMatch([...match]); setMatch([...match]);
// 添加到exclue // 添加到exclue
if (!item.self) { if (!item.byUser) {
exclude.push(item); exclude.push(item);
scriptCtrl scriptClient
.resetExclude( .resetExclude(
script.id, script.uuid,
exclude.map((m) => m.match) exclude.map((m) => m.match)
) )
.then(() => { .then(() => {
@ -205,13 +176,13 @@ const Match: React.FC<{
match.push({ match.push({
id: Math.random(), id: Math.random(),
match: matchValue, match: matchValue,
self: true, byUser: true,
hasMatch: false, hasMatch: false,
isExclude: false, isExclude: false,
}); });
scriptCtrl scriptClient
.resetMatch( .resetMatch(
script.id, script.uuid,
match.map((m) => m.match) match.map((m) => m.match)
) )
.then(() => { .then(() => {
@ -237,13 +208,13 @@ const Match: React.FC<{
exclude.push({ exclude.push({
id: Math.random(), id: Math.random(),
match: excludeValue, match: excludeValue,
self: true, byUser: true,
hasMatch: false, hasMatch: false,
isExclude: true, isExclude: true,
}); });
scriptCtrl scriptClient
.resetExclude( .resetExclude(
script.id, script.uuid,
exclude.map((m) => m.match) exclude.map((m) => m.match)
) )
.then(() => { .then(() => {
@ -276,7 +247,7 @@ const Match: React.FC<{
<Popconfirm <Popconfirm
title={t("confirm_reset")} title={t("confirm_reset")}
onOk={() => { onOk={() => {
scriptCtrl.resetMatch(script.id, undefined).then(() => { scriptClient.resetMatch(script.uuid, undefined).then(() => {
setMatch([]); setMatch([]);
}); });
}} }}
@ -305,7 +276,7 @@ const Match: React.FC<{
<Popconfirm <Popconfirm
title={t("confirm_reset")} title={t("confirm_reset")}
onOk={() => { onOk={() => {
scriptCtrl.resetExclude(script.id, undefined).then(() => { scriptClient.resetExclude(script.uuid, undefined).then(() => {
setExclude([]); setExclude([]);
}); });
}} }}

View File

@ -2,26 +2,14 @@ import React, { useEffect, useState } from "react";
import { Permission } from "@App/app/repo/permission"; import { Permission } from "@App/app/repo/permission";
import { Script } from "@App/app/repo/scripts"; import { Script } from "@App/app/repo/scripts";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import { Space, Popconfirm, Message, Button, Checkbox, Input, Modal, Select, Typography } from "@arco-design/web-react";
Space,
Popconfirm,
Message,
Button,
Checkbox,
Input,
Modal,
Select,
Typography,
} from "@arco-design/web-react";
import Table, { ColumnProps } from "@arco-design/web-react/es/Table"; import Table, { ColumnProps } from "@arco-design/web-react/es/Table";
import { IconDelete } from "@arco-design/web-react/icon"; import { IconDelete } from "@arco-design/web-react/icon";
import { permissionClient } from "@App/pages/store/features/script";
const PermissionManager: React.FC<{ const PermissionManager: React.FC<{
script: Script; script: Script;
}> = ({ script }) => { }> = ({ script }) => {
// const permissionCtrl = IoC.instance(
// PermissionController
// ) as PermissionController;
const [permission, setPermission] = useState<Permission[]>([]); const [permission, setPermission] = useState<Permission[]>([]);
const [permissionVisible, setPermissionVisible] = useState<boolean>(false); const [permissionVisible, setPermissionVisible] = useState<boolean>(false);
const [permissionValue, setPermissionValue] = useState<Permission>(); const [permissionValue, setPermissionValue] = useState<Permission>();
@ -59,14 +47,15 @@ const PermissionManager: React.FC<{
<Popconfirm <Popconfirm
title={t("confirm_delete_permission")} title={t("confirm_delete_permission")}
onOk={() => { onOk={() => {
permissionCtrl permissionClient
.deletePermission(script!.id, { .deletePermission(script.uuid, item.permission, item.permissionValue)
permission: item.permission,
permissionValue: item.permissionValue,
})
.then(() => { .then(() => {
Message.success(t("delete_success")!); Message.success(t("delete_success")!);
setPermission(permission.filter((i) => i.id !== item.id)); setPermission(
permission.filter(
(i) => !(i.permission == item.permission && i.permissionValue == item.permissionValue)
)
);
}) })
.catch(() => { .catch(() => {
Message.error(t("delete_failed")!); Message.error(t("delete_failed")!);
@ -83,7 +72,7 @@ const PermissionManager: React.FC<{
useEffect(() => { useEffect(() => {
if (script) { if (script) {
permissionCtrl.getPermissions(script.id).then((list) => { permissionClient.getScriptPermissions(script.uuid).then((list) => {
setPermission(list); setPermission(list);
}); });
} }
@ -98,20 +87,17 @@ const PermissionManager: React.FC<{
onOk={() => { onOk={() => {
if (permissionValue) { if (permissionValue) {
permission.push({ permission.push({
id: 0, uuid: script.uuid,
uuid: script.id,
permission: permissionValue.permission, permission: permissionValue.permission,
permissionValue: permissionValue.permissionValue, permissionValue: permissionValue.permissionValue,
allow: permissionValue.allow, allow: permissionValue.allow,
createtime: new Date().getTime(), createtime: new Date().getTime(),
updatetime: 0, updatetime: 0,
}); });
permissionCtrl permissionClient.addPermission(permissionValue).then(() => {
.addPermission(script.id, permissionValue) setPermission([...permission]);
.then(() => { setPermissionVisible(false);
setPermission([...permission]); });
setPermissionVisible(false);
});
} }
}} }}
> >
@ -119,27 +105,22 @@ const PermissionManager: React.FC<{
<Select <Select
value={permissionValue?.permission} value={permissionValue?.permission}
onChange={(e) => { onChange={(e) => {
permissionValue && permissionValue && setPermissionValue({ ...permissionValue, permission: e });
setPermissionValue({ ...permissionValue, permission: e });
}} }}
> >
<Select.Option value="cors">{t("permission_cors")}</Select.Option> <Select.Option value="cors">{t("permission_cors")}</Select.Option>
<Select.Option value="cookie"> <Select.Option value="cookie">{t("permission_cookie")}</Select.Option>
{t("permission_cookie")}
</Select.Option>
</Select> </Select>
<Input <Input
value={permissionValue?.permissionValue} value={permissionValue?.permissionValue}
onChange={(e) => { onChange={(e) => {
permissionValue && permissionValue && setPermissionValue({ ...permissionValue, permissionValue: e });
setPermissionValue({ ...permissionValue, permissionValue: e });
}} }}
/> />
<Checkbox <Checkbox
checked={permissionValue?.allow} checked={permissionValue?.allow}
onChange={(e) => { onChange={(e) => {
permissionValue && permissionValue && setPermissionValue({ ...permissionValue, allow: e });
setPermissionValue({ ...permissionValue, allow: e });
}} }}
> >
{t("allow")} {t("allow")}
@ -147,17 +128,14 @@ const PermissionManager: React.FC<{
</Space> </Space>
</Modal> </Modal>
<div className="flex flex-row justify-between pb-2"> <div className="flex flex-row justify-between pb-2">
<Typography.Title heading={6}> <Typography.Title heading={6}>{t("permission_management")}</Typography.Title>
{t("permission_management")}
</Typography.Title>
<Space> <Space>
<Button <Button
type="primary" type="primary"
size="small" size="small"
onClick={() => { onClick={() => {
setPermissionValue({ setPermissionValue({
id: 0, uuid: script.uuid,
uuid: script.id,
permission: "cors", permission: "cors",
permissionValue: "", permissionValue: "",
allow: true, allow: true,
@ -172,7 +150,7 @@ const PermissionManager: React.FC<{
<Popconfirm <Popconfirm
title={t("confirm_reset")} title={t("confirm_reset")}
onOk={() => { onOk={() => {
permissionCtrl.resetPermission(script.id).then(() => { permissionClient.resetPermission(script.uuid).then(() => {
setPermission([]); setPermission([]);
}); });
}} }}
@ -183,12 +161,7 @@ const PermissionManager: React.FC<{
</Popconfirm> </Popconfirm>
</Space> </Space>
</div> </div>
<Table <Table columns={columns} data={permission} rowKey="id" pagination={false} />
columns={columns}
data={permission}
rowKey="id"
pagination={false}
/>
</> </>
); );
}; };

View File

@ -1,13 +1,6 @@
import { Script } from "@App/app/repo/scripts"; import { Script, ScriptDAO } from "@App/app/repo/scripts";
import { formatUnixTime } from "@App/pkg/utils/utils"; import { formatUnixTime } from "@App/pkg/utils/utils";
import { import { Descriptions, Divider, Drawer, Empty, Input, Message } from "@arco-design/web-react";
Descriptions,
Divider,
Drawer,
Empty,
Input,
Message,
} from "@arco-design/web-react";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Match from "./Match"; import Match from "./Match";
@ -19,14 +12,14 @@ const ScriptSetting: React.FC<{
onOk: () => void; onOk: () => void;
onCancel: () => void; onCancel: () => void;
}> = ({ script, visible, onCancel, onOk }) => { }> = ({ script, visible, onCancel, onOk }) => {
// const scriptCtrl = IoC.instance(ScriptController) as ScriptController; const scriptDAO = new ScriptDAO();
const [checkUpdateUrl, setCheckUpdateUrl] = useState<string>(""); const [checkUpdateUrl, setCheckUpdateUrl] = useState<string>("");
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => { useEffect(() => {
if (script) { if (script) {
scriptCtrl.scriptDAO.findById(script.id).then((v) => { scriptDAO.get(script.uuid).then((v) => {
setCheckUpdateUrl(v?.downloadUrl || ""); setCheckUpdateUrl(v?.downloadUrl || "");
}); });
} }
@ -56,9 +49,7 @@ const ScriptSetting: React.FC<{
data={[ data={[
{ {
label: t("last_updated"), label: t("last_updated"),
value: formatUnixTime( value: formatUnixTime((script?.updatetime || script?.createtime || 0) / 1000),
(script?.updatetime || script?.createtime || 0) / 1000
),
}, },
{ {
label: "UUID", label: "UUID",
@ -83,8 +74,8 @@ const ScriptSetting: React.FC<{
setCheckUpdateUrl(e); setCheckUpdateUrl(e);
}} }}
onBlur={() => { onBlur={() => {
scriptCtrl scriptDAO
.updateCheckUpdateUrl(script!.id, checkUpdateUrl) .update(script.uuid, { downloadUrl: checkUpdateUrl, checkUpdateUrl: checkUpdateUrl })
.then(() => { .then(() => {
Message.success(t("update_success")!); Message.success(t("update_success")!);
}); });

View File

@ -1,5 +1,6 @@
import { Script } from "@App/app/repo/scripts"; import { Script } from "@App/app/repo/scripts";
import { Value } from "@App/app/repo/value"; import { Value } from "@App/app/repo/value";
import { valueClient } from "@App/pages/store/features/script";
import { valueType } from "@App/pkg/utils/utils"; import { valueType } from "@App/pkg/utils/utils";
import { Button, Drawer, Form, Input, Message, Modal, Popconfirm, Select, Space, Table } from "@arco-design/web-react"; import { Button, Drawer, Form, Input, Message, Modal, Popconfirm, Select, Space, Table } from "@arco-design/web-react";
import { RefInputType } from "@arco-design/web-react/es/Input/interface"; import { RefInputType } from "@arco-design/web-react/es/Input/interface";
@ -10,16 +11,20 @@ import { useTranslation } from "react-i18next";
const FormItem = Form.Item; const FormItem = Form.Item;
interface ValueModel {
key: string;
value: any;
}
const ScriptStorage: React.FC<{ const ScriptStorage: React.FC<{
// eslint-disable-next-line react/require-default-props
script?: Script; script?: Script;
visible: boolean; visible: boolean;
onOk: () => void; onOk: () => void;
onCancel: () => void; onCancel: () => void;
}> = ({ script, visible, onCancel, onOk }) => { }> = ({ script, visible, onCancel, onOk }) => {
const [data, setData] = useState<Value[]>([]); const [data, setData] = useState<ValueModel[]>([]);
const inputRef = useRef<RefInputType>(null); const inputRef = useRef<RefInputType>(null);
const [currentValue, setCurrentValue] = useState<Value>(); const [currentValue, setCurrentValue] = useState<ValueModel>();
const [visibleEdit, setVisibleEdit] = useState(false); const [visibleEdit, setVisibleEdit] = useState(false);
const [form] = Form.useForm(); const [form] = Form.useForm();
const { t } = useTranslation(); const { t } = useTranslation();
@ -28,31 +33,13 @@ const ScriptStorage: React.FC<{
if (!script) { if (!script) {
return () => {}; return () => {};
} }
// valueCtrl.getValues(script).then((values) => { valueClient.getScriptValue(script).then((value) => {
// setData(values); setData(
// }); Object.keys(value).map((key) => {
// Monitor value changes return { key: key, value: value[key] };
// const channel = valueCtrl.watchValue(script); })
// channel.setHandler((value: Value) => { );
// setData((prev) => { });
// const index = prev.findIndex((item) => item.key === value.key);
// if (index === -1) {
// if (value.value === undefined) {
// return prev;
// }
// return [value, ...prev];
// }
// if (value.value === undefined) {
// prev.splice(index, 1);
// return [...prev];
// }
// prev[index] = value;
// return [...prev];
// });
// });
return () => {
// channel.disChannel();
};
}, [script]); }, [script]);
const columns: ColumnProps[] = [ const columns: ColumnProps[] = [
{ {
@ -61,7 +48,6 @@ const ScriptStorage: React.FC<{
key: "key", key: "key",
filterIcon: <IconSearch />, filterIcon: <IconSearch />,
width: 140, width: 140,
// eslint-disable-next-line react/no-unstable-nested-components
filterDropdown: ({ filterKeys, setFilterKeys, confirm }: any) => { filterDropdown: ({ filterKeys, setFilterKeys, confirm }: any) => {
return ( return (
<div className="arco-table-custom-filter"> <div className="arco-table-custom-filter">
@ -120,7 +106,7 @@ const ScriptStorage: React.FC<{
}, },
{ {
title: t("action"), title: t("action"),
render(_col, value: Value, index) { render(_col, value: { key: string; value: string }, index) {
return ( return (
<Space> <Space>
<Button <Button
@ -136,7 +122,7 @@ const ScriptStorage: React.FC<{
iconOnly iconOnly
icon={<IconDelete />} icon={<IconDelete />}
onClick={() => { onClick={() => {
valueCtrl.setValue(script!.id, value.key, undefined); valueClient.setScriptValue(script!.uuid, value.key, undefined);
Message.info({ Message.info({
content: t("delete_success"), content: t("delete_success"),
}); });
@ -179,7 +165,7 @@ const ScriptStorage: React.FC<{
default: default:
break; break;
} }
valueCtrl.setValue(script!.id, value.key, value.value); valueClient.setScriptValue(script!.uuid, value.key, value.value);
if (currentValue) { if (currentValue) {
Message.info({ Message.info({
content: t("update_success"), content: t("update_success"),
@ -201,13 +187,8 @@ const ScriptStorage: React.FC<{
}); });
setData([ setData([
{ {
id: 0,
scriptId: script!.id,
storageName: (script?.metadata.storagename && script?.metadata.storagename[0]) || "",
key: value.key, key: value.key,
value: value.value, value: value.value,
createtime: Date.now(),
updatetime: 0,
}, },
...data, ...data,
]); ]);
@ -254,7 +235,7 @@ const ScriptStorage: React.FC<{
onOk={() => { onOk={() => {
setData((prev) => { setData((prev) => {
prev.forEach((v) => { prev.forEach((v) => {
valueCtrl.setValue(script!.id, v.key, undefined); valueClient.setScriptValue(script!.uuid, v.key, undefined);
}); });
Message.info({ Message.info({
content: t("clear_success"), content: t("clear_success"),

View File

@ -8,12 +8,12 @@ import "@App/index.css";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import { store } from "@App/pages/store/store.ts"; import { store } from "@App/pages/store/store.ts";
import LoggerCore from "@App/app/logger/core.ts"; import LoggerCore from "@App/app/logger/core.ts";
import { LoggerDAO } from "@App/app/repo/logger.ts"; import MessageWriter from "@App/app/logger/message_writer.ts";
import DBWriter from "@App/app/logger/db_writer.ts"; import { message } from "../store/global.ts";
// 初始化日志组件 // 初始化日志组件
const loggerCore = new LoggerCore({ const loggerCore = new LoggerCore({
writer: new DBWriter(new LoggerDAO()), writer: new MessageWriter(message),
labels: { env: "confirm" }, labels: { env: "confirm" },
}); });

View File

@ -8,12 +8,12 @@ import "@App/index.css";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import { store } from "@App/pages/store/store.ts"; import { store } from "@App/pages/store/store.ts";
import LoggerCore from "@App/app/logger/core.ts"; import LoggerCore from "@App/app/logger/core.ts";
import { LoggerDAO } from "@App/app/repo/logger.ts"; import MessageWriter from "@App/app/logger/message_writer.ts";
import DBWriter from "@App/app/logger/db_writer.ts"; import { message } from "../store/global.ts";
// 初始化日志组件 // 初始化日志组件
const loggerCore = new LoggerCore({ const loggerCore = new LoggerCore({
writer: new DBWriter(new LoggerDAO()), writer: new MessageWriter(message),
labels: { env: "import" }, labels: { env: "import" },
}); });

View File

@ -8,12 +8,12 @@ import "@App/index.css";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import { store } from "@App/pages/store/store.ts"; import { store } from "@App/pages/store/store.ts";
import LoggerCore from "@App/app/logger/core.ts"; import LoggerCore from "@App/app/logger/core.ts";
import { LoggerDAO } from "@App/app/repo/logger.ts"; import MessageWriter from "@App/app/logger/message_writer.ts";
import DBWriter from "@App/app/logger/db_writer.ts"; import { message } from "../store/global.ts";
// 初始化日志组件 // 初始化日志组件
const loggerCore = new LoggerCore({ const loggerCore = new LoggerCore({
writer: new DBWriter(new LoggerDAO()), writer: new MessageWriter(message),
labels: { env: "install" }, labels: { env: "install" },
}); });

View File

@ -13,8 +13,12 @@ import { LoggerDAO } from "@App/app/repo/logger.ts";
import DBWriter from "@App/app/logger/db_writer.ts"; import DBWriter from "@App/app/logger/db_writer.ts";
import registerEditor from "@App/pkg/utils/monaco-editor.ts"; import registerEditor from "@App/pkg/utils/monaco-editor.ts";
import storeSubscribe from "../store/subscribe.ts"; import storeSubscribe from "../store/subscribe.ts";
import migrate from "@App/app/migrate.ts";
migrate();
registerEditor(); registerEditor();
// 初始化日志组件 // 初始化日志组件
const loggerCore = new LoggerCore({ const loggerCore = new LoggerCore({
writer: new DBWriter(new LoggerDAO()), writer: new DBWriter(new LoggerDAO()),

View File

@ -81,6 +81,8 @@ import {
requestStopScript, requestStopScript,
requestRunScript, requestRunScript,
scriptClient, scriptClient,
enableLoading,
updateEnableStatus,
} from "@App/pages/store/features/script"; } from "@App/pages/store/features/script";
import { message, systemConfig } from "@App/pages/store/global"; import { message, systemConfig } from "@App/pages/store/global";
import { SynchronizeClient, ValueClient } from "@App/app/service/service_worker/client"; import { SynchronizeClient, ValueClient } from "@App/app/service/service_worker/client";
@ -615,6 +617,7 @@ function ScriptList() {
const dealColumns: ColumnProps[] = []; const dealColumns: ColumnProps[] = [];
newColumns.forEach((item) => { newColumns.forEach((item) => {
console.log(newColumns);
switch (item.width) { switch (item.width) {
case -1: case -1:
break; break;
@ -625,37 +628,39 @@ function ScriptList() {
}); });
const sortIndex = dealColumns.findIndex((item) => item.key === "sort"); const sortIndex = dealColumns.findIndex((item) => item.key === "sort");
let SortableItem;
if (sortIndex !== -1) {
SortableItem = (props: any) => {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: props!.record.uuid });
const SortableItem = (props: any) => { const style = {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: props!.record.uuid }); transform: CSS.Transform.toString(transform),
transition,
};
const style = { // 替换排序列,使其可以拖拽
transform: CSS.Transform.toString(transform), props.children[sortIndex + 1] = (
transition, <td
className="arco-table-td"
style={{
textAlign: "center",
}}
key="drag"
>
<div className="arco-table-cell">
<IconMenu
style={{
cursor: "move",
}}
{...listeners}
/>
</div>
</td>
);
return <tr ref={setNodeRef} style={style} {...attributes} {...props} />;
}; };
}
// 替换排序列,使其可以拖拽
props.children[sortIndex + 1] = (
<td
className="arco-table-td"
style={{
textAlign: "center",
}}
key="drag"
>
<div className="arco-table-cell">
<IconMenu
style={{
cursor: "move",
}}
{...listeners}
/>
</div>
</td>
);
return <tr ref={setNodeRef} style={style} {...attributes} {...props} />;
};
const components: ComponentsProps = { const components: ComponentsProps = {
table: React.forwardRef(SortableWrapper), table: React.forwardRef(SortableWrapper),
@ -703,19 +708,23 @@ function ScriptList() {
type="primary" type="primary"
size="mini" size="mini"
onClick={() => { onClick={() => {
const uuids: string[] = []; const enableAction = (enable: boolean) => {
const uuids = select.map((item) => item.uuid);
dispatch(enableLoading({ uuids: uuids, loading: true }));
Promise.allSettled(uuids.map((uuid) => scriptClient.enable(uuid, enable))).finally(() => {
dispatch(updateEnableStatus({ uuids: uuids, enable: enable }));
dispatch(enableLoading({ uuids: uuids, loading: false }));
});
};
switch (action) { switch (action) {
case "enable": case "enable":
select.forEach((item) => { enableAction(true);
dispatch(requestEnableScript({ uuid: item.uuid, enable: true }));
});
break; break;
case "disable": case "disable":
select.forEach((item) => { enableAction(false);
dispatch(requestEnableScript({ uuid: item.uuid, enable: false }));
});
break; break;
case "export": case "export":
const uuids: string[] = [];
select.forEach((item) => { select.forEach((item) => {
uuids.push(item.uuid); uuids.push(item.uuid);
}); });

View File

@ -30,7 +30,7 @@ function Tools() {
useEffect(() => { useEffect(() => {
// 获取配置 // 获取配置
const loadConfig = async () => { const loadConfig = async () => {
const [backup, vscodeUrl] = await Promise.all([ const [backup, vscodeUrl, vscodeReconnect] = await Promise.all([
systemConfig.getBackup(), systemConfig.getBackup(),
systemConfig.getVscodeUrl(), systemConfig.getVscodeUrl(),
systemConfig.getVscodeReconnect(), systemConfig.getVscodeReconnect(),
@ -38,7 +38,7 @@ function Tools() {
setFilesystemType(backup.filesystem); setFilesystemType(backup.filesystem);
setFilesystemParam(backup.params[backup.filesystem] || {}); setFilesystemParam(backup.params[backup.filesystem] || {});
setVscodeUrl(vscodeUrl); setVscodeUrl(vscodeUrl);
setVscodeReconnect(systemConfig.vscodeReconnect); setVscodeReconnect(vscodeReconnect);
}; };
loadConfig(); loadConfig();
}, []); }, []);

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 CodeEditor from "@App/pages/components/CodeEditor";
import React, { useCallback, useEffect, useRef, useState } from "react"; import React, { useCallback, useEffect, useRef, useState } from "react";
import { useNavigate, useParams, useSearchParams } from "react-router-dom"; 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 ScriptStorage from "@App/pages/components/ScriptStorage";
import ScriptResource from "@App/pages/components/ScriptResource"; import ScriptResource from "@App/pages/components/ScriptResource";
import ScriptSetting from "@App/pages/components/ScriptSetting"; 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 { i18nName } from "@App/locales/locales";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -32,11 +32,12 @@ type HotKey = {
const Editor: React.FC<{ const Editor: React.FC<{
id: string; id: string;
script: ScriptAndCode; script: Script;
code: string;
hotKeys: HotKey[]; hotKeys: HotKey[];
callbackEditor: (e: editor.IStandaloneCodeEditor) => void; callbackEditor: (e: editor.IStandaloneCodeEditor) => void;
onChange: (code: string) => void; onChange: (code: string) => void;
}> = ({ id, script, hotKeys, callbackEditor, onChange }) => { }> = ({ id, script, code, hotKeys, callbackEditor, onChange }) => {
const [node, setNode] = useState<{ editor: editor.IStandaloneCodeEditor }>(); const [node, setNode] = useState<{ editor: editor.IStandaloneCodeEditor }>();
const ref = useCallback<(node: { editor: editor.IStandaloneCodeEditor }) => void>( const ref = useCallback<(node: { editor: editor.IStandaloneCodeEditor }) => void>(
(inlineNode) => { (inlineNode) => {
@ -77,7 +78,7 @@ const Editor: React.FC<{
}; };
}, [node?.editor]); }, [node?.editor]);
return <CodeEditor key={id} id={id} ref={ref} code={script.code} diffCode="" editable />; return <CodeEditor key={id} id={id} ref={ref} code={code} diffCode="" editable />;
}; };
const WarpEditor = React.memo(Editor, (prev, next) => { const WarpEditor = React.memo(Editor, (prev, next) => {
@ -154,7 +155,8 @@ function ScriptEditor() {
const [visible, setVisible] = useState<{ [key: string]: boolean }>({}); const [visible, setVisible] = useState<{ [key: string]: boolean }>({});
const [editors, setEditors] = useState< const [editors, setEditors] = useState<
{ {
script: ScriptAndCode; script: Script;
code: string;
active: boolean; active: boolean;
hotKeys: HotKey[]; hotKeys: HotKey[];
editor?: editor.IStandaloneCodeEditor; editor?: editor.IStandaloneCodeEditor;
@ -186,57 +188,58 @@ function ScriptEditor() {
const save = (script: Script, e: editor.IStandaloneCodeEditor): Promise<Script> => { const save = (script: Script, e: editor.IStandaloneCodeEditor): Promise<Script> => {
// 解析code生成新的script并更新 // 解析code生成新的script并更新
return new Promise(() => { return prepareScriptByCode(e.getValue(), script.origin || "", script.uuid)
prepareScriptByCode(e.getValue(), script.origin || "", script.uuid) .then((prepareScript) => {
.then((prepareScript) => { const newScript = prepareScript.script;
const newScript = prepareScript.script; if (!newScript.name) {
if (!newScript.name) { Message.warning(t("script_name_cannot_be_set_to_empty"));
Message.warning(t("script_name_cannot_be_set_to_empty")); return Promise.reject(new Error("script name cannot be empty"));
return; }
} return scriptClient
scriptClient.install(newScript, e.getValue()).then( .install(newScript, e.getValue())
(update) => { .then((update): Script => {
if (!update) { if (!update) {
Message.success("新建成功,请注意后台脚本不会默认开启"); Message.success("新建成功,请注意后台脚本不会默认开启");
// 保存的时候如何左侧没有脚本即新建 // 保存的时候如何左侧没有脚本即新建
setScriptList((prev) => { setScriptList((prev) => {
setSelectSciptButtonAndTab(newScript.uuid); setSelectSciptButtonAndTab(newScript.uuid);
return [newScript, ...prev]; return [newScript, ...prev];
}); });
} else { } else {
setScriptList((prev) => { setScriptList((prev) => {
// eslint-disable-next-line no-shadow, array-callback-return prev.map((script: Script) => {
prev.map((script: Script) => { if (script.uuid === newScript.uuid) {
if (script.uuid === newScript.uuid) { script.name = newScript.name;
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].script.code = prepareScript.scriptCode;
prev[i].isChanged = false;
prev[i].script.name = newScript.name;
break;
} }
} });
return [...prev]; return [...prev];
}); });
}, Message.success("保存成功");
(err: any) => {
Message.error(`保存失败: ${err}`);
} }
); setEditors((prev) => {
}) for (let i = 0; i < prev.length; i += 1) {
.catch((err) => { if (prev[i].script.uuid === newScript.uuid) {
Message.error(`错误的脚本代码: ${err}`); 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) => { const saveAs = (script: Script, e: editor.IStandaloneCodeEditor) => {
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
chrome.downloads.download( chrome.downloads.download(
@ -287,30 +290,35 @@ function ScriptEditor() {
title: t("run"), title: t("run"),
items: [ items: [
{ {
id: "debug", id: "run",
title: t("debug"), title: t("run"),
hotKey: KeyMod.CtrlCmd | KeyCode.F5, hotKey: KeyMod.CtrlCmd | KeyCode.F5,
hotKeyString: "Ctrl+F5", hotKeyString: "Ctrl+F5",
tooltip: "只有后台脚本/定时脚本才能调试, 且调试模式下不对进行权限校验(例如@connect)", tooltip: "只有后台脚本/定时脚本才能运行",
action: async (script, e) => { action: async (script, e) => {
// 保存更新代码之后再调试 // 保存更新代码之后再调试
const newScript = await save(script, e); const newScript = await save(script, e);
// 判断脚本类型
if (newScript.type === SCRIPT_TYPE_NORMAL) {
Message.error("只有后台脚本/定时脚本才能运行");
return;
}
Message.loading({ Message.loading({
id: "debug_script", id: "debug_script",
content: "正在准备脚本资源...", content: "正在准备脚本资源...",
duration: 3000, duration: 3000,
}); });
runtimeCtrl runtimeClient
.debugScript(newScript) .runScript(newScript.uuid)
.then(() => { .then(() => {
Message.success({ Message.success({
id: "debug_script", id: "debug_script",
content: "构建成功, 可以打开开发者工具在控制台中查看输出", content: "构建成功, 可以在扩展页打开开发者工具在控制台中查看输出",
duration: 3000, duration: 3000,
}); });
}) })
.catch((err) => { .catch((err) => {
LoggerCore.getLogger(Logger.E(err)).debug("debug script error"); LoggerCore.logger(Logger.E(err)).debug("run script error");
Message.error({ Message.error({
id: "debug_script", id: "debug_script",
content: `构建失败: ${err}`, content: `构建失败: ${err}`,
@ -398,7 +406,8 @@ function ScriptEditor() {
}); });
} }
prev.push({ prev.push({
script: Object.assign(scripts[i], code), script: scripts[i],
code: code?.code || "",
active: true, active: true,
hotKeys, hotKeys,
isChanged: false, isChanged: false,
@ -725,7 +734,8 @@ function ScriptEditor() {
return; return;
} }
editors.push({ editors.push({
script: Object.assign(script, code), script,
code: code.code,
active: true, active: true,
hotKeys, hotKeys,
isChanged: false, isChanged: false,
@ -874,6 +884,7 @@ function ScriptEditor() {
key={`e_${item.script.uuid}`} key={`e_${item.script.uuid}`}
id={`e_${item.script.uuid}`} id={`e_${item.script.uuid}`}
script={item.script} script={item.script}
code={item.code}
hotKeys={item.hotKeys} hotKeys={item.hotKeys}
callbackEditor={(e) => { callbackEditor={(e) => {
setEditors((prev) => { setEditors((prev) => {
@ -886,7 +897,7 @@ function ScriptEditor() {
}); });
}} }}
onChange={(code) => { onChange={(code) => {
const isChanged = !(item.script.code === code); const isChanged = !(item.code === code);
if (isChanged !== item.isChanged) { if (isChanged !== item.isChanged) {
setEditors((prev) => { setEditors((prev) => {
prev.forEach((v) => { prev.forEach((v) => {

View File

@ -11,12 +11,14 @@ import {
IconSearch, IconSearch,
} from "@arco-design/web-react/icon"; } from "@arco-design/web-react/icon";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { RiMessage2Line } from "react-icons/ri"; import { RiMessage2Line, RiZzzFill } from "react-icons/ri";
import semver from "semver"; import semver from "semver";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import ScriptMenuList from "../components/ScriptMenuList"; import ScriptMenuList from "../components/ScriptMenuList";
import { popupClient } from "../store/features/script"; import { popupClient } from "../store/features/script";
import { ScriptMenu } from "@App/app/service/service_worker/popup"; import { ScriptMenu } from "@App/app/service/service_worker/popup";
import { systemConfig } from "../store/global";
import { isUserScriptsAvailable } from "@App/pkg/utils/utils";
const CollapseItem = Collapse.Item; const CollapseItem = Collapse.Item;
@ -30,11 +32,13 @@ function App() {
const [scriptList, setScriptList] = useState<ScriptMenu[]>([]); const [scriptList, setScriptList] = useState<ScriptMenu[]>([]);
const [backScriptList, setBackScriptList] = useState<ScriptMenu[]>([]); const [backScriptList, setBackScriptList] = useState<ScriptMenu[]>([]);
const [showAlert, setShowAlert] = useState(false); const [showAlert, setShowAlert] = useState(false);
const [notice, setNotice] = useState(""); const [checkUpdate, setCheckUpdate] = useState<Parameters<typeof systemConfig.setCheckUpdate>[0]>({
const [isRead, setIsRead] = useState(true); version: ExtVersion,
const [version, setVersion] = useState(ExtVersion); notice: "",
isRead: false,
});
const [currentUrl, setCurrentUrl] = useState(""); const [currentUrl, setCurrentUrl] = useState("");
const [isEnableScript, setIsEnableScript] = useState(localStorage.enable_script !== "false"); const [isEnableScript, setIsEnableScript] = useState(true);
const { t } = useTranslation(); const { t } = useTranslation();
let url: URL | undefined; let url: URL | undefined;
@ -45,22 +49,21 @@ function App() {
} }
useEffect(() => { useEffect(() => {
// systemManage.getNotice().then((res) => { const loadConfig = async () => {
// if (res) { const [isEnableScript, checkUpdate] = await Promise.all([
// setNotice(res.notice); systemConfig.getEnableScript(),
// setIsRead(res.isRead); systemConfig.getCheckUpdate(),
// } ]);
// }); setIsEnableScript(isEnableScript);
// systemManage.getVersion().then((res) => { setCheckUpdate(checkUpdate);
// res && setVersion(res); };
// }); loadConfig();
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (!tabs.length) { if (!tabs.length) {
return; return;
} }
setCurrentUrl(tabs[0].url || ""); setCurrentUrl(tabs[0].url || "");
popupClient.getPopupData({ url: tabs[0].url!, tabId: tabs[0].id! }).then((resp) => { popupClient.getPopupData({ url: tabs[0].url!, tabId: tabs[0].id! }).then((resp) => {
console.log(resp);
// 按照开启状态和更新时间排序 // 按照开启状态和更新时间排序
const list = resp.scriptList; const list = resp.scriptList;
list.sort((a, b) => { list.sort((a, b) => {
@ -82,141 +85,147 @@ function App() {
}); });
}, []); }, []);
return ( return (
<Card <>
size="small" {!isUserScriptsAvailable() && (
title={ <Alert type="warning" content={<div dangerouslySetInnerHTML={{ __html: t("develop_mode_guide") }} />} />
<div className="flex justify-between"> )}
<span className="text-xl">ScriptCat</span> <Card
<div className="flex flex-row items-center"> size="small"
<Switch title={
size="small" <div className="flex justify-between">
checked={isEnableScript} <span className="text-xl">ScriptCat</span>
onChange={(val) => { <div className="flex flex-row items-center">
setIsEnableScript(val); <Switch
if (val) { size="small"
localStorage.enable_script = "true"; checked={isEnableScript}
} else { onChange={(val) => {
localStorage.enable_script = "false"; setIsEnableScript(val);
} if (val) {
}} systemConfig.setEnableScript(true);
/> } else {
<Button systemConfig.setEnableScript(false);
type="text" }
icon={<IconHome />}
iconOnly
onClick={() => {
// 用a链接的方式,vivaldi竟然会直接崩溃
window.open("/src/options.html", "_blank");
}}
/>
<Badge count={isRead ? 0 : 1} dot offset={[-8, 6]}>
<Button
type="text"
icon={<IconNotification />}
iconOnly
onClick={() => {
setShowAlert(!showAlert);
setIsRead(true);
systemManage.setRead(true);
}} }}
/> />
</Badge> <Button
<Dropdown type="text"
droplist={ icon={<IconHome />}
<Menu iconOnly
style={{ onClick={() => {
maxHeight: "none", // 用a链接的方式,vivaldi竟然会直接崩溃
window.open("/src/options.html", "_blank");
}}
/>
<Badge count={checkUpdate.isRead ? 0 : 1} dot offset={[-8, 6]}>
<Button
type="text"
icon={<IconNotification />}
iconOnly
onClick={() => {
setShowAlert(!showAlert);
checkUpdate.isRead = true;
setCheckUpdate(checkUpdate);
systemConfig.setCheckUpdate(checkUpdate);
}} }}
onClickMenuItem={async (key) => { />
switch (key) { </Badge>
case "newScript": <Dropdown
await chrome.storage.local.set({ droplist={
activeTabUrl: { <Menu
url: currentUrl, style={{
}, maxHeight: "none",
}); }}
window.open("/src/options.html#/script/editor?target=initial", "_blank"); onClickMenuItem={async (key) => {
break; switch (key) {
default: case "newScript":
window.open(key, "_blank"); await chrome.storage.local.set({
break; activeTabUrl: {
} url: currentUrl,
}} },
> });
<Menu.Item key="newScript"> window.open("/src/options.html#/script/editor?target=initial", "_blank");
<IconPlus style={iconStyle} /> break;
{t("create_script")} default:
</Menu.Item> window.open(key, "_blank");
<Menu.Item key={`https://scriptcat.org/search?domain=${url && url.host}`}> break;
<IconSearch style={iconStyle} /> }
{t("get_script")} }}
</Menu.Item> >
<Menu.Item key="https://github.com/scriptscat/scriptcat/issues"> <Menu.Item key="newScript">
<IconBug style={iconStyle} /> <IconPlus style={iconStyle} />
{t("report_issue")} {t("create_script")}
</Menu.Item> </Menu.Item>
<Menu.Item key="https://docs.scriptcat.org/"> <Menu.Item key={`https://scriptcat.org/search?domain=${url && url.host}`}>
<IconBook style={iconStyle} /> <IconSearch style={iconStyle} />
{t("project_docs")} {t("get_script")}
</Menu.Item> </Menu.Item>
<Menu.Item key="https://bbs.tampermonkey.net.cn/"> <Menu.Item key="https://github.com/scriptscat/scriptcat/issues">
<RiMessage2Line style={iconStyle} /> <IconBug style={iconStyle} />
{t("community")} {t("report_issue")}
</Menu.Item> </Menu.Item>
<Menu.Item key="https://github.com/scriptscat/scriptcat"> <Menu.Item key="https://docs.scriptcat.org/">
<IconGithub style={iconStyle} /> <IconBook style={iconStyle} />
GitHub {t("project_docs")}
</Menu.Item> </Menu.Item>
</Menu> <Menu.Item key="https://bbs.tampermonkey.net.cn/">
} <RiMessage2Line style={iconStyle} />
trigger="click" {t("community")}
> </Menu.Item>
<Button type="text" icon={<IconMoreVertical />} iconOnly /> <Menu.Item key="https://github.com/scriptscat/scriptcat">
</Dropdown> <IconGithub style={iconStyle} />
GitHub
</Menu.Item>
</Menu>
}
trigger="click"
>
<Button type="text" icon={<IconMoreVertical />} iconOnly />
</Dropdown>
</div>
</div> </div>
</div> }
} bodyStyle={{ padding: 0 }}
bodyStyle={{ padding: 0 }} >
> <Alert
<Alert style={{ display: showAlert ? "flex" : "none" }}
style={{ marginBottom: 20, display: showAlert ? "flex" : "none" }} type="info"
type="info" content={<div dangerouslySetInnerHTML={{ __html: checkUpdate.notice || "" }} />}
content={<div dangerouslySetInnerHTML={{ __html: notice }} />} />
/> <Collapse bordered={false} defaultActiveKey={["script", "background"]} style={{ maxWidth: 640 }}>
<Collapse bordered={false} defaultActiveKey={["script", "background"]} style={{ maxWidth: 640 }}> <CollapseItem
<CollapseItem header={t("current_page_scripts")}
header={t("current_page_scripts")} name="script"
name="script" style={{ padding: "0" }}
style={{ padding: "0" }} contentStyle={{ padding: "0" }}
contentStyle={{ padding: "0" }}
>
<ScriptMenuList script={scriptList} isBackscript={false} currentUrl={currentUrl} />
</CollapseItem>
<CollapseItem
header={t("enabled_background_scripts")}
name="background"
style={{ padding: "0" }}
contentStyle={{ padding: "0" }}
>
<ScriptMenuList script={backScriptList} isBackscript currentUrl={currentUrl} />
</CollapseItem>
</Collapse>
<div className="flex flex-row arco-card-header !h-6">
<span className="text-[12px] font-500">{`v${ExtVersion}`}</span>
{semver.lt(ExtVersion, version) && (
<span
onClick={() => {
window.open(`https://github.com/scriptscat/scriptcat/releases/tag/v${version}`);
}}
className="text-1 font-500"
style={{ cursor: "pointer" }}
> >
{t("popup.new_version_available")} <ScriptMenuList script={scriptList} isBackscript={false} currentUrl={currentUrl} />
</span> </CollapseItem>
)}
</div> <CollapseItem
</Card> header={t("enabled_background_scripts")}
name="background"
style={{ padding: "0" }}
contentStyle={{ padding: "0" }}
>
<ScriptMenuList script={backScriptList} isBackscript currentUrl={currentUrl} />
</CollapseItem>
</Collapse>
<div className="flex flex-row arco-card-header !h-6">
<span className="text-[12px] font-500">{`v${ExtVersion}`}</span>
{semver.lt(ExtVersion, checkUpdate.version) && (
<span
onClick={() => {
window.open(`https://github.com/scriptscat/scriptcat/releases/tag/v${checkUpdate.version}`);
}}
className="text-1 font-500"
style={{ cursor: "pointer" }}
>
{t("popup.new_version_available")}
</span>
)}
</div>
</Card>
</>
); );
} }

View File

@ -2,18 +2,18 @@ import React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import App from "./App.tsx"; import App from "./App.tsx";
import LoggerCore from "@App/app/logger/core.ts"; import LoggerCore from "@App/app/logger/core.ts";
import { LoggerDAO } from "@App/app/repo/logger.ts";
import DBWriter from "@App/app/logger/db_writer.ts";
import "@arco-design/web-react/dist/css/arco.css"; import "@arco-design/web-react/dist/css/arco.css";
import "@App/locales/locales"; import "@App/locales/locales";
import "@App/index.css"; import "@App/index.css";
import "./index.css"; import "./index.css";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import { store } from "../store/store.ts"; import { store } from "../store/store.ts";
import MessageWriter from "@App/app/logger/message_writer.ts";
import { message } from "../store/global.ts";
// 初始化日志组件 // 初始化日志组件
const loggerCore = new LoggerCore({ const loggerCore = new LoggerCore({
writer: new DBWriter(new LoggerDAO()), writer: new MessageWriter(message),
labels: { env: "install" }, labels: { env: "install" },
}); });

View File

@ -102,6 +102,22 @@ export const scriptSlice = createAppSlice({
script.runStatus = action.payload.runStatus; script.runStatus = action.payload.runStatus;
} }
}, },
updateEnableStatus: (state, action: PayloadAction<{ uuids: string[]; enable: boolean }>) => {
state.scripts = state.scripts.map((s) => {
if (action.payload.uuids.includes(s.uuid)) {
s.status = action.payload.enable ? SCRIPT_STATUS_ENABLE : SCRIPT_STATUS_DISABLE;
}
return s;
});
},
enableLoading(state, action: PayloadAction<{ uuids: string[]; loading: boolean }>) {
state.scripts = state.scripts.map((s) => {
if (action.payload.uuids.includes(s.uuid)) {
s.enableLoading = action.payload.loading;
}
return s;
});
},
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder builder
@ -144,6 +160,6 @@ export const scriptSlice = createAppSlice({
}, },
}); });
export const { sortScript, upsertScript, deleteScript } = scriptSlice.actions; export const { sortScript, upsertScript, deleteScript, enableLoading, updateEnableStatus } = scriptSlice.actions;
export const { selectScripts } = scriptSlice.selectors; export const { selectScripts } = scriptSlice.selectors;

View File

@ -5,6 +5,7 @@ import { FileSystemType } from "@Packages/filesystem/factory";
import { MessageQueue } from "@Packages/message/message_queue"; import { MessageQueue } from "@Packages/message/message_queue";
import i18n from "@App/locales/locales"; import i18n from "@App/locales/locales";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { ExtVersion } from "@App/app/const";
export const SystamConfigChange = "systemConfigChange"; export const SystamConfigChange = "systemConfigChange";
@ -34,8 +35,11 @@ export class SystemConfig {
} }
addListener(key: string, callback: (value: any) => void) { addListener(key: string, callback: (value: any) => void) {
this.mq.subscribe(key, (msg) => { this.mq.subscribe(SystamConfigChange, (data: { key: string; value: string }) => {
const { value } = msg; if (data.key !== key) {
return;
}
const { value } = data;
callback(value); callback(value);
}); });
} }
@ -65,9 +69,7 @@ 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).then(() => { this.storage.set(key, val);
console.log(chrome.runtime.lastError, val);
});
// 发送消息通知更新 // 发送消息通知更新
this.mq.publish(SystamConfigChange, { this.mq.publish(SystamConfigChange, {
key, key,
@ -226,18 +228,19 @@ export class SystemConfig {
this.set("menu_expand_num", val); this.set("menu_expand_num", val);
} }
async getLanguage() { async getLanguage(acceptLanguages?: string[]): Promise<string> {
const defaultLanguage = await new Promise<string>((resolve) => { const defaultLanguage = await new Promise<string>(async (resolve) => {
chrome.i18n.getAcceptLanguages((lngs) => { if (!acceptLanguages) {
// 遍历数组寻找匹配语言 acceptLanguages = await chrome.i18n.getAcceptLanguages();
for (let i = 0; i < lngs.length; i += 1) { }
const lng = lngs[i]; // 遍历数组寻找匹配语言
if (i18n.hasResourceBundle(lng, "translation")) { for (let i = 0; i < acceptLanguages.length; i += 1) {
resolve(lng); const lng = acceptLanguages[i];
break; if (i18n.hasResourceBundle(lng, "translation")) {
} resolve(lng);
break;
} }
}); }
}); });
return this.get("language", defaultLanguage || chrome.i18n.getUILanguage()); return this.get("language", defaultLanguage || chrome.i18n.getUILanguage());
} }
@ -247,4 +250,28 @@ export class SystemConfig {
i18n.changeLanguage(value); i18n.changeLanguage(value);
dayjs.locale(value.toLocaleLowerCase()); 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,
});
}
setEnableScript(enable: boolean) {
this.set("enable_script", enable);
}
getEnableScript(): Promise<boolean> {
return this.get("enable_script", true);
}
} }

View File

@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { dealPatternMatches, parsePatternMatchesURL, UrlMatch } from "./match"; import { dealPatternMatches, parsePatternMatchesURL, UrlMatch } from "./match";
import path from "path";
// https://developer.chrome.com/docs/extensions/mv3/match_patterns/ // https://developer.chrome.com/docs/extensions/mv3/match_patterns/
describe("UrlMatch-google", () => { describe("UrlMatch-google", () => {
@ -39,16 +40,10 @@ describe("UrlMatch-google", () => {
describe("UrlMatch-google-error", () => { describe("UrlMatch-google-error", () => {
const url = new UrlMatch<string>(); const url = new UrlMatch<string>();
it("error-1", () => { it("error-1", () => {
expect(() => {
url.add("https://*foo/bar", "ok1");
}).toThrow(Error);
});
// 从v0.17.0开始允许这种
it("error-2", () => {
url.add("https://foo.*.bar/baz", "ok1"); url.add("https://foo.*.bar/baz", "ok1");
expect(url.match("https://foo.api.bar/baz")).toEqual(["ok1"]); expect(url.match("https://foo.api.bar/baz")).toEqual(["ok1"]);
}); });
it("error-3", () => { it("error-2", () => {
expect(() => { expect(() => {
url.add("http:/bar", "ok1"); url.add("http:/bar", "ok1");
}).toThrow(Error); }).toThrow(Error);
@ -77,6 +72,13 @@ describe("UrlMatch-search", () => {
expect(url.match("http://api.bar.example.com/")).toEqual(["ok1"]); expect(url.match("http://api.bar.example.com/")).toEqual(["ok1"]);
expect(url.match("http://api.example.com/")).toEqual([]); expect(url.match("http://api.example.com/")).toEqual([]);
}); });
it("*://example*/*/example.path*", () => {
const url = new UrlMatch<string>();
url.add("*://example*/*/example.path*", "ok1");
expect(url.match("https://example.com/foo/example.path")).toEqual(["ok1"]);
expect(url.match("https://example.com/foo/bar/example.path")).toEqual(["ok1"]);
expect(url.match("https://example.com/foo/bar/example.path2")).toEqual(["ok1"]);
});
}); });
describe("UrlMatch-port1", () => { describe("UrlMatch-port1", () => {
@ -177,6 +179,12 @@ describe("parsePatternMatchesURL", () => {
host: "127.0.0.1", host: "127.0.0.1",
path: "", path: "",
}); });
const matches4 = parsePatternMatchesURL("*://*/*");
expect(matches4).toEqual({
scheme: "*",
host: "*",
path: "*",
});
}); });
it("search", () => { it("search", () => {
// 会忽略掉search部分 // 会忽略掉search部分
@ -191,7 +199,7 @@ describe("parsePatternMatchesURL", () => {
const matches = parsePatternMatchesURL("*://www.example.com*"); const matches = parsePatternMatchesURL("*://www.example.com*");
expect(matches).toEqual({ expect(matches).toEqual({
scheme: "*", scheme: "*",
host: "www.example.com", host: "*",
path: "*", path: "*",
}); });
}); });
@ -203,4 +211,24 @@ describe("parsePatternMatchesURL", () => {
path: "*", path: "*",
}); });
}); });
it("一些怪异的情况", () => {
let matches = parsePatternMatchesURL("*://*./*");
expect(matches).toEqual({
scheme: "*",
host: "*",
path: "*",
});
matches = parsePatternMatchesURL("*://example*/*");
expect(matches).toEqual({
scheme: "*",
host: "*",
path: "*",
});
matches = parsePatternMatchesURL("http*://*.example.com/*");
expect(matches).toEqual({
scheme: "*",
host: "*.example.com",
path: "*",
});
});
}); });

View File

@ -64,20 +64,11 @@ export default class Match<T> {
let pos = u.host.indexOf("*"); let pos = u.host.indexOf("*");
if (u.host === "*" || u.host === "**") { if (u.host === "*" || u.host === "**") {
pos = -1; pos = -1;
} else if (u.host.endsWith("*")) {
// 处理*结尾
if (!u.host.endsWith(":*")) {
u.host = u.host.substring(0, u.host.length - 1);
}
} }
u.host = u.host.replace(/\*/g, "[^/]*?"); u.host = u.host.replace(/\*/g, "[^/]*?");
// 处理 *.开头 // 处理 *.开头
if (u.host.startsWith("[^/]*?.")) { if (u.host.startsWith("[^/]*?.")) {
u.host = `([^/]*?\\.?)${u.host.substring(7)}`; u.host = `([^/]*?\\.?)${u.host.substring(7)}`;
} else if (pos !== -1) {
if (u.host.indexOf(".") === -1) {
return "";
}
} }
// 处理顶域 // 处理顶域
if (u.host.endsWith("tld")) { if (u.host.endsWith("tld")) {
@ -223,6 +214,7 @@ export interface PatternMatchesUrl {
} }
// 解析URL, 根据https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns?hl=zh-cn进行处理 // 解析URL, 根据https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns?hl=zh-cn进行处理
// 将一些异常情况直接转为通配用最大的范围去注册userScript在执行的时候再用UrlMatch去匹配过滤
export function parsePatternMatchesURL( export function parsePatternMatchesURL(
url: string, url: string,
options?: { options?: {
@ -251,6 +243,9 @@ export function parsePatternMatchesURL(
} }
} }
if (result) { if (result) {
if (result.scheme === "http*") {
result.scheme = "*";
}
if (result.host !== "*") { if (result.host !== "*") {
// *开头但是不是*.的情况 // *开头但是不是*.的情况
if (result.host.startsWith("*")) { if (result.host.startsWith("*")) {
@ -261,6 +256,10 @@ export function parsePatternMatchesURL(
} }
// 结尾是*的情况 // 结尾是*的情况
if (result.host.endsWith("*")) { if (result.host.endsWith("*")) {
result.host = "*";
}
// 结尾是.的情况
if (result.host.endsWith(".")) {
result.host = result.host.slice(0, -1); result.host = result.host.slice(0, -1);
} }
// 处理 www.*.example.com 的情况为 *.example.com // 处理 www.*.example.com 的情况为 *.example.com

View File

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

View File

@ -48,14 +48,14 @@ async function setupOffscreenDocument() {
} }
async function main() { async function main() {
// 初始化管理器
const message = new ExtensionMessage(true);
// 初始化日志组件 // 初始化日志组件
const loggerCore = new LoggerCore({ const loggerCore = new LoggerCore({
writer: new DBWriter(new LoggerDAO()), writer: new DBWriter(new LoggerDAO()),
labels: { env: "service_worker" }, labels: { env: "service_worker" },
}); });
loggerCore.logger().debug("service worker start"); loggerCore.logger().debug("service worker start");
// 初始化管理器
const message = new ExtensionMessage(true);
const server = new Server("serviceWorker", message); const server = new Server("serviceWorker", message);
const messageQueue = new MessageQueue(); const messageQueue = new MessageQueue();
const manager = new ServiceWorkerManager(server, messageQueue, new ServiceWorkerMessageSend()); const manager = new ServiceWorkerManager(server, messageQueue, new ServiceWorkerMessageSend());

View File

@ -139,21 +139,6 @@ declare function GM_cookie(
ondone: (cookie: GMTypes.Cookie[], error: unknown | undefined) => void ondone: (cookie: GMTypes.Cookie[], error: unknown | undefined) => void
): void; ): void;
/**
* 可以通过GM_addValueChangeListener获取tabid
* 再通过tabid(前后端通信可能用到,ValueChangeListener会返回tabid),获取storeid,后台脚本用.
* 请注意这是一个实验性质的API,后续可能会改变
* @param tabid 页面的tabid
* @param ondone 完成事件
* @param callback.storeid 该页面的storeid,可以给GM_cookie使用
* @param callback.error 错误信息
* @deprecated 已废弃,请使用GM_cookie("store", tabid)替代
*/
declare function GM_getCookieStore(
tabid: number,
ondone: (storeId: number | undefined, error: unknown | undefined) => void
): void;
/** /**
* 设置浏览器代理 * 设置浏览器代理
* @deprecated 正式版中已废弃,后续可能会在beta版本中添加 * @deprecated 正式版中已废弃,后续可能会在beta版本中添加
@ -289,10 +274,7 @@ declare namespace CATType {
} }
declare namespace GMTypes { declare namespace GMTypes {
/* type CookieAction = "list" | "delete" | "set";
* store为获取隐身窗口之类的cookie,API,
*/
type CookieAction = "list" | "delete" | "set" | "store";
type LoggerLevel = "debug" | "info" | "warn" | "error"; type LoggerLevel = "debug" | "info" | "warn" | "error";
@ -308,17 +290,13 @@ declare namespace GMTypes {
path?: string; path?: string;
secure?: boolean; secure?: boolean;
session?: boolean; session?: boolean;
storeId?: string;
httpOnly?: boolean; httpOnly?: boolean;
expirationDate?: number; expirationDate?: number;
// store用
tabId?: number;
} }
interface Cookie { interface Cookie {
domain: string; domain: string;
name: string; name: string;
storeId: string;
value: string; value: string;
session: boolean; session: boolean;
hostOnly: boolean; hostOnly: boolean;
@ -341,7 +319,7 @@ declare namespace GMTypes {
active?: boolean; active?: boolean;
insert?: boolean; insert?: boolean;
setParent?: boolean; setParent?: boolean;
useOpen?: boolean; // //Firefox的功能 useOpen?: boolean; // //Firefox的功能 使window.open打开新窗口 #178
} }
interface XHRResponse { interface XHRResponse {
@ -384,7 +362,7 @@ declare namespace GMTypes {
user?: string; user?: string;
password?: string; password?: string;
nocache?: boolean; nocache?: boolean;
maxRedirects?: number; redirect?: "follow" | "error" | "manual";// tm保持一致, v0.17.0maxRedirects, 使redirect替代, 使fetch模式
onload?: Listener<XHRResponse>; onload?: Listener<XHRResponse>;
onloadstart?: Listener<XHRResponse>; onloadstart?: Listener<XHRResponse>;

1
src/types/main.d.ts vendored
View File

@ -31,7 +31,6 @@ declare namespace GMSend {
nocache?: boolean; nocache?: boolean;
dataType?: "FormData" | "Blob"; dataType?: "FormData" | "Blob";
redirect?: "follow" | "error" | "manual"; redirect?: "follow" | "error" | "manual";
maxRedirects?: number; // 为了与tm保持一致, 在v0.17.0后废弃, 使用redirect替代
} }
interface XHRFormData { interface XHRFormData {

View File

@ -362,7 +362,7 @@ declare namespace GMTypes {
user?: string; user?: string;
password?: string; password?: string;
nocache?: boolean; nocache?: boolean;
maxRedirects?: number; redirect?: "follow" | "error" | "manual";// 为了与tm保持一致, 在v0.17.0后废弃maxRedirects, 使用redirect替代, 会强制使用fetch模式
onload?: Listener<XHRResponse>; onload?: Listener<XHRResponse>;
onloadstart?: Listener<XHRResponse>; onloadstart?: Listener<XHRResponse>;