Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 0 additions & 2 deletions src/app/cache_key.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
export const CACHE_KEY_IMPORT_FILE = "importFile:"; // importFile 导入文件
export const CACHE_KEY_TAB_SCRIPT = "tabScript:";
export const CACHE_KEY_SCRIPT_INFO = "scriptInfo:"; // 加载脚本信息时的缓存
export const CACHE_KEY_FAVICON = "favicon:";
export const CACHE_KEY_SET_VALUE = "setValue:";
export const CACHE_KEY_PERMISSION = "permission:";
43 changes: 43 additions & 0 deletions src/app/repo/tempStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Repo } from "./repo";

export const TempStorageItemType = {
tempCode: 1,
} as const;

export type TempStorageItemType = ValueOf<typeof TempStorageItemType>;

export interface TempStorageItem {
key: string;
value: any;
savedAt: number;
type: TempStorageItemType;
}

export const TEMP_ENTRY_MIN_TIME = 60_000;

export class TempStorageDAO extends Repo<TempStorageItem> {
constructor() {
super("tempStorage");
}

save(value: TempStorageItem) {
return super._save(value.key, value);
}

async getValue<T>(key: string) {
return (await super.get(key))?.value as T | undefined;
}

async entries(type?: TempStorageItemType) {
const data = await super.find((key, value) => {
return type ? value.type === type : true;
});
return data;
}

async staleEntries(keeps: Set<string>) {
const now = Date.now();
const entries = await new TempStorageDAO().entries();
return entries.filter((entry) => !keeps.has(entry.key) && now - entry.savedAt > TEMP_ENTRY_MIN_TIME);
}
}
22 changes: 14 additions & 8 deletions src/app/service/service_worker/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import { uuidv4 } from "@App/pkg/utils/uuid";
import type { Group } from "@Packages/message/server";
import Logger from "@App/app/logger/logger";
import LoggerCore from "@App/app/logger/core";
import { cacheInstance } from "@App/app/cache";
import { CACHE_KEY_SCRIPT_INFO } from "@App/app/cache_key";
import {
checkSilenceUpdate,
getBrowserType,
Expand All @@ -23,7 +21,7 @@ import type {
} from "@App/app/repo/scripts";
import { SCRIPT_STATUS_DISABLE, SCRIPT_STATUS_ENABLE, ScriptCodeDAO } 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 ScriptInfo, type InstallSource, createTempCodeEntry } from "@App/pkg/utils/scriptInstall";
import { type ResourceService } from "./resource";
import { type ValueService } from "./value";
import { compileScriptCode } from "../content/utils";
Expand All @@ -47,6 +45,8 @@ import { getSimilarityScore, ScriptUpdateCheck } from "./script_update_check";
import { LocalStorageDAO } from "@App/app/repo/localStorage";
import { CompiledResourceDAO } from "@App/app/repo/resource";
import { initRegularUpdateCheck } from "./regular_updatecheck";
import { TempStorageDAO, TempStorageItemType } from "@App/app/repo/tempStorage";
import { cleanupStaleTempStorageEntries } from "./temp";

export type TCheckScriptUpdateOption = Partial<
{ checkType: "user"; noUpdateCheck?: number } | ({ checkType: "system" } & Record<string, any>)
Expand Down Expand Up @@ -371,9 +371,10 @@ export class ScriptService {
}

