Skip to content

Commit 3c6c946

Browse files
committed
Merge branch 'worktree-event-bus-refactor'
2 parents 5d08ed3 + 4590e71 commit 3c6c946

15 files changed

Lines changed: 772 additions & 95 deletions

File tree

src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import updateChecker from './scripts/updateChecker';
2121
import echoLog from './scripts/echoLog';
2222
import SteamASF from './scripts/social/SteamASF';
2323
import { debug } from './scripts/tools/debug';
24+
import EventBus from './scripts/events/eventBus';
2425
// import { getAllLocalStorageAsObjects } from './scripts/tools/tools';
2526
// import browser from 'browser-tool';
2627
// import { v4 as uuidv4 } from 'uuid';
@@ -365,11 +366,16 @@ const loadScript = async (): Promise<void> => {
365366
return;
366367
}
367368

369+
const eventBus = new EventBus();
370+
368371
let website: Website | undefined;
369372
for (const Website of (Websites as unknown as WebsiteClass[])) {
370373
if (Website.test()) {
371374
debug('识别到支持的网站', { website: Website.name });
372375
website = new Website();
376+
if (website.setEventBus) {
377+
website.setEventBus(eventBus);
378+
}
373379
break;
374380
}
375381
}

src/scripts/events/eventBus.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* @Author : HCLonely
3+
* @Date : 2026-05-11 10:00:00
4+
* @LastEditTime : 2026-05-11 10:00:00
5+
* @LastEditors : HCLonely
6+
* @FilePath : /auto-task/src/scripts/events/eventBus.ts
7+
* @Description : 事件总线核心
8+
*/
9+
10+
import type { EventMap } from './eventTypes';
11+
12+
export type EventHandler<K extends keyof EventMap> = (payload: EventMap[K]) => void | Promise<void>
13+
14+
type ListenerMap = {
15+
[K in keyof EventMap]?: Set<EventHandler<K>>
16+
}
17+
18+
class EventBus {
19+
private readonly listeners: ListenerMap = {};
20+
21+
on<K extends keyof EventMap>(event: K, handler: EventHandler<K>): void {
22+
const set = this.listeners[event] as Set<EventHandler<K>> | undefined;
23+
if (set) {
24+
set.add(handler);
25+
return;
26+
}
27+
this.listeners[event] = new Set<EventHandler<K>>([handler]) as ListenerMap[K];
28+
}
29+
30+
off<K extends keyof EventMap>(event: K, handler: EventHandler<K>): void {
31+
const set = this.listeners[event] as Set<EventHandler<K>> | undefined;
32+
if (!set) return;
33+
set.delete(handler);
34+
if (set.size === 0) {
35+
delete this.listeners[event];
36+
}
37+
}
38+
39+
once<K extends keyof EventMap>(event: K, handler: EventHandler<K>): void {
40+
const wrapper: EventHandler<K> = async (payload) => {
41+
this.off(event, wrapper);
42+
await handler(payload);
43+
};
44+
this.on(event, wrapper);
45+
}
46+
47+
async emit<K extends keyof EventMap>(event: K, payload: EventMap[K]): Promise<void> {
48+
const set = this.listeners[event] as Set<EventHandler<K>> | undefined;
49+
if (!set || set.size === 0) return;
50+
await Promise.allSettled(Array.from(set, (handler) => handler(payload)));
51+
}
52+
}
53+
54+
export default EventBus;

