viruagent 1.0.0 → 1.1.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/README.md +12 -13
- package/package.json +10 -5
- package/src/agent.js +179 -22
- package/src/cli-post.js +11 -1
- package/src/lib/ai.js +14 -15
- package/src/lib/unsplash.js +5 -7
- package/.env.example +0 -2
package/README.md
CHANGED
|
@@ -28,22 +28,21 @@
|
|
|
28
28
|
**필요한 것**: Node.js 18+, OpenAI API Key
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
npm install
|
|
34
|
-
cp .env.example .env
|
|
35
|
-
# .env 파일 편집
|
|
36
|
-
npm start
|
|
31
|
+
npm install -g viruagent
|
|
32
|
+
viruagent
|
|
37
33
|
```
|
|
38
34
|
|
|
39
|
-
|
|
35
|
+
또는 설치 없이 바로 실행:
|
|
40
36
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
37
|
+
```bash
|
|
38
|
+
npx viruagent
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
최초 실행 시 OpenAI API Key를 입력하면 `~/.viruagent/config.json`에 자동 저장됩니다.
|
|
42
|
+
Unsplash API Key는 `/set api` 명령어로 나중에 설정할 수 있습니다.
|
|
45
43
|
|
|
46
|
-
|
|
44
|
+
- OpenAI API Key: [platform.openai.com/api-keys](https://platform.openai.com/api-keys)
|
|
45
|
+
- Unsplash API Key (선택): [unsplash.com/developers](https://unsplash.com/developers) (시간당 50회 제한)
|
|
47
46
|
|
|
48
47
|
---
|
|
49
48
|
|
|
@@ -103,7 +102,7 @@ node src/cli-post.js --topic "주제" --draft
|
|
|
103
102
|
|
|
104
103
|
## Unsplash 이미지 연동
|
|
105
104
|
|
|
106
|
-
글 생성 시 본문에 `<!-- IMAGE: keyword -->` 플레이스홀더가 자동 삽입됩니다.
|
|
105
|
+
글 생성 시 본문에 `<!-- IMAGE: keyword -->` 플레이스홀더가 자동 삽입됩니다. Unsplash API Key가 설정되어 있으면 (`/set api`):
|
|
107
106
|
|
|
108
107
|
1. 키워드로 Unsplash에서 이미지 검색
|
|
109
108
|
2. 이미지 다운로드 → 티스토리에 업로드
|
package/package.json
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viruagent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "AI 기반 티스토리 블로그 자동 발행 CLI 도구",
|
|
5
5
|
"main": "src/agent.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"viruagent": "
|
|
7
|
+
"viruagent": "bin/viruagent.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node --no-deprecation src/agent.js",
|
|
11
11
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
12
12
|
},
|
|
13
|
-
"keywords": [
|
|
13
|
+
"keywords": [
|
|
14
|
+
"tistory",
|
|
15
|
+
"blog",
|
|
16
|
+
"openai",
|
|
17
|
+
"cli",
|
|
18
|
+
"ai"
|
|
19
|
+
],
|
|
14
20
|
"author": "tkman",
|
|
15
21
|
"license": "MIT",
|
|
16
22
|
"repository": {
|
|
17
23
|
"type": "git",
|
|
18
|
-
"url": "https://github.com/greekr4/Viruagent.git"
|
|
24
|
+
"url": "git+https://github.com/greekr4/Viruagent.git"
|
|
19
25
|
},
|
|
20
26
|
"homepage": "https://github.com/greekr4/Viruagent#readme",
|
|
21
27
|
"engines": {
|
|
@@ -24,7 +30,6 @@
|
|
|
24
30
|
"type": "commonjs",
|
|
25
31
|
"dependencies": {
|
|
26
32
|
"chalk": "^4.1.2",
|
|
27
|
-
"dotenv": "^16.4.7",
|
|
28
33
|
"oh-my-logo": "^0.4.0",
|
|
29
34
|
"openai": "^4.77.0",
|
|
30
35
|
"playwright": "^1.58.2"
|
package/src/agent.js
CHANGED
|
@@ -63,23 +63,32 @@ const selectMenu = (items, title = '선택하세요') =>
|
|
|
63
63
|
// 설정 저장/로드
|
|
64
64
|
const fs = require('fs');
|
|
65
65
|
const path = require('path');
|
|
66
|
-
const
|
|
66
|
+
const os = require('os');
|
|
67
|
+
const CONFIG_DIR = path.join(os.homedir(), '.viruagent');
|
|
68
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
67
69
|
|
|
68
70
|
const DEFAULTS = { category: 0, visibility: 20, model: 'gpt-4o-mini', tone: loadConfig().defaultTone };
|
|
69
71
|
|
|
70
72
|
const visLabel = (v) => (v === 20 ? '공개 발행' : v === 15 ? '보호 발행' : '비공개 발행');
|
|
71
73
|
|
|
74
|
+
const ensureConfigDir = () => {
|
|
75
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
76
|
+
};
|
|
77
|
+
|
|
72
78
|
const loadSettings = () => {
|
|
73
79
|
try {
|
|
74
|
-
return { ...DEFAULTS, ...JSON.parse(fs.readFileSync(
|
|
80
|
+
return { ...DEFAULTS, ...JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')) };
|
|
75
81
|
} catch {
|
|
76
82
|
return { ...DEFAULTS };
|
|
77
83
|
}
|
|
78
84
|
};
|
|
79
85
|
|
|
80
86
|
const saveSettings = () => {
|
|
87
|
+
ensureConfigDir();
|
|
88
|
+
const current = loadSettings();
|
|
81
89
|
const { category, visibility, model, tone } = state;
|
|
82
|
-
|
|
90
|
+
const merged = { ...current, category, visibility, model, tone };
|
|
91
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2));
|
|
83
92
|
};
|
|
84
93
|
|
|
85
94
|
const saved = loadSettings();
|
|
@@ -115,7 +124,8 @@ const animateBanner = async () => {
|
|
|
115
124
|
const text = figlet.textSync('ViruAgent', { font: 'ANSI Shadow' });
|
|
116
125
|
const lines = text.split('\n');
|
|
117
126
|
const totalLines = lines.length;
|
|
118
|
-
const
|
|
127
|
+
const { version } = require('../package.json');
|
|
128
|
+
const sub = ` 대화형 티스토리 블로그 에이전트 v${version}`;
|
|
119
129
|
|
|
120
130
|
console.log();
|
|
121
131
|
|
|
@@ -177,7 +187,7 @@ const COMMANDS = [
|
|
|
177
187
|
'/help',
|
|
178
188
|
'/exit',
|
|
179
189
|
];
|
|
180
|
-
const SET_KEYS = ['category', 'visibility', 'model', 'tone'];
|
|
190
|
+
const SET_KEYS = ['category', 'visibility', 'model', 'tone', 'api'];
|
|
181
191
|
|
|
182
192
|
const completer = (line) => {
|
|
183
193
|
// /set model <값> 자동완성
|
|
@@ -234,7 +244,7 @@ const COMMAND_HINTS = {
|
|
|
234
244
|
'/draft': '/draft',
|
|
235
245
|
'/list': '/list',
|
|
236
246
|
'/categories': '/categories',
|
|
237
|
-
'/set': '/set <category|visibility|model|tone>',
|
|
247
|
+
'/set': '/set <category|visibility|model|tone|api>',
|
|
238
248
|
'/login': '/login',
|
|
239
249
|
'/logout': '/logout',
|
|
240
250
|
'/help': '/help',
|
|
@@ -517,8 +527,53 @@ const commands = {
|
|
|
517
527
|
log.dim('취소됨');
|
|
518
528
|
}
|
|
519
529
|
}
|
|
530
|
+
} else if (key === 'api') {
|
|
531
|
+
const keys = loadApiKeys();
|
|
532
|
+
const mask = (v) => v ? `${v.slice(0, 8)}${'*'.repeat(8)}` : chalk.dim('(미설정)');
|
|
533
|
+
|
|
534
|
+
console.log();
|
|
535
|
+
log.title('API Key 설정');
|
|
536
|
+
console.log(` OpenAI: ${mask(keys.OPENAI_API_KEY)}`);
|
|
537
|
+
console.log(` Unsplash: ${mask(keys.UNSPLASH_ACCESS_KEY)}`);
|
|
538
|
+
console.log();
|
|
539
|
+
|
|
540
|
+
rl.pause();
|
|
541
|
+
const apiOptions = ['OpenAI API Key', 'Unsplash Access Key', '취소'];
|
|
542
|
+
const idx = await selectMenu(apiOptions, 'API Key 선택 (↑↓ 이동, Enter 선택, Esc 취소)');
|
|
543
|
+
rl.resume();
|
|
544
|
+
|
|
545
|
+
if (idx === 0) {
|
|
546
|
+
rl.pause();
|
|
547
|
+
const newKey = await askQuestion(chalk.cyan(' 새 OpenAI API Key: '));
|
|
548
|
+
rl.resume();
|
|
549
|
+
if (newKey) {
|
|
550
|
+
saveApiKeys({ OPENAI_API_KEY: newKey });
|
|
551
|
+
log.success('OpenAI API Key가 저장되었습니다.');
|
|
552
|
+
log.warn('다음 세션부터 적용됩니다. 프로그램을 재시작하세요.');
|
|
553
|
+
} else {
|
|
554
|
+
log.dim('변경 없음');
|
|
555
|
+
}
|
|
556
|
+
} else if (idx === 1) {
|
|
557
|
+
rl.pause();
|
|
558
|
+
const newKey = await askQuestion(chalk.cyan(' 새 Unsplash Access Key (삭제하려면 빈 값): '));
|
|
559
|
+
rl.resume();
|
|
560
|
+
if (newKey) {
|
|
561
|
+
saveApiKeys({ UNSPLASH_ACCESS_KEY: newKey });
|
|
562
|
+
log.success('Unsplash Access Key가 저장되었습니다.');
|
|
563
|
+
log.warn('다음 세션부터 적용됩니다. 프로그램을 재시작하세요.');
|
|
564
|
+
} else if (keys.UNSPLASH_ACCESS_KEY) {
|
|
565
|
+
saveApiKeys({ UNSPLASH_ACCESS_KEY: '' });
|
|
566
|
+
log.success('Unsplash Access Key가 삭제되었습니다.');
|
|
567
|
+
log.warn('다음 세션부터 적용됩니다. 프로그램을 재시작하세요.');
|
|
568
|
+
} else {
|
|
569
|
+
log.dim('변경 없음');
|
|
570
|
+
}
|
|
571
|
+
} else {
|
|
572
|
+
log.dim('취소됨');
|
|
573
|
+
}
|
|
574
|
+
return;
|
|
520
575
|
} else {
|
|
521
|
-
return log.warn('사용법: /set category | /set visibility | /set model | /set tone');
|
|
576
|
+
return log.warn('사용법: /set category | /set visibility | /set model | /set tone | /set api');
|
|
522
577
|
}
|
|
523
578
|
saveSettings();
|
|
524
579
|
},
|
|
@@ -538,6 +593,7 @@ ${chalk.cyan('/set category')} 카테고리 설정
|
|
|
538
593
|
${chalk.cyan('/set visibility')} 공개설정
|
|
539
594
|
${chalk.cyan('/set model')} AI 모델 선택
|
|
540
595
|
${chalk.cyan('/set tone')} 글쓰기 톤 설정
|
|
596
|
+
${chalk.cyan('/set api')} API Key 관리 (OpenAI, Unsplash)
|
|
541
597
|
${chalk.cyan('/login')} 티스토리 로그인
|
|
542
598
|
${chalk.cyan('/logout')} 로그아웃 (세션 삭제)
|
|
543
599
|
${chalk.cyan('/help')} 도움말
|
|
@@ -613,9 +669,102 @@ const handleInput = async (input) => {
|
|
|
613
669
|
}
|
|
614
670
|
};
|
|
615
671
|
|
|
672
|
+
const askQuestion = (query) =>
|
|
673
|
+
new Promise((resolve) => {
|
|
674
|
+
const tmpRl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
675
|
+
tmpRl.question(query, (answer) => {
|
|
676
|
+
tmpRl.close();
|
|
677
|
+
resolve(answer.trim());
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
const loadApiKeys = () => {
|
|
682
|
+
const settings = loadSettings();
|
|
683
|
+
return {
|
|
684
|
+
OPENAI_API_KEY: settings.openaiApiKey || '',
|
|
685
|
+
UNSPLASH_ACCESS_KEY: settings.unsplashAccessKey || '',
|
|
686
|
+
};
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
const saveApiKeys = (keys) => {
|
|
690
|
+
ensureConfigDir();
|
|
691
|
+
const current = loadSettings();
|
|
692
|
+
if (keys.OPENAI_API_KEY !== undefined) current.openaiApiKey = keys.OPENAI_API_KEY || undefined;
|
|
693
|
+
if (keys.UNSPLASH_ACCESS_KEY !== undefined) current.unsplashAccessKey = keys.UNSPLASH_ACCESS_KEY || undefined;
|
|
694
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(current, null, 2));
|
|
695
|
+
// 환경 변수에 반영
|
|
696
|
+
if (keys.OPENAI_API_KEY) process.env.OPENAI_API_KEY = keys.OPENAI_API_KEY;
|
|
697
|
+
if (keys.UNSPLASH_ACCESS_KEY) process.env.UNSPLASH_ACCESS_KEY = keys.UNSPLASH_ACCESS_KEY;
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const applyApiKeys = () => {
|
|
701
|
+
const keys = loadApiKeys();
|
|
702
|
+
if (keys.OPENAI_API_KEY) process.env.OPENAI_API_KEY = keys.OPENAI_API_KEY;
|
|
703
|
+
if (keys.UNSPLASH_ACCESS_KEY) process.env.UNSPLASH_ACCESS_KEY = keys.UNSPLASH_ACCESS_KEY;
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
const setupEnv = async () => {
|
|
707
|
+
applyApiKeys();
|
|
708
|
+
if (process.env.OPENAI_API_KEY) return;
|
|
709
|
+
|
|
710
|
+
console.log();
|
|
711
|
+
log.title('━━━ 초기 설정 ━━━');
|
|
712
|
+
console.log();
|
|
713
|
+
log.info('OpenAI API Key가 설정되지 않았습니다.\n');
|
|
714
|
+
log.dim(' https://platform.openai.com/api-keys 에서 발급받을 수 있습니다.\n');
|
|
715
|
+
|
|
716
|
+
const openaiKey = await askQuestion(chalk.cyan(' OpenAI API Key: '));
|
|
717
|
+
if (!openaiKey) {
|
|
718
|
+
log.error('OpenAI API Key는 필수입니다. 프로그램을 종료합니다.');
|
|
719
|
+
process.exit(1);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// OpenAI Key 검증
|
|
723
|
+
try {
|
|
724
|
+
const res = await fetch('https://api.openai.com/v1/models', {
|
|
725
|
+
headers: { Authorization: `Bearer ${openaiKey}` },
|
|
726
|
+
});
|
|
727
|
+
if (!res.ok) {
|
|
728
|
+
log.error('OpenAI API Key가 유효하지 않습니다. 키를 확인 후 다시 시도하세요.');
|
|
729
|
+
process.exit(1);
|
|
730
|
+
}
|
|
731
|
+
log.success('OpenAI API Key 확인 완료!');
|
|
732
|
+
} catch {
|
|
733
|
+
log.error('OpenAI 서버에 연결할 수 없습니다. 네트워크를 확인하세요.');
|
|
734
|
+
process.exit(1);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const unsplashKey = await askQuestion(chalk.cyan(' Unsplash Access Key (선택, Enter로 건너뛰기): '));
|
|
738
|
+
|
|
739
|
+
if (unsplashKey) {
|
|
740
|
+
try {
|
|
741
|
+
const res = await fetch('https://api.unsplash.com/photos/random?count=1', {
|
|
742
|
+
headers: { Authorization: `Client-ID ${unsplashKey}` },
|
|
743
|
+
});
|
|
744
|
+
if (!res.ok) {
|
|
745
|
+
log.warn('Unsplash Key가 유효하지 않습니다. 건너뜁니다.');
|
|
746
|
+
} else {
|
|
747
|
+
log.success('Unsplash Access Key 확인 완료!');
|
|
748
|
+
}
|
|
749
|
+
} catch {
|
|
750
|
+
log.warn('Unsplash 서버에 연결할 수 없습니다. 건너뜁니다.');
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
saveApiKeys({
|
|
755
|
+
OPENAI_API_KEY: openaiKey,
|
|
756
|
+
...(unsplashKey && { UNSPLASH_ACCESS_KEY: unsplashKey }),
|
|
757
|
+
});
|
|
758
|
+
log.success(`설정이 저장되었습니다! (${CONFIG_PATH})\n`);
|
|
759
|
+
if (!unsplashKey) log.dim(' Unsplash는 나중에 /set api 에서 설정할 수 있습니다.\n');
|
|
760
|
+
};
|
|
761
|
+
|
|
616
762
|
const main = async () => {
|
|
617
763
|
await animateBanner();
|
|
618
764
|
|
|
765
|
+
// API Key 없으면 초기 설정
|
|
766
|
+
await setupEnv();
|
|
767
|
+
|
|
619
768
|
// 세션 체크 — 로그인 필수
|
|
620
769
|
while (!fs.existsSync(path.join(__dirname, '..', 'data', 'session.json'))) {
|
|
621
770
|
log.warn('세션 파일(session.json)이 없습니다. 티스토리 로그인이 필요합니다.');
|
|
@@ -633,28 +782,36 @@ const main = async () => {
|
|
|
633
782
|
// 부팅 시퀀스
|
|
634
783
|
console.log(chalk.dim(' 시스템 초기화 중...\n'));
|
|
635
784
|
|
|
636
|
-
|
|
637
|
-
'OpenAI 연결',
|
|
638
|
-
async () => {
|
|
639
|
-
const res = await fetch('https://api.openai.com/v1/models', {
|
|
640
|
-
headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` },
|
|
641
|
-
});
|
|
642
|
-
if (!res.ok) throw new Error('키가 유효하지 않습니다');
|
|
643
|
-
},
|
|
644
|
-
1000,
|
|
645
|
-
);
|
|
646
|
-
|
|
647
|
-
if (process.env.UNSPLASH_ACCESS_KEY) {
|
|
785
|
+
try {
|
|
648
786
|
await showBootStep(
|
|
649
|
-
'
|
|
787
|
+
'OpenAI 연결',
|
|
650
788
|
async () => {
|
|
651
|
-
const res = await fetch('https://api.
|
|
652
|
-
headers: { Authorization: `
|
|
789
|
+
const res = await fetch('https://api.openai.com/v1/models', {
|
|
790
|
+
headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` },
|
|
653
791
|
});
|
|
654
792
|
if (!res.ok) throw new Error('키가 유효하지 않습니다');
|
|
655
793
|
},
|
|
656
794
|
1000,
|
|
657
795
|
);
|
|
796
|
+
} catch (e) {
|
|
797
|
+
log.warn(` OpenAI 연결 실패: ${e.message}\n ${chalk.dim('/set api 명령어로 키를 확인하세요.')}`);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (process.env.UNSPLASH_ACCESS_KEY) {
|
|
801
|
+
try {
|
|
802
|
+
await showBootStep(
|
|
803
|
+
'Unsplash 연결',
|
|
804
|
+
async () => {
|
|
805
|
+
const res = await fetch('https://api.unsplash.com/photos/random?count=1', {
|
|
806
|
+
headers: { Authorization: `Client-ID ${process.env.UNSPLASH_ACCESS_KEY}` },
|
|
807
|
+
});
|
|
808
|
+
if (!res.ok) throw new Error('키가 유효하지 않습니다');
|
|
809
|
+
},
|
|
810
|
+
1000,
|
|
811
|
+
);
|
|
812
|
+
} catch (e) {
|
|
813
|
+
log.warn(` Unsplash 연결 실패: ${e.message}\n ${chalk.dim('/set api 명령어로 키를 확인하세요.')}`);
|
|
814
|
+
}
|
|
658
815
|
}
|
|
659
816
|
|
|
660
817
|
let blogOk = false;
|
package/src/cli-post.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
|
-
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
// ~/.viruagent/config.json에서 API 키 로드
|
|
8
|
+
const configPath = path.join(os.homedir(), '.viruagent', 'config.json');
|
|
9
|
+
try {
|
|
10
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
11
|
+
if (config.openaiApiKey) process.env.OPENAI_API_KEY = config.openaiApiKey;
|
|
12
|
+
if (config.unsplashAccessKey) process.env.UNSPLASH_ACCESS_KEY = config.unsplashAccessKey;
|
|
13
|
+
} catch {}
|
|
14
|
+
|
|
5
15
|
|
|
6
16
|
const { generatePost, loadConfig } = require('./lib/ai');
|
|
7
17
|
const { initBlog, publishPost, saveDraft, getCategories, VISIBILITY } = require('./lib/tistory');
|
package/src/lib/ai.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
const OpenAI = require('openai');
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
if (!
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
4
|
+
let client;
|
|
5
|
+
const getClient = () => {
|
|
6
|
+
if (!client) {
|
|
7
|
+
if (!process.env.OPENAI_API_KEY) {
|
|
8
|
+
throw new Error('OPENAI_API_KEY가 설정되지 않았습니다. /set api 로 키를 설정하세요.');
|
|
9
|
+
}
|
|
10
|
+
client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
11
|
+
}
|
|
12
|
+
return client;
|
|
13
|
+
};
|
|
15
14
|
|
|
16
15
|
const { replaceImagePlaceholders } = require('./unsplash');
|
|
17
16
|
const { createLogger } = require('./logger');
|
|
@@ -19,7 +18,7 @@ const aiLog = createLogger('ai');
|
|
|
19
18
|
|
|
20
19
|
const handleApiError = (e) => {
|
|
21
20
|
if (e?.status === 401 || e?.code === 'invalid_api_key') {
|
|
22
|
-
throw new Error('OpenAI API 키가 유효하지 않습니다.
|
|
21
|
+
throw new Error('OpenAI API 키가 유효하지 않습니다. /set api 로 키를 확인하세요.');
|
|
23
22
|
}
|
|
24
23
|
if (e?.status === 429) {
|
|
25
24
|
throw new Error('API 요청 한도를 초과했습니다. 잠시 후 다시 시도하거나 요금제를 확인하세요.');
|
|
@@ -77,7 +76,7 @@ const generatePost = async (topic, options = {}) => {
|
|
|
77
76
|
|
|
78
77
|
let res;
|
|
79
78
|
try {
|
|
80
|
-
res = await
|
|
79
|
+
res = await getClient().chat.completions.create({
|
|
81
80
|
model,
|
|
82
81
|
messages: [
|
|
83
82
|
{ role: 'system', content: config.systemPrompt },
|
|
@@ -136,7 +135,7 @@ const revisePost = async (content, instruction, model) => {
|
|
|
136
135
|
|
|
137
136
|
let res;
|
|
138
137
|
try {
|
|
139
|
-
res = await
|
|
138
|
+
res = await getClient().chat.completions.create({
|
|
140
139
|
model,
|
|
141
140
|
messages: [
|
|
142
141
|
{ role: 'system', content: config.systemPrompt },
|
|
@@ -174,7 +173,7 @@ const chat = async (messages, model) => {
|
|
|
174
173
|
|
|
175
174
|
let res;
|
|
176
175
|
try {
|
|
177
|
-
res = await
|
|
176
|
+
res = await getClient().chat.completions.create({
|
|
178
177
|
model,
|
|
179
178
|
messages: [
|
|
180
179
|
{ role: 'system', content: '당신은 블로그 글쓰기를 돕는 AI 어시스턴트입니다. 주제 논의, 아이디어 브레인스토밍, 글 구조 제안 등을 도와줍니다. 한국어로 대화하세요.' },
|
package/src/lib/unsplash.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
const path = require('path');
|
|
2
|
-
require('dotenv').config({ path: path.join(__dirname, '..', '..', '.env') });
|
|
3
1
|
const { createLogger } = require('./logger');
|
|
4
2
|
const log = createLogger('unsplash');
|
|
5
3
|
|
|
6
|
-
const
|
|
4
|
+
const getUnsplashKey = () => process.env.UNSPLASH_ACCESS_KEY;
|
|
7
5
|
|
|
8
6
|
/**
|
|
9
7
|
* Unsplash에서 키워드로 이미지 검색
|
|
@@ -11,12 +9,12 @@ const UNSPLASH_ACCESS_KEY = process.env.UNSPLASH_ACCESS_KEY;
|
|
|
11
9
|
* @returns {Promise<Object|null>} { url, alt, credit, link } 또는 null
|
|
12
10
|
*/
|
|
13
11
|
const searchImage = async (keyword) => {
|
|
14
|
-
if (!
|
|
12
|
+
if (!getUnsplashKey()) return null;
|
|
15
13
|
|
|
16
14
|
try {
|
|
17
15
|
const url = `https://api.unsplash.com/search/photos?query=${encodeURIComponent(keyword)}&per_page=1&orientation=landscape`;
|
|
18
16
|
const res = await fetch(url, {
|
|
19
|
-
headers: { Authorization: `Client-ID ${
|
|
17
|
+
headers: { Authorization: `Client-ID ${getUnsplashKey()}` },
|
|
20
18
|
});
|
|
21
19
|
|
|
22
20
|
if (!res.ok) {
|
|
@@ -75,8 +73,8 @@ const replaceImagePlaceholders = async (html, options = {}) => {
|
|
|
75
73
|
let thumbnailUrl = null;
|
|
76
74
|
let thumbnailKage = null;
|
|
77
75
|
|
|
78
|
-
if (!
|
|
79
|
-
log.warn('
|
|
76
|
+
if (!getUnsplashKey()) {
|
|
77
|
+
log.warn('getUnsplashKey() 미설정 — 이미지 처리 건너뜀');
|
|
80
78
|
return { html, thumbnailUrl, thumbnailKage };
|
|
81
79
|
}
|
|
82
80
|
|
package/.env.example
DELETED