@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.
- 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 +13 -4
- 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,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProcessRegistry - 进程生命周期管理
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { promises as fs } from 'node:fs';
|
|
8
|
+
import { execSync } from 'node:child_process';
|
|
9
|
+
|
|
10
|
+
export type ProcessType = 'weibo-search' | 'weibo-detail' | 'xhs-search' | 'xhs-detail' | 'xhs-interact';
|
|
11
|
+
|
|
12
|
+
export interface ProcessEntry {
|
|
13
|
+
pid: number;
|
|
14
|
+
type: ProcessType;
|
|
15
|
+
profileId: string;
|
|
16
|
+
keyword?: string;
|
|
17
|
+
startedAt: number;
|
|
18
|
+
lastHeartbeat: number;
|
|
19
|
+
status: 'running' | 'stale' | 'dead';
|
|
20
|
+
metadata?: Record<string, any>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ProcessRegistryConfig {
|
|
24
|
+
heartbeatIntervalMs: number;
|
|
25
|
+
staleTimeoutMs: number;
|
|
26
|
+
cleanupIntervalMs: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const DEFAULT_CONFIG: ProcessRegistryConfig = {
|
|
30
|
+
heartbeatIntervalMs: 30000,
|
|
31
|
+
staleTimeoutMs: 120000,
|
|
32
|
+
cleanupIntervalMs: 60000,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function resolveRegistryPath(): string {
|
|
36
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
37
|
+
return path.join(home, '.webauto', 'process-registry.json');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class ProcessRegistry {
|
|
41
|
+
private static instance: ProcessRegistry | null = null;
|
|
42
|
+
|
|
43
|
+
private entries: Map<number, ProcessEntry> = new Map();
|
|
44
|
+
private registryPath: string;
|
|
45
|
+
private config: ProcessRegistryConfig;
|
|
46
|
+
private ownPid: number;
|
|
47
|
+
private heartbeatTimer: NodeJS.Timeout | null = null;
|
|
48
|
+
private cleanupTimer: NodeJS.Timeout | null = null;
|
|
49
|
+
private initialized = false;
|
|
50
|
+
private shutdownHandlersRegistered = false;
|
|
51
|
+
|
|
52
|
+
private constructor(config: Partial<ProcessRegistryConfig> = {}) {
|
|
53
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
54
|
+
this.registryPath = resolveRegistryPath();
|
|
55
|
+
this.ownPid = process.pid;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static getInstance(config?: Partial<ProcessRegistryConfig>): ProcessRegistry {
|
|
59
|
+
if (!ProcessRegistry.instance) {
|
|
60
|
+
ProcessRegistry.instance = new ProcessRegistry(config);
|
|
61
|
+
}
|
|
62
|
+
return ProcessRegistry.instance;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async init(): Promise<void> {
|
|
66
|
+
if (this.initialized) return;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const content = await fs.readFile(this.registryPath, 'utf-8');
|
|
70
|
+
const data = JSON.parse(content);
|
|
71
|
+
for (const entry of Object.values(data) as ProcessEntry[]) {
|
|
72
|
+
this.entries.set(entry.pid, entry);
|
|
73
|
+
}
|
|
74
|
+
console.log('[ProcessRegistry] Loaded', this.entries.size, 'entries');
|
|
75
|
+
} catch {
|
|
76
|
+
// 无记录
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.startCleanupTimer();
|
|
80
|
+
this.initialized = true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async register(
|
|
84
|
+
type: ProcessType,
|
|
85
|
+
profileId: string,
|
|
86
|
+
metadata?: { keyword?: string; [key: string]: any }
|
|
87
|
+
): Promise<number> {
|
|
88
|
+
await this.init();
|
|
89
|
+
|
|
90
|
+
const entry: ProcessEntry = {
|
|
91
|
+
pid: this.ownPid,
|
|
92
|
+
type,
|
|
93
|
+
profileId,
|
|
94
|
+
keyword: metadata?.keyword,
|
|
95
|
+
startedAt: Date.now(),
|
|
96
|
+
lastHeartbeat: Date.now(),
|
|
97
|
+
status: 'running',
|
|
98
|
+
metadata,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
this.entries.set(this.ownPid, entry);
|
|
102
|
+
await this.persist();
|
|
103
|
+
|
|
104
|
+
console.log(`[ProcessRegistry] Registered: pid=${this.ownPid} type=${type} profile=${profileId}`);
|
|
105
|
+
|
|
106
|
+
this.startHeartbeat();
|
|
107
|
+
|
|
108
|
+
// 注册一次 exit 处理器
|
|
109
|
+
if (!this.shutdownHandlersRegistered) {
|
|
110
|
+
this.registerShutdownHandlers();
|
|
111
|
+
this.shutdownHandlersRegistered = true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return this.ownPid;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private registerShutdownHandlers(): void {
|
|
118
|
+
const handler = async () => {
|
|
119
|
+
await this.unregister();
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
process.once('SIGINT', handler);
|
|
123
|
+
process.once('SIGTERM', handler);
|
|
124
|
+
process.once('beforeExit', handler);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
heartbeat(): void {
|
|
128
|
+
const entry = this.entries.get(this.ownPid);
|
|
129
|
+
if (entry) {
|
|
130
|
+
entry.lastHeartbeat = Date.now();
|
|
131
|
+
entry.status = 'running';
|
|
132
|
+
this.persist().catch(() => {});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async unregister(): Promise<void> {
|
|
137
|
+
if (!this.entries.has(this.ownPid)) return;
|
|
138
|
+
|
|
139
|
+
this.stopHeartbeat();
|
|
140
|
+
this.entries.delete(this.ownPid);
|
|
141
|
+
await this.persist();
|
|
142
|
+
console.log(`[ProcessRegistry] Unregistered: pid=${this.ownPid}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
getAll(): ProcessEntry[] {
|
|
146
|
+
return Array.from(this.entries.values());
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
getRunning(): ProcessEntry[] {
|
|
150
|
+
return this.getAll().filter(e => e.status === 'running');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async cleanStale(): Promise<{ cleaned: number; killed: string[] }> {
|
|
154
|
+
await this.init();
|
|
155
|
+
|
|
156
|
+
const now = Date.now();
|
|
157
|
+
const cleaned: string[] = [];
|
|
158
|
+
|
|
159
|
+
for (const [pid, entry] of this.entries) {
|
|
160
|
+
if (pid === this.ownPid) continue;
|
|
161
|
+
|
|
162
|
+
if (now - entry.lastHeartbeat > this.config.staleTimeoutMs) {
|
|
163
|
+
entry.status = 'stale';
|
|
164
|
+
const isAlive = this.isProcessAlive(pid);
|
|
165
|
+
|
|
166
|
+
if (!isAlive) {
|
|
167
|
+
this.entries.delete(pid);
|
|
168
|
+
cleaned.push(`pid=${pid} (dead)`);
|
|
169
|
+
await this.cleanupBrowserSession(entry.profileId);
|
|
170
|
+
} else {
|
|
171
|
+
console.log(`[ProcessRegistry] Killing stale process: pid=${pid} type=${entry.type}`);
|
|
172
|
+
this.killProcess(pid);
|
|
173
|
+
this.entries.delete(pid);
|
|
174
|
+
cleaned.push(`pid=${pid} (killed)`);
|
|
175
|
+
await this.cleanupBrowserSession(entry.profileId);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (cleaned.length > 0) {
|
|
181
|
+
await this.persist();
|
|
182
|
+
console.log(`[ProcessRegistry] Cleaned ${cleaned.length} stale entries`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { cleaned: cleaned.length, killed: cleaned };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
getStatus(): { pid: number; type: ProcessType; profileId: string; uptime: number } | null {
|
|
189
|
+
const entry = this.entries.get(this.ownPid);
|
|
190
|
+
if (!entry) return null;
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
pid: entry.pid,
|
|
194
|
+
type: entry.type,
|
|
195
|
+
profileId: entry.profileId,
|
|
196
|
+
uptime: Date.now() - entry.startedAt,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private startHeartbeat(): void {
|
|
201
|
+
if (this.heartbeatTimer) return;
|
|
202
|
+
|
|
203
|
+
this.heartbeatTimer = setInterval(() => {
|
|
204
|
+
this.heartbeat();
|
|
205
|
+
}, this.config.heartbeatIntervalMs);
|
|
206
|
+
|
|
207
|
+
this.heartbeatTimer.unref();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private stopHeartbeat(): void {
|
|
211
|
+
if (this.heartbeatTimer) {
|
|
212
|
+
clearInterval(this.heartbeatTimer);
|
|
213
|
+
this.heartbeatTimer = null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private startCleanupTimer(): void {
|
|
218
|
+
if (this.cleanupTimer) return;
|
|
219
|
+
|
|
220
|
+
this.cleanupTimer = setInterval(() => {
|
|
221
|
+
this.cleanStale().catch(() => {});
|
|
222
|
+
}, this.config.cleanupIntervalMs);
|
|
223
|
+
|
|
224
|
+
this.cleanupTimer.unref();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private isProcessAlive(pid: number): boolean {
|
|
228
|
+
try {
|
|
229
|
+
process.kill(pid, 0);
|
|
230
|
+
return true;
|
|
231
|
+
} catch {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private killProcess(pid: number): void {
|
|
237
|
+
try {
|
|
238
|
+
if (process.platform === 'win32') {
|
|
239
|
+
execSync(`taskkill /PID ${pid} /T /F`, { stdio: 'ignore' });
|
|
240
|
+
} else {
|
|
241
|
+
process.kill(pid, 'SIGTERM');
|
|
242
|
+
setTimeout(() => {
|
|
243
|
+
if (this.isProcessAlive(pid)) {
|
|
244
|
+
try { process.kill(pid, 'SIGKILL'); } catch {}
|
|
245
|
+
}
|
|
246
|
+
}, 5000);
|
|
247
|
+
}
|
|
248
|
+
} catch {}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private async cleanupBrowserSession(profileId: string): Promise<void> {
|
|
252
|
+
try {
|
|
253
|
+
execSync(`camo stop ${profileId}`, { stdio: 'ignore', timeout: 10000 });
|
|
254
|
+
console.log(`[ProcessRegistry] Cleaned browser: ${profileId}`);
|
|
255
|
+
} catch {}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private async persist(): Promise<void> {
|
|
259
|
+
try {
|
|
260
|
+
const dir = path.dirname(this.registryPath);
|
|
261
|
+
await fs.mkdir(dir, { recursive: true });
|
|
262
|
+
|
|
263
|
+
const data: Record<number, ProcessEntry> = {};
|
|
264
|
+
for (const [pid, entry] of this.entries) {
|
|
265
|
+
data[pid] = entry;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
await fs.writeFile(this.registryPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
269
|
+
} catch (err) {
|
|
270
|
+
console.error('[ProcessRegistry] Failed to persist:', err);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async shutdown(): Promise<void> {
|
|
275
|
+
this.stopHeartbeat();
|
|
276
|
+
if (this.cleanupTimer) {
|
|
277
|
+
clearInterval(this.cleanupTimer);
|
|
278
|
+
this.cleanupTimer = null;
|
|
279
|
+
}
|
|
280
|
+
await this.unregister();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export const processRegistry = ProcessRegistry.getInstance();
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RateLimiter - 统一流控配额系统
|
|
3
|
+
*
|
|
4
|
+
* 功能:
|
|
5
|
+
* - 管理 search/like/comment 等操作的配额
|
|
6
|
+
* - 支持多维度限制(关键字、profile、全局)
|
|
7
|
+
* - 可配置规则,支持持久化
|
|
8
|
+
*
|
|
9
|
+
* 使用方式:
|
|
10
|
+
* const limiter = RateLimiter.getInstance();
|
|
11
|
+
* const result = await limiter.acquire('search', { keyword: '春晚' });
|
|
12
|
+
* if (!result.granted) {
|
|
13
|
+
* console.log(`被拒绝: ${result.reason}, 需等待 ${result.waitMs}ms`);
|
|
14
|
+
* return;
|
|
15
|
+
* }
|
|
16
|
+
* // 执行搜索...
|
|
17
|
+
* await limiter.record('search', { keyword: '春晚' });
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import os from 'node:os';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { promises as fs } from 'node:fs';
|
|
23
|
+
|
|
24
|
+
// 配额类型
|
|
25
|
+
export type QuotaType = 'search' | 'like' | 'comment' | 'follow' | 'repost';
|
|
26
|
+
|
|
27
|
+
// 配额范围
|
|
28
|
+
export type QuotaScope = 'keyword' | 'profile' | 'global';
|
|
29
|
+
|
|
30
|
+
// 规则定义
|
|
31
|
+
export interface QuotaRule {
|
|
32
|
+
scope: QuotaScope;
|
|
33
|
+
windowMs: number; // 时间窗口(毫秒)
|
|
34
|
+
max: number; // 窗口内最大次数
|
|
35
|
+
key?: string; // 可读描述
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 配额类型配置
|
|
39
|
+
export interface QuotaConfig {
|
|
40
|
+
rules: QuotaRule[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 完整配置
|
|
44
|
+
export interface RateLimiterConfig {
|
|
45
|
+
[type: string]: QuotaConfig;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 历史记录条目
|
|
49
|
+
interface HistoryEntry {
|
|
50
|
+
ts: number; // 时间戳
|
|
51
|
+
key: string; // 维度键(如 keyword:春晚, profile:xxx)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 申请结果
|
|
55
|
+
export interface AcquireResult {
|
|
56
|
+
granted: boolean;
|
|
57
|
+
reason?: string;
|
|
58
|
+
waitMs?: number; // 建议等待时间
|
|
59
|
+
ruleKey?: string; // 触发的规则
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 默认配置
|
|
63
|
+
const DEFAULT_CONFIG: RateLimiterConfig = {
|
|
64
|
+
search: {
|
|
65
|
+
rules: [
|
|
66
|
+
{ scope: 'keyword', windowMs: 600000, max: 3, key: '同一关键字10分钟内最多3次' },
|
|
67
|
+
{ scope: 'global', windowMs: 60000, max: 10, key: '全局搜索1分钟内最多10次' },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
like: {
|
|
71
|
+
rules: [
|
|
72
|
+
{ scope: 'profile', windowMs: 60000, max: 6, key: '同一账号1分钟内最多点赞6次' },
|
|
73
|
+
{ scope: 'profile', windowMs: 3600000, max: 100, key: '同一账号1小时内最多点赞100次' },
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
comment: {
|
|
77
|
+
rules: [
|
|
78
|
+
{ scope: 'profile', windowMs: 60000, max: 1, key: '同一账号1分钟内最多评论1次' },
|
|
79
|
+
{ scope: 'profile', windowMs: 3600000, max: 30, key: '同一账号1小时内最多评论30次' },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
follow: {
|
|
83
|
+
rules: [
|
|
84
|
+
{ scope: 'profile', windowMs: 60000, max: 2, key: '同一账号1分钟内最多关注2次' },
|
|
85
|
+
{ scope: 'profile', windowMs: 3600000, max: 20, key: '同一账号1小时内最多关注20次' },
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
repost: {
|
|
89
|
+
rules: [
|
|
90
|
+
{ scope: 'profile', windowMs: 60000, max: 1, key: '同一账号1分钟内最多转发1次' },
|
|
91
|
+
{ scope: 'profile', windowMs: 3600000, max: 10, key: '同一账号1小时内最多转发10次' },
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
function resolveConfigPath(): string {
|
|
97
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
98
|
+
return path.join(home, '.webauto', 'config', 'rate-limits.json');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function resolveHistoryPath(): string {
|
|
102
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
103
|
+
return path.join(home, '.webauto', 'rate-limiter-history.json');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export class RateLimiter {
|
|
107
|
+
private static instance: RateLimiter | null = null;
|
|
108
|
+
|
|
109
|
+
private config: RateLimiterConfig;
|
|
110
|
+
private history: Map<string, HistoryEntry[]> = new Map();
|
|
111
|
+
private configPath: string;
|
|
112
|
+
private historyPath: string;
|
|
113
|
+
private initialized = false;
|
|
114
|
+
|
|
115
|
+
private constructor() {
|
|
116
|
+
this.config = { ...DEFAULT_CONFIG };
|
|
117
|
+
this.configPath = resolveConfigPath();
|
|
118
|
+
this.historyPath = resolveHistoryPath();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
static getInstance(): RateLimiter {
|
|
122
|
+
if (!RateLimiter.instance) {
|
|
123
|
+
RateLimiter.instance = new RateLimiter();
|
|
124
|
+
}
|
|
125
|
+
return RateLimiter.instance;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 初始化:加载配置和历史
|
|
129
|
+
async init(): Promise<void> {
|
|
130
|
+
if (this.initialized) return;
|
|
131
|
+
|
|
132
|
+
// 加载用户配置(覆盖默认值)
|
|
133
|
+
try {
|
|
134
|
+
const content = await fs.readFile(this.configPath, 'utf-8');
|
|
135
|
+
const userConfig = JSON.parse(content);
|
|
136
|
+
this.config = { ...DEFAULT_CONFIG, ...userConfig };
|
|
137
|
+
console.log('[RateLimiter] Loaded user config from', this.configPath);
|
|
138
|
+
} catch {
|
|
139
|
+
// 使用默认配置
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 加载历史记录
|
|
143
|
+
try {
|
|
144
|
+
const content = await fs.readFile(this.historyPath, 'utf-8');
|
|
145
|
+
const data = JSON.parse(content);
|
|
146
|
+
for (const [key, entries] of Object.entries(data)) {
|
|
147
|
+
this.history.set(key, entries as HistoryEntry[]);
|
|
148
|
+
}
|
|
149
|
+
// 清理过期历史
|
|
150
|
+
this.cleanExpiredHistory();
|
|
151
|
+
} catch {
|
|
152
|
+
// 无历史记录
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
this.initialized = true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 申请配额
|
|
159
|
+
async acquire(
|
|
160
|
+
type: QuotaType,
|
|
161
|
+
params: { keyword?: string; profileId?: string; platform?: string }
|
|
162
|
+
): Promise<AcquireResult> {
|
|
163
|
+
await this.init();
|
|
164
|
+
|
|
165
|
+
const typeConfig = this.config[type];
|
|
166
|
+
if (!typeConfig) {
|
|
167
|
+
return { granted: true }; // 无配置的类型默认允许
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const now = Date.now();
|
|
171
|
+
|
|
172
|
+
for (const rule of typeConfig.rules) {
|
|
173
|
+
const key = this.buildKey(type, rule.scope, params);
|
|
174
|
+
const entries = this.history.get(key) || [];
|
|
175
|
+
|
|
176
|
+
// 统计窗口内的次数
|
|
177
|
+
const windowStart = now - rule.windowMs;
|
|
178
|
+
const count = entries.filter(e => e.ts > windowStart).length;
|
|
179
|
+
|
|
180
|
+
if (count >= rule.max) {
|
|
181
|
+
// 找到最早的过期时间,计算等待时间
|
|
182
|
+
const oldestInWindow = entries.find(e => e.ts > windowStart);
|
|
183
|
+
const waitMs = oldestInWindow ? (oldestInWindow.ts + rule.windowMs - now) : rule.windowMs;
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
granted: false,
|
|
187
|
+
reason: rule.key || `配额已用尽: ${rule.scope} ${rule.windowMs}ms 内最多 ${rule.max} 次`,
|
|
188
|
+
waitMs: Math.max(0, waitMs),
|
|
189
|
+
ruleKey: `${type}:${rule.scope}:${rule.windowMs}`,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { granted: true };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 记录执行
|
|
198
|
+
async record(
|
|
199
|
+
type: QuotaType,
|
|
200
|
+
params: { keyword?: string; profileId?: string; platform?: string }
|
|
201
|
+
): Promise<void> {
|
|
202
|
+
await this.init();
|
|
203
|
+
|
|
204
|
+
const typeConfig = this.config[type];
|
|
205
|
+
if (!typeConfig) return;
|
|
206
|
+
|
|
207
|
+
const now = Date.now();
|
|
208
|
+
|
|
209
|
+
// 为所有相关维度记录
|
|
210
|
+
for (const rule of typeConfig.rules) {
|
|
211
|
+
const key = this.buildKey(type, rule.scope, params);
|
|
212
|
+
const entries = this.history.get(key) || [];
|
|
213
|
+
entries.push({ ts: now, key });
|
|
214
|
+
this.history.set(key, entries);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 持久化历史(异步,不阻塞)
|
|
218
|
+
this.persistHistory().catch(() => {});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 构建维度键
|
|
222
|
+
private buildKey(
|
|
223
|
+
type: QuotaType,
|
|
224
|
+
scope: QuotaScope,
|
|
225
|
+
params: { keyword?: string; profileId?: string; platform?: string }
|
|
226
|
+
): string {
|
|
227
|
+
switch (scope) {
|
|
228
|
+
case 'keyword':
|
|
229
|
+
return `${type}:keyword:${params.keyword || '*'}`;
|
|
230
|
+
case 'profile':
|
|
231
|
+
return `${type}:profile:${params.profileId || '*'}`;
|
|
232
|
+
case 'global':
|
|
233
|
+
return `${type}:global`;
|
|
234
|
+
default:
|
|
235
|
+
return `${type}:unknown`;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 清理过期历史
|
|
240
|
+
private cleanExpiredHistory(): void {
|
|
241
|
+
const now = Date.now();
|
|
242
|
+
const maxWindow = 3600000; // 1小时
|
|
243
|
+
|
|
244
|
+
for (const [key, entries] of this.history) {
|
|
245
|
+
const filtered = entries.filter(e => now - e.ts < maxWindow);
|
|
246
|
+
if (filtered.length === 0) {
|
|
247
|
+
this.history.delete(key);
|
|
248
|
+
} else if (filtered.length !== entries.length) {
|
|
249
|
+
this.history.set(key, filtered);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 持久化历史
|
|
255
|
+
private async persistHistory(): Promise<void> {
|
|
256
|
+
try {
|
|
257
|
+
const dir = path.dirname(this.historyPath);
|
|
258
|
+
await fs.mkdir(dir, { recursive: true });
|
|
259
|
+
|
|
260
|
+
const data: Record<string, HistoryEntry[]> = {};
|
|
261
|
+
for (const [key, entries] of this.history) {
|
|
262
|
+
data[key] = entries;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
await fs.writeFile(this.historyPath, JSON.stringify(data), 'utf-8');
|
|
266
|
+
} catch (err) {
|
|
267
|
+
console.error('[RateLimiter] Failed to persist history:', err);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 获取当前配置
|
|
272
|
+
getConfig(): RateLimiterConfig {
|
|
273
|
+
return { ...this.config };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// 更新配置
|
|
277
|
+
async updateConfig(newConfig: Partial<RateLimiterConfig>): Promise<void> {
|
|
278
|
+
this.config = { ...this.config, ...newConfig };
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
const dir = path.dirname(this.configPath);
|
|
282
|
+
await fs.mkdir(dir, { recursive: true });
|
|
283
|
+
await fs.writeFile(this.configPath, JSON.stringify(this.config, null, 2), 'utf-8');
|
|
284
|
+
console.log('[RateLimiter] Config saved to', this.configPath);
|
|
285
|
+
} catch (err) {
|
|
286
|
+
console.error('[RateLimiter] Failed to save config:', err);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 获取状态(用于调试/UI)
|
|
291
|
+
getStatus(): { type: QuotaType; scope: QuotaScope; count: number; max: number; windowMs: number }[] {
|
|
292
|
+
const result: { type: QuotaType; scope: QuotaScope; count: number; max: number; windowMs: number }[] = [];
|
|
293
|
+
const now = Date.now();
|
|
294
|
+
|
|
295
|
+
for (const [type, typeConfig] of Object.entries(this.config)) {
|
|
296
|
+
for (const rule of typeConfig.rules) {
|
|
297
|
+
const key = `${type}:${rule.scope}`;
|
|
298
|
+
let totalCount = 0;
|
|
299
|
+
|
|
300
|
+
for (const [k, entries] of this.history) {
|
|
301
|
+
if (k.startsWith(key)) {
|
|
302
|
+
const windowStart = now - rule.windowMs;
|
|
303
|
+
totalCount += entries.filter(e => e.ts > windowStart).length;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
result.push({
|
|
308
|
+
type: type as QuotaType,
|
|
309
|
+
scope: rule.scope,
|
|
310
|
+
count: totalCount,
|
|
311
|
+
max: rule.max,
|
|
312
|
+
windowMs: rule.windowMs,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// 单例导出
|
|
322
|
+
export const rateLimiter = RateLimiter.getInstance();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
3
4
|
|
|
4
5
|
export function resolveHomeDir(): string {
|
|
5
6
|
const fromEnv = process.platform === 'win32' ? process.env.USERPROFILE : process.env.HOME;
|
|
@@ -11,6 +12,14 @@ export function resolveHomeDir(): string {
|
|
|
11
12
|
export function resolveDownloadRoot(): string {
|
|
12
13
|
const custom = String(process.env.WEBAUTO_DOWNLOAD_ROOT || process.env.WEBAUTO_DOWNLOAD_DIR || '').trim();
|
|
13
14
|
if (custom) return custom;
|
|
15
|
+
if (process.platform === 'win32') {
|
|
16
|
+
try {
|
|
17
|
+
if (existsSync('D:\\')) return 'D:\\webauto';
|
|
18
|
+
} catch {
|
|
19
|
+
// ignore
|
|
20
|
+
}
|
|
21
|
+
return path.join(resolveHomeDir(), '.webauto');
|
|
22
|
+
}
|
|
14
23
|
return path.join(resolveHomeDir(), '.webauto', 'download');
|
|
15
24
|
}
|
|
16
25
|
|
|
@@ -29,4 +38,3 @@ export function resolvePlatformEnvKeywordDir(input: {
|
|
|
29
38
|
if (!keyword) throw new Error('keyword 不能为空');
|
|
30
39
|
return path.join(downloadRoot, platform, env, keyword);
|
|
31
40
|
}
|
|
32
|
-
|