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
25 changes: 13 additions & 12 deletions src/app/repo/subscribe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,28 @@ import type { SCMetadata } from "./metadata";

export { SCMetadata };

export type SUBSCRIBE_STATUS = 1 | 2 | 3 | 4;
export const SUBSCRIBE_STATUS_ENABLE: SUBSCRIBE_STATUS = 1;
export const SUBSCRIBE_STATUS_DISABLE: SUBSCRIBE_STATUS = 2;
export const SubscribeStatusType = {
enable: 1, // 启动 checkSubscribeUpdate
disable: 2, // 停用 checkSubscribeUpdate
unknown3: 3, // 3 是什么?
unknown4: 4, // 4 是什么?
Comment on lines +9 to +10
Copy link
Collaborator Author

@cyfung1031 cyfung1031 Feb 4, 2026

Choose a reason for hiding this comment

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

@CodFrm
你看看要不要删掉 3 和 4
现在的代码已经没有 3 和 4
可能是你的旧代码留下来的?

Copy link
Member

Choose a reason for hiding this comment

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

我也不记得了,应该可以删除

} as const;

export type SubscribeStatusType = ValueOf<typeof SubscribeStatusType>;

export interface SubscribeScript {
uuid: string;
url: string;
url: string; // url of the user.js
}

export interface Subscribe {
url: string;
url: string; // url of the user.sub.js; 作为唯一键。暂时只支持网址。( 如需要支持 手动生成 Subscribe,日后可升级成 url / uuid )
name: string;
code: string;
code: string; // (meta) code of the user.sub.js
author: string;
scripts: { [key: string]: SubscribeScript };
scripts: Record<string, SubscribeScript>; // 这里只储存脚本的 uuid 和 url 等资讯,而不是实际的代码
metadata: SCMetadata;
status: SUBSCRIBE_STATUS;
status: SubscribeStatusType; // 表示启动或停用。 3 和 4 不详
createtime: number;
updatetime?: number;
checktime: number;
Expand All @@ -30,10 +35,6 @@ export class SubscribeDAO extends Repo<Subscribe> {
super("subscribe");
}

public findByUrl(url: string) {
return this.get(url);
}

public save(val: Subscribe) {
return super._save(val.url, val);
}
Expand Down
136 changes: 89 additions & 47 deletions src/app/service/service_worker/subscribe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import LoggerCore from "@App/app/logger/core";
import Logger from "@App/app/logger/logger";
import { ScriptDAO } from "@App/app/repo/scripts";
import type { SCMetadata, Subscribe, SubscribeScript } from "@App/app/repo/subscribe";
import { SUBSCRIBE_STATUS_DISABLE, SUBSCRIBE_STATUS_ENABLE, SubscribeDAO } from "@App/app/repo/subscribe";
import { SubscribeStatusType, SubscribeDAO } from "@App/app/repo/subscribe";
import { type SystemConfig } from "@App/pkg/config/config";
import { type IMessageQueue } from "@Packages/message/message_queue";
import { type Group } from "@Packages/message/server";
Expand Down Expand Up @@ -32,13 +32,17 @@ export class SubscribeService {
}

async install(param: { subscribe: Subscribe }) {
// 1)由安装页呼叫,进行 user.sub.js 的安装
// 2)静默更新启动状态下,Subscribe 列表自动更新
const logger = this.logger.with({
subscribeUrl: param.subscribe.url,
name: param.subscribe.name,
});
try {
await this.subscribeDAO.save(param.subscribe);
await this.subscribeDAO.save(param.subscribe); // 所谓的安装,仅储存脚本资源。
logger.info("upsert subscribe success");
// 广播后才会根据 subscrbie.scripts 的 url 取得/更新脚本
// 注:installSubscribe 的广播是自己和自己对话。(不等待回应)
this.mq.publish<TInstallSubscribe>("installSubscribe", {
subscribe: param.subscribe,
});
Expand Down Expand Up @@ -84,85 +88,116 @@ export class SubscribeService {
}
}

// 更新订阅的脚本
// 更新订阅的脚本( installSubscribe )
// 已订阅的脚本则根据 Script脚本 本身的更新逻辑更新,与 Subscribe脚本 的更新无关
async upsertScript(url: string) {
const subscribe = await this.subscribeDAO.get(url);
if (!subscribe) return;
if (!subscribe || !subscribe.metadata.usersubscribe) return; // 有效的 Subscribe 必定有 usersubscribe
const logger = this.logger.with({
url: subscribe.url,
name: subscribe.name,
});
// 对比脚本是否有变化
const addScript: string[] = [];
const removeScript: SubscribeScript[] = [];
const scriptUrl = subscribe.metadata.scripturl || [];
const scripts = Object.keys(subscribe.scripts);
for (const url of scriptUrl) {
const addedScripts: string[] = [];
const removedScripts: SubscribeScript[] = [];
const metaScriptUrlSet = new Set(subscribe.metadata.scripturl || []); // 订阅列表
const subscribeScripts = new Set(Object.keys(subscribe.scripts)); // 已关联 uuid 的列表
// 注:首次安装时, subscribeScripts 是空的。
for (const url of metaScriptUrlSet) {
// 不存在于已安装的脚本中, 则添加
if (!scripts.includes(url)) {
addScript.push(url);
if (!subscribeScripts.has(url)) {
addedScripts.push(url);
}
}
for (const url of scripts) {
for (const url of subscribeScripts) {
// 不存在于订阅的脚本中, 则删除
if (!scriptUrl.includes(url)) {
removeScript.push(subscribe.scripts[url]);
if (!metaScriptUrlSet.has(url)) {
removedScripts.push(subscribe.scripts[url]);
}
}

const notification: string[][] = [[], []];
const result: Promise<boolean>[] = [];
// 添加脚本
addScript.forEach((url) => {
result.push(
const addedScriptNames: string[] = [];
const removedScriptNames: string[] = [];
const promises: Promise<void>[] = [];
// 添加脚本: 根据 订阅列表 的 Script脚本URLs 进行安装
addedScripts.forEach((url) => {
promises.push(
(async () => {
const script = await this.scriptService.installByUrl(url, "subscribe", subscribe.url);
subscribe.scripts[url] = {
url,
uuid: script.uuid,
};
notification[0].push(i18nName(script));
return true;
// 先找一下已安装的 scripts
const existingScript = await this.scriptDAO.find((_key, script) => {
return script.downloadUrl === url || script.origin === url;
});
if (existingScript?.[0]) {
// 仅关联至 已安装脚本的 uuid
// 注:1)已安装的脚本可能是用户用直接下载方式安装
// 2)已安装的脚本可能是用户用其他 Subscribe 安装
// 这里的 existingScript 的 subscribeUrl 值不一定是这个 Subscribe 的 url
subscribe.scripts[url] = {
url,
uuid: existingScript[0].uuid,
};
} else {
// 安装Script脚本 ( script.subscribeUrl 会指定为这个 Subscribe. 当移除 Subscribe 时会一并移除 )
const script = await this.scriptService.installByUrl(url, "subscribe", subscribe.url);
const name = i18nName(script);
// 把Script脚本关联至Subscribe
subscribe.scripts[url] = {
url,
uuid: script.uuid,
};
addedScriptNames.push(name);
}
})().catch((e) => {
logger.error("install script failed", Logger.E(e));
return false;
})
);
});
// 删除脚本
removeScript.forEach((item) => {
// 删除脚本: 根据 subscribeScripts 的 Script脚本UUIDs 进行反安装
removedScripts.forEach((item) => {
// 通过uuid查询脚本id
result.push(
promises.push(
(async () => {
const script = await this.scriptDAO.findByUUID(item.uuid);
// 以 uuid 找出已安装的Script脚本资讯
const script = await this.scriptDAO.get(item.uuid);
const url = item.url;
if (script) {
notification[1].push(i18nName(script));
// 删除脚本
this.scriptService.deleteScript(script.uuid);
const name = i18nName(script);
// 如果不是以 此 Subscribe 安装的话则略过删除 ( 例如其他 Subscribe, 直接Script安装,本地安装,等等 )
if (script.subscribeUrl === subscribe.url) {
delete subscribe.scripts[url];
// 删除脚本
await this.scriptService.deleteScript(script.uuid);
removedScriptNames.push(name);
} else {
logger.warn("Subscribe Update: skip deletion", {
scriptUUID: script.uuid,
scriptUrl: url,
scriptName: name,
});
}
}
return true;
})().catch((e) => {
logger.error("delete script failed", Logger.E(e));
return false;
})
);
});

await Promise.allSettled(result);
await Promise.allSettled(promises);

// 把 subscribe.scripts 的新资讯储存到 subscribeDAO
await this.subscribeDAO.update(subscribe.url, subscribe);

InfoNotification(
i18n.t("notification.subscribe_update", { subscribeName: subscribe.name }),
i18n.t("notification.subscribe_update_desc", {
newScripts: notification[0].join(","),
deletedScripts: notification[1].join(","),
newScripts: addedScriptNames.join(","),
deletedScripts: removedScriptNames.join(","),
})
);

logger.info("subscribe update", {
install: notification[0],
update: notification[1],
logger.info("subscribe list update", {
installed: addedScriptNames,
deleted: removedScriptNames,
});

return true;
Expand All @@ -184,9 +219,9 @@ export class SubscribeService {
});
try {
if (delayFn) await delayFn();
const code = await fetchScriptBody(url);
const metadata = parseMetadata(code);
if (!metadata) {
const code = await fetchScriptBody(url); // user.sub.js 的 代码
const metadata = parseMetadata(code); // user.sub.js 的 metadata = 代码内容分析; metadata.usersubscribe 是 空阵列
if (!metadata || !metadata.usersubscribe) {
logger.error("parse metadata failed");
return false;
}
Expand All @@ -211,11 +246,17 @@ export class SubscribeService {
}

// 检查更新
/**
* @param url Subscribe脚本 的 url
* @param source 系统自动检查: "system"; subscribeClient.checkUpdate(subscribe.url) 的时候: "user"
* @returns
*/
async checkUpdate(url: string, source: InstallSource) {
const subscribe = await this.subscribeDAO.get(url);
if (!subscribe) {
return false;
}
// 先写入更新触发时间
await this.subscribeDAO.update(url, { checktime: Date.now() });
const logger = this.logger.with({
url: subscribe.url,
Expand Down Expand Up @@ -254,6 +295,7 @@ export class SubscribeService {
if (silenceUpdate) {
try {
const newSubscribe = await prepareSubscribeByCode(code, url);
// 由于 Subscribe 不会含有 @connect, 因此静默更新启动的话, Subscribe 列表本身总是自动更新。
if (checkSilenceUpdate(newSubscribe.oldSubscribe!.metadata, newSubscribe.subscribe.metadata)) {
logger.info("silence update subscribe");
this.install({
Expand All @@ -273,7 +315,7 @@ export class SubscribeService {
});

for (const subscribe of list) {
if (!checkDisable && subscribe.status === SUBSCRIBE_STATUS_ENABLE) {
if (!checkDisable && subscribe.status === SubscribeStatusType.disable) {
continue;
}
this.checkUpdate(subscribe.url, "system");
Expand All @@ -290,7 +332,7 @@ export class SubscribeService {
});
try {
await this.subscribeDAO.update(param.url, {
status: param.enable ? SUBSCRIBE_STATUS_ENABLE : SUBSCRIBE_STATUS_DISABLE,
status: param.enable ? SubscribeStatusType.enable : SubscribeStatusType.disable,
});
logger.info("enable subscribe success");
return true;
Expand Down
2 changes: 1 addition & 1 deletion src/pages/install/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@

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

Check warning on line 329 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 @@ -475,7 +475,7 @@

try {
if (scriptInfo?.userSubscribe) {
await subscribeClient.install(upsertScript as Subscribe);
await subscribeClient.install(upsertScript as Subscribe); // 首次安装时,upsertScript 里的 scripts 为空物件
Message.success(t("subscribe_success")!);
setBtnText(t("subscribe_success")!);
} else {
Expand Down Expand Up @@ -641,7 +641,7 @@
return () => {
unmountFileTrack(handle);
};
}, [memoWatchFile]);

Check warning on line 644 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 hasUUIDorFile = useMemo(() => {
Expand Down Expand Up @@ -708,7 +708,7 @@
useEffect(() => {
if (!urlHref) return;
loadURLAsync(urlHref);
}, [urlHref]);

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

View workflow job for this annotation

GitHub Actions / Run tests

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

if (!hasUUIDorFile) {
return urlHref ? (
Expand Down
10 changes: 5 additions & 5 deletions src/pages/options/routes/SubscribeList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
Typography,
} from "@arco-design/web-react";
import type { Subscribe } from "@App/app/repo/subscribe";
import { SUBSCRIBE_STATUS_DISABLE, SUBSCRIBE_STATUS_ENABLE, SubscribeDAO } from "@App/app/repo/subscribe";
import { SubscribeStatusType, SubscribeDAO } from "@App/app/repo/subscribe";
import type { ColumnProps } from "@arco-design/web-react/es/Table";
import { IconSearch, IconUserAdd } from "@arco-design/web-react/icon";
import { semTime } from "@App/pkg/utils/dayjs";
Expand Down Expand Up @@ -69,18 +69,18 @@ function SubscribeList() {
filters: [
{
text: t("enable"),
value: SUBSCRIBE_STATUS_ENABLE,
value: SubscribeStatusType.enable,
},
{
text: t("disable"),
value: SUBSCRIBE_STATUS_DISABLE,
value: SubscribeStatusType.disable,
},
],
onFilter: (value, row) => row.status === value,
render: (col, item: ListType, index) => {
return (
<Switch
checked={item.status === SUBSCRIBE_STATUS_ENABLE}
checked={item.status === SubscribeStatusType.enable}
loading={item.loading}
disabled={item.loading}
onChange={(checked) => {
Expand All @@ -98,7 +98,7 @@ function SubscribeList() {
setListEntry(index, {
loading: false,
...(statusChange && {
status: checked ? SUBSCRIBE_STATUS_ENABLE : SUBSCRIBE_STATUS_DISABLE,
status: checked ? SubscribeStatusType.enable : SubscribeStatusType.disable,
}),
});
});
Expand Down
2 changes: 1 addition & 1 deletion src/pkg/backup/struct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export type SubscribeMeta = {

export type SubscribeOptionsFile = {
settings: { enabled: boolean };
scripts: { [key: string]: SubscribeScript };
scripts: Record<string, SubscribeScript>;
meta: SubscribeMeta;
};

Expand Down
Loading
Loading