viruagent-cli 0.6.0 → 0.6.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/package.json +1 -1
- package/skills/va-insta/SKILL.md +2 -1
- package/skills/va-insta-publish/SKILL.md +157 -0
- package/skills/va-naver-publish/SKILL.md +272 -32
- package/skills/va-shared/SKILL.md +53 -15
- package/skills/va-tistory-publish/SKILL.md +379 -33
- package/src/providers/chromeManager.js +186 -0
- package/src/providers/insta/index.js +6 -6
- package/src/providers/insta/session.js +4 -7
- package/src/providers/naver/auth.js +37 -23
- package/src/providers/naver/index.js +6 -10
- package/src/providers/naver/session.js +22 -28
- package/src/providers/tistory/auth.js +129 -105
- package/src/providers/tistory/index.js +16 -7
- package/src/providers/tistory/session.js +66 -24
- package/src/runner.js +2 -1
- package/src/services/providerManager.js +7 -5
- package/src/storage/sessionStore.js +18 -9
|
@@ -3,19 +3,67 @@ const path = require('path');
|
|
|
3
3
|
const { saveProviderMeta } = require('../../storage/sessionStore');
|
|
4
4
|
const { sleep, readCredentialsFromEnv, parseSessionError, buildLoginErrorMessage } = require('./utils');
|
|
5
5
|
const { clickKakaoAccountContinue } = require('./browserHelpers');
|
|
6
|
+
const { extractAllCookies, filterCookies, cookiesToSessionFormat } = require('../chromeManager');
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
// ── Rate Limit (일일 발행 제한) ──
|
|
9
|
+
const DAILY_LIMIT = { publish: 15 };
|
|
10
|
+
|
|
11
|
+
const readSessionFile = (sessionPath) => {
|
|
12
|
+
if (!fs.existsSync(sessionPath)) return null;
|
|
13
|
+
try { return JSON.parse(fs.readFileSync(sessionPath, 'utf-8')); } catch { return null; }
|
|
14
|
+
};
|
|
15
|
+
const writeSessionFile = (sessionPath, data) => {
|
|
16
|
+
fs.mkdirSync(path.dirname(sessionPath), { recursive: true });
|
|
17
|
+
fs.writeFileSync(sessionPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const checkAndIncrementRateLimit = (sessionPath, type) => {
|
|
21
|
+
const raw = readSessionFile(sessionPath) || {};
|
|
22
|
+
if (!raw.rateLimits) raw.rateLimits = {};
|
|
23
|
+
const c = raw.rateLimits[type] || { daily: 0, dayStart: Date.now() };
|
|
24
|
+
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
if (now - c.dayStart > 86400000) { c.daily = 0; c.dayStart = now; }
|
|
27
|
+
|
|
28
|
+
const dailyMax = DAILY_LIMIT[type];
|
|
29
|
+
if (dailyMax && c.daily >= dailyMax) {
|
|
30
|
+
throw new Error(`daily_limit: ${type} exceeded daily limit of ${dailyMax}. Try again tomorrow.`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
c.daily++;
|
|
34
|
+
raw.rateLimits[type] = { ...c, savedAt: new Date().toISOString() };
|
|
35
|
+
writeSessionFile(sessionPath, raw);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const getRateLimitStatus = (sessionPath) => {
|
|
39
|
+
const raw = readSessionFile(sessionPath) || {};
|
|
40
|
+
const result = {};
|
|
41
|
+
for (const [type, max] of Object.entries(DAILY_LIMIT)) {
|
|
42
|
+
const c = raw.rateLimits?.[type] || { daily: 0 };
|
|
43
|
+
result[type] = { daily: `${c.daily}/${max}` };
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const isLoggedInByCookies = async (context, page) => {
|
|
49
|
+
try {
|
|
50
|
+
// Use CDP to get all cookies including httpOnly (TSSESSION)
|
|
51
|
+
const all = await extractAllCookies(context, page);
|
|
52
|
+
return all.some((c) => c.domain.includes('tistory') && c.name === 'TSSESSION');
|
|
53
|
+
} catch {
|
|
54
|
+
// Fallback to context.cookies if CDP fails
|
|
55
|
+
const cookies = await context.cookies('https://www.tistory.com');
|
|
56
|
+
return cookies.some((cookie) => {
|
|
57
|
+
const name = cookie.name.toLowerCase();
|
|
58
|
+
return name.includes('tistory') || name.includes('access') || name.includes('login');
|
|
59
|
+
});
|
|
60
|
+
}
|
|
13
61
|
};
|
|
14
62
|
|
|
15
63
|
const waitForLoginFinish = async (page, context, timeoutMs = 45000) => {
|
|
16
64
|
const deadline = Date.now() + timeoutMs;
|
|
17
65
|
while (Date.now() < deadline) {
|
|
18
|
-
if (await isLoggedInByCookies(context)) {
|
|
66
|
+
if (await isLoggedInByCookies(context, page)) {
|
|
19
67
|
return true;
|
|
20
68
|
}
|
|
21
69
|
|
|
@@ -33,16 +81,11 @@ const waitForLoginFinish = async (page, context, timeoutMs = 45000) => {
|
|
|
33
81
|
return false;
|
|
34
82
|
};
|
|
35
83
|
|
|
36
|
-
const persistTistorySession = async (context, targetSessionPath) => {
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
size: undefined,
|
|
42
|
-
partitionKey: undefined,
|
|
43
|
-
sourcePort: undefined,
|
|
44
|
-
sourceScheme: undefined,
|
|
45
|
-
}));
|
|
84
|
+
const persistTistorySession = async (context, page, targetSessionPath) => {
|
|
85
|
+
// Use CDP Network.getAllCookies to capture httpOnly cookies (e.g. TSSESSION)
|
|
86
|
+
const all = await extractAllCookies(context, page);
|
|
87
|
+
const tistoryCookies = filterCookies(all, ['tistory.com']);
|
|
88
|
+
const sanitized = cookiesToSessionFormat(tistoryCookies);
|
|
46
89
|
|
|
47
90
|
const payload = {
|
|
48
91
|
cookies: sanitized,
|
|
@@ -60,16 +103,13 @@ const persistTistorySession = async (context, targetSessionPath) => {
|
|
|
60
103
|
* withProviderSession factory.
|
|
61
104
|
* Receives askForAuthentication via dependency injection to avoid scope issues.
|
|
62
105
|
*/
|
|
63
|
-
const createWithProviderSession = (askForAuthentication) => async (fn) => {
|
|
106
|
+
const createWithProviderSession = (askForAuthentication, account) => async (fn) => {
|
|
64
107
|
const credentials = readCredentialsFromEnv();
|
|
65
108
|
const hasCredentials = Boolean(credentials.username && credentials.password);
|
|
66
109
|
|
|
67
110
|
try {
|
|
68
111
|
const result = await fn();
|
|
69
|
-
saveProviderMeta('tistory', {
|
|
70
|
-
loggedIn: true,
|
|
71
|
-
lastValidatedAt: new Date().toISOString(),
|
|
72
|
-
});
|
|
112
|
+
saveProviderMeta('tistory', { loggedIn: true, lastValidatedAt: new Date().toISOString() }, account);
|
|
73
113
|
return result;
|
|
74
114
|
} catch (error) {
|
|
75
115
|
if (!parseSessionError(error) || !hasCredentials) {
|
|
@@ -91,7 +131,7 @@ const createWithProviderSession = (askForAuthentication) => async (fn) => {
|
|
|
91
131
|
sessionPath: loginResult.sessionPath,
|
|
92
132
|
lastRefreshedAt: new Date().toISOString(),
|
|
93
133
|
lastError: null,
|
|
94
|
-
});
|
|
134
|
+
}, account);
|
|
95
135
|
|
|
96
136
|
if (!loginResult.loggedIn) {
|
|
97
137
|
throw new Error(loginResult.message || 'Login status could not be confirmed after session refresh.');
|
|
@@ -103,7 +143,7 @@ const createWithProviderSession = (askForAuthentication) => async (fn) => {
|
|
|
103
143
|
loggedIn: false,
|
|
104
144
|
lastError: buildLoginErrorMessage(reloginError),
|
|
105
145
|
lastValidatedAt: new Date().toISOString(),
|
|
106
|
-
});
|
|
146
|
+
}, account);
|
|
107
147
|
throw reloginError;
|
|
108
148
|
}
|
|
109
149
|
}
|
|
@@ -114,4 +154,6 @@ module.exports = {
|
|
|
114
154
|
waitForLoginFinish,
|
|
115
155
|
persistTistorySession,
|
|
116
156
|
createWithProviderSession,
|
|
157
|
+
checkAndIncrementRateLimit,
|
|
158
|
+
getRateLimitStatus,
|
|
117
159
|
};
|
package/src/runner.js
CHANGED
|
@@ -86,9 +86,10 @@ const runCommand = async (command, opts = {}) => {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
const providerName = opts.provider || 'tistory';
|
|
89
|
+
const account = opts.account || undefined;
|
|
89
90
|
let provider;
|
|
90
91
|
try {
|
|
91
|
-
provider = manager.getProvider(providerName);
|
|
92
|
+
provider = manager.getProvider(providerName, account);
|
|
92
93
|
} catch {
|
|
93
94
|
throw createError(
|
|
94
95
|
'PROVIDER_NOT_FOUND',
|
|
@@ -15,23 +15,25 @@ const providers = ['tistory', 'naver', 'insta'];
|
|
|
15
15
|
const createProviderManager = () => {
|
|
16
16
|
const cache = new Map();
|
|
17
17
|
|
|
18
|
-
const getProvider = (provider = 'tistory') => {
|
|
18
|
+
const getProvider = (provider = 'tistory', account) => {
|
|
19
19
|
const normalized = String(provider || 'tistory').toLowerCase();
|
|
20
20
|
if (!providerFactory[normalized]) {
|
|
21
21
|
throw new Error(`Unsupported provider: ${provider}. Available options: ${providers.join(', ')}`);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
const cacheKey = account ? `${normalized}:${String(account).toLowerCase()}` : normalized;
|
|
25
|
+
if (!cache.has(cacheKey)) {
|
|
26
|
+
const sessionPath = getSessionPath(normalized, account);
|
|
26
27
|
const options = {
|
|
27
28
|
provider: normalized,
|
|
28
29
|
sessionPath,
|
|
30
|
+
account: account || undefined,
|
|
29
31
|
};
|
|
30
32
|
const providerInstance = providerFactory[normalized](options);
|
|
31
|
-
cache.set(
|
|
33
|
+
cache.set(cacheKey, providerInstance);
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
return cache.get(
|
|
36
|
+
return cache.get(cacheKey);
|
|
35
37
|
};
|
|
36
38
|
|
|
37
39
|
const providerNames = { tistory: 'Tistory', naver: 'Naver Blog', insta: 'Instagram' };
|
|
@@ -27,9 +27,11 @@ const writeJson = (target, data) => {
|
|
|
27
27
|
fs.writeFileSync(target, JSON.stringify(data, null, 2), 'utf-8');
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
const getSessionPath = (provider) => {
|
|
30
|
+
const getSessionPath = (provider, account) => {
|
|
31
31
|
ensureDir(SESSION_DIR);
|
|
32
|
-
|
|
32
|
+
const base = normalizeProvider(provider);
|
|
33
|
+
const suffix = account ? `-${String(account).toLowerCase().replace(/[^a-z0-9._-]/g, '_')}` : '';
|
|
34
|
+
return path.join(SESSION_DIR, `${base}${suffix}-session.json`);
|
|
33
35
|
};
|
|
34
36
|
|
|
35
37
|
const getProvidersMeta = () => {
|
|
@@ -37,25 +39,32 @@ const getProvidersMeta = () => {
|
|
|
37
39
|
return readJson(META_FILE);
|
|
38
40
|
};
|
|
39
41
|
|
|
40
|
-
const
|
|
42
|
+
const metaKey = (provider, account) => {
|
|
43
|
+
const base = normalizeProvider(provider);
|
|
44
|
+
return account ? `${base}:${String(account).toLowerCase()}` : base;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const saveProviderMeta = (provider, patch, account) => {
|
|
41
48
|
const meta = getProvidersMeta();
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
const key = metaKey(provider, account);
|
|
50
|
+
meta[key] = {
|
|
51
|
+
...(meta[key] || {}),
|
|
44
52
|
...patch,
|
|
45
53
|
provider: normalizeProvider(provider),
|
|
54
|
+
...(account ? { account: String(account).toLowerCase() } : {}),
|
|
46
55
|
updatedAt: new Date().toISOString(),
|
|
47
56
|
};
|
|
48
57
|
writeJson(META_FILE, meta);
|
|
49
58
|
};
|
|
50
59
|
|
|
51
|
-
const getProviderMeta = (provider) => {
|
|
60
|
+
const getProviderMeta = (provider, account) => {
|
|
52
61
|
const meta = getProvidersMeta();
|
|
53
|
-
return meta[
|
|
62
|
+
return meta[metaKey(provider, account)] || null;
|
|
54
63
|
};
|
|
55
64
|
|
|
56
|
-
const clearProviderMeta = (provider) => {
|
|
65
|
+
const clearProviderMeta = (provider, account) => {
|
|
57
66
|
const meta = getProvidersMeta();
|
|
58
|
-
delete meta[
|
|
67
|
+
delete meta[metaKey(provider, account)];
|
|
59
68
|
writeJson(META_FILE, meta);
|
|
60
69
|
};
|
|
61
70
|
|