Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
14 changes: 11 additions & 3 deletions src/app/service/service_worker/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@ import { type Resource } from "@App/app/repo/resource";
import { type Subscribe } from "@App/app/repo/subscribe";
import { type Logger } from "@App/app/repo/logger";
import { type Permission } from "@App/app/repo/permission";
import type { InstallSource, ScriptMenu, ScriptMenuItem, TBatchUpdateListAction, TPopupPageStatus } from "./types";
import type {
InstallSource,
ScriptMenu,
ScriptMenuItem,
TBatchUpdateListAction,
TCheckScriptUpdateResult,
TOpenUpdatePageResult,
TPopupPageStatus,
} from "./types";
import { Client } from "@Packages/message/client";
import type { MessageSend } from "@Packages/message/types";
import type PermissionVerify from "./permission_verify";
Expand Down Expand Up @@ -273,15 +281,15 @@ export class ScriptClient extends Client {
}

async openUpdatePageByUUID(uuid: string) {
return this.do<boolean>("openUpdatePageByUUID", uuid);
return this.do<TOpenUpdatePageResult>("openUpdatePageByUUID", uuid);
}

async openBatchUpdatePage(opts: TOpenBatchUpdatePageOption) {
return this.do<boolean>("openBatchUpdatePage", opts);
}

async checkScriptUpdate(opts: TCheckScriptUpdateOption) {
return this.do<void>("checkScriptUpdate", opts);
return this.do<TCheckScriptUpdateResult>("checkScriptUpdate", opts);
}
}

Expand Down
77 changes: 71 additions & 6 deletions src/app/service/service_worker/script.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1508,7 +1508,7 @@ describe("ScriptService.openUpdatePageByUUID —— 打开单条更新详情", (
await saveTarget(service, scriptDAO);
primeCache(service, userscript("2.0.0"));

await expect(service.openUpdatePageByUUID("u-open")).resolves.toBe(true);
await expect(service.openUpdatePageByUUID("u-open")).resolves.toBe("opened");

expect(h.fetchScriptBody).not.toHaveBeenCalled();
expect(h.openInCurrentTab).toHaveBeenCalledWith("/src/install.html?uuid=u-open");
Expand All @@ -1519,27 +1519,92 @@ describe("ScriptService.openUpdatePageByUUID —— 打开单条更新详情", (
await saveTarget(service, scriptDAO);
h.fetchScriptBody.mockResolvedValue(userscript("2.0.0"));

await expect(service.openUpdatePageByUUID("u-open")).resolves.toBe(true);
await expect(service.openUpdatePageByUUID("u-open")).resolves.toBe("opened");

expect(h.fetchScriptBody).toHaveBeenCalledWith(URL);
expect(h.openInCurrentTab).toHaveBeenCalledWith("/src/install.html?uuid=u-open");
});

it("拉取失败时回报 false,让更新页能给出失败反馈而不是一直转圈", async () => {
it("拉取失败时回报 failed,让更新页能给出失败反馈而不是一直转圈", async () => {
const { service, scriptDAO } = buildService();
await saveTarget(service, scriptDAO);
h.fetchScriptBody.mockRejectedValue(new Error("network error"));

await expect(service.openUpdatePageByUUID("u-open")).resolves.toBe(false);
await expect(service.openUpdatePageByUUID("u-open")).resolves.toBe("failed");

expect(h.openInCurrentTab).not.toHaveBeenCalled();
});

it("脚本已不存在时回报 false 而不是静默无反应", async () => {
it("脚本已不存在时回报 failed 而不是静默无反应", async () => {
const { service } = buildService();

await expect(service.openUpdatePageByUUID("missing")).resolves.toBe(false);
await expect(service.openUpdatePageByUUID("missing")).resolves.toBe("failed");

expect(h.openInCurrentTab).not.toHaveBeenCalled();
});

it("命中静默更新时回报 silent:不开安装页,由调用方补一条反馈", async () => {
const { service, scriptDAO, systemConfig } = buildService();
await saveTarget(service, scriptDAO);
systemConfig.setSilenceUpdateScript(true);
primeCache(service, userscript("2.0.0"));

await expect(service.openUpdatePageByUUID("u-open")).resolves.toBe("silent");

// 静默更新是真的装了,只是页面上什么都不会发生
expect(h.openInCurrentTab).not.toHaveBeenCalled();
expect((await scriptDAO.get("u-open"))?.metadata.version?.[0]).toBe("2.0.0");
});
});

describe("ScriptService.batchUpdateListAction —— 忽略更新", () => {
const saveIgnoreTarget = (scriptDAO: ScriptDAO) =>
scriptDAO.save(
makeScript({
uuid: "u-ignore",
name: "忽略目标",
namespace: "scriptcat-test",
metadata: { name: ["忽略目标"], namespace: ["scriptcat-test"], version: ["1.0.0"] },
})
);

it("逐条回报忽略结果,页面据此收起该行", async () => {
const { service, scriptDAO } = buildService();
await saveIgnoreTarget(scriptDAO);

const res = await service.batchUpdateListAction({
actionCode: BatchUpdateListActionCode.IGNORE,
actionPayload: [{ uuid: "u-ignore", ignoreVersion: "2.0.0" }],
});

expect(res).toEqual({ ok: true, items: [{ uuid: "u-ignore", success: true }] });
expect((await scriptDAO.get("u-ignore"))?.ignoreVersion).toBe("2.0.0");
});

it("检查缓存已随 Service Worker 回收时,忽略照样生效并照常回报", async () => {
const { service, scriptDAO } = buildService();
await saveIgnoreTarget(scriptDAO);
// 忽略写的是脚本自身的 ignoreVersion,与检查缓存无关
expect(service["scriptUpdateCheck"].cacheFull).toBeFalsy();

const res = await service.batchUpdateListAction({
actionCode: BatchUpdateListActionCode.IGNORE,
actionPayload: [{ uuid: "u-ignore", ignoreVersion: "2.0.0" }],
});

expect(res).toEqual({ ok: true, items: [{ uuid: "u-ignore", success: true }] });
expect((await scriptDAO.get("u-ignore"))?.ignoreVersion).toBe("2.0.0");
});

it("脚本已不存在时该条回报失败,而不是静默当作成功", async () => {
const { service } = buildService();

const res = await service.batchUpdateListAction({
actionCode: BatchUpdateListActionCode.IGNORE,
actionPayload: [{ uuid: "missing", ignoreVersion: "2.0.0" }],
});

expect(res?.ok).toBe(true);
expect(res?.items[0]).toMatchObject({ uuid: "missing", success: false });
});
});
75 changes: 34 additions & 41 deletions src/app/service/service_worker/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ import {
type TBatchUpdateRecord,
type TBatchUpdateItemResult,
type TBatchUpdateResult,
type TCheckScriptUpdateResult,
type TOpenUpdatePageResult,
} from "./types";
import { getSimilarityScore, ScriptUpdateCheck } from "./script_update_check";
import { LocalStorageDAO } from "@App/app/repo/localStorage";
Expand Down Expand Up @@ -1234,9 +1236,13 @@ export class ScriptService {

/**
* 打开更新窗口。cachedNewCode 是检查阶段已经拉到的新版代码,命中时直接复用。
* @returns 是否已处理(打开了安装页或完成了静默更新)
* 静默更新与打开安装页必须让调用方能区分:前者页面上什么都不会发生,需要自己补一条反馈。
*/
public async openUpdatePage(script: Script, source: "user" | "system", cachedNewCode?: string) {
public async openUpdatePage(
script: Script,
source: "user" | "system",
cachedNewCode?: string
): Promise<TOpenUpdatePageResult> {
const { uuid, name, downloadUrl, checkUpdateUrl } = script;
const logger = this.logger.with({
uuid,
Expand All @@ -1249,13 +1255,13 @@ export class ScriptService {
const ret = cachedNewCode
? await this.prepareUpdateOrInstallPage(uuid, cachedNewCode, url, { source }, true, logger)
: await this.openUpdateOrInstallPage(uuid, url, { source }, true, logger);
if (ret === 2) return true; // slience update
if (ret === 2) return "silent";
// 打开安装页面
openInCurrentTab(`/src/install.html?uuid=${uuid}`);
return true;
return "opened";
} catch (e) {
logger.error("fetch script info failed", Logger.E(e));
return false;
return "failed";
}
}

Expand All @@ -1275,20 +1281,7 @@ export class ScriptService {
}

// 用于定时自动检查脚本更新
async _checkScriptUpdate(opts: TCheckScriptUpdateOption): Promise<
| {
ok: true;
targetSites: string[];
err?: undefined;
fresh: boolean;
checktime: number;
}
| {
ok: false;
targetSites?: undefined;
err?: string | Error;
}
> {
async _checkScriptUpdate(opts: TCheckScriptUpdateOption): Promise<TCheckScriptUpdateResult> {
const executeSlienceUpdate = opts.checkType === "system" && (await this.systemConfig.getSilenceUpdateScript());
const checkCycle = await this.systemConfig.getCheckScriptUpdateCycle();
if (!checkCycle) {
Expand Down Expand Up @@ -1467,16 +1460,13 @@ export class ScriptService {
};
}

async checkScriptUpdate(opts: TCheckScriptUpdateOption) {
let res;
async checkScriptUpdate(opts: TCheckScriptUpdateOption): Promise<TCheckScriptUpdateResult> {
let res: TCheckScriptUpdateResult;
if ((this.scriptUpdateCheck.state.status & UpdateStatusCode.CHECKING_UPDATE) === UpdateStatusCode.CHECKING_UPDATE) {
res = {
ok: false,
reason: "busy",
err: "checkScriptUpdate is busy. Please try again later.",
} as {
ok: false;
targetSites?: undefined;
err?: string | Error;
};
} else if (this.scriptUpdateCheck.canSkipScriptUpdateCheck(opts)) {
return {
Expand All @@ -1493,14 +1483,7 @@ export class ScriptService {
res = await this._checkScriptUpdate(opts);
} catch (e) {
this.logger.error("check script updates failed", Logger.E(e));
res = {
ok: false,
err: e,
} as {
ok: false;
targetSites?: undefined;
err?: string | Error;
};
res = { ok: false, err: e as Error };
}
// clear CHECKING_UPDATE
this.scriptUpdateCheck.state.status &= ~UpdateStatusCode.CHECKING_UPDATE;
Expand Down Expand Up @@ -1704,12 +1687,21 @@ export class ScriptService {
async batchUpdateListAction(action: TBatchUpdateListAction) {
if (action.actionCode === BatchUpdateListActionCode.IGNORE) {
const map = new Map();
await Promise.allSettled(
action.actionPayload.map(async (script) => {
const { uuid, ignoreVersion } = script;
const updatedScript = await this.scriptDAO.update(uuid, { ignoreVersion });
if (!updatedScript || updatedScript.uuid !== uuid) return;
map.set(uuid, updatedScript);
// 逐条回报结果:忽略写的是脚本本身的 ignoreVersion,与检查缓存无关,
// 因此即使缓存已随 Service Worker 回收,忽略照样生效,页面据此收起该行
const items: TBatchUpdateItemResult[] = await Promise.all(
action.actionPayload.map(async ({ uuid, ignoreVersion }) => {
try {
const updatedScript = await this.scriptDAO.update(uuid, { ignoreVersion });
if (!updatedScript || updatedScript.uuid !== uuid) {
return { uuid, success: false, error: "script not found" };
}
map.set(uuid, updatedScript);
return { uuid, success: true };
} catch (e) {
this.logger.error("ignore script update failed", { uuid }, Logger.E(e));
return { uuid, success: false, error: e instanceof Error ? e.message : String(e) };
}
})
);
if (this.scriptUpdateCheck.cacheFull) {
Expand All @@ -1723,6 +1715,7 @@ export class ScriptService {
this.scriptUpdateCheck.setCacheFull(this.scriptUpdateCheck.cacheFull);
this.scriptUpdateCheck.announceMessage({ refreshRecord: true });
}
return { ok: true, items } satisfies TBatchUpdateResult;
} else if (action.actionCode === BatchUpdateListActionCode.UPDATE) {
const uuids = action.actionPayload.map((entry) => entry.uuid);
const list = this.scriptUpdateCheck.cacheFull?.list;
Expand Down Expand Up @@ -1782,10 +1775,10 @@ export class ScriptService {
}
}

async openUpdatePageByUUID(uuid: string) {
async openUpdatePageByUUID(uuid: string): Promise<TOpenUpdatePageResult> {
const source = "user"; // TBC
const script = await this.scriptDAO.get(uuid);
if (!script || script.uuid !== uuid) return false;
if (!script || script.uuid !== uuid) return "failed";
// 检查记录里已经带着这次要装的新版代码:复用它既省掉一次让用户干等的网络往返,
// 也保证打开的正是列表上展示的那一版
const cachedNewCode = this.scriptUpdateCheck.cacheFull?.list?.find((entry) => entry.uuid === uuid)?.newCode;
Expand Down
23 changes: 22 additions & 1 deletion src/app/service/service_worker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ export type TBatchUpdateItemResult = {
};

/**
* UPDATE 动作的执行结果。
* UPDATE / IGNORE 动作的执行结果。
* ok 为 false 表示整批根本没有执行:Service Worker 的检查结果只存在于内存(ScriptUpdateCheck.cacheFull),
* MV3 回收 Service Worker 后即丢失,此时必须让调用方能与「逐条安装失败」区分开,提示用户重新检查更新。
*/
Expand All @@ -305,4 +305,25 @@ export type TBatchUpdateResult = {
items: TBatchUpdateItemResult[];
};

/** 检查更新的结果 */
export type TCheckScriptUpdateResult =
| {
ok: true;
targetSites: string[];
/** false 表示上次结果仍够新、本次并没有真的重新检查 */
fresh: boolean;
checktime: number;
err?: undefined;
}
| {
ok: false;
/** busy 区分「已有检查在跑」与真正的失败,二者对用户是不同的话 */
reason?: "busy";
targetSites?: undefined;
err?: string | Error;
};

/** 打开更新详情的结果:opened=已开出安装页,silent=已静默更新完成,failed=没能处理 */
export type TOpenUpdatePageResult = "opened" | "silent" | "failed";

export type TPopupScript = { tabId: number; uuids: string[] };
23 changes: 21 additions & 2 deletions src/locales/de-DE/install.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
"code_expand": "Ausklappen",
"loading_title": "Skript wird geladen",
"loading_desc": "Skriptinhalt wird von der Quelle heruntergeladen und ausgewertet",
"loading_title_update": "Update-Details werden vorbereitet",
"loading_desc_prepare": "Der vorbereitete Skriptinhalt wird gelesen",
"loading_desc_prepare_update": "Die heruntergeladene neue Version wird gelesen und mit der aktuellen verglichen",
"error_retry": "Erneut versuchen",
"error_invalid_desc": "Es fehlt ein gültiger Installationsquellen-Parameter, das Skript kann nicht geladen werden.",
"context_install": "Skript installieren",
Expand Down Expand Up @@ -129,7 +132,19 @@
"batch_done_partial": "{{updated}} aktualisiert, {{failed}} fehlgeschlagen",
"view_updated_scripts": "Aktualisierte Skripte anzeigen",
"record_expired": "Die Update-Daten sind abgelaufen. Bitte erneut nach Updates suchen.",
"open_failed": "Update-Details konnten nicht geöffnet werden. Bitte erneut versuchen."
"open_failed": "Update-Details konnten nicht geöffnet werden. Bitte erneut versuchen.",
"loading_list": "Update-Liste wird geladen",
"load_failed_title": "Update-Datensätze konnten nicht geladen werden",
"load_failed_desc": "Das Prüfergebnis konnte nicht aus dem Hintergrund gelesen werden. Bitte erneut versuchen.",
"script_list": "Skriptliste",
"check_busy": "Es läuft bereits eine Update-Prüfung",
"check_skipped": "Ergebnisse sind noch aktuell, es wurde nicht erneut geprüft",
"check_failed": "Update-Prüfung fehlgeschlagen, bitte später erneut versuchen",
"row_ignoring": "Wird ignoriert…",
"row_ignored": "v{{version}} ignoriert",
"row_ignore_failed": "Ignorieren fehlgeschlagen",
"batch_interrupted": "{{count}} aktualisiert, der Rest wurde abgebrochen, da das Prüfergebnis abgelaufen ist",
"silent_updated": "Automatisch auf v{{version}} aktualisiert, keine Bestätigung nötig"
},
"importpage": {
"title": "Datenimport",
Expand Down Expand Up @@ -234,5 +249,9 @@
"script_info_load_failed": "Skript-Informationen laden fehlgeschlagen!",
"btn_restore": "Wiederherstellen",
"btn_restore_update": "Wiederherstellen und aktualisieren",
"in_trash_hint": "Dieses Skript befindet sich im Papierkorb. Die Installation stellt es wieder her und behält die vorhandenen Daten."
"in_trash_hint": "Dieses Skript befindet sich im Papierkorb. Die Installation stellt es wieder her und behält die vorhandenen Daten.",
"expired_title": "Update-Inhalt ist abgelaufen",
"expired_desc": "Der für dieses Update vorbereitete Code wurde bereinigt. Bitte erneut nach Updates suchen.",
"expired_recheck": "Erneut nach Updates suchen",
"code_loading": "Code wird geladen"
}
Loading
Loading