sns-auto-builder 1.0.6 → 1.0.8
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/CLAUDE.md +5 -0
- package/README.md +24 -0
- package/package.json +1 -1
- package/scripts/chatgpt-image-gen.js +57 -13
package/CLAUDE.md
CHANGED
|
@@ -48,6 +48,11 @@ Phase 3c (ChatGPT 웹 — 배경만 뽑고 한글은 HTML 로 얹기):
|
|
|
48
48
|
# 그다음 3a 의 HTML 단계를 이어서 수행
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
**크롬이 떴는데 프롬프트를 입력하지 않으면** 레퍼런스 첨부에서 막힌 것이다. ChatGPT 가
|
|
52
|
+
DOM 을 바꾸면 생긴다. `logs/app.log` 의 `첨부 실패` 줄에 그때 화면의 file input 목록이
|
|
53
|
+
남으니 그걸 보고 `SELECTORS` 를 고쳐라. **임의로 레퍼런스를 빼고 다시 돌리지 마라** —
|
|
54
|
+
레퍼런스 없이 만들면 디자인이 달라진다.
|
|
55
|
+
|
|
51
56
|
`chatgpt-image-gen.js` 는 로그인된 Chrome 프로필(`.chrome-profile`)로 chatgpt.com 을 몰아
|
|
52
57
|
프롬프트를 한 장씩 넣고 이미지를 받아 1080×1350 의 2배로 크롭해 저장한다.
|
|
53
58
|
참고 이미지가 있으면 `--ref <경로>` 로 첫 장에 첨부해 그 디자인을 따라가게 한다.
|
package/README.md
CHANGED
|
@@ -173,6 +173,30 @@ Chrome 은 인스턴스를 하나만 허용하므로 빌더가 영영 포트를
|
|
|
173
173
|
로그는 **버튼과 무관하게 항상 자동으로 쌓입니다** — 버튼은 내려받는 손잡이일 뿐입니다.
|
|
174
174
|
버전 · OS · 실행 기록 · 오류와 발생 위치가 담기고, **브랜드 값 내용 · 토큰 · 쿠키는 안 담깁니다.**
|
|
175
175
|
|
|
176
|
+
#### 크롬 자동화가 멈출 때
|
|
177
|
+
|
|
178
|
+
ChatGPT 는 화면 구조를 예고 없이 바꿉니다. 그러면 **크롬은 뜨는데 프롬프트를 입력하지 않는**
|
|
179
|
+
모습이 됩니다 (첨부 단계에서 썸네일을 2분 기다리다 실패). 이때 로그에 이런 줄이 남습니다:
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
[…] 첨부 실패 1/2 {"used":"input[type=\"file\"][accept*=\"image\"]","formImgs":0,"forms":1,
|
|
183
|
+
"composer":true,"fileInputs":[{"testid":null,"accept":null,"inForm":true},
|
|
184
|
+
{"testid":"upload-photos-input","accept":"image/*","inForm":false}, …]}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`fileInputs` 가 그 순간 화면에 있던 파일 입력의 목록입니다. 어느 것을 썼는지(`used`)와
|
|
188
|
+
대조하면 셀렉터를 어떻게 고쳐야 하는지 바로 나옵니다. 실제로 이 목록이 있었다면
|
|
189
|
+
`accept` 없는 input 을 고르고 있다는 걸 즉시 알 수 있었습니다.
|
|
190
|
+
→ [scripts/chatgpt-image-gen.js](scripts/chatgpt-image-gen.js) 의 `SELECTORS`
|
|
191
|
+
|
|
192
|
+
#### 크롬 포트
|
|
193
|
+
|
|
194
|
+
빌더는 CDP 포트 **19222** 를 씁니다 (`SNS_BUILDER_CDP_PORT` 로 변경 가능).
|
|
195
|
+
관례 포트인 9222 를 쓰면 사용자 본인의 크롬이나 chrome-devtools MCP 와 겹치는데,
|
|
196
|
+
**겹쳐도 에러가 안 납니다** — 한쪽이 `::1:9222`, 다른 쪽이 `127.0.0.1:9222` 를 잡고
|
|
197
|
+
둘 다 정상으로 보입니다. 그 상태에서 빌더는 조용히 **남의 브라우저를 조종합니다.**
|
|
198
|
+
`bin/sns-auto-builder.mjs` 와 `scripts/chatgpt-image-gen.js` 의 `CDP_PORT` 는 항상 같아야 합니다.
|
|
199
|
+
|
|
176
200
|
---
|
|
177
201
|
|
|
178
202
|
## 구조
|
package/package.json
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* 떠 있으면(예: chrome-devtools MCP) 프로필이 잠겨 실패한다. 그때는
|
|
27
27
|
* --browser-url http://127.0.0.1:19222 로 떠 있는 Chrome 에 붙어라.
|
|
28
28
|
*/
|
|
29
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, statSync } from 'node:fs';
|
|
29
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, statSync, appendFileSync } from 'node:fs';
|
|
30
30
|
import { join, resolve, dirname } from 'node:path';
|
|
31
31
|
import { fileURLToPath } from 'node:url';
|
|
32
32
|
import { cropTo45 } from './openai-image-gen.js';
|
|
@@ -154,6 +154,34 @@ async function waitForComposer(page) {
|
|
|
154
154
|
// 로그인이 풀렸으면 여기서 프롬프트 창이 안 뜬다 — 위 waitForSelector 가 던진다.
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
// 첨부가 실패했을 때 "그때 화면에 뭐가 있었는지" 를 남긴다.
|
|
158
|
+
//
|
|
159
|
+
// ChatGPT 는 DOM 을 예고 없이 바꾼다. 실제로 업로드 input 이 <form> 밖으로 옮겨진
|
|
160
|
+
// 적이 있고, 그때 증상은 "크롬은 떴는데 프롬프트를 입력하지 않는다" 뿐이라 원인을
|
|
161
|
+
// 찾는 데 오래 걸렸다. 다음에 또 바뀌면 이 스냅샷만 보면 된다.
|
|
162
|
+
async function domSnapshot(page) {
|
|
163
|
+
return page.evaluate(() => ({
|
|
164
|
+
formImgs: document.querySelectorAll('form img').length,
|
|
165
|
+
forms: document.querySelectorAll('form').length,
|
|
166
|
+
composer: !!document.getElementById('prompt-textarea'),
|
|
167
|
+
// 값이 아니라 구조만 남긴다 — 로그에 개인 정보가 들어가면 안 된다.
|
|
168
|
+
fileInputs: [...document.querySelectorAll('input[type="file"]')].map((el) => ({
|
|
169
|
+
testid: el.dataset.testid || null,
|
|
170
|
+
accept: el.accept || null,
|
|
171
|
+
inForm: !!el.closest('form'),
|
|
172
|
+
})),
|
|
173
|
+
})).catch((e) => ({ error: e.message }));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 워크스페이스의 logs/app.log 에 붙인다 — server.mjs 와 같은 파일, 같은 형식이라
|
|
177
|
+
// 빌더 첫 화면의 로그 버튼으로 그대로 받아진다.
|
|
178
|
+
function logDom(msg, data) {
|
|
179
|
+
try {
|
|
180
|
+
mkdirSync('logs', { recursive: true });
|
|
181
|
+
appendFileSync('logs/app.log', `[${new Date().toISOString()}] ${msg} ${JSON.stringify(data)}\n`);
|
|
182
|
+
} catch { /* 로그 실패가 생성을 막을 이유는 없다 */ }
|
|
183
|
+
}
|
|
184
|
+
|
|
157
185
|
// 레퍼런스 첨부. **실패하면 던진다.**
|
|
158
186
|
//
|
|
159
187
|
// 예전에는 썸네일 확인이 실패해도 경고만 찍고 그냥 보냈다. 그러면 레퍼런스 없이 만들어진
|
|
@@ -161,33 +189,49 @@ async function waitForComposer(page) {
|
|
|
161
189
|
// 조용히 틀리느니 시끄럽게 멈추는 게 낫다.
|
|
162
190
|
async function attachRef(page, refPaths) {
|
|
163
191
|
const want = refPaths.length;
|
|
164
|
-
//
|
|
192
|
+
// 화면에 input[type=file] 이 네 개쯤 있다. **accept 로 고른다.**
|
|
193
|
+
//
|
|
194
|
+
// 예전에는 "form 안에 있는 것"을 골랐는데, ChatGPT 가 이미지 업로드 input 을
|
|
195
|
+
// form 밖으로 옮기면서 form 안에는 accept 없는 다른 input 만 남았다. 거기에
|
|
196
|
+
// 파일을 넣으면 아무 일도 안 일어나고, 썸네일을 2분 기다리다 실패한다
|
|
197
|
+
// (증상: "크롬은 떴는데 프롬프트를 입력하지 않는다").
|
|
198
|
+
const SELECTORS = [
|
|
199
|
+
'input[type="file"][data-testid="upload-photos-input"]', // 현재 ChatGPT
|
|
200
|
+
'input[type="file"][accept*="image"]', // testid 가 바뀌어도 살아남는 조건
|
|
201
|
+
'input[type="file"]',
|
|
202
|
+
];
|
|
165
203
|
const pickInput = async () => {
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
if (
|
|
204
|
+
for (const sel of SELECTORS) {
|
|
205
|
+
const h = await page.$(sel);
|
|
206
|
+
if (h) return { sel, handle: h };
|
|
169
207
|
}
|
|
170
|
-
return
|
|
208
|
+
return null;
|
|
171
209
|
};
|
|
172
210
|
|
|
173
211
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
174
|
-
const
|
|
175
|
-
if (!
|
|
176
|
-
|
|
212
|
+
const picked = await pickInput();
|
|
213
|
+
if (!picked) {
|
|
214
|
+
logDom('첨부 input 없음', await domSnapshot(page));
|
|
215
|
+
throw new Error('첨부용 파일 입력을 찾지 못했습니다 (ChatGPT 화면 구조가 바뀌었을 수 있음)');
|
|
216
|
+
}
|
|
217
|
+
await picked.handle.uploadFile(...refPaths);
|
|
177
218
|
|
|
178
219
|
// 한 장만 붙어도 통과시키면 안 된다. **올린 개수만큼** 붙을 때까지 기다린다.
|
|
179
220
|
const ok = await page
|
|
180
221
|
.waitForFunction((n) => document.querySelectorAll('form img').length >= n, { timeout: 60_000 }, want)
|
|
181
222
|
.then(() => true).catch(() => false);
|
|
182
223
|
if (ok) {
|
|
183
|
-
console.log(` 레퍼런스 ${want}장 첨부
|
|
224
|
+
console.log(` 레퍼런스 ${want}장 첨부 확인 (${picked.sel})`);
|
|
184
225
|
return;
|
|
185
226
|
}
|
|
186
|
-
const
|
|
187
|
-
console.warn(` (재시도 ${attempt}/2) 첨부가 ${
|
|
227
|
+
const snap = await domSnapshot(page);
|
|
228
|
+
console.warn(` (재시도 ${attempt}/2) 첨부가 ${snap.formImgs}/${want}장만 확인됨 — 쓴 input: ${picked.sel}`);
|
|
229
|
+
logDom(`첨부 실패 ${attempt}/2`, { used: picked.sel, ...snap });
|
|
188
230
|
await new Promise((r) => setTimeout(r, 3000));
|
|
189
231
|
}
|
|
190
|
-
throw new Error(`레퍼런스 ${want}장을 첨부하지 못했습니다. 레퍼런스 없이 만들면 디자인이 달라지므로
|
|
232
|
+
throw new Error(`레퍼런스 ${want}장을 첨부하지 못했습니다. 레퍼런스 없이 만들면 디자인이 달라지므로 중단합니다.\n`
|
|
233
|
+
+ ` ChatGPT 화면 구조가 바뀌었을 수 있습니다. logs/app.log 의 "첨부 실패" 줄에\n`
|
|
234
|
+
+ ` 그때 화면에 있던 file input 목록이 남습니다 — 빌더 첫 화면의 로그 버튼으로 받으세요.`);
|
|
191
235
|
}
|
|
192
236
|
|
|
193
237
|
async function sendPrompt(page, text) {
|