@web-auto/webauto 0.1.4 → 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.
Files changed (174) hide show
  1. package/apps/desktop-console/default-settings.json +2 -2
  2. package/apps/desktop-console/dist/main/index.mjs +915 -85
  3. package/apps/desktop-console/dist/main/preload.mjs +7 -0
  4. package/apps/desktop-console/dist/renderer/index.html +622 -50
  5. package/apps/desktop-console/dist/renderer/index.js +2415 -470
  6. package/apps/desktop-console/dist/renderer/run.mts +6 -5
  7. package/apps/desktop-console/entry/ui-cli.mjs +672 -0
  8. package/apps/desktop-console/entry/ui-console.mjs +416 -29
  9. package/apps/webauto/entry/account.mjs +89 -53
  10. package/apps/webauto/entry/browser-status.mjs +7 -10
  11. package/apps/webauto/entry/lib/account-detect.mjs +254 -28
  12. package/apps/webauto/entry/lib/account-store.mjs +219 -30
  13. package/apps/webauto/entry/lib/bus-publish.mjs +63 -0
  14. package/apps/webauto/entry/lib/camo-cli.mjs +93 -0
  15. package/apps/webauto/entry/lib/profilepool.mjs +14 -5
  16. package/apps/webauto/entry/lib/quota-status.mjs +23 -0
  17. package/apps/webauto/entry/lib/schedule-store.mjs +1068 -0
  18. package/apps/webauto/entry/profilepool.mjs +106 -17
  19. package/apps/webauto/entry/schedule.mjs +612 -0
  20. package/apps/webauto/entry/weibo-unified.mjs +134 -0
  21. package/apps/webauto/entry/xhs-install.mjs +236 -29
  22. package/apps/webauto/entry/xhs-status.mjs +5 -2
  23. package/apps/webauto/entry/xhs-unified.mjs +631 -98
  24. package/apps/webauto/resources/container-library/weibo/weibo_detail_page/comment_item/container.json +40 -0
  25. package/apps/webauto/resources/container-library/weibo/weibo_detail_page/reply_expand_button/container.json +38 -0
  26. package/apps/webauto/resources/container-library/weibo/weibo_detail_page/reply_list/container.json +37 -0
  27. package/apps/webauto/resources/container-library/weibo/weibo_search_page/container.json +8 -3
  28. package/apps/webauto/resources/container-library/weibo/weibo_search_page/login_anchor/container.json +30 -0
  29. package/apps/webauto/resources/container-library/weibo/weibo_search_page/search_bar/container.json +47 -0
  30. package/apps/webauto/resources/container-library/weibo/weibo_search_page/search_button/container.json +39 -0
  31. package/bin/camoufox-cli.mjs +61 -0
  32. package/bin/webauto.mjs +301 -54
  33. package/dist/modules/camo-backend/src/index.js +49 -1
  34. package/dist/modules/camo-backend/src/internal/BrowserSession.js +572 -3
  35. package/dist/modules/camo-backend/src/internal/SessionManager.js +13 -1
  36. package/dist/modules/camo-backend/src/internal/storage-paths.js +6 -0
  37. package/dist/modules/collection-manager/bloom-filter.js +91 -0
  38. package/dist/modules/collection-manager/date-utils.js +275 -0
  39. package/dist/modules/collection-manager/index.js +258 -0
  40. package/dist/modules/collection-manager/storage.js +195 -0
  41. package/dist/modules/collection-manager/types.js +47 -0
  42. package/dist/modules/logging/src/index.js +1 -1
  43. package/dist/modules/process-registry/index.js +230 -0
  44. package/dist/modules/rate-limiter/index.js +242 -0
  45. package/dist/modules/workflow/blocks/ExecuteWeiboSearchBlock.js +128 -0
  46. package/dist/modules/workflow/blocks/PersistXhsNoteBlock.js +7 -3
  47. package/dist/modules/workflow/blocks/RenderMarkdown.js +4 -1
  48. package/dist/modules/workflow/blocks/WeiboCollectCommentsBlock.js +282 -0
  49. package/dist/modules/workflow/blocks/WeiboCollectFromLinksBlock.js +283 -0
  50. package/dist/modules/workflow/blocks/WeiboCollectSearchLinksBlock.js +208 -0
  51. package/dist/modules/workflow/blocks/WeiboCollectTimelineListBlock.js +128 -0
  52. package/dist/modules/workflow/blocks/WeiboCollectUserPostsListBlock.js +127 -0
  53. package/dist/modules/workflow/blocks/helpers/downloadPaths.js +21 -0
  54. package/dist/modules/workflow/config/workflowRegistry.js +2 -0
  55. package/dist/modules/workflow/definitions/weibo-search-workflow-v1.js +47 -0
  56. package/dist/modules/workflow/src/runner.js +6 -0
  57. package/dist/modules/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.js +4 -0
  58. package/dist/modules/xiaohongshu/app/src/blocks/Phase3InteractBlock.js +2 -2
  59. package/dist/modules/xiaohongshu/app/src/blocks/helpers/sharding.js +123 -0
  60. package/dist/modules/xiaohongshu/app/src/container-registry/src/index.d.ts +37 -0
  61. package/dist/modules/xiaohongshu/app/src/container-registry/src/index.js +184 -0
  62. package/dist/modules/xiaohongshu/app/src/workflow/blocks/AnchorVerificationBlock.d.ts +31 -0
  63. package/dist/modules/xiaohongshu/app/src/workflow/blocks/AnchorVerificationBlock.js +71 -0
  64. package/dist/modules/xiaohongshu/app/src/workflow/blocks/DetectPageStateBlock.d.ts +48 -0
  65. package/dist/modules/xiaohongshu/app/src/workflow/blocks/DetectPageStateBlock.js +259 -0
  66. package/dist/modules/xiaohongshu/app/src/workflow/blocks/ErrorRecoveryBlock.d.ts +28 -0
  67. package/dist/modules/xiaohongshu/app/src/workflow/blocks/ErrorRecoveryBlock.js +319 -0
  68. package/dist/modules/xiaohongshu/app/src/workflow/blocks/WaitSearchPermitBlock.d.ts +36 -0
  69. package/dist/modules/xiaohongshu/app/src/workflow/blocks/WaitSearchPermitBlock.js +162 -0
  70. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/containerAnchors.d.ts +36 -0
  71. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/containerAnchors.js +301 -0
  72. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/operationLogger.d.ts +29 -0
  73. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/operationLogger.js +195 -0
  74. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/searchPageState.d.ts +25 -0
  75. package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/searchPageState.js +164 -0
  76. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/MatchCommentsBlock.d.ts +66 -0
  77. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/MatchCommentsBlock.js +139 -0
  78. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1EnsureServicesBlock.d.ts +16 -0
  79. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1EnsureServicesBlock.js +36 -0
  80. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1MonitorCookieBlock.d.ts +27 -0
  81. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1MonitorCookieBlock.js +213 -0
  82. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.d.ts +18 -0
  83. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.js +121 -0
  84. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2CollectLinksBlock.d.ts +34 -0
  85. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2CollectLinksBlock.js +1249 -0
  86. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2SearchBlock.d.ts +17 -0
  87. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2SearchBlock.js +703 -0
  88. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseDetailBlock.d.ts +15 -0
  89. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseDetailBlock.js +41 -0
  90. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseTabsBlock.d.ts +26 -0
  91. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseTabsBlock.js +44 -0
  92. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CollectCommentsBlock.d.ts +29 -0
  93. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CollectCommentsBlock.js +150 -0
  94. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ExtractDetailBlock.d.ts +38 -0
  95. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ExtractDetailBlock.js +117 -0
  96. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenDetailBlock.d.ts +30 -0
  97. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenDetailBlock.js +102 -0
  98. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenTabsBlock.d.ts +23 -0
  99. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenTabsBlock.js +109 -0
  100. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.d.ts +32 -0
  101. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.js +117 -0
  102. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ProcessSingleNoteBlock.d.ts +35 -0
  103. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ProcessSingleNoteBlock.js +114 -0
  104. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ValidateLinksBlock.d.ts +34 -0
  105. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ValidateLinksBlock.js +90 -0
  106. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase3InteractBlock.d.ts +111 -0
  107. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase3InteractBlock.js +1009 -0
  108. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase4MultiTabHarvestBlock.d.ts +20 -0
  109. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase4MultiTabHarvestBlock.js +233 -0
  110. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/ReplyInteractBlock.d.ts +48 -0
  111. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/ReplyInteractBlock.js +291 -0
  112. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/XhsDiscoverFallbackBlock.d.ts +23 -0
  113. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/XhsDiscoverFallbackBlock.js +240 -0
  114. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatchDsl.d.ts +55 -0
  115. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatchDsl.js +126 -0
  116. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatcher.d.ts +21 -0
  117. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatcher.js +99 -0
  118. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/evidence.d.ts +5 -0
  119. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/evidence.js +27 -0
  120. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/sharding.d.ts +37 -0
  121. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/sharding.js +165 -0
  122. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/xhsComments.d.ts +33 -0
  123. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/xhsComments.js +270 -0
  124. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/index.d.ts +9 -0
  125. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/index.js +9 -0
  126. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/checkpoints.d.ts +50 -0
  127. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/checkpoints.js +222 -0
  128. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/controllerAction.d.ts +10 -0
  129. package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/controllerAction.js +43 -0
  130. package/dist/services/shared/serviceProcessLogger.js +1 -1
  131. package/dist/services/unified-api/server.js +105 -11
  132. package/modules/camo-backend/src/index.ts +46 -1
  133. package/modules/camo-backend/src/internal/BrowserSession.ts +619 -3
  134. package/modules/camo-backend/src/internal/SessionManager.ts +12 -1
  135. package/modules/camo-backend/src/internal/storage-paths.ts +5 -0
  136. package/modules/camo-runtime/src/autoscript/action-providers/xhs/comments.mjs +38 -2
  137. package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +47 -2
  138. package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +94 -11
  139. package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +208 -2
  140. package/modules/camo-runtime/src/autoscript/runtime.mjs +7 -1
  141. package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +76 -43
  142. package/modules/camo-runtime/src/container/runtime-core/operations/index.mjs +75 -1
  143. package/modules/camo-runtime/src/container/runtime-core/operations/selector-scripts.mjs +71 -4
  144. package/modules/camo-runtime/src/container/runtime-core/operations/tab-pool.mjs +183 -27
  145. package/modules/collection-manager/bloom-filter.ts +112 -0
  146. package/modules/collection-manager/date-utils.ts +316 -0
  147. package/modules/collection-manager/index.ts +309 -0
  148. package/modules/collection-manager/package.json +10 -0
  149. package/modules/collection-manager/storage.ts +174 -0
  150. package/modules/collection-manager/types.ts +156 -0
  151. package/modules/logging/src/index.ts +1 -1
  152. package/modules/process-registry/index.ts +284 -0
  153. package/modules/rate-limiter/index.ts +322 -0
  154. package/modules/state/src/paths.ts +9 -1
  155. package/modules/task-scheduler/index.ts +293 -0
  156. package/modules/workflow/blocks/ExecuteWeiboSearchBlock.ts +167 -0
  157. package/modules/workflow/blocks/PersistXhsNoteBlock.ts +7 -3
  158. package/modules/workflow/blocks/RenderMarkdown.ts +4 -1
  159. package/modules/workflow/blocks/WeiboCollectCommentsBlock.ts +339 -0
  160. package/modules/workflow/blocks/WeiboCollectFromLinksBlock.ts +338 -0
  161. package/modules/workflow/blocks/helpers/downloadPaths.ts +16 -0
  162. package/modules/workflow/config/workflowRegistry.ts +2 -0
  163. package/modules/workflow/definitions/weibo-search-workflow-v1.ts +47 -0
  164. package/modules/workflow/src/runner.ts +6 -0
  165. package/modules/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.ts +1 -1
  166. package/modules/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.ts +4 -0
  167. package/modules/xiaohongshu/app/src/blocks/Phase3InteractBlock.ts +2 -3
  168. package/modules/xiaohongshu/app/src/blocks/helpers/sharding.ts +152 -0
  169. package/package.json +13 -4
  170. package/scripts/postinstall-resources.mjs +62 -0
  171. package/scripts/test/run-coverage.mjs +76 -0
  172. package/scripts/weibo/search.ts +49 -0
  173. package/services/shared/serviceProcessLogger.ts +1 -1
  174. 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(`- 采集时间: ${new Date().toISOString()}`);
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(`- 采集时间: ${new Date().toISOString()}`);
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: new Date().toISOString(),
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
- lines.push(`采集时间: ${new Date().toISOString()}`);
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';