viruagent-cli 0.4.0 → 0.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/README.ko.md +7 -7
- package/README.md +7 -7
- package/bin/index.js +6 -0
- package/package.json +1 -1
- package/src/providers/insta/apiClient.js +57 -0
- package/src/providers/insta/auth.js +55 -4
- package/src/providers/insta/index.js +10 -0
- package/src/runner.js +3 -0
package/README.ko.md
CHANGED
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
|
|
27
27
|
| 플랫폼 | 로그인 | 주요 기능 | 가이드 |
|
|
28
28
|
|--------|--------|----------|--------|
|
|
29
|
-
| **Tistory** | Playwright (카카오) | 글 발행, 임시저장, 카테고리, 이미지 업로드 | [가이드](docs/guide-tistory.md) |
|
|
30
|
-
| **Naver Blog** | Playwright (네이버) | 글 발행, 카테고리, SE Editor, 이미지 업로드 | [가이드](docs/guide-naver.md) |
|
|
31
|
-
| **Instagram** | HTTP (브라우저 불필요) | 좋아요, 댓글, 팔로우, 포스팅, 프로필, 피드 | [가이드](docs/guide-instagram.md) |
|
|
29
|
+
| **Tistory** | Playwright (카카오) | 글 발행, 임시저장, 카테고리, 이미지 업로드 | [가이드](docs/ko/guide-tistory.md) |
|
|
30
|
+
| **Naver Blog** | Playwright (네이버) | 글 발행, 카테고리, SE Editor, 이미지 업로드 | [가이드](docs/ko/guide-naver.md) |
|
|
31
|
+
| **Instagram** | HTTP (브라우저 불필요) | 좋아요, 댓글, 팔로우, 포스팅, 프로필, 피드 | [가이드](docs/ko/guide-instagram.md) |
|
|
32
32
|
|
|
33
33
|
## 동작 방식
|
|
34
34
|
|
|
@@ -93,7 +93,7 @@ npx viruagent-cli login --provider insta --username <인스타 ID> --password <
|
|
|
93
93
|
```
|
|
94
94
|
> "인스타 로그인해줘" — 에이전트가 알아서 처리
|
|
95
95
|
>
|
|
96
|
-
> 전체 API 레퍼런스와 rate limit 규칙은 [Instagram 가이드](docs/guide-instagram.md)를 참고하세요.
|
|
96
|
+
> 전체 API 레퍼런스와 rate limit 규칙은 [Instagram 가이드](docs/ko/guide-instagram.md)를 참고하세요.
|
|
97
97
|
|
|
98
98
|
## 사용법
|
|
99
99
|
|
|
@@ -110,9 +110,9 @@ npx viruagent-cli login --provider insta --username <인스타 ID> --password <
|
|
|
110
110
|
|
|
111
111
|
## 플랫폼별 가이드
|
|
112
112
|
|
|
113
|
-
- **[Tistory 가이드](docs/guide-tistory.md)** — 블로그 발행, 이미지 업로드, 카테고리
|
|
114
|
-
- **[Naver Blog 가이드](docs/guide-naver.md)** — SE Editor, 블로그 발행, 이미지 업로드
|
|
115
|
-
- **[Instagram 가이드](docs/guide-instagram.md)** — 18개 API 메서드, rate limit 규칙, AI 댓글
|
|
113
|
+
- **[Tistory 가이드](docs/ko/guide-tistory.md)** — 블로그 발행, 이미지 업로드, 카테고리
|
|
114
|
+
- **[Naver Blog 가이드](docs/ko/guide-naver.md)** — SE Editor, 블로그 발행, 이미지 업로드
|
|
115
|
+
- **[Instagram 가이드](docs/ko/guide-instagram.md)** — 18개 API 메서드, rate limit 규칙, AI 댓글
|
|
116
116
|
|
|
117
117
|
## 지원 환경
|
|
118
118
|
|
package/README.md
CHANGED
|
@@ -26,9 +26,9 @@ Designed not for humans, but for **AI agents**.
|
|
|
26
26
|
|
|
27
27
|
| Platform | Login | Features | Guide |
|
|
28
28
|
|----------|-------|----------|-------|
|
|
29
|
-
| **Tistory** | Playwright (Kakao) | Publish, Draft, Categories, Image Upload | [Guide](docs/guide-tistory.md) |
|
|
30
|
-
| **Naver Blog** | Playwright (Naver) | Publish, Categories, SE Editor, Image Upload | [Guide](docs/guide-naver.md) |
|
|
31
|
-
| **Instagram** | HTTP (No Browser) | Like, Comment, Follow, Post, Profile, Feed, Rate Limit | [Guide](docs/guide-instagram.md) |
|
|
29
|
+
| **Tistory** | Playwright (Kakao) | Publish, Draft, Categories, Image Upload | [Guide](docs/en/guide-tistory.md) |
|
|
30
|
+
| **Naver Blog** | Playwright (Naver) | Publish, Categories, SE Editor, Image Upload | [Guide](docs/en/guide-naver.md) |
|
|
31
|
+
| **Instagram** | HTTP (No Browser) | Like, Comment, Follow, Post, Profile, Feed, Rate Limit | [Guide](docs/en/guide-instagram.md) |
|
|
32
32
|
|
|
33
33
|
## How It Works
|
|
34
34
|
|
|
@@ -93,7 +93,7 @@ npx viruagent-cli login --provider insta --username <id> --password <pw>
|
|
|
93
93
|
```
|
|
94
94
|
> "Login to Instagram" — Agent handles it automatically
|
|
95
95
|
>
|
|
96
|
-
> See the [Instagram Guide](docs/guide-instagram.md) for full API reference and rate limit rules.
|
|
96
|
+
> See the [Instagram Guide](docs/en/guide-instagram.md) for full API reference and rate limit rules.
|
|
97
97
|
|
|
98
98
|
## Usage
|
|
99
99
|
|
|
@@ -110,9 +110,9 @@ npx viruagent-cli login --provider insta --username <id> --password <pw>
|
|
|
110
110
|
|
|
111
111
|
## Platform Guides
|
|
112
112
|
|
|
113
|
-
- **[Tistory Guide](docs/guide-tistory.
|
|
114
|
-
- **[Naver Blog Guide](docs/guide-naver.
|
|
115
|
-
- **[Instagram Guide](docs/guide-instagram.
|
|
113
|
+
- **[Tistory Guide](docs/en/guide-tistory.md)** — Blog publishing, image upload, categories
|
|
114
|
+
- **[Naver Blog Guide](docs/en/guide-naver.md)** — SE Editor, blog publishing, image upload
|
|
115
|
+
- **[Instagram Guide](docs/en/guide-instagram.md)** — 18 API methods, rate limits, AI commenting
|
|
116
116
|
|
|
117
117
|
## Supported Environments
|
|
118
118
|
|
package/bin/index.js
CHANGED
|
@@ -212,6 +212,12 @@ analyzePostCmd
|
|
|
212
212
|
.option('--post-id <shortcode>', 'Post shortcode')
|
|
213
213
|
.action((opts) => execute('analyze-post', opts));
|
|
214
214
|
|
|
215
|
+
const resolveChallengeCmd = program
|
|
216
|
+
.command('resolve-challenge')
|
|
217
|
+
.description('Resolve Instagram challenge (auto-verify identity)');
|
|
218
|
+
addProviderOption(resolveChallengeCmd);
|
|
219
|
+
resolveChallengeCmd.action((opts) => execute('resolve-challenge', opts));
|
|
220
|
+
|
|
215
221
|
const rateLimitCmd = program
|
|
216
222
|
.command('rate-limit-status')
|
|
217
223
|
.description('Show current rate limit usage');
|
package/package.json
CHANGED
|
@@ -174,9 +174,36 @@ const createInstaApiClient = ({ sessionPath }) => {
|
|
|
174
174
|
if (location.includes('/accounts/login')) {
|
|
175
175
|
throw new Error('세션이 만료되었습니다. 다시 로그인해 주세요.');
|
|
176
176
|
}
|
|
177
|
+
if (location.includes('/challenge')) {
|
|
178
|
+
// challenge 자동 해결 시도
|
|
179
|
+
const resolved = await resolveChallenge();
|
|
180
|
+
if (resolved) {
|
|
181
|
+
// 해결 후 원래 요청 재시도
|
|
182
|
+
return fetch(url, { ...options, headers, redirect: 'manual' });
|
|
183
|
+
}
|
|
184
|
+
throw new Error('challenge_required: 본인 인증이 필요합니다. 브라우저에서 수동으로 처리해 주세요.');
|
|
185
|
+
}
|
|
177
186
|
throw new Error(`리다이렉트 발생: ${res.status} → ${location}`);
|
|
178
187
|
}
|
|
179
188
|
|
|
189
|
+
// challenge_required JSON 응답 처리
|
|
190
|
+
if (res.status === 400 && !options.allowError) {
|
|
191
|
+
const cloned = res.clone();
|
|
192
|
+
try {
|
|
193
|
+
const data = await cloned.json();
|
|
194
|
+
if (data.message === 'challenge_required') {
|
|
195
|
+
const resolved = await resolveChallenge();
|
|
196
|
+
if (resolved) {
|
|
197
|
+
return fetch(url, { ...options, headers, redirect: 'manual' });
|
|
198
|
+
}
|
|
199
|
+
throw new Error('challenge_required: 본인 인증이 필요합니다.');
|
|
200
|
+
}
|
|
201
|
+
} catch (e) {
|
|
202
|
+
if (e.message.includes('challenge_required')) throw e;
|
|
203
|
+
// JSON 파싱 실패는 무시하고 원래 흐름
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
180
207
|
if (res.status === 401 || res.status === 403) {
|
|
181
208
|
throw new Error(`인증 오류 (${res.status}). 다시 로그인해 주세요.`);
|
|
182
209
|
}
|
|
@@ -316,6 +343,35 @@ const createInstaApiClient = ({ sessionPath }) => {
|
|
|
316
343
|
};
|
|
317
344
|
};
|
|
318
345
|
|
|
346
|
+
// ── Challenge 자동 해결 ──
|
|
347
|
+
|
|
348
|
+
const resolveChallenge = async () => {
|
|
349
|
+
try {
|
|
350
|
+
const cookies = getCookies();
|
|
351
|
+
const res = await fetch('https://www.instagram.com/api/v1/challenge/web/action/', {
|
|
352
|
+
method: 'POST',
|
|
353
|
+
headers: {
|
|
354
|
+
'User-Agent': USER_AGENT,
|
|
355
|
+
'X-IG-App-ID': IG_APP_ID,
|
|
356
|
+
'X-CSRFToken': getCsrfToken(),
|
|
357
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
358
|
+
'X-Instagram-AJAX': '1',
|
|
359
|
+
Referer: 'https://www.instagram.com/challenge/',
|
|
360
|
+
Origin: 'https://www.instagram.com',
|
|
361
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
362
|
+
Cookie: cookiesToHeader(cookies),
|
|
363
|
+
},
|
|
364
|
+
body: 'choice=0',
|
|
365
|
+
redirect: 'manual',
|
|
366
|
+
});
|
|
367
|
+
if (!res.ok) return false;
|
|
368
|
+
const data = await res.json();
|
|
369
|
+
return data.status === 'ok';
|
|
370
|
+
} catch {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
|
|
319
375
|
const parseLikeResponse = async (res) => {
|
|
320
376
|
if (res.ok) return res.json();
|
|
321
377
|
if (res.status === 400) {
|
|
@@ -611,6 +667,7 @@ const createInstaApiClient = ({ sessionPath }) => {
|
|
|
611
667
|
configurePost,
|
|
612
668
|
publishPost,
|
|
613
669
|
deletePost,
|
|
670
|
+
resolveChallenge,
|
|
614
671
|
resetState,
|
|
615
672
|
getRateLimitStatus: () => {
|
|
616
673
|
const status = {};
|
|
@@ -99,10 +99,61 @@ const createAskForAuthentication = ({ sessionPath }) => async ({
|
|
|
99
99
|
|
|
100
100
|
const loginData = await loginRes.json();
|
|
101
101
|
|
|
102
|
-
if (loginData.checkpoint_url) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
if (loginData.checkpoint_url || loginData.message === 'challenge_required') {
|
|
103
|
+
// challenge 자동 해결 시도 (choice=0 = 본인입니다)
|
|
104
|
+
const challengeRes = await fetch('https://www.instagram.com/api/v1/challenge/web/action/', {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: {
|
|
107
|
+
'User-Agent': USER_AGENT,
|
|
108
|
+
'X-CSRFToken': csrfToken,
|
|
109
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
110
|
+
'X-Instagram-AJAX': '1',
|
|
111
|
+
'X-IG-App-ID': IG_APP_ID,
|
|
112
|
+
Referer: 'https://www.instagram.com/challenge/',
|
|
113
|
+
Origin: 'https://www.instagram.com',
|
|
114
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
115
|
+
Cookie: cookieHeader,
|
|
116
|
+
},
|
|
117
|
+
body: 'choice=0',
|
|
118
|
+
redirect: 'manual',
|
|
119
|
+
});
|
|
120
|
+
const challengeData = await challengeRes.json().catch(() => ({}));
|
|
121
|
+
if (challengeData.status !== 'ok') {
|
|
122
|
+
throw new Error(
|
|
123
|
+
'challenge_required: 자동 해결 실패. 브라우저에서 본인 인증을 완료해 주세요. ' +
|
|
124
|
+
(loginData.checkpoint_url || ''),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
// challenge 해결 후 재로그인
|
|
128
|
+
const retryRes = await fetch('https://www.instagram.com/api/v1/web/accounts/login/ajax/', {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: {
|
|
131
|
+
'User-Agent': USER_AGENT,
|
|
132
|
+
'X-CSRFToken': csrfToken,
|
|
133
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
134
|
+
'X-IG-App-ID': IG_APP_ID,
|
|
135
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
136
|
+
Referer: 'https://www.instagram.com/accounts/login/',
|
|
137
|
+
Origin: 'https://www.instagram.com',
|
|
138
|
+
Cookie: cookieHeader,
|
|
139
|
+
},
|
|
140
|
+
body: body.toString(),
|
|
141
|
+
redirect: 'manual',
|
|
142
|
+
});
|
|
143
|
+
const retryCookies = parseCookiesFromHeaders(retryRes.headers);
|
|
144
|
+
cookies = mergeCookies(cookies, retryCookies);
|
|
145
|
+
const retryData = await retryRes.json().catch(() => ({}));
|
|
146
|
+
if (retryData.authenticated) {
|
|
147
|
+
saveInstaSession(sessionPath, cookies);
|
|
148
|
+
return {
|
|
149
|
+
provider: 'insta',
|
|
150
|
+
loggedIn: true,
|
|
151
|
+
userId: retryData.userId || null,
|
|
152
|
+
username: resolvedUsername,
|
|
153
|
+
sessionPath,
|
|
154
|
+
challengeResolved: true,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
106
157
|
}
|
|
107
158
|
|
|
108
159
|
if (loginData.two_factor_required) {
|
|
@@ -271,6 +271,16 @@ const createInstaProvider = ({ sessionPath }) => {
|
|
|
271
271
|
});
|
|
272
272
|
},
|
|
273
273
|
|
|
274
|
+
async resolveChallenge() {
|
|
275
|
+
const resolved = await instaApi.resolveChallenge();
|
|
276
|
+
return {
|
|
277
|
+
provider: 'insta',
|
|
278
|
+
mode: 'resolveChallenge',
|
|
279
|
+
resolved,
|
|
280
|
+
message: resolved ? 'Challenge 해결 완료' : 'Challenge 해결 실패. 브라우저에서 수동으로 처리해 주세요.',
|
|
281
|
+
};
|
|
282
|
+
},
|
|
283
|
+
|
|
274
284
|
rateLimitStatus() {
|
|
275
285
|
return {
|
|
276
286
|
provider: 'insta',
|
package/src/runner.js
CHANGED
|
@@ -237,6 +237,9 @@ const runCommand = async (command, opts = {}) => {
|
|
|
237
237
|
}
|
|
238
238
|
return withProvider(() => provider.analyzePost({ postId: opts.postId }))();
|
|
239
239
|
|
|
240
|
+
case 'resolve-challenge':
|
|
241
|
+
return withProvider(() => provider.resolveChallenge())();
|
|
242
|
+
|
|
240
243
|
case 'rate-limit-status':
|
|
241
244
|
return withProvider(() => Promise.resolve(provider.rateLimitStatus()))();
|
|
242
245
|
|