@doppelgangerdev/doppelganger 0.4.3 → 0.5.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.
Files changed (57) hide show
  1. package/LICENSE +162 -162
  2. package/README.md +39 -37
  3. package/agent.js +342 -130
  4. package/dist/assets/index-BXRKDZ1_.css +1 -0
  5. package/dist/assets/index-Deb2QMGx.js +19 -0
  6. package/dist/captures/run_1769042882403_589_agent_1769042884446_initial.png +0 -0
  7. package/dist/captures/run_1769042882403_589_agent_1769042887058.png +0 -0
  8. package/dist/captures/run_1769042882403_589_agent_1769042888468.webm +0 -0
  9. package/dist/captures/run_1769043202318_943_agent_1769043206237.png +0 -0
  10. package/dist/captures/run_1769043202318_943_agent_1769043207415.webm +0 -0
  11. package/dist/captures/run_1769043449517_97_agent_1769043451350_initial.png +0 -0
  12. package/dist/captures/run_1769043449517_97_agent_1769043455038.png +0 -0
  13. package/dist/captures/run_1769043449517_97_agent_1769043456476.webm +0 -0
  14. package/dist/captures/run_1769043471164_239_agent_1769043472720_initial.png +0 -0
  15. package/dist/captures/run_1769043471164_239_agent_1769043474022.png +0 -0
  16. package/dist/captures/run_1769043471164_239_agent_1769043476419.png +0 -0
  17. package/dist/captures/run_1769043471164_239_agent_1769043477795.webm +0 -0
  18. package/dist/captures/run_1769080585290_151_agent_1769080595110.png +0 -0
  19. package/dist/captures/run_1769080585290_151_agent_1769080596335.webm +0 -0
  20. package/dist/index.html +2 -2
  21. package/dist/screenshots/agent_1769037343598.png +0 -0
  22. package/dist/screenshots/agent_1769037357541.png +0 -0
  23. package/dist/screenshots/scrape_1769037382254.png +0 -0
  24. package/dist/screenshots/scrape_1769037413189.png +0 -0
  25. package/dist/screenshots/scrape_1769037449707.png +0 -0
  26. package/dist/screenshots/scrape_1769037461756.png +0 -0
  27. package/dist/screenshots/scrape_1769037490581.png +0 -0
  28. package/dist/screenshots/scrape_1769038242368.png +0 -0
  29. package/headful.js +76 -21
  30. package/package.json +3 -1
  31. package/proxy-rotation.js +133 -90
  32. package/public/captures/run_1769042882403_589_agent_1769042884446_initial.png +0 -0
  33. package/public/captures/run_1769042882403_589_agent_1769042887058.png +0 -0
  34. package/public/captures/run_1769042882403_589_agent_1769042888468.webm +0 -0
  35. package/public/captures/run_1769043202318_943_agent_1769043206237.png +0 -0
  36. package/public/captures/run_1769043202318_943_agent_1769043207415.webm +0 -0
  37. package/public/captures/run_1769043449517_97_agent_1769043451350_initial.png +0 -0
  38. package/public/captures/run_1769043449517_97_agent_1769043455038.png +0 -0
  39. package/public/captures/run_1769043449517_97_agent_1769043456476.webm +0 -0
  40. package/public/captures/run_1769043471164_239_agent_1769043472720_initial.png +0 -0
  41. package/public/captures/run_1769043471164_239_agent_1769043474022.png +0 -0
  42. package/public/captures/run_1769043471164_239_agent_1769043476419.png +0 -0
  43. package/public/captures/run_1769043471164_239_agent_1769043477795.webm +0 -0
  44. package/public/captures/run_1769080585290_151_agent_1769080595110.png +0 -0
  45. package/public/captures/run_1769080585290_151_agent_1769080596335.webm +0 -0
  46. package/public/screenshots/agent_1769037343598.png +0 -0
  47. package/public/screenshots/agent_1769037357541.png +0 -0
  48. package/public/screenshots/scrape_1769037382254.png +0 -0
  49. package/public/screenshots/scrape_1769037413189.png +0 -0
  50. package/public/screenshots/scrape_1769037449707.png +0 -0
  51. package/public/screenshots/scrape_1769037461756.png +0 -0
  52. package/public/screenshots/scrape_1769037490581.png +0 -0
  53. package/public/screenshots/scrape_1769038242368.png +0 -0
  54. package/scrape.js +163 -66
  55. package/server.js +127 -72
  56. package/dist/assets/index-D68YZVOp.js +0 -19
  57. package/dist/assets/index-WbwoTnJa.css +0 -1
