Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 31 additions & 8 deletions src/app/migrate.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getStorageName } from "@App/pkg/utils/utils";
import { db } from "./repo/dao";
import type { Script, ScriptAndCode } from "./repo/scripts";
import { ScriptCodeDAO, ScriptDAO } from "./repo/scripts";
import { ScriptCodeDAO, ScriptCodeDAONew, ScriptDAO } from "./repo/scripts";
import type { Subscribe } from "./repo/subscribe";
import { SubscribeDAO } from "./repo/subscribe";
import type { Value } from "./repo/value";
Expand All @@ -19,7 +19,7 @@ export function migrateToChromeStorage() {
// 迁移脚本
const scripts = await db.table("scripts").toArray();
const scriptDAO = new ScriptDAO();
const scriptCodeDAO = new ScriptCodeDAO();
const scriptCodeDAO = new ScriptCodeDAONew();
console.log("开始迁移脚本数据", scripts.length);
await Promise.all(
// 不处理 Promise.reject ?
Expand Down Expand Up @@ -267,25 +267,48 @@ export function migrateChromeStorage() {
);
},
},
{
version: 2,
upgrade: async () => {
const scriptCodeDAO = new ScriptCodeDAO();
const scriptCodeDAONew = new ScriptCodeDAONew();
const scriptCodes = await scriptCodeDAO.all();
await Promise.all(scriptCodes.map(async (scriptCode) => scriptCodeDAONew.save(scriptCode)));
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

migrate v2 这里用 ScriptCodeDAONew.save() 迁移旧 ScriptCodeDAO 数据到 OPFS,但 ScriptCodeDAONew.save() 内部又会“过渡期间同步保存至 ScriptCodeDAO”,等于迁移过程中把所有 code 再写回一次 chrome.storage.local,仍会触发大量 onChanged/写放大。建议为迁移提供仅写 OPFS 的路径(例如 save({..., legacy: false}) 或独立的 saveToOPFSOnly)。

Suggested change
await Promise.all(scriptCodes.map(async (scriptCode) => scriptCodeDAONew.save(scriptCode)));
// 迁移阶段仅写入 OPFS,避免通过 ScriptCodeDAONew.save 再次写回 chrome.storage.local 造成写放大
await Promise.all(
scriptCodes.map(async (scriptCode) => scriptCodeDAONew.saveToOPFSOnly(scriptCode))
);

Copilot uses AI. Check for mistakes.
// 过渡期间不删除旧代码
// await scriptCodeDAO.deletes(scriptCodes.map((e) => e.uuid));
},
},
// {
// version: 3,
// upgrade: async () => {
// // const scriptCodeDAO = new ScriptCodeDAO();
// // 过渡期间后删除旧代码
// // await scriptCodeDAO.deletes(scriptCodes.map((e) => e.uuid));
// },
// },
];
const localstorageDAO = new LocalStorageDAO();
localstorageDAO.get("migrations").then(async (item) => {
// 旧代码 logic 错误。只好维持 array 形式但只储单一版本号
Copy link
Copy Markdown
Member

@CodFrm CodFrm Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

哪里有错误?如果是顺序,也不应该这样去处理,应该保留完整版本

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

单一版本号升级 1 -> 2 -> 3 -> 4...
不是一个阵列
如果有20个 migration 动作,就会储存20个数字

const migrations = item?.value || [];
let migrationVersion = migrations[0] || 0;
for (let i = 0; i < migrationList.length; i++) {
const m = migrationList[i];
if (!migrations.includes(m.version)) {
if (migrationVersion < m.version) {
// 需要升级
try {
await m.upgrade();
migrations.push(m.version);
migrationVersion = m.version;
localstorageDAO.save({
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

migrateChromeStorage 在升级循环内调用 localstorageDAO.save(...) 但未 await;如果 service worker 在写入完成前被挂起/回收,可能导致迁移版本号未落盘,从而重复执行迁移(尤其是 v2 这种全量迁移)。建议对 save 返回的 Promise 做 await(或把整个 migrateChromeStorage 改为 async 并在调用处等待)。

Suggested change
localstorageDAO.save({
await localstorageDAO.save({

Copilot uses AI. Check for mistakes.
key: "migrations",
value: [migrationVersion],
});
} catch (e) {
// throw 后停止升级
throw new Error(`Chrome storage migration v${m.version} failed: ${e}`);
}
}
}
localstorageDAO.save({
key: "migrations",
value: migrations,
});
});
}

Expand Down
104 changes: 80 additions & 24 deletions src/app/repo/scripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export interface Script {
checktime: number; // 脚本检查更新时间戳
lastruntime?: number; // 脚本最后一次运行时间戳
nextruntime?: number; // 脚本下一次运行时间戳
ignoreVersion?: string; // 忽略單一版本的更新檢查
ignoreVersion?: string; // 忽略单一版本的更新检查
}

// 分开存储脚本代码
Expand Down Expand Up @@ -147,33 +147,18 @@ export type TClientPageLoadInfo =
| { ok: false };

export class ScriptDAO extends Repo<Script> {
scriptCodeDAO: ScriptCodeDAO = new ScriptCodeDAO();

constructor() {
super("script");
}

enableCache(): void {
super.enableCache();
this.scriptCodeDAO.enableCache();
}

public save(val: Script) {
return super._save(val.uuid, val);
}

findByUUID(uuid: string) {
return this.get(uuid);
}

async getAndCode(uuid: string): Promise<ScriptAndCode | undefined> {
const [script, code] = await Promise.all([this.get(uuid), this.scriptCodeDAO.get(uuid)]);
if (!script || !code) {
return undefined;
}
return Object.assign(script, code);
}

public findByName(name: string) {
return this.findOne((key, value) => {
return value.name === name;
Expand Down Expand Up @@ -235,22 +220,22 @@ export class ScriptDAO extends Repo<Script> {
if (val1.length < 2) {
return val1[0] === val2[0];
}
// 無視次序
// 无视次序
const s = new Set([...val1, ...val2]);
if (s.size !== val1.length) return false;
return true;
}
return val1 === val2;
};
const isScriptInfoEqual = (script1: Script, script2: Script) => {
// @author, @copyright, @license 應該不會改
// @author, @copyright, @license 应该不会改
if (!valEqual(script1.metadata.author, script2.metadata.author)) return false;
if (!valEqual(script1.metadata.copyright, script2.metadata.copyright)) return false;
if (!valEqual(script1.metadata.license, script2.metadata.license)) return false;
// @grant, @connect 應該不會改
// @grant, @connect 应该不会改
if (!valEqual(script1.metadata.grant, script2.metadata.grant)) return false;
if (!valEqual(script1.metadata.connect, script2.metadata.connect)) return false;
// @match @include 應該不會改
// @match @include 应该不会改
if (!valEqual(script1.metadata.match, script2.metadata.match)) return false;
if (!valEqual(script1.metadata.include, script2.metadata.include)) return false;
return true;
Expand Down Expand Up @@ -298,11 +283,82 @@ export class ScriptCodeDAO extends Repo<ScriptCode> {
super("scriptCode");
}

findByUUID(uuid: string) {
return this.get(uuid);
}

public save(val: ScriptCode) {
return super._save(val.uuid, val);
}
}

// 不能 extends Repo<ScriptCode>. 没有 dao.gets()
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里的注释“不能 extends Repo. 没有 dao.gets()”与当前 Repo 实现不一致:Repo 本身就有 gets()。建议更新为真实原因(例如:此 DAO 不走 chrome.storage.local 的 Repo 前缀/缓存逻辑,而是走 OPFS),避免误导后续维护。

Suggested change
// 不能 extends Repo<ScriptCode>. 没有 dao.gets()
// 注意:此 DAO 不继承 Repo<ScriptCode>,而是改用 OPFS 单独存储大体积脚本代码,

Copilot uses AI. Check for mistakes.
export class ScriptCodeDAONew {
private readonly _scriptCodeDAO = new ScriptCodeDAO();
private _dirHandlePromise: Promise<FileSystemDirectoryHandle> | null = null;
static getDirHandle(): Promise<FileSystemDirectoryHandle> {
return navigator.storage
.getDirectory()
.then((opfsRoot) => opfsRoot.getDirectoryHandle("storage_script_codes", { create: true }));
}
public async save(val: ScriptCode) {
if (!this._dirHandlePromise) this._dirHandlePromise = ScriptCodeDAONew.getDirHandle();
const folder = await this._dirHandlePromise;
const handle = await folder.getFileHandle(`${val.uuid}.user.js`, { create: true });
const writable = await handle.createWritable({ keepExistingData: false });
await writable.write(val.code);
await writable.close();
// 过渡期间同步保存至 ScriptCodeDAO
await this._scriptCodeDAO.save(val); // [Version: 2] ONLY
}
public async delete(uuid: string) {
if (!this._dirHandlePromise) this._dirHandlePromise = ScriptCodeDAONew.getDirHandle();
const folder = await this._dirHandlePromise;
try {
await folder.removeEntry(`${uuid}.user.js`);
} catch {
// ignore delete failure. e.g. no file
}
}
public async get(uuid: string): Promise<ScriptCode> {
if (!this._dirHandlePromise) this._dirHandlePromise = ScriptCodeDAONew.getDirHandle();
const folder = await this._dirHandlePromise;
let code: string = "";
let handle: FileSystemFileHandle;
try {
handle = await folder.getFileHandle(`${uuid}.user.js`, { create: false });
} catch {
// no file -> empty code
return {
uuid,
code,
};
}
code = await handle.getFile().then((f) => f.text());
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ScriptCodeDAONew.get() 在文件不存在时返回 { uuid, code: "" }(始终为 truthy),会导致调用方用 if (!scriptCode) / if (!code) 判断“代码缺失”永远不会触发,从而把“缺失”当成“空代码”继续执行。建议让 get() 在文件不存在时返回 undefined(并把返回类型改为 Promise<ScriptCode | undefined>),或提供一个明确的 getOrEmpty() 并统一调用方语义。

Suggested change
public async get(uuid: string): Promise<ScriptCode> {
if (!this._dirHandlePromise) this._dirHandlePromise = ScriptCodeDAONew.getDirHandle();
const folder = await this._dirHandlePromise;
let code: string = "";
let handle: FileSystemFileHandle;
try {
handle = await folder.getFileHandle(`${uuid}.user.js`, { create: false });
} catch {
// no file -> empty code
return {
uuid,
code,
};
}
code = await handle.getFile().then((f) => f.text());
public async get(uuid: string): Promise<ScriptCode | undefined> {
if (!this._dirHandlePromise) this._dirHandlePromise = ScriptCodeDAONew.getDirHandle();
const folder = await this._dirHandlePromise;
let handle: FileSystemFileHandle;
try {
handle = await folder.getFileHandle(`${uuid}.user.js`, { create: false });
} catch {
// no file -> undefined
return undefined;
}
const code = await handle.getFile().then((f) => f.text());

Copilot uses AI. Check for mistakes.
return { uuid, code };
}
public async deletes(uuids: string[]) {
if (!this._dirHandlePromise) this._dirHandlePromise = ScriptCodeDAONew.getDirHandle();
const folder = await this._dirHandlePromise;
await Promise.all(
uuids.map(async (uuid) => {
try {
await folder.removeEntry(`${uuid}.user.js`);
} catch {
// ignore delete failure. e.g. no file
}
})
);
}
public async gets(uuids: string[]): Promise<(ScriptCode | undefined)[]> {
if (!this._dirHandlePromise) this._dirHandlePromise = ScriptCodeDAONew.getDirHandle();
const folder = await this._dirHandlePromise;
return Promise.all(
uuids.map(async (uuid) => {
try {
const handle = await folder.getFileHandle(`${uuid}.user.js`, { create: false });
const code = await handle.getFile().then((f) => f.text());
return { uuid, code };
} catch {
return undefined;
}
})
);
}
}
7 changes: 4 additions & 3 deletions src/app/service/service_worker/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { EmitEventRequest, ScriptLoadInfo, ScriptMatchInfo, ScriptMenu } fr
import type { IMessageQueue } from "@Packages/message/message_queue";
import type { Group, IGetSender } from "@Packages/message/server";
import type { ExtMessageSender, MessageSend } from "@Packages/message/types";
import type { TClientPageLoadInfo } from "@App/app/repo/scripts";
import { ScriptCodeDAONew, type TClientPageLoadInfo } from "@App/app/repo/scripts";
import type { Script, ScriptDAO, ScriptRunResource, ScriptSite, TScriptInfo } from "@App/app/repo/scripts";
import { SCRIPT_STATUS_DISABLE, SCRIPT_STATUS_ENABLE, SCRIPT_TYPE_NORMAL } from "@App/app/repo/scripts";
import { type ValueService } from "./value";
Expand Down Expand Up @@ -128,6 +128,7 @@ export class RuntimeService {
initialCompiledResourcePromise: Promise<any> | undefined;

compiledResourceDAO: CompiledResourceDAO = new CompiledResourceDAO();
private readonly scriptCodeDAO: ScriptCodeDAONew = new ScriptCodeDAONew();

constructor(
private systemConfig: SystemConfig,
Expand Down Expand Up @@ -1245,7 +1246,7 @@ export class RuntimeService {
}
}

const { value, resource, scriptDAO } = this;
const { value, resource, scriptCodeDAO } = this;
await Promise.all(
enableScriptList.flatMap((script) => [
// 加载value
Expand All @@ -1265,7 +1266,7 @@ export class RuntimeService {
}
}),
// 加载code相关的信息
scriptDAO.scriptCodeDAO.get(script.uuid).then((code) => {
scriptCodeDAO.get(script.uuid).then((code) => {
if (code) {
const metadataStr = getMetadataStr(code.code) || "";
const userConfigStr = getUserConfigStr(code.code) || "";
Expand Down
6 changes: 3 additions & 3 deletions src/app/service/service_worker/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import type {
ScriptRunResource,
ScriptSite,
} from "@App/app/repo/scripts";
import { SCRIPT_STATUS_DISABLE, SCRIPT_STATUS_ENABLE, ScriptCodeDAO } from "@App/app/repo/scripts";
import { SCRIPT_STATUS_DISABLE, SCRIPT_STATUS_ENABLE, ScriptCodeDAONew } from "@App/app/repo/scripts";
import { type IMessageQueue } from "@Packages/message/message_queue";
import { createScriptInfo, type ScriptInfo, type InstallSource } from "@App/pkg/utils/scriptInstall";
import { type ResourceService } from "./resource";
Expand Down Expand Up @@ -69,7 +69,7 @@ export type TScriptInstallReturn = {

export class ScriptService {
logger: Logger;
scriptCodeDAO: ScriptCodeDAO = new ScriptCodeDAO();
scriptCodeDAO: ScriptCodeDAONew = new ScriptCodeDAONew();
localStorageDAO: LocalStorageDAO = new LocalStorageDAO();
compiledResourceDAO: CompiledResourceDAO = new CompiledResourceDAO();
private readonly scriptUpdateCheck;
Expand All @@ -83,7 +83,7 @@ export class ScriptService {
private readonly scriptDAO: ScriptDAO
) {
this.logger = LoggerCore.logger().with({ service: "script" });
this.scriptCodeDAO.enableCache();
// this.scriptCodeDAO.enableCache();
this.scriptUpdateCheck = new ScriptUpdateCheck(systemConfig, group, mq, valueService, resourceService, scriptDAO);
}

Expand Down
2 changes: 1 addition & 1 deletion src/app/service/service_worker/subscribe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export class SubscribeService {
// 通过uuid查询脚本id
result.push(
(async () => {
const script = await this.scriptDAO.findByUUID(item.uuid);
const script = await this.scriptDAO.get(item.uuid);
if (script) {
notification[1].push(i18nName(script));
// 删除脚本
Expand Down
5 changes: 2 additions & 3 deletions src/app/service/service_worker/synchronize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {
type Script,
SCRIPT_STATUS_DISABLE,
SCRIPT_STATUS_ENABLE,
ScriptCodeDAONew,
type ScriptDAO,
type ScriptCodeDAO,
} from "@App/app/repo/scripts";
import BackupExport from "@App/pkg/backup/export";
import type { BackupData, ResourceBackup, ScriptBackupData, ScriptOptions, ValueStorage } from "@App/pkg/backup/struct";
Expand Down Expand Up @@ -69,7 +69,7 @@ type PushScriptParam = TInstallScriptParams;
export class SynchronizeService {
logger: Logger;

scriptCodeDAO: ScriptCodeDAO;
readonly scriptCodeDAO: ScriptCodeDAONew = new ScriptCodeDAONew();

storage: ChromeStorage = new ChromeStorage("sync", false);

Expand All @@ -84,7 +84,6 @@ export class SynchronizeService {
private scriptDAO: ScriptDAO
) {
this.logger = LoggerCore.logger().with({ service: "synchronize" });
this.scriptCodeDAO = this.scriptDAO.scriptCodeDAO;
}

// 生成备份文件到文件系统
Expand Down
4 changes: 2 additions & 2 deletions src/pages/components/CloudScriptPlan/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import type { Export, ExportTarget } from "@App/app/repo/export";
import { ExportDAO } from "@App/app/repo/export";
import type { Script } from "@App/app/repo/scripts";
import { ScriptCodeDAO } from "@App/app/repo/scripts";
import { ScriptCodeDAONew } from "@App/app/repo/scripts";
import { localePath } from "@App/locales/locales";
import { makeBlobURL } from "@App/pkg/utils/utils";
import { Button, Checkbox, Form, Input, Message, Modal, Select } from "@arco-design/web-react";
Expand Down Expand Up @@ -57,7 +57,7 @@
}
});
}
}, [script]);

Check warning on line 60 in src/pages/components/CloudScriptPlan/index.tsx

View workflow job for this annotation

GitHub Actions / Run tests

React Hook useEffect has a missing dependency: 'form'. Either include it or remove the dependency array
return (
<Modal
title={
Expand Down Expand Up @@ -123,7 +123,7 @@
zip: zipFile,
...params,
});
const code = await new ScriptCodeDAO().findByUUID(script.uuid);
const code = await new ScriptCodeDAONew().get(script.uuid);
if (!code) {
Message.error(t("invalid_script_code"));
return;
Expand Down
6 changes: 3 additions & 3 deletions src/pages/options/routes/script/ScriptEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Script } from "@App/app/repo/scripts";
import { SCRIPT_TYPE_NORMAL, ScriptCodeDAO, ScriptDAO } from "@App/app/repo/scripts";
import { SCRIPT_TYPE_NORMAL, ScriptCodeDAONew, ScriptDAO } from "@App/app/repo/scripts";
import CodeEditor from "@App/pages/components/CodeEditor";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
Expand Down Expand Up @@ -201,7 +201,7 @@ type EditorState = {
};

const scriptDAO = new ScriptDAO();
const scriptCodeDAO = new ScriptCodeDAO();
const scriptCodeDAO = new ScriptCodeDAONew();

function ScriptEditor() {
const [visible, setVisible] = useState<{ [key: string]: boolean }>({});
Expand Down Expand Up @@ -273,7 +273,7 @@ function ScriptEditor() {
// 如果不在,从数据库读取
const script = scriptList.find((s) => s.uuid === uuid);
if (script) {
const code = await scriptCodeDAO.findByUUID(uuid);
const code = await scriptCodeDAO.get(uuid);
const newEditor: EditorState = {
script,
code: code?.code || "",
Expand Down
4 changes: 2 additions & 2 deletions src/pkg/utils/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
SCRIPT_TYPE_BACKGROUND,
SCRIPT_TYPE_CRONTAB,
SCRIPT_TYPE_NORMAL,
ScriptCodeDAO,
ScriptCodeDAONew,
ScriptDAO,
} from "@App/app/repo/scripts";
import type { Subscribe } from "@App/app/repo/subscribe";
Expand Down Expand Up @@ -190,7 +190,7 @@ export async function prepareScriptByCode(
) {
throw new Error(i18n_t("error_script_type_mismatch"));
}
const scriptCode = await new ScriptCodeDAO().get(old.uuid);
const scriptCode = await new ScriptCodeDAONew().get(old.uuid);
if (!scriptCode) {
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prepareScriptByCode 这里仍用 if (!scriptCode) 判断旧脚本 code 是否存在,但 ScriptCodeDAONew.get() 当前不会返回 undefined(缺文件时返回空字符串对象),会导致缺失 code 的情况不再抛 error_old_script_code_missing,而是继续用空 code 走后续逻辑。建议配合调整 ScriptCodeDAONew.get() 的返回语义,或在这里改为检查 code.code 是否为空。

Suggested change
if (!scriptCode) {
// ScriptCodeDAONew.get() 在缺文件时可能返回空字符串对象,此处需同时检查 code 字段
if (!scriptCode || !scriptCode.code) {

Copilot uses AI. Check for mistakes.
throw new Error(i18n_t("error_old_script_code_missing"));
}
Expand Down
Loading