src/scripts/events/eventTypes.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* @Author : HCLonely
3+
* @Date : 2026-05-11 10:00:00
4+
* @LastEditTime : 2026-05-11 10:00:00
5+
* @LastEditors : HCLonely
6+
* @FilePath : /auto-task/src/scripts/events/eventTypes.ts
7+
* @Description : 事件总线类型定义
8+
*/
9+
10+
export type TaskAction = 'do' | 'undo'
11+
12+
export type EventTarget = 'reddit' | 'twitch' | 'twitter' | 'vk' | 'youtube' | 'steamStore' | 'steamCommunity' | 'links' | 'extra'
13+
14+
export interface BaseEventPayload {
15+
runId: string
16+
timestamp: number
17+
source: 'website' | 'social' | 'link' | 'system'
18+
}
19+
20+
export interface RequestedPayload extends BaseEventPayload {
21+
action: TaskAction
22+
target: EventTarget
23+
tasks: Record<string, Array<string>>
24+
}
25+
26+
export interface CompletedPayload extends BaseEventPayload {
27+
target: EventTarget
28+
ok: boolean
29+
processedCount?: number
30+
error?: string
31+
}
32+
33+
export interface EventMap {
34+
'task.classified': BaseEventPayload & { action: TaskAction }
35+
'social.init.requested': RequestedPayload
36+
'social.init.completed': CompletedPayload
37+
'social.toggle.requested': RequestedPayload
38+
'social.toggle.completed': CompletedPayload
39+
'task.links.requested': RequestedPayload
40+
'task.links.completed': CompletedPayload
41+
'task.extra.requested': RequestedPayload
42+
'task.extra.completed': CompletedPayload
43+
'task.batch.completed': BaseEventPayload & {
44+
ok: boolean
45+
successCount: number
46+
failedTargets: Array<EventTarget>
47+
}
48+
}

src/scripts/social/Reddit.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,14 @@ class Reddit extends Social {
8383
debug('初始化Reddit实例');
8484
this.tasks = defaultTasksTemplate;
8585
this.whiteList = { ...defaultTasksTemplate, ...(GM_getValue<whiteList>('whiteList')?.reddit || {}) };
86+
this.registerEventBusHandlers({
87+
target: 'reddit',
88+
init: async () => this.init(),
89+
toggle: async (payload) => this.toggle({
90+
doTask: payload.action === 'do',
91+
...(payload.tasks as { redditLinks?: Array<string> })
92+
})
93+
});
8694
}
8795

8896
/**

src/scripts/social/Social.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,19 @@
1010
import throwError from '../tools/throwError';
1111
import { unique } from '../tools/tools';
1212
import { debug } from '../tools/debug';
13+
import type EventBus from '../events/eventBus';
14+
import type { EventTarget, RequestedPayload } from '../events/eventTypes';
1315

1416
interface toggleParams {
1517
[name:string]:unknown
1618
}
1719

20+
interface SocialEventHandlers {
21+
target: EventTarget
22+
init?: (payload: RequestedPayload) => Promise<boolean | 'skip'>
23+
toggle?: (payload: RequestedPayload) => Promise<boolean>
24+
}
25+
1826
/**
1927
* 抽象类,表示社交功能的基类。
2028
*
@@ -29,6 +37,64 @@ abstract class Social {
2937
* @description 当前的社交任务列表。
3038
*/
3139
protected tasks!: socialTasks;
40+
protected eventBus?: EventBus;
41+
private readonly eventHandlers: Array<SocialEventHandlers> = [];
42+
private listenersAttached = false;
43+
private readonly handleInitRequested = async (payload: RequestedPayload): Promise<void> => {
44+
for (const handler of this.eventHandlers) {
45+
if (!handler.init || payload.target !== handler.target) continue;
46+
try {
47+
const result = await handler.init(payload);
48+
const isSkip = result === 'skip';
49+
const isSuccess = result === true || isSkip;
50+
await this.eventBus?.emit('social.init.completed', {
51+
runId: payload.runId,
52+
timestamp: Date.now(),
53+
source: 'social',
54+
target: handler.target,
55+
ok: isSuccess,
56+
processedCount: result === true ? 1 : 0
57+
});
58+
} catch (error) {
59+
await this.eventBus?.emit('social.init.completed', {
60+
runId: payload.runId,
61+
timestamp: Date.now(),
62+
source: 'social',
63+
target: handler.target,
64+
ok: false,
65+
processedCount: 0,
66+
error: (error as Error)?.message || String(error)
67+
});
68+
}
69+
}
70+
};
71+
private readonly handleToggleRequested = async (payload: RequestedPayload): Promise<void> => {
72+
const sanitizedPayload = this.sanitizeRequestedPayload(payload);
73+
for (const handler of this.eventHandlers) {
74+
if (!handler.toggle || sanitizedPayload.target !== handler.target) continue;
75+
try {
76+
const result = await handler.toggle(sanitizedPayload);
77+
await this.eventBus?.emit('social.toggle.completed', {
78+
runId: sanitizedPayload.runId,
79+
timestamp: Date.now(),
80+
source: 'social',
81+
target: handler.target,
82+
ok: !!result,
83+
processedCount: result ? 1 : 0
84+
});
85+
} catch (error) {
86+
await this.eventBus?.emit('social.toggle.completed', {
87+
runId: sanitizedPayload.runId,
88+
timestamp: Date.now(),
89+
source: 'social',
90+
target: handler.target,
91+
ok: false,
92+
processedCount: 0,
93+
error: (error as Error)?.message || String(error)
94+
});
95+
}
96+
}
97+
};
3298

