viruagent-cli 0.3.4 → 0.3.5
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/README.ko.md +21 -5
- package/README.md +21 -5
- package/package.json +1 -1
- package/skills/viruagent.md +100 -6
- package/src/providers/naver/auth.js +180 -0
- package/src/providers/naver/chromeImport.js +195 -0
- package/src/providers/naver/editorConvert.js +198 -0
- package/src/providers/naver/imageUpload.js +111 -0
- package/src/providers/naver/index.js +253 -0
- package/src/providers/naver/selectors.js +23 -0
- package/src/providers/naver/session.js +119 -0
- package/src/providers/naver/utils.js +56 -0
- package/src/providers/tistory/chromeImport.js +86 -41
- package/src/services/naverApiClient.js +493 -0
- package/src/services/providerManager.js +1 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const readNaverCredentials = () => {
|
|
2
|
+
const username = process.env.NAVER_USERNAME || process.env.NAVER_USER || process.env.NAVER_ID;
|
|
3
|
+
const password = process.env.NAVER_PASSWORD || process.env.NAVER_PW;
|
|
4
|
+
return {
|
|
5
|
+
username: typeof username === 'string' && username.trim() ? username.trim() : null,
|
|
6
|
+
password: typeof password === 'string' && password.trim() ? password.trim() : null,
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const parseNaverSessionError = (error) => {
|
|
11
|
+
const message = String(error?.message || '').toLowerCase();
|
|
12
|
+
return [
|
|
13
|
+
'세션 파일이 없습니다',
|
|
14
|
+
'세션에 유효한 쿠키',
|
|
15
|
+
'세션이 만료',
|
|
16
|
+
'로그인이 필요합니다',
|
|
17
|
+
'blogid를 찾을 수 없습니다',
|
|
18
|
+
'블로그 정보 조회 실패',
|
|
19
|
+
'다시 로그인',
|
|
20
|
+
'401',
|
|
21
|
+
'403',
|
|
22
|
+
].some((token) => message.includes(token.toLowerCase()));
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const buildLoginErrorMessage = (error) => String(error?.message || '세션 검증에 실패했습니다.');
|
|
26
|
+
|
|
27
|
+
const normalizeNaverTagList = (value = '') => {
|
|
28
|
+
const source = Array.isArray(value)
|
|
29
|
+
? value
|
|
30
|
+
: String(value || '').replace(/\r?\n/g, ',').split(',');
|
|
31
|
+
return source
|
|
32
|
+
.map((tag) => String(tag || '').trim())
|
|
33
|
+
.filter(Boolean)
|
|
34
|
+
.map((tag) => tag.replace(/["']/g, '').trim())
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.slice(0, 10)
|
|
37
|
+
.join(',');
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const mapNaverVisibility = (visibility) => {
|
|
41
|
+
const normalized = String(visibility || 'public').toLowerCase();
|
|
42
|
+
if (normalized === 'private') return 0;
|
|
43
|
+
if (normalized === 'protected' || normalized === 'mutual') return 1;
|
|
44
|
+
return 2; // public (openType: 2)
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
readNaverCredentials,
|
|
51
|
+
parseNaverSessionError,
|
|
52
|
+
buildLoginErrorMessage,
|
|
53
|
+
normalizeNaverTagList,
|
|
54
|
+
mapNaverVisibility,
|
|
55
|
+
sleep,
|
|
56
|
+
};
|
|
@@ -192,6 +192,19 @@ const findWindowsChromePath = () => {
|
|
|
192
192
|
return candidates.find(p => fs.existsSync(p)) || null;
|
|
193
193
|
};
|
|
194
194
|
|
|
195
|
+
const findMacChromePath = () => {
|
|
196
|
+
const candidates = [
|
|
197
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
198
|
+
path.join(os.homedir(), 'Applications', 'Google Chrome.app', 'Contents', 'MacOS', 'Google Chrome'),
|
|
199
|
+
];
|
|
200
|
+
return candidates.find(p => fs.existsSync(p)) || null;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const findChromePath = () => {
|
|
204
|
+
if (process.platform === 'win32') return findWindowsChromePath();
|
|
205
|
+
return findMacChromePath();
|
|
206
|
+
};
|
|
207
|
+
|
|
195
208
|
const generateSelfSignedCert = (domain) => {
|
|
196
209
|
const tempDir = path.join(os.tmpdir(), `viruagent-cert-${Date.now()}`);
|
|
197
210
|
fs.mkdirSync(tempDir, { recursive: true });
|
|
@@ -245,19 +258,28 @@ const findChromeDebugPort = async () => {
|
|
|
245
258
|
const ws = await tryConnectCDP(CDP_DEBUG_PORT);
|
|
246
259
|
if (ws) return { port: CDP_DEBUG_PORT, wsUrl: ws };
|
|
247
260
|
|
|
248
|
-
// 2. DevToolsActivePort 파일 확인
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
+
// 2. DevToolsActivePort 파일 확인 (Windows/macOS 모두)
|
|
262
|
+
const dtpPaths = [];
|
|
263
|
+
if (process.platform === 'win32') {
|
|
264
|
+
dtpPaths.push(path.join(
|
|
265
|
+
process.env.LOCALAPPDATA || '',
|
|
266
|
+
'Google', 'Chrome', 'User Data', 'DevToolsActivePort'
|
|
267
|
+
));
|
|
268
|
+
} else {
|
|
269
|
+
dtpPaths.push(path.join(
|
|
270
|
+
os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome', 'DevToolsActivePort'
|
|
271
|
+
));
|
|
272
|
+
}
|
|
273
|
+
for (const dtpPath of dtpPaths) {
|
|
274
|
+
try {
|
|
275
|
+
const content = fs.readFileSync(dtpPath, 'utf-8').trim();
|
|
276
|
+
const port = parseInt(content.split('\n')[0], 10);
|
|
277
|
+
if (port > 0) {
|
|
278
|
+
const ws2 = await tryConnectCDP(port);
|
|
279
|
+
if (ws2) return { port, wsUrl: ws2 };
|
|
280
|
+
}
|
|
281
|
+
} catch {}
|
|
282
|
+
}
|
|
261
283
|
|
|
262
284
|
return null;
|
|
263
285
|
};
|
|
@@ -440,10 +462,32 @@ const getOrCreateJunctionPath = (chromeRoot) => {
|
|
|
440
462
|
return junctionPath;
|
|
441
463
|
};
|
|
442
464
|
|
|
465
|
+
const gracefulKillChrome = async () => {
|
|
466
|
+
try {
|
|
467
|
+
if (process.platform === 'win32') {
|
|
468
|
+
execSync('cmd /c "taskkill /IM chrome.exe"', { stdio: 'ignore', timeout: 10000 });
|
|
469
|
+
} else {
|
|
470
|
+
// macOS: SIGTERM으로 graceful 종료
|
|
471
|
+
execSync('pkill -TERM "Google Chrome" 2>/dev/null || true', { stdio: 'ignore', timeout: 10000 });
|
|
472
|
+
}
|
|
473
|
+
} catch {}
|
|
474
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
475
|
+
if (isChromeRunning()) {
|
|
476
|
+
try {
|
|
477
|
+
if (process.platform === 'win32') {
|
|
478
|
+
execSync('cmd /c "taskkill /F /IM chrome.exe"', { stdio: 'ignore', timeout: 5000 });
|
|
479
|
+
} else {
|
|
480
|
+
execSync('pkill -9 "Google Chrome" 2>/dev/null || true', { stdio: 'ignore', timeout: 5000 });
|
|
481
|
+
}
|
|
482
|
+
} catch {}
|
|
483
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
|
|
443
487
|
const extractCookiesViaCDP = async (targetSessionPath, chromeRoot, profileName) => {
|
|
444
488
|
// Chrome 실행 중: CDP(Chrome DevTools Protocol)로 쿠키 추출
|
|
445
489
|
// 1단계: 이미 디버그 포트가 열려있으면 바로 연결 (크롬 종료 없음)
|
|
446
|
-
// 2단계: 없으면 한 번만 재시작
|
|
490
|
+
// 2단계: 없으면 한 번만 재시작 (Windows: 바로가기 수정, macOS: 직접 재시작)
|
|
447
491
|
const { spawn } = require('child_process');
|
|
448
492
|
|
|
449
493
|
// 1. 이미 디버그 포트가 열려있는지 확인
|
|
@@ -453,42 +497,39 @@ const extractCookiesViaCDP = async (targetSessionPath, chromeRoot, profileName)
|
|
|
453
497
|
return await extractCookiesFromCDP(existing.port, targetSessionPath);
|
|
454
498
|
}
|
|
455
499
|
|
|
456
|
-
// 2. 디버그 포트 없음 →
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
500
|
+
// 2. 디버그 포트 없음 → Windows만 바로가기 수정 (macOS는 바로가기 개념 없음)
|
|
501
|
+
if (process.platform === 'win32') {
|
|
502
|
+
console.log('[chrome-cdp] Chrome 디버그 포트 미감지 — 바로가기에 --remote-debugging-port 추가 중...');
|
|
503
|
+
const shortcutModified = enableChromeDebugPort();
|
|
504
|
+
if (shortcutModified) {
|
|
505
|
+
console.log('[chrome-cdp] Chrome 바로가기 수정 완료 — 다음부터는 크롬 종료 없이 쿠키 추출 가능');
|
|
506
|
+
}
|
|
461
507
|
}
|
|
462
508
|
|
|
463
|
-
// 3. Chrome
|
|
464
|
-
const chromePath =
|
|
509
|
+
// 3. Chrome 경로 확인
|
|
510
|
+
const chromePath = findChromePath();
|
|
465
511
|
if (!chromePath) throw new Error('Chrome 실행 파일을 찾을 수 없습니다.');
|
|
466
512
|
|
|
513
|
+
// 4. Chrome을 graceful하게 종료하고 디버그 포트로 재시작 (최초 1회만)
|
|
467
514
|
console.log('[chrome-cdp] Chrome을 디버그 포트와 함께 재시작합니다 (탭 자동 복원)...');
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// 4. Junction 경로로 디버그 포트 + 세션 복원 재시작
|
|
480
|
-
// Chrome 145+는 기본 user-data-dir에서 디버그 포트를 거부하므로 junction으로 우회
|
|
481
|
-
const junctionRoot = getOrCreateJunctionPath(chromeRoot);
|
|
515
|
+
await gracefulKillChrome();
|
|
516
|
+
|
|
517
|
+
// 5. 디버그 포트 + 세션 복원 재시작
|
|
518
|
+
// Windows Chrome 145+: Junction으로 user-data-dir 우회
|
|
519
|
+
// macOS: user-data-dir 직접 지정
|
|
520
|
+
const userDataDir = process.platform === 'win32'
|
|
521
|
+
? getOrCreateJunctionPath(chromeRoot)
|
|
522
|
+
: chromeRoot;
|
|
482
523
|
const chromeProc = spawn(chromePath, [
|
|
483
524
|
`--remote-debugging-port=${CDP_DEBUG_PORT}`,
|
|
484
525
|
'--remote-allow-origins=*',
|
|
485
526
|
'--restore-last-session',
|
|
486
|
-
`--user-data-dir=${
|
|
527
|
+
`--user-data-dir=${userDataDir}`,
|
|
487
528
|
`--profile-directory=${profileName}`,
|
|
488
529
|
], { detached: true, stdio: 'ignore' });
|
|
489
530
|
chromeProc.unref();
|
|
490
531
|
|
|
491
|
-
//
|
|
532
|
+
// 6. CDP 연결 대기
|
|
492
533
|
let connected = null;
|
|
493
534
|
const maxWait = 15000;
|
|
494
535
|
const start = Date.now();
|
|
@@ -499,7 +540,7 @@ const extractCookiesViaCDP = async (targetSessionPath, chromeRoot, profileName)
|
|
|
499
540
|
}
|
|
500
541
|
if (!connected) throw new Error('Chrome 디버그 포트 연결 시간 초과');
|
|
501
542
|
|
|
502
|
-
//
|
|
543
|
+
// 7. 쿠키 추출 (Chrome은 계속 실행 상태 유지 — 종료하지 않음)
|
|
503
544
|
return await extractCookiesFromCDP(connected.port, targetSessionPath);
|
|
504
545
|
};
|
|
505
546
|
|
|
@@ -676,12 +717,16 @@ const importSessionFromChrome = async (targetSessionPath, profileName = 'Default
|
|
|
676
717
|
// 3) 카카오 세션 쿠키가 있으면 Playwright에 주입 후 자동 로그인
|
|
677
718
|
const hasKakaoSession = kakaoCookies.some(c => c.domain.includes('kakao.com') && (c.name === '_kawlt' || c.name === '_kawltea' || c.name === '_karmt'));
|
|
678
719
|
if (!hasKakaoSession) {
|
|
679
|
-
//
|
|
680
|
-
|
|
720
|
+
// 쿠키 복호화로 세션을 얻을 수 없는 경우 → CDP로 Chrome에서 직접 추출
|
|
721
|
+
if (isChromeRunning()) {
|
|
722
|
+
return await extractCookiesViaCDP(targetSessionPath, chromeRoot, profileName);
|
|
723
|
+
}
|
|
724
|
+
// Windows: Chrome이 꺼져있으면 DirectLaunch 방식으로 추출
|
|
681
725
|
if (process.platform === 'win32') {
|
|
682
726
|
return await importSessionViaChromeDirectLaunch(targetSessionPath, chromeRoot, profileName);
|
|
683
727
|
}
|
|
684
|
-
|
|
728
|
+
// macOS: Chrome이 꺼져있으면 CDP로 재시작하여 추출
|
|
729
|
+
return await extractCookiesViaCDP(targetSessionPath, chromeRoot, profileName);
|
|
685
730
|
}
|
|
686
731
|
|
|
687
732
|
const browser = await chromium.launch({ headless: true });
|