n8n-nodes-nvk-browser 1.0.0 → 1.0.3
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/dist/nodes/PageInteraction/MoveAndClick/MoveAndClick.description.js +34 -9
- package/dist/nodes/PageInteraction/MoveAndClick/MoveAndClick.node.js +190 -41
- package/dist/nodes/PageInteraction/RunJavaScript/RunJavaScript.description.js +13 -0
- package/dist/nodes/PageInteraction/RunJavaScript/RunJavaScript.node.js +7 -3
- package/dist/nodes/ProfileManagement/DeleteProfile/DeleteProfile.node.js +0 -1
- package/dist/nodes/ProfileManagement/StartProfile/StartProfile.node.js +0 -1
- package/dist/nodes/ProfileManagement/StopProfile/StopProfile.node.js +0 -1
- package/dist/utils/BrowserManager.d.ts +6 -3
- package/dist/utils/BrowserManager.js +100 -9
- package/dist/utils/ProfileManager.d.ts +1 -0
- package/dist/utils/ProfileManager.js +58 -0
- package/package.json +1 -1
|
@@ -22,7 +22,23 @@ exports.moveAndClickFields = [
|
|
|
22
22
|
type: 'string',
|
|
23
23
|
required: true,
|
|
24
24
|
default: '',
|
|
25
|
-
description: 'CSS selector
|
|
25
|
+
description: 'For Puppeteer: CSS selector, XPath (with >XPATH> prefix), or code pattern with targetPage.locator()/page.locator() and puppeteer.Locator.race(). Examples: "textarea", ">XPATH>/html/body/div", or code with Locator.race(). For Javascript: CSS selector only.',
|
|
26
|
+
typeOptions: {
|
|
27
|
+
rows: 4,
|
|
28
|
+
},
|
|
29
|
+
displayOptions: {
|
|
30
|
+
show: {
|
|
31
|
+
resource: ['page'],
|
|
32
|
+
operation: ['moveAndClick'],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
displayName: 'Timeout (Milliseconds)',
|
|
38
|
+
name: 'timeout',
|
|
39
|
+
type: 'number',
|
|
40
|
+
default: 30000,
|
|
41
|
+
description: 'Timeout in milliseconds for waiting for the element',
|
|
26
42
|
displayOptions: {
|
|
27
43
|
show: {
|
|
28
44
|
resource: ['page'],
|
|
@@ -35,10 +51,6 @@ exports.moveAndClickFields = [
|
|
|
35
51
|
name: 'clickMethod',
|
|
36
52
|
type: 'options',
|
|
37
53
|
options: [
|
|
38
|
-
{
|
|
39
|
-
name: 'Use GhostCursor',
|
|
40
|
-
value: 'ghostcursor',
|
|
41
|
-
},
|
|
42
54
|
{
|
|
43
55
|
name: 'Use Puppeteer',
|
|
44
56
|
value: 'puppeteer',
|
|
@@ -61,13 +73,13 @@ exports.moveAndClickFields = [
|
|
|
61
73
|
displayName: 'Wait For Click (Milliseconds)',
|
|
62
74
|
name: 'waitForClick',
|
|
63
75
|
type: 'number',
|
|
64
|
-
default:
|
|
76
|
+
default: 500,
|
|
65
77
|
description: 'Wait time before clicking in milliseconds',
|
|
66
78
|
displayOptions: {
|
|
67
79
|
show: {
|
|
68
80
|
resource: ['page'],
|
|
69
81
|
operation: ['moveAndClick'],
|
|
70
|
-
clickMethod: ['
|
|
82
|
+
clickMethod: ['puppeteer'],
|
|
71
83
|
},
|
|
72
84
|
},
|
|
73
85
|
},
|
|
@@ -95,7 +107,7 @@ exports.moveAndClickFields = [
|
|
|
95
107
|
show: {
|
|
96
108
|
resource: ['page'],
|
|
97
109
|
operation: ['moveAndClick'],
|
|
98
|
-
clickMethod: ['
|
|
110
|
+
clickMethod: ['puppeteer'],
|
|
99
111
|
},
|
|
100
112
|
},
|
|
101
113
|
},
|
|
@@ -112,7 +124,7 @@ exports.moveAndClickFields = [
|
|
|
112
124
|
show: {
|
|
113
125
|
resource: ['page'],
|
|
114
126
|
operation: ['moveAndClick'],
|
|
115
|
-
clickMethod: ['
|
|
127
|
+
clickMethod: ['puppeteer'],
|
|
116
128
|
},
|
|
117
129
|
},
|
|
118
130
|
},
|
|
@@ -129,4 +141,17 @@ exports.moveAndClickFields = [
|
|
|
129
141
|
},
|
|
130
142
|
},
|
|
131
143
|
},
|
|
144
|
+
{
|
|
145
|
+
displayName: 'Auto Start Profile',
|
|
146
|
+
name: 'autoStart',
|
|
147
|
+
type: 'boolean',
|
|
148
|
+
default: false,
|
|
149
|
+
description: 'Automatically start the profile if it is not running',
|
|
150
|
+
displayOptions: {
|
|
151
|
+
show: {
|
|
152
|
+
resource: ['page'],
|
|
153
|
+
operation: ['moveAndClick'],
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
132
157
|
];
|
|
@@ -22,11 +22,15 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
22
22
|
__setModuleDefault(result, mod);
|
|
23
23
|
return result;
|
|
24
24
|
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
25
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
29
|
exports.MoveAndClick = void 0;
|
|
27
30
|
const BrowserManager_1 = require("../../../utils/BrowserManager");
|
|
28
31
|
const MoveAndClick_description_1 = require("./MoveAndClick.description");
|
|
29
32
|
const path = __importStar(require("path"));
|
|
33
|
+
const puppeteer_core_1 = __importDefault(require("puppeteer-core"));
|
|
30
34
|
class MoveAndClick {
|
|
31
35
|
constructor() {
|
|
32
36
|
this.description = {
|
|
@@ -86,69 +90,214 @@ class MoveAndClick {
|
|
|
86
90
|
const resolvedProfilesDir = path.isAbsolute(profilesDir)
|
|
87
91
|
? profilesDir
|
|
88
92
|
: path.resolve(workspacePath, profilesDir);
|
|
89
|
-
BrowserManager_1.BrowserManager.resetInstance(); // Reset để đảm bảo dùng đúng paths
|
|
90
93
|
const browserManager = BrowserManager_1.BrowserManager.getInstance(resolvedBrowserPath, resolvedProfilesDir);
|
|
91
94
|
for (let i = 0; i < items.length; i++) {
|
|
92
95
|
try {
|
|
93
96
|
const profileId = this.getNodeParameter('profileId', i);
|
|
94
97
|
const selector = this.getNodeParameter('selector', i);
|
|
95
98
|
const clickMethod = this.getNodeParameter('clickMethod', i) || 'puppeteer';
|
|
96
|
-
const
|
|
97
|
-
const button = this.getNodeParameter('button', i) || 'left';
|
|
98
|
-
const clickCount = this.getNodeParameter('clickCount', i) || 1;
|
|
99
|
+
const timeout = this.getNodeParameter('timeout', i) || 30000;
|
|
99
100
|
const tabIndex = this.getNodeParameter('tabIndex', i) || 0;
|
|
100
|
-
const
|
|
101
|
+
const autoStart = this.getNodeParameter('autoStart', i) || false;
|
|
102
|
+
let instance = browserManager.getInstance(profileId);
|
|
103
|
+
// Auto start profile nếu chưa chạy và option được bật
|
|
104
|
+
if (!instance && autoStart) {
|
|
105
|
+
instance = await browserManager.startProfileIfNotRunning(profileId);
|
|
106
|
+
}
|
|
101
107
|
if (!instance) {
|
|
102
|
-
throw new Error(`Profile ${profileId} is not running
|
|
108
|
+
throw new Error(`Profile ${profileId} is not running. Enable "Auto Start Profile" to start it automatically.`);
|
|
103
109
|
}
|
|
104
110
|
const page = await browserManager.getPage(profileId, tabIndex);
|
|
105
111
|
if (!page) {
|
|
106
112
|
throw new Error(`Could not get page for profile ${profileId}`);
|
|
107
113
|
}
|
|
108
|
-
//
|
|
109
|
-
|
|
114
|
+
// Set default timeout
|
|
115
|
+
page.setDefaultTimeout(timeout);
|
|
110
116
|
if (clickMethod === 'javascript') {
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
await page.waitForTimeout(waitForClick);
|
|
114
|
-
}
|
|
117
|
+
// Use Javascript Click - simple implementation
|
|
118
|
+
await page.waitForSelector(selector, { timeout });
|
|
115
119
|
// JavaScript click
|
|
116
|
-
|
|
117
|
-
left: 0,
|
|
118
|
-
middle: 1,
|
|
119
|
-
right: 2,
|
|
120
|
-
};
|
|
121
|
-
await page.evaluate((sel, btn, count) => {
|
|
120
|
+
await page.evaluate((sel) => {
|
|
122
121
|
// This code runs in browser context, so DOM APIs are available
|
|
123
122
|
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
124
123
|
const element = document.querySelector(sel);
|
|
125
124
|
if (element) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
element.dispatchEvent(event);
|
|
134
|
-
}
|
|
125
|
+
const event = new MouseEvent('click', {
|
|
126
|
+
view: window,
|
|
127
|
+
bubbles: true,
|
|
128
|
+
cancelable: true,
|
|
129
|
+
button: 0,
|
|
130
|
+
});
|
|
131
|
+
element.dispatchEvent(event);
|
|
135
132
|
}
|
|
136
|
-
}, selector
|
|
133
|
+
}, selector);
|
|
137
134
|
}
|
|
138
135
|
else if (clickMethod === 'puppeteer') {
|
|
139
|
-
// Puppeteer
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
//
|
|
144
|
-
const
|
|
145
|
-
if
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
136
|
+
// Use Puppeteer - support Locator.race() pattern
|
|
137
|
+
const waitForClick = this.getNodeParameter('waitForClick', i) || 500;
|
|
138
|
+
const button = this.getNodeParameter('button', i) || 'left';
|
|
139
|
+
const clickCount = this.getNodeParameter('clickCount', i) || 1;
|
|
140
|
+
// Parse selector - check if it's code with Locator.race() or simple selector
|
|
141
|
+
const selectorTrimmed = selector.trim();
|
|
142
|
+
// Check if selector contains Locator.race pattern or is a code block
|
|
143
|
+
if (selectorTrimmed.includes('Locator.race') || selectorTrimmed.includes('puppeteer.Locator')) {
|
|
144
|
+
// Execute as code - user provided full code
|
|
145
|
+
// Extract the selector part from the code or execute the code directly
|
|
146
|
+
try {
|
|
147
|
+
// Parse selectors from the code pattern
|
|
148
|
+
const locators = [];
|
|
149
|
+
// Parse CSS selectors (lines with targetPage.locator('...') or page.locator('...'))
|
|
150
|
+
const cssMatches = selector.match(/(?:targetPage|page)\.locator\(['"]([^'"]+)['"]\)/g);
|
|
151
|
+
if (cssMatches) {
|
|
152
|
+
cssMatches.forEach(match => {
|
|
153
|
+
const cssSel = match.match(/['"]([^'"]+)['"]/)?.[1];
|
|
154
|
+
if (cssSel && !cssSel.startsWith('::-p-xpath')) {
|
|
155
|
+
// Use page.locator if available, otherwise fallback
|
|
156
|
+
if (typeof page.locator === 'function') {
|
|
157
|
+
locators.push(page.locator(cssSel));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
// Parse XPath selectors (::-p-xpath(...))
|
|
163
|
+
const xpathMatches = selector.match(/::-p-xpath\(([^)]+)\)/g);
|
|
164
|
+
if (xpathMatches) {
|
|
165
|
+
xpathMatches.forEach(match => {
|
|
166
|
+
const xpath = match.match(/::-p-xpath\(([^)]+)\)/)?.[1];
|
|
167
|
+
if (xpath && typeof page.locator === 'function') {
|
|
168
|
+
locators.push(page.locator(`::-p-xpath(${xpath})`));
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
// Also check for >CSS> and >XPATH> prefixes
|
|
173
|
+
if (selector.includes('>CSS>') || selector.includes('>XPATH>')) {
|
|
174
|
+
const parts = selector.split(/\s+/);
|
|
175
|
+
parts.forEach(part => {
|
|
176
|
+
if (part.startsWith('>CSS>') && typeof page.locator === 'function') {
|
|
177
|
+
const cssSel = part.replace('>CSS>', '');
|
|
178
|
+
locators.push(page.locator(cssSel));
|
|
179
|
+
}
|
|
180
|
+
else if (part.startsWith('>XPATH>') && typeof page.locator === 'function') {
|
|
181
|
+
const xpath = part.replace('>XPATH>', '');
|
|
182
|
+
locators.push(page.locator(`::-p-xpath(${xpath})`));
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
if (locators.length > 0 && typeof page.locator === 'function') {
|
|
187
|
+
// Use Locator.race() if multiple locators and API is available
|
|
188
|
+
let locator;
|
|
189
|
+
if (locators.length > 1) {
|
|
190
|
+
// Check if Locator.race is available
|
|
191
|
+
const LocatorClass = puppeteer_core_1.default.Locator;
|
|
192
|
+
if (LocatorClass && typeof LocatorClass.race === 'function') {
|
|
193
|
+
locator = LocatorClass.race(locators);
|
|
194
|
+
if (typeof locator.setTimeout === 'function') {
|
|
195
|
+
locator = locator.setTimeout(timeout);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
// Fallback: try first locator
|
|
200
|
+
locator = locators[0];
|
|
201
|
+
if (typeof locator.setTimeout === 'function') {
|
|
202
|
+
locator = locator.setTimeout(timeout);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
locator = locators[0];
|
|
208
|
+
if (typeof locator.setTimeout === 'function') {
|
|
209
|
+
locator = locator.setTimeout(timeout);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Wait before clicking if specified
|
|
213
|
+
if (waitForClick > 0) {
|
|
214
|
+
await page.waitForTimeout(waitForClick);
|
|
215
|
+
}
|
|
216
|
+
// Click with options
|
|
217
|
+
const clickOptions = {
|
|
218
|
+
button: button,
|
|
219
|
+
clickCount: clickCount,
|
|
220
|
+
};
|
|
221
|
+
if (typeof locator.click === 'function') {
|
|
222
|
+
await locator.click(clickOptions);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
throw new Error('Locator.click is not available');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
// Fallback to simple selector parsing
|
|
230
|
+
throw new Error('Could not parse selector from code or Locator API not available');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
// If code parsing fails, try to use as simple selector
|
|
235
|
+
await page.waitForSelector(selector, { timeout });
|
|
236
|
+
if (waitForClick > 0) {
|
|
237
|
+
await page.waitForTimeout(waitForClick);
|
|
238
|
+
}
|
|
239
|
+
await page.click(selector, {
|
|
240
|
+
button: button,
|
|
241
|
+
clickCount: clickCount,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
// Simple selector - parse CSS or XPath prefixes
|
|
247
|
+
// Try to use Locator API if available, otherwise fallback to traditional API
|
|
248
|
+
if (typeof page.locator === 'function') {
|
|
249
|
+
let locator;
|
|
250
|
+
if (selectorTrimmed.startsWith('>CSS>')) {
|
|
251
|
+
const cssSel = selectorTrimmed.replace('>CSS>', '').trim();
|
|
252
|
+
locator = page.locator(cssSel);
|
|
253
|
+
}
|
|
254
|
+
else if (selectorTrimmed.startsWith('>XPATH>')) {
|
|
255
|
+
const xpath = selectorTrimmed.replace('>XPATH>', '').trim();
|
|
256
|
+
locator = page.locator(`::-p-xpath(${xpath})`);
|
|
257
|
+
}
|
|
258
|
+
else if (selectorTrimmed.startsWith('::-p-xpath')) {
|
|
259
|
+
// Already in XPath format
|
|
260
|
+
locator = page.locator(selectorTrimmed);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
// Default to CSS selector
|
|
264
|
+
locator = page.locator(selectorTrimmed);
|
|
265
|
+
}
|
|
266
|
+
// Set timeout if method exists
|
|
267
|
+
if (typeof locator.setTimeout === 'function') {
|
|
268
|
+
locator = locator.setTimeout(timeout);
|
|
269
|
+
}
|
|
270
|
+
// Wait before clicking if specified
|
|
271
|
+
if (waitForClick > 0) {
|
|
272
|
+
await page.waitForTimeout(waitForClick);
|
|
273
|
+
}
|
|
274
|
+
// Click with options
|
|
275
|
+
const clickOptions = {
|
|
276
|
+
button: button,
|
|
277
|
+
clickCount: clickCount,
|
|
278
|
+
};
|
|
279
|
+
if (typeof locator.click === 'function') {
|
|
280
|
+
await locator.click(clickOptions);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
// Fallback to traditional API
|
|
284
|
+
await page.waitForSelector(selectorTrimmed, { timeout });
|
|
285
|
+
if (waitForClick > 0) {
|
|
286
|
+
await page.waitForTimeout(waitForClick);
|
|
287
|
+
}
|
|
288
|
+
await page.click(selectorTrimmed, clickOptions);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
// Fallback to traditional API
|
|
293
|
+
await page.waitForSelector(selectorTrimmed, { timeout });
|
|
294
|
+
if (waitForClick > 0) {
|
|
295
|
+
await page.waitForTimeout(waitForClick);
|
|
296
|
+
}
|
|
297
|
+
await page.click(selectorTrimmed, {
|
|
298
|
+
button: button,
|
|
299
|
+
clickCount: clickCount,
|
|
300
|
+
});
|
|
152
301
|
}
|
|
153
302
|
}
|
|
154
303
|
}
|
|
@@ -59,4 +59,17 @@ exports.runJavaScriptFields = [
|
|
|
59
59
|
},
|
|
60
60
|
},
|
|
61
61
|
},
|
|
62
|
+
{
|
|
63
|
+
displayName: 'Auto Start Profile',
|
|
64
|
+
name: 'autoStart',
|
|
65
|
+
type: 'boolean',
|
|
66
|
+
default: false,
|
|
67
|
+
description: 'Automatically start the profile if it is not running',
|
|
68
|
+
displayOptions: {
|
|
69
|
+
show: {
|
|
70
|
+
resource: ['page'],
|
|
71
|
+
operation: ['runJavaScript'],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
62
75
|
];
|
|
@@ -86,16 +86,20 @@ class RunJavaScript {
|
|
|
86
86
|
const resolvedProfilesDir = path.isAbsolute(profilesDir)
|
|
87
87
|
? profilesDir
|
|
88
88
|
: path.resolve(workspacePath, profilesDir);
|
|
89
|
-
BrowserManager_1.BrowserManager.resetInstance(); // Reset để đảm bảo dùng đúng paths
|
|
90
89
|
const browserManager = BrowserManager_1.BrowserManager.getInstance(resolvedBrowserPath, resolvedProfilesDir);
|
|
91
90
|
for (let i = 0; i < items.length; i++) {
|
|
92
91
|
try {
|
|
93
92
|
const profileId = this.getNodeParameter('profileId', i);
|
|
94
93
|
const tabIndex = this.getNodeParameter('tabIndex', i) || 0;
|
|
95
94
|
const javascriptCode = this.getNodeParameter('javascriptCode', i);
|
|
96
|
-
const
|
|
95
|
+
const autoStart = this.getNodeParameter('autoStart', i) || false;
|
|
96
|
+
let instance = browserManager.getInstance(profileId);
|
|
97
|
+
// Auto start profile nếu chưa chạy và option được bật
|
|
98
|
+
if (!instance && autoStart) {
|
|
99
|
+
instance = await browserManager.startProfileIfNotRunning(profileId);
|
|
100
|
+
}
|
|
97
101
|
if (!instance) {
|
|
98
|
-
throw new Error(`Profile ${profileId} is not running
|
|
102
|
+
throw new Error(`Profile ${profileId} is not running. Enable "Auto Start Profile" to start it automatically.`);
|
|
99
103
|
}
|
|
100
104
|
const page = await browserManager.getPage(profileId, tabIndex);
|
|
101
105
|
if (!page) {
|
|
@@ -88,7 +88,6 @@ class DeleteProfile {
|
|
|
88
88
|
? profilesDir
|
|
89
89
|
: path.resolve(workspacePath, profilesDir);
|
|
90
90
|
const profileManager = new ProfileManager_1.ProfileManager(resolvedProfilesDir);
|
|
91
|
-
BrowserManager_1.BrowserManager.resetInstance(); // Reset để đảm bảo dùng đúng paths
|
|
92
91
|
const browserManager = BrowserManager_1.BrowserManager.getInstance(resolvedBrowserPath, resolvedProfilesDir);
|
|
93
92
|
for (let i = 0; i < items.length; i++) {
|
|
94
93
|
try {
|
|
@@ -87,7 +87,6 @@ class StartProfile {
|
|
|
87
87
|
const resolvedProfilesDir = path.isAbsolute(profilesDir)
|
|
88
88
|
? profilesDir
|
|
89
89
|
: path.resolve(workspacePath, profilesDir);
|
|
90
|
-
BrowserManager_1.BrowserManager.resetInstance(); // Reset để đảm bảo dùng đúng paths
|
|
91
90
|
const browserManager = BrowserManager_1.BrowserManager.getInstance(resolvedBrowserPath, resolvedProfilesDir);
|
|
92
91
|
for (let i = 0; i < items.length; i++) {
|
|
93
92
|
try {
|
|
@@ -86,7 +86,6 @@ class StopProfile {
|
|
|
86
86
|
const resolvedProfilesDir = path.isAbsolute(profilesDir)
|
|
87
87
|
? profilesDir
|
|
88
88
|
: path.resolve(workspacePath, profilesDir);
|
|
89
|
-
BrowserManager_1.BrowserManager.resetInstance(); // Reset để đảm bảo dùng đúng paths
|
|
90
89
|
const browserManager = BrowserManager_1.BrowserManager.getInstance(resolvedBrowserPath, resolvedProfilesDir);
|
|
91
90
|
for (let i = 0; i < items.length; i++) {
|
|
92
91
|
try {
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { Page } from 'puppeteer-core';
|
|
2
2
|
import { BrowserInstance, WindowConfig } from './types';
|
|
3
3
|
export declare class BrowserManager {
|
|
4
|
-
private static
|
|
4
|
+
private static instances;
|
|
5
5
|
private instances;
|
|
6
6
|
private browserPath;
|
|
7
7
|
private profilesManager;
|
|
8
|
+
private profilesDir;
|
|
8
9
|
private constructor();
|
|
9
|
-
static getInstance(browserPath
|
|
10
|
-
static resetInstance(): void;
|
|
10
|
+
static getInstance(browserPath: string, profilesDir: string): BrowserManager;
|
|
11
|
+
static resetInstance(browserPath?: string, profilesDir?: string): void;
|
|
11
12
|
startProfile(profileId: string, windowConfig?: WindowConfig): Promise<BrowserInstance>;
|
|
12
13
|
stopProfile(profileId: string): Promise<boolean>;
|
|
13
14
|
getInstance(profileId: string): BrowserInstance | undefined;
|
|
@@ -15,4 +16,6 @@ export declare class BrowserManager {
|
|
|
15
16
|
private getDebugPort;
|
|
16
17
|
private setWindowPosition;
|
|
17
18
|
getPage(profileId: string, tabIndex?: number): Promise<Page | null>;
|
|
19
|
+
private setProfileName;
|
|
20
|
+
startProfileIfNotRunning(profileId: string, windowConfig?: WindowConfig): Promise<BrowserInstance>;
|
|
18
21
|
}
|
|
@@ -1,9 +1,34 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
2
25
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
26
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
27
|
};
|
|
5
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
29
|
exports.BrowserManager = void 0;
|
|
30
|
+
const path = __importStar(require("path"));
|
|
31
|
+
const fs = __importStar(require("fs"));
|
|
7
32
|
const puppeteer_core_1 = __importDefault(require("puppeteer-core"));
|
|
8
33
|
const ProxyHandler_1 = require("./ProxyHandler");
|
|
9
34
|
const ExtensionHandler_1 = require("./ExtensionHandler");
|
|
@@ -12,19 +37,25 @@ class BrowserManager {
|
|
|
12
37
|
constructor(browserPath, profilesDir) {
|
|
13
38
|
this.instances = new Map();
|
|
14
39
|
this.browserPath = browserPath;
|
|
40
|
+
this.profilesDir = profilesDir;
|
|
15
41
|
this.profilesManager = new ProfileManager_1.ProfileManager(profilesDir);
|
|
16
42
|
}
|
|
17
43
|
static getInstance(browserPath, profilesDir) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
BrowserManager.instance = new BrowserManager(browserPath, profilesDir);
|
|
44
|
+
// Tạo key từ paths để có thể có nhiều BrowserManager với paths khác nhau
|
|
45
|
+
const key = `${browserPath}:${profilesDir}`;
|
|
46
|
+
if (!BrowserManager.instances.has(key)) {
|
|
47
|
+
BrowserManager.instances.set(key, new BrowserManager(browserPath, profilesDir));
|
|
23
48
|
}
|
|
24
|
-
return BrowserManager.
|
|
49
|
+
return BrowserManager.instances.get(key);
|
|
25
50
|
}
|
|
26
|
-
static resetInstance() {
|
|
27
|
-
|
|
51
|
+
static resetInstance(browserPath, profilesDir) {
|
|
52
|
+
if (browserPath && profilesDir) {
|
|
53
|
+
const key = `${browserPath}:${profilesDir}`;
|
|
54
|
+
BrowserManager.instances.delete(key);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
BrowserManager.instances.clear();
|
|
58
|
+
}
|
|
28
59
|
}
|
|
29
60
|
async startProfile(profileId, windowConfig) {
|
|
30
61
|
// Kiểm tra xem profile đã đang chạy chưa
|
|
@@ -37,6 +68,8 @@ class BrowserManager {
|
|
|
37
68
|
throw new Error(`Profile ${profileId} not found`);
|
|
38
69
|
}
|
|
39
70
|
const profilePath = this.profilesManager.getProfilePath(profileId);
|
|
71
|
+
// Set profile name trong Chrome preferences
|
|
72
|
+
this.setProfileName(profilePath, profile.name);
|
|
40
73
|
// Chuẩn bị Chrome arguments
|
|
41
74
|
const args = [
|
|
42
75
|
`--user-data-dir=${profilePath}`,
|
|
@@ -169,6 +202,64 @@ class BrowserManager {
|
|
|
169
202
|
instance.pages.set(tabIndex, newPage);
|
|
170
203
|
return newPage;
|
|
171
204
|
}
|
|
205
|
+
setProfileName(profilePath, profileName) {
|
|
206
|
+
try {
|
|
207
|
+
// Set profile name trong Local State file
|
|
208
|
+
const localStatePath = path.join(profilePath, 'Local State');
|
|
209
|
+
let localState = {};
|
|
210
|
+
if (fs.existsSync(localStatePath)) {
|
|
211
|
+
try {
|
|
212
|
+
const content = fs.readFileSync(localStatePath, 'utf-8');
|
|
213
|
+
localState = JSON.parse(content);
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
// File exists but invalid JSON, start fresh
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// Set profile name trong Chrome profile structure
|
|
220
|
+
if (!localState.profile) {
|
|
221
|
+
localState.profile = {};
|
|
222
|
+
}
|
|
223
|
+
localState.profile.name = profileName;
|
|
224
|
+
// Cũng set trong profile info
|
|
225
|
+
if (!localState.profile.info_cache) {
|
|
226
|
+
localState.profile.info_cache = {};
|
|
227
|
+
}
|
|
228
|
+
localState.profile.info_cache.Default = {
|
|
229
|
+
name: profileName,
|
|
230
|
+
is_using_default_name: false,
|
|
231
|
+
};
|
|
232
|
+
// Write back
|
|
233
|
+
fs.writeFileSync(localStatePath, JSON.stringify(localState, null, 2), 'utf-8');
|
|
234
|
+
// Cũng set trong Preferences file nếu có
|
|
235
|
+
const prefsPath = path.join(profilePath, 'Default', 'Preferences');
|
|
236
|
+
if (fs.existsSync(prefsPath)) {
|
|
237
|
+
try {
|
|
238
|
+
const prefsContent = fs.readFileSync(prefsPath, 'utf-8');
|
|
239
|
+
const prefs = JSON.parse(prefsContent);
|
|
240
|
+
if (!prefs.profile) {
|
|
241
|
+
prefs.profile = {};
|
|
242
|
+
}
|
|
243
|
+
prefs.profile.name = profileName;
|
|
244
|
+
fs.writeFileSync(prefsPath, JSON.stringify(prefs, null, 2), 'utf-8');
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
// Ignore preferences errors
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
// Ignore errors - profile name is optional
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
async startProfileIfNotRunning(profileId, windowConfig) {
|
|
256
|
+
// Kiểm tra xem profile đã đang chạy chưa
|
|
257
|
+
if (this.instances.has(profileId)) {
|
|
258
|
+
return this.instances.get(profileId);
|
|
259
|
+
}
|
|
260
|
+
// Nếu chưa chạy, start nó
|
|
261
|
+
return await this.startProfile(profileId, windowConfig);
|
|
262
|
+
}
|
|
172
263
|
}
|
|
173
|
-
BrowserManager.
|
|
264
|
+
BrowserManager.instances = new Map();
|
|
174
265
|
exports.BrowserManager = BrowserManager;
|
|
@@ -6,6 +6,7 @@ export declare class ProfileManager {
|
|
|
6
6
|
private getProfileMetadataPath;
|
|
7
7
|
private getProfileDir;
|
|
8
8
|
createProfile(name: string, proxy?: string, note?: string, extensions?: string[]): ProfileData;
|
|
9
|
+
private setProfileName;
|
|
9
10
|
getProfile(profileId: string): ProfileData | null;
|
|
10
11
|
getAllProfiles(): ProfileData[];
|
|
11
12
|
updateProfile(profileId: string, updates: Partial<ProfileData>): ProfileData | null;
|
|
@@ -60,8 +60,66 @@ class ProfileManager {
|
|
|
60
60
|
// Lưu metadata
|
|
61
61
|
const metadataPath = this.getProfileMetadataPath(profileId);
|
|
62
62
|
fs.writeFileSync(metadataPath, JSON.stringify(profileData, null, 2), 'utf-8');
|
|
63
|
+
// Set profile name trong Chrome files ngay khi tạo
|
|
64
|
+
this.setProfileName(profileDir, name);
|
|
63
65
|
return profileData;
|
|
64
66
|
}
|
|
67
|
+
setProfileName(profilePath, profileName) {
|
|
68
|
+
try {
|
|
69
|
+
// Set profile name trong Local State file
|
|
70
|
+
const localStatePath = path.join(profilePath, 'Local State');
|
|
71
|
+
let localState = {};
|
|
72
|
+
if (fs.existsSync(localStatePath)) {
|
|
73
|
+
try {
|
|
74
|
+
const content = fs.readFileSync(localStatePath, 'utf-8');
|
|
75
|
+
localState = JSON.parse(content);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
// File exists but invalid JSON, start fresh
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Set profile name trong Chrome profile structure
|
|
82
|
+
if (!localState.profile) {
|
|
83
|
+
localState.profile = {};
|
|
84
|
+
}
|
|
85
|
+
localState.profile.name = profileName;
|
|
86
|
+
// Cũng set trong profile info
|
|
87
|
+
if (!localState.profile.info_cache) {
|
|
88
|
+
localState.profile.info_cache = {};
|
|
89
|
+
}
|
|
90
|
+
localState.profile.info_cache.Default = {
|
|
91
|
+
name: profileName,
|
|
92
|
+
is_using_default_name: false,
|
|
93
|
+
};
|
|
94
|
+
// Write back
|
|
95
|
+
fs.writeFileSync(localStatePath, JSON.stringify(localState, null, 2), 'utf-8');
|
|
96
|
+
// Tạo Default directory nếu chưa có
|
|
97
|
+
const defaultDir = path.join(profilePath, 'Default');
|
|
98
|
+
if (!fs.existsSync(defaultDir)) {
|
|
99
|
+
fs.mkdirSync(defaultDir, { recursive: true });
|
|
100
|
+
}
|
|
101
|
+
// Cũng set trong Preferences file
|
|
102
|
+
const prefsPath = path.join(defaultDir, 'Preferences');
|
|
103
|
+
let prefs = {};
|
|
104
|
+
if (fs.existsSync(prefsPath)) {
|
|
105
|
+
try {
|
|
106
|
+
const prefsContent = fs.readFileSync(prefsPath, 'utf-8');
|
|
107
|
+
prefs = JSON.parse(prefsContent);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
// Ignore
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (!prefs.profile) {
|
|
114
|
+
prefs.profile = {};
|
|
115
|
+
}
|
|
116
|
+
prefs.profile.name = profileName;
|
|
117
|
+
fs.writeFileSync(prefsPath, JSON.stringify(prefs, null, 2), 'utf-8');
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
// Ignore errors - profile name is optional
|
|
121
|
+
}
|
|
122
|
+
}
|
|
65
123
|
getProfile(profileId) {
|
|
66
124
|
const metadataPath = this.getProfileMetadataPath(profileId);
|
|
67
125
|
if (!fs.existsSync(metadataPath)) {
|