@web-auto/webauto 0.1.3 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/desktop-console/default-settings.json +2 -2
- package/apps/desktop-console/dist/main/index.mjs +915 -85
- package/apps/desktop-console/dist/main/preload.mjs +7 -0
- package/apps/desktop-console/dist/renderer/index.html +622 -50
- package/apps/desktop-console/dist/renderer/index.js +2415 -470
- package/apps/desktop-console/dist/renderer/run.mts +6 -5
- package/apps/desktop-console/entry/ui-cli.mjs +672 -0
- package/apps/desktop-console/entry/ui-console.mjs +416 -29
- package/apps/webauto/entry/account.mjs +89 -53
- package/apps/webauto/entry/browser-status.mjs +7 -10
- package/apps/webauto/entry/lib/account-detect.mjs +254 -28
- package/apps/webauto/entry/lib/account-store.mjs +219 -30
- package/apps/webauto/entry/lib/bus-publish.mjs +63 -0
- package/apps/webauto/entry/lib/camo-cli.mjs +93 -0
- package/apps/webauto/entry/lib/profilepool.mjs +14 -5
- package/apps/webauto/entry/lib/quota-status.mjs +23 -0
- package/apps/webauto/entry/lib/schedule-store.mjs +1068 -0
- package/apps/webauto/entry/profilepool.mjs +106 -17
- package/apps/webauto/entry/schedule.mjs +612 -0
- package/apps/webauto/entry/weibo-unified.mjs +134 -0
- package/apps/webauto/entry/xhs-install.mjs +236 -29
- package/apps/webauto/entry/xhs-status.mjs +5 -2
- package/apps/webauto/entry/xhs-unified.mjs +631 -98
- package/apps/webauto/resources/container-library/weibo/weibo_detail_page/comment_item/container.json +40 -0
- package/apps/webauto/resources/container-library/weibo/weibo_detail_page/reply_expand_button/container.json +38 -0
- package/apps/webauto/resources/container-library/weibo/weibo_detail_page/reply_list/container.json +37 -0
- package/apps/webauto/resources/container-library/weibo/weibo_search_page/container.json +8 -3
- package/apps/webauto/resources/container-library/weibo/weibo_search_page/login_anchor/container.json +30 -0
- package/apps/webauto/resources/container-library/weibo/weibo_search_page/search_bar/container.json +47 -0
- package/apps/webauto/resources/container-library/weibo/weibo_search_page/search_button/container.json +39 -0
- package/bin/camoufox-cli.mjs +61 -0
- package/bin/webauto.mjs +301 -54
- package/dist/modules/camo-backend/src/index.js +49 -1
- package/dist/modules/camo-backend/src/internal/BrowserSession.js +572 -3
- package/dist/modules/camo-backend/src/internal/SessionManager.js +13 -1
- package/dist/modules/camo-backend/src/internal/storage-paths.js +6 -0
- package/dist/modules/collection-manager/bloom-filter.js +91 -0
- package/dist/modules/collection-manager/date-utils.js +275 -0
- package/dist/modules/collection-manager/index.js +258 -0
- package/dist/modules/collection-manager/storage.js +195 -0
- package/dist/modules/collection-manager/types.js +47 -0
- package/dist/modules/logging/src/index.js +1 -1
- package/dist/modules/process-registry/index.js +230 -0
- package/dist/modules/rate-limiter/index.js +242 -0
- package/dist/modules/workflow/blocks/ExecuteWeiboSearchBlock.js +128 -0
- package/dist/modules/workflow/blocks/PersistXhsNoteBlock.js +7 -3
- package/dist/modules/workflow/blocks/RenderMarkdown.js +4 -1
- package/dist/modules/workflow/blocks/WeiboCollectCommentsBlock.js +282 -0
- package/dist/modules/workflow/blocks/WeiboCollectFromLinksBlock.js +283 -0
- package/dist/modules/workflow/blocks/WeiboCollectSearchLinksBlock.js +208 -0
- package/dist/modules/workflow/blocks/WeiboCollectTimelineListBlock.js +128 -0
- package/dist/modules/workflow/blocks/WeiboCollectUserPostsListBlock.js +127 -0
- package/dist/modules/workflow/blocks/helpers/downloadPaths.js +21 -0
- package/dist/modules/workflow/config/workflowRegistry.js +2 -0
- package/dist/modules/workflow/definitions/weibo-search-workflow-v1.js +47 -0
- package/dist/modules/workflow/src/runner.js +6 -0
- package/dist/modules/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.js +4 -0
- package/dist/modules/xiaohongshu/app/src/blocks/Phase3InteractBlock.js +2 -2
- package/dist/modules/xiaohongshu/app/src/blocks/helpers/sharding.js +123 -0
- package/dist/modules/xiaohongshu/app/src/container-registry/src/index.d.ts +37 -0
- package/dist/modules/xiaohongshu/app/src/container-registry/src/index.js +184 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/AnchorVerificationBlock.d.ts +31 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/AnchorVerificationBlock.js +71 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/DetectPageStateBlock.d.ts +48 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/DetectPageStateBlock.js +259 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/ErrorRecoveryBlock.d.ts +28 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/ErrorRecoveryBlock.js +319 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/WaitSearchPermitBlock.d.ts +36 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/WaitSearchPermitBlock.js +162 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/containerAnchors.d.ts +36 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/containerAnchors.js +301 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/operationLogger.d.ts +29 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/operationLogger.js +195 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/searchPageState.d.ts +25 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/searchPageState.js +164 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/MatchCommentsBlock.d.ts +66 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/MatchCommentsBlock.js +139 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1EnsureServicesBlock.d.ts +16 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1EnsureServicesBlock.js +36 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1MonitorCookieBlock.d.ts +27 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1MonitorCookieBlock.js +213 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.d.ts +18 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.js +121 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2CollectLinksBlock.d.ts +34 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2CollectLinksBlock.js +1249 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2SearchBlock.d.ts +17 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2SearchBlock.js +703 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseDetailBlock.d.ts +15 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseDetailBlock.js +41 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseTabsBlock.d.ts +26 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseTabsBlock.js +44 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CollectCommentsBlock.d.ts +29 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CollectCommentsBlock.js +150 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ExtractDetailBlock.d.ts +38 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ExtractDetailBlock.js +117 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenDetailBlock.d.ts +30 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenDetailBlock.js +102 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenTabsBlock.d.ts +23 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenTabsBlock.js +109 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.d.ts +32 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.js +117 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ProcessSingleNoteBlock.d.ts +35 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ProcessSingleNoteBlock.js +114 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ValidateLinksBlock.d.ts +34 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ValidateLinksBlock.js +90 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase3InteractBlock.d.ts +111 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase3InteractBlock.js +1009 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase4MultiTabHarvestBlock.d.ts +20 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase4MultiTabHarvestBlock.js +233 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/ReplyInteractBlock.d.ts +48 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/ReplyInteractBlock.js +291 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/XhsDiscoverFallbackBlock.d.ts +23 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/XhsDiscoverFallbackBlock.js +240 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatchDsl.d.ts +55 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatchDsl.js +126 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatcher.d.ts +21 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatcher.js +99 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/evidence.d.ts +5 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/evidence.js +27 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/sharding.d.ts +37 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/sharding.js +165 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/xhsComments.d.ts +33 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/xhsComments.js +270 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/index.d.ts +9 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/index.js +9 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/checkpoints.d.ts +50 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/checkpoints.js +222 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/controllerAction.d.ts +10 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/controllerAction.js +43 -0
- package/dist/services/shared/serviceProcessLogger.js +1 -1
- package/dist/services/unified-api/server.js +105 -11
- package/modules/camo-backend/src/index.ts +46 -1
- package/modules/camo-backend/src/internal/BrowserSession.ts +619 -3
- package/modules/camo-backend/src/internal/SessionManager.ts +12 -1
- package/modules/camo-backend/src/internal/storage-paths.ts +5 -0
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/comments.mjs +38 -2
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +47 -2
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +94 -11
- package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +208 -2
- package/modules/camo-runtime/src/autoscript/runtime.mjs +7 -1
- package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +76 -43
- package/modules/camo-runtime/src/container/runtime-core/operations/index.mjs +75 -1
- package/modules/camo-runtime/src/container/runtime-core/operations/selector-scripts.mjs +71 -4
- package/modules/camo-runtime/src/container/runtime-core/operations/tab-pool.mjs +183 -27
- package/modules/collection-manager/bloom-filter.ts +112 -0
- package/modules/collection-manager/date-utils.ts +316 -0
- package/modules/collection-manager/index.ts +309 -0
- package/modules/collection-manager/package.json +10 -0
- package/modules/collection-manager/storage.ts +174 -0
- package/modules/collection-manager/types.ts +156 -0
- package/modules/logging/src/index.ts +1 -1
- package/modules/process-registry/index.ts +284 -0
- package/modules/rate-limiter/index.ts +322 -0
- package/modules/state/src/paths.ts +9 -1
- package/modules/task-scheduler/index.ts +293 -0
- package/modules/workflow/blocks/ExecuteWeiboSearchBlock.ts +167 -0
- package/modules/workflow/blocks/PersistXhsNoteBlock.ts +7 -3
- package/modules/workflow/blocks/RenderMarkdown.ts +4 -1
- package/modules/workflow/blocks/WeiboCollectCommentsBlock.ts +339 -0
- package/modules/workflow/blocks/WeiboCollectFromLinksBlock.ts +338 -0
- package/modules/workflow/blocks/helpers/downloadPaths.ts +16 -0
- package/modules/workflow/config/workflowRegistry.ts +2 -0
- package/modules/workflow/definitions/weibo-search-workflow-v1.ts +47 -0
- package/modules/workflow/src/runner.ts +6 -0
- package/modules/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.ts +1 -1
- package/modules/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.ts +4 -0
- package/modules/xiaohongshu/app/src/blocks/Phase3InteractBlock.ts +2 -3
- package/modules/xiaohongshu/app/src/blocks/helpers/sharding.ts +152 -0
- package/package.json +14 -5
- package/scripts/postinstall-resources.mjs +62 -0
- package/scripts/test/run-coverage.mjs +76 -0
- package/scripts/weibo/search.ts +49 -0
- package/services/shared/serviceProcessLogger.ts +1 -1
- package/services/unified-api/server.ts +98 -12
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Scheduler Module
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Task queuing with priority
|
|
6
|
+
* - Conflict detection (same profile)
|
|
7
|
+
* - Task lifecycle management
|
|
8
|
+
* - Max runs tracking
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { EventEmitter } from 'events';
|
|
12
|
+
import { execSync } from 'child_process';
|
|
13
|
+
|
|
14
|
+
export type TaskStatus = 'pending' | 'queued' | 'running' | 'completed' | 'failed' | 'cancelled';
|
|
15
|
+
|
|
16
|
+
export interface ScheduledTask {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
profile: string;
|
|
20
|
+
platform: 'xiaohongshu' | 'weibo' | '1688';
|
|
21
|
+
taskType: 'search' | 'timeline' | 'user-monitor';
|
|
22
|
+
config: Record<string, any>;
|
|
23
|
+
schedule: {
|
|
24
|
+
type: 'interval' | 'daily' | 'once';
|
|
25
|
+
intervalMinutes?: number;
|
|
26
|
+
dailyTime?: string; // HH:MM
|
|
27
|
+
};
|
|
28
|
+
maxRuns: number | null;
|
|
29
|
+
currentRuns: number;
|
|
30
|
+
priority: number;
|
|
31
|
+
status: TaskStatus;
|
|
32
|
+
createdAt: string;
|
|
33
|
+
lastRunAt: string | null;
|
|
34
|
+
nextRunAt: string | null;
|
|
35
|
+
pid: number | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface TaskRunResult {
|
|
39
|
+
taskId: string;
|
|
40
|
+
runId: string;
|
|
41
|
+
success: boolean;
|
|
42
|
+
error?: string;
|
|
43
|
+
startedAt: string;
|
|
44
|
+
finishedAt: string;
|
|
45
|
+
durationMs: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class TaskScheduler extends EventEmitter {
|
|
49
|
+
private tasks: Map<string, ScheduledTask> = new Map();
|
|
50
|
+
private runningProfiles: Map<string, string> = new Map(); // profile -> taskId
|
|
51
|
+
private queue: string[] = [];
|
|
52
|
+
private maxRunTimeMs: number;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Terminate a process by PID (cross-platform)
|
|
56
|
+
*/
|
|
57
|
+
private terminateProcess(pid: number): void {
|
|
58
|
+
if (process.platform === 'win32') {
|
|
59
|
+
try {
|
|
60
|
+
execSync(`taskkill /PID ${pid} /T /F`, { stdio: 'ignore' });
|
|
61
|
+
} catch {
|
|
62
|
+
// Ignore errors if process already terminated
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
try {
|
|
66
|
+
process.kill(pid, 'SIGTERM');
|
|
67
|
+
} catch {
|
|
68
|
+
// Ignore errors if process already terminated
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
constructor(options: { maxRunTimeMs?: number } = {}) {
|
|
74
|
+
super();
|
|
75
|
+
this.maxRunTimeMs = options.maxRunTimeMs || 30 * 60 * 1000; // 30 min default
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Add a new scheduled task
|
|
80
|
+
*/
|
|
81
|
+
addTask(task: Omit<ScheduledTask, 'currentRuns' | 'status' | 'lastRunAt' | 'nextRunAt' | 'pid'>): string {
|
|
82
|
+
const fullTask: ScheduledTask = {
|
|
83
|
+
...task,
|
|
84
|
+
currentRuns: 0,
|
|
85
|
+
status: 'pending',
|
|
86
|
+
lastRunAt: null,
|
|
87
|
+
nextRunAt: null,
|
|
88
|
+
pid: null
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
this.tasks.set(task.id, fullTask);
|
|
92
|
+
this.emit('task:added', { task: fullTask });
|
|
93
|
+
|
|
94
|
+
return task.id;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if a profile is available (not running another task)
|
|
99
|
+
*/
|
|
100
|
+
isProfileAvailable(profile: string): boolean {
|
|
101
|
+
return !this.runningProfiles.has(profile);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get running task for a profile
|
|
106
|
+
*/
|
|
107
|
+
getRunningTaskForProfile(profile: string): ScheduledTask | null {
|
|
108
|
+
const taskId = this.runningProfiles.get(profile);
|
|
109
|
+
return taskId ? this.tasks.get(taskId) || null : null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Queue a task for execution
|
|
114
|
+
* Returns false if conflict detected
|
|
115
|
+
*/
|
|
116
|
+
queueTask(taskId: string): { queued: boolean; reason?: string; conflictingTask?: ScheduledTask } {
|
|
117
|
+
const task = this.tasks.get(taskId);
|
|
118
|
+
if (!task) {
|
|
119
|
+
return { queued: false, reason: 'Task not found' };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check max runs
|
|
123
|
+
if (task.maxRuns !== null && task.currentRuns >= task.maxRuns) {
|
|
124
|
+
return { queued: false, reason: 'Max runs reached' };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check for profile conflict
|
|
128
|
+
const runningTask = this.getRunningTaskForProfile(task.profile);
|
|
129
|
+
if (runningTask) {
|
|
130
|
+
// Already running - add to queue
|
|
131
|
+
task.status = 'queued';
|
|
132
|
+
this.queue.push(taskId);
|
|
133
|
+
this.queue.sort((a, b) => {
|
|
134
|
+
const taskA = this.tasks.get(a)!;
|
|
135
|
+
const taskB = this.tasks.get(b)!;
|
|
136
|
+
return taskB.priority - taskA.priority;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
queued: true,
|
|
141
|
+
reason: `Profile ${task.profile} busy with task ${runningTask.id}`,
|
|
142
|
+
conflictingTask: runningTask
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Profile available - add to queue
|
|
147
|
+
task.status = 'queued';
|
|
148
|
+
this.queue.push(taskId);
|
|
149
|
+
this.emit('task:queued', { task });
|
|
150
|
+
|
|
151
|
+
return { queued: true };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Start next task in queue
|
|
156
|
+
*/
|
|
157
|
+
async startNext(): Promise<ScheduledTask | null> {
|
|
158
|
+
// Find next task that can run
|
|
159
|
+
while (this.queue.length > 0) {
|
|
160
|
+
const taskId = this.queue.shift()!;
|
|
161
|
+
const task = this.tasks.get(taskId);
|
|
162
|
+
|
|
163
|
+
if (!task) continue;
|
|
164
|
+
if (task.status !== 'queued') continue;
|
|
165
|
+
|
|
166
|
+
// Check profile availability
|
|
167
|
+
if (!this.isProfileAvailable(task.profile)) {
|
|
168
|
+
// Put back at front of queue
|
|
169
|
+
this.queue.unshift(taskId);
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Start task
|
|
174
|
+
task.status = 'running';
|
|
175
|
+
task.lastRunAt = new Date().toISOString();
|
|
176
|
+
task.currentRuns++;
|
|
177
|
+
this.runningProfiles.set(task.profile, taskId);
|
|
178
|
+
|
|
179
|
+
this.emit('task:started', { task });
|
|
180
|
+
|
|
181
|
+
return task;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Mark task as completed
|
|
189
|
+
*/
|
|
190
|
+
completeTask(taskId: string, result: TaskRunResult): void {
|
|
191
|
+
const task = this.tasks.get(taskId);
|
|
192
|
+
if (!task) return;
|
|
193
|
+
|
|
194
|
+
task.status = result.success ? 'completed' : 'failed';
|
|
195
|
+
task.pid = null;
|
|
196
|
+
|
|
197
|
+
// Release profile
|
|
198
|
+
this.runningProfiles.delete(task.profile);
|
|
199
|
+
|
|
200
|
+
this.emit('task:completed', { task, result });
|
|
201
|
+
|
|
202
|
+
// Check if task should continue
|
|
203
|
+
if (task.maxRuns !== null && task.currentRuns >= task.maxRuns) {
|
|
204
|
+
this.emit('task:max-runs-reached', { task });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Cancel a task
|
|
210
|
+
*/
|
|
211
|
+
cancelTask(taskId: string): boolean {
|
|
212
|
+
const task = this.tasks.get(taskId);
|
|
213
|
+
if (!task) return false;
|
|
214
|
+
|
|
215
|
+
// Remove from queue if pending
|
|
216
|
+
const queueIndex = this.queue.indexOf(taskId);
|
|
217
|
+
if (queueIndex >= 0) {
|
|
218
|
+
this.queue.splice(queueIndex, 1);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// If running, mark for termination
|
|
222
|
+
if (task.status === 'running') {
|
|
223
|
+
task.status = 'cancelled';
|
|
224
|
+
if (task.pid) {
|
|
225
|
+
this.terminateProcess(task.pid);
|
|
226
|
+
}
|
|
227
|
+
this.runningProfiles.delete(task.profile);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
this.emit('task:cancelled', { task });
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get all tasks
|
|
236
|
+
*/
|
|
237
|
+
getTasks(): ScheduledTask[] {
|
|
238
|
+
return Array.from(this.tasks.values());
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Get tasks by status
|
|
243
|
+
*/
|
|
244
|
+
getTasksByStatus(status: TaskStatus): ScheduledTask[] {
|
|
245
|
+
return this.getTasks().filter(t => t.status === status);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get queue length
|
|
250
|
+
*/
|
|
251
|
+
getQueueLength(): number {
|
|
252
|
+
return this.queue.length;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Simulate task overlap scenario
|
|
257
|
+
*/
|
|
258
|
+
simulateOverlap(): {
|
|
259
|
+
scenario: string;
|
|
260
|
+
queue: string[];
|
|
261
|
+
running: Array<{ profile: string; taskId: string }>;
|
|
262
|
+
conflicts: string[];
|
|
263
|
+
} {
|
|
264
|
+
const running = Array.from(this.runningProfiles.entries()).map(([profile, taskId]) => ({
|
|
265
|
+
profile,
|
|
266
|
+
taskId
|
|
267
|
+
}));
|
|
268
|
+
|
|
269
|
+
const conflicts: string[] = [];
|
|
270
|
+
const profileMap = new Map<string, string[]>();
|
|
271
|
+
|
|
272
|
+
for (const task of this.tasks.values()) {
|
|
273
|
+
if (task.status === 'queued' || task.status === 'running') {
|
|
274
|
+
const existing = profileMap.get(task.profile) || [];
|
|
275
|
+
existing.push(task.id);
|
|
276
|
+
profileMap.set(task.profile, existing);
|
|
277
|
+
|
|
278
|
+
if (existing.length > 1) {
|
|
279
|
+
conflicts.push(...existing);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
scenario: 'Task overlap simulation',
|
|
286
|
+
queue: [...this.queue],
|
|
287
|
+
running,
|
|
288
|
+
conflicts
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export default TaskScheduler;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Block: ExecuteWeiboSearchBlock
|
|
3
|
+
*
|
|
4
|
+
* 微博搜索执行:直接导航到搜索结果页
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { promises as fs } from 'node:fs';
|
|
10
|
+
|
|
11
|
+
export interface ExecuteWeiboSearchInput {
|
|
12
|
+
sessionId: string;
|
|
13
|
+
keyword: string;
|
|
14
|
+
env?: string;
|
|
15
|
+
serviceUrl?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ExecuteWeiboSearchOutput {
|
|
19
|
+
success: boolean;
|
|
20
|
+
searchExecuted: boolean;
|
|
21
|
+
url: string;
|
|
22
|
+
steps?: Array<{
|
|
23
|
+
id: string;
|
|
24
|
+
status: 'pending' | 'running' | 'success' | 'failed' | 'skipped';
|
|
25
|
+
error?: string;
|
|
26
|
+
meta?: Record<string, any>;
|
|
27
|
+
}>;
|
|
28
|
+
error?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sanitizeFilenamePart(value: string): string {
|
|
32
|
+
return String(value || '')
|
|
33
|
+
.trim()
|
|
34
|
+
.replace(/[\\/:"*?<>|]+/g, '_')
|
|
35
|
+
.replace(/\s+/g, '_')
|
|
36
|
+
.slice(0, 80);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveDownloadRoot(): string {
|
|
40
|
+
const custom = process.env.WEBAUTO_DOWNLOAD_ROOT || process.env.WEBAUTO_DOWNLOAD_DIR;
|
|
41
|
+
if (custom && custom.trim()) return custom;
|
|
42
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
43
|
+
return path.join(home, '.webauto', 'download');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isDebugArtifactsEnabled(): boolean {
|
|
47
|
+
return (
|
|
48
|
+
process.env.WEBAUTO_DEBUG === '1' ||
|
|
49
|
+
process.env.WEBAUTO_DEBUG_ARTIFACTS === '1'
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function execute(input: ExecuteWeiboSearchInput): Promise<ExecuteWeiboSearchOutput> {
|
|
54
|
+
const {
|
|
55
|
+
sessionId,
|
|
56
|
+
keyword,
|
|
57
|
+
env = 'debug',
|
|
58
|
+
serviceUrl = 'http://127.0.0.1:7704',
|
|
59
|
+
} = input;
|
|
60
|
+
|
|
61
|
+
const profile = sessionId;
|
|
62
|
+
const controllerUrl = `${serviceUrl}/command`;
|
|
63
|
+
const steps: ExecuteWeiboSearchOutput['steps'] = [];
|
|
64
|
+
const debugEnabled = isDebugArtifactsEnabled();
|
|
65
|
+
|
|
66
|
+
async function controllerAction(action: string, args: any = {}): Promise<any> {
|
|
67
|
+
const res = await fetch(controllerUrl, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: { 'Content-Type': 'application/json' },
|
|
70
|
+
body: JSON.stringify({ action, args: { profileId: profile, ...args } }),
|
|
71
|
+
signal: (AbortSignal as any).timeout ? (AbortSignal as any).timeout(30000) : undefined,
|
|
72
|
+
});
|
|
73
|
+
const raw = await res.text();
|
|
74
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${raw}`);
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(raw);
|
|
77
|
+
} catch {
|
|
78
|
+
return { raw };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function getCurrentUrl(): Promise<string> {
|
|
83
|
+
const res = await controllerAction('evaluate', { script: 'window.location.href' });
|
|
84
|
+
return res?.body?.result ?? res?.result ?? '';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function saveDebugScreenshot(kind: string, meta: Record<string, any> = {}): Promise<void> {
|
|
88
|
+
if (!debugEnabled) return;
|
|
89
|
+
try {
|
|
90
|
+
const keywordDir = path.join(resolveDownloadRoot(), 'weibo', env, sanitizeFilenamePart(keyword));
|
|
91
|
+
const debugDir = path.join(keywordDir, '_debug', 'search');
|
|
92
|
+
await fs.mkdir(debugDir, { recursive: true });
|
|
93
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
94
|
+
const pngPath = path.join(debugDir, `${ts}-${sanitizeFilenamePart(kind)}.png`);
|
|
95
|
+
|
|
96
|
+
const shot = await controllerAction('screenshot');
|
|
97
|
+
const b64 = shot?.body?.data ?? shot?.data;
|
|
98
|
+
if (typeof b64 === 'string' && b64.length > 10) {
|
|
99
|
+
await fs.writeFile(pngPath, Buffer.from(b64, 'base64'));
|
|
100
|
+
}
|
|
101
|
+
console.log(`[ExecuteWeiboSearch][debug] saved ${kind}: ${pngPath}`);
|
|
102
|
+
} catch (e: any) {
|
|
103
|
+
console.warn(`[ExecuteWeiboSearch][debug] save failed (${kind}): ${e?.message || String(e)}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function pushStep(step: NonNullable<ExecuteWeiboSearchOutput['steps']>[number]) {
|
|
108
|
+
steps.push(step);
|
|
109
|
+
console.log('[ExecuteWeiboSearch][step]', step.id, step.status, step.error || '');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
// 直接导航到搜索结果页
|
|
114
|
+
const currentUrl = await getCurrentUrl();
|
|
115
|
+
|
|
116
|
+
if (!currentUrl.includes('s.weibo.com/weibo')) {
|
|
117
|
+
pushStep({ id: 'goto_search_page', status: 'running', meta: { from: currentUrl, keyword } });
|
|
118
|
+
|
|
119
|
+
const searchUrl = `https://s.weibo.com/weibo?q=${encodeURIComponent(keyword)}`;
|
|
120
|
+
console.log('[ExecuteWeiboSearch] navigating to:', searchUrl);
|
|
121
|
+
await controllerAction('goto', { url: searchUrl });
|
|
122
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
123
|
+
|
|
124
|
+
pushStep({ id: 'goto_search_page', status: 'success' });
|
|
125
|
+
} else {
|
|
126
|
+
pushStep({ id: 'goto_search_page', status: 'skipped', meta: { reason: 'already_on_search_page' } });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 检查是否成功加载搜索页
|
|
130
|
+
const finalUrl = await getCurrentUrl();
|
|
131
|
+
console.log('[ExecuteWeiboSearch] current URL:', finalUrl);
|
|
132
|
+
|
|
133
|
+
// 检查是否有搜索结果卡片
|
|
134
|
+
const cardsScript = `document.querySelectorAll('.card-wrap').length`;
|
|
135
|
+
const cardsRes = await controllerAction('evaluate', { script: cardsScript });
|
|
136
|
+
const cardsCount = cardsRes?.result ?? 0;
|
|
137
|
+
|
|
138
|
+
if (cardsCount > 0) {
|
|
139
|
+
console.log('[ExecuteWeiboSearch] search results loaded:', cardsCount, 'cards');
|
|
140
|
+
return {
|
|
141
|
+
success: true,
|
|
142
|
+
searchExecuted: true,
|
|
143
|
+
url: finalUrl,
|
|
144
|
+
steps,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 没有找到搜索结果卡片
|
|
149
|
+
console.log('[ExecuteWeiboSearch] no search results found');
|
|
150
|
+
await saveDebugScreenshot('no_search_results', { keyword, url: finalUrl });
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
success: true, // 仍然返回成功,因为已经导航到搜索页
|
|
154
|
+
searchExecuted: true,
|
|
155
|
+
url: finalUrl,
|
|
156
|
+
steps,
|
|
157
|
+
};
|
|
158
|
+
} catch (error: any) {
|
|
159
|
+
return {
|
|
160
|
+
success: false,
|
|
161
|
+
searchExecuted: false,
|
|
162
|
+
url: '',
|
|
163
|
+
steps,
|
|
164
|
+
error: `ExecuteWeiboSearch failed: ${error.message}`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import os from 'node:os';
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import { promises as fs } from 'node:fs';
|
|
13
|
+
import { getCurrentTimestamp } from '../../collection-manager/date-utils.js';
|
|
13
14
|
|
|
14
15
|
export interface PersistXhsNoteInput {
|
|
15
16
|
sessionId: string;
|
|
@@ -178,6 +179,7 @@ export async function execute(input: PersistXhsNoteInput): Promise<PersistXhsNot
|
|
|
178
179
|
const detailPath = path.join(postDir, 'content.md');
|
|
179
180
|
const commentsPath = path.join(postDir, 'comments.md');
|
|
180
181
|
const commentsDonePath = path.join(postDir, 'comments.done.json');
|
|
182
|
+
const persistTs = getCurrentTimestamp();
|
|
181
183
|
|
|
182
184
|
// 1) 详情(正文/图片)
|
|
183
185
|
if (wantDetail) {
|
|
@@ -223,7 +225,8 @@ export async function execute(input: PersistXhsNoteInput): Promise<PersistXhsNot
|
|
|
223
225
|
if (searchUrl) lines.push(`- Search URL: ${searchUrl}`);
|
|
224
226
|
if (detailUrl) lines.push(`- 链接: ${detailUrl}`);
|
|
225
227
|
if (author) lines.push(`- 作者: ${author}`);
|
|
226
|
-
lines.push(`- 采集时间: ${
|
|
228
|
+
lines.push(`- 采集时间: ${persistTs.collectedAt}`);
|
|
229
|
+
lines.push(`- 采集时间(本地): ${persistTs.collectedAtLocal}`);
|
|
227
230
|
lines.push('');
|
|
228
231
|
lines.push('## 正文');
|
|
229
232
|
lines.push('');
|
|
@@ -276,7 +279,8 @@ export async function execute(input: PersistXhsNoteInput): Promise<PersistXhsNot
|
|
|
276
279
|
lines.push(`- 关键词: ${keyword || '未知'}`);
|
|
277
280
|
if (searchUrl) lines.push(`- Search URL: ${searchUrl}`);
|
|
278
281
|
if (detailUrl) lines.push(`- 链接: ${detailUrl}`);
|
|
279
|
-
lines.push(`- 采集时间: ${
|
|
282
|
+
lines.push(`- 采集时间: ${persistTs.collectedAt}`);
|
|
283
|
+
lines.push(`- 采集时间(本地): ${persistTs.collectedAtLocal}`);
|
|
280
284
|
lines.push(
|
|
281
285
|
`- 评论统计: 抓取=${comments.length}, header=${
|
|
282
286
|
headerTotal !== null ? headerTotal : '未知'
|
|
@@ -329,7 +333,7 @@ export async function execute(input: PersistXhsNoteInput): Promise<PersistXhsNot
|
|
|
329
333
|
stoppedByMaxComments: Boolean(commentsResult?.stoppedByMaxComments),
|
|
330
334
|
totalComments: comments.length,
|
|
331
335
|
headerTotal,
|
|
332
|
-
ts:
|
|
336
|
+
ts: persistTs.collectedAt,
|
|
333
337
|
},
|
|
334
338
|
null,
|
|
335
339
|
2,
|
|
@@ -39,7 +39,9 @@ export async function execute(input: RenderMarkdownInput): Promise<RenderMarkdow
|
|
|
39
39
|
|
|
40
40
|
const lines: string[] = [];
|
|
41
41
|
lines.push(`# 微博采集结果`);
|
|
42
|
-
|
|
42
|
+
const ts = getCurrentTimestamp();
|
|
43
|
+
lines.push(`采集时间: ${ts.collectedAt}`);
|
|
44
|
+
lines.push(`采集时间(本地): ${ts.collectedAtLocal}`);
|
|
43
45
|
lines.push(`总计: ${posts.length} 条`);
|
|
44
46
|
lines.push('');
|
|
45
47
|
|
|
@@ -70,3 +72,4 @@ export async function execute(input: RenderMarkdownInput): Promise<RenderMarkdow
|
|
|
70
72
|
count: posts.length
|
|
71
73
|
};
|
|
72
74
|
}
|
|
75
|
+
import { getCurrentTimestamp } from '../../collection-manager/date-utils.js';
|