viruagent-cli 0.4.1 → 0.5.0

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.
@@ -30,28 +30,28 @@ const checkLoginResult = async (page) => {
30
30
  const patterns = NAVER_LOGIN_ERROR_PATTERNS;
31
31
 
32
32
  if (content.includes(patterns.wrongPassword)) {
33
- return { success: false, error: 'wrong_password', message: '비밀번호가 잘못되었습니다.' };
33
+ return { success: false, error: 'wrong_password', message: 'Incorrect password.' };
34
34
  }
35
35
  if (content.includes(patterns.accountProtected)) {
36
- return { success: false, error: 'account_protected', message: '계정 보호조치가 활성화되어 있습니다.' };
36
+ return { success: false, error: 'account_protected', message: 'Account protection is enabled.' };
37
37
  }
38
38
  if (content.includes(patterns.regionBlocked)) {
39
- return { success: false, error: 'region_blocked', message: '허용하지 않은 지역에서 접속이 감지되었습니다.' };
39
+ return { success: false, error: 'region_blocked', message: 'Access from a disallowed region was detected.' };
40
40
  }
41
41
  if (content.includes(patterns.usageRestricted)) {
42
- return { success: false, error: 'usage_restricted', message: '비정상적인 활동이 감지되어 이용이 제한되었습니다.' };
42
+ return { success: false, error: 'usage_restricted', message: 'Abnormal activity detected. Usage has been restricted.' };
43
43
  }
44
44
  if (content.includes(patterns.twoFactor)) {
45
- return { success: false, error: 'two_factor', message: '2단계 인증이 필요합니다. --manual 모드로 로그인해 주세요.' };
45
+ return { success: false, error: 'two_factor', message: 'Two-factor authentication required. Please log in using --manual mode.' };
46
46
  }
47
47
 
48
- // 캡차 감지
48
+ // Captcha detection
49
49
  const hasCaptcha = patterns.captcha.some((p) => content.toLowerCase().includes(p.toLowerCase()));
50
50
  if (hasCaptcha) {
51
- return { success: false, error: 'captcha', message: '캡차가 감지되었습니다. --manual 모드를 사용해 주세요.' };
51
+ return { success: false, error: 'captcha', message: 'Captcha detected. Please use --manual mode.' };
52
52
  }
53
53
 
54
- // 성공 (운영원칙 위반 포함)
54
+ // Success (including operation violation notice)
55
55
  if (content.includes(patterns.operationViolation) || content.includes(patterns.newDevice)) {
56
56
  return { success: true };
57
57
  }
@@ -61,7 +61,7 @@ const checkLoginResult = async (page) => {
61
61
  return { success: true };
62
62
  }
63
63
 
64
- return { success: false, error: 'unknown', message: '로그인 상태를 확인할 없습니다.' };
64
+ return { success: false, error: 'unknown', message: 'Unable to verify login status.' };
65
65
  };
66
66
 
