ones-fetch 1.4.1 → 1.4.2

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/CHANGELOG.md ADDED
@@ -0,0 +1,21 @@
1
+ # 更新日志
2
+
3
+ ## [1.5.0] - 2024-04-03
4
+
5
+ ### 新增
6
+ - 页面加载时自动检测认证状态,无凭据时自动显示"连接 ONES"按钮
7
+ - 安装新版本时自动清理旧文件,同时保留用户凭据和依赖包
8
+
9
+ ### 修复
10
+ - 修复使用 playwright-core 后无法打开登录窗口的问题(现使用系统 Edge 浏览器)
11
+ - 修复 Windows 下 `start` 命令无法正确处理 URL 特殊字符的问题
12
+ - 修复前端"Assignment to constant variable"错误
13
+ - 修复解析按钮在无凭据时未置灰的问题
14
+
15
+ ### 优化
16
+ - 复制按钮固定宽度,避免文字变化时按钮大小改变
17
+ - 默认只解析任务编号(#数字格式),不再解析 URL 中的 UUID
18
+ - 改进错误提示信息,提供更清晰的浏览器安装指引
19
+
20
+ ## [1.4.1] - 之前版本
21
+ - 基础功能实现
package/README.md CHANGED
@@ -49,8 +49,7 @@ npm install
49
49
  ### 方式 1:双击启动(推荐给非技术用户)
50
50
 
51
51
  **Windows 用户:**
52
- - 双击 `启动 ONES 采集工具.vbs`
53
- - 首次运行会自动安装依赖
52
+ - 双击桌面 `ONES 采集工具.lnk`
54
53
  - 浏览器会自动打开工具页面
55
54
 
56
55
  ### 方式 2:命令行启动
@@ -151,6 +150,8 @@ npm run dev
151
150
 
152
151
  ```
153
152
  ones-fetch/
153
+ ├── bin/
154
+ │ └── install.mjs # 安装依赖及快捷方式
154
155
  ├── src/
155
156
  │ ├── server.mjs # HTTP 服务器和任务爬取逻辑
156
157
  │ └── auth.mjs # 浏览器登录和凭证管理
package/bin/install.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { homedir, platform } from 'node:os';
6
- import { writeFile, mkdir, cp, access } from 'node:fs/promises';
6
+ import { writeFile, mkdir, cp, access, rm, readFile } from 'node:fs/promises';
7
7
  import { exec } from 'node:child_process';
8
8
  import { promisify } from 'node:util';
9
9
 
@@ -17,9 +17,59 @@ const installDir = join(homedir(), '.ones-fetch');
17
17
  async function ensureInstallDir() {
18
18
  try {
19
19
  await access(installDir);
20
- console.log(`使用现有安装目录: ${installDir}`);
20
+ console.log(`检测到现有安装目录: ${installDir}`);
21
+
22
+ // 检查版本是否需要更新
23
+ let needsUpdate = false;
24
+ try {
25
+ const installedPkgPath = join(installDir, 'package.json');
26
+ const currentPkgPath = join(projectRoot, 'package.json');
27
+
28
+ const installedPkg = JSON.parse(await readFile(installedPkgPath, 'utf8'));
29
+ const currentPkg = JSON.parse(await readFile(currentPkgPath, 'utf8'));
30
+
31
+ if (installedPkg.version !== currentPkg.version) {
32
+ console.log(`检测到版本更新: ${installedPkg.version} → ${currentPkg.version}`);
33
+ needsUpdate = true;
34
+ }
35
+ } catch {
36
+ // 如果无法读取版本信息,假设需要更新
37
+ needsUpdate = true;
38
+ }
39
+
40
+ if (needsUpdate) {
41
+ console.log('正在清理旧版本文件...');
42
+ // 清理旧的项目文件,但保留 credentials.json 和 node_modules
43
+ const dirsToClean = ['src', 'public', 'bin'];
44
+ const filesToClean = ['package.json', 'package-lock.json'];
45
+
46
+ for (const dir of dirsToClean) {
47
+ try {
48
+ await rm(join(installDir, dir), { recursive: true, force: true });
49
+ } catch {}
50
+ }
51
+
52
+ for (const file of filesToClean) {
53
+ try {
54
+ await rm(join(installDir, file), { force: true });
55
+ } catch {}
56
+ }
57
+
58
+ console.log('✓ 旧版本文件清理完成');
59
+ console.log('正在复制新版本文件...');
60
+
61
+ // 复制新版本文件
62
+ await cp(join(projectRoot, 'src'), join(installDir, 'src'), { recursive: true });
63
+ await cp(join(projectRoot, 'public'), join(installDir, 'public'), { recursive: true });
64
+ await cp(join(projectRoot, 'bin'), join(installDir, 'bin'), { recursive: true });
65
+ await cp(join(projectRoot, 'package.json'), join(installDir, 'package.json'));
66
+ await cp(join(projectRoot, 'package-lock.json'), join(installDir, 'package-lock.json')).catch(() => {});
67
+ console.log('✓ 新版本文件复制完成');
68
+ } else {
69
+ console.log('版本已是最新,跳过文件更新');
70
+ }
21
71
  } catch {
22
- // 目录不存在,复制项目文件
72
+ // 目录不存在,首次安装
23
73
  console.log(`正在复制项目文件到 ${installDir}...`);
24
74
  await mkdir(installDir, { recursive: true });
25
75
 
@@ -78,7 +128,7 @@ async function createMacShortcut() {
78
128
  cd "${projectRoot}"
79
129
  node src/server.mjs &
80
130
  sleep 2
81
- open http://localhost:3000
131
+ open http://localhost:36781
82
132
  `;
83
133
 
84
134
  await writeFile(appPath, scriptContent, 'utf8');
@@ -94,7 +144,7 @@ async function createLinuxShortcut() {
94
144
  Version=1.0
95
145
  Type=Application
96
146
  Name=ONES 采集工具
97
- Exec=bash -c "cd ${projectRoot} && node src/server.mjs & sleep 2 && xdg-open http://localhost:3000"
147
+ Exec=bash -c "cd ${projectRoot} && node src/server.mjs & sleep 2 && xdg-open http://localhost:36781"
98
148
  Icon=utilities-terminal
99
149
  Terminal=false
100
150
  Categories=Utility;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ones-fetch",
3
- "version": "1.4.1",
3
+ "version": "1.4.2",
4
4
  "type": "module",
5
5
  "description": "ONES Fetch — Web app for recursive ONES subtask crawling",
6
6
  "bin": {
package/public/index.html CHANGED
@@ -23,6 +23,7 @@
23
23
  .btn-secondary:hover:not(:disabled) { background: #d1d5db; }
24
24
  .btn-success { background: #10b981; color: #fff; }
25
25
  .btn-success:hover:not(:disabled) { background: #059669; }
26
+ #copyBtn { min-width: 110px; }
26
27
  .btn-link { background: #f59e0b; color: #fff; }
27
28
  .btn-link:hover:not(:disabled) { background: #d97706; }
28
29
  .status { font-size: 0.8125rem; color: #6b7280; }
@@ -115,7 +116,7 @@ function startHeartbeat() {
115
116
  if (heartbeatTimer) clearInterval(heartbeatTimer);
116
117
  heartbeatTimer = setInterval(() => {
117
118
  fetch('/api/heartbeat').catch(() => {});
118
- }, 60000); // Every 60 seconds
119
+ }, 10000); // Every 10 seconds
119
120
  }
120
121
 
121
122
  // Stop heartbeat when page unloads
@@ -126,27 +127,56 @@ window.addEventListener('beforeunload', () => {
126
127
  // Start heartbeat on page load
127
128
  startHeartbeat();
128
129
 
130
+ // Check auth status on page load
131
+ async function checkInitialAuthStatus() {
132
+ try {
133
+ const resp = await fetch('/api/auth/status');
134
+ const data = await resp.json();
135
+
136
+ if (data.status === 'unauthenticated') {
137
+ updateAuthContext(data);
138
+ if (data.baseUrl) {
139
+ setAuthHint(`当前站点:${data.baseUrl}${data.teamId ? `,团队:${data.teamId}` : ''}`);
140
+ }
141
+ } else if (data.status === 'authenticated') {
142
+ updateAuthContext(data);
143
+ showAuthButton(false);
144
+ if (data.baseUrl) {
145
+ setAuthHint(`已连接:${data.baseUrl}${data.teamId ? `,团队:${data.teamId}` : ''}`);
146
+ }
147
+ }
148
+ } catch (e) {
149
+ // Ignore initial auth check errors
150
+ }
151
+ }
152
+
153
+ // Check auth status when page loads
154
+ checkInitialAuthStatus();
155
+
129
156
  function extractContext(text) {
130
157
  const match = text.match(/(https?:\/\/[^/\s]+)\/project\/#\/team\/([^/\s]+)\/task\/([A-Za-z0-9]{10,})/);
131
158
  if (!match) return { baseUrl: '', teamId: '' };
132
159
  return { baseUrl: match[1], teamId: match[2] };
133
160
  }
134
161
 
162
+ function getCurrentInputState() {
163
+ const text = document.getElementById('input').value.trim();
164
+ const taskIds = extractTaskIds(text);
165
+ const context = extractContext(text);
166
+ return { text, taskIds, context, hasBaseUrl: Boolean(context.baseUrl) };
167
+ }
168
+
135
169
  function extractTaskIds(text) {
136
- const uuids = [];
137
170
  const numbers = [];
138
171
 
139
- for (const match of text.matchAll(/\/task\/([A-Za-z0-9]{10,})/g)) {
140
- uuids.push(match[1]);
141
- }
142
-
172
+ // 只提取 #数字 格式的编号
143
173
  const textWithoutUrls = text.replace(/https?:\/\/[^\s]+/g, '');
144
174
  for (const match of textWithoutUrls.matchAll(/#(\d+)/g)) {
145
175
  numbers.push(match[1]);
146
176
  }
147
177
 
148
178
  const seen = new Set();
149
- return [...uuids, ...numbers].filter(id => {
179
+ return numbers.filter(id => {
150
180
  if (seen.has(id)) return false;
151
181
  seen.add(id);
152
182
  return true;
@@ -200,12 +230,24 @@ function updateProgress(current, total, message) {
200
230
  }
201
231
 
202
232
  async function parse() {
203
- const text = document.getElementById('input').value.trim();
233
+ const { text, taskIds, context, hasBaseUrl } = getCurrentInputState();
204
234
  if (!text) { setStatus('请先粘贴文本', true); return; }
205
- const taskIds = extractTaskIds(text);
206
- const context = extractContext(text);
207
235
  updateAuthContext(context);
208
- if (taskIds.length === 0) { setStatus('未找到任务 ID(支持 /task/UUID 链接或 #数字编号)', true); return; }
236
+
237
+ if (!hasBaseUrl) {
238
+ showAuthButton(false);
239
+ setAuthHint('');
240
+ setStatus('未检测到有效 ONES 地址,请粘贴正确的任务文本', true);
241
+ return;
242
+ }
243
+
244
+ if (taskIds.length === 0) {
245
+ showAuthButton(false);
246
+ setStatus('未找到任务 ID(支持 /task/UUID 链接或 #数字编号)', true);
247
+ setAuthHint(`已识别站点:${context.baseUrl}${context.teamId ? `,团队:${context.teamId}` : ''}`);
248
+ return;
249
+ }
250
+
209
251
  renderChips(taskIds);
210
252
  setStatus(`已提取 ${taskIds.length} 个任务 ID,正在抓取...`);
211
253
  setAuthHint(context.baseUrl ? `已识别站点:${context.baseUrl}${context.teamId ? `,团队:${context.teamId}` : ''}` : '');
@@ -216,7 +258,7 @@ async function parse() {
216
258
 
217
259
  try {
218
260
  // 逐个解析任务以显示进度
219
- const allTasks = [];
261
+ const fetchedTasks = [];
220
262
  const roots = [];
221
263
 
222
264
  for (let i = 0; i < taskIds.length; i++) {
@@ -236,7 +278,7 @@ async function parse() {
236
278
  if (data.error === 'auth_required' || data.error === 'token_expired') {
237
279
  updateAuthContext(data);
238
280
  showAuthButton(true, data.error === 'token_expired' ? '重新连接 ONES' : '连接 ONES');
239
- setAuthHint(`将打开一个浏览器窗口用于登录 ONES,完成后会自动重试。${authContext.baseUrl ? ` 当前站点:${authContext.baseUrl}` : ''}`);
281
+ setAuthHint(`已识别站点:${authContext.baseUrl}${authContext.teamId ? `,团队:${authContext.teamId}` : ''}。第一次解析前请先连接 ONES。`);
240
282
  }
241
283
  const msg = data.error === 'token_expired'
242
284
  ? '登录状态已过期,请点击”连接 ONES”重新登录'
@@ -250,12 +292,12 @@ async function parse() {
250
292
  }
251
293
 
252
294
  if (data.roots) roots.push(...data.roots);
253
- if (data.tasks) allTasks.push(...data.tasks);
295
+ if (data.tasks) fetchedTasks.push(...data.tasks);
254
296
  }
255
297
 
256
298
  showAuthButton(false);
257
299
  updateAuthContext({ baseUrl: authContext.baseUrl, teamId: authContext.teamId });
258
- allTasks = allTasks;
300
+ allTasks = fetchedTasks;
259
301
  collapsed = new Set();
260
302
  renderTable();
261
303
  setStatus(`共获取 ${allTasks.length} 条任务`);
@@ -270,13 +312,14 @@ async function parse() {
270
312
  }
271
313
 
272
314
  async function connectOnes() {
273
- const text = document.getElementById('input').value;
274
- const context = extractContext(text);
315
+ const { context, hasBaseUrl } = getCurrentInputState();
275
316
  updateAuthContext(context);
276
317
  const btn = document.getElementById('authBtn');
277
318
 
278
- if (!authContext.baseUrl) {
279
- setStatus('当前没有可用的 ONES 站点地址,请先粘贴任务链接或配置 ONES_BASE_URL', true);
319
+ if (!hasBaseUrl) {
320
+ showAuthButton(false);
321
+ setAuthHint('');
322
+ setStatus('未检测到有效 ONES 地址,请先粘贴正确的任务文本', true);
280
323
  return;
281
324
  }
282
325
 
@@ -287,7 +330,7 @@ async function connectOnes() {
287
330
  const resp = await fetch('/api/auth/login', {
288
331
  method: 'POST',
289
332
  headers: { 'Content-Type': 'application/json' },
290
- body: JSON.stringify({ baseUrl: authContext.baseUrl, teamId: authContext.teamId || undefined }),
333
+ body: JSON.stringify({ baseUrl: context.baseUrl, teamId: context.teamId || undefined }),
291
334
  });
292
335
  const data = await resp.json();
293
336
  if (!resp.ok) {
@@ -298,35 +341,45 @@ async function connectOnes() {
298
341
  }
299
342
 
300
343
  updateAuthContext(data);
301
- setAuthHint(`已打开 ${authContext.baseUrl} 的登录窗口,登录完成后将自动重试解析。`);
344
+ setAuthHint(`已打开 ${context.baseUrl} 的登录窗口,登录完成后将自动重试本次解析。`);
302
345
  startAuthPolling();
303
346
  }
304
347
 
305
348
  function startAuthPolling() {
306
349
  if (authPollingTimer) window.clearInterval(authPollingTimer);
307
350
  authPollingTimer = window.setInterval(async () => {
308
- const resp = await fetch('/api/auth/status');
309
- const data = await resp.json();
310
- if (data.status === 'pending') {
311
- showAuthButton(true, '等待登录完成...');
312
- document.getElementById('authBtn').disabled = true;
313
- return;
314
- }
351
+ try {
352
+ const resp = await fetch('/api/auth/status');
353
+ const data = await resp.json();
354
+ if (data.status === 'pending') {
355
+ showAuthButton(true, '等待登录完成...');
356
+ document.getElementById('authBtn').disabled = true;
357
+ return;
358
+ }
315
359
 
316
- window.clearInterval(authPollingTimer);
317
- authPollingTimer = null;
360
+ window.clearInterval(authPollingTimer);
361
+ authPollingTimer = null;
362
+
363
+ if (data.status === 'authenticated') {
364
+ updateAuthContext(data);
365
+ showAuthButton(false);
366
+ setStatus('ONES 已连接,正在重新解析...');
367
+ setAuthHint(`当前站点:${authContext.baseUrl}${authContext.teamId ? `,团队:${authContext.teamId}` : ''}`);
368
+ const { text, hasBaseUrl, taskIds } = getCurrentInputState();
369
+ if (text && hasBaseUrl && taskIds.length > 0) {
370
+ await parse();
371
+ }
372
+ return;
373
+ }
318
374
 
319
- if (data.status === 'authenticated') {
320
- updateAuthContext(data);
321
- showAuthButton(false);
322
- setStatus('ONES 已连接,正在重新解析...');
323
- setAuthHint(`当前站点:${authContext.baseUrl}${authContext.teamId ? `,团队:${authContext.teamId}` : ''}`);
324
- await parse();
325
- return;
375
+ showAuthButton(true, '连接 ONES');
376
+ setStatus(data.error === 'LOGIN_TIMEOUT' ? '登录超时,请重试' : '登录未完成,请重试', true);
377
+ } catch (e) {
378
+ window.clearInterval(authPollingTimer);
379
+ authPollingTimer = null;
380
+ showAuthButton(true, '连接 ONES');
381
+ setStatus('登录状态检查失败:' + e.message, true);
326
382
  }
327
-
328
- showAuthButton(true, '连接 ONES');
329
- setStatus(data.error === 'LOGIN_TIMEOUT' ? '登录超时,请重试' : '登录未完成,请重试', true);
330
383
  }, 1500);
331
384
  }
332
385
 
@@ -427,6 +480,10 @@ function clearAll() {
427
480
  document.getElementById('status').textContent = '';
428
481
  setAuthHint('');
429
482
  showAuthButton(false);
483
+ if (authPollingTimer) {
484
+ window.clearInterval(authPollingTimer);
485
+ authPollingTimer = null;
486
+ }
430
487
  updateProgress(0, 0, '');
431
488
  authContext = { baseUrl: '', teamId: '' };
432
489
  document.getElementById('resultsSection').style.display = 'none';
@@ -435,13 +492,12 @@ function clearAll() {
435
492
 
436
493
  // Auto-extract chips on input
437
494
  document.getElementById('input').addEventListener('input', () => {
438
- const text = document.getElementById('input').value;
439
- const taskIds = extractTaskIds(text);
440
- const context = extractContext(text);
495
+ const { taskIds, context, hasBaseUrl } = getCurrentInputState();
441
496
  updateAuthContext(context);
442
497
  renderChips(taskIds);
443
498
  setStatus(taskIds.length ? `检测到 ${taskIds.length} 个任务 ID` : '');
444
- setAuthHint(authContext.baseUrl ? `已识别站点:${authContext.baseUrl}${authContext.teamId ? `,团队:${authContext.teamId}` : ''}` : '');
499
+ showAuthButton(false);
500
+ setAuthHint(hasBaseUrl ? `已识别站点:${context.baseUrl}${context.teamId ? `,团队:${context.teamId}` : ''}` : '');
445
501
  });
446
502
  </script>
447
503
  </body>
package/src/auth.mjs CHANGED
@@ -70,16 +70,42 @@ async function extractSessionFromPage(page) {
70
70
  }
71
71
 
72
72
  export async function runBrowserLoginCapture({ baseUrl, timeoutMs = 300000, verbose = false }) {
73
- const browser = await chromium.launch({ headless: false });
73
+ const diag = (message) => {
74
+ if (!verbose) return;
75
+ process.stdout.write(`[ones-auth] ${message}\n`);
76
+ };
77
+
78
+ // Use system browser: Edge on Windows, Chrome on other platforms
79
+ const launchOptions = {
80
+ headless: false,
81
+ channel: process.platform === 'win32' ? 'msedge' : 'chrome',
82
+ };
83
+
84
+ let browser;
85
+ try {
86
+ browser = await chromium.launch(launchOptions);
87
+ } catch (error) {
88
+ // If system browser fails, try without channel (requires playwright browsers installed)
89
+ if (verbose) process.stderr.write(`System browser launch failed, trying bundled browser: ${error.message}\n`);
90
+ try {
91
+ browser = await chromium.launch({ headless: false });
92
+ } catch (fallbackError) {
93
+ throw new Error(`Failed to launch browser. Please ensure Microsoft Edge is installed, or run: npx playwright install chromium`);
94
+ }
95
+ }
96
+
74
97
  const context = await browser.newContext();
75
98
  const page = await context.newPage();
76
99
  const loginUrl = getLoginUrl(baseUrl);
100
+ diag(`start capture baseUrl=${baseUrl} loginUrl=${loginUrl} channel=${launchOptions.channel ?? 'bundled'}`);
77
101
 
78
102
  let resolved = false;
79
103
 
80
104
  return new Promise(async (resolve, reject) => {
81
105
  const finish = async (result, error) => {
82
106
  if (resolved) return;
107
+ const outcome = error ? `error=${error.message}` : `success userId=${result?.userId ?? 'unknown'} teamId=${result?.teamId ?? 'unknown'}`;
108
+ diag(`finish ${outcome}; closing browser`);
83
109
  try {
84
110
  if (error) throw error;
85
111
  const credentialsToSave = { ...result, baseUrl };
@@ -110,6 +136,7 @@ export async function runBrowserLoginCapture({ baseUrl, timeoutMs = 300000, verb
110
136
  // Cross-page transitions can temporarily break page evaluation.
111
137
  }
112
138
  if (session?.authToken && session?.userId) {
139
+ diag(`session detected from page url=${page.url()} userId=${session.userId} teamId=${session.teamId ?? 'unknown'}`);
113
140
  await finish(session, null);
114
141
  }
115
142
  };
@@ -128,6 +155,7 @@ export async function runBrowserLoginCapture({ baseUrl, timeoutMs = 300000, verb
128
155
  const teamMatch = currentUrl.match(/\/team\/([a-zA-Z0-9]{16,})/);
129
156
  if (teamMatch) teamId = teamMatch[1];
130
157
  } catch { /* ignore */ }
158
+ diag(`session detected from response url=${res.url()} userId=${userId} teamId=${teamId ?? 'unknown'}`);
131
159
  await finish({ authToken, userId, teamId }, null);
132
160
  return;
133
161
  }
@@ -138,10 +166,12 @@ export async function runBrowserLoginCapture({ baseUrl, timeoutMs = 300000, verb
138
166
  };
139
167
 
140
168
  const onNavigation = () => {
169
+ diag(`navigated url=${page.url()}`);
141
170
  void tryPageSession();
142
171
  };
143
172
 
144
173
  const timeout = setTimeout(() => {
174
+ diag(`timeout after ${timeoutMs}ms`);
145
175
  void finish(null, new Error("LOGIN_TIMEOUT"));
146
176
  }, timeoutMs);
147
177
 
@@ -153,10 +183,11 @@ export async function runBrowserLoginCapture({ baseUrl, timeoutMs = 300000, verb
153
183
  page.on("framenavigated", onNavigation);
154
184
 
155
185
  try {
156
- if (verbose) process.stderr.write(`Opening browser login at ${loginUrl}\n`);
186
+ diag(`goto ${loginUrl}`);
157
187
  await page.goto(loginUrl, { waitUntil: "domcontentloaded", timeout: 60000 });
158
188
  await tryPageSession();
159
189
  } catch (error) {
190
+ diag(`goto failed error=${error instanceof Error ? error.message : String(error)}`);
160
191
  await finish(null, error instanceof Error ? error : new Error(String(error)));
161
192
  }
162
193
  });
package/src/server.mjs CHANGED
@@ -7,11 +7,19 @@ import { exec } from 'node:child_process';
7
7
 
8
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
9
9
  const PUBLIC_DIR = join(__dirname, '..', 'public');
10
- const PORT = process.env.PORT ?? 3000;
10
+ const PORT = process.env.PORT ?? 36781;
11
11
 
12
12
  function openBrowser(url) {
13
- const start = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
14
- exec(`${start} ${url}`);
13
+ let command;
14
+ if (process.platform === 'darwin') {
15
+ command = `open "${url}"`;
16
+ } else if (process.platform === 'win32') {
17
+ // On Windows, use 'start ""' with quoted URL to handle special characters
18
+ command = `start "" "${url}"`;
19
+ } else {
20
+ command = `xdg-open "${url}"`;
21
+ }
22
+ exec(command);
15
23
  }
16
24
 
17
25
  const authFlow = {
@@ -296,7 +304,7 @@ async function handleAuthLogin(req, res) {
296
304
  void (async () => {
297
305
  try {
298
306
  const { runBrowserLoginCapture } = await import('./auth.mjs');
299
- await runBrowserLoginCapture({ baseUrl: context.baseUrl });
307
+ await runBrowserLoginCapture({ baseUrl: context.baseUrl, verbose: true });
300
308
  authFlow.status = 'idle';
301
309
  authFlow.error = null;
302
310
  authFlow.baseUrl = '';
@@ -367,14 +375,14 @@ async function handleCrawl(req, res) {
367
375
 
368
376
  // Heartbeat tracking
369
377
  let lastHeartbeat = Date.now();
370
- const HEARTBEAT_TIMEOUT = 2 * 60 * 1000; // 2 minutes
371
- const HEARTBEAT_CHECK_INTERVAL = 30 * 1000; // 30 seconds
378
+ const HEARTBEAT_TIMEOUT = 30 * 1000; // 30 seconds
379
+ const HEARTBEAT_CHECK_INTERVAL = 10 * 1000; // 10 seconds
372
380
 
373
381
  function startHeartbeatMonitor() {
374
382
  setInterval(() => {
375
383
  const now = Date.now();
376
384
  if (now - lastHeartbeat > HEARTBEAT_TIMEOUT) {
377
- process.stdout.write('No heartbeat received for 5 minutes, shutting down server...\n');
385
+ process.stdout.write('No heartbeat received for 30 seconds, shutting down server...\n');
378
386
  process.exit(0);
379
387
  }
380
388
  }, HEARTBEAT_CHECK_INTERVAL);