deel-local-cli 1.20.12 → 2.0.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.ko.md +96 -77
- package/README.md +119 -80
- package/bin/deel.js +398 -32
- package/package.json +2 -1
- package/src/acp/jsonrpc.js +87 -7
- package/src/acp/map.js +62 -13
- package/src/acp/serve.js +338 -30
- package/src/agent/agents.js +110 -12
- package/src/agent/askcheck.js +50 -6
- package/src/agent/asks.js +59 -5
- package/src/agent/budget.js +9 -2
- package/src/agent/card.js +8 -2
- package/src/agent/commit.js +168 -16
- package/src/agent/compact.js +93 -21
- package/src/agent/{/354/213/240/353/242/260/353/217/204.js → confidence.js} +1 -1
- package/src/agent/effort.js +85 -15
- package/src/agent/evidence.js +63 -7
- package/src/agent/evolve.js +132 -24
- package/src/agent/grade.js +12 -3
- package/src/agent/loop.js +408 -81
- package/src/agent/memory.js +176 -29
- package/src/agent/mention.js +22 -5
- package/src/agent/models.js +31 -7
- package/src/agent/modes.js +25 -10
- package/src/agent/outschema.js +381 -56
- package/src/agent/pins.js +33 -2
- package/src/agent/project.js +73 -8
- package/src/agent/recall.js +40 -9
- package/src/agent/{/354/204/261/355/225/234/352/270/260/353/241/235.js → recordshape.js} +19 -8
- package/src/agent/review.js +143 -13
- package/src/agent/route.js +506 -32
- package/src/agent/salvage.js +2 -1
- package/src/agent/session.js +457 -31
- package/src/agent/store.js +232 -22
- package/src/agent/threads.js +52 -3
- package/src/backend/adapter.js +508 -85
- package/src/backend/azure.js +12 -1
- package/src/backend/cachemark.js +31 -5
- package/src/backend/clientcert.js +23 -1
- package/src/backend/ctxsize.js +56 -9
- package/src/backend/detect.js +252 -19
- package/src/backend/http.js +383 -42
- package/src/backend/learn.js +101 -11
- package/src/backend/mcp.js +333 -24
- package/src/backend/price.js +61 -13
- package/src/backend/probe.js +188 -36
- package/src/backend/proxy.js +108 -14
- package/src/backend/quota.js +226 -30
- package/src/backend/retry.js +46 -3
- package/src/backend/scan.js +47 -10
- package/src/backend/scanui.js +57 -7
- package/src/backend/toolfit.js +204 -22
- package/src/backend/vision.js +14 -1
- package/src/backend/wire.js +58 -10
- package/src/cmdnames.js +16 -0
- package/src/commands/common.js +70 -0
- package/src/commands/extend.js +300 -0
- package/src/commands/model.js +852 -0
- package/src/commands/view.js +328 -0
- package/src/commands/work.js +823 -0
- package/src/commands.js +193 -2082
- package/src/completion.js +45 -3
- package/src/config.js +452 -39
- package/src/configexplain.js +125 -14
- package/src/doctor.js +164 -26
- package/src/i18n/en.js +22 -0
- package/src/i18n/index.js +31 -1
- package/src/i18n/ja.js +25 -1
- package/src/i18n/ko.js +25 -2
- package/src/i18n/zh.js +25 -1
- package/src/lsp/client.js +111 -9
- package/src/lsp/servers.js +86 -22
- package/src/oneshot.js +325 -65
- package/src/pack/sbom.js +28 -5
- package/src/pack/selfpack.js +54 -5
- package/src/pack/sheet.en.js +24 -3
- package/src/pack/tar.js +45 -8
- package/src/pack/zip.js +103 -8
- package/src/plugins/manage.js +165 -14
- package/src/preview/serve.js +239 -23
- package/src/providers/bedrock.js +30 -5
- package/src/providers/gemini.js +8 -0
- package/src/providers/index.js +17 -1
- package/src/repl.js +254 -61
- package/src/report.js +7 -2
- package/src/reset.js +151 -25
- package/src/safety/audit.js +108 -8
- package/src/safety/authcmd.js +75 -5
- package/src/safety/guard.js +507 -15
- package/src/safety/hooks.js +172 -20
- package/src/safety/keystore.js +178 -18
- package/src/safety/network.js +209 -6
- package/src/safety/policy.js +377 -36
- package/src/safety/runmode.js +9 -2
- package/src/safety/secrets.js +243 -17
- package/src/safety/shellenv.js +64 -3
- package/src/safety/trust.js +298 -32
- package/src/safety/undo.js +409 -17
- package/src/setup.js +175 -17
- package/src/skills/discover.js +122 -19
- package/src/stats.js +57 -16
- package/src/tools/{/355/231/225/354/235/270/353/262/225.js → checkmethods.js} +26 -1
- package/src/tools/clipboard.js +53 -20
- package/src/tools/convert.js +118 -37
- package/src/tools/desc.en.js +48 -5
- package/src/tools/doc2md.js +11 -1
- package/src/tools/docs.js +94 -18
- package/src/tools/edit-match.js +212 -13
- package/src/tools/encoding.js +525 -28
- package/src/tools/excel-com.js +63 -15
- package/src/tools/excel.js +12 -7
- package/src/tools/fastgrep.js +444 -36
- package/src/tools/fig.js +40 -9
- package/src/tools/fsutil.js +235 -21
- package/src/tools/hwpxwrite.js +80 -19
- package/src/tools/ignore.js +55 -7
- package/src/tools/index.js +1029 -145
- package/src/tools/jobs.js +325 -50
- package/src/tools/kiwi.js +50 -5
- package/src/tools/lsp.js +164 -32
- package/src/tools/outline.js +52 -12
- package/src/tools/pdf.js +366 -56
- package/src/tools/shell.js +12 -4
- package/src/tools/spawn.js +203 -18
- package/src/tools/todo.js +73 -4
- package/src/tools/verify.js +312 -43
- package/src/tools/webfetch.js +184 -25
- package/src/tools/xlsx.js +232 -31
- package/src/ui/ansi.js +209 -15
- package/src/ui/banner.js +2 -1
- package/src/ui/complete.js +46 -4
- package/src/ui/diff.js +4 -2
- package/src/ui/export.js +91 -17
- package/src/ui/inputbox.js +58 -18
- package/src/ui/intro.js +9 -2
- package/src/ui/level.js +32 -7
- package/src/ui/md.js +101 -15
- package/src/ui/motion.js +5 -2
- package/src/ui/notify.js +7 -1
- package/src/ui/office.js +37 -5
- package/src/ui/pastechip.js +3 -2
- package/src/ui/pick.js +85 -12
- package/src/ui/prompt.js +176 -13
- package/src/ui/screen.js +2 -1
- package/src/ui/spinner.js +6 -1
- package/src/ui/status.js +80 -12
- package/src/ui/wrap.js +23 -37
- /package/src/agent/{/353/213/250/352/263/204.js" → phase.js} +0 -0
- /package/src/skills/builtin/{/352/271/212/354/235/264/354/236/210/352/262/214-/353/247/214/353/223/244/352/270/260/SKILL.md" → build-deep/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/260/250/352/267/274/354/260/250/352/267/274-/353/224/224/353/262/204/352/271/205/SKILL.md" → debug-step-by-step/SKILL.md} +0 -0
- /package/src/skills/builtin/{/353/201/235/352/271/214/354/247/200-/355/225/230/352/270/260/SKILL.md" → finish-all/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/212/244/354/212/244/353/241/234-/352/262/200/355/206/240/SKILL.md" → self-review/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/275/224/353/223/234-/354/244/204/354/235/264/352/270/260/SKILL.md" → simplify-code/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/260/224/353/237/254/353/263/264/352/270/260/SKILL.md" → spike/SKILL.md} +0 -0
- /package/src/skills/builtin/{/352/262/200/354/202/254-/353/250/274/354/240/200/SKILL.md" → test-first/SKILL.md} +0 -0
package/src/preview/serve.js
CHANGED
|
@@ -17,14 +17,19 @@
|
|
|
17
17
|
* 1. 127.0.0.1 에만 묶는다. 0.0.0.0 은 아예 못 쓰게 해 뒀다 —
|
|
18
18
|
* 같은 사무실 망에서 아무나 내 소스를 읽게 된다.
|
|
19
19
|
* 2. 포트는 0(커널이 빈 것을 준다). 고정 포트는 남이 쓰던 것을 뺏는다.
|
|
20
|
-
* 3. 경로는 guard 의 scope
|
|
21
|
-
*
|
|
20
|
+
* 3. 경로는 guard 의 scope 로 풀고, **띄운 폴더 밖인지는 여기서 한 번 더 본다.**
|
|
21
|
+
* scope 는 작업 범위(프로젝트 전체)까지 열어 주므로 그것만으로는 모자란다 —
|
|
22
|
+
* 하위 폴더 하나만 띄웠을 때 `/../다른폴더/비밀.env` 가 그대로 나간다.
|
|
23
|
+
* 두 벌이라 한쪽만 고쳐질 걱정은 남지만, 없으면 진짜로 샌다 — 그 자리
|
|
24
|
+
* 인라인 주석(`밖인지는 길 조각으로 본다`)이 어떻게 재는지 적어 뒀다.
|
|
22
25
|
* 4. 파일을 **주기만** 한다. PUT·POST·DELETE 는 받지 않는다.
|
|
23
26
|
*/
|
|
24
27
|
import { createServer } from 'node:http';
|
|
25
|
-
import { createReadStream, statSync, existsSync, readdirSync, watch, realpathSync } from 'node:fs';
|
|
26
|
-
import { join, extname, relative, sep } from 'node:path';
|
|
28
|
+
import { createReadStream, statSync, existsSync, readdirSync, watch, realpathSync, openSync, readSync, closeSync } from 'node:fs';
|
|
29
|
+
import { join, extname, relative, sep, isAbsolute, basename } from 'node:path';
|
|
27
30
|
import { spawn } from 'node:child_process';
|
|
31
|
+
import { 내부살림 } from '../tools/fsutil.js';
|
|
32
|
+
import { detect } from '../tools/encoding.js';
|
|
28
33
|
|
|
29
34
|
// 확장자 → 형식. 없는 것은 그냥 내려받게 둔다.
|
|
30
35
|
// glb·gltf·wasm 을 넣어 뒀다 — 형식이 틀리면 Three.js 가 조용히 아무것도 안 그린다.
|
|
@@ -48,13 +53,18 @@ const 형식 = {
|
|
|
48
53
|
//
|
|
49
54
|
// 파일을 고치면 화면이 저절로 새로 뜬다. 이게 없으면 결국 브라우저를 손으로
|
|
50
55
|
// 새로 고치게 되고, 그러면 굳이 여기서 띄울 이유가 절반은 사라진다.
|
|
56
|
+
//
|
|
57
|
+
// 조각에는 **ASCII 만** 싣는다 (2.0.0 6회차 미리보기6). 브라우저는 끼운 조각도 그 페이지의
|
|
58
|
+
// 인코딩으로 읽는다 — EUC-KR·Shift_JIS 페이지에 한글 주석과 한글 통로 이름을 끼웠더니 통로
|
|
59
|
+
// 주소부터 깨졌다. 그래서 통로 이름은 퍼센트로 싣고, 알리는 말은 영어 낱말로 하고, 사람이 읽을
|
|
60
|
+
// 풀이는 조각 바깥(여기)에 둔다. 서버가 꺼지면 EventSource 는 계속 다시 붙으려 하는데 조용히
|
|
61
|
+
// 놔둔다 — deel 을 다시 띄우면 그대로 이어 붙어서 화면을 새로 고치지 않아도 된다.
|
|
62
|
+
const 되살림통로 = '__deel__/살아있나';
|
|
51
63
|
const 되살림 = `
|
|
52
|
-
<script>/* deel
|
|
64
|
+
<script>/* deel preview: reload on change */
|
|
53
65
|
(function(){try{
|
|
54
|
-
var s=new EventSource('/
|
|
55
|
-
s.onmessage=function(e){ if(e.data==='
|
|
56
|
-
// 서버가 꺼지면 EventSource 가 계속 다시 붙으려 한다. 조용히 놔둔다 —
|
|
57
|
-
// deel 을 다시 띄우면 그대로 이어 붙어서 화면을 새로 고치지 않아도 된다.
|
|
66
|
+
var s=new EventSource('/${되살림통로.split('/').map(encodeURIComponent).join('/')}');
|
|
67
|
+
s.onmessage=function(e){ if(e.data==='reload')location.reload(); };
|
|
58
68
|
}catch(err){}})();
|
|
59
69
|
</script>`;
|
|
60
70
|
|
|
@@ -69,6 +79,79 @@ const 안전한경로 = (u) => {
|
|
|
69
79
|
return p.replace(/^\/+/, '');
|
|
70
80
|
};
|
|
71
81
|
|
|
82
|
+
/*
|
|
83
|
+
* ── 우리 이름으로 부른 것만 받는다 (사냥5 H5-7) ─────────────────────────────
|
|
84
|
+
*
|
|
85
|
+
* 127.0.0.1 에 묶는 것으로는 **브라우저**를 못 막는다. 사람이 연 아무 웹 페이지가
|
|
86
|
+
* 제 도메인을 잠깐 127.0.0.1 로 풀리게 바꾸면(DNS 리바인딩) 그 페이지 스크립트가
|
|
87
|
+
* `http://evil.example:포트/…` 를 **같은 출처**로 읽는다. 연결은 127.0.0.1 로 오니
|
|
88
|
+
* 묶은 주소로는 못 가르고, 가르는 자리는 Host 머리글 하나다. 실제로 재 보니 Host 를
|
|
89
|
+
* evil.example 로 줘도 200 이었다. 브라우저가 우리를 부르는 이름은 셋뿐이고 포트까지
|
|
90
|
+
* 붙는다(기본 포트가 아니므로). 그 밖이면 안 준다.
|
|
91
|
+
*/
|
|
92
|
+
function 우리이름인가(host, port) {
|
|
93
|
+
const 이름 = String(host ?? '').trim().toLowerCase();
|
|
94
|
+
return [`127.0.0.1:${port}`, `localhost:${port}`, `[::1]:${port}`].includes(이름);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/*
|
|
98
|
+
* ── 살림·비밀 파일은 안 준다 (사냥5 H5-7) ───────────────────────────────────
|
|
99
|
+
*
|
|
100
|
+
* 경로가 띄운 폴더 안이면 무엇이든 줬다. 프로젝트 폴더를 그대로 띄우는 일이 흔한데,
|
|
101
|
+
* 거기에는 `.deel/config.json`(게이트웨이 열쇠)과 `.env` 가 같이 산다 — 위 리바인딩
|
|
102
|
+
* 한 번이면 둘 다 나간다. 도구가 읽으면 안 되는 살림은 도구 울타리와 **같은 자**
|
|
103
|
+
* (tools/fsutil.js 의 내부살림)로 가른다. 자를 여기 또 적으면 한쪽만 고쳐진다.
|
|
104
|
+
* 그 자가 안 보는 비밀은 Vite 개발 서버가 기본으로 거부하는 목록을 따른다 —
|
|
105
|
+
* `.env`·`.env.*`·`*.pem`·`*.crt`·`.git/` — 미리보기가 그걸 줘야 할 까닭이 없다.
|
|
106
|
+
*
|
|
107
|
+
* 거기에 **개인키**를 보탠다 (2.0.0 7회차). Vite 목록에는 증서(`.pem`·`.crt`)만 있는데,
|
|
108
|
+
* deel 자신이 프로필 `"인증서"` 에 `client.pem`(증서) 곁에 `client.key`(개인키)와
|
|
109
|
+
* `client.pfx`·`client.p12`(둘 한 덩이)를 두라고 적어 둔다(backend/clientcert.js 머리말).
|
|
110
|
+
* 그래서 이 프로그램을 쓰는 사람의 폴더에는 그 파일들이 실제로 있다 — 그런데 증서는
|
|
111
|
+
* 가리고 **새면 안 되는 쪽인 개인키는 그대로 줬다.** 재 보니 `/client.key` 가 200 으로
|
|
112
|
+
* 내용까지 나갔다. 둘을 갈라 막을 까닭이 없다.
|
|
113
|
+
*
|
|
114
|
+
* `.cer` 도 같이 막는다 — 윈도우 「인증서 내보내기」 가 기본으로 내놓는 이름이고 알맹이는
|
|
115
|
+
* `.crt` 와 같은 것이다. 미리보기가 못 보여 줘서 아쉬울 파일이 아니다. 잘못 막았을 때 드는
|
|
116
|
+
* 값(파일 하나가 안 보인다)이 잘못 줬을 때 드는 값(개인키가 나간다)보다 훨씬 싸다.
|
|
117
|
+
*/
|
|
118
|
+
function 안줄것인가(abs, 뿌리) {
|
|
119
|
+
/*
|
|
120
|
+
* 살림인지는 **띄운 폴더 이름 + 그 안쪽 길**로 본다 (막판 훑기).
|
|
121
|
+
*
|
|
122
|
+
* 여태 절대경로를 그대로 넘겼다. 그러면 띄운 폴더가 하필 `.claude/` 나
|
|
123
|
+
* `.cursor/` **아래**에 있을 때 — 거기에 만든 것을 두는 사람이 실제로 있다 —
|
|
124
|
+
* 그 폴더의 **모든 파일**이 403 이 된다. 재 보니 `index.html` 도 `a.css` 도
|
|
125
|
+
* 전부 「살림이나 비밀 파일이라 안 줍니다」 였다. 제가 만든 제 사이트인데.
|
|
126
|
+
* 거짓 경고도 결함이다.
|
|
127
|
+
*
|
|
128
|
+
* 그렇다고 안쪽 길만 보면 `.deel` 을 **통째로** 띄운 사람이 `config.json` 을
|
|
129
|
+
* 그대로 내주게 된다 — 그때는 안쪽 길이 그냥 `config.json` 이라 아무 데도 안
|
|
130
|
+
* 걸린다. 그래서 뿌리의 **제 이름 한 칸**을 앞에 붙여서 본다.
|
|
131
|
+
*
|
|
132
|
+
* 띄운 곳 `…/.claude/내사이트` · `index.html` → `내사이트/index.html` 준다
|
|
133
|
+
* 띄운 곳 `…/.claude` · `history.jsonl` → `.claude/history.jsonl` 안 준다
|
|
134
|
+
* 띄운 곳 `…/.deel` · `config.json` → `.deel/config.json` 안 준다
|
|
135
|
+
* 띄운 곳 프로젝트 · `.deel/config.json` → `myproj/.deel/…` 안 준다
|
|
136
|
+
*
|
|
137
|
+
* 띄운 폴더 **밖**은 위 담장(안쪽 검사 · 이음줄 검사)이 이미 막으므로,
|
|
138
|
+
* 여기서 위쪽 조각을 안 봐도 새지 않는다.
|
|
139
|
+
*/
|
|
140
|
+
const 안쪽길 = relative(뿌리, abs);
|
|
141
|
+
const 볼길 = [basename(뿌리), ...(안쪽길 ? [안쪽길] : [])].join(sep);
|
|
142
|
+
if (내부살림(볼길)) return true;
|
|
143
|
+
const 조각 = 안쪽길.split(sep).map((x) => x.toLowerCase());
|
|
144
|
+
/*
|
|
145
|
+
* 윈도 NTFS 는 조각 끝의 `:스트림`(`.env::$DATA` · `.git::$INDEX_ALLOCATION`)과 끝 점·빈칸이 **같은
|
|
146
|
+
* 파일·폴더**를 연다. 아래 이름 견주기는 글자 그대로라, 재 보니 `/.env::$DATA` · `/x.pem::$DATA` ·
|
|
147
|
+
* `/.git::$INDEX_ALLOCATION/config` 가 200 으로 나갔다 (2.0.0 6회차 · `재현-미리보기6.mjs`). `.deel` 은
|
|
148
|
+
* 내부살림 이 꼬리를 벗겨 봐서 막혔다. 윈도 파일 이름에는 `:` 가 못 들어가므로 그런 조각은 통째로 안 준다.
|
|
149
|
+
*/
|
|
150
|
+
if (process.platform === 'win32' && 조각.some((x) => x.includes(':') || /[. ]$/.test(x))) return true;
|
|
151
|
+
if (조각.some((x) => x === '.git' || x === '.env' || x.startsWith('.env.'))) return true;
|
|
152
|
+
return /\.(pem|crt|cer|key|pfx|p12)$/.test(조각[조각.length - 1] ?? '');
|
|
153
|
+
}
|
|
154
|
+
|
|
72
155
|
/** 이용자 파일 이름을 HTML 안에 그대로 꽂기 전에 씌운다 — 파일 이름에
|
|
73
156
|
* `<script>` 가 들어 있어도 태그로 안 읽히게. */
|
|
74
157
|
const html씌우기 = (s) => String(s).replace(/[&<>"']/g, (c) => ({
|
|
@@ -86,6 +169,15 @@ const html씌우기 = (s) => String(s).replace(/[&<>"']/g, (c) => ({
|
|
|
86
169
|
*/
|
|
87
170
|
export function 띄우기({ 뿌리, scope, 되살리기 = true }) {
|
|
88
171
|
const 듣는이들 = new Set(); // 살아 있는 EventSource 응답들
|
|
172
|
+
/*
|
|
173
|
+
* 뿌리의 **진짜** 자리. 이음줄을 따라간 뒤 견주려면 양쪽이 다 진짜여야 한다.
|
|
174
|
+
*
|
|
175
|
+
* 맥의 `/tmp` 는 `/private/tmp` 로 가는 이음줄이고, 윈도의 `C:\Users\나\문서` 도
|
|
176
|
+
* 원드라이브를 켜면 이음줄이 된다. 한쪽만 따라가면 제자리에 있는 파일이 전부
|
|
177
|
+
* 「폴더 밖」 이 되어 403 을 받는다 — 거짓 경고도 결함이다.
|
|
178
|
+
*/
|
|
179
|
+
let 뿌리진짜 = 뿌리;
|
|
180
|
+
try { 뿌리진짜 = realpathSync.native(뿌리); } catch { /* 못 바꾸면 준 대로 */ }
|
|
89
181
|
let 바뀐수 = 0;
|
|
90
182
|
let 감시 = null;
|
|
91
183
|
let 늦추기 = null;
|
|
@@ -93,11 +185,16 @@ export function 띄우기({ 뿌리, scope, 되살리기 = true }) {
|
|
|
93
185
|
const 알리기 = () => {
|
|
94
186
|
바뀐수 += 1;
|
|
95
187
|
for (const res of 듣는이들) {
|
|
96
|
-
try { res.write('data:
|
|
188
|
+
try { res.write('data: reload\n\n'); } catch { 듣는이들.delete(res); }
|
|
97
189
|
}
|
|
98
190
|
};
|
|
99
191
|
|
|
100
192
|
const srv = createServer((req, res) => {
|
|
193
|
+
// 우리 이름으로 부른 것만 받는다 — 되살림 통로도 이 뒤다 (위 우리이름인가 머리말).
|
|
194
|
+
if (!우리이름인가(req.headers.host, srv.address()?.port)) {
|
|
195
|
+
res.writeHead(403);
|
|
196
|
+
return res.end('이 이름으로는 안 받습니다 — 127.0.0.1 이나 localhost 로 여세요');
|
|
197
|
+
}
|
|
101
198
|
// 주기만 한다. 받는 길은 아예 열지 않는다.
|
|
102
199
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
103
200
|
res.writeHead(405, { allow: 'GET, HEAD' });
|
|
@@ -108,12 +205,22 @@ export function 띄우기({ 뿌리, scope, 되살리기 = true }) {
|
|
|
108
205
|
if (길 === null) { res.writeHead(400); return res.end('주소가 이상합니다'); }
|
|
109
206
|
|
|
110
207
|
// 되살림 통로. 열어 두고 파일이 바뀔 때마다 한 줄씩 흘린다.
|
|
111
|
-
if (길 ===
|
|
208
|
+
if (길 === 되살림통로) {
|
|
112
209
|
res.writeHead(200, {
|
|
113
210
|
'content-type': 'text/event-stream; charset=utf-8',
|
|
114
211
|
'cache-control': 'no-cache',
|
|
115
212
|
connection: 'keep-alive',
|
|
116
213
|
});
|
|
214
|
+
/*
|
|
215
|
+
* HEAD 는 머리만 주고 끝낸다 (9회차 · 2차 눈).
|
|
216
|
+
*
|
|
217
|
+
* 여태 HEAD 도 이 아래로 흘러 몸을 쓰고 **연결을 안 닫은 채** 듣는이
|
|
218
|
+
* 목록에 들어갔다. HEAD 에 몸을 주는 것은 규격 위반이고(RFC 9110),
|
|
219
|
+
* 그 연결은 브라우저가 아니라 프록시·검사기가 두드린 것이라 영영 안
|
|
220
|
+
* 닫힌다 — 재 보니 **응답이 끝나지 않았다.** 그 뒤 파일이 바뀔 때마다
|
|
221
|
+
* 아무도 안 보는 곳으로 `data: reload` 를 계속 흘린다.
|
|
222
|
+
*/
|
|
223
|
+
if (req.method === 'HEAD') return res.end();
|
|
117
224
|
res.write(': 붙었습니다\n\n');
|
|
118
225
|
듣는이들.add(res);
|
|
119
226
|
req.on('close', () => 듣는이들.delete(res));
|
|
@@ -125,14 +232,51 @@ export function 띄우기({ 뿌리, scope, 되살리기 = true }) {
|
|
|
125
232
|
abs = scope.resolve(길 ? join(뿌리, 길) : 뿌리);
|
|
126
233
|
// scope 는 작업 범위까지만 본다. 띄운 폴더 밖도 막아야 한다 —
|
|
127
234
|
// 아니면 /../다른폴더/비밀.env 로 프로젝트 전체가 열린다.
|
|
235
|
+
/*
|
|
236
|
+
* 밖인지는 **길 조각**으로 본다. 글자로 보면 안 된다 (9회차 · 2차 눈).
|
|
237
|
+
*
|
|
238
|
+
* `..config.json` 은 윈도·리눅스 둘 다에서 만들 수 있는 이름이다.
|
|
239
|
+
* `startsWith('..')` 로 보면 그 진짜 파일이 「띄운 폴더 밖」 이 되어
|
|
240
|
+
* 403 을 받는다 — 거짓 경고도 결함이다. 그리고 뒤에 있던
|
|
241
|
+
* `split(sep)[0] === '..'` 는 앞 조건에 통째로 먹혀 **어떤 입력에도
|
|
242
|
+
* 혼자 걸리지 않았다.** 안 걸리는 규칙은 없느니만 못하다.
|
|
243
|
+
*
|
|
244
|
+
* `isAbsolute` 도 같이 본다. 윈도에서 relative 는 드라이브가 다르면
|
|
245
|
+
* `..` 이 아니라 `D:\…` 를 그대로 돌려준다 — 그것은 `..` 검사에 안 걸린다.
|
|
246
|
+
*/
|
|
128
247
|
const 안쪽 = relative(뿌리, abs);
|
|
129
|
-
if (
|
|
248
|
+
if (isAbsolute(안쪽) || 안쪽.split(sep)[0] === '..') {
|
|
130
249
|
res.writeHead(403); return res.end('띄운 폴더 밖입니다');
|
|
131
250
|
}
|
|
251
|
+
/*
|
|
252
|
+
* 이음줄은 **따라간 뒤에** 한 번 더 본다 (막판 훑기).
|
|
253
|
+
*
|
|
254
|
+
* 위 검사는 글자로 짠 길만 본다. `공개/키 → C:\Users\나\.ssh` 같은 이음줄은
|
|
255
|
+
* 글자로는 `공개/키` 라 떳떳이 안쪽이고, 그대로 열어 준다. 만든 사람이
|
|
256
|
+
* 일부러 건 것도 있지만 `node_modules/.bin`·pnpm 저장고처럼 도구가 저절로
|
|
257
|
+
* 거는 것이 훨씬 많다 — 폴더 하나 띄웠다고 홈 전체가 열리면 안 된다.
|
|
258
|
+
*
|
|
259
|
+
* 없는 파일은 여기서 안 막는다. realpath 가 던지면 아래 existsSync 가
|
|
260
|
+
* 404 로 맡는다 — 있는 것에 403, 없는 것에 404 를 주면 있는지가 샌다.
|
|
261
|
+
*/
|
|
262
|
+
try {
|
|
263
|
+
const 진짜 = realpathSync.native(abs);
|
|
264
|
+
if (진짜 !== abs) {
|
|
265
|
+
const 진짜안쪽 = relative(뿌리진짜, 진짜);
|
|
266
|
+
if (isAbsolute(진짜안쪽) || 진짜안쪽.split(sep)[0] === '..') {
|
|
267
|
+
res.writeHead(403); return res.end('띄운 폴더 밖으로 가는 이음줄입니다');
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
} catch { /* 없는 것은 아래에서 404 */ }
|
|
132
271
|
} catch {
|
|
133
272
|
res.writeHead(403);
|
|
134
273
|
return res.end('작업 범위 밖입니다');
|
|
135
274
|
}
|
|
275
|
+
// 있는지 보기 **전에** 가른다 — 없는 .env 에 404, 있는 .env 에 403 이면 있는지가 샌다.
|
|
276
|
+
if (안줄것인가(abs, 뿌리)) {
|
|
277
|
+
res.writeHead(403);
|
|
278
|
+
return res.end('살림이나 비밀 파일이라 안 줍니다');
|
|
279
|
+
}
|
|
136
280
|
|
|
137
281
|
if (!existsSync(abs)) {
|
|
138
282
|
/*
|
|
@@ -172,7 +316,11 @@ export function 띄우기({ 뿌리, scope, 되살리기 = true }) {
|
|
|
172
316
|
// 헤더는 ASCII 만 실린다. 한글 폴더 이름을 그대로 넣으면 ERR_INVALID_CHAR 로
|
|
173
317
|
// **서버가 죽는다.** 미리보기 하나 켰다가 deel 이 통째로 끝나는 셈이다.
|
|
174
318
|
// 길은 이미 디코드된 값이므로 다시 인코드해서 싣는다.
|
|
175
|
-
|
|
319
|
+
//
|
|
320
|
+
// 조각마다 encodeURIComponent 로 싣는다 (2.0.0 6회차 미리보기6). encodeURI 는 `#`·`?` 를
|
|
321
|
+
// 주소의 뼈대로 보고 그대로 둬서, `c#` 폴더로 넘길 때 `location: /c#/` 가 나갔다 — 브라우저는
|
|
322
|
+
// `#` 뒤를 조각으로 떼고 `/c` 를 다시 불러 같은 넘김을 되풀이했다.
|
|
323
|
+
res.writeHead(301, { location: `/${길.split('/').map(encodeURIComponent).join('/')}/${물음표 ?? ''}` });
|
|
176
324
|
return res.end();
|
|
177
325
|
}
|
|
178
326
|
const 첫장 = join(abs, 'index.html');
|
|
@@ -214,6 +362,12 @@ export function 띄우기({ 뿌리, scope, 되살리기 = true }) {
|
|
|
214
362
|
// 이 시계가 프로그램을 붙잡고 있으면 안 된다.
|
|
215
363
|
늦추기.unref?.();
|
|
216
364
|
});
|
|
365
|
+
/*
|
|
366
|
+
* 도는 중에 오는 감시 오류(지켜보던 폴더가 지워짐·권한이 바뀜)는 위 try 가 못 잡는다.
|
|
367
|
+
* 'error' 를 받는 이가 없으면 EventEmitter 가 던져 **프로세스가 죽는다** (2.0.0 6회차
|
|
368
|
+
* 미리보기6 V7). 되살림만 멈추고 파일은 계속 준다.
|
|
369
|
+
*/
|
|
370
|
+
감시.on('error', () => { try { 감시?.close(); } catch { /* 이미 닫혔다 */ } });
|
|
217
371
|
} catch {
|
|
218
372
|
// 리눅스 옛 커널 등에서 recursive 가 안 될 수 있다. 그때는 되살림만 없다.
|
|
219
373
|
감시 = null;
|
|
@@ -240,22 +394,62 @@ export function 띄우기({ 뿌리, scope, 되살리기 = true }) {
|
|
|
240
394
|
});
|
|
241
395
|
}
|
|
242
396
|
|
|
397
|
+
/*
|
|
398
|
+
* 표에 적힌 `charset=utf-8` 을 **파일에 맞춰 고친다** (막판 훑기).
|
|
399
|
+
*
|
|
400
|
+
* HTTP 머리에 적은 charset 은 문서 안의 `<meta charset>` 을 **이긴다**(WHATWG).
|
|
401
|
+
* 그래서 EUC-KR 로 적힌 옛 페이지는 제 입으로 euc-kr 이라 적어 둬도 여기서
|
|
402
|
+
* utf-8 이라고 우기는 순간 통째로 깨져 보인다. 되살림 조각을 바이트째로 끼운
|
|
403
|
+
* 것도(위 머리말) 이 한 줄이 도로 무르고 있었다 — 반쪽 고침이었다.
|
|
404
|
+
*
|
|
405
|
+
* 건드리는 것은 글 갈래뿐이다. 그림·소리·wasm 에는 charset 이 없다.
|
|
406
|
+
*/
|
|
407
|
+
function 글자맞추기(mime, 머리) {
|
|
408
|
+
if (!머리 || !mime.includes('charset=utf-8')) return mime;
|
|
409
|
+
// 잘림:true — 앞부분만 봤으니 끝에서 잘린 글자를 깨진 것으로 세지 말라는 뜻이다.
|
|
410
|
+
const r = detect(머리, { 잘림: true });
|
|
411
|
+
return r.id === 'utf-8' ? mime : mime.replace('charset=utf-8', `charset=${r.id}`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** 인코딩을 가리려고 앞부분만 읽는다. 다 읽으면 큰 파일에서 흘려보내는 뜻이 없어진다. */
|
|
415
|
+
function 머리읽기(abs, 몇 = 64 * 1024) {
|
|
416
|
+
let fd = null;
|
|
417
|
+
try {
|
|
418
|
+
fd = openSync(abs, 'r');
|
|
419
|
+
const buf = Buffer.alloc(몇);
|
|
420
|
+
const 센것 = readSync(fd, buf, 0, 몇, 0);
|
|
421
|
+
return buf.subarray(0, 센것);
|
|
422
|
+
} catch { return null; } finally {
|
|
423
|
+
if (fd !== null) { try { closeSync(fd); } catch { /* 이미 닫혔다 */ } }
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
243
427
|
function 파일주기(abs, req, res, 되살리기) {
|
|
244
428
|
const ext = extname(abs).toLowerCase();
|
|
245
|
-
const
|
|
429
|
+
const 적힌형식 = 형식[ext] ?? 'application/octet-stream';
|
|
430
|
+
const mime = 적힌형식.includes('charset=utf-8') ? 글자맞추기(적힌형식, 머리읽기(abs)) : 적힌형식;
|
|
246
431
|
let st;
|
|
247
432
|
try { st = statSync(abs); } catch { res.writeHead(404); return res.end(); }
|
|
248
433
|
|
|
249
434
|
// HTML 에는 되살림 조각을 끼워 넣는다. 길이가 달라지므로 흘려보내지 않고 통째로 읽는다.
|
|
250
435
|
if (되살리기 && (ext === '.html' || ext === '.htm')) {
|
|
251
|
-
|
|
252
|
-
const s = createReadStream(abs
|
|
253
|
-
s.on('data', (d) => {
|
|
436
|
+
const 조각들 = [];
|
|
437
|
+
const s = createReadStream(abs);
|
|
438
|
+
s.on('data', (d) => { 조각들.push(d); });
|
|
254
439
|
s.on('error', () => { res.writeHead(500); res.end(); });
|
|
255
440
|
s.on('end', () => {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
441
|
+
/*
|
|
442
|
+
* **바이트째로** 끼운다 (2.0.0 6회차 미리보기6). utf8 글로 읽었다가 utf8 로 다시 적었더니
|
|
443
|
+
* EUC-KR 페이지의 한글이 전부 U+FFFD 로 바뀌어 나갔다 — 되살리기는 기본으로 켜져 있어서
|
|
444
|
+
* 옛 한글 페이지는 미리보기에서 늘 깨져 보였다. `</body>` 는 그런 인코딩에서도 같은 ASCII
|
|
445
|
+
* 바이트이고, 끼우는 조각도 ASCII 뿐이라(위 되살림 머리말) 원래 바이트를 한 개도 안 건드린다.
|
|
446
|
+
* `</body>` 앞에 넣는 게 정석이지만 없는 문서도 많다. 없으면 그냥 뒤에 붙인다.
|
|
447
|
+
*/
|
|
448
|
+
const 원 = Buffer.concat(조각들);
|
|
449
|
+
const 자리 = 원.indexOf('</body>');
|
|
450
|
+
const buf = 자리 >= 0
|
|
451
|
+
? Buffer.concat([원.subarray(0, 자리), Buffer.from(`${되살림}\n`), 원.subarray(자리)])
|
|
452
|
+
: Buffer.concat([원, Buffer.from(되살림)]);
|
|
259
453
|
res.writeHead(200, { 'content-type': mime, 'content-length': buf.length, 'cache-control': 'no-store' });
|
|
260
454
|
res.end(req.method === 'HEAD' ? undefined : buf);
|
|
261
455
|
});
|
|
@@ -267,8 +461,14 @@ function 파일주기(abs, req, res, 되살리기) {
|
|
|
267
461
|
* 이걸 안 받아 주면 <video> 가 아예 안 돈다 — 파일은 멀쩡한데 화면만 검다.
|
|
268
462
|
*/
|
|
269
463
|
const range = req.headers.range;
|
|
464
|
+
/*
|
|
465
|
+
* `bytes=-` 는 **문법 오류**다 — 시작도 끝도 없다. 규격은 「못 알아들을
|
|
466
|
+
* Range 는 없는 것처럼 보라」 고 한다(RFC 9110 §14.2). 여태는 정규식에
|
|
467
|
+
* 걸려 처음=0 · 끝=마지막 으로 채워져 **파일 전체를 206 으로** 줬다.
|
|
468
|
+
* 받는 쪽은 자기가 청한 조각을 받았다고 믿는다 (9회차 · 2차 눈).
|
|
469
|
+
*/
|
|
270
470
|
const m = range && /^bytes=(\d*)-(\d*)$/.exec(String(range).trim());
|
|
271
|
-
if (m && st.size > 0) {
|
|
471
|
+
if (m && st.size > 0 && (m[1] !== '' || m[2] !== '')) {
|
|
272
472
|
let 처음 = m[1] === '' ? null : Number(m[1]);
|
|
273
473
|
let 끝 = m[2] === '' ? null : Number(m[2]);
|
|
274
474
|
if (처음 === null && 끝 !== null) { 처음 = Math.max(0, st.size - 끝); 끝 = st.size - 1; }
|
|
@@ -284,7 +484,7 @@ function 파일주기(abs, req, res, 되살리기) {
|
|
|
284
484
|
'content-length': 끝 - 처음 + 1,
|
|
285
485
|
});
|
|
286
486
|
if (req.method === 'HEAD') return res.end();
|
|
287
|
-
return createReadStream(abs, { start: 처음, end: 끝 })
|
|
487
|
+
return 흘리기(createReadStream(abs, { start: 처음, end: 끝 }), res);
|
|
288
488
|
}
|
|
289
489
|
|
|
290
490
|
res.writeHead(200, {
|
|
@@ -295,13 +495,29 @@ function 파일주기(abs, req, res, 되살리기) {
|
|
|
295
495
|
'cache-control': 'no-store',
|
|
296
496
|
});
|
|
297
497
|
if (req.method === 'HEAD') return res.end();
|
|
298
|
-
return createReadStream(abs)
|
|
498
|
+
return 흘리기(createReadStream(abs), res);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/*
|
|
502
|
+
* 읽기 흐름을 답에 잇는다 — **흐름의 오류를 받는 자리까지.**
|
|
503
|
+
*
|
|
504
|
+
* `pipe` 는 오류를 넘겨 주지 않는다. 다른 프로그램이 파일을 잠가 두면(윈도 EBUSY — 편집기·백신·
|
|
505
|
+
* 빌드 도구가 흔히 그런다) statSync 는 되고 여는 자리에서 오류가 나는데, 그걸 아무도 안 받아
|
|
506
|
+
* **deel 프로세스가 통째로 죽었다** (2.0.0 6회차 미리보기6). 머리말(200 · 길이)은 이미 나갔으니
|
|
507
|
+
* 답을 고칠 수는 없고, 연결을 끊어 받는 쪽이 「덜 받은 답」 으로 알게 한다 — 빈 파일을 성한
|
|
508
|
+
* 답인 척 끝내지 않는다.
|
|
509
|
+
*/
|
|
510
|
+
function 흘리기(흐름, res) {
|
|
511
|
+
흐름.on('error', () => res.destroy());
|
|
512
|
+
return 흐름.pipe(res);
|
|
299
513
|
}
|
|
300
514
|
|
|
301
515
|
// index.html 이 없는 폴더. 무엇이 있는지라도 보여 준다 — 흰 화면보다 낫다.
|
|
302
516
|
function 목록주기(abs, 뿌리, res) {
|
|
303
517
|
let 것들 = [];
|
|
304
518
|
try { 것들 = readdirSync(abs, { withFileTypes: true }); } catch { /* 못 읽으면 빈 채로 */ }
|
|
519
|
+
// 눌러도 안 주는 것은 목록에도 안 올린다 — 이름만으로도 무엇이 있는지 샌다 (사냥5 H5-7).
|
|
520
|
+
것들 = 것들.filter((e) => !안줄것인가(join(abs, e.name), 뿌리));
|
|
305
521
|
const 여기 = relative(뿌리, abs).split(sep).join('/');
|
|
306
522
|
// 파일 이름은 디스크에서 온 것이라 `<script>` 같은 것이 그대로 들어 있을 수
|
|
307
523
|
// 있다 — 씌우지 않고 꽂으면 그 파일이 있는 폴더를 미리보기로 여는 사람 화면에서
|
package/src/providers/bedrock.js
CHANGED
|
@@ -51,8 +51,24 @@ export const 제공자 = {
|
|
|
51
51
|
리전들,
|
|
52
52
|
|
|
53
53
|
주소들: ({ 리전 } = {}) => {
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
/*
|
|
55
|
+
* 리전은 소문자로 받고, 가운데 이름이 여러 칸인 것도 받는다 (사냥5 B5-12).
|
|
56
|
+
*
|
|
57
|
+
* `xx-이름-숫자` 한 칸짜리만 받아서 GovCloud(`us-gov-west-1`)와 대문자로 적은
|
|
58
|
+
* `US-EAST-1` 이 **아무 말 없이** 후보 0개였다 — 화면은 「붙을 곳을 못 찾았다」 로만
|
|
59
|
+
* 보인다. 모양 검사 자체는 남긴다. 이 값이 주소 한가운데에 들어가므로 점·빗금이
|
|
60
|
+
* 끼면 남의 집 주소를 지을 수 있다.
|
|
61
|
+
*/
|
|
62
|
+
/*
|
|
63
|
+
* 빈 글자도 「안 골랐다」 다.
|
|
64
|
+
*
|
|
65
|
+
* `?? 'us-east-1'` 은 null·undefined 만 막는다. 그래서 설치 화면에서 리전을
|
|
66
|
+
* **엔터로 넘기면**(setup.js 는 trim 만 한다) 빈 글자가 그대로 내려와 아래
|
|
67
|
+
* 모양 검사에 걸리고 후보가 0개가 됐다 — 화면에는 「붙을 곳을 못 찾았습니다」
|
|
68
|
+
* 한 줄뿐이라, 엔터 한 번이 원인이라는 것을 알 길이 없다.
|
|
69
|
+
*/
|
|
70
|
+
const r = String(리전 ?? '').trim().toLowerCase() || 'us-east-1';
|
|
71
|
+
if (!/^[a-z]{2}(?:-[a-z]+)+-\d{1,2}$/.test(r)) return [];
|
|
56
72
|
return [
|
|
57
73
|
`https://bedrock-runtime.${r}.amazonaws.com/v1`,
|
|
58
74
|
`https://bedrock-runtime.${r}.amazonaws.com/openai/v1`,
|
|
@@ -96,6 +112,18 @@ export const 제공자 = {
|
|
|
96
112
|
요금표주소: 'https://aws.amazon.com/bedrock/pricing/',
|
|
97
113
|
|
|
98
114
|
오류읽기({ status, 서버말 }) {
|
|
115
|
+
/*
|
|
116
|
+
* **좁은 말이 먼저다.**
|
|
117
|
+
*
|
|
118
|
+
* 만료된 단기 키도 AWS 는 403 + `AccessDeniedException` 으로 내놓고, 본문에
|
|
119
|
+
* 「security token … is expired」 를 같이 적는다. 그런데 아래 넓은
|
|
120
|
+
* AccessDenied 검사가 이 검사보다 위에 있어서, 열쇠를 새로 받아야 하는
|
|
121
|
+
* 사람이 「콘솔에서 모델 접근을 신청하세요」 를 봤다 — 신청은 이미 돼
|
|
122
|
+
* 있으니 콘솔을 아무리 봐도 고칠 것이 없다.
|
|
123
|
+
*/
|
|
124
|
+
if (status === 403 && /security token|ExpiredToken/i.test(서버말)) {
|
|
125
|
+
return '열쇠가 만료됐습니다 — Bedrock 콘솔에서 새로 발급받으세요 (단기 키는 만료됩니다).';
|
|
126
|
+
}
|
|
99
127
|
/*
|
|
100
128
|
* Bedrock 에서 제일 흔한 첫 실패는 **모델 접근 신청을 안 한 것**이다.
|
|
101
129
|
* 열쇠도 맞고 주소도 맞는데 그 리전의 그 모델을 아직 안 열어 둔 상태다.
|
|
@@ -108,9 +136,6 @@ export const 제공자 = {
|
|
|
108
136
|
if (/ValidationException/i.test(서버말) && /model/i.test(서버말)) {
|
|
109
137
|
return '그 모델 이름을 이 리전에서 못 씁니다 — 리전을 바꾸거나 이름을 다시 보세요.';
|
|
110
138
|
}
|
|
111
|
-
if (status === 403 && /security token|ExpiredToken/i.test(서버말)) {
|
|
112
|
-
return '열쇠가 만료됐습니다 — Bedrock 콘솔에서 새로 발급받으세요 (단기 키는 만료됩니다).';
|
|
113
|
-
}
|
|
114
139
|
if (status === 404 && /^$|Not Found/i.test(서버말)) {
|
|
115
140
|
return '그 리전에 이 창구가 없습니다 — 리전 이름을 다시 보세요.';
|
|
116
141
|
}
|
package/src/providers/gemini.js
CHANGED
|
@@ -56,6 +56,14 @@ export const 제공자 = {
|
|
|
56
56
|
return '도구 스키마를 거절했습니다 — Gemini 는 스키마의 일부만 받습니다.'
|
|
57
57
|
+ ' deel diagnose 로 어느 도구가 걸리는지 확인하세요.';
|
|
58
58
|
}
|
|
59
|
+
/*
|
|
60
|
+
* 모델 이름이 틀리면 404 로 온다(`models/… is not found for API version …`). 여기서 안 받으면
|
|
61
|
+
* 공통 까닭이 404 를 「주소를 다시 보세요」 로 읽어, 표에 박힌 멀쩡한 주소를 의심하게 했다
|
|
62
|
+
* (2.0.0 6회차 · Gemini 제공자6). openai.js 의 같은 자리와 같은 말이다.
|
|
63
|
+
*/
|
|
64
|
+
if (status === 404 && /model/i.test(서버말)) {
|
|
65
|
+
return '그 모델 이름을 이 열쇠로는 못 씁니다 — 이름이 틀렸거나, 아직 안 열린 모델입니다.';
|
|
66
|
+
}
|
|
59
67
|
if (status === 403 && /SERVICE_DISABLED|PERMISSION_DENIED/i.test(서버말)) {
|
|
60
68
|
return '이 프로젝트에서 Generative Language API 가 꺼져 있습니다 — 콘솔에서 켜세요.';
|
|
61
69
|
}
|
package/src/providers/index.js
CHANGED
|
@@ -60,7 +60,7 @@ export function 제공자고르기(id) {
|
|
|
60
60
|
* 열쇠를 OpenAI 로 보내게 된다. 그게 여기서 제일 조심할 일이다.
|
|
61
61
|
*/
|
|
62
62
|
export function 어디것일까(열쇠) {
|
|
63
|
-
const k =
|
|
63
|
+
const k = 열쇠다듬기(열쇠);
|
|
64
64
|
if (!k) return null;
|
|
65
65
|
let 제일긴것 = null;
|
|
66
66
|
for (const p of 제공자들) {
|
|
@@ -74,6 +74,22 @@ export function 어디것일까(열쇠) {
|
|
|
74
74
|
return { 제공자: 제일긴것.제공자, 왜: `열쇠가 ${제일긴것.무늬.source.replace(/[\^\\]/g, '')} 로 시작합니다` };
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/*
|
|
78
|
+
* ── 붙여 넣은 열쇠의 겉을 다듬는다 (2.0.0 6회차 · Gemini 제공자6) ──────────
|
|
79
|
+
*
|
|
80
|
+
* 앞뒤 빈칸, 그리고 **양끝이 같은** 따옴표 한 쌍을 뗀다. `.env` 의 `OPENAI_API_KEY="sk-…"`
|
|
81
|
+
* 에서 값을 따옴표째 긁어 오는 일이 흔한데, 그러면 앞머리 무늬(`^sk-`)가 하나도 안 맞아
|
|
82
|
+
* 어디 열쇠인지 모른다고 했고, 골라서 넘어가도 따옴표째 저장돼 첫 요청이 401 이었다 —
|
|
83
|
+
* 화면은 「열쇠가 틀렸거나 만료됐다」 라고 하니 사람은 멀쩡한 열쇠를 새로 발급받는다.
|
|
84
|
+
* 열쇠 글자에는 따옴표가 안 들어가므로 양끝 한 쌍만 뗀다. 한쪽만 있거나 짝이 안 맞으면
|
|
85
|
+
* 손대지 않는다 — 무엇이 잘못 붙었는지 짐작해 고치지 않는다.
|
|
86
|
+
*/
|
|
87
|
+
export function 열쇠다듬기(열쇠) {
|
|
88
|
+
const k = String(열쇠 ?? '').trim();
|
|
89
|
+
const 짝 = /^(["'])(.*)\1$/.exec(k);
|
|
90
|
+
return 짝 ? 짝[2].trim() : k;
|
|
91
|
+
}
|
|
92
|
+
|
|
77
93
|
/**
|
|
78
94
|
* 이 제공자에 붙을 주소 후보들. 앞엣것이 제일 그럴듯한 것이다.
|
|
79
95
|
*
|