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 +21 -0
- package/README.md +3 -2
- package/bin/install.mjs +55 -5
- package/package.json +1 -1
- package/public/index.html +100 -44
- package/src/auth.mjs +33 -2
- package/src/server.mjs +15 -7
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
|
-
-
|
|
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(
|
|
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:
|
|
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:
|
|
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
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
|
-
},
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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(
|
|
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)
|
|
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 =
|
|
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
|
|
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 (!
|
|
279
|
-
|
|
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:
|
|
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(`已打开 ${
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
317
|
-
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 ??
|
|
10
|
+
const PORT = process.env.PORT ?? 36781;
|
|
11
11
|
|
|
12
12
|
function openBrowser(url) {
|
|
13
|
-
|
|
14
|
-
|
|
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 =
|
|
371
|
-
const HEARTBEAT_CHECK_INTERVAL =
|
|
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
|
|
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);
|