3399
/**
34100
* 初始化社交功能。
@@ -55,6 +121,49 @@ abstract class Social {
55121
*/
56122
abstract toggle(toggleParams: toggleParams): Promise<boolean>;
57123

124+
setEventBus(eventBus: EventBus): void {
125+
if (this.eventBus && this.listenersAttached) {
126+
this.eventBus.off('social.init.requested', this.handleInitRequested);
127+
this.eventBus.off('social.toggle.requested', this.handleToggleRequested);
128+
this.listenersAttached = false;
129+
}
130+
this.eventBus = eventBus;
131+
this.attachEventBusListeners();
132+
}
133+
134+
protected registerEventBusHandlers(handlers: SocialEventHandlers): void {
135+
this.eventHandlers.push(handlers);
136+
this.attachEventBusListeners();
137+
}
138+
139+
private attachEventBusListeners(): void {
140+
if (!this.eventBus || this.listenersAttached || this.eventHandlers.length === 0) return;
141+
this.listenersAttached = true;
142+
this.eventBus.on('social.init.requested', this.handleInitRequested);
143+
this.eventBus.on('social.toggle.requested', this.handleToggleRequested);
144+
}
145+
146+
private sanitizeRequestedPayload(payload: RequestedPayload): RequestedPayload {
147+
const rawTasks = payload.tasks as Record<string, unknown> | undefined;
148+
if (!rawTasks || typeof rawTasks !== 'object') {
149+
return {
150+
...payload,
151+
tasks: {}
152+
};
153+
}
154+
155+
const tasks = Object.entries(rawTasks).reduce<Record<string, Array<string>>>((acc, [key, value]) => {
156+
if (!Array.isArray(value)) return acc;
157+
acc[key] = value.filter((item): item is string => typeof item === 'string');
158+
return acc;
159+
}, {});
160+
161+
return {
162+
...payload,
163+
tasks
164+
};
165+
}
166+
58167
/**
59168
* 获取实际参数数组,用于执行任务。
60169
*

src/scripts/social/Steam.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,50 @@ class Steam extends Social {
100100
this.tasks = defaultTasksTemplate;
101101
this.whiteList = { ...defaultTasksTemplate, ...(GM_getValue<whiteList>('whiteList')?.steam || {}) };
102102
this.#TaskExecutor = this.#getTaskExecutionOrder(globalOptions.ASF.AsfEnabled, globalOptions.ASF.steamWeb, globalOptions.ASF.preferASF);
103+
this.registerEventBusHandlers({
104+
target: 'steamStore',
105+
init: async () => this.init('store'),
106+
toggle: async (payload) => this.toggle({
107+
doTask: payload.action === 'do',
108+
...(payload.tasks as {
109+
groupLinks?: string[];
110+
officialGroupLinks?: string[];
111+
wishlistLinks?: string[];
112+
followLinks?: string[];
113+
forumLinks?: string[];
114+
workshopLinks?: string[];
115+
workshopVoteLinks?: string[];
116+
curatorLinks?: string[];
117+
curatorLikeLinks?: string[];
118+
announcementLinks?: string[];
119+
licenseLinks?: string[];
120+
playtestLinks?: string[];
121+
playTimeLinks?: string[];
122+
})
123+
})
124+
});
125+
this.registerEventBusHandlers({
126+
target: 'steamCommunity',
127+
init: async () => this.init('community'),
128+
toggle: async (payload) => this.toggle({
129+
doTask: payload.action === 'do',
130+
...(payload.tasks as {
131+
groupLinks?: string[];
132+
officialGroupLinks?: string[];
133+
wishlistLinks?: string[];
134+
followLinks?: string[];
135+
forumLinks?: string[];
136+
workshopLinks?: string[];
137+
workshopVoteLinks?: string[];
138+
curatorLinks?: string[];
139+
curatorLikeLinks?: string[];
140+
announcementLinks?: string[];
141+
licenseLinks?: string[];
142+
playtestLinks?: string[];
143+
playTimeLinks?: string[];
144+
})
145+
})
146+
});
103147
debug('Steam实例初始化完成', { taskExecutorCount: this.#TaskExecutor.length });
104148
}
105149
/**

src/scripts/social/Twitch.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,14 @@ class Twitch extends Social {
104104
debug('初始化Twitch实例');
105105
this.tasks = defaultTasksTemplate;
106106
this.whiteList = { ...defaultTasksTemplate, ...(GM_getValue<whiteList>('whiteList')?.twitch || {}) };
107+
this.registerEventBusHandlers({
108+
target: 'twitch',
109+
init: async () => this.init(),
110+
toggle: async (payload) => this.toggle({
111+
doTask: payload.action === 'do',
112+
...(payload.tasks as { channelLinks?: Array<string> })
113+
})
114+
});
107115
}
108116

109117
/**

src/scripts/social/Twitter.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,14 @@ class Twitter extends Social {
104104
'sec-ch-ua-mobile': '?0',
105105
'sec-ch-ua': generateSecCHUA()
106106
};
107+
this.registerEventBusHandlers({
108+
target: 'twitter',
109+
init: async () => this.init(),
110+
toggle: async (payload) => this.toggle({
111+
doTask: payload.action === 'do',
112+
...(payload.tasks as { userLinks?: Array<string>, retweetLinks?: Array<string> })
113+
})
114+
});
107115
}
108116

109117
/**

src/scripts/social/Vk.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,14 @@ class Vk extends Social {
130130
debug('初始化Vk实例');
131131
this.tasks = defaultTasksTemplate;
132132
this.whiteList = { ...defaultTasksTemplate, ...(GM_getValue<whiteList>('whiteList')?.vk || {}) };
133+
this.registerEventBusHandlers({
134+
target: 'vk',
135+
init: async () => this.init(),
136+
toggle: async (payload) => this.toggle({
137+
doTask: payload.action === 'do',
138+
...(payload.tasks as { nameLinks?: Array<string> })
139+
})
140+
});
133141
}
134142

135143
/**

src/scripts/social/Youtube.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,14 @@ class Youtube extends Social {
194194
debug('初始化YouTube实例');
195195
this.tasks = defaultTasksTemplate;
196196
this.whiteList = { ...defaultTasksTemplate, ...(GM_getValue<whiteList>('whiteList')?.youtube || {}) };
197+
this.registerEventBusHandlers({
198+
target: 'youtube',
199+
init: async () => this.init(),
200+
toggle: async (payload) => this.toggle({
201+
doTask: payload.action === 'do',
202+
...(payload.tasks as { channelLinks?: Array<string>, videoLinks?: Array<string> })
203+
})
204+
});
197205
}
198206

199207
/**

0 commit comments

Comments
 (0)