viruagent-cli 0.4.2 → 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.
- package/package.json +1 -1
- package/src/providers/insta/apiClient.js +54 -54
- package/src/providers/insta/auth.js +10 -10
- package/src/providers/insta/index.js +17 -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 +1 -1
- package/src/services/naverApiClient.js +17 -17
- package/src/services/providerManager.js +1 -1
- package/src/services/tistoryApiClient.js +13 -13
|
@@ -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 {
|
|
@@ -57,8 +57,8 @@ const persistTistorySession = async (context, targetSessionPath) => {
|
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
* withProviderSession
|
|
61
|
-
* askForAuthentication
|
|
60
|
+
* withProviderSession factory.
|
|
61
|
+
* Receives askForAuthentication via dependency injection to avoid scope issues.
|
|
62
62
|
*/
|
|
63
63
|
const createWithProviderSession = (askForAuthentication) => async (fn) => {
|
|
64
64
|
const credentials = readCredentialsFromEnv();
|
|
@@ -94,7 +94,7 @@ const createWithProviderSession = (askForAuthentication) => async (fn) => {
|
|
|
94
94
|
});
|
|
95
95
|
|
|
96
96
|
if (!loginResult.loggedIn) {
|
|
97
|
-
throw new Error(loginResult.message || '
|
|
97
|
+
throw new Error(loginResult.message || 'Login status could not be confirmed after session refresh.');
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
return fn();
|
|
@@ -11,10 +11,10 @@ const imageTrace = (message, data) => {
|
|
|
11
11
|
return;
|
|
12
12
|
}
|
|
13
13
|
if (data === undefined) {
|
|
14
|
-
console.log(`[
|
|
14
|
+
console.log(`[Image Trace] ${message}`);
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
17
|
-
console.log(`[
|
|
17
|
+
console.log(`[Image Trace] ${message}`, data);
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
const MAX_IMAGE_UPLOAD_COUNT = 1;
|
|
@@ -57,17 +57,17 @@ const normalizeTagList = (value = '') => {
|
|
|
57
57
|
const parseSessionError = (error) => {
|
|
58
58
|
const message = String(error?.message || '').toLowerCase();
|
|
59
59
|
return [
|
|
60
|
-
'
|
|
61
|
-
'
|
|
62
|
-
'
|
|
63
|
-
'
|
|
64
|
-
'
|
|
65
|
-
'
|
|
66
|
-
'
|
|
60
|
+
'session expired',
|
|
61
|
+
'valid cookie in session',
|
|
62
|
+
'session file not found',
|
|
63
|
+
'blog info fetch failed: 401',
|
|
64
|
+
'blog info fetch failed: 403',
|
|
65
|
+
'session has expired',
|
|
66
|
+
'please log in again',
|
|
67
67
|
].some((token) => message.includes(token.toLowerCase()));
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
-
const buildLoginErrorMessage = (error) => String(error?.message || '
|
|
70
|
+
const buildLoginErrorMessage = (error) => String(error?.message || 'Session validation failed.');
|
|
71
71
|
|
|
72
72
|
const promptCategorySelection = async (categories = []) => {
|
|
73
73
|
if (!process.stdin || !process.stdin.isTTY) {
|
|
@@ -79,9 +79,9 @@ const promptCategorySelection = async (categories = []) => {
|
|
|
79
79
|
|
|
80
80
|
const candidates = categories.map((category, index) => `${index + 1}. ${category.name} (${category.id})`);
|
|
81
81
|
const lines = [
|
|
82
|
-
'
|
|
82
|
+
'Please select a category for publishing.',
|
|
83
83
|
...candidates,
|
|
84
|
-
|
|
84
|
+
`Enter: number (1-${categories.length}) or category ID (press Enter to skip)`,
|
|
85
85
|
];
|
|
86
86
|
const prompt = `${lines.join('\n')}\n> `;
|
|
87
87
|
|
|
@@ -126,7 +126,7 @@ const promptCategorySelection = async (categories = []) => {
|
|
|
126
126
|
return;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
console.log('
|
|
129
|
+
console.log('Invalid input. Please enter a number or category ID again.');
|
|
130
130
|
ask(retryCount + 1);
|
|
131
131
|
});
|
|
132
132
|
};
|
|
@@ -137,7 +137,7 @@ const promptCategorySelection = async (categories = []) => {
|
|
|
137
137
|
|
|
138
138
|
const isPublishLimitError = (error) => {
|
|
139
139
|
const message = String(error?.message || '');
|
|
140
|
-
return
|
|
140
|
+
return /publish failed:\s*403/i.test(message) || /\b403\b/.test(message);
|
|
141
141
|
};
|
|
142
142
|
|
|
143
143
|
const isProvidedCategory = (value) => {
|
package/src/runner.js
CHANGED
|
@@ -178,7 +178,7 @@ const runCommand = async (command, opts = {}) => {
|
|
|
178
178
|
case 'logout':
|
|
179
179
|
return withProvider(() => provider.logout())();
|
|
180
180
|
|
|
181
|
-
// ── Instagram
|
|
181
|
+
// ── Instagram-specific (works with other providers if the method exists) ──
|
|
182
182
|
|
|
183
183
|
case 'get-profile':
|
|
184
184
|
if (!opts.username) {
|
|
@@ -29,19 +29,19 @@ const normalizeCookies = (session) => {
|
|
|
29
29
|
const readSessionCookies = (sessionPath) => {
|
|
30
30
|
const resolvedPath = path.resolve(sessionPath);
|
|
31
31
|
if (!fs.existsSync(resolvedPath)) {
|
|
32
|
-
throw new Error(
|
|
32
|
+
throw new Error(`Session file not found. Please save login credentials to ${resolvedPath} first.`);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
let raw;
|
|
36
36
|
try {
|
|
37
37
|
raw = JSON.parse(fs.readFileSync(resolvedPath, 'utf-8'));
|
|
38
38
|
} catch (error) {
|
|
39
|
-
throw new Error(
|
|
39
|
+
throw new Error(`Failed to parse session file: ${error.message}`);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
const cookies = normalizeCookies(raw);
|
|
43
43
|
if (!cookies.length) {
|
|
44
|
-
throw new Error('
|
|
44
|
+
throw new Error('No valid cookies found in session. Please log in again.');
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
return cookies.join('; ');
|
|
@@ -88,7 +88,7 @@ const createNaverApiClient = ({ sessionPath }) => {
|
|
|
88
88
|
detail = await response.text();
|
|
89
89
|
detail = detail ? `: ${detail.slice(0, 200)}` : '';
|
|
90
90
|
} catch {}
|
|
91
|
-
throw new Error(
|
|
91
|
+
throw new Error(`Request failed: ${response.status} ${response.statusText}${detail}`);
|
|
92
92
|
}
|
|
93
93
|
return response.json();
|
|
94
94
|
} finally {
|
|
@@ -105,7 +105,7 @@ const createNaverApiClient = ({ sessionPath }) => {
|
|
|
105
105
|
...options,
|
|
106
106
|
});
|
|
107
107
|
if (!response.ok) {
|
|
108
|
-
throw new Error(
|
|
108
|
+
throw new Error(`Request failed: ${response.status} ${response.statusText}`);
|
|
109
109
|
}
|
|
110
110
|
return response.text();
|
|
111
111
|
} finally {
|
|
@@ -122,11 +122,11 @@ const createNaverApiClient = ({ sessionPath }) => {
|
|
|
122
122
|
|
|
123
123
|
const match = html.match(/blogId\s*=\s*'([^']+)'/);
|
|
124
124
|
if (!match) {
|
|
125
|
-
//
|
|
125
|
+
// Also check if response is a login page
|
|
126
126
|
if (html.includes('로그인') || html.includes('login')) {
|
|
127
|
-
throw new Error('
|
|
127
|
+
throw new Error('Session expired. Please log in again.');
|
|
128
128
|
}
|
|
129
|
-
throw new Error('
|
|
129
|
+
throw new Error('Could not find blogId in MyBlog response.');
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
blogId = match[1];
|
|
@@ -144,7 +144,7 @@ const createNaverApiClient = ({ sessionPath }) => {
|
|
|
144
144
|
}
|
|
145
145
|
);
|
|
146
146
|
const token = json?.result?.token;
|
|
147
|
-
if (!token) throw new Error('Se-Authorization
|
|
147
|
+
if (!token) throw new Error('Failed to retrieve Se-Authorization token.');
|
|
148
148
|
return token;
|
|
149
149
|
};
|
|
150
150
|
|
|
@@ -185,7 +185,7 @@ const createNaverApiClient = ({ sessionPath }) => {
|
|
|
185
185
|
}
|
|
186
186
|
);
|
|
187
187
|
const editorId = configJson?.editorInfo?.id;
|
|
188
|
-
if (!editorId) throw new Error('
|
|
188
|
+
if (!editorId) throw new Error('Failed to retrieve editor ID.');
|
|
189
189
|
|
|
190
190
|
const managerJson = await requestJson(
|
|
191
191
|
`${BLOG_HOST}/PostWriteFormManagerOptions.naver?blogId=${encodeURIComponent(id)}&categoryNo=${encodeURIComponent(categoryNo)}`,
|
|
@@ -216,7 +216,7 @@ const createNaverApiClient = ({ sessionPath }) => {
|
|
|
216
216
|
const uploadImage = async (imageBuffer, filename, token) => {
|
|
217
217
|
const id = blogId || await initBlog();
|
|
218
218
|
const sessionKey = await getUploadSessionKey(token);
|
|
219
|
-
if (!sessionKey) throw new Error('
|
|
219
|
+
if (!sessionKey) throw new Error('Failed to retrieve image upload session key.');
|
|
220
220
|
|
|
221
221
|
const uploadUrl = `https://blog.upphoto.naver.com/${sessionKey}/simpleUpload/0?userId=${encodeURIComponent(id)}&extractExif=true&extractAnimatedCnt=true&autorotate=true&extractDominantColor=false&denyAnimatedImage=false&skipXcamFiltering=false`;
|
|
222
222
|
|
|
@@ -238,12 +238,12 @@ const createNaverApiClient = ({ sessionPath }) => {
|
|
|
238
238
|
});
|
|
239
239
|
|
|
240
240
|
if (!response.ok) {
|
|
241
|
-
throw new Error(
|
|
241
|
+
throw new Error(`Image upload failed: ${response.status}`);
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
const xml = await response.text();
|
|
245
245
|
if (!xml.includes('<url>')) {
|
|
246
|
-
throw new Error('
|
|
246
|
+
throw new Error('Image upload response does not contain a URL.');
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
const extractTag = (tag) => {
|
|
@@ -309,7 +309,7 @@ const createNaverApiClient = ({ sessionPath }) => {
|
|
|
309
309
|
: await getDefaultCategoryNo();
|
|
310
310
|
const { editorId, editorSource, token } = await getEditorInfo(resolvedCategoryNo);
|
|
311
311
|
|
|
312
|
-
// content
|
|
312
|
+
// Use content as-is if already a component array, otherwise empty array
|
|
313
313
|
const contentComponents = Array.isArray(content) ? content : [];
|
|
314
314
|
|
|
315
315
|
const titleComponent = {
|
|
@@ -396,12 +396,12 @@ const createNaverApiClient = ({ sessionPath }) => {
|
|
|
396
396
|
});
|
|
397
397
|
|
|
398
398
|
if (!response.ok) {
|
|
399
|
-
throw new Error(
|
|
399
|
+
throw new Error(`Post publish failed: ${response.status}`);
|
|
400
400
|
}
|
|
401
401
|
|
|
402
402
|
const json = await response.json();
|
|
403
403
|
if (!json.isSuccess) {
|
|
404
|
-
throw new Error(
|
|
404
|
+
throw new Error(`Post publish failed: ${JSON.stringify(json).slice(0, 200)}`);
|
|
405
405
|
}
|
|
406
406
|
|
|
407
407
|
const redirectUrl = json.result?.redirectUrl || '';
|
|
@@ -432,7 +432,7 @@ const createNaverApiClient = ({ sessionPath }) => {
|
|
|
432
432
|
|
|
433
433
|
let json;
|
|
434
434
|
try {
|
|
435
|
-
//
|
|
435
|
+
// Naver response may contain invalid escape sequences (\'), so sanitize
|
|
436
436
|
const sanitized = text.replace(/\\'/g, "'");
|
|
437
437
|
json = JSON.parse(sanitized);
|
|
438
438
|
} catch {
|
|
@@ -18,7 +18,7 @@ const createProviderManager = () => {
|
|
|
18
18
|
const getProvider = (provider = 'tistory') => {
|
|
19
19
|
const normalized = String(provider || 'tistory').toLowerCase();
|
|
20
20
|
if (!providerFactory[normalized]) {
|
|
21
|
-
throw new Error(
|
|
21
|
+
throw new Error(`Unsupported provider: ${provider}. Available options: ${providers.join(', ')}`);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
if (!cache.has(normalized)) {
|