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.
@@ -22,7 +22,23 @@ exports.moveAndClickFields = [
22
22
  type: 'string',
23
23
  required: true,
24
24
  default: '',
25
- description: 'CSS selector of the element to click',
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: 0,
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: ['javascript'],
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: ['javascript'],
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: ['javascript'],
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 waitForClick = this.getNodeParameter('waitForClick', i) || 0;
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 instance = browserManager.getInstance(profileId);
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
- // Wait for element if needed
109
- await page.waitForSelector(selector, { timeout: 30000 });
114
+ // Set default timeout
115
+ page.setDefaultTimeout(timeout);
110
116
  if (clickMethod === 'javascript') {
111
- // Wait before clicking
112
- if (waitForClick > 0) {
113
- await page.waitForTimeout(waitForClick);
114
- }
117
+ // Use Javascript Click - simple implementation
118
+ await page.waitForSelector(selector, { timeout });
115
119
  // JavaScript click
116
- const buttonMap = {
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
- for (let i = 0; i < count; i++) {
127
- const event = new MouseEvent('click', {
128
- view: window,
129
- bubbles: true,
130
- cancelable: true,
131
- button: btn,
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, buttonMap[button] || 0, clickCount);
133
+ }, selector);
137
134
  }
138
135
  else if (clickMethod === 'puppeteer') {
139
- // Puppeteer native click
140
- await page.click(selector);
141
- }
142
- else if (clickMethod === 'ghostcursor') {
143
- // GhostCursor simulation (simplified - would need ghost-cursor package)
144
- const element = await page.$(selector);
145
- if (element) {
146
- const box = await element.boundingBox();
147
- if (box) {
148
- // Move mouse to center of element
149
- await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
150
- await page.waitForTimeout(100);
151
- await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
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 instance = browserManager.getInstance(profileId);
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 instance;
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?: string, profilesDir?: string): BrowserManager;
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
- if (!BrowserManager.instance) {
19
- if (!browserPath || !profilesDir) {
20
- throw new Error('BrowserManager must be initialized with browserPath and profilesDir');
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.instance;
49
+ return BrowserManager.instances.get(key);
25
50
  }
26
- static resetInstance() {
27
- BrowserManager.instance = null;
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.instance = null;
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)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-nvk-browser",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "description": "n8n nodes for managing Chrome browser profiles and page interactions",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",