hyper-agent-browser 0.1.0
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/LICENSE +21 -0
- package/README.md +196 -0
- package/package.json +63 -0
- package/src/browser/context.ts +66 -0
- package/src/browser/manager.ts +414 -0
- package/src/browser/sync-chrome-data.ts +53 -0
- package/src/cli.ts +628 -0
- package/src/cli.ts.backup +529 -0
- package/src/commands/actions.ts +232 -0
- package/src/commands/advanced.ts +252 -0
- package/src/commands/config.ts +44 -0
- package/src/commands/getters.ts +110 -0
- package/src/commands/info.ts +195 -0
- package/src/commands/navigation.ts +50 -0
- package/src/commands/session.ts +83 -0
- package/src/daemon/browser-pool.ts +65 -0
- package/src/daemon/client.ts +128 -0
- package/src/daemon/main.ts +200 -0
- package/src/daemon/server.ts +562 -0
- package/src/session/manager.ts +110 -0
- package/src/session/store.ts +172 -0
- package/src/snapshot/accessibility.ts +182 -0
- package/src/snapshot/dom-extractor.ts +220 -0
- package/src/snapshot/formatter.ts +115 -0
- package/src/snapshot/reference-store.ts +97 -0
- package/src/utils/config.ts +183 -0
- package/src/utils/errors.ts +121 -0
- package/src/utils/logger.ts +12 -0
- package/src/utils/selector.ts +23 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { chromium } from "patchright";
|
|
5
|
+
import type { Browser, BrowserContext, Page } from "patchright";
|
|
6
|
+
import type { Session } from "../session/store";
|
|
7
|
+
import { syncChromeData } from "./sync-chrome-data";
|
|
8
|
+
|
|
9
|
+
export interface BrowserManagerOptions {
|
|
10
|
+
headed?: boolean;
|
|
11
|
+
timeout?: number;
|
|
12
|
+
channel?: "chrome" | "msedge" | "chromium";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class BrowserManager {
|
|
16
|
+
private browser: Browser | null = null;
|
|
17
|
+
private context: BrowserContext | null = null;
|
|
18
|
+
private page: Page | null = null;
|
|
19
|
+
private session: Session;
|
|
20
|
+
private options: BrowserManagerOptions;
|
|
21
|
+
|
|
22
|
+
constructor(session: Session, options: BrowserManagerOptions = {}) {
|
|
23
|
+
this.session = session;
|
|
24
|
+
this.options = {
|
|
25
|
+
headed: options.headed ?? false,
|
|
26
|
+
timeout: options.timeout ?? 30000,
|
|
27
|
+
channel: options.channel ?? session.channel,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async connect(): Promise<void> {
|
|
32
|
+
// Try to reconnect to existing browser via CDP
|
|
33
|
+
if (this.session.wsEndpoint && (await this.isBrowserRunning())) {
|
|
34
|
+
try {
|
|
35
|
+
this.browser = await chromium.connectOverCDP(this.session.wsEndpoint);
|
|
36
|
+
const contexts = this.browser.contexts();
|
|
37
|
+
|
|
38
|
+
if (contexts.length > 0) {
|
|
39
|
+
this.context = contexts[0];
|
|
40
|
+
const pages = this.context.pages();
|
|
41
|
+
this.page = pages.length > 0 ? pages[0] : await this.context.newPage();
|
|
42
|
+
} else {
|
|
43
|
+
// Create new context
|
|
44
|
+
this.context = await this.browser.newContext({
|
|
45
|
+
viewport: { width: 1280, height: 720 },
|
|
46
|
+
});
|
|
47
|
+
this.page = await this.context.newPage();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (this.options.timeout) {
|
|
51
|
+
this.page.setDefaultTimeout(this.options.timeout);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(`Reconnected to existing browser (PID: ${this.session.pid})`);
|
|
55
|
+
return;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error("Failed to reconnect, launching new browser:", error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Launch new browser
|
|
62
|
+
await this.launch();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async launch(): Promise<void> {
|
|
66
|
+
// Sync Chrome data from system profile (only if enabled)
|
|
67
|
+
const syncSystemChrome = process.env.HAB_SYNC_CHROME === "true";
|
|
68
|
+
if (syncSystemChrome) {
|
|
69
|
+
console.log("Syncing data from system Chrome profile...");
|
|
70
|
+
syncChromeData(this.session.userDataDir);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const launchArgs = [
|
|
74
|
+
"--disable-blink-features=AutomationControlled",
|
|
75
|
+
"--no-first-run",
|
|
76
|
+
"--no-default-browser-check",
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
// Check if extensions should be loaded (opt-in via environment variable)
|
|
80
|
+
const loadExtensions = process.env.HAB_LOAD_EXTENSIONS === "true";
|
|
81
|
+
|
|
82
|
+
if (loadExtensions) {
|
|
83
|
+
// Load extensions from Chrome profile
|
|
84
|
+
const extensionPaths = this.loadChromeExtensions();
|
|
85
|
+
|
|
86
|
+
// Add extension paths if any found (limit to 10 for better stability)
|
|
87
|
+
if (extensionPaths.length > 0) {
|
|
88
|
+
const limitedExtensions = extensionPaths.slice(0, 10);
|
|
89
|
+
console.log(
|
|
90
|
+
`Loading ${limitedExtensions.length} Chrome extensions (limited from ${extensionPaths.length} total)...`,
|
|
91
|
+
);
|
|
92
|
+
launchArgs.push(`--load-extension=${limitedExtensions.join(",")}`);
|
|
93
|
+
|
|
94
|
+
if (extensionPaths.length > 10) {
|
|
95
|
+
console.log(
|
|
96
|
+
`Note: Limited to 10 extensions to ensure stability. ${extensionPaths.length - 10} extensions skipped.`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
console.log("Extensions disabled by default. Set HAB_LOAD_EXTENSIONS=true to enable.");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
// Security: Check if system Keychain should be used (opt-in for security)
|
|
106
|
+
const useSystemKeychain = process.env.HAB_USE_SYSTEM_KEYCHAIN === "true";
|
|
107
|
+
|
|
108
|
+
const ignoreArgs = ["--enable-automation"];
|
|
109
|
+
|
|
110
|
+
if (!useSystemKeychain) {
|
|
111
|
+
// Default: Use isolated password store (more secure)
|
|
112
|
+
launchArgs.push("--password-store=basic");
|
|
113
|
+
console.log(
|
|
114
|
+
"🔒 Using isolated password store (secure mode). Set HAB_USE_SYSTEM_KEYCHAIN=true to use system Keychain.",
|
|
115
|
+
);
|
|
116
|
+
} else {
|
|
117
|
+
// Opt-in: Allow system Keychain access
|
|
118
|
+
ignoreArgs.push("--password-store=basic", "--use-mock-keychain");
|
|
119
|
+
console.log("⚠️ Using system Keychain (HAB_USE_SYSTEM_KEYCHAIN=true)");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (loadExtensions) {
|
|
123
|
+
ignoreArgs.push("--disable-extensions");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Use launchPersistentContext for UserData persistence
|
|
127
|
+
this.context = await chromium.launchPersistentContext(this.session.userDataDir, {
|
|
128
|
+
channel: this.options.channel,
|
|
129
|
+
headless: !this.options.headed,
|
|
130
|
+
args: launchArgs,
|
|
131
|
+
ignoreDefaultArgs: ignoreArgs,
|
|
132
|
+
viewport: { width: 1280, height: 720 },
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Extract browser from context
|
|
136
|
+
// @ts-ignore - context has _browser property
|
|
137
|
+
this.browser = this.context._browser;
|
|
138
|
+
|
|
139
|
+
// Get or create page
|
|
140
|
+
const pages = this.context.pages();
|
|
141
|
+
this.page = pages.length > 0 ? pages[0] : await this.context.newPage();
|
|
142
|
+
|
|
143
|
+
// Set default timeout
|
|
144
|
+
if (this.options.timeout) {
|
|
145
|
+
this.page.setDefaultTimeout(this.options.timeout);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// @ts-ignore - Browser has process and wsEndpoint methods
|
|
149
|
+
const pid = this.browser?.process?.()?.pid;
|
|
150
|
+
// @ts-ignore
|
|
151
|
+
const wsEndpoint = this.browser?.wsEndpoint?.();
|
|
152
|
+
|
|
153
|
+
console.log(
|
|
154
|
+
`Launched new browser (PID: ${pid}, Extensions: ${loadExtensions ? "enabled" : "disabled"})`,
|
|
155
|
+
);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error("Failed to launch browser:", error);
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Security: Whitelist of trusted extension IDs
|
|
163
|
+
// Users can add their trusted extensions here
|
|
164
|
+
private ALLOWED_EXTENSION_IDS = new Set<string>([
|
|
165
|
+
// Example: MetaMask
|
|
166
|
+
// 'nkbihfbeogaeaoehlefnkodbefgpgknn',
|
|
167
|
+
// Add your trusted extensions here
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
// Security: Check if extension manifest contains dangerous permissions
|
|
171
|
+
private isExtensionSafe(manifestPath: string): boolean {
|
|
172
|
+
try {
|
|
173
|
+
const { readFileSync } = require("node:fs");
|
|
174
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
175
|
+
|
|
176
|
+
// Check for dangerous permissions
|
|
177
|
+
const dangerousPerms = [
|
|
178
|
+
"debugger",
|
|
179
|
+
"webRequest",
|
|
180
|
+
"proxy",
|
|
181
|
+
"<all_urls>",
|
|
182
|
+
"webRequestBlocking",
|
|
183
|
+
];
|
|
184
|
+
const permissions = [
|
|
185
|
+
...(manifest.permissions || []),
|
|
186
|
+
...(manifest.host_permissions || []),
|
|
187
|
+
...(manifest.optional_permissions || []),
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
for (const perm of dangerousPerms) {
|
|
191
|
+
if (permissions.includes(perm)) {
|
|
192
|
+
console.log(`⚠️ Extension blocked: contains dangerous permission '${perm}'`);
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return true;
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.log(`⚠️ Failed to validate extension manifest: ${error}`);
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private loadChromeExtensions(): string[] {
|
|
205
|
+
const extensionPaths: string[] = [];
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
// Chrome extensions directory
|
|
209
|
+
const chromeExtensionsDir = join(
|
|
210
|
+
homedir(),
|
|
211
|
+
"Library/Application Support/Google/Chrome/Default/Extensions",
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
if (!existsSync(chromeExtensionsDir)) {
|
|
215
|
+
console.log("Chrome extensions directory not found, skipping extension loading");
|
|
216
|
+
return extensionPaths;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Read all extension directories
|
|
220
|
+
const extensionDirs = readdirSync(chromeExtensionsDir);
|
|
221
|
+
|
|
222
|
+
for (const extensionId of extensionDirs) {
|
|
223
|
+
// Skip hidden files and .DS_Store
|
|
224
|
+
if (extensionId.startsWith(".")) continue;
|
|
225
|
+
|
|
226
|
+
// Security: Whitelist check (if whitelist is not empty, enforce it)
|
|
227
|
+
if (this.ALLOWED_EXTENSION_IDS.size > 0 && !this.ALLOWED_EXTENSION_IDS.has(extensionId)) {
|
|
228
|
+
console.log(`⚠️ Extension ${extensionId} not in whitelist, skipping`);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const extensionDir = join(chromeExtensionsDir, extensionId);
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
// Check if it's a directory
|
|
236
|
+
const { statSync } = require("node:fs");
|
|
237
|
+
const stat = statSync(extensionDir);
|
|
238
|
+
if (!stat.isDirectory()) continue;
|
|
239
|
+
|
|
240
|
+
// Read version subdirectories
|
|
241
|
+
const versions = readdirSync(extensionDir);
|
|
242
|
+
const validVersions = versions.filter((v) => !v.startsWith("."));
|
|
243
|
+
|
|
244
|
+
if (validVersions.length === 0) continue;
|
|
245
|
+
|
|
246
|
+
// Sort versions and get the latest (simple lexicographic sort)
|
|
247
|
+
const latestVersion = validVersions.sort().reverse()[0];
|
|
248
|
+
const extensionPath = join(extensionDir, latestVersion);
|
|
249
|
+
|
|
250
|
+
// Verify the extension path exists and has manifest
|
|
251
|
+
const manifestPath = join(extensionPath, "manifest.json");
|
|
252
|
+
if (existsSync(manifestPath)) {
|
|
253
|
+
// Security: Validate extension safety
|
|
254
|
+
if (!this.isExtensionSafe(manifestPath)) {
|
|
255
|
+
console.log(`⚠️ Extension ${extensionId} blocked due to dangerous permissions`);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
extensionPaths.push(extensionPath);
|
|
260
|
+
console.log(`✅ Loaded safe extension: ${extensionId}`);
|
|
261
|
+
}
|
|
262
|
+
} catch (error) {
|
|
263
|
+
// Skip invalid extension directories
|
|
264
|
+
console.log(`Skipping invalid extension: ${extensionId}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
console.log(`Found ${extensionPaths.length} valid and safe Chrome extensions to load`);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
console.error("Error loading Chrome extensions:", error);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return extensionPaths;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private async isBrowserRunning(): Promise<boolean> {
|
|
277
|
+
if (!this.session.pid) return false;
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
// Check if process exists
|
|
281
|
+
process.kill(this.session.pid, 0);
|
|
282
|
+
return true;
|
|
283
|
+
} catch {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async getPage(): Promise<Page> {
|
|
289
|
+
if (!this.page) {
|
|
290
|
+
await this.connect();
|
|
291
|
+
}
|
|
292
|
+
return this.page!;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
getContext(): BrowserContext | null {
|
|
296
|
+
return this.context;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
getWsEndpoint(): string | undefined {
|
|
300
|
+
// @ts-ignore - Patchright Browser has wsEndpoint method
|
|
301
|
+
return this.browser?.wsEndpoint?.();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
getPid(): number | undefined {
|
|
305
|
+
// @ts-ignore - Patchright Browser has process method
|
|
306
|
+
return this.browser?.process?.()?.pid;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async close(): Promise<void> {
|
|
310
|
+
if (this.browser) {
|
|
311
|
+
await this.browser.close();
|
|
312
|
+
this.browser = null;
|
|
313
|
+
this.context = null;
|
|
314
|
+
this.page = null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
isConnected(): boolean {
|
|
319
|
+
return this.browser !== null && this.page !== null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Inject visual indicator for agent operations
|
|
323
|
+
async showOperationIndicator(operation: string): Promise<void> {
|
|
324
|
+
if (!this.page || !this.options.headed) return;
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
await this.page.evaluate((op) => {
|
|
328
|
+
// Remove existing indicator
|
|
329
|
+
const existing = document.getElementById("hab-operation-indicator");
|
|
330
|
+
if (existing) existing.remove();
|
|
331
|
+
|
|
332
|
+
// Create new indicator
|
|
333
|
+
const indicator = document.createElement("div");
|
|
334
|
+
indicator.id = "hab-operation-indicator";
|
|
335
|
+
indicator.innerHTML = `
|
|
336
|
+
<div style="
|
|
337
|
+
position: fixed;
|
|
338
|
+
top: 20px;
|
|
339
|
+
right: 20px;
|
|
340
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
341
|
+
color: white;
|
|
342
|
+
padding: 12px 20px;
|
|
343
|
+
border-radius: 8px;
|
|
344
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
345
|
+
font-size: 14px;
|
|
346
|
+
font-weight: 500;
|
|
347
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
348
|
+
z-index: 2147483647;
|
|
349
|
+
display: flex;
|
|
350
|
+
align-items: center;
|
|
351
|
+
gap: 10px;
|
|
352
|
+
animation: hab-slide-in 0.3s ease-out;
|
|
353
|
+
">
|
|
354
|
+
<div style="
|
|
355
|
+
width: 8px;
|
|
356
|
+
height: 8px;
|
|
357
|
+
background: #4ade80;
|
|
358
|
+
border-radius: 50%;
|
|
359
|
+
animation: hab-pulse 1.5s ease-in-out infinite;
|
|
360
|
+
"></div>
|
|
361
|
+
<span>🤖 Agent操作中: ${op}</span>
|
|
362
|
+
</div>
|
|
363
|
+
`;
|
|
364
|
+
|
|
365
|
+
// Add animations
|
|
366
|
+
if (!document.getElementById("hab-styles")) {
|
|
367
|
+
const style = document.createElement("style");
|
|
368
|
+
style.id = "hab-styles";
|
|
369
|
+
style.textContent = `
|
|
370
|
+
@keyframes hab-slide-in {
|
|
371
|
+
from { transform: translateX(400px); opacity: 0; }
|
|
372
|
+
to { transform: translateX(0); opacity: 1; }
|
|
373
|
+
}
|
|
374
|
+
@keyframes hab-pulse {
|
|
375
|
+
0%, 100% { opacity: 1; transform: scale(1); }
|
|
376
|
+
50% { opacity: 0.5; transform: scale(1.2); }
|
|
377
|
+
}
|
|
378
|
+
`;
|
|
379
|
+
document.head.appendChild(style);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
document.body.appendChild(indicator);
|
|
383
|
+
}, operation);
|
|
384
|
+
} catch (error) {
|
|
385
|
+
// Silently fail if page is not ready
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async hideOperationIndicator(): Promise<void> {
|
|
390
|
+
if (!this.page || !this.options.headed) return;
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
await this.page.evaluate(() => {
|
|
394
|
+
const indicator = document.getElementById("hab-operation-indicator");
|
|
395
|
+
if (indicator) {
|
|
396
|
+
indicator.style.animation = "hab-slide-in 0.3s ease-out reverse";
|
|
397
|
+
setTimeout(() => indicator.remove(), 300);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
} catch (error) {
|
|
401
|
+
// Silently fail
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async getCurrentUrl(): Promise<string> {
|
|
406
|
+
const page = await this.getPage();
|
|
407
|
+
return page.url();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async getTitle(): Promise<string> {
|
|
411
|
+
const page = await this.getPage();
|
|
412
|
+
return page.title();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { copyFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 同步系统 Chrome Profile 数据到 hab session
|
|
7
|
+
* 只复制登录凭证和 Cookies,不复制扩展(避免冲突)
|
|
8
|
+
*/
|
|
9
|
+
export function syncChromeData(targetUserDataDir: string): void {
|
|
10
|
+
try {
|
|
11
|
+
const systemChromeProfile = join(
|
|
12
|
+
homedir(),
|
|
13
|
+
"Library/Application Support/Google/Chrome/Default",
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
if (!existsSync(systemChromeProfile)) {
|
|
17
|
+
console.log("System Chrome profile not found, skipping sync");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 需要同步的文件列表
|
|
22
|
+
const filesToSync = [
|
|
23
|
+
"Cookies", // Cookie 数据
|
|
24
|
+
"Login Data", // 登录凭证
|
|
25
|
+
"Preferences", // Chrome 偏好设置
|
|
26
|
+
"Web Data", // 表单自动填充数据
|
|
27
|
+
"Network", // 网络相关数据
|
|
28
|
+
"Local Storage", // LocalStorage 数据
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
let syncedCount = 0;
|
|
32
|
+
|
|
33
|
+
for (const fileName of filesToSync) {
|
|
34
|
+
const sourcePath = join(systemChromeProfile, fileName);
|
|
35
|
+
const targetPath = join(targetUserDataDir, fileName);
|
|
36
|
+
|
|
37
|
+
if (existsSync(sourcePath)) {
|
|
38
|
+
try {
|
|
39
|
+
copyFileSync(sourcePath, targetPath);
|
|
40
|
+
syncedCount++;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
// 某些文件可能被锁定,跳过即可
|
|
43
|
+
console.log(`Skipped ${fileName} (in use)`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log(`Synced ${syncedCount}/${filesToSync.length} files from system Chrome profile`);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error("Error syncing Chrome data:", error);
|
|
51
|
+
// 不抛出错误,允许继续启动
|
|
52
|
+
}
|
|
53
|
+
}
|