viruagent-cli 0.4.2 → 0.5.1
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/bin/index.js +26 -0
- package/package.json +1 -1
- package/src/providers/insta/apiClient.js +92 -54
- package/src/providers/insta/auth.js +10 -10
- package/src/providers/insta/index.js +215 -17
- package/src/providers/insta/session.js +2 -2
- package/src/providers/insta/smartComment.js +8 -8
- package/src/providers/insta/utils.js +5 -5
- package/src/providers/naver/auth.js +19 -19
- package/src/providers/naver/editorConvert.js +16 -16
- package/src/providers/naver/imageUpload.js +7 -7
- package/src/providers/naver/index.js +9 -9
- package/src/providers/naver/session.js +2 -2
- package/src/providers/naver/utils.js +8 -8
- package/src/providers/tistory/auth.js +12 -12
- package/src/providers/tistory/fetchLayer.js +5 -5
- package/src/providers/tistory/imageEnrichment.js +19 -19
- package/src/providers/tistory/imageNormalization.js +1 -1
- package/src/providers/tistory/imageSources.js +5 -5
- package/src/providers/tistory/index.js +15 -15
- package/src/providers/tistory/session.js +3 -3
- package/src/providers/tistory/utils.js +14 -14
- package/src/runner.js +22 -1
- package/src/services/naverApiClient.js +17 -17
- package/src/services/providerManager.js +1 -1
- package/src/services/tistoryApiClient.js +13 -13
|
@@ -4,7 +4,7 @@ const { createImageComponent } = require('./editorConvert');
|
|
|
4
4
|
const { buildKeywordImageCandidates } = require('../tistory/imageSources');
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
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(
|
|
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
|
|
69
|
-
*
|
|
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
|
-
//
|
|
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('
|
|
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: '
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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: '
|
|
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
|
-
//
|
|
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('
|
|
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('
|
|
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('
|
|
73
|
-
console.log('
|
|
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('
|
|
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('
|
|
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('
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
63
|
+
throw new Error(`Web text download failed: ${error.message}`);
|
|
64
64
|
} finally {
|
|
65
65
|
clearTimeout(timeout);
|
|
66
66
|
}
|
|
@@ -37,14 +37,14 @@ const extractImagePlaceholders = (content = '') => {
|
|
|
37
37
|
|
|
38
38
|
const fetchImageBuffer = async (url, retryCount = 0) => {
|
|
39
39
|
if (!url) {
|
|
40
|
-
throw new Error('
|
|
40
|
+
throw new Error('Image URL is missing.');
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
const localPath = resolveLocalImagePath(url);
|
|
44
44
|
if (localPath && !/https?:/.test(url)) {
|
|
45
45
|
const buffer = await fs.promises.readFile(localPath);
|
|
46
46
|
if (!buffer || buffer.length === 0) {
|
|
47
|
-
throw new Error(
|
|
47
|
+
throw new Error(`Image file is empty: ${localPath}`);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
const extensionFromSignature = getImageSignatureExtension(buffer);
|
|
@@ -73,7 +73,7 @@ const fetchImageBuffer = async (url, retryCount = 0) => {
|
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
if (!response.ok) {
|
|
76
|
-
throw new Error(
|
|
76
|
+
throw new Error(`Image download failed: ${response.status} ${response.statusText} (${url})`);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
const contentType = response.headers.get('content-type') || '';
|
|
@@ -99,7 +99,7 @@ const fetchImageBuffer = async (url, retryCount = 0) => {
|
|
|
99
99
|
|| extensionFromSignature;
|
|
100
100
|
|
|
101
101
|
if (!isImage) {
|
|
102
|
-
throw new Error(
|
|
102
|
+
throw new Error(`Not an image content type: ${contentType || '(unknown)'}, url=${finalUrl}`);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
return {
|
|
@@ -112,7 +112,7 @@ const fetchImageBuffer = async (url, retryCount = 0) => {
|
|
|
112
112
|
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
113
113
|
return fetchImageBuffer(url, retryCount + 1);
|
|
114
114
|
}
|
|
115
|
-
throw new Error(
|
|
115
|
+
throw new Error(`Image download failed: ${error.message}`);
|
|
116
116
|
} finally {
|
|
117
117
|
clearTimeout(timeout);
|
|
118
118
|
}
|
|
@@ -124,10 +124,10 @@ const uploadImageFromRemote = async (api, remoteUrl, fallbackName = 'image', dep
|
|
|
124
124
|
if (downloaded?.isHtml && downloaded?.html) {
|
|
125
125
|
const extractedImageUrl = extractImageFromHtml(downloaded.html, downloaded.finalUrl || remoteUrl);
|
|
126
126
|
if (!extractedImageUrl) {
|
|
127
|
-
throw new Error('
|
|
127
|
+
throw new Error('Could not find a valid representative image from the image page.');
|
|
128
128
|
}
|
|
129
129
|
if (depth >= 1 || extractedImageUrl === remoteUrl) {
|
|
130
|
-
throw new Error('
|
|
130
|
+
throw new Error('The extracted URL from the image page is invalid. Upload aborted.');
|
|
131
131
|
}
|
|
132
132
|
return uploadImageFromRemote(api, extractedImageUrl, fallbackName, depth + 1);
|
|
133
133
|
}
|
|
@@ -145,7 +145,7 @@ const uploadImageFromRemote = async (api, remoteUrl, fallbackName = 'image', dep
|
|
|
145
145
|
const uploadedKage = normalizeUploadedImageThumbnail(uploaded) || (uploaded?.key ? `kage@${uploaded.key}` : null);
|
|
146
146
|
|
|
147
147
|
if (!uploaded || !(uploaded.url || uploaded.key)) {
|
|
148
|
-
throw new Error('
|
|
148
|
+
throw new Error('Image upload response is invalid.');
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
return {
|
|
@@ -256,7 +256,7 @@ const replaceImagePlaceholdersWithUploaded = async (
|
|
|
256
256
|
uploaded: [],
|
|
257
257
|
uploadedCount: 0,
|
|
258
258
|
status: 'need_image_urls',
|
|
259
|
-
message: '
|
|
259
|
+
message: 'No image candidate keywords available for auto-upload. Please provide imageUrls or relatedImageKeywords/placeholder keywords.',
|
|
260
260
|
requestedKeywords,
|
|
261
261
|
requestedCount: requestedImageCount,
|
|
262
262
|
providedImageUrls: collectedImageUrls.length,
|
|
@@ -275,7 +275,7 @@ const replaceImagePlaceholdersWithUploaded = async (
|
|
|
275
275
|
index: i,
|
|
276
276
|
sourceUrl: null,
|
|
277
277
|
keyword: target.keyword,
|
|
278
|
-
message: '
|
|
278
|
+
message: 'No image source available.',
|
|
279
279
|
});
|
|
280
280
|
continue;
|
|
281
281
|
}
|
|
@@ -302,7 +302,7 @@ const replaceImagePlaceholdersWithUploaded = async (
|
|
|
302
302
|
break;
|
|
303
303
|
} catch (error) {
|
|
304
304
|
lastMessage = error.message;
|
|
305
|
-
console.log('
|
|
305
|
+
console.log('Image processing failed:', sourceUrl, error.message);
|
|
306
306
|
}
|
|
307
307
|
}
|
|
308
308
|
|
|
@@ -337,7 +337,7 @@ const replaceImagePlaceholdersWithUploaded = async (
|
|
|
337
337
|
break;
|
|
338
338
|
} catch (error) {
|
|
339
339
|
lastMessage = error.message;
|
|
340
|
-
console.log('
|
|
340
|
+
console.log('Image processing failed (fallback source):', sourceUrl, error.message);
|
|
341
341
|
}
|
|
342
342
|
}
|
|
343
343
|
}
|
|
@@ -347,7 +347,7 @@ const replaceImagePlaceholdersWithUploaded = async (
|
|
|
347
347
|
index: i,
|
|
348
348
|
sourceUrl: uniqueSources[0],
|
|
349
349
|
keyword: target.keyword,
|
|
350
|
-
message:
|
|
350
|
+
message: `Image upload failed (including fallback retries): ${lastMessage}`,
|
|
351
351
|
});
|
|
352
352
|
continue;
|
|
353
353
|
}
|
|
@@ -369,7 +369,7 @@ const replaceImagePlaceholdersWithUploaded = async (
|
|
|
369
369
|
uploaded: [],
|
|
370
370
|
uploadedCount: 0,
|
|
371
371
|
status: 'image_upload_failed',
|
|
372
|
-
message: '
|
|
372
|
+
message: 'Image upload failed. Please verify the collected image URLs and try again.',
|
|
373
373
|
errors: uploadErrors,
|
|
374
374
|
requestedKeywords,
|
|
375
375
|
requestedCount: requestedImageCount,
|
|
@@ -384,7 +384,7 @@ const replaceImagePlaceholdersWithUploaded = async (
|
|
|
384
384
|
uploaded: uploadedImages,
|
|
385
385
|
uploadedCount: uploadedImages.length,
|
|
386
386
|
status: 'insufficient_images',
|
|
387
|
-
message:
|
|
387
|
+
message: `Minimum image upload count not met. (required: ${safeMinimumImageCount} / actual: ${uploadedImages.length})`,
|
|
388
388
|
errors: uploadErrors,
|
|
389
389
|
requestedKeywords,
|
|
390
390
|
requestedCount: requestedImageCount,
|
|
@@ -400,7 +400,7 @@ const replaceImagePlaceholdersWithUploaded = async (
|
|
|
400
400
|
uploaded: uploadedImages,
|
|
401
401
|
uploadedCount: uploadedImages.length,
|
|
402
402
|
status: 'image_upload_partial',
|
|
403
|
-
message: '
|
|
403
|
+
message: 'Some image uploads failed.',
|
|
404
404
|
errors: uploadErrors,
|
|
405
405
|
requestedCount: requestedImageCount,
|
|
406
406
|
uploadedPlaceholders: uploadedImages.length,
|
|
@@ -414,7 +414,7 @@ const replaceImagePlaceholdersWithUploaded = async (
|
|
|
414
414
|
uploaded: uploadedImages,
|
|
415
415
|
uploadedCount: uploadedImages.length,
|
|
416
416
|
status: 'insufficient_images',
|
|
417
|
-
message:
|
|
417
|
+
message: `Minimum image upload count not met. (required: ${safeMinimumImageCount} / actual: ${uploadedImages.length})`,
|
|
418
418
|
errors: uploadErrors,
|
|
419
419
|
requestedKeywords,
|
|
420
420
|
requestedCount: requestedImageCount,
|
|
@@ -546,8 +546,8 @@ const resolveMandatoryThumbnail = async ({
|
|
|
546
546
|
.split(',')
|
|
547
547
|
.map((item) => item.trim())),
|
|
548
548
|
String(title || '').trim(),
|
|
549
|
-
'
|
|
550
|
-
'
|
|
549
|
+
'news image',
|
|
550
|
+
'news',
|
|
551
551
|
'thumbnail',
|
|
552
552
|
]);
|
|
553
553
|
|
|
@@ -174,8 +174,8 @@ const buildKeywordImageCandidates = async (keyword = '') => {
|
|
|
174
174
|
|
|
175
175
|
const duckduckgoQueries = [
|
|
176
176
|
safeKeyword,
|
|
177
|
-
`${safeKeyword}
|
|
178
|
-
`${safeKeyword}
|
|
177
|
+
`${safeKeyword} image`,
|
|
178
|
+
`${safeKeyword} news`,
|
|
179
179
|
];
|
|
180
180
|
const searchCandidates = [];
|
|
181
181
|
const seen = new Set();
|
|
@@ -203,10 +203,10 @@ const buildKeywordImageCandidates = async (keyword = '') => {
|
|
|
203
203
|
|
|
204
204
|
const fallbackQueries = [
|
|
205
205
|
safeKeyword,
|
|
206
|
-
`${safeKeyword}
|
|
206
|
+
`${safeKeyword} image`,
|
|
207
207
|
`${safeKeyword} news`,
|
|
208
|
-
'
|
|
209
|
-
'
|
|
208
|
+
'news',
|
|
209
|
+
'world news',
|
|
210
210
|
];
|
|
211
211
|
for (const query of fallbackQueries) {
|
|
212
212
|
if (searchCandidates.length >= 6) {
|
|
@@ -23,8 +23,8 @@ const createTistoryProvider = ({ sessionPath }) => {
|
|
|
23
23
|
status: 'pending_2fa',
|
|
24
24
|
loggedIn: false,
|
|
25
25
|
message: mode === 'otp'
|
|
26
|
-
? '
|
|
27
|
-
: '
|
|
26
|
+
? '2FA is required. Please provide the OTP code via twoFactorCode.'
|
|
27
|
+
: 'Kakao 2FA is required. Please verify in the app and try again.',
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
const askForAuthentication = createAskForAuthentication({
|
|
@@ -80,7 +80,7 @@ const createTistoryProvider = ({ sessionPath }) => {
|
|
|
80
80
|
};
|
|
81
81
|
|
|
82
82
|
if (!resolved.manual && (!resolved.username || !resolved.password)) {
|
|
83
|
-
throw new Error('
|
|
83
|
+
throw new Error('Tistory auto-login requires username/password. Please provide them directly or set TISTORY_USERNAME/TISTORY_PASSWORD environment variables.');
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
const result = await askForAuthentication(resolved);
|
|
@@ -95,7 +95,7 @@ const createTistoryProvider = ({ sessionPath }) => {
|
|
|
95
95
|
|
|
96
96
|
async publish(payload) {
|
|
97
97
|
return withProviderSession(async () => {
|
|
98
|
-
const title = payload.title || '
|
|
98
|
+
const title = payload.title || 'Untitled';
|
|
99
99
|
const rawContent = payload.content || '';
|
|
100
100
|
const visibility = mapVisibility(payload.visibility);
|
|
101
101
|
const tag = normalizeTagList(payload.tags);
|
|
@@ -198,7 +198,7 @@ const createTistoryProvider = ({ sessionPath }) => {
|
|
|
198
198
|
title,
|
|
199
199
|
visibility,
|
|
200
200
|
tags: tag,
|
|
201
|
-
message: '
|
|
201
|
+
message: 'A category is required for publishing. Please check categories and specify a category.',
|
|
202
202
|
categories,
|
|
203
203
|
};
|
|
204
204
|
}
|
|
@@ -208,7 +208,7 @@ const createTistoryProvider = ({ sessionPath }) => {
|
|
|
208
208
|
} else {
|
|
209
209
|
if (!process.stdin || !process.stdin.isTTY) {
|
|
210
210
|
const sampleCategory = categories.slice(0, 5).map((item) => `${item.id}: ${item.name}`).join(', ');
|
|
211
|
-
const sampleText = sampleCategory.length > 0 ? `
|
|
211
|
+
const sampleText = sampleCategory.length > 0 ? ` e.g. ${sampleCategory}` : '';
|
|
212
212
|
return {
|
|
213
213
|
provider: 'tistory',
|
|
214
214
|
mode: 'publish',
|
|
@@ -217,7 +217,7 @@ const createTistoryProvider = ({ sessionPath }) => {
|
|
|
217
217
|
title,
|
|
218
218
|
visibility,
|
|
219
219
|
tags: tag,
|
|
220
|
-
message:
|
|
220
|
+
message: `No category specified. The --category option is required in non-interactive mode. Usage: --category <categoryID>.${sampleText}`,
|
|
221
221
|
categories,
|
|
222
222
|
};
|
|
223
223
|
}
|
|
@@ -232,7 +232,7 @@ const createTistoryProvider = ({ sessionPath }) => {
|
|
|
232
232
|
title,
|
|
233
233
|
visibility,
|
|
234
234
|
tags: tag,
|
|
235
|
-
message: '
|
|
235
|
+
message: 'No category specified. Please enter a category to proceed with publishing.',
|
|
236
236
|
categories,
|
|
237
237
|
};
|
|
238
238
|
}
|
|
@@ -251,7 +251,7 @@ const createTistoryProvider = ({ sessionPath }) => {
|
|
|
251
251
|
title,
|
|
252
252
|
visibility,
|
|
253
253
|
tags: tag,
|
|
254
|
-
message: '
|
|
254
|
+
message: 'Please specify a valid category as a number.',
|
|
255
255
|
categories,
|
|
256
256
|
};
|
|
257
257
|
}
|
|
@@ -266,7 +266,7 @@ const createTistoryProvider = ({ sessionPath }) => {
|
|
|
266
266
|
title,
|
|
267
267
|
visibility,
|
|
268
268
|
tags: tag,
|
|
269
|
-
message: '
|
|
269
|
+
message: 'The specified category does not exist. Please check the available categories.',
|
|
270
270
|
categories,
|
|
271
271
|
};
|
|
272
272
|
}
|
|
@@ -324,7 +324,7 @@ const createTistoryProvider = ({ sessionPath }) => {
|
|
|
324
324
|
minimumImageCount: safeMinimumImageCount,
|
|
325
325
|
url: fallbackPublishResult.entryUrl || null,
|
|
326
326
|
raw: fallbackPublishResult,
|
|
327
|
-
message: '
|
|
327
|
+
message: 'Published as private due to publish limit (403).',
|
|
328
328
|
fallbackThumbnail: finalThumbnail,
|
|
329
329
|
};
|
|
330
330
|
} catch (fallbackError) {
|
|
@@ -344,7 +344,7 @@ const createTistoryProvider = ({ sessionPath }) => {
|
|
|
344
344
|
images: enrichedImages.images,
|
|
345
345
|
imageCount: enrichedImages.uploadedCount,
|
|
346
346
|
minimumImageCount: safeMinimumImageCount,
|
|
347
|
-
message: '
|
|
347
|
+
message: 'Both public and private publishing failed due to publish limit (403).',
|
|
348
348
|
raw: {
|
|
349
349
|
success: false,
|
|
350
350
|
error: fallbackError.message,
|
|
@@ -357,7 +357,7 @@ const createTistoryProvider = ({ sessionPath }) => {
|
|
|
357
357
|
|
|
358
358
|
async saveDraft(payload) {
|
|
359
359
|
return withProviderSession(async () => {
|
|
360
|
-
const title = payload.title || '
|
|
360
|
+
const title = payload.title || 'Draft';
|
|
361
361
|
const rawContent = payload.content || '';
|
|
362
362
|
const rawThumbnail = payload.thumbnail || null;
|
|
363
363
|
const tag = normalizeTagList(payload.tags);
|
|
@@ -499,7 +499,7 @@ const createTistoryProvider = ({ sessionPath }) => {
|
|
|
499
499
|
provider: 'tistory',
|
|
500
500
|
mode: 'post',
|
|
501
501
|
status: 'invalid_post_id',
|
|
502
|
-
message: 'postId
|
|
502
|
+
message: 'postId is required.',
|
|
503
503
|
};
|
|
504
504
|
}
|
|
505
505
|
|
|
@@ -515,7 +515,7 @@ const createTistoryProvider = ({ sessionPath }) => {
|
|
|
515
515
|
status: 'not_found',
|
|
516
516
|
postId: resolvedPostId,
|
|
517
517
|
includeDraft: Boolean(includeDraft),
|
|
518
|
-
message: '
|
|
518
|
+
message: 'No post found with the specified postId.',
|
|
519
519
|
};
|
|
520
520
|
}
|
|
521
521
|
return {
|