package/dist/index.html CHANGED
@@ -11,8 +11,8 @@
11
11
  <link
12
12
  href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap"
13
13
  rel="stylesheet">
14
- <script type="module" crossorigin src="/assets/index-D68YZVOp.js"></script>
15
- <link rel="stylesheet" crossorigin href="/assets/index-WbwoTnJa.css">
14
+ <script type="module" crossorigin src="/assets/index-Deb2QMGx.js"></script>
15
+ <link rel="stylesheet" crossorigin href="/assets/index-BXRKDZ1_.css">
16
16
  </head>
17
17
 
18
18
  <body class="bg-[#020202] text-gray-100 font-sans h-full overflow-hidden selection:bg-white selection:text-black">
package/headful.js CHANGED
@@ -1,7 +1,8 @@
1
1
  const { chromium } = require('playwright');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
- const { getProxySelection } = require('./proxy-rotation');
4
+ const { getProxySelection } = require('./proxy-rotation');
5
+ const { selectUserAgent } = require('./user-agent-settings');
5
6
 
6
7
  const STORAGE_STATE_PATH = path.join(__dirname, 'storage_state.json');
7
8
  const STORAGE_STATE_FILE = (() => {
@@ -16,12 +17,6 @@ const STORAGE_STATE_FILE = (() => {
16
17
  return STORAGE_STATE_PATH;
17
18
  })();
18
19
 
19
- // Use a consistent User Agent or the same pool
20
- const userAgents = [
21
- 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
22
- 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'
23
- ];
24
-
25
20
  let activeSession = null;
26
21
 
27
22
  const teardownActiveSession = async () => {
@@ -53,8 +48,7 @@ async function handleHeadful(req, res) {
53
48
  const rotateProxiesRaw = req.body.rotateProxies ?? req.query.rotateProxies;
54
49
  const rotateProxies = String(rotateProxiesRaw).toLowerCase() === 'true' || rotateProxiesRaw === true;
55
50
 
56
- // We stick to the first UA in the list for headful mode to ensure consistency
57
- const selectedUA = userAgents[0];
51
+ const selectedUA = selectUserAgent(false);
58
52
 
59
53
  console.log(`Opening headful browser for: ${url}`);
60
54
 
@@ -89,18 +83,79 @@ async function handleHeadful(req, res) {
89
83
  contextOptions.storageState = STORAGE_STATE_FILE;
90
84
  }
91
85
 
92
- const context = await browser.newContext(contextOptions);
93
- await context.addInitScript(() => {
94
- window.open = () => null;
95
- document.addEventListener('click', (event) => {
96
- const target = event.target;
97
- const anchor = target && target.closest ? target.closest('a[target="_blank"]') : null;
98
- if (anchor) {
99
- event.preventDefault();
100
- }
101
- }, true);
102
- });
103
- const page = await context.newPage();
86
+ const context = await browser.newContext(contextOptions);
87
+ await context.addInitScript(() => {
88
+ window.open = () => null;
89
+ document.addEventListener('click', (event) => {
90
+ const target = event.target;
91
+ const anchor = target && target.closest ? target.closest('a[target="_blank"]') : null;
92
+ if (anchor) {
93
+ event.preventDefault();
94
+ }
95
+ }, true);
96
+ });
97
+ await context.addInitScript(() => {
98
+ const cursorId = 'dg-cursor-overlay';
99
+ const dotId = 'dg-click-dot';
100
+ if (document.getElementById(cursorId)) return;
101
+ const cursor = document.createElement('div');
102
+ cursor.id = cursorId;
103
+ cursor.style.cssText = [
104
+ 'position:fixed',
105
+ 'top:0',
106
+ 'left:0',
107
+ 'width:18px',
108
+ 'height:18px',
109
+ 'margin-left:-9px',
110
+ 'margin-top:-9px',
111
+ 'border:2px solid rgba(56,189,248,0.7)',
112
+ 'background:rgba(56,189,248,0.25)',
113
+ 'border-radius:50%',
114
+ 'box-shadow:0 0 10px rgba(56,189,248,0.6)',
115
+ 'pointer-events:none',
116
+ 'z-index:2147483647',
117
+ 'transform:translate3d(0,0,0)',
118
+ 'transition:transform 60ms ease-out'
119
+ ].join(';');
120
+ const dot = document.createElement('div');
121
+ dot.id = dotId;
122
+ dot.style.cssText = [
123
+ 'position:fixed',
124
+ 'top:0',
125
+ 'left:0',
126
+ 'width:10px',
127
+ 'height:10px',
128
+ 'margin-left:-5px',
129
+ 'margin-top:-5px',
130
+ 'background:rgba(239,68,68,0.9)',
131
+ 'border-radius:50%',
132
+ 'box-shadow:0 0 12px rgba(239,68,68,0.8)',
133
+ 'pointer-events:none',
134
+ 'z-index:2147483647',
135
+ 'opacity:0',
136
+ 'transform:translate3d(0,0,0) scale(0.6)',
137
+ 'transition:opacity 120ms ease, transform 120ms ease'
138
+ ].join(';');
139
+ document.documentElement.appendChild(cursor);
140
+ document.documentElement.appendChild(dot);
141
+ const move = (x, y) => {
142
+ cursor.style.transform = `translate3d(${x}px, ${y}px, 0)`;
143
+ };
144
+ window.addEventListener('mousemove', (e) => move(e.clientX, e.clientY), { passive: true });
145
+ window.addEventListener('click', (e) => {
146
+ dot.style.left = `${e.clientX}px`;
147
+ dot.style.top = `${e.clientY}px`;
148
+ dot.style.opacity = '1';
149
+ dot.style.transform = 'translate3d(0,0,0) scale(1)';
150
+ cursor.style.transform = `translate3d(${e.clientX}px, ${e.clientY}px, 0) scale(0.65)`;
151
+ setTimeout(() => {
152
+ dot.style.opacity = '0';
153
+ dot.style.transform = 'translate3d(0,0,0) scale(0.6)';
154
+ cursor.style.transform = `translate3d(${e.clientX}px, ${e.clientY}px, 0) scale(1)`;
155
+ }, 180);
156
+ }, true);
157
+ });
158
+ const page = await context.newPage();
104
159
 
105
160
  const closeIfExtra = async (extraPage) => {
106
161
  if (!extraPage || extraPage === page) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doppelgangerdev/doppelganger",
3
- "version": "0.4.3",
3
+ "version": "0.5.3",
4
4
  "main": "index.js",
5
5
  "bin": {
6
6
  "doppelganger": "bin/cli.js"
@@ -46,6 +46,7 @@
46
46
  "react": "^19.2.3",
47
47
  "react-dom": "^19.2.3",
48
48
  "react-router-dom": "^7.11.0",
49
+ "react-window": "^1.8.8",
49
50
  "session-file-store": "^1.5.0"
50
51
  },
51
52
  "devDependencies": {
@@ -53,6 +54,7 @@
53
54
  "@types/node": "^25.0.3",
54
55
  "@types/react": "^19.2.7",
55
56
  "@types/react-dom": "^19.2.3",
57
+ "@types/react-window": "^1.8.8",
56
58
  "@vitejs/plugin-react": "^5.1.2",
57
59
  "autoprefixer": "^10.4.23",
58
60
  "cross-env": "^10.1.0",
package/proxy-rotation.js CHANGED
@@ -8,12 +8,14 @@ const PROXY_FILES = [
8
8
  path.join(__dirname, 'proxies.json')
9
9
  ];
10
10
 
11
- let cached = {
12
- file: null,
13
- mtimeMs: 0,
14
- config: { proxies: [], defaultProxyId: null, includeDefaultInRotation: false }
15
- };
16
- let rotationIndex = 0;
11
+ const ROTATION_MODES = new Set(['round-robin', 'random']);
12
+
13
+ let cached = {
14
+ file: null,
15
+ mtimeMs: 0,
16
+ config: { proxies: [], defaultProxyId: null, includeDefaultInRotation: false, rotationMode: 'round-robin' }
17
+ };
18
+ let rotationIndex = 0;
17
19
 
18
20
  const normalizeServer = (raw) => {
19
21
  if (!raw) return '';
@@ -71,21 +73,27 @@ const normalizeProxy = (entry) => {
71
73
  return null;
72
74
  };
73
75
 
74
- const loadProxyFile = (filePath) => {
75
- try {
76
- const raw = fs.readFileSync(filePath, 'utf8');
77
- const parsed = JSON.parse(raw);
78
- if (Array.isArray(parsed)) {
79
- return { proxies: parsed, defaultProxyId: null, includeDefaultInRotation: false };
80
- }
81
- const proxies = Array.isArray(parsed.proxies) ? parsed.proxies : [];
82
- const defaultProxyId = parsed.defaultProxyId || null;
83
- const includeDefaultInRotation = !!parsed.includeDefaultInRotation;
84
- return { proxies, defaultProxyId, includeDefaultInRotation };
85
- } catch {
86
- return { proxies: [], defaultProxyId: null, includeDefaultInRotation: false };
87
- }
88
- };
76
+ const normalizeRotationMode = (mode) => {
77
+ if (ROTATION_MODES.has(mode)) return mode;
78
+ return 'round-robin';
79
+ };
80
+
81
+ const loadProxyFile = (filePath) => {
82
+ try {
83
+ const raw = fs.readFileSync(filePath, 'utf8');
84
+ const parsed = JSON.parse(raw);
85
+ if (Array.isArray(parsed)) {
86
+ return { proxies: parsed, defaultProxyId: null, includeDefaultInRotation: false, rotationMode: 'round-robin' };
87
+ }
88
+ const proxies = Array.isArray(parsed.proxies) ? parsed.proxies : [];
89
+ const defaultProxyId = parsed.defaultProxyId || null;
90
+ const includeDefaultInRotation = !!parsed.includeDefaultInRotation;
91
+ const rotationMode = normalizeRotationMode(parsed.rotationMode);
92
+ return { proxies, defaultProxyId, includeDefaultInRotation, rotationMode };
93
+ } catch {
94
+ return { proxies: [], defaultProxyId: null, includeDefaultInRotation: false, rotationMode: 'round-robin' };
95
+ }
96
+ };
89
97
 
90
98
  const loadProxyConfig = () => {
91
99
  const filePath = PROXY_FILES.find((candidate) => {
@@ -97,9 +105,9 @@ const loadProxyConfig = () => {
97
105
  });
98
106
 
99
107
  if (!filePath) {
100
- cached = { file: null, mtimeMs: 0, config: { proxies: [], defaultProxyId: null, includeDefaultInRotation: false } };
101
- return cached.config;
102
- }
108
+ cached = { file: null, mtimeMs: 0, config: { proxies: [], defaultProxyId: null, includeDefaultInRotation: false, rotationMode: 'round-robin' } };
109
+ return cached.config;
110
+ }
103
111
 
104
112
  try {
105
113
  const stat = fs.statSync(filePath);
@@ -112,26 +120,28 @@ const loadProxyConfig = () => {
112
120
  const defaultProxyId = rawConfig.defaultProxyId && proxies.some((proxy) => proxy.id === rawConfig.defaultProxyId)
113
121
  ? rawConfig.defaultProxyId
114
122
  : null;
115
- const config = {
116
- proxies,
117
- defaultProxyId,
118
- includeDefaultInRotation: !!rawConfig.includeDefaultInRotation
119
- };
120
- cached = { file: filePath, mtimeMs, config };
121
- return config;
122
- } catch {
123
- cached = { file: filePath, mtimeMs: 0, config: { proxies: [], defaultProxyId: null, includeDefaultInRotation: false } };
124
- return cached.config;
125
- }
126
- };
127
-
128
- const saveProxyConfig = (config) => {
129
- const target = DATA_PROXY_FILE;
130
- const payload = {
131
- defaultProxyId: config.defaultProxyId || null,
132
- proxies: Array.isArray(config.proxies) ? config.proxies : [],
133
- includeDefaultInRotation: !!config.includeDefaultInRotation
134
- };
123
+ const config = {
124
+ proxies,
125
+ defaultProxyId,
126
+ includeDefaultInRotation: !!rawConfig.includeDefaultInRotation,
127
+ rotationMode: normalizeRotationMode(rawConfig.rotationMode)
128
+ };
129
+ cached = { file: filePath, mtimeMs, config };
130
+ return config;
131
+ } catch {
132
+ cached = { file: filePath, mtimeMs: 0, config: { proxies: [], defaultProxyId: null, includeDefaultInRotation: false, rotationMode: 'round-robin' } };
133
+ return cached.config;
134
+ }
135
+ };
136
+
137
+ const saveProxyConfig = (config) => {
138
+ const target = DATA_PROXY_FILE;
139
+ const payload = {
140
+ defaultProxyId: config.defaultProxyId || null,
141
+ proxies: Array.isArray(config.proxies) ? config.proxies : [],
142
+ includeDefaultInRotation: !!config.includeDefaultInRotation,
143
+ rotationMode: normalizeRotationMode(config.rotationMode)
144
+ };
135
145
  fs.writeFileSync(target, JSON.stringify(payload, null, 2));
136
146
  try {
137
147
  const stat = fs.statSync(target);
@@ -149,12 +159,13 @@ const listProxies = () => {
149
159
  server: 'host_ip',
150
160
  label: 'Host IP (no proxy)'
151
161
  };
152
- return {
153
- proxies: [hostEntry, ...(config.proxies || [])],
154
- defaultProxyId: config.defaultProxyId || 'host',
155
- includeDefaultInRotation: !!config.includeDefaultInRotation
156
- };
157
- };
162
+ return {
163
+ proxies: [hostEntry, ...(config.proxies || [])],
164
+ defaultProxyId: config.defaultProxyId || 'host',
165
+ includeDefaultInRotation: !!config.includeDefaultInRotation,
166
+ rotationMode: normalizeRotationMode(config.rotationMode)
167
+ };
168
+ };
158
169
 
159
170
  const addProxy = (entry) => {
160
171
  const normalized = normalizeProxy(entry);
@@ -170,11 +181,32 @@ const addProxies = (entries) => {
170
181
  const normalizedEntries = entries.map(normalizeProxy).filter(Boolean);
171
182
  if (normalizedEntries.length === 0) return null;
172
183
  const config = loadProxyConfig();
173
- const additions = normalizedEntries.map((proxy) => ({
174
- ...proxy,
175
- id: `proxy_${crypto.randomBytes(6).toString('hex')}`
176
- }));
177
- const proxies = [...config.proxies, ...additions];
184
+ const existingByServer = new Map(
185
+ config.proxies.map((proxy) => [String(proxy.server || '').toLowerCase(), proxy])
186
+ );
187
+ const seenServers = new Set();
188
+ const updates = [];
189
+ const additions = [];
190
+
191
+ normalizedEntries.forEach((proxy) => {
192
+ const serverKey = String(proxy.server || '').toLowerCase();
193
+ if (!serverKey || seenServers.has(serverKey)) return;
194
+ seenServers.add(serverKey);
195
+ const existing = existingByServer.get(serverKey);
196
+ if (existing) {
197
+ updates.push({ ...existing, ...proxy, id: existing.id });
198
+ } else {
199
+ additions.push({ ...proxy, id: `proxy_${crypto.randomBytes(6).toString('hex')}` });
200
+ }
201
+ });
202
+
203
+ const merged = config.proxies.map((proxy) => {
204
+ const serverKey = String(proxy.server || '').toLowerCase();
205
+ const replacement = updates.find((item) => String(item.server || '').toLowerCase() === serverKey);
206
+ return replacement || proxy;
207
+ });
208
+
209
+ const proxies = [...merged, ...additions];
178
210
  const next = { ...config, proxies };
179
211
  return saveProxyConfig(next);
180
212
  };
@@ -192,13 +224,13 @@ const updateProxy = (id, entry) => {
192
224
  return saveProxyConfig({ ...config, proxies });
193
225
  };
194
226
 
195
- const deleteProxy = (id) => {
196
- if (!id) return null;
197
- const config = loadProxyConfig();
198
- const proxies = config.proxies.filter((proxy) => proxy.id !== id);
199
- const defaultProxyId = config.defaultProxyId === id ? null : config.defaultProxyId;
200
- return saveProxyConfig({ proxies, defaultProxyId });
201
- };
227
+ const deleteProxy = (id) => {
228
+ if (!id) return null;
229
+ const config = loadProxyConfig();
230
+ const proxies = config.proxies.filter((proxy) => proxy.id !== id);
231
+ const defaultProxyId = config.defaultProxyId === id ? null : config.defaultProxyId;
232
+ return saveProxyConfig({ ...config, proxies, defaultProxyId });
233
+ };
202
234
 
203
235
  const setDefaultProxy = (id) => {
204
236
  const config = loadProxyConfig();
@@ -209,17 +241,26 @@ const setDefaultProxy = (id) => {
209
241
  return saveProxyConfig({ ...config, defaultProxyId: id });
210
242
  };
211
243
 
212
- const setIncludeDefaultInRotation = (enabled) => {
213
- const config = loadProxyConfig();
214
- return saveProxyConfig({ ...config, includeDefaultInRotation: !!enabled });
215
- };
216
-
217
- const getNextProxy = (proxies) => {
218
- if (!proxies.length) return null;
219
- const selected = proxies[rotationIndex % proxies.length];
220
- rotationIndex += 1;
221
- return selected;
222
- };
244
+ const setIncludeDefaultInRotation = (enabled) => {
245
+ const config = loadProxyConfig();
246
+ return saveProxyConfig({ ...config, includeDefaultInRotation: !!enabled });
247
+ };
248
+
249
+ const setRotationMode = (mode) => {
250
+ const config = loadProxyConfig();
251
+ return saveProxyConfig({ ...config, rotationMode: normalizeRotationMode(mode) });
252
+ };
253
+
254
+ const getNextProxy = (proxies, mode) => {
255
+ if (!proxies.length) return null;
256
+ if (mode === 'random') {
257
+ const index = Math.floor(Math.random() * proxies.length);
258
+ return proxies[index];
259
+ }
260
+ const selected = proxies[rotationIndex % proxies.length];
261
+ rotationIndex += 1;
262
+ return selected;
263
+ };
223
264
 
224
265
  const getProxySelection = (rotateProxies) => {
225
266
  const config = loadProxyConfig();
@@ -229,24 +270,25 @@ const getProxySelection = (rotateProxies) => {
229
270
  const defaultProxy = config.defaultProxyId
230
271
  ? proxies.find((proxy) => proxy.id === config.defaultProxyId) || null
231
272
  : null;
232
- const defaultIsHost = !config.defaultProxyId;
233
- const includeDefaultInRotation = !!config.includeDefaultInRotation;
234
-
235
- if (rotateProxies) {
236
- let rotationPool = pool;
237
- if (!includeDefaultInRotation) {
238
- if (defaultIsHost) {
273
+ const defaultIsHost = !config.defaultProxyId;
274
+ const includeDefaultInRotation = !!config.includeDefaultInRotation;
275
+ const rotationMode = normalizeRotationMode(config.rotationMode);
276
+
277
+ if (rotateProxies) {
278
+ let rotationPool = pool;
279
+ if (!includeDefaultInRotation) {
280
+ if (defaultIsHost) {
239
281
  rotationPool = pool.filter((proxy) => proxy.id !== 'host');
240
282
  } else {
241
- rotationPool = pool.filter((proxy) => proxy.id !== config.defaultProxyId);
242
- }
243
- }
244
- if (rotationPool.length > 0) {
245
- const picked = getNextProxy(rotationPool);
246
- return { proxy: picked && picked.id !== 'host' ? picked : null, mode: 'rotate' };
247
- }
248
- if (defaultProxy) return { proxy: defaultProxy, mode: 'default' };
249
- return { proxy: null, mode: 'host' };
283
+ rotationPool = pool.filter((proxy) => proxy.id !== config.defaultProxyId);
284
+ }
285
+ }
286
+ if (rotationPool.length > 0) {
287
+ const picked = getNextProxy(rotationPool, rotationMode);
288
+ return { proxy: picked && picked.id !== 'host' ? picked : null, mode: 'rotate' };
289
+ }
290
+ if (defaultProxy) return { proxy: defaultProxy, mode: 'default' };
291
+ return { proxy: null, mode: 'host' };
250
292
  }
251
293
 
252
294
  if (defaultProxy) return { proxy: defaultProxy, mode: 'default' };
@@ -261,5 +303,6 @@ module.exports = {
261
303
  updateProxy,
262
304
  deleteProxy,
263
305
  setDefaultProxy,
264
- setIncludeDefaultInRotation
306
+ setIncludeDefaultInRotation,
307
+ setRotationMode
265
308
  };