// 获取安装信息
getInstallInfo(uuid: string) {
const cacheKey = `${CACHE_KEY_SCRIPT_INFO}${uuid}`;
return cacheInstance.get<[boolean, ScriptInfo]>(cacheKey);
async getInstallInfo(uuid: string) {
const entry = await new TempStorageDAO().get(uuid);
cleanupStaleTempStorageEntries();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

每次调用都会启动cleanupStaleTempStorageEntries,而且内部逻辑复杂;我觉得做一个定时任务,每隔多久,扫描一次全部的,清理一下就可以了

return <[boolean, ScriptInfo, Record<string, any>]>entry?.value;
}

publishInstallScript(scriptFull: Script, options: any) {
Expand Down Expand Up @@ -906,8 +907,13 @@ export class ScriptService {
if (!metadata) {
throw new Error("parse script info failed");
}
const si = [update, createScriptInfo(uuid, code, url, upsertBy, metadata), options];
await cacheInstance.set(`${CACHE_KEY_SCRIPT_INFO}${uuid}`, si);
const si = await createTempCodeEntry(update, uuid, code, url, upsertBy, metadata, options);
await new TempStorageDAO().save({
key: uuid,
value: si,
savedAt: Date.now(),
type: TempStorageItemType.tempCode,
});
return 1;
}

Expand Down
14 changes: 9 additions & 5 deletions src/app/service/service_worker/subscribe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,15 @@ import { type SystemConfig } from "@App/pkg/config/config";
import { type IMessageQueue } from "@Packages/message/message_queue";
import { type Group } from "@Packages/message/server";
import { type ScriptService } from "./script";
import { createScriptInfo, type InstallSource } from "@App/pkg/utils/scriptInstall";
import { createTempCodeEntry, type InstallSource } from "@App/pkg/utils/scriptInstall";
import { type TInstallSubscribe } from "../queue";
import { checkSilenceUpdate } from "@App/pkg/utils/utils";
import { ltever } from "@App/pkg/utils/semver";
import { fetchScriptBody, parseMetadata, prepareSubscribeByCode } from "@App/pkg/utils/script";
import { cacheInstance } from "@App/app/cache";
import { uuidv4 } from "@App/pkg/utils/uuid";
import { CACHE_KEY_SCRIPT_INFO } from "@App/app/cache_key";
import i18n, { i18nName } from "@App/locales/locales";
import { InfoNotification } from "./utils";
import { TempStorageDAO, TempStorageItemType } from "@App/app/repo/tempStorage";

export class SubscribeService {
logger: Logger;
Expand Down Expand Up @@ -225,8 +224,13 @@ export class SubscribeService {
if (true === (await this.trySilenceUpdate(code, url))) {
// slience update
} else {
const si = [false, createScriptInfo(uuid, code, url, source, metadata), {}];
await cacheInstance.set(`${CACHE_KEY_SCRIPT_INFO}${uuid}`, si);
const si = await createTempCodeEntry(false, uuid, code, url, source, metadata, {});
await new TempStorageDAO().save({
key: uuid,
value: si,
savedAt: Date.now(),
type: TempStorageItemType.tempCode,
});
chrome.tabs.create({
url: `/src/install.html?uuid=${uuid}`,
});
Expand Down
39 changes: 39 additions & 0 deletions src/app/service/service_worker/temp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { cacheInstance } from "@App/app/cache";
import { TEMP_ENTRY_MIN_TIME, TempStorageDAO, TempStorageItemType } from "@App/app/repo/tempStorage";
import { removeCachedCodes } from "@App/pkg/utils/scriptInstall";
import { timeoutExecution } from "@App/pkg/utils/timer";

export const cleanupStaleTempStorageEntries = () => {
// 清除旧记录
const delay = Math.floor(5000 * Math.random()) + 10000; // 使用随机时间避免浏览器重启时大量Tabs同时执行清除
timeoutExecution(
`cid_100_cleanupStaleTempStorageEntries`,
() => {
cacheInstance
.tx(`keepTemp`, (val: Record<string, number> | undefined, tx) => {
const now = Date.now();
const keeps = new Set<string>();
const out: Record<string, number> = {};
for (const [k, ts] of Object.entries(val ?? {})) {
if (ts > 0 && now - ts < TEMP_ENTRY_MIN_TIME) {
keeps.add(`${k}`);
out[k] = ts;
}
}
tx.set(out);
return keeps;
})
.then(async (keeps) => {
const stales = await new TempStorageDAO().staleEntries(keeps);
if (stales.length) {
// 清理缓存
const list = stales.map((e) => e.key);
const list1 = stales.filter((entry) => entry.type === TempStorageItemType.tempCode).map((e) => e.key);
await removeCachedCodes(list1);
cacheInstance.dels(list);
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.

这里清理 stale TempStorage 条目时使用了 cacheInstance.dels(list),但 cacheInstancechrome.storage.session(见 src/app/cache.ts),而 TempStorageDAO 数据实际保存在 chrome.storage.localRepo 基类)。这会导致 tempStorage 条目永远不会被删除,长时间运行会持续堆积。建议改为调用 new TempStorageDAO().deletes(list)(或等价的 delete API)来删除 tempStorage: 前缀下的记录。

Suggested change
cacheInstance.dels(list);
await new TempStorageDAO().deletes(list);

Copilot uses AI. Check for mistakes.
}
});
},
delay
);
};
83 changes: 33 additions & 50 deletions src/pages/install/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
import type { Subscribe } from "@App/app/repo/subscribe";
import { i18nDescription, i18nName } from "@App/locales/locales";
import { useTranslation } from "react-i18next";
import { createScriptInfo, type ScriptInfo } from "@App/pkg/utils/scriptInstall";
import { createScriptInfoLocal, createTempCodeEntry, type ScriptInfo } from "@App/pkg/utils/scriptInstall";
import { getTempCode } from "@App/pkg/utils/scriptInstall";
import { parseMetadata, prepareScriptByCode, prepareSubscribeByCode } from "@App/pkg/utils/script";
import { nextTimeDisplay } from "@App/pkg/utils/cron";
import { scriptClient, subscribeClient } from "../store/features/script";
Expand All @@ -29,12 +30,12 @@
import { dayFormat } from "@App/pkg/utils/day_format";
import { intervalExecution, timeoutExecution } from "@App/pkg/utils/timer";
import { useSearchParams } from "react-router-dom";
import { CACHE_KEY_SCRIPT_INFO } from "@App/app/cache_key";
import { cacheInstance } from "@App/app/cache";
import { formatBytes, isPermissionOk } from "@App/pkg/utils/utils";
import { ScriptIcons } from "../options/routes/utils";
import { bytesDecode, detectEncoding } from "@App/pkg/utils/encoding";
import { prettyUrl } from "@App/pkg/utils/url-utils";
import { TempStorageDAO, TempStorageItemType } from "@App/app/repo/tempStorage";

const backgroundPromptShownKey = "background_prompt_shown";

Expand Down Expand Up @@ -129,51 +130,26 @@
return { code, metadata };
};

const cleanupStaleInstallInfo = (uuid: string) => {
// 页面打开时不清除当前uuid,每30秒更新一次记录
const f = () => {
cacheInstance.tx(`scriptInfoKeeps`, (val: Record<string, number> | undefined, tx) => {
val = val || {};
val[uuid] = Date.now();
tx.set(val);
});
};
f();
setInterval(f, 30_000);

// 页面打开后清除旧记录
const delay = Math.floor(5000 * Math.random()) + 10000; // 使用随机时间避免浏览器重启时大量Tabs同时执行清除
timeoutExecution(
`${cIdKey}cleanupStaleInstallInfo`,
() => {
cacheInstance
.tx(`scriptInfoKeeps`, (val: Record<string, number> | undefined, tx) => {
const now = Date.now();
const keeps = new Set<string>();
const out: Record<string, number> = {};
for (const [k, ts] of Object.entries(val ?? {})) {
if (ts > 0 && now - ts < 60_000) {
keeps.add(`${CACHE_KEY_SCRIPT_INFO}${k}`);
out[k] = ts;
}
}
tx.set(out);
return keeps;
})
.then(async (keeps) => {
const list = await cacheInstance.list();
const filtered = list.filter((key) => key.startsWith(CACHE_KEY_SCRIPT_INFO) && !keeps.has(key));
if (filtered.length) {
// 清理缓存
cacheInstance.dels(filtered);
}
});
},
delay
);
const cIdKey = `(cid_${Math.random()})`;

let activeSessionKey = "";
let keepAliveTimerId: ReturnType<typeof setTimeout> | number = 0;

const updateSessionTimestamp = () => {
cacheInstance.tx(`keepTemp`, (val: Record<string, number> | undefined, tx) => {
val = val || {};
val[activeSessionKey] = Date.now();
tx.set(val);
});
};

const cIdKey = `(cid_${Math.random()})`;
const startKeepAlive = (key: string) => {
activeSessionKey = key;
// 页面打开时不清除当前uuid,每30秒更新一次记录
updateSessionTimestamp();
clearInterval(keepAliveTimerId);
keepAliveTimerId = setInterval(updateSessionTimestamp, 30_000);
};

function App() {
const [enable, setEnable] = useState<boolean>(false);
Expand Down Expand Up @@ -235,14 +211,16 @@

let paramOptions = {};
if (uuid) {
startKeepAlive(uuid);
const cachedInfo = await scriptClient.getInstallInfo(uuid);
cleanupStaleInstallInfo(uuid);
if (cachedInfo?.[0]) isKnownUpdate = true;
info = cachedInfo?.[1] || undefined;
paramOptions = cachedInfo?.[2] || {};
if (!info) {
throw new Error("fetch script info failed");
}
const code = await getTempCode(uuid);
if (code) info.code = code;
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.

这里从 OPFS 读取安装代码后仅在 code 为 truthy 时才写回 info.code。但 getTempCode() 在任何异常都会返回空字符串,导致后续 prepareScriptByCode/prepareSubscribeByCode 直接拿到空代码并出现难以定位的错误/空白安装页。建议在 uuid 场景下把“读不到代码”视为错误(例如空字符串就抛错/提示用户),或让 getTempCode 返回 undefined/抛出以便区分“文件不存在/读取失败”和“代码为空”。

Suggested change
if (code) info.code = code;
// 这里从 OPFS 读不到代码(getTempCode 返回空字符串)应视为错误,避免后续拿到空代码导致空白安装页
if (code === "") {
throw new Error("failed to load script code from temp storage");
}
if (code) {
info.code = code;
}

Copilot uses AI. Check for mistakes.
} else {
// 检查是不是本地文件安装
if (!fid) {
Expand Down Expand Up @@ -272,7 +250,8 @@
if (!metadata) {
throw new Error("parse script info failed");
}
info = createScriptInfo(uuidv4(), code, `file:///*from-local*/${file.name}`, "user", metadata);
const uuid = uuidv4();
info = await createScriptInfoLocal(uuid, code, `file:///*from-local*/${file.name}`, "user", metadata);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

把uuid拆开的意义何在

}

let prepare:
Expand Down Expand Up @@ -326,7 +305,7 @@

useEffect(() => {
!loaded && initAsync();
}, [searchParams, loaded]);

Check warning on line 308 in src/pages/install/App.tsx

View workflow job for this annotation

GitHub Actions / Run tests

React Hook useEffect has a missing dependency: 'initAsync'. Either include it or remove the dependency array

const [watchFile, setWatchFile] = useState(false);
const metadataLive = useMemo(() => (scriptInfo?.metadata || {}) as SCMetadata, [scriptInfo]);
Expand Down Expand Up @@ -640,7 +619,7 @@
return () => {
unmountFileTrack(handle);
};
}, [memoWatchFile]);

Check warning on line 622 in src/pages/install/App.tsx

View workflow job for this annotation

GitHub Actions / Run tests

React Hook useEffect has missing dependencies: 'localFileHandle', 'scriptInfo?.uuid', 'setupWatchFile', and 'watchFile'. Either include them or remove the dependency array

// 检查是否有 uuid 或 file
const searchParamUrl = searchParams.get("url");
Expand Down Expand Up @@ -693,9 +672,13 @@

// 3. 处理数据与缓存
const uuid = uuidv4();
const scriptData = [false, createScriptInfo(uuid, code, url, "user", metadata)];

await cacheInstance.set(`${CACHE_KEY_SCRIPT_INFO}${uuid}`, scriptData);
const si = await createTempCodeEntry(false, uuid, code, url, "user", metadata, {});
await new TempStorageDAO().save({
key: uuid,
value: si,
savedAt: Date.now(),
type: TempStorageItemType.tempCode,
});

// 4. 更新导向
setSearchParams(new URLSearchParams(`?uuid=${uuid}`), { replace: true });
Expand Down
Loading
Loading