67
67
  const createAskForAuthentication = ({ sessionPath, naverApi }) => async ({
@@ -76,7 +76,7 @@ const createAskForAuthentication = ({ sessionPath, naverApi }) => async ({
76
76
  const resolvedPassword = password || readNaverCredentials().password;
77
77
 
78
78
  if (!manual && (!resolvedUsername || !resolvedPassword)) {
79
- throw new Error('네이버 로그인에 id/pw 필요합니다. 환경변수 NAVER_USERNAME/NAVER_PASSWORD 설정하거나 --manual 모드를 사용해 주세요.');
79
+ throw new Error('Naver login requires id/pw. Set the NAVER_USERNAME/NAVER_PASSWORD environment variables or use --manual mode.');
80
80
  }
81
81
 
82
82
  const browser = await chromium.launch({
@@ -102,13 +102,13 @@ const createAskForAuthentication = ({ sessionPath, naverApi }) => async ({
102
102
  if (manual) {
103
103
  console.log('');
104
104
  console.log('==============================');
105
- console.log('수동 로그인 모드로 전환합니다.');
106
- console.log('브라우저에서 직접 네이버 로그인을 완료해 주세요.');
107
- console.log('최대 5분 내에 로그인을 완료해 주세요.');
105
+ console.log('Switching to manual login mode.');
106
+ console.log('Please complete the Naver login in the browser.');
107
+ console.log('Please complete the login within 5 minutes.');
108
108
  console.log('==============================');
109
109
  loginSuccess = await waitForNaverLoginFinish(page, context, 300000);
110
110
  } else {
111
- // JS 인젝션으로 ID/PW 입력 (fill() 대신 감지 우회)
111
+ // Inject ID/PW via JS (instead of fill() — bypasses bot detection)
112
112
  await page.evaluate((id) => {
113
113
  const el = document.getElementById('id');
114
114
  if (el) el.value = id;
@@ -121,14 +121,14 @@ const createAskForAuthentication = ({ sessionPath, naverApi }) => async ({
121
121
  }, resolvedPassword);
122
122
  await sleep(300);
123
123
 
124
- // 로그인 유지 체크
124
+ // Check "keep me logged in"
125
125
  const keepCheck = await page.$(NAVER_LOGIN_SELECTORS.keepLogin);
126
126
  if (keepCheck) {
127
127
  await keepCheck.click().catch(() => {});
128
128
  await sleep(300);
129
129
  }
130
130
 
131
- // 로그인 버튼 클릭
131
+ // Click login button
132
132
  const loginBtn = await page.$(NAVER_LOGIN_SELECTORS.submit);
133
133
  if (loginBtn) {
134
134
  await loginBtn.click();
@@ -137,7 +137,7 @@ const createAskForAuthentication = ({ sessionPath, naverApi }) => async ({
137
137
  }
138
138
  await sleep(3000);
139
139
 
140
- // 결과 확인
140
+ // Check result
141
141
  const result = await checkLoginResult(page);
142
142
  if (!result.success) {
143
143
  throw new Error(result.message);
@@ -145,7 +145,7 @@ const createAskForAuthentication = ({ sessionPath, naverApi }) => async ({
145
145
 
146
146
  loginSuccess = await waitForNaverLoginFinish(page, context, 15000);
147
147
  if (!loginSuccess) {
148
- // URL 기반 추가 확인
148
+ // Additional URL-based check
149
149
  const url = page.url();
150
150
  if (url.includes('naver.com') && !url.includes('nid.naver.com/nidlogin')) {
151
151
  loginSuccess = true;
@@ -154,7 +154,7 @@ const createAskForAuthentication = ({ sessionPath, naverApi }) => async ({
154
154
  }
155
155
 
156
156
  if (!loginSuccess) {
157
- throw new Error('네이버 로그인에 실패했습니다. 아이디/비밀번호를 확인하거나 --manual 모드를 사용해 주세요.');
157
+ throw new Error('Naver login failed. Please verify your id/password or use --manual mode.');
158
158
  }
159
159
 
160
160
  await persistNaverSession(context, sessionPath);
@@ -60,29 +60,29 @@ const createImageComponent = (imgData) => ({
60
60
  const stripHtmlTags = (html) => html.replace(/<[^>]*>/g, '');
61
61
 
62
62
  /**
63
- * HTML 네이버 에디터 컴포넌트 배열로 변환한다.
64
- * Primary: 네이버 API (upconvert.editor.naver.com)
65
- * Fallback: 커스텀 파싱
63
+ * Converts HTML to an array of Naver editor components.
64
+ * Primary: Naver API (upconvert.editor.naver.com)
65
+ * Fallback: Custom parsing
66
66
  */
67
67
  const convertHtmlToEditorComponents = async (naverApi, html, imageComponents = []) => {
68
- // 1. 네이버 API 변환 시도
68
+ // 1. Try Naver API conversion
69
69
  const apiComponents = await naverApi.convertHtmlToComponents(html);
70
70
  if (Array.isArray(apiComponents) && apiComponents.length > 0) {
71
- // 이미지를 위에 배치 (티스토리 스타일)
71
+ // Place images at the top of the post (Tistory style)
72
72
  return [...imageComponents, ...apiComponents];
73
73
  }
74
74
 
75
- // 2. Fallback: 커스텀 파싱 (이미지를 위에 배치)
75
+ // 2. Fallback: Custom parsing (images placed at the top)
76
76
  const textComponents = parseHtmlToComponents(html, []);
77
77
  return [...imageComponents, ...textComponents];
78
78
  };
79
79
 
80
80
  /**
81
- * HTML 수동으로 파싱하여 네이버 에디터 컴포넌트로 변환한다.
82
- * Python process_html_to_components() 포팅
81
+ * Manually parses HTML and converts it to Naver editor components.
82
+ * Ported from Python's process_html_to_components()
83
83
  */
84
84
  const parseHtmlToComponents = (html, imageComponents = []) => {
85
- // heading(h1-h6) 또는 strong 태그 기준으로 분할
85
+ // Split by heading (h1-h6) or strong tags
86
86
  const segments = html.split(/(<h[1-6][^>]*>.*?<\/h[1-6]>|<strong>.*?<\/strong>)/is);
87
87
  const components = [];
88
88
  const images = [...imageComponents];
@@ -96,7 +96,7 @@ const parseHtmlToComponents = (html, imageComponents = []) => {
96
96
  const isStrong = /<strong>/i.test(trimmed);
97
97
  const isBoldSection = isHeading || isStrong;
98
98
 
99
- // heading 태그 자체는 건너뛰기 (Python 코드의 continue와 동일)
99
+ // Skip heading tags themselves (same as Python code's continue)
100
100
  if (/^<h[1-6][^>]*>.*<\/h[1-6]>$/is.test(trimmed)) {
101
101
  const text = stripHtmlTags(trimmed);
102
102
  if (!text.trim()) continue;
@@ -111,7 +111,7 @@ const parseHtmlToComponents = (html, imageComponents = []) => {
111
111
  ctype: 'text',
112
112
  }));
113
113
  } else {
114
- // 이미지 삽입
114
+ // Insert image
115
115
  if (images.length > 0) {
116
116
  components.push(images.shift());
117
117
  }
@@ -125,7 +125,7 @@ const parseHtmlToComponents = (html, imageComponents = []) => {
125
125
  continue;
126
126
  }
127
127
 
128
- // 일반 텍스트 세그먼트
128
+ // Plain text segment
129
129
  const text = stripHtmlTags(trimmed);
130
130
  if (!text.trim()) continue;
131
131
 
@@ -146,7 +146,7 @@ const parseHtmlToComponents = (html, imageComponents = []) => {
146
146
  ctype: 'quotation',
147
147
  }));
148
148
  } else {
149
- // 일반 단락: <p> 또는 <br> 기준으로 분할
149
+ // Regular paragraphs: split by <p> or <br>
150
150
  const paragraphs = text.split(/\n+/).filter((p) => p.trim());
151
151
  for (const para of paragraphs) {
152
152
  components.push(createTextComponent(para.trim()));
@@ -154,7 +154,7 @@ const parseHtmlToComponents = (html, imageComponents = []) => {
154
154
  }
155
155
  }
156
156
 
157
- // 남은 이미지 append
157
+ // Append remaining images
158
158
  for (const img of images) {
159
159
  components.push(img);
160
160
  }
@@ -163,7 +163,7 @@ const parseHtmlToComponents = (html, imageComponents = []) => {
163
163
  };
164
164
 
165
165
  /**
166
- * API 반환 컴포넌트 사이에 이미지를 삽입한다.
166
+ * Intersperses images between API-returned components.
167
167
  */
168
168
  const intersperse = (components, imageComponents) => {
169
169
  if (!imageComponents.length) return components;
@@ -181,7 +181,7 @@ const intersperse = (components, imageComponents) => {
181
181
  result.push(comp);
182
182
  }
183
183
 
184
- // 남은 이미지 append
184
+ // Append remaining images
185
185
  for (const img of images) {
186
186
  result.push(img);
187
187
  }
@@ -4,7 +4,7 @@ const { createImageComponent } = require('./editorConvert');
4
4
  const { buildKeywordImageCandidates } = require('../tistory/imageSources');
5
5
 
6
6
  /**
7
- * 이미지 URL 또는 로컬 경로에서 이미지 버퍼를 가져온다.
7
+ * Fetches an image buffer from a URL or local file path.
8
8
  */
9
9
  const fetchImageBuffer = async (source) => {
10
10
  if (fs.existsSync(source)) {
@@ -26,7 +26,7 @@ const fetchImageBuffer = async (source) => {
26
26
  },
27
27
  });
28
28
  if (!response.ok) {
29
- throw new Error(`이미지 다운로드 실패: ${response.status} — ${source}`);
29
+ throw new Error(`Image download failed: ${response.status} — ${source}`);
30
30
  }
31
31
  const arrayBuffer = await response.arrayBuffer();
32
32
  const urlPath = new URL(response.url || source).pathname;
@@ -41,7 +41,7 @@ const fetchImageBuffer = async (source) => {
41
41
  };
42
42
 
43
43
  /**
44
- * 이미지 소스들을 네이버에 업로드하고 에디터 컴포넌트 배열로 반환한다.
44
+ * Uploads image sources to Naver and returns an array of editor components.
45
45
  */
46
46
  const uploadAndCreateImageComponents = async (naverApi, imageSources, token) => {
47
47
  const components = [];
@@ -65,8 +65,8 @@ const uploadAndCreateImageComponents = async (naverApi, imageSources, token) =>
65
65
  };
66
66
 
67
67
  /**
68
- * relatedImageKeywords에서 이미지를 검색하고, imageUrls 합쳐서 업로드한다.
69
- * 티스토리 imageSources.js의 buildKeywordImageCandidates를 재사용한다.
68
+ * Searches for images from relatedImageKeywords, merges with imageUrls, and uploads them.
69
+ * Reuses buildKeywordImageCandidates from Tistory's imageSources.js.
70
70
  */
71
71
  const collectAndUploadImages = async (naverApi, {
72
72
  imageUrls = [],
@@ -76,7 +76,7 @@ const collectAndUploadImages = async (naverApi, {
76
76
  }) => {
77
77
  const collectedUrls = [...imageUrls];
78
78
 
79
- // 키워드에서 이미지 URL 검색
79
+ // Search image URLs from keywords
80
80
  const normalizedKeywords = Array.isArray(relatedImageKeywords)
81
81
  ? relatedImageKeywords.map((k) => String(k || '').trim()).filter(Boolean)
82
82
  : String(relatedImageKeywords || '').split(',').map((k) => k.trim()).filter(Boolean);
@@ -92,7 +92,7 @@ const collectAndUploadImages = async (naverApi, {
92
92
  }
93
93
  }
94
94
  } catch {
95
- // 검색 실패 시 무시
95
+ // Ignore search failures
96
96
  }
97
97
  }
98
98
 
@@ -63,7 +63,7 @@ const createNaverProvider = ({ sessionPath }) => {
63
63
  };
64
64
 
65
65
  if (!resolved.manual && (!resolved.username || !resolved.password)) {
66
- throw new Error('네이버 자동 로그인을 진행하려면 username/password 필요합니다. 환경변수 NAVER_USERNAME / NAVER_PASSWORD 설정하거나 --manual 모드를 사용해 주세요.');
66
+ throw new Error('Naver auto-login requires username/password. Set the NAVER_USERNAME / NAVER_PASSWORD environment variables or use --manual mode.');
67
67
  }
68
68
 
69
69
  const result = await askForAuthentication(resolved);
@@ -78,7 +78,7 @@ const createNaverProvider = ({ sessionPath }) => {
78
78
 
79
79
  async publish(payload) {
80
80
  return withProviderSession(async () => {
81
- const title = payload.title || '제목 없음';
81
+ const title = payload.title || 'Untitled';
82
82
  const rawContent = payload.content || '';
83
83
  const openType = mapNaverVisibility(payload.visibility);
84
84
  const tags = normalizeNaverTagList(payload.tags);
@@ -91,7 +91,7 @@ const createNaverProvider = ({ sessionPath }) => {
91
91
  const rawCategories = await naverApi.getCategories();
92
92
  const categories = Object.entries(rawCategories).map(([name, id]) => ({ name, id: Number(id) })).sort((a, b) => a.id - b.id);
93
93
 
94
- // 카테고리 결정
94
+ // Determine category
95
95
  let categoryNo = payload.category;
96
96
  if (categoryNo === undefined || categoryNo === null || String(categoryNo).trim() === '') {
97
97
  if (categories.length === 0) {
@@ -107,14 +107,14 @@ const createNaverProvider = ({ sessionPath }) => {
107
107
  title,
108
108
  openType,
109
109
  tags,
110
- message: '발행을 위해 카테고리가 필요합니다. categories 확인하고 category를 지정해 주세요.',
110
+ message: 'A category is required for publishing. Please check categories and specify a category.',
111
111
  categories,
112
112
  };
113
113
  }
114
114
  }
115
115
  categoryNo = String(categoryNo);
116
116
 
117
- // 이미지 업로드 (imageUrls + relatedImageKeywords 기반 자동 검색)
117
+ // Image upload (auto-search based on imageUrls + relatedImageKeywords)
118
118
  let imageComponents = [];
119
119
  const hasImageSources = imageUrls.length > 0 || relatedImageKeywords.length > 0;
120
120
  if (autoUploadImages && hasImageSources) {
@@ -128,7 +128,7 @@ const createNaverProvider = ({ sessionPath }) => {
128
128
  imageComponents = uploadResult.components;
129
129
  }
130
130
 
131
- // HTML 에디터 컴포넌트 변환
131
+ // Convert HTML to editor components
132
132
  const contentComponents = await convertHtmlToEditorComponents(naverApi, rawContent, imageComponents);
133
133
 
134
134
  const result = await naverApi.publishPost({
@@ -154,7 +154,7 @@ const createNaverProvider = ({ sessionPath }) => {
154
154
  },
155
155
 
156
156
  async saveDraft(payload) {
157
- // 네이버는 임시저장 API 없으므로 비공개(openType: 0)로 발행
157
+ // Naver has no draft API, so publish as private (openType: 0)
158
158
  return this.publish({
159
159
  ...payload,
160
160
  visibility: 'private',
@@ -196,7 +196,7 @@ const createNaverProvider = ({ sessionPath }) => {
196
196
  provider: 'naver',
197
197
  mode: 'post',
198
198
  status: 'invalid_post_id',
199
- message: 'postId 필요합니다.',
199
+ message: 'postId is required.',
200
200
  };
201
201
  }
202
202
 
@@ -208,7 +208,7 @@ const createNaverProvider = ({ sessionPath }) => {
208
208
  mode: 'post',
209
209
  status: 'not_found',
210
210
  postId: resolvedPostId,
211
- message: '해당 postId의 글을 찾을 없습니다.',
211
+ message: 'Post not found for the given postId.',
212
212
  };
213
213
  }
214
214
  return {
@@ -21,7 +21,7 @@ const persistNaverSession = async (context, targetSessionPath) => {
21
21
  allCookies.push(...cookies);
22
22
  }
23
23
 
24
- // 중복 제거 (name+domain 기준)
24
+ // Deduplicate by name+domain
25
25
  const seen = new Set();
26
26
  const unique = allCookies.filter((c) => {
27
27
  const key = `${c.name}@${c.domain}`;
@@ -96,7 +96,7 @@ const createNaverWithProviderSession = (askForAuthentication) => async (fn) => {
96
96
  });
97
97
 
98
98
  if (!loginResult.loggedIn) {
99
- throw new Error(loginResult.message || '세션 갱신 로그인 상태가 확인되지 않았습니다.');
99
+ throw new Error(loginResult.message || 'Login status could not be verified after session refresh.');
100
100
  }
101
101
 
102
102
  return fn();
@@ -10,19 +10,19 @@ const readNaverCredentials = () => {
10
10
  const parseNaverSessionError = (error) => {
11
11
  const message = String(error?.message || '').toLowerCase();
12
12
  return [
13
- '세션 파일이 없습니다',
14
- '세션에 유효한 쿠키',
15
- '세션이 만료',
16
- '로그인이 필요합니다',
17
- 'blogid를 찾을 수 없습니다',
18
- '블로그 정보 조회 실패',
19
- '다시 로그인',
13
+ 'session file not found',
14
+ 'no valid cookies',
15
+ 'session expired',
16
+ 'login required',
17
+ 'find blogid',
18
+ 'failed to fetch blog info',
19
+ 'log in again',
20
20
  '401',
21
21
  '403',
22
22
  ].some((token) => message.includes(token.toLowerCase()));
23
23
  };
24
24
 
25
- const buildLoginErrorMessage = (error) => String(error?.message || '세션 검증에 실패했습니다.');
25
+ const buildLoginErrorMessage = (error) => String(error?.message || 'Session validation failed.');
26
26
 
27
27
  const normalizeNaverTagList = (value = '') => {
28
28
  const source = Array.isArray(value)
@@ -18,8 +18,8 @@ const {
18
18
  } = require('./selectors');
19
19
 
20
20
  /**
21
- * askForAuthentication 팩토리.
22
- * sessionPath, tistoryApi, pending2faResult 외부에서 주입받는다.
21
+ * askForAuthentication factory.
22
+ * Receives sessionPath, tistoryApi, and pending2faResult via dependency injection.
23
23
  */
24
24
  const createAskForAuthentication = ({ sessionPath, tistoryApi, pending2faResult }) => async ({
25
25
  headless = false,
@@ -35,7 +35,7 @@ const createAskForAuthentication = ({ sessionPath, tistoryApi, pending2faResult
35
35
  const shouldAutoFill = !manual;
36
36
 
37
37
  if (!manual && (!resolvedUsername || !resolvedPassword)) {
38
- throw new Error('티스토리 로그인 요청에 id/pw 없습니다. id/pw 먼저 전달하거나 TISTORY_USERNAME/TISTORY_PASSWORD 설정해 주세요.');
38
+ throw new Error('Tistory login requires id/pw. Please provide id/pw or set TISTORY_USERNAME/TISTORY_PASSWORD environment variables.');
39
39
  }
40
40
 
41
41
  const browser = await chromium.launch({
@@ -55,7 +55,7 @@ const createAskForAuthentication = ({ sessionPath, tistoryApi, pending2faResult
55
55
 
56
56
  const kakaoLoginSelector = await pickValue(page, KAKAO_TRIGGER_SELECTORS);
57
57
  if (!kakaoLoginSelector) {
58
- throw new Error('카카오 로그인 버튼을 찾지 못했습니다. 로그인 화면 UI 변경되었는지 확인해 주세요.');
58
+ throw new Error('Could not find the Kakao login button. Please check if the login page UI has changed.');
59
59
  }
60
60
 
61
61
  await page.locator(kakaoLoginSelector).click({ timeout: 5000 }).catch(() => {});
@@ -68,16 +68,16 @@ const createAskForAuthentication = ({ sessionPath, tistoryApi, pending2faResult
68
68
  if (manual) {
69
69
  console.log('');
70
70
  console.log('==============================');
71
- console.log('수동 로그인 모드로 전환합니다.');
72
- console.log('브라우저에서 직접 ID/PW/2차 인증을 완료한 뒤, 로그인 완료 상태를 기다립니다.');
73
- console.log('로그인 완료 또는 2차 인증은 최대 5 내에 처리해 주세요.');
71
+ console.log('Switching to manual login mode.');
72
+ console.log('Please complete ID/PW/2FA verification in the browser.');
73
+ console.log('Login or 2FA must be completed within 5 minutes.');
74
74
  console.log('==============================');
75
75
  finalLoginStatus = await waitForLoginFinish(page, context, 300000);
76
76
  } else if (shouldAutoFill) {
77
77
  const usernameFilled = await fillBySelector(page, KAKAO_LOGIN_SELECTORS.username, loginId);
78
78
  const passwordFilled = await fillBySelector(page, KAKAO_LOGIN_SELECTORS.password, loginPw);
79
79
  if (!usernameFilled || !passwordFilled) {
80
- throw new Error('카카오 로그인 입력 필드를 찾지 못했습니다. 티스토리 로그인 화면 변경 시도를 확인해 주세요.');
80
+ throw new Error('Could not find Kakao login form input fields. Please check if the Tistory login page has changed.');
81
81
  }
82
82
 
83
83
  await checkBySelector(page, KAKAO_LOGIN_SELECTORS.rememberLogin);
@@ -94,7 +94,7 @@ const createAskForAuthentication = ({ sessionPath, tistoryApi, pending2faResult
94
94
  }
95
95
  const otpFilled = await fillBySelector(page, LOGIN_OTP_SELECTORS, twoFactorCode);
96
96
  if (!otpFilled) {
97
- throw new Error('OTP 입력 필드를 찾지 못했습니다. 로그인 페이지를 확인해 주세요.');
97
+ throw new Error('Could not find the OTP input field. Please check the login page.');
98
98
  }
99
99
  await page.keyboard.press('Enter');
100
100
  finalLoginStatus = await waitForLoginFinish(page, context, 45000);
@@ -106,7 +106,7 @@ const createAskForAuthentication = ({ sessionPath, tistoryApi, pending2faResult
106
106
  if (hasEmailCodeInput && twoFactorCode) {
107
107
  const codeFilled = await fillBySelector(page, KAKAO_2FA_SELECTORS.codeInput, twoFactorCode);
108
108
  if (!codeFilled) {
109
- throw new Error('2차 인증 입력 필드를 찾지 못했습니다. 로그인 페이지를 확인해 주세요.');
109
+ throw new Error('Could not find the 2FA input field. Please check the login page.');
110
110
  }
111
111
  const confirmed = await clickSubmit(page, KAKAO_2FA_SELECTORS.confirm);
112
112
  if (!confirmed) {
@@ -122,7 +122,7 @@ const createAskForAuthentication = ({ sessionPath, tistoryApi, pending2faResult
122
122
 
123
123
  const codeFilled = await fillBySelector(page, KAKAO_2FA_SELECTORS.codeInput, twoFactorCode);
124
124
  if (!codeFilled) {
125
- throw new Error('카카오 이메일 인증 입력 필드를 찾지 못했습니다. 로그인 페이지를 확인해 주세요.');
125
+ throw new Error('Could not find the Kakao email verification input field. Please check the login page.');
126
126
  }
127
127
 
128
128
  const confirmed = await clickSubmit(page, KAKAO_2FA_SELECTORS.confirm);
@@ -140,7 +140,7 @@ const createAskForAuthentication = ({ sessionPath, tistoryApi, pending2faResult
140
140
  if (pendingTwoFactorAction) {
141
141
  return pending2faResult('kakao');
142
142
  }
143
- throw new Error('로그인에 실패했습니다. 아이디/비밀번호가 정확한지 확인하고, 없으면 환경변수 TISTORY_USERNAME/TISTORY_PASSWORD 다시 설정해 주세요.');
143
+ throw new Error('Login failed. Please verify your credentials and ensure TISTORY_USERNAME/TISTORY_PASSWORD environment variables are set correctly.');
144
144
  }
145
145
 
146
146
  await context.storageState({ path: sessionPath });
@@ -2,7 +2,7 @@ const { sleep, imageTrace } = require('./utils');
2
2
 
3
3
  const fetchText = async (url, retryCount = 0) => {
4
4
  if (!url) {
5
- throw new Error('텍스트 URL 없습니다.');
5
+ throw new Error('Text URL is missing.');
6
6
  }
7
7
 
8
8
  const headers = {
@@ -22,7 +22,7 @@ const fetchText = async (url, retryCount = 0) => {
22
22
  });
23
23
 
24
24
  if (!response.ok) {
25
- throw new Error(`텍스트 요청 실패: ${response.status} ${response.statusText}, url=${url}`);
25
+ throw new Error(`Text request failed: ${response.status} ${response.statusText}, url=${url}`);
26
26
  }
27
27
 
28
28
  return response.text();
@@ -31,7 +31,7 @@ const fetchText = async (url, retryCount = 0) => {
31
31
  await sleep(700);
32
32
  return fetchText(url, retryCount + 1);
33
33
  }
34
- throw new Error(`웹 텍스트 다운로드 실패: ${error.message}`);
34
+ throw new Error(`Web text download failed: ${error.message}`);
35
35
  } finally {
36
36
  clearTimeout(timeout);
37
37
  }
@@ -52,7 +52,7 @@ const fetchTextWithHeaders = async (url, headers = {}, retryCount = 0) => {
52
52
  signal: controller.signal,
53
53
  });
54
54
  if (!response.ok) {
55
- throw new Error(`텍스트 요청 실패: ${response.status} ${response.statusText}, url=${url}`);
55
+ throw new Error(`Text request failed: ${response.status} ${response.statusText}, url=${url}`);
56
56
  }
57
57
  return response.text();
58
58
  } catch (error) {
@@ -60,7 +60,7 @@ const fetchTextWithHeaders = async (url, headers = {}, retryCount = 0) => {
60
60
  await sleep(700);
61
61
  return fetchTextWithHeaders(url, headers, retryCount + 1);
62
62
  }
63
- throw new Error(`웹 텍스트 다운로드 실패: ${error.message}`);
63
+ throw new Error(`Web text download failed: ${error.message}`);
64
64
  } finally {
65
65
  clearTimeout(timeout);
66